diff --git a/.eslintignore b/.eslintignore index 357d735e8044b..1f22b6074e76e 100644 --- a/.eslintignore +++ b/.eslintignore @@ -9,14 +9,14 @@ bower_components /built_assets /html_docs /src/plugins/data/common/es_query/kuery/ast/_generated_/** -/src/legacy/core_plugins/vis_type_timelion/public/_generated_/** +/src/plugins/vis_type_timelion/public/_generated_/** src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/lib/fixtures/mock_data /src/legacy/ui/public/flot-charts /test/fixtures/scenarios /src/legacy/core_plugins/console/public/webpackShims /src/legacy/core_plugins/console/public/tests/webpackShims /src/legacy/ui/public/utils/decode_geo_hash.js -/src/legacy/core_plugins/vis_type_timelion/public/webpackShims/jquery.flot.* +/src/plugins/vis_type_timelion/public/webpackShims/jquery.flot.* /src/core/lib/kbn_internal_native_observable /packages/*/target /packages/eslint-config-kibana diff --git a/.eslintrc.js b/.eslintrc.js index 2ce6d279d93a9..a2b8ae7622d0b 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -109,7 +109,7 @@ module.exports = { }, }, { - files: ['x-pack/legacy/plugins/lens/**/*.{js,ts,tsx}'], + files: ['x-pack/plugins/lens/**/*.{js,ts,tsx}'], rules: { 'react-hooks/exhaustive-deps': 'off', 'react-hooks/rules-of-hooks': 'off', @@ -536,9 +536,15 @@ module.exports = { * ML overrides */ { - files: ['x-pack/legacy/plugins/ml/**/*.js'], + files: ['x-pack/plugins/ml/**/*.js'], rules: { 'no-shadow': 'error', + 'import/no-extraneous-dependencies': [ + 'error', + { + packageDir: './x-pack', + }, + ], }, }, @@ -561,7 +567,7 @@ module.exports = { }, { // typescript only for front and back end - files: ['x-pack/legacy/plugins/siem/**/*.{ts,tsx}'], + files: ['x-pack/{,legacy/}plugins/siem/**/*.{ts,tsx}'], rules: { // This will be turned on after bug fixes are complete // '@typescript-eslint/explicit-member-accessibility': 'warn', @@ -607,7 +613,7 @@ module.exports = { // }, { // typescript and javascript for front and back end - files: ['x-pack/legacy/plugins/siem/**/*.{js,ts,tsx}'], + files: ['x-pack/{,legacy/}plugins/siem/**/*.{js,ts,tsx}'], plugins: ['eslint-plugin-node', 'react'], env: { mocha: true, @@ -728,7 +734,7 @@ module.exports = { * Lens overrides */ { - files: ['x-pack/legacy/plugins/lens/**/*.{ts,tsx}', 'x-pack/plugins/lens/**/*.{ts,tsx}'], + files: ['x-pack/plugins/lens/**/*.{ts,tsx}'], rules: { '@typescript-eslint/no-explicit-any': 'error', }, diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index e707250ff3261..ab05b32ab063e 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -3,22 +3,24 @@ # For more info, see https://help.github.com/articles/about-codeowners/ # App -/x-pack/legacy/plugins/lens/ @elastic/kibana-app -/x-pack/legacy/plugins/graph/ @elastic/kibana-app +/x-pack/plugins/lens/ @elastic/kibana-app +/x-pack/plugins/graph/ @elastic/kibana-app /src/legacy/server/url_shortening/ @elastic/kibana-app /src/legacy/server/sample_data/ @elastic/kibana-app /src/legacy/core_plugins/kibana/public/dashboard/ @elastic/kibana-app /src/legacy/core_plugins/kibana/public/discover/ @elastic/kibana-app -/src/legacy/core_plugins/kibana/public/visualize/ @elastic/kibana-app /src/legacy/core_plugins/kibana/public/local_application_service/ @elastic/kibana-app /src/legacy/core_plugins/kibana/public/dev_tools/ @elastic/kibana-app -/src/legacy/core_plugins/metrics/ @elastic/kibana-app /src/legacy/core_plugins/vis_type_vislib/ @elastic/kibana-app -/src/legacy/core_plugins/vis_type_xy/ @elastic/kibana-app +/src/plugins/vis_type_xy/ @elastic/kibana-app /src/plugins/kibana_legacy/ @elastic/kibana-app -/src/plugins/timelion/ @elastic/kibana-app +/src/plugins/vis_type_timelion/ @elastic/kibana-app /src/plugins/dashboard/ @elastic/kibana-app /src/plugins/discover/ @elastic/kibana-app +/src/plugins/visualize/ @elastic/kibana-app +/src/plugins/vis_type_timeseries/ @elastic/kibana-app +/src/plugins/vis_type_metric/ @elastic/kibana-app +/src/plugins/vis_type_markdown/ @elastic/kibana-app # Core UI # Exclude tutorials folder for now because they are not owned by Kibana app and most will move out soon @@ -80,6 +82,8 @@ /x-pack/legacy/plugins/ingest_manager/ @elastic/ingest-management /x-pack/plugins/observability/ @elastic/logs-metrics-ui @elastic/apm-ui @elastic/uptime @elastic/ingest-management /x-pack/legacy/plugins/monitoring/ @elastic/stack-monitoring-ui +/x-pack/legacy/plugins/uptime @elastic/uptime +/x-pack/plugins/uptime @elastic/uptime # Machine Learning /x-pack/legacy/plugins/ml/ @elastic/ml-ui @@ -95,6 +99,7 @@ # Maps /x-pack/legacy/plugins/maps/ @elastic/kibana-gis +/x-pack/plugins/maps/ @elastic/kibana-gis /x-pack/test/api_integration/apis/maps/ @elastic/kibana-gis /x-pack/test/functional/apps/maps/ @elastic/kibana-gis /x-pack/test/functional/es_archives/maps/ @elastic/kibana-gis @@ -127,7 +132,6 @@ /packages/kbn-config-schema/ @elastic/kibana-platform /src/legacy/server/config/ @elastic/kibana-platform /src/legacy/server/http/ @elastic/kibana-platform -/src/legacy/server/i18n/ @elastic/kibana-platform /src/legacy/server/logging/ @elastic/kibana-platform /src/legacy/server/saved_objects/ @elastic/kibana-platform /src/legacy/server/status/ @elastic/kibana-platform @@ -146,7 +150,10 @@ /x-pack/test/api_integration/apis/security/ @elastic/kibana-security # Kibana Localization -/src/dev/i18n/ @elastic/kibana-localization +/src/dev/i18n/ @elastic/kibana-localization +/src/legacy/server/i18n/ @elastic/kibana-localization +/src/core/public/i18n/ @elastic/kibana-localization +/packages/kbn-i18n/ @elastic/kibana-localization # Pulse /packages/kbn-analytics/ @elastic/pulse @@ -180,7 +187,7 @@ /src/plugins/console/ @elastic/es-ui /src/plugins/es_ui_shared/ @elastic/es-ui /x-pack/legacy/plugins/cross_cluster_replication/ @elastic/es-ui -/x-pack/legacy/plugins/index_lifecycle_management/ @elastic/es-ui +/x-pack/plugins/index_lifecycle_management/ @elastic/es-ui /x-pack/legacy/plugins/index_management/ @elastic/es-ui /x-pack/legacy/plugins/license_management/ @elastic/es-ui /x-pack/legacy/plugins/rollup/ @elastic/es-ui @@ -202,9 +209,12 @@ # Endpoint /x-pack/plugins/endpoint/ @elastic/endpoint-app-team /x-pack/test/api_integration/apis/endpoint/ @elastic/endpoint-app-team +/x-pack/test/endpoint_api_integration_no_ingest/ @elastic/endpoint-app-team /x-pack/test/functional_endpoint/ @elastic/endpoint-app-team /x-pack/test/functional_endpoint_ingest_failure/ @elastic/endpoint-app-team /x-pack/test/functional/es_archives/endpoint/ @elastic/endpoint-app-team +/x-pack/test/plugin_functional/plugins/resolver_test/ @elastic/endpoint-app-team +/x-pack/test/plugin_functional/test_suites/resolver/ @elastic/endpoint-app-team # SIEM /x-pack/legacy/plugins/siem/ @elastic/siem diff --git a/.github/paths-labeller.yml b/.github/paths-labeller.yml index 3d35cd74e0718..544dd577313df 100644 --- a/.github/paths-labeller.yml +++ b/.github/paths-labeller.yml @@ -8,3 +8,6 @@ - "Feature:ExpressionLanguage": - "src/plugins/expressions/**/*.*" - "src/plugins/bfetch/**/*.*" + - "Team:uptime": + - "x-pack/plugins/uptime/**/*.*" + - "x-pack/legacy/plugins/uptime/**/*.*" diff --git a/.i18nrc.json b/.i18nrc.json index 19d361aed9344..4a516f23ebf05 100644 --- a/.i18nrc.json +++ b/.i18nrc.json @@ -24,6 +24,7 @@ "src/legacy/core_plugins/management", "src/plugins/management" ], + "maps_legacy": "src/plugins/maps_legacy", "indexPatternManagement": "src/plugins/index_pattern_management", "advancedSettings": "src/plugins/advanced_settings", "kibana_legacy": "src/plugins/kibana_legacy", @@ -42,18 +43,19 @@ "src/plugins/telemetry_management_section" ], "tileMap": "src/legacy/core_plugins/tile_map", - "timelion": ["src/legacy/core_plugins/timelion", "src/legacy/core_plugins/vis_type_timelion", "src/plugins/timelion"], + "timelion": ["src/legacy/core_plugins/timelion", "src/plugins/vis_type_timelion"], "uiActions": "src/plugins/ui_actions", "visDefaultEditor": "src/plugins/vis_default_editor", - "visTypeMarkdown": "src/legacy/core_plugins/vis_type_markdown", - "visTypeMetric": "src/legacy/core_plugins/vis_type_metric", + "visTypeMarkdown": "src/plugins/vis_type_markdown", + "visTypeMetric": "src/plugins/vis_type_metric", "visTypeTable": "src/legacy/core_plugins/vis_type_table", "visTypeTagCloud": "src/legacy/core_plugins/vis_type_tagcloud", "visTypeTimeseries": ["src/legacy/core_plugins/vis_type_timeseries", "src/plugins/vis_type_timeseries"], "visTypeVega": "src/legacy/core_plugins/vis_type_vega", "visTypeVislib": "src/legacy/core_plugins/vis_type_vislib", "visTypeXy": "src/legacy/core_plugins/vis_type_xy", - "visualizations": "src/plugins/visualizations" + "visualizations": "src/plugins/visualizations", + "visualize": "src/plugins/visualize" }, "exclude": [ "src/legacy/ui/ui_render/ui_render_mixin.js" diff --git a/.sass-lint.yml b/.sass-lint.yml index dd7bc0576692b..5c2c88a1dad5d 100644 --- a/.sass-lint.yml +++ b/.sass-lint.yml @@ -8,10 +8,11 @@ files: - 'x-pack/legacy/plugins/security/**/*.s+(a|c)ss' - 'x-pack/legacy/plugins/canvas/**/*.s+(a|c)ss' - 'x-pack/plugins/triggers_actions_ui/**/*.s+(a|c)ss' + - 'x-pack/plugins/lens/**/*.s+(a|c)ss' + - 'x-pack/legacy/plugins/maps/**/*.s+(a|c)ss' + - 'x-pack/plugins/maps/**/*.s+(a|c)ss' ignore: - 'x-pack/legacy/plugins/canvas/shareable_runtime/**/*.s+(a|c)ss' - - 'x-pack/legacy/plugins/lens/**/*.s+(a|c)ss' - - 'x-pack/legacy/plugins/maps/**/*.s+(a|c)ss' rules: quotes: - 2 diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 5c745f1611cce..e4a9d87bc56fc 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -22,6 +22,7 @@ A high level overview of our contributing guidelines. - [Setting Up SSL](#setting-up-ssl) - [Linting](#linting) - [Internationalization](#internationalization) + - [Localization](#localization) - [Testing and Building](#testing-and-building) - [Debugging server code](#debugging-server-code) - [Instrumenting with Elastic APM](#instrumenting-with-elastic-apm) @@ -408,6 +409,11 @@ ReactDOM.render( There are a number of tools created to support internationalization in Kibana that would allow one to validate internationalized labels, extract them to a `JSON` file or integrate translations back to Kibana. To know more, please read corresponding [readme](src/dev/i18n/README.md) file. +### Localization + +We cannot support accepting contributions to the translations from any source other than the translators we have engaged to do the work. +We are still to develop a proper process to accept any contributed translations. We certainly appreciate that people care enough about the localization effort to want to help improve the quality. We aim to build out a more comprehensive localization process for the future and will notify you once contributions can be supported, but for the time being, we are not able to incorporate suggestions. + ### Testing and Building To ensure that your changes will not break other functionality, please run the test suite and build process before submitting your Pull Request. diff --git a/Jenkinsfile b/Jenkinsfile index 79d3c93006cb6..6646ee15ba1c2 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -41,7 +41,7 @@ kibanaPipeline(timeoutMinutes: 135, checkPrChanges: true) { 'xpack-ciGroup10': kibanaPipeline.xpackCiGroupProcess(10), 'xpack-accessibility': kibanaPipeline.functionalTestProcess('xpack-accessibility', './test/scripts/jenkins_xpack_accessibility.sh'), 'xpack-siemCypress': { processNumber -> - whenChanged(['x-pack/legacy/plugins/siem/', 'x-pack/test/siem_cypress/']) { + whenChanged(['x-pack/plugins/siem/', 'x-pack/legacy/plugins/siem/', 'x-pack/test/siem_cypress/']) { kibanaPipeline.functionalTestProcess('xpack-siemCypress', './test/scripts/jenkins_siem_cypress.sh')(processNumber) } }, diff --git a/docs/api/saved-objects/bulk_create.asciidoc b/docs/api/saved-objects/bulk_create.asciidoc index 9daba224b317c..fbd4c6e77f8bf 100644 --- a/docs/api/saved-objects/bulk_create.asciidoc +++ b/docs/api/saved-objects/bulk_create.asciidoc @@ -104,7 +104,7 @@ The API returns the following: "type": "dashboard", "error": { "statusCode": 409, - "message": "version conflict, document already exists" + "message": "Saved object [dashboard/be3733a0-9efe-11e7-acb3-3dab96693fab] conflict" } } ] diff --git a/docs/api/saved-objects/create.asciidoc b/docs/api/saved-objects/create.asciidoc index dc010c80fd012..571b57a5ef9c2 100644 --- a/docs/api/saved-objects/create.asciidoc +++ b/docs/api/saved-objects/create.asciidoc @@ -57,12 +57,12 @@ any data that you send to the API is properly formed. [source,sh] -------------------------------------------------- -$ curl -X POST "localhost:5601/api/saved_objects/index-pattern/my-pattern" +$ curl -X POST "localhost:5601/api/saved_objects/index-pattern/my-pattern" -H 'kbn-xsrf: true' -H 'Content-Type: application/json' -d ' { "attributes": { "title": "my-pattern-*" } -} +}' -------------------------------------------------- // KIBANA diff --git a/docs/api/saved-objects/export.asciidoc b/docs/api/saved-objects/export.asciidoc index e8c762b9543a1..a992d13ed9b9c 100644 --- a/docs/api/saved-objects/export.asciidoc +++ b/docs/api/saved-objects/export.asciidoc @@ -68,10 +68,10 @@ Export all index pattern saved objects: [source,sh] -------------------------------------------------- -$ curl -X POST "localhost:5601/api/saved_objects/_export" +$ curl -X POST "localhost:5601/api/saved_objects/_export" -H 'kbn-xsrf: true' -H 'Content-Type: application/json' -d ' { "type": "index-pattern" -} +}' -------------------------------------------------- // KIBANA @@ -79,11 +79,11 @@ Export all index pattern saved objects and exclude the export summary from the s [source,sh] -------------------------------------------------- -$ curl -X POST "localhost:5601/api/saved_objects/_export" +$ curl -X POST "localhost:5601/api/saved_objects/_export" -H 'kbn-xsrf: true' -H 'Content-Type: application/json' -d ' { "type": "index-pattern", "excludeExportDetails": true -} +}' -------------------------------------------------- // KIBANA @@ -91,7 +91,7 @@ Export a specific saved object: [source,sh] -------------------------------------------------- -$ curl -X POST "localhost:5601/api/saved_objects/_export" +$ curl -X POST "localhost:5601/api/saved_objects/_export" -H 'kbn-xsrf: true' -H 'Content-Type: application/json' -d ' { "objects": [ { @@ -99,7 +99,7 @@ $ curl -X POST "localhost:5601/api/saved_objects/_export" "id": "be3733a0-9efe-11e7-acb3-3dab96693fab" } ] -} +}' -------------------------------------------------- // KIBANA @@ -107,7 +107,7 @@ Export a specific saved object and it's related objects : [source,sh] -------------------------------------------------- -$ curl -X POST "localhost:5601/api/saved_objects/_export" +$ curl -X POST "localhost:5601/api/saved_objects/_export" -H 'kbn-xsrf: true' -H 'Content-Type: application/json' -d ' { "objects": [ { @@ -116,6 +116,6 @@ $ curl -X POST "localhost:5601/api/saved_objects/_export" } ], "includeReferencesDeep": true -} +}' -------------------------------------------------- // KIBANA diff --git a/docs/apm/advanced-queries.asciidoc b/docs/apm/advanced-queries.asciidoc index add6f601489e1..f89f994e59e57 100644 --- a/docs/apm/advanced-queries.asciidoc +++ b/docs/apm/advanced-queries.asciidoc @@ -1,14 +1,11 @@ +[role="xpack"] [[advanced-queries]] -=== Advanced queries +=== Query your data -When querying in the APM app, you're simply searching and selecting data from fields in Elasticsearch documents. -Queries entered into the query bar are also added as parameters to the URL, -so it's easy to share a specific query or view with others. - -You can begin to see some of the transaction fields available for filtering: - -[role="screenshot"] -image::apm/images/apm-query-bar.png[Example of the Kibana Query bar in APM app in Kibana] +Querying your APM data is a powerful tool that can make finding bottlenecks in your code even easier. +Imagine you have a user that complains about a slow response time in a specific service. +With the query bar, you can easily filter the APM app to only display trace data for that user, +or, to only show transactions that are slower than a specified time threshold. [float] ==== Example APM app queries @@ -17,15 +14,24 @@ image::apm/images/apm-query-bar.png[Example of the Kibana Query bar in APM app i * Filter by response status code: `context.response.status_code >= 400` * Filter by single user ID: `context.user.id : 12` +When querying in the APM app, you're merely searching and selecting data from fields in Elasticsearch documents. +Queries entered into the query bar are also added as parameters to the URL, +so it's easy to share a specific query or view with others. + +When you type, you can begin to see some of the transaction fields available for filtering: + +[role="screenshot"] +image::apm/images/apm-query-bar.png[Example of the Kibana Query bar in APM app in Kibana] + TIP: Read the {kibana-ref}/kuery-query.html[Kibana Query Language Enhancements] documentation to learn more about the capabilities of the {kib} query language. [float] [[discover-advanced-queries]] === Querying in Discover -It may also be helpful to view your APM data in {kibana-ref}/discover.html[*Discover*]. +Alternatively, you can query your APM documents in {kibana-ref}/discover.html[*Discover*]. Querying documents in *Discover* works the same way as querying in the APM app, -and all of the example APM app queries can also be used in *Discover*. +and *Discover* supports all of the example APM app queries shown on this page. [float] ==== Example Discover query @@ -33,7 +39,7 @@ and all of the example APM app queries can also be used in *Discover*. One example where you may want to make use of *Discover*, is for viewing _all_ transactions for an endpoint, instead of just a sample. -TIP: Starting in v7.6, you can view 10 samples per bucket in the APM app, instead of just one. +TIP: Starting in v7.6, you can view ten samples per bucket in the APM app, instead of just one. Use the APM app to find a transaction name and time bucket that you're interested in learning more about. Then, switch to *Discover* and make a search: diff --git a/docs/apm/agent-configuration.asciidoc b/docs/apm/agent-configuration.asciidoc index 0d2834c1a400e..d911c2154ea4c 100644 --- a/docs/apm/agent-configuration.asciidoc +++ b/docs/apm/agent-configuration.asciidoc @@ -1,12 +1,16 @@ [role="xpack"] [[agent-configuration]] -=== APM Agent configuration +=== APM Agent central configuration -APM Agent configuration allows you to fine-tune your agent configuration directly in Kibana. -Best of all, changes are automatically propagated to your APM agents so there's no need to redeploy. +++++ +Configure APM agents with central config +++++ -To get started, simply choose the services and environments you wish to configure. -The APM app will let you know when your configurations have been applied by your agents. +APM Agent configuration allows you to fine-tune your agent configuration from within the APM app. +Changes are automatically propagated to your APM agents, so there's no need to redeploy. + +To get started, choose the services and environments you wish to configure. +The APM app will let you know when your agents have applied your configurations. [role="screenshot"] image::apm/images/apm-agent-configuration.png[APM Agent configuration in Kibana] @@ -14,29 +18,28 @@ image::apm/images/apm-agent-configuration.png[APM Agent configuration in Kibana] [float] ==== Precedence -Configurations set with APM Agent configuration take precedence over configurations set locally in the Agent. +Configurations set from the APM app take precedence over configurations set locally in each Agent. However, if APM Server is slow to respond, is offline, reports an error, etc., APM agents will use local defaults until they're able to update the configuration. -For this reason, it is still important to set custom default configurations locally in each of your agents. +For this reason, it is still essential to set custom default configurations locally in each of your agents. [float] ==== APM Server setup This feature requires {apm-server-ref}/setup-kibana-endpoint.html[Kibana endpoint configuration] in APM Server. -Why is additional configuration needed in APM Server? -That's because APM Server acts as a proxy between the agents and Kibana. +APM Server acts as a proxy between the agents and Kibana. Kibana communicates any changed settings to APM Server so that your agents only need to poll APM Server to determine which settings have changed. [float] ==== Supported configurations -Each Agent has its own list of supported configurations. +Each Agent has a list of supported configurations. After selecting a Service name and environment in the APM app, -a list of all available configuration options, +a list of all supported configuration options, including descriptions and default values, will be displayed. -Supported configurations are also marked in each Agent's configuration documentation: +Supported configurations are also tagged with the image:./images/dynamic-config.svg[] badge in each Agent's configuration reference: [horizontal] Go Agent:: {apm-go-ref}/configuration.html[Configuration reference] diff --git a/docs/apm/api.asciidoc b/docs/apm/api.asciidoc index a8f4f4bf0baaa..93733f5990a46 100644 --- a/docs/apm/api.asciidoc +++ b/docs/apm/api.asciidoc @@ -1,6 +1,10 @@ [role="xpack"] [[apm-api]] -== API +== APM app API + +++++ +REST API +++++ Some APM app features are provided via a REST API: diff --git a/docs/apm/apm-alerts.asciidoc b/docs/apm/apm-alerts.asciidoc index b8552c007b13d..75ce5f56c96c6 100644 --- a/docs/apm/apm-alerts.asciidoc +++ b/docs/apm/apm-alerts.asciidoc @@ -1,12 +1,16 @@ [role="xpack"] [[apm-alerts]] -=== Create an alert +=== Alerts + +++++ +Create an alert +++++ beta::[] -The APM app is integrated with Kibana's {kibana-ref}/alerting-getting-started.html[alerting and actions] feature. -It provides a set of built-in **actions** and APM specific threshold **alerts** for you to use, -and allows all alerts to be centrally managed from <>. +The APM app integrates with Kibana's {kibana-ref}/alerting-getting-started.html[alerting and actions] feature. +It provides a set of built-in **actions** and APM specific threshold **alerts** for you to use +and enables central management of all alerts from <>. [role="screenshot"] image::apm/images/apm-alert.png[Create an alert in the APM app] @@ -28,9 +32,9 @@ This guide creates an alert for the `opbeans-java` service based on the followin From the APM app, navigate to the `opbeans-java` service and select **Alerts** > **Create threshold alert** > **Transaction duration**. -The name of your alert will automatically be set as `Transaction duration | opbeans-java`, -and the alert will be tagged with `apm` and `service.name:opbeans-java`. -Feel free to edit either of these defaults. +`Transaction duration | opbeans-java` is automatically set as the name of the alert, +and `apm` and `service.name:opbeans-java` are added as tags. +It's fine to change the name of the alert, but do not edit the tags. Based on the alert criteria, define the following alert details: @@ -42,7 +46,7 @@ Based on the alert criteria, define the following alert details: * **FOR THE LAST** - `5 minutes` Select an action type. -Multiple action types can be selected, but in this example we want to post to a slack channel. +Multiple action types can be selected, but in this example, we want to post to a Slack channel. Select **Slack** > **Create a connector**. Enter a name for the connector, and paste the webhook URL. @@ -63,9 +67,9 @@ This guide creates an alert for the `opbeans-python` service based on the follow From the APM app, navigate to the `opbeans-python` service and select **Alerts** > **Create threshold alert** > **Error rate**. -The name of your alert will automatically be set as `Error rate | opbeans-python`, -and the alert will be tagged with `apm` and `service.name:opbeans-python`. -Feel free to edit either of these defaults. +`Error rate | opbeans-python` is automatically set as the name of the alert, +and `apm` and `service.name:opbeans-python` are added as tags. +It's fine to change the name of the alert, but do not edit the tags. Based on the alert criteria, define the following alert details: @@ -93,5 +97,5 @@ From this page, you can create, edit, disable, mute, and delete alerts, and crea See {kibana-ref}/alerting-getting-started.html[alerting and actions] for more information. NOTE: If you are using an **on-premise** Elastic Stack deployment with security, -TLS must be configured for communication between Elasticsearch and Kibana. +communication between Elasticsearch and Kibana must have TLS configured. More information is in the alerting {kibana-ref}/alerting-getting-started.html#alerting-setup-prerequisites[prerequisites]. \ No newline at end of file diff --git a/docs/apm/bottlenecks.asciidoc b/docs/apm/bottlenecks.asciidoc deleted file mode 100644 index fbde3e9ddcbd6..0000000000000 --- a/docs/apm/bottlenecks.asciidoc +++ /dev/null @@ -1,25 +0,0 @@ -[role="xpack"] -[[apm-bottlenecks]] -== Visualizing Application Bottlenecks - -Elastic APM captures different types of information from within instrumented applications: - -* {apm-overview-ref-v}/transaction-spans.html[*Spans*] contain information about a specific code path that has been executed. -They measure from the start to end of an activity, -and they can have a parent/child relationship with other spans. -* {apm-overview-ref-v}/transactions.html[*Transactions*] are a special kind of span that have extra metadata associated with them. -You can think of transactions as the highest level of work you’re measuring within a service. -As an example, a transaction could be a request to your server, a batch job, or a custom transaction type. -* {apm-overview-ref-v}/errors.html[*Errors*] contain information about the original exception that occurred or about a log created when the exception occurred. - -Each of these information types have a specific page associated with them in the APM app. -These various pages display the captured data in curated charts and tables that allow you to easily compare and debug your applications. - -For example, you can see information about response times, requests per minute, and status codes per endpoint. -You can even dive into a specific request sample and get a complete waterfall view of what your application is spending its time on. -You might see that your bottlenecks are in database queries, cache calls, or external requests. -For each incoming request and each application error, -you can also see contextual information such as the request header, user information, -system values, or custom data that you manually attached to the request. - -Having access to application-level insights with just a few clicks can drastically decrease the time you spend debugging errors, slow response times, and crashes. diff --git a/docs/apm/custom-links.asciidoc b/docs/apm/custom-links.asciidoc index 75c1c9d0009a2..4fdf39b643f94 100644 --- a/docs/apm/custom-links.asciidoc +++ b/docs/apm/custom-links.asciidoc @@ -1,6 +1,11 @@ +[role="xpack"] [[custom-links]] === Custom links +++++ +Create custom links +++++ + Elastic's custom link feature allows you to easily create up to 500 dynamic links based on your specific APM data. Custom links can be filtered to only appear in the APM app for relevant services, @@ -12,7 +17,7 @@ Ready to dive in? Jump straight to the <>. [[custom-links-create]] === Create a link -Each custom link consists of a label, url, and optional filter. +Each custom link consists of a label, URL, and optional filter. The easiest way to create a custom link is from within the actions dropdown in the transaction detail page. This method will automatically apply filters, scoping the link to that specific service, environment, transaction type, and transaction name. @@ -25,8 +30,7 @@ and selecting **Create custom link**. ==== Label The name of your custom link. -This text will be shown in the actions context menu, -so keep it as short as possible. +The actions context menu displays this text, so keep it as short as possible. TIP: Custom links are displayed alphabetically in the actions menu. @@ -39,8 +43,8 @@ URLs support dynamic field name variables, encapsulated in double curly brackets These variables will be replaced with transaction metadata when the link is clicked. Because everyone's data is different, -you'll need to examine your own traces to see what metadata is available for use. -The easiest way to do this is to select a trace in the APM app, and click **Metadata** in the **Trace Sample** table. +you'll need to examine your traces to see what metadata is available for use. +To do this, select a trace in the APM app, and click **Metadata** in the **Trace Sample** table. [role="screenshot"] image::apm/images/example-metadata.png[Example metadata] @@ -49,7 +53,7 @@ image::apm/images/example-metadata.png[Example metadata] [[custom-links-filters]] ==== Filters -Filter each link to only appear so it only appears for specific services or transactions. +Filter each link to only appear for specific services or transactions. You can filter on the following fields: * `service.name` @@ -57,7 +61,7 @@ You can filter on the following fields: * `transaction.type` * `transaction.name` -Multiple values are allowed when comma separated. +Multiple values are allowed when comma-separated. [float] [[custom-links-examples]] @@ -68,7 +72,7 @@ Multiple values are allowed when comma separated. :github-query-params: https://help.github.com/en/github/managing-your-work-on-github/about-automation-for-issues-and-pull-requests-with-query-parameters Not sure where to start with custom links? -Take a look at the examples below, and customize them to your liking! +Take a look at the examples below and customize them to your liking! [float] [[custom-links-examples-email]] diff --git a/docs/apm/deployment-annotations.asciidoc b/docs/apm/deployment-annotations.asciidoc new file mode 100644 index 0000000000000..6feadf8463226 --- /dev/null +++ b/docs/apm/deployment-annotations.asciidoc @@ -0,0 +1,17 @@ +[role="xpack"] +[[transactions-annotations]] +=== Track deployments with annotations + +++++ +Track deployments +++++ + +For enhanced visibility into your deployments, we offer deployment annotations on all transaction charts. +This feature automatically tags new deployments, so you can easily see if your deploy has increased response times +for an end-user, or if the memory/CPU footprint of your application has changed. +Being able to identify bad deployments quickly enables you to rollback and fix issues without causing costly outages. + +Deployment annotations are automatically enabled, and appear when the `service.version` of your app changes. + +[role="screenshot"] +image::apm/images/apm-transaction-annotation.png[Example view of transactions annotation in the APM app in Kibana] diff --git a/docs/apm/error-reports-watcher.asciidoc b/docs/apm/error-reports-watcher.asciidoc new file mode 100644 index 0000000000000..f41597932b751 --- /dev/null +++ b/docs/apm/error-reports-watcher.asciidoc @@ -0,0 +1,18 @@ +[role="xpack"] +[[errors-alerts-with-watcher]] +=== Error reports with Watcher + +++++ +Enable error reports +++++ + +You can use the power of the alerting features with Watcher to get reports on error occurrences. +The Watcher assistant, which is available on the errors overview, can help you set up a watch per service. + +Configure the watch with an occurrences threshold, time interval, and the desired actions, such as email or Slack notifications. +With Watcher, your team can set up reports within minutes. + +Watches are managed separately in the dedicated Watcher UI available in Advanced Settings. + +[role="screenshot"] +image::apm/images/apm-errors-watcher-assistant.png[Example view of the Watcher assistant for errors in APM app in Kibana] diff --git a/docs/apm/errors.asciidoc b/docs/apm/errors.asciidoc index 689fa1fffa89e..49351ec255858 100644 --- a/docs/apm/errors.asciidoc +++ b/docs/apm/errors.asciidoc @@ -1,7 +1,8 @@ +[role="xpack"] [[errors]] === Errors overview -TIP: {apm-overview-ref-v}/errors.html[Errors] are defined as groups of exceptions with matching exception or log messages. +TIP: {apm-overview-ref-v}/errors.html[Errors] are groups of exceptions with a similar exception or log message. The *Errors* overview provides a high-level view of the error message and culprit, the number of occurrences, and the most recent occurrence. @@ -20,7 +21,7 @@ image::apm/images/apm-error-group.png[Example view of the error group page in th Here, you'll see the error message, culprit, and the number of occurrences over time. Further down, you'll see the Error occurrence table. -This is where you can see the details of a sampled error within this group. +This table shows the details of a sampled error within this group. The error shown is always the most recent to occur. Each error occurrence features a breakdown of the exception, including the stack trace from when the error occurred, @@ -28,19 +29,4 @@ and additional contextual information to help debug the issue. In some cases, you might also see a Transaction sample ID. This feature allows you to make a connection between the errors and transactions, by linking you to the specific transaction where the error occurred. -This allows you to see the whole trace, including which services the request went through. - -[float] -[[errors-alerts-with-watcher]] -==== Error reports with Watcher - -You can use the power of the alerting features with Watcher to get reports on error occurrences. -The Watcher assistant, which is available on the errors overview, can help you set up a watch per service. - -Configure the watch with an occurrences threshold, time interval, and the desired actions, such as email or Slack notifications. -With Watcher, your team can set up reports within minutes. - -Watches are managed separately in the dedicated Watcher UI available in Advanced Settings. - -[role="screenshot"] -image::apm/images/apm-errors-watcher-assistant.png[Example view of the Watcher assistant for errors in APM app in Kibana] \ No newline at end of file +This allows you to see the whole trace, including which services the request went through. diff --git a/docs/apm/filters.asciidoc b/docs/apm/filters.asciidoc index 99ba827b0198d..d53adb439f0c8 100644 --- a/docs/apm/filters.asciidoc +++ b/docs/apm/filters.asciidoc @@ -1,6 +1,11 @@ +[role="xpack"] [[filters]] === Filters +++++ +Filter data +++++ + APM provides two different ways you can filter your data within the APM App: * <> @@ -42,7 +47,7 @@ It allows you to view only relevant data, and is especially useful for separatin By default, all environments are displayed. If there are no environment options, you'll see "not defined". Service environments are defined when configuring your APM agents. -It's very important to be consistent when naming environments in your agents. +It's vital to be consistent when naming environments in your agents. See the documentation for each agent you're using to learn how to configure service environments: * *Go:* {apm-go-ref}/configuration.html#config-environment[`ELASTIC_APM_ENVIRONMENT`] @@ -62,9 +67,9 @@ but only where they are applicable -- they are typically most useful in their or As an example, if you select a host on the Services overview, then select a transaction group, the host filter will still be applied. -These filters are very useful for quickly and easily removing noise from your data. +These filters are very useful for quickly and easily removing noise from your data. With just a click, you can filter your transactions by the transaction result, -host, container ID, and more. +host, container ID, and more. [role="screenshot"] image::apm/images/local-filter.png[Local filters available in the APM app in Kibana] \ No newline at end of file diff --git a/docs/apm/getting-started.asciidoc b/docs/apm/getting-started.asciidoc index 4a391f1a49672..89ce0be1499c5 100644 --- a/docs/apm/getting-started.asciidoc +++ b/docs/apm/getting-started.asciidoc @@ -1,22 +1,45 @@ [role="xpack"] [[apm-getting-started]] -== Getting Started +== Get started with the APM app -If you have not already installed and configured Elastic APM, -the *Setup Instructions* will get you started. +++++ +Get started +++++ -[role="screenshot"] -image::apm/images/apm-setup.png[Installation instructions on the APM page in Kibana] +Elastic APM captures different types of information from within instrumented applications: +* *Spans* contain information about the execution of a specific code path. +They measure from the start to end of an activity, +and they can have a parent/child relationship with other spans. +* *Transactions* are a special kind of span; +they are the first span for a particular service and have extra metadata associated with them. +As an example, a transaction could be a request to your server, a batch job, or a custom transaction type. +*Traces* link together related transactions to show an end-to-end performance of how a request was served and which services were part of it. +* *Errors* contain information about the original exception that occurred or about a log created when the exception occurred. -Index patterns tell Kibana which Elasticsearch indices you want to explore. -An APM index pattern is necessary for certain features in the APM app, like the query bar. -To set up the correct index pattern, -simply click *Load Kibana objects* at the bottom of the Setup Instructions. +Curated charts and tables display the different types of APM data, which allows you to compare and debug your applications easily. -After you install an Elastic APM agent library in your application, -the application automatically appears in the APM app in {kib}. -No further configuration is required. +* <> +* <> +* <> +* <> +* <> +* <> +* <> -[role="screenshot"] -image::apm/images/apm-index-pattern.png[Setup index pattern for APM in Kibana] +TIP: Want to learn more about the Elastic APM ecosystem? +See the {apm-get-started-ref}/overview.html[APM Overview]. + +include::services.asciidoc[] + +include::traces.asciidoc[] + +include::transactions.asciidoc[] + +include::spans.asciidoc[] + +include::errors.asciidoc[] + +include::metrics.asciidoc[] + +include::service-maps.asciidoc[] diff --git a/docs/apm/how-to-guides.asciidoc b/docs/apm/how-to-guides.asciidoc new file mode 100644 index 0000000000000..9b0efb4f7a359 --- /dev/null +++ b/docs/apm/how-to-guides.asciidoc @@ -0,0 +1,32 @@ +[role="xpack"] +[[apm-how-to]] +== How-to guides + +Learn how to perform common APM app tasks. + + +* <> +* <> +* <> +* <> +* <> +* <> +* <> +* <> + + +include::agent-configuration.asciidoc[] + +include::apm-alerts.asciidoc[] + +include::custom-links.asciidoc[] + +include::error-reports-watcher.asciidoc[] + +include::filters.asciidoc[] + +include::machine-learning.asciidoc[] + +include::advanced-queries.asciidoc[] + +include::deployment-annotations.asciidoc[] \ No newline at end of file diff --git a/docs/apm/images/dynamic-config.svg b/docs/apm/images/dynamic-config.svg new file mode 100644 index 0000000000000..df62a3c84f4b4 --- /dev/null +++ b/docs/apm/images/dynamic-config.svg @@ -0,0 +1 @@ + DynamicDynamic \ No newline at end of file diff --git a/docs/apm/index.asciidoc b/docs/apm/index.asciidoc index d3f0dc5b7f11f..79190efccdff2 100644 --- a/docs/apm/index.asciidoc +++ b/docs/apm/index.asciidoc @@ -4,25 +4,35 @@ [partintro] -- -Elastic Application Performance Monitoring (APM) automatically collects in-depth -performance metrics and errors from inside your applications. - -The **APM** app in {kib} is provided with the basic license. It -enables developers to drill down into the performance data for their applications -and quickly locate the performance bottlenecks. - -* <> -* <> -* <> - -NOTE: For more information about the components of Elastic APM, -see the {apm-get-started-ref}/overview.html[APM Overview]. +The APM app in {kib} is provided with the basic license. +It allows you to monitor your software services and applications in real-time; +visualize detailed performance information on your services, +identify and analyze errors, +and monitor host-level and agent-specific metrics like JVM and Go runtime metrics. + +[float] +[[apm-bottlenecks]] +== Visualizing application bottlenecks + +Having access to application-level insights with just a few clicks can drastically decrease the time you spend +debugging errors, slow response times, and crashes. + +For example, you can see information about response times, requests per minute, and status codes per endpoint. +You can even dive into a specific request sample and get a complete waterfall view of what your application is spending its time on. +You might see that your bottlenecks are in database queries, cache calls, or external requests. +For each incoming request and each application error, +you can also see contextual information such as the request header, user information, +system values, or custom data that you manually attached to the request. -- +include::set-up.asciidoc[] + include::getting-started.asciidoc[] -include::bottlenecks.asciidoc[] +include::how-to-guides.asciidoc[] -include::using-the-apm-ui.asciidoc[] +include::settings.asciidoc[] include::api.asciidoc[] + +include::troubleshooting.asciidoc[] diff --git a/docs/apm/machine-learning.asciidoc b/docs/apm/machine-learning.asciidoc new file mode 100644 index 0000000000000..9d347fc4f1111 --- /dev/null +++ b/docs/apm/machine-learning.asciidoc @@ -0,0 +1,27 @@ +[role="xpack"] +[[machine-learning-integration]] +=== Machine Learning integration + +++++ +Integrate with machine learning +++++ + +The Machine Learning integration will initiate a new job predefined to calculate anomaly scores on transaction response times. +The response time graph will show the expected bounds and add an annotation when the anomaly score is 75 or above. +Jobs can be created per transaction type, and based on the average response time. +Manage jobs in the *Machine Learning jobs management*. + +[role="screenshot"] +image::apm/images/apm-ml-integration.png[Example view of anomaly scores on response times in APM app in Kibana] + +[float] +[[create-ml-integration]] +=== Create a new machine learning job + +To enable machine learning anomaly detection, first choose a service to monitor. +Then, select **Integrations** > **Enable ML anomaly detection** and click **Create job**. +That's it! After a few minutes, the job will begin calculating results; +it might take additional time for results to appear on your graph. + +APM specific anomaly detection wizards are also available for certain Agents. +See the machine learning {ml-docs}/ootb-ml-jobs-apm.html[APM anomaly detection configurations] for more information. diff --git a/docs/apm/metrics.asciidoc b/docs/apm/metrics.asciidoc index ab394b785ef84..e82a4fbd5c291 100644 --- a/docs/apm/metrics.asciidoc +++ b/docs/apm/metrics.asciidoc @@ -1,3 +1,4 @@ +[role="xpack"] [[metrics]] === Metrics overview @@ -5,7 +6,7 @@ The *Metrics* overview provides agent-specific metrics, which lets you perform more in-depth root cause analysis investigations within the APM app. If you're experiencing a problem with your service, you can use this page to attempt to find the underlying cause. -For example, you might be able to correlate a high number of errors with a long transaction duration, high CPU usage, or a memory leak. +For example, you might be able to correlate a high number of errors with a long transaction duration, high CPU usage, or a memory leak. [role="screenshot"] image::apm/images/apm-metrics.png[Example view of the Metrics overview in APM app in Kibana] @@ -17,19 +18,3 @@ thread count, garbage collection rate, and garbage collection time spent per min [role="screenshot"] image::apm/images/jvm-metrics.png[Example view of the Metrics overview for the Java Agent] - -[[machine-learning-integration]] -=== Machine Learning integration - -The Machine Learning integration will initiate a new job predefined to calculate anomaly scores on transaction response times. -The response time graph will show the expected bounds and annotate the graph when the anomaly score is 75 or above. - -[role="screenshot"] -image::apm/images/apm-ml-integration.png[Example view of anomaly scores on response times in APM app in Kibana] - -Jobs can be created per transaction type and based on the average response time. -You can manage jobs in the *Machine Learning jobs management*. -It might take some time for results to appear on the graph. - -Machine learning is a platinum feature. For a comparison of the Elastic license levels, -see https://www.elastic.co/subscriptions[the subscription page]. \ No newline at end of file diff --git a/docs/apm/service-maps.asciidoc b/docs/apm/service-maps.asciidoc index e0d84f33b4dcb..be86b9d522ac5 100644 --- a/docs/apm/service-maps.asciidoc +++ b/docs/apm/service-maps.asciidoc @@ -1,37 +1,53 @@ +[role="xpack"] [[service-maps]] === Service maps beta::[] -A service map is a real-time diagram of the interactions occurring in your application’s architecture. -It allows you to easily visualize data flow and high-level statistics, like average transaction duration, -requests per minute, errors per minute, and metrics, allowing you to quickly assess the status of your services. +WARNING: Service map support for Internet Explorer 11 is extremely limited. +Please use Chrome or Firefox if available. -Our beta offering creates two types of service maps: +A service map is a real-time visual representation of the instrumented services in your application's architecture. +It shows you how these services are connected, along with high-level metrics like average transaction duration, +requests per minute, and errors per minute, that allow you to quickly assess the status of your services. -* Global: All services and connections are shown. -* Service-specific: Selecting a specific service will highlight it's connections. +We currently surface two types of service maps: + +* Global: All services instrumented with APM agents and the connections between them are shown. +* Service-specific: Highlight connections for a selected service. [role="screenshot"] image::apm/images/service-maps.png[Example view of service maps in the APM app in Kibana] +[float] +[[service-maps-how]] +=== How do service maps work? + +Service maps rely on distributed traces to draw connections between services. +As {apm-overview-ref-v}/distributed-tracing.html[distributed tracing] is enabled out-of-the-box for supported technologies, so are service maps. +However, if a service isn't instrumented, +or a `traceparent` header isn't being propagated to it, +distributed tracing will not work, and the connection will not be drawn on the map. + [float] [[visualize-your-architecture]] === Visualize your architecture Select the **Service Map** tab to get started. -By default, all services and connections are shown. -Whether your onboarding a new engineer, or just trying to grasp the big picture, +By default, all instrumented services and connections are shown. +Whether you're onboarding a new engineer, or just trying to grasp the big picture, click around, zoom in and out, and begin to visualize how your services are connected. If there's a specific service that interests you, select that service to highlight its connections. Clicking **Focus map** will refocus the map on that specific service and lock the connection highlighting. -From here, select **Service Details**, or click on the **Transaction** tab to jump to the Transaction overview. +From here, select **Service Details**, or click on the **Transaction** tab to jump to the Transaction overview +for the selected service. You can also use the tabs at the top of the page to easily jump to the **Errors** or **Metrics** overview. -While it's not possible to query in service maps, it is possible to filter by environment. +Filter out your maps by picking the environment from the environment drop-down filter. This can be useful if you have two or more services, in separate environments, but with the same name. -Use the environment drop down to only see the data you're interested in, like `dev` or `production`. +Use the environment drop-down to only see the data you're interested in, like `dev` or `production`. +Additional filters are not currently available for service maps. [role="screenshot"] image::apm/images/service-maps-java.png[Example view of service maps with Java highlighted in the APM app in Kibana] @@ -46,3 +62,18 @@ Nodes appear on the map in one of two shapes: * **Diamond**: Databases, external, and messaging. Interior icons represent the generic type, with specific icons for known entities, like Elasticsearch. Type and subtype are based on `span.type`, and `span.subtype`. + +[float] +[[service-maps-supported]] +=== Supported APM Agents + +Service maps are supported for the following Agent versions: + +[horizontal] +Go Agent:: >= v1.7.0 +Java Agent:: >= v1.13.0 +.NET Agent:: >= v1.3.0 +Node.js Agent:: >= v3.6.0 +Python Agent:: >= v5.5.0 +Ruby Agent:: >= v3.6.0 +Real User Monitoring (RUM) Agent:: >= v4.7.0 diff --git a/docs/apm/services.asciidoc b/docs/apm/services.asciidoc index 9af3e74562dab..395e23c379306 100644 --- a/docs/apm/services.asciidoc +++ b/docs/apm/services.asciidoc @@ -1,9 +1,9 @@ +[role="xpack"] [[services]] === Services overview -The *Services* overview gives you quick insights into the health and general performance of each service. - -You can add services by setting the `service.name` configuration in each of the {apm-agents-ref}[APM agents] you’re instrumenting. +The *Services* overview gives you quick insights into the health and general performance of all of your instrumented services. +Services are sorted by the `service.name` configured in each of the {apm-agents-ref}[APM agents] you’ve installed. [role="screenshot"] image::apm/images/apm-services-overview.png[Example view of services table the APM app in Kibana] \ No newline at end of file diff --git a/docs/apm/set-up.asciidoc b/docs/apm/set-up.asciidoc new file mode 100644 index 0000000000000..c5bf5e13b640b --- /dev/null +++ b/docs/apm/set-up.asciidoc @@ -0,0 +1,35 @@ +[role="xpack"] +[[apm-ui]] +== Set up the APM app + +++++ +Set up +++++ + +APM is available via the navigation sidebar in {Kib}. +If you have not already installed and configured Elastic APM, +the *Setup Instructions* in Kibana will get you started. + +[role="screenshot"] +image::apm/images/apm-setup.png[Installation instructions on the APM page in Kibana] + +[float] +[[apm-configure-index-pattern]] +=== Load the index pattern + +Index patterns tell Kibana which Elasticsearch indices you want to explore. +An APM index pattern is necessary for certain features in the APM app, like the query bar. +To set up the correct index pattern, +simply click *Load Kibana objects* at the bottom of the Setup Instructions. + +[role="screenshot"] +image::apm/images/apm-index-pattern.png[Setup index pattern for APM in Kibana] + +To use a custom index pattern, see <>. + +[float] +[[apm-getting-started-next]] +=== Next steps + +No further configuration in the APM app is required. +Install an APM Agent library in your service to begin visualizing and analyzing your data! diff --git a/docs/apm/settings.asciidoc b/docs/apm/settings.asciidoc index 37122fc9c635d..44da63143f63f 100644 --- a/docs/apm/settings.asciidoc +++ b/docs/apm/settings.asciidoc @@ -1,18 +1,23 @@ // Do not link directly to this page. // Link to the anchor in `/docs/settings/apm-settings.asciidoc` instead. +[role="xpack"] [[apm-settings-in-kibana]] -=== APM settings in Kibana +== APM app settings + +++++ +Settings +++++ You do not need to configure any settings to use the APM app. It is enabled by default. [float] [[apm-indices-settings]] -==== APM Indices +=== APM Indices include::./../settings/apm-settings.asciidoc[tag=apm-indices-settings] [float] [[general-apm-settings]] -==== General APM settings +=== General APM settings include::./../settings/apm-settings.asciidoc[tag=general-apm-settings] diff --git a/docs/apm/spans.asciidoc b/docs/apm/spans.asciidoc index ef21e1c5333e0..2eed339160fc4 100644 --- a/docs/apm/spans.asciidoc +++ b/docs/apm/spans.asciidoc @@ -1,7 +1,8 @@ +[role="xpack"] [[spans]] === Span timeline -TIP: A {apm-overview-ref-v}/transaction-spans.html[span] is defined as the duration of a single event. +TIP: A {apm-overview-ref-v}/transaction-spans.html[span] is the duration of a single event. Spans are automatically captured by APM agents, and you can also define custom spans. Each span has a type and is defined by a different color in the timeline/waterfall visualization. @@ -28,7 +29,7 @@ Services in a distributed trace are separated by color and listed in the order t [role="screenshot"] image::apm/images/apm-services-trace.png[Example of distributed trace colors in the APM app in Kibana] -Don't forget, a distributed trace includes more than one transaction. +Don't forget; a distributed trace includes more than one transaction. When viewing these distributed traces in the timeline waterfall, you'll see this image:apm/images/transaction-icon.png[APM icon] icon, which indicates the next transaction in the trace. These transactions can be expanded and viewed in detail by clicking on them. diff --git a/docs/apm/traces.asciidoc b/docs/apm/traces.asciidoc index 09d8f52b92840..8eef3d9bed4db 100644 --- a/docs/apm/traces.asciidoc +++ b/docs/apm/traces.asciidoc @@ -1,12 +1,17 @@ +[role="xpack"] [[traces]] === Traces overview +TIP: Traces link together related transactions to show an end-to-end performance of how a request was served +and which services were part of it. +In addition to the Traces overview, you can view your application traces in the <>. + The *Traces* overview displays the entry transaction for all traces in your application. If you're using <>, this view is key to finding the critical paths within your application. Transactions with the same name are grouped together and only shown once in this table. By default, transactions are sorted by _Impact_. -Impact helps show the most used and slowest endpoints in your service - in other words, +Impact helps show the most used and slowest endpoints in your service--in other words, it's the collective amount of pain a specific endpoint is causing your users. If there's a particular endpoint you're worried about, you can click on it to view the <>. @@ -33,4 +38,4 @@ You can use the <> to view a waterfall displa [role="screenshot"] image::apm/images/apm-distributed-tracing.png[Example view of the distributed tracing in APM app in Kibana] -TIP: Distributed tracing is supported by all APM agents and there’s no additional configuration needed. \ No newline at end of file +TIP: Distributed tracing is supported by all APM agents, and there's no additional configuration needed. \ No newline at end of file diff --git a/docs/apm/transactions.asciidoc b/docs/apm/transactions.asciidoc index 1eb037009efff..2e1022e6d684c 100644 --- a/docs/apm/transactions.asciidoc +++ b/docs/apm/transactions.asciidoc @@ -1,3 +1,4 @@ +[role="xpack"] [[transactions]] === Transaction overview @@ -56,20 +57,6 @@ For further details, including troubleshooting and custom implementation instruc refer to the documentation for each {apm-agents-ref}[APM Agent] you've implemented. ==== -[[transactions-annotations]] -==== Transaction annotations - -For enhanced visibility into your deployments, we offer deployment annotations on all transaction charts. -This feature automatically tags new deployments, so you can easily see if your deploy has increased response times -for an end-user, or if the memory/CPU footprint of your application has increased. -Being able to quickly identify bad deployments enables you to rollback and fix issues without causing costly outages. - -Deployment annotations are automatically enabled, and appear when the `service.version` of your app changes. - -[role="screenshot"] -image::apm/images/apm-transaction-annotation.png[Example view of transactions annotation in the APM app in Kibana] - - [[rum-transaction-overview]] ==== RUM Transaction overview @@ -82,7 +69,7 @@ image::apm/images/apm-geo-ui.jpg[average page load duration distribution] This data is available due to the geo-ip and user agent pipelines being enabled by default, which allows for the capture of geo-location and user agent data. These visualizations make it easy for you to visualize performance information about your -end users' experience based on their location. +end-users' experience based on their location. [[transaction-details]] ==== Transaction details @@ -103,7 +90,7 @@ The number of requests per bucket is displayed when hovering over the graph, and [role="screenshot"] image::apm/images/apm-transaction-duration-dist.png[Example view of transactions duration distribution graph] -This graph shows a typical distribution, and indicates most of our requests were served quickly - awesome! +This graph shows a typical distribution, and indicates most of our requests were served quickly--awesome! It's the requests on the right, the ones taking longer than average, that we probably want to focus on. When you select one of these buckets, diff --git a/docs/apm/troubleshooting.asciidoc b/docs/apm/troubleshooting.asciidoc index c6174e1786c78..eb4fb790afd7f 100644 --- a/docs/apm/troubleshooting.asciidoc +++ b/docs/apm/troubleshooting.asciidoc @@ -1,19 +1,24 @@ -[[troubleshooting]] -=== Troubleshooting common problems +[[troubleshooting]] +== Troubleshoot common problems + +++++ +Troubleshooting +++++ If you have something to add to this section, please consider creating a pull request with your proposed changes at https://github.com/elastic/kibana. -Also check out the https://discuss.elastic.co/c/apm[APM discussion forum]. +Also, check out the https://discuss.elastic.co/c/apm[APM discussion forum]. +[float] [[no-apm-data-found]] -==== No APM data found +=== No APM data found This section can help with any of the following: * Data isn't displaying in the APM app -* You're seeing a message like "No Services Found", -* You're seeing errors like "Fielddata is disabled on text fields by default..." +* You see a message like "No Services Found", +* You see errors like "Fielddata is disabled on text fields by default..." There are a number of factors that could be at play here. One important thing to double-check first is your index template. @@ -52,12 +57,13 @@ you can customize the indices that the APM app uses to display data. Navigate to *APM* > *Settings* > *Indices*, and change all `apm_oss.*Pattern` values to include the new index pattern. For example: `customIndexName-*`. -==== Unknown route +[float] +=== Unknown route The {apm-app-ref}/transactions.html[transaction overview] will only display helpful information when the transactions in your services are named correctly. If you're seeing "GET unknown route" or "unknown route" in the APM app, -it could be a sign that something isn't working like it should. +it could be a sign that something isn't working as it should. Elastic APM Agents come with built-in support for popular frameworks out-of-the-box. This means, among other things, that the Agent will try to automatically name HTTP requests. @@ -71,7 +77,8 @@ To resolve this, you'll need to head over to the relevant {apm-agents-ref}[Agent Specifically, view the Agent's supported technologies page. You can also use the Agent's public API to manually set a name for the transaction. -==== Fields are not searchable +[float] +=== Fields are not searchable In Elasticsearch, index templates are used to define settings and mappings that determine how fields should be analyzed. The recommended index template file for APM Server is installed by the APM Server packages. @@ -92,7 +99,7 @@ Selecting the `apm-*` index pattern shows a listing of every field defined in th *Ensure a field is searchable* There are two things you can do to if you'd like to ensure a field is searchable: -1. Index your additional data as {apm-overview-ref}/metadata.html[labels] instead. +1. Index your additional data as {apm-overview-ref-v}/metadata.html[labels] instead. These are dynamic by default, which means they will be indexed and become searchable and aggregatable. 2. Use the {apm-server-ref}/configuration-template.html[`append_fields`] feature. As an example, diff --git a/docs/apm/using-the-apm-ui.asciidoc b/docs/apm/using-the-apm-ui.asciidoc deleted file mode 100644 index 904718999069d..0000000000000 --- a/docs/apm/using-the-apm-ui.asciidoc +++ /dev/null @@ -1,51 +0,0 @@ -[role="xpack"] -[[apm-ui]] -== Using APM - -APM is designed to be as intuitive as possible, -but you might come across certain terms or concepts that don’t feel native to you. -Not to worry, we've created this guide to help you get the most out of Elastic APM. - -APM is available via the navigation sidebar in {Kib}. - -* <> -* <> -* <> -* <> -* <> -* <> -* <> -* <> -* <> -* <> -* <> -* <> -* <> - -include::filters.asciidoc[] - -include::services.asciidoc[] - -include::traces.asciidoc[] - -include::transactions.asciidoc[] - -include::spans.asciidoc[] - -include::service-maps.asciidoc[] - -include::errors.asciidoc[] - -include::metrics.asciidoc[] - -include::apm-alerts.asciidoc[] - -include::agent-configuration.asciidoc[] - -include::custom-links.asciidoc[] - -include::advanced-queries.asciidoc[] - -include::settings.asciidoc[] - -include::troubleshooting.asciidoc[] diff --git a/docs/dev-tools/painlesslab/images/painless-lab.png b/docs/dev-tools/painlesslab/images/painless-lab.png new file mode 100644 index 0000000000000..f65257852792e Binary files /dev/null and b/docs/dev-tools/painlesslab/images/painless-lab.png differ diff --git a/docs/dev-tools/painlesslab/index.asciidoc b/docs/dev-tools/painlesslab/index.asciidoc new file mode 100644 index 0000000000000..e55424baf3142 --- /dev/null +++ b/docs/dev-tools/painlesslab/index.asciidoc @@ -0,0 +1,17 @@ +[role="xpack"] +[[painlesslab]] +== Painless Lab + +beta::[] + +The Painless Lab is an interactive code editor that lets you test and +debug {ref}/modules-scripting-painless.html[Painless scripts] in real-time. +You can use the Painless scripting +language to create <>, +process {ref}/docs-reindex.html[reindexed data], define complex +<>, +and work with data in other contexts. + +To get started, go to *Dev Tools > Painless Lab*. + +image::dev-tools/painlesslab/images/painless-lab.png[Painless Lab] diff --git a/docs/development/core/public/kibana-plugin-core-public.savedobject.md b/docs/development/core/public/kibana-plugin-core-public.savedobject.md index c6bc13b98bc06..b67d0536fb336 100644 --- a/docs/development/core/public/kibana-plugin-core-public.savedobject.md +++ b/docs/development/core/public/kibana-plugin-core-public.savedobject.md @@ -18,6 +18,7 @@ export interface SavedObject | [error](./kibana-plugin-core-public.savedobject.error.md) | {
message: string;
statusCode: number;
} | | | [id](./kibana-plugin-core-public.savedobject.id.md) | string | The ID of this Saved Object, guaranteed to be unique for all objects of the same type | | [migrationVersion](./kibana-plugin-core-public.savedobject.migrationversion.md) | SavedObjectsMigrationVersion | Information about the migrations that have been applied to this SavedObject. When Kibana starts up, KibanaMigrator detects outdated documents and migrates them based on this value. For each migration that has been applied, the plugin's name is used as a key and the latest migration version as the value. | +| [namespaces](./kibana-plugin-core-public.savedobject.namespaces.md) | string[] | Namespace(s) that this saved object exists in. This attribute is only used for multi-namespace saved object types. | | [references](./kibana-plugin-core-public.savedobject.references.md) | SavedObjectReference[] | A reference to another saved object. | | [type](./kibana-plugin-core-public.savedobject.type.md) | string | The type of Saved Object. Each plugin can define it's own custom Saved Object types. | | [updated\_at](./kibana-plugin-core-public.savedobject.updated_at.md) | string | Timestamp of the last time this document had been updated. | diff --git a/docs/development/core/public/kibana-plugin-core-public.savedobject.namespaces.md b/docs/development/core/public/kibana-plugin-core-public.savedobject.namespaces.md new file mode 100644 index 0000000000000..257df45934506 --- /dev/null +++ b/docs/development/core/public/kibana-plugin-core-public.savedobject.namespaces.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [SavedObject](./kibana-plugin-core-public.savedobject.md) > [namespaces](./kibana-plugin-core-public.savedobject.namespaces.md) + +## SavedObject.namespaces property + +Namespace(s) that this saved object exists in. This attribute is only used for multi-namespace saved object types. + +Signature: + +```typescript +namespaces?: string[]; +``` diff --git a/docs/development/core/public/kibana-plugin-core-public.scopedhistory.createhref.md b/docs/development/core/public/kibana-plugin-core-public.scopedhistory.createhref.md index 7058656d09947..6bbab43ff6ffc 100644 --- a/docs/development/core/public/kibana-plugin-core-public.scopedhistory.createhref.md +++ b/docs/development/core/public/kibana-plugin-core-public.scopedhistory.createhref.md @@ -4,10 +4,12 @@ ## ScopedHistory.createHref property -Creates an href (string) to the location. +Creates an href (string) to the location. If `prependBasePath` is true (default), it will prepend the location's path with the scoped history basePath. Signature: ```typescript -createHref: (location: LocationDescriptorObject) => string; +createHref: (location: LocationDescriptorObject, { prependBasePath }?: { + prependBasePath?: boolean | undefined; + }) => string; ``` diff --git a/docs/development/core/public/kibana-plugin-core-public.scopedhistory.md b/docs/development/core/public/kibana-plugin-core-public.scopedhistory.md index 5ea47d2090d71..fa29b32c0bafc 100644 --- a/docs/development/core/public/kibana-plugin-core-public.scopedhistory.md +++ b/docs/development/core/public/kibana-plugin-core-public.scopedhistory.md @@ -28,7 +28,7 @@ export declare class ScopedHistory implements Hi | --- | --- | --- | --- | | [action](./kibana-plugin-core-public.scopedhistory.action.md) | | Action | The last action dispatched on the history stack. | | [block](./kibana-plugin-core-public.scopedhistory.block.md) | | (prompt?: string | boolean | History.TransitionPromptHook<HistoryLocationState> | undefined) => UnregisterCallback | Not supported. Use [AppMountParameters.onAppLeave](./kibana-plugin-core-public.appmountparameters.onappleave.md). | -| [createHref](./kibana-plugin-core-public.scopedhistory.createhref.md) | | (location: LocationDescriptorObject<HistoryLocationState>) => string | Creates an href (string) to the location. | +| [createHref](./kibana-plugin-core-public.scopedhistory.createhref.md) | | (location: LocationDescriptorObject<HistoryLocationState>, { prependBasePath }?: {
prependBasePath?: boolean | undefined;
}) => string | Creates an href (string) to the location. If prependBasePath is true (default), it will prepend the location's path with the scoped history basePath. | | [createSubHistory](./kibana-plugin-core-public.scopedhistory.createsubhistory.md) | | <SubHistoryLocationState = unknown>(basePath: string) => ScopedHistory<SubHistoryLocationState> | Creates a ScopedHistory for a subpath of this ScopedHistory. Useful for applications that may have sub-apps that do not need access to the containing application's history. | | [go](./kibana-plugin-core-public.scopedhistory.go.md) | | (n: number) => void | Send the user forward or backwards in the history stack. | | [goBack](./kibana-plugin-core-public.scopedhistory.goback.md) | | () => void | Send the user one location back in the history stack. Equivalent to calling [ScopedHistory.go(-1)](./kibana-plugin-core-public.scopedhistory.go.md). If no more entries are available backwards, this is a no-op. | diff --git a/docs/development/core/server/kibana-plugin-core-server.coresetup.http.md b/docs/development/core/server/kibana-plugin-core-server.coresetup.http.md index dcc1d754feb7c..a8028827cc0a4 100644 --- a/docs/development/core/server/kibana-plugin-core-server.coresetup.http.md +++ b/docs/development/core/server/kibana-plugin-core-server.coresetup.http.md @@ -9,5 +9,7 @@ Signature: ```typescript -http: HttpServiceSetup; +http: HttpServiceSetup & { + resources: HttpResources; + }; ``` diff --git a/docs/development/core/server/kibana-plugin-core-server.coresetup.md b/docs/development/core/server/kibana-plugin-core-server.coresetup.md index c10b460da8b4f..30c054345928b 100644 --- a/docs/development/core/server/kibana-plugin-core-server.coresetup.md +++ b/docs/development/core/server/kibana-plugin-core-server.coresetup.md @@ -20,7 +20,7 @@ export interface CoreSetupContextSetup | [ContextSetup](./kibana-plugin-core-server.contextsetup.md) | | [elasticsearch](./kibana-plugin-core-server.coresetup.elasticsearch.md) | ElasticsearchServiceSetup | [ElasticsearchServiceSetup](./kibana-plugin-core-server.elasticsearchservicesetup.md) | | [getStartServices](./kibana-plugin-core-server.coresetup.getstartservices.md) | StartServicesAccessor<TPluginsStart, TStart> | [StartServicesAccessor](./kibana-plugin-core-server.startservicesaccessor.md) | -| [http](./kibana-plugin-core-server.coresetup.http.md) | HttpServiceSetup | [HttpServiceSetup](./kibana-plugin-core-server.httpservicesetup.md) | +| [http](./kibana-plugin-core-server.coresetup.http.md) | HttpServiceSetup & {
resources: HttpResources;
} | [HttpServiceSetup](./kibana-plugin-core-server.httpservicesetup.md) | | [metrics](./kibana-plugin-core-server.coresetup.metrics.md) | MetricsServiceSetup | [MetricsServiceSetup](./kibana-plugin-core-server.metricsservicesetup.md) | | [savedObjects](./kibana-plugin-core-server.coresetup.savedobjects.md) | SavedObjectsServiceSetup | [SavedObjectsServiceSetup](./kibana-plugin-core-server.savedobjectsservicesetup.md) | | [status](./kibana-plugin-core-server.coresetup.status.md) | StatusServiceSetup | [StatusServiceSetup](./kibana-plugin-core-server.statusservicesetup.md) | diff --git a/docs/development/core/server/kibana-plugin-core-server.httpresources.md b/docs/development/core/server/kibana-plugin-core-server.httpresources.md new file mode 100644 index 0000000000000..cb3170e989e17 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.httpresources.md @@ -0,0 +1,20 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [HttpResources](./kibana-plugin-core-server.httpresources.md) + +## HttpResources interface + +HttpResources service is responsible for serving static & dynamic assets for Kibana application via HTTP. Provides API allowing plug-ins to respond with: - a pre-configured HTML page bootstrapping Kibana client app - custom HTML page - custom JS script file. + +Signature: + +```typescript +export interface HttpResources +``` + +## Properties + +| Property | Type | Description | +| --- | --- | --- | +| [register](./kibana-plugin-core-server.httpresources.register.md) | <P, Q, B>(route: RouteConfig<P, Q, B, 'get'>, handler: HttpResourcesRequestHandler<P, Q, B>) => void | To register a route handler executing passed function to form response. | + diff --git a/docs/development/core/server/kibana-plugin-core-server.httpresources.register.md b/docs/development/core/server/kibana-plugin-core-server.httpresources.register.md new file mode 100644 index 0000000000000..fe3803a6ffe52 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.httpresources.register.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [HttpResources](./kibana-plugin-core-server.httpresources.md) > [register](./kibana-plugin-core-server.httpresources.register.md) + +## HttpResources.register property + +To register a route handler executing passed function to form response. + +Signature: + +```typescript +register: (route: RouteConfig, handler: HttpResourcesRequestHandler) => void; +``` diff --git a/docs/development/core/server/kibana-plugin-core-server.httpresourcesrenderoptions.headers.md b/docs/development/core/server/kibana-plugin-core-server.httpresourcesrenderoptions.headers.md new file mode 100644 index 0000000000000..bb6dec504ff42 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.httpresourcesrenderoptions.headers.md @@ -0,0 +1,18 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [HttpResourcesRenderOptions](./kibana-plugin-core-server.httpresourcesrenderoptions.md) > [headers](./kibana-plugin-core-server.httpresourcesrenderoptions.headers.md) + +## HttpResourcesRenderOptions.headers property + +HTTP Headers with additional information about response. + +Signature: + +```typescript +headers?: ResponseHeaders; +``` + +## Remarks + +All HTML pages are already pre-configured with `content-security-policy` header that cannot be overridden. + diff --git a/docs/development/core/server/kibana-plugin-core-server.httpresourcesrenderoptions.md b/docs/development/core/server/kibana-plugin-core-server.httpresourcesrenderoptions.md new file mode 100644 index 0000000000000..6563e3c636a99 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.httpresourcesrenderoptions.md @@ -0,0 +1,20 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [HttpResourcesRenderOptions](./kibana-plugin-core-server.httpresourcesrenderoptions.md) + +## HttpResourcesRenderOptions interface + +Allows to configure HTTP response parameters + +Signature: + +```typescript +export interface HttpResourcesRenderOptions +``` + +## Properties + +| Property | Type | Description | +| --- | --- | --- | +| [headers](./kibana-plugin-core-server.httpresourcesrenderoptions.headers.md) | ResponseHeaders | HTTP Headers with additional information about response. | + diff --git a/docs/development/core/server/kibana-plugin-core-server.httpresourcesrequesthandler.md b/docs/development/core/server/kibana-plugin-core-server.httpresourcesrequesthandler.md new file mode 100644 index 0000000000000..20f930382955e --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.httpresourcesrequesthandler.md @@ -0,0 +1,18 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [HttpResourcesRequestHandler](./kibana-plugin-core-server.httpresourcesrequesthandler.md) + +## HttpResourcesRequestHandler type + +Extended version of [RequestHandler](./kibana-plugin-core-server.requesthandler.md) having access to [HttpResourcesServiceToolkit](./kibana-plugin-core-server.httpresourcesservicetoolkit.md) to respond with HTML or JS resources. + +Signature: + +```typescript +export declare type HttpResourcesRequestHandler

= RequestHandler; +``` + +## Example + +\`\`\`typescript httpResources.register({ path: '/login', validate: { params: schema.object({ id: schema.string() }), }, }, async (context, request, response) => { //.. return response.renderCoreApp(); }); + diff --git a/docs/development/core/server/kibana-plugin-core-server.httpresourcesresponseoptions.md b/docs/development/core/server/kibana-plugin-core-server.httpresourcesresponseoptions.md new file mode 100644 index 0000000000000..2ea3ea7e58c78 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.httpresourcesresponseoptions.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [HttpResourcesResponseOptions](./kibana-plugin-core-server.httpresourcesresponseoptions.md) + +## HttpResourcesResponseOptions type + +HTTP Resources response parameters + +Signature: + +```typescript +export declare type HttpResourcesResponseOptions = HttpResponseOptions; +``` diff --git a/docs/development/core/server/kibana-plugin-core-server.httpresourcesservicetoolkit.md b/docs/development/core/server/kibana-plugin-core-server.httpresourcesservicetoolkit.md new file mode 100644 index 0000000000000..1c221e13f534f --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.httpresourcesservicetoolkit.md @@ -0,0 +1,23 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [HttpResourcesServiceToolkit](./kibana-plugin-core-server.httpresourcesservicetoolkit.md) + +## HttpResourcesServiceToolkit interface + +Extended set of [KibanaResponseFactory](./kibana-plugin-core-server.kibanaresponsefactory.md) helpers used to respond with HTML or JS resource. + +Signature: + +```typescript +export interface HttpResourcesServiceToolkit +``` + +## Properties + +| Property | Type | Description | +| --- | --- | --- | +| [renderAnonymousCoreApp](./kibana-plugin-core-server.httpresourcesservicetoolkit.renderanonymouscoreapp.md) | (options?: HttpResourcesRenderOptions) => Promise<IKibanaResponse> | To respond with HTML page bootstrapping Kibana application without retrieving user-specific information. | +| [renderCoreApp](./kibana-plugin-core-server.httpresourcesservicetoolkit.rendercoreapp.md) | (options?: HttpResourcesRenderOptions) => Promise<IKibanaResponse> | To respond with HTML page bootstrapping Kibana application. | +| [renderHtml](./kibana-plugin-core-server.httpresourcesservicetoolkit.renderhtml.md) | (options: HttpResourcesResponseOptions) => IKibanaResponse | To respond with a custom HTML page. | +| [renderJs](./kibana-plugin-core-server.httpresourcesservicetoolkit.renderjs.md) | (options: HttpResourcesResponseOptions) => IKibanaResponse | To respond with a custom JS script file. | + diff --git a/docs/development/core/server/kibana-plugin-core-server.httpresourcesservicetoolkit.renderanonymouscoreapp.md b/docs/development/core/server/kibana-plugin-core-server.httpresourcesservicetoolkit.renderanonymouscoreapp.md new file mode 100644 index 0000000000000..3dce9d88c8036 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.httpresourcesservicetoolkit.renderanonymouscoreapp.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [HttpResourcesServiceToolkit](./kibana-plugin-core-server.httpresourcesservicetoolkit.md) > [renderAnonymousCoreApp](./kibana-plugin-core-server.httpresourcesservicetoolkit.renderanonymouscoreapp.md) + +## HttpResourcesServiceToolkit.renderAnonymousCoreApp property + +To respond with HTML page bootstrapping Kibana application without retrieving user-specific information. + +Signature: + +```typescript +renderAnonymousCoreApp: (options?: HttpResourcesRenderOptions) => Promise; +``` diff --git a/docs/development/core/server/kibana-plugin-core-server.httpresourcesservicetoolkit.rendercoreapp.md b/docs/development/core/server/kibana-plugin-core-server.httpresourcesservicetoolkit.rendercoreapp.md new file mode 100644 index 0000000000000..eb4f095bc19be --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.httpresourcesservicetoolkit.rendercoreapp.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [HttpResourcesServiceToolkit](./kibana-plugin-core-server.httpresourcesservicetoolkit.md) > [renderCoreApp](./kibana-plugin-core-server.httpresourcesservicetoolkit.rendercoreapp.md) + +## HttpResourcesServiceToolkit.renderCoreApp property + +To respond with HTML page bootstrapping Kibana application. + +Signature: + +```typescript +renderCoreApp: (options?: HttpResourcesRenderOptions) => Promise; +``` diff --git a/docs/development/core/server/kibana-plugin-core-server.httpresourcesservicetoolkit.renderhtml.md b/docs/development/core/server/kibana-plugin-core-server.httpresourcesservicetoolkit.renderhtml.md new file mode 100644 index 0000000000000..325d19625df44 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.httpresourcesservicetoolkit.renderhtml.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [HttpResourcesServiceToolkit](./kibana-plugin-core-server.httpresourcesservicetoolkit.md) > [renderHtml](./kibana-plugin-core-server.httpresourcesservicetoolkit.renderhtml.md) + +## HttpResourcesServiceToolkit.renderHtml property + +To respond with a custom HTML page. + +Signature: + +```typescript +renderHtml: (options: HttpResourcesResponseOptions) => IKibanaResponse; +``` diff --git a/docs/development/core/server/kibana-plugin-core-server.httpresourcesservicetoolkit.renderjs.md b/docs/development/core/server/kibana-plugin-core-server.httpresourcesservicetoolkit.renderjs.md new file mode 100644 index 0000000000000..f8d4418fc6cba --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.httpresourcesservicetoolkit.renderjs.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [HttpResourcesServiceToolkit](./kibana-plugin-core-server.httpresourcesservicetoolkit.md) > [renderJs](./kibana-plugin-core-server.httpresourcesservicetoolkit.renderjs.md) + +## HttpResourcesServiceToolkit.renderJs property + +To respond with a custom JS script file. + +Signature: + +```typescript +renderJs: (options: HttpResourcesResponseOptions) => IKibanaResponse; +``` diff --git a/docs/development/core/server/kibana-plugin-core-server.irouter.handlelegacyerrors.md b/docs/development/core/server/kibana-plugin-core-server.irouter.handlelegacyerrors.md index 94cf3c94187b0..35d109975c83a 100644 --- a/docs/development/core/server/kibana-plugin-core-server.irouter.handlelegacyerrors.md +++ b/docs/development/core/server/kibana-plugin-core-server.irouter.handlelegacyerrors.md @@ -9,5 +9,5 @@ Wrap a router handler to catch and converts legacy boom errors to proper custom Signature: ```typescript -handleLegacyErrors: (handler: RequestHandler) => RequestHandler; +handleLegacyErrors: RequestHandlerWrapper; ``` diff --git a/docs/development/core/server/kibana-plugin-core-server.irouter.md b/docs/development/core/server/kibana-plugin-core-server.irouter.md index 073f02f1a4191..4bade638a65a5 100644 --- a/docs/development/core/server/kibana-plugin-core-server.irouter.md +++ b/docs/development/core/server/kibana-plugin-core-server.irouter.md @@ -18,7 +18,7 @@ export interface IRouter | --- | --- | --- | | [delete](./kibana-plugin-core-server.irouter.delete.md) | RouteRegistrar<'delete'> | Register a route handler for DELETE request. | | [get](./kibana-plugin-core-server.irouter.get.md) | RouteRegistrar<'get'> | Register a route handler for GET request. | -| [handleLegacyErrors](./kibana-plugin-core-server.irouter.handlelegacyerrors.md) | <P, Q, B>(handler: RequestHandler<P, Q, B>) => RequestHandler<P, Q, B> | Wrap a router handler to catch and converts legacy boom errors to proper custom errors. | +| [handleLegacyErrors](./kibana-plugin-core-server.irouter.handlelegacyerrors.md) | RequestHandlerWrapper | Wrap a router handler to catch and converts legacy boom errors to proper custom errors. | | [patch](./kibana-plugin-core-server.irouter.patch.md) | RouteRegistrar<'patch'> | Register a route handler for PATCH request. | | [post](./kibana-plugin-core-server.irouter.post.md) | RouteRegistrar<'post'> | Register a route handler for POST request. | | [put](./kibana-plugin-core-server.irouter.put.md) | RouteRegistrar<'put'> | Register a route handler for PUT request. | diff --git a/docs/development/core/server/kibana-plugin-core-server.isavedobjecttyperegistry.md b/docs/development/core/server/kibana-plugin-core-server.isavedobjecttyperegistry.md index 245cb1a56439f..f9c621885c001 100644 --- a/docs/development/core/server/kibana-plugin-core-server.isavedobjecttyperegistry.md +++ b/docs/development/core/server/kibana-plugin-core-server.isavedobjecttyperegistry.md @@ -9,5 +9,5 @@ See [SavedObjectTypeRegistry](./kibana-plugin-core-server.savedobjecttyperegistr Signature: ```typescript -export declare type ISavedObjectTypeRegistry = Pick; +export declare type ISavedObjectTypeRegistry = Omit; ``` diff --git a/docs/development/core/server/kibana-plugin-core-server.iscopedrenderingclient.md b/docs/development/core/server/kibana-plugin-core-server.iscopedrenderingclient.md deleted file mode 100644 index 0632b5e5e2297..0000000000000 --- a/docs/development/core/server/kibana-plugin-core-server.iscopedrenderingclient.md +++ /dev/null @@ -1,19 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [IScopedRenderingClient](./kibana-plugin-core-server.iscopedrenderingclient.md) - -## IScopedRenderingClient interface - - -Signature: - -```typescript -export interface IScopedRenderingClient -``` - -## Methods - -| Method | Description | -| --- | --- | -| [render(options)](./kibana-plugin-core-server.iscopedrenderingclient.render.md) | Generate a KibanaResponse which renders an HTML page bootstrapped with the core bundle. Intended as a response body for HTTP route handlers. | - diff --git a/docs/development/core/server/kibana-plugin-core-server.iscopedrenderingclient.render.md b/docs/development/core/server/kibana-plugin-core-server.iscopedrenderingclient.render.md deleted file mode 100644 index ca114bed21149..0000000000000 --- a/docs/development/core/server/kibana-plugin-core-server.iscopedrenderingclient.render.md +++ /dev/null @@ -1,41 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [IScopedRenderingClient](./kibana-plugin-core-server.iscopedrenderingclient.md) > [render](./kibana-plugin-core-server.iscopedrenderingclient.render.md) - -## IScopedRenderingClient.render() method - -Generate a `KibanaResponse` which renders an HTML page bootstrapped with the `core` bundle. Intended as a response body for HTTP route handlers. - -Signature: - -```typescript -render(options?: Pick): Promise; -``` - -## Parameters - -| Parameter | Type | Description | -| --- | --- | --- | -| options | Pick<IRenderOptions, 'includeUserSettings'> | | - -Returns: - -`Promise` - -## Example - - -```ts -router.get( - { path: '/', validate: false }, - (context, request, response) => - response.ok({ - body: await context.core.rendering.render(), - headers: { - 'content-security-policy': context.core.http.csp.header, - }, - }) -); - -``` - diff --git a/docs/development/core/server/kibana-plugin-core-server.legacyservicesetupdeps.md b/docs/development/core/server/kibana-plugin-core-server.legacyservicesetupdeps.md index f037b7b3e7cb2..a5c1d59be06d3 100644 --- a/docs/development/core/server/kibana-plugin-core-server.legacyservicesetupdeps.md +++ b/docs/development/core/server/kibana-plugin-core-server.legacyservicesetupdeps.md @@ -20,4 +20,5 @@ export interface LegacyServiceSetupDeps | --- | --- | --- | | [core](./kibana-plugin-core-server.legacyservicesetupdeps.core.md) | LegacyCoreSetup | | | [plugins](./kibana-plugin-core-server.legacyservicesetupdeps.plugins.md) | Record<string, unknown> | | +| [uiPlugins](./kibana-plugin-core-server.legacyservicesetupdeps.uiplugins.md) | UiPlugins | | diff --git a/docs/development/core/server/kibana-plugin-core-server.legacyservicesetupdeps.uiplugins.md b/docs/development/core/server/kibana-plugin-core-server.legacyservicesetupdeps.uiplugins.md new file mode 100644 index 0000000000000..d19a7dfcbfcfa --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.legacyservicesetupdeps.uiplugins.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [LegacyServiceSetupDeps](./kibana-plugin-core-server.legacyservicesetupdeps.md) > [uiPlugins](./kibana-plugin-core-server.legacyservicesetupdeps.uiplugins.md) + +## LegacyServiceSetupDeps.uiPlugins property + +Signature: + +```typescript +uiPlugins: UiPlugins; +``` diff --git a/docs/development/core/server/kibana-plugin-core-server.md b/docs/development/core/server/kibana-plugin-core-server.md index accab9bf0cb36..5450e84417f89 100644 --- a/docs/development/core/server/kibana-plugin-core-server.md +++ b/docs/development/core/server/kibana-plugin-core-server.md @@ -80,6 +80,9 @@ The plugin integrates with the core system via lifecycle events: `setup` | [EnvironmentMode](./kibana-plugin-core-server.environmentmode.md) | | | [ErrorHttpResponseOptions](./kibana-plugin-core-server.errorhttpresponseoptions.md) | HTTP response parameters | | [FakeRequest](./kibana-plugin-core-server.fakerequest.md) | Fake request object created manually by Kibana plugins. | +| [HttpResources](./kibana-plugin-core-server.httpresources.md) | HttpResources service is responsible for serving static & dynamic assets for Kibana application via HTTP. Provides API allowing plug-ins to respond with: - a pre-configured HTML page bootstrapping Kibana client app - custom HTML page - custom JS script file. | +| [HttpResourcesRenderOptions](./kibana-plugin-core-server.httpresourcesrenderoptions.md) | Allows to configure HTTP response parameters | +| [HttpResourcesServiceToolkit](./kibana-plugin-core-server.httpresourcesservicetoolkit.md) | Extended set of [KibanaResponseFactory](./kibana-plugin-core-server.kibanaresponsefactory.md) helpers used to respond with HTML or JS resource. | | [HttpResponseOptions](./kibana-plugin-core-server.httpresponseoptions.md) | HTTP response parameters | | [HttpServerInfo](./kibana-plugin-core-server.httpserverinfo.md) | | | [HttpServiceSetup](./kibana-plugin-core-server.httpservicesetup.md) | Kibana HTTP Service provides own abstraction for work with HTTP stack. Plugins don't have direct access to hapi server and its primitives anymore. Moreover, plugins shouldn't rely on the fact that HTTP Service uses one or another library under the hood. This gives the platform flexibility to upgrade or changing our internal HTTP stack without breaking plugins. If the HTTP Service lacks functionality you need, we are happy to discuss and support your needs. | @@ -92,7 +95,6 @@ The plugin integrates with the core system via lifecycle events: `setup` | [IndexSettingsDeprecationInfo](./kibana-plugin-core-server.indexsettingsdeprecationinfo.md) | | | [IRenderOptions](./kibana-plugin-core-server.irenderoptions.md) | | | [IRouter](./kibana-plugin-core-server.irouter.md) | Registers route handlers for specified resource path and method. See [RouteConfig](./kibana-plugin-core-server.routeconfig.md) and [RequestHandler](./kibana-plugin-core-server.requesthandler.md) for more information about arguments to route registrations. | -| [IScopedRenderingClient](./kibana-plugin-core-server.iscopedrenderingclient.md) | | | [IUiSettingsClient](./kibana-plugin-core-server.iuisettingsclient.md) | Server-side client that provides access to the advanced settings stored in elasticsearch. The settings provide control over the behavior of the Kibana application. For example, a user can specify how to display numeric or date fields. Users can adjust the settings via Management UI. | | [KibanaRequestEvents](./kibana-plugin-core-server.kibanarequestevents.md) | Request events. | | [KibanaRequestRoute](./kibana-plugin-core-server.kibanarequestroute.md) | Request specific route information exposed to a handler. | @@ -118,7 +120,7 @@ The plugin integrates with the core system via lifecycle events: `setup` | [PluginConfigDescriptor](./kibana-plugin-core-server.pluginconfigdescriptor.md) | Describes a plugin configuration properties. | | [PluginInitializerContext](./kibana-plugin-core-server.plugininitializercontext.md) | Context that's available to plugins during initialization stage. | | [PluginManifest](./kibana-plugin-core-server.pluginmanifest.md) | Describes the set of required and optional properties plugin can define in its mandatory JSON manifest file. | -| [RequestHandlerContext](./kibana-plugin-core-server.requesthandlercontext.md) | Plugin specific context passed to a route handler.Provides the following clients and services: - [rendering](./kibana-plugin-core-server.iscopedrenderingclient.md) - Rendering client which uses the data of the incoming request - [savedObjects.client](./kibana-plugin-core-server.savedobjectsclient.md) - Saved Objects client which uses the credentials of the incoming request - [savedObjects.typeRegistry](./kibana-plugin-core-server.isavedobjecttyperegistry.md) - Type registry containing all the registered types. - [elasticsearch.dataClient](./kibana-plugin-core-server.scopedclusterclient.md) - Elasticsearch data client which uses the credentials of the incoming request - [elasticsearch.adminClient](./kibana-plugin-core-server.scopedclusterclient.md) - Elasticsearch admin client which uses the credentials of the incoming request - [uiSettings.client](./kibana-plugin-core-server.iuisettingsclient.md) - uiSettings client which uses the credentials of the incoming request | +| [RequestHandlerContext](./kibana-plugin-core-server.requesthandlercontext.md) | Plugin specific context passed to a route handler.Provides the following clients and services: - [savedObjects.client](./kibana-plugin-core-server.savedobjectsclient.md) - Saved Objects client which uses the credentials of the incoming request - [savedObjects.typeRegistry](./kibana-plugin-core-server.isavedobjecttyperegistry.md) - Type registry containing all the registered types. - [elasticsearch.dataClient](./kibana-plugin-core-server.scopedclusterclient.md) - Elasticsearch data client which uses the credentials of the incoming request - [elasticsearch.adminClient](./kibana-plugin-core-server.scopedclusterclient.md) - Elasticsearch admin client which uses the credentials of the incoming request - [uiSettings.client](./kibana-plugin-core-server.iuisettingsclient.md) - uiSettings client which uses the credentials of the incoming request | | [RouteConfig](./kibana-plugin-core-server.routeconfig.md) | Route specific configuration. | | [RouteConfigOptions](./kibana-plugin-core-server.routeconfigoptions.md) | Additional route options. | | [RouteConfigOptionsBody](./kibana-plugin-core-server.routeconfigoptionsbody.md) | Additional body options for a route | @@ -130,6 +132,7 @@ The plugin integrates with the core system via lifecycle events: `setup` | [SavedObjectMigrationContext](./kibana-plugin-core-server.savedobjectmigrationcontext.md) | Migration context provided when invoking a [migration handler](./kibana-plugin-core-server.savedobjectmigrationfn.md) | | [SavedObjectMigrationMap](./kibana-plugin-core-server.savedobjectmigrationmap.md) | A map of [migration functions](./kibana-plugin-core-server.savedobjectmigrationfn.md) to be used for a given type. The map's keys must be valid semver versions.For a given document, only migrations with a higher version number than that of the document will be applied. Migrations are executed in order, starting from the lowest version and ending with the highest one. | | [SavedObjectReference](./kibana-plugin-core-server.savedobjectreference.md) | A reference to another saved object. | +| [SavedObjectsAddToNamespacesOptions](./kibana-plugin-core-server.savedobjectsaddtonamespacesoptions.md) | | | [SavedObjectsBaseOptions](./kibana-plugin-core-server.savedobjectsbaseoptions.md) | | | [SavedObjectsBulkCreateObject](./kibana-plugin-core-server.savedobjectsbulkcreateobject.md) | | | [SavedObjectsBulkGetObject](./kibana-plugin-core-server.savedobjectsbulkgetobject.md) | | @@ -143,6 +146,7 @@ The plugin integrates with the core system via lifecycle events: `setup` | [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) | | +| [SavedObjectsDeleteFromNamespacesOptions](./kibana-plugin-core-server.savedobjectsdeletefromnamespacesoptions.md) | | | [SavedObjectsDeleteOptions](./kibana-plugin-core-server.savedobjectsdeleteoptions.md) | | | [SavedObjectsExportOptions](./kibana-plugin-core-server.savedobjectsexportoptions.md) | Options controlling the export operation. | | [SavedObjectsExportResultDetails](./kibana-plugin-core-server.savedobjectsexportresultdetails.md) | Structure of the export result details entry | @@ -214,6 +218,8 @@ The plugin integrates with the core system via lifecycle events: `setup` | [HandlerFunction](./kibana-plugin-core-server.handlerfunction.md) | A function that accepts a context object and an optional number of additional arguments. Used for the generic types in [IContextContainer](./kibana-plugin-core-server.icontextcontainer.md) | | [HandlerParameters](./kibana-plugin-core-server.handlerparameters.md) | Extracts the types of the additional arguments of a [HandlerFunction](./kibana-plugin-core-server.handlerfunction.md), excluding the [HandlerContextType](./kibana-plugin-core-server.handlercontexttype.md). | | [Headers](./kibana-plugin-core-server.headers.md) | Http request headers to read. | +| [HttpResourcesRequestHandler](./kibana-plugin-core-server.httpresourcesrequesthandler.md) | Extended version of [RequestHandler](./kibana-plugin-core-server.requesthandler.md) having access to [HttpResourcesServiceToolkit](./kibana-plugin-core-server.httpresourcesservicetoolkit.md) to respond with HTML or JS resources. | +| [HttpResourcesResponseOptions](./kibana-plugin-core-server.httpresourcesresponseoptions.md) | HTTP Resources response parameters | | [HttpResponsePayload](./kibana-plugin-core-server.httpresponsepayload.md) | Data send to the client as a response payload. | | [IBasePath](./kibana-plugin-core-server.ibasepath.md) | Access or manipulate the Kibana base path[BasePath](./kibana-plugin-core-server.basepath.md) | | [IClusterClient](./kibana-plugin-core-server.iclusterclient.md) | Represents an Elasticsearch cluster API client created by the platform. It allows to call API on behalf of the internal Kibana user and the actual user that is derived from the request headers (via asScoped(...)).See [ClusterClient](./kibana-plugin-core-server.clusterclient.md). | @@ -243,6 +249,7 @@ The plugin integrates with the core system via lifecycle events: `setup` | [RequestHandler](./kibana-plugin-core-server.requesthandler.md) | A function executed when route path matched requested resource path. Request handler is expected to return a result of one of [KibanaResponseFactory](./kibana-plugin-core-server.kibanaresponsefactory.md) functions. | | [RequestHandlerContextContainer](./kibana-plugin-core-server.requesthandlercontextcontainer.md) | An object that handles registration of http request context providers. | | [RequestHandlerContextProvider](./kibana-plugin-core-server.requesthandlercontextprovider.md) | Context provider for request handler. Extends request context object with provided functionality or data. | +| [RequestHandlerWrapper](./kibana-plugin-core-server.requesthandlerwrapper.md) | Type-safe wrapper for [RequestHandler](./kibana-plugin-core-server.requesthandler.md) function. | | [ResponseError](./kibana-plugin-core-server.responseerror.md) | Error message and optional data send to the client in case of error. | | [ResponseErrorAttributes](./kibana-plugin-core-server.responseerrorattributes.md) | Additional data to provide error details. | | [ResponseHeaders](./kibana-plugin-core-server.responseheaders.md) | Http response headers to set. | @@ -262,6 +269,7 @@ The plugin integrates with the core system via lifecycle events: `setup` | [SavedObjectsClientFactoryProvider](./kibana-plugin-core-server.savedobjectsclientfactoryprovider.md) | Provider to invoke to retrieve a [SavedObjectsClientFactory](./kibana-plugin-core-server.savedobjectsclientfactory.md). | | [SavedObjectsClientWrapperFactory](./kibana-plugin-core-server.savedobjectsclientwrapperfactory.md) | Describes the factory used to create instances of Saved Objects Client Wrappers. | | [SavedObjectsFieldMapping](./kibana-plugin-core-server.savedobjectsfieldmapping.md) | Describe a [saved object type mapping](./kibana-plugin-core-server.savedobjectstypemappingdefinition.md) field.Please refer to [elasticsearch documentation](https://www.elastic.co/guide/en/elasticsearch/reference/current/mapping-types.html) For the mapping documentation | +| [SavedObjectsNamespaceType](./kibana-plugin-core-server.savedobjectsnamespacetype.md) | The namespace type dictates how a saved object can be interacted in relation to namespaces. Each type is mutually exclusive: \* single (default): this type of saved object is namespace-isolated, e.g., it exists in only one namespace. \* multiple: this type of saved object is shareable, e.g., it can exist in one or more namespaces. \* agnostic: this type of saved object is global.Note: do not write logic that uses this value directly; instead, use the appropriate accessors in the [type registry](./kibana-plugin-core-server.savedobjecttyperegistry.md). | | [ScopeableRequest](./kibana-plugin-core-server.scopeablerequest.md) | A user credentials container. It accommodates the necessary auth credentials to impersonate the current user.See [KibanaRequest](./kibana-plugin-core-server.kibanarequest.md). | | [ServiceStatusLevel](./kibana-plugin-core-server.servicestatuslevel.md) | A convenience type that represents the union of each value in [ServiceStatusLevels](./kibana-plugin-core-server.servicestatuslevels.md). | | [SharedGlobalConfig](./kibana-plugin-core-server.sharedglobalconfig.md) | | diff --git a/docs/development/core/server/kibana-plugin-core-server.requesthandler.md b/docs/development/core/server/kibana-plugin-core-server.requesthandler.md index 156f38fab0983..cecef7c923568 100644 --- a/docs/development/core/server/kibana-plugin-core-server.requesthandler.md +++ b/docs/development/core/server/kibana-plugin-core-server.requesthandler.md @@ -9,7 +9,7 @@ A function executed when route path matched requested resource path. Request han Signature: ```typescript -export declare type RequestHandler

= (context: RequestHandlerContext, request: KibanaRequest, response: KibanaResponseFactory) => IKibanaResponse | Promise>; +export declare type RequestHandler

= (context: RequestHandlerContext, request: KibanaRequest, response: ResponseFactory) => IKibanaResponse | Promise>; ``` ## Example diff --git a/docs/development/core/server/kibana-plugin-core-server.requesthandlercontext.core.md b/docs/development/core/server/kibana-plugin-core-server.requesthandlercontext.core.md index 3c6bee114b6ab..0d640e52c3a03 100644 --- a/docs/development/core/server/kibana-plugin-core-server.requesthandlercontext.core.md +++ b/docs/development/core/server/kibana-plugin-core-server.requesthandlercontext.core.md @@ -8,7 +8,6 @@ ```typescript core: { - rendering: IScopedRenderingClient; savedObjects: { client: SavedObjectsClientContract; typeRegistry: ISavedObjectTypeRegistry; diff --git a/docs/development/core/server/kibana-plugin-core-server.requesthandlercontext.md b/docs/development/core/server/kibana-plugin-core-server.requesthandlercontext.md index b65ae47f0e0c1..0966b91a4ebf2 100644 --- a/docs/development/core/server/kibana-plugin-core-server.requesthandlercontext.md +++ b/docs/development/core/server/kibana-plugin-core-server.requesthandlercontext.md @@ -6,7 +6,7 @@ Plugin specific context passed to a route handler. -Provides the following clients and services: - [rendering](./kibana-plugin-core-server.iscopedrenderingclient.md) - Rendering client which uses the data of the incoming request - [savedObjects.client](./kibana-plugin-core-server.savedobjectsclient.md) - Saved Objects client which uses the credentials of the incoming request - [savedObjects.typeRegistry](./kibana-plugin-core-server.isavedobjecttyperegistry.md) - Type registry containing all the registered types. - [elasticsearch.dataClient](./kibana-plugin-core-server.scopedclusterclient.md) - Elasticsearch data client which uses the credentials of the incoming request - [elasticsearch.adminClient](./kibana-plugin-core-server.scopedclusterclient.md) - Elasticsearch admin client which uses the credentials of the incoming request - [uiSettings.client](./kibana-plugin-core-server.iuisettingsclient.md) - uiSettings client which uses the credentials of the incoming request +Provides the following clients and services: - [savedObjects.client](./kibana-plugin-core-server.savedobjectsclient.md) - Saved Objects client which uses the credentials of the incoming request - [savedObjects.typeRegistry](./kibana-plugin-core-server.isavedobjecttyperegistry.md) - Type registry containing all the registered types. - [elasticsearch.dataClient](./kibana-plugin-core-server.scopedclusterclient.md) - Elasticsearch data client which uses the credentials of the incoming request - [elasticsearch.adminClient](./kibana-plugin-core-server.scopedclusterclient.md) - Elasticsearch admin client which uses the credentials of the incoming request - [uiSettings.client](./kibana-plugin-core-server.iuisettingsclient.md) - uiSettings client which uses the credentials of the incoming request Signature: @@ -18,5 +18,5 @@ export interface RequestHandlerContext | Property | Type | Description | | --- | --- | --- | -| [core](./kibana-plugin-core-server.requesthandlercontext.core.md) | {
rendering: IScopedRenderingClient;
savedObjects: {
client: SavedObjectsClientContract;
typeRegistry: ISavedObjectTypeRegistry;
};
elasticsearch: {
dataClient: IScopedClusterClient;
adminClient: IScopedClusterClient;
};
uiSettings: {
client: IUiSettingsClient;
};
} | | +| [core](./kibana-plugin-core-server.requesthandlercontext.core.md) | {
savedObjects: {
client: SavedObjectsClientContract;
typeRegistry: ISavedObjectTypeRegistry;
};
elasticsearch: {
dataClient: IScopedClusterClient;
adminClient: IScopedClusterClient;
};
uiSettings: {
client: IUiSettingsClient;
};
} | | diff --git a/docs/development/core/server/kibana-plugin-core-server.requesthandlerwrapper.md b/docs/development/core/server/kibana-plugin-core-server.requesthandlerwrapper.md new file mode 100644 index 0000000000000..a9fe188ee2bff --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.requesthandlerwrapper.md @@ -0,0 +1,27 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [RequestHandlerWrapper](./kibana-plugin-core-server.requesthandlerwrapper.md) + +## RequestHandlerWrapper type + +Type-safe wrapper for [RequestHandler](./kibana-plugin-core-server.requesthandler.md) function. + +Signature: + +```typescript +export declare type RequestHandlerWrapper = (handler: RequestHandler) => RequestHandler; +``` + +## Example + + +```typescript +export const wrapper: RequestHandlerWrapper = handler => { + return async (context, request, response) => { + // do some logic + ... + }; +} + +``` + diff --git a/docs/development/core/server/kibana-plugin-core-server.responseheaders.md b/docs/development/core/server/kibana-plugin-core-server.responseheaders.md index 4551d1cab8632..fb7d6a10c6b6c 100644 --- a/docs/development/core/server/kibana-plugin-core-server.responseheaders.md +++ b/docs/development/core/server/kibana-plugin-core-server.responseheaders.md @@ -9,9 +9,5 @@ Http response headers to set. Signature: ```typescript -export declare type ResponseHeaders = { - [header in KnownHeaders]?: string | string[]; -} & { - [header: string]: string | string[]; -}; +export declare type ResponseHeaders = Record | Record; ``` diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobject.md b/docs/development/core/server/kibana-plugin-core-server.savedobject.md index 0df97b0d4221a..94d1c378899df 100644 --- a/docs/development/core/server/kibana-plugin-core-server.savedobject.md +++ b/docs/development/core/server/kibana-plugin-core-server.savedobject.md @@ -18,6 +18,7 @@ export interface SavedObject | [error](./kibana-plugin-core-server.savedobject.error.md) | {
message: string;
statusCode: number;
} | | | [id](./kibana-plugin-core-server.savedobject.id.md) | string | The ID of this Saved Object, guaranteed to be unique for all objects of the same type | | [migrationVersion](./kibana-plugin-core-server.savedobject.migrationversion.md) | SavedObjectsMigrationVersion | Information about the migrations that have been applied to this SavedObject. When Kibana starts up, KibanaMigrator detects outdated documents and migrates them based on this value. For each migration that has been applied, the plugin's name is used as a key and the latest migration version as the value. | +| [namespaces](./kibana-plugin-core-server.savedobject.namespaces.md) | string[] | Namespace(s) that this saved object exists in. This attribute is only used for multi-namespace saved object types. | | [references](./kibana-plugin-core-server.savedobject.references.md) | SavedObjectReference[] | A reference to another saved object. | | [type](./kibana-plugin-core-server.savedobject.type.md) | string | The type of Saved Object. Each plugin can define it's own custom Saved Object types. | | [updated\_at](./kibana-plugin-core-server.savedobject.updated_at.md) | string | Timestamp of the last time this document had been updated. | diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobject.namespaces.md b/docs/development/core/server/kibana-plugin-core-server.savedobject.namespaces.md new file mode 100644 index 0000000000000..2a555db01df3b --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.savedobject.namespaces.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObject](./kibana-plugin-core-server.savedobject.md) > [namespaces](./kibana-plugin-core-server.savedobject.namespaces.md) + +## SavedObject.namespaces property + +Namespace(s) that this saved object exists in. This attribute is only used for multi-namespace saved object types. + +Signature: + +```typescript +namespaces?: string[]; +``` diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsaddtonamespacesoptions.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsaddtonamespacesoptions.md new file mode 100644 index 0000000000000..711588bdd608c --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsaddtonamespacesoptions.md @@ -0,0 +1,20 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectsAddToNamespacesOptions](./kibana-plugin-core-server.savedobjectsaddtonamespacesoptions.md) + +## SavedObjectsAddToNamespacesOptions interface + + +Signature: + +```typescript +export interface SavedObjectsAddToNamespacesOptions extends SavedObjectsBaseOptions +``` + +## Properties + +| Property | Type | Description | +| --- | --- | --- | +| [refresh](./kibana-plugin-core-server.savedobjectsaddtonamespacesoptions.refresh.md) | MutatingOperationRefreshSetting | The Elasticsearch Refresh setting for this operation | +| [version](./kibana-plugin-core-server.savedobjectsaddtonamespacesoptions.version.md) | string | An opaque version number which changes on each successful write operation. Can be used for implementing optimistic concurrency control. | + diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsaddtonamespacesoptions.refresh.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsaddtonamespacesoptions.refresh.md new file mode 100644 index 0000000000000..c0a1008ab5331 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsaddtonamespacesoptions.refresh.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectsAddToNamespacesOptions](./kibana-plugin-core-server.savedobjectsaddtonamespacesoptions.md) > [refresh](./kibana-plugin-core-server.savedobjectsaddtonamespacesoptions.refresh.md) + +## SavedObjectsAddToNamespacesOptions.refresh property + +The Elasticsearch Refresh setting for this operation + +Signature: + +```typescript +refresh?: MutatingOperationRefreshSetting; +``` diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsaddtonamespacesoptions.version.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsaddtonamespacesoptions.version.md new file mode 100644 index 0000000000000..9432b4bf80da6 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsaddtonamespacesoptions.version.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectsAddToNamespacesOptions](./kibana-plugin-core-server.savedobjectsaddtonamespacesoptions.md) > [version](./kibana-plugin-core-server.savedobjectsaddtonamespacesoptions.version.md) + +## SavedObjectsAddToNamespacesOptions.version property + +An opaque version number which changes on each successful write operation. Can be used for implementing optimistic concurrency control. + +Signature: + +```typescript +version?: string; +``` diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsclient.addtonamespaces.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsclient.addtonamespaces.md new file mode 100644 index 0000000000000..45c9c39f9626a --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsclient.addtonamespaces.md @@ -0,0 +1,27 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectsClient](./kibana-plugin-core-server.savedobjectsclient.md) > [addToNamespaces](./kibana-plugin-core-server.savedobjectsclient.addtonamespaces.md) + +## SavedObjectsClient.addToNamespaces() method + +Adds namespaces to a SavedObject + +Signature: + +```typescript +addToNamespaces(type: string, id: string, namespaces: string[], options?: SavedObjectsAddToNamespacesOptions): Promise<{}>; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| type | string | | +| id | string | | +| namespaces | string[] | | +| options | SavedObjectsAddToNamespacesOptions | | + +Returns: + +`Promise<{}>` + diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsclient.deletefromnamespaces.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsclient.deletefromnamespaces.md new file mode 100644 index 0000000000000..80b58d29d393b --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsclient.deletefromnamespaces.md @@ -0,0 +1,27 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectsClient](./kibana-plugin-core-server.savedobjectsclient.md) > [deleteFromNamespaces](./kibana-plugin-core-server.savedobjectsclient.deletefromnamespaces.md) + +## SavedObjectsClient.deleteFromNamespaces() method + +Removes namespaces from a SavedObject + +Signature: + +```typescript +deleteFromNamespaces(type: string, id: string, namespaces: string[], options?: SavedObjectsDeleteFromNamespacesOptions): Promise<{}>; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| type | string | | +| id | string | | +| namespaces | string[] | | +| options | SavedObjectsDeleteFromNamespacesOptions | | + +Returns: + +`Promise<{}>` + diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsclient.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsclient.md index 5a8a213d2bccc..7038c0c07012f 100644 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectsclient.md +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsclient.md @@ -25,11 +25,13 @@ The constructor for this class is marked as internal. Third-party code should no | Method | Modifiers | Description | | --- | --- | --- | +| [addToNamespaces(type, id, namespaces, options)](./kibana-plugin-core-server.savedobjectsclient.addtonamespaces.md) | | Adds namespaces to a SavedObject | | [bulkCreate(objects, options)](./kibana-plugin-core-server.savedobjectsclient.bulkcreate.md) | | Persists multiple documents batched together as a single request | | [bulkGet(objects, options)](./kibana-plugin-core-server.savedobjectsclient.bulkget.md) | | Returns an array of objects by id | | [bulkUpdate(objects, options)](./kibana-plugin-core-server.savedobjectsclient.bulkupdate.md) | | Bulk Updates multiple SavedObject at once | | [create(type, attributes, options)](./kibana-plugin-core-server.savedobjectsclient.create.md) | | Persists a SavedObject | | [delete(type, id, options)](./kibana-plugin-core-server.savedobjectsclient.delete.md) | | Deletes a SavedObject | +| [deleteFromNamespaces(type, id, namespaces, options)](./kibana-plugin-core-server.savedobjectsclient.deletefromnamespaces.md) | | Removes namespaces from a SavedObject | | [find(options)](./kibana-plugin-core-server.savedobjectsclient.find.md) | | Find all SavedObjects matching the search query | | [get(type, id, options)](./kibana-plugin-core-server.savedobjectsclient.get.md) | | Retrieves a single object | | [update(type, id, attributes, options)](./kibana-plugin-core-server.savedobjectsclient.update.md) | | Updates an SavedObject | diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsdeletefromnamespacesoptions.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsdeletefromnamespacesoptions.md new file mode 100644 index 0000000000000..8a2afe6656fa4 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsdeletefromnamespacesoptions.md @@ -0,0 +1,19 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectsDeleteFromNamespacesOptions](./kibana-plugin-core-server.savedobjectsdeletefromnamespacesoptions.md) + +## SavedObjectsDeleteFromNamespacesOptions interface + + +Signature: + +```typescript +export interface SavedObjectsDeleteFromNamespacesOptions extends SavedObjectsBaseOptions +``` + +## Properties + +| Property | Type | Description | +| --- | --- | --- | +| [refresh](./kibana-plugin-core-server.savedobjectsdeletefromnamespacesoptions.refresh.md) | MutatingOperationRefreshSetting | The Elasticsearch Refresh setting for this operation | + diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsdeletefromnamespacesoptions.refresh.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsdeletefromnamespacesoptions.refresh.md new file mode 100644 index 0000000000000..1175b79bc1abd --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsdeletefromnamespacesoptions.refresh.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectsDeleteFromNamespacesOptions](./kibana-plugin-core-server.savedobjectsdeletefromnamespacesoptions.md) > [refresh](./kibana-plugin-core-server.savedobjectsdeletefromnamespacesoptions.refresh.md) + +## SavedObjectsDeleteFromNamespacesOptions.refresh property + +The Elasticsearch Refresh setting for this operation + +Signature: + +```typescript +refresh?: MutatingOperationRefreshSetting; +``` diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectserrorhelpers.createconflicterror.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectserrorhelpers.createconflicterror.md new file mode 100644 index 0000000000000..8e04282ce0c71 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectserrorhelpers.createconflicterror.md @@ -0,0 +1,23 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectsErrorHelpers](./kibana-plugin-core-server.savedobjectserrorhelpers.md) > [createConflictError](./kibana-plugin-core-server.savedobjectserrorhelpers.createconflicterror.md) + +## SavedObjectsErrorHelpers.createConflictError() method + +Signature: + +```typescript +static createConflictError(type: string, id: string): DecoratedError; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| type | string | | +| id | string | | + +Returns: + +`DecoratedError` + diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectserrorhelpers.decorateescannotexecutescripterror.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectserrorhelpers.decorateescannotexecutescripterror.md new file mode 100644 index 0000000000000..94060bba50067 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectserrorhelpers.decorateescannotexecutescripterror.md @@ -0,0 +1,23 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectsErrorHelpers](./kibana-plugin-core-server.savedobjectserrorhelpers.md) > [decorateEsCannotExecuteScriptError](./kibana-plugin-core-server.savedobjectserrorhelpers.decorateescannotexecutescripterror.md) + +## SavedObjectsErrorHelpers.decorateEsCannotExecuteScriptError() method + +Signature: + +```typescript +static decorateEsCannotExecuteScriptError(error: Error, reason?: string): DecoratedError; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| error | Error | | +| reason | string | | + +Returns: + +`DecoratedError` + diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectserrorhelpers.isescannotexecutescripterror.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectserrorhelpers.isescannotexecutescripterror.md new file mode 100644 index 0000000000000..debb94fe4f8d8 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectserrorhelpers.isescannotexecutescripterror.md @@ -0,0 +1,22 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectsErrorHelpers](./kibana-plugin-core-server.savedobjectserrorhelpers.md) > [isEsCannotExecuteScriptError](./kibana-plugin-core-server.savedobjectserrorhelpers.isescannotexecutescripterror.md) + +## SavedObjectsErrorHelpers.isEsCannotExecuteScriptError() method + +Signature: + +```typescript +static isEsCannotExecuteScriptError(error: Error | DecoratedError): boolean; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| error | Error | DecoratedError | | + +Returns: + +`boolean` + diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectserrorhelpers.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectserrorhelpers.md index 55a42e4f4eb7a..250b9d3899670 100644 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectserrorhelpers.md +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectserrorhelpers.md @@ -16,12 +16,14 @@ export declare class SavedObjectsErrorHelpers | Method | Modifiers | Description | | --- | --- | --- | | [createBadRequestError(reason)](./kibana-plugin-core-server.savedobjectserrorhelpers.createbadrequesterror.md) | static | | +| [createConflictError(type, id)](./kibana-plugin-core-server.savedobjectserrorhelpers.createconflicterror.md) | static | | | [createEsAutoCreateIndexError()](./kibana-plugin-core-server.savedobjectserrorhelpers.createesautocreateindexerror.md) | static | | | [createGenericNotFoundError(type, id)](./kibana-plugin-core-server.savedobjectserrorhelpers.creategenericnotfounderror.md) | static | | | [createInvalidVersionError(versionInput)](./kibana-plugin-core-server.savedobjectserrorhelpers.createinvalidversionerror.md) | static | | | [createUnsupportedTypeError(type)](./kibana-plugin-core-server.savedobjectserrorhelpers.createunsupportedtypeerror.md) | static | | | [decorateBadRequestError(error, reason)](./kibana-plugin-core-server.savedobjectserrorhelpers.decoratebadrequesterror.md) | static | | | [decorateConflictError(error, reason)](./kibana-plugin-core-server.savedobjectserrorhelpers.decorateconflicterror.md) | static | | +| [decorateEsCannotExecuteScriptError(error, reason)](./kibana-plugin-core-server.savedobjectserrorhelpers.decorateescannotexecutescripterror.md) | static | | | [decorateEsUnavailableError(error, reason)](./kibana-plugin-core-server.savedobjectserrorhelpers.decorateesunavailableerror.md) | static | | | [decorateForbiddenError(error, reason)](./kibana-plugin-core-server.savedobjectserrorhelpers.decorateforbiddenerror.md) | static | | | [decorateGeneralError(error, reason)](./kibana-plugin-core-server.savedobjectserrorhelpers.decorategeneralerror.md) | static | | @@ -30,6 +32,7 @@ export declare class SavedObjectsErrorHelpers | [isBadRequestError(error)](./kibana-plugin-core-server.savedobjectserrorhelpers.isbadrequesterror.md) | static | | | [isConflictError(error)](./kibana-plugin-core-server.savedobjectserrorhelpers.isconflicterror.md) | static | | | [isEsAutoCreateIndexError(error)](./kibana-plugin-core-server.savedobjectserrorhelpers.isesautocreateindexerror.md) | static | | +| [isEsCannotExecuteScriptError(error)](./kibana-plugin-core-server.savedobjectserrorhelpers.isescannotexecutescripterror.md) | static | | | [isEsUnavailableError(error)](./kibana-plugin-core-server.savedobjectserrorhelpers.isesunavailableerror.md) | static | | | [isForbiddenError(error)](./kibana-plugin-core-server.savedobjectserrorhelpers.isforbiddenerror.md) | static | | | [isInvalidVersionError(error)](./kibana-plugin-core-server.savedobjectserrorhelpers.isinvalidversionerror.md) | static | | diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsnamespacetype.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsnamespacetype.md new file mode 100644 index 0000000000000..173b9e19321d0 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsnamespacetype.md @@ -0,0 +1,15 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectsNamespaceType](./kibana-plugin-core-server.savedobjectsnamespacetype.md) + +## SavedObjectsNamespaceType type + +The namespace type dictates how a saved object can be interacted in relation to namespaces. Each type is mutually exclusive: \* single (default): this type of saved object is namespace-isolated, e.g., it exists in only one namespace. \* multiple: this type of saved object is shareable, e.g., it can exist in one or more namespaces. \* agnostic: this type of saved object is global. + +Note: do not write logic that uses this value directly; instead, use the appropriate accessors in the [type registry](./kibana-plugin-core-server.savedobjecttyperegistry.md). + +Signature: + +```typescript +export declare type SavedObjectsNamespaceType = 'single' | 'multiple' | 'agnostic'; +``` diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsrepository.addtonamespaces.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsrepository.addtonamespaces.md new file mode 100644 index 0000000000000..bbb20d2bc3b96 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsrepository.addtonamespaces.md @@ -0,0 +1,27 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectsRepository](./kibana-plugin-core-server.savedobjectsrepository.md) > [addToNamespaces](./kibana-plugin-core-server.savedobjectsrepository.addtonamespaces.md) + +## SavedObjectsRepository.addToNamespaces() method + +Adds one or more namespaces to a given multi-namespace saved object. This method and \[`deleteFromNamespaces`\][SavedObjectsRepository.deleteFromNamespaces()](./kibana-plugin-core-server.savedobjectsrepository.deletefromnamespaces.md) are the only ways to change which Spaces a multi-namespace saved object is shared to. + +Signature: + +```typescript +addToNamespaces(type: string, id: string, namespaces: string[], options?: SavedObjectsAddToNamespacesOptions): Promise<{}>; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| type | string | | +| id | string | | +| namespaces | string[] | | +| options | SavedObjectsAddToNamespacesOptions | | + +Returns: + +`Promise<{}>` + diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsrepository.deletefromnamespaces.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsrepository.deletefromnamespaces.md new file mode 100644 index 0000000000000..471c3e3c5092d --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsrepository.deletefromnamespaces.md @@ -0,0 +1,27 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectsRepository](./kibana-plugin-core-server.savedobjectsrepository.md) > [deleteFromNamespaces](./kibana-plugin-core-server.savedobjectsrepository.deletefromnamespaces.md) + +## SavedObjectsRepository.deleteFromNamespaces() method + +Removes one or more namespaces from a given multi-namespace saved object. If no namespaces remain, the saved object is deleted entirely. This method and \[`addToNamespaces`\][SavedObjectsRepository.addToNamespaces()](./kibana-plugin-core-server.savedobjectsrepository.addtonamespaces.md) are the only ways to change which Spaces a multi-namespace saved object is shared to. + +Signature: + +```typescript +deleteFromNamespaces(type: string, id: string, namespaces: string[], options?: SavedObjectsDeleteFromNamespacesOptions): Promise<{}>; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| type | string | | +| id | string | | +| namespaces | string[] | | +| options | SavedObjectsDeleteFromNamespacesOptions | | + +Returns: + +`Promise<{}>` + diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsrepository.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsrepository.md index 37547af2497e9..bd86ff3abbe9b 100644 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectsrepository.md +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsrepository.md @@ -15,12 +15,14 @@ export declare class SavedObjectsRepository | Method | Modifiers | Description | | --- | --- | --- | +| [addToNamespaces(type, id, namespaces, options)](./kibana-plugin-core-server.savedobjectsrepository.addtonamespaces.md) | | Adds one or more namespaces to a given multi-namespace saved object. This method and \[deleteFromNamespaces\][SavedObjectsRepository.deleteFromNamespaces()](./kibana-plugin-core-server.savedobjectsrepository.deletefromnamespaces.md) are the only ways to change which Spaces a multi-namespace saved object is shared to. | | [bulkCreate(objects, options)](./kibana-plugin-core-server.savedobjectsrepository.bulkcreate.md) | | Creates multiple documents at once | | [bulkGet(objects, options)](./kibana-plugin-core-server.savedobjectsrepository.bulkget.md) | | Returns an array of objects by id | | [bulkUpdate(objects, options)](./kibana-plugin-core-server.savedobjectsrepository.bulkupdate.md) | | Updates multiple objects in bulk | | [create(type, attributes, options)](./kibana-plugin-core-server.savedobjectsrepository.create.md) | | Persists an object | | [delete(type, id, options)](./kibana-plugin-core-server.savedobjectsrepository.delete.md) | | Deletes an object | | [deleteByNamespace(namespace, options)](./kibana-plugin-core-server.savedobjectsrepository.deletebynamespace.md) | | Deletes all objects from the provided namespace. | +| [deleteFromNamespaces(type, id, namespaces, options)](./kibana-plugin-core-server.savedobjectsrepository.deletefromnamespaces.md) | | Removes one or more namespaces from a given multi-namespace saved object. If no namespaces remain, the saved object is deleted entirely. This method and \[addToNamespaces\][SavedObjectsRepository.addToNamespaces()](./kibana-plugin-core-server.savedobjectsrepository.addtonamespaces.md) are the only ways to change which Spaces a multi-namespace saved object is shared to. | | [find({ search, defaultSearchOperator, searchFields, hasReference, page, perPage, sortField, sortOrder, fields, namespace, type, filter, })](./kibana-plugin-core-server.savedobjectsrepository.find.md) | | | | [get(type, id, options)](./kibana-plugin-core-server.savedobjectsrepository.get.md) | | Gets a single object | | [incrementCounter(type, id, counterFieldName, options)](./kibana-plugin-core-server.savedobjectsrepository.incrementcounter.md) | | Increases a counter field by one. Creates the document if one doesn't exist for the given id. | diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsservicesetup.registertype.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsservicesetup.registertype.md index c24fa7e7038c6..57c9e04966c1b 100644 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectsservicesetup.registertype.md +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsservicesetup.registertype.md @@ -29,7 +29,7 @@ import * as migrations from './migrations'; export const myType: SavedObjectsType = { name: 'MyType', hidden: false, - namespaceAgnostic: true, + namespaceType: 'multiple', mappings: { properties: { textField: { diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectstype.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectstype.md index ad7bf9a0f00d0..a8894286de910 100644 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectstype.md +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectstype.md @@ -25,5 +25,5 @@ This is only internal for now, and will only be public when we expose the regist | [mappings](./kibana-plugin-core-server.savedobjectstype.mappings.md) | SavedObjectsTypeMappingDefinition | The [mapping definition](./kibana-plugin-core-server.savedobjectstypemappingdefinition.md) for the type. | | [migrations](./kibana-plugin-core-server.savedobjectstype.migrations.md) | SavedObjectMigrationMap | An optional map of [migrations](./kibana-plugin-core-server.savedobjectmigrationfn.md) to be used to migrate the type. | | [name](./kibana-plugin-core-server.savedobjectstype.name.md) | string | The name of the type, which is also used as the internal id. | -| [namespaceAgnostic](./kibana-plugin-core-server.savedobjectstype.namespaceagnostic.md) | boolean | Is the type global (true), or namespaced (false). | +| [namespaceType](./kibana-plugin-core-server.savedobjectstype.namespacetype.md) | SavedObjectsNamespaceType | The [namespace type](./kibana-plugin-core-server.savedobjectsnamespacetype.md) for the type. | diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectstype.namespaceagnostic.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectstype.namespaceagnostic.md deleted file mode 100644 index 8f43db86449d0..0000000000000 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectstype.namespaceagnostic.md +++ /dev/null @@ -1,13 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectsType](./kibana-plugin-core-server.savedobjectstype.md) > [namespaceAgnostic](./kibana-plugin-core-server.savedobjectstype.namespaceagnostic.md) - -## SavedObjectsType.namespaceAgnostic property - -Is the type global (true), or namespaced (false). - -Signature: - -```typescript -namespaceAgnostic: boolean; -``` diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectstype.namespacetype.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectstype.namespacetype.md new file mode 100644 index 0000000000000..3a3b0f7f3a9a5 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectstype.namespacetype.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectsType](./kibana-plugin-core-server.savedobjectstype.md) > [namespaceType](./kibana-plugin-core-server.savedobjectstype.namespacetype.md) + +## SavedObjectsType.namespaceType property + +The [namespace type](./kibana-plugin-core-server.savedobjectsnamespacetype.md) for the type. + +Signature: + +```typescript +namespaceType: SavedObjectsNamespaceType; +``` diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjecttyperegistry.ismultinamespace.md b/docs/development/core/server/kibana-plugin-core-server.savedobjecttyperegistry.ismultinamespace.md new file mode 100644 index 0000000000000..6532c5251d816 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjecttyperegistry.ismultinamespace.md @@ -0,0 +1,24 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectTypeRegistry](./kibana-plugin-core-server.savedobjecttyperegistry.md) > [isMultiNamespace](./kibana-plugin-core-server.savedobjecttyperegistry.ismultinamespace.md) + +## SavedObjectTypeRegistry.isMultiNamespace() method + +Returns whether the type is multi-namespace (shareable); resolves to `false` if the type is not registered + +Signature: + +```typescript +isMultiNamespace(type: string): boolean; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| type | string | | + +Returns: + +`boolean` + diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjecttyperegistry.isnamespaceagnostic.md b/docs/development/core/server/kibana-plugin-core-server.savedobjecttyperegistry.isnamespaceagnostic.md index 92dfa5465235a..859c7b9711816 100644 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjecttyperegistry.isnamespaceagnostic.md +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjecttyperegistry.isnamespaceagnostic.md @@ -4,7 +4,7 @@ ## SavedObjectTypeRegistry.isNamespaceAgnostic() method -Returns the `namespaceAgnostic` property for given type, or `false` if the type is not registered. +Returns whether the type is namespace-agnostic (global); resolves to `false` if the type is not registered Signature: diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjecttyperegistry.issinglenamespace.md b/docs/development/core/server/kibana-plugin-core-server.savedobjecttyperegistry.issinglenamespace.md new file mode 100644 index 0000000000000..18146b2fd6ea1 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjecttyperegistry.issinglenamespace.md @@ -0,0 +1,24 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectTypeRegistry](./kibana-plugin-core-server.savedobjecttyperegistry.md) > [isSingleNamespace](./kibana-plugin-core-server.savedobjecttyperegistry.issinglenamespace.md) + +## SavedObjectTypeRegistry.isSingleNamespace() method + +Returns whether the type is single-namespace (isolated); resolves to `true` if the type is not registered + +Signature: + +```typescript +isSingleNamespace(type: string): boolean; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| type | string | | + +Returns: + +`boolean` + diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjecttyperegistry.md b/docs/development/core/server/kibana-plugin-core-server.savedobjecttyperegistry.md index 410b709252b72..69a94e4ad8c88 100644 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjecttyperegistry.md +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjecttyperegistry.md @@ -22,6 +22,8 @@ export declare class SavedObjectTypeRegistry | [getType(type)](./kibana-plugin-core-server.savedobjecttyperegistry.gettype.md) | | Return the [type](./kibana-plugin-core-server.savedobjectstype.md) definition for given type name. | | [isHidden(type)](./kibana-plugin-core-server.savedobjecttyperegistry.ishidden.md) | | Returns the hidden property for given type, or false if the type is not registered. | | [isImportableAndExportable(type)](./kibana-plugin-core-server.savedobjecttyperegistry.isimportableandexportable.md) | | Returns the management.importableAndExportable property for given type, or false if the type is not registered or does not define a management section. | -| [isNamespaceAgnostic(type)](./kibana-plugin-core-server.savedobjecttyperegistry.isnamespaceagnostic.md) | | Returns the namespaceAgnostic property for given type, or false if the type is not registered. | +| [isMultiNamespace(type)](./kibana-plugin-core-server.savedobjecttyperegistry.ismultinamespace.md) | | Returns whether the type is multi-namespace (shareable); resolves to false if the type is not registered | +| [isNamespaceAgnostic(type)](./kibana-plugin-core-server.savedobjecttyperegistry.isnamespaceagnostic.md) | | Returns whether the type is namespace-agnostic (global); resolves to false if the type is not registered | +| [isSingleNamespace(type)](./kibana-plugin-core-server.savedobjecttyperegistry.issinglenamespace.md) | | Returns whether the type is single-namespace (isolated); resolves to true if the type is not registered | | [registerType(type)](./kibana-plugin-core-server.savedobjecttyperegistry.registertype.md) | | Register a [type](./kibana-plugin-core-server.savedobjectstype.md) inside the registry. A type can only be registered once. subsequent calls with the same type name will throw an error. | diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternfield.indexpattern.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternfield.indexpattern.md new file mode 100644 index 0000000000000..d1a1ee0905c6e --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternfield.indexpattern.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [IndexPatternField](./kibana-plugin-plugins-data-public.indexpatternfield.md) > [indexPattern](./kibana-plugin-plugins-data-public.indexpatternfield.indexpattern.md) + +## IndexPatternField.indexPattern property + +Signature: + +```typescript +indexPattern?: IndexPattern; +``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternfield.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternfield.md index 430590c7a2505..df0de6ce0e541 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternfield.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternfield.md @@ -27,9 +27,9 @@ export declare class Field implements IFieldType | [esTypes](./kibana-plugin-plugins-data-public.indexpatternfield.estypes.md) | | string[] | | | [filterable](./kibana-plugin-plugins-data-public.indexpatternfield.filterable.md) | | boolean | | | [format](./kibana-plugin-plugins-data-public.indexpatternfield.format.md) | | any | | +| [indexPattern](./kibana-plugin-plugins-data-public.indexpatternfield.indexpattern.md) | | IndexPattern | | | [lang](./kibana-plugin-plugins-data-public.indexpatternfield.lang.md) | | string | | | [name](./kibana-plugin-plugins-data-public.indexpatternfield.name.md) | | string | | -| [routes](./kibana-plugin-plugins-data-public.indexpatternfield.routes.md) | | Record<string, string> | | | [script](./kibana-plugin-plugins-data-public.indexpatternfield.script.md) | | string | | | [scripted](./kibana-plugin-plugins-data-public.indexpatternfield.scripted.md) | | boolean | | | [searchable](./kibana-plugin-plugins-data-public.indexpatternfield.searchable.md) | | boolean | | diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternfield.routes.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternfield.routes.md deleted file mode 100644 index 664a7b7b7ca0e..0000000000000 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternfield.routes.md +++ /dev/null @@ -1,11 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [IndexPatternField](./kibana-plugin-plugins-data-public.indexpatternfield.md) > [routes](./kibana-plugin-plugins-data-public.indexpatternfield.routes.md) - -## IndexPatternField.routes property - -Signature: - -```typescript -routes: Record; -``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternfieldlist.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternfieldlist.md index 4b7184b7dc151..478b73f5f8581 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternfieldlist.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternfieldlist.md @@ -24,5 +24,5 @@ export declare class FieldList extends Array implements IFieldList | [getByName](./kibana-plugin-plugins-data-public.indexpatternfieldlist.getbyname.md) | | (name: string) => Field | undefined | | | [getByType](./kibana-plugin-plugins-data-public.indexpatternfieldlist.getbytype.md) | | (type: string) => any[] | | | [remove](./kibana-plugin-plugins-data-public.indexpatternfieldlist.remove.md) | | (field: IFieldType) => void | | -| [update](./kibana-plugin-plugins-data-public.indexpatternfieldlist.update.md) | | (field: Field) => void | | +| [update](./kibana-plugin-plugins-data-public.indexpatternfieldlist.update.md) | | (field: Record<string, any>) => void | | diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternfieldlist.update.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternfieldlist.update.md index ca03ec4b72893..d5156ed41e493 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternfieldlist.update.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternfieldlist.update.md @@ -7,5 +7,5 @@ Signature: ```typescript -update: (field: Field) => void; +update: (field: Record) => void; ``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.md index fc0dab94a0f65..bf29c883e4eb9 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.md @@ -88,7 +88,6 @@ | [SavedQuery](./kibana-plugin-plugins-data-public.savedquery.md) | | | [SavedQueryService](./kibana-plugin-plugins-data-public.savedqueryservice.md) | | | [SearchSourceFields](./kibana-plugin-plugins-data-public.searchsourcefields.md) | | -| [SearchStrategyProvider](./kibana-plugin-plugins-data-public.searchstrategyprovider.md) | | | [TabbedAggColumn](./kibana-plugin-plugins-data-public.tabbedaggcolumn.md) | \* | | [TabbedTable](./kibana-plugin-plugins-data-public.tabbedtable.md) | \* | | [TimeRange](./kibana-plugin-plugins-data-public.timerange.md) | | diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchstrategyprovider.id.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchstrategyprovider.id.md deleted file mode 100644 index d60ffba6a05ca..0000000000000 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchstrategyprovider.id.md +++ /dev/null @@ -1,11 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [SearchStrategyProvider](./kibana-plugin-plugins-data-public.searchstrategyprovider.md) > [id](./kibana-plugin-plugins-data-public.searchstrategyprovider.id.md) - -## SearchStrategyProvider.id property - -Signature: - -```typescript -id: string; -``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchstrategyprovider.isviable.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchstrategyprovider.isviable.md deleted file mode 100644 index aa8ed49051ee9..0000000000000 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchstrategyprovider.isviable.md +++ /dev/null @@ -1,11 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [SearchStrategyProvider](./kibana-plugin-plugins-data-public.searchstrategyprovider.md) > [isViable](./kibana-plugin-plugins-data-public.searchstrategyprovider.isviable.md) - -## SearchStrategyProvider.isViable property - -Signature: - -```typescript -isViable: (indexPattern: IndexPattern) => boolean; -``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchstrategyprovider.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchstrategyprovider.md deleted file mode 100644 index b271a921906a7..0000000000000 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchstrategyprovider.md +++ /dev/null @@ -1,20 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [SearchStrategyProvider](./kibana-plugin-plugins-data-public.searchstrategyprovider.md) - -## SearchStrategyProvider interface - -Signature: - -```typescript -export interface SearchStrategyProvider -``` - -## Properties - -| Property | Type | Description | -| --- | --- | --- | -| [id](./kibana-plugin-plugins-data-public.searchstrategyprovider.id.md) | string | | -| [isViable](./kibana-plugin-plugins-data-public.searchstrategyprovider.isviable.md) | (indexPattern: IndexPattern) => boolean | | -| [search](./kibana-plugin-plugins-data-public.searchstrategyprovider.search.md) | (params: SearchStrategySearchParams) => SearchStrategyResponse | | - diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchstrategyprovider.search.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchstrategyprovider.search.md deleted file mode 100644 index 6e2561c3b0ad0..0000000000000 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchstrategyprovider.search.md +++ /dev/null @@ -1,11 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [SearchStrategyProvider](./kibana-plugin-plugins-data-public.searchstrategyprovider.md) > [search](./kibana-plugin-plugins-data-public.searchstrategyprovider.search.md) - -## SearchStrategyProvider.search property - -Signature: - -```typescript -search: (params: SearchStrategySearchParams) => SearchStrategyResponse; -``` diff --git a/docs/images/add_remote_cluster.png b/docs/images/add_remote_cluster.png index 376b1d8392366..160d29b741c62 100755 Binary files a/docs/images/add_remote_cluster.png and b/docs/images/add_remote_cluster.png differ diff --git a/docs/images/auto_follow_pattern.png b/docs/images/auto_follow_pattern.png index 3bf86458eddd7..f80de9352280f 100755 Binary files a/docs/images/auto_follow_pattern.png and b/docs/images/auto_follow_pattern.png differ diff --git a/docs/images/cross-cluster-replication-list-view.png b/docs/images/cross-cluster-replication-list-view.png new file mode 100755 index 0000000000000..4c45174cff7f1 Binary files /dev/null and b/docs/images/cross-cluster-replication-list-view.png differ diff --git a/docs/images/follower_indices.png b/docs/images/follower_indices.png old mode 100644 new mode 100755 index f103bb3cf2acf..505adeb45ae23 Binary files a/docs/images/follower_indices.png and b/docs/images/follower_indices.png differ diff --git a/docs/images/remote-clusters-list-view.png b/docs/images/remote-clusters-list-view.png new file mode 100755 index 0000000000000..c28379863b74b Binary files /dev/null and b/docs/images/remote-clusters-list-view.png differ diff --git a/docs/images/tutorial-ilm-custom-policy.png b/docs/images/tutorial-ilm-custom-policy.png new file mode 100644 index 0000000000000..03b67829f605c Binary files /dev/null and b/docs/images/tutorial-ilm-custom-policy.png differ diff --git a/docs/images/tutorial-ilm-delete-phase-creation.png b/docs/images/tutorial-ilm-delete-phase-creation.png new file mode 100644 index 0000000000000..91a55733c284e Binary files /dev/null and b/docs/images/tutorial-ilm-delete-phase-creation.png differ diff --git a/docs/images/tutorial-ilm-delete-rollover.png b/docs/images/tutorial-ilm-delete-rollover.png new file mode 100644 index 0000000000000..ba021ecc2ac5c Binary files /dev/null and b/docs/images/tutorial-ilm-delete-rollover.png differ diff --git a/docs/images/tutorial-ilm-hotphaserollover-default.png b/docs/images/tutorial-ilm-hotphaserollover-default.png new file mode 100644 index 0000000000000..a9088c63d885d Binary files /dev/null and b/docs/images/tutorial-ilm-hotphaserollover-default.png differ diff --git a/docs/images/tutorial-ilm-modify-default-warm-phase-rollover.png b/docs/images/tutorial-ilm-modify-default-warm-phase-rollover.png new file mode 100644 index 0000000000000..c6f1e9b40e977 Binary files /dev/null and b/docs/images/tutorial-ilm-modify-default-warm-phase-rollover.png differ diff --git a/docs/ingest_manager/index-templates.asciidoc b/docs/ingest_manager/index-templates.asciidoc new file mode 100644 index 0000000000000..e19af63c3116f --- /dev/null +++ b/docs/ingest_manager/index-templates.asciidoc @@ -0,0 +1,7 @@ +# Elasticsearch Index Templates + +## Generation + +* Index templates are generated from `YAML` files contained in the package. +* There is one index template per dataset. +* For the generation of an index template, all `yml` files contained in the package subdirectory `dataset/DATASET_NAME/fields/` are used. diff --git a/docs/ingest_manager/index.asciidoc b/docs/ingest_manager/index.asciidoc index 1254f412e14c5..866935d1fa580 100644 --- a/docs/ingest_manager/index.asciidoc +++ b/docs/ingest_manager/index.asciidoc @@ -113,7 +113,7 @@ Ingest Management enforces an indexing strategy to allow the system to automatic {type}-{dataset}-{namespace} ``` -The `{type}` can be `logs` or `metrics`. The `{namespace}` is the part where the user can use free form. The only two requirement are that it has only characters allowed in an Elasticsearch index name and does NOT contain a `-`. The `dataset` is defined by the data that is indexed. The same requirements as for the namespace apply. It is expected that the fields for type, namespace and dataset are part of each event and are constant keywords. +The `{type}` can be `logs` or `metrics`. The `{namespace}` is the part where the user can use free form. The only two requirement are that it has only characters allowed in an Elasticsearch index name and does NOT contain a `-`. The `dataset` is defined by the data that is indexed. The same requirements as for the namespace apply. It is expected that the fields for type, namespace and dataset are part of each event and are constant keywords. If there is a dataset or a namespace with a `-` inside, it is recommended to replace it either by a `.` or a `_`. Note: More `{type}`s might be added in the future like `apm` and `endpoint`. @@ -126,6 +126,8 @@ This indexing strategy has a few advantages: * Having a global metrics and logs template, allows to create new indices on demand which still follow the convention. This is common in the case of k8s as an example. * Constant keywords allow to narrow down the indices we need to access for querying very efficiently. This is especially relevant in environments which a large number of indices or with indices on slower nodes. +Overall it creates smaller indices in size, makes querying more efficient and allows users to define their own naming parts in namespace and still benefiting from all features that can be built on top of the indexing startegy. + === Ingest Pipeline The ingest pipelines for a specific dataset will have the following naming scheme: @@ -197,3 +199,10 @@ The new ingest pipeline is expected to still work with the data coming from olde In case of a breaking change in the data structure, the new ingest pipeline is also expected to deal with this change. In case there are breaking changes which cannot be dealt with in an ingest pipeline, a new package has to be created. Each package lists its minimal required agent version. In case there are agents enrolled with an older version, the user is notified to upgrade these agents as otherwise the new configs cannot be rolled out. + +=== Generated assets + +When a package is installed or upgraded, certain Kibana and Elasticsearch assets are generated from . These follow the naming conventions explained above (see "indexing strategy") and contain configuration for the elastic stack that makes ingesting and displaying data work with as little user interaction as possible. + +* link:index-templates.asciidoc[Elasticsearch Index Templates] +* Kibana Index Patterns diff --git a/docs/management/index-lifecycle-policies/example-index-lifecycle-policy.asciidoc b/docs/management/index-lifecycle-policies/example-index-lifecycle-policy.asciidoc index f68708f1b6394..e6d94e9ca61a3 100644 --- a/docs/management/index-lifecycle-policies/example-index-lifecycle-policy.asciidoc +++ b/docs/management/index-lifecycle-policies/example-index-lifecycle-policy.asciidoc @@ -1,23 +1,179 @@ [role="xpack"] + [[example-using-index-lifecycle-policy]] -=== Example of using an index lifecycle policy +=== Tutorial: Use {ilm-init} to manage {filebeat} time-based indices + +With {ilm} ({ilm-init}), you can create policies that perform actions automatically +on indices as they age and grow. {ilm-init} policies help you to manage +performance, resilience, and retention of your data during its lifecycle. This tutorial shows +you how to use {kib}’s *Index Lifecycle Policies* to modify and create {ilm-init} +policies. You can learn more about all of the actions, benefits, and lifecycle +phases in the {ref}/overview-index-lifecycle-management.html[{ilm-init} overview]. + + +[discrete] +[[example-using-index-lifecycle-policy-scenario]] +==== Scenario + +You’re tasked with sending syslog files to an {es} cluster. This +log data has the following data retention guidelines: + +* Keep logs on hot data nodes for 30 days +* Roll over to a new index if the size reaches 50GB +* After 30 days: +** Move the logs to warm data nodes +** Set {ref}/glossary.html#glossary-replica-shard[replica shards] to 1 +** {ref}/indices-forcemerge.html[Force merge] multiple index segments to free up the space used by deleted documents +* Delete logs after 90 days + + +[discrete] +[[example-using-index-lifecycle-policy-prerequisites]] +==== Prerequisites + +To complete this tutorial, you'll need: + +* An {es} cluster with hot and warm nodes configured for shard allocation +awareness. If you’re using {cloud}/ec-getting-started-templates-hot-warm.html[{ess}], +choose the hot-warm architecture deployment template. + ++ +For a self-managed cluster, add node attributes as described for {ref}/shard-allocation-filtering.html[shard allocation filtering] +to label data nodes as hot or warm. This step is required to migrate shards between +nodes configured with specific hardware for the hot or warm phases. ++ +For example, you can set this in your `elasticsearch.yml` for each data node: ++ +[source,yaml] +-------------------------------------------------------------------------------- +node.attr.data: "warm" +-------------------------------------------------------------------------------- + +* A server with {filebeat} installed and configured to send logs to the `elasticsearch` +output as described in {filebeat-ref}/filebeat-getting-started.html[Getting Started with {filebeat}]. + +[discrete] +[[example-using-index-lifecycle-policy-view-fb-ilm-policy]] +==== View the {filebeat} {ilm-init} policy + +{filebeat} includes a default {ilm-init} policy that enables rollover. {ilm-init} +is enabled automatically if you’re using the default `filebeat.yml` and index template. + +To view the default policy in {kib}, go to *Management > Index Lifecycle Policies*, +search for _filebeat_, and choose the _filebeat-version_ policy. + +This policy initiates the rollover action when the index size reaches 50GB or +becomes 30 days old. + +[role="screenshot"] +image::images/tutorial-ilm-hotphaserollover-default.png["Default policy"] + + +[float] +==== Modify the policy + +The default policy is enough to prevent the creation of many tiny daily indices. +You can modify the policy to meet more complex requirements. + +. Activate the warm phase. + ++ +. Set either of the following options to control when the index moves to the warm phase: + +** Provide a value for *Timing for warm phase*. Setting this to *15* keeps the +indices on hot nodes for a range of 15-45 days, depending on when the initial +rollover occurred. + +** Enable *Move to warm phase on rollover*. The index might move to the warm phase +more quickly than intended if it reaches the *Maximum index size* before the +the *Maximum age*. + +. In the *Select a node attribute to control shard allocation* dropdown, select +*data:warm(2)* to migrate shards to warm data nodes. + +. Change *Number of replicas* to *1*. + +. Enable *Force merge data* and set *Number of segments* to *1*. ++ +NOTE: When rollover is enabled in the hot phase, action timing in the other phases +is based on the rollover date. + ++ +[role="screenshot"] +image::images/tutorial-ilm-modify-default-warm-phase-rollover.png["Modify to add warm phase"] + +. Activate the delete phase and set *Timing for delete phase* to *90* days. ++ +[role="screenshot"] +image::images/tutorial-ilm-delete-rollover.png["Add a delete phase"] + +[float] +==== Create a custom policy + +If meeting a specific retention time period is most important, you can create a +custom policy. For this option, you will use {filebeat} daily indices without +rollover. + +. Create a custom policy in {kib}, go to *Management > Index Lifecycle Policies > +Create Policy*. + +. Activate the warm phase and configure it as follows: ++ +|=== +|*Setting* |*Value* + +|Timing for warm phase +|30 days from index creation + +|Node attribute +|`data:warm` + +|Number of replicas +|1 + +|Force merge data +|enable + +|Number of segments +|1 +|=== + ++ +[role="screenshot"] +image::images/tutorial-ilm-custom-policy.png["Modify the custom policy to add a warm phase"] + -A common use case for managing index lifecycle policies is when you’re using -{beats-ref}/beats-reference.html[Beats] to continually send time-series data, -such as metrics and log data, to {es}. When you create the Beats packages, an -index template is installed. The template includes a default policy to apply -when new indices are created. ++ +. Activate the delete phase and set the timing. ++ +|=== +|*Setting* |*Value* +|Timing for delete phase +|90 +|=== -You can edit the policy in {kib}'s *Index Lifecycle Policies*. For example, you might: ++ +[role="screenshot"] +image::images/tutorial-ilm-delete-phase-creation.png["Delete phase"] -* Rollover the index when it reaches 50 GB in size or is 30 days old. These -settings are the default for the Beats lifecycle policy. This avoids -having 1000s of tiny indices. When a rollover occurs, a new “hot” index is -created and added to the index alias. +. Configure the index to use the new policy in *{kib} > Management > Index Lifecycle +Policies* -* Move the index into the warm phase, shrink the index down to a single shard, -and force merge to a single segment. +.. Find your {ilm-init} policy. +.. Click the *Actions* link next to your policy name. +.. Choose *Add policy to index template*. +.. Select your {filebeat} index template name from the *Index template* list. For example, `filebeat-7.5.x`. +.. Click *Add Policy* to save the changes. -* After 60 days, move the index into the cold phase and onto less expensive hardware. ++ +NOTE: If you initially used the default {filebeat} {ilm-init} policy, you will +see a notice that the template already has a policy associated with it. Confirm +that you want to overwrite that configuration. -* Delete the index after 90 days. ++ ++ +TIP: When you change the policy associated with the index template, the active +index will continue to use the policy it was associated with at index creation +unless you manually update it. The next new index will use the updated policy. +For more reasons that your {ilm-init} policy changes might be delayed, see +{ref}/update-lifecycle-policy.html#update-lifecycle-policy[Update Lifecycle Policy]. diff --git a/docs/management/managing-ccr.asciidoc b/docs/management/managing-ccr.asciidoc new file mode 100644 index 0000000000000..b2db5a80cfe7e --- /dev/null +++ b/docs/management/managing-ccr.asciidoc @@ -0,0 +1,73 @@ +[role="xpack"] +[[managing-cross-cluster-replication]] +== Cross-Cluster Replication + +Use *Cross-Cluster Replication* to reproduce indices in +remote clusters on a local cluster. {ref}/xpack-ccr.html[Cross-cluster replication] +is commonly used to provide remote backups for disaster recovery and for +geo-proximite copies of data. + +To get started, go to *Management > Cross-Cluster Replication*. + +[role="screenshot"] +image::images/cross-cluster-replication-list-view.png[][Cross-cluster replication list view] + +[float] +=== Prerequisites + +* You must have a {ref}/modules-remote-clusters.html[remote cluster]. +* Leader indices must meet {ref}/ccr-requirements.html[these requirements]. +* The Elasticsearch version of the local cluster must be the same as or newer than the remote cluster. +Refer to {ref}/ccr-overview.html[this document] for more information. + +[float] +[[configure-replication]] +=== Configure replication + +Replication requires a leader index, the index being replicated, and a +follower index, which will contain the leader index's replicated data. +The follower index is passive in that it can read requests and searches, +but cannot accept direct writes. Only the leader index is active for direct writes. + +You can configure follower indices in two ways: + +* Create specific follower indices +* Create follower indices from an auto-follow pattern + +[float] +==== Create specific follower indices + +To replicate data from existing indices, or set up local followers on a case-by-case basis, +go to *Follower indices*. When you create the follower index, you must reference the +remote cluster and the leader index that you created in the remote cluster. + +[role="screenshot"] +image::images/follower_indices.png[][UI for adding follower indices] + +[float] +==== Create follower indices from an auto-follow pattern + +To automatically detect and follow new indices when they are created on a remote cluster, +go to *Auto-follow patterns*. Creating an auto-follow pattern is useful when you have +time series data, like event logs, on the remote cluster that is created or rolled over on a daily basis. + +When creating the pattern, you must reference the remote cluster that you +connected to your local cluster. You must also specify a collection of index patterns +that match the indices you want to automatically follow. + +Once you configure an +auto-follow pattern, any time a new index with a name that matches the pattern is +created in the remote cluster, a follower index is automatically configured in the local cluster. + +[role="screenshot"] +image::images/auto_follow_pattern.png[UI for adding an auto-follow pattern] + +[float] +[[manage-replication]] +=== Manage replication + +Use the list views in *Cross-Cluster Replication* to monitor whether the replication is active and +pause and resume replication. You can also edit and remove the follower indices and auto-follow patterns. + +For an example of cross-cluster replication, +refer to https://www.elastic.co/blog/bi-directional-replication-with-elasticsearch-cross-cluster-replication-ccr[Bi-directional replication with Elasticsearch cross-cluster replication]. diff --git a/docs/management/managing-remote-clusters.asciidoc b/docs/management/managing-remote-clusters.asciidoc index 6b69cfef5b768..00ec5c7d2ddea 100644 --- a/docs/management/managing-remote-clusters.asciidoc +++ b/docs/management/managing-remote-clusters.asciidoc @@ -1,67 +1,39 @@ [[working-remote-clusters]] == Remote Clusters -{kib} *Management* provides user interfaces for working with data from remote -clusters and managing the {ccr} process. You can replicate indices from a -leader remote cluster to a follower index in a local cluster. The local follower indices -can be used to provide remote backups for disaster recovery or for geo-proximite copies of data. +Use *Remote Clusters* to establish a unidirectional +connection from your cluster to other clusters. This functionality is +required for {ref}/xpack-ccr.html[cross-cluster replication] and +{ref}/modules-cross-cluster-search.html[cross-cluster search]. -Before using these features, you should be familiar with the following concepts: +To get started, go to *Management > Remote Clusters*. -* {ref}/xpack-ccr.html[{ccr-cap}] -* {ref}/modules-cross-cluster-search.html[{ccs-cap}] -* {ref}/cross-cluster-configuring.html[Cross-cluster security requirements] +[role="screenshot"] +image::images/remote-clusters-list-view.png[Remote Clusters list view, including Add a remote cluster button] [float] [[managing-remote-clusters]] -== Managing remote clusters - -*Remote clusters* helps you manage remote clusters for use with -{ccs} and {ccr}. You can add and remove remote clusters and check their connectivity. +=== Add a remote cluster -Before you use this feature, you should be familiar with the concept of -{ref}/modules-remote-clusters.html[remote clusters]. +A {ref}/modules-remote-clusters.html[remote cluster] connection works by configuring a remote cluster and +connecting to a limited number of nodes, called {ref}/modules-remote-clusters.html#sniff-mode[seed nodes], +in that cluster. +Alternatively, you can define a single proxy address for the remote cluster. -Go to *Management > Elasticsearch > Remote clusters* to create or manage your remotes. +By default, a cross-cluster request, such as a cross-cluster search or +replication request, fails if any cluster in the request is unavailable. +To skip a cluster when its unavailable, +set *Skip if unavailable* to true. -To set up a new remote, click *Add a remote cluster*. Give the cluster a unique name -and define the seed nodes for cluster discovery. You can edit or remove your remote clusters -from the *Remote clusters* list view. +Once you add a remote cluster, you can configure <> +to reproduce indices in the remote cluster on a local cluster. [role="screenshot"] image::images/add_remote_cluster.png[][UI for adding a remote cluster] -Once a remote cluster is registered, you can use the tools under *{ccr-cap}* -to add and manage follower indices on the local cluster, and replicate data from -indices on the remote cluster based on an auto-follow index pattern. - [float] -[[managing-cross-cluster-replication]] -== [xpack]#Managing {ccr}# - -*{ccr-cap}* helps you create and manage the {ccr} process. -If you want to replicate data from existing indices, or set up -local followers on a case-by-case basis, go to *Follower indices*. -If you want to automatically detect and follow new indices when they are created -on a remote cluster, you can do so from *Auto-follow patterns*. - -Creating an auto-follow pattern is useful when you have time-series data, like a logs index, on the -remote cluster that is created or rolled over on a daily basis. Once you have configured an -auto-follow pattern, any time a new index with a name that matches the pattern is -created in the remote cluster, a follower index is automatically configured in the local cluster. - -From the same view, you can also see a list of your saved auto-follow patterns for -a given remote cluster, and monitor whether the replication is active. +[[manage-remote-clusters]] +=== Manage remote clusters -Before you use these features, you should be familiar with the following concepts: - -* {ref}/ccr-requirements.html[Requirements for leader indices] -* {ref}/ccr-auto-follow.html[Automatically following indices] - -To get started, go to *Management > Elasticsearch > {ccr-cap}*. - -[role="screenshot"] -image::images/auto_follow_pattern.png[][UI for adding an auto-follow pattern] - -[role="screenshot"] -image::images/follower_indices.png[][UI for adding follower indices] +From the *Remote Clusters* list view, you can drill down into each cluster and +view its status. You can also edit and delete a cluster. diff --git a/docs/redirects.asciidoc b/docs/redirects.asciidoc index fd835bde83322..a5503969a3ec1 100644 --- a/docs/redirects.asciidoc +++ b/docs/redirects.asciidoc @@ -23,7 +23,7 @@ For more {kib} configuration settings, see <>. [role="exclude",id="uptime-security"] == Uptime security -This page has moved. Please see the new section in the {uptime-guide}/uptime-security.html[Uptime Monitoring Guide]. +This page has moved. Please see the new section in the {heartbeat-ref}/securing-heartbeat.html[Uptime Monitoring Guide]. [role="exclude",id="infra-read-only-access"] == Configure source read-only access diff --git a/docs/settings/alert-action-settings.asciidoc b/docs/settings/alert-action-settings.asciidoc index d7f1ec637d1df..d4dbe9407b7a9 100644 --- a/docs/settings/alert-action-settings.asciidoc +++ b/docs/settings/alert-action-settings.asciidoc @@ -9,7 +9,7 @@ Alerts and actions are enabled by default in {kib}, but require you configure th . <>. . <>. -. <>. +. <>. You can configure the following settings in the `kibana.yml` file. @@ -18,7 +18,7 @@ You can configure the following settings in the `kibana.yml` file. [[general-alert-action-settings]] ==== General settings -`xpack.encrypted_saved_objects.encryptionKey`:: +`xpack.encryptedSavedObjects.encryptionKey`:: A string of 32 or more characters used to encrypt sensitive properties on alerts and actions before they're stored in {es}. Third party credentials — such as the username and password used to connect to an SMTP service — are an example of encrypted properties. + diff --git a/docs/settings/apm-settings.asciidoc b/docs/settings/apm-settings.asciidoc index 91bbef5690fd5..fd53c3aeb3605 100644 --- a/docs/settings/apm-settings.asciidoc +++ b/docs/settings/apm-settings.asciidoc @@ -5,7 +5,10 @@ APM settings ++++ -You do not need to configure any settings to use the APM app. It is enabled by default. +These settings allow the APM app to function, and specify the data that it surfaces. +Unless you've customized your setup, +you do not need to configure any settings to use the APM app. +It is enabled by default. [float] [[apm-indices-settings-kb]] @@ -33,29 +36,29 @@ image::settings/images/apm-settings.png[APM app settings in Kibana] If you'd like to change any of the default values, copy and paste the relevant settings into your `kibana.yml` configuration file. +Changing these settings may disable features of the APM App. -xpack.apm.enabled:: Set to `false` to disabled the APM plugin {kib}. Defaults to -`true`. +xpack.apm.enabled:: Set to `false` to disable the APM app. Defaults to `true`. -xpack.apm.ui.enabled:: Set to `false` to hide the APM plugin {kib} from the menu. Defaults to -`true`. +xpack.apm.ui.enabled:: Set to `false` to hide the APM app from the menu. Defaults to `true`. -xpack.apm.ui.transactionGroupBucketSize:: Number of top transaction groups displayed in APM plugin in Kibana. Defaults to `100`. +xpack.apm.ui.transactionGroupBucketSize:: Number of top transaction groups displayed in the APM app. Defaults to `100`. -xpack.apm.ui.maxTraceItems:: Max number of child items displayed when viewing trace details. Defaults to `1000`. +xpack.apm.ui.maxTraceItems:: Maximum number of child items displayed when viewing trace details. Defaults to `1000`. -apm_oss.indexPattern:: Index pattern is used for integrations with Machine Learning and Kuery Bar. It must match all apm indices. Defaults to `apm-*`. +apm_oss.indexPattern:: The index pattern used for integrations with Machine Learning and Query Bar. +It must match all apm indices. Defaults to `apm-*`. -apm_oss.errorIndices:: Matcher for indices containing error documents. Defaults to `apm-*`. +apm_oss.errorIndices:: Matcher for all {apm-server-ref}/error-indices.html[error indices]. Defaults to `apm-*`. -apm_oss.onboardingIndices:: Matcher for indices containing onboarding documents. Defaults to `apm-*`. +apm_oss.onboardingIndices:: Matcher for all onboarding indices. Defaults to `apm-*`. -apm_oss.spanIndices:: Matcher for indices containing span documents. Defaults to `apm-*`. +apm_oss.spanIndices:: Matcher for all {apm-server-ref}/span-indices.html[span indices]. Defaults to `apm-*`. -apm_oss.transactionIndices:: Matcher for indices containing transaction documents. Defaults to `apm-*`. +apm_oss.transactionIndices:: Matcher for all {apm-server-ref}/transaction-indices.html[transaction indices]. Defaults to `apm-*`. -apm_oss.metricsIndices:: Matcher for indices containing metric documents. Defaults to `apm-*`. +apm_oss.metricsIndices:: Matcher for all {apm-server-ref}/metricset-indices.html[metrics indices]. Defaults to `apm-*`. -apm_oss.sourcemapIndices:: Matcher for indices containing sourcemap documents. Defaults to `apm-*`. +apm_oss.sourcemapIndices:: Matcher for all {apm-server-ref}/sourcemap-indices.html[source map indices]. Defaults to `apm-*`. // end::general-apm-settings[] diff --git a/docs/uptime-guide/index.asciidoc b/docs/uptime-guide/index.asciidoc index 7bbc01bb303f1..09763182fa88f 100644 --- a/docs/uptime-guide/index.asciidoc +++ b/docs/uptime-guide/index.asciidoc @@ -12,4 +12,3 @@ include::install.asciidoc[] include::deployment-arch.asciidoc[] -include::security.asciidoc[] diff --git a/docs/uptime-guide/install.asciidoc b/docs/uptime-guide/install.asciidoc index e7c50bb7604ce..0ed1270ca92ce 100644 --- a/docs/uptime-guide/install.asciidoc +++ b/docs/uptime-guide/install.asciidoc @@ -56,6 +56,11 @@ Additional information is available in {heartbeat-ref}/heartbeat-configuration.h [role="screenshot"] image::images/uptime-setup.png[Installation instructions on the Uptime page in Kibana] +[[setup-security]] +=== Step 4: Setup Security + +Secure your installation by following the {heartbeat-ref}/securing-heartbeat.html[Secure Heartbeat] documentation. + [float] ==== Important considerations diff --git a/docs/uptime-guide/security.asciidoc b/docs/uptime-guide/security.asciidoc deleted file mode 100644 index 0c6fa4c6c4f56..0000000000000 --- a/docs/uptime-guide/security.asciidoc +++ /dev/null @@ -1,60 +0,0 @@ -[[uptime-security]] -== Elasticsearch Security - -If you use Elasticsearch security, you'll need to enable certain privileges for users -that would like to access the Uptime app. For example, create user and support roles to implement the privileges: - -[float] -=== Create a role - -You'll need a role that lets you access the Heartbeat indices, which by default are `heartbeat-*`. -You can create this with the following request: - -["source","sh",subs="attributes,callouts"] ---------------------------------------------------------------- -PUT /_security/role/uptime -{ "indices" : [ - { - "names" : [ - "heartbeat-*" - ], - "privileges" : [ - "read", - "view_index_metadata" - ], - "field_security" : { - "grant" : [ - "*" - ] - }, - "allow_restricted_indices" : false - } - ], - "transient_metadata" : { - "enabled" : true - } -} ---------------------------------------------------------------- -// CONSOLE - -[float] -=== Assign the role to a user - -Next, you'll need to create a user with both the `uptime` role, and another role with sufficient {kibana-ref}/kibana-privileges.html[Kibana privileges], -such as the `kibana_admin` role. -You can do this with the following request: - -["source","sh",subs="attributes,callouts"] ---------------------------------------------------------------- -PUT /_security/user/jacknich -{ - "password" : "j@rV1s", - "roles" : [ "uptime", "kibana_admin" ], - "full_name" : "Jack Nicholson", - "email" : "jacknich@example.com", - "metadata" : { - "intelligence" : 7 - } -} ---------------------------------------------------------------- -// CONSOLE diff --git a/docs/uptime/images/alert-flyout.png b/docs/uptime/images/alert-flyout.png new file mode 100644 index 0000000000000..7fc1e3d9aefe2 Binary files /dev/null and b/docs/uptime/images/alert-flyout.png differ diff --git a/docs/uptime/images/check-history.png b/docs/uptime/images/check-history.png index 6418495eee9ed..91565bf59aa7f 100644 Binary files a/docs/uptime/images/check-history.png and b/docs/uptime/images/check-history.png differ diff --git a/docs/uptime/images/error-list.png b/docs/uptime/images/error-list.png deleted file mode 100644 index 99f017f2945a5..0000000000000 Binary files a/docs/uptime/images/error-list.png and /dev/null differ diff --git a/docs/uptime/images/monitor-charts.png b/docs/uptime/images/monitor-charts.png index dbfa43f47656e..522f34662657e 100644 Binary files a/docs/uptime/images/monitor-charts.png and b/docs/uptime/images/monitor-charts.png differ diff --git a/docs/uptime/images/observability_integrations.png b/docs/uptime/images/observability_integrations.png index d5c612c7589ca..6589c0c5565dd 100644 Binary files a/docs/uptime/images/observability_integrations.png and b/docs/uptime/images/observability_integrations.png differ diff --git a/docs/uptime/images/settings.png b/docs/uptime/images/settings.png new file mode 100644 index 0000000000000..dd36f0a6d702b Binary files /dev/null and b/docs/uptime/images/settings.png differ diff --git a/docs/uptime/images/snapshot-view.png b/docs/uptime/images/snapshot-view.png index 020396d0f3e4c..1fce2e9592c14 100644 Binary files a/docs/uptime/images/snapshot-view.png and b/docs/uptime/images/snapshot-view.png differ diff --git a/docs/uptime/images/status-bar.png b/docs/uptime/images/status-bar.png index e0e9b27555900..8d242789cdccd 100644 Binary files a/docs/uptime/images/status-bar.png and b/docs/uptime/images/status-bar.png differ diff --git a/docs/uptime/index.asciidoc b/docs/uptime/index.asciidoc index 785b9f818f5bf..a355f8ecf4843 100644 --- a/docs/uptime/index.asciidoc +++ b/docs/uptime/index.asciidoc @@ -12,8 +12,10 @@ To get started with Elastic Uptime, refer to {uptime-guide}/install-uptime.html[ * <> * <> +* <> -- include::overview.asciidoc[] include::monitor.asciidoc[] +include::settings.asciidoc[] diff --git a/docs/uptime/monitor.asciidoc b/docs/uptime/monitor.asciidoc index d54fd02c7c069..8a4be1f11a721 100644 --- a/docs/uptime/monitor.asciidoc +++ b/docs/uptime/monitor.asciidoc @@ -5,21 +5,24 @@ The Monitor page will help you get further insight into the performance of a specific network endpoint. You'll see a detailed visualization of the monitor's request duration over time, as well as the `up`/`down` -status over time. +status over time. You can also also detect anomalies in response time data +by configuring Machine Learning jobs on this page. [float] -=== Status bar +=== Status panel [role="screenshot"] image::uptime/images/status-bar.png[Status bar] -The Status bar displays a quick summary of the latest information +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. -You can use the Status bar to get a quick summary of current performance, -beyond simply knowing if the monitor is `up` or `down`. +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. + [float] === Monitor charts @@ -32,12 +35,14 @@ date range. These charts can help you gain insight into how quickly requests are by the targeted endpoint, and give you a sense of how frequently a host or endpoint was down in your selected timespan. -The first chart displays request duration information for your monitor. +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. +bucket. The line is the average time. Anomaly detection using Machine Learning +can be configured in the upper right hand of this panel. When response times change +in an unexpected way the time range in which they occurred will be given filled with a color. -Next, is a graphical representation of the check statuses over time. Hover over -the charts to display crosshairs with more specific numeric data. +The pings over time chart is a graphical representation of the check statuses over time. +Hover over the charts to display crosshairs with more specific numeric data. [role="screenshot"] image::uptime/images/crosshair-example.png[Chart crosshair] @@ -49,6 +54,6 @@ image::uptime/images/crosshair-example.png[Chart crosshair] image::uptime/images/check-history.png[Check history view] The Check history displays the total count of this monitor's checks for the selected -date range. You can additionally filter the checks by `status` to help find recent problems +date range. You can additionally filter the checks by status and location to help find recent problems on a per-check basis. This table can help you gain some insight into more granular details about recent individual data points Heartbeat is logging about your host or endpoint. diff --git a/docs/uptime/overview.asciidoc b/docs/uptime/overview.asciidoc index 098ce12a56991..71c09c968e512 100644 --- a/docs/uptime/overview.asciidoc +++ b/docs/uptime/overview.asciidoc @@ -21,12 +21,12 @@ This control allows you to use automated filter options, as well as input custom text to select specific monitors by field, URL, ID, and other attributes. [float] -=== Snapshot view +=== Snapshot panel [role="screenshot"] image::uptime/images/snapshot-view.png[Snapshot view] -This view is intended to quickly give you a sense of the overall +This panel is intended to quickly give you a sense of the overall status of the environment you're monitoring, or a subset of those monitors. Here, you can see the total number of detected monitors within the selected Uptime date range. In addition to the total, the counts for the number of monitors @@ -49,6 +49,17 @@ way to navigate to a more in-depth visualization for interesting hosts or endpoi This table includes information like the most recent status, when the monitor was last checked, its ID and URL, its IP address, and a dedicated sparkline showing its check status over time. +[float] +=== Creating and managing alerts + +[role="screenshot"] +image::uptime/images/alert-flyout.png[Create alert flyout] + +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. + [float] === Observability integrations @@ -60,14 +71,3 @@ Docker related host information, it will provide links to open the Metrics app o for this host. Additionally, this feature supplies links to simply filter the other views on the host's IP address, to help you quickly determine if these other solutions contain data relevant to your current interest. - -[float] -=== Error list - -[role="screenshot"] -image::uptime/images/error-list.png[Error list] - -The Error list displays aggregations of errors that Heartbeat has logged. Errors are -displayed by Error type, monitor ID, and message. Clicking a monitor's ID will take you -to the corresponding Monitor view, which can provide you richer information about the individual -data points that are resulting in the displayed errors. diff --git a/docs/uptime/settings.asciidoc b/docs/uptime/settings.asciidoc new file mode 100644 index 0000000000000..55da6e802bec6 --- /dev/null +++ b/docs/uptime/settings.asciidoc @@ -0,0 +1,27 @@ +[role="xpack"] +[[uptime-settings]] + +== Settings + +[role="screenshot"] +image::uptime/images/settings.png[Filter bar] + +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. + +As an example, 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 that 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! + +See the <> +and {heartbeat-ref}/securing-heartbeat.html[Heartbeat security] +docs for more information. diff --git a/docs/user/alerting/action-types.asciidoc b/docs/user/alerting/action-types.asciidoc index 02c09736e1fa0..49e7bd1d77743 100644 --- a/docs/user/alerting/action-types.asciidoc +++ b/docs/user/alerting/action-types.asciidoc @@ -2,181 +2,55 @@ [[action-types]] == Action and connector types -{kib} provides the following types of actions: +Actions are Kibana services or integrations with third-party systems that run as background tasks on the Kibana server when alert conditions are met. {kib} provides the following types of actions: -* <> -* <> -* <> -* <> -* <> -* <> +[cols="2"] +|=== -This section describes how to configure connectors and actions for each type. +a| <> -[NOTE] -============================================== -Some action types are paid commercial features, while others are free. -For a comparison of the Elastic license levels, -see https://www.elastic.co/subscriptions[the subscription page]. -============================================== - -[float] -[[email-action-type]] -=== Email - -The email action type uses the SMTP protocol to send mail message, using an integration of https://nodemailer.com/[Nodemailer]. Email message text is sent as both plain text and html text. - -[float] -[[email-connector-configuration]] -==== Connector configuration - -Email connectors have the following configuration properties: - -Name:: The name of the connector. The name is used to identify a connector in the management UI connector listing, or in the connector list when configuring an action. -Sender:: The from address for all emails sent with this connector, specified in `user@host-name` format. -Host:: Host name of the service provider. If you are using the <> setting, make sure this hostname is whitelisted. -Port:: The port to connect to on the service provider. -Secure:: If true the connection will use TLS when connecting to the service provider. See https://nodemailer.com/smtp/#tls-options[nodemailer TLS documentation] for more information. -Username:: username for 'login' type authentication. -Password:: password for 'login' type authentication. - -[float] -[[email-action-configuration]] -==== Action configuration - -Email actions have the following configuration properties: - -To, CC, BCC:: Each is a list of addresses. Addresses can be specified in `user@host-name` format, or in `name ` format. One of To, CC, or BCC must contain an entry. -Subject:: The subject line of the email. -Message:: The message text of the email. Markdown format is supported. - -[float] -[[index-action-type]] -=== Index - -The index action type will index a document into {es}. - -[float] -[[index-connector-configuration]] -==== Connector configuration - -Index connectors have the following configuration properties: - -Name:: The name of the connector. The name is used to identify a connector in the management UI connector listing, or in the connector list when configuring an action. -Index:: The {es} index to be written to. -Refresh:: Setting for the {ref}/docs-refresh.html[refresh] policy for the write request. -Execution time field:: This field will be automatically set to the time the alert condition was detected. - -[float] -[[index-action-configuration]] -==== Action configuration - -Index actions have the following properties: - -Document:: The document to index in json format. - -[float] -[[pagerduty-action-type]] -=== PagerDuty - -The PagerDuty action type uses the https://v2.developer.pagerduty.com/docs/events-api-v2[v2 Events API] to trigger, acknowledge, and resolve PagerDuty alerts. - -[float] -[[pagerduty-connector-configuration]] -==== Connector configuration - -PagerDuty connectors have the following configuration properties: - -Name:: The name of the connector. The name is used to identify a connector in the management UI connector listing, or in the connector list when configuring an action. -API URL:: An optional PagerDuty event URL. Defaults to `https://events.pagerduty.com/v2/enqueue`. If you are using the <> setting, make sure the hostname is whitelisted. -Routing Key:: A 32 character PagerDuty Integration Key for an integration on a service or on a global ruleset. - -[float] -[[pagerduty-action-configuration]] -==== Action configuration +| Send email from your server. -PagerDuty actions have the following properties: +a| <> -Severity:: The perceived severity of on the affected system. This can be one of `Critical`, `Error`, `Warning` or `Info`(default). -Event action:: One of `Trigger` (default), `Resolve`, or `Acknowledge`. See https://v2.developer.pagerduty.com/docs/events-api-v2#event-action[event action] for more details. -Dedup Key:: All actions sharing this key will be associated with the same PagerDuty alert. This value is used to correlate trigger and resolution. This value is *optional*, and if unset defaults to `action:`. The maximum length is *255* characters. See https://v2.developer.pagerduty.com/docs/events-api-v2#alert-de-duplication[alert deduplication] for details. -Timestamp:: An *optional* https://v2.developer.pagerduty.com/v2/docs/types#datetime[ISO-8601 format date-time], indicating the time the event was detected or generated. -Component:: An *optional* value indicating the component of the source machine that is responsible for the event, for example `mysql` or `eth0`. -Group:: An *optional* value indicating the logical grouping of components of a service, for example `app-stack`. -Source:: An *optional* value indicating the affected system, preferably a hostname or fully qualified domain name. Defaults to the {kib} saved object id of the action. -Summary:: An *optional* text summary of the event, defaults to `No summary provided`. The maximum length is 1024 characters. -Class:: An *optional* value indicating the class/type of the event, for example `ping failure` or `cpu load`. +| Index data into Elasticsearch. -For more details on these properties, see https://v2.developer.pagerduty.com/v2/docs/send-an-event-events-api-v2[PagerDuty v2 event parameters]. +a| <> -[float] -[[server-log-action-type]] -=== Server log +| Send an event in PagerDuty. -This action type writes and entry to the {kib} server log. +a| <> -[float] -[[server-log-connector-configuration]] -==== Connector configuration +| Add a message to a Kibana log. -Server log connectors have the following configuration properties: +a| <> -Name:: The name of the connector. The name is used to identify a connector in the management UI connector listing, or in the connector list when configuring an action. +| Send a message to a Slack channel or user. -[float] -[[server-log-action-configuration]] -==== Action configuration - -Server log actions have the following properties: - -Message:: The message to log. - -[float] -[[slack-action-type]] -=== Slack - -The Slack action type uses https://api.slack.com/incoming-webhooks[Slack Incoming Webhooks]. - -[float] -[[slack-connector-configuration]] -==== Connector configuration +a| <> -Slack connectors have the following configuration properties: +| Send a request to a web service. +|=== -Name:: The name of the connector. The name is used to identify a connector in the management UI connector listing, or in the connector list when configuring an action. -Webhook URL:: The URL of the incoming webhook. See https://api.slack.com/messaging/webhooks#getting_started[Slack Incoming Webhooks] for instructions on generating this URL. If you are using the <> setting, make sure the hostname is whitelisted. - -[float] -[[slack-action-configuration]] -==== Action configuration - -Slack actions have the following properties: - -Message:: The message text, converted to the `text` field in the Webhook JSON payload. Currently only the text field is supported. Markdown, images, and other advanced formatting are not yet supported. - -[float] -[[webhook-action-type]] -=== Webhook - -The Webhook action type uses https://github.com/axios/axios[axios] to send a POST or PUT request to a web service. - -[float] -[[webhook-connector-configuration]] -==== Connector configuration - -Webhook connectors have the following configuration properties: - -Name:: The name of the connector. The name is used to identify a connector in the management UI connector listing, or in the connector list when configuring an action. -URL:: The request URL. If you are using the <> setting, make sure the hostname is whitelisted. -Method:: HTTP request method, either `post`(default) or `put`. -Headers:: A set of key-value pairs sent as headers with the request -User:: An optional username. If set, HTTP basic authentication is used. Currently only basic authentication is supported. -Password:: An optional password. If set, HTTP basic authentication is used. Currently only basic authentication is supported. +[NOTE] +============================================== +Some action types are paid commercial features, while others are free. +For a comparison of the Elastic subscription levels, +see https://www.elastic.co/subscriptions[the subscription page]. +============================================== [float] -[[webhook-action-configuration]] -==== Action configuration +[[create-connectors]] +=== Connectors -Webhook actions have the following properties: +You can create connectors for actions in <> or via the action API. +For out-of-the-box and standardized connectors, you can <> +before {kib} starts. -Body:: A json payload sent to the request URL. \ No newline at end of file +include::action-types/email.asciidoc[] +include::action-types/index.asciidoc[] +include::action-types/pagerduty.asciidoc[] +include::action-types/server-log.asciidoc[] +include::action-types/slack.asciidoc[] +include::action-types/webhook.asciidoc[] +include::pre-configured-connectors.asciidoc[] diff --git a/docs/user/alerting/action-types/email.asciidoc b/docs/user/alerting/action-types/email.asciidoc new file mode 100644 index 0000000000000..be3623dd9e59c --- /dev/null +++ b/docs/user/alerting/action-types/email.asciidoc @@ -0,0 +1,29 @@ +[role="xpack"] +[[email-action-type]] +== Email action type + +The email action type uses the SMTP protocol to send mail message, using an integration of https://nodemailer.com/[Nodemailer]. Email message text is sent as both plain text and html text. + +[float] +[[email-connector-configuration]] +==== Connector configuration + +Email connectors have the following configuration properties: + +Name:: The name of the connector. The name is used to identify a connector in the management UI connector listing, or in the connector list when configuring an action. +Sender:: The from address for all emails sent with this connector, specified in `user@host-name` format. +Host:: Host name of the service provider. If you are using the <> setting, make sure this hostname is whitelisted. +Port:: The port to connect to on the service provider. +Secure:: If true the connection will use TLS when connecting to the service provider. See https://nodemailer.com/smtp/#tls-options[nodemailer TLS documentation] for more information. +Username:: username for 'login' type authentication. +Password:: password for 'login' type authentication. + +[float] +[[email-action-configuration]] +==== Action configuration + +Email actions have the following configuration properties: + +To, CC, BCC:: Each is a list of addresses. Addresses can be specified in `user@host-name` format, or in `name ` format. One of To, CC, or BCC must contain an entry. +Subject:: The subject line of the email. +Message:: The message text of the email. Markdown format is supported. \ No newline at end of file diff --git a/docs/user/alerting/action-types/index.asciidoc b/docs/user/alerting/action-types/index.asciidoc new file mode 100644 index 0000000000000..75d9e57b1f212 --- /dev/null +++ b/docs/user/alerting/action-types/index.asciidoc @@ -0,0 +1,24 @@ +[role="xpack"] +[[index-action-type]] +== Index action type + +The index action type will index a document into {es}. + +[float] +[[index-connector-configuration]] +==== Connector configuration + +Index connectors have the following configuration properties: + +Name:: The name of the connector. The name is used to identify a connector in the management UI connector listing, or in the connector list when configuring an action. +Index:: The {es} index to be written to. +Refresh:: Setting for the {ref}/docs-refresh.html[refresh] policy for the write request. +Execution time field:: This field will be automatically set to the time the alert condition was detected. + +[float] +[[index-action-configuration]] +==== Action configuration + +Index actions have the following properties: + +Document:: The document to index in json format. \ No newline at end of file diff --git a/docs/user/alerting/action-types/pagerduty.asciidoc b/docs/user/alerting/action-types/pagerduty.asciidoc new file mode 100644 index 0000000000000..da34c6e0855d7 --- /dev/null +++ b/docs/user/alerting/action-types/pagerduty.asciidoc @@ -0,0 +1,156 @@ +[role="xpack"] +[[pagerduty-action-type]] +== PagerDuty action type + +The PagerDuty action type uses the https://v2.developer.pagerduty.com/docs/events-api-v2[v2 Events API] to trigger, acknowledge, and resolve PagerDuty alerts. + +* <> +* <> +* <> + +[float] +[[pagerduty-benefits]] +=== PagerDuty + Elastic integration benefits + +By integrating PagerDuty with alerts, you can: + +* Route your alerts to the right PagerDuty responder within your team, based on your structure, escalation policies, and workflows. +* Automatically generate incidents of different types and severity based on each alert’s context. +* Tailor the incident data to match your needs by easily passing the alerting context from Kibana to PagerDuty. + +[float] +[[pagerduty-how-it-works]] +==== How it works + +{kib} allows you to create alerts to notify you of a significant move +in your dataset. +You can create alerts for all your Observability, Security, and Elastic Stack use cases. +Alerts will trigger a new incident on the corresponding PagerDuty service. + +[float] +==== Requirements + +In the `kibana.yml` configuration file, you must add the <>. +This is required to encrypt parameters that must be secured, for example PagerDuty’s integration key. + +If you have security enabled: + +* You must have +application privileges to access Metrics, APM, Uptime, or SIEM. +* If you are using a self-managed deployment with security, you must have +Transport Security Layer (TLS) enabled for communication <>. +Alerts uses API keys to secure background alert checks and actions, +and API keys require {ref}/configuring-tls.html#tls-http[TLS on the HTTP interface]. + +Although not a requirement, to harden the integrations security you might want to +review the <> that are available to you. + +[float] +[[pagerduty-support]] +==== Support +If you need help with this integration, get in touch with the {kib} team by visiting +https://support.elastic.co[support.elastic.co] or by using the *Ask Elastic* option in the {kib} Help menu. +You can also select the {kib} category at https://discuss.elastic.co/[discuss.elastic.co]. + +[float] +[[pagerduty-integration-walkthrough]] +==== Integration with PagerDuty walkthrough + +[float] +[[pagerduty-in-pagerduty]] +===== In PagerDuty + +. From the *Configuration* menu, select *Services*. +. Add an integration to a service: ++ +* If you are adding your integration to an existing service, +click the name of the service you want to add the integration to. +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. +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. +. 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. ++ +[role="screenshot"] +image::user/alerting/images/pagerduty-integration.png[PagerDuty Integrations tab] + +. Save this key, as you will use it when you configure the integration with Elastic in the next section. + +[float] +[[pagerduty-in-elastic]] +===== In Elastic + +. Create a PagerDuty Connector in Kibana. You can: ++ +* Create a connector as part of creating an alert by selecting PagerDuty in the *Actions* +section of the alert configuration and selecting *Add new*. +* Alternatively, create a connector by navigating to *Management* from the {kib} navbar and selecting +*Alerts and Actions*. Then, select the *Connectors* tab, click the *Create connector* button, and select the PagerDuty option. + +. Configure the connector by giving it a name and optionally entering the API URL and Routing Key, or using the defaults. ++ +See <> for how to obtain the endpoint and key information from PagerDuty and +<> for more details. + +. Save the Connector. + +. Create an alert using *Management > Alerts and Actions* or the application of your choice. + +. Set up an action using your PagerDuty connector, by determining: ++ +* The action’s type: Trigger, Resolve, or Acknowledge. +* The event’s severity: Info, warning, error, or critical. +* An array of different fields, including the timestamp, group, class, component, and your dedup key. +Depending on your custom needs, assign them variables from the alerting context. +To see the available context variables, click on the *Add alert variable* icon next +to each corresponding field. For more details on these parameters, see the +<> and the PagerDuty +https://v2.developer.pagerduty.com/v2/docs/send-an-event-events-api-v2[API v2 documentation]. + + +[float] +[[pagerduty-uninstall]] +==== How to uninstall +To remove a PagerDuty connector from an alert, simply remove it +from the *Actions* section of that alert, using the remove (x) icon. +This will disable the integration for the particular alert. + +To delete the connector entirely, go to *Management > Alerts and Actions*. +Select the *Connectors* tab, and then click on the delete icon. +This is an irreversible action and impacts all alerts that use this connector. + + +[float] +[[pagerduty-connector-configuration]] +=== Connector configuration + +PagerDuty connectors have the following configuration properties: + +Name:: The name of the connector. The name is used to identify a connector in the management UI connector listing, or in the connector list when configuring an action. +API URL:: An optional PagerDuty event URL. Defaults to `https://events.pagerduty.com/v2/enqueue`. If you are using the <> setting, make sure the hostname is whitelisted. +Routing Key:: A 32 character PagerDuty Integration Key for an integration on a service or on a global ruleset. + +[float] +[[pagerduty-action-configuration]] +=== Action configuration + +PagerDuty actions have the following properties: + +Severity:: The perceived severity of on the affected system. This can be one of `Critical`, `Error`, `Warning` or `Info`(default). +Event action:: One of `Trigger` (default), `Resolve`, or `Acknowledge`. See https://v2.developer.pagerduty.com/docs/events-api-v2#event-action[event action] for more details. +Dedup Key:: All actions sharing this key will be associated with the same PagerDuty alert. This value is used to correlate trigger and resolution. This value is *optional*, and if unset defaults to `action:`. The maximum length is *255* characters. See https://v2.developer.pagerduty.com/docs/events-api-v2#alert-de-duplication[alert deduplication] for details. +Timestamp:: An *optional* https://v2.developer.pagerduty.com/v2/docs/types#datetime[ISO-8601 format date-time], indicating the time the event was detected or generated. +Component:: An *optional* value indicating the component of the source machine that is responsible for the event, for example `mysql` or `eth0`. +Group:: An *optional* value indicating the logical grouping of components of a service, for example `app-stack`. +Source:: An *optional* value indicating the affected system, preferably a hostname or fully qualified domain name. Defaults to the {kib} saved object id of the action. +Summary:: An *optional* text summary of the event, defaults to `No summary provided`. The maximum length is 1024 characters. +Class:: An *optional* value indicating the class/type of the event, for example `ping failure` or `cpu load`. + +For more details on these properties, see https://v2.developer.pagerduty.com/v2/docs/send-an-event-events-api-v2[PagerDuty v2 event parameters]. diff --git a/docs/user/alerting/action-types/server-log.asciidoc b/docs/user/alerting/action-types/server-log.asciidoc new file mode 100644 index 0000000000000..4efbdf3bea099 --- /dev/null +++ b/docs/user/alerting/action-types/server-log.asciidoc @@ -0,0 +1,21 @@ +[role="xpack"] +[[server-log-action-type]] +== Server log action type + +This action type writes and entry to the {kib} server log. + +[float] +[[server-log-connector-configuration]] +==== Connector configuration + +Server log connectors have the following configuration properties: + +Name:: The name of the connector. The name is used to identify a connector in the management UI connector listing, or in the connector list when configuring an action. + +[float] +[[server-log-action-configuration]] +==== Action configuration + +Server log actions have the following properties: + +Message:: The message to log. \ No newline at end of file diff --git a/docs/user/alerting/action-types/slack.asciidoc b/docs/user/alerting/action-types/slack.asciidoc new file mode 100644 index 0000000000000..a4bacbf162e46 --- /dev/null +++ b/docs/user/alerting/action-types/slack.asciidoc @@ -0,0 +1,22 @@ +[role="xpack"] +[[slack-action-type]] +== Slack action type + +The Slack action type uses https://api.slack.com/incoming-webhooks[Slack Incoming Webhooks]. + +[float] +[[slack-connector-configuration]] +==== Connector configuration + +Slack connectors have the following configuration properties: + +Name:: The name of the connector. The name is used to identify a connector in the management UI connector listing, or in the connector list when configuring an action. +Webhook URL:: The URL of the incoming webhook. See https://api.slack.com/messaging/webhooks#getting_started[Slack Incoming Webhooks] for instructions on generating this URL. If you are using the <> setting, make sure the hostname is whitelisted. + +[float] +[[slack-action-configuration]] +==== Action configuration + +Slack actions have the following properties: + +Message:: The message text, converted to the `text` field in the Webhook JSON payload. Currently only the text field is supported. Markdown, images, and other advanced formatting are not yet supported. \ No newline at end of file diff --git a/docs/user/alerting/action-types/webhook.asciidoc b/docs/user/alerting/action-types/webhook.asciidoc new file mode 100644 index 0000000000000..8c211aa83af89 --- /dev/null +++ b/docs/user/alerting/action-types/webhook.asciidoc @@ -0,0 +1,26 @@ +[role="xpack"] +[[webhook-action-type]] +== Webhook action type + +The Webhook action type uses https://github.com/axios/axios[axios] to send a POST or PUT request to a web service. + +[float] +[[webhook-connector-configuration]] +==== Connector configuration + +Webhook connectors have the following configuration properties: + +Name:: The name of the connector. The name is used to identify a connector in the management UI connector listing, or in the connector list when configuring an action. +URL:: The request URL. If you are using the <> setting, make sure the hostname is whitelisted. +Method:: HTTP request method, either `post`(default) or `put`. +Headers:: A set of key-value pairs sent as headers with the request +User:: An optional username. If set, HTTP basic authentication is used. Currently only basic authentication is supported. +Password:: An optional password. If set, HTTP basic authentication is used. Currently only basic authentication is supported. + +[float] +[[webhook-action-configuration]] +==== Action configuration + +Webhook actions have the following properties: + +Body:: A json payload sent to the request URL. \ No newline at end of file diff --git a/docs/user/alerting/images/alert-pre-configured-connectors-dropdown.png b/docs/user/alerting/images/alert-pre-configured-connectors-dropdown.png new file mode 100644 index 0000000000000..4e6c713298626 Binary files /dev/null and b/docs/user/alerting/images/alert-pre-configured-connectors-dropdown.png differ diff --git a/docs/user/alerting/images/alert-pre-configured-slack-connector.png b/docs/user/alerting/images/alert-pre-configured-slack-connector.png new file mode 100644 index 0000000000000..de05e2074ddde Binary files /dev/null and b/docs/user/alerting/images/alert-pre-configured-slack-connector.png differ diff --git a/docs/user/alerting/images/pagerduty-integration.png b/docs/user/alerting/images/pagerduty-integration.png new file mode 100755 index 0000000000000..0a270de866b36 Binary files /dev/null and b/docs/user/alerting/images/pagerduty-integration.png differ diff --git a/docs/user/alerting/images/pre-configured-connectors-managing.png b/docs/user/alerting/images/pre-configured-connectors-managing.png new file mode 100644 index 0000000000000..f97e93175fa36 Binary files /dev/null and b/docs/user/alerting/images/pre-configured-connectors-managing.png differ diff --git a/docs/user/alerting/images/pre-configured-connectors-view-screen.png b/docs/user/alerting/images/pre-configured-connectors-view-screen.png new file mode 100644 index 0000000000000..43ac44e7536d8 Binary files /dev/null and b/docs/user/alerting/images/pre-configured-connectors-view-screen.png differ diff --git a/docs/user/alerting/index.asciidoc b/docs/user/alerting/index.asciidoc index c7cf1186a44be..df11f5f03a7de 100644 --- a/docs/user/alerting/index.asciidoc +++ b/docs/user/alerting/index.asciidoc @@ -154,10 +154,13 @@ Pre-packaged *alert types* simplify setup, hide the details complex domain-speci [[alerting-setup-prerequisites]] == Setup and prerequisites +If you are using an *on-premises* Elastic Stack deployment: + +* In the kibana.yml configuration file, add the <> setting. + If you are using an *on-premises* Elastic Stack deployment with <>: -* TLS must be configured for communication <>. {kib} alerting uses <> to secure background alert checks and actions, and API keys require {ref}/configuring-tls.html#tls-http[TLS on the HTTP interface]. -* In the kibana.yml configuration file, add the <> +* Transport Layer Security (TLS) must be configured for communication <>. {kib} alerting uses <> to secure background alert checks and actions, and API keys require {ref}/configuring-tls.html#tls-http[TLS on the HTTP interface]. [float] [[alerting-security]] diff --git a/docs/user/alerting/pre-configured-connectors.asciidoc b/docs/user/alerting/pre-configured-connectors.asciidoc new file mode 100644 index 0000000000000..3db13acfb423e --- /dev/null +++ b/docs/user/alerting/pre-configured-connectors.asciidoc @@ -0,0 +1,88 @@ +[role="xpack"] +[[pre-configured-connectors]] + +== Preconfigured connectors + +You can preconfigure an action connector to have all the information it needs prior to startup +by adding it to the `kibana.yml` file. +Sensitive configuration information, such as credentials, can use the {kib} keystore. + +Preconfigured connectors offer the following capabilities: + +- Require no setup. Configuration and credentials needed to execute an +action are predefined, including the connector name and ID. +- Appear in all spaces because they are not saved objects. +- Cannot be edited or deleted. + +[float] +[[preconfigured-connector-example]] +=== Example of a preconfigured connector + +The following example shows a valid configuration 2 out-of-the box connector. + +[source,console] +------------------------ + xpack.actions.preconfigured: + - id: 'my-slack1' <1> + actionTypeId: .slack <2> + name: 'Slack #xyz' <3> + config: <4> + webhookUrl: 'https://hooks.slack.com/services/abcd/efgh/ijklmnopqrstuvwxyz' + - id: 'webhook-service' + actionTypeId: .webhook + name: 'Email service' + config: + url: 'https://email-alert-service.elastic.co' + method: post + headers: + header1: value1 + header2: value2 + secrets: <5> + user: elastic + password: changeme +------------------------ + +<1> `id` is the action connector identifier. +<2> `actionTypeId` is the action type identifier. +<3> `name` is the name of the preconfigured connector. +<4> `config` is the action type specific to the configuration. +<5> `secrets` is sensitive configuration, such as username, password, and keys. + +[NOTE] +============================================== +Sensitive properties, such as passwords, can also be stored in the {kib} keystore. +============================================== + +[float] +[[pre-configured-connector-alert-form]] +=== Creating an alert with a preconfigured connector + +When attaching an action to an alert, +select from a list of available action types, and +then select the Slack or Webhook type. Those action types were configured previously. +The preconfigured connector is installed and is automatically selected. + +[role="screenshot"] +image::images/alert-pre-configured-slack-connector.png[Create alert with selected Slack action type] + +The dropdown is populated with additional preconfigured Slack connectors. +The `preconfigured` label distinguishes them from space-aware connectors that use saved objects. + +[role="screenshot"] +image::images/alert-pre-configured-connectors-dropdown.png[Dropdown list with pre-cofigured connectors] + +[float] +[[managing-pre-configured-connectors]] +=== Managing preconfigured connectors + +Preconfigured connectors appear in the connector list, regardless of which space the user is in. +They are tagged as “preconfigured” and cannot be deleted. + +[role="screenshot"] +image::images/pre-configured-connectors-managing.png[Connectors managing tab with pre-cofigured] + +Clicking on a preconfigured connector shows the description, but not any of the configuration. +A message indicates that this is a preconfigured connector. + +[role="screenshot"] +image::images/pre-configured-connectors-view-screen.png[Pre-configured connector view details] diff --git a/docs/user/dev-tools.asciidoc b/docs/user/dev-tools.asciidoc index 77a781fd069e4..0ee7fbc741e00 100644 --- a/docs/user/dev-tools.asciidoc +++ b/docs/user/dev-tools.asciidoc @@ -4,13 +4,29 @@ [partintro] -- -The *Dev Tools* page contains development tools that you can use to interact -with your data in Kibana. +*Dev Tools* contains tools that you can use to interact +with your data. -* <> -* <> -* <> +[cols="2"] +|=== +a| <> + +| Interact with the REST API of Elasticsearch, including sending requests +and viewing API documentation. + +a| <> + +| Inspect and analyze your search queries. + +a| <> + +| Build and debug grok patterns before you use them in your data processing pipelines. + +a| <> + +| beta:[] Test and debug Painless scripts in real-time. +|=== -- @@ -19,3 +35,5 @@ include::{kib-repo-dir}/dev-tools/console/console.asciidoc[] include::{kib-repo-dir}/dev-tools/searchprofiler/index.asciidoc[] include::{kib-repo-dir}/dev-tools/grokdebugger/index.asciidoc[] + +include::{kib-repo-dir}/dev-tools/painlesslab/index.asciidoc[] diff --git a/docs/user/management.asciidoc b/docs/user/management.asciidoc index fa34802abe2a9..a4ba320e826b1 100644 --- a/docs/user/management.asciidoc +++ b/docs/user/management.asciidoc @@ -13,7 +13,7 @@ indices, clusters, licenses, UI settings, index patterns, spaces, and more. [cols="50, 50"] |=== -a| <> +a| <> Replicate indices on a remote cluster and copy them to a follower index on a local cluster. This is important for @@ -85,7 +85,8 @@ set the timespan for notification messages, and much more. | <> -Centrally manage your alerts from across {kib}. Create and manage re-usable connectors for triggering actions. +Centrally manage your alerts across {kib}. Create and manage reusable +connectors for triggering actions. | <> @@ -125,6 +126,8 @@ include::{kib-repo-dir}/management/alerting/connector-management.asciidoc[] include::{kib-repo-dir}/management/managing-beats.asciidoc[] +include::{kib-repo-dir}/management/managing-ccr.asciidoc[] + include::{kib-repo-dir}/management/index-lifecycle-policies/intro-to-lifecycle-policies.asciidoc[] include::{kib-repo-dir}/management/index-lifecycle-policies/create-policy.asciidoc[] diff --git a/docs/user/reporting/chromium-sandbox.asciidoc b/docs/user/reporting/chromium-sandbox.asciidoc index 5d4fbfb153a0b..bfef5b8b86c6b 100644 --- a/docs/user/reporting/chromium-sandbox.asciidoc +++ b/docs/user/reporting/chromium-sandbox.asciidoc @@ -11,12 +11,12 @@ sandboxing techniques differ for each operating system. The Linux sandbox depends on user namespaces, which were introduced with the 3.8 Linux kernel. However, many distributions don't have user namespaces enabled by default, or they require the CAP_SYS_ADMIN capability. {reporting} will automatically disable the sandbox when it is running on Debian and CentOS as additional steps are required to enable -unprivileged usernamespaces. In these situations, you'll see the following message in your {kib} logs: -`Enabling the Chromium sandbox provides an additional layer of protection`. +unprivileged usernamespaces. In these situations, you'll see the following message in your {kib} startup logs: +`Chromium sandbox provides an additional layer of protection, but is not supported for your OS. +Automatically setting 'xpack.reporting.capture.browser.chromium.disableSandbox: true'.` -If your kernel is 3.8 or newer, it's -recommended to enable usernamespaces and set `xpack.reporting.capture.browser.chromium.disableSandbox: false` in your -`kibana.yml` to enable the sandbox. +Reporting will automatically enable the Chromium sandbox at startup when a supported OS is detected. However, if your kernel is 3.8 or newer, it's +recommended to set `xpack.reporting.capture.browser.chromium.disableSandbox: false` in your `kibana.yml` to explicitly enable usernamespaces. ==== Docker When running {kib} in a Docker container, all container processes are run within a usernamespace with seccomp-bpf and diff --git a/examples/embeddable_examples/common/index.ts b/examples/embeddable_examples/common/index.ts new file mode 100644 index 0000000000000..726420fb9bdc3 --- /dev/null +++ b/examples/embeddable_examples/common/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 { TodoSavedObjectAttributes } from './todo_saved_object_attributes'; diff --git a/examples/embeddable_examples/common/todo_saved_object_attributes.ts b/examples/embeddable_examples/common/todo_saved_object_attributes.ts new file mode 100644 index 0000000000000..21b6df20fea90 --- /dev/null +++ b/examples/embeddable_examples/common/todo_saved_object_attributes.ts @@ -0,0 +1,26 @@ +/* + * 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 interface TodoSavedObjectAttributes extends SavedObjectAttributes { + task: string; + icon?: string; + title?: string; +} diff --git a/examples/embeddable_examples/kibana.json b/examples/embeddable_examples/kibana.json index c70bc7009ff51..f446e7f31ac8e 100644 --- a/examples/embeddable_examples/kibana.json +++ b/examples/embeddable_examples/kibana.json @@ -3,7 +3,7 @@ "version": "0.0.1", "kibanaVersion": "kibana", "configPath": ["embeddable_examples"], - "server": false, + "server": true, "ui": true, "requiredPlugins": ["embeddable"], "optionalPlugins": [] diff --git a/examples/embeddable_examples/public/create_sample_data.ts b/examples/embeddable_examples/public/create_sample_data.ts new file mode 100644 index 0000000000000..bd5ade18aa91e --- /dev/null +++ b/examples/embeddable_examples/public/create_sample_data.ts @@ -0,0 +1,36 @@ +/* + * 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 { SavedObjectsClientContract } from 'kibana/public'; +import { TodoSavedObjectAttributes } from '../common'; + +export async function createSampleData(client: SavedObjectsClientContract) { + await client.create( + 'todo', + { + task: 'Take the garbage out', + title: 'Garbage', + icon: 'trash', + }, + { + id: 'sample-todo-saved-object', + overwrite: true, + } + ); +} diff --git a/examples/embeddable_examples/public/index.ts b/examples/embeddable_examples/public/index.ts index 5fcd454b17a5c..4aac63fb52e2b 100644 --- a/examples/embeddable_examples/public/index.ts +++ b/examples/embeddable_examples/public/index.ts @@ -17,7 +17,6 @@ * under the License. */ -import { PluginInitializer } from 'kibana/public'; export { HELLO_WORLD_EMBEDDABLE, HelloWorldEmbeddable, @@ -26,18 +25,8 @@ export { export { ListContainer, LIST_CONTAINER } from './list_container'; export { TODO_EMBEDDABLE } from './todo'; -import { - EmbeddableExamplesPlugin, - EmbeddableExamplesSetupDependencies, - EmbeddableExamplesStartDependencies, -} from './plugin'; +import { EmbeddableExamplesPlugin } from './plugin'; export { SearchableListContainer, SEARCHABLE_LIST_CONTAINER } from './searchable_list_container'; export { MULTI_TASK_TODO_EMBEDDABLE } from './multi_task_todo'; - -export const plugin: PluginInitializer< - void, - void, - EmbeddableExamplesSetupDependencies, - EmbeddableExamplesStartDependencies -> = () => new EmbeddableExamplesPlugin(); +export const plugin = () => new EmbeddableExamplesPlugin(); diff --git a/examples/embeddable_examples/public/plugin.ts b/examples/embeddable_examples/public/plugin.ts index 31a3037332dda..75d34d2d6878f 100644 --- a/examples/embeddable_examples/public/plugin.ts +++ b/examples/embeddable_examples/public/plugin.ts @@ -21,12 +21,20 @@ import { EmbeddableSetup, EmbeddableStart } from '../../../src/plugins/embeddabl import { Plugin, CoreSetup, CoreStart } from '../../../src/core/public'; import { HelloWorldEmbeddableFactory, HELLO_WORLD_EMBEDDABLE } from './hello_world'; import { TODO_EMBEDDABLE, TodoEmbeddableFactory, TodoInput, TodoOutput } from './todo'; -import { MULTI_TASK_TODO_EMBEDDABLE, MultiTaskTodoEmbeddableFactory } from './multi_task_todo'; +import { + MULTI_TASK_TODO_EMBEDDABLE, + MultiTaskTodoEmbeddableFactory, + MultiTaskTodoInput, + MultiTaskTodoOutput, +} from './multi_task_todo'; import { SEARCHABLE_LIST_CONTAINER, SearchableListContainerFactory, } from './searchable_list_container'; import { LIST_CONTAINER, ListContainerFactory } from './list_container'; +import { createSampleData } from './create_sample_data'; +import { TodoRefInput, TodoRefOutput, TODO_REF_EMBEDDABLE } from './todo/todo_ref_embeddable'; +import { TodoRefEmbeddableFactory } from './todo/todo_ref_embeddable_factory'; export interface EmbeddableExamplesSetupDependencies { embeddable: EmbeddableSetup; @@ -36,9 +44,18 @@ export interface EmbeddableExamplesStartDependencies { embeddable: EmbeddableStart; } +export interface EmbeddableExamplesStart { + createSampleData: () => Promise; +} + export class EmbeddableExamplesPlugin implements - Plugin { + Plugin< + void, + EmbeddableExamplesStart, + EmbeddableExamplesSetupDependencies, + EmbeddableExamplesStartDependencies + > { public setup( core: CoreSetup, deps: EmbeddableExamplesSetupDependencies @@ -48,7 +65,7 @@ export class EmbeddableExamplesPlugin new HelloWorldEmbeddableFactory() ); - deps.embeddable.registerEmbeddableFactory( + deps.embeddable.registerEmbeddableFactory( MULTI_TASK_TODO_EMBEDDABLE, new MultiTaskTodoEmbeddableFactory() ); @@ -73,9 +90,21 @@ export class EmbeddableExamplesPlugin openModal: (await core.getStartServices())[0].overlays.openModal, })) ); + + deps.embeddable.registerEmbeddableFactory( + TODO_REF_EMBEDDABLE, + new TodoRefEmbeddableFactory(async () => ({ + savedObjectsClient: (await core.getStartServices())[0].savedObjects.client, + getEmbeddableFactory: (await core.getStartServices())[1].embeddable.getEmbeddableFactory, + })) + ); } - public start(core: CoreStart, deps: EmbeddableExamplesStartDependencies) {} + public start(core: CoreStart, deps: EmbeddableExamplesStartDependencies) { + return { + createSampleData: () => createSampleData(core.savedObjects.client), + }; + } public stop() {} } diff --git a/examples/embeddable_examples/public/todo/README.md b/examples/embeddable_examples/public/todo/README.md new file mode 100644 index 0000000000000..e782511f093b3 --- /dev/null +++ b/examples/embeddable_examples/public/todo/README.md @@ -0,0 +1,43 @@ +There are two examples in here: + - TodoEmbeddable + - TodoRefEmbeddable + + # TodoEmbeddable + + The first example you should review is the HelloWorldEmbeddable. That is as basic an embeddable as you can get. + This embeddable is the next step up - an embeddable that renders dynamic input data. The data is simple: + - a required task string + - an optional title + - an optional icon string + - an optional search string + +It also has output data, which is `hasMatch` - whether or not the search string has matched any input data. + +`hasMatch` is a better fit for output data than input data, because it's state that is _derived_ from input data. + +For example, if it was input data, you could create a TodoEmbeddable with input like this: + +```ts + todoEmbeddableFactory.create({ task: 'take out the garabage', search: 'garbage', hasMatch: false }); +``` + +That's wrong because there is actually a match from the search string inside the task. + +The TodoEmbeddable component itself doesn't do anything with the `hasMatch` variable other than set it, but +if you check out `SearchableListContainer`, you can see an example where this output data is being used. + +## TodoRefEmbeddable + +This is an example of an embeddable based off of a saved object. The input is just the `savedObjectId` and +the `search` string. It has even more output parameters, and this time, it does read it's own output parameters in +order to calculate `hasMatch`. + +Output: +```ts +{ + hasMatch: boolean, + savedAttributes?: TodoSavedAttributes +} +``` + +`savedAttributes` is optional because it's possible a TodoSavedObject could not be found with the given savedObjectId. diff --git a/examples/embeddable_examples/public/todo/todo_ref_component.tsx b/examples/embeddable_examples/public/todo/todo_ref_component.tsx new file mode 100644 index 0000000000000..8e0a17be1ec72 --- /dev/null +++ b/examples/embeddable_examples/public/todo/todo_ref_component.tsx @@ -0,0 +1,86 @@ +/* + * 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 } from '@elastic/eui'; + +import { EuiText } from '@elastic/eui'; +import { EuiAvatar } from '@elastic/eui'; +import { EuiIcon } from '@elastic/eui'; +import { EuiFlexGrid } from '@elastic/eui'; +import { withEmbeddableSubscription } from '../../../../src/plugins/embeddable/public'; +import { TodoRefInput, TodoRefOutput, TodoRefEmbeddable } from './todo_ref_embeddable'; + +interface Props { + embeddable: TodoRefEmbeddable; + input: TodoRefInput; + output: TodoRefOutput; +} + +function wrapSearchTerms(task?: string, search?: string) { + if (!search) return task; + if (!task) return task; + const parts = task.split(new RegExp(`(${search})`, 'g')); + return parts.map((part, i) => + part === search ? ( + + {part} + + ) : ( + part + ) + ); +} + +export function TodoRefEmbeddableComponentInner({ + input: { search }, + output: { savedAttributes }, +}: Props) { + const icon = savedAttributes?.icon; + const title = savedAttributes?.title; + const task = savedAttributes?.task; + return ( + + + {icon ? ( + + ) : ( + + )} + + + + + +

{wrapSearchTerms(title || '', search)}

+ + + + {wrapSearchTerms(task, search)} + + + + + ); +} + +export const TodoRefEmbeddableComponent = withEmbeddableSubscription< + TodoRefInput, + TodoRefOutput, + TodoRefEmbeddable +>(TodoRefEmbeddableComponentInner); diff --git a/examples/embeddable_examples/public/todo/todo_ref_embeddable.tsx b/examples/embeddable_examples/public/todo/todo_ref_embeddable.tsx new file mode 100644 index 0000000000000..da2dfb2c1a290 --- /dev/null +++ b/examples/embeddable_examples/public/todo/todo_ref_embeddable.tsx @@ -0,0 +1,153 @@ +/* + * 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 { TodoSavedObjectAttributes } from 'examples/embeddable_examples/common'; +import { SavedObjectsClientContract } from 'kibana/public'; +import { + Embeddable, + IContainer, + EmbeddableOutput, + SavedObjectEmbeddableInput, +} from '../../../../src/plugins/embeddable/public'; +import { TodoRefEmbeddableComponent } from './todo_ref_component'; + +// Notice this is not the same value as the 'todo' saved object type. Many of our +// cases in prod today use the same value, but this is unnecessary. +export const TODO_REF_EMBEDDABLE = 'TODO_REF_EMBEDDABLE'; + +export interface TodoRefInput extends SavedObjectEmbeddableInput { + /** + * Optional search string which will be used to highlight search terms as + * well as calculate `output.hasMatch`. + */ + search?: string; +} + +export interface TodoRefOutput extends EmbeddableOutput { + /** + * Should be true if input.search is defined and the task or title contain + * search as a substring. + */ + hasMatch: boolean; + /** + * Will contain the saved object attributes of the Todo Saved Object that matches + * `input.savedObjectId`. If the id is invalid, this may be undefined. + */ + savedAttributes?: TodoSavedObjectAttributes; +} + +/** + * 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?: TodoSavedObjectAttributes): boolean { + if (!search) return true; + if (!savedAttributes) return false; + return Boolean( + (savedAttributes.task && savedAttributes.task.match(search)) || + (savedAttributes.title && savedAttributes.title.match(search)) + ); +} + +/** + * This is an example of an embeddable that is backed by a saved object. It's essentially the + * same as `TodoEmbeddable` but that is "by value", while this is "by reference". + */ +export class TodoRefEmbeddable extends Embeddable { + public readonly type = TODO_REF_EMBEDDABLE; + private subscription: Subscription; + private node?: HTMLElement; + private savedObjectsClient: SavedObjectsClientContract; + private savedObjectId?: string; + + constructor( + initialInput: TodoRefInput, + { + parent, + savedObjectsClient, + }: { + parent?: IContainer; + savedObjectsClient: SavedObjectsClientContract; + } + ) { + super(initialInput, { hasMatch: false }, parent); + this.savedObjectsClient = savedObjectsClient; + + this.subscription = this.getInput$().subscribe(async () => { + // There is a little more work today for this embeddable because it has + // more output it needs to update in response to input state changes. + let savedAttributes: TodoSavedObjectAttributes | undefined; + + // Since this is an expensive task, we save a local copy of the previous + // savedObjectId locally and only retrieve the new saved object if the id + // actually changed. + if (this.savedObjectId !== this.input.savedObjectId) { + this.savedObjectId = this.input.savedObjectId; + const todoSavedObject = await this.savedObjectsClient.get( + 'todo', + this.input.savedObjectId + ); + savedAttributes = todoSavedObject?.attributes; + } + + // The search string might have changed as well so we need to make sure we recalculate + // hasMatch. + this.updateOutput({ + hasMatch: getHasMatch(this.input.search, savedAttributes), + savedAttributes, + }); + }); + } + + public render(node: HTMLElement) { + if (this.node) { + ReactDOM.unmountComponentAtNode(this.node); + } + this.node = node; + ReactDOM.render(, node); + } + + /** + * Lets re-sync our saved object to make sure it's up to date! + */ + public async reload() { + this.savedObjectId = this.input.savedObjectId; + const todoSavedObject = await this.savedObjectsClient.get( + 'todo', + this.input.savedObjectId + ); + const savedAttributes = todoSavedObject?.attributes; + this.updateOutput({ + hasMatch: getHasMatch(this.input.search, savedAttributes), + savedAttributes, + }); + } + + public destroy() { + super.destroy(); + this.subscription.unsubscribe(); + if (this.node) { + ReactDOM.unmountComponentAtNode(this.node); + } + } +} diff --git a/examples/embeddable_examples/public/todo/todo_ref_embeddable_factory.tsx b/examples/embeddable_examples/public/todo/todo_ref_embeddable_factory.tsx new file mode 100644 index 0000000000000..e585ddd89674f --- /dev/null +++ b/examples/embeddable_examples/public/todo/todo_ref_embeddable_factory.tsx @@ -0,0 +1,83 @@ +/* + * 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 { i18n } from '@kbn/i18n'; +import { SavedObjectsClientContract } from 'kibana/public'; +import { TodoSavedObjectAttributes } from 'examples/embeddable_examples/common'; +import { + IContainer, + EmbeddableStart, + ErrorEmbeddable, + EmbeddableFactoryDefinition, +} from '../../../../src/plugins/embeddable/public'; +import { + TodoRefEmbeddable, + TODO_REF_EMBEDDABLE, + TodoRefInput, + TodoRefOutput, +} from './todo_ref_embeddable'; + +interface StartServices { + getEmbeddableFactory: EmbeddableStart['getEmbeddableFactory']; + savedObjectsClient: SavedObjectsClientContract; +} + +export class TodoRefEmbeddableFactory + implements + EmbeddableFactoryDefinition< + TodoRefInput, + TodoRefOutput, + TodoRefEmbeddable, + TodoSavedObjectAttributes + > { + public readonly type = TODO_REF_EMBEDDABLE; + public readonly savedObjectMetaData = { + name: 'Todo', + includeFields: ['task', 'icon', 'title'], + type: 'todo', + getIconForSavedObject: () => 'pencil', + }; + + constructor(private getStartServices: () => Promise) {} + + public async isEditable() { + return true; + } + + public createFromSavedObject = ( + savedObjectId: string, + input: Partial & { id: string }, + parent?: IContainer + ): Promise => { + return this.create({ ...input, savedObjectId }, parent); + }; + + public async create(input: TodoRefInput, parent?: IContainer) { + const { savedObjectsClient } = await this.getStartServices(); + return new TodoRefEmbeddable(input, { + parent, + savedObjectsClient, + }); + } + + public getDisplayName() { + return i18n.translate('embeddableExamples.todo.displayName', { + defaultMessage: 'Todo (by reference)', + }); + } +} diff --git a/examples/embeddable_examples/server/index.ts b/examples/embeddable_examples/server/index.ts new file mode 100644 index 0000000000000..9ddc3bc2cf715 --- /dev/null +++ b/examples/embeddable_examples/server/index.ts @@ -0,0 +1,24 @@ +/* + * 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 { PluginInitializer } from 'kibana/server'; + +import { EmbeddableExamplesPlugin } from './plugin'; + +export const plugin: PluginInitializer = () => new EmbeddableExamplesPlugin(); diff --git a/examples/embeddable_examples/server/plugin.ts b/examples/embeddable_examples/server/plugin.ts new file mode 100644 index 0000000000000..d956b834d0d3c --- /dev/null +++ b/examples/embeddable_examples/server/plugin.ts @@ -0,0 +1,31 @@ +/* + * 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 { Plugin, CoreSetup, CoreStart } from 'kibana/server'; +import { todoSavedObject } from './todo_saved_object'; + +export class EmbeddableExamplesPlugin implements Plugin { + public setup(core: CoreSetup) { + core.savedObjects.registerType(todoSavedObject); + } + + public start(core: CoreStart) {} + + public stop() {} +} diff --git a/examples/embeddable_examples/server/todo_saved_object.ts b/examples/embeddable_examples/server/todo_saved_object.ts new file mode 100644 index 0000000000000..58da2014de498 --- /dev/null +++ b/examples/embeddable_examples/server/todo_saved_object.ts @@ -0,0 +1,40 @@ +/* + * 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 { SavedObjectsType } from 'kibana/server'; + +export const todoSavedObject: SavedObjectsType = { + name: 'todo', + hidden: false, + namespaceType: 'agnostic', + mappings: { + properties: { + title: { + type: 'keyword', + }, + task: { + type: 'text', + }, + icon: { + type: 'keyword', + }, + }, + }, + migrations: {}, +}; diff --git a/examples/embeddable_examples/tsconfig.json b/examples/embeddable_examples/tsconfig.json index 091130487791b..7fa03739119b4 100644 --- a/examples/embeddable_examples/tsconfig.json +++ b/examples/embeddable_examples/tsconfig.json @@ -6,6 +6,7 @@ }, "include": [ "index.ts", + "common/**/*.ts", "public/**/*.ts", "public/**/*.tsx", "server/**/*.ts", diff --git a/examples/embeddable_explorer/public/plugin.tsx b/examples/embeddable_explorer/public/plugin.tsx index 7c75b108d9912..bba1b1748e207 100644 --- a/examples/embeddable_explorer/public/plugin.tsx +++ b/examples/embeddable_explorer/public/plugin.tsx @@ -18,6 +18,7 @@ */ import { Plugin, CoreSetup, AppMountParameters } from 'kibana/public'; +import { EmbeddableExamplesStart } from 'examples/embeddable_examples/public/plugin'; import { UiActionsService } from '../../../src/plugins/ui_actions/public'; import { EmbeddableStart } from '../../../src/plugins/embeddable/public'; import { Start as InspectorStart } from '../../../src/plugins/inspector/public'; @@ -26,6 +27,7 @@ interface StartDeps { uiActions: UiActionsService; embeddable: EmbeddableStart; inspector: InspectorStart; + embeddableExamples: EmbeddableExamplesStart; } export class EmbeddableExplorerPlugin implements Plugin { @@ -36,6 +38,7 @@ export class EmbeddableExplorerPlugin implements Plugin) { return new AnyType(options); diff --git a/packages/kbn-config-schema/src/typeguards/index.ts b/packages/kbn-config-schema/src/typeguards/index.ts new file mode 100644 index 0000000000000..e724878eb33e9 --- /dev/null +++ b/packages/kbn-config-schema/src/typeguards/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 { isConfigSchema } from './is_config_schema'; diff --git a/packages/kbn-config-schema/src/typeguards/is_config_schema.test.ts b/packages/kbn-config-schema/src/typeguards/is_config_schema.test.ts new file mode 100644 index 0000000000000..e0ef3835ca0a3 --- /dev/null +++ b/packages/kbn-config-schema/src/typeguards/is_config_schema.test.ts @@ -0,0 +1,56 @@ +/* + * 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 { schema } from '..'; +import { isConfigSchema } from './is_config_schema'; + +describe('isConfigSchema', () => { + it('returns true for every sub classes of `Type`', () => { + expect(isConfigSchema(schema.any())).toBe(true); + expect(isConfigSchema(schema.arrayOf(schema.string()))).toBe(true); + expect(isConfigSchema(schema.boolean())).toBe(true); + expect(isConfigSchema(schema.buffer())).toBe(true); + expect(isConfigSchema(schema.byteSize())).toBe(true); + expect(isConfigSchema(schema.duration())).toBe(true); + expect(isConfigSchema(schema.literal(''))).toBe(true); + expect(isConfigSchema(schema.mapOf(schema.string(), schema.number()))).toBe(true); + expect(isConfigSchema(schema.nullable(schema.string()))).toBe(true); + expect(isConfigSchema(schema.number())).toBe(true); + expect(isConfigSchema(schema.object({}))).toBe(true); + expect(isConfigSchema(schema.oneOf([schema.string()]))).toBe(true); + expect(isConfigSchema(schema.recordOf(schema.string(), schema.object({})))).toBe(true); + expect(isConfigSchema(schema.string())).toBe(true); + expect(isConfigSchema(schema.stream())).toBe(true); + }); + + it('returns false for every javascript data type', () => { + expect(isConfigSchema('foo')).toBe(false); + expect(isConfigSchema(42)).toBe(false); + expect(isConfigSchema(new Date())).toBe(false); + expect(isConfigSchema(null)).toBe(false); + expect(isConfigSchema(undefined)).toBe(false); + expect(isConfigSchema([1, 2, 3])).toBe(false); + expect(isConfigSchema({ foo: 'bar' })).toBe(false); + expect(isConfigSchema(function() {})).toBe(false); + }); + + it('returns true as long as `__isKbnConfigSchemaType` is true', () => { + expect(isConfigSchema({ __isKbnConfigSchemaType: true })).toBe(true); + }); +}); diff --git a/packages/kbn-config-schema/src/typeguards/is_config_schema.ts b/packages/kbn-config-schema/src/typeguards/is_config_schema.ts new file mode 100644 index 0000000000000..20e68ab2ead25 --- /dev/null +++ b/packages/kbn-config-schema/src/typeguards/is_config_schema.ts @@ -0,0 +1,24 @@ +/* + * 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 { Type } from '../types'; + +export function isConfigSchema(obj: any): obj is Type { + return obj ? obj.__isKbnConfigSchemaType === true : false; +} diff --git a/packages/kbn-config-schema/src/types/type.ts b/packages/kbn-config-schema/src/types/type.ts index 6d5ddf6b24afb..5ca16c61399e7 100644 --- a/packages/kbn-config-schema/src/types/type.ts +++ b/packages/kbn-config-schema/src/types/type.ts @@ -32,6 +32,9 @@ export abstract class Type { // sets the value to `null` while still keeping the type. public readonly type: V = null! as V; + // used for the `isConfigSchema` typeguard + public readonly __isKbnConfigSchemaType = true; + /** * Internal "schema" backed by Joi. * @type {Schema} diff --git a/packages/kbn-optimizer/src/optimizer/optimizer_config.test.ts b/packages/kbn-optimizer/src/optimizer/optimizer_config.test.ts index d67b957416753..cc564dd4a8387 100644 --- a/packages/kbn-optimizer/src/optimizer/optimizer_config.test.ts +++ b/packages/kbn-optimizer/src/optimizer/optimizer_config.test.ts @@ -146,6 +146,7 @@ describe('OptimizerConfig::parseOptions()', () => { /x-pack/plugins, /plugins, /examples, + /x-pack/examples, -extra, ], "profileWebpack": false, diff --git a/packages/kbn-optimizer/src/optimizer/optimizer_config.ts b/packages/kbn-optimizer/src/optimizer/optimizer_config.ts index 1c8ae265bf6bb..7e1514058446b 100644 --- a/packages/kbn-optimizer/src/optimizer/optimizer_config.ts +++ b/packages/kbn-optimizer/src/optimizer/optimizer_config.ts @@ -91,14 +91,14 @@ export class OptimizerConfig { /** * BEWARE: this needs to stay roughly synchronized with - * `src/core/server/config/env.ts` which determins which paths + * `src/core/server/config/env.ts` which determines which paths * should be searched for plugins to load */ const pluginScanDirs = options.pluginScanDirs || [ Path.resolve(repoRoot, 'src/plugins'), ...(oss ? [] : [Path.resolve(repoRoot, 'x-pack/plugins')]), Path.resolve(repoRoot, 'plugins'), - ...(examples ? [Path.resolve('examples')] : []), + ...(examples ? [Path.resolve('examples'), Path.resolve('x-pack/examples')] : []), Path.resolve(repoRoot, '../kibana-extra'), ]; if (!pluginScanDirs.every(p => Path.isAbsolute(p))) { diff --git a/packages/kbn-pm/dist/index.js b/packages/kbn-pm/dist/index.js index 7a858deff41d3..6a2d02ee778dd 100644 --- a/packages/kbn-pm/dist/index.js +++ b/packages/kbn-pm/dist/index.js @@ -94,7 +94,7 @@ __webpack_require__.r(__webpack_exports__); /* harmony import */ var _cli__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(1); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "run", function() { return _cli__WEBPACK_IMPORTED_MODULE_0__["run"]; }); -/* harmony import */ var _production__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(704); +/* harmony import */ var _production__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(703); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "buildProductionProjects", function() { return _production__WEBPACK_IMPORTED_MODULE_1__["buildProductionProjects"]; }); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "prepareExternalProjectDependencies", function() { return _production__WEBPACK_IMPORTED_MODULE_1__["prepareExternalProjectDependencies"]; }); @@ -105,10 +105,10 @@ __webpack_require__.r(__webpack_exports__); /* harmony import */ var _utils_project__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(515); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "Project", function() { return _utils_project__WEBPACK_IMPORTED_MODULE_3__["Project"]; }); -/* harmony import */ var _utils_workspaces__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(577); +/* harmony import */ var _utils_workspaces__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(576); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "copyWorkspacePackages", function() { return _utils_workspaces__WEBPACK_IMPORTED_MODULE_4__["copyWorkspacePackages"]; }); -/* harmony import */ var _config__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(578); +/* harmony import */ var _config__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(577); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "getProjectPaths", function() { return _config__WEBPACK_IMPORTED_MODULE_5__["getProjectPaths"]; }); /* @@ -152,7 +152,7 @@ __webpack_require__.r(__webpack_exports__); /* harmony import */ var path__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(16); /* harmony import */ var path__WEBPACK_IMPORTED_MODULE_3___default = /*#__PURE__*/__webpack_require__.n(path__WEBPACK_IMPORTED_MODULE_3__); /* harmony import */ var _commands__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(17); -/* harmony import */ var _run__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(688); +/* harmony import */ var _run__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(687); /* harmony import */ var _utils_log__WEBPACK_IMPORTED_MODULE_6__ = __webpack_require__(34); /* * Licensed to Elasticsearch B.V. under one or more contributor @@ -2506,9 +2506,9 @@ module.exports = require("path"); __webpack_require__.r(__webpack_exports__); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "commands", function() { return commands; }); /* harmony import */ var _bootstrap__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(18); -/* harmony import */ var _clean__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(585); -/* harmony import */ var _run__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(685); -/* harmony import */ var _watch__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(686); +/* harmony import */ var _clean__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(584); +/* harmony import */ var _run__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(684); +/* harmony import */ var _watch__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(685); /* * Licensed to Elasticsearch B.V. under one or more contributor * license agreements. See the NOTICE file distributed with @@ -2551,8 +2551,8 @@ __webpack_require__.r(__webpack_exports__); /* harmony import */ var _utils_log__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(34); /* harmony import */ var _utils_parallelize__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(499); /* harmony import */ var _utils_projects__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(500); -/* harmony import */ var _utils_project_checksums__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(579); -/* harmony import */ var _utils_bootstrap_cache_file__WEBPACK_IMPORTED_MODULE_6__ = __webpack_require__(584); +/* harmony import */ var _utils_project_checksums__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(578); +/* harmony import */ var _utils_bootstrap_cache_file__WEBPACK_IMPORTED_MODULE_6__ = __webpack_require__(583); /* * Licensed to Elasticsearch B.V. under one or more contributor * license agreements. See the NOTICE file distributed with @@ -42623,28 +42623,21 @@ module.exports = require("tty"); const os = __webpack_require__(11); const hasFlag = __webpack_require__(12); -const {env} = process; +const env = process.env; let forceColor; if (hasFlag('no-color') || hasFlag('no-colors') || - hasFlag('color=false') || - hasFlag('color=never')) { - forceColor = 0; + hasFlag('color=false')) { + forceColor = false; } else if (hasFlag('color') || hasFlag('colors') || hasFlag('color=true') || hasFlag('color=always')) { - forceColor = 1; + forceColor = true; } if ('FORCE_COLOR' in env) { - if (env.FORCE_COLOR === true || env.FORCE_COLOR === 'true') { - forceColor = 1; - } else if (env.FORCE_COLOR === false || env.FORCE_COLOR === 'false') { - forceColor = 0; - } else { - forceColor = env.FORCE_COLOR.length === 0 ? 1 : Math.min(parseInt(env.FORCE_COLOR, 10), 3); - } + forceColor = env.FORCE_COLOR.length === 0 || parseInt(env.FORCE_COLOR, 10) !== 0; } function translateLevel(level) { @@ -42661,7 +42654,7 @@ function translateLevel(level) { } function supportsColor(stream) { - if (forceColor === 0) { + if (forceColor === false) { return 0; } @@ -42675,15 +42668,11 @@ function supportsColor(stream) { return 2; } - if (stream && !stream.isTTY && forceColor === undefined) { + if (stream && !stream.isTTY && forceColor !== true) { return 0; } - const min = forceColor || 0; - - if (env.TERM === 'dumb') { - return min; - } + const min = forceColor ? 1 : 0; if (process.platform === 'win32') { // Node.js 7.5.0 is the first version of Node.js to include a patch to @@ -42744,6 +42733,10 @@ function supportsColor(stream) { return 1; } + if (env.TERM === 'dumb') { + return min; + } + return min; } @@ -43866,7 +43859,7 @@ __webpack_require__.r(__webpack_exports__); /* harmony import */ var util__WEBPACK_IMPORTED_MODULE_2___default = /*#__PURE__*/__webpack_require__.n(util__WEBPACK_IMPORTED_MODULE_2__); /* harmony import */ var _errors__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(514); /* harmony import */ var _project__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(515); -/* harmony import */ var _workspaces__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(577); +/* harmony import */ var _workspaces__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(576); /* * Licensed to Elasticsearch B.V. under one or more contributor * license agreements. See the NOTICE file distributed with @@ -47386,7 +47379,7 @@ __webpack_require__.r(__webpack_exports__); /* harmony import */ var _errors__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(514); /* harmony import */ var _log__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(34); /* harmony import */ var _package_json__WEBPACK_IMPORTED_MODULE_6__ = __webpack_require__(516); -/* harmony import */ var _scripts__WEBPACK_IMPORTED_MODULE_7__ = __webpack_require__(562); +/* harmony import */ var _scripts__WEBPACK_IMPORTED_MODULE_7__ = __webpack_require__(561); function ownKeys(object, enumerableOnly) { var keys = Object.keys(object); if (Object.getOwnPropertySymbols) { var symbols = Object.getOwnPropertySymbols(object); if (enumerableOnly) symbols = symbols.filter(function (sym) { return Object.getOwnPropertyDescriptor(object, sym).enumerable; }); keys.push.apply(keys, symbols); } return keys; } function _objectSpread(target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i] != null ? arguments[i] : {}; if (i % 2) { ownKeys(Object(source), true).forEach(function (key) { _defineProperty(target, key, source[key]); }); } else if (Object.getOwnPropertyDescriptors) { Object.defineProperties(target, Object.getOwnPropertyDescriptors(source)); } else { ownKeys(Object(source)).forEach(function (key) { Object.defineProperty(target, key, Object.getOwnPropertyDescriptor(source, key)); }); } } return target; } @@ -52557,8 +52550,8 @@ const fs = __webpack_require__(545); const writeFileAtomic = __webpack_require__(549); const sortKeys = __webpack_require__(556); const makeDir = __webpack_require__(558); -const pify = __webpack_require__(560); -const detectIndent = __webpack_require__(561); +const pify = __webpack_require__(559); +const detectIndent = __webpack_require__(560); const init = (fn, filePath, data, options) => { if (!filePath) { @@ -54852,81 +54845,6 @@ module.exports = (input, options) => { "use strict"; -const processFn = (fn, options) => function (...args) { - const P = options.promiseModule; - - return new P((resolve, reject) => { - if (options.multiArgs) { - args.push((...result) => { - if (options.errorFirst) { - if (result[0]) { - reject(result); - } else { - result.shift(); - resolve(result); - } - } else { - resolve(result); - } - }); - } else if (options.errorFirst) { - args.push((error, result) => { - if (error) { - reject(error); - } else { - resolve(result); - } - }); - } else { - args.push(resolve); - } - - fn.apply(this, args); - }); -}; - -module.exports = (input, options) => { - options = Object.assign({ - exclude: [/.+(Sync|Stream)$/], - errorFirst: true, - promiseModule: Promise - }, options); - - const objType = typeof input; - if (!(input !== null && (objType === 'object' || objType === 'function'))) { - throw new TypeError(`Expected \`input\` to be a \`Function\` or \`Object\`, got \`${input === null ? 'null' : objType}\``); - } - - const filter = key => { - const match = pattern => typeof pattern === 'string' ? key === pattern : pattern.test(key); - return options.include ? options.include.some(match) : !options.exclude.some(match); - }; - - let ret; - if (objType === 'function') { - ret = function (...args) { - return options.excludeMain ? input(...args) : processFn(input, options).apply(this, args); - }; - } else { - ret = Object.create(Object.getPrototypeOf(input)); - } - - for (const key in input) { // eslint-disable-line guard-for-in - const property = input[key]; - ret[key] = typeof property === 'function' && filter(key) ? processFn(property, options) : property; - } - - return ret; -}; - - -/***/ }), -/* 561 */ -/***/ (function(module, exports, __webpack_require__) { - -"use strict"; - - // detect either spaces or tabs but not both to properly handle tabs // for indentation and spaces for alignment const INDENT_RE = /^(?:( )+|\t+)/; @@ -55050,7 +54968,7 @@ module.exports = str => { /***/ }), -/* 562 */ +/* 561 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -55059,7 +54977,7 @@ __webpack_require__.r(__webpack_exports__); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "runScriptInPackage", function() { return runScriptInPackage; }); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "runScriptInPackageStreaming", function() { return runScriptInPackageStreaming; }); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "yarnWorkspacesInfo", function() { return yarnWorkspacesInfo; }); -/* harmony import */ var _child_process__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(563); +/* harmony import */ var _child_process__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(562); /* * Licensed to Elasticsearch B.V. under one or more contributor * license agreements. See the NOTICE file distributed with @@ -55129,7 +55047,7 @@ async function yarnWorkspacesInfo(directory) { } /***/ }), -/* 563 */ +/* 562 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -55140,9 +55058,9 @@ __webpack_require__.r(__webpack_exports__); /* harmony import */ var chalk__WEBPACK_IMPORTED_MODULE_0___default = /*#__PURE__*/__webpack_require__.n(chalk__WEBPACK_IMPORTED_MODULE_0__); /* harmony import */ var execa__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(351); /* harmony import */ var execa__WEBPACK_IMPORTED_MODULE_1___default = /*#__PURE__*/__webpack_require__.n(execa__WEBPACK_IMPORTED_MODULE_1__); -/* harmony import */ var log_symbols__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(564); +/* harmony import */ var log_symbols__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(563); /* harmony import */ var log_symbols__WEBPACK_IMPORTED_MODULE_2___default = /*#__PURE__*/__webpack_require__.n(log_symbols__WEBPACK_IMPORTED_MODULE_2__); -/* harmony import */ var strong_log_transformer__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(569); +/* harmony import */ var strong_log_transformer__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(568); /* harmony import */ var strong_log_transformer__WEBPACK_IMPORTED_MODULE_3___default = /*#__PURE__*/__webpack_require__.n(strong_log_transformer__WEBPACK_IMPORTED_MODULE_3__); function ownKeys(object, enumerableOnly) { var keys = Object.keys(object); if (Object.getOwnPropertySymbols) { var symbols = Object.getOwnPropertySymbols(object); if (enumerableOnly) symbols = symbols.filter(function (sym) { return Object.getOwnPropertyDescriptor(object, sym).enumerable; }); keys.push.apply(keys, symbols); } return keys; } @@ -55208,12 +55126,12 @@ function spawnStreaming(command, args, opts, { } /***/ }), -/* 564 */ +/* 563 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -const chalk = __webpack_require__(565); +const chalk = __webpack_require__(564); const isSupported = process.platform !== 'win32' || process.env.CI || process.env.TERM === 'xterm-256color'; @@ -55235,16 +55153,16 @@ module.exports = isSupported ? main : fallbacks; /***/ }), -/* 565 */ +/* 564 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; const escapeStringRegexp = __webpack_require__(3); -const ansiStyles = __webpack_require__(566); -const stdoutColor = __webpack_require__(567).stdout; +const ansiStyles = __webpack_require__(565); +const stdoutColor = __webpack_require__(566).stdout; -const template = __webpack_require__(568); +const template = __webpack_require__(567); const isSimpleWindowsTerm = process.platform === 'win32' && !(process.env.TERM || '').toLowerCase().startsWith('xterm'); @@ -55470,7 +55388,7 @@ module.exports.default = module.exports; // For TypeScript /***/ }), -/* 566 */ +/* 565 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -55643,7 +55561,7 @@ Object.defineProperty(module, 'exports', { /* WEBPACK VAR INJECTION */}.call(this, __webpack_require__(5)(module))) /***/ }), -/* 567 */ +/* 566 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -55785,7 +55703,7 @@ module.exports = { /***/ }), -/* 568 */ +/* 567 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -55920,7 +55838,7 @@ module.exports = (chalk, tmp) => { /***/ }), -/* 569 */ +/* 568 */ /***/ (function(module, exports, __webpack_require__) { // Copyright IBM Corp. 2014,2018. All Rights Reserved. @@ -55928,12 +55846,12 @@ module.exports = (chalk, tmp) => { // This file is licensed under the Apache License 2.0. // License text available at https://opensource.org/licenses/Apache-2.0 -module.exports = __webpack_require__(570); -module.exports.cli = __webpack_require__(574); +module.exports = __webpack_require__(569); +module.exports.cli = __webpack_require__(573); /***/ }), -/* 570 */ +/* 569 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -55948,9 +55866,9 @@ var stream = __webpack_require__(27); var util = __webpack_require__(29); var fs = __webpack_require__(23); -var through = __webpack_require__(571); -var duplexer = __webpack_require__(572); -var StringDecoder = __webpack_require__(573).StringDecoder; +var through = __webpack_require__(570); +var duplexer = __webpack_require__(571); +var StringDecoder = __webpack_require__(572).StringDecoder; module.exports = Logger; @@ -56139,7 +56057,7 @@ function lineMerger(host) { /***/ }), -/* 571 */ +/* 570 */ /***/ (function(module, exports, __webpack_require__) { var Stream = __webpack_require__(27) @@ -56253,7 +56171,7 @@ function through (write, end, opts) { /***/ }), -/* 572 */ +/* 571 */ /***/ (function(module, exports, __webpack_require__) { var Stream = __webpack_require__(27) @@ -56346,13 +56264,13 @@ function duplex(writer, reader) { /***/ }), -/* 573 */ +/* 572 */ /***/ (function(module, exports) { module.exports = require("string_decoder"); /***/ }), -/* 574 */ +/* 573 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -56363,11 +56281,11 @@ module.exports = require("string_decoder"); -var minimist = __webpack_require__(575); +var minimist = __webpack_require__(574); var path = __webpack_require__(16); -var Logger = __webpack_require__(570); -var pkg = __webpack_require__(576); +var Logger = __webpack_require__(569); +var pkg = __webpack_require__(575); module.exports = cli; @@ -56421,7 +56339,7 @@ function usage($0, p) { /***/ }), -/* 575 */ +/* 574 */ /***/ (function(module, exports) { module.exports = function (args, opts) { @@ -56663,13 +56581,13 @@ function isNumber (x) { /***/ }), -/* 576 */ +/* 575 */ /***/ (function(module) { module.exports = JSON.parse("{\"name\":\"strong-log-transformer\",\"version\":\"2.1.0\",\"description\":\"Stream transformer that prefixes lines with timestamps and other things.\",\"author\":\"Ryan Graham \",\"license\":\"Apache-2.0\",\"repository\":{\"type\":\"git\",\"url\":\"git://github.com/strongloop/strong-log-transformer\"},\"keywords\":[\"logging\",\"streams\"],\"bugs\":{\"url\":\"https://github.com/strongloop/strong-log-transformer/issues\"},\"homepage\":\"https://github.com/strongloop/strong-log-transformer\",\"directories\":{\"test\":\"test\"},\"bin\":{\"sl-log-transformer\":\"bin/sl-log-transformer.js\"},\"main\":\"index.js\",\"scripts\":{\"test\":\"tap --100 test/test-*\"},\"dependencies\":{\"duplexer\":\"^0.1.1\",\"minimist\":\"^1.2.0\",\"through\":\"^2.3.4\"},\"devDependencies\":{\"tap\":\"^12.0.1\"},\"engines\":{\"node\":\">=4\"}}"); /***/ }), -/* 577 */ +/* 576 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -56682,7 +56600,7 @@ __webpack_require__.r(__webpack_exports__); /* harmony import */ var path__WEBPACK_IMPORTED_MODULE_1___default = /*#__PURE__*/__webpack_require__.n(path__WEBPACK_IMPORTED_MODULE_1__); /* harmony import */ var util__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(29); /* harmony import */ var util__WEBPACK_IMPORTED_MODULE_2___default = /*#__PURE__*/__webpack_require__.n(util__WEBPACK_IMPORTED_MODULE_2__); -/* harmony import */ var _config__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(578); +/* harmony import */ var _config__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(577); /* harmony import */ var _fs__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(20); /* harmony import */ var _package_json__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(516); /* harmony import */ var _projects__WEBPACK_IMPORTED_MODULE_6__ = __webpack_require__(500); @@ -56777,7 +56695,7 @@ function packagesFromGlobPattern({ } /***/ }), -/* 578 */ +/* 577 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -56847,7 +56765,7 @@ function getProjectPaths({ } /***/ }), -/* 579 */ +/* 578 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -56855,13 +56773,13 @@ __webpack_require__.r(__webpack_exports__); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "getAllChecksums", function() { return getAllChecksums; }); /* harmony import */ var fs__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(23); /* harmony import */ var fs__WEBPACK_IMPORTED_MODULE_0___default = /*#__PURE__*/__webpack_require__.n(fs__WEBPACK_IMPORTED_MODULE_0__); -/* harmony import */ var crypto__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(580); +/* harmony import */ var crypto__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(579); /* harmony import */ var crypto__WEBPACK_IMPORTED_MODULE_1___default = /*#__PURE__*/__webpack_require__.n(crypto__WEBPACK_IMPORTED_MODULE_1__); /* harmony import */ var util__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(29); /* harmony import */ var util__WEBPACK_IMPORTED_MODULE_2___default = /*#__PURE__*/__webpack_require__.n(util__WEBPACK_IMPORTED_MODULE_2__); /* harmony import */ var execa__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(351); /* harmony import */ var execa__WEBPACK_IMPORTED_MODULE_3___default = /*#__PURE__*/__webpack_require__.n(execa__WEBPACK_IMPORTED_MODULE_3__); -/* harmony import */ var _yarn_lock__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(581); +/* harmony import */ var _yarn_lock__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(580); /* * Licensed to Elasticsearch B.V. under one or more contributor * license agreements. See the NOTICE file distributed with @@ -57087,19 +57005,19 @@ async function getAllChecksums(kbn, log) { } /***/ }), -/* 580 */ +/* 579 */ /***/ (function(module, exports) { module.exports = require("crypto"); /***/ }), -/* 581 */ +/* 580 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; __webpack_require__.r(__webpack_exports__); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "readYarnLock", function() { return readYarnLock; }); -/* harmony import */ var _yarnpkg_lockfile__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(582); +/* harmony import */ var _yarnpkg_lockfile__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(581); /* harmony import */ var _yarnpkg_lockfile__WEBPACK_IMPORTED_MODULE_0___default = /*#__PURE__*/__webpack_require__.n(_yarnpkg_lockfile__WEBPACK_IMPORTED_MODULE_0__); /* harmony import */ var _utils_fs__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(20); /* @@ -57143,7 +57061,7 @@ async function readYarnLock(kbn) { } /***/ }), -/* 582 */ +/* 581 */ /***/ (function(module, exports, __webpack_require__) { module.exports = @@ -58702,7 +58620,7 @@ module.exports = invariant; /* 9 */ /***/ (function(module, exports) { -module.exports = __webpack_require__(580); +module.exports = __webpack_require__(579); /***/ }), /* 10 */, @@ -61026,7 +60944,7 @@ function onceStrict (fn) { /* 63 */ /***/ (function(module, exports) { -module.exports = __webpack_require__(583); +module.exports = __webpack_require__(582); /***/ }), /* 64 */, @@ -67421,13 +67339,13 @@ module.exports = process && support(supportLevel); /******/ ]); /***/ }), -/* 583 */ +/* 582 */ /***/ (function(module, exports) { module.exports = require("buffer"); /***/ }), -/* 584 */ +/* 583 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -67524,7 +67442,7 @@ class BootstrapCacheFile { } /***/ }), -/* 585 */ +/* 584 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -67532,9 +67450,9 @@ __webpack_require__.r(__webpack_exports__); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "CleanCommand", function() { return CleanCommand; }); /* harmony import */ var chalk__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(2); /* harmony import */ var chalk__WEBPACK_IMPORTED_MODULE_0___default = /*#__PURE__*/__webpack_require__.n(chalk__WEBPACK_IMPORTED_MODULE_0__); -/* harmony import */ var del__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(586); +/* harmony import */ var del__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(585); /* harmony import */ var del__WEBPACK_IMPORTED_MODULE_1___default = /*#__PURE__*/__webpack_require__.n(del__WEBPACK_IMPORTED_MODULE_1__); -/* harmony import */ var ora__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(674); +/* harmony import */ var ora__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(673); /* harmony import */ var ora__WEBPACK_IMPORTED_MODULE_2___default = /*#__PURE__*/__webpack_require__.n(ora__WEBPACK_IMPORTED_MODULE_2__); /* harmony import */ var path__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(16); /* harmony import */ var path__WEBPACK_IMPORTED_MODULE_3___default = /*#__PURE__*/__webpack_require__.n(path__WEBPACK_IMPORTED_MODULE_3__); @@ -67633,21 +67551,21 @@ const CleanCommand = { }; /***/ }), -/* 586 */ +/* 585 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; const {promisify} = __webpack_require__(29); const path = __webpack_require__(16); -const globby = __webpack_require__(587); -const isGlob = __webpack_require__(604); -const slash = __webpack_require__(665); +const globby = __webpack_require__(586); +const isGlob = __webpack_require__(603); +const slash = __webpack_require__(664); const gracefulFs = __webpack_require__(22); -const isPathCwd = __webpack_require__(667); -const isPathInside = __webpack_require__(668); -const rimraf = __webpack_require__(669); -const pMap = __webpack_require__(670); +const isPathCwd = __webpack_require__(666); +const isPathInside = __webpack_require__(667); +const rimraf = __webpack_require__(668); +const pMap = __webpack_require__(669); const rimrafP = promisify(rimraf); @@ -67761,19 +67679,19 @@ module.exports.sync = (patterns, {force, dryRun, cwd = process.cwd(), ...options /***/ }), -/* 587 */ +/* 586 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; const fs = __webpack_require__(23); -const arrayUnion = __webpack_require__(588); -const merge2 = __webpack_require__(589); -const glob = __webpack_require__(590); -const fastGlob = __webpack_require__(595); -const dirGlob = __webpack_require__(661); -const gitignore = __webpack_require__(663); -const {FilterStream, UniqueStream} = __webpack_require__(666); +const arrayUnion = __webpack_require__(587); +const merge2 = __webpack_require__(588); +const glob = __webpack_require__(589); +const fastGlob = __webpack_require__(594); +const dirGlob = __webpack_require__(660); +const gitignore = __webpack_require__(662); +const {FilterStream, UniqueStream} = __webpack_require__(665); const DEFAULT_FILTER = () => false; @@ -67946,7 +67864,7 @@ module.exports.gitignore = gitignore; /***/ }), -/* 588 */ +/* 587 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -67958,7 +67876,7 @@ module.exports = (...arguments_) => { /***/ }), -/* 589 */ +/* 588 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -68072,7 +67990,7 @@ function pauseStreams (streams, options) { /***/ }), -/* 590 */ +/* 589 */ /***/ (function(module, exports, __webpack_require__) { // Approach: @@ -68121,13 +68039,13 @@ var fs = __webpack_require__(23) var rp = __webpack_require__(502) var minimatch = __webpack_require__(504) var Minimatch = minimatch.Minimatch -var inherits = __webpack_require__(591) +var inherits = __webpack_require__(590) var EE = __webpack_require__(379).EventEmitter var path = __webpack_require__(16) var assert = __webpack_require__(30) var isAbsolute = __webpack_require__(510) -var globSync = __webpack_require__(593) -var common = __webpack_require__(594) +var globSync = __webpack_require__(592) +var common = __webpack_require__(593) var alphasort = common.alphasort var alphasorti = common.alphasorti var setopts = common.setopts @@ -68868,7 +68786,7 @@ Glob.prototype._stat2 = function (f, abs, er, stat, cb) { /***/ }), -/* 591 */ +/* 590 */ /***/ (function(module, exports, __webpack_require__) { try { @@ -68878,12 +68796,12 @@ try { module.exports = util.inherits; } catch (e) { /* istanbul ignore next */ - module.exports = __webpack_require__(592); + module.exports = __webpack_require__(591); } /***/ }), -/* 592 */ +/* 591 */ /***/ (function(module, exports) { if (typeof Object.create === 'function') { @@ -68916,7 +68834,7 @@ if (typeof Object.create === 'function') { /***/ }), -/* 593 */ +/* 592 */ /***/ (function(module, exports, __webpack_require__) { module.exports = globSync @@ -68926,12 +68844,12 @@ var fs = __webpack_require__(23) var rp = __webpack_require__(502) var minimatch = __webpack_require__(504) var Minimatch = minimatch.Minimatch -var Glob = __webpack_require__(590).Glob +var Glob = __webpack_require__(589).Glob var util = __webpack_require__(29) var path = __webpack_require__(16) var assert = __webpack_require__(30) var isAbsolute = __webpack_require__(510) -var common = __webpack_require__(594) +var common = __webpack_require__(593) var alphasort = common.alphasort var alphasorti = common.alphasorti var setopts = common.setopts @@ -69408,7 +69326,7 @@ GlobSync.prototype._makeAbs = function (f) { /***/ }), -/* 594 */ +/* 593 */ /***/ (function(module, exports, __webpack_require__) { exports.alphasort = alphasort @@ -69654,17 +69572,17 @@ function childrenIgnored (self, path) { /***/ }), -/* 595 */ +/* 594 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -const taskManager = __webpack_require__(596); -const async_1 = __webpack_require__(624); -const stream_1 = __webpack_require__(657); -const sync_1 = __webpack_require__(658); -const settings_1 = __webpack_require__(660); -const utils = __webpack_require__(597); +const taskManager = __webpack_require__(595); +const async_1 = __webpack_require__(623); +const stream_1 = __webpack_require__(656); +const sync_1 = __webpack_require__(657); +const settings_1 = __webpack_require__(659); +const utils = __webpack_require__(596); function FastGlob(source, options) { try { assertPatternsInput(source); @@ -69722,13 +69640,13 @@ module.exports = FastGlob; /***/ }), -/* 596 */ +/* 595 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); -const utils = __webpack_require__(597); +const utils = __webpack_require__(596); function generate(patterns, settings) { const positivePatterns = getPositivePatterns(patterns); const negativePatterns = getNegativePatternsAsPositive(patterns, settings.ignore); @@ -69796,28 +69714,28 @@ exports.convertPatternGroupToTask = convertPatternGroupToTask; /***/ }), -/* 597 */ +/* 596 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); -const array = __webpack_require__(598); +const array = __webpack_require__(597); exports.array = array; -const errno = __webpack_require__(599); +const errno = __webpack_require__(598); exports.errno = errno; -const fs = __webpack_require__(600); +const fs = __webpack_require__(599); exports.fs = fs; -const path = __webpack_require__(601); +const path = __webpack_require__(600); exports.path = path; -const pattern = __webpack_require__(602); +const pattern = __webpack_require__(601); exports.pattern = pattern; -const stream = __webpack_require__(623); +const stream = __webpack_require__(622); exports.stream = stream; /***/ }), -/* 598 */ +/* 597 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -69830,7 +69748,7 @@ exports.flatten = flatten; /***/ }), -/* 599 */ +/* 598 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -69843,7 +69761,7 @@ exports.isEnoentCodeError = isEnoentCodeError; /***/ }), -/* 600 */ +/* 599 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -69868,7 +69786,7 @@ exports.createDirentFromStats = createDirentFromStats; /***/ }), -/* 601 */ +/* 600 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -69889,16 +69807,16 @@ exports.makeAbsolute = makeAbsolute; /***/ }), -/* 602 */ +/* 601 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); const path = __webpack_require__(16); -const globParent = __webpack_require__(603); -const isGlob = __webpack_require__(604); -const micromatch = __webpack_require__(606); +const globParent = __webpack_require__(602); +const isGlob = __webpack_require__(603); +const micromatch = __webpack_require__(605); const GLOBSTAR = '**'; function isStaticPattern(pattern) { return !isDynamicPattern(pattern); @@ -69987,13 +69905,13 @@ exports.matchAny = matchAny; /***/ }), -/* 603 */ +/* 602 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var isGlob = __webpack_require__(604); +var isGlob = __webpack_require__(603); var pathPosixDirname = __webpack_require__(16).posix.dirname; var isWin32 = __webpack_require__(11).platform() === 'win32'; @@ -70028,7 +69946,7 @@ module.exports = function globParent(str) { /***/ }), -/* 604 */ +/* 603 */ /***/ (function(module, exports, __webpack_require__) { /*! @@ -70038,7 +69956,7 @@ module.exports = function globParent(str) { * Released under the MIT License. */ -var isExtglob = __webpack_require__(605); +var isExtglob = __webpack_require__(604); var chars = { '{': '}', '(': ')', '[': ']'}; var strictRegex = /\\(.)|(^!|\*|[\].+)]\?|\[[^\\\]]+\]|\{[^\\}]+\}|\(\?[:!=][^\\)]+\)|\([^|]+\|[^\\)]+\))/; var relaxedRegex = /\\(.)|(^!|[*?{}()[\]]|\(\?)/; @@ -70082,7 +70000,7 @@ module.exports = function isGlob(str, options) { /***/ }), -/* 605 */ +/* 604 */ /***/ (function(module, exports) { /*! @@ -70108,16 +70026,16 @@ module.exports = function isExtglob(str) { /***/ }), -/* 606 */ +/* 605 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; const util = __webpack_require__(29); -const braces = __webpack_require__(607); -const picomatch = __webpack_require__(617); -const utils = __webpack_require__(620); +const braces = __webpack_require__(606); +const picomatch = __webpack_require__(616); +const utils = __webpack_require__(619); const isEmptyString = val => typeof val === 'string' && (val === '' || val === './'); /** @@ -70582,16 +70500,16 @@ module.exports = micromatch; /***/ }), -/* 607 */ +/* 606 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -const stringify = __webpack_require__(608); -const compile = __webpack_require__(610); -const expand = __webpack_require__(614); -const parse = __webpack_require__(615); +const stringify = __webpack_require__(607); +const compile = __webpack_require__(609); +const expand = __webpack_require__(613); +const parse = __webpack_require__(614); /** * Expand the given pattern or create a regex-compatible string. @@ -70759,13 +70677,13 @@ module.exports = braces; /***/ }), -/* 608 */ +/* 607 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -const utils = __webpack_require__(609); +const utils = __webpack_require__(608); module.exports = (ast, options = {}) => { let stringify = (node, parent = {}) => { @@ -70798,7 +70716,7 @@ module.exports = (ast, options = {}) => { /***/ }), -/* 609 */ +/* 608 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -70917,14 +70835,14 @@ exports.flatten = (...args) => { /***/ }), -/* 610 */ +/* 609 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -const fill = __webpack_require__(611); -const utils = __webpack_require__(609); +const fill = __webpack_require__(610); +const utils = __webpack_require__(608); const compile = (ast, options = {}) => { let walk = (node, parent = {}) => { @@ -70981,7 +70899,7 @@ module.exports = compile; /***/ }), -/* 611 */ +/* 610 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -70995,7 +70913,7 @@ module.exports = compile; const util = __webpack_require__(29); -const toRegexRange = __webpack_require__(612); +const toRegexRange = __webpack_require__(611); const isObject = val => val !== null && typeof val === 'object' && !Array.isArray(val); @@ -71237,7 +71155,7 @@ module.exports = fill; /***/ }), -/* 612 */ +/* 611 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -71250,7 +71168,7 @@ module.exports = fill; -const isNumber = __webpack_require__(613); +const isNumber = __webpack_require__(612); const toRegexRange = (min, max, options) => { if (isNumber(min) === false) { @@ -71532,7 +71450,7 @@ module.exports = toRegexRange; /***/ }), -/* 613 */ +/* 612 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -71557,15 +71475,15 @@ module.exports = function(num) { /***/ }), -/* 614 */ +/* 613 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -const fill = __webpack_require__(611); -const stringify = __webpack_require__(608); -const utils = __webpack_require__(609); +const fill = __webpack_require__(610); +const stringify = __webpack_require__(607); +const utils = __webpack_require__(608); const append = (queue = '', stash = '', enclose = false) => { let result = []; @@ -71677,13 +71595,13 @@ module.exports = expand; /***/ }), -/* 615 */ +/* 614 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -const stringify = __webpack_require__(608); +const stringify = __webpack_require__(607); /** * Constants @@ -71705,7 +71623,7 @@ const { CHAR_SINGLE_QUOTE, /* ' */ CHAR_NO_BREAK_SPACE, CHAR_ZERO_WIDTH_NOBREAK_SPACE -} = __webpack_require__(616); +} = __webpack_require__(615); /** * parse @@ -72017,7 +71935,7 @@ module.exports = parse; /***/ }), -/* 616 */ +/* 615 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -72081,26 +71999,26 @@ module.exports = { /***/ }), -/* 617 */ +/* 616 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -module.exports = __webpack_require__(618); +module.exports = __webpack_require__(617); /***/ }), -/* 618 */ +/* 617 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; const path = __webpack_require__(16); -const scan = __webpack_require__(619); -const parse = __webpack_require__(622); -const utils = __webpack_require__(620); +const scan = __webpack_require__(618); +const parse = __webpack_require__(621); +const utils = __webpack_require__(619); /** * Creates a matcher function from one or more glob patterns. The @@ -72403,7 +72321,7 @@ picomatch.toRegex = (source, options) => { * @return {Object} */ -picomatch.constants = __webpack_require__(621); +picomatch.constants = __webpack_require__(620); /** * Expose "picomatch" @@ -72413,13 +72331,13 @@ module.exports = picomatch; /***/ }), -/* 619 */ +/* 618 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -const utils = __webpack_require__(620); +const utils = __webpack_require__(619); const { CHAR_ASTERISK, /* * */ @@ -72437,7 +72355,7 @@ const { CHAR_RIGHT_CURLY_BRACE, /* } */ CHAR_RIGHT_PARENTHESES, /* ) */ CHAR_RIGHT_SQUARE_BRACKET /* ] */ -} = __webpack_require__(621); +} = __webpack_require__(620); const isPathSeparator = code => { return code === CHAR_FORWARD_SLASH || code === CHAR_BACKWARD_SLASH; @@ -72639,7 +72557,7 @@ module.exports = (input, options) => { /***/ }), -/* 620 */ +/* 619 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -72651,7 +72569,7 @@ const { REGEX_SPECIAL_CHARS, REGEX_SPECIAL_CHARS_GLOBAL, REGEX_REMOVE_BACKSLASH -} = __webpack_require__(621); +} = __webpack_require__(620); exports.isObject = val => val !== null && typeof val === 'object' && !Array.isArray(val); exports.hasRegexChars = str => REGEX_SPECIAL_CHARS.test(str); @@ -72689,7 +72607,7 @@ exports.escapeLast = (input, char, lastIdx) => { /***/ }), -/* 621 */ +/* 620 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -72875,14 +72793,14 @@ module.exports = { /***/ }), -/* 622 */ +/* 621 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -const utils = __webpack_require__(620); -const constants = __webpack_require__(621); +const utils = __webpack_require__(619); +const constants = __webpack_require__(620); /** * Constants @@ -73893,13 +73811,13 @@ module.exports = parse; /***/ }), -/* 623 */ +/* 622 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); -const merge2 = __webpack_require__(589); +const merge2 = __webpack_require__(588); function merge(streams) { const mergedStream = merge2(streams); streams.forEach((stream) => { @@ -73911,14 +73829,14 @@ exports.merge = merge; /***/ }), -/* 624 */ +/* 623 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); -const stream_1 = __webpack_require__(625); -const provider_1 = __webpack_require__(652); +const stream_1 = __webpack_require__(624); +const provider_1 = __webpack_require__(651); class ProviderAsync extends provider_1.default { constructor() { super(...arguments); @@ -73946,16 +73864,16 @@ exports.default = ProviderAsync; /***/ }), -/* 625 */ +/* 624 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); const stream_1 = __webpack_require__(27); -const fsStat = __webpack_require__(626); -const fsWalk = __webpack_require__(631); -const reader_1 = __webpack_require__(651); +const fsStat = __webpack_require__(625); +const fsWalk = __webpack_require__(630); +const reader_1 = __webpack_require__(650); class ReaderStream extends reader_1.default { constructor() { super(...arguments); @@ -74008,15 +73926,15 @@ exports.default = ReaderStream; /***/ }), -/* 626 */ +/* 625 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); -const async = __webpack_require__(627); -const sync = __webpack_require__(628); -const settings_1 = __webpack_require__(629); +const async = __webpack_require__(626); +const sync = __webpack_require__(627); +const settings_1 = __webpack_require__(628); exports.Settings = settings_1.default; function stat(path, optionsOrSettingsOrCallback, callback) { if (typeof optionsOrSettingsOrCallback === 'function') { @@ -74039,7 +73957,7 @@ function getSettings(settingsOrOptions = {}) { /***/ }), -/* 627 */ +/* 626 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -74077,7 +73995,7 @@ function callSuccessCallback(callback, result) { /***/ }), -/* 628 */ +/* 627 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -74106,13 +74024,13 @@ exports.read = read; /***/ }), -/* 629 */ +/* 628 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); -const fs = __webpack_require__(630); +const fs = __webpack_require__(629); class Settings { constructor(_options = {}) { this._options = _options; @@ -74129,7 +74047,7 @@ exports.default = Settings; /***/ }), -/* 630 */ +/* 629 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -74152,16 +74070,16 @@ exports.createFileSystemAdapter = createFileSystemAdapter; /***/ }), -/* 631 */ +/* 630 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); -const async_1 = __webpack_require__(632); -const stream_1 = __webpack_require__(647); -const sync_1 = __webpack_require__(648); -const settings_1 = __webpack_require__(650); +const async_1 = __webpack_require__(631); +const stream_1 = __webpack_require__(646); +const sync_1 = __webpack_require__(647); +const settings_1 = __webpack_require__(649); exports.Settings = settings_1.default; function walk(dir, optionsOrSettingsOrCallback, callback) { if (typeof optionsOrSettingsOrCallback === 'function') { @@ -74191,13 +74109,13 @@ function getSettings(settingsOrOptions = {}) { /***/ }), -/* 632 */ +/* 631 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); -const async_1 = __webpack_require__(633); +const async_1 = __webpack_require__(632); class AsyncProvider { constructor(_root, _settings) { this._root = _root; @@ -74228,17 +74146,17 @@ function callSuccessCallback(callback, entries) { /***/ }), -/* 633 */ +/* 632 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); const events_1 = __webpack_require__(379); -const fsScandir = __webpack_require__(634); -const fastq = __webpack_require__(643); -const common = __webpack_require__(645); -const reader_1 = __webpack_require__(646); +const fsScandir = __webpack_require__(633); +const fastq = __webpack_require__(642); +const common = __webpack_require__(644); +const reader_1 = __webpack_require__(645); class AsyncReader extends reader_1.default { constructor(_root, _settings) { super(_root, _settings); @@ -74328,15 +74246,15 @@ exports.default = AsyncReader; /***/ }), -/* 634 */ +/* 633 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); -const async = __webpack_require__(635); -const sync = __webpack_require__(640); -const settings_1 = __webpack_require__(641); +const async = __webpack_require__(634); +const sync = __webpack_require__(639); +const settings_1 = __webpack_require__(640); exports.Settings = settings_1.default; function scandir(path, optionsOrSettingsOrCallback, callback) { if (typeof optionsOrSettingsOrCallback === 'function') { @@ -74359,16 +74277,16 @@ function getSettings(settingsOrOptions = {}) { /***/ }), -/* 635 */ +/* 634 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); -const fsStat = __webpack_require__(626); -const rpl = __webpack_require__(636); -const constants_1 = __webpack_require__(637); -const utils = __webpack_require__(638); +const fsStat = __webpack_require__(625); +const rpl = __webpack_require__(635); +const constants_1 = __webpack_require__(636); +const utils = __webpack_require__(637); function read(dir, settings, callback) { if (!settings.stats && constants_1.IS_SUPPORT_READDIR_WITH_FILE_TYPES) { return readdirWithFileTypes(dir, settings, callback); @@ -74457,7 +74375,7 @@ function callSuccessCallback(callback, result) { /***/ }), -/* 636 */ +/* 635 */ /***/ (function(module, exports) { module.exports = runParallel @@ -74511,7 +74429,7 @@ function runParallel (tasks, cb) { /***/ }), -/* 637 */ +/* 636 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -74527,18 +74445,18 @@ exports.IS_SUPPORT_READDIR_WITH_FILE_TYPES = MAJOR_VERSION > 10 || (MAJOR_VERSIO /***/ }), -/* 638 */ +/* 637 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); -const fs = __webpack_require__(639); +const fs = __webpack_require__(638); exports.fs = fs; /***/ }), -/* 639 */ +/* 638 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -74563,15 +74481,15 @@ exports.createDirentFromStats = createDirentFromStats; /***/ }), -/* 640 */ +/* 639 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); -const fsStat = __webpack_require__(626); -const constants_1 = __webpack_require__(637); -const utils = __webpack_require__(638); +const fsStat = __webpack_require__(625); +const constants_1 = __webpack_require__(636); +const utils = __webpack_require__(637); function read(dir, settings) { if (!settings.stats && constants_1.IS_SUPPORT_READDIR_WITH_FILE_TYPES) { return readdirWithFileTypes(dir, settings); @@ -74622,15 +74540,15 @@ exports.readdir = readdir; /***/ }), -/* 641 */ +/* 640 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); const path = __webpack_require__(16); -const fsStat = __webpack_require__(626); -const fs = __webpack_require__(642); +const fsStat = __webpack_require__(625); +const fs = __webpack_require__(641); class Settings { constructor(_options = {}) { this._options = _options; @@ -74653,7 +74571,7 @@ exports.default = Settings; /***/ }), -/* 642 */ +/* 641 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -74678,13 +74596,13 @@ exports.createFileSystemAdapter = createFileSystemAdapter; /***/ }), -/* 643 */ +/* 642 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var reusify = __webpack_require__(644) +var reusify = __webpack_require__(643) function fastqueue (context, worker, concurrency) { if (typeof context === 'function') { @@ -74858,7 +74776,7 @@ module.exports = fastqueue /***/ }), -/* 644 */ +/* 643 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -74898,7 +74816,7 @@ module.exports = reusify /***/ }), -/* 645 */ +/* 644 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -74929,13 +74847,13 @@ exports.joinPathSegments = joinPathSegments; /***/ }), -/* 646 */ +/* 645 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); -const common = __webpack_require__(645); +const common = __webpack_require__(644); class Reader { constructor(_root, _settings) { this._root = _root; @@ -74947,14 +74865,14 @@ exports.default = Reader; /***/ }), -/* 647 */ +/* 646 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); const stream_1 = __webpack_require__(27); -const async_1 = __webpack_require__(633); +const async_1 = __webpack_require__(632); class StreamProvider { constructor(_root, _settings) { this._root = _root; @@ -74984,13 +74902,13 @@ exports.default = StreamProvider; /***/ }), -/* 648 */ +/* 647 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); -const sync_1 = __webpack_require__(649); +const sync_1 = __webpack_require__(648); class SyncProvider { constructor(_root, _settings) { this._root = _root; @@ -75005,15 +74923,15 @@ exports.default = SyncProvider; /***/ }), -/* 649 */ +/* 648 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); -const fsScandir = __webpack_require__(634); -const common = __webpack_require__(645); -const reader_1 = __webpack_require__(646); +const fsScandir = __webpack_require__(633); +const common = __webpack_require__(644); +const reader_1 = __webpack_require__(645); class SyncReader extends reader_1.default { constructor() { super(...arguments); @@ -75071,14 +74989,14 @@ exports.default = SyncReader; /***/ }), -/* 650 */ +/* 649 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); const path = __webpack_require__(16); -const fsScandir = __webpack_require__(634); +const fsScandir = __webpack_require__(633); class Settings { constructor(_options = {}) { this._options = _options; @@ -75104,15 +75022,15 @@ exports.default = Settings; /***/ }), -/* 651 */ +/* 650 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); const path = __webpack_require__(16); -const fsStat = __webpack_require__(626); -const utils = __webpack_require__(597); +const fsStat = __webpack_require__(625); +const utils = __webpack_require__(596); class Reader { constructor(_settings) { this._settings = _settings; @@ -75144,17 +75062,17 @@ exports.default = Reader; /***/ }), -/* 652 */ +/* 651 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); const path = __webpack_require__(16); -const deep_1 = __webpack_require__(653); -const entry_1 = __webpack_require__(654); -const error_1 = __webpack_require__(655); -const entry_2 = __webpack_require__(656); +const deep_1 = __webpack_require__(652); +const entry_1 = __webpack_require__(653); +const error_1 = __webpack_require__(654); +const entry_2 = __webpack_require__(655); class Provider { constructor(_settings) { this._settings = _settings; @@ -75199,13 +75117,13 @@ exports.default = Provider; /***/ }), -/* 653 */ +/* 652 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); -const utils = __webpack_require__(597); +const utils = __webpack_require__(596); class DeepFilter { constructor(_settings, _micromatchOptions) { this._settings = _settings; @@ -75265,13 +75183,13 @@ exports.default = DeepFilter; /***/ }), -/* 654 */ +/* 653 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); -const utils = __webpack_require__(597); +const utils = __webpack_require__(596); class EntryFilter { constructor(_settings, _micromatchOptions) { this._settings = _settings; @@ -75326,13 +75244,13 @@ exports.default = EntryFilter; /***/ }), -/* 655 */ +/* 654 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); -const utils = __webpack_require__(597); +const utils = __webpack_require__(596); class ErrorFilter { constructor(_settings) { this._settings = _settings; @@ -75348,13 +75266,13 @@ exports.default = ErrorFilter; /***/ }), -/* 656 */ +/* 655 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); -const utils = __webpack_require__(597); +const utils = __webpack_require__(596); class EntryTransformer { constructor(_settings) { this._settings = _settings; @@ -75381,15 +75299,15 @@ exports.default = EntryTransformer; /***/ }), -/* 657 */ +/* 656 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); const stream_1 = __webpack_require__(27); -const stream_2 = __webpack_require__(625); -const provider_1 = __webpack_require__(652); +const stream_2 = __webpack_require__(624); +const provider_1 = __webpack_require__(651); class ProviderStream extends provider_1.default { constructor() { super(...arguments); @@ -75417,14 +75335,14 @@ exports.default = ProviderStream; /***/ }), -/* 658 */ +/* 657 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); -const sync_1 = __webpack_require__(659); -const provider_1 = __webpack_require__(652); +const sync_1 = __webpack_require__(658); +const provider_1 = __webpack_require__(651); class ProviderSync extends provider_1.default { constructor() { super(...arguments); @@ -75447,15 +75365,15 @@ exports.default = ProviderSync; /***/ }), -/* 659 */ +/* 658 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); -const fsStat = __webpack_require__(626); -const fsWalk = __webpack_require__(631); -const reader_1 = __webpack_require__(651); +const fsStat = __webpack_require__(625); +const fsWalk = __webpack_require__(630); +const reader_1 = __webpack_require__(650); class ReaderSync extends reader_1.default { constructor() { super(...arguments); @@ -75497,7 +75415,7 @@ exports.default = ReaderSync; /***/ }), -/* 660 */ +/* 659 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -75557,13 +75475,13 @@ exports.default = Settings; /***/ }), -/* 661 */ +/* 660 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; const path = __webpack_require__(16); -const pathType = __webpack_require__(662); +const pathType = __webpack_require__(661); const getExtensions = extensions => extensions.length > 1 ? `{${extensions.join(',')}}` : extensions[0]; @@ -75639,7 +75557,7 @@ module.exports.sync = (input, options) => { /***/ }), -/* 662 */ +/* 661 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -75689,7 +75607,7 @@ exports.isSymlinkSync = isTypeSync.bind(null, 'lstatSync', 'isSymbolicLink'); /***/ }), -/* 663 */ +/* 662 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -75697,9 +75615,9 @@ exports.isSymlinkSync = isTypeSync.bind(null, 'lstatSync', 'isSymbolicLink'); const {promisify} = __webpack_require__(29); const fs = __webpack_require__(23); const path = __webpack_require__(16); -const fastGlob = __webpack_require__(595); -const gitIgnore = __webpack_require__(664); -const slash = __webpack_require__(665); +const fastGlob = __webpack_require__(594); +const gitIgnore = __webpack_require__(663); +const slash = __webpack_require__(664); const DEFAULT_IGNORE = [ '**/node_modules/**', @@ -75813,7 +75731,7 @@ module.exports.sync = options => { /***/ }), -/* 664 */ +/* 663 */ /***/ (function(module, exports) { // A simple implementation of make-array @@ -76404,7 +76322,7 @@ if ( /***/ }), -/* 665 */ +/* 664 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -76422,7 +76340,7 @@ module.exports = path => { /***/ }), -/* 666 */ +/* 665 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -76475,7 +76393,7 @@ module.exports = { /***/ }), -/* 667 */ +/* 666 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -76497,7 +76415,7 @@ module.exports = path_ => { /***/ }), -/* 668 */ +/* 667 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -76525,7 +76443,7 @@ module.exports = (childPath, parentPath) => { /***/ }), -/* 669 */ +/* 668 */ /***/ (function(module, exports, __webpack_require__) { const assert = __webpack_require__(30) @@ -76533,7 +76451,7 @@ const path = __webpack_require__(16) const fs = __webpack_require__(23) let glob = undefined try { - glob = __webpack_require__(590) + glob = __webpack_require__(589) } catch (_err) { // treat glob as optional. } @@ -76899,12 +76817,12 @@ rimraf.sync = rimrafSync /***/ }), -/* 670 */ +/* 669 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -const AggregateError = __webpack_require__(671); +const AggregateError = __webpack_require__(670); module.exports = async ( iterable, @@ -76987,13 +76905,13 @@ module.exports = async ( /***/ }), -/* 671 */ +/* 670 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -const indentString = __webpack_require__(672); -const cleanStack = __webpack_require__(673); +const indentString = __webpack_require__(671); +const cleanStack = __webpack_require__(672); const cleanInternalStack = stack => stack.replace(/\s+at .*aggregate-error\/index.js:\d+:\d+\)?/g, ''); @@ -77041,7 +76959,7 @@ module.exports = AggregateError; /***/ }), -/* 672 */ +/* 671 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -77083,7 +77001,7 @@ module.exports = (string, count = 1, options) => { /***/ }), -/* 673 */ +/* 672 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -77130,15 +77048,15 @@ module.exports = (stack, options) => { /***/ }), -/* 674 */ +/* 673 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -const chalk = __webpack_require__(675); -const cliCursor = __webpack_require__(679); -const cliSpinners = __webpack_require__(683); -const logSymbols = __webpack_require__(564); +const chalk = __webpack_require__(674); +const cliCursor = __webpack_require__(678); +const cliSpinners = __webpack_require__(682); +const logSymbols = __webpack_require__(563); class Ora { constructor(options) { @@ -77285,16 +77203,16 @@ module.exports.promise = (action, options) => { /***/ }), -/* 675 */ +/* 674 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; const escapeStringRegexp = __webpack_require__(3); -const ansiStyles = __webpack_require__(676); -const stdoutColor = __webpack_require__(677).stdout; +const ansiStyles = __webpack_require__(675); +const stdoutColor = __webpack_require__(676).stdout; -const template = __webpack_require__(678); +const template = __webpack_require__(677); const isSimpleWindowsTerm = process.platform === 'win32' && !(process.env.TERM || '').toLowerCase().startsWith('xterm'); @@ -77520,7 +77438,7 @@ module.exports.default = module.exports; // For TypeScript /***/ }), -/* 676 */ +/* 675 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -77693,7 +77611,7 @@ Object.defineProperty(module, 'exports', { /* WEBPACK VAR INJECTION */}.call(this, __webpack_require__(5)(module))) /***/ }), -/* 677 */ +/* 676 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -77835,7 +77753,7 @@ module.exports = { /***/ }), -/* 678 */ +/* 677 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -77970,12 +77888,12 @@ module.exports = (chalk, tmp) => { /***/ }), -/* 679 */ +/* 678 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -const restoreCursor = __webpack_require__(680); +const restoreCursor = __webpack_require__(679); let hidden = false; @@ -78016,12 +77934,12 @@ exports.toggle = (force, stream) => { /***/ }), -/* 680 */ +/* 679 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -const onetime = __webpack_require__(681); +const onetime = __webpack_require__(680); const signalExit = __webpack_require__(377); module.exports = onetime(() => { @@ -78032,12 +77950,12 @@ module.exports = onetime(() => { /***/ }), -/* 681 */ +/* 680 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -const mimicFn = __webpack_require__(682); +const mimicFn = __webpack_require__(681); module.exports = (fn, opts) => { // TODO: Remove this in v3 @@ -78078,7 +77996,7 @@ module.exports = (fn, opts) => { /***/ }), -/* 682 */ +/* 681 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -78094,22 +78012,22 @@ module.exports = (to, from) => { /***/ }), -/* 683 */ +/* 682 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -module.exports = __webpack_require__(684); +module.exports = __webpack_require__(683); /***/ }), -/* 684 */ +/* 683 */ /***/ (function(module) { module.exports = JSON.parse("{\"dots\":{\"interval\":80,\"frames\":[\"⠋\",\"⠙\",\"⠹\",\"⠸\",\"⠼\",\"⠴\",\"⠦\",\"⠧\",\"⠇\",\"⠏\"]},\"dots2\":{\"interval\":80,\"frames\":[\"⣾\",\"⣽\",\"⣻\",\"⢿\",\"⡿\",\"⣟\",\"⣯\",\"⣷\"]},\"dots3\":{\"interval\":80,\"frames\":[\"⠋\",\"⠙\",\"⠚\",\"⠞\",\"⠖\",\"⠦\",\"⠴\",\"⠲\",\"⠳\",\"⠓\"]},\"dots4\":{\"interval\":80,\"frames\":[\"⠄\",\"⠆\",\"⠇\",\"⠋\",\"⠙\",\"⠸\",\"⠰\",\"⠠\",\"⠰\",\"⠸\",\"⠙\",\"⠋\",\"⠇\",\"⠆\"]},\"dots5\":{\"interval\":80,\"frames\":[\"⠋\",\"⠙\",\"⠚\",\"⠒\",\"⠂\",\"⠂\",\"⠒\",\"⠲\",\"⠴\",\"⠦\",\"⠖\",\"⠒\",\"⠐\",\"⠐\",\"⠒\",\"⠓\",\"⠋\"]},\"dots6\":{\"interval\":80,\"frames\":[\"⠁\",\"⠉\",\"⠙\",\"⠚\",\"⠒\",\"⠂\",\"⠂\",\"⠒\",\"⠲\",\"⠴\",\"⠤\",\"⠄\",\"⠄\",\"⠤\",\"⠴\",\"⠲\",\"⠒\",\"⠂\",\"⠂\",\"⠒\",\"⠚\",\"⠙\",\"⠉\",\"⠁\"]},\"dots7\":{\"interval\":80,\"frames\":[\"⠈\",\"⠉\",\"⠋\",\"⠓\",\"⠒\",\"⠐\",\"⠐\",\"⠒\",\"⠖\",\"⠦\",\"⠤\",\"⠠\",\"⠠\",\"⠤\",\"⠦\",\"⠖\",\"⠒\",\"⠐\",\"⠐\",\"⠒\",\"⠓\",\"⠋\",\"⠉\",\"⠈\"]},\"dots8\":{\"interval\":80,\"frames\":[\"⠁\",\"⠁\",\"⠉\",\"⠙\",\"⠚\",\"⠒\",\"⠂\",\"⠂\",\"⠒\",\"⠲\",\"⠴\",\"⠤\",\"⠄\",\"⠄\",\"⠤\",\"⠠\",\"⠠\",\"⠤\",\"⠦\",\"⠖\",\"⠒\",\"⠐\",\"⠐\",\"⠒\",\"⠓\",\"⠋\",\"⠉\",\"⠈\",\"⠈\"]},\"dots9\":{\"interval\":80,\"frames\":[\"⢹\",\"⢺\",\"⢼\",\"⣸\",\"⣇\",\"⡧\",\"⡗\",\"⡏\"]},\"dots10\":{\"interval\":80,\"frames\":[\"⢄\",\"⢂\",\"⢁\",\"⡁\",\"⡈\",\"⡐\",\"⡠\"]},\"dots11\":{\"interval\":100,\"frames\":[\"⠁\",\"⠂\",\"⠄\",\"⡀\",\"⢀\",\"⠠\",\"⠐\",\"⠈\"]},\"dots12\":{\"interval\":80,\"frames\":[\"⢀⠀\",\"⡀⠀\",\"⠄⠀\",\"⢂⠀\",\"⡂⠀\",\"⠅⠀\",\"⢃⠀\",\"⡃⠀\",\"⠍⠀\",\"⢋⠀\",\"⡋⠀\",\"⠍⠁\",\"⢋⠁\",\"⡋⠁\",\"⠍⠉\",\"⠋⠉\",\"⠋⠉\",\"⠉⠙\",\"⠉⠙\",\"⠉⠩\",\"⠈⢙\",\"⠈⡙\",\"⢈⠩\",\"⡀⢙\",\"⠄⡙\",\"⢂⠩\",\"⡂⢘\",\"⠅⡘\",\"⢃⠨\",\"⡃⢐\",\"⠍⡐\",\"⢋⠠\",\"⡋⢀\",\"⠍⡁\",\"⢋⠁\",\"⡋⠁\",\"⠍⠉\",\"⠋⠉\",\"⠋⠉\",\"⠉⠙\",\"⠉⠙\",\"⠉⠩\",\"⠈⢙\",\"⠈⡙\",\"⠈⠩\",\"⠀⢙\",\"⠀⡙\",\"⠀⠩\",\"⠀⢘\",\"⠀⡘\",\"⠀⠨\",\"⠀⢐\",\"⠀⡐\",\"⠀⠠\",\"⠀⢀\",\"⠀⡀\"]},\"line\":{\"interval\":130,\"frames\":[\"-\",\"\\\\\",\"|\",\"/\"]},\"line2\":{\"interval\":100,\"frames\":[\"⠂\",\"-\",\"–\",\"—\",\"–\",\"-\"]},\"pipe\":{\"interval\":100,\"frames\":[\"┤\",\"┘\",\"┴\",\"└\",\"├\",\"┌\",\"┬\",\"┐\"]},\"simpleDots\":{\"interval\":400,\"frames\":[\". \",\".. \",\"...\",\" \"]},\"simpleDotsScrolling\":{\"interval\":200,\"frames\":[\". \",\".. \",\"...\",\" ..\",\" .\",\" \"]},\"star\":{\"interval\":70,\"frames\":[\"✶\",\"✸\",\"✹\",\"✺\",\"✹\",\"✷\"]},\"star2\":{\"interval\":80,\"frames\":[\"+\",\"x\",\"*\"]},\"flip\":{\"interval\":70,\"frames\":[\"_\",\"_\",\"_\",\"-\",\"`\",\"`\",\"'\",\"´\",\"-\",\"_\",\"_\",\"_\"]},\"hamburger\":{\"interval\":100,\"frames\":[\"☱\",\"☲\",\"☴\"]},\"growVertical\":{\"interval\":120,\"frames\":[\"▁\",\"▃\",\"▄\",\"▅\",\"▆\",\"▇\",\"▆\",\"▅\",\"▄\",\"▃\"]},\"growHorizontal\":{\"interval\":120,\"frames\":[\"▏\",\"▎\",\"▍\",\"▌\",\"▋\",\"▊\",\"▉\",\"▊\",\"▋\",\"▌\",\"▍\",\"▎\"]},\"balloon\":{\"interval\":140,\"frames\":[\" \",\".\",\"o\",\"O\",\"@\",\"*\",\" \"]},\"balloon2\":{\"interval\":120,\"frames\":[\".\",\"o\",\"O\",\"°\",\"O\",\"o\",\".\"]},\"noise\":{\"interval\":100,\"frames\":[\"▓\",\"▒\",\"░\"]},\"bounce\":{\"interval\":120,\"frames\":[\"⠁\",\"⠂\",\"⠄\",\"⠂\"]},\"boxBounce\":{\"interval\":120,\"frames\":[\"▖\",\"▘\",\"▝\",\"▗\"]},\"boxBounce2\":{\"interval\":100,\"frames\":[\"▌\",\"▀\",\"▐\",\"▄\"]},\"triangle\":{\"interval\":50,\"frames\":[\"◢\",\"◣\",\"◤\",\"◥\"]},\"arc\":{\"interval\":100,\"frames\":[\"◜\",\"◠\",\"◝\",\"◞\",\"◡\",\"◟\"]},\"circle\":{\"interval\":120,\"frames\":[\"◡\",\"⊙\",\"◠\"]},\"squareCorners\":{\"interval\":180,\"frames\":[\"◰\",\"◳\",\"◲\",\"◱\"]},\"circleQuarters\":{\"interval\":120,\"frames\":[\"◴\",\"◷\",\"◶\",\"◵\"]},\"circleHalves\":{\"interval\":50,\"frames\":[\"◐\",\"◓\",\"◑\",\"◒\"]},\"squish\":{\"interval\":100,\"frames\":[\"╫\",\"╪\"]},\"toggle\":{\"interval\":250,\"frames\":[\"⊶\",\"⊷\"]},\"toggle2\":{\"interval\":80,\"frames\":[\"▫\",\"▪\"]},\"toggle3\":{\"interval\":120,\"frames\":[\"□\",\"■\"]},\"toggle4\":{\"interval\":100,\"frames\":[\"■\",\"□\",\"▪\",\"▫\"]},\"toggle5\":{\"interval\":100,\"frames\":[\"▮\",\"▯\"]},\"toggle6\":{\"interval\":300,\"frames\":[\"ဝ\",\"၀\"]},\"toggle7\":{\"interval\":80,\"frames\":[\"⦾\",\"⦿\"]},\"toggle8\":{\"interval\":100,\"frames\":[\"◍\",\"◌\"]},\"toggle9\":{\"interval\":100,\"frames\":[\"◉\",\"◎\"]},\"toggle10\":{\"interval\":100,\"frames\":[\"㊂\",\"㊀\",\"㊁\"]},\"toggle11\":{\"interval\":50,\"frames\":[\"⧇\",\"⧆\"]},\"toggle12\":{\"interval\":120,\"frames\":[\"☗\",\"☖\"]},\"toggle13\":{\"interval\":80,\"frames\":[\"=\",\"*\",\"-\"]},\"arrow\":{\"interval\":100,\"frames\":[\"←\",\"↖\",\"↑\",\"↗\",\"→\",\"↘\",\"↓\",\"↙\"]},\"arrow2\":{\"interval\":80,\"frames\":[\"⬆️ \",\"↗️ \",\"➡️ \",\"↘️ \",\"⬇️ \",\"↙️ \",\"⬅️ \",\"↖️ \"]},\"arrow3\":{\"interval\":120,\"frames\":[\"▹▹▹▹▹\",\"▸▹▹▹▹\",\"▹▸▹▹▹\",\"▹▹▸▹▹\",\"▹▹▹▸▹\",\"▹▹▹▹▸\"]},\"bouncingBar\":{\"interval\":80,\"frames\":[\"[ ]\",\"[= ]\",\"[== ]\",\"[=== ]\",\"[ ===]\",\"[ ==]\",\"[ =]\",\"[ ]\",\"[ =]\",\"[ ==]\",\"[ ===]\",\"[====]\",\"[=== ]\",\"[== ]\",\"[= ]\"]},\"bouncingBall\":{\"interval\":80,\"frames\":[\"( ● )\",\"( ● )\",\"( ● )\",\"( ● )\",\"( ●)\",\"( ● )\",\"( ● )\",\"( ● )\",\"( ● )\",\"(● )\"]},\"smiley\":{\"interval\":200,\"frames\":[\"😄 \",\"😝 \"]},\"monkey\":{\"interval\":300,\"frames\":[\"🙈 \",\"🙈 \",\"🙉 \",\"🙊 \"]},\"hearts\":{\"interval\":100,\"frames\":[\"💛 \",\"💙 \",\"💜 \",\"💚 \",\"❤️ \"]},\"clock\":{\"interval\":100,\"frames\":[\"🕐 \",\"🕑 \",\"🕒 \",\"🕓 \",\"🕔 \",\"🕕 \",\"🕖 \",\"🕗 \",\"🕘 \",\"🕙 \",\"🕚 \"]},\"earth\":{\"interval\":180,\"frames\":[\"🌍 \",\"🌎 \",\"🌏 \"]},\"moon\":{\"interval\":80,\"frames\":[\"🌑 \",\"🌒 \",\"🌓 \",\"🌔 \",\"🌕 \",\"🌖 \",\"🌗 \",\"🌘 \"]},\"runner\":{\"interval\":140,\"frames\":[\"🚶 \",\"🏃 \"]},\"pong\":{\"interval\":80,\"frames\":[\"▐⠂ ▌\",\"▐⠈ ▌\",\"▐ ⠂ ▌\",\"▐ ⠠ ▌\",\"▐ ⡀ ▌\",\"▐ ⠠ ▌\",\"▐ ⠂ ▌\",\"▐ ⠈ ▌\",\"▐ ⠂ ▌\",\"▐ ⠠ ▌\",\"▐ ⡀ ▌\",\"▐ ⠠ ▌\",\"▐ ⠂ ▌\",\"▐ ⠈ ▌\",\"▐ ⠂▌\",\"▐ ⠠▌\",\"▐ ⡀▌\",\"▐ ⠠ ▌\",\"▐ ⠂ ▌\",\"▐ ⠈ ▌\",\"▐ ⠂ ▌\",\"▐ ⠠ ▌\",\"▐ ⡀ ▌\",\"▐ ⠠ ▌\",\"▐ ⠂ ▌\",\"▐ ⠈ ▌\",\"▐ ⠂ ▌\",\"▐ ⠠ ▌\",\"▐ ⡀ ▌\",\"▐⠠ ▌\"]},\"shark\":{\"interval\":120,\"frames\":[\"▐|\\\\____________▌\",\"▐_|\\\\___________▌\",\"▐__|\\\\__________▌\",\"▐___|\\\\_________▌\",\"▐____|\\\\________▌\",\"▐_____|\\\\_______▌\",\"▐______|\\\\______▌\",\"▐_______|\\\\_____▌\",\"▐________|\\\\____▌\",\"▐_________|\\\\___▌\",\"▐__________|\\\\__▌\",\"▐___________|\\\\_▌\",\"▐____________|\\\\▌\",\"▐____________/|▌\",\"▐___________/|_▌\",\"▐__________/|__▌\",\"▐_________/|___▌\",\"▐________/|____▌\",\"▐_______/|_____▌\",\"▐______/|______▌\",\"▐_____/|_______▌\",\"▐____/|________▌\",\"▐___/|_________▌\",\"▐__/|__________▌\",\"▐_/|___________▌\",\"▐/|____________▌\"]},\"dqpb\":{\"interval\":100,\"frames\":[\"d\",\"q\",\"p\",\"b\"]},\"weather\":{\"interval\":100,\"frames\":[\"☀️ \",\"☀️ \",\"☀️ \",\"🌤 \",\"⛅️ \",\"🌥 \",\"☁️ \",\"🌧 \",\"🌨 \",\"🌧 \",\"🌨 \",\"🌧 \",\"🌨 \",\"⛈ \",\"🌨 \",\"🌧 \",\"🌨 \",\"☁️ \",\"🌥 \",\"⛅️ \",\"🌤 \",\"☀️ \",\"☀️ \"]},\"christmas\":{\"interval\":400,\"frames\":[\"🌲\",\"🎄\"]}}"); /***/ }), -/* 685 */ +/* 684 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -78169,7 +78087,7 @@ const RunCommand = { }; /***/ }), -/* 686 */ +/* 685 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -78180,7 +78098,7 @@ __webpack_require__.r(__webpack_exports__); /* harmony import */ var _utils_log__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(34); /* harmony import */ var _utils_parallelize__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(499); /* harmony import */ var _utils_projects__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(500); -/* harmony import */ var _utils_watch__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(687); +/* harmony import */ var _utils_watch__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(686); /* * Licensed to Elasticsearch B.V. under one or more contributor * license agreements. See the NOTICE file distributed with @@ -78264,7 +78182,7 @@ const WatchCommand = { }; /***/ }), -/* 687 */ +/* 686 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -78338,7 +78256,7 @@ function waitUntilWatchIsReady(stream, opts = {}) { } /***/ }), -/* 688 */ +/* 687 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -78346,15 +78264,15 @@ __webpack_require__.r(__webpack_exports__); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "runCommand", function() { return runCommand; }); /* harmony import */ var chalk__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(2); /* harmony import */ var chalk__WEBPACK_IMPORTED_MODULE_0___default = /*#__PURE__*/__webpack_require__.n(chalk__WEBPACK_IMPORTED_MODULE_0__); -/* harmony import */ var indent_string__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(689); +/* harmony import */ var indent_string__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(688); /* harmony import */ var indent_string__WEBPACK_IMPORTED_MODULE_1___default = /*#__PURE__*/__webpack_require__.n(indent_string__WEBPACK_IMPORTED_MODULE_1__); -/* harmony import */ var wrap_ansi__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(690); +/* harmony import */ var wrap_ansi__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(689); /* harmony import */ var wrap_ansi__WEBPACK_IMPORTED_MODULE_2___default = /*#__PURE__*/__webpack_require__.n(wrap_ansi__WEBPACK_IMPORTED_MODULE_2__); /* harmony import */ var _utils_errors__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(514); /* harmony import */ var _utils_log__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(34); /* harmony import */ var _utils_projects__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(500); -/* harmony import */ var _utils_projects_tree__WEBPACK_IMPORTED_MODULE_6__ = __webpack_require__(697); -/* harmony import */ var _utils_kibana__WEBPACK_IMPORTED_MODULE_7__ = __webpack_require__(698); +/* harmony import */ var _utils_projects_tree__WEBPACK_IMPORTED_MODULE_6__ = __webpack_require__(696); +/* harmony import */ var _utils_kibana__WEBPACK_IMPORTED_MODULE_7__ = __webpack_require__(697); function ownKeys(object, enumerableOnly) { var keys = Object.keys(object); if (Object.getOwnPropertySymbols) { var symbols = Object.getOwnPropertySymbols(object); if (enumerableOnly) symbols = symbols.filter(function (sym) { return Object.getOwnPropertyDescriptor(object, sym).enumerable; }); keys.push.apply(keys, symbols); } return keys; } function _objectSpread(target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i] != null ? arguments[i] : {}; if (i % 2) { ownKeys(Object(source), true).forEach(function (key) { _defineProperty(target, key, source[key]); }); } else if (Object.getOwnPropertyDescriptors) { Object.defineProperties(target, Object.getOwnPropertyDescriptors(source)); } else { ownKeys(Object(source)).forEach(function (key) { Object.defineProperty(target, key, Object.getOwnPropertyDescriptor(source, key)); }); } } return target; } @@ -78442,7 +78360,7 @@ function toArray(value) { } /***/ }), -/* 689 */ +/* 688 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -78476,13 +78394,13 @@ module.exports = (str, count, opts) => { /***/ }), -/* 690 */ +/* 689 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -const stringWidth = __webpack_require__(691); -const stripAnsi = __webpack_require__(695); +const stringWidth = __webpack_require__(690); +const stripAnsi = __webpack_require__(694); const ESCAPES = new Set([ '\u001B', @@ -78676,13 +78594,13 @@ module.exports = (str, cols, opts) => { /***/ }), -/* 691 */ +/* 690 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -const stripAnsi = __webpack_require__(692); -const isFullwidthCodePoint = __webpack_require__(694); +const stripAnsi = __webpack_require__(691); +const isFullwidthCodePoint = __webpack_require__(693); module.exports = str => { if (typeof str !== 'string' || str.length === 0) { @@ -78719,18 +78637,18 @@ module.exports = str => { /***/ }), -/* 692 */ +/* 691 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -const ansiRegex = __webpack_require__(693); +const ansiRegex = __webpack_require__(692); module.exports = input => typeof input === 'string' ? input.replace(ansiRegex(), '') : input; /***/ }), -/* 693 */ +/* 692 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -78747,7 +78665,7 @@ module.exports = () => { /***/ }), -/* 694 */ +/* 693 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -78800,18 +78718,18 @@ module.exports = x => { /***/ }), -/* 695 */ +/* 694 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -const ansiRegex = __webpack_require__(696); +const ansiRegex = __webpack_require__(695); module.exports = input => typeof input === 'string' ? input.replace(ansiRegex(), '') : input; /***/ }), -/* 696 */ +/* 695 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -78828,7 +78746,7 @@ module.exports = () => { /***/ }), -/* 697 */ +/* 696 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -78981,7 +78899,7 @@ function addProjectToTree(tree, pathParts, project) { } /***/ }), -/* 698 */ +/* 697 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -78989,12 +78907,12 @@ __webpack_require__.r(__webpack_exports__); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "Kibana", function() { return Kibana; }); /* harmony import */ var path__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(16); /* harmony import */ var path__WEBPACK_IMPORTED_MODULE_0___default = /*#__PURE__*/__webpack_require__.n(path__WEBPACK_IMPORTED_MODULE_0__); -/* harmony import */ var multimatch__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(699); +/* harmony import */ var multimatch__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(698); /* harmony import */ var multimatch__WEBPACK_IMPORTED_MODULE_1___default = /*#__PURE__*/__webpack_require__.n(multimatch__WEBPACK_IMPORTED_MODULE_1__); -/* harmony import */ var is_path_inside__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(703); +/* harmony import */ var is_path_inside__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(702); /* harmony import */ var is_path_inside__WEBPACK_IMPORTED_MODULE_2___default = /*#__PURE__*/__webpack_require__.n(is_path_inside__WEBPACK_IMPORTED_MODULE_2__); /* harmony import */ var _projects__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(500); -/* harmony import */ var _config__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(578); +/* harmony import */ var _config__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(577); function ownKeys(object, enumerableOnly) { var keys = Object.keys(object); if (Object.getOwnPropertySymbols) { var symbols = Object.getOwnPropertySymbols(object); if (enumerableOnly) symbols = symbols.filter(function (sym) { return Object.getOwnPropertyDescriptor(object, sym).enumerable; }); keys.push.apply(keys, symbols); } return keys; } function _objectSpread(target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i] != null ? arguments[i] : {}; if (i % 2) { ownKeys(Object(source), true).forEach(function (key) { _defineProperty(target, key, source[key]); }); } else if (Object.getOwnPropertyDescriptors) { Object.defineProperties(target, Object.getOwnPropertyDescriptors(source)); } else { ownKeys(Object(source)).forEach(function (key) { Object.defineProperty(target, key, Object.getOwnPropertyDescriptor(source, key)); }); } } return target; } @@ -79135,15 +79053,15 @@ class Kibana { } /***/ }), -/* 699 */ +/* 698 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; const minimatch = __webpack_require__(504); -const arrayUnion = __webpack_require__(700); -const arrayDiffer = __webpack_require__(701); -const arrify = __webpack_require__(702); +const arrayUnion = __webpack_require__(699); +const arrayDiffer = __webpack_require__(700); +const arrify = __webpack_require__(701); module.exports = (list, patterns, options = {}) => { list = arrify(list); @@ -79167,7 +79085,7 @@ module.exports = (list, patterns, options = {}) => { /***/ }), -/* 700 */ +/* 699 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -79179,7 +79097,7 @@ module.exports = (...arguments_) => { /***/ }), -/* 701 */ +/* 700 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -79194,7 +79112,7 @@ module.exports = arrayDiffer; /***/ }), -/* 702 */ +/* 701 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -79224,7 +79142,7 @@ module.exports = arrify; /***/ }), -/* 703 */ +/* 702 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -79252,15 +79170,15 @@ module.exports = (childPath, parentPath) => { /***/ }), -/* 704 */ +/* 703 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; __webpack_require__.r(__webpack_exports__); -/* harmony import */ var _build_production_projects__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(705); +/* harmony import */ var _build_production_projects__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(704); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "buildProductionProjects", function() { return _build_production_projects__WEBPACK_IMPORTED_MODULE_0__["buildProductionProjects"]; }); -/* harmony import */ var _prepare_project_dependencies__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(923); +/* harmony import */ var _prepare_project_dependencies__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(927); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "prepareExternalProjectDependencies", function() { return _prepare_project_dependencies__WEBPACK_IMPORTED_MODULE_1__["prepareExternalProjectDependencies"]; }); /* @@ -79285,19 +79203,19 @@ __webpack_require__.r(__webpack_exports__); /***/ }), -/* 705 */ +/* 704 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; __webpack_require__.r(__webpack_exports__); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "buildProductionProjects", function() { return buildProductionProjects; }); -/* harmony import */ var cpy__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(706); +/* harmony import */ var cpy__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(705); /* harmony import */ var cpy__WEBPACK_IMPORTED_MODULE_0___default = /*#__PURE__*/__webpack_require__.n(cpy__WEBPACK_IMPORTED_MODULE_0__); -/* harmony import */ var del__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(586); +/* harmony import */ var del__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(585); /* harmony import */ var del__WEBPACK_IMPORTED_MODULE_1___default = /*#__PURE__*/__webpack_require__.n(del__WEBPACK_IMPORTED_MODULE_1__); /* harmony import */ var path__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(16); /* harmony import */ var path__WEBPACK_IMPORTED_MODULE_2___default = /*#__PURE__*/__webpack_require__.n(path__WEBPACK_IMPORTED_MODULE_2__); -/* harmony import */ var _config__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(578); +/* harmony import */ var _config__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(577); /* harmony import */ var _utils_fs__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(20); /* harmony import */ var _utils_log__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(34); /* harmony import */ var _utils_package_json__WEBPACK_IMPORTED_MODULE_6__ = __webpack_require__(516); @@ -79433,7 +79351,7 @@ async function copyToBuild(project, kibanaRoot, buildRoot) { } /***/ }), -/* 706 */ +/* 705 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -79441,13 +79359,13 @@ async function copyToBuild(project, kibanaRoot, buildRoot) { const EventEmitter = __webpack_require__(379); const path = __webpack_require__(16); const os = __webpack_require__(11); -const pAll = __webpack_require__(707); -const arrify = __webpack_require__(709); -const globby = __webpack_require__(710); -const isGlob = __webpack_require__(604); -const cpFile = __webpack_require__(908); -const junk = __webpack_require__(920); -const CpyError = __webpack_require__(921); +const pAll = __webpack_require__(706); +const arrify = __webpack_require__(708); +const globby = __webpack_require__(709); +const isGlob = __webpack_require__(603); +const cpFile = __webpack_require__(912); +const junk = __webpack_require__(924); +const CpyError = __webpack_require__(925); const defaultOptions = { ignoreJunk: true @@ -79566,12 +79484,12 @@ module.exports = (source, destination, { /***/ }), -/* 707 */ +/* 706 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -const pMap = __webpack_require__(708); +const pMap = __webpack_require__(707); module.exports = (iterable, options) => pMap(iterable, element => element(), options); // TODO: Remove this for the next major release @@ -79579,7 +79497,7 @@ module.exports.default = module.exports; /***/ }), -/* 708 */ +/* 707 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -79658,7 +79576,7 @@ module.exports.default = pMap; /***/ }), -/* 709 */ +/* 708 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -79688,17 +79606,17 @@ module.exports = arrify; /***/ }), -/* 710 */ +/* 709 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; const fs = __webpack_require__(23); -const arrayUnion = __webpack_require__(711); -const glob = __webpack_require__(713); -const fastGlob = __webpack_require__(718); -const dirGlob = __webpack_require__(901); -const gitignore = __webpack_require__(904); +const arrayUnion = __webpack_require__(710); +const glob = __webpack_require__(712); +const fastGlob = __webpack_require__(717); +const dirGlob = __webpack_require__(905); +const gitignore = __webpack_require__(908); const DEFAULT_FILTER = () => false; @@ -79843,12 +79761,12 @@ module.exports.gitignore = gitignore; /***/ }), -/* 711 */ +/* 710 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var arrayUniq = __webpack_require__(712); +var arrayUniq = __webpack_require__(711); module.exports = function () { return arrayUniq([].concat.apply([], arguments)); @@ -79856,7 +79774,7 @@ module.exports = function () { /***/ }), -/* 712 */ +/* 711 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -79925,7 +79843,7 @@ if ('Set' in global) { /***/ }), -/* 713 */ +/* 712 */ /***/ (function(module, exports, __webpack_require__) { // Approach: @@ -79974,13 +79892,13 @@ var fs = __webpack_require__(23) var rp = __webpack_require__(502) var minimatch = __webpack_require__(504) var Minimatch = minimatch.Minimatch -var inherits = __webpack_require__(714) +var inherits = __webpack_require__(713) var EE = __webpack_require__(379).EventEmitter var path = __webpack_require__(16) var assert = __webpack_require__(30) var isAbsolute = __webpack_require__(510) -var globSync = __webpack_require__(716) -var common = __webpack_require__(717) +var globSync = __webpack_require__(715) +var common = __webpack_require__(716) var alphasort = common.alphasort var alphasorti = common.alphasorti var setopts = common.setopts @@ -80721,7 +80639,7 @@ Glob.prototype._stat2 = function (f, abs, er, stat, cb) { /***/ }), -/* 714 */ +/* 713 */ /***/ (function(module, exports, __webpack_require__) { try { @@ -80731,12 +80649,12 @@ try { module.exports = util.inherits; } catch (e) { /* istanbul ignore next */ - module.exports = __webpack_require__(715); + module.exports = __webpack_require__(714); } /***/ }), -/* 715 */ +/* 714 */ /***/ (function(module, exports) { if (typeof Object.create === 'function') { @@ -80769,7 +80687,7 @@ if (typeof Object.create === 'function') { /***/ }), -/* 716 */ +/* 715 */ /***/ (function(module, exports, __webpack_require__) { module.exports = globSync @@ -80779,12 +80697,12 @@ var fs = __webpack_require__(23) var rp = __webpack_require__(502) var minimatch = __webpack_require__(504) var Minimatch = minimatch.Minimatch -var Glob = __webpack_require__(713).Glob +var Glob = __webpack_require__(712).Glob var util = __webpack_require__(29) var path = __webpack_require__(16) var assert = __webpack_require__(30) var isAbsolute = __webpack_require__(510) -var common = __webpack_require__(717) +var common = __webpack_require__(716) var alphasort = common.alphasort var alphasorti = common.alphasorti var setopts = common.setopts @@ -81261,7 +81179,7 @@ GlobSync.prototype._makeAbs = function (f) { /***/ }), -/* 717 */ +/* 716 */ /***/ (function(module, exports, __webpack_require__) { exports.alphasort = alphasort @@ -81507,10 +81425,10 @@ function childrenIgnored (self, path) { /***/ }), -/* 718 */ +/* 717 */ /***/ (function(module, exports, __webpack_require__) { -const pkg = __webpack_require__(719); +const pkg = __webpack_require__(718); module.exports = pkg.async; module.exports.default = pkg.async; @@ -81523,19 +81441,19 @@ module.exports.generateTasks = pkg.generateTasks; /***/ }), -/* 719 */ +/* 718 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); -var optionsManager = __webpack_require__(720); -var taskManager = __webpack_require__(721); -var reader_async_1 = __webpack_require__(872); -var reader_stream_1 = __webpack_require__(896); -var reader_sync_1 = __webpack_require__(897); -var arrayUtils = __webpack_require__(899); -var streamUtils = __webpack_require__(900); +var optionsManager = __webpack_require__(719); +var taskManager = __webpack_require__(720); +var reader_async_1 = __webpack_require__(876); +var reader_stream_1 = __webpack_require__(900); +var reader_sync_1 = __webpack_require__(901); +var arrayUtils = __webpack_require__(903); +var streamUtils = __webpack_require__(904); /** * Synchronous API. */ @@ -81601,7 +81519,7 @@ function isString(source) { /***/ }), -/* 720 */ +/* 719 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -81639,13 +81557,13 @@ exports.prepare = prepare; /***/ }), -/* 721 */ +/* 720 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); -var patternUtils = __webpack_require__(722); +var patternUtils = __webpack_require__(721); /** * Generate tasks based on parent directory of each pattern. */ @@ -81736,16 +81654,16 @@ exports.convertPatternGroupToTask = convertPatternGroupToTask; /***/ }), -/* 722 */ +/* 721 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); var path = __webpack_require__(16); -var globParent = __webpack_require__(723); -var isGlob = __webpack_require__(726); -var micromatch = __webpack_require__(727); +var globParent = __webpack_require__(722); +var isGlob = __webpack_require__(725); +var micromatch = __webpack_require__(726); var GLOBSTAR = '**'; /** * Return true for static pattern. @@ -81891,15 +81809,15 @@ exports.matchAny = matchAny; /***/ }), -/* 723 */ +/* 722 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; var path = __webpack_require__(16); -var isglob = __webpack_require__(724); -var pathDirname = __webpack_require__(725); +var isglob = __webpack_require__(723); +var pathDirname = __webpack_require__(724); var isWin32 = __webpack_require__(11).platform() === 'win32'; module.exports = function globParent(str) { @@ -81922,7 +81840,7 @@ module.exports = function globParent(str) { /***/ }), -/* 724 */ +/* 723 */ /***/ (function(module, exports, __webpack_require__) { /*! @@ -81932,7 +81850,7 @@ module.exports = function globParent(str) { * Licensed under the MIT License. */ -var isExtglob = __webpack_require__(605); +var isExtglob = __webpack_require__(604); module.exports = function isGlob(str) { if (typeof str !== 'string' || str === '') { @@ -81953,7 +81871,7 @@ module.exports = function isGlob(str) { /***/ }), -/* 725 */ +/* 724 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -82103,7 +82021,7 @@ module.exports.win32 = win32; /***/ }), -/* 726 */ +/* 725 */ /***/ (function(module, exports, __webpack_require__) { /*! @@ -82113,7 +82031,7 @@ module.exports.win32 = win32; * Released under the MIT License. */ -var isExtglob = __webpack_require__(605); +var isExtglob = __webpack_require__(604); var chars = { '{': '}', '(': ')', '[': ']'}; module.exports = function isGlob(str, options) { @@ -82155,7 +82073,7 @@ module.exports = function isGlob(str, options) { /***/ }), -/* 727 */ +/* 726 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -82166,18 +82084,18 @@ module.exports = function isGlob(str, options) { */ var util = __webpack_require__(29); -var braces = __webpack_require__(728); -var toRegex = __webpack_require__(830); -var extend = __webpack_require__(838); +var braces = __webpack_require__(727); +var toRegex = __webpack_require__(829); +var extend = __webpack_require__(837); /** * Local dependencies */ -var compilers = __webpack_require__(841); -var parsers = __webpack_require__(868); -var cache = __webpack_require__(869); -var utils = __webpack_require__(870); +var compilers = __webpack_require__(840); +var parsers = __webpack_require__(872); +var cache = __webpack_require__(873); +var utils = __webpack_require__(874); var MAX_LENGTH = 1024 * 64; /** @@ -83039,7 +82957,7 @@ module.exports = micromatch; /***/ }), -/* 728 */ +/* 727 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -83049,18 +82967,18 @@ module.exports = micromatch; * Module dependencies */ -var toRegex = __webpack_require__(729); -var unique = __webpack_require__(741); -var extend = __webpack_require__(738); +var toRegex = __webpack_require__(728); +var unique = __webpack_require__(740); +var extend = __webpack_require__(737); /** * Local dependencies */ -var compilers = __webpack_require__(742); -var parsers = __webpack_require__(757); -var Braces = __webpack_require__(767); -var utils = __webpack_require__(743); +var compilers = __webpack_require__(741); +var parsers = __webpack_require__(756); +var Braces = __webpack_require__(766); +var utils = __webpack_require__(742); var MAX_LENGTH = 1024 * 64; var cache = {}; @@ -83364,15 +83282,15 @@ module.exports = braces; /***/ }), -/* 729 */ +/* 728 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var define = __webpack_require__(730); -var extend = __webpack_require__(738); -var not = __webpack_require__(740); +var define = __webpack_require__(729); +var extend = __webpack_require__(737); +var not = __webpack_require__(739); var MAX_LENGTH = 1024 * 64; /** @@ -83519,7 +83437,7 @@ module.exports.makeRe = makeRe; /***/ }), -/* 730 */ +/* 729 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -83532,7 +83450,7 @@ module.exports.makeRe = makeRe; -var isDescriptor = __webpack_require__(731); +var isDescriptor = __webpack_require__(730); module.exports = function defineProperty(obj, prop, val) { if (typeof obj !== 'object' && typeof obj !== 'function') { @@ -83557,7 +83475,7 @@ module.exports = function defineProperty(obj, prop, val) { /***/ }), -/* 731 */ +/* 730 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -83570,9 +83488,9 @@ module.exports = function defineProperty(obj, prop, val) { -var typeOf = __webpack_require__(732); -var isAccessor = __webpack_require__(733); -var isData = __webpack_require__(736); +var typeOf = __webpack_require__(731); +var isAccessor = __webpack_require__(732); +var isData = __webpack_require__(735); module.exports = function isDescriptor(obj, key) { if (typeOf(obj) !== 'object') { @@ -83586,7 +83504,7 @@ module.exports = function isDescriptor(obj, key) { /***/ }), -/* 732 */ +/* 731 */ /***/ (function(module, exports) { var toString = Object.prototype.toString; @@ -83739,7 +83657,7 @@ function isBuffer(val) { /***/ }), -/* 733 */ +/* 732 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -83752,7 +83670,7 @@ function isBuffer(val) { -var typeOf = __webpack_require__(734); +var typeOf = __webpack_require__(733); // accessor descriptor properties var accessor = { @@ -83815,10 +83733,10 @@ module.exports = isAccessorDescriptor; /***/ }), -/* 734 */ +/* 733 */ /***/ (function(module, exports, __webpack_require__) { -var isBuffer = __webpack_require__(735); +var isBuffer = __webpack_require__(734); var toString = Object.prototype.toString; /** @@ -83937,7 +83855,7 @@ module.exports = function kindOf(val) { /***/ }), -/* 735 */ +/* 734 */ /***/ (function(module, exports) { /*! @@ -83964,7 +83882,7 @@ function isSlowBuffer (obj) { /***/ }), -/* 736 */ +/* 735 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -83977,7 +83895,7 @@ function isSlowBuffer (obj) { -var typeOf = __webpack_require__(737); +var typeOf = __webpack_require__(736); // data descriptor properties var data = { @@ -84026,10 +83944,10 @@ module.exports = isDataDescriptor; /***/ }), -/* 737 */ +/* 736 */ /***/ (function(module, exports, __webpack_require__) { -var isBuffer = __webpack_require__(735); +var isBuffer = __webpack_require__(734); var toString = Object.prototype.toString; /** @@ -84148,13 +84066,13 @@ module.exports = function kindOf(val) { /***/ }), -/* 738 */ +/* 737 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var isObject = __webpack_require__(739); +var isObject = __webpack_require__(738); module.exports = function extend(o/*, objects*/) { if (!isObject(o)) { o = {}; } @@ -84188,7 +84106,7 @@ function hasOwn(obj, key) { /***/ }), -/* 739 */ +/* 738 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -84208,13 +84126,13 @@ module.exports = function isExtendable(val) { /***/ }), -/* 740 */ +/* 739 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var extend = __webpack_require__(738); +var extend = __webpack_require__(737); /** * The main export is a function that takes a `pattern` string and an `options` object. @@ -84281,7 +84199,7 @@ module.exports = toRegex; /***/ }), -/* 741 */ +/* 740 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -84331,13 +84249,13 @@ module.exports.immutable = function uniqueImmutable(arr) { /***/ }), -/* 742 */ +/* 741 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var utils = __webpack_require__(743); +var utils = __webpack_require__(742); module.exports = function(braces, options) { braces.compiler @@ -84620,25 +84538,25 @@ function hasQueue(node) { /***/ }), -/* 743 */ +/* 742 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var splitString = __webpack_require__(744); +var splitString = __webpack_require__(743); var utils = module.exports; /** * Module dependencies */ -utils.extend = __webpack_require__(738); -utils.flatten = __webpack_require__(750); -utils.isObject = __webpack_require__(748); -utils.fillRange = __webpack_require__(751); -utils.repeat = __webpack_require__(756); -utils.unique = __webpack_require__(741); +utils.extend = __webpack_require__(737); +utils.flatten = __webpack_require__(749); +utils.isObject = __webpack_require__(747); +utils.fillRange = __webpack_require__(750); +utils.repeat = __webpack_require__(755); +utils.unique = __webpack_require__(740); utils.define = function(obj, key, val) { Object.defineProperty(obj, key, { @@ -84970,7 +84888,7 @@ utils.escapeRegex = function(str) { /***/ }), -/* 744 */ +/* 743 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -84983,7 +84901,7 @@ utils.escapeRegex = function(str) { -var extend = __webpack_require__(745); +var extend = __webpack_require__(744); module.exports = function(str, options, fn) { if (typeof str !== 'string') { @@ -85148,14 +85066,14 @@ function keepEscaping(opts, str, idx) { /***/ }), -/* 745 */ +/* 744 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var isExtendable = __webpack_require__(746); -var assignSymbols = __webpack_require__(749); +var isExtendable = __webpack_require__(745); +var assignSymbols = __webpack_require__(748); module.exports = Object.assign || function(obj/*, objects*/) { if (obj === null || typeof obj === 'undefined') { @@ -85215,7 +85133,7 @@ function isEnum(obj, key) { /***/ }), -/* 746 */ +/* 745 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -85228,7 +85146,7 @@ function isEnum(obj, key) { -var isPlainObject = __webpack_require__(747); +var isPlainObject = __webpack_require__(746); module.exports = function isExtendable(val) { return isPlainObject(val) || typeof val === 'function' || Array.isArray(val); @@ -85236,7 +85154,7 @@ module.exports = function isExtendable(val) { /***/ }), -/* 747 */ +/* 746 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -85249,7 +85167,7 @@ module.exports = function isExtendable(val) { -var isObject = __webpack_require__(748); +var isObject = __webpack_require__(747); function isObjectObject(o) { return isObject(o) === true @@ -85280,7 +85198,7 @@ module.exports = function isPlainObject(o) { /***/ }), -/* 748 */ +/* 747 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -85299,7 +85217,7 @@ module.exports = function isObject(val) { /***/ }), -/* 749 */ +/* 748 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -85346,7 +85264,7 @@ module.exports = function(receiver, objects) { /***/ }), -/* 750 */ +/* 749 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -85375,7 +85293,7 @@ function flat(arr, res) { /***/ }), -/* 751 */ +/* 750 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -85389,10 +85307,10 @@ function flat(arr, res) { var util = __webpack_require__(29); -var isNumber = __webpack_require__(752); -var extend = __webpack_require__(738); -var repeat = __webpack_require__(754); -var toRegex = __webpack_require__(755); +var isNumber = __webpack_require__(751); +var extend = __webpack_require__(737); +var repeat = __webpack_require__(753); +var toRegex = __webpack_require__(754); /** * Return a range of numbers or letters. @@ -85590,7 +85508,7 @@ module.exports = fillRange; /***/ }), -/* 752 */ +/* 751 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -85603,7 +85521,7 @@ module.exports = fillRange; -var typeOf = __webpack_require__(753); +var typeOf = __webpack_require__(752); module.exports = function isNumber(num) { var type = typeOf(num); @@ -85619,10 +85537,10 @@ module.exports = function isNumber(num) { /***/ }), -/* 753 */ +/* 752 */ /***/ (function(module, exports, __webpack_require__) { -var isBuffer = __webpack_require__(735); +var isBuffer = __webpack_require__(734); var toString = Object.prototype.toString; /** @@ -85741,7 +85659,7 @@ module.exports = function kindOf(val) { /***/ }), -/* 754 */ +/* 753 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -85818,7 +85736,7 @@ function repeat(str, num) { /***/ }), -/* 755 */ +/* 754 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -85831,8 +85749,8 @@ function repeat(str, num) { -var repeat = __webpack_require__(754); -var isNumber = __webpack_require__(752); +var repeat = __webpack_require__(753); +var isNumber = __webpack_require__(751); var cache = {}; function toRegexRange(min, max, options) { @@ -86119,7 +86037,7 @@ module.exports = toRegexRange; /***/ }), -/* 756 */ +/* 755 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -86144,14 +86062,14 @@ module.exports = function repeat(ele, num) { /***/ }), -/* 757 */ +/* 756 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var Node = __webpack_require__(758); -var utils = __webpack_require__(743); +var Node = __webpack_require__(757); +var utils = __webpack_require__(742); /** * Braces parsers @@ -86511,15 +86429,15 @@ function concatNodes(pos, node, parent, options) { /***/ }), -/* 758 */ +/* 757 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var isObject = __webpack_require__(748); -var define = __webpack_require__(759); -var utils = __webpack_require__(766); +var isObject = __webpack_require__(747); +var define = __webpack_require__(758); +var utils = __webpack_require__(765); var ownNames; /** @@ -87010,7 +86928,7 @@ exports = module.exports = Node; /***/ }), -/* 759 */ +/* 758 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -87023,7 +86941,7 @@ exports = module.exports = Node; -var isDescriptor = __webpack_require__(760); +var isDescriptor = __webpack_require__(759); module.exports = function defineProperty(obj, prop, val) { if (typeof obj !== 'object' && typeof obj !== 'function') { @@ -87048,7 +86966,7 @@ module.exports = function defineProperty(obj, prop, val) { /***/ }), -/* 760 */ +/* 759 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -87061,9 +86979,9 @@ module.exports = function defineProperty(obj, prop, val) { -var typeOf = __webpack_require__(761); -var isAccessor = __webpack_require__(762); -var isData = __webpack_require__(764); +var typeOf = __webpack_require__(760); +var isAccessor = __webpack_require__(761); +var isData = __webpack_require__(763); module.exports = function isDescriptor(obj, key) { if (typeOf(obj) !== 'object') { @@ -87077,7 +86995,7 @@ module.exports = function isDescriptor(obj, key) { /***/ }), -/* 761 */ +/* 760 */ /***/ (function(module, exports) { var toString = Object.prototype.toString; @@ -87212,7 +87130,7 @@ function isBuffer(val) { /***/ }), -/* 762 */ +/* 761 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -87225,7 +87143,7 @@ function isBuffer(val) { -var typeOf = __webpack_require__(763); +var typeOf = __webpack_require__(762); // accessor descriptor properties var accessor = { @@ -87288,7 +87206,7 @@ module.exports = isAccessorDescriptor; /***/ }), -/* 763 */ +/* 762 */ /***/ (function(module, exports) { var toString = Object.prototype.toString; @@ -87423,7 +87341,7 @@ function isBuffer(val) { /***/ }), -/* 764 */ +/* 763 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -87436,7 +87354,7 @@ function isBuffer(val) { -var typeOf = __webpack_require__(765); +var typeOf = __webpack_require__(764); module.exports = function isDataDescriptor(obj, prop) { // data descriptor properties @@ -87479,7 +87397,7 @@ module.exports = function isDataDescriptor(obj, prop) { /***/ }), -/* 765 */ +/* 764 */ /***/ (function(module, exports) { var toString = Object.prototype.toString; @@ -87614,13 +87532,13 @@ function isBuffer(val) { /***/ }), -/* 766 */ +/* 765 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var typeOf = __webpack_require__(753); +var typeOf = __webpack_require__(752); var utils = module.exports; /** @@ -88640,17 +88558,17 @@ function assert(val, message) { /***/ }), -/* 767 */ +/* 766 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var extend = __webpack_require__(738); -var Snapdragon = __webpack_require__(768); -var compilers = __webpack_require__(742); -var parsers = __webpack_require__(757); -var utils = __webpack_require__(743); +var extend = __webpack_require__(737); +var Snapdragon = __webpack_require__(767); +var compilers = __webpack_require__(741); +var parsers = __webpack_require__(756); +var utils = __webpack_require__(742); /** * Customize Snapdragon parser and renderer @@ -88751,17 +88669,17 @@ module.exports = Braces; /***/ }), -/* 768 */ +/* 767 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var Base = __webpack_require__(769); -var define = __webpack_require__(730); -var Compiler = __webpack_require__(798); -var Parser = __webpack_require__(827); -var utils = __webpack_require__(807); +var Base = __webpack_require__(768); +var define = __webpack_require__(729); +var Compiler = __webpack_require__(797); +var Parser = __webpack_require__(826); +var utils = __webpack_require__(806); var regexCache = {}; var cache = {}; @@ -88932,20 +88850,20 @@ module.exports.Parser = Parser; /***/ }), -/* 769 */ +/* 768 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; var util = __webpack_require__(29); -var define = __webpack_require__(770); -var CacheBase = __webpack_require__(771); -var Emitter = __webpack_require__(772); -var isObject = __webpack_require__(748); -var merge = __webpack_require__(789); -var pascal = __webpack_require__(792); -var cu = __webpack_require__(793); +var define = __webpack_require__(769); +var CacheBase = __webpack_require__(770); +var Emitter = __webpack_require__(771); +var isObject = __webpack_require__(747); +var merge = __webpack_require__(788); +var pascal = __webpack_require__(791); +var cu = __webpack_require__(792); /** * Optionally define a custom `cache` namespace to use. @@ -89374,7 +89292,7 @@ module.exports.namespace = namespace; /***/ }), -/* 770 */ +/* 769 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -89387,7 +89305,7 @@ module.exports.namespace = namespace; -var isDescriptor = __webpack_require__(760); +var isDescriptor = __webpack_require__(759); module.exports = function defineProperty(obj, prop, val) { if (typeof obj !== 'object' && typeof obj !== 'function') { @@ -89412,21 +89330,21 @@ module.exports = function defineProperty(obj, prop, val) { /***/ }), -/* 771 */ +/* 770 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var isObject = __webpack_require__(748); -var Emitter = __webpack_require__(772); -var visit = __webpack_require__(773); -var toPath = __webpack_require__(776); -var union = __webpack_require__(777); -var del = __webpack_require__(781); -var get = __webpack_require__(779); -var has = __webpack_require__(786); -var set = __webpack_require__(780); +var isObject = __webpack_require__(747); +var Emitter = __webpack_require__(771); +var visit = __webpack_require__(772); +var toPath = __webpack_require__(775); +var union = __webpack_require__(776); +var del = __webpack_require__(780); +var get = __webpack_require__(778); +var has = __webpack_require__(785); +var set = __webpack_require__(779); /** * Create a `Cache` constructor that when instantiated will @@ -89680,7 +89598,7 @@ module.exports.namespace = namespace; /***/ }), -/* 772 */ +/* 771 */ /***/ (function(module, exports, __webpack_require__) { @@ -89849,7 +89767,7 @@ Emitter.prototype.hasListeners = function(event){ /***/ }), -/* 773 */ +/* 772 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -89862,8 +89780,8 @@ Emitter.prototype.hasListeners = function(event){ -var visit = __webpack_require__(774); -var mapVisit = __webpack_require__(775); +var visit = __webpack_require__(773); +var mapVisit = __webpack_require__(774); module.exports = function(collection, method, val) { var result; @@ -89886,7 +89804,7 @@ module.exports = function(collection, method, val) { /***/ }), -/* 774 */ +/* 773 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -89899,7 +89817,7 @@ module.exports = function(collection, method, val) { -var isObject = __webpack_require__(748); +var isObject = __webpack_require__(747); module.exports = function visit(thisArg, method, target, val) { if (!isObject(thisArg) && typeof thisArg !== 'function') { @@ -89926,14 +89844,14 @@ module.exports = function visit(thisArg, method, target, val) { /***/ }), -/* 775 */ +/* 774 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; var util = __webpack_require__(29); -var visit = __webpack_require__(774); +var visit = __webpack_require__(773); /** * Map `visit` over an array of objects. @@ -89970,7 +89888,7 @@ function isObject(val) { /***/ }), -/* 776 */ +/* 775 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -89983,7 +89901,7 @@ function isObject(val) { -var typeOf = __webpack_require__(753); +var typeOf = __webpack_require__(752); module.exports = function toPath(args) { if (typeOf(args) !== 'arguments') { @@ -90010,16 +89928,16 @@ function filter(arr) { /***/ }), -/* 777 */ +/* 776 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var isObject = __webpack_require__(739); -var union = __webpack_require__(778); -var get = __webpack_require__(779); -var set = __webpack_require__(780); +var isObject = __webpack_require__(738); +var union = __webpack_require__(777); +var get = __webpack_require__(778); +var set = __webpack_require__(779); module.exports = function unionValue(obj, prop, value) { if (!isObject(obj)) { @@ -90047,7 +89965,7 @@ function arrayify(val) { /***/ }), -/* 778 */ +/* 777 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -90083,7 +90001,7 @@ module.exports = function union(init) { /***/ }), -/* 779 */ +/* 778 */ /***/ (function(module, exports) { /*! @@ -90139,7 +90057,7 @@ function toString(val) { /***/ }), -/* 780 */ +/* 779 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -90152,10 +90070,10 @@ function toString(val) { -var split = __webpack_require__(744); -var extend = __webpack_require__(738); -var isPlainObject = __webpack_require__(747); -var isObject = __webpack_require__(739); +var split = __webpack_require__(743); +var extend = __webpack_require__(737); +var isPlainObject = __webpack_require__(746); +var isObject = __webpack_require__(738); module.exports = function(obj, prop, val) { if (!isObject(obj)) { @@ -90201,7 +90119,7 @@ function isValidKey(key) { /***/ }), -/* 781 */ +/* 780 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -90214,8 +90132,8 @@ function isValidKey(key) { -var isObject = __webpack_require__(748); -var has = __webpack_require__(782); +var isObject = __webpack_require__(747); +var has = __webpack_require__(781); module.exports = function unset(obj, prop) { if (!isObject(obj)) { @@ -90240,7 +90158,7 @@ module.exports = function unset(obj, prop) { /***/ }), -/* 782 */ +/* 781 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -90253,9 +90171,9 @@ module.exports = function unset(obj, prop) { -var isObject = __webpack_require__(783); -var hasValues = __webpack_require__(785); -var get = __webpack_require__(779); +var isObject = __webpack_require__(782); +var hasValues = __webpack_require__(784); +var get = __webpack_require__(778); module.exports = function(obj, prop, noZero) { if (isObject(obj)) { @@ -90266,7 +90184,7 @@ module.exports = function(obj, prop, noZero) { /***/ }), -/* 783 */ +/* 782 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -90279,7 +90197,7 @@ module.exports = function(obj, prop, noZero) { -var isArray = __webpack_require__(784); +var isArray = __webpack_require__(783); module.exports = function isObject(val) { return val != null && typeof val === 'object' && isArray(val) === false; @@ -90287,7 +90205,7 @@ module.exports = function isObject(val) { /***/ }), -/* 784 */ +/* 783 */ /***/ (function(module, exports) { var toString = {}.toString; @@ -90298,7 +90216,7 @@ module.exports = Array.isArray || function (arr) { /***/ }), -/* 785 */ +/* 784 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -90341,7 +90259,7 @@ module.exports = function hasValue(o, noZero) { /***/ }), -/* 786 */ +/* 785 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -90354,9 +90272,9 @@ module.exports = function hasValue(o, noZero) { -var isObject = __webpack_require__(748); -var hasValues = __webpack_require__(787); -var get = __webpack_require__(779); +var isObject = __webpack_require__(747); +var hasValues = __webpack_require__(786); +var get = __webpack_require__(778); module.exports = function(val, prop) { return hasValues(isObject(val) && prop ? get(val, prop) : val); @@ -90364,7 +90282,7 @@ module.exports = function(val, prop) { /***/ }), -/* 787 */ +/* 786 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -90377,8 +90295,8 @@ module.exports = function(val, prop) { -var typeOf = __webpack_require__(788); -var isNumber = __webpack_require__(752); +var typeOf = __webpack_require__(787); +var isNumber = __webpack_require__(751); module.exports = function hasValue(val) { // is-number checks for NaN and other edge cases @@ -90431,10 +90349,10 @@ module.exports = function hasValue(val) { /***/ }), -/* 788 */ +/* 787 */ /***/ (function(module, exports, __webpack_require__) { -var isBuffer = __webpack_require__(735); +var isBuffer = __webpack_require__(734); var toString = Object.prototype.toString; /** @@ -90556,14 +90474,14 @@ module.exports = function kindOf(val) { /***/ }), -/* 789 */ +/* 788 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var isExtendable = __webpack_require__(790); -var forIn = __webpack_require__(791); +var isExtendable = __webpack_require__(789); +var forIn = __webpack_require__(790); function mixinDeep(target, objects) { var len = arguments.length, i = 0; @@ -90627,7 +90545,7 @@ module.exports = mixinDeep; /***/ }), -/* 790 */ +/* 789 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -90640,7 +90558,7 @@ module.exports = mixinDeep; -var isPlainObject = __webpack_require__(747); +var isPlainObject = __webpack_require__(746); module.exports = function isExtendable(val) { return isPlainObject(val) || typeof val === 'function' || Array.isArray(val); @@ -90648,7 +90566,7 @@ module.exports = function isExtendable(val) { /***/ }), -/* 791 */ +/* 790 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -90671,7 +90589,7 @@ module.exports = function forIn(obj, fn, thisArg) { /***/ }), -/* 792 */ +/* 791 */ /***/ (function(module, exports) { /*! @@ -90698,14 +90616,14 @@ module.exports = pascalcase; /***/ }), -/* 793 */ +/* 792 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; var util = __webpack_require__(29); -var utils = __webpack_require__(794); +var utils = __webpack_require__(793); /** * Expose class utils @@ -91070,7 +90988,7 @@ cu.bubble = function(Parent, events) { /***/ }), -/* 794 */ +/* 793 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -91084,10 +91002,10 @@ var utils = {}; * Lazily required module dependencies */ -utils.union = __webpack_require__(778); -utils.define = __webpack_require__(730); -utils.isObj = __webpack_require__(748); -utils.staticExtend = __webpack_require__(795); +utils.union = __webpack_require__(777); +utils.define = __webpack_require__(729); +utils.isObj = __webpack_require__(747); +utils.staticExtend = __webpack_require__(794); /** @@ -91098,7 +91016,7 @@ module.exports = utils; /***/ }), -/* 795 */ +/* 794 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -91111,8 +91029,8 @@ module.exports = utils; -var copy = __webpack_require__(796); -var define = __webpack_require__(730); +var copy = __webpack_require__(795); +var define = __webpack_require__(729); var util = __webpack_require__(29); /** @@ -91195,15 +91113,15 @@ module.exports = extend; /***/ }), -/* 796 */ +/* 795 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var typeOf = __webpack_require__(753); -var copyDescriptor = __webpack_require__(797); -var define = __webpack_require__(730); +var typeOf = __webpack_require__(752); +var copyDescriptor = __webpack_require__(796); +var define = __webpack_require__(729); /** * Copy static properties, prototype properties, and descriptors from one object to another. @@ -91376,7 +91294,7 @@ module.exports.has = has; /***/ }), -/* 797 */ +/* 796 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -91464,16 +91382,16 @@ function isObject(val) { /***/ }), -/* 798 */ +/* 797 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var use = __webpack_require__(799); -var define = __webpack_require__(730); -var debug = __webpack_require__(801)('snapdragon:compiler'); -var utils = __webpack_require__(807); +var use = __webpack_require__(798); +var define = __webpack_require__(729); +var debug = __webpack_require__(800)('snapdragon:compiler'); +var utils = __webpack_require__(806); /** * Create a new `Compiler` with the given `options`. @@ -91627,7 +91545,7 @@ Compiler.prototype = { // source map support if (opts.sourcemap) { - var sourcemaps = __webpack_require__(826); + var sourcemaps = __webpack_require__(825); sourcemaps(this); this.mapVisit(this.ast.nodes); this.applySourceMaps(); @@ -91648,7 +91566,7 @@ module.exports = Compiler; /***/ }), -/* 799 */ +/* 798 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -91661,7 +91579,7 @@ module.exports = Compiler; -var utils = __webpack_require__(800); +var utils = __webpack_require__(799); module.exports = function base(app, opts) { if (!utils.isObject(app) && typeof app !== 'function') { @@ -91776,7 +91694,7 @@ module.exports = function base(app, opts) { /***/ }), -/* 800 */ +/* 799 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -91790,8 +91708,8 @@ var utils = {}; * Lazily required module dependencies */ -utils.define = __webpack_require__(730); -utils.isObject = __webpack_require__(748); +utils.define = __webpack_require__(729); +utils.isObject = __webpack_require__(747); utils.isString = function(val) { @@ -91806,7 +91724,7 @@ module.exports = utils; /***/ }), -/* 801 */ +/* 800 */ /***/ (function(module, exports, __webpack_require__) { /** @@ -91815,14 +91733,14 @@ module.exports = utils; */ if (typeof process !== 'undefined' && process.type === 'renderer') { - module.exports = __webpack_require__(802); + module.exports = __webpack_require__(801); } else { - module.exports = __webpack_require__(805); + module.exports = __webpack_require__(804); } /***/ }), -/* 802 */ +/* 801 */ /***/ (function(module, exports, __webpack_require__) { /** @@ -91831,7 +91749,7 @@ if (typeof process !== 'undefined' && process.type === 'renderer') { * Expose `debug()` as the module. */ -exports = module.exports = __webpack_require__(803); +exports = module.exports = __webpack_require__(802); exports.log = log; exports.formatArgs = formatArgs; exports.save = save; @@ -92013,7 +91931,7 @@ function localstorage() { /***/ }), -/* 803 */ +/* 802 */ /***/ (function(module, exports, __webpack_require__) { @@ -92029,7 +91947,7 @@ exports.coerce = coerce; exports.disable = disable; exports.enable = enable; exports.enabled = enabled; -exports.humanize = __webpack_require__(804); +exports.humanize = __webpack_require__(803); /** * The currently active debug mode names, and names to skip. @@ -92221,7 +92139,7 @@ function coerce(val) { /***/ }), -/* 804 */ +/* 803 */ /***/ (function(module, exports) { /** @@ -92379,7 +92297,7 @@ function plural(ms, n, name) { /***/ }), -/* 805 */ +/* 804 */ /***/ (function(module, exports, __webpack_require__) { /** @@ -92395,7 +92313,7 @@ var util = __webpack_require__(29); * Expose `debug()` as the module. */ -exports = module.exports = __webpack_require__(803); +exports = module.exports = __webpack_require__(802); exports.init = init; exports.log = log; exports.formatArgs = formatArgs; @@ -92574,7 +92492,7 @@ function createWritableStdioStream (fd) { case 'PIPE': case 'TCP': - var net = __webpack_require__(806); + var net = __webpack_require__(805); stream = new net.Socket({ fd: fd, readable: false, @@ -92633,13 +92551,13 @@ exports.enable(load()); /***/ }), -/* 806 */ +/* 805 */ /***/ (function(module, exports) { module.exports = require("net"); /***/ }), -/* 807 */ +/* 806 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -92649,9 +92567,9 @@ module.exports = require("net"); * Module dependencies */ -exports.extend = __webpack_require__(738); -exports.SourceMap = __webpack_require__(808); -exports.sourceMapResolve = __webpack_require__(819); +exports.extend = __webpack_require__(737); +exports.SourceMap = __webpack_require__(807); +exports.sourceMapResolve = __webpack_require__(818); /** * Convert backslash in the given string to forward slashes @@ -92694,7 +92612,7 @@ exports.last = function(arr, n) { /***/ }), -/* 808 */ +/* 807 */ /***/ (function(module, exports, __webpack_require__) { /* @@ -92702,13 +92620,13 @@ exports.last = function(arr, n) { * Licensed under the New BSD license. See LICENSE.txt or: * http://opensource.org/licenses/BSD-3-Clause */ -exports.SourceMapGenerator = __webpack_require__(809).SourceMapGenerator; -exports.SourceMapConsumer = __webpack_require__(815).SourceMapConsumer; -exports.SourceNode = __webpack_require__(818).SourceNode; +exports.SourceMapGenerator = __webpack_require__(808).SourceMapGenerator; +exports.SourceMapConsumer = __webpack_require__(814).SourceMapConsumer; +exports.SourceNode = __webpack_require__(817).SourceNode; /***/ }), -/* 809 */ +/* 808 */ /***/ (function(module, exports, __webpack_require__) { /* -*- Mode: js; js-indent-level: 2; -*- */ @@ -92718,10 +92636,10 @@ exports.SourceNode = __webpack_require__(818).SourceNode; * http://opensource.org/licenses/BSD-3-Clause */ -var base64VLQ = __webpack_require__(810); -var util = __webpack_require__(812); -var ArraySet = __webpack_require__(813).ArraySet; -var MappingList = __webpack_require__(814).MappingList; +var base64VLQ = __webpack_require__(809); +var util = __webpack_require__(811); +var ArraySet = __webpack_require__(812).ArraySet; +var MappingList = __webpack_require__(813).MappingList; /** * An instance of the SourceMapGenerator represents a source map which is @@ -93130,7 +93048,7 @@ exports.SourceMapGenerator = SourceMapGenerator; /***/ }), -/* 810 */ +/* 809 */ /***/ (function(module, exports, __webpack_require__) { /* -*- Mode: js; js-indent-level: 2; -*- */ @@ -93170,7 +93088,7 @@ exports.SourceMapGenerator = SourceMapGenerator; * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ -var base64 = __webpack_require__(811); +var base64 = __webpack_require__(810); // A single base 64 digit can contain 6 bits of data. For the base 64 variable // length quantities we use in the source map spec, the first bit is the sign, @@ -93276,7 +93194,7 @@ exports.decode = function base64VLQ_decode(aStr, aIndex, aOutParam) { /***/ }), -/* 811 */ +/* 810 */ /***/ (function(module, exports) { /* -*- Mode: js; js-indent-level: 2; -*- */ @@ -93349,7 +93267,7 @@ exports.decode = function (charCode) { /***/ }), -/* 812 */ +/* 811 */ /***/ (function(module, exports) { /* -*- Mode: js; js-indent-level: 2; -*- */ @@ -93772,7 +93690,7 @@ exports.compareByGeneratedPositionsInflated = compareByGeneratedPositionsInflate /***/ }), -/* 813 */ +/* 812 */ /***/ (function(module, exports, __webpack_require__) { /* -*- Mode: js; js-indent-level: 2; -*- */ @@ -93782,7 +93700,7 @@ exports.compareByGeneratedPositionsInflated = compareByGeneratedPositionsInflate * http://opensource.org/licenses/BSD-3-Clause */ -var util = __webpack_require__(812); +var util = __webpack_require__(811); var has = Object.prototype.hasOwnProperty; var hasNativeMap = typeof Map !== "undefined"; @@ -93899,7 +93817,7 @@ exports.ArraySet = ArraySet; /***/ }), -/* 814 */ +/* 813 */ /***/ (function(module, exports, __webpack_require__) { /* -*- Mode: js; js-indent-level: 2; -*- */ @@ -93909,7 +93827,7 @@ exports.ArraySet = ArraySet; * http://opensource.org/licenses/BSD-3-Clause */ -var util = __webpack_require__(812); +var util = __webpack_require__(811); /** * Determine whether mappingB is after mappingA with respect to generated @@ -93984,7 +93902,7 @@ exports.MappingList = MappingList; /***/ }), -/* 815 */ +/* 814 */ /***/ (function(module, exports, __webpack_require__) { /* -*- Mode: js; js-indent-level: 2; -*- */ @@ -93994,11 +93912,11 @@ exports.MappingList = MappingList; * http://opensource.org/licenses/BSD-3-Clause */ -var util = __webpack_require__(812); -var binarySearch = __webpack_require__(816); -var ArraySet = __webpack_require__(813).ArraySet; -var base64VLQ = __webpack_require__(810); -var quickSort = __webpack_require__(817).quickSort; +var util = __webpack_require__(811); +var binarySearch = __webpack_require__(815); +var ArraySet = __webpack_require__(812).ArraySet; +var base64VLQ = __webpack_require__(809); +var quickSort = __webpack_require__(816).quickSort; function SourceMapConsumer(aSourceMap) { var sourceMap = aSourceMap; @@ -95072,7 +94990,7 @@ exports.IndexedSourceMapConsumer = IndexedSourceMapConsumer; /***/ }), -/* 816 */ +/* 815 */ /***/ (function(module, exports) { /* -*- Mode: js; js-indent-level: 2; -*- */ @@ -95189,7 +95107,7 @@ exports.search = function search(aNeedle, aHaystack, aCompare, aBias) { /***/ }), -/* 817 */ +/* 816 */ /***/ (function(module, exports) { /* -*- Mode: js; js-indent-level: 2; -*- */ @@ -95309,7 +95227,7 @@ exports.quickSort = function (ary, comparator) { /***/ }), -/* 818 */ +/* 817 */ /***/ (function(module, exports, __webpack_require__) { /* -*- Mode: js; js-indent-level: 2; -*- */ @@ -95319,8 +95237,8 @@ exports.quickSort = function (ary, comparator) { * http://opensource.org/licenses/BSD-3-Clause */ -var SourceMapGenerator = __webpack_require__(809).SourceMapGenerator; -var util = __webpack_require__(812); +var SourceMapGenerator = __webpack_require__(808).SourceMapGenerator; +var util = __webpack_require__(811); // Matches a Windows-style `\r\n` newline or a `\n` newline used by all other // operating systems these days (capturing the result). @@ -95728,17 +95646,17 @@ exports.SourceNode = SourceNode; /***/ }), -/* 819 */ +/* 818 */ /***/ (function(module, exports, __webpack_require__) { // Copyright 2014, 2015, 2016, 2017 Simon Lydell // X11 (“MIT”) Licensed. (See LICENSE.) -var sourceMappingURL = __webpack_require__(820) -var resolveUrl = __webpack_require__(821) -var decodeUriComponent = __webpack_require__(822) -var urix = __webpack_require__(824) -var atob = __webpack_require__(825) +var sourceMappingURL = __webpack_require__(819) +var resolveUrl = __webpack_require__(820) +var decodeUriComponent = __webpack_require__(821) +var urix = __webpack_require__(823) +var atob = __webpack_require__(824) @@ -96036,7 +95954,7 @@ module.exports = { /***/ }), -/* 820 */ +/* 819 */ /***/ (function(module, exports, __webpack_require__) { var __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_RESULT__;// Copyright 2014 Simon Lydell @@ -96099,7 +96017,7 @@ void (function(root, factory) { /***/ }), -/* 821 */ +/* 820 */ /***/ (function(module, exports, __webpack_require__) { // Copyright 2014 Simon Lydell @@ -96117,13 +96035,13 @@ module.exports = resolveUrl /***/ }), -/* 822 */ +/* 821 */ /***/ (function(module, exports, __webpack_require__) { // Copyright 2017 Simon Lydell // X11 (“MIT”) Licensed. (See LICENSE.) -var decodeUriComponent = __webpack_require__(823) +var decodeUriComponent = __webpack_require__(822) function customDecodeUriComponent(string) { // `decodeUriComponent` turns `+` into ` `, but that's not wanted. @@ -96134,7 +96052,7 @@ module.exports = customDecodeUriComponent /***/ }), -/* 823 */ +/* 822 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -96235,7 +96153,7 @@ module.exports = function (encodedURI) { /***/ }), -/* 824 */ +/* 823 */ /***/ (function(module, exports, __webpack_require__) { // Copyright 2014 Simon Lydell @@ -96258,7 +96176,7 @@ module.exports = urix /***/ }), -/* 825 */ +/* 824 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -96272,7 +96190,7 @@ module.exports = atob.atob = atob; /***/ }), -/* 826 */ +/* 825 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -96280,8 +96198,8 @@ module.exports = atob.atob = atob; var fs = __webpack_require__(23); var path = __webpack_require__(16); -var define = __webpack_require__(730); -var utils = __webpack_require__(807); +var define = __webpack_require__(729); +var utils = __webpack_require__(806); /** * Expose `mixin()`. @@ -96424,19 +96342,19 @@ exports.comment = function(node) { /***/ }), -/* 827 */ +/* 826 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var use = __webpack_require__(799); +var use = __webpack_require__(798); var util = __webpack_require__(29); -var Cache = __webpack_require__(828); -var define = __webpack_require__(730); -var debug = __webpack_require__(801)('snapdragon:parser'); -var Position = __webpack_require__(829); -var utils = __webpack_require__(807); +var Cache = __webpack_require__(827); +var define = __webpack_require__(729); +var debug = __webpack_require__(800)('snapdragon:parser'); +var Position = __webpack_require__(828); +var utils = __webpack_require__(806); /** * Create a new `Parser` with the given `input` and `options`. @@ -96964,7 +96882,7 @@ module.exports = Parser; /***/ }), -/* 828 */ +/* 827 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -97071,13 +96989,13 @@ MapCache.prototype.del = function mapDelete(key) { /***/ }), -/* 829 */ +/* 828 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var define = __webpack_require__(730); +var define = __webpack_require__(729); /** * Store position for a node @@ -97092,16 +97010,16 @@ module.exports = function Position(start, parser) { /***/ }), -/* 830 */ +/* 829 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var safe = __webpack_require__(831); -var define = __webpack_require__(837); -var extend = __webpack_require__(838); -var not = __webpack_require__(840); +var safe = __webpack_require__(830); +var define = __webpack_require__(836); +var extend = __webpack_require__(837); +var not = __webpack_require__(839); var MAX_LENGTH = 1024 * 64; /** @@ -97254,10 +97172,10 @@ module.exports.makeRe = makeRe; /***/ }), -/* 831 */ +/* 830 */ /***/ (function(module, exports, __webpack_require__) { -var parse = __webpack_require__(832); +var parse = __webpack_require__(831); var types = parse.types; module.exports = function (re, opts) { @@ -97303,13 +97221,13 @@ function isRegExp (x) { /***/ }), -/* 832 */ +/* 831 */ /***/ (function(module, exports, __webpack_require__) { -var util = __webpack_require__(833); -var types = __webpack_require__(834); -var sets = __webpack_require__(835); -var positions = __webpack_require__(836); +var util = __webpack_require__(832); +var types = __webpack_require__(833); +var sets = __webpack_require__(834); +var positions = __webpack_require__(835); module.exports = function(regexpStr) { @@ -97591,11 +97509,11 @@ module.exports.types = types; /***/ }), -/* 833 */ +/* 832 */ /***/ (function(module, exports, __webpack_require__) { -var types = __webpack_require__(834); -var sets = __webpack_require__(835); +var types = __webpack_require__(833); +var sets = __webpack_require__(834); // All of these are private and only used by randexp. @@ -97708,7 +97626,7 @@ exports.error = function(regexp, msg) { /***/ }), -/* 834 */ +/* 833 */ /***/ (function(module, exports) { module.exports = { @@ -97724,10 +97642,10 @@ module.exports = { /***/ }), -/* 835 */ +/* 834 */ /***/ (function(module, exports, __webpack_require__) { -var types = __webpack_require__(834); +var types = __webpack_require__(833); var INTS = function() { return [{ type: types.RANGE , from: 48, to: 57 }]; @@ -97812,10 +97730,10 @@ exports.anyChar = function() { /***/ }), -/* 836 */ +/* 835 */ /***/ (function(module, exports, __webpack_require__) { -var types = __webpack_require__(834); +var types = __webpack_require__(833); exports.wordBoundary = function() { return { type: types.POSITION, value: 'b' }; @@ -97835,7 +97753,7 @@ exports.end = function() { /***/ }), -/* 837 */ +/* 836 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -97848,8 +97766,8 @@ exports.end = function() { -var isobject = __webpack_require__(748); -var isDescriptor = __webpack_require__(760); +var isobject = __webpack_require__(747); +var isDescriptor = __webpack_require__(759); var define = (typeof Reflect !== 'undefined' && Reflect.defineProperty) ? Reflect.defineProperty : Object.defineProperty; @@ -97880,14 +97798,14 @@ module.exports = function defineProperty(obj, key, val) { /***/ }), -/* 838 */ +/* 837 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var isExtendable = __webpack_require__(839); -var assignSymbols = __webpack_require__(749); +var isExtendable = __webpack_require__(838); +var assignSymbols = __webpack_require__(748); module.exports = Object.assign || function(obj/*, objects*/) { if (obj === null || typeof obj === 'undefined') { @@ -97947,7 +97865,7 @@ function isEnum(obj, key) { /***/ }), -/* 839 */ +/* 838 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -97960,7 +97878,7 @@ function isEnum(obj, key) { -var isPlainObject = __webpack_require__(747); +var isPlainObject = __webpack_require__(746); module.exports = function isExtendable(val) { return isPlainObject(val) || typeof val === 'function' || Array.isArray(val); @@ -97968,14 +97886,14 @@ module.exports = function isExtendable(val) { /***/ }), -/* 840 */ +/* 839 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var extend = __webpack_require__(838); -var safe = __webpack_require__(831); +var extend = __webpack_require__(837); +var safe = __webpack_require__(830); /** * The main export is a function that takes a `pattern` string and an `options` object. @@ -98047,14 +97965,14 @@ module.exports = toRegex; /***/ }), -/* 841 */ +/* 840 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var nanomatch = __webpack_require__(842); -var extglob = __webpack_require__(857); +var nanomatch = __webpack_require__(841); +var extglob = __webpack_require__(856); module.exports = function(snapdragon) { var compilers = snapdragon.compiler.compilers; @@ -98131,7 +98049,7 @@ function escapeExtglobs(compiler) { /***/ }), -/* 842 */ +/* 841 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -98142,17 +98060,17 @@ function escapeExtglobs(compiler) { */ var util = __webpack_require__(29); -var toRegex = __webpack_require__(729); -var extend = __webpack_require__(843); +var toRegex = __webpack_require__(728); +var extend = __webpack_require__(842); /** * Local dependencies */ -var compilers = __webpack_require__(845); -var parsers = __webpack_require__(846); -var cache = __webpack_require__(849); -var utils = __webpack_require__(851); +var compilers = __webpack_require__(844); +var parsers = __webpack_require__(845); +var cache = __webpack_require__(848); +var utils = __webpack_require__(850); var MAX_LENGTH = 1024 * 64; /** @@ -98976,14 +98894,14 @@ module.exports = nanomatch; /***/ }), -/* 843 */ +/* 842 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var isExtendable = __webpack_require__(844); -var assignSymbols = __webpack_require__(749); +var isExtendable = __webpack_require__(843); +var assignSymbols = __webpack_require__(748); module.exports = Object.assign || function(obj/*, objects*/) { if (obj === null || typeof obj === 'undefined') { @@ -99043,7 +98961,7 @@ function isEnum(obj, key) { /***/ }), -/* 844 */ +/* 843 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -99056,7 +98974,7 @@ function isEnum(obj, key) { -var isPlainObject = __webpack_require__(747); +var isPlainObject = __webpack_require__(746); module.exports = function isExtendable(val) { return isPlainObject(val) || typeof val === 'function' || Array.isArray(val); @@ -99064,7 +98982,7 @@ module.exports = function isExtendable(val) { /***/ }), -/* 845 */ +/* 844 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -99410,15 +99328,15 @@ module.exports = function(nanomatch, options) { /***/ }), -/* 846 */ +/* 845 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var regexNot = __webpack_require__(740); -var toRegex = __webpack_require__(729); -var isOdd = __webpack_require__(847); +var regexNot = __webpack_require__(739); +var toRegex = __webpack_require__(728); +var isOdd = __webpack_require__(846); /** * Characters to use in negation regex (we want to "not" match @@ -99804,7 +99722,7 @@ module.exports.not = NOT_REGEX; /***/ }), -/* 847 */ +/* 846 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -99817,7 +99735,7 @@ module.exports.not = NOT_REGEX; -var isNumber = __webpack_require__(848); +var isNumber = __webpack_require__(847); module.exports = function isOdd(i) { if (!isNumber(i)) { @@ -99831,7 +99749,7 @@ module.exports = function isOdd(i) { /***/ }), -/* 848 */ +/* 847 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -99859,14 +99777,14 @@ module.exports = function isNumber(num) { /***/ }), -/* 849 */ +/* 848 */ /***/ (function(module, exports, __webpack_require__) { -module.exports = new (__webpack_require__(850))(); +module.exports = new (__webpack_require__(849))(); /***/ }), -/* 850 */ +/* 849 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -99879,7 +99797,7 @@ module.exports = new (__webpack_require__(850))(); -var MapCache = __webpack_require__(828); +var MapCache = __webpack_require__(827); /** * Create a new `FragmentCache` with an optional object to use for `caches`. @@ -100001,7 +99919,7 @@ exports = module.exports = FragmentCache; /***/ }), -/* 851 */ +/* 850 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -100014,14 +99932,14 @@ var path = __webpack_require__(16); * Module dependencies */ -var isWindows = __webpack_require__(852)(); -var Snapdragon = __webpack_require__(768); -utils.define = __webpack_require__(853); -utils.diff = __webpack_require__(854); -utils.extend = __webpack_require__(843); -utils.pick = __webpack_require__(855); -utils.typeOf = __webpack_require__(856); -utils.unique = __webpack_require__(741); +var isWindows = __webpack_require__(851)(); +var Snapdragon = __webpack_require__(767); +utils.define = __webpack_require__(852); +utils.diff = __webpack_require__(853); +utils.extend = __webpack_require__(842); +utils.pick = __webpack_require__(854); +utils.typeOf = __webpack_require__(855); +utils.unique = __webpack_require__(740); /** * Returns true if the given value is effectively an empty string @@ -100387,7 +100305,7 @@ utils.unixify = function(options) { /***/ }), -/* 852 */ +/* 851 */ /***/ (function(module, exports, __webpack_require__) { var __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_DEFINE_RESULT__;/*! @@ -100415,7 +100333,7 @@ var __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_ /***/ }), -/* 853 */ +/* 852 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -100428,8 +100346,8 @@ var __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_ -var isobject = __webpack_require__(748); -var isDescriptor = __webpack_require__(760); +var isobject = __webpack_require__(747); +var isDescriptor = __webpack_require__(759); var define = (typeof Reflect !== 'undefined' && Reflect.defineProperty) ? Reflect.defineProperty : Object.defineProperty; @@ -100460,7 +100378,7 @@ module.exports = function defineProperty(obj, key, val) { /***/ }), -/* 854 */ +/* 853 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -100514,7 +100432,7 @@ function diffArray(one, two) { /***/ }), -/* 855 */ +/* 854 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -100527,7 +100445,7 @@ function diffArray(one, two) { -var isObject = __webpack_require__(748); +var isObject = __webpack_require__(747); module.exports = function pick(obj, keys) { if (!isObject(obj) && typeof obj !== 'function') { @@ -100556,7 +100474,7 @@ module.exports = function pick(obj, keys) { /***/ }), -/* 856 */ +/* 855 */ /***/ (function(module, exports) { var toString = Object.prototype.toString; @@ -100691,7 +100609,7 @@ function isBuffer(val) { /***/ }), -/* 857 */ +/* 856 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -100701,18 +100619,18 @@ function isBuffer(val) { * Module dependencies */ -var extend = __webpack_require__(738); -var unique = __webpack_require__(741); -var toRegex = __webpack_require__(729); +var extend = __webpack_require__(737); +var unique = __webpack_require__(740); +var toRegex = __webpack_require__(728); /** * Local dependencies */ -var compilers = __webpack_require__(858); -var parsers = __webpack_require__(864); -var Extglob = __webpack_require__(867); -var utils = __webpack_require__(866); +var compilers = __webpack_require__(857); +var parsers = __webpack_require__(868); +var Extglob = __webpack_require__(871); +var utils = __webpack_require__(870); var MAX_LENGTH = 1024 * 64; /** @@ -101029,13 +100947,13 @@ module.exports = extglob; /***/ }), -/* 858 */ +/* 857 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var brackets = __webpack_require__(859); +var brackets = __webpack_require__(858); /** * Extglob compilers @@ -101205,7 +101123,7 @@ module.exports = function(extglob) { /***/ }), -/* 859 */ +/* 858 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -101215,17 +101133,17 @@ module.exports = function(extglob) { * Local dependencies */ -var compilers = __webpack_require__(860); -var parsers = __webpack_require__(862); +var compilers = __webpack_require__(859); +var parsers = __webpack_require__(861); /** * Module dependencies */ -var debug = __webpack_require__(801)('expand-brackets'); -var extend = __webpack_require__(738); -var Snapdragon = __webpack_require__(768); -var toRegex = __webpack_require__(729); +var debug = __webpack_require__(863)('expand-brackets'); +var extend = __webpack_require__(737); +var Snapdragon = __webpack_require__(767); +var toRegex = __webpack_require__(728); /** * Parses the given POSIX character class `pattern` and returns a @@ -101423,13 +101341,13 @@ module.exports = brackets; /***/ }), -/* 860 */ +/* 859 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var posix = __webpack_require__(861); +var posix = __webpack_require__(860); module.exports = function(brackets) { brackets.compiler @@ -101517,7 +101435,7 @@ module.exports = function(brackets) { /***/ }), -/* 861 */ +/* 860 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -101546,14 +101464,14 @@ module.exports = { /***/ }), -/* 862 */ +/* 861 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var utils = __webpack_require__(863); -var define = __webpack_require__(730); +var utils = __webpack_require__(862); +var define = __webpack_require__(729); /** * Text regex @@ -101772,14 +101690,14 @@ module.exports.TEXT_REGEX = TEXT_REGEX; /***/ }), -/* 863 */ +/* 862 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var toRegex = __webpack_require__(729); -var regexNot = __webpack_require__(740); +var toRegex = __webpack_require__(728); +var regexNot = __webpack_require__(739); var cached; /** @@ -101813,195 +101731,1022 @@ exports.createRegex = function(pattern, include) { /***/ }), -/* 864 */ +/* 863 */ /***/ (function(module, exports, __webpack_require__) { -"use strict"; +/** + * Detect Electron renderer process, which is node, but we should + * treat as a browser. + */ + +if (typeof process !== 'undefined' && process.type === 'renderer') { + module.exports = __webpack_require__(864); +} else { + module.exports = __webpack_require__(867); +} -var brackets = __webpack_require__(859); -var define = __webpack_require__(865); -var utils = __webpack_require__(866); +/***/ }), +/* 864 */ +/***/ (function(module, exports, __webpack_require__) { /** - * Characters to use in text regex (we want to "not" match - * characters that are matched by other parsers) + * This is the web browser implementation of `debug()`. + * + * Expose `debug()` as the module. */ -var TEXT_REGEX = '([!@*?+]?\\(|\\)|[*?.+\\\\]|\\[:?(?=.*\\])|:?\\])+'; -var not = utils.createRegex(TEXT_REGEX); +exports = module.exports = __webpack_require__(865); +exports.log = log; +exports.formatArgs = formatArgs; +exports.save = save; +exports.load = load; +exports.useColors = useColors; +exports.storage = 'undefined' != typeof chrome + && 'undefined' != typeof chrome.storage + ? chrome.storage.local + : localstorage(); /** - * Extglob parsers + * Colors. */ -function parsers(extglob) { - extglob.state = extglob.state || {}; - - /** - * Use `expand-brackets` parsers - */ +exports.colors = [ + 'lightseagreen', + 'forestgreen', + 'goldenrod', + 'dodgerblue', + 'darkorchid', + 'crimson' +]; - extglob.use(brackets.parsers); - extglob.parser.sets.paren = extglob.parser.sets.paren || []; - extglob.parser +/** + * Currently only WebKit-based Web Inspectors, Firefox >= v31, + * and the Firebug extension (any Firefox version) are known + * to support "%c" CSS customizations. + * + * TODO: add a `localStorage` variable to explicitly enable/disable colors + */ - /** - * Extglob open: "*(" - */ +function useColors() { + // NB: In an Electron preload script, document will be defined but not fully + // initialized. Since we know we're in Chrome, we'll just detect this case + // explicitly + if (typeof window !== 'undefined' && window.process && window.process.type === 'renderer') { + return true; + } - .capture('paren.open', function() { - var parsed = this.parsed; - var pos = this.position(); - var m = this.match(/^([!@*?+])?\(/); - if (!m) return; + // is webkit? http://stackoverflow.com/a/16459606/376773 + // document is undefined in react-native: https://github.com/facebook/react-native/pull/1632 + return (typeof document !== 'undefined' && document.documentElement && document.documentElement.style && document.documentElement.style.WebkitAppearance) || + // is firebug? http://stackoverflow.com/a/398120/376773 + (typeof window !== 'undefined' && window.console && (window.console.firebug || (window.console.exception && window.console.table))) || + // is firefox >= v31? + // https://developer.mozilla.org/en-US/docs/Tools/Web_Console#Styling_messages + (typeof navigator !== 'undefined' && navigator.userAgent && navigator.userAgent.toLowerCase().match(/firefox\/(\d+)/) && parseInt(RegExp.$1, 10) >= 31) || + // double check webkit in userAgent just in case we are in a worker + (typeof navigator !== 'undefined' && navigator.userAgent && navigator.userAgent.toLowerCase().match(/applewebkit\/(\d+)/)); +} - var prev = this.prev(); - var prefix = m[1]; - var val = m[0]; +/** + * Map %j to `JSON.stringify()`, since no Web Inspectors do that by default. + */ - var open = pos({ - type: 'paren.open', - parsed: parsed, - val: val - }); +exports.formatters.j = function(v) { + try { + return JSON.stringify(v); + } catch (err) { + return '[UnexpectedJSONParseError]: ' + err.message; + } +}; - var node = pos({ - type: 'paren', - prefix: prefix, - nodes: [open] - }); - // if nested negation extglobs, just cancel them out to simplify - if (prefix === '!' && prev.type === 'paren' && prev.prefix === '!') { - prev.prefix = '@'; - node.prefix = '@'; - } +/** + * Colorize log arguments if enabled. + * + * @api public + */ - define(node, 'rest', this.input); - define(node, 'parsed', parsed); - define(node, 'parent', prev); - define(open, 'parent', node); +function formatArgs(args) { + var useColors = this.useColors; - this.push('paren', node); - prev.nodes.push(node); - }) + args[0] = (useColors ? '%c' : '') + + this.namespace + + (useColors ? ' %c' : ' ') + + args[0] + + (useColors ? '%c ' : ' ') + + '+' + exports.humanize(this.diff); - /** - * Extglob close: ")" - */ + if (!useColors) return; - .capture('paren.close', function() { - var parsed = this.parsed; - var pos = this.position(); - var m = this.match(/^\)/); - if (!m) return; + var c = 'color: ' + this.color; + args.splice(1, 0, c, 'color: inherit') - var parent = this.pop('paren'); - var node = pos({ - type: 'paren.close', - rest: this.input, - parsed: parsed, - val: m[0] - }); + // the final "%c" is somewhat tricky, because there could be other + // arguments passed either before or after the %c, so we need to + // figure out the correct index to insert the CSS into + var index = 0; + var lastC = 0; + args[0].replace(/%[a-zA-Z%]/g, function(match) { + if ('%%' === match) return; + index++; + if ('%c' === match) { + // we only are interested in the *last* %c + // (the user may have provided their own) + lastC = index; + } + }); - if (!this.isType(parent, 'paren')) { - if (this.options.strict) { - throw new Error('missing opening paren: "("'); - } - node.escaped = true; - return node; - } + args.splice(lastC, 0, c); +} - node.prefix = parent.prefix; - parent.nodes.push(node); - define(node, 'parent', parent); - }) +/** + * Invokes `console.log()` when available. + * No-op when `console.log` is not a "function". + * + * @api public + */ - /** - * Escape: "\\." - */ +function log() { + // this hackery is required for IE8/9, where + // the `console.log` function doesn't have 'apply' + return 'object' === typeof console + && console.log + && Function.prototype.apply.call(console.log, console, arguments); +} - .capture('escape', function() { - var pos = this.position(); - var m = this.match(/^\\(.)/); - if (!m) return; +/** + * Save `namespaces`. + * + * @param {String} namespaces + * @api private + */ - return pos({ - type: 'escape', - val: m[0], - ch: m[1] - }); - }) +function save(namespaces) { + try { + if (null == namespaces) { + exports.storage.removeItem('debug'); + } else { + exports.storage.debug = namespaces; + } + } catch(e) {} +} - /** - * Question marks: "?" - */ +/** + * Load `namespaces`. + * + * @return {String} returns the previously persisted debug modes + * @api private + */ - .capture('qmark', function() { - var parsed = this.parsed; - var pos = this.position(); - var m = this.match(/^\?+(?!\()/); - if (!m) return; - extglob.state.metachar = true; - return pos({ - type: 'qmark', - rest: this.input, - parsed: parsed, - val: m[0] - }); - }) +function load() { + var r; + try { + r = exports.storage.debug; + } catch(e) {} - /** - * Character parsers - */ + // If debug isn't set in LS, and we're in Electron, try to load $DEBUG + if (!r && typeof process !== 'undefined' && 'env' in process) { + r = process.env.DEBUG; + } - .capture('star', /^\*(?!\()/) - .capture('plus', /^\+(?!\()/) - .capture('dot', /^\./) - .capture('text', not); -}; + return r; +} /** - * Expose text regex string + * Enable namespaces listed in `localStorage.debug` initially. */ -module.exports.TEXT_REGEX = TEXT_REGEX; +exports.enable(load()); /** - * Extglob parsers + * Localstorage attempts to return the localstorage. + * + * This is necessary because safari throws + * when a user disables cookies/localstorage + * and you attempt to access it. + * + * @return {LocalStorage} + * @api private */ -module.exports = parsers; +function localstorage() { + try { + return window.localStorage; + } catch (e) {} +} /***/ }), /* 865 */ /***/ (function(module, exports, __webpack_require__) { -"use strict"; -/*! - * define-property + +/** + * This is the common logic for both the Node.js and web browser + * implementations of `debug()`. * - * Copyright (c) 2015, 2017, Jon Schlinkert. - * Released under the MIT License. + * Expose `debug()` as the module. */ +exports = module.exports = createDebug.debug = createDebug['default'] = createDebug; +exports.coerce = coerce; +exports.disable = disable; +exports.enable = enable; +exports.enabled = enabled; +exports.humanize = __webpack_require__(866); +/** + * The currently active debug mode names, and names to skip. + */ -var isDescriptor = __webpack_require__(760); +exports.names = []; +exports.skips = []; -module.exports = function defineProperty(obj, prop, val) { - if (typeof obj !== 'object' && typeof obj !== 'function') { - throw new TypeError('expected an object or function.'); - } +/** + * Map of special "%n" handling functions, for the debug "format" argument. + * + * Valid key names are a single, lower or upper-case letter, i.e. "n" and "N". + */ - if (typeof prop !== 'string') { - throw new TypeError('expected `prop` to be a string.'); - } +exports.formatters = {}; - if (isDescriptor(val) && ('set' in val || 'get' in val)) { - return Object.defineProperty(obj, prop, val); +/** + * Previous log timestamp. + */ + +var prevTime; + +/** + * Select a color. + * @param {String} namespace + * @return {Number} + * @api private + */ + +function selectColor(namespace) { + var hash = 0, i; + + for (i in namespace) { + hash = ((hash << 5) - hash) + namespace.charCodeAt(i); + hash |= 0; // Convert to 32bit integer + } + + return exports.colors[Math.abs(hash) % exports.colors.length]; +} + +/** + * Create a debugger with the given `namespace`. + * + * @param {String} namespace + * @return {Function} + * @api public + */ + +function createDebug(namespace) { + + function debug() { + // disabled? + if (!debug.enabled) return; + + var self = debug; + + // set `diff` timestamp + var curr = +new Date(); + var ms = curr - (prevTime || curr); + self.diff = ms; + self.prev = prevTime; + self.curr = curr; + prevTime = curr; + + // turn the `arguments` into a proper Array + var args = new Array(arguments.length); + for (var i = 0; i < args.length; i++) { + args[i] = arguments[i]; + } + + args[0] = exports.coerce(args[0]); + + if ('string' !== typeof args[0]) { + // anything else let's inspect with %O + args.unshift('%O'); + } + + // apply any `formatters` transformations + var index = 0; + args[0] = args[0].replace(/%([a-zA-Z%])/g, function(match, format) { + // if we encounter an escaped % then don't increase the array index + if (match === '%%') return match; + index++; + var formatter = exports.formatters[format]; + if ('function' === typeof formatter) { + var val = args[index]; + match = formatter.call(self, val); + + // now we need to remove `args[index]` since it's inlined in the `format` + args.splice(index, 1); + index--; + } + return match; + }); + + // apply env-specific formatting (colors, etc.) + exports.formatArgs.call(self, args); + + var logFn = debug.log || exports.log || console.log.bind(console); + logFn.apply(self, args); + } + + debug.namespace = namespace; + debug.enabled = exports.enabled(namespace); + debug.useColors = exports.useColors(); + debug.color = selectColor(namespace); + + // env-specific initialization logic for debug instances + if ('function' === typeof exports.init) { + exports.init(debug); + } + + return debug; +} + +/** + * Enables a debug mode by namespaces. This can include modes + * separated by a colon and wildcards. + * + * @param {String} namespaces + * @api public + */ + +function enable(namespaces) { + exports.save(namespaces); + + exports.names = []; + exports.skips = []; + + var split = (typeof namespaces === 'string' ? namespaces : '').split(/[\s,]+/); + var len = split.length; + + for (var i = 0; i < len; i++) { + if (!split[i]) continue; // ignore empty strings + namespaces = split[i].replace(/\*/g, '.*?'); + if (namespaces[0] === '-') { + exports.skips.push(new RegExp('^' + namespaces.substr(1) + '$')); + } else { + exports.names.push(new RegExp('^' + namespaces + '$')); + } + } +} + +/** + * Disable debug output. + * + * @api public + */ + +function disable() { + exports.enable(''); +} + +/** + * Returns true if the given mode name is enabled, false otherwise. + * + * @param {String} name + * @return {Boolean} + * @api public + */ + +function enabled(name) { + var i, len; + for (i = 0, len = exports.skips.length; i < len; i++) { + if (exports.skips[i].test(name)) { + return false; + } + } + for (i = 0, len = exports.names.length; i < len; i++) { + if (exports.names[i].test(name)) { + return true; + } + } + return false; +} + +/** + * Coerce `val`. + * + * @param {Mixed} val + * @return {Mixed} + * @api private + */ + +function coerce(val) { + if (val instanceof Error) return val.stack || val.message; + return val; +} + + +/***/ }), +/* 866 */ +/***/ (function(module, exports) { + +/** + * Helpers. + */ + +var s = 1000; +var m = s * 60; +var h = m * 60; +var d = h * 24; +var y = d * 365.25; + +/** + * Parse or format the given `val`. + * + * Options: + * + * - `long` verbose formatting [false] + * + * @param {String|Number} val + * @param {Object} [options] + * @throws {Error} throw an error if val is not a non-empty string or a number + * @return {String|Number} + * @api public + */ + +module.exports = function(val, options) { + options = options || {}; + var type = typeof val; + if (type === 'string' && val.length > 0) { + return parse(val); + } else if (type === 'number' && isNaN(val) === false) { + return options.long ? fmtLong(val) : fmtShort(val); + } + throw new Error( + 'val is not a non-empty string or a valid number. val=' + + JSON.stringify(val) + ); +}; + +/** + * Parse the given `str` and return milliseconds. + * + * @param {String} str + * @return {Number} + * @api private + */ + +function parse(str) { + str = String(str); + if (str.length > 100) { + return; + } + var match = /^((?:\d+)?\.?\d+) *(milliseconds?|msecs?|ms|seconds?|secs?|s|minutes?|mins?|m|hours?|hrs?|h|days?|d|years?|yrs?|y)?$/i.exec( + str + ); + if (!match) { + return; + } + var n = parseFloat(match[1]); + var type = (match[2] || 'ms').toLowerCase(); + switch (type) { + case 'years': + case 'year': + case 'yrs': + case 'yr': + case 'y': + return n * y; + case 'days': + case 'day': + case 'd': + return n * d; + case 'hours': + case 'hour': + case 'hrs': + case 'hr': + case 'h': + return n * h; + case 'minutes': + case 'minute': + case 'mins': + case 'min': + case 'm': + return n * m; + case 'seconds': + case 'second': + case 'secs': + case 'sec': + case 's': + return n * s; + case 'milliseconds': + case 'millisecond': + case 'msecs': + case 'msec': + case 'ms': + return n; + default: + return undefined; + } +} + +/** + * Short format for `ms`. + * + * @param {Number} ms + * @return {String} + * @api private + */ + +function fmtShort(ms) { + if (ms >= d) { + return Math.round(ms / d) + 'd'; + } + if (ms >= h) { + return Math.round(ms / h) + 'h'; + } + if (ms >= m) { + return Math.round(ms / m) + 'm'; + } + if (ms >= s) { + return Math.round(ms / s) + 's'; + } + return ms + 'ms'; +} + +/** + * Long format for `ms`. + * + * @param {Number} ms + * @return {String} + * @api private + */ + +function fmtLong(ms) { + return plural(ms, d, 'day') || + plural(ms, h, 'hour') || + plural(ms, m, 'minute') || + plural(ms, s, 'second') || + ms + ' ms'; +} + +/** + * Pluralization helper. + */ + +function plural(ms, n, name) { + if (ms < n) { + return; + } + if (ms < n * 1.5) { + return Math.floor(ms / n) + ' ' + name; + } + return Math.ceil(ms / n) + ' ' + name + 's'; +} + + +/***/ }), +/* 867 */ +/***/ (function(module, exports, __webpack_require__) { + +/** + * Module dependencies. + */ + +var tty = __webpack_require__(478); +var util = __webpack_require__(29); + +/** + * This is the Node.js implementation of `debug()`. + * + * Expose `debug()` as the module. + */ + +exports = module.exports = __webpack_require__(865); +exports.init = init; +exports.log = log; +exports.formatArgs = formatArgs; +exports.save = save; +exports.load = load; +exports.useColors = useColors; + +/** + * Colors. + */ + +exports.colors = [6, 2, 3, 4, 5, 1]; + +/** + * Build up the default `inspectOpts` object from the environment variables. + * + * $ DEBUG_COLORS=no DEBUG_DEPTH=10 DEBUG_SHOW_HIDDEN=enabled node script.js + */ + +exports.inspectOpts = Object.keys(process.env).filter(function (key) { + return /^debug_/i.test(key); +}).reduce(function (obj, key) { + // camel-case + var prop = key + .substring(6) + .toLowerCase() + .replace(/_([a-z])/g, function (_, k) { return k.toUpperCase() }); + + // coerce string value into JS value + var val = process.env[key]; + if (/^(yes|on|true|enabled)$/i.test(val)) val = true; + else if (/^(no|off|false|disabled)$/i.test(val)) val = false; + else if (val === 'null') val = null; + else val = Number(val); + + obj[prop] = val; + return obj; +}, {}); + +/** + * The file descriptor to write the `debug()` calls to. + * Set the `DEBUG_FD` env variable to override with another value. i.e.: + * + * $ DEBUG_FD=3 node script.js 3>debug.log + */ + +var fd = parseInt(process.env.DEBUG_FD, 10) || 2; + +if (1 !== fd && 2 !== fd) { + util.deprecate(function(){}, 'except for stderr(2) and stdout(1), any other usage of DEBUG_FD is deprecated. Override debug.log if you want to use a different log function (https://git.io/debug_fd)')() +} + +var stream = 1 === fd ? process.stdout : + 2 === fd ? process.stderr : + createWritableStdioStream(fd); + +/** + * Is stdout a TTY? Colored output is enabled when `true`. + */ + +function useColors() { + return 'colors' in exports.inspectOpts + ? Boolean(exports.inspectOpts.colors) + : tty.isatty(fd); +} + +/** + * Map %o to `util.inspect()`, all on a single line. + */ + +exports.formatters.o = function(v) { + this.inspectOpts.colors = this.useColors; + return util.inspect(v, this.inspectOpts) + .split('\n').map(function(str) { + return str.trim() + }).join(' '); +}; + +/** + * Map %o to `util.inspect()`, allowing multiple lines if needed. + */ + +exports.formatters.O = function(v) { + this.inspectOpts.colors = this.useColors; + return util.inspect(v, this.inspectOpts); +}; + +/** + * Adds ANSI color escape codes if enabled. + * + * @api public + */ + +function formatArgs(args) { + var name = this.namespace; + var useColors = this.useColors; + + if (useColors) { + var c = this.color; + var prefix = ' \u001b[3' + c + ';1m' + name + ' ' + '\u001b[0m'; + + args[0] = prefix + args[0].split('\n').join('\n' + prefix); + args.push('\u001b[3' + c + 'm+' + exports.humanize(this.diff) + '\u001b[0m'); + } else { + args[0] = new Date().toUTCString() + + ' ' + name + ' ' + args[0]; + } +} + +/** + * Invokes `util.format()` with the specified arguments and writes to `stream`. + */ + +function log() { + return stream.write(util.format.apply(util, arguments) + '\n'); +} + +/** + * Save `namespaces`. + * + * @param {String} namespaces + * @api private + */ + +function save(namespaces) { + if (null == namespaces) { + // If you set a process.env field to null or undefined, it gets cast to the + // string 'null' or 'undefined'. Just delete instead. + delete process.env.DEBUG; + } else { + process.env.DEBUG = namespaces; + } +} + +/** + * Load `namespaces`. + * + * @return {String} returns the previously persisted debug modes + * @api private + */ + +function load() { + return process.env.DEBUG; +} + +/** + * Copied from `node/src/node.js`. + * + * XXX: It's lame that node doesn't expose this API out-of-the-box. It also + * relies on the undocumented `tty_wrap.guessHandleType()` which is also lame. + */ + +function createWritableStdioStream (fd) { + var stream; + var tty_wrap = process.binding('tty_wrap'); + + // Note stream._type is used for test-module-load-list.js + + switch (tty_wrap.guessHandleType(fd)) { + case 'TTY': + stream = new tty.WriteStream(fd); + stream._type = 'tty'; + + // Hack to have stream not keep the event loop alive. + // See https://github.com/joyent/node/issues/1726 + if (stream._handle && stream._handle.unref) { + stream._handle.unref(); + } + break; + + case 'FILE': + var fs = __webpack_require__(23); + stream = new fs.SyncWriteStream(fd, { autoClose: false }); + stream._type = 'fs'; + break; + + case 'PIPE': + case 'TCP': + var net = __webpack_require__(805); + stream = new net.Socket({ + fd: fd, + readable: false, + writable: true + }); + + // FIXME Should probably have an option in net.Socket to create a + // stream from an existing fd which is writable only. But for now + // we'll just add this hack and set the `readable` member to false. + // Test: ./node test/fixtures/echo.js < /etc/passwd + stream.readable = false; + stream.read = null; + stream._type = 'pipe'; + + // FIXME Hack to have stream not keep the event loop alive. + // See https://github.com/joyent/node/issues/1726 + if (stream._handle && stream._handle.unref) { + stream._handle.unref(); + } + break; + + default: + // Probably an error on in uv_guess_handle() + throw new Error('Implement me. Unknown stream file type!'); + } + + // For supporting legacy API we put the FD here. + stream.fd = fd; + + stream._isStdio = true; + + return stream; +} + +/** + * Init logic for `debug` instances. + * + * Create a new `inspectOpts` object in case `useColors` is set + * differently for a particular `debug` instance. + */ + +function init (debug) { + debug.inspectOpts = {}; + + var keys = Object.keys(exports.inspectOpts); + for (var i = 0; i < keys.length; i++) { + debug.inspectOpts[keys[i]] = exports.inspectOpts[keys[i]]; + } +} + +/** + * Enable namespaces listed in `process.env.DEBUG` initially. + */ + +exports.enable(load()); + + +/***/ }), +/* 868 */ +/***/ (function(module, exports, __webpack_require__) { + +"use strict"; + + +var brackets = __webpack_require__(858); +var define = __webpack_require__(869); +var utils = __webpack_require__(870); + +/** + * Characters to use in text regex (we want to "not" match + * characters that are matched by other parsers) + */ + +var TEXT_REGEX = '([!@*?+]?\\(|\\)|[*?.+\\\\]|\\[:?(?=.*\\])|:?\\])+'; +var not = utils.createRegex(TEXT_REGEX); + +/** + * Extglob parsers + */ + +function parsers(extglob) { + extglob.state = extglob.state || {}; + + /** + * Use `expand-brackets` parsers + */ + + extglob.use(brackets.parsers); + extglob.parser.sets.paren = extglob.parser.sets.paren || []; + extglob.parser + + /** + * Extglob open: "*(" + */ + + .capture('paren.open', function() { + var parsed = this.parsed; + var pos = this.position(); + var m = this.match(/^([!@*?+])?\(/); + if (!m) return; + + var prev = this.prev(); + var prefix = m[1]; + var val = m[0]; + + var open = pos({ + type: 'paren.open', + parsed: parsed, + val: val + }); + + var node = pos({ + type: 'paren', + prefix: prefix, + nodes: [open] + }); + + // if nested negation extglobs, just cancel them out to simplify + if (prefix === '!' && prev.type === 'paren' && prev.prefix === '!') { + prev.prefix = '@'; + node.prefix = '@'; + } + + define(node, 'rest', this.input); + define(node, 'parsed', parsed); + define(node, 'parent', prev); + define(open, 'parent', node); + + this.push('paren', node); + prev.nodes.push(node); + }) + + /** + * Extglob close: ")" + */ + + .capture('paren.close', function() { + var parsed = this.parsed; + var pos = this.position(); + var m = this.match(/^\)/); + if (!m) return; + + var parent = this.pop('paren'); + var node = pos({ + type: 'paren.close', + rest: this.input, + parsed: parsed, + val: m[0] + }); + + if (!this.isType(parent, 'paren')) { + if (this.options.strict) { + throw new Error('missing opening paren: "("'); + } + node.escaped = true; + return node; + } + + node.prefix = parent.prefix; + parent.nodes.push(node); + define(node, 'parent', parent); + }) + + /** + * Escape: "\\." + */ + + .capture('escape', function() { + var pos = this.position(); + var m = this.match(/^\\(.)/); + if (!m) return; + + return pos({ + type: 'escape', + val: m[0], + ch: m[1] + }); + }) + + /** + * Question marks: "?" + */ + + .capture('qmark', function() { + var parsed = this.parsed; + var pos = this.position(); + var m = this.match(/^\?+(?!\()/); + if (!m) return; + extglob.state.metachar = true; + return pos({ + type: 'qmark', + rest: this.input, + parsed: parsed, + val: m[0] + }); + }) + + /** + * Character parsers + */ + + .capture('star', /^\*(?!\()/) + .capture('plus', /^\+(?!\()/) + .capture('dot', /^\./) + .capture('text', not); +}; + +/** + * Expose text regex string + */ + +module.exports.TEXT_REGEX = TEXT_REGEX; + +/** + * Extglob parsers + */ + +module.exports = parsers; + + +/***/ }), +/* 869 */ +/***/ (function(module, exports, __webpack_require__) { + +"use strict"; +/*! + * define-property + * + * Copyright (c) 2015, 2017, Jon Schlinkert. + * Released under the MIT License. + */ + + + +var isDescriptor = __webpack_require__(759); + +module.exports = function defineProperty(obj, prop, val) { + if (typeof obj !== 'object' && typeof obj !== 'function') { + throw new TypeError('expected an object or function.'); + } + + if (typeof prop !== 'string') { + throw new TypeError('expected `prop` to be a string.'); + } + + if (isDescriptor(val) && ('set' in val || 'get' in val)) { + return Object.defineProperty(obj, prop, val); } return Object.defineProperty(obj, prop, { @@ -102014,14 +102759,14 @@ module.exports = function defineProperty(obj, prop, val) { /***/ }), -/* 866 */ +/* 870 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var regex = __webpack_require__(740); -var Cache = __webpack_require__(850); +var regex = __webpack_require__(739); +var Cache = __webpack_require__(849); /** * Utils @@ -102090,7 +102835,7 @@ utils.createRegex = function(str) { /***/ }), -/* 867 */ +/* 871 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -102100,16 +102845,16 @@ utils.createRegex = function(str) { * Module dependencies */ -var Snapdragon = __webpack_require__(768); -var define = __webpack_require__(865); -var extend = __webpack_require__(738); +var Snapdragon = __webpack_require__(767); +var define = __webpack_require__(869); +var extend = __webpack_require__(737); /** * Local dependencies */ -var compilers = __webpack_require__(858); -var parsers = __webpack_require__(864); +var compilers = __webpack_require__(857); +var parsers = __webpack_require__(868); /** * Customize Snapdragon parser and renderer @@ -102175,16 +102920,16 @@ module.exports = Extglob; /***/ }), -/* 868 */ +/* 872 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var extglob = __webpack_require__(857); -var nanomatch = __webpack_require__(842); -var regexNot = __webpack_require__(740); -var toRegex = __webpack_require__(830); +var extglob = __webpack_require__(856); +var nanomatch = __webpack_require__(841); +var regexNot = __webpack_require__(739); +var toRegex = __webpack_require__(829); var not; /** @@ -102265,14 +103010,14 @@ function textRegex(pattern) { /***/ }), -/* 869 */ +/* 873 */ /***/ (function(module, exports, __webpack_require__) { -module.exports = new (__webpack_require__(850))(); +module.exports = new (__webpack_require__(849))(); /***/ }), -/* 870 */ +/* 874 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -102285,13 +103030,13 @@ var path = __webpack_require__(16); * Module dependencies */ -var Snapdragon = __webpack_require__(768); -utils.define = __webpack_require__(837); -utils.diff = __webpack_require__(854); -utils.extend = __webpack_require__(838); -utils.pick = __webpack_require__(855); -utils.typeOf = __webpack_require__(871); -utils.unique = __webpack_require__(741); +var Snapdragon = __webpack_require__(767); +utils.define = __webpack_require__(836); +utils.diff = __webpack_require__(853); +utils.extend = __webpack_require__(837); +utils.pick = __webpack_require__(854); +utils.typeOf = __webpack_require__(875); +utils.unique = __webpack_require__(740); /** * Returns true if the platform is windows, or `path.sep` is `\\`. @@ -102588,7 +103333,7 @@ utils.unixify = function(options) { /***/ }), -/* 871 */ +/* 875 */ /***/ (function(module, exports) { var toString = Object.prototype.toString; @@ -102723,7 +103468,7 @@ function isBuffer(val) { /***/ }), -/* 872 */ +/* 876 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -102742,9 +103487,9 @@ var __extends = (this && this.__extends) || (function () { }; })(); Object.defineProperty(exports, "__esModule", { value: true }); -var readdir = __webpack_require__(873); -var reader_1 = __webpack_require__(886); -var fs_stream_1 = __webpack_require__(890); +var readdir = __webpack_require__(877); +var reader_1 = __webpack_require__(890); +var fs_stream_1 = __webpack_require__(894); var ReaderAsync = /** @class */ (function (_super) { __extends(ReaderAsync, _super); function ReaderAsync() { @@ -102805,15 +103550,15 @@ exports.default = ReaderAsync; /***/ }), -/* 873 */ +/* 877 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -const readdirSync = __webpack_require__(874); -const readdirAsync = __webpack_require__(882); -const readdirStream = __webpack_require__(885); +const readdirSync = __webpack_require__(878); +const readdirAsync = __webpack_require__(886); +const readdirStream = __webpack_require__(889); module.exports = exports = readdirAsyncPath; exports.readdir = exports.readdirAsync = exports.async = readdirAsyncPath; @@ -102897,7 +103642,7 @@ function readdirStreamStat (dir, options) { /***/ }), -/* 874 */ +/* 878 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -102905,11 +103650,11 @@ function readdirStreamStat (dir, options) { module.exports = readdirSync; -const DirectoryReader = __webpack_require__(875); +const DirectoryReader = __webpack_require__(879); let syncFacade = { - fs: __webpack_require__(880), - forEach: __webpack_require__(881), + fs: __webpack_require__(884), + forEach: __webpack_require__(885), sync: true }; @@ -102938,7 +103683,7 @@ function readdirSync (dir, options, internalOptions) { /***/ }), -/* 875 */ +/* 879 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -102947,9 +103692,9 @@ function readdirSync (dir, options, internalOptions) { const Readable = __webpack_require__(27).Readable; const EventEmitter = __webpack_require__(379).EventEmitter; const path = __webpack_require__(16); -const normalizeOptions = __webpack_require__(876); -const stat = __webpack_require__(878); -const call = __webpack_require__(879); +const normalizeOptions = __webpack_require__(880); +const stat = __webpack_require__(882); +const call = __webpack_require__(883); /** * Asynchronously reads the contents of a directory and streams the results @@ -103325,14 +104070,14 @@ module.exports = DirectoryReader; /***/ }), -/* 876 */ +/* 880 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; const path = __webpack_require__(16); -const globToRegExp = __webpack_require__(877); +const globToRegExp = __webpack_require__(881); module.exports = normalizeOptions; @@ -103509,7 +104254,7 @@ function normalizeOptions (options, internalOptions) { /***/ }), -/* 877 */ +/* 881 */ /***/ (function(module, exports) { module.exports = function (glob, opts) { @@ -103646,13 +104391,13 @@ module.exports = function (glob, opts) { /***/ }), -/* 878 */ +/* 882 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -const call = __webpack_require__(879); +const call = __webpack_require__(883); module.exports = stat; @@ -103727,7 +104472,7 @@ function symlinkStat (fs, path, lstats, callback) { /***/ }), -/* 879 */ +/* 883 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -103788,14 +104533,14 @@ function callOnce (fn) { /***/ }), -/* 880 */ +/* 884 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; const fs = __webpack_require__(23); -const call = __webpack_require__(879); +const call = __webpack_require__(883); /** * A facade around {@link fs.readdirSync} that allows it to be called @@ -103859,7 +104604,7 @@ exports.lstat = function (path, callback) { /***/ }), -/* 881 */ +/* 885 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -103888,7 +104633,7 @@ function syncForEach (array, iterator, done) { /***/ }), -/* 882 */ +/* 886 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -103896,12 +104641,12 @@ function syncForEach (array, iterator, done) { module.exports = readdirAsync; -const maybe = __webpack_require__(883); -const DirectoryReader = __webpack_require__(875); +const maybe = __webpack_require__(887); +const DirectoryReader = __webpack_require__(879); let asyncFacade = { fs: __webpack_require__(23), - forEach: __webpack_require__(884), + forEach: __webpack_require__(888), async: true }; @@ -103943,7 +104688,7 @@ function readdirAsync (dir, options, callback, internalOptions) { /***/ }), -/* 883 */ +/* 887 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -103970,7 +104715,7 @@ module.exports = function maybe (cb, promise) { /***/ }), -/* 884 */ +/* 888 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -104006,7 +104751,7 @@ function asyncForEach (array, iterator, done) { /***/ }), -/* 885 */ +/* 889 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -104014,11 +104759,11 @@ function asyncForEach (array, iterator, done) { module.exports = readdirStream; -const DirectoryReader = __webpack_require__(875); +const DirectoryReader = __webpack_require__(879); let streamFacade = { fs: __webpack_require__(23), - forEach: __webpack_require__(884), + forEach: __webpack_require__(888), async: true }; @@ -104038,16 +104783,16 @@ function readdirStream (dir, options, internalOptions) { /***/ }), -/* 886 */ +/* 890 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); var path = __webpack_require__(16); -var deep_1 = __webpack_require__(887); -var entry_1 = __webpack_require__(889); -var pathUtil = __webpack_require__(888); +var deep_1 = __webpack_require__(891); +var entry_1 = __webpack_require__(893); +var pathUtil = __webpack_require__(892); var Reader = /** @class */ (function () { function Reader(options) { this.options = options; @@ -104113,14 +104858,14 @@ exports.default = Reader; /***/ }), -/* 887 */ +/* 891 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); -var pathUtils = __webpack_require__(888); -var patternUtils = __webpack_require__(722); +var pathUtils = __webpack_require__(892); +var patternUtils = __webpack_require__(721); var DeepFilter = /** @class */ (function () { function DeepFilter(options, micromatchOptions) { this.options = options; @@ -104203,7 +104948,7 @@ exports.default = DeepFilter; /***/ }), -/* 888 */ +/* 892 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -104234,14 +104979,14 @@ exports.makeAbsolute = makeAbsolute; /***/ }), -/* 889 */ +/* 893 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); -var pathUtils = __webpack_require__(888); -var patternUtils = __webpack_require__(722); +var pathUtils = __webpack_require__(892); +var patternUtils = __webpack_require__(721); var EntryFilter = /** @class */ (function () { function EntryFilter(options, micromatchOptions) { this.options = options; @@ -104326,7 +105071,7 @@ exports.default = EntryFilter; /***/ }), -/* 890 */ +/* 894 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -104346,8 +105091,8 @@ var __extends = (this && this.__extends) || (function () { })(); Object.defineProperty(exports, "__esModule", { value: true }); var stream = __webpack_require__(27); -var fsStat = __webpack_require__(891); -var fs_1 = __webpack_require__(895); +var fsStat = __webpack_require__(895); +var fs_1 = __webpack_require__(899); var FileSystemStream = /** @class */ (function (_super) { __extends(FileSystemStream, _super); function FileSystemStream() { @@ -104397,14 +105142,14 @@ exports.default = FileSystemStream; /***/ }), -/* 891 */ +/* 895 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); -const optionsManager = __webpack_require__(892); -const statProvider = __webpack_require__(894); +const optionsManager = __webpack_require__(896); +const statProvider = __webpack_require__(898); /** * Asynchronous API. */ @@ -104435,13 +105180,13 @@ exports.statSync = statSync; /***/ }), -/* 892 */ +/* 896 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); -const fsAdapter = __webpack_require__(893); +const fsAdapter = __webpack_require__(897); function prepare(opts) { const options = Object.assign({ fs: fsAdapter.getFileSystemAdapter(opts ? opts.fs : undefined), @@ -104454,7 +105199,7 @@ exports.prepare = prepare; /***/ }), -/* 893 */ +/* 897 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -104477,7 +105222,7 @@ exports.getFileSystemAdapter = getFileSystemAdapter; /***/ }), -/* 894 */ +/* 898 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -104529,7 +105274,7 @@ exports.isFollowedSymlink = isFollowedSymlink; /***/ }), -/* 895 */ +/* 899 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -104560,7 +105305,7 @@ exports.default = FileSystem; /***/ }), -/* 896 */ +/* 900 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -104580,9 +105325,9 @@ var __extends = (this && this.__extends) || (function () { })(); Object.defineProperty(exports, "__esModule", { value: true }); var stream = __webpack_require__(27); -var readdir = __webpack_require__(873); -var reader_1 = __webpack_require__(886); -var fs_stream_1 = __webpack_require__(890); +var readdir = __webpack_require__(877); +var reader_1 = __webpack_require__(890); +var fs_stream_1 = __webpack_require__(894); var TransformStream = /** @class */ (function (_super) { __extends(TransformStream, _super); function TransformStream(reader) { @@ -104650,7 +105395,7 @@ exports.default = ReaderStream; /***/ }), -/* 897 */ +/* 901 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -104669,9 +105414,9 @@ var __extends = (this && this.__extends) || (function () { }; })(); Object.defineProperty(exports, "__esModule", { value: true }); -var readdir = __webpack_require__(873); -var reader_1 = __webpack_require__(886); -var fs_sync_1 = __webpack_require__(898); +var readdir = __webpack_require__(877); +var reader_1 = __webpack_require__(890); +var fs_sync_1 = __webpack_require__(902); var ReaderSync = /** @class */ (function (_super) { __extends(ReaderSync, _super); function ReaderSync() { @@ -104731,7 +105476,7 @@ exports.default = ReaderSync; /***/ }), -/* 898 */ +/* 902 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -104750,8 +105495,8 @@ var __extends = (this && this.__extends) || (function () { }; })(); Object.defineProperty(exports, "__esModule", { value: true }); -var fsStat = __webpack_require__(891); -var fs_1 = __webpack_require__(895); +var fsStat = __webpack_require__(895); +var fs_1 = __webpack_require__(899); var FileSystemSync = /** @class */ (function (_super) { __extends(FileSystemSync, _super); function FileSystemSync() { @@ -104797,7 +105542,7 @@ exports.default = FileSystemSync; /***/ }), -/* 899 */ +/* 903 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -104813,13 +105558,13 @@ exports.flatten = flatten; /***/ }), -/* 900 */ +/* 904 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); -var merge2 = __webpack_require__(589); +var merge2 = __webpack_require__(588); /** * Merge multiple streams and propagate their errors into one stream in parallel. */ @@ -104834,13 +105579,13 @@ exports.merge = merge; /***/ }), -/* 901 */ +/* 905 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; const path = __webpack_require__(16); -const pathType = __webpack_require__(902); +const pathType = __webpack_require__(906); const getExtensions = extensions => extensions.length > 1 ? `{${extensions.join(',')}}` : extensions[0]; @@ -104906,13 +105651,13 @@ module.exports.sync = (input, opts) => { /***/ }), -/* 902 */ +/* 906 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; const fs = __webpack_require__(23); -const pify = __webpack_require__(903); +const pify = __webpack_require__(907); function type(fn, fn2, fp) { if (typeof fp !== 'string') { @@ -104955,7 +105700,7 @@ exports.symlinkSync = typeSync.bind(null, 'lstatSync', 'isSymbolicLink'); /***/ }), -/* 903 */ +/* 907 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -105046,17 +105791,17 @@ module.exports = (obj, opts) => { /***/ }), -/* 904 */ +/* 908 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; const fs = __webpack_require__(23); const path = __webpack_require__(16); -const fastGlob = __webpack_require__(718); -const gitIgnore = __webpack_require__(905); -const pify = __webpack_require__(906); -const slash = __webpack_require__(907); +const fastGlob = __webpack_require__(717); +const gitIgnore = __webpack_require__(909); +const pify = __webpack_require__(910); +const slash = __webpack_require__(911); const DEFAULT_IGNORE = [ '**/node_modules/**', @@ -105154,7 +105899,7 @@ module.exports.sync = options => { /***/ }), -/* 905 */ +/* 909 */ /***/ (function(module, exports) { // A simple implementation of make-array @@ -105623,7 +106368,7 @@ module.exports = options => new IgnoreBase(options) /***/ }), -/* 906 */ +/* 910 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -105698,7 +106443,7 @@ module.exports = (input, options) => { /***/ }), -/* 907 */ +/* 911 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -105716,17 +106461,17 @@ module.exports = input => { /***/ }), -/* 908 */ +/* 912 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; const path = __webpack_require__(16); const {constants: fsConstants} = __webpack_require__(23); -const pEvent = __webpack_require__(909); -const CpFileError = __webpack_require__(912); -const fs = __webpack_require__(916); -const ProgressEmitter = __webpack_require__(919); +const pEvent = __webpack_require__(913); +const CpFileError = __webpack_require__(916); +const fs = __webpack_require__(920); +const ProgressEmitter = __webpack_require__(923); const cpFileAsync = async (source, destination, options, progressEmitter) => { let readError; @@ -105840,12 +106585,12 @@ module.exports.sync = (source, destination, options) => { /***/ }), -/* 909 */ +/* 913 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -const pTimeout = __webpack_require__(910); +const pTimeout = __webpack_require__(914); const symbolAsyncIterator = Symbol.asyncIterator || '@@asyncIterator'; @@ -106136,12 +106881,12 @@ module.exports.iterator = (emitter, event, options) => { /***/ }), -/* 910 */ +/* 914 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -const pFinally = __webpack_require__(911); +const pFinally = __webpack_require__(915); class TimeoutError extends Error { constructor(message) { @@ -106187,7 +106932,7 @@ module.exports.TimeoutError = TimeoutError; /***/ }), -/* 911 */ +/* 915 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -106209,12 +106954,12 @@ module.exports = (promise, onFinally) => { /***/ }), -/* 912 */ +/* 916 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -const NestedError = __webpack_require__(913); +const NestedError = __webpack_require__(917); class CpFileError extends NestedError { constructor(message, nested) { @@ -106228,10 +106973,10 @@ module.exports = CpFileError; /***/ }), -/* 913 */ +/* 917 */ /***/ (function(module, exports, __webpack_require__) { -var inherits = __webpack_require__(914); +var inherits = __webpack_require__(918); var NestedError = function (message, nested) { this.nested = nested; @@ -106282,7 +107027,7 @@ module.exports = NestedError; /***/ }), -/* 914 */ +/* 918 */ /***/ (function(module, exports, __webpack_require__) { try { @@ -106290,12 +107035,12 @@ try { if (typeof util.inherits !== 'function') throw ''; module.exports = util.inherits; } catch (e) { - module.exports = __webpack_require__(915); + module.exports = __webpack_require__(919); } /***/ }), -/* 915 */ +/* 919 */ /***/ (function(module, exports) { if (typeof Object.create === 'function') { @@ -106324,16 +107069,16 @@ if (typeof Object.create === 'function') { /***/ }), -/* 916 */ +/* 920 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; const {promisify} = __webpack_require__(29); const fs = __webpack_require__(22); -const makeDir = __webpack_require__(917); -const pEvent = __webpack_require__(909); -const CpFileError = __webpack_require__(912); +const makeDir = __webpack_require__(921); +const pEvent = __webpack_require__(913); +const CpFileError = __webpack_require__(916); const stat = promisify(fs.stat); const lstat = promisify(fs.lstat); @@ -106430,7 +107175,7 @@ exports.copyFileSync = (source, destination, flags) => { /***/ }), -/* 917 */ +/* 921 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -106438,7 +107183,7 @@ exports.copyFileSync = (source, destination, flags) => { const fs = __webpack_require__(23); const path = __webpack_require__(16); const {promisify} = __webpack_require__(29); -const semver = __webpack_require__(918); +const semver = __webpack_require__(922); const defaults = { mode: 0o777 & (~process.umask()), @@ -106587,7 +107332,7 @@ module.exports.sync = (input, options) => { /***/ }), -/* 918 */ +/* 922 */ /***/ (function(module, exports) { exports = module.exports = SemVer @@ -108189,7 +108934,7 @@ function coerce (version, options) { /***/ }), -/* 919 */ +/* 923 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -108230,7 +108975,7 @@ module.exports = ProgressEmitter; /***/ }), -/* 920 */ +/* 924 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -108276,12 +109021,12 @@ exports.default = module.exports; /***/ }), -/* 921 */ +/* 925 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -const NestedError = __webpack_require__(922); +const NestedError = __webpack_require__(926); class CpyError extends NestedError { constructor(message, nested) { @@ -108295,7 +109040,7 @@ module.exports = CpyError; /***/ }), -/* 922 */ +/* 926 */ /***/ (function(module, exports, __webpack_require__) { var inherits = __webpack_require__(29).inherits; @@ -108351,7 +109096,7 @@ module.exports = NestedError; /***/ }), -/* 923 */ +/* 927 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; diff --git a/packages/kbn-ui-shared-deps/package.json b/packages/kbn-ui-shared-deps/package.json index e2823f23d0431..c8614b1df9d5d 100644 --- a/packages/kbn-ui-shared-deps/package.json +++ b/packages/kbn-ui-shared-deps/package.json @@ -9,7 +9,7 @@ "kbn:watch": "node scripts/build --watch" }, "dependencies": { - "@elastic/charts": "^18.1.1", + "@elastic/charts": "18.3.0", "@elastic/eui": "21.0.1", "@kbn/i18n": "1.0.0", "abortcontroller-polyfill": "^1.4.0", diff --git a/rfcs/text/0002_encrypted_attributes.md b/rfcs/text/0002_encrypted_attributes.md index aa7307edb66bd..c6553c177d995 100644 --- a/rfcs/text/0002_encrypted_attributes.md +++ b/rfcs/text/0002_encrypted_attributes.md @@ -166,7 +166,7 @@ take a look at the source code of this library to know how encryption is perform parameters are used, but in short it's AES Encryption with AES-256-GCM that uses random initialization vector and salt. As with encryption key for Kibana's session cookie, master encryption key used by `encrypted_saved_objects` plugin can be -defined as a configuration value (`xpack.encrypted_saved_objects.encryptionKey`) via `kibana.yml`, but it's **highly +defined as a configuration value (`xpack.encryptedSavedObjects.encryptionKey`) via `kibana.yml`, but it's **highly recommended** to define this key in the [Kibana Keystore](https://www.elastic.co/guide/en/kibana/current/secure-settings.html) instead. The master key should be cryptographically safe and be equal or greater than 32 bytes. diff --git a/src/cli/cluster/cluster_manager.ts b/src/cli/cluster/cluster_manager.ts index 44b6c39556afd..a87e2aa11f2c0 100644 --- a/src/cli/cluster/cluster_manager.ts +++ b/src/cli/cluster/cluster_manager.ts @@ -263,7 +263,7 @@ export class ClusterManager { ...pluginInternalDirsIgnore, fromRoot('src/legacy/server/sass/__tmp__'), fromRoot('x-pack/legacy/plugins/reporting/.chromium'), - fromRoot('x-pack/legacy/plugins/siem/cypress'), + fromRoot('x-pack/plugins/siem/cypress'), fromRoot('x-pack/legacy/plugins/apm/e2e'), fromRoot('x-pack/legacy/plugins/apm/scripts'), fromRoot('x-pack/legacy/plugins/canvas/canvas_plugin_src'), // prevents server from restarting twice for Canvas plugin changes, diff --git a/src/core/CONVENTIONS.md b/src/core/CONVENTIONS.md index 0f592d108c561..a82cc27839a1d 100644 --- a/src/core/CONVENTIONS.md +++ b/src/core/CONVENTIONS.md @@ -1,6 +1,6 @@ -# Kibana Conventions - -- [Kibana Conventions](#kibana-conventions) +- [Organisational Conventions](#organisational-conventions) + - [Definition of done](#definition-of-done) +- [Technical Conventions](#technical-conventions) - [Plugin Structure](#plugin-structure) - [The PluginInitializer](#the-plugininitializer) - [The Plugin class](#the-plugin-class) @@ -8,8 +8,34 @@ - [Services](#services) - [Usage Collection](#usage-collection) - [Saved Objects Types](#saved-objects-types) - -## Plugin Structure + - [Naming conventions](#naming-conventions) + +## Organisational Conventions +### Definition of done +Definition of done for a feature: +- has a dedicated Github issue describing problem space +- an umbrella task closed/updated with follow-ups +- all code review comments are resolved +- has been verified manually by at least one reviewer +- can be used by first & third party plugins +- there is no contradiction between client and server API +- works for OSS version + - works with and without a `server.basePath` configured + - cannot crash the Kibana server when it fails +- works for the commercial version with a license + - for a logged-in user + - for anonymous user + - compatible with Spaces +- has unit & integration tests for public contracts +- has functional tests for user scenarios +- uses standard tooling: + - code - `TypeScript` + - UI - `React` + - tests - `jest` & `FTR` +- has documentation for the public contract, provides a usage example + +## Technical Conventions +### Plugin Structure All Kibana plugins built at Elastic should follow the same structure. @@ -60,7 +86,7 @@ my_plugin/ - More should be fleshed out here... - Usage collectors for Telemetry should be defined in a separate `server/collectors/` directory. -### The PluginInitializer +#### The PluginInitializer ```ts // my_plugin/public/index.ts @@ -75,7 +101,7 @@ export { } ``` -### The Plugin class +#### The Plugin class ```ts // my_plugin/public/plugin.ts @@ -130,7 +156,7 @@ Difference between `setup` and `start`: The bulk of your plugin logic will most likely live inside _handlers_ registered during `setup`. -### Applications +#### Applications It's important that UI code is not included in the main bundle for your plugin. Our webpack configuration supports dynamic async imports to split out imports into a separate bundle. Every app's rendering logic and UI code should @@ -175,7 +201,7 @@ export class MyPlugin implements Plugin { } ``` -### Services +#### Services Service structure should mirror the plugin lifecycle to make reasoning about how the service is executed more clear. @@ -226,7 +252,7 @@ export class Plugin { } ``` -### Usage Collection +#### Usage Collection For creating and registering a Usage Collector. Collectors should be defined in a separate directory `server/collectors/`. You can read more about usage collectors on `src/plugins/usage_collection/README.md`. @@ -263,7 +289,7 @@ export function registerMyPluginUsageCollector(usageCollection?: UsageCollection } ``` -### Saved Objects Types +#### Saved Objects Types Saved object type definitions should be defined in their own `server/saved_objects` directory. diff --git a/src/core/MIGRATION.md b/src/core/MIGRATION.md index 5d7b467052029..80f12dd78214d 100644 --- a/src/core/MIGRATION.md +++ b/src/core/MIGRATION.md @@ -24,6 +24,7 @@ - [7. Switch to new platform services](#7-switch-to-new-platform-services) - [8. Migrate to the new plugin system](#8-migrate-to-the-new-plugin-system) - [Bonus: Tips for complex migration scenarios](#bonus-tips-for-complex-migration-scenarios) + - [Keep Kibana fast](#keep-kibana-fast) - [Frequently asked questions](#frequently-asked-questions) - [Is migrating a plugin an all-or-nothing thing?](#is-migrating-a-plugin-an-all-or-nothing-thing) - [Do plugins need to be converted to TypeScript?](#do-plugins-need-to-be-converted-to-typescript) @@ -933,6 +934,66 @@ For a few plugins, some of these steps (such as angular removal) could be a mont One convention that is useful for this is creating a dedicated `public/np_ready` directory to house the code that is ready to migrate, and gradually move more and more code into it until the rest of your plugin is essentially empty. At that point, you'll be able to copy your `index.ts`, `plugin.ts`, and the contents of `./np_ready` over into your plugin in the new platform, leaving your legacy shim behind. This carries the added benefit of providing a way for us to introduce helpful tooling in the future, such as [custom eslint rules](https://github.com/elastic/kibana/pull/40537), which could be run against that specific directory to ensure your code is ready to migrate. +## Keep Kibana fast +**tl;dr**: Load as much code lazily as possible. +Everyone loves snappy applications with responsive UI and hates spinners. Users deserve the best user experiences regardless of whether they run Kibana locally or in the cloud, regardless of their hardware & environment. +There are 2 main aspects of the perceived speed of an application: loading time and responsiveness to user actions. +New platform loads and bootstraps **all** the plugins whenever a user lands on any page. It means that adding every new application affects overall **loading performance** in the new platform, as plugin code is loaded **eagerly** to initialize the plugin and provide plugin API to dependent plugins. +However, it's usually not necessary that the whole plugin code should be loaded and initialized at once. The plugin could keep on loading code covering API functionality on Kibana bootstrap but load UI related code lazily on-demand, when an application page or management section is mounted. +Always prefer to require UI root components lazily when possible (such as in mount handlers). Even if their size may seem negligible, they are likely using some heavy-weight libraries that will also be removed from the initial plugin bundle, therefore, reducing its size by a significant amount. + +```typescript +import { Plugin, CoreSetup, AppMountParameters } from 'src/core/public'; +export class MyPlugin implements Plugin { + setup(core: CoreSetup, plugins: SetupDeps){ + core.application.register({ + id: 'app', + title: 'My app', + async mount(params: AppMountParameters) { + const { mountApp } = await import('./app/mount_app'); + return mountApp(await core.getStartServices(), params); + }, + }); + plugins.management.sections.getSection('another').registerApp({ + id: 'app', + title: 'My app', + order: 1, + async mount(params) { + const { mountManagementSection } = await import('./app/mount_management_section'); + return mountManagementSection(coreSetup, params); + }, + }) + return { + doSomething(){} + } + } +} +``` + +#### How to understand how big the bundle size of my plugin is? +New platform plugins are distributed as a pre-built with `@kbn/optimizer` package artifacts. It allows us to get rid of the shipping of `optimizer` in the distributable version of Kibana. +Every NP plugin artifact contains all plugin dependencies required to run the plugin, except some stateful dependencies shared across plugin bundles via `@kbn/ui-shared-deps`. +It means that NP plugin artifacts tend to have a bigger size than the legacy platform version. +To understand the current size of your plugin artifact, run `@kbn/optimizer` as +```bash +node scripts/build_kibana_platform_plugins.js --dist --no-examples +``` +and check the output in the `target` sub-folder of your plugin folder +```bash +ls -lh plugins/my_plugin/target/public/ +# output +# an async chunk loaded on demand +... 262K 0.plugin.js +# eagerly loaded chunk +... 50K my_plugin.plugin.js +``` +you might see at least one js bundle - `my_plugin.plugin.js`. This is the only artifact loaded by the platform during bootstrap in the browser. The rule of thumb is to keep its size as small as possible. +Other lazily loaded parts of your plugin present in the same folder as separate chunks under `{number}.plugin.js` names. +If you want to investigate what your plugin bundle consists of you need to run `@kbn/optimizer` with `--profile` flag to get generated [webpack stats file](https://webpack.js.org/api/stats/). +Many OSS tools are allowing you to analyze generated stats file +- [an official tool](http://webpack.github.io/analyse/#modules) from webpack authors +- [webpack-visualizer](https://chrisbateman.github.io/webpack-visualizer/) + ## Frequently asked questions ### Is migrating a plugin an all-or-nothing thing? @@ -1191,26 +1252,27 @@ import { npStart: { plugins } } from 'ui/new_platform'; In server code, `core` can be accessed from either `server.newPlatform` or `kbnServer.newPlatform`. There are not currently very many services available on the server-side: -| Legacy Platform | New Platform | Notes | -| ----------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------- | -| `server.config()` | [`initializerContext.config.create()`](/docs/development/core/server/kibana-plugin-core-server.plugininitializercontext.config.md) | Must also define schema. See _[how to configure plugin](#configure-plugin)_ | -| `server.route` | [`core.http.createRouter`](/docs/development/core/server/kibana-plugin-core-server.httpservicesetup.createrouter.md) | [Examples](./MIGRATION_EXAMPLES.md#route-registration) | -| `server.renderApp()` / `server.renderAppWithDefaultConfig()` | [`context.rendering.render()`](/docs/development/core/server/kibana-plugin-core-server.iscopedrenderingclient.render.md) | [Examples](./MIGRATION_EXAMPLES.md#render-html-content) | -| `request.getBasePath()` | [`core.http.basePath.get`](/docs/development/core/server/kibana-plugin-core-server.httpservicesetup.basepath.md) | | +| Legacy Platform | New Platform | Notes | +| ----------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------- | +| `server.config()` | [`initializerContext.config.create()`](/docs/development/core/server/kibana-plugin-core-server.plugininitializercontext.config.md) | Must also define schema. See _[how to configure plugin](#configure-plugin)_ | +| `server.route` | [`core.http.createRouter`](/docs/development/core/server/kibana-plugin-core-server.httpservicesetup.createrouter.md) | [Examples](./MIGRATION_EXAMPLES.md#route-registration) | +| `server.renderApp()` | [`response.renderCoreApp()`](docs/development/core/server/kibana-plugin-core-server.httpresourcesservicetoolkit.rendercoreapp.md) | [Examples](./MIGRATION_EXAMPLES.md#render-html-content) | +| `server.renderAppWithDefaultConfig()` | [`response.renderAnonymousCoreApp()`](docs/development/core/server/kibana-plugin-core-server.httpresourcesservicetoolkit.renderanonymouscoreapp.md) | [Examples](./MIGRATION_EXAMPLES.md#render-html-content) | +| `request.getBasePath()` | [`core.http.basePath.get`](/docs/development/core/server/kibana-plugin-core-server.httpservicesetup.basepath.md) | | | `server.plugins.elasticsearch.getCluster('data')` | [`context.core.elasticsearch.dataClient`](/docs/development/core/server/kibana-plugin-core-server.iscopedclusterclient.md) | | | `server.plugins.elasticsearch.getCluster('admin')` | [`context.core.elasticsearch.adminClient`](/docs/development/core/server/kibana-plugin-core-server.iscopedclusterclient.md) | | -| `server.plugins.elasticsearch.createCluster(...)` | [`core.elasticsearch.legacy.createClient`](/docs/development/core/server/kibana-plugin-core-server.elasticsearchservicestart.legacy.md) | | -| `server.savedObjects.setScopedSavedObjectsClientFactory` | [`core.savedObjects.setClientFactoryProvider`](/docs/development/core/server/kibana-plugin-core-server.savedobjectsservicesetup.setclientfactoryprovider.md) | | -| `server.savedObjects.addScopedSavedObjectsClientWrapperFactory` | [`core.savedObjects.addClientWrapper`](/docs/development/core/server/kibana-plugin-core-server.savedobjectsservicesetup.addclientwrapper.md) | | +| `server.plugins.elasticsearch.createCluster(...)` | [`core.elasticsearch.legacy.createClient`](/docs/development/core/server/kibana-plugin-core-server.elasticsearchservicestart.legacy.md) | | +| `server.savedObjects.setScopedSavedObjectsClientFactory` | [`core.savedObjects.setClientFactoryProvider`](/docs/development/core/server/kibana-plugin-core-server.savedobjectsservicesetup.setclientfactoryprovider.md) | | +| `server.savedObjects.addScopedSavedObjectsClientWrapperFactory` | [`core.savedObjects.addClientWrapper`](/docs/development/core/server/kibana-plugin-core-server.savedobjectsservicesetup.addclientwrapper.md) | | | `server.savedObjects.getSavedObjectsRepository` | [`core.savedObjects.createInternalRepository`](/docs/development/core/server/kibana-plugin-core-server.savedobjectsservicestart.createinternalrepository.md) [`core.savedObjects.createScopedRepository`](/docs/development/core/server/kibana-plugin-core-server.savedobjectsservicestart.createscopedrepository.md) | | -| `server.savedObjects.getScopedSavedObjectsClient` | [`core.savedObjects.getScopedClient`](/docs/development/core/server/kibana-plugin-core-server.savedobjectsservicestart.getscopedclient.md) | | -| `request.getSavedObjectsClient` | [`context.core.savedObjects.client`](/docs/development/core/server/kibana-plugin-core-server.requesthandlercontext.core.md) | | +| `server.savedObjects.getScopedSavedObjectsClient` | [`core.savedObjects.getScopedClient`](/docs/development/core/server/kibana-plugin-core-server.savedobjectsservicestart.getscopedclient.md) | | +| `request.getSavedObjectsClient` | [`context.core.savedObjects.client`](/docs/development/core/server/kibana-plugin-core-server.requesthandlercontext.core.md) | | | `request.getUiSettingsService` | [`context.core.uiSettings.client`](/docs/development/core/server/kibana-plugin-core-server.iuisettingsclient.md) | | -| `kibana.Plugin.deprecations` | [Handle plugin configuration deprecations](#handle-plugin-config-deprecations) and [`PluginConfigDescriptor.deprecations`](docs/development/core/server/kibana-plugin-core-server.pluginconfigdescriptor.md) | Deprecations from New Platform are not applied to legacy configuration | -| `kibana.Plugin.savedObjectSchemas` | [`core.savedObjects.registerType`](docs/development/core/server/kibana-plugin-core-server.savedobjectsservicesetup.registertype.md) | [Examples](./MIGRATION_EXAMPLES.md#saved-objects-types) | -| `kibana.Plugin.mappings` | [`core.savedObjects.registerType`](docs/development/core/server/kibana-plugin-core-server.savedobjectsservicesetup.registertype.md) | [Examples](./MIGRATION_EXAMPLES.md#saved-objects-types) | -| `kibana.Plugin.migrations` | [`core.savedObjects.registerType`](docs/development/core/server/kibana-plugin-core-server.savedobjectsservicesetup.registertype.md) | [Examples](./MIGRATION_EXAMPLES.md#saved-objects-types) | -| `kibana.Plugin.savedObjectsManagement` | [`core.savedObjects.registerType`](docs/development/core/server/kibana-plugin-core-server.savedobjectsservicesetup.registertype.md) | [Examples](./MIGRATION_EXAMPLES.md#saved-objects-types) | +| `kibana.Plugin.deprecations` | [Handle plugin configuration deprecations](#handle-plugin-config-deprecations) and [`PluginConfigDescriptor.deprecations`](docs/development/core/server/kibana-plugin-core-server.pluginconfigdescriptor.md) | Deprecations from New Platform are not applied to legacy configuration | +| `kibana.Plugin.savedObjectSchemas` | [`core.savedObjects.registerType`](docs/development/core/server/kibana-plugin-core-server.savedobjectsservicesetup.registertype.md) | [Examples](./MIGRATION_EXAMPLES.md#saved-objects-types) | +| `kibana.Plugin.mappings` | [`core.savedObjects.registerType`](docs/development/core/server/kibana-plugin-core-server.savedobjectsservicesetup.registertype.md) | [Examples](./MIGRATION_EXAMPLES.md#saved-objects-types) | +| `kibana.Plugin.migrations` | [`core.savedObjects.registerType`](docs/development/core/server/kibana-plugin-core-server.savedobjectsservicesetup.registertype.md) | [Examples](./MIGRATION_EXAMPLES.md#saved-objects-types) | +| `kibana.Plugin.savedObjectsManagement` | [`core.savedObjects.registerType`](docs/development/core/server/kibana-plugin-core-server.savedobjectsservicesetup.registertype.md) | [Examples](./MIGRATION_EXAMPLES.md#saved-objects-types) | _See also: [Server's CoreSetup API Docs](/docs/development/core/server/kibana-plugin-core-server.coresetup.md)_ @@ -1433,8 +1495,9 @@ The above example looks in the new platform as ``` The [request handler context](/docs/development/core/server/kibana-plugin-core-server.requesthandlercontext.md) exposed the next scoped **core** services: -| Legacy Platform | New Platform | -| --------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------| + +| Legacy Platform | New Platform | +| --------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------- | | `request.getSavedObjectsClient` | [`context.savedObjects.client`](/docs/development/core/server/kibana-plugin-core-server.savedobjectsclient.md) | | `server.plugins.elasticsearch.getCluster('admin')` | [`context.elasticsearch.adminClient`](/docs/development/core/server/kibana-plugin-core-server.iscopedclusterclient.md) | | `server.plugins.elasticsearch.getCluster('data')` | [`context.elasticsearch.dataClient`](/docs/development/core/server/kibana-plugin-core-server.iscopedclusterclient.md) | diff --git a/src/core/MIGRATION_EXAMPLES.md b/src/core/MIGRATION_EXAMPLES.md index 37d0b9297ed3c..8c5fe4875aaea 100644 --- a/src/core/MIGRATION_EXAMPLES.md +++ b/src/core/MIGRATION_EXAMPLES.md @@ -700,21 +700,15 @@ application.register({ ## Render HTML Content You can return a blank HTML page bootstrapped with the core application bundle from an HTTP route handler -via the `rendering` context. You may wish to do this if you are rendering a chromeless application with a +via the `httpResources` service. You may wish to do this if you are rendering a chromeless application with a custom application route or have other custom rendering needs. -```ts -router.get( +```typescript +httpResources.register( { path: '/chromeless', validate: false }, (context, request, response) => { - const { http, rendering } = context.core; - - return response.ok({ - body: await rendering.render(), // generates an HTML document - headers: { - 'content-security-policy': http.csp.header, - }, - }); + //... some logic + return response.renderCoreApp(); } ); ``` @@ -724,18 +718,12 @@ comprises all UI Settings that are *user provided*, then injected into the page. You may wish to exclude fetching this data if not authorized or to slim the page size. -```ts -router.get( - { path: '/', validate: false }, +```typescript +httpResources.register( + { path: '/', validate: false, options: { authRequired: false } }, (context, request, response) => { - const { http, rendering } = context.core; - - return response.ok({ - body: await rendering.render({ includeUserSettings: false }), - headers: { - 'content-security-policy': http.csp.header, - }, - }); + //... some logic + return response.renderAnonymousCoreApp(); } ); ``` diff --git a/src/core/public/application/scoped_history.test.ts b/src/core/public/application/scoped_history.test.ts index c01eb50830516..a56cffef1e2f2 100644 --- a/src/core/public/application/scoped_history.test.ts +++ b/src/core/public/application/scoped_history.test.ts @@ -268,11 +268,32 @@ describe('ScopedHistory', () => { const gh = createMemoryHistory(); gh.push('/app/wow'); const h = new ScopedHistory(gh, '/app/wow'); - expect(h.createHref({ pathname: '' })).toEqual(`/`); + expect(h.createHref({ pathname: '' })).toEqual(`/app/wow`); + expect(h.createHref({})).toEqual(`/app/wow`); expect(h.createHref({ pathname: '/new-page', search: '?alpha=true' })).toEqual( - `/new-page?alpha=true` + `/app/wow/new-page?alpha=true` ); }); + + it('behave correctly with slash-ending basePath', () => { + const gh = createMemoryHistory(); + gh.push('/app/wow/'); + const h = new ScopedHistory(gh, '/app/wow/'); + expect(h.createHref({ pathname: '' })).toEqual(`/app/wow/`); + expect(h.createHref({ pathname: '/new-page', search: '?alpha=true' })).toEqual( + `/app/wow/new-page?alpha=true` + ); + }); + + it('skips the scoped history path when `prependBasePath` is false', () => { + const gh = createMemoryHistory(); + gh.push('/app/wow'); + const h = new ScopedHistory(gh, '/app/wow'); + expect(h.createHref({ pathname: '' }, { prependBasePath: false })).toEqual(`/`); + expect( + h.createHref({ pathname: '/new-page', search: '?alpha=true' }, { prependBasePath: false }) + ).toEqual(`/new-page?alpha=true`); + }); }); describe('action', () => { diff --git a/src/core/public/application/scoped_history.ts b/src/core/public/application/scoped_history.ts index c5febc7604feb..9fa8f0b7f8148 100644 --- a/src/core/public/application/scoped_history.ts +++ b/src/core/public/application/scoped_history.ts @@ -219,11 +219,26 @@ export class ScopedHistory /** * Creates an href (string) to the location. + * If `prependBasePath` is true (default), it will prepend the location's path with the scoped history basePath. * * @param location + * @param prependBasePath */ - public createHref = (location: LocationDescriptorObject): Href => { + public createHref = ( + location: LocationDescriptorObject, + { prependBasePath = true }: { prependBasePath?: boolean } = {} + ): Href => { this.verifyActive(); + if (prependBasePath) { + location = this.prependBasePath(location); + if (location.pathname === undefined) { + // we always want to create an url relative to the basePath + // so if pathname is not present, we use the history's basePath as default + // we are doing that here because `prependBasePath` should not + // alter pathname for other method calls + location.pathname = this.basePath; + } + } return this.parentHistory.createHref(location); }; @@ -254,8 +269,7 @@ export class ScopedHistory * Prepends the base path to string. */ private prependBasePathToString(path: string): string { - path = path.startsWith('/') ? path.slice(1) : path; - return path.length ? `${this.basePath}/${path}` : this.basePath; + return path.length ? `${this.basePath}/${path}`.replace(/\/{2,}/g, '/') : this.basePath; } /** diff --git a/src/core/public/mocks.ts b/src/core/public/mocks.ts index 0c4930592b233..959ffaa7e7e08 100644 --- a/src/core/public/mocks.ts +++ b/src/core/public/mocks.ts @@ -48,6 +48,7 @@ export { overlayServiceMock } from './overlays/overlay_service.mock'; export { uiSettingsServiceMock } from './ui_settings/ui_settings_service.mock'; export { savedObjectsServiceMock } from './saved_objects/saved_objects_service.mock'; export { scopedHistoryMock } from './application/scoped_history.mock'; +export { applicationServiceMock } from './application/application_service.mock'; function createCoreSetupMock({ basePath = '', @@ -62,9 +63,8 @@ function createCoreSetupMock({ application: applicationServiceMock.createSetupContract(), context: contextServiceMock.createSetupContract(), fatalErrors: fatalErrorsServiceMock.createSetupContract(), - getStartServices: jest.fn, object, any]>, []>( - () => - Promise.resolve([createCoreStartMock({ basePath }), pluginStartDeps, pluginStartContract]) + getStartServices: jest.fn, any, any]>, []>(() => + Promise.resolve([createCoreStartMock({ basePath }), pluginStartDeps, pluginStartContract]) ), http: httpServiceMock.createSetupContract({ basePath }), notifications: notificationServiceMock.createSetupContract(), diff --git a/src/core/public/public.api.md b/src/core/public/public.api.md index 9f7f649f1e2a5..6d95d1bc7069c 100644 --- a/src/core/public/public.api.md +++ b/src/core/public/public.api.md @@ -956,6 +956,7 @@ export interface SavedObject { }; id: string; migrationVersion?: SavedObjectsMigrationVersion; + namespaces?: string[]; references: SavedObjectReference[]; type: string; updated_at?: string; @@ -1195,7 +1196,9 @@ export class ScopedHistory implements History | undefined) => UnregisterCallback; - createHref: (location: LocationDescriptorObject) => string; + createHref: (location: LocationDescriptorObject, { prependBasePath }?: { + prependBasePath?: boolean | undefined; + }) => string; createSubHistory: (basePath: string) => ScopedHistory; go: (n: number) => void; goBack: () => void; diff --git a/src/core/server/config/env.test.ts b/src/core/server/config/env.test.ts index c244012e34469..0fffcc44781d9 100644 --- a/src/core/server/config/env.test.ts +++ b/src/core/server/config/env.test.ts @@ -164,6 +164,17 @@ test('pluginSearchPaths contains examples plugins path if --run-examples flag is expect(env.pluginSearchPaths).toContain('/some/home/dir/examples'); }); +test('pluginSearchPaths contains x-pack/examples plugins path if --run-examples flag is true', () => { + const env = new Env( + '/some/home/dir', + getEnvOptions({ + cliArgs: { runExamples: true }, + }) + ); + + expect(env.pluginSearchPaths).toContain('/some/home/dir/x-pack/examples'); +}); + test('pluginSearchPaths does not contains examples plugins path if --run-examples flag is false', () => { const env = new Env( '/some/home/dir', @@ -174,3 +185,14 @@ test('pluginSearchPaths does not contains examples plugins path if --run-example expect(env.pluginSearchPaths).not.toContain('/some/home/dir/examples'); }); + +test('pluginSearchPaths does not contains x-pack/examples plugins path if --run-examples flag is false', () => { + const env = new Env( + '/some/home/dir', + getEnvOptions({ + cliArgs: { runExamples: false }, + }) + ); + + expect(env.pluginSearchPaths).not.toContain('/some/home/dir/x-pack/examples'); +}); diff --git a/src/core/server/config/env.ts b/src/core/server/config/env.ts index 05a8f40a09a88..d8068c5b383fa 100644 --- a/src/core/server/config/env.ts +++ b/src/core/server/config/env.ts @@ -109,7 +109,9 @@ export class Env { resolve(this.homeDir, 'src', 'plugins'), ...(options.cliArgs.oss ? [] : [resolve(this.homeDir, 'x-pack', 'plugins')]), resolve(this.homeDir, 'plugins'), - ...(options.cliArgs.runExamples ? [resolve(this.homeDir, 'examples')] : []), + ...(options.cliArgs.runExamples + ? [resolve(this.homeDir, 'examples'), resolve(this.homeDir, 'x-pack', 'examples')] + : []), resolve(this.homeDir, '..', 'kibana-extra'), ]; diff --git a/src/core/server/elasticsearch/elasticsearch_service.ts b/src/core/server/elasticsearch/elasticsearch_service.ts index 684f6e15caff9..18725f04a05b5 100644 --- a/src/core/server/elasticsearch/elasticsearch_service.ts +++ b/src/core/server/elasticsearch/elasticsearch_service.ts @@ -33,7 +33,12 @@ import { CoreService } from '../../types'; import { merge } from '../../utils'; import { CoreContext } from '../core_context'; import { Logger } from '../logging'; -import { ClusterClient, ScopeableRequest } from './cluster_client'; +import { + ClusterClient, + ScopeableRequest, + IClusterClient, + ICustomClusterClient, +} from './cluster_client'; import { ElasticsearchClientConfig } from './elasticsearch_client_config'; import { ElasticsearchConfig, ElasticsearchConfigType } from './elasticsearch_config'; import { InternalHttpServiceSetup, GetAuthHeaders } from '../http/'; @@ -58,12 +63,14 @@ export class ElasticsearchService implements CoreService { private readonly log: Logger; private readonly config$: Observable; - private subscription: Subscription | undefined; + private subscription?: Subscription; private stop$ = new Subject(); private kibanaVersion: string; - createClient: InternalElasticsearchServiceSetup['createClient'] | undefined; - dataClient: InternalElasticsearchServiceSetup['dataClient'] | undefined; - adminClient: InternalElasticsearchServiceSetup['adminClient'] | undefined; + private createClient?: ( + type: string, + clientConfig?: Partial + ) => ICustomClusterClient; + private adminClient?: IClusterClient; constructor(private readonly coreContext: CoreContext) { this.kibanaVersion = coreContext.env.packageInfo.version; diff --git a/src/core/server/http/index.ts b/src/core/server/http/index.ts index a75eb04fa0120..ca9dfde2e71dc 100644 --- a/src/core/server/http/index.ts +++ b/src/core/server/http/index.ts @@ -38,6 +38,7 @@ export { LifecycleResponseFactory, RedirectResponseOptions, RequestHandler, + RequestHandlerWrapper, ResponseError, ResponseErrorAttributes, ResponseHeaders, diff --git a/src/core/server/http/lifecycle/on_pre_response.ts b/src/core/server/http/lifecycle/on_pre_response.ts index 50d3d7b47bf8d..050881472bc80 100644 --- a/src/core/server/http/lifecycle/on_pre_response.ts +++ b/src/core/server/http/lifecycle/on_pre_response.ts @@ -148,7 +148,7 @@ function findHeadersIntersection( log: Logger ) { Object.keys(headers).forEach(headerName => { - if (responseHeaders[headerName] !== undefined) { + if (Reflect.has(responseHeaders, headerName)) { log.warn(`onPreResponseHandler rewrote a response header [${headerName}].`); } }); diff --git a/src/core/server/http/router/error_wrapper.ts b/src/core/server/http/router/error_wrapper.ts index 8f895753c38c3..af99812eff4b3 100644 --- a/src/core/server/http/router/error_wrapper.ts +++ b/src/core/server/http/router/error_wrapper.ts @@ -18,20 +18,10 @@ */ import Boom from 'boom'; -import { KibanaRequest } from './request'; -import { KibanaResponseFactory } from './response'; -import { RequestHandler } from './router'; -import { RequestHandlerContext } from '../../../server'; -import { RouteMethod } from './route'; +import { RequestHandlerWrapper } from './router'; -export const wrapErrors = ( - handler: RequestHandler -): RequestHandler => { - return async ( - context: RequestHandlerContext, - request: KibanaRequest, - response: KibanaResponseFactory - ) => { +export const wrapErrors: RequestHandlerWrapper = handler => { + return async (context, request, response) => { try { return await handler(context, request, response); } catch (e) { diff --git a/src/core/server/http/router/headers.ts b/src/core/server/http/router/headers.ts index 19eaee5081996..b79cc0d325f1e 100644 --- a/src/core/server/http/router/headers.ts +++ b/src/core/server/http/router/headers.ts @@ -56,9 +56,9 @@ export type Headers = { [header in KnownHeaders]?: string | string[] | undefined * Http response headers to set. * @public */ -export type ResponseHeaders = { [header in KnownHeaders]?: string | string[] } & { - [header: string]: string | string[]; -}; +export type ResponseHeaders = + | Record + | Record; const normalizeHeaderField = (field: string) => field.trim().toLowerCase(); diff --git a/src/core/server/http/router/index.ts b/src/core/server/http/router/index.ts index d254f391ca5e4..83ceff4a25d86 100644 --- a/src/core/server/http/router/index.ts +++ b/src/core/server/http/router/index.ts @@ -18,7 +18,7 @@ */ export { Headers, filterHeaders, ResponseHeaders, KnownHeaders } from './headers'; -export { Router, RequestHandler, IRouter, RouteRegistrar } from './router'; +export { Router, RequestHandler, RequestHandlerWrapper, IRouter, RouteRegistrar } from './router'; export { KibanaRequest, KibanaRequestEvents, diff --git a/src/core/server/http/router/router.ts b/src/core/server/http/router/router.ts index bb56ee3727d1a..69402a74eda5f 100644 --- a/src/core/server/http/router/router.ts +++ b/src/core/server/http/router/router.ts @@ -20,7 +20,7 @@ import { Request, ResponseObject, ResponseToolkit } from 'hapi'; import Boom from 'boom'; -import { Type } from '@kbn/config-schema'; +import { isConfigSchema } from '@kbn/config-schema'; import { Logger } from '../../logging'; import { KibanaRequest } from './request'; import { KibanaResponseFactory, kibanaResponseFactory, IKibanaResponse } from './response'; @@ -98,7 +98,7 @@ export interface IRouter { * Wrap a router handler to catch and converts legacy boom errors to proper custom errors. * @param handler {@link RequestHandler} - a route handler to wrap */ - handleLegacyErrors: (handler: RequestHandler) => RequestHandler; + handleLegacyErrors: RequestHandlerWrapper; /** * Returns all routes registered with this router. @@ -139,7 +139,7 @@ function routeSchemasFromRouteConfig( if (route.validate !== false) { Object.entries(route.validate).forEach(([key, schema]) => { - if (!(schema instanceof Type || typeof schema === 'function')) { + if (!(isConfigSchema(schema) || typeof schema === 'function')) { throw new Error( `Expected a valid validation logic declared with '@kbn/config-schema' package or a RouteValidationFunction at key: [${key}].` ); @@ -237,9 +237,7 @@ export class Router implements IRouter { return [...this.routes]; } - public handleLegacyErrors(handler: RequestHandler): RequestHandler { - return wrapErrors(handler); - } + public handleLegacyErrors = wrapErrors; private async handle({ routeSchemas, @@ -316,9 +314,33 @@ export type RequestHandler< P = unknown, Q = unknown, B = unknown, - Method extends RouteMethod = any + Method extends RouteMethod = any, + ResponseFactory extends KibanaResponseFactory = KibanaResponseFactory > = ( context: RequestHandlerContext, request: KibanaRequest, - response: KibanaResponseFactory + response: ResponseFactory ) => IKibanaResponse | Promise>; + +/** + * Type-safe wrapper for {@link RequestHandler} function. + * @example + * ```typescript + * export const wrapper: RequestHandlerWrapper = handler => { + * return async (context, request, response) => { + * // do some logic + * ... + * }; + * } + * ``` + * @public + */ +export type RequestHandlerWrapper = < + P, + Q, + B, + Method extends RouteMethod = any, + ResponseFactory extends KibanaResponseFactory = KibanaResponseFactory +>( + handler: RequestHandler +) => RequestHandler; diff --git a/src/core/server/http/router/validator/validator.ts b/src/core/server/http/router/validator/validator.ts index 97dd2bc894f81..6c766e69f0f37 100644 --- a/src/core/server/http/router/validator/validator.ts +++ b/src/core/server/http/router/validator/validator.ts @@ -17,7 +17,7 @@ * under the License. */ -import { ValidationError, Type, schema, ObjectType } from '@kbn/config-schema'; +import { ValidationError, Type, schema, ObjectType, isConfigSchema } from '@kbn/config-schema'; import { Stream } from 'stream'; import { RouteValidationError } from './validator_error'; @@ -236,7 +236,7 @@ export class RouteValidator

{ data?: unknown, namespace?: string ): RouteValidationResultType { - if (validationRule instanceof Type) { + if (isConfigSchema(validationRule)) { return validationRule.validate(data, {}, namespace); } else if (typeof validationRule === 'function') { return this.validateFunction(validationRule, data, namespace); diff --git a/src/core/server/http_resources/http_resources_service.mock.ts b/src/core/server/http_resources/http_resources_service.mock.ts new file mode 100644 index 0000000000000..4536b0898cad9 --- /dev/null +++ b/src/core/server/http_resources/http_resources_service.mock.ts @@ -0,0 +1,50 @@ +/* + * 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 { httpServerMock } from '../http/http_server.mocks'; +import { HttpResources, HttpResourcesServiceToolkit } from './types'; + +const createHttpResourcesMock = (): jest.Mocked => ({ + register: jest.fn(), +}); + +function createInternalHttpResourcesSetup() { + return { + createRegistrar: createHttpResourcesMock, + }; +} + +function createHttpResourcesResponseFactory() { + const mocked: jest.Mocked = { + renderCoreApp: jest.fn(), + renderAnonymousCoreApp: jest.fn(), + renderHtml: jest.fn(), + renderJs: jest.fn(), + }; + + return { + ...httpServerMock.createResponseFactory(), + ...mocked, + }; +} + +export const httpResourcesMock = { + createRegistrar: createHttpResourcesMock, + createSetupContract: createInternalHttpResourcesSetup, + createResponseFactory: createHttpResourcesResponseFactory, +}; diff --git a/src/core/server/http_resources/http_resources_service.test.ts b/src/core/server/http_resources/http_resources_service.test.ts new file mode 100644 index 0000000000000..e6f129ba12d78 --- /dev/null +++ b/src/core/server/http_resources/http_resources_service.test.ts @@ -0,0 +1,258 @@ +/* + * 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 { IRouter, RouteConfig } from '../http'; + +import { coreMock } from '../mocks'; +import { mockCoreContext } from '../core_context.mock'; +import { httpServiceMock } from '../http/http_service.mock'; +import { httpServerMock } from '../http/http_server.mocks'; +import { renderingMock } from '../rendering/rendering_service.mock'; +import { HttpResourcesService, SetupDeps } from './http_resources_service'; +import { httpResourcesMock } from './http_resources_service.mock'; + +const coreContext = mockCoreContext.create(); + +describe('HttpResources service', () => { + let service: HttpResourcesService; + let setupDeps: SetupDeps; + let router: jest.Mocked; + const kibanaRequest = httpServerMock.createKibanaRequest(); + const context = { core: coreMock.createRequestHandlerContext() }; + describe('#createRegistrar', () => { + beforeEach(() => { + setupDeps = { + http: httpServiceMock.createSetupContract(), + rendering: renderingMock.createSetupContract(), + }; + service = new HttpResourcesService(coreContext); + router = httpServiceMock.createRouter(); + }); + + describe('register', () => { + describe('renderCoreApp', () => { + it('formats successful response', async () => { + const routeConfig: RouteConfig = { path: '/', validate: false }; + const { createRegistrar } = await service.setup(setupDeps); + const { register } = createRegistrar(router); + register(routeConfig, async (ctx, req, res) => { + return res.renderCoreApp(); + }); + const [[, routeHandler]] = router.get.mock.calls; + + const responseFactory = httpResourcesMock.createResponseFactory(); + await routeHandler(context, kibanaRequest, responseFactory); + expect(setupDeps.rendering.render).toHaveBeenCalledWith( + kibanaRequest, + context.core.uiSettings.client, + { + includeUserSettings: true, + } + ); + }); + + it('can attach headers, except the CSP header', async () => { + const routeConfig: RouteConfig = { path: '/', validate: false }; + const { createRegistrar } = await service.setup(setupDeps); + const { register } = createRegistrar(router); + register(routeConfig, async (ctx, req, res) => { + return res.renderCoreApp({ + headers: { + 'content-security-policy': "script-src 'unsafe-eval'", + 'x-kibana': '42', + }, + }); + }); + + const [[, routeHandler]] = router.get.mock.calls; + + const responseFactory = httpResourcesMock.createResponseFactory(); + await routeHandler(context, kibanaRequest, responseFactory); + + expect(responseFactory.ok).toHaveBeenCalledWith({ + body: '', + headers: { + 'x-kibana': '42', + 'content-security-policy': + "script-src 'unsafe-eval' 'self'; worker-src blob: 'self'; style-src 'unsafe-inline' 'self'", + }, + }); + }); + }); + describe('renderAnonymousCoreApp', () => { + it('formats successful response', async () => { + const routeConfig: RouteConfig = { path: '/', validate: false }; + const { createRegistrar } = await service.setup(setupDeps); + const { register } = createRegistrar(router); + register(routeConfig, async (ctx, req, res) => { + return res.renderAnonymousCoreApp(); + }); + const [[, routeHandler]] = router.get.mock.calls; + + const responseFactory = httpResourcesMock.createResponseFactory(); + await routeHandler(context, kibanaRequest, responseFactory); + expect(setupDeps.rendering.render).toHaveBeenCalledWith( + kibanaRequest, + context.core.uiSettings.client, + { + includeUserSettings: false, + } + ); + }); + + it('can attach headers, except the CSP header', async () => { + const routeConfig: RouteConfig = { path: '/', validate: false }; + const { createRegistrar } = await service.setup(setupDeps); + const { register } = createRegistrar(router); + register(routeConfig, async (ctx, req, res) => { + return res.renderAnonymousCoreApp({ + headers: { + 'content-security-policy': "script-src 'unsafe-eval'", + 'x-kibana': '42', + }, + }); + }); + + const [[, routeHandler]] = router.get.mock.calls; + + const responseFactory = httpResourcesMock.createResponseFactory(); + await routeHandler(context, kibanaRequest, responseFactory); + + expect(responseFactory.ok).toHaveBeenCalledWith({ + body: '', + headers: { + 'x-kibana': '42', + 'content-security-policy': + "script-src 'unsafe-eval' 'self'; worker-src blob: 'self'; style-src 'unsafe-inline' 'self'", + }, + }); + }); + }); + describe('renderHtml', () => { + it('formats successful response', async () => { + const htmlBody = ''; + const routeConfig: RouteConfig = { path: '/', validate: false }; + const { createRegistrar } = await service.setup(setupDeps); + const { register } = createRegistrar(router); + register(routeConfig, async (ctx, req, res) => { + return res.renderHtml({ body: htmlBody }); + }); + const [[, routeHandler]] = router.get.mock.calls; + + const responseFactory = httpResourcesMock.createResponseFactory(); + await routeHandler(context, kibanaRequest, responseFactory); + expect(responseFactory.ok).toHaveBeenCalledWith({ + body: htmlBody, + headers: { + 'content-type': 'text/html', + 'content-security-policy': + "script-src 'unsafe-eval' 'self'; worker-src blob: 'self'; style-src 'unsafe-inline' 'self'", + }, + }); + }); + + it('can attach headers, except the CSP & "content-type" headers', async () => { + const htmlBody = ''; + const routeConfig: RouteConfig = { path: '/', validate: false }; + const { createRegistrar } = await service.setup(setupDeps); + const { register } = createRegistrar(router); + register(routeConfig, async (ctx, req, res) => { + return res.renderHtml({ + body: htmlBody, + headers: { + 'content-type': 'text/html5', + 'content-security-policy': "script-src 'unsafe-eval'", + 'x-kibana': '42', + }, + }); + }); + + const [[, routeHandler]] = router.get.mock.calls; + + const responseFactory = httpResourcesMock.createResponseFactory(); + await routeHandler(context, kibanaRequest, responseFactory); + + expect(responseFactory.ok).toHaveBeenCalledWith({ + body: htmlBody, + headers: { + 'content-type': 'text/html', + 'x-kibana': '42', + 'content-security-policy': + "script-src 'unsafe-eval' 'self'; worker-src blob: 'self'; style-src 'unsafe-inline' 'self'", + }, + }); + }); + }); + describe('renderJs', () => { + it('formats successful response', async () => { + const jsBody = 'alert(1);'; + const routeConfig: RouteConfig = { path: '/', validate: false }; + const { createRegistrar } = await service.setup(setupDeps); + const { register } = createRegistrar(router); + register(routeConfig, async (ctx, req, res) => { + return res.renderJs({ body: jsBody }); + }); + const [[, routeHandler]] = router.get.mock.calls; + + const responseFactory = httpResourcesMock.createResponseFactory(); + await routeHandler(context, kibanaRequest, responseFactory); + expect(responseFactory.ok).toHaveBeenCalledWith({ + body: jsBody, + headers: { + 'content-type': 'text/javascript', + 'content-security-policy': + "script-src 'unsafe-eval' 'self'; worker-src blob: 'self'; style-src 'unsafe-inline' 'self'", + }, + }); + }); + + it('can attach headers, except the CSP & "content-type" headers', async () => { + const jsBody = 'alert(1);'; + const routeConfig: RouteConfig = { path: '/', validate: false }; + const { createRegistrar } = await service.setup(setupDeps); + const { register } = createRegistrar(router); + register(routeConfig, async (ctx, req, res) => { + return res.renderJs({ + body: jsBody, + headers: { + 'content-type': 'text/html', + 'content-security-policy': "script-src 'unsafe-eval'", + 'x-kibana': '42', + }, + }); + }); + + const [[, routeHandler]] = router.get.mock.calls; + + const responseFactory = httpResourcesMock.createResponseFactory(); + await routeHandler(context, kibanaRequest, responseFactory); + + expect(responseFactory.ok).toHaveBeenCalledWith({ + body: jsBody, + headers: { + 'content-type': 'text/javascript', + 'x-kibana': '42', + 'content-security-policy': + "script-src 'unsafe-eval' 'self'; worker-src blob: 'self'; style-src 'unsafe-inline' 'self'", + }, + }); + }); + }); + }); + }); +}); diff --git a/src/core/server/http_resources/http_resources_service.ts b/src/core/server/http_resources/http_resources_service.ts new file mode 100644 index 0000000000000..bc79ad68f4099 --- /dev/null +++ b/src/core/server/http_resources/http_resources_service.ts @@ -0,0 +1,130 @@ +/* + * 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 { RequestHandlerContext } from 'src/core/server'; + +import { CoreContext } from '../core_context'; +import { + IRouter, + RouteConfig, + InternalHttpServiceSetup, + KibanaRequest, + KibanaResponseFactory, +} from '../http'; + +import { Logger } from '../logging'; +import { InternalRenderingServiceSetup } from '../rendering'; +import { CoreService } from '../../types'; + +import { + InternalHttpResourcesSetup, + HttpResources, + HttpResourcesResponseOptions, + HttpResourcesRenderOptions, + HttpResourcesRequestHandler, + HttpResourcesServiceToolkit, +} from './types'; + +export interface SetupDeps { + http: InternalHttpServiceSetup; + rendering: InternalRenderingServiceSetup; +} + +export class HttpResourcesService implements CoreService { + private readonly logger: Logger; + constructor(core: CoreContext) { + this.logger = core.logger.get('http-resources'); + } + + setup(deps: SetupDeps) { + this.logger.debug('setting up HttpResourcesService'); + return { + createRegistrar: this.createRegistrar.bind(this, deps), + }; + } + + start() {} + stop() {} + + private createRegistrar(deps: SetupDeps, router: IRouter): HttpResources { + return { + register: ( + route: RouteConfig, + handler: HttpResourcesRequestHandler + ) => { + return router.get(route, (context, request, response) => { + return handler(context, request, { + ...response, + ...this.createResponseToolkit(deps, context, request, response), + }); + }); + }, + }; + } + + private createResponseToolkit( + deps: SetupDeps, + context: RequestHandlerContext, + request: KibanaRequest, + response: KibanaResponseFactory + ): HttpResourcesServiceToolkit { + const cspHeader = deps.http.csp.header; + return { + async renderCoreApp(options: HttpResourcesRenderOptions = {}) { + const body = await deps.rendering.render(request, context.core.uiSettings.client, { + includeUserSettings: true, + }); + + return response.ok({ + body, + headers: { ...options.headers, 'content-security-policy': cspHeader }, + }); + }, + async renderAnonymousCoreApp(options: HttpResourcesRenderOptions = {}) { + const body = await deps.rendering.render(request, context.core.uiSettings.client, { + includeUserSettings: false, + }); + + return response.ok({ + body, + headers: { ...options.headers, 'content-security-policy': cspHeader }, + }); + }, + renderHtml(options: HttpResourcesResponseOptions) { + return response.ok({ + body: options.body, + headers: { + ...options.headers, + 'content-type': 'text/html', + 'content-security-policy': cspHeader, + }, + }); + }, + renderJs(options: HttpResourcesResponseOptions) { + return response.ok({ + body: options.body, + headers: { + ...options.headers, + 'content-type': 'text/javascript', + 'content-security-policy': cspHeader, + }, + }); + }, + }; + } +} diff --git a/src/core/server/http_resources/index.ts b/src/core/server/http_resources/index.ts new file mode 100644 index 0000000000000..b373c6a9efa89 --- /dev/null +++ b/src/core/server/http_resources/index.ts @@ -0,0 +1,29 @@ +/* + * 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 { HttpResourcesService } from './http_resources_service'; + +export { + HttpResourcesRenderOptions, + HttpResourcesResponseOptions, + HttpResourcesServiceToolkit, + HttpResourcesRequestHandler, + HttpResources, + InternalHttpResourcesSetup, +} from './types'; diff --git a/src/core/server/http_resources/integration_tests/http_resources_service.test.ts b/src/core/server/http_resources/integration_tests/http_resources_service.test.ts new file mode 100644 index 0000000000000..0a5daa02e17e9 --- /dev/null +++ b/src/core/server/http_resources/integration_tests/http_resources_service.test.ts @@ -0,0 +1,203 @@ +/* + * 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 { schema } from '@kbn/config-schema'; +import * as kbnTestServer from '../../../../test_utils/kbn_server'; + +describe('http resources service', () => { + describe('register', () => { + let root: ReturnType; + const defaultCspRules = "script-src 'self'"; + beforeEach(async () => { + root = kbnTestServer.createRoot({ + csp: { + rules: [defaultCspRules], + }, + }); + }, 30000); + + afterEach(async () => { + await root.shutdown(); + }); + + describe('renderAnonymousCoreApp', () => { + it('renders core application', async () => { + const { http, httpResources } = await root.setup(); + + const router = http.createRouter(''); + const resources = httpResources.createRegistrar(router); + resources.register({ path: '/render-core', validate: false }, (context, req, res) => + res.renderAnonymousCoreApp() + ); + + await root.start(); + const response = await kbnTestServer.request.get(root, '/render-core').expect(200); + + expect(response.text.length).toBeGreaterThan(0); + }); + + it('attaches CSP header', async () => { + const { http, httpResources } = await root.setup(); + + const router = http.createRouter(''); + const resources = httpResources.createRegistrar(router); + resources.register({ path: '/render-core', validate: false }, (context, req, res) => + res.renderAnonymousCoreApp() + ); + + await root.start(); + const response = await kbnTestServer.request.get(root, '/render-core').expect(200); + + expect(response.header['content-security-policy']).toBe(defaultCspRules); + }); + + it('can attach headers, except the CSP header', async () => { + const { http, httpResources } = await root.setup(); + + const router = http.createRouter(''); + const resources = httpResources.createRegistrar(router); + resources.register({ path: '/render-core', validate: false }, (context, req, res) => + res.renderAnonymousCoreApp({ + headers: { + 'content-security-policy': "script-src 'unsafe-eval'", + 'x-kibana': '42', + }, + }) + ); + + await root.start(); + const response = await kbnTestServer.request.get(root, '/render-core').expect(200); + + expect(response.header['content-security-policy']).toBe(defaultCspRules); + expect(response.header['x-kibana']).toBe('42'); + }); + }); + + describe('custom renders', () => { + it('renders html', async () => { + const { http, httpResources } = await root.setup(); + + const router = http.createRouter(''); + const resources = httpResources.createRegistrar(router); + const htmlBody = ` + + + +

HTML body

+ + + `; + resources.register({ path: '/render-html', validate: false }, (context, req, res) => + res.renderHtml({ body: htmlBody }) + ); + + await root.start(); + const response = await kbnTestServer.request.get(root, '/render-html').expect(200); + + expect(response.text).toBe(htmlBody); + expect(response.header['content-type']).toBe('text/html; charset=utf-8'); + }); + + it('renders javascript', async () => { + const { http, httpResources } = await root.setup(); + + const router = http.createRouter(''); + const resources = httpResources.createRegistrar(router); + const jsBody = 'window.alert("from js body");'; + resources.register({ path: '/render-js', validate: false }, (context, req, res) => + res.renderJs({ body: jsBody }) + ); + + await root.start(); + const response = await kbnTestServer.request.get(root, '/render-js').expect(200); + + expect(response.text).toBe(jsBody); + expect(response.header['content-type']).toBe('text/javascript; charset=utf-8'); + }); + + it('attaches CSP header', async () => { + const { http, httpResources } = await root.setup(); + + const router = http.createRouter(''); + const resources = httpResources.createRegistrar(router); + const htmlBody = ` + + + +

HTML body

+ + + `; + resources.register({ path: '/render-html', validate: false }, (context, req, res) => + res.renderHtml({ body: htmlBody }) + ); + + await root.start(); + const response = await kbnTestServer.request.get(root, '/render-html').expect(200); + + expect(response.header['content-security-policy']).toBe(defaultCspRules); + }); + + it('can attach headers, except the CSP & "content-type" headers', async () => { + const { http, httpResources } = await root.setup(); + + const router = http.createRouter(''); + const resources = httpResources.createRegistrar(router); + resources.register({ path: '/render-core', validate: false }, (context, req, res) => + res.renderHtml({ + body: '

Hi

', + headers: { + 'content-security-policy': "script-src 'unsafe-eval'", + 'content-type': 'text/html', + 'x-kibana': '42', + }, + }) + ); + + await root.start(); + const response = await kbnTestServer.request.get(root, '/render-core').expect(200); + + expect(response.header['content-security-policy']).toBe(defaultCspRules); + expect(response.header['x-kibana']).toBe('42'); + }); + + it('can adjust route config', async () => { + const { http, httpResources } = await root.setup(); + + const router = http.createRouter(''); + const resources = httpResources.createRegistrar(router); + const validate = { + params: schema.object({ + id: schema.string(), + }), + }; + + resources.register({ path: '/render-js-with-param/{id}', validate }, (context, req, res) => + res.renderJs({ body: `window.alert(${req.params.id});` }) + ); + + await root.start(); + const response = await kbnTestServer.request + .get(root, '/render-js-with-param/42') + .expect(200); + + expect(response.text).toBe('window.alert(42);'); + }); + }); + }); +}); diff --git a/src/core/server/http_resources/types.ts b/src/core/server/http_resources/types.ts new file mode 100644 index 0000000000000..d761e2def1023 --- /dev/null +++ b/src/core/server/http_resources/types.ts @@ -0,0 +1,116 @@ +/* + * 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 { + IRouter, + RouteConfig, + IKibanaResponse, + ResponseHeaders, + HttpResponseOptions, + KibanaResponseFactory, + RequestHandler, +} from '../http'; + +/** + * Allows to configure HTTP response parameters + * @public + */ +export interface HttpResourcesRenderOptions { + /** + * HTTP Headers with additional information about response. + * @remarks + * All HTML pages are already pre-configured with `content-security-policy` header that cannot be overridden. + * */ + headers?: ResponseHeaders; +} + +/** + * HTTP Resources response parameters + * @public + */ +export type HttpResourcesResponseOptions = HttpResponseOptions; + +/** + * Extended set of {@link KibanaResponseFactory} helpers used to respond with HTML or JS resource. + * @public + */ +export interface HttpResourcesServiceToolkit { + /** To respond with HTML page bootstrapping Kibana application. */ + renderCoreApp: (options?: HttpResourcesRenderOptions) => Promise; + /** To respond with HTML page bootstrapping Kibana application without retrieving user-specific information. */ + renderAnonymousCoreApp: (options?: HttpResourcesRenderOptions) => Promise; + /** To respond with a custom HTML page. */ + renderHtml: (options: HttpResourcesResponseOptions) => IKibanaResponse; + /** To respond with a custom JS script file. */ + renderJs: (options: HttpResourcesResponseOptions) => IKibanaResponse; +} + +/** + * Extended version of {@link RequestHandler} having access to {@link HttpResourcesServiceToolkit} + * to respond with HTML or JS resources. + * @param context {@link RequestHandlerContext} - the core context exposed for this request. + * @param request {@link KibanaRequest} - object containing information about requested resource, + * such as path, method, headers, parameters, query, body, etc. + * @param response {@link KibanaResponseFactory} {@libk HttpResourcesServiceToolkit} - a set of helper functions used to respond to a request. + * + * @example + * ```typescript + * httpResources.register({ + * path: '/login', + * validate: { + * params: schema.object({ id: schema.string() }), + * }, + * }, + * async (context, request, response) => { + * //.. + * return response.renderCoreApp(); + * }); + * @public + */ +export type HttpResourcesRequestHandler

= RequestHandler< + P, + Q, + B, + 'get', + KibanaResponseFactory & HttpResourcesServiceToolkit +>; + +/** + * Allows to configure HTTP response parameters + * @internal + */ +export interface InternalHttpResourcesSetup { + createRegistrar(router: IRouter): HttpResources; +} + +/** + * HttpResources service is responsible for serving static & dynamic assets for Kibana application via HTTP. + * Provides API allowing plug-ins to respond with: + * - a pre-configured HTML page bootstrapping Kibana client app + * - custom HTML page + * - custom JS script file. + * @public + */ +export interface HttpResources { + /** To register a route handler executing passed function to form response. */ + register: ( + route: RouteConfig, + handler: HttpResourcesRequestHandler + ) => void; +} diff --git a/src/core/server/index.ts b/src/core/server/index.ts index a298f80f96d8f..ef57fae159d7e 100644 --- a/src/core/server/index.ts +++ b/src/core/server/index.ts @@ -47,7 +47,8 @@ import { } from './elasticsearch'; import { HttpServiceSetup } from './http'; -import { IScopedRenderingClient } from './rendering'; +import { HttpResources } from './http_resources'; + import { PluginsServiceSetup, PluginsServiceStart, PluginOpaqueId } from './plugins'; import { ContextSetup } from './context'; import { IUiSettingsClient, UiSettingsServiceSetup, UiSettingsServiceStart } from './ui_settings'; @@ -146,6 +147,7 @@ export { OnPreResponseInfo, RedirectResponseOptions, RequestHandler, + RequestHandlerWrapper, RequestHandlerContextContainer, RequestHandlerContextProvider, ResponseError, @@ -175,7 +177,15 @@ export { DestructiveRouteMethod, SafeRouteMethod, } from './http'; -export { RenderingServiceSetup, IRenderOptions } from './rendering'; + +export { + HttpResourcesRenderOptions, + HttpResourcesResponseOptions, + HttpResourcesServiceToolkit, + HttpResourcesRequestHandler, +} from './http_resources'; + +export { IRenderOptions } from './rendering'; export { Logger, LoggerFactory, LogMeta, LogRecord, LogLevel } from './logging'; export { @@ -227,6 +237,8 @@ export { SavedObjectsLegacyService, SavedObjectsUpdateOptions, SavedObjectsUpdateResponse, + SavedObjectsAddToNamespacesOptions, + SavedObjectsDeleteFromNamespacesOptions, SavedObjectsServiceStart, SavedObjectsServiceSetup, SavedObjectStatusMeta, @@ -242,6 +254,7 @@ export { SavedObjectsMappingProperties, SavedObjectTypeRegistry, ISavedObjectTypeRegistry, + SavedObjectsNamespaceType, SavedObjectsType, SavedObjectsTypeManagementDefinition, SavedObjectMigrationMap, @@ -310,8 +323,6 @@ export { * Plugin specific context passed to a route handler. * * Provides the following clients and services: - * - {@link IScopedRenderingClient | rendering} - Rendering client - * which uses the data of the incoming request * - {@link SavedObjectsClient | savedObjects.client} - Saved Objects client * which uses the credentials of the incoming request * - {@link ISavedObjectTypeRegistry | savedObjects.typeRegistry} - Type registry containing @@ -327,7 +338,6 @@ export { */ export interface RequestHandlerContext { core: { - rendering: IScopedRenderingClient; savedObjects: { client: SavedObjectsClientContract; typeRegistry: ISavedObjectTypeRegistry; @@ -359,7 +369,10 @@ export interface CoreSetup ({ +jest.doMock('./plugins/find_legacy_plugin_specs', () => ({ findLegacyPluginSpecs: findLegacyPluginSpecsMock, })); + +export const logLegacyThirdPartyPluginDeprecationWarningMock = jest.fn(); +jest.doMock('./plugins/log_legacy_plugins_warning', () => ({ + logLegacyThirdPartyPluginDeprecationWarning: logLegacyThirdPartyPluginDeprecationWarningMock, +})); diff --git a/src/core/server/legacy/legacy_service.test.ts b/src/core/server/legacy/legacy_service.test.ts index 0cf2ebe55ea10..a75f7dda302c2 100644 --- a/src/core/server/legacy/legacy_service.test.ts +++ b/src/core/server/legacy/legacy_service.test.ts @@ -22,7 +22,10 @@ jest.mock('../../../cli/cluster/cluster_manager'); jest.mock('./config/legacy_deprecation_adapters', () => ({ convertLegacyDeprecationProvider: (provider: any) => Promise.resolve(provider), })); -import { findLegacyPluginSpecsMock } from './legacy_service.test.mocks'; +import { + findLegacyPluginSpecsMock, + logLegacyThirdPartyPluginDeprecationWarningMock, +} from './legacy_service.test.mocks'; import { BehaviorSubject, throwError } from 'rxjs'; @@ -41,6 +44,7 @@ import { httpServiceMock } from '../http/http_service.mock'; import { uiSettingsServiceMock } from '../ui_settings/ui_settings_service.mock'; import { savedObjectsServiceMock } from '../saved_objects/saved_objects_service.mock'; import { capabilitiesServiceMock } from '../capabilities/capabilities_service.mock'; +import { httpResourcesMock } from '../http_resources/http_resources_service.mock'; import { setupMock as renderingServiceMock } from '../rendering/__mocks__/rendering_service'; import { uuidServiceMock } from '../uuid/uuid_service.mock'; import { metricsServiceMock } from '../metrics/metrics_service.mock'; @@ -86,23 +90,11 @@ beforeEach(() => { getAuthHeaders: () => undefined, } as any, }, + httpResources: httpResourcesMock.createSetupContract(), savedObjects: savedObjectsServiceMock.createInternalSetupContract(), plugins: { initialized: true, contracts: new Map([['plugin-id', 'plugin-value']]), - uiPlugins: { - public: new Map([['plugin-id', {} as DiscoveredPlugin]]), - internal: new Map([ - [ - 'plugin-id', - { - publicTargetDir: 'path/to/target/public', - publicAssetsDir: '/plugins/name/assets/', - }, - ], - ]), - browserConfigs: new Map(), - }, }, rendering: renderingServiceMock, metrics: metricsServiceMock.createInternalSetupContract(), @@ -110,6 +102,19 @@ beforeEach(() => { status: statusServiceMock.createInternalSetupContract(), }, plugins: { 'plugin-id': 'plugin-value' }, + uiPlugins: { + public: new Map([['plugin-id', {} as DiscoveredPlugin]]), + internal: new Map([ + [ + 'plugin-id', + { + publicTargetDir: 'path/to/target/public', + publicAssetsDir: '/plugins/name/assets/', + }, + ], + ]), + browserConfigs: new Map(), + }, }; startDeps = { @@ -474,6 +479,38 @@ describe('#discoverPlugins()', () => { expect(configService.addDeprecationProvider).toHaveBeenCalledWith('', 'providerA'); expect(configService.addDeprecationProvider).toHaveBeenCalledWith('', 'providerB'); }); + + it(`logs deprecations for legacy third party plugins`, async () => { + const pluginSpecs = [ + { getId: () => 'pluginA', getDeprecationsProvider: () => undefined }, + { getId: () => 'pluginB', getDeprecationsProvider: () => undefined }, + ]; + findLegacyPluginSpecsMock.mockImplementation( + settings => + Promise.resolve({ + pluginSpecs, + pluginExtendedConfig: settings, + disabledPluginSpecs: [], + uiExports: {}, + navLinks: [], + }) as any + ); + + const legacyService = new LegacyService({ + coreId, + env, + logger, + configService: configService as any, + }); + + await legacyService.discoverPlugins(); + + expect(logLegacyThirdPartyPluginDeprecationWarningMock).toHaveBeenCalledTimes(1); + expect(logLegacyThirdPartyPluginDeprecationWarningMock).toHaveBeenCalledWith({ + specs: pluginSpecs, + log: expect.any(Object), + }); + }); }); test('Sets the server.uuid property on the legacy configuration', async () => { diff --git a/src/core/server/legacy/legacy_service.ts b/src/core/server/legacy/legacy_service.ts index f77230301ce02..b95362e1ea26e 100644 --- a/src/core/server/legacy/legacy_service.ts +++ b/src/core/server/legacy/legacy_service.ts @@ -28,7 +28,7 @@ import { DevConfig, DevConfigType, config as devConfig } from '../dev'; import { BasePathProxyServer, HttpConfig, HttpConfigType, config as httpConfig } from '../http'; import { Logger } from '../logging'; import { PathConfigType } from '../path'; -import { findLegacyPluginSpecs } from './plugins'; +import { findLegacyPluginSpecs, logLegacyThirdPartyPluginDeprecationWarning } from './plugins'; import { convertLegacyDeprecationProvider } from './config'; import { ILegacyInternals, @@ -133,6 +133,11 @@ export class LegacyService implements CoreService { this.coreContext.env.packageInfo ); + logLegacyThirdPartyPluginDeprecationWarning({ + specs: pluginSpecs, + log: this.log, + }); + this.legacyPlugins = { pluginSpecs, disabledPluginSpecs, @@ -269,6 +274,7 @@ export class LegacyService implements CoreService { uiSettings: { asScopedToClient: startDeps.core.uiSettings.asScopedToClient }, }; + const router = setupDeps.core.http.createRouter('', this.legacyId); const coreSetup: CoreSetup = { capabilities: setupDeps.core.capabilities, context: setupDeps.core.context, @@ -283,7 +289,8 @@ export class LegacyService implements CoreService { null, this.legacyId ), - createRouter: () => setupDeps.core.http.createRouter('', this.legacyId), + createRouter: () => router, + resources: setupDeps.core.httpResources.createRegistrar(router), registerOnPreAuth: setupDeps.core.http.registerOnPreAuth, registerAuth: setupDeps.core.http.registerAuth, registerOnPostAuth: setupDeps.core.http.registerOnPostAuth, @@ -342,7 +349,7 @@ export class LegacyService implements CoreService { }, hapiServer: setupDeps.core.http.server, kibanaMigrator: startDeps.core.savedObjects.migrator, - uiPlugins: setupDeps.core.plugins.uiPlugins, + uiPlugins: setupDeps.uiPlugins, elasticsearch: setupDeps.core.elasticsearch, rendering: setupDeps.core.rendering, uiSettings: setupDeps.core.uiSettings, diff --git a/src/core/server/legacy/plugins/index.ts b/src/core/server/legacy/plugins/index.ts index a6d55e1da7839..7ec5dbc1983ab 100644 --- a/src/core/server/legacy/plugins/index.ts +++ b/src/core/server/legacy/plugins/index.ts @@ -18,3 +18,4 @@ */ export { findLegacyPluginSpecs } from './find_legacy_plugin_specs'; +export { logLegacyThirdPartyPluginDeprecationWarning } from './log_legacy_plugins_warning'; diff --git a/src/core/server/legacy/plugins/log_legacy_plugins_warning.test.ts b/src/core/server/legacy/plugins/log_legacy_plugins_warning.test.ts new file mode 100644 index 0000000000000..1790b096a71ae --- /dev/null +++ b/src/core/server/legacy/plugins/log_legacy_plugins_warning.test.ts @@ -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 { loggerMock } from '../../logging/logger.mock'; +import { logLegacyThirdPartyPluginDeprecationWarning } from './log_legacy_plugins_warning'; +import { LegacyPluginSpec } from '../types'; + +const createPluginSpec = ({ id, path }: { id: string; path: string }): LegacyPluginSpec => { + return { + getId: () => id, + getExpectedKibanaVersion: () => 'kibana', + getConfigPrefix: () => 'plugin.config', + getDeprecationsProvider: () => undefined, + getPack: () => ({ + getPath: () => path, + }), + }; +}; + +describe('logLegacyThirdPartyPluginDeprecationWarning', () => { + let log: ReturnType; + + beforeEach(() => { + log = loggerMock.create(); + }); + + it('logs warning for third party plugins', () => { + logLegacyThirdPartyPluginDeprecationWarning({ + specs: [createPluginSpec({ id: 'plugin', path: '/some-external-path' })], + log, + }); + expect(log.warn).toHaveBeenCalledTimes(1); + expect(log.warn.mock.calls[0]).toMatchInlineSnapshot(` + Array [ + "Some installed third party plugin(s) [plugin] are using the legacy plugin format and will no longer work in a future Kibana release. Please refer to https://www.elastic.co/guide/en/kibana/master/breaking-changes-8.0.html for a list of breaking changes and https://github.com/elastic/kibana/blob/master/src/core/MIGRATION.md for documentation on how to migrate legacy plugins.", + ] + `); + }); + + it('lists all the deprecated plugins and only log once', () => { + logLegacyThirdPartyPluginDeprecationWarning({ + specs: [ + createPluginSpec({ id: 'pluginA', path: '/abs/path/to/pluginA' }), + createPluginSpec({ id: 'pluginB', path: '/abs/path/to/pluginB' }), + createPluginSpec({ id: 'pluginC', path: '/abs/path/to/pluginC' }), + ], + log, + }); + expect(log.warn).toHaveBeenCalledTimes(1); + expect(log.warn.mock.calls[0]).toMatchInlineSnapshot(` + Array [ + "Some installed third party plugin(s) [pluginA, pluginB, pluginC] are using the legacy plugin format and will no longer work in a future Kibana release. Please refer to https://www.elastic.co/guide/en/kibana/master/breaking-changes-8.0.html for a list of breaking changes and https://github.com/elastic/kibana/blob/master/src/core/MIGRATION.md for documentation on how to migrate legacy plugins.", + ] + `); + }); + + it('does not log warning for internal legacy plugins', () => { + logLegacyThirdPartyPluginDeprecationWarning({ + specs: [ + createPluginSpec({ + id: 'plugin', + path: '/absolute/path/to/kibana/src/legacy/core_plugins', + }), + createPluginSpec({ + id: 'plugin', + path: '/absolute/path/to/kibana/x-pack', + }), + ], + log, + }); + + expect(log.warn).not.toHaveBeenCalled(); + }); +}); diff --git a/src/core/server/legacy/plugins/log_legacy_plugins_warning.ts b/src/core/server/legacy/plugins/log_legacy_plugins_warning.ts new file mode 100644 index 0000000000000..f9c3dcbf554cb --- /dev/null +++ b/src/core/server/legacy/plugins/log_legacy_plugins_warning.ts @@ -0,0 +1,52 @@ +/* + * 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 { Logger } from '../../logging'; +import { LegacyPluginSpec } from '../types'; + +const internalPaths = ['/src/legacy/core_plugins', '/x-pack']; + +const breakingChangesUrl = + 'https://www.elastic.co/guide/en/kibana/master/breaking-changes-8.0.html'; +const migrationGuideUrl = 'https://github.com/elastic/kibana/blob/master/src/core/MIGRATION.md'; + +export const logLegacyThirdPartyPluginDeprecationWarning = ({ + specs, + log, +}: { + specs: LegacyPluginSpec[]; + log: Logger; +}) => { + const thirdPartySpecs = specs.filter(isThirdPartyPluginSpec); + if (thirdPartySpecs.length > 0) { + const pluginIds = thirdPartySpecs.map(spec => spec.getId()); + log.warn( + `Some installed third party plugin(s) [${pluginIds.join( + ', ' + )}] are using the legacy plugin format and will no longer work in a future Kibana release. ` + + `Please refer to ${breakingChangesUrl} for a list of breaking changes ` + + `and ${migrationGuideUrl} for documentation on how to migrate legacy plugins.` + ); + } +}; + +const isThirdPartyPluginSpec = (spec: LegacyPluginSpec): boolean => { + const pluginPath = spec.getPack().getPath(); + return !internalPaths.some(internalPath => pluginPath.indexOf(internalPath) > -1); +}; diff --git a/src/core/server/legacy/types.ts b/src/core/server/legacy/types.ts index 0c1a7730f92a7..2567ca790e04f 100644 --- a/src/core/server/legacy/types.ts +++ b/src/core/server/legacy/types.ts @@ -22,8 +22,8 @@ import { Server } from 'hapi'; import { ChromeNavLink } from '../../public'; import { KibanaRequest, LegacyRequest } from '../http'; import { InternalCoreSetup, InternalCoreStart } from '../internal_types'; -import { PluginsServiceSetup, PluginsServiceStart } from '../plugins'; -import { RenderingServiceSetup } from '../rendering'; +import { PluginsServiceSetup, PluginsServiceStart, UiPlugins } from '../plugins'; +import { InternalRenderingServiceSetup } from '../rendering'; import { SavedObjectsLegacyUiExports } from '../types'; /** @@ -34,7 +34,7 @@ export type LegacyVars = Record; type LegacyCoreSetup = InternalCoreSetup & { plugins: PluginsServiceSetup; - rendering: RenderingServiceSetup; + rendering: InternalRenderingServiceSetup; }; type LegacyCoreStart = InternalCoreStart & { plugins: PluginsServiceStart }; @@ -98,6 +98,7 @@ export interface LegacyPluginSpec { getExpectedKibanaVersion: () => string; getConfigPrefix: () => string; getDeprecationsProvider: () => LegacyConfigDeprecationProvider | undefined; + getPack: () => LegacyPluginPack; } /** @@ -173,6 +174,7 @@ export type LegacyUiExports = SavedObjectsLegacyUiExports & { export interface LegacyServiceSetupDeps { core: LegacyCoreSetup; plugins: Record; + uiPlugins: UiPlugins; } /** diff --git a/src/core/server/mocks.ts b/src/core/server/mocks.ts index faf73044cac4d..3b9a39db72278 100644 --- a/src/core/server/mocks.ts +++ b/src/core/server/mocks.ts @@ -23,10 +23,12 @@ import { CspConfig } from './csp'; import { loggingServiceMock } from './logging/logging_service.mock'; import { elasticsearchServiceMock } from './elasticsearch/elasticsearch_service.mock'; import { httpServiceMock } from './http/http_service.mock'; +import { httpResourcesMock } from './http_resources/http_resources_service.mock'; import { contextServiceMock } from './context/context_service.mock'; import { savedObjectsServiceMock } from './saved_objects/saved_objects_service.mock'; import { savedObjectsClientMock } from './saved_objects/service/saved_objects_client.mock'; import { typeRegistryMock as savedObjectsTypeRegistryMock } from './saved_objects/saved_objects_type_registry.mock'; +import { renderingMock } from './rendering/rendering_service.mock'; import { uiSettingsServiceMock } from './ui_settings/ui_settings_service.mock'; import { SharedGlobalConfig } from './plugins'; import { InternalCoreSetup, InternalCoreStart } from './internal_types'; @@ -36,6 +38,7 @@ import { uuidServiceMock } from './uuid/uuid_service.mock'; import { statusServiceMock } from './status/status_service.mock'; export { httpServerMock } from './http/http_server.mocks'; +export { httpResourcesMock } from './http_resources/http_resources_service.mock'; export { sessionStorageMock } from './http/cookie_session_storage.mocks'; export { configServiceMock } from './config/config_service.mock'; export { elasticsearchServiceMock } from './elasticsearch/elasticsearch_service.mock'; @@ -45,6 +48,7 @@ export { savedObjectsRepositoryMock } from './saved_objects/service/lib/reposito export { typeRegistryMock as savedObjectsTypeRegistryMock } from './saved_objects/saved_objects_type_registry.mock'; export { uiSettingsServiceMock } from './ui_settings/ui_settings_service.mock'; export { metricsServiceMock } from './metrics/metrics_service.mock'; +export { renderingMock } from './rendering/rendering_service.mock'; export function pluginInitializerContextConfigMock(config: T) { const globalConfig: SharedGlobalConfig = { @@ -120,6 +124,7 @@ function createCoreSetupMock({ get: httpService.auth.get, isAuthenticated: httpService.auth.isAuthenticated, }, + resources: httpResourcesMock.createRegistrar(), getServerInfo: httpService.getServerInfo, }; httpMock.createRouter.mockImplementation(() => httpService.createRouter('')); @@ -167,6 +172,8 @@ function createInternalCoreSetupMock() { savedObjects: savedObjectsServiceMock.createInternalSetupContract(), status: statusServiceMock.createInternalSetupContract(), uuid: uuidServiceMock.createSetupContract(), + httpResources: httpResourcesMock.createSetupContract(), + rendering: renderingMock.createSetupContract(), uiSettings: uiSettingsServiceMock.createSetupContract(), }; return setupDeps; @@ -184,9 +191,6 @@ function createInternalCoreStartMock() { function createCoreRequestHandlerContextMock() { return { - rendering: { - render: jest.fn(), - }, savedObjects: { client: savedObjectsClientMock.create(), typeRegistry: savedObjectsTypeRegistryMock.create(), diff --git a/src/core/server/plugins/index.ts b/src/core/server/plugins/index.ts index c7ef213c8f187..e480de750bb1a 100644 --- a/src/core/server/plugins/index.ts +++ b/src/core/server/plugins/index.ts @@ -17,7 +17,12 @@ * under the License. */ -export { PluginsService, PluginsServiceSetup, PluginsServiceStart } from './plugins_service'; +export { + PluginsService, + PluginsServiceSetup, + PluginsServiceStart, + UiPlugins, +} from './plugins_service'; export { config } from './plugins_config'; /** @internal */ export { isNewPlatformPlugin } from './discovery'; diff --git a/src/core/server/plugins/plugin.ts b/src/core/server/plugins/plugin.ts index 7c67ab7a48df1..d7cfaa14d2343 100644 --- a/src/core/server/plugins/plugin.ts +++ b/src/core/server/plugins/plugin.ts @@ -21,7 +21,7 @@ import { join } from 'path'; import typeDetect from 'type-detect'; import { Subject } from 'rxjs'; import { first } from 'rxjs/operators'; -import { Type } from '@kbn/config-schema'; +import { isConfigSchema } from '@kbn/config-schema'; import { Logger } from '../logging'; import { @@ -150,7 +150,7 @@ export class PluginWrapper< } const configDescriptor = pluginDefinition.config; - if (!(configDescriptor.schema instanceof Type)) { + if (!isConfigSchema(configDescriptor.schema)) { throw new Error('Configuration schema expected to be an instance of Type'); } return configDescriptor; diff --git a/src/core/server/plugins/plugin_context.ts b/src/core/server/plugins/plugin_context.ts index 61d97aea97459..ab18a9cbbc062 100644 --- a/src/core/server/plugins/plugin_context.ts +++ b/src/core/server/plugins/plugin_context.ts @@ -136,6 +136,8 @@ export function createPluginSetupContext( deps: PluginsServiceSetupDeps, plugin: PluginWrapper ): CoreSetup { + const router = deps.http.createRouter('', plugin.opaqueId); + return { capabilities: { registerProvider: deps.capabilities.registerProvider, @@ -155,7 +157,8 @@ export function createPluginSetupContext( null, plugin.opaqueId ), - createRouter: () => deps.http.createRouter('', plugin.opaqueId), + createRouter: () => router, + resources: deps.httpResources.createRegistrar(router), registerOnPreAuth: deps.http.registerOnPreAuth, registerAuth: deps.http.registerAuth, registerOnPostAuth: deps.http.registerOnPostAuth, diff --git a/src/core/server/plugins/plugins_service.mock.ts b/src/core/server/plugins/plugins_service.mock.ts index 29e5b83b2e4c7..a40566767ddae 100644 --- a/src/core/server/plugins/plugins_service.mock.ts +++ b/src/core/server/plugins/plugins_service.mock.ts @@ -23,14 +23,10 @@ type PluginsServiceMock = jest.Mocked>; const createSetupContractMock = (): PluginsServiceSetup => ({ contracts: new Map(), - uiPlugins: { - browserConfigs: new Map(), - internal: new Map(), - public: new Map(), - }, initialized: true, }); const createStartContractMock = () => ({ contracts: new Map() }); + const createServiceMock = (): PluginsServiceMock => ({ discover: jest.fn(), setup: jest.fn().mockResolvedValue(createSetupContractMock()), @@ -38,8 +34,17 @@ const createServiceMock = (): PluginsServiceMock => ({ stop: jest.fn(), }); +function createUiPlugins() { + return { + browserConfigs: new Map(), + internal: new Map(), + public: new Map(), + }; +} + export const pluginServiceMock = { create: createServiceMock, createSetupContract: createSetupContractMock, createStartContract: createStartContractMock, + createUiPlugins, }; diff --git a/src/core/server/plugins/plugins_service.test.ts b/src/core/server/plugins/plugins_service.test.ts index 14147ab9f2a8d..38fda12bd290f 100644 --- a/src/core/server/plugins/plugins_service.test.ts +++ b/src/core/server/plugins/plugins_service.test.ts @@ -120,6 +120,7 @@ describe('PluginsService', () => { pluginsService = new PluginsService({ coreId, env, logger, configService }); [mockPluginSystem] = MockPluginsSystem.mock.instances as any; + mockPluginSystem.uiPlugins.mockReturnValue(new Map()); }); afterEach(() => { @@ -202,7 +203,6 @@ describe('PluginsService', () => { .mockImplementation(path => Promise.resolve(!path.includes('disabled'))); mockPluginSystem.setupPlugins.mockResolvedValue(new Map()); - mockPluginSystem.uiPlugins.mockReturnValue(new Map()); mockDiscover.mockReturnValue({ error$: from([]), @@ -234,8 +234,6 @@ describe('PluginsService', () => { const setup = await pluginsService.setup(setupDeps); expect(setup.contracts).toBeInstanceOf(Map); - expect(setup.uiPlugins.public).toBeInstanceOf(Map); - expect(setup.uiPlugins.internal).toBeInstanceOf(Map); expect(mockPluginSystem.addPlugin).not.toHaveBeenCalled(); expect(mockPluginSystem.setupPlugins).toHaveBeenCalledTimes(1); expect(mockPluginSystem.setupPlugins).toHaveBeenCalledWith(setupDeps); @@ -273,7 +271,8 @@ describe('PluginsService', () => { plugin$: from([firstPlugin, secondPlugin]), }); - await expect(pluginsService.discover()).resolves.toBeUndefined(); + const { pluginTree } = await pluginsService.discover(); + expect(pluginTree).toBeUndefined(); expect(mockDiscover).toHaveBeenCalledTimes(1); expect(mockPluginSystem.addPlugin).toHaveBeenCalledTimes(2); @@ -308,7 +307,8 @@ describe('PluginsService', () => { plugin$: from([firstPlugin, secondPlugin, thirdPlugin, lastPlugin, missingDepsPlugin]), }); - await expect(pluginsService.discover()).resolves.toBeUndefined(); + const { pluginTree } = await pluginsService.discover(); + expect(pluginTree).toBeUndefined(); expect(mockDiscover).toHaveBeenCalledTimes(1); expect(mockPluginSystem.addPlugin).toHaveBeenCalledTimes(4); @@ -466,12 +466,8 @@ describe('PluginsService', () => { }); mockPluginSystem.uiPlugins.mockReturnValue(new Map([pluginToDiscoveredEntry(plugin)])); - await pluginsService.discover(); - const { - uiPlugins: { browserConfigs }, - } = await pluginsService.setup(setupDeps); - - const uiConfig$ = browserConfigs.get('plugin-with-expose'); + const { uiPlugins } = await pluginsService.discover(); + const uiConfig$ = uiPlugins.browserConfigs.get('plugin-with-expose'); expect(uiConfig$).toBeDefined(); const uiConfig = await uiConfig$!.pipe(take(1)).toPromise(); @@ -506,12 +502,8 @@ describe('PluginsService', () => { }); mockPluginSystem.uiPlugins.mockReturnValue(new Map([pluginToDiscoveredEntry(plugin)])); - await pluginsService.discover(); - const { - uiPlugins: { browserConfigs }, - } = await pluginsService.setup(setupDeps); - - expect([...browserConfigs.entries()]).toHaveLength(0); + const { uiPlugins } = await pluginsService.discover(); + expect([...uiPlugins.browserConfigs.entries()]).toHaveLength(0); }); }); @@ -539,8 +531,7 @@ describe('PluginsService', () => { describe('uiPlugins.internal', () => { it('includes disabled plugins', async () => { config$.next({ plugins: { initialize: true }, plugin1: { enabled: false } }); - await pluginsService.discover(); - const { uiPlugins } = await pluginsService.setup(setupDeps); + const { uiPlugins } = await pluginsService.discover(); expect(uiPlugins.internal).toMatchInlineSnapshot(` Map { "plugin-1" => Object { diff --git a/src/core/server/plugins/plugins_service.ts b/src/core/server/plugins/plugins_service.ts index a0ecee47c675f..d7a348affe94f 100644 --- a/src/core/server/plugins/plugins_service.ts +++ b/src/core/server/plugins/plugins_service.ts @@ -39,23 +39,25 @@ export interface PluginsServiceSetup { initialized: boolean; /** Setup contracts returned by plugins. */ contracts: Map; - uiPlugins: { - /** - * Paths to all discovered ui plugin entrypoints on the filesystem, even if - * disabled. - */ - internal: Map; - - /** - * Information needed by client-side to load plugins and wire dependencies. - */ - public: Map; - - /** - * Configuration for plugins to be exposed to the client-side. - */ - browserConfigs: Map>; - }; +} + +/** @internal */ +export interface UiPlugins { + /** + * Paths to all discovered ui plugin entrypoints on the filesystem, even if + * disabled. + */ + internal: Map; + + /** + * Information needed by client-side to load plugins and wire dependencies. + */ + public: Map; + + /** + * Configuration for plugins to be exposed to the client-side. + */ + browserConfigs: Map>; } /** @internal */ @@ -97,8 +99,17 @@ export class PluginsService implements CoreService; -export const setupMock: jest.Mocked = { +export const setupMock: jest.Mocked = { render: jest.fn(), }; export const mockSetup = jest.fn().mockResolvedValue(setupMock); diff --git a/src/core/server/rendering/rendering_service.mock.ts b/src/core/server/rendering/rendering_service.mock.ts new file mode 100644 index 0000000000000..7eba332512386 --- /dev/null +++ b/src/core/server/rendering/rendering_service.mock.ts @@ -0,0 +1,31 @@ +/* + * 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 { InternalRenderingServiceSetup } from './types'; + +function createRenderingSetup() { + const mocked: jest.Mocked = { + render: jest.fn().mockResolvedValue(''), + }; + return mocked; +} + +export const renderingMock = { + createSetupContract: createRenderingSetup, +}; diff --git a/src/core/server/rendering/rendering_service.test.ts b/src/core/server/rendering/rendering_service.test.ts index 43ff4f633085c..d1c527aca4dba 100644 --- a/src/core/server/rendering/rendering_service.test.ts +++ b/src/core/server/rendering/rendering_service.test.ts @@ -22,7 +22,7 @@ import { load } from 'cheerio'; import { httpServerMock } from '../http/http_server.mocks'; import { uiSettingsServiceMock } from '../ui_settings/ui_settings_service.mock'; import { mockRenderingServiceParams, mockRenderingSetupDeps } from './__mocks__/params'; -import { RenderingServiceSetup } from './types'; +import { InternalRenderingServiceSetup } from './types'; import { RenderingService } from './rendering_service'; const INJECTED_METADATA = { @@ -62,15 +62,9 @@ describe('RenderingService', () => { }); describe('setup()', () => { - it('creates instance of RenderingServiceSetup', async () => { - const rendering = await service.setup(mockRenderingSetupDeps); - - expect(rendering.render).toBeInstanceOf(Function); - }); - describe('render()', () => { let uiSettings: ReturnType; - let render: RenderingServiceSetup['render']; + let render: InternalRenderingServiceSetup['render']; beforeEach(async () => { uiSettings = uiSettingsServiceMock.createClient(); @@ -78,6 +72,13 @@ describe('RenderingService', () => { registered: { name: 'title' }, }); render = (await service.setup(mockRenderingSetupDeps)).render; + await service.start({ + legacy: { + legacyInternals: { + getVars: () => ({}), + }, + }, + } as any); }); it('renders "core" page', async () => { diff --git a/src/core/server/rendering/rendering_service.tsx b/src/core/server/rendering/rendering_service.tsx index dbafd5806bd74..a02d85d22b2cb 100644 --- a/src/core/server/rendering/rendering_service.tsx +++ b/src/core/server/rendering/rendering_service.tsx @@ -23,41 +23,37 @@ import { take } from 'rxjs/operators'; import { i18n } from '@kbn/i18n'; +import { UiPlugins } from '../plugins'; import { CoreService } from '../../types'; import { CoreContext } from '../core_context'; import { Template } from './views'; +import { LegacyService } from '../legacy'; import { IRenderOptions, RenderingSetupDeps, - RenderingServiceSetup, + InternalRenderingServiceSetup, RenderingMetadata, } from './types'; /** @internal */ -export class RenderingService implements CoreService { +export class RenderingService implements CoreService { + private legacyInternals?: LegacyService['legacyInternals']; constructor(private readonly coreContext: CoreContext) {} public async setup({ http, legacyPlugins, - plugins, - }: RenderingSetupDeps): Promise { - async function getUiConfig(pluginId: string) { - const browserConfig = plugins.uiPlugins.browserConfigs.get(pluginId); - - return ((await browserConfig?.pipe(take(1)).toPromise()) ?? {}) as Record; - } - + uiPlugins, + }: RenderingSetupDeps): Promise { return { render: async ( request, uiSettings, - { - app = { getId: () => 'core' }, - includeUserSettings = true, - vars = {}, - }: IRenderOptions = {} + { app = { getId: () => 'core' }, includeUserSettings = true, vars }: IRenderOptions = {} ) => { + if (!this.legacyInternals) { + throw new Error('Cannot render before "start"'); + } const { env } = this.coreContext; const basePath = http.basePath.get(request); const serverBasePath = http.basePath.serverBasePath; @@ -87,12 +83,12 @@ export class RenderingService implements CoreService { translationsUrl: `${basePath}/translations/${i18n.getLocale()}.json`, }, csp: { warnLegacyBrowsers: http.csp.warnLegacyBrowsers }, - vars, + vars: vars ?? (await this.legacyInternals!.getVars('core', request)), uiPlugins: await Promise.all( - [...plugins.uiPlugins.public].map(async ([id, plugin]) => ({ + [...uiPlugins.public].map(async ([id, plugin]) => ({ id, plugin, - config: await getUiConfig(id), + config: await this.getUiConfig(uiPlugins, id), })) ), legacyMetadata: { @@ -116,7 +112,15 @@ export class RenderingService implements CoreService { }; } - public async start() {} + public async start({ legacy }: { legacy: LegacyService }) { + this.legacyInternals = legacy.legacyInternals; + } public async stop() {} + + private async getUiConfig(uiPlugins: UiPlugins, pluginId: string) { + const browserConfig = uiPlugins.browserConfigs.get(pluginId); + + return ((await browserConfig?.pipe(take(1)).toPromise()) ?? {}) as Record; + } } diff --git a/src/core/server/rendering/types.ts b/src/core/server/rendering/types.ts index cfaa23d491139..2a3be93055006 100644 --- a/src/core/server/rendering/types.ts +++ b/src/core/server/rendering/types.ts @@ -23,7 +23,7 @@ import { Env } from '../config'; import { ICspConfig } from '../csp'; import { InternalHttpServiceSetup, KibanaRequest, LegacyRequest } from '../http'; import { LegacyNavLink, LegacyServiceDiscoverPlugins } from '../legacy'; -import { PluginsServiceSetup, DiscoveredPlugin } from '../plugins'; +import { UiPlugins, DiscoveredPlugin } from '../plugins'; import { IUiSettingsClient, UserProvidedValues } from '../ui_settings'; /** @internal */ @@ -75,7 +75,7 @@ export interface RenderingMetadata { export interface RenderingSetupDeps { http: InternalHttpServiceSetup; legacyPlugins: LegacyServiceDiscoverPlugins; - plugins: PluginsServiceSetup; + uiPlugins: UiPlugins; } /** @public */ @@ -102,31 +102,8 @@ export interface IRenderOptions { vars?: Record; } -/** @public */ -export interface IScopedRenderingClient { - /** - * Generate a `KibanaResponse` which renders an HTML page bootstrapped - * with the `core` bundle. Intended as a response body for HTTP route handlers. - * - * @example - * ```ts - * router.get( - * { path: '/', validate: false }, - * (context, request, response) => - * response.ok({ - * body: await context.core.rendering.render(), - * headers: { - * 'content-security-policy': context.core.http.csp.header, - * }, - * }) - * ); - * ``` - */ - render(options?: Pick): Promise; -} - /** @internal */ -export interface RenderingServiceSetup { +export interface InternalRenderingServiceSetup { /** * Generate a `KibanaResponse` which renders an HTML page bootstrapped * with the `core` bundle or the ID of another specified legacy bundle. diff --git a/src/core/server/saved_objects/__snapshots__/utils.test.ts.snap b/src/core/server/saved_objects/__snapshots__/utils.test.ts.snap index 5431d2ca47892..7cd0297e57857 100644 --- a/src/core/server/saved_objects/__snapshots__/utils.test.ts.snap +++ b/src/core/server/saved_objects/__snapshots__/utils.test.ts.snap @@ -16,7 +16,7 @@ Array [ }, "migrations": Object {}, "name": "typeA", - "namespaceAgnostic": false, + "namespaceType": "single", }, Object { "convertToAliasScript": undefined, @@ -32,7 +32,7 @@ Array [ }, "migrations": Object {}, "name": "typeB", - "namespaceAgnostic": false, + "namespaceType": "single", }, Object { "convertToAliasScript": undefined, @@ -48,7 +48,7 @@ Array [ }, "migrations": Object {}, "name": "typeC", - "namespaceAgnostic": false, + "namespaceType": "single", }, ] `; @@ -72,7 +72,7 @@ Array [ "2.0.4": [Function], }, "name": "typeA", - "namespaceAgnostic": true, + "namespaceType": "agnostic", }, Object { "convertToAliasScript": "some alias script", @@ -91,7 +91,7 @@ Array [ }, "migrations": Object {}, "name": "typeB", - "namespaceAgnostic": false, + "namespaceType": "single", }, Object { "convertToAliasScript": undefined, @@ -109,7 +109,7 @@ Array [ "1.5.3": [Function], }, "name": "typeC", - "namespaceAgnostic": false, + "namespaceType": "single", }, ] `; @@ -130,7 +130,23 @@ Array [ }, "migrations": Object {}, "name": "typeA", - "namespaceAgnostic": true, + "namespaceType": "agnostic", + }, + Object { + "convertToAliasScript": undefined, + "hidden": false, + "indexPattern": "barBaz", + "management": undefined, + "mappings": Object { + "properties": Object { + "fieldB": Object { + "type": "text", + }, + }, + }, + "migrations": Object {}, + "name": "typeB", + "namespaceType": "multiple", }, Object { "convertToAliasScript": undefined, @@ -146,7 +162,23 @@ Array [ }, "migrations": Object {}, "name": "typeC", - "namespaceAgnostic": false, + "namespaceType": "single", + }, + Object { + "convertToAliasScript": undefined, + "hidden": false, + "indexPattern": "bazQux", + "management": undefined, + "mappings": Object { + "properties": Object { + "fieldD": Object { + "type": "text", + }, + }, + }, + "migrations": Object {}, + "name": "typeD", + "namespaceType": "agnostic", }, ] `; diff --git a/src/core/server/saved_objects/index.ts b/src/core/server/saved_objects/index.ts index fe4795cad11a5..a294b28753f7b 100644 --- a/src/core/server/saved_objects/index.ts +++ b/src/core/server/saved_objects/index.ts @@ -69,6 +69,7 @@ export { } from './migrations'; export { + SavedObjectsNamespaceType, SavedObjectStatusMeta, SavedObjectsType, SavedObjectsTypeManagementDefinition, diff --git a/src/core/server/saved_objects/migrations/core/__snapshots__/build_active_mappings.test.ts.snap b/src/core/server/saved_objects/migrations/core/__snapshots__/build_active_mappings.test.ts.snap index fc26d7e9cf6e9..bc9a66926e880 100644 --- a/src/core/server/saved_objects/migrations/core/__snapshots__/build_active_mappings.test.ts.snap +++ b/src/core/server/saved_objects/migrations/core/__snapshots__/build_active_mappings.test.ts.snap @@ -8,6 +8,7 @@ Object { "bbb": "18c78c995965207ed3f6e7fc5c6e55fe", "migrationVersion": "4a1746014a75ade3a714e1db5763276f", "namespace": "2f4316de49999235636386fe51dc06c1", + "namespaces": "2f4316de49999235636386fe51dc06c1", "references": "7997cf5a56cc02bdc9c93361bde732b0", "type": "2f4316de49999235636386fe51dc06c1", "updated_at": "00da57df13e94e9d98437d13ace4bfe0", @@ -28,6 +29,9 @@ Object { "namespace": Object { "type": "keyword", }, + "namespaces": Object { + "type": "keyword", + }, "references": Object { "properties": Object { "id": Object { @@ -59,6 +63,7 @@ Object { "firstType": "635418ab953d81d93f1190b70a8d3f57", "migrationVersion": "4a1746014a75ade3a714e1db5763276f", "namespace": "2f4316de49999235636386fe51dc06c1", + "namespaces": "2f4316de49999235636386fe51dc06c1", "references": "7997cf5a56cc02bdc9c93361bde732b0", "secondType": "72d57924f415fbadb3ee293b67d233ab", "thirdType": "510f1f0adb69830cf8a1c5ce2923ed82", @@ -83,6 +88,9 @@ Object { "namespace": Object { "type": "keyword", }, + "namespaces": Object { + "type": "keyword", + }, "references": Object { "properties": Object { "id": Object { diff --git a/src/core/server/saved_objects/migrations/core/build_active_mappings.ts b/src/core/server/saved_objects/migrations/core/build_active_mappings.ts index 4d1a607414ca6..418ed95f14e05 100644 --- a/src/core/server/saved_objects/migrations/core/build_active_mappings.ts +++ b/src/core/server/saved_objects/migrations/core/build_active_mappings.ts @@ -142,6 +142,9 @@ function defaultMapping(): IndexMapping { namespace: { type: 'keyword', }, + namespaces: { + type: 'keyword', + }, updated_at: { type: 'date', }, diff --git a/src/core/server/saved_objects/migrations/core/build_index_map.test.ts b/src/core/server/saved_objects/migrations/core/build_index_map.test.ts index 44add4e977006..2c710d4eaa079 100644 --- a/src/core/server/saved_objects/migrations/core/build_index_map.test.ts +++ b/src/core/server/saved_objects/migrations/core/build_index_map.test.ts @@ -26,7 +26,7 @@ const createRegistry = (...types: Array>) => { types.forEach(type => registry.registerType({ name: 'unknown', - namespaceAgnostic: false, + namespaceType: 'single', hidden: false, mappings: { properties: {} }, migrations: {}, @@ -41,7 +41,7 @@ test('mappings without index pattern goes to default index', () => { kibanaIndexName: '.kibana', registry: createRegistry({ name: 'type1', - namespaceAgnostic: false, + namespaceType: 'single', }), indexMap: { type1: { @@ -73,7 +73,7 @@ test(`mappings with custom index pattern doesn't go to default index`, () => { kibanaIndexName: '.kibana', registry: createRegistry({ name: 'type1', - namespaceAgnostic: false, + namespaceType: 'single', indexPattern: '.other_kibana', }), indexMap: { @@ -106,7 +106,7 @@ test('creating a script gets added to the index pattern', () => { kibanaIndexName: '.kibana', registry: createRegistry({ name: 'type1', - namespaceAgnostic: false, + namespaceType: 'single', indexPattern: '.other_kibana', convertToAliasScript: `ctx._id = ctx._source.type + ':' + ctx._id`, }), @@ -141,12 +141,12 @@ test('throws when two scripts are defined for an index pattern', () => { const registry = createRegistry( { name: 'type1', - namespaceAgnostic: false, + namespaceType: 'single', convertToAliasScript: `ctx._id = ctx._source.type + ':' + ctx._id`, }, { name: 'type2', - namespaceAgnostic: false, + namespaceType: 'single', convertToAliasScript: `ctx._id = ctx._source.type + ':' + ctx._id`, } ); diff --git a/src/core/server/saved_objects/migrations/core/document_migrator.test.ts b/src/core/server/saved_objects/migrations/core/document_migrator.test.ts index ef3f546b5e574..64270c677ff20 100644 --- a/src/core/server/saved_objects/migrations/core/document_migrator.test.ts +++ b/src/core/server/saved_objects/migrations/core/document_migrator.test.ts @@ -32,7 +32,7 @@ const createRegistry = (...types: Array>) => { types.forEach(type => registry.registerType({ name: 'unknown', - namespaceAgnostic: false, + namespaceType: 'single', hidden: false, mappings: { properties: {} }, migrations: {}, 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 1c2d3f501ff80..19208e6c83596 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 @@ -61,6 +61,7 @@ describe('IndexMigrator', () => { foo: '18c78c995965207ed3f6e7fc5c6e55fe', migrationVersion: '4a1746014a75ade3a714e1db5763276f', namespace: '2f4316de49999235636386fe51dc06c1', + namespaces: '2f4316de49999235636386fe51dc06c1', references: '7997cf5a56cc02bdc9c93361bde732b0', type: '2f4316de49999235636386fe51dc06c1', updated_at: '00da57df13e94e9d98437d13ace4bfe0', @@ -70,6 +71,7 @@ describe('IndexMigrator', () => { foo: { type: 'long' }, migrationVersion: { dynamic: 'true', type: 'object' }, namespace: { type: 'keyword' }, + namespaces: { type: 'keyword' }, type: { type: 'keyword' }, updated_at: { type: 'date' }, references: { @@ -178,6 +180,7 @@ describe('IndexMigrator', () => { foo: '625b32086eb1d1203564cf85062dd22e', migrationVersion: '4a1746014a75ade3a714e1db5763276f', namespace: '2f4316de49999235636386fe51dc06c1', + namespaces: '2f4316de49999235636386fe51dc06c1', references: '7997cf5a56cc02bdc9c93361bde732b0', type: '2f4316de49999235636386fe51dc06c1', updated_at: '00da57df13e94e9d98437d13ace4bfe0', @@ -188,6 +191,7 @@ describe('IndexMigrator', () => { foo: { type: 'text' }, migrationVersion: { dynamic: 'true', type: 'object' }, namespace: { type: 'keyword' }, + namespaces: { type: 'keyword' }, type: { type: 'keyword' }, updated_at: { type: 'date' }, references: { diff --git a/src/core/server/saved_objects/migrations/kibana/__snapshots__/kibana_migrator.test.ts.snap b/src/core/server/saved_objects/migrations/kibana/__snapshots__/kibana_migrator.test.ts.snap index 507c0b0d9339f..3453f3fc80310 100644 --- a/src/core/server/saved_objects/migrations/kibana/__snapshots__/kibana_migrator.test.ts.snap +++ b/src/core/server/saved_objects/migrations/kibana/__snapshots__/kibana_migrator.test.ts.snap @@ -8,6 +8,7 @@ Object { "bmap": "510f1f0adb69830cf8a1c5ce2923ed82", "migrationVersion": "4a1746014a75ade3a714e1db5763276f", "namespace": "2f4316de49999235636386fe51dc06c1", + "namespaces": "2f4316de49999235636386fe51dc06c1", "references": "7997cf5a56cc02bdc9c93361bde732b0", "type": "2f4316de49999235636386fe51dc06c1", "updated_at": "00da57df13e94e9d98437d13ace4bfe0", @@ -36,6 +37,9 @@ Object { "namespace": Object { "type": "keyword", }, + "namespaces": Object { + "type": "keyword", + }, "references": Object { "properties": Object { "id": Object { diff --git a/src/core/server/saved_objects/migrations/kibana/kibana_migrator.mock.ts b/src/core/server/saved_objects/migrations/kibana/kibana_migrator.mock.ts index 257b32c1e4c23..3f5c0c3876615 100644 --- a/src/core/server/saved_objects/migrations/kibana/kibana_migrator.mock.ts +++ b/src/core/server/saved_objects/migrations/kibana/kibana_migrator.mock.ts @@ -27,7 +27,7 @@ const defaultSavedObjectTypes: SavedObjectsType[] = [ { name: 'testtype', hidden: false, - namespaceAgnostic: false, + namespaceType: 'single', mappings: { properties: { name: { type: 'keyword' }, diff --git a/src/core/server/saved_objects/migrations/kibana/kibana_migrator.test.ts b/src/core/server/saved_objects/migrations/kibana/kibana_migrator.test.ts index 336eeff99f47b..cda0e86f15bdf 100644 --- a/src/core/server/saved_objects/migrations/kibana/kibana_migrator.test.ts +++ b/src/core/server/saved_objects/migrations/kibana/kibana_migrator.test.ts @@ -28,8 +28,8 @@ const createRegistry = (types: Array>) => { types.forEach(type => registry.registerType({ name: 'unknown', - namespaceAgnostic: false, hidden: false, + namespaceType: 'single', mappings: { properties: {} }, migrations: {}, ...type, @@ -120,7 +120,7 @@ function mockOptions(): KibanaMigratorOptions { { name: 'testtype', hidden: false, - namespaceAgnostic: false, + namespaceType: 'single', mappings: { properties: { name: { type: 'keyword' }, @@ -131,7 +131,7 @@ function mockOptions(): KibanaMigratorOptions { { name: 'testtype2', hidden: false, - namespaceAgnostic: false, + namespaceType: 'single', indexPattern: 'other-index', mappings: { properties: { diff --git a/src/core/server/saved_objects/routes/integration_tests/import.test.ts b/src/core/server/saved_objects/routes/integration_tests/import.test.ts index c72d3e241b882..c4a03a0e2e7d2 100644 --- a/src/core/server/saved_objects/routes/integration_tests/import.test.ts +++ b/src/core/server/saved_objects/routes/integration_tests/import.test.ts @@ -187,7 +187,7 @@ describe('POST /internal/saved_objects/_import', () => { references: [], error: { statusCode: 409, - message: 'version conflict, document already exists', + message: 'Saved object [index-pattern/my-pattern] conflict', }, }, { diff --git a/src/core/server/saved_objects/routes/integration_tests/test_utils.ts b/src/core/server/saved_objects/routes/integration_tests/test_utils.ts index 82a889f75d3c1..23e0285201dc7 100644 --- a/src/core/server/saved_objects/routes/integration_tests/test_utils.ts +++ b/src/core/server/saved_objects/routes/integration_tests/test_utils.ts @@ -49,7 +49,7 @@ export const createExportableType = (name: string): SavedObjectsType => { return { name, hidden: false, - namespaceAgnostic: false, + namespaceType: 'single', mappings: { properties: {}, }, diff --git a/src/core/server/saved_objects/saved_objects_service.test.ts b/src/core/server/saved_objects/saved_objects_service.test.ts index 018117776dcc8..819d79803f371 100644 --- a/src/core/server/saved_objects/saved_objects_service.test.ts +++ b/src/core/server/saved_objects/saved_objects_service.test.ts @@ -138,7 +138,7 @@ describe('SavedObjectsService', () => { const type = { name: 'someType', hidden: false, - namespaceAgnostic: false, + namespaceType: 'single' as 'single', mappings: { properties: {} }, }; setup.registerType(type); @@ -251,7 +251,7 @@ describe('SavedObjectsService', () => { setup.registerType({ name: 'someType', hidden: false, - namespaceAgnostic: false, + namespaceType: 'single' as 'single', mappings: { properties: {} }, }); }).toThrowErrorMatchingInlineSnapshot( diff --git a/src/core/server/saved_objects/saved_objects_service.ts b/src/core/server/saved_objects/saved_objects_service.ts index 62027928c0bb5..ed4ffef5729ab 100644 --- a/src/core/server/saved_objects/saved_objects_service.ts +++ b/src/core/server/saved_objects/saved_objects_service.ts @@ -124,7 +124,7 @@ export interface SavedObjectsServiceSetup { * export const myType: SavedObjectsType = { * name: 'MyType', * hidden: false, - * namespaceAgnostic: true, + * namespaceType: 'multiple', * mappings: { * properties: { * textField: { diff --git a/src/core/server/saved_objects/saved_objects_type_registry.mock.ts b/src/core/server/saved_objects/saved_objects_type_registry.mock.ts index 8c8458d7a5ce4..8bb66859feca2 100644 --- a/src/core/server/saved_objects/saved_objects_type_registry.mock.ts +++ b/src/core/server/saved_objects/saved_objects_type_registry.mock.ts @@ -27,6 +27,8 @@ const createRegistryMock = (): jest.Mocked type === 'global'); + mock.isSingleNamespace.mockImplementation( + (type: string) => type !== 'global' && type !== 'shared' + ); + mock.isMultiNamespace.mockImplementation((type: string) => type === 'shared'); mock.isImportableAndExportable.mockReturnValue(true); return mock; diff --git a/src/core/server/saved_objects/saved_objects_type_registry.test.ts b/src/core/server/saved_objects/saved_objects_type_registry.test.ts index 4d1d5c1eacc25..f82822f90f489 100644 --- a/src/core/server/saved_objects/saved_objects_type_registry.test.ts +++ b/src/core/server/saved_objects/saved_objects_type_registry.test.ts @@ -23,7 +23,7 @@ import { SavedObjectsType } from './types'; const createType = (type: Partial): SavedObjectsType => ({ name: 'unknown', hidden: false, - namespaceAgnostic: false, + namespaceType: 'single' as 'single', mappings: { properties: {} }, migrations: {}, ...type, @@ -164,18 +164,68 @@ describe('SavedObjectTypeRegistry', () => { }); describe('#isNamespaceAgnostic', () => { - it('returns correct value for the type', () => { - registry.registerType(createType({ name: 'typeA', namespaceAgnostic: true })); - registry.registerType(createType({ name: 'typeB', namespaceAgnostic: false })); + const expectResult = (expected: boolean, schemaDefinition?: Partial) => { + registry = new SavedObjectTypeRegistry(); + registry.registerType(createType({ name: 'foo', ...schemaDefinition })); + expect(registry.isNamespaceAgnostic('foo')).toBe(expected); + }; - expect(registry.isNamespaceAgnostic('typeA')).toEqual(true); - expect(registry.isNamespaceAgnostic('typeB')).toEqual(false); + it(`returns false when the type is not registered`, () => { + expect(registry.isNamespaceAgnostic('unknownType')).toEqual(false); }); - it('returns false when the type is not registered', () => { - registry.registerType(createType({ name: 'typeA', namespaceAgnostic: true })); - registry.registerType(createType({ name: 'typeB', namespaceAgnostic: false })); - expect(registry.isNamespaceAgnostic('unknownType')).toEqual(false); + it(`returns true for namespaceType 'agnostic'`, () => { + expectResult(true, { namespaceType: 'agnostic' }); + }); + + it(`returns false for other namespaceType`, () => { + expectResult(false, { namespaceType: 'multiple' }); + expectResult(false, { namespaceType: 'single' }); + expectResult(false, { namespaceType: undefined }); + }); + }); + + describe('#isSingleNamespace', () => { + const expectResult = (expected: boolean, schemaDefinition?: Partial) => { + registry = new SavedObjectTypeRegistry(); + registry.registerType(createType({ name: 'foo', ...schemaDefinition })); + expect(registry.isSingleNamespace('foo')).toBe(expected); + }; + + it(`returns true when the type is not registered`, () => { + expect(registry.isSingleNamespace('unknownType')).toEqual(true); + }); + + it(`returns true for namespaceType 'single'`, () => { + expectResult(true, { namespaceType: 'single' }); + expectResult(true, { namespaceType: undefined }); + }); + + it(`returns false for other namespaceType`, () => { + expectResult(false, { namespaceType: 'agnostic' }); + expectResult(false, { namespaceType: 'multiple' }); + }); + }); + + describe('#isMultiNamespace', () => { + const expectResult = (expected: boolean, schemaDefinition?: Partial) => { + registry = new SavedObjectTypeRegistry(); + registry.registerType(createType({ name: 'foo', ...schemaDefinition })); + expect(registry.isMultiNamespace('foo')).toBe(expected); + }; + + it(`returns false when the type is not registered`, () => { + expect(registry.isMultiNamespace('unknownType')).toEqual(false); + }); + + it(`returns true for namespaceType 'multiple'`, () => { + expectResult(true, { namespaceType: 'multiple' }); + }); + + it(`returns false for other namespaceType`, () => { + expectResult(false, { namespaceType: 'agnostic' }); + expectResult(false, { namespaceType: 'single' }); + expectResult(false, { namespaceType: undefined }); }); }); @@ -206,8 +256,8 @@ describe('SavedObjectTypeRegistry', () => { expect(registry.getIndex('typeWithNoIndex')).toBeUndefined(); }); it('returns undefined when the type is not registered', () => { - registry.registerType(createType({ name: 'typeA', namespaceAgnostic: true })); - registry.registerType(createType({ name: 'typeB', namespaceAgnostic: false })); + registry.registerType(createType({ name: 'typeA', namespaceType: 'agnostic' })); + registry.registerType(createType({ name: 'typeB', namespaceType: 'single' })); expect(registry.getIndex('unknownType')).toBeUndefined(); }); diff --git a/src/core/server/saved_objects/saved_objects_type_registry.ts b/src/core/server/saved_objects/saved_objects_type_registry.ts index 5580ce3815d0d..740313a53d1e2 100644 --- a/src/core/server/saved_objects/saved_objects_type_registry.ts +++ b/src/core/server/saved_objects/saved_objects_type_registry.ts @@ -25,16 +25,7 @@ import { SavedObjectsType } from './types'; * * @public */ -export type ISavedObjectTypeRegistry = Pick< - SavedObjectTypeRegistry, - | 'getType' - | 'getAllTypes' - | 'getIndex' - | 'isNamespaceAgnostic' - | 'isHidden' - | 'getImportableAndExportableTypes' - | 'isImportableAndExportable' ->; +export type ISavedObjectTypeRegistry = Omit; /** * Registry holding information about all the registered {@link SavedObjectsType | saved object types}. @@ -77,11 +68,28 @@ export class SavedObjectTypeRegistry { } /** - * Returns the `namespaceAgnostic` property for given type, or `false` if - * the type is not registered. + * Returns whether the type is namespace-agnostic (global); + * resolves to `false` if the type is not registered */ public isNamespaceAgnostic(type: string) { - return this.types.get(type)?.namespaceAgnostic ?? false; + return this.types.get(type)?.namespaceType === 'agnostic'; + } + + /** + * Returns whether the type is single-namespace (isolated); + * resolves to `true` if the type is not registered + */ + public isSingleNamespace(type: string) { + // in the case we somehow registered a type with an invalid `namespaceType`, treat it as single-namespace + return !this.isNamespaceAgnostic(type) && !this.isMultiNamespace(type); + } + + /** + * Returns whether the type is multi-namespace (shareable); + * resolves to `false` if the type is not registered + */ + public isMultiNamespace(type: string) { + return this.types.get(type)?.namespaceType === 'multiple'; } /** diff --git a/src/core/server/saved_objects/schema/schema.test.ts b/src/core/server/saved_objects/schema/schema.test.ts index 43cf27fbae790..f2daa13e43fce 100644 --- a/src/core/server/saved_objects/schema/schema.test.ts +++ b/src/core/server/saved_objects/schema/schema.test.ts @@ -17,32 +17,90 @@ * under the License. */ -import { SavedObjectsSchema } from './schema'; +import { SavedObjectsSchema, SavedObjectsSchemaDefinition } from './schema'; describe('#isNamespaceAgnostic', () => { + const expectResult = (expected: boolean, schemaDefinition?: SavedObjectsSchemaDefinition) => { + const schema = new SavedObjectsSchema(schemaDefinition); + const result = schema.isNamespaceAgnostic('foo'); + expect(result).toBe(expected); + }; + + it(`returns false when no schema is defined`, () => { + expectResult(false); + }); + it(`returns false for unknown types`, () => { - const schema = new SavedObjectsSchema(); - const result = schema.isNamespaceAgnostic('bar'); - expect(result).toBe(false); + expectResult(false, { bar: {} }); }); - it(`returns true for explicitly namespace agnostic type`, () => { - const schema = new SavedObjectsSchema({ - foo: { - isNamespaceAgnostic: true, - }, - }); - const result = schema.isNamespaceAgnostic('foo'); - expect(result).toBe(true); + it(`returns false for non-namespace-agnostic type`, () => { + expectResult(false, { foo: { isNamespaceAgnostic: false } }); + expectResult(false, { foo: { isNamespaceAgnostic: undefined } }); }); - it(`returns false for explicitly namespaced type`, () => { - const schema = new SavedObjectsSchema({ - foo: { - isNamespaceAgnostic: false, - }, - }); - const result = schema.isNamespaceAgnostic('foo'); - expect(result).toBe(false); + it(`returns true for explicitly namespace-agnostic type`, () => { + expectResult(true, { foo: { isNamespaceAgnostic: true } }); + }); +}); + +describe('#isSingleNamespace', () => { + const expectResult = (expected: boolean, schemaDefinition?: SavedObjectsSchemaDefinition) => { + const schema = new SavedObjectsSchema(schemaDefinition); + const result = schema.isSingleNamespace('foo'); + expect(result).toBe(expected); + }; + + it(`returns true when no schema is defined`, () => { + expectResult(true); + }); + + it(`returns true for unknown types`, () => { + expectResult(true, { bar: {} }); + }); + + it(`returns false for explicitly namespace-agnostic type`, () => { + expectResult(false, { foo: { isNamespaceAgnostic: true } }); + }); + + it(`returns false for explicitly multi-namespace type`, () => { + expectResult(false, { foo: { multiNamespace: true } }); + }); + + it(`returns true for non-namespace-agnostic and non-multi-namespace type`, () => { + expectResult(true, { foo: { isNamespaceAgnostic: false, multiNamespace: false } }); + expectResult(true, { foo: { isNamespaceAgnostic: false, multiNamespace: undefined } }); + expectResult(true, { foo: { isNamespaceAgnostic: undefined, multiNamespace: false } }); + expectResult(true, { foo: { isNamespaceAgnostic: undefined, multiNamespace: undefined } }); + }); +}); + +describe('#isMultiNamespace', () => { + const expectResult = (expected: boolean, schemaDefinition?: SavedObjectsSchemaDefinition) => { + const schema = new SavedObjectsSchema(schemaDefinition); + const result = schema.isMultiNamespace('foo'); + expect(result).toBe(expected); + }; + + it(`returns false when no schema is defined`, () => { + expectResult(false); + }); + + it(`returns false for unknown types`, () => { + expectResult(false, { bar: {} }); + }); + + it(`returns false for explicitly namespace-agnostic type`, () => { + expectResult(false, { foo: { isNamespaceAgnostic: true } }); + }); + + it(`returns false for non-multi-namespace type`, () => { + expectResult(false, { foo: { multiNamespace: false } }); + expectResult(false, { foo: { multiNamespace: undefined } }); + }); + + it(`returns true for non-namespace-agnostic and explicitly multi-namespace type`, () => { + expectResult(true, { foo: { isNamespaceAgnostic: false, multiNamespace: true } }); + expectResult(true, { foo: { isNamespaceAgnostic: undefined, multiNamespace: true } }); }); }); diff --git a/src/core/server/saved_objects/schema/schema.ts b/src/core/server/saved_objects/schema/schema.ts index 17ca406ea109a..ba1905158e822 100644 --- a/src/core/server/saved_objects/schema/schema.ts +++ b/src/core/server/saved_objects/schema/schema.ts @@ -24,7 +24,8 @@ import { LegacyConfig } from '../../legacy'; * @internal **/ interface SavedObjectsSchemaTypeDefinition { - isNamespaceAgnostic: boolean; + isNamespaceAgnostic?: boolean; + multiNamespace?: boolean; hidden?: boolean; indexPattern?: ((config: LegacyConfig) => string) | string; convertToAliasScript?: string; @@ -72,7 +73,7 @@ export class SavedObjectsSchema { } public isNamespaceAgnostic(type: string) { - // if no plugins have registered a uiExports.savedObjectSchemas, + // if no plugins have registered a Saved Objects Schema, // this.schema will be undefined, and no types are namespace agnostic if (!this.definition) { return false; @@ -84,4 +85,32 @@ export class SavedObjectsSchema { } return Boolean(typeSchema.isNamespaceAgnostic); } + + public isSingleNamespace(type: string) { + // if no plugins have registered a Saved Objects Schema, + // this.schema will be undefined, and all types are namespace isolated + if (!this.definition) { + return true; + } + + const typeSchema = this.definition[type]; + if (!typeSchema) { + return true; + } + return !Boolean(typeSchema.isNamespaceAgnostic) && !Boolean(typeSchema.multiNamespace); + } + + public isMultiNamespace(type: string) { + // if no plugins have registered a Saved Objects Schema, + // this.schema will be undefined, and no types are multi-namespace + if (!this.definition) { + return false; + } + + const typeSchema = this.definition[type]; + if (!typeSchema) { + return false; + } + return !Boolean(typeSchema.isNamespaceAgnostic) && Boolean(typeSchema.multiNamespace); + } } diff --git a/src/core/server/saved_objects/serialization/serializer.test.ts b/src/core/server/saved_objects/serialization/serializer.test.ts index 8f09b25bb3908..1a7dfdd2d130e 100644 --- a/src/core/server/saved_objects/serialization/serializer.test.ts +++ b/src/core/server/saved_objects/serialization/serializer.test.ts @@ -19,101 +19,101 @@ import _ from 'lodash'; import { SavedObjectsSerializer } from './serializer'; +import { SavedObjectsRawDoc } from './types'; import { typeRegistryMock } from '../saved_objects_type_registry.mock'; import { encodeVersion } from '../version'; -describe('saved object conversion', () => { - let typeRegistry: ReturnType; - - beforeEach(() => { - typeRegistry = typeRegistryMock.create(); - typeRegistry.isNamespaceAgnostic.mockReturnValue(false); +let typeRegistry = typeRegistryMock.create(); +typeRegistry.isNamespaceAgnostic.mockReturnValue(true); +typeRegistry.isSingleNamespace.mockReturnValue(false); +typeRegistry.isMultiNamespace.mockReturnValue(false); +const namespaceAgnosticSerializer = new SavedObjectsSerializer(typeRegistry); + +typeRegistry = typeRegistryMock.create(); +typeRegistry.isNamespaceAgnostic.mockReturnValue(false); +typeRegistry.isSingleNamespace.mockReturnValue(true); +typeRegistry.isMultiNamespace.mockReturnValue(false); +const singleNamespaceSerializer = new SavedObjectsSerializer(typeRegistry); + +typeRegistry = typeRegistryMock.create(); +typeRegistry.isNamespaceAgnostic.mockReturnValue(false); +typeRegistry.isSingleNamespace.mockReturnValue(false); +typeRegistry.isMultiNamespace.mockReturnValue(true); +const multiNamespaceSerializer = new SavedObjectsSerializer(typeRegistry); + +const sampleTemplate = { + _id: 'foo:bar', + _source: { + type: 'foo', + }, +}; +const createSampleDoc = (raw: any, template = sampleTemplate): SavedObjectsRawDoc => + _.defaultsDeep(raw, template); + +describe('#rawToSavedObject', () => { + test('it copies the _source.type property to type', () => { + const actual = singleNamespaceSerializer.rawToSavedObject({ + _id: 'foo:bar', + _source: { + type: 'foo', + }, + }); + expect(actual).toHaveProperty('type', 'foo'); }); - describe('#rawToSavedObject', () => { - test('it copies the _source.type property to type', () => { - const serializer = new SavedObjectsSerializer(typeRegistry); - const actual = serializer.rawToSavedObject({ - _id: 'foo:bar', - _source: { - type: 'foo', - }, - }); - expect(actual).toHaveProperty('type', 'foo'); + test('it copies the _source.references property to references', () => { + const actual = singleNamespaceSerializer.rawToSavedObject({ + _id: 'foo:bar', + _source: { + type: 'foo', + references: [{ name: 'ref_0', type: 'index-pattern', id: 'pattern*' }], + }, }); + expect(actual).toHaveProperty('references', [ + { + name: 'ref_0', + type: 'index-pattern', + id: 'pattern*', + }, + ]); + }); - test('it copies the _source.references property to references', () => { - const serializer = new SavedObjectsSerializer(typeRegistry); - const actual = serializer.rawToSavedObject({ - _id: 'foo:bar', - _source: { - type: 'foo', - references: [{ name: 'ref_0', type: 'index-pattern', id: 'pattern*' }], - }, - }); - expect(actual).toHaveProperty('references', [ - { - name: 'ref_0', - type: 'index-pattern', - id: 'pattern*', + test('if specified it copies the _source.migrationVersion property to migrationVersion', () => { + const actual = singleNamespaceSerializer.rawToSavedObject({ + _id: 'foo:bar', + _source: { + type: 'foo', + migrationVersion: { + hello: '1.2.3', + acl: '33.3.5', }, - ]); + }, }); - - test('if specified it copies the _source.migrationVersion property to migrationVersion', () => { - const serializer = new SavedObjectsSerializer(typeRegistry); - const actual = serializer.rawToSavedObject({ - _id: 'foo:bar', - _source: { - type: 'foo', - migrationVersion: { - hello: '1.2.3', - acl: '33.3.5', - }, - }, - }); - expect(actual).toHaveProperty('migrationVersion', { - hello: '1.2.3', - acl: '33.3.5', - }); + expect(actual).toHaveProperty('migrationVersion', { + hello: '1.2.3', + acl: '33.3.5', }); + }); - test(`if _source.migrationVersion is unspecified it doesn't set migrationVersion`, () => { - const serializer = new SavedObjectsSerializer(typeRegistry); - const actual = serializer.rawToSavedObject({ - _id: 'foo:bar', - _source: { - type: 'foo', - }, - }); - expect(actual).not.toHaveProperty('migrationVersion'); + test(`if _source.migrationVersion is unspecified it doesn't set migrationVersion`, () => { + const actual = singleNamespaceSerializer.rawToSavedObject({ + _id: 'foo:bar', + _source: { + type: 'foo', + }, }); + expect(actual).not.toHaveProperty('migrationVersion'); + }); - test('it converts the id and type properties, and retains migrationVersion', () => { - const now = String(new Date()); - const serializer = new SavedObjectsSerializer(typeRegistry); - const actual = serializer.rawToSavedObject({ - _id: 'hello:world', - _seq_no: 3, - _primary_term: 1, - _source: { - type: 'hello', - hello: { - a: 'b', - c: 'd', - }, - migrationVersion: { - hello: '1.2.3', - acl: '33.3.5', - }, - updated_at: now, - }, - }); - const expected = { - id: 'world', + test('it converts the id and type properties, and retains migrationVersion', () => { + const now = String(new Date()); + const actual = singleNamespaceSerializer.rawToSavedObject({ + _id: 'hello:world', + _seq_no: 3, + _primary_term: 1, + _source: { type: 'hello', - version: encodeVersion(3, 1), - attributes: { + hello: { a: 'b', c: 'd', }, @@ -122,909 +122,937 @@ describe('saved object conversion', () => { acl: '33.3.5', }, updated_at: now, - references: [], - }; - expect(expected).toEqual(actual); + }, + }); + const expected = { + id: 'world', + type: 'hello', + version: encodeVersion(3, 1), + attributes: { + a: 'b', + c: 'd', + }, + migrationVersion: { + hello: '1.2.3', + acl: '33.3.5', + }, + updated_at: now, + references: [], + }; + expect(expected).toEqual(actual); + }); + + test(`if version is unspecified it doesn't set version`, () => { + const actual = singleNamespaceSerializer.rawToSavedObject({ + _id: 'foo:bar', + _source: { + type: 'foo', + hello: {}, + }, }); + expect(actual).not.toHaveProperty('version'); + }); - test(`if version is unspecified it doesn't set version`, () => { - const serializer = new SavedObjectsSerializer(typeRegistry); - const actual = serializer.rawToSavedObject({ + test(`if specified it encodes _seq_no and _primary_term to version`, () => { + const actual = singleNamespaceSerializer.rawToSavedObject({ + _id: 'foo:bar', + _seq_no: 4, + _primary_term: 1, + _source: { + type: 'foo', + hello: {}, + }, + }); + expect(actual).toHaveProperty('version', encodeVersion(4, 1)); + }); + + test(`if only _seq_no is specified it throws`, () => { + expect(() => + singleNamespaceSerializer.rawToSavedObject({ _id: 'foo:bar', + _seq_no: 4, _source: { type: 'foo', hello: {}, }, - }); - expect(actual).not.toHaveProperty('version'); - }); + }) + ).toThrowErrorMatchingInlineSnapshot(`"_primary_term from elasticsearch must be an integer"`); + }); - test(`if specified it encodes _seq_no and _primary_term to version`, () => { - const serializer = new SavedObjectsSerializer(typeRegistry); - const actual = serializer.rawToSavedObject({ + test(`if only _primary_term is throws`, () => { + expect(() => + singleNamespaceSerializer.rawToSavedObject({ _id: 'foo:bar', - _seq_no: 4, _primary_term: 1, _source: { type: 'foo', hello: {}, }, - }); - expect(actual).toHaveProperty('version', encodeVersion(4, 1)); - }); + }) + ).toThrowErrorMatchingInlineSnapshot(`"_seq_no from elasticsearch must be an integer"`); + }); - test(`if only _seq_no is specified it throws`, () => { - const serializer = new SavedObjectsSerializer(typeRegistry); - expect(() => - serializer.rawToSavedObject({ - _id: 'foo:bar', - _seq_no: 4, - _source: { - type: 'foo', - hello: {}, - }, - }) - ).toThrowErrorMatchingInlineSnapshot(`"_primary_term from elasticsearch must be an integer"`); + test('if specified it copies the _source.updated_at property to updated_at', () => { + const now = Date(); + const actual = singleNamespaceSerializer.rawToSavedObject({ + _id: 'foo:bar', + _source: { + type: 'foo', + updated_at: now, + }, }); + expect(actual).toHaveProperty('updated_at', now); + }); - test(`if only _primary_term is throws`, () => { - const serializer = new SavedObjectsSerializer(typeRegistry); - expect(() => - serializer.rawToSavedObject({ - _id: 'foo:bar', - _primary_term: 1, - _source: { - type: 'foo', - hello: {}, - }, - }) - ).toThrowErrorMatchingInlineSnapshot(`"_seq_no from elasticsearch must be an integer"`); + test(`if _source.updated_at is unspecified it doesn't set updated_at`, () => { + const actual = singleNamespaceSerializer.rawToSavedObject({ + _id: 'foo:bar', + _source: { + type: 'foo', + }, }); + expect(actual).not.toHaveProperty('updated_at'); + }); - test('if specified it copies the _source.updated_at property to updated_at', () => { - const serializer = new SavedObjectsSerializer(typeRegistry); - const now = Date(); - const actual = serializer.rawToSavedObject({ - _id: 'foo:bar', - _source: { - type: 'foo', - updated_at: now, + test('it does not pass unknown properties through', () => { + const actual = singleNamespaceSerializer.rawToSavedObject({ + _id: 'universe', + _source: { + type: 'hello', + hello: { + world: 'earth', }, - }); - expect(actual).toHaveProperty('updated_at', now); + banjo: 'Steve Martin', + }, + }); + expect(actual).toEqual({ + id: 'universe', + type: 'hello', + attributes: { + world: 'earth', + }, + references: [], }); + }); - test(`if _source.updated_at is unspecified it doesn't set updated_at`, () => { - const serializer = new SavedObjectsSerializer(typeRegistry); - const actual = serializer.rawToSavedObject({ - _id: 'foo:bar', - _source: { - type: 'foo', - }, - }); - expect(actual).not.toHaveProperty('updated_at'); + test('it does not create attributes if [type] is missing', () => { + const actual = singleNamespaceSerializer.rawToSavedObject({ + _id: 'universe', + _source: { + type: 'hello', + }, }); + expect(actual).toEqual({ + id: 'universe', + type: 'hello', + references: [], + }); + }); - test('it does not pass unknown properties through', () => { - const serializer = new SavedObjectsSerializer(typeRegistry); - const actual = serializer.rawToSavedObject({ + test('it fails for documents which do not specify a type', () => { + expect(() => + singleNamespaceSerializer.rawToSavedObject({ _id: 'universe', _source: { - type: 'hello', hello: { world: 'earth', }, - banjo: 'Steve Martin', + } as any, + }) + ).toThrow(/Expected "undefined" to be a saved object type/); + }); + + test('it is complimentary with savedObjectToRaw', () => { + const raw = { + _id: 'foo-namespace:foo:bar', + _primary_term: 24, + _seq_no: 42, + _source: { + type: 'foo', + foo: { + meaning: 42, + nested: { stuff: 'here' }, }, - }); - expect(actual).toEqual({ - id: 'universe', - type: 'hello', - attributes: { - world: 'earth', + migrationVersion: { + foo: '1.2.3', + bar: '9.8.7', }, + namespace: 'foo-namespace', + updated_at: String(new Date()), references: [], - }); - }); + }, + }; + + expect( + singleNamespaceSerializer.savedObjectToRaw( + singleNamespaceSerializer.rawToSavedObject(_.cloneDeep(raw)) + ) + ).toEqual(raw); + }); - test('it does not create attributes if [type] is missing', () => { - const serializer = new SavedObjectsSerializer(typeRegistry); - const actual = serializer.rawToSavedObject({ - _id: 'universe', - _source: { - type: 'hello', - }, - }); - expect(actual).toEqual({ - id: 'universe', + test('it handles unprefixed ids', () => { + const actual = singleNamespaceSerializer.rawToSavedObject({ + _id: 'universe', + _source: { type: 'hello', - references: [], - }); + }, }); - test('it fails for documents which do not specify a type', () => { - const serializer = new SavedObjectsSerializer(typeRegistry); - expect(() => - serializer.rawToSavedObject({ - _id: 'universe', - _source: { - hello: { - world: 'earth', - }, - } as any, - }) - ).toThrow(/Expected "undefined" to be a saved object type/); + expect(actual).toHaveProperty('id', 'universe'); + }); + + describe('namespace-agnostic type with a namespace', () => { + const raw = createSampleDoc({ _source: { namespace: 'baz' } }); + const actual = namespaceAgnosticSerializer.rawToSavedObject(raw); + + test(`removes type prefix from _id`, () => { + expect(actual).toHaveProperty('id', 'bar'); }); - test('it is complimentary with savedObjectToRaw', () => { - const serializer = new SavedObjectsSerializer(typeRegistry); - const raw = { - _id: 'foo-namespace:foo:bar', - _primary_term: 24, - _seq_no: 42, - _source: { - type: 'foo', - foo: { - meaning: 42, - nested: { stuff: 'here' }, - }, - migrationVersion: { - foo: '1.2.3', - bar: '9.8.7', - }, - namespace: 'foo-namespace', - updated_at: String(new Date()), - references: [], - }, - }; + test(`copies _id to id if prefixed by namespace and type`, () => { + const _id = `${raw._source.namespace}:${raw._id}`; + const _actual = namespaceAgnosticSerializer.rawToSavedObject({ ...raw, _id }); + expect(_actual).toHaveProperty('id', _id); + }); - expect(serializer.savedObjectToRaw(serializer.rawToSavedObject(_.cloneDeep(raw)))).toEqual( - raw - ); + test(`doesn't copy _source.namespace to namespace`, () => { + expect(actual).not.toHaveProperty('namespace'); }); + }); - test('it handles unprefixed ids', () => { - const serializer = new SavedObjectsSerializer(typeRegistry); - const actual = serializer.rawToSavedObject({ - _id: 'universe', - _source: { - type: 'hello', - }, - }); + describe('namespace-agnostic type with namespaces', () => { + const raw = createSampleDoc({ _source: { namespaces: ['baz'] } }); + const actual = namespaceAgnosticSerializer.rawToSavedObject(raw); - expect(actual).toHaveProperty('id', 'universe'); + test(`doesn't copy _source.namespaces to namespaces`, () => { + expect(actual).not.toHaveProperty('namespaces'); }); + }); - describe('namespaced type without a namespace', () => { - test(`removes type prefix from _id`, () => { - const serializer = new SavedObjectsSerializer(typeRegistry); - const actual = serializer.rawToSavedObject({ - _id: 'foo:bar', - _source: { - type: 'foo', - }, - }); + describe('single-namespace type without a namespace', () => { + const raw = createSampleDoc({}); + const actual = singleNamespaceSerializer.rawToSavedObject(raw); - expect(actual).toHaveProperty('id', 'bar'); - }); + test(`removes type prefix from _id`, () => { + expect(actual).toHaveProperty('id', 'bar'); + }); - test(`if prefixed by random prefix and type it copies _id to id`, () => { - const serializer = new SavedObjectsSerializer(typeRegistry); - const actual = serializer.rawToSavedObject({ - _id: 'random:foo:bar', - _source: { - type: 'foo', - }, - }); + test(`copies _id to id if prefixed by random prefix and type`, () => { + const _id = `random:${raw._id}`; + const _actual = singleNamespaceSerializer.rawToSavedObject({ ...raw, _id }); + expect(_actual).toHaveProperty('id', _id); + }); - expect(actual).toHaveProperty('id', 'random:foo:bar'); - }); + test(`doesn't specify namespace`, () => { + expect(actual).not.toHaveProperty('namespace'); + }); + }); - test(`doesn't specify namespace`, () => { - const serializer = new SavedObjectsSerializer(typeRegistry); - const actual = serializer.rawToSavedObject({ - _id: 'foo:bar', - _source: { - type: 'foo', - }, - }); + describe('single-namespace type with a namespace', () => { + const namespace = 'baz'; + const raw = createSampleDoc({ _source: { namespace } }); + const actual = singleNamespaceSerializer.rawToSavedObject(raw); - expect(actual).not.toHaveProperty('namespace'); - }); + test(`removes type and namespace prefix from _id`, () => { + const _id = `${namespace}:${raw._id}`; + const _actual = singleNamespaceSerializer.rawToSavedObject({ ...raw, _id }); + expect(_actual).toHaveProperty('id', 'bar'); }); - describe('namespaced type with a namespace', () => { - test(`removes type and namespace prefix from _id`, () => { - const serializer = new SavedObjectsSerializer(typeRegistry); - const actual = serializer.rawToSavedObject({ - _id: 'baz:foo:bar', - _source: { - type: 'foo', - namespace: 'baz', - }, - }); + test(`copies _id to id if prefixed only by type`, () => { + expect(actual).toHaveProperty('id', raw._id); + }); - expect(actual).toHaveProperty('id', 'bar'); - }); + test(`copies _id to id if prefixed by random prefix and type`, () => { + const _id = `random:${raw._id}`; + const _actual = singleNamespaceSerializer.rawToSavedObject({ ...raw, _id }); + expect(_actual).toHaveProperty('id', _id); + }); - test(`if prefixed by only type it copies _id to id`, () => { - const serializer = new SavedObjectsSerializer(typeRegistry); - const actual = serializer.rawToSavedObject({ - _id: 'foo:bar', - _source: { - type: 'foo', - namespace: 'baz', - }, - }); + test(`copies _source.namespace to namespace`, () => { + expect(actual).toHaveProperty('namespace', 'baz'); + }); + }); - expect(actual).toHaveProperty('id', 'foo:bar'); - }); + describe('single-namespace type with namespaces', () => { + const raw = createSampleDoc({ _source: { namespaces: ['baz'] } }); + const actual = singleNamespaceSerializer.rawToSavedObject(raw); - test(`if prefixed by random prefix and type it copies _id to id`, () => { - const serializer = new SavedObjectsSerializer(typeRegistry); - const actual = serializer.rawToSavedObject({ - _id: 'random:foo:bar', - _source: { - type: 'foo', - namespace: 'baz', - }, - }); + test(`doesn't copy _source.namespaces to namespaces`, () => { + expect(actual).not.toHaveProperty('namespaces'); + }); + }); - expect(actual).toHaveProperty('id', 'random:foo:bar'); - }); + describe('multi-namespace type with a namespace', () => { + const raw = createSampleDoc({ _source: { namespace: 'baz' } }); + const actual = multiNamespaceSerializer.rawToSavedObject(raw); - test(`copies _source.namespace to namespace`, () => { - const serializer = new SavedObjectsSerializer(typeRegistry); - const actual = serializer.rawToSavedObject({ - _id: 'baz:foo:bar', - _source: { - type: 'foo', - namespace: 'baz', - }, - }); + test(`removes type prefix from _id`, () => { + expect(actual).toHaveProperty('id', 'bar'); + }); - expect(actual).toHaveProperty('namespace', 'baz'); - }); + test(`copies _id to id if prefixed by namespace and type`, () => { + const _id = `${raw._source.namespace}:${raw._id}`; + const _actual = multiNamespaceSerializer.rawToSavedObject({ ...raw, _id }); + expect(_actual).toHaveProperty('id', _id); }); - describe('namespace agnostic type with a namespace', () => { - beforeEach(() => { - typeRegistry.isNamespaceAgnostic.mockReturnValue(true); - }); + test(`doesn't copy _source.namespace to namespace`, () => { + expect(actual).not.toHaveProperty('namespace'); + }); + }); - test(`removes type prefix from _id`, () => { - const serializer = new SavedObjectsSerializer(typeRegistry); - const actual = serializer.rawToSavedObject({ - _id: 'foo:bar', - _source: { - type: 'foo', - namespace: 'baz', - }, - }); + describe('multi-namespace type with namespaces', () => { + const raw = createSampleDoc({ _source: { namespaces: ['baz'] } }); + const actual = multiNamespaceSerializer.rawToSavedObject(raw); - expect(actual).toHaveProperty('id', 'bar'); - }); + test(`copies _source.namespaces to namespaces`, () => { + expect(actual).toHaveProperty('namespaces', ['baz']); + }); + }); +}); - test(`if prefixed by namespace and type it copies _id to id`, () => { - const serializer = new SavedObjectsSerializer(typeRegistry); - const actual = serializer.rawToSavedObject({ - _id: 'baz:foo:bar', - _source: { - type: 'foo', - namespace: 'baz', - }, - }); +describe('#savedObjectToRaw', () => { + test('it copies the type property to _source.type and uses the ROOT_TYPE as _type', () => { + const actual = singleNamespaceSerializer.savedObjectToRaw({ + type: 'foo', + attributes: {}, + } as any); - expect(actual).toHaveProperty('id', 'baz:foo:bar'); - }); + expect(actual._source).toHaveProperty('type', 'foo'); + }); - test(`doesn't copy _source.namespace to namespace`, () => { - const serializer = new SavedObjectsSerializer(typeRegistry); - const actual = serializer.rawToSavedObject({ - _id: 'baz:foo:bar', - _source: { - type: 'foo', - namespace: 'baz', - }, - }); + test('it copies the references property to _source.references', () => { + const actual = singleNamespaceSerializer.savedObjectToRaw({ + id: '1', + type: 'foo', + attributes: {}, + references: [{ name: 'ref_0', type: 'index-pattern', id: 'pattern*' }], + }); + expect(actual._source).toHaveProperty('references', [ + { + name: 'ref_0', + type: 'index-pattern', + id: 'pattern*', + }, + ]); + }); + + test('if specified it copies the updated_at property to _source.updated_at', () => { + const now = new Date(); + const actual = singleNamespaceSerializer.savedObjectToRaw({ + type: '', + attributes: {}, + updated_at: now, + } as any); + + expect(actual._source).toHaveProperty('updated_at', now); + }); + + test(`if unspecified it doesn't add updated_at property to _source`, () => { + const actual = singleNamespaceSerializer.savedObjectToRaw({ + type: '', + attributes: {}, + } as any); - expect(actual).not.toHaveProperty('namespace'); - }); + expect(actual._source).not.toHaveProperty('updated_at'); + }); + + test('it copies the migrationVersion property to _source.migrationVersion', () => { + const actual = singleNamespaceSerializer.savedObjectToRaw({ + type: '', + attributes: {}, + migrationVersion: { + foo: '1.2.3', + bar: '9.8.7', + }, + } as any); + + expect(actual._source).toHaveProperty('migrationVersion', { + foo: '1.2.3', + bar: '9.8.7', }); }); - describe('#savedObjectToRaw', () => { - test('it copies the type property to _source.type and uses the ROOT_TYPE as _type', () => { - const serializer = new SavedObjectsSerializer(typeRegistry); - const actual = serializer.savedObjectToRaw({ - type: 'foo', + test(`if unspecified it doesn't add migrationVersion property to _source`, () => { + const actual = singleNamespaceSerializer.savedObjectToRaw({ + type: '', + attributes: {}, + } as any); + + expect(actual._source).not.toHaveProperty('migrationVersion'); + }); + + test('it decodes the version property to _seq_no and _primary_term', () => { + const actual = singleNamespaceSerializer.savedObjectToRaw({ + type: '', + attributes: {}, + version: encodeVersion(1, 2), + } as any); + + expect(actual).toHaveProperty('_seq_no', 1); + expect(actual).toHaveProperty('_primary_term', 2); + }); + + test(`if unspecified it doesn't add _seq_no or _primary_term properties`, () => { + const actual = singleNamespaceSerializer.savedObjectToRaw({ + type: '', + attributes: {}, + } as any); + + expect(actual).not.toHaveProperty('_seq_no'); + expect(actual).not.toHaveProperty('_primary_term'); + }); + + test(`if version invalid it throws`, () => { + expect(() => + singleNamespaceSerializer.savedObjectToRaw({ + type: '', attributes: {}, - } as any); + version: 'foo', + } as any) + ).toThrowErrorMatchingInlineSnapshot(`"Invalid version [foo]"`); + }); - expect(actual._source).toHaveProperty('type', 'foo'); + test('it copies attributes to _source[type]', () => { + const actual = singleNamespaceSerializer.savedObjectToRaw({ + type: 'foo', + attributes: { + foo: true, + bar: 'quz', + }, + } as any); + + expect(actual._source).toHaveProperty('foo', { + foo: true, + bar: 'quz', }); + }); - test('it copies the references property to _source.references', () => { - const serializer = new SavedObjectsSerializer(typeRegistry); - const actual = serializer.savedObjectToRaw({ - id: '1', + describe('single-namespace type without a namespace', () => { + test('generates an id prefixed with type, if no id is specified', () => { + const v1 = singleNamespaceSerializer.savedObjectToRaw({ type: 'foo', - attributes: {}, - references: [{ name: 'ref_0', type: 'index-pattern', id: 'pattern*' }], - }); - expect(actual._source).toHaveProperty('references', [ - { - name: 'ref_0', - type: 'index-pattern', - id: 'pattern*', - }, - ]); + attributes: { bar: true }, + } as any); + + const v2 = singleNamespaceSerializer.savedObjectToRaw({ + type: 'foo', + attributes: { bar: true }, + } as any); + + expect(v1._id).toMatch(/^foo\:[\w-]+$/); + expect(v1._id).not.toEqual(v2._id); }); - test('if specified it copies the updated_at property to _source.updated_at', () => { - const serializer = new SavedObjectsSerializer(typeRegistry); - const now = new Date(); - const actual = serializer.savedObjectToRaw({ + test(`doesn't specify _source.namespace`, () => { + const actual = singleNamespaceSerializer.savedObjectToRaw({ type: '', attributes: {}, - updated_at: now, } as any); - expect(actual._source).toHaveProperty('updated_at', now); + expect(actual._source).not.toHaveProperty('namespace'); }); + }); - test(`if unspecified it doesn't add updated_at property to _source`, () => { - const serializer = new SavedObjectsSerializer(typeRegistry); - const actual = serializer.savedObjectToRaw({ - type: '', + describe('single-namespace type with a namespace', () => { + test('generates an id prefixed with namespace and type, if no id is specified', () => { + const v1 = singleNamespaceSerializer.savedObjectToRaw({ + type: 'foo', + namespace: 'bar', + attributes: { bar: true }, + } as any); + + const v2 = singleNamespaceSerializer.savedObjectToRaw({ + type: 'foo', + namespace: 'bar', + attributes: { bar: true }, + } as any); + + expect(v1._id).toMatch(/^bar\:foo\:[\w-]+$/); + expect(v1._id).not.toEqual(v2._id); + }); + + test(`it copies namespace to _source.namespace`, () => { + const actual = singleNamespaceSerializer.savedObjectToRaw({ + type: 'foo', attributes: {}, + namespace: 'bar', } as any); - expect(actual._source).not.toHaveProperty('updated_at'); + expect(actual._source).toHaveProperty('namespace', 'bar'); }); + }); - test('it copies the migrationVersion property to _source.migrationVersion', () => { - const serializer = new SavedObjectsSerializer(typeRegistry); - const actual = serializer.savedObjectToRaw({ - type: '', + describe('single-namespace type with namespaces', () => { + test('generates an id prefixed with type, if no id is specified', () => { + const v1 = namespaceAgnosticSerializer.savedObjectToRaw({ + type: 'foo', + namespaces: ['bar'], + attributes: { bar: true }, + } as any); + + const v2 = namespaceAgnosticSerializer.savedObjectToRaw({ + type: 'foo', + namespaces: ['bar'], + attributes: { bar: true }, + } as any); + + expect(v1._id).toMatch(/^foo\:[\w-]+$/); + expect(v1._id).not.toEqual(v2._id); + }); + + test(`doesn't specify _source.namespaces`, () => { + const actual = namespaceAgnosticSerializer.savedObjectToRaw({ + type: 'foo', + namespaces: ['bar'], attributes: {}, - migrationVersion: { - foo: '1.2.3', - bar: '9.8.7', - }, } as any); - expect(actual._source).toHaveProperty('migrationVersion', { - foo: '1.2.3', - bar: '9.8.7', - }); + expect(actual._source).not.toHaveProperty('namespaces'); }); + }); - test(`if unspecified it doesn't add migrationVersion property to _source`, () => { - const serializer = new SavedObjectsSerializer(typeRegistry); - const actual = serializer.savedObjectToRaw({ - type: '', + describe('namespace-agnostic type with a namespace', () => { + test('generates an id prefixed with type, if no id is specified', () => { + const v1 = namespaceAgnosticSerializer.savedObjectToRaw({ + type: 'foo', + namespace: 'bar', + attributes: { bar: true }, + } as any); + + const v2 = namespaceAgnosticSerializer.savedObjectToRaw({ + type: 'foo', + namespace: 'bar', + attributes: { bar: true }, + } as any); + + expect(v1._id).toMatch(/^foo\:[\w-]+$/); + expect(v1._id).not.toEqual(v2._id); + }); + + test(`doesn't specify _source.namespace`, () => { + const actual = namespaceAgnosticSerializer.savedObjectToRaw({ + type: 'foo', + namespace: 'bar', attributes: {}, } as any); - expect(actual._source).not.toHaveProperty('migrationVersion'); + expect(actual._source).not.toHaveProperty('namespace'); }); + }); - test('it decodes the version property to _seq_no and _primary_term', () => { - const serializer = new SavedObjectsSerializer(typeRegistry); - const actual = serializer.savedObjectToRaw({ - type: '', + describe('namespace-agnostic type with namespaces', () => { + test('generates an id prefixed with type, if no id is specified', () => { + const v1 = namespaceAgnosticSerializer.savedObjectToRaw({ + type: 'foo', + namespaces: ['bar'], + attributes: { bar: true }, + } as any); + + const v2 = namespaceAgnosticSerializer.savedObjectToRaw({ + type: 'foo', + namespaces: ['bar'], + attributes: { bar: true }, + } as any); + + expect(v1._id).toMatch(/^foo\:[\w-]+$/); + expect(v1._id).not.toEqual(v2._id); + }); + + test(`doesn't specify _source.namespaces`, () => { + const actual = namespaceAgnosticSerializer.savedObjectToRaw({ + type: 'foo', + namespaces: ['bar'], attributes: {}, - version: encodeVersion(1, 2), } as any); - expect(actual).toHaveProperty('_seq_no', 1); - expect(actual).toHaveProperty('_primary_term', 2); + expect(actual._source).not.toHaveProperty('namespaces'); }); + }); - test(`if unspecified it doesn't add _seq_no or _primary_term properties`, () => { - const serializer = new SavedObjectsSerializer(typeRegistry); - const actual = serializer.savedObjectToRaw({ - type: '', + describe('multi-namespace type with a namespace', () => { + test('generates an id prefixed with type, if no id is specified', () => { + const v1 = multiNamespaceSerializer.savedObjectToRaw({ + type: 'foo', + namespace: 'bar', + attributes: { bar: true }, + } as any); + + const v2 = multiNamespaceSerializer.savedObjectToRaw({ + type: 'foo', + namespace: 'bar', + attributes: { bar: true }, + } as any); + + expect(v1._id).toMatch(/^foo\:[\w-]+$/); + expect(v1._id).not.toEqual(v2._id); + }); + + test(`doesn't specify _source.namespace`, () => { + const actual = multiNamespaceSerializer.savedObjectToRaw({ + type: 'foo', + namespace: 'bar', attributes: {}, } as any); - expect(actual).not.toHaveProperty('_seq_no'); - expect(actual).not.toHaveProperty('_primary_term'); + expect(actual._source).not.toHaveProperty('namespace'); }); + }); + + describe('multi-namespace type with namespaces', () => { + test('generates an id prefixed with type, if no id is specified', () => { + const v1 = multiNamespaceSerializer.savedObjectToRaw({ + type: 'foo', + namespaces: ['bar'], + attributes: { bar: true }, + } as any); + + const v2 = multiNamespaceSerializer.savedObjectToRaw({ + type: 'foo', + namespaces: ['bar'], + attributes: { bar: true }, + } as any); - test(`if version invalid it throws`, () => { - const serializer = new SavedObjectsSerializer(typeRegistry); - expect(() => - serializer.savedObjectToRaw({ - type: '', - attributes: {}, - version: 'foo', - } as any) - ).toThrowErrorMatchingInlineSnapshot(`"Invalid version [foo]"`); + expect(v1._id).toMatch(/^foo\:[\w-]+$/); + expect(v1._id).not.toEqual(v2._id); }); - test('it copies attributes to _source[type]', () => { - const serializer = new SavedObjectsSerializer(typeRegistry); - const actual = serializer.savedObjectToRaw({ + test(`it copies namespaces to _source.namespaces`, () => { + const actual = multiNamespaceSerializer.savedObjectToRaw({ type: 'foo', - attributes: { - foo: true, - bar: 'quz', - }, + namespaces: ['bar'], + attributes: {}, } as any); - expect(actual._source).toHaveProperty('foo', { - foo: true, - bar: 'quz', - }); + expect(actual._source).toHaveProperty('namespaces', ['bar']); }); + }); +}); - describe('namespaced type without a namespace', () => { - test('generates an id prefixed with type, if no id is specified', () => { - const serializer = new SavedObjectsSerializer(typeRegistry); - const v1 = serializer.savedObjectToRaw({ - type: 'foo', - attributes: { - bar: true, +describe('#isRawSavedObject', () => { + describe('single-namespace type without a namespace', () => { + test('is true if the id is prefixed and the type matches', () => { + expect( + singleNamespaceSerializer.isRawSavedObject({ + _id: 'hello:world', + _source: { + type: 'hello', + hello: {}, }, - } as any); + }) + ).toBeTruthy(); + }); - const v2 = serializer.savedObjectToRaw({ - type: 'foo', - attributes: { - bar: true, + test('is false if the id is not prefixed', () => { + expect( + singleNamespaceSerializer.isRawSavedObject({ + _id: 'world', + _source: { + type: 'hello', + hello: {}, }, - } as any); + }) + ).toBeFalsy(); + }); - expect(v1._id).toMatch(/foo\:[\w-]+$/); - expect(v1._id).not.toEqual(v2._id); - }); + test('is false if the type attribute is missing', () => { + expect( + singleNamespaceSerializer.isRawSavedObject({ + _id: 'hello:world', + _source: { + hello: {}, + } as any, + }) + ).toBeFalsy(); + }); - test(`doesn't specify _source.namespace`, () => { - const serializer = new SavedObjectsSerializer(typeRegistry); - const actual = serializer.savedObjectToRaw({ - type: '', - attributes: {}, - } as any); + test(`is false if the type prefix omits the :`, () => { + expect( + singleNamespaceSerializer.isRawSavedObject({ + _id: 'helloworld', + _source: { + type: 'hello', + hello: {}, + }, + }) + ).toBeFalsy(); + }); - expect(actual._source).not.toHaveProperty('namespace'); - }); + test('is false if the type attribute does not match the id', () => { + expect( + singleNamespaceSerializer.isRawSavedObject({ + _id: 'hello:world', + _source: { + type: 'jam', + jam: {}, + hello: {}, + }, + }) + ).toBeFalsy(); }); - describe('namespaced type with a namespace', () => { - test('generates an id prefixed with namespace and type, if no id is specified', () => { - const serializer = new SavedObjectsSerializer(typeRegistry); - const v1 = serializer.savedObjectToRaw({ - type: 'foo', - namespace: 'bar', - attributes: { - bar: true, + test('is false if there is no [type] attribute', () => { + expect( + singleNamespaceSerializer.isRawSavedObject({ + _id: 'hello:world', + _source: { + type: 'hello', + jam: {}, }, - } as any); + }) + ).toBeFalsy(); + }); + }); - const v2 = serializer.savedObjectToRaw({ - type: 'foo', - namespace: 'bar', - attributes: { - bar: true, + describe('single-namespace type with a namespace', () => { + test('is true if the id is prefixed with type and namespace and the type matches', () => { + expect( + singleNamespaceSerializer.isRawSavedObject({ + _id: 'foo:hello:world', + _source: { + type: 'hello', + hello: {}, + namespace: 'foo', }, - } as any); + }) + ).toBeTruthy(); + }); - expect(v1._id).toMatch(/bar\:foo\:[\w-]+$/); - expect(v1._id).not.toEqual(v2._id); - }); + test('is false if the id is not prefixed by anything', () => { + expect( + singleNamespaceSerializer.isRawSavedObject({ + _id: 'world', + _source: { + type: 'hello', + hello: {}, + namespace: 'foo', + }, + }) + ).toBeFalsy(); + }); - test(`it copies namespace to _source.namespace`, () => { - const serializer = new SavedObjectsSerializer(typeRegistry); - const actual = serializer.savedObjectToRaw({ - type: 'foo', - attributes: {}, - namespace: 'bar', - } as any); + test('is false if the id is prefixed only with type and the type matches', () => { + expect( + singleNamespaceSerializer.isRawSavedObject({ + _id: 'hello:world', + _source: { + type: 'hello', + hello: {}, + namespace: 'foo', + }, + }) + ).toBeFalsy(); + }); + + test('is false if the id is prefixed only with namespace and the namespace matches', () => { + expect( + singleNamespaceSerializer.isRawSavedObject({ + _id: 'foo:world', + _source: { + type: 'hello', + hello: {}, + namespace: 'foo', + }, + }) + ).toBeFalsy(); + }); - expect(actual._source).toHaveProperty('namespace', 'bar'); - }); + test(`is false if the id prefix omits the trailing :`, () => { + expect( + singleNamespaceSerializer.isRawSavedObject({ + _id: 'foo:helloworld', + _source: { + type: 'hello', + hello: {}, + namespace: 'foo', + }, + }) + ).toBeFalsy(); }); - describe('namespace agnostic type with a namespace', () => { - beforeEach(() => { - typeRegistry.isNamespaceAgnostic.mockReturnValue(true); - }); + test('is false if the type attribute is missing', () => { + expect( + singleNamespaceSerializer.isRawSavedObject({ + _id: 'foo:hello:world', + _source: { + hello: {}, + namespace: 'foo', + } as any, + }) + ).toBeFalsy(); + }); - test('generates an id prefixed with type, if no id is specified', () => { - const serializer = new SavedObjectsSerializer(typeRegistry); - const v1 = serializer.savedObjectToRaw({ - type: 'foo', - namespace: 'bar', - attributes: { - bar: true, + test('is false if the type attribute does not match the id', () => { + expect( + singleNamespaceSerializer.isRawSavedObject({ + _id: 'foo:hello:world', + _source: { + type: 'jam', + jam: {}, + hello: {}, + namespace: 'foo', }, - } as any); + }) + ).toBeFalsy(); + }); - const v2 = serializer.savedObjectToRaw({ - type: 'foo', - namespace: 'bar', - attributes: { - bar: true, + test('is false if the namespace attribute does not match the id', () => { + expect( + singleNamespaceSerializer.isRawSavedObject({ + _id: 'bar:jam:world', + _source: { + type: 'jam', + jam: {}, + hello: {}, + namespace: 'foo', }, - } as any); + }) + ).toBeFalsy(); + }); - expect(v1._id).toMatch(/foo\:[\w-]+$/); - expect(v1._id).not.toEqual(v2._id); - }); + test('is false if there is no [type] attribute', () => { + expect( + singleNamespaceSerializer.isRawSavedObject({ + _id: 'hello:world', + _source: { + type: 'hello', + jam: {}, + namespace: 'foo', + }, + }) + ).toBeFalsy(); + }); + }); - test(`doesn't specify _source.namespace`, () => { - const serializer = new SavedObjectsSerializer(typeRegistry); - const actual = serializer.savedObjectToRaw({ - type: 'foo', - namespace: 'bar', - attributes: {}, - } as any); + describe('namespace-agnostic type with a namespace', () => { + test('is true if the id is prefixed with type and the type matches', () => { + expect( + namespaceAgnosticSerializer.isRawSavedObject({ + _id: 'hello:world', + _source: { + type: 'hello', + hello: {}, + namespace: 'foo', + }, + }) + ).toBeTruthy(); + }); + + test('is false if the id is not prefixed', () => { + expect( + namespaceAgnosticSerializer.isRawSavedObject({ + _id: 'world', + _source: { + type: 'hello', + hello: {}, + namespace: 'foo', + }, + }) + ).toBeFalsy(); + }); + + test('is false if the id is prefixed with type and namespace', () => { + expect( + namespaceAgnosticSerializer.isRawSavedObject({ + _id: 'foo:hello:world', + _source: { + type: 'hello', + hello: {}, + namespace: 'foo', + }, + }) + ).toBeFalsy(); + }); + + test(`is false if the type prefix omits the :`, () => { + expect( + namespaceAgnosticSerializer.isRawSavedObject({ + _id: 'helloworld', + _source: { + type: 'hello', + hello: {}, + namespace: 'foo', + }, + }) + ).toBeFalsy(); + }); + + test('is false if the type attribute is missing', () => { + expect( + namespaceAgnosticSerializer.isRawSavedObject({ + _id: 'hello:world', + _source: { + hello: {}, + namespace: 'foo', + } as any, + }) + ).toBeFalsy(); + }); + + test('is false if the type attribute does not match the id', () => { + expect( + namespaceAgnosticSerializer.isRawSavedObject({ + _id: 'hello:world', + _source: { + type: 'jam', + jam: {}, + hello: {}, + namespace: 'foo', + }, + }) + ).toBeFalsy(); + }); + + test('is false if there is no [type] attribute', () => { + expect( + namespaceAgnosticSerializer.isRawSavedObject({ + _id: 'hello:world', + _source: { + type: 'hello', + jam: {}, + namespace: 'foo', + }, + }) + ).toBeFalsy(); + }); + }); +}); - expect(actual._source).not.toHaveProperty('namespace'); - }); +describe('#generateRawId', () => { + describe('single-namespace type without a namespace', () => { + test('generates an id if none is specified', () => { + const id = singleNamespaceSerializer.generateRawId('', 'goodbye'); + expect(id).toMatch(/^goodbye\:[\w-]+$/); + }); + + test('uses the id that is specified', () => { + const id = singleNamespaceSerializer.generateRawId('', 'hello', 'world'); + expect(id).toEqual('hello:world'); }); }); - describe('#isRawSavedObject', () => { - describe('namespaced type without a namespace', () => { - test('is true if the id is prefixed and the type matches', () => { - const serializer = new SavedObjectsSerializer(typeRegistry); - expect( - serializer.isRawSavedObject({ - _id: 'hello:world', - _source: { - type: 'hello', - hello: {}, - }, - }) - ).toBeTruthy(); - }); - - test('is false if the id is not prefixed', () => { - const serializer = new SavedObjectsSerializer(typeRegistry); - expect( - serializer.isRawSavedObject({ - _id: 'world', - _source: { - type: 'hello', - hello: {}, - }, - }) - ).toBeFalsy(); - }); - - test('is false if the type attribute is missing', () => { - const serializer = new SavedObjectsSerializer(typeRegistry); - expect( - serializer.isRawSavedObject({ - _id: 'hello:world', - _source: { - hello: {}, - } as any, - }) - ).toBeFalsy(); - }); - - test(`is false if the type prefix omits the :`, () => { - const serializer = new SavedObjectsSerializer(typeRegistry); - expect( - serializer.isRawSavedObject({ - _id: 'helloworld', - _source: { - type: 'hello', - hello: {}, - }, - }) - ).toBeFalsy(); - }); - - test('is false if the type attribute does not match the id', () => { - const serializer = new SavedObjectsSerializer(typeRegistry); - expect( - serializer.isRawSavedObject({ - _id: 'hello:world', - _source: { - type: 'jam', - jam: {}, - hello: {}, - }, - }) - ).toBeFalsy(); - }); - - test('is false if there is no [type] attribute', () => { - const serializer = new SavedObjectsSerializer(typeRegistry); - expect( - serializer.isRawSavedObject({ - _id: 'hello:world', - _source: { - type: 'hello', - jam: {}, - }, - }) - ).toBeFalsy(); - }); - }); - - describe('namespaced type with a namespace', () => { - test('is true if the id is prefixed with type and namespace and the type matches', () => { - const serializer = new SavedObjectsSerializer(typeRegistry); - expect( - serializer.isRawSavedObject({ - _id: 'foo:hello:world', - _source: { - type: 'hello', - hello: {}, - namespace: 'foo', - }, - }) - ).toBeTruthy(); - }); - - test('is false if the id is not prefixed by anything', () => { - const serializer = new SavedObjectsSerializer(typeRegistry); - expect( - serializer.isRawSavedObject({ - _id: 'world', - _source: { - type: 'hello', - hello: {}, - namespace: 'foo', - }, - }) - ).toBeFalsy(); - }); - - test('is false if the id is prefixed only with type and the type matches', () => { - const serializer = new SavedObjectsSerializer(typeRegistry); - expect( - serializer.isRawSavedObject({ - _id: 'hello:world', - _source: { - type: 'hello', - hello: {}, - namespace: 'foo', - }, - }) - ).toBeFalsy(); - }); - - test('is false if the id is prefixed only with namespace and the namespace matches', () => { - const serializer = new SavedObjectsSerializer(typeRegistry); - expect( - serializer.isRawSavedObject({ - _id: 'foo:world', - _source: { - type: 'hello', - hello: {}, - namespace: 'foo', - }, - }) - ).toBeFalsy(); - }); - - test(`is false if the id prefix omits the trailing :`, () => { - const serializer = new SavedObjectsSerializer(typeRegistry); - expect( - serializer.isRawSavedObject({ - _id: 'foo:helloworld', - _source: { - type: 'hello', - hello: {}, - namespace: 'foo', - }, - }) - ).toBeFalsy(); - }); - - test('is false if the type attribute is missing', () => { - const serializer = new SavedObjectsSerializer(typeRegistry); - expect( - serializer.isRawSavedObject({ - _id: 'foo:hello:world', - _source: { - hello: {}, - namespace: 'foo', - } as any, - }) - ).toBeFalsy(); - }); - - test('is false if the type attribute does not match the id', () => { - const serializer = new SavedObjectsSerializer(typeRegistry); - expect( - serializer.isRawSavedObject({ - _id: 'foo:hello:world', - _source: { - type: 'jam', - jam: {}, - hello: {}, - namespace: 'foo', - }, - }) - ).toBeFalsy(); - }); - - test('is false if the namespace attribute does not match the id', () => { - const serializer = new SavedObjectsSerializer(typeRegistry); - expect( - serializer.isRawSavedObject({ - _id: 'bar:jam:world', - _source: { - type: 'jam', - jam: {}, - hello: {}, - namespace: 'foo', - }, - }) - ).toBeFalsy(); - }); - - test('is false if there is no [type] attribute', () => { - const serializer = new SavedObjectsSerializer(typeRegistry); - expect( - serializer.isRawSavedObject({ - _id: 'hello:world', - _source: { - type: 'hello', - jam: {}, - namespace: 'foo', - }, - }) - ).toBeFalsy(); - }); - }); - - describe('namespace agnostic type with a namespace', () => { - beforeEach(() => { - typeRegistry.isNamespaceAgnostic.mockReturnValue(true); - }); - - test('is true if the id is prefixed with type and the type matches', () => { - const serializer = new SavedObjectsSerializer(typeRegistry); - expect( - serializer.isRawSavedObject({ - _id: 'hello:world', - _source: { - type: 'hello', - hello: {}, - namespace: 'foo', - }, - }) - ).toBeTruthy(); - }); - - test('is false if the id is not prefixed', () => { - const serializer = new SavedObjectsSerializer(typeRegistry); - expect( - serializer.isRawSavedObject({ - _id: 'world', - _source: { - type: 'hello', - hello: {}, - namespace: 'foo', - }, - }) - ).toBeFalsy(); - }); - - test('is false if the id is prefixed with type and namespace', () => { - const serializer = new SavedObjectsSerializer(typeRegistry); - expect( - serializer.isRawSavedObject({ - _id: 'foo:hello:world', - _source: { - type: 'hello', - hello: {}, - namespace: 'foo', - }, - }) - ).toBeFalsy(); - }); - - test(`is false if the type prefix omits the :`, () => { - const serializer = new SavedObjectsSerializer(typeRegistry); - expect( - serializer.isRawSavedObject({ - _id: 'helloworld', - _source: { - type: 'hello', - hello: {}, - namespace: 'foo', - }, - }) - ).toBeFalsy(); - }); - - test('is false if the type attribute is missing', () => { - const serializer = new SavedObjectsSerializer(typeRegistry); - expect( - serializer.isRawSavedObject({ - _id: 'hello:world', - _source: { - hello: {}, - namespace: 'foo', - } as any, - }) - ).toBeFalsy(); - }); - - test('is false if the type attribute does not match the id', () => { - const serializer = new SavedObjectsSerializer(typeRegistry); - expect( - serializer.isRawSavedObject({ - _id: 'hello:world', - _source: { - type: 'jam', - jam: {}, - hello: {}, - namespace: 'foo', - }, - }) - ).toBeFalsy(); - }); - - test('is false if there is no [type] attribute', () => { - const serializer = new SavedObjectsSerializer(typeRegistry); - expect( - serializer.isRawSavedObject({ - _id: 'hello:world', - _source: { - type: 'hello', - jam: {}, - namespace: 'foo', - }, - }) - ).toBeFalsy(); - }); + describe('single-namespace type with a namespace', () => { + test('generates an id if none is specified and prefixes namespace', () => { + const id = singleNamespaceSerializer.generateRawId('foo', 'goodbye'); + expect(id).toMatch(/^foo:goodbye\:[\w-]+$/); + }); + + test('uses the id that is specified and prefixes the namespace', () => { + const id = singleNamespaceSerializer.generateRawId('foo', 'hello', 'world'); + expect(id).toEqual('foo:hello:world'); }); }); - describe('#generateRawId', () => { - describe('namespaced type without a namespace', () => { - test('generates an id if none is specified', () => { - const serializer = new SavedObjectsSerializer(typeRegistry); - const id = serializer.generateRawId('', 'goodbye'); - expect(id).toMatch(/goodbye\:[\w-]+$/); - }); - - test('uses the id that is specified', () => { - const serializer = new SavedObjectsSerializer(typeRegistry); - const id = serializer.generateRawId('', 'hello', 'world'); - expect(id).toMatch('hello:world'); - }); - }); - - describe('namespaced type with a namespace', () => { - test('generates an id if none is specified and prefixes namespace', () => { - const serializer = new SavedObjectsSerializer(typeRegistry); - const id = serializer.generateRawId('foo', 'goodbye'); - expect(id).toMatch(/foo:goodbye\:[\w-]+$/); - }); - - test('uses the id that is specified and prefixes the namespace', () => { - const serializer = new SavedObjectsSerializer(typeRegistry); - const id = serializer.generateRawId('foo', 'hello', 'world'); - expect(id).toMatch('foo:hello:world'); - }); - }); - - describe('namespace agnostic type with a namespace', () => { - test(`generates an id if none is specified and doesn't prefix namespace`, () => { - typeRegistry.isNamespaceAgnostic.mockReturnValue(true); - - const serializer = new SavedObjectsSerializer(typeRegistry); - const id = serializer.generateRawId('foo', 'goodbye'); - expect(id).toMatch(/goodbye\:[\w-]+$/); - }); - - test(`uses the id that is specified and doesn't prefix the namespace`, () => { - const serializer = new SavedObjectsSerializer(typeRegistry); - const id = serializer.generateRawId('foo', 'hello', 'world'); - expect(id).toMatch('hello:world'); - }); + describe('namespace-agnostic type with a namespace', () => { + test(`generates an id if none is specified and doesn't prefix namespace`, () => { + const id = namespaceAgnosticSerializer.generateRawId('foo', 'goodbye'); + expect(id).toMatch(/^goodbye\:[\w-]+$/); + }); + + test(`uses the id that is specified and doesn't prefix the namespace`, () => { + const id = namespaceAgnosticSerializer.generateRawId('foo', 'hello', 'world'); + expect(id).toEqual('hello:world'); }); }); }); diff --git a/src/core/server/saved_objects/serialization/serializer.ts b/src/core/server/saved_objects/serialization/serializer.ts index 99d6b0c6b59f9..3b19d494d8ecf 100644 --- a/src/core/server/saved_objects/serialization/serializer.ts +++ b/src/core/server/saved_objects/serialization/serializer.ts @@ -49,7 +49,7 @@ export class SavedObjectsSerializer { public isRawSavedObject(rawDoc: SavedObjectsRawDoc) { const { type, namespace } = rawDoc._source; const namespacePrefix = - namespace && !this.registry.isNamespaceAgnostic(type) ? `${namespace}:` : ''; + namespace && this.registry.isSingleNamespace(type) ? `${namespace}:` : ''; return Boolean( type && rawDoc._id.startsWith(`${namespacePrefix}${type}:`) && @@ -64,7 +64,7 @@ export class SavedObjectsSerializer { */ public rawToSavedObject(doc: SavedObjectsRawDoc): SavedObjectSanitizedDoc { const { _id, _source, _seq_no, _primary_term } = doc; - const { type, namespace } = _source; + const { type, namespace, namespaces } = _source; const version = _seq_no != null || _primary_term != null @@ -74,7 +74,8 @@ export class SavedObjectsSerializer { return { type, id: this.trimIdPrefix(namespace, type, _id), - ...(namespace && !this.registry.isNamespaceAgnostic(type) && { namespace }), + ...(namespace && this.registry.isSingleNamespace(type) && { namespace }), + ...(namespaces && this.registry.isMultiNamespace(type) && { namespaces }), attributes: _source[type], references: _source.references || [], ...(_source.migrationVersion && { migrationVersion: _source.migrationVersion }), @@ -93,6 +94,7 @@ export class SavedObjectsSerializer { id, type, namespace, + namespaces, attributes, migrationVersion, updated_at, @@ -103,7 +105,8 @@ export class SavedObjectsSerializer { [type]: attributes, type, references, - ...(namespace && !this.registry.isNamespaceAgnostic(type) && { namespace }), + ...(namespace && this.registry.isSingleNamespace(type) && { namespace }), + ...(namespaces && this.registry.isMultiNamespace(type) && { namespaces }), ...(migrationVersion && { migrationVersion }), ...(updated_at && { updated_at }), }; @@ -124,7 +127,7 @@ export class SavedObjectsSerializer { */ public generateRawId(namespace: string | undefined, type: string, id?: string) { const namespacePrefix = - namespace && !this.registry.isNamespaceAgnostic(type) ? `${namespace}:` : ''; + namespace && this.registry.isSingleNamespace(type) ? `${namespace}:` : ''; return `${namespacePrefix}${type}:${id || uuid.v1()}`; } @@ -133,7 +136,7 @@ export class SavedObjectsSerializer { assertNonEmptyString(type, 'saved object type'); const namespacePrefix = - namespace && !this.registry.isNamespaceAgnostic(type) ? `${namespace}:` : ''; + namespace && this.registry.isSingleNamespace(type) ? `${namespace}:` : ''; const prefix = `${namespacePrefix}${type}:`; if (!id.startsWith(prefix)) { diff --git a/src/core/server/saved_objects/serialization/types.ts b/src/core/server/saved_objects/serialization/types.ts index dfaec127ba159..7ea61f67e9496 100644 --- a/src/core/server/saved_objects/serialization/types.ts +++ b/src/core/server/saved_objects/serialization/types.ts @@ -36,6 +36,7 @@ export interface SavedObjectsRawDoc { export interface SavedObjectsRawDocSource { type: string; namespace?: string; + namespaces?: string[]; migrationVersion?: SavedObjectsMigrationVersion; updated_at?: string; references?: SavedObjectReference[]; @@ -54,6 +55,7 @@ interface SavedObjectDoc { id?: string; // NOTE: SavedObjectDoc is used for uncreated objects where `id` is optional type: string; namespace?: string; + namespaces?: string[]; migrationVersion?: SavedObjectsMigrationVersion; version?: string; updated_at?: string; diff --git a/src/core/server/saved_objects/service/index.ts b/src/core/server/saved_objects/service/index.ts index f44824238aa21..9f625b4732e26 100644 --- a/src/core/server/saved_objects/service/index.ts +++ b/src/core/server/saved_objects/service/index.ts @@ -36,7 +36,6 @@ export interface SavedObjectsLegacyService { getScopedSavedObjectsClient: SavedObjectsClientProvider['getClient']; SavedObjectsClient: typeof SavedObjectsClient; types: string[]; - importAndExportableTypes: string[]; schema: SavedObjectsSchema; getSavedObjectsRepository(...rest: any[]): any; importExport: { diff --git a/src/core/server/saved_objects/service/lib/__snapshots__/repository.test.js.snap b/src/core/server/saved_objects/service/lib/__snapshots__/repository.test.js.snap deleted file mode 100644 index 609906c97d599..0000000000000 --- a/src/core/server/saved_objects/service/lib/__snapshots__/repository.test.js.snap +++ /dev/null @@ -1,5 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`SavedObjectsRepository #deleteByNamespace requires namespace to be a string 1`] = `"namespace is required, and must be a string"`; - -exports[`SavedObjectsRepository #deleteByNamespace requires namespace to be defined 1`] = `"namespace is required, and must be a string"`; diff --git a/src/core/server/saved_objects/service/lib/decorate_es_error.test.ts b/src/core/server/saved_objects/service/lib/decorate_es_error.test.ts index 2fd9b487f470a..1fdebd87397eb 100644 --- a/src/core/server/saved_objects/service/lib/decorate_es_error.test.ts +++ b/src/core/server/saved_objects/service/lib/decorate_es_error.test.ts @@ -100,6 +100,38 @@ describe('savedObjectsClient/decorateEsError', () => { expect(SavedObjectsErrorHelpers.isBadRequestError(error)).toBe(true); }); + describe('when es.BadRequest has a reason', () => { + it('makes a SavedObjectsClient/esCannotExecuteScriptError error when script context is disabled', () => { + const error = new esErrors.BadRequest(); + (error as Record).body = { + error: { reason: 'cannot execute scripts using [update] context' }, + }; + expect(SavedObjectsErrorHelpers.isEsCannotExecuteScriptError(error)).toBe(false); + expect(decorateEsError(error)).toBe(error); + expect(SavedObjectsErrorHelpers.isEsCannotExecuteScriptError(error)).toBe(true); + expect(SavedObjectsErrorHelpers.isBadRequestError(error)).toBe(false); + }); + + it('makes a SavedObjectsClient/esCannotExecuteScriptError error when inline scripts are disabled', () => { + const error = new esErrors.BadRequest(); + (error as Record).body = { + error: { reason: 'cannot execute [inline] scripts' }, + }; + expect(SavedObjectsErrorHelpers.isEsCannotExecuteScriptError(error)).toBe(false); + expect(decorateEsError(error)).toBe(error); + expect(SavedObjectsErrorHelpers.isEsCannotExecuteScriptError(error)).toBe(true); + expect(SavedObjectsErrorHelpers.isBadRequestError(error)).toBe(false); + }); + + it('makes a SavedObjectsClient/BadRequest error for any other reason', () => { + const error = new esErrors.BadRequest(); + (error as Record).body = { error: { reason: 'some other reason' } }; + expect(SavedObjectsErrorHelpers.isBadRequestError(error)).toBe(false); + expect(decorateEsError(error)).toBe(error); + expect(SavedObjectsErrorHelpers.isBadRequestError(error)).toBe(true); + }); + }); + it('returns other errors as Boom errors', () => { const error = new Error(); expect(error).not.toHaveProperty('isBoom'); diff --git a/src/core/server/saved_objects/service/lib/decorate_es_error.ts b/src/core/server/saved_objects/service/lib/decorate_es_error.ts index eb9bc89636435..e57f08aa7a527 100644 --- a/src/core/server/saved_objects/service/lib/decorate_es_error.ts +++ b/src/core/server/saved_objects/service/lib/decorate_es_error.ts @@ -35,6 +35,8 @@ const { NotFound, BadRequest, } = legacyElasticsearch.errors; +const SCRIPT_CONTEXT_DISABLED_REGEX = /(?:cannot execute scripts using \[)([a-z]*)(?:\] context)/; +const INLINE_SCRIPTS_DISABLED_MESSAGE = 'cannot execute [inline] scripts'; import { SavedObjectsErrorHelpers } from './errors'; @@ -43,7 +45,7 @@ export function decorateEsError(error: Error) { throw new Error('Expected an instance of Error'); } - const { reason } = get(error, 'body.error', { reason: undefined }); + const { reason } = get(error, 'body.error', { reason: undefined }) as { reason?: string }; if ( error instanceof ConnectionFault || error instanceof ServiceUnavailable || @@ -74,6 +76,12 @@ export function decorateEsError(error: Error) { } if (error instanceof BadRequest) { + if ( + SCRIPT_CONTEXT_DISABLED_REGEX.test(reason || '') || + reason === INLINE_SCRIPTS_DISABLED_MESSAGE + ) { + return SavedObjectsErrorHelpers.decorateEsCannotExecuteScriptError(error, reason); + } return SavedObjectsErrorHelpers.decorateBadRequestError(error, reason); } diff --git a/src/core/server/saved_objects/service/lib/errors.test.ts b/src/core/server/saved_objects/service/lib/errors.test.ts index 12fc913f93090..4a43835d795d1 100644 --- a/src/core/server/saved_objects/service/lib/errors.test.ts +++ b/src/core/server/saved_objects/service/lib/errors.test.ts @@ -34,6 +34,7 @@ describe('savedObjectsClient/errorTypes', () => { }); it('has boom properties', () => { + expect(errorObj).toHaveProperty('isBoom', true); expect(errorObj.output.payload).toMatchObject({ statusCode: 400, message: "Unsupported saved object type: 'someType': Bad Request", @@ -57,6 +58,7 @@ describe('savedObjectsClient/errorTypes', () => { }); it('has boom properties', () => { + expect(errorObj).toHaveProperty('isBoom', true); expect(errorObj.output.payload).toMatchObject({ statusCode: 400, message: 'test reason message: Bad Request', @@ -80,14 +82,7 @@ describe('savedObjectsClient/errorTypes', () => { it('adds boom properties', () => { const error = SavedObjectsErrorHelpers.decorateBadRequestError(new Error()); - expect(typeof error.output).toBe('object'); - expect(error.output.statusCode).toBe(400); - }); - - it('preserves boom properties of input', () => { - const error = Boom.notFound(); - SavedObjectsErrorHelpers.decorateBadRequestError(error); - expect(error.output.statusCode).toBe(404); + expect(error).toHaveProperty('isBoom', true); }); describe('error.output', () => { @@ -95,6 +90,7 @@ describe('savedObjectsClient/errorTypes', () => { const error = SavedObjectsErrorHelpers.decorateBadRequestError(new Error('foobar')); expect(error.output.payload).toHaveProperty('message', 'foobar'); }); + it('prefixes message with passed reason', () => { const error = SavedObjectsErrorHelpers.decorateBadRequestError( new Error('foobar'), @@ -102,13 +98,21 @@ describe('savedObjectsClient/errorTypes', () => { ); expect(error.output.payload).toHaveProperty('message', 'biz: foobar'); }); + it('sets statusCode to 400', () => { const error = SavedObjectsErrorHelpers.decorateBadRequestError(new Error('foo')); expect(error.output).toHaveProperty('statusCode', 400); }); + + it('preserves boom properties of input', () => { + const error = Boom.notFound(); + SavedObjectsErrorHelpers.decorateBadRequestError(error); + expect(error.output).toHaveProperty('statusCode', 404); + }); }); }); }); + describe('NotAuthorized error', () => { describe('decorateNotAuthorizedError', () => { it('returns original object', () => { @@ -125,14 +129,7 @@ describe('savedObjectsClient/errorTypes', () => { it('adds boom properties', () => { const error = SavedObjectsErrorHelpers.decorateNotAuthorizedError(new Error()); - expect(typeof error.output).toBe('object'); - expect(error.output.statusCode).toBe(401); - }); - - it('preserves boom properties of input', () => { - const error = Boom.notFound(); - SavedObjectsErrorHelpers.decorateNotAuthorizedError(error); - expect(error.output.statusCode).toBe(404); + expect(error).toHaveProperty('isBoom', true); }); describe('error.output', () => { @@ -140,6 +137,7 @@ describe('savedObjectsClient/errorTypes', () => { const error = SavedObjectsErrorHelpers.decorateNotAuthorizedError(new Error('foobar')); expect(error.output.payload).toHaveProperty('message', 'foobar'); }); + it('prefixes message with passed reason', () => { const error = SavedObjectsErrorHelpers.decorateNotAuthorizedError( new Error('foobar'), @@ -147,13 +145,21 @@ describe('savedObjectsClient/errorTypes', () => { ); expect(error.output.payload).toHaveProperty('message', 'biz: foobar'); }); + it('sets statusCode to 401', () => { const error = SavedObjectsErrorHelpers.decorateNotAuthorizedError(new Error('foo')); expect(error.output).toHaveProperty('statusCode', 401); }); + + it('preserves boom properties of input', () => { + const error = Boom.notFound(); + SavedObjectsErrorHelpers.decorateNotAuthorizedError(error); + expect(error.output).toHaveProperty('statusCode', 404); + }); }); }); }); + describe('Forbidden error', () => { describe('decorateForbiddenError', () => { it('returns original object', () => { @@ -170,14 +176,7 @@ describe('savedObjectsClient/errorTypes', () => { it('adds boom properties', () => { const error = SavedObjectsErrorHelpers.decorateForbiddenError(new Error()); - expect(typeof error.output).toBe('object'); - expect(error.output.statusCode).toBe(403); - }); - - it('preserves boom properties of input', () => { - const error = Boom.notFound(); - SavedObjectsErrorHelpers.decorateForbiddenError(error); - expect(error.output.statusCode).toBe(404); + expect(error).toHaveProperty('isBoom', true); }); describe('error.output', () => { @@ -185,17 +184,26 @@ describe('savedObjectsClient/errorTypes', () => { const error = SavedObjectsErrorHelpers.decorateForbiddenError(new Error('foobar')); expect(error.output.payload).toHaveProperty('message', 'foobar'); }); + it('prefixes message with passed reason', () => { const error = SavedObjectsErrorHelpers.decorateForbiddenError(new Error('foobar'), 'biz'); expect(error.output.payload).toHaveProperty('message', 'biz: foobar'); }); + it('sets statusCode to 403', () => { const error = SavedObjectsErrorHelpers.decorateForbiddenError(new Error('foo')); expect(error.output).toHaveProperty('statusCode', 403); }); + + it('preserves boom properties of input', () => { + const error = Boom.notFound(); + SavedObjectsErrorHelpers.decorateForbiddenError(error); + expect(error.output).toHaveProperty('statusCode', 404); + }); }); }); }); + describe('NotFound error', () => { describe('createGenericNotFoundError', () => { it('makes an error identifiable as a NotFound error', () => { @@ -203,11 +211,9 @@ describe('savedObjectsClient/errorTypes', () => { expect(SavedObjectsErrorHelpers.isNotFoundError(error)).toBe(true); }); - it('is a boom error, has boom properties', () => { + it('returns a boom error', () => { const error = SavedObjectsErrorHelpers.createGenericNotFoundError(); - expect(error).toHaveProperty('isBoom'); - expect(typeof error.output).toBe('object'); - expect(error.output.statusCode).toBe(404); + expect(error).toHaveProperty('isBoom', true); }); describe('error.output', () => { @@ -215,6 +221,7 @@ describe('savedObjectsClient/errorTypes', () => { const error = SavedObjectsErrorHelpers.createGenericNotFoundError(); expect(error.output.payload).toHaveProperty('message', 'Not Found'); }); + it('sets statusCode to 404', () => { const error = SavedObjectsErrorHelpers.createGenericNotFoundError(); expect(error.output).toHaveProperty('statusCode', 404); @@ -222,6 +229,7 @@ describe('savedObjectsClient/errorTypes', () => { }); }); }); + describe('Conflict error', () => { describe('decorateConflictError', () => { it('returns original object', () => { @@ -238,14 +246,7 @@ describe('savedObjectsClient/errorTypes', () => { it('adds boom properties', () => { const error = SavedObjectsErrorHelpers.decorateConflictError(new Error()); - expect(typeof error.output).toBe('object'); - expect(error.output.statusCode).toBe(409); - }); - - it('preserves boom properties of input', () => { - const error = Boom.notFound(); - SavedObjectsErrorHelpers.decorateConflictError(error); - expect(error.output.statusCode).toBe(404); + expect(error).toHaveProperty('isBoom', true); }); describe('error.output', () => { @@ -253,17 +254,77 @@ describe('savedObjectsClient/errorTypes', () => { const error = SavedObjectsErrorHelpers.decorateConflictError(new Error('foobar')); expect(error.output.payload).toHaveProperty('message', 'foobar'); }); + it('prefixes message with passed reason', () => { const error = SavedObjectsErrorHelpers.decorateConflictError(new Error('foobar'), 'biz'); expect(error.output.payload).toHaveProperty('message', 'biz: foobar'); }); + it('sets statusCode to 409', () => { const error = SavedObjectsErrorHelpers.decorateConflictError(new Error('foo')); expect(error.output).toHaveProperty('statusCode', 409); }); + + it('preserves boom properties of input', () => { + const error = Boom.notFound(); + SavedObjectsErrorHelpers.decorateConflictError(error); + expect(error.output).toHaveProperty('statusCode', 404); + }); + }); + }); + }); + + describe('EsCannotExecuteScript error', () => { + describe('decorateEsCannotExecuteScriptError', () => { + it('returns original object', () => { + const error = new Error(); + expect(SavedObjectsErrorHelpers.decorateEsCannotExecuteScriptError(error)).toBe(error); + }); + + it('makes the error identifiable as a EsCannotExecuteScript error', () => { + const error = new Error(); + expect(SavedObjectsErrorHelpers.isEsCannotExecuteScriptError(error)).toBe(false); + SavedObjectsErrorHelpers.decorateEsCannotExecuteScriptError(error); + expect(SavedObjectsErrorHelpers.isEsCannotExecuteScriptError(error)).toBe(true); + }); + + it('adds boom properties', () => { + const error = SavedObjectsErrorHelpers.decorateEsCannotExecuteScriptError(new Error()); + expect(error).toHaveProperty('isBoom', true); + }); + + describe('error.output', () => { + it('defaults to message of error', () => { + const error = SavedObjectsErrorHelpers.decorateEsCannotExecuteScriptError( + new Error('foobar') + ); + expect(error.output.payload).toHaveProperty('message', 'foobar'); + }); + + it('prefixes message with passed reason', () => { + const error = SavedObjectsErrorHelpers.decorateEsCannotExecuteScriptError( + new Error('foobar'), + 'biz' + ); + expect(error.output.payload).toHaveProperty('message', 'biz: foobar'); + }); + + it('sets statusCode to 501', () => { + const error = SavedObjectsErrorHelpers.decorateEsCannotExecuteScriptError( + new Error('foo') + ); + expect(error.output).toHaveProperty('statusCode', 400); + }); + + it('preserves boom properties of input', () => { + const error = Boom.notFound(); + SavedObjectsErrorHelpers.decorateEsCannotExecuteScriptError(error); + expect(error.output).toHaveProperty('statusCode', 404); + }); }); }); }); + describe('EsUnavailable error', () => { describe('decorateEsUnavailableError', () => { it('returns original object', () => { @@ -280,14 +341,7 @@ describe('savedObjectsClient/errorTypes', () => { it('adds boom properties', () => { const error = SavedObjectsErrorHelpers.decorateEsUnavailableError(new Error()); - expect(typeof error.output).toBe('object'); - expect(error.output.statusCode).toBe(503); - }); - - it('preserves boom properties of input', () => { - const error = Boom.notFound(); - SavedObjectsErrorHelpers.decorateEsUnavailableError(error); - expect(error.output.statusCode).toBe(404); + expect(error).toHaveProperty('isBoom', true); }); describe('error.output', () => { @@ -295,6 +349,7 @@ describe('savedObjectsClient/errorTypes', () => { const error = SavedObjectsErrorHelpers.decorateEsUnavailableError(new Error('foobar')); expect(error.output.payload).toHaveProperty('message', 'foobar'); }); + it('prefixes message with passed reason', () => { const error = SavedObjectsErrorHelpers.decorateEsUnavailableError( new Error('foobar'), @@ -302,13 +357,21 @@ describe('savedObjectsClient/errorTypes', () => { ); expect(error.output.payload).toHaveProperty('message', 'biz: foobar'); }); + it('sets statusCode to 503', () => { const error = SavedObjectsErrorHelpers.decorateEsUnavailableError(new Error('foo')); expect(error.output).toHaveProperty('statusCode', 503); }); + + it('preserves boom properties of input', () => { + const error = Boom.notFound(); + SavedObjectsErrorHelpers.decorateEsUnavailableError(error); + expect(error.output).toHaveProperty('statusCode', 404); + }); }); }); }); + describe('General error', () => { describe('decorateGeneralError', () => { it('returns original object', () => { @@ -318,14 +381,7 @@ describe('savedObjectsClient/errorTypes', () => { it('adds boom properties', () => { const error = SavedObjectsErrorHelpers.decorateGeneralError(new Error()); - expect(typeof error.output).toBe('object'); - expect(error.output.statusCode).toBe(500); - }); - - it('preserves boom properties of input', () => { - const error = Boom.notFound(); - SavedObjectsErrorHelpers.decorateGeneralError(error); - expect(error.output.statusCode).toBe(404); + expect(error).toHaveProperty('isBoom', true); }); describe('error.output', () => { @@ -333,10 +389,17 @@ describe('savedObjectsClient/errorTypes', () => { const error = SavedObjectsErrorHelpers.decorateGeneralError(new Error('foobar')); expect(error.output.payload.message).toMatch(/internal server error/i); }); + it('sets statusCode to 500', () => { const error = SavedObjectsErrorHelpers.decorateGeneralError(new Error('foo')); expect(error.output).toHaveProperty('statusCode', 500); }); + + it('preserves boom properties of input', () => { + const error = Boom.notFound(); + SavedObjectsErrorHelpers.decorateGeneralError(error); + expect(error.output).toHaveProperty('statusCode', 404); + }); }); }); }); @@ -363,9 +426,7 @@ describe('savedObjectsClient/errorTypes', () => { it('returns a boom error', () => { const error = SavedObjectsErrorHelpers.createEsAutoCreateIndexError(); - expect(error).toHaveProperty('isBoom'); - expect(typeof error.output).toBe('object'); - expect(error.output.statusCode).toBe(503); + expect(error).toHaveProperty('isBoom', true); }); describe('error.output', () => { @@ -373,6 +434,7 @@ describe('savedObjectsClient/errorTypes', () => { const error = SavedObjectsErrorHelpers.createEsAutoCreateIndexError(); expect(error.output.payload).toHaveProperty('message', 'Automatic index creation failed'); }); + it('sets statusCode to 503', () => { const error = SavedObjectsErrorHelpers.createEsAutoCreateIndexError(); expect(error.output).toHaveProperty('statusCode', 503); diff --git a/src/core/server/saved_objects/service/lib/errors.ts b/src/core/server/saved_objects/service/lib/errors.ts index e9138e9b8a347..478c6b6d26d53 100644 --- a/src/core/server/saved_objects/service/lib/errors.ts +++ b/src/core/server/saved_objects/service/lib/errors.ts @@ -33,6 +33,8 @@ const CODE_REQUEST_ENTITY_TOO_LARGE = 'SavedObjectsClient/requestEntityTooLarge' const CODE_NOT_FOUND = 'SavedObjectsClient/notFound'; // 409 - Conflict const CODE_CONFLICT = 'SavedObjectsClient/conflict'; +// 400 - Es Cannot Execute Script +const CODE_ES_CANNOT_EXECUTE_SCRIPT = 'SavedObjectsClient/esCannotExecuteScript'; // 503 - Es Unavailable const CODE_ES_UNAVAILABLE = 'SavedObjectsClient/esUnavailable'; // 503 - Unable to automatically create index because of action.auto_create_index setting @@ -152,10 +154,24 @@ export class SavedObjectsErrorHelpers { return decorate(error, CODE_CONFLICT, 409, reason); } + public static createConflictError(type: string, id: string) { + return SavedObjectsErrorHelpers.decorateConflictError( + Boom.conflict(`Saved object [${type}/${id}] conflict`) + ); + } + public static isConflictError(error: Error | DecoratedError) { return isSavedObjectsClientError(error) && error[code] === CODE_CONFLICT; } + public static decorateEsCannotExecuteScriptError(error: Error, reason?: string) { + return decorate(error, CODE_ES_CANNOT_EXECUTE_SCRIPT, 400, reason); + } + + public static isEsCannotExecuteScriptError(error: Error | DecoratedError) { + return isSavedObjectsClientError(error) && error[code] === CODE_ES_CANNOT_EXECUTE_SCRIPT; + } + public static decorateEsUnavailableError(error: Error, reason?: string) { return decorate(error, CODE_ES_UNAVAILABLE, 503, reason); } diff --git a/src/core/server/saved_objects/service/lib/included_fields.test.ts b/src/core/server/saved_objects/service/lib/included_fields.test.ts index 40d6552c2ad5f..ced99361f1ea0 100644 --- a/src/core/server/saved_objects/service/lib/included_fields.test.ts +++ b/src/core/server/saved_objects/service/lib/included_fields.test.ts @@ -26,7 +26,7 @@ describe('includedFields', () => { it('accepts type string', () => { const fields = includedFields('config', 'foo'); - expect(fields).toHaveLength(7); + expect(fields).toHaveLength(8); expect(fields).toContain('type'); }); @@ -37,6 +37,7 @@ Array [ "config.foo", "secret.foo", "namespace", + "namespaces", "type", "references", "migrationVersion", @@ -48,14 +49,14 @@ Array [ it('accepts field as string', () => { const fields = includedFields('config', 'foo'); - expect(fields).toHaveLength(7); + expect(fields).toHaveLength(8); expect(fields).toContain('config.foo'); }); it('accepts fields as an array', () => { const fields = includedFields('config', ['foo', 'bar']); - expect(fields).toHaveLength(9); + expect(fields).toHaveLength(10); expect(fields).toContain('config.foo'); expect(fields).toContain('config.bar'); }); @@ -69,6 +70,7 @@ Array [ "secret.foo", "secret.bar", "namespace", + "namespaces", "type", "references", "migrationVersion", @@ -81,31 +83,37 @@ Array [ it('includes namespace', () => { const fields = includedFields('config', 'foo'); - expect(fields).toHaveLength(7); + expect(fields).toHaveLength(8); expect(fields).toContain('namespace'); }); + it('includes namespaces', () => { + const fields = includedFields('config', 'foo'); + expect(fields).toHaveLength(8); + expect(fields).toContain('namespaces'); + }); + it('includes references', () => { const fields = includedFields('config', 'foo'); - expect(fields).toHaveLength(7); + expect(fields).toHaveLength(8); expect(fields).toContain('references'); }); it('includes migrationVersion', () => { const fields = includedFields('config', 'foo'); - expect(fields).toHaveLength(7); + expect(fields).toHaveLength(8); expect(fields).toContain('migrationVersion'); }); it('includes updated_at', () => { const fields = includedFields('config', 'foo'); - expect(fields).toHaveLength(7); + expect(fields).toHaveLength(8); expect(fields).toContain('updated_at'); }); it('uses wildcard when type is not provided', () => { const fields = includedFields(undefined, 'foo'); - expect(fields).toHaveLength(7); + expect(fields).toHaveLength(8); expect(fields).toContain('*.foo'); }); @@ -113,7 +121,7 @@ Array [ it('includes legacy field path', () => { const fields = includedFields('config', ['foo', 'bar']); - expect(fields).toHaveLength(9); + expect(fields).toHaveLength(10); expect(fields).toContain('foo'); expect(fields).toContain('bar'); }); diff --git a/src/core/server/saved_objects/service/lib/included_fields.ts b/src/core/server/saved_objects/service/lib/included_fields.ts index f372db5a1a635..c50ac22594008 100644 --- a/src/core/server/saved_objects/service/lib/included_fields.ts +++ b/src/core/server/saved_objects/service/lib/included_fields.ts @@ -37,6 +37,7 @@ export function includedFields(type: string | string[] = '*', fields?: string[] return [...acc, ...sourceFields.map(f => `${t}.${f}`)]; }, []) .concat('namespace') + .concat('namespaces') .concat('type') .concat('references') .concat('migrationVersion') diff --git a/src/core/server/saved_objects/service/lib/repository.mock.ts b/src/core/server/saved_objects/service/lib/repository.mock.ts index e69c0ff37d1be..afef378b7307b 100644 --- a/src/core/server/saved_objects/service/lib/repository.mock.ts +++ b/src/core/server/saved_objects/service/lib/repository.mock.ts @@ -28,6 +28,8 @@ const create = (): jest.Mocked => ({ find: jest.fn(), get: jest.fn(), update: jest.fn(), + addToNamespaces: jest.fn(), + deleteFromNamespaces: jest.fn(), deleteByNamespace: jest.fn(), incrementCounter: jest.fn(), }); diff --git a/src/core/server/saved_objects/service/lib/repository.test.js b/src/core/server/saved_objects/service/lib/repository.test.js index 2e5eeec04e0a8..927171438ae99 100644 --- a/src/core/server/saved_objects/service/lib/repository.test.js +++ b/src/core/server/saved_objects/service/lib/repository.test.js @@ -16,7 +16,6 @@ * specific language governing permissions and limitations * under the License. */ -import _ from 'lodash'; import { SavedObjectsRepository } from './repository'; import * as getSearchDslNS from './search_dsl/search_dsl'; @@ -30,162 +29,31 @@ jest.mock('./search_dsl/search_dsl', () => ({ getSearchDsl: jest.fn() })); // BEWARE: The SavedObjectClient depends on the implementation details of the SavedObjectsRepository // so any breaking changes to this repository are considered breaking changes to the SavedObjectsClient. +const createBadRequestError = (...args) => + SavedObjectsErrorHelpers.createBadRequestError(...args).output.payload; +const createConflictError = (...args) => + SavedObjectsErrorHelpers.createConflictError(...args).output.payload; +const createGenericNotFoundError = (...args) => + SavedObjectsErrorHelpers.createGenericNotFoundError(...args).output.payload; +const createUnsupportedTypeError = (...args) => + SavedObjectsErrorHelpers.createUnsupportedTypeError(...args).output.payload; + describe('SavedObjectsRepository', () => { let callAdminCluster; let savedObjectsRepository; let migrator; + let serializer; const mockTimestamp = '2017-08-14T15:49:14.886Z'; const mockTimestampFields = { updated_at: mockTimestamp }; const mockVersionProps = { _seq_no: 1, _primary_term: 1 }; const mockVersion = encodeHitVersion(mockVersionProps); - const noNamespaceSearchResults = { - hits: { - total: 4, - hits: [ - { - _index: '.kibana', - _id: 'index-pattern:logstash-*', - _score: 1, - ...mockVersionProps, - _source: { - type: 'index-pattern', - ...mockTimestampFields, - 'index-pattern': { - title: 'logstash-*', - timeFieldName: '@timestamp', - notExpandable: true, - }, - }, - }, - { - _index: '.kibana', - _id: 'config:6.0.0-alpha1', - _score: 1, - ...mockVersionProps, - _source: { - type: 'config', - ...mockTimestampFields, - config: { - buildNum: 8467, - defaultIndex: 'logstash-*', - }, - }, - }, - { - _index: '.kibana', - _id: 'index-pattern:stocks-*', - _score: 1, - ...mockVersionProps, - _source: { - type: 'index-pattern', - ...mockTimestampFields, - 'index-pattern': { - title: 'stocks-*', - timeFieldName: '@timestamp', - notExpandable: true, - }, - }, - }, - { - _index: '.kibana', - _id: 'globaltype:something', - _score: 1, - ...mockVersionProps, - _source: { - type: 'globaltype', - ...mockTimestampFields, - globaltype: { - name: 'bar', - }, - }, - }, - ], - }, - }; - - const namespacedSearchResults = { - hits: { - total: 4, - hits: [ - { - _index: '.kibana', - _id: 'foo-namespace:index-pattern:logstash-*', - _score: 1, - ...mockVersionProps, - _source: { - namespace: 'foo-namespace', - type: 'index-pattern', - ...mockTimestampFields, - 'index-pattern': { - title: 'logstash-*', - timeFieldName: '@timestamp', - notExpandable: true, - }, - }, - }, - { - _index: '.kibana', - _id: 'foo-namespace:config:6.0.0-alpha1', - _score: 1, - ...mockVersionProps, - _source: { - namespace: 'foo-namespace', - type: 'config', - ...mockTimestampFields, - config: { - buildNum: 8467, - defaultIndex: 'logstash-*', - }, - }, - }, - { - _index: '.kibana', - _id: 'foo-namespace:index-pattern:stocks-*', - _score: 1, - ...mockVersionProps, - _source: { - namespace: 'foo-namespace', - type: 'index-pattern', - ...mockTimestampFields, - 'index-pattern': { - title: 'stocks-*', - timeFieldName: '@timestamp', - notExpandable: true, - }, - }, - }, - { - _index: '.kibana', - _id: 'globaltype:something', - _score: 1, - ...mockVersionProps, - _source: { - type: 'globaltype', - ...mockTimestampFields, - globaltype: { - name: 'bar', - }, - }, - }, - ], - }, - }; - const deleteByQueryResults = { - took: 27, - timed_out: false, - total: 23, - deleted: 23, - batches: 1, - version_conflicts: 0, - noops: 0, - retries: { bulk: 0, search: 0 }, - throttled_millis: 0, - requests_per_second: -1, - throttled_until_millis: 0, - failures: [], - }; + const CUSTOM_INDEX_TYPE = 'customIndex'; + const NAMESPACE_AGNOSTIC_TYPE = 'globalType'; + const MULTI_NAMESPACE_TYPE = 'shareableType'; + const MULTI_NAMESPACE_CUSTOM_INDEX_TYPE = 'shareableTypeCustomIndex'; + const HIDDEN_TYPE = 'hiddenType'; const mappings = { properties: { @@ -194,43 +62,47 @@ describe('SavedObjectsRepository', () => { type: 'keyword', }, }, - foo: { + 'index-pattern': { properties: { - type: 'keyword', + someField: { + type: 'keyword', + }, }, }, - bar: { + dashboard: { properties: { - type: 'keyword', + otherField: { + type: 'keyword', + }, }, }, - baz: { + [CUSTOM_INDEX_TYPE]: { properties: { type: 'keyword', }, }, - 'index-pattern': { + [NAMESPACE_AGNOSTIC_TYPE]: { properties: { - someField: { + yetAnotherField: { type: 'keyword', }, }, }, - dashboard: { + [MULTI_NAMESPACE_TYPE]: { properties: { - otherField: { + evenYetAnotherField: { type: 'keyword', }, }, }, - globaltype: { + [MULTI_NAMESPACE_CUSTOM_INDEX_TYPE]: { properties: { - yetAnotherField: { + evenYetAnotherField: { type: 'keyword', }, }, }, - hiddenType: { + [HIDDEN_TYPE]: { properties: { someField: { type: 'keyword', @@ -240,96 +112,97 @@ describe('SavedObjectsRepository', () => { }, }; - const typeRegistry = new SavedObjectTypeRegistry(); - typeRegistry.registerType({ - name: 'config', - hidden: false, - namespaceAgnostic: false, - mappings: { - properties: { - type: 'keyword', - }, - }, + const createType = type => ({ + name: type, + mappings: { properties: mappings.properties[type].properties }, }); - typeRegistry.registerType({ - name: 'index-pattern', - hidden: false, - namespaceAgnostic: false, - mappings: { - properties: { - someField: { - type: 'keyword', - }, - }, - }, + + const registry = new SavedObjectTypeRegistry(); + registry.registerType(createType('config')); + registry.registerType(createType('index-pattern')); + registry.registerType(createType('dashboard')); + registry.registerType({ + ...createType(CUSTOM_INDEX_TYPE), + indexPattern: 'custom', }); - typeRegistry.registerType({ - name: 'dashboard', - hidden: false, - namespaceAgnostic: false, - mappings: { - properties: { - otherField: { - type: 'keyword', - }, - }, - }, + registry.registerType({ + ...createType(NAMESPACE_AGNOSTIC_TYPE), + namespaceType: 'agnostic', }); - typeRegistry.registerType({ - name: 'globaltype', - hidden: false, - namespaceAgnostic: true, - mappings: { - properties: { - yetAnotherField: { - type: 'keyword', - }, - }, - }, + registry.registerType({ + ...createType(MULTI_NAMESPACE_TYPE), + namespaceType: 'multiple', }); - typeRegistry.registerType({ - name: 'foo', - hidden: false, - namespaceAgnostic: true, - mappings: { - properties: { - type: 'keyword', - }, - }, + registry.registerType({ + ...createType(MULTI_NAMESPACE_CUSTOM_INDEX_TYPE), + namespaceType: 'multiple', + indexPattern: 'custom', }); - typeRegistry.registerType({ - name: 'bar', - hidden: false, - namespaceAgnostic: true, - mappings: { - properties: { - type: 'keyword', - }, - }, + registry.registerType({ + ...createType(HIDDEN_TYPE), + hidden: true, + namespaceType: 'agnostic', }); - typeRegistry.registerType({ - name: 'baz', - hidden: false, - namespaceAgnostic: false, - indexPattern: 'beats', - mappings: { - properties: { - type: 'keyword', - }, + + const getMockGetResponse = ({ type, id, references, namespace }) => ({ + // NOTE: Elasticsearch returns more fields (_index, _type) but the SavedObjectsRepository method ignores these + found: true, + _id: `${registry.isSingleNamespace(type) && namespace ? `${namespace}:` : ''}${type}:${id}`, + ...mockVersionProps, + _source: { + ...(registry.isSingleNamespace(type) && { namespace }), + ...(registry.isMultiNamespace(type) && { namespaces: [namespace ?? 'default'] }), + type, + [type]: { title: 'Testing' }, + references, + specialProperty: 'specialValue', + ...mockTimestampFields, }, }); - typeRegistry.registerType({ - name: 'hiddenType', - hidden: true, - namespaceAgnostic: true, - mappings: { - properties: { - someField: { - type: 'keyword', - }, - }, + + const getMockMgetResponse = (objects, namespace) => ({ + status: 200, + docs: objects.map(obj => + obj.found === false ? obj : getMockGetResponse({ ...obj, namespace }) + ), + }); + + const expectClusterCalls = (...actions) => { + for (let i = 0; i < actions.length; i++) { + expect(callAdminCluster).toHaveBeenNthCalledWith(i + 1, actions[i], expect.any(Object)); + } + expect(callAdminCluster).toHaveBeenCalledTimes(actions.length); + }; + const expectClusterCallArgs = (args, n = 1) => { + expect(callAdminCluster).toHaveBeenNthCalledWith( + n, + expect.any(String), + expect.objectContaining(args) + ); + }; + + expect.extend({ + toBeDocumentWithoutError(received, type, id) { + if (received.type === type && received.id === id && !received.error) { + return { message: () => `expected type and id not to match without error`, pass: true }; + } else { + return { message: () => `expected type and id to match without error`, pass: false }; + } }, }); + const expectSuccess = ({ type, id }) => expect.toBeDocumentWithoutError(type, id); + const expectError = ({ type, id }) => ({ type, id, error: expect.any(Object) }); + const expectErrorResult = ({ type, id }, error) => ({ type, id, error }); + const expectErrorNotFound = obj => + expectErrorResult(obj, createGenericNotFoundError(obj.type, obj.id)); + const expectErrorConflict = obj => expectErrorResult(obj, createConflictError(obj.type, obj.id)); + const expectErrorInvalidType = obj => + expectErrorResult(obj, createUnsupportedTypeError(obj.type, obj.id)); + + const expectMigrationArgs = (args, contains = true, n = 1) => { + const obj = contains ? expect.objectContaining(args) : expect.not.objectContaining(args); + expect(migrator.migrateDocument).toHaveBeenNthCalledWith(n, obj); + }; beforeEach(() => { callAdminCluster = jest.fn(); @@ -338,16 +211,28 @@ describe('SavedObjectsRepository', () => { runMigrations: async () => ({ status: 'skipped' }), }; - const serializer = new SavedObjectsSerializer(typeRegistry); - const allTypes = typeRegistry.getAllTypes().map(type => type.name); - const allowedTypes = [...new Set(allTypes.filter(type => !typeRegistry.isHidden(type)))]; + // create a mock serializer "shim" so we can track function calls, but use the real serializer's implementation + serializer = { + isRawSavedObject: jest.fn(), + rawToSavedObject: jest.fn(), + savedObjectToRaw: jest.fn(), + generateRawId: jest.fn(), + trimIdPrefix: jest.fn(), + }; + const _serializer = new SavedObjectsSerializer(registry); + Object.keys(serializer).forEach(key => { + serializer[key].mockImplementation((...args) => _serializer[key](...args)); + }); + + const allTypes = registry.getAllTypes().map(type => type.name); + const allowedTypes = [...new Set(allTypes.filter(type => !registry.isHidden(type)))]; savedObjectsRepository = new SavedObjectsRepository({ index: '.kibana-test', mappings, callCluster: callAdminCluster, migrator, - typeRegistry, + typeRegistry: registry, serializer, allowedTypes, }); @@ -356,2644 +241,2819 @@ describe('SavedObjectsRepository', () => { getSearchDslNS.getSearchDsl.mockReset(); }); - describe('#create', () => { - beforeEach(() => { - callAdminCluster.mockImplementation((method, params) => ({ - _id: params.id, - ...mockVersionProps, - })); - }); + const mockMigrationVersion = { foo: '2.3.4' }; + const mockMigrateDocument = doc => ({ + ...doc, + attributes: { + ...doc.attributes, + ...(doc.attributes?.title && { title: `${doc.attributes.title}!!` }), + }, + migrationVersion: mockMigrationVersion, + references: [{ name: 'search_0', type: 'search', id: '123' }], + }); - it('waits until migrations are complete before proceeding', async () => { - migrator.runMigrations = jest.fn(async () => expect(callAdminCluster).not.toHaveBeenCalled()); + describe('#addToNamespaces', () => { + const id = 'some-id'; + const type = MULTI_NAMESPACE_TYPE; + const currentNs1 = 'default'; + const currentNs2 = 'foo-namespace'; + const newNs1 = 'bar-namespace'; + const newNs2 = 'baz-namespace'; + + const mockGetResponse = (type, id) => { + // mock a document that exists in two namespaces + const mockResponse = getMockGetResponse({ type, id }); + mockResponse._source.namespaces = [currentNs1, currentNs2]; + callAdminCluster.mockResolvedValueOnce(mockResponse); // this._callCluster('get', ...) + }; - await expect( - savedObjectsRepository.create( - 'index-pattern', - { - title: 'Logstash', - }, - { - id: 'logstash-*', - namespace: 'foo-namespace', - } - ) - ).resolves.toBeDefined(); - expect(migrator.runMigrations).toHaveBeenCalledTimes(1); - }); + const addToNamespacesSuccess = async (type, id, namespaces, options) => { + mockGetResponse(type, id); // this._callCluster('get', ...) + callAdminCluster.mockResolvedValue({ + _id: `${type}:${id}`, + ...mockVersionProps, + result: 'updated', + }); // this._writeToCluster('update', ...) + const result = await savedObjectsRepository.addToNamespaces(type, id, namespaces, options); + expect(callAdminCluster).toHaveBeenCalledTimes(2); + return result; + }; - it('formats Elasticsearch response', async () => { - const response = await savedObjectsRepository.create( - 'index-pattern', - { - title: 'Logstash', - }, - { - id: 'logstash-*', - namespace: 'foo-namespace', - references: [ - { - name: 'ref_0', - type: 'test', - id: '123', - }, - ], - } - ); + describe('cluster calls', () => { + it(`should use ES get action then update action`, async () => { + await addToNamespacesSuccess(type, id, [newNs1, newNs2]); + expectClusterCalls('get', 'update'); + }); - expect(response).toEqual({ - type: 'index-pattern', - id: 'logstash-*', - ...mockTimestampFields, - version: mockVersion, - attributes: { - title: 'Logstash', - }, - references: [ - { - name: 'ref_0', - type: 'test', - id: '123', - }, - ], + it(`defaults to the version of the existing document`, async () => { + await addToNamespacesSuccess(type, id, [newNs1, newNs2]); + const versionProperties = { + if_seq_no: mockVersionProps._seq_no, + if_primary_term: mockVersionProps._primary_term, + }; + expectClusterCallArgs(versionProperties, 2); + }); + + it(`accepts version`, async () => { + await addToNamespacesSuccess(type, id, [newNs1, newNs2], { + version: encodeHitVersion({ _seq_no: 100, _primary_term: 200 }), + }); + expectClusterCallArgs({ if_seq_no: 100, if_primary_term: 200 }, 2); }); - }); - it('should use ES index action', async () => { - await savedObjectsRepository.create('index-pattern', { - id: 'logstash-*', - title: 'Logstash', + it(`defaults to a refresh setting of wait_for`, async () => { + await addToNamespacesSuccess(type, id, [newNs1, newNs2]); + expectClusterCallArgs({ refresh: 'wait_for' }, 2); }); - expect(callAdminCluster).toHaveBeenCalledTimes(1); - expect(callAdminCluster).toHaveBeenCalledWith('index', expect.any(Object)); + it(`accepts a custom refresh setting`, async () => { + const refresh = 'foo'; + await addToNamespacesSuccess(type, id, [newNs1, newNs2], { refresh }); + expectClusterCallArgs({ refresh }, 2); + }); }); - it('should use default index', async () => { - await savedObjectsRepository.create('index-pattern', { - id: 'logstash-*', - title: 'Logstash', + describe('errors', () => { + const expectNotFoundError = async (type, id, namespaces, options) => { + await expect( + savedObjectsRepository.addToNamespaces(type, id, namespaces, options) + ).rejects.toThrowError(createGenericNotFoundError(type, id)); + }; + const expectBadRequestError = async (type, id, namespaces, message) => { + await expect( + savedObjectsRepository.addToNamespaces(type, id, namespaces) + ).rejects.toThrowError(createBadRequestError(message)); + }; + + it(`throws when type is invalid`, async () => { + await expectNotFoundError('unknownType', id, [newNs1, newNs2]); + expect(callAdminCluster).not.toHaveBeenCalled(); }); - expect(callAdminCluster).toHaveBeenCalledTimes(1); - expect(callAdminCluster).toHaveBeenCalledWith( - 'index', - expect.objectContaining({ - index: '.kibana-test', - }) - ); - }); + it(`throws when type is hidden`, async () => { + await expectNotFoundError(HIDDEN_TYPE, id, [newNs1, newNs2]); + expect(callAdminCluster).not.toHaveBeenCalled(); + }); - it('should use custom index', async () => { - await savedObjectsRepository.create('baz', { - id: 'logstash-*', - title: 'Logstash', + it(`throws when type is not multi-namespace`, async () => { + const test = async type => { + const message = `${type} doesn't support multiple namespaces`; + await expectBadRequestError(type, id, [newNs1, newNs2], message); + expect(callAdminCluster).not.toHaveBeenCalled(); + }; + await test('index-pattern'); + await test(NAMESPACE_AGNOSTIC_TYPE); }); - expect(callAdminCluster).toHaveBeenCalledTimes(1); - expect(callAdminCluster).toHaveBeenCalledWith( - 'index', - expect.objectContaining({ - index: 'beats', - }) - ); - }); + it(`throws when namespaces is an empty array`, async () => { + const test = async namespaces => { + const message = 'namespaces must be a non-empty array of strings'; + await expectBadRequestError(type, id, namespaces, message); + expect(callAdminCluster).not.toHaveBeenCalled(); + }; + await test([]); + }); - it('migrates the doc', async () => { - migrator.migrateDocument = doc => { - doc.attributes.title = doc.attributes.title + '!!'; - doc.migrationVersion = { foo: '2.3.4' }; - doc.references = [{ name: 'search_0', type: 'search', id: '123' }]; - return doc; - }; + it(`throws when ES is unable to find the document during get`, async () => { + callAdminCluster.mockResolvedValue({ found: false }); // this._callCluster('get', ...) + await expectNotFoundError(type, id, [newNs1, newNs2]); + expectClusterCalls('get'); + }); - await savedObjectsRepository.create('index-pattern', { - id: 'logstash-*', - title: 'Logstash', + it(`throws when ES is unable to find the index during get`, async () => { + callAdminCluster.mockResolvedValue({ status: 404 }); // this._callCluster('get', ...) + await expectNotFoundError(type, id, [newNs1, newNs2]); + expectClusterCalls('get'); }); - expect(callAdminCluster).toHaveBeenCalledTimes(1); - expect(callAdminCluster.mock.calls[0][1]).toMatchObject({ - body: { - 'index-pattern': { id: 'logstash-*', title: 'Logstash!!' }, - migrationVersion: { foo: '2.3.4' }, - type: 'index-pattern', - updated_at: '2017-08-14T15:49:14.886Z', - references: [{ name: 'search_0', type: 'search', id: '123' }], - }, + it(`throws when the document exists, but not in this namespace`, async () => { + mockGetResponse(type, id); // this._callCluster('get', ...) + await expectNotFoundError(type, id, [newNs1, newNs2], { + namespace: 'some-other-namespace', + }); + expectClusterCalls('get'); }); - }); - it('defaults to a refresh setting of `wait_for`', async () => { - await savedObjectsRepository.create('index-pattern', { - id: 'logstash-*', - title: 'Logstash', + it(`throws when ES is unable to find the document during update`, async () => { + mockGetResponse(type, id); // this._callCluster('get', ...) + callAdminCluster.mockResolvedValue({ status: 404 }); // this._writeToCluster('update', ...) + await expectNotFoundError(type, id, [newNs1, newNs2]); + expectClusterCalls('get', 'update'); }); + }); - expect(callAdminCluster).toHaveBeenCalledTimes(1); - expect(callAdminCluster.mock.calls[0][1]).toMatchObject({ - refresh: 'wait_for', + describe('migration', () => { + it(`waits until migrations are complete before proceeding`, async () => { + let callAdminClusterCount = 0; + migrator.runMigrations = jest.fn(async () => + // runMigrations should resolve before callAdminCluster is initiated + expect(callAdminCluster).toHaveBeenCalledTimes(callAdminClusterCount++) + ); + await expect(addToNamespacesSuccess(type, id, [newNs1, newNs2])).resolves.toBeDefined(); + expect(migrator.runMigrations).toHaveReturnedTimes(2); }); }); - it('accepts custom refresh settings', async () => { - await savedObjectsRepository.create( - 'index-pattern', - { - id: 'logstash-*', - title: 'Logstash', - }, - { - refresh: true, - } - ); + describe('returns', () => { + it(`returns an empty object on success`, async () => { + const result = await addToNamespacesSuccess(type, id, [newNs1, newNs2]); + expect(result).toEqual({}); + }); - expect(callAdminCluster).toHaveBeenCalledTimes(1); - expect(callAdminCluster.mock.calls[0][1]).toMatchObject({ - refresh: true, + it(`succeeds when adding existing namespaces`, async () => { + const result = await addToNamespacesSuccess(type, id, [currentNs1]); + expect(result).toEqual({}); }); }); + }); - it('should use create action if ID defined and overwrite=false', async () => { - await savedObjectsRepository.create( - 'index-pattern', - { - title: 'Logstash', - }, - { - id: 'logstash-*', - } - ); + describe('#bulkCreate', () => { + const obj1 = { + type: 'config', + id: '6.0.0-alpha1', + attributes: { title: 'Test One' }, + references: [{ name: 'ref_0', type: 'test', id: '1' }], + }; + const obj2 = { + type: 'index-pattern', + id: 'logstash-*', + attributes: { title: 'Test Two' }, + references: [{ name: 'ref_0', type: 'test', id: '2' }], + }; + const namespace = 'foo-namespace'; - expect(callAdminCluster).toHaveBeenCalledTimes(1); - expect(callAdminCluster).toHaveBeenCalledWith('create', expect.any(Object)); - }); + const getMockBulkCreateResponse = (objects, namespace) => { + return { + items: objects.map(({ type, id }) => ({ + create: { + _id: `${namespace ? `${namespace}:` : ''}${type}:${id}`, + ...mockVersionProps, + }, + })), + }; + }; - it('allows for id to be provided', async () => { - await savedObjectsRepository.create( - 'index-pattern', - { - title: 'Logstash', - }, - { id: 'logstash-*' } - ); + const bulkCreateSuccess = async (objects, options) => { + const multiNamespaceObjects = + options?.overwrite && + objects.filter(({ type, id }) => registry.isMultiNamespace(type) && id); + if (multiNamespaceObjects?.length) { + const response = getMockMgetResponse(multiNamespaceObjects, options?.namespace); + callAdminCluster.mockResolvedValueOnce(response); // this._callCluster('mget', ...) + } + const response = getMockBulkCreateResponse(objects, options?.namespace); + callAdminCluster.mockResolvedValue(response); // this._writeToCluster('bulk', ...) + const result = await savedObjectsRepository.bulkCreate(objects, options); + expect(callAdminCluster).toHaveBeenCalledTimes(multiNamespaceObjects?.length ? 2 : 1); + return result; + }; - expect(callAdminCluster).toHaveBeenCalledTimes(1); - expect(callAdminCluster).toHaveBeenCalledWith( - expect.any(String), - expect.objectContaining({ - id: 'index-pattern:logstash-*', - }) - ); + // bulk create calls have two objects for each source -- the action, and the source + const expectClusterCallArgsAction = ( + objects, + { method, _index = expect.any(String), getId = () => expect.any(String) } + ) => { + const body = []; + for (const { type, id } of objects) { + body.push({ [method]: { _index, _id: getId(type, id) } }); + body.push(expect.any(Object)); + } + expectClusterCallArgs({ body }); + }; + + const expectObjArgs = ({ type, attributes, references }, overrides) => [ + expect.any(Object), + expect.objectContaining({ + [type]: attributes, + references, + type, + ...overrides, + ...mockTimestampFields, + }), + ]; + + const expectSuccessResult = obj => ({ + ...obj, + migrationVersion: undefined, + version: mockVersion, + ...mockTimestampFields, }); - it('self-generates an ID', async () => { - await savedObjectsRepository.create('index-pattern', { - title: 'Logstash', + describe('cluster calls', () => { + it(`should use the ES bulk action by default`, async () => { + await bulkCreateSuccess([obj1, obj2]); + expectClusterCalls('bulk'); }); - expect(callAdminCluster).toHaveBeenCalledTimes(1); - expect(callAdminCluster).toHaveBeenCalledWith( - expect.any(String), - expect.objectContaining({ - id: expect.objectContaining(/index-pattern:[0-9a-f]{8}-([0-9a-f]{4}-){3}[0-9a-f]{12}/), - }) - ); - }); + it(`should use the ES mget action before bulk action for any types that are multi-namespace, when overwrite=true`, async () => { + const objects = [obj1, { ...obj2, type: MULTI_NAMESPACE_TYPE }]; + await bulkCreateSuccess(objects, { overwrite: true }); + expectClusterCalls('mget', 'bulk'); + const docs = [expect.objectContaining({ _id: `${MULTI_NAMESPACE_TYPE}:${obj2.id}` })]; + expectClusterCallArgs({ body: { docs } }, 1); + }); - it('prepends namespace to the id and adds namespace to body when providing namespace for namespaced type', async () => { - await savedObjectsRepository.create( - 'index-pattern', - { - title: 'Logstash', - }, - { - id: 'foo-id', - namespace: 'foo-namespace', - } - ); - expect(callAdminCluster).toHaveBeenCalledTimes(1); - expect(callAdminCluster).toHaveBeenCalledWith( - expect.any(String), - expect.objectContaining({ - id: `foo-namespace:index-pattern:foo-id`, - body: expect.objectContaining({ - [`index-pattern`]: { title: 'Logstash' }, - namespace: 'foo-namespace', - type: 'index-pattern', - updated_at: '2017-08-14T15:49:14.886Z', - }), - }) - ); - }); + it(`should use the ES create method if ID is undefined and overwrite=true`, async () => { + const objects = [obj1, obj2].map(obj => ({ ...obj, id: undefined })); + await bulkCreateSuccess(objects, { overwrite: true }); + expectClusterCallArgsAction(objects, { method: 'create' }); + }); - it(`doesn't prepend namespace to the id or add namespace property when providing no namespace for namespaced type`, async () => { - await savedObjectsRepository.create( - 'index-pattern', - { - title: 'Logstash', - }, - { - id: 'foo-id', - } - ); - expect(callAdminCluster).toHaveBeenCalledTimes(1); - expect(callAdminCluster).toHaveBeenCalledWith( - expect.any(String), - expect.objectContaining({ - id: `index-pattern:foo-id`, - body: expect.objectContaining({ - [`index-pattern`]: { title: 'Logstash' }, - type: 'index-pattern', - updated_at: '2017-08-14T15:49:14.886Z', - }), - }) - ); - }); + it(`should use the ES create method if ID is undefined and overwrite=false`, async () => { + const objects = [obj1, obj2].map(obj => ({ ...obj, id: undefined })); + await bulkCreateSuccess(objects); + expectClusterCallArgsAction(objects, { method: 'create' }); + }); - it(`doesn't prepend namespace to the id or add namespace property when providing namespace for namespace agnostic type`, async () => { - await savedObjectsRepository.create( - 'globaltype', - { - title: 'Logstash', - }, - { - id: 'foo-id', - namespace: 'foo-namespace', - } - ); - expect(callAdminCluster).toHaveBeenCalledTimes(1); - expect(callAdminCluster).toHaveBeenCalledWith( - expect.any(String), - expect.objectContaining({ - id: `globaltype:foo-id`, - body: expect.objectContaining({ - [`globaltype`]: { title: 'Logstash' }, - type: 'globaltype', - updated_at: '2017-08-14T15:49:14.886Z', - }), - }) - ); - }); - - it('defaults to empty references array if none are provided', async () => { - await savedObjectsRepository.create( - 'index-pattern', - { - title: 'Logstash', - }, - { - id: 'logstash-*', - } - ); + it(`should use the ES index method if ID is defined and overwrite=true`, async () => { + await bulkCreateSuccess([obj1, obj2], { overwrite: true }); + expectClusterCallArgsAction([obj1, obj2], { method: 'index' }); + }); - expect(callAdminCluster).toHaveBeenCalledTimes(1); - expect(callAdminCluster).toHaveBeenCalledWith( - expect.any(String), - expect.objectContaining({ - body: expect.objectContaining({ - references: [], - }), - }) - ); - }); - }); + it(`should use the ES create method if ID is defined and overwrite=false`, async () => { + await bulkCreateSuccess([obj1, obj2]); + expectClusterCallArgsAction([obj1, obj2], { method: 'create' }); + }); - describe('#bulkCreate', () => { - it('waits until migrations are complete before proceeding', async () => { - migrator.runMigrations = jest.fn(async () => expect(callAdminCluster).not.toHaveBeenCalled()); - callAdminCluster.mockReturnValue({ - items: [ - { create: { type: 'config', id: 'config:one', _primary_term: 1, _seq_no: 1 } }, - { - create: { - type: 'index-pattern', - id: 'index-pattern:two', - _primary_term: 1, - _seq_no: 1, - }, - }, - ], + it(`formats the ES request`, async () => { + await bulkCreateSuccess([obj1, obj2]); + const body = [...expectObjArgs(obj1), ...expectObjArgs(obj2)]; + expectClusterCallArgs({ body }); }); - await expect( - savedObjectsRepository.bulkCreate([ - { type: 'config', id: 'one', attributes: { title: 'Test One' } }, - { type: 'index-pattern', id: 'two', attributes: { title: 'Test Two' } }, - ]) - ).resolves.toBeDefined(); + it(`adds namespace to request body for any types that are single-namespace`, async () => { + await bulkCreateSuccess([obj1, obj2], { namespace }); + const expected = expect.objectContaining({ namespace }); + const body = [expect.any(Object), expected, expect.any(Object), expected]; + expectClusterCallArgs({ body }); + }); - expect(migrator.runMigrations).toHaveBeenCalledTimes(1); - }); + it(`doesn't add namespace to request body for any types that are not single-namespace`, async () => { + const objects = [ + { ...obj1, type: NAMESPACE_AGNOSTIC_TYPE }, + { ...obj2, type: MULTI_NAMESPACE_TYPE }, + ]; + await bulkCreateSuccess(objects, { namespace }); + const expected = expect.not.objectContaining({ namespace: expect.anything() }); + const body = [expect.any(Object), expected, expect.any(Object), expected]; + expectClusterCallArgs({ body }); + }); - it('formats Elasticsearch request', async () => { - callAdminCluster.mockReturnValue({ - items: [ - { create: { type: 'config', id: 'config:one', _primary_term: 1, _seq_no: 1 } }, - { create: { type: 'index-pattern', id: 'config:two', _primary_term: 1, _seq_no: 1 } }, - ], + it(`adds namespaces to request body for any types that are multi-namespace`, async () => { + const test = async namespace => { + const objects = [obj1, obj2].map(x => ({ ...x, type: MULTI_NAMESPACE_TYPE })); + const namespaces = [namespace ?? 'default']; + await bulkCreateSuccess(objects, { namespace, overwrite: true }); + const expected = expect.objectContaining({ namespaces }); + const body = [expect.any(Object), expected, expect.any(Object), expected]; + expectClusterCallArgs({ body }, 2); + callAdminCluster.mockReset(); + }; + await test(undefined); + await test(namespace); }); - await savedObjectsRepository.bulkCreate([ - { - type: 'config', - id: 'one', - attributes: { title: 'Test One' }, - references: [{ name: 'ref_0', type: 'test', id: '1' }], - }, - { - type: 'index-pattern', - id: 'two', - attributes: { title: 'Test Two' }, - references: [{ name: 'ref_0', type: 'test', id: '2' }], - }, - ]); + it(`doesn't add namespaces to request body for any types that are not multi-namespace`, async () => { + const test = async namespace => { + const objects = [obj1, { ...obj2, type: NAMESPACE_AGNOSTIC_TYPE }]; + await bulkCreateSuccess(objects, { namespace, overwrite: true }); + const expected = expect.not.objectContaining({ namespaces: expect.anything() }); + const body = [expect.any(Object), expected, expect.any(Object), expected]; + expectClusterCallArgs({ body }); + callAdminCluster.mockReset(); + }; + await test(undefined); + await test(namespace); + }); - expect(callAdminCluster).toHaveBeenCalledTimes(1); - const bulkCalls = callAdminCluster.mock.calls.filter(([path]) => path === 'bulk'); + it(`defaults to a refresh setting of wait_for`, async () => { + await bulkCreateSuccess([obj1, obj2]); + expectClusterCallArgs({ refresh: 'wait_for' }); + }); - expect(bulkCalls.length).toEqual(1); + it(`accepts a custom refresh setting`, async () => { + const refresh = 'foo'; + await bulkCreateSuccess([obj1, obj2], { refresh }); + expectClusterCallArgs({ refresh }); + }); - expect(bulkCalls[0][1].body).toEqual([ - { create: { _index: '.kibana-test', _id: 'config:one' } }, - { - type: 'config', - ...mockTimestampFields, - config: { title: 'Test One' }, - references: [{ name: 'ref_0', type: 'test', id: '1' }], - }, - { create: { _index: '.kibana-test', _id: 'index-pattern:two' } }, - { - type: 'index-pattern', - ...mockTimestampFields, - 'index-pattern': { title: 'Test Two' }, - references: [{ name: 'ref_0', type: 'test', id: '2' }], - }, - ]); - }); + it(`should use default index`, async () => { + await bulkCreateSuccess([obj1, obj2]); + expectClusterCallArgsAction([obj1, obj2], { method: 'create', _index: '.kibana-test' }); + }); - it('defaults to a refresh setting of `wait_for`', async () => { - callAdminCluster.mockReturnValue({ - items: [{ create: { type: 'config', id: 'config:one', _primary_term: 1, _seq_no: 1 } }], + it(`should use custom index`, async () => { + await bulkCreateSuccess([obj1, obj2].map(x => ({ ...x, type: CUSTOM_INDEX_TYPE }))); + expectClusterCallArgsAction([obj1, obj2], { method: 'create', _index: 'custom' }); }); - await savedObjectsRepository.bulkCreate([ - { - type: 'config', - id: 'one', - attributes: { title: 'Test One' }, - references: [{ name: 'ref_0', type: 'test', id: '1' }], - }, - ]); + it(`prepends namespace to the id when providing namespace for single-namespace type`, async () => { + const getId = (type, id) => `${namespace}:${type}:${id}`; + await bulkCreateSuccess([obj1, obj2], { namespace }); + expectClusterCallArgsAction([obj1, obj2], { method: 'create', getId }); + }); - expect(callAdminCluster).toHaveBeenCalledTimes(1); + it(`doesn't prepend namespace to the id when providing no namespace for single-namespace type`, async () => { + const getId = (type, id) => `${type}:${id}`; + await bulkCreateSuccess([obj1, obj2]); + expectClusterCallArgsAction([obj1, obj2], { method: 'create', getId }); + }); - expect(callAdminCluster.mock.calls[0][1]).toMatchObject({ - refresh: 'wait_for', + it(`doesn't prepend namespace to the id when not using single-namespace type`, async () => { + const getId = (type, id) => `${type}:${id}`; + const objects = [ + { ...obj1, type: NAMESPACE_AGNOSTIC_TYPE }, + { ...obj2, type: MULTI_NAMESPACE_TYPE }, + ]; + await bulkCreateSuccess(objects, { namespace }); + expectClusterCallArgsAction(objects, { method: 'create', getId }); }); }); - it('accepts a custom refresh setting', async () => { - callAdminCluster.mockReturnValue({ - items: [ - { create: { type: 'config', id: 'config:one', _primary_term: 1, _seq_no: 1 } }, - { create: { type: 'index-pattern', id: 'config:two', _primary_term: 1, _seq_no: 1 } }, - ], - }); + describe('errors', () => { + const obj3 = { + type: 'dashboard', + id: 'three', + attributes: { title: 'Test Three' }, + references: [{ name: 'ref_0', type: 'test', id: '2' }], + }; - await savedObjectsRepository.bulkCreate( - [ - { - type: 'config', - id: 'one', - attributes: { title: 'Test One' }, - references: [{ name: 'ref_0', type: 'test', id: '1' }], - }, - { - type: 'index-pattern', - id: 'two', - attributes: { title: 'Test Two' }, - references: [{ name: 'ref_0', type: 'test', id: '2' }], - }, - ], - { - refresh: true, + const bulkCreateError = async (obj, esError, expectedError) => { + const objects = [obj1, obj, obj2]; + const response = getMockBulkCreateResponse(objects); + if (esError) { + response.items[1].create = { error: esError }; } - ); - - expect(callAdminCluster).toHaveBeenCalledTimes(1); + callAdminCluster.mockResolvedValue(response); // this._writeToCluster('bulk', ...) + + const result = await savedObjectsRepository.bulkCreate(objects); + expectClusterCalls('bulk'); + const objCall = esError ? expectObjArgs(obj) : []; + const body = [...expectObjArgs(obj1), ...objCall, ...expectObjArgs(obj2)]; + expectClusterCallArgs({ body }); + expect(result).toEqual({ + saved_objects: [expectSuccess(obj1), expectedError, expectSuccess(obj2)], + }); + }; - expect(callAdminCluster.mock.calls[0][1]).toMatchObject({ - refresh: true, + it(`returns error when type is invalid`, async () => { + const obj = { ...obj3, type: 'unknownType' }; + await bulkCreateError(obj, undefined, expectErrorInvalidType(obj)); }); - }); - it('migrates the docs', async () => { - callAdminCluster.mockReturnValue({ - items: [ - { - create: { - error: false, - _id: '1', - _seq_no: 1, - _primary_term: 1, - }, - }, - { - create: { - error: false, - _id: '2', - _seq_no: 1, - _primary_term: 1, - }, - }, - ], + it(`returns error when type is hidden`, async () => { + const obj = { ...obj3, type: HIDDEN_TYPE }; + await bulkCreateError(obj, undefined, expectErrorInvalidType(obj)); }); - migrator.migrateDocument = doc => { - doc.attributes.title = doc.attributes.title + '!!'; - doc.migrationVersion = { foo: '2.3.4' }; - doc.references = [{ name: 'search_0', type: 'search', id: '123' }]; - return doc; - }; - - const bulkCreateResp = await savedObjectsRepository.bulkCreate([ - { type: 'config', id: 'one', attributes: { title: 'Test One' } }, - { type: 'index-pattern', id: 'two', attributes: { title: 'Test Two' } }, - ]); - - expect(callAdminCluster).toHaveBeenCalledWith( - 'bulk', - expect.objectContaining({ - body: [ - { create: { _index: '.kibana-test', _id: 'config:one' } }, - { - type: 'config', - ...mockTimestampFields, - config: { title: 'Test One!!' }, - migrationVersion: { foo: '2.3.4' }, - references: [{ name: 'search_0', type: 'search', id: '123' }], - }, - { create: { _index: '.kibana-test', _id: 'index-pattern:two' } }, + it(`returns error when there is a conflict with an existing multi-namespace saved object (get)`, async () => { + const obj = { ...obj3, type: MULTI_NAMESPACE_TYPE }; + const response1 = { + status: 200, + docs: [ { - type: 'index-pattern', - ...mockTimestampFields, - 'index-pattern': { title: 'Test Two!!' }, - migrationVersion: { foo: '2.3.4' }, - references: [{ name: 'search_0', type: 'search', id: '123' }], + found: true, + _source: { + type: obj.type, + namespaces: ['bar-namespace'], + }, }, ], - }) - ); - - expect(bulkCreateResp).toEqual({ - saved_objects: [ - { - id: 'one', - type: 'config', - version: mockVersion, - updated_at: mockTimestamp, - attributes: { - title: 'Test One!!', - }, - references: [{ name: 'search_0', type: 'search', id: '123' }], - }, - { - id: 'two', - type: 'index-pattern', - version: mockVersion, - updated_at: mockTimestamp, - attributes: { - title: 'Test Two!!', - }, - references: [{ name: 'search_0', type: 'search', id: '123' }], - }, - ], + }; + callAdminCluster.mockResolvedValueOnce(response1); // this._callCluster('mget', ...) + const response2 = getMockBulkCreateResponse([obj1, obj2]); + callAdminCluster.mockResolvedValue(response2); // this._writeToCluster('bulk', ...) + + const options = { overwrite: true }; + const result = await savedObjectsRepository.bulkCreate([obj1, obj, obj2], options); + expectClusterCalls('mget', 'bulk'); + const body1 = { docs: [expect.objectContaining({ _id: `${obj.type}:${obj.id}` })] }; + expectClusterCallArgs({ body: body1 }, 1); + const body2 = [...expectObjArgs(obj1), ...expectObjArgs(obj2)]; + expectClusterCallArgs({ body: body2 }, 2); + expect(result).toEqual({ + saved_objects: [expectSuccess(obj1), expectErrorConflict(obj), expectSuccess(obj2)], + }); }); - }); - it('should overwrite objects if overwrite is truthy', async () => { - callAdminCluster.mockReturnValue({ - items: [{ create: { type: 'foo', id: 'bar', _primary_term: 1, _seq_no: 1 } }], + it(`returns error when there is a version conflict (bulk)`, async () => { + const esError = { type: 'version_conflict_engine_exception' }; + await bulkCreateError(obj3, esError, expectErrorConflict(obj3)); }); - await savedObjectsRepository.bulkCreate([{ type: 'foo', id: 'bar', attributes: {} }], { - overwrite: false, + it(`returns error when document is missing`, async () => { + const esError = { type: 'document_missing_exception' }; + await bulkCreateError(obj3, esError, expectErrorNotFound(obj3)); }); - expect(callAdminCluster).toHaveBeenCalledTimes(1); - expect(callAdminCluster).toHaveBeenCalledWith( - 'bulk', - expect.objectContaining({ - body: [ - // uses create because overwriting is not allowed - { create: { _index: '.kibana-test', _id: 'foo:bar' } }, - { type: 'foo', ...mockTimestampFields, foo: {}, references: [] }, - ], - }) - ); - - callAdminCluster.mockReset(); - callAdminCluster.mockReturnValue({ - items: [{ create: { type: 'foo', id: 'bar', _primary_term: 1, _seq_no: 1 } }], + it(`returns error reason for other errors`, async () => { + const esError = { reason: 'some_other_error' }; + await bulkCreateError(obj3, esError, expectErrorResult(obj3, { message: esError.reason })); }); - await savedObjectsRepository.bulkCreate([{ type: 'foo', id: 'bar', attributes: {} }], { - overwrite: true, + it(`returns error string for other errors if no reason is defined`, async () => { + const esError = { foo: 'some_other_error' }; + const expectedError = expectErrorResult(obj3, { message: JSON.stringify(esError) }); + await bulkCreateError(obj3, esError, expectedError); }); - expect(callAdminCluster).toHaveBeenCalledTimes(1); - expect(callAdminCluster).toHaveBeenCalledWith( - 'bulk', - expect.objectContaining({ - body: [ - // uses index because overwriting is allowed - { index: { _index: '.kibana-test', _id: 'foo:bar' } }, - { type: 'foo', ...mockTimestampFields, foo: {}, references: [] }, - ], - }) - ); }); - it('mockReturnValue document errors', async () => { - callAdminCluster.mockResolvedValue({ - errors: false, - items: [ - { - create: { - _id: 'config:one', - error: { - reason: 'type[config] missing', - }, - }, - }, - { - create: { - _id: 'index-pattern:two', - ...mockVersionProps, - }, - }, - ], + describe('migration', () => { + it(`waits until migrations are complete before proceeding`, async () => { + migrator.runMigrations = jest.fn(async () => + expect(callAdminCluster).not.toHaveBeenCalled() + ); + await expect(bulkCreateSuccess([obj1, obj2])).resolves.toBeDefined(); + expect(migrator.runMigrations).toHaveBeenCalledTimes(1); }); - const response = await savedObjectsRepository.bulkCreate([ - { type: 'config', id: 'one', attributes: { title: 'Test One' } }, - { type: 'index-pattern', id: 'two', attributes: { title: 'Test Two' } }, - ]); + it(`migrates the docs and serializes the migrated docs`, async () => { + migrator.migrateDocument.mockImplementation(mockMigrateDocument); + await bulkCreateSuccess([obj1, obj2]); + const docs = [obj1, obj2].map(x => ({ ...x, ...mockTimestampFields })); + expectMigrationArgs(docs[0], true, 1); + expectMigrationArgs(docs[1], true, 2); - expect(response).toEqual({ - saved_objects: [ - { - id: 'one', - type: 'config', - error: { message: 'type[config] missing' }, - }, - { - id: 'two', - type: 'index-pattern', - version: mockVersion, - ...mockTimestampFields, - attributes: { title: 'Test Two' }, - references: [], - }, - ], + const migratedDocs = docs.map(x => migrator.migrateDocument(x)); + expect(serializer.savedObjectToRaw).toHaveBeenNthCalledWith(1, migratedDocs[0]); + expect(serializer.savedObjectToRaw).toHaveBeenNthCalledWith(2, migratedDocs[1]); }); - }); - it('formats Elasticsearch response', async () => { - callAdminCluster.mockResolvedValue({ - errors: false, - items: [ - { - create: { - _id: 'config:one', - ...mockVersionProps, - }, - }, - { - create: { - _id: 'index-pattern:two', - ...mockVersionProps, - }, - }, - ], + it(`adds namespace to body when providing namespace for single-namespace type`, async () => { + await bulkCreateSuccess([obj1, obj2], { namespace }); + expectMigrationArgs({ namespace }, true, 1); + expectMigrationArgs({ namespace }, true, 2); }); - const response = await savedObjectsRepository.bulkCreate( - [ - { type: 'config', id: 'one', attributes: { title: 'Test One' } }, - { type: 'index-pattern', id: 'two', attributes: { title: 'Test Two' } }, - ], - { - namespace: 'foo-namespace', - } - ); + it(`doesn't add namespace to body when providing no namespace for single-namespace type`, async () => { + await bulkCreateSuccess([obj1, obj2]); + expectMigrationArgs({ namespace: expect.anything() }, false, 1); + expectMigrationArgs({ namespace: expect.anything() }, false, 2); + }); - expect(response).toEqual({ - saved_objects: [ - { - id: 'one', - type: 'config', - version: mockVersion, - ...mockTimestampFields, - attributes: { title: 'Test One' }, - references: [], - }, - { - id: 'two', - type: 'index-pattern', - version: mockVersion, - ...mockTimestampFields, - attributes: { title: 'Test Two' }, - references: [], - }, - ], + it(`doesn't add namespace to body when not using single-namespace type`, async () => { + const objects = [ + { ...obj1, type: NAMESPACE_AGNOSTIC_TYPE }, + { ...obj2, type: MULTI_NAMESPACE_TYPE }, + ]; + await bulkCreateSuccess(objects, { namespace }); + expectMigrationArgs({ namespace: expect.anything() }, false, 1); + expectMigrationArgs({ namespace: expect.anything() }, false, 2); }); - }); - it('prepends namespace to the id and adds namespace to body when providing namespace for namespaced type', async () => { - callAdminCluster.mockReturnValue({ - items: [ - { - create: { - _id: 'foo-namespace:config:one', - _index: '.kibana-test', - _primary_term: 1, - _seq_no: 2, - }, - }, - { - create: { - _id: 'foo-namespace:index-pattern:two', - _primary_term: 1, - _seq_no: 2, - }, - }, - ], + it(`adds namespaces to body when providing namespace for multi-namespace type`, async () => { + const objects = [obj1, obj2].map(obj => ({ ...obj, type: MULTI_NAMESPACE_TYPE })); + await bulkCreateSuccess(objects, { namespace }); + expectMigrationArgs({ namespaces: [namespace] }, true, 1); + expectMigrationArgs({ namespaces: [namespace] }, true, 2); }); - await savedObjectsRepository.bulkCreate( - [ - { type: 'config', id: 'one', attributes: { title: 'Test One' } }, - { type: 'index-pattern', id: 'two', attributes: { title: 'Test Two' } }, - ], - { - namespace: 'foo-namespace', - } - ); - expect(callAdminCluster).toHaveBeenCalledTimes(1); - expect(callAdminCluster).toHaveBeenCalledWith( - 'bulk', - expect.objectContaining({ - body: [ - { create: { _index: '.kibana-test', _id: 'foo-namespace:config:one' } }, - { - namespace: 'foo-namespace', - type: 'config', - ...mockTimestampFields, - config: { title: 'Test One' }, - references: [], - }, - { create: { _index: '.kibana-test', _id: 'foo-namespace:index-pattern:two' } }, - { - namespace: 'foo-namespace', - type: 'index-pattern', - ...mockTimestampFields, - 'index-pattern': { title: 'Test Two' }, - references: [], - }, - ], - }) - ); - }); - it(`doesn't prepend namespace to the id or add namespace property when providing no namespace for namespaced type`, async () => { - callAdminCluster.mockResolvedValue({ - errors: false, - items: [ - { - create: { - _id: 'config:one', - ...mockVersionProps, - }, - }, - { - create: { - _id: 'index-pattern:two', - ...mockVersionProps, - }, - }, - ], + it(`adds default namespaces to body when providing no namespace for multi-namespace type`, async () => { + const objects = [obj1, obj2].map(obj => ({ ...obj, type: MULTI_NAMESPACE_TYPE })); + await bulkCreateSuccess(objects); + expectMigrationArgs({ namespaces: ['default'] }, true, 1); + expectMigrationArgs({ namespaces: ['default'] }, true, 2); }); - await savedObjectsRepository.bulkCreate([ - { type: 'config', id: 'one', attributes: { title: 'Test One' } }, - { type: 'index-pattern', id: 'two', attributes: { title: 'Test Two' } }, - ]); - expect(callAdminCluster).toHaveBeenCalledTimes(1); - expect(callAdminCluster).toHaveBeenCalledWith( - 'bulk', - expect.objectContaining({ - body: [ - { create: { _id: 'config:one', _index: '.kibana-test' } }, - { - type: 'config', - ...mockTimestampFields, - config: { title: 'Test One' }, - references: [], - }, - { create: { _id: 'index-pattern:two', _index: '.kibana-test' } }, - { - type: 'index-pattern', - ...mockTimestampFields, - 'index-pattern': { title: 'Test Two' }, - references: [], - }, - ], - }) - ); - }); - it(`doesn't prepend namespace to the id or add namespace property when providing namespace for namespace agnostic type`, async () => { - callAdminCluster.mockReturnValue({ - items: [{ create: { _type: '_doc', _id: 'globaltype:one', _primary_term: 1, _seq_no: 2 } }], + it(`doesn't add namespaces to body when not using multi-namespace type`, async () => { + const objects = [obj1, { ...obj2, type: NAMESPACE_AGNOSTIC_TYPE }]; + await bulkCreateSuccess(objects); + expectMigrationArgs({ namespaces: expect.anything() }, false, 1); + expectMigrationArgs({ namespaces: expect.anything() }, false, 2); }); - await savedObjectsRepository.bulkCreate( - [{ type: 'globaltype', id: 'one', attributes: { title: 'Test One' } }], - { - namespace: 'foo-namespace', - } - ); - expect(callAdminCluster).toHaveBeenCalledTimes(1); - expect(callAdminCluster).toHaveBeenCalledWith( - 'bulk', - expect.objectContaining({ - body: [ - { create: { _id: 'globaltype:one', _index: '.kibana-test' } }, - { - type: 'globaltype', - ...mockTimestampFields, - globaltype: { title: 'Test One' }, - references: [], - }, - ], - }) - ); }); - it('should return objects in the same order regardless of type', () => {}); - }); + describe('returns', () => { + it(`formats the ES response`, async () => { + const result = await bulkCreateSuccess([obj1, obj2]); + expect(result).toEqual({ + saved_objects: [obj1, obj2].map(x => expectSuccessResult(x)), + }); + }); - describe('#delete', () => { - it('waits until migrations are complete before proceeding', async () => { - migrator.runMigrations = jest.fn(async () => expect(callAdminCluster).not.toHaveBeenCalled()); - callAdminCluster.mockReturnValue({ result: 'deleted' }); - await expect( - savedObjectsRepository.delete('index-pattern', 'logstash-*', { - namespace: 'foo-namespace', - }) - ).resolves.toBeDefined(); - - expect(migrator.runMigrations).toHaveBeenCalledTimes(1); + it(`should return objects in the same order regardless of type`, async () => { + // TODO + }); + + it(`handles a mix of successful creates and errors`, async () => { + const obj = { + type: 'unknownType', + id: 'three', + }; + const objects = [obj1, obj, obj2]; + const response = getMockBulkCreateResponse(objects); + callAdminCluster.mockResolvedValue(response); // this._writeToCluster('bulk', ...) + const result = await savedObjectsRepository.bulkCreate(objects); + expect(callAdminCluster).toHaveBeenCalledTimes(1); + expect(result).toEqual({ + saved_objects: [expectSuccessResult(obj1), expectError(obj), expectSuccessResult(obj2)], + }); + }); }); + }); - it('throws notFound when ES is unable to find the document', async () => { - expect.assertions(1); + describe('#bulkGet', () => { + const obj1 = { + type: 'config', + id: '6.0.0-alpha1', + attributes: { title: 'Testing' }, + references: [ + { + name: 'ref_0', + type: 'test', + id: '1', + }, + ], + }; + const obj2 = { + type: 'index-pattern', + id: 'logstash-*', + attributes: { title: 'Testing' }, + references: [ + { + name: 'ref_0', + type: 'test', + id: '2', + }, + ], + }; + const namespace = 'foo-namespace'; - callAdminCluster.mockResolvedValue({ result: 'not_found' }); + const bulkGet = async (objects, options) => + savedObjectsRepository.bulkGet( + objects.map(({ type, id }) => ({ type, id })), // bulkGet only uses type and id + options + ); + const bulkGetSuccess = async (objects, options) => { + const response = getMockMgetResponse(objects, options?.namespace); + callAdminCluster.mockReturnValue(response); + const result = await bulkGet(objects, options); + expect(callAdminCluster).toHaveBeenCalledTimes(1); + return result; + }; - try { - await savedObjectsRepository.delete('index-pattern', 'logstash-*'); - } catch (e) { - expect(e.output.statusCode).toEqual(404); - } - }); + const _expectClusterCallArgs = ( + objects, + { _index = expect.any(String), getId = () => expect.any(String) } + ) => { + expectClusterCallArgs({ + body: { + docs: objects.map(({ type, id }) => + expect.objectContaining({ + _index, + _id: getId(type, id), + }) + ), + }, + }); + }; - it(`prepends namespace to the id when providing namespace for namespaced type`, async () => { - callAdminCluster.mockReturnValue({ result: 'deleted' }); - await savedObjectsRepository.delete('index-pattern', 'logstash-*', { - namespace: 'foo-namespace', + describe('cluster calls', () => { + it(`prepends namespace to the id when providing namespace for single-namespace type`, async () => { + const getId = (type, id) => `${namespace}:${type}:${id}`; + await bulkGetSuccess([obj1, obj2], { namespace }); + _expectClusterCallArgs([obj1, obj2], { getId }); }); - expect(callAdminCluster).toHaveBeenCalledTimes(1); - expect(callAdminCluster).toHaveBeenCalledWith('delete', { - id: 'foo-namespace:index-pattern:logstash-*', - refresh: 'wait_for', - index: '.kibana-test', - ignore: [404], + it(`doesn't prepend namespace to the id when providing no namespace for single-namespace type`, async () => { + const getId = (type, id) => `${type}:${id}`; + await bulkGetSuccess([obj1, obj2]); + _expectClusterCallArgs([obj1, obj2], { getId }); }); - }); - it(`doesn't prepend namespace to the id when providing no namespace for namespaced type`, async () => { - callAdminCluster.mockReturnValue({ result: 'deleted' }); - await savedObjectsRepository.delete('index-pattern', 'logstash-*'); + it(`doesn't prepend namespace to the id when not using single-namespace type`, async () => { + const getId = (type, id) => `${type}:${id}`; + let objects = [obj1, obj2].map(obj => ({ ...obj, type: NAMESPACE_AGNOSTIC_TYPE })); + await bulkGetSuccess(objects, { namespace }); + _expectClusterCallArgs(objects, { getId }); - expect(callAdminCluster).toHaveBeenCalledTimes(1); - expect(callAdminCluster).toHaveBeenCalledWith('delete', { - id: 'index-pattern:logstash-*', - refresh: 'wait_for', - index: '.kibana-test', - ignore: [404], + callAdminCluster.mockReset(); + objects = [obj1, obj2].map(obj => ({ ...obj, type: MULTI_NAMESPACE_TYPE })); + await bulkGetSuccess(objects, { namespace }); + _expectClusterCallArgs(objects, { getId }); }); }); - it(`doesn't prepend namespace to the id when providing namespace for namespace agnostic type`, async () => { - callAdminCluster.mockReturnValue({ result: 'deleted' }); - await savedObjectsRepository.delete('globaltype', 'logstash-*', { - namespace: 'foo-namespace', - }); + describe('errors', () => { + const bulkGetErrorInvalidType = async ([obj1, obj, obj2]) => { + const response = getMockMgetResponse([obj1, obj2]); + callAdminCluster.mockResolvedValue(response); + const result = await bulkGet([obj1, obj, obj2]); + expectClusterCalls('mget'); + expect(result).toEqual({ + saved_objects: [expectSuccess(obj1), expectErrorInvalidType(obj), expectSuccess(obj2)], + }); + }; - expect(callAdminCluster).toHaveBeenCalledTimes(1); - expect(callAdminCluster).toHaveBeenCalledWith('delete', { - id: 'globaltype:logstash-*', - refresh: 'wait_for', - index: '.kibana-test', - ignore: [404], - }); - }); + const bulkGetErrorNotFound = async ([obj1, obj, obj2], options, response) => { + callAdminCluster.mockResolvedValue(response); + const result = await bulkGet([obj1, obj, obj2], options); + expectClusterCalls('mget'); + expect(result).toEqual({ + saved_objects: [expectSuccess(obj1), expectErrorNotFound(obj), expectSuccess(obj2)], + }); + }; - it('defaults to a refresh setting of `wait_for`', async () => { - callAdminCluster.mockReturnValue({ result: 'deleted' }); - await savedObjectsRepository.delete('globaltype', 'logstash-*'); + it(`returns error when type is invalid`, async () => { + const obj = { type: 'unknownType', id: 'three' }; + await bulkGetErrorInvalidType([obj1, obj, obj2]); + }); - expect(callAdminCluster).toHaveBeenCalledTimes(1); - expect(callAdminCluster.mock.calls[0][1]).toMatchObject({ - refresh: 'wait_for', + it(`returns error when type is hidden`, async () => { + const obj = { type: HIDDEN_TYPE, id: 'three' }; + await bulkGetErrorInvalidType([obj1, obj, obj2]); }); - }); - it(`accepts a custom refresh setting`, async () => { - callAdminCluster.mockReturnValue({ result: 'deleted' }); - await savedObjectsRepository.delete('globaltype', 'logstash-*', { - refresh: false, + it(`returns error when document is not found`, async () => { + const obj = { type: 'dashboard', id: 'three', found: false }; + const response = getMockMgetResponse([obj1, obj, obj2]); + await bulkGetErrorNotFound([obj1, obj, obj2], undefined, response); }); - expect(callAdminCluster).toHaveBeenCalledTimes(1); - expect(callAdminCluster.mock.calls[0][1]).toMatchObject({ - refresh: false, + it(`handles missing ids gracefully`, async () => { + const obj = { type: 'dashboard', id: undefined, found: false }; + const response = getMockMgetResponse([obj1, obj, obj2]); + await bulkGetErrorNotFound([obj1, obj, obj2], undefined, response); }); - }); - }); - describe('#deleteByNamespace', () => { - it('requires namespace to be defined', async () => { - callAdminCluster.mockReturnValue(deleteByQueryResults); - expect(savedObjectsRepository.deleteByNamespace()).rejects.toThrowErrorMatchingSnapshot(); - expect(callAdminCluster).not.toHaveBeenCalled(); + it(`returns error when type is multi-namespace and the document exists, but not in this namespace`, async () => { + const obj = { type: MULTI_NAMESPACE_TYPE, id: 'three' }; + const response = getMockMgetResponse([obj1, obj, obj2]); + response.docs[1].namespaces = ['bar-namespace']; + await bulkGetErrorNotFound([obj1, obj, obj2], { namespace }, response); + }); }); - it('requires namespace to be a string', async () => { - callAdminCluster.mockReturnValue(deleteByQueryResults); - expect( - savedObjectsRepository.deleteByNamespace(['namespace-1', 'namespace-2']) - ).rejects.toThrowErrorMatchingSnapshot(); - expect(callAdminCluster).not.toHaveBeenCalled(); + describe('migration', () => { + it(`waits until migrations are complete before proceeding`, async () => { + migrator.runMigrations = jest.fn(async () => + expect(callAdminCluster).not.toHaveBeenCalled() + ); + await expect(bulkGetSuccess([obj1, obj2])).resolves.toBeDefined(); + expect(migrator.runMigrations).toHaveBeenCalledTimes(1); + }); }); - it('constructs a deleteByQuery call using all types that are namespace aware', async () => { - callAdminCluster.mockReturnValue(deleteByQueryResults); - const result = await savedObjectsRepository.deleteByNamespace('my-namespace'); - - expect(result).toEqual(deleteByQueryResults); - expect(callAdminCluster).toHaveBeenCalledTimes(1); - - expect(getSearchDslNS.getSearchDsl).toHaveBeenCalledWith(mappings, typeRegistry, { - namespace: 'my-namespace', - type: ['config', 'baz', 'index-pattern', 'dashboard'], + describe('returns', () => { + const expectSuccessResult = ({ type, id }, doc) => ({ + type, + id, + ...(doc._source.namespaces && { namespaces: doc._source.namespaces }), + ...(doc._source.updated_at && { updated_at: doc._source.updated_at }), + version: encodeHitVersion(doc), + attributes: doc._source[type], + references: doc._source.references || [], + migrationVersion: doc._source.migrationVersion, }); - expect(callAdminCluster).toHaveBeenCalledWith('deleteByQuery', { - body: { conflicts: 'proceed' }, - ignore: [404], - index: ['.kibana-test', 'beats'], - refresh: 'wait_for', + it(`returns early for empty objects argument`, async () => { + const result = await bulkGet([]); + expect(result).toEqual({ saved_objects: [] }); + expect(callAdminCluster).not.toHaveBeenCalled(); }); - }); - - it('defaults to a refresh setting of `wait_for`', async () => { - callAdminCluster.mockReturnValue(deleteByQueryResults); - await savedObjectsRepository.deleteByNamespace('my-namespace'); - expect(callAdminCluster).toHaveBeenCalledTimes(1); - expect(callAdminCluster.mock.calls[0][1]).toMatchObject({ - refresh: 'wait_for', + it(`formats the ES response`, async () => { + const response = getMockMgetResponse([obj1, obj2]); + callAdminCluster.mockResolvedValue(response); + const result = await bulkGet([obj1, obj2]); + expect(callAdminCluster).toHaveBeenCalledTimes(1); + expect(result).toEqual({ + saved_objects: [ + expectSuccessResult(obj1, response.docs[0]), + expectSuccessResult(obj2, response.docs[1]), + ], + }); }); - }); - it('accepts a custom refresh setting', async () => { - callAdminCluster.mockReturnValue(deleteByQueryResults); - await savedObjectsRepository.deleteByNamespace('my-namespace', { refresh: true }); + it(`handles a mix of successful gets and errors`, async () => { + const response = getMockMgetResponse([obj1, obj2]); + callAdminCluster.mockResolvedValue(response); + const obj = { type: 'unknownType', id: 'three' }; + const result = await bulkGet([obj1, obj, obj2]); + expect(callAdminCluster).toHaveBeenCalledTimes(1); + expect(result).toEqual({ + saved_objects: [ + expectSuccessResult(obj1, response.docs[0]), + expectError(obj), + expectSuccessResult(obj2, response.docs[1]), + ], + }); + }); - expect(callAdminCluster).toHaveBeenCalledTimes(1); - expect(callAdminCluster.mock.calls[0][1]).toMatchObject({ - refresh: true, + it(`includes namespaces property for multi-namespace documents`, async () => { + const obj = { type: MULTI_NAMESPACE_TYPE, id: 'three' }; + const result = await bulkGetSuccess([obj1, obj]); + expect(result).toEqual({ + saved_objects: [ + expect.not.objectContaining({ namespaces: expect.anything() }), + expect.objectContaining({ namespaces: expect.any(Array) }), + ], + }); }); }); }); - describe('#find', () => { - it('waits until migrations are complete before proceeding', async () => { - migrator.runMigrations = jest.fn(async () => expect(callAdminCluster).not.toHaveBeenCalled()); - - callAdminCluster.mockReturnValue(noNamespaceSearchResults); - await expect(savedObjectsRepository.find({ type: 'foo' })).resolves.toBeDefined(); - - expect(migrator.runMigrations).toHaveBeenCalledTimes(1); - }); - - it('requires type to be defined', async () => { - await expect(savedObjectsRepository.find({})).rejects.toThrow(/options\.type must be/); - expect(callAdminCluster).not.toHaveBeenCalled(); + describe('#bulkUpdate', () => { + const obj1 = { + type: 'config', + id: '6.0.0-alpha1', + attributes: { title: 'Test One' }, + }; + const obj2 = { + type: 'index-pattern', + id: 'logstash-*', + attributes: { title: 'Test Two' }, + }; + const references = [{ name: 'ref_0', type: 'test', id: '1' }]; + const namespace = 'foo-namespace'; + + const getMockBulkUpdateResponse = (objects, options) => ({ + items: objects.map(({ type, id }) => ({ + update: { + _id: `${ + registry.isSingleNamespace(type) && options?.namespace ? `${options?.namespace}:` : '' + }${type}:${id}`, + ...mockVersionProps, + result: 'updated', + }, + })), }); - it('requires searchFields be an array if defined', async () => { - callAdminCluster.mockReturnValue(noNamespaceSearchResults); - try { - await savedObjectsRepository.find({ type: 'foo', searchFields: 'string' }); - throw new Error('expected find() to reject'); - } catch (error) { - expect(callAdminCluster).not.toHaveBeenCalled(); - expect(error.message).toMatch('must be an array'); + const bulkUpdateSuccess = async (objects, options) => { + const multiNamespaceObjects = objects.filter(({ type }) => registry.isMultiNamespace(type)); + if (multiNamespaceObjects?.length) { + const response = getMockMgetResponse(multiNamespaceObjects, options?.namespace); + callAdminCluster.mockResolvedValueOnce(response); // this._callCluster('mget', ...) } - }); + const response = getMockBulkUpdateResponse(objects, options?.namespace); + callAdminCluster.mockResolvedValue(response); // this._writeToCluster('bulk', ...) + const result = await savedObjectsRepository.bulkUpdate(objects, options); + expect(callAdminCluster).toHaveBeenCalledTimes(multiNamespaceObjects?.length ? 2 : 1); + return result; + }; - it('requires fields be an array if defined', async () => { - callAdminCluster.mockReturnValue(noNamespaceSearchResults); - try { - await savedObjectsRepository.find({ type: 'foo', fields: 'string' }); - throw new Error('expected find() to reject'); - } catch (error) { - expect(callAdminCluster).not.toHaveBeenCalled(); - expect(error.message).toMatch('must be an array'); + // bulk create calls have two objects for each source -- the action, and the source + const expectClusterCallArgsAction = ( + objects, + { method, _index = expect.any(String), getId = () => expect.any(String), overrides }, + n + ) => { + const body = []; + for (const { type, id } of objects) { + body.push({ + [method]: { + _index, + _id: getId(type, id), + ...overrides, + }, + }); + body.push(expect.any(Object)); } - }); - - it('passes mappings, schema, search, defaultSearchOperator, searchFields, type, sortField, sortOrder and hasReference to getSearchDsl', async () => { - callAdminCluster.mockReturnValue(namespacedSearchResults); - const relevantOpts = { - namespace: 'foo-namespace', - search: 'foo*', - searchFields: ['foo'], - type: ['bar'], - sortField: 'name', - sortOrder: 'desc', - defaultSearchOperator: 'AND', - hasReference: { - type: 'foo', - id: '1', - }, - kueryNode: undefined, - }; + expectClusterCallArgs({ body }, n); + }; - await savedObjectsRepository.find(relevantOpts); - expect(getSearchDslNS.getSearchDsl).toHaveBeenCalledTimes(1); - expect(getSearchDslNS.getSearchDsl).toHaveBeenCalledWith( - mappings, - typeRegistry, - relevantOpts - ); - }); + const expectObjArgs = ({ type, attributes }) => [ + expect.any(Object), + { + doc: expect.objectContaining({ + [type]: attributes, + ...mockTimestampFields, + }), + }, + ]; - it('accepts KQL filter and passes keuryNode to getSearchDsl', async () => { - callAdminCluster.mockReturnValue(namespacedSearchResults); - const findOpts = { - namespace: 'foo-namespace', - search: 'foo*', - searchFields: ['foo'], - type: ['dashboard'], - sortField: 'name', - sortOrder: 'desc', - defaultSearchOperator: 'AND', - hasReference: { - type: 'foo', - id: '1', - }, - indexPattern: undefined, - filter: 'dashboard.attributes.otherField: *', + describe('cluster calls', () => { + it(`should use the ES bulk action by default`, async () => { + await bulkUpdateSuccess([obj1, obj2]); + expectClusterCalls('bulk'); + }); + + it(`should use the ES mget action before bulk action for any types that are multi-namespace`, async () => { + const objects = [obj1, { ...obj2, type: MULTI_NAMESPACE_TYPE }]; + await bulkUpdateSuccess(objects); + expectClusterCalls('mget', 'bulk'); + const docs = [expect.objectContaining({ _id: `${MULTI_NAMESPACE_TYPE}:${obj2.id}` })]; + expectClusterCallArgs({ body: { docs } }, 1); + }); + + it(`formats the ES request`, async () => { + await bulkUpdateSuccess([obj1, obj2]); + const body = [...expectObjArgs(obj1), ...expectObjArgs(obj2)]; + expectClusterCallArgs({ body }); + }); + + it(`formats the ES request for any types that are multi-namespace`, async () => { + const _obj2 = { ...obj2, type: MULTI_NAMESPACE_TYPE }; + await bulkUpdateSuccess([obj1, _obj2]); + const body = [...expectObjArgs(obj1), ...expectObjArgs(_obj2)]; + expectClusterCallArgs({ body }, 2); + }); + + it(`doesnt call Elasticsearch if there are no valid objects to update`, async () => { + const objects = [obj1, obj2].map(x => ({ ...x, type: 'unknownType' })); + await savedObjectsRepository.bulkUpdate(objects); + expect(callAdminCluster).toHaveBeenCalledTimes(0); + }); + + it(`defaults to no references`, async () => { + await bulkUpdateSuccess([obj1, obj2]); + const expected = { doc: expect.not.objectContaining({ references: expect.anything() }) }; + const body = [expect.any(Object), expected, expect.any(Object), expected]; + expectClusterCallArgs({ body }); + }); + + it(`accepts custom references array`, async () => { + const test = async references => { + const objects = [obj1, obj2].map(obj => ({ ...obj, references })); + await bulkUpdateSuccess(objects); + const expected = { doc: expect.objectContaining({ references }) }; + const body = [expect.any(Object), expected, expect.any(Object), expected]; + expectClusterCallArgs({ body }); + callAdminCluster.mockReset(); + }; + await test(references); + await test(['string']); + await test([]); + }); + + it(`doesn't accept custom references if not an array`, async () => { + const test = async references => { + const objects = [obj1, obj2].map(obj => ({ ...obj, references })); + await bulkUpdateSuccess(objects); + const expected = { doc: expect.not.objectContaining({ references: expect.anything() }) }; + const body = [expect.any(Object), expected, expect.any(Object), expected]; + expectClusterCallArgs({ body }); + callAdminCluster.mockReset(); + }; + await test('string'); + await test(123); + await test(true); + await test(null); + }); + + it(`defaults to a refresh setting of wait_for`, async () => { + await bulkUpdateSuccess([obj1, obj2]); + expectClusterCallArgs({ refresh: 'wait_for' }); + }); + + it(`accepts a custom refresh setting`, async () => { + const refresh = 'foo'; + await bulkUpdateSuccess([obj1, obj2], { refresh }); + expectClusterCallArgs({ refresh }); + }); + + it(`defaults to the version of the existing document for multi-namespace types`, async () => { + // only multi-namespace documents are obtained using a pre-flight mget request + const objects = [ + { ...obj1, type: MULTI_NAMESPACE_TYPE }, + { ...obj2, type: MULTI_NAMESPACE_TYPE }, + ]; + await bulkUpdateSuccess(objects); + const overrides = { + if_seq_no: mockVersionProps._seq_no, + if_primary_term: mockVersionProps._primary_term, + }; + expectClusterCallArgsAction(objects, { method: 'update', overrides }, 2); + }); + + it(`defaults to no version for types that are not multi-namespace`, async () => { + const objects = [obj1, { ...obj2, type: NAMESPACE_AGNOSTIC_TYPE }]; + await bulkUpdateSuccess(objects); + expectClusterCallArgsAction(objects, { method: 'update' }); + }); + + it(`accepts version`, async () => { + const version = encodeHitVersion({ _seq_no: 100, _primary_term: 200 }); + // test with both non-multi-namespace and multi-namespace types + const objects = [ + { ...obj1, version }, + { ...obj2, type: MULTI_NAMESPACE_TYPE, version }, + ]; + await bulkUpdateSuccess(objects); + const overrides = { if_seq_no: 100, if_primary_term: 200 }; + expectClusterCallArgsAction(objects, { method: 'update', overrides }, 2); + }); + + it(`prepends namespace to the id when providing namespace for single-namespace type`, async () => { + const getId = (type, id) => `${namespace}:${type}:${id}`; + await bulkUpdateSuccess([obj1, obj2], { namespace }); + expectClusterCallArgsAction([obj1, obj2], { method: 'update', getId }); + }); + + it(`doesn't prepend namespace to the id when providing no namespace for single-namespace type`, async () => { + const getId = (type, id) => `${type}:${id}`; + await bulkUpdateSuccess([obj1, obj2]); + expectClusterCallArgsAction([obj1, obj2], { method: 'update', getId }); + }); + + it(`doesn't prepend namespace to the id when not using single-namespace type`, async () => { + const getId = (type, id) => `${type}:${id}`; + const objects1 = [{ ...obj1, type: NAMESPACE_AGNOSTIC_TYPE }]; + await bulkUpdateSuccess(objects1, { namespace }); + expectClusterCallArgsAction(objects1, { method: 'update', getId }); + callAdminCluster.mockReset(); + const overrides = { + // bulkUpdate uses a preflight `get` request for multi-namespace saved objects, and specifies that version on `update` + // we aren't testing for this here, but we need to include Jest assertions so this test doesn't fail + if_primary_term: expect.any(Number), + if_seq_no: expect.any(Number), + }; + const objects2 = [{ ...obj2, type: MULTI_NAMESPACE_TYPE }]; + await bulkUpdateSuccess(objects2, { namespace }); + expectClusterCallArgsAction(objects2, { method: 'update', getId, overrides }, 2); + }); + }); + + describe('errors', () => { + const obj = { + type: 'dashboard', + id: 'three', }; - await savedObjectsRepository.find(findOpts); - expect(getSearchDslNS.getSearchDsl).toHaveBeenCalledTimes(1); - const { kueryNode } = getSearchDslNS.getSearchDsl.mock.calls[0][2]; - expect(kueryNode).toMatchInlineSnapshot(` - Object { - "arguments": Array [ - Object { - "type": "literal", - "value": "dashboard.otherField", - }, - Object { - "type": "wildcard", - "value": "@kuery-wildcard@", - }, - Object { - "type": "literal", - "value": false, - }, - ], - "function": "is", - "type": "function", + const bulkUpdateError = async (obj, esError, expectedError) => { + const objects = [obj1, obj, obj2]; + const mockResponse = getMockBulkUpdateResponse(objects); + if (esError) { + mockResponse.items[1].update = { error: esError }; } - `); - }); + callAdminCluster.mockResolvedValue(mockResponse); // this._writeToCluster('bulk', ...) + + const result = await savedObjectsRepository.bulkUpdate(objects); + expectClusterCalls('bulk'); + const objCall = esError ? expectObjArgs(obj) : []; + const body = [...expectObjArgs(obj1), ...objCall, ...expectObjArgs(obj2)]; + expectClusterCallArgs({ body }); + expect(result).toEqual({ + saved_objects: [expectSuccess(obj1), expectedError, expectSuccess(obj2)], + }); + }; - it('KQL filter syntax errors rejects with bad request', async () => { - callAdminCluster.mockReturnValue(namespacedSearchResults); - const findOpts = { - namespace: 'foo-namespace', - search: 'foo*', - searchFields: ['foo'], - type: ['dashboard'], - sortField: 'name', - sortOrder: 'desc', - defaultSearchOperator: 'AND', - hasReference: { - type: 'foo', - id: '1', - }, - indexPattern: undefined, - filter: 'dashboard.attributes.otherField:<', + const bulkUpdateMultiError = async ([obj1, _obj, obj2], options, mgetResponse) => { + callAdminCluster.mockResolvedValueOnce(mgetResponse); // this._callCluster('mget', ...) + const bulkResponse = getMockBulkUpdateResponse([obj1, obj2], namespace); + callAdminCluster.mockResolvedValue(bulkResponse); // this._writeToCluster('bulk', ...) + + const result = await savedObjectsRepository.bulkUpdate([obj1, _obj, obj2], options); + expectClusterCalls('mget', 'bulk'); + const body = [...expectObjArgs(obj1), ...expectObjArgs(obj2)]; + expectClusterCallArgs({ body }, 2); + expect(result).toEqual({ + saved_objects: [expectSuccess(obj1), expectErrorNotFound(_obj), expectSuccess(obj2)], + }); }; - await expect(savedObjectsRepository.find(findOpts)).rejects.toMatchInlineSnapshot(` - [Error: KQLSyntaxError: Expected "(", "{", value, whitespace but "<" found. - dashboard.attributes.otherField:< - --------------------------------^: Bad Request] - `); - expect(getSearchDslNS.getSearchDsl).toHaveBeenCalledTimes(0); - }); + it(`returns error when type is invalid`, async () => { + const _obj = { ...obj, type: 'unknownType' }; + await bulkUpdateError(_obj, undefined, expectErrorNotFound(_obj)); + }); - it('merges output of getSearchDsl into es request body', async () => { - callAdminCluster.mockReturnValue(noNamespaceSearchResults); - getSearchDslNS.getSearchDsl.mockReturnValue({ query: 1, aggregations: 2 }); - await savedObjectsRepository.find({ type: 'foo' }); - expect(callAdminCluster).toHaveBeenCalledTimes(1); - expect(callAdminCluster).toHaveBeenCalledWith( - 'search', - expect.objectContaining({ - body: expect.objectContaining({ - query: 1, - aggregations: 2, - }), - }) - ); - }); + it(`returns error when type is hidden`, async () => { + const _obj = { ...obj, type: HIDDEN_TYPE }; + await bulkUpdateError(_obj, undefined, expectErrorNotFound(_obj)); + }); - it('formats Elasticsearch response when there is no namespace', async () => { - callAdminCluster.mockReturnValue(noNamespaceSearchResults); - const count = noNamespaceSearchResults.hits.hits.length; + it(`returns error when ES is unable to find the document (mget)`, async () => { + const _obj = { ...obj, type: MULTI_NAMESPACE_TYPE, found: false }; + const mgetResponse = getMockMgetResponse([_obj]); + await bulkUpdateMultiError([obj1, _obj, obj2], undefined, mgetResponse); + }); - const response = await savedObjectsRepository.find({ type: 'foo' }); + it(`returns error when ES is unable to find the index (mget)`, async () => { + const _obj = { ...obj, type: MULTI_NAMESPACE_TYPE }; + const mgetResponse = { status: 404 }; + await bulkUpdateMultiError([obj1, _obj, obj2], { namespace }, mgetResponse); + }); - expect(response.total).toBe(count); - expect(response.saved_objects).toHaveLength(count); + it(`returns error when there is a conflict with an existing multi-namespace saved object (mget)`, async () => { + const _obj = { ...obj, type: MULTI_NAMESPACE_TYPE }; + const mgetResponse = getMockMgetResponse([_obj], 'bar-namespace'); + await bulkUpdateMultiError([obj1, _obj, obj2], { namespace }, mgetResponse); + }); - noNamespaceSearchResults.hits.hits.forEach((doc, i) => { - expect(response.saved_objects[i]).toEqual({ - id: doc._id.replace(/(index-pattern|config|globaltype)\:/, ''), - type: doc._source.type, - ...mockTimestampFields, - version: mockVersion, - attributes: doc._source[doc._source.type], - references: [], - }); + it(`returns error when there is a version conflict (bulk)`, async () => { + const esError = { type: 'version_conflict_engine_exception' }; + await bulkUpdateError(obj, esError, expectErrorConflict(obj)); }); - }); - it('formats Elasticsearch response when there is a namespace', async () => { - callAdminCluster.mockReturnValue(namespacedSearchResults); - const count = namespacedSearchResults.hits.hits.length; + it(`returns error when document is missing (bulk)`, async () => { + const esError = { type: 'document_missing_exception' }; + await bulkUpdateError(obj, esError, expectErrorNotFound(obj)); + }); - const response = await savedObjectsRepository.find({ - type: 'foo', - namespace: 'foo-namespace', + it(`returns error reason for other errors (bulk)`, async () => { + const esError = { reason: 'some_other_error' }; + await bulkUpdateError(obj, esError, expectErrorResult(obj, { message: esError.reason })); }); - expect(response.total).toBe(count); - expect(response.saved_objects).toHaveLength(count); + it(`returns error string for other errors if no reason is defined (bulk)`, async () => { + const esError = { foo: 'some_other_error' }; + const expectedError = expectErrorResult(obj, { message: JSON.stringify(esError) }); + await bulkUpdateError(obj, esError, expectedError); + }); + }); - namespacedSearchResults.hits.hits.forEach((doc, i) => { - expect(response.saved_objects[i]).toEqual({ - id: doc._id.replace(/(foo-namespace\:)?(index-pattern|config|globaltype)\:/, ''), - type: doc._source.type, - ...mockTimestampFields, - version: mockVersion, - attributes: doc._source[doc._source.type], - references: [], - }); + describe('migration', () => { + it(`waits until migrations are complete before proceeding`, async () => { + migrator.runMigrations = jest.fn(async () => + expect(callAdminCluster).not.toHaveBeenCalled() + ); + await expect(bulkUpdateSuccess([obj1, obj2])).resolves.toBeDefined(); + expect(migrator.runMigrations).toHaveReturnedTimes(1); }); }); - it('accepts per_page/page', async () => { - callAdminCluster.mockReturnValue(noNamespaceSearchResults); - await savedObjectsRepository.find({ type: 'foo', perPage: 10, page: 6 }); + describe('returns', () => { + const expectSuccessResult = ({ type, id, attributes, references }) => ({ + type, + id, + attributes, + references, + version: mockVersion, + ...mockTimestampFields, + }); - expect(callAdminCluster).toHaveBeenCalledTimes(1); - expect(callAdminCluster).toHaveBeenCalledWith( - expect.any(String), - expect.objectContaining({ - size: 10, - from: 50, - }) - ); - }); + it(`formats the ES response`, async () => { + const response = await bulkUpdateSuccess([obj1, obj2]); + expect(response).toEqual({ + saved_objects: [obj1, obj2].map(expectSuccessResult), + }); + }); - it('can filter by fields', async () => { - callAdminCluster.mockReturnValue(noNamespaceSearchResults); - await savedObjectsRepository.find({ type: 'foo', fields: ['title'] }); + it(`includes references`, async () => { + const objects = [obj1, obj2].map(obj => ({ ...obj, references })); + const response = await bulkUpdateSuccess(objects); + expect(response).toEqual({ + saved_objects: objects.map(expectSuccessResult), + }); + }); - expect(callAdminCluster).toHaveBeenCalledTimes(1); - expect(callAdminCluster).toHaveBeenCalledWith( - expect.any(String), - expect.objectContaining({ - _source: [ - 'foo.title', - 'namespace', - 'type', - 'references', - 'migrationVersion', - 'updated_at', - 'title', + it(`handles a mix of successful updates and errors`, async () => { + const obj = { + type: 'unknownType', + id: 'three', + }; + const objects = [obj1, obj, obj2]; + const mockResponse = getMockBulkUpdateResponse(objects); + callAdminCluster.mockResolvedValue(mockResponse); // this._writeToCluster('bulk', ...) + const result = await savedObjectsRepository.bulkUpdate(objects); + expect(callAdminCluster).toHaveBeenCalledTimes(1); + expect(result).toEqual({ + saved_objects: [expectSuccessResult(obj1), expectError(obj), expectSuccessResult(obj2)], + }); + }); + + it(`includes namespaces property for multi-namespace documents`, async () => { + const obj = { type: MULTI_NAMESPACE_TYPE, id: 'three' }; + const result = await bulkUpdateSuccess([obj1, obj]); + expect(result).toEqual({ + saved_objects: [ + expect.not.objectContaining({ namespaces: expect.anything() }), + expect.objectContaining({ namespaces: expect.any(Array) }), ], - }) - ); + }); + }); }); + }); - it('should set rest_total_hits_as_int to true on a request', async () => { - callAdminCluster.mockReturnValue(noNamespaceSearchResults); - await savedObjectsRepository.find({ type: 'foo' }); - expect(callAdminCluster).toHaveBeenCalledTimes(1); - expect(callAdminCluster.mock.calls[0][1]).toHaveProperty('rest_total_hits_as_int', true); + describe('#create', () => { + beforeEach(() => { + callAdminCluster.mockImplementation((method, params) => ({ + _id: params.id, + ...mockVersionProps, + })); }); - }); - describe('#get', () => { - const noNamespaceResult = { - _id: 'index-pattern:logstash-*', - ...mockVersionProps, - _source: { - type: 'index-pattern', - specialProperty: 'specialValue', - ...mockTimestampFields, - 'index-pattern': { - title: 'Testing', - }, - }, - }; - const namespacedResult = { - _id: 'foo-namespace:index-pattern:logstash-*', - ...mockVersionProps, - _source: { - namespace: 'foo-namespace', - type: 'index-pattern', - specialProperty: 'specialValue', - ...mockTimestampFields, - 'index-pattern': { - title: 'Testing', - }, + const type = 'index-pattern'; + const attributes = { title: 'Logstash' }; + const id = 'logstash-*'; + const namespace = 'foo-namespace'; + const references = [ + { + name: 'ref_0', + type: 'test', + id: '123', }, - }; + ]; - it('waits until migrations are complete before proceeding', async () => { - migrator.runMigrations = jest.fn(async () => expect(callAdminCluster).not.toHaveBeenCalled()); + const createSuccess = async (type, attributes, options) => { + const result = await savedObjectsRepository.create(type, attributes, options); + expect(callAdminCluster).toHaveBeenCalledTimes( + registry.isMultiNamespace(type) && options.overwrite ? 2 : 1 + ); + return result; + }; - callAdminCluster.mockResolvedValue(noNamespaceResult); - await expect( - savedObjectsRepository.get('index-pattern', 'logstash-*') - ).resolves.toBeDefined(); + describe('cluster calls', () => { + it(`should use the ES create action if ID is undefined and overwrite=true`, async () => { + await createSuccess(type, attributes, { overwrite: true }); + expectClusterCalls('create'); + }); - expect(migrator.runMigrations).toHaveBeenCalledTimes(1); - }); + it(`should use the ES create action if ID is undefined and overwrite=false`, async () => { + await createSuccess(type, attributes); + expectClusterCalls('create'); + }); - it('formats Elasticsearch response when there is no namespace', async () => { - callAdminCluster.mockResolvedValue(noNamespaceResult); - const response = await savedObjectsRepository.get('index-pattern', 'logstash-*'); - expect(response).toEqual({ - id: 'logstash-*', - type: 'index-pattern', - updated_at: mockTimestamp, - version: mockVersion, - attributes: { - title: 'Testing', - }, - references: [], + it(`should use the ES index action if ID is defined and overwrite=true`, async () => { + await createSuccess(type, attributes, { id, overwrite: true }); + expectClusterCalls('index'); }); - }); - it('formats Elasticsearch response when there are namespaces', async () => { - callAdminCluster.mockResolvedValue(namespacedResult); - const response = await savedObjectsRepository.get('index-pattern', 'logstash-*'); - expect(response).toEqual({ - id: 'logstash-*', - type: 'index-pattern', - updated_at: mockTimestamp, - version: mockVersion, - attributes: { - title: 'Testing', - }, - references: [], + it(`should use the ES create action if ID is defined and overwrite=false`, async () => { + await createSuccess(type, attributes, { id }); + expectClusterCalls('create'); }); - }); - it('prepends namespace and type to the id when providing namespace for namespaced type', async () => { - callAdminCluster.mockResolvedValue(namespacedResult); - await savedObjectsRepository.get('index-pattern', 'logstash-*', { - namespace: 'foo-namespace', + it(`should use the ES get action then index action if type is multi-namespace, ID is defined, and overwrite=true`, async () => { + await createSuccess(MULTI_NAMESPACE_TYPE, attributes, { id, overwrite: true }); + expectClusterCalls('get', 'index'); }); - expect(callAdminCluster).toHaveBeenCalledTimes(1); - expect(callAdminCluster).toHaveBeenCalledWith( - expect.any(String), - expect.objectContaining({ - id: 'foo-namespace:index-pattern:logstash-*', - }) - ); - }); + it(`defaults to empty references array`, async () => { + await createSuccess(type, attributes, { id }); + expectClusterCallArgs({ + body: expect.objectContaining({ references: [] }), + }); + }); - it(`only prepends type to the id when providing no namespace for namespaced type`, async () => { - callAdminCluster.mockResolvedValue(noNamespaceResult); - await savedObjectsRepository.get('index-pattern', 'logstash-*'); + it(`accepts custom references array`, async () => { + const test = async references => { + await createSuccess(type, attributes, { id, references }); + expectClusterCallArgs({ + body: expect.objectContaining({ references }), + }); + callAdminCluster.mockReset(); + }; + await test(references); + await test(['string']); + await test([]); + }); - expect(callAdminCluster).toHaveBeenCalledTimes(1); - expect(callAdminCluster).toHaveBeenCalledWith( - expect.any(String), - expect.objectContaining({ - id: 'index-pattern:logstash-*', - }) - ); - }); + it(`doesn't accept custom references if not an array`, async () => { + const test = async references => { + await createSuccess(type, attributes, { id, references }); + expectClusterCallArgs({ + body: expect.not.objectContaining({ references: expect.anything() }), + }); + callAdminCluster.mockReset(); + }; + await test('string'); + await test(123); + await test(true); + await test(null); + }); - it(`doesn't prepend namespace to the id when providing namespace for namespace agnostic type`, async () => { - callAdminCluster.mockResolvedValue(namespacedResult); - await savedObjectsRepository.get('globaltype', 'logstash-*', { - namespace: 'foo-namespace', + it(`defaults to a refresh setting of wait_for`, async () => { + await createSuccess(type, attributes); + expectClusterCallArgs({ refresh: 'wait_for' }); }); - expect(callAdminCluster).toHaveBeenCalledTimes(1); - expect(callAdminCluster).toHaveBeenCalledWith( - expect.any(String), - expect.objectContaining({ - id: 'globaltype:logstash-*', - }) - ); - }); - }); + it(`accepts a custom refresh setting`, async () => { + const refresh = 'foo'; + await createSuccess(type, attributes, { refresh }); + expectClusterCallArgs({ refresh }); + }); - describe('#bulkGet', () => { - it('waits until migrations are complete before proceeding', async () => { - migrator.runMigrations = jest.fn(async () => expect(callAdminCluster).not.toHaveBeenCalled()); - - callAdminCluster.mockReturnValue({ docs: [] }); - await expect( - savedObjectsRepository.bulkGet([ - { id: 'one', type: 'config' }, - { id: 'two', type: 'index-pattern' }, - { id: 'three', type: 'globaltype' }, - ]) - ).resolves.toBeDefined(); - - expect(migrator.runMigrations).toHaveBeenCalledTimes(1); - }); + it(`should use default index`, async () => { + await createSuccess(type, attributes, { id }); + expectClusterCallArgs({ index: '.kibana-test' }); + }); - it('prepends type to id when getting objects when there is no namespace', async () => { - callAdminCluster.mockReturnValue({ docs: [] }); + it(`should use custom index`, async () => { + await createSuccess(CUSTOM_INDEX_TYPE, attributes, { id }); + expectClusterCallArgs({ index: 'custom' }); + }); - await savedObjectsRepository.bulkGet([ - { id: 'one', type: 'config' }, - { id: 'two', type: 'index-pattern' }, - { id: 'three', type: 'globaltype' }, - ]); + it(`self-generates an id if none is provided`, async () => { + await createSuccess(type, attributes); + expectClusterCallArgs({ + id: expect.objectContaining(/index-pattern:[0-9a-f]{8}-([0-9a-f]{4}-){3}[0-9a-f]{12}/), + }); + }); - expect(callAdminCluster).toHaveBeenCalledTimes(1); - expect(callAdminCluster).toHaveBeenCalledWith( - expect.any(String), - expect.objectContaining({ - body: { - docs: [ - { _id: 'config:one', _index: '.kibana-test' }, - { _id: 'index-pattern:two', _index: '.kibana-test' }, - { _id: 'globaltype:three', _index: '.kibana-test' }, - ], - }, - }) - ); - }); + it(`prepends namespace to the id when providing namespace for single-namespace type`, async () => { + await createSuccess(type, attributes, { id, namespace }); + expectClusterCallArgs({ id: `${namespace}:${type}:${id}` }); + }); - it('prepends namespace and type appropriately to id when getting objects when there is a namespace', async () => { - callAdminCluster.mockReturnValue({ docs: [] }); + it(`doesn't prepend namespace to the id when providing no namespace for single-namespace type`, async () => { + await createSuccess(type, attributes, { id }); + expectClusterCallArgs({ id: `${type}:${id}` }); + }); - await savedObjectsRepository.bulkGet( - [ - { id: 'one', type: 'config' }, - { id: 'two', type: 'index-pattern' }, - { id: 'three', type: 'globaltype' }, - ], - { - namespace: 'foo-namespace', - } - ); + it(`doesn't prepend namespace to the id when not using single-namespace type`, async () => { + await createSuccess(NAMESPACE_AGNOSTIC_TYPE, attributes, { id, namespace }); + expectClusterCallArgs({ id: `${NAMESPACE_AGNOSTIC_TYPE}:${id}` }); + callAdminCluster.mockReset(); - expect(callAdminCluster).toHaveBeenCalledTimes(1); - expect(callAdminCluster).toHaveBeenCalledWith( - expect.any(String), - expect.objectContaining({ - body: { - docs: [ - { _id: 'foo-namespace:config:one', _index: '.kibana-test' }, - { _id: 'foo-namespace:index-pattern:two', _index: '.kibana-test' }, - { _id: 'globaltype:three', _index: '.kibana-test' }, - ], - }, - }) - ); + await createSuccess(MULTI_NAMESPACE_TYPE, attributes, { id, namespace }); + expectClusterCallArgs({ id: `${MULTI_NAMESPACE_TYPE}:${id}` }); + }); }); - it('mockReturnValue early for empty objects argument', async () => { - callAdminCluster.mockReturnValue({ docs: [] }); + describe('errors', () => { + it(`throws when type is invalid`, async () => { + await expect(savedObjectsRepository.create('unknownType', attributes)).rejects.toThrowError( + createUnsupportedTypeError('unknownType') + ); + expect(callAdminCluster).not.toHaveBeenCalled(); + }); - const response = await savedObjectsRepository.bulkGet([]); + it(`throws when type is hidden`, async () => { + await expect(savedObjectsRepository.create(HIDDEN_TYPE, attributes)).rejects.toThrowError( + createUnsupportedTypeError(HIDDEN_TYPE) + ); + expect(callAdminCluster).not.toHaveBeenCalled(); + }); - expect(response.saved_objects).toHaveLength(0); - expect(callAdminCluster).not.toHaveBeenCalled(); + it(`throws when there is a conflict with an existing multi-namespace saved object (get)`, async () => { + const response = getMockGetResponse({ + type: MULTI_NAMESPACE_TYPE, + id, + namespace: 'bar-namespace', + }); + callAdminCluster.mockResolvedValue(response); // this._callCluster('get', ...) + await expect( + savedObjectsRepository.create(MULTI_NAMESPACE_TYPE, attributes, { + id, + overwrite: true, + namespace, + }) + ).rejects.toThrowError(createConflictError(MULTI_NAMESPACE_TYPE, id)); + expectClusterCalls('get'); + }); + + it(`throws when automatic index creation fails`, async () => { + // TODO + }); + + it(`throws when an unexpected failure occurs`, async () => { + // TODO + }); }); - it('handles missing ids gracefully', async () => { - callAdminCluster.mockResolvedValue({ - docs: [ - { - _id: 'config:good', - found: true, - ...mockVersionProps, - _source: { ...mockTimestampFields, config: { title: 'Test' } }, - }, - { - _id: 'config:bad', - found: false, - }, - ], + describe('migration', () => { + beforeEach(() => { + migrator.migrateDocument.mockImplementation(mockMigrateDocument); }); - const { saved_objects: savedObjects } = await savedObjectsRepository.bulkGet([ - { id: 'good', type: 'config' }, - { type: 'config' }, - ]); + it(`waits until migrations are complete before proceeding`, async () => { + migrator.runMigrations = jest.fn(async () => + expect(callAdminCluster).not.toHaveBeenCalled() + ); + await expect(createSuccess(type, attributes, { id, namespace })).resolves.toBeDefined(); + expect(migrator.runMigrations).toHaveBeenCalledTimes(1); + }); - expect(savedObjects[1]).toEqual({ - type: 'config', - error: { statusCode: 404, message: 'Not found' }, + it(`migrates a document and serializes the migrated doc`, async () => { + const migrationVersion = mockMigrationVersion; + await createSuccess(type, attributes, { id, references, migrationVersion }); + const doc = { type, id, attributes, references, migrationVersion, ...mockTimestampFields }; + expectMigrationArgs(doc); + + const migratedDoc = migrator.migrateDocument(doc); + expect(serializer.savedObjectToRaw).toHaveBeenLastCalledWith(migratedDoc); }); - }); - it('reports error on missed objects', async () => { - callAdminCluster.mockResolvedValue({ - docs: [ - { - _id: 'config:good', - found: true, - ...mockVersionProps, - _source: { ...mockTimestampFields, config: { title: 'Test' } }, - }, - { - _id: 'config:bad', - found: false, - }, - ], + it(`adds namespace to body when providing namespace for single-namespace type`, async () => { + await createSuccess(type, attributes, { id, namespace }); + expectMigrationArgs({ namespace }); }); - const { saved_objects: savedObjects } = await savedObjectsRepository.bulkGet([ - { id: 'good', type: 'config' }, - { id: 'bad', type: 'config' }, - ]); + it(`doesn't add namespace to body when providing no namespace for single-namespace type`, async () => { + await createSuccess(type, attributes, { id }); + expectMigrationArgs({ namespace: expect.anything() }, false); + }); - expect(callAdminCluster).toHaveBeenCalledTimes(1); + it(`doesn't add namespace to body when not using single-namespace type`, async () => { + await createSuccess(NAMESPACE_AGNOSTIC_TYPE, attributes, { id, namespace }); + expectMigrationArgs({ namespace: expect.anything() }, false, 1); - expect(savedObjects).toHaveLength(2); - expect(savedObjects[0]).toEqual({ - id: 'good', - type: 'config', - ...mockTimestampFields, - version: mockVersion, - attributes: { title: 'Test' }, - references: [], + callAdminCluster.mockReset(); + await createSuccess(MULTI_NAMESPACE_TYPE, attributes, { id }); + expectMigrationArgs({ namespace: expect.anything() }, false, 2); }); - expect(savedObjects[1]).toEqual({ - id: 'bad', - type: 'config', - error: { statusCode: 404, message: 'Not found' }, + + it(`adds namespaces to body when providing namespace for multi-namespace type`, async () => { + await createSuccess(MULTI_NAMESPACE_TYPE, attributes, { id, namespace }); + expectMigrationArgs({ namespaces: [namespace] }); }); - }); - it('returns errors when requesting unsupported types', async () => { - callAdminCluster.mockResolvedValue({ - docs: [ - { - _id: 'one', - found: true, - ...mockVersionProps, - _source: { ...mockTimestampFields, config: { title: 'Test1' } }, - }, - { - _id: 'three', - found: true, - ...mockVersionProps, - _source: { ...mockTimestampFields, config: { title: 'Test3' } }, - }, - { - _id: 'five', - found: true, - ...mockVersionProps, - _source: { ...mockTimestampFields, config: { title: 'Test5' } }, - }, - ], + it(`adds default namespaces to body when providing no namespace for multi-namespace type`, async () => { + await createSuccess(MULTI_NAMESPACE_TYPE, attributes, { id }); + expectMigrationArgs({ namespaces: ['default'] }); }); - const { saved_objects: savedObjects } = await savedObjectsRepository.bulkGet([ - { id: 'one', type: 'config' }, - { id: 'two', type: 'invalidtype' }, - { id: 'three', type: 'config' }, - { id: 'four', type: 'invalidtype' }, - { id: 'five', type: 'config' }, - ]); + it(`doesn't add namespaces to body when not using multi-namespace type`, async () => { + await createSuccess(type, attributes, { id }); + expectMigrationArgs({ namespaces: expect.anything() }, false, 1); - expect(savedObjects).toEqual([ - { - attributes: { title: 'Test1' }, - id: 'one', - ...mockTimestampFields, - references: [], - type: 'config', - version: mockVersion, - migrationVersion: undefined, - }, - { - attributes: { title: 'Test3' }, - id: 'three', - ...mockTimestampFields, - references: [], - type: 'config', - version: mockVersion, - migrationVersion: undefined, - }, - { - attributes: { title: 'Test5' }, - id: 'five', + callAdminCluster.mockReset(); + await createSuccess(NAMESPACE_AGNOSTIC_TYPE, attributes, { id }); + expectMigrationArgs({ namespaces: expect.anything() }, false, 2); + }); + }); + + describe('returns', () => { + it(`formats the ES response`, async () => { + const result = await createSuccess(type, attributes, { id, namespace, references }); + expect(result).toEqual({ + type, + id, ...mockTimestampFields, - references: [], - type: 'config', version: mockVersion, - migrationVersion: undefined, - }, - { - error: { - error: 'Bad Request', - message: "Unsupported saved object type: 'invalidtype': Bad Request", - statusCode: 400, - }, - id: 'two', - type: 'invalidtype', - }, - { - error: { - error: 'Bad Request', - message: "Unsupported saved object type: 'invalidtype': Bad Request", - statusCode: 400, - }, - id: 'four', - type: 'invalidtype', - }, - ]); + attributes, + references, + }); + }); }); }); - describe('#update', () => { - const id = 'logstash-*'; + describe('#delete', () => { const type = 'index-pattern'; - const attributes = { title: 'Testing' }; + const id = 'logstash-*'; + const namespace = 'foo-namespace'; - beforeEach(() => { - callAdminCluster.mockResolvedValue({ - _id: `${type}:${id}`, - ...mockVersionProps, - result: 'updated', + const deleteSuccess = async (type, id, options) => { + if (registry.isMultiNamespace(type)) { + const mockGetResponse = getMockGetResponse({ type, id, namespace: options?.namespace }); + callAdminCluster.mockResolvedValueOnce(mockGetResponse); // this._callCluster('get', ...) + } + callAdminCluster.mockResolvedValue({ result: 'deleted' }); // this._writeToCluster('delete', ...) + const result = await savedObjectsRepository.delete(type, id, options); + expect(callAdminCluster).toHaveBeenCalledTimes(registry.isMultiNamespace(type) ? 2 : 1); + return result; + }; + + describe('cluster calls', () => { + it(`should use the ES delete action when not using a multi-namespace type`, async () => { + await deleteSuccess(type, id); + expectClusterCalls('delete'); }); - }); - it('waits until migrations are complete before proceeding', async () => { - migrator.runMigrations = jest.fn(async () => expect(callAdminCluster).not.toHaveBeenCalled()); + it(`should use ES get action then delete action when using a multi-namespace type with no namespaces remaining`, async () => { + await deleteSuccess(MULTI_NAMESPACE_TYPE, id); + expectClusterCalls('get', 'delete'); + }); - await expect( - savedObjectsRepository.update('index-pattern', 'logstash-*', attributes, { - namespace: 'foo-namespace', - }) - ).resolves.toBeDefined(); + it(`should use ES get action then update action when using a multi-namespace type with one or more namespaces remaining`, async () => { + const mockResponse = getMockGetResponse({ type: MULTI_NAMESPACE_TYPE, id }); + mockResponse._source.namespaces = ['default', 'some-other-nameespace']; + callAdminCluster + .mockResolvedValueOnce(mockResponse) // this._callCluster('get', ...) + .mockResolvedValue({ result: 'updated' }); // this._writeToCluster('update', ...) + await savedObjectsRepository.delete(MULTI_NAMESPACE_TYPE, id); + expectClusterCalls('get', 'update'); + }); - expect(migrator.runMigrations).toHaveReturnedTimes(1); - }); + it(`includes the version of the existing document when type is multi-namespace`, async () => { + await deleteSuccess(MULTI_NAMESPACE_TYPE, id); + const versionProperties = { + if_seq_no: mockVersionProps._seq_no, + if_primary_term: mockVersionProps._primary_term, + }; + expectClusterCallArgs(versionProperties, 2); + }); - it('mockReturnValue current ES document _seq_no and _primary_term encoded as version', async () => { - const response = await savedObjectsRepository.update( - 'index-pattern', - 'logstash-*', - attributes, - { - namespace: 'foo-namespace', - references: [ - { - name: 'ref_0', - type: 'test', - id: '1', - }, - ], - } - ); - expect(response).toEqual({ - id, - type, - ...mockTimestampFields, - version: mockVersion, - attributes, - references: [ - { - name: 'ref_0', - type: 'test', - id: '1', - }, - ], + it(`defaults to a refresh setting of wait_for`, async () => { + await deleteSuccess(type, id); + expectClusterCallArgs({ refresh: 'wait_for' }); }); - }); - it('accepts version', async () => { - await savedObjectsRepository.update( - type, - id, - { title: 'Testing' }, - { - version: encodeHitVersion({ - _seq_no: 100, - _primary_term: 200, - }), - } - ); + it(`accepts a custom refresh setting`, async () => { + const refresh = 'foo'; + await deleteSuccess(type, id, { refresh }); + expectClusterCallArgs({ refresh }); + }); - expect(callAdminCluster).toHaveBeenCalledTimes(1); - expect(callAdminCluster).toHaveBeenCalledWith( - expect.any(String), - expect.objectContaining({ - if_seq_no: 100, - if_primary_term: 200, - }) - ); + it(`prepends namespace to the id when providing namespace for single-namespace type`, async () => { + await deleteSuccess(type, id, { namespace }); + expectClusterCallArgs({ id: `${namespace}:${type}:${id}` }); + }); + + it(`doesn't prepend namespace to the id when providing no namespace for single-namespace type`, async () => { + await deleteSuccess(type, id); + expectClusterCallArgs({ id: `${type}:${id}` }); + }); + + it(`doesn't prepend namespace to the id when not using single-namespace type`, async () => { + await deleteSuccess(NAMESPACE_AGNOSTIC_TYPE, id, { namespace }); + expectClusterCallArgs({ id: `${NAMESPACE_AGNOSTIC_TYPE}:${id}` }); + + callAdminCluster.mockReset(); + await deleteSuccess(MULTI_NAMESPACE_TYPE, id, { namespace }); + expectClusterCallArgs({ id: `${MULTI_NAMESPACE_TYPE}:${id}` }); + }); }); - it('does not pass references if omitted', async () => { - await savedObjectsRepository.update(type, id, { title: 'Testing' }); + describe('errors', () => { + const expectNotFoundError = async (type, id, options) => { + await expect(savedObjectsRepository.delete(type, id, options)).rejects.toThrowError( + createGenericNotFoundError(type, id) + ); + }; + + it(`throws when type is invalid`, async () => { + await expectNotFoundError('unknownType', id); + expect(callAdminCluster).not.toHaveBeenCalled(); + }); + + it(`throws when type is hidden`, async () => { + await expectNotFoundError(HIDDEN_TYPE, id); + expect(callAdminCluster).not.toHaveBeenCalled(); + }); + + it(`throws when ES is unable to find the document during get`, async () => { + callAdminCluster.mockResolvedValue({ found: false }); // this._callCluster('get', ...) + await expectNotFoundError(MULTI_NAMESPACE_TYPE, id); + expectClusterCalls('get'); + }); + + it(`throws when ES is unable to find the index during get`, async () => { + callAdminCluster.mockResolvedValue({ status: 404 }); // this._callCluster('get', ...) + await expectNotFoundError(MULTI_NAMESPACE_TYPE, id); + expectClusterCalls('get'); + }); + + it(`throws when the type is multi-namespace and the document exists, but not in this namespace`, async () => { + const response = getMockGetResponse({ type: MULTI_NAMESPACE_TYPE, id, namespace }); + callAdminCluster.mockResolvedValue(response); // this._callCluster('get', ...) + await expectNotFoundError(MULTI_NAMESPACE_TYPE, id, { namespace: 'bar-namespace' }); + expectClusterCalls('get'); + }); + + it(`throws when ES is unable to find the document during update`, async () => { + const mockResponse = getMockGetResponse({ type: MULTI_NAMESPACE_TYPE, id }); + mockResponse._source.namespaces = ['default', 'some-other-nameespace']; + callAdminCluster + .mockResolvedValueOnce(mockResponse) // this._callCluster('get', ...) + .mockResolvedValue({ status: 404 }); // this._writeToCluster('update', ...) + await expectNotFoundError(MULTI_NAMESPACE_TYPE, id); + expectClusterCalls('get', 'update'); + }); + + it(`throws when ES is unable to find the document during delete`, async () => { + callAdminCluster.mockResolvedValue({ result: 'not_found' }); // this._writeToCluster('delete', ...) + await expectNotFoundError(type, id); + expectClusterCalls('delete'); + }); + + it(`throws when ES is unable to find the index during delete`, async () => { + callAdminCluster.mockResolvedValue({ error: { type: 'index_not_found_exception' } }); // this._writeToCluster('delete', ...) + await expectNotFoundError(type, id); + expectClusterCalls('delete'); + }); - expect(callAdminCluster).toHaveBeenCalledTimes(1); - expect(callAdminCluster).not.toHaveBeenCalledWith( - expect.any(String), - expect.objectContaining({ - body: { - doc: expect.objectContaining({ - references: [], - }), - }, - }) - ); + it(`throws when ES returns an unexpected response`, async () => { + callAdminCluster.mockResolvedValue({ result: 'something unexpected' }); // this._writeToCluster('delete', ...) + await expect(savedObjectsRepository.delete(type, id)).rejects.toThrowError( + 'Unexpected Elasticsearch DELETE response' + ); + expectClusterCalls('delete'); + }); }); - it('passes references if they are provided', async () => { - await savedObjectsRepository.update(type, id, { title: 'Testing' }, { references: ['foo'] }); + describe('migration', () => { + it(`waits until migrations are complete before proceeding`, async () => { + let callAdminClusterCount = 0; + migrator.runMigrations = jest.fn(async () => + // runMigrations should resolve before callAdminCluster is initiated + expect(callAdminCluster).toHaveBeenCalledTimes(callAdminClusterCount++) + ); + await expect(deleteSuccess(type, id)).resolves.toBeDefined(); + expect(migrator.runMigrations).toHaveBeenCalledTimes(1); + }); + }); - expect(callAdminCluster).toHaveBeenCalledTimes(1); - expect(callAdminCluster).toHaveBeenCalledWith( - expect.any(String), - expect.objectContaining({ - body: { - doc: expect.objectContaining({ - references: ['foo'], - }), - }, - }) - ); + describe('returns', () => { + it(`returns an empty object on success`, async () => { + const result = await deleteSuccess(type, id); + expect(result).toEqual({}); + }); }); + }); - it('passes empty references array if empty references array is provided', async () => { - await savedObjectsRepository.update(type, id, { title: 'Testing' }, { references: [] }); + describe('#deleteByNamespace', () => { + const namespace = 'foo-namespace'; + const mockUpdateResults = { + took: 15, + timed_out: false, + total: 3, + updated: 2, + deleted: 1, + batches: 1, + version_conflicts: 0, + noops: 0, + retries: { bulk: 0, search: 0 }, + throttled_millis: 0, + requests_per_second: -1.0, + throttled_until_millis: 0, + failures: [], + }; + const deleteByNamespaceSuccess = async (namespace, options) => { + callAdminCluster.mockResolvedValue(mockUpdateResults); + const result = await savedObjectsRepository.deleteByNamespace(namespace, options); + expect(getSearchDslNS.getSearchDsl).toHaveBeenCalledTimes(1); expect(callAdminCluster).toHaveBeenCalledTimes(1); - expect(callAdminCluster).toHaveBeenCalledWith( - expect.any(String), - expect.objectContaining({ - body: { - doc: expect.objectContaining({ - references: [], - }), - }, - }) - ); - }); + return result; + }; - it(`prepends namespace to the id but doesn't add namespace to body when providing namespace for namespaced type`, async () => { - await savedObjectsRepository.update( - 'index-pattern', - 'logstash-*', - { - title: 'Testing', - }, - { - namespace: 'foo-namespace', - references: [ - { - name: 'ref_0', - type: 'test', - id: '1', - }, - ], - } - ); + describe('cluster calls', () => { + it(`should use the ES updateByQuery action`, async () => { + await deleteByNamespaceSuccess(namespace); + expectClusterCalls('updateByQuery'); + }); - expect(callAdminCluster).toHaveBeenCalledTimes(1); - expect(callAdminCluster).toHaveBeenCalledWith('update', { - id: 'foo-namespace:index-pattern:logstash-*', - body: { - doc: { - updated_at: mockTimestamp, - 'index-pattern': { title: 'Testing' }, - references: [ - { - name: 'ref_0', - type: 'test', - id: '1', - }, - ], - }, - }, - ignore: [404], - refresh: 'wait_for', - index: '.kibana-test', + it(`defaults to a refresh setting of wait_for`, async () => { + await deleteByNamespaceSuccess(namespace); + expectClusterCallArgs({ refresh: 'wait_for' }); }); - }); - it(`doesn't prepend namespace to the id or add namespace property when providing no namespace for namespaced type`, async () => { - await savedObjectsRepository.update( - 'index-pattern', - 'logstash-*', - { - title: 'Testing', - }, - { - references: [ - { - name: 'ref_0', - type: 'test', - id: '1', - }, - ], - } - ); + it(`accepts a custom refresh setting`, async () => { + const refresh = 'foo'; + await deleteByNamespaceSuccess(namespace, { refresh }); + expectClusterCallArgs({ refresh }); + }); - expect(callAdminCluster).toHaveBeenCalledTimes(1); - expect(callAdminCluster).toHaveBeenCalledWith('update', { - id: 'index-pattern:logstash-*', - body: { - doc: { - updated_at: mockTimestamp, - 'index-pattern': { title: 'Testing' }, - references: [ - { - name: 'ref_0', - type: 'test', - id: '1', - }, - ], - }, - }, - ignore: [404], - refresh: 'wait_for', - index: '.kibana-test', + it(`should use all indices for types that are not namespace-agnostic`, async () => { + await deleteByNamespaceSuccess(namespace); + expectClusterCallArgs({ index: ['.kibana-test', 'custom'] }, 1); }); }); - it(`doesn't prepend namespace to the id or add namespace property when providing namespace for namespace agnostic type`, async () => { - await savedObjectsRepository.update( - 'globaltype', - 'foo', - { - name: 'bar', - }, - { - namespace: 'foo-namespace', - references: [ - { - name: 'ref_0', - type: 'test', - id: '1', - }, - ], - } - ); - - expect(callAdminCluster).toHaveBeenCalledTimes(1); - expect(callAdminCluster).toHaveBeenCalledWith('update', { - id: 'globaltype:foo', - body: { - doc: { - updated_at: mockTimestamp, - globaltype: { name: 'bar' }, - references: [ - { - name: 'ref_0', - type: 'test', - id: '1', - }, - ], - }, - }, - ignore: [404], - refresh: 'wait_for', - index: '.kibana-test', + describe('errors', () => { + it(`throws when namespace is not a string`, async () => { + const test = async namespace => { + await expect(savedObjectsRepository.deleteByNamespace(namespace)).rejects.toThrowError( + `namespace is required, and must be a string` + ); + expect(callAdminCluster).not.toHaveBeenCalled(); + }; + await test(undefined); + await test(['namespace']); + await test(123); + await test(true); }); }); - it('defaults to a refresh setting of `wait_for`', async () => { - await savedObjectsRepository.update('globaltype', 'foo', { - name: 'bar', + describe('migration', () => { + it(`waits until migrations are complete before proceeding`, async () => { + migrator.runMigrations = jest.fn(async () => + expect(callAdminCluster).not.toHaveBeenCalled() + ); + await expect(deleteByNamespaceSuccess(namespace)).resolves.toBeDefined(); + expect(migrator.runMigrations).toHaveBeenCalledTimes(1); }); + }); - expect(callAdminCluster).toHaveBeenCalledTimes(1); - expect(callAdminCluster.mock.calls[0][1]).toMatchObject({ - refresh: 'wait_for', + describe('returns', () => { + it(`returns the query results on success`, async () => { + const result = await deleteByNamespaceSuccess(namespace); + expect(result).toEqual(mockUpdateResults); }); }); - it('accepts a custom refresh setting', async () => { - await savedObjectsRepository.update( - 'globaltype', - 'foo', - { - name: 'bar', - }, - { - refresh: true, - namespace: 'foo-namespace', - } - ); - - expect(callAdminCluster).toHaveBeenCalledTimes(1); - expect(callAdminCluster.mock.calls[0][1]).toMatchObject({ - refresh: true, + describe('search dsl', () => { + it(`constructs a query using all multi-namespace types, and another using all single-namespace types`, async () => { + await deleteByNamespaceSuccess(namespace); + const allTypes = registry.getAllTypes().map(type => type.name); + expect(getSearchDslNS.getSearchDsl).toHaveBeenCalledWith(mappings, registry, { + namespace, + type: allTypes.filter(type => !registry.isNamespaceAgnostic(type)), + }); }); }); }); - describe('#bulkUpdate', () => { - const { generateSavedObject, reset } = (() => { - let count = 0; + describe('#find', () => { + const generateSearchResults = namespace => { return { - generateSavedObject(overrides) { - count++; - return _.merge( + hits: { + total: 4, + hits: [ { - type: 'index-pattern', - id: `logstash-${count}`, - attributes: { title: `Testing ${count}` }, - references: [ - { - name: 'ref_0', - type: 'test', - id: '1', + _index: '.kibana', + _id: `${namespace ? `${namespace}:` : ''}index-pattern:logstash-*`, + _score: 1, + ...mockVersionProps, + _source: { + namespace, + type: 'index-pattern', + ...mockTimestampFields, + 'index-pattern': { + title: 'logstash-*', + timeFieldName: '@timestamp', + notExpandable: true, }, - ], + }, }, - overrides - ); - }, - reset() { - count = 0; + { + _index: '.kibana', + _id: `${namespace ? `${namespace}:` : ''}config:6.0.0-alpha1`, + _score: 1, + ...mockVersionProps, + _source: { + namespace, + type: 'config', + ...mockTimestampFields, + config: { + buildNum: 8467, + defaultIndex: 'logstash-*', + }, + }, + }, + { + _index: '.kibana', + _id: `${namespace ? `${namespace}:` : ''}index-pattern:stocks-*`, + _score: 1, + ...mockVersionProps, + _source: { + namespace, + type: 'index-pattern', + ...mockTimestampFields, + 'index-pattern': { + title: 'stocks-*', + timeFieldName: '@timestamp', + notExpandable: true, + }, + }, + }, + { + _index: '.kibana', + _id: `${NAMESPACE_AGNOSTIC_TYPE}:something`, + _score: 1, + ...mockVersionProps, + _source: { + type: NAMESPACE_AGNOSTIC_TYPE, + ...mockTimestampFields, + [NAMESPACE_AGNOSTIC_TYPE]: { + name: 'bar', + }, + }, + }, + ], }, }; - })(); + }; - beforeEach(() => { - reset(); - }); + const type = 'index-pattern'; + const namespace = 'foo-namespace'; - const mockValidResponse = objects => - callAdminCluster.mockReturnValue({ - items: objects.map(items => ({ - update: { - _id: `${items.type}:${items.id}`, - ...mockVersionProps, - result: 'updated', - }, - })), + const findSuccess = async (options, namespace) => { + callAdminCluster.mockResolvedValue(generateSearchResults(namespace)); + const result = await savedObjectsRepository.find(options); + expect(getSearchDslNS.getSearchDsl).toHaveBeenCalledTimes(1); + expect(callAdminCluster).toHaveBeenCalledTimes(1); + return result; + }; + + describe('cluster calls', () => { + it(`should use the ES search action`, async () => { + await findSuccess({ type }); + expectClusterCalls('search'); + }); + + it(`merges output of getSearchDsl into es request body`, async () => { + const query = { query: 1, aggregations: 2 }; + getSearchDslNS.getSearchDsl.mockReturnValue(query); + await findSuccess({ type }); + expectClusterCallArgs({ body: expect.objectContaining({ ...query }) }); }); - it('waits until migrations are complete before proceeding', async () => { - const objects = [generateSavedObject(), generateSavedObject()]; + it(`accepts per_page/page`, async () => { + await findSuccess({ type, perPage: 10, page: 6 }); + expectClusterCallArgs({ + size: 10, + from: 50, + }); + }); - migrator.runMigrations = jest.fn(async () => expect(callAdminCluster).not.toHaveBeenCalled()); + it(`can filter by fields`, async () => { + await findSuccess({ type, fields: ['title'] }); + expectClusterCallArgs({ + _source: [ + `${type}.title`, + 'namespace', + 'namespaces', + 'type', + 'references', + 'migrationVersion', + 'updated_at', + 'title', + ], + }); + }); - mockValidResponse(objects); + it(`should set rest_total_hits_as_int to true on a request`, async () => { + await findSuccess({ type }); + expectClusterCallArgs({ rest_total_hits_as_int: true }); + }); - await expect( - savedObjectsRepository.bulkUpdate([generateSavedObject()]) - ).resolves.toBeDefined(); + it(`should not make a cluster call when attempting to find only invalid or hidden types`, async () => { + const test = async types => { + await savedObjectsRepository.find({ type: types }); + expect(callAdminCluster).not.toHaveBeenCalled(); + }; - expect(migrator.runMigrations).toHaveReturnedTimes(1); + await test('unknownType'); + await test(HIDDEN_TYPE); + await test(['unknownType', HIDDEN_TYPE]); + }); }); - it('returns current ES document, _seq_no and _primary_term encoded as version', async () => { - const objects = [generateSavedObject(), generateSavedObject()]; - - mockValidResponse(objects); + describe('errors', () => { + it(`throws when type is not defined`, async () => { + await expect(savedObjectsRepository.find({})).rejects.toThrowError( + 'options.type must be a string or an array of strings' + ); + expect(callAdminCluster).not.toHaveBeenCalled(); + }); - const response = await savedObjectsRepository.bulkUpdate(objects); + it(`throws when searchFields is defined but not an array`, async () => { + await expect( + savedObjectsRepository.find({ type, searchFields: 'string' }) + ).rejects.toThrowError('options.searchFields must be an array'); + expect(callAdminCluster).not.toHaveBeenCalled(); + }); - expect(response.saved_objects[0]).toMatchObject({ - ..._.pick(objects[0], 'id', 'type', 'attributes'), - version: mockVersion, - references: objects[0].references, + it(`throws when fields is defined but not an array`, async () => { + await expect(savedObjectsRepository.find({ type, fields: 'string' })).rejects.toThrowError( + 'options.fields must be an array' + ); + expect(callAdminCluster).not.toHaveBeenCalled(); }); - expect(response.saved_objects[1]).toMatchObject({ - ..._.pick(objects[1], 'id', 'type', 'attributes'), - version: mockVersion, - references: objects[1].references, + + it(`throws when KQL filter syntax is invalid`, async () => { + const findOpts = { + namespace, + search: 'foo*', + searchFields: ['foo'], + type: ['dashboard'], + sortField: 'name', + sortOrder: 'desc', + defaultSearchOperator: 'AND', + hasReference: { + type: 'foo', + id: '1', + }, + indexPattern: undefined, + filter: 'dashboard.attributes.otherField:<', + }; + + await expect(savedObjectsRepository.find(findOpts)).rejects.toMatchInlineSnapshot(` + [Error: KQLSyntaxError: Expected "(", "{", value, whitespace but "<" found. + dashboard.attributes.otherField:< + --------------------------------^: Bad Request] + `); + expect(getSearchDslNS.getSearchDsl).not.toHaveBeenCalled(); + expect(callAdminCluster).not.toHaveBeenCalled(); }); }); - it('handles a mix of succesfull updates and errors', async () => { - const objects = [ - generateSavedObject(), - { - type: 'invalid-type', - id: 'invalid', - attributes: { title: 'invalid' }, - }, - generateSavedObject(), - generateSavedObject({ - id: 'version_clash', - }), - ]; - - callAdminCluster.mockReturnValue({ - items: objects - // remove invalid from mocks - .filter(item => item.id !== 'invalid') - .map(items => { - switch (items.id) { - case 'version_clash': - return { - update: { - _id: `${items.type}:${items.id}`, - error: { - type: 'version_conflict_engine_exception', - }, - }, - }; - default: - return { - update: { - _id: `${items.type}:${items.id}`, - ...mockVersionProps, - result: 'updated', - }, - }; - } - }), - }); - - const { - saved_objects: [firstUpdatedObject, invalidType, secondUpdatedObject, versionClashObject], - } = await savedObjectsRepository.bulkUpdate(objects); - - expect(firstUpdatedObject).toMatchObject({ - ..._.pick(objects[0], 'id', 'type', 'attributes', 'references'), - version: mockVersion, + describe('migration', () => { + it(`waits until migrations are complete before proceeding`, async () => { + migrator.runMigrations = jest.fn(async () => + expect(callAdminCluster).not.toHaveBeenCalled() + ); + await expect(findSuccess({ type })).resolves.toBeDefined(); + expect(migrator.runMigrations).toHaveBeenCalledTimes(1); }); + }); - expect(invalidType).toMatchObject({ - ..._.pick(objects[1], 'id', 'type'), - error: SavedObjectsErrorHelpers.createGenericNotFoundError('invalid-type', 'invalid').output - .payload, - }); + describe('returns', () => { + it(`formats the ES response when there is no namespace`, async () => { + const noNamespaceSearchResults = generateSearchResults(); + callAdminCluster.mockReturnValue(noNamespaceSearchResults); + const count = noNamespaceSearchResults.hits.hits.length; - expect(secondUpdatedObject).toMatchObject({ - ..._.pick(objects[2], 'id', 'type', 'attributes', 'references'), - version: mockVersion, - }); + const response = await savedObjectsRepository.find({ type }); + + expect(response.total).toBe(count); + expect(response.saved_objects).toHaveLength(count); - expect(versionClashObject).toMatchObject({ - ..._.pick(objects[3], 'id', 'type'), - error: { statusCode: 409, message: 'version conflict, document already exists' }, + noNamespaceSearchResults.hits.hits.forEach((doc, i) => { + expect(response.saved_objects[i]).toEqual({ + id: doc._id.replace(/(index-pattern|config|globalType)\:/, ''), + type: doc._source.type, + ...mockTimestampFields, + version: mockVersion, + attributes: doc._source[doc._source.type], + references: [], + }); + }); }); - }); - it('doesnt call Elasticsearch if there are no valid objects to update', async () => { - const objects = [ - { - type: 'invalid-type', - id: 'invalid', - attributes: { title: 'invalid' }, - }, - { - type: 'invalid-type', - id: 'invalid 2', - attributes: { title: 'invalid' }, - }, - ]; + it(`formats the ES response when there is a namespace`, async () => { + const namespacedSearchResults = generateSearchResults(namespace); + callAdminCluster.mockReturnValue(namespacedSearchResults); + const count = namespacedSearchResults.hits.hits.length; - const { - saved_objects: [invalidType, invalidType2], - } = await savedObjectsRepository.bulkUpdate(objects); + const response = await savedObjectsRepository.find({ type, namespace }); - expect(callAdminCluster).not.toHaveBeenCalled(); + expect(response.total).toBe(count); + expect(response.saved_objects).toHaveLength(count); - expect(invalidType).toMatchObject({ - ..._.pick(objects[0], 'id', 'type'), - error: SavedObjectsErrorHelpers.createGenericNotFoundError('invalid-type', 'invalid').output - .payload, + namespacedSearchResults.hits.hits.forEach((doc, i) => { + expect(response.saved_objects[i]).toEqual({ + id: doc._id.replace(/(foo-namespace\:)?(index-pattern|config|globalType)\:/, ''), + type: doc._source.type, + ...mockTimestampFields, + version: mockVersion, + attributes: doc._source[doc._source.type], + references: [], + }); + }); }); - expect(invalidType2).toMatchObject({ - ..._.pick(objects[1], 'id', 'type'), - error: SavedObjectsErrorHelpers.createGenericNotFoundError('invalid-type', 'invalid 2') - .output.payload, + it(`should return empty results when attempting to find only invalid or hidden types`, async () => { + const test = async types => { + const result = await savedObjectsRepository.find({ type: types }); + expect(result).toEqual(expect.objectContaining({ saved_objects: [] })); + }; + + await test('unknownType'); + await test(HIDDEN_TYPE); + await test(['unknownType', HIDDEN_TYPE]); }); }); - it('accepts version', async () => { - const objects = [ - generateSavedObject({ - version: encodeHitVersion({ - _seq_no: 100, - _primary_term: 200, - }), - }), - generateSavedObject({ - version: encodeHitVersion({ - _seq_no: 300, - _primary_term: 400, - }), - }), - ]; + describe('search dsl', () => { + it(`passes mappings, registry, search, defaultSearchOperator, searchFields, type, sortField, sortOrder and hasReference to getSearchDsl`, async () => { + const relevantOpts = { + namespace, + search: 'foo*', + searchFields: ['foo'], + type: [type], + sortField: 'name', + sortOrder: 'desc', + defaultSearchOperator: 'AND', + hasReference: { + type: 'foo', + id: '1', + }, + kueryNode: undefined, + }; - mockValidResponse(objects); + await findSuccess(relevantOpts, namespace); + expect(getSearchDslNS.getSearchDsl).toHaveBeenCalledWith(mappings, registry, relevantOpts); + }); + + it(`accepts KQL filter and passes kueryNode to getSearchDsl`, async () => { + const findOpts = { + namespace, + search: 'foo*', + searchFields: ['foo'], + type: ['dashboard'], + sortField: 'name', + sortOrder: 'desc', + defaultSearchOperator: 'AND', + hasReference: { + type: 'foo', + id: '1', + }, + indexPattern: undefined, + filter: 'dashboard.attributes.otherField: *', + }; + + await findSuccess(findOpts, namespace); + const { kueryNode } = getSearchDslNS.getSearchDsl.mock.calls[0][2]; + expect(kueryNode).toMatchInlineSnapshot(` + Object { + "arguments": Array [ + Object { + "type": "literal", + "value": "dashboard.otherField", + }, + Object { + "type": "wildcard", + "value": "@kuery-wildcard@", + }, + Object { + "type": "literal", + "value": false, + }, + ], + "function": "is", + "type": "function", + } + `); + }); - await savedObjectsRepository.bulkUpdate(objects); + it(`supports multiple types`, async () => { + const types = ['config', 'index-pattern']; + await findSuccess({ type: types }); - expect(callAdminCluster).toHaveBeenCalledTimes(1); + expect(getSearchDslNS.getSearchDsl).toHaveBeenCalledWith( + mappings, + registry, + expect.objectContaining({ + type: types, + }) + ); + }); - const [ - , - { - body: [{ update: firstUpdate }, , { update: secondUpdate }], - }, - ] = callAdminCluster.mock.calls[0]; + it(`filters out invalid types`, async () => { + const types = ['config', 'unknownType', 'index-pattern']; + await findSuccess({ type: types }); - expect(firstUpdate).toMatchObject({ - if_seq_no: 100, - if_primary_term: 200, + expect(getSearchDslNS.getSearchDsl).toHaveBeenCalledWith( + mappings, + registry, + expect.objectContaining({ + type: ['config', 'index-pattern'], + }) + ); }); - expect(secondUpdate).toMatchObject({ - if_seq_no: 300, - if_primary_term: 400, + it(`filters out hidden types`, async () => { + const types = ['config', HIDDEN_TYPE, 'index-pattern']; + await findSuccess({ type: types }); + + expect(getSearchDslNS.getSearchDsl).toHaveBeenCalledWith( + mappings, + registry, + expect.objectContaining({ + type: ['config', 'index-pattern'], + }) + ); }); }); + }); - it('does not pass references if omitted', async () => { - const objects = [ - { - type: 'index-pattern', - id: `logstash-no-ref`, - attributes: { title: `Testing no-ref` }, - }, - ]; + describe('#get', () => { + const type = 'index-pattern'; + const id = 'logstash-*'; + const namespace = 'foo-namespace'; + + const getSuccess = async (type, id, options) => { + const response = getMockGetResponse({ type, id, namespace: options?.namespace }); + callAdminCluster.mockResolvedValue(response); + const result = await savedObjectsRepository.get(type, id, options); + expect(callAdminCluster).toHaveBeenCalledTimes(1); + return result; + }; - mockValidResponse(objects); + describe('cluster calls', () => { + it(`should use the ES get action`, async () => { + await getSuccess(type, id); + expectClusterCalls('get'); + }); - await savedObjectsRepository.bulkUpdate(objects); + it(`prepends namespace to the id when providing namespace for single-namespace type`, async () => { + await getSuccess(type, id, { namespace }); + expectClusterCallArgs({ id: `${namespace}:${type}:${id}` }); + }); - expect(callAdminCluster).toHaveBeenCalledTimes(1); + it(`doesn't prepend namespace to the id when providing no namespace for single-namespace type`, async () => { + await getSuccess(type, id); + expectClusterCallArgs({ id: `${type}:${id}` }); + }); - const [ - , - { - body: [, { doc: firstDoc }], - }, - ] = callAdminCluster.mock.calls[0]; + it(`doesn't prepend namespace to the id when not using single-namespace type`, async () => { + await getSuccess(NAMESPACE_AGNOSTIC_TYPE, id, { namespace }); + expectClusterCallArgs({ id: `${NAMESPACE_AGNOSTIC_TYPE}:${id}` }); - expect(firstDoc).not.toMatchObject({ - references: [], + callAdminCluster.mockReset(); + await getSuccess(MULTI_NAMESPACE_TYPE, id, { namespace }); + expectClusterCallArgs({ id: `${MULTI_NAMESPACE_TYPE}:${id}` }); }); }); - it('passes references if they are provided', async () => { - const objects = [ - generateSavedObject({ - references: [ - { - name: 'ref_0', - type: 'test', - id: '1', - }, - ], - }), - ]; + describe('errors', () => { + const expectNotFoundError = async (type, id, options) => { + await expect(savedObjectsRepository.get(type, id, options)).rejects.toThrowError( + createGenericNotFoundError(type, id) + ); + }; - mockValidResponse(objects); + it(`throws when type is invalid`, async () => { + await expectNotFoundError('unknownType', id); + expect(callAdminCluster).not.toHaveBeenCalled(); + }); - await savedObjectsRepository.bulkUpdate(objects); + it(`throws when type is hidden`, async () => { + await expectNotFoundError(HIDDEN_TYPE, id); + expect(callAdminCluster).not.toHaveBeenCalled(); + }); - expect(callAdminCluster).toHaveBeenCalledTimes(1); + it(`throws when ES is unable to find the document during get`, async () => { + callAdminCluster.mockResolvedValue({ found: false }); + await expectNotFoundError(type, id); + expectClusterCalls('get'); + }); - const [ - , - { - body: [, { doc }], - }, - ] = callAdminCluster.mock.calls[0]; + it(`throws when ES is unable to find the index during get`, async () => { + callAdminCluster.mockResolvedValue({ status: 404 }); + await expectNotFoundError(type, id); + expectClusterCalls('get'); + }); - expect(doc).toMatchObject({ - references: [ - { - name: 'ref_0', - type: 'test', - id: '1', - }, - ], + it(`throws when type is multi-namespace and the document exists, but not in this namespace`, async () => { + const response = getMockGetResponse({ type: MULTI_NAMESPACE_TYPE, id, namespace }); + callAdminCluster.mockResolvedValue(response); + await expectNotFoundError(MULTI_NAMESPACE_TYPE, id, { namespace: 'bar-namespace' }); + expectClusterCalls('get'); }); }); - it('passes empty references array if empty references array is provided', async () => { - const objects = [ - { - type: 'index-pattern', - id: `logstash-no-ref`, - attributes: { title: `Testing no-ref` }, - references: [], - }, - ]; - - mockValidResponse(objects); - - await savedObjectsRepository.bulkUpdate(objects); + describe('migration', () => { + it(`waits until migrations are complete before proceeding`, async () => { + migrator.runMigrations = jest.fn(async () => + expect(callAdminCluster).not.toHaveBeenCalled() + ); + await expect(getSuccess(type, id)).resolves.toBeDefined(); + expect(migrator.runMigrations).toHaveBeenCalledTimes(1); + }); + }); - expect(callAdminCluster).toHaveBeenCalledTimes(1); + describe('returns', () => { + it(`formats the ES response`, async () => { + const result = await getSuccess(type, id); + expect(result).toEqual({ + id, + type, + updated_at: mockTimestamp, + version: mockVersion, + attributes: { + title: 'Testing', + }, + references: [], + }); + }); - const [ - , - { - body: [, { doc }], - }, - ] = callAdminCluster.mock.calls[0]; + it(`includes namespaces if type is multi-namespace`, async () => { + const result = await getSuccess(MULTI_NAMESPACE_TYPE, id); + expect(result).toMatchObject({ + namespaces: expect.any(Array), + }); + }); - expect(doc).toMatchObject({ - references: [], + it(`doesn't include namespaces if type is not multi-namespace`, async () => { + const result = await getSuccess(type, id); + expect(result).not.toMatchObject({ + namespaces: expect.anything(), + }); }); }); + }); - it('defaults to a refresh setting of `wait_for`', async () => { - const objects = [ - { - type: 'index-pattern', - id: `logstash-no-ref`, - attributes: { title: `Testing no-ref` }, - references: [], + describe('#incrementCounter', () => { + const type = 'config'; + const id = 'one'; + const field = 'buildNum'; + const namespace = 'foo-namespace'; + + const incrementCounterSuccess = async (type, id, field, options) => { + const isMultiNamespace = registry.isMultiNamespace(type); + if (isMultiNamespace) { + const response = getMockGetResponse({ type, id, namespace: options?.namespace }); + callAdminCluster.mockResolvedValueOnce(response); // this._callCluster('get', ...) + } + callAdminCluster.mockImplementation((method, params) => ({ + _id: params.id, + ...mockVersionProps, + _index: '.kibana', + get: { + found: true, + _source: { + type, + ...mockTimestampFields, + [type]: { + [field]: 8468, + defaultIndex: 'logstash-*', + }, + }, }, - ]; - - mockValidResponse(objects); + })); + const result = await savedObjectsRepository.incrementCounter(type, id, field, options); + expect(callAdminCluster).toHaveBeenCalledTimes(isMultiNamespace ? 2 : 1); + return result; + }; - await savedObjectsRepository.bulkUpdate(objects); + describe('cluster calls', () => { + it(`should use the ES update action if type is not multi-namespace`, async () => { + await incrementCounterSuccess(type, id, field, { namespace }); + expectClusterCalls('update'); + }); - expect(callAdminCluster).toHaveBeenCalledTimes(1); + it(`should use the ES get action then update action if type is multi-namespace, ID is defined, and overwrite=true`, async () => { + await incrementCounterSuccess(MULTI_NAMESPACE_TYPE, id, field, { namespace }); + expectClusterCalls('get', 'update'); + }); - expect(callAdminCluster.mock.calls[0][1]).toMatchObject({ refresh: 'wait_for' }); - }); + it(`defaults to a refresh setting of wait_for`, async () => { + await incrementCounterSuccess(type, id, field, { namespace }); + expectClusterCallArgs({ refresh: 'wait_for' }); + }); - it('accepts a custom refresh setting', async () => { - const objects = [ - { - type: 'index-pattern', - id: `logstash-no-ref`, - attributes: { title: `Testing no-ref` }, - references: [], - }, - ]; + it(`accepts a custom refresh setting`, async () => { + const refresh = 'foo'; + await incrementCounterSuccess(type, id, field, { namespace, refresh }); + expectClusterCallArgs({ refresh }); + }); - mockValidResponse(objects); + it(`prepends namespace to the id when providing namespace for single-namespace type`, async () => { + await incrementCounterSuccess(type, id, field, { namespace }); + expectClusterCallArgs({ id: `${namespace}:${type}:${id}` }); + }); - await savedObjectsRepository.bulkUpdate(objects, { refresh: true }); + it(`doesn't prepend namespace to the id when providing no namespace for single-namespace type`, async () => { + await incrementCounterSuccess(type, id, field); + expectClusterCallArgs({ id: `${type}:${id}` }); + }); - expect(callAdminCluster).toHaveBeenCalledTimes(1); + it(`doesn't prepend namespace to the id when not using single-namespace type`, async () => { + await incrementCounterSuccess(NAMESPACE_AGNOSTIC_TYPE, id, field, { namespace }); + expectClusterCallArgs({ id: `${NAMESPACE_AGNOSTIC_TYPE}:${id}` }); - expect(callAdminCluster.mock.calls[0][1]).toMatchObject({ refresh: true }); + callAdminCluster.mockReset(); + await incrementCounterSuccess(MULTI_NAMESPACE_TYPE, id, field, { namespace }); + expectClusterCallArgs({ id: `${MULTI_NAMESPACE_TYPE}:${id}` }); + }); }); - it(`prepends namespace to the id but doesn't add namespace to body when providing namespace for namespaced type`, async () => { - const objects = [generateSavedObject(), generateSavedObject()]; + describe('errors', () => { + const expectUnsupportedTypeError = async (type, id, field) => { + await expect(savedObjectsRepository.incrementCounter(type, id, field)).rejects.toThrowError( + createUnsupportedTypeError(type) + ); + }; - mockValidResponse(objects); + it(`throws when type is not a string`, async () => { + const test = async type => { + await expect( + savedObjectsRepository.incrementCounter(type, id, field) + ).rejects.toThrowError(`"type" argument must be a string`); + expect(callAdminCluster).not.toHaveBeenCalled(); + }; - await savedObjectsRepository.bulkUpdate(objects, { - namespace: 'foo-namespace', + await test(null); + await test(42); + await test(false); + await test({}); }); - const [ - , - { - body: [ - { update: firstUpdate }, - { doc: firstUpdateDoc }, - { update: secondUpdate }, - { doc: secondUpdateDoc }, - ], - }, - ] = callAdminCluster.mock.calls[0]; + it(`throws when counterFieldName is not a string`, async () => { + const test = async field => { + await expect( + savedObjectsRepository.incrementCounter(type, id, field) + ).rejects.toThrowError(`"counterFieldName" argument must be a string`); + expect(callAdminCluster).not.toHaveBeenCalled(); + }; - expect(firstUpdate).toMatchObject({ - _id: 'foo-namespace:index-pattern:logstash-1', - _index: '.kibana-test', + await test(null); + await test(42); + await test(false); + await test({}); }); - expect(firstUpdateDoc).toMatchObject({ - updated_at: mockTimestamp, - 'index-pattern': { title: 'Testing 1' }, - references: [ - { - name: 'ref_0', - type: 'test', - id: '1', - }, - ], + it(`throws when type is invalid`, async () => { + await expectUnsupportedTypeError('unknownType', id, field); + expect(callAdminCluster).not.toHaveBeenCalled(); }); - expect(secondUpdate).toMatchObject({ - _id: 'foo-namespace:index-pattern:logstash-2', - _index: '.kibana-test', + it(`throws when type is hidden`, async () => { + await expectUnsupportedTypeError(HIDDEN_TYPE, id, field); + expect(callAdminCluster).not.toHaveBeenCalled(); }); - expect(secondUpdateDoc).toMatchObject({ - updated_at: mockTimestamp, - 'index-pattern': { title: 'Testing 2' }, - references: [ - { - name: 'ref_0', - type: 'test', - id: '1', - }, - ], + it(`throws when there is a conflict with an existing multi-namespace saved object (get)`, async () => { + const response = getMockGetResponse({ + type: MULTI_NAMESPACE_TYPE, + id, + namespace: 'bar-namespace', + }); + callAdminCluster.mockResolvedValue(response); // this._callCluster('get', ...) + await expect( + savedObjectsRepository.incrementCounter(MULTI_NAMESPACE_TYPE, id, field, { namespace }) + ).rejects.toThrowError(createConflictError(MULTI_NAMESPACE_TYPE, id)); + expectClusterCalls('get'); }); }); - it(`doesn't prepend namespace to the id or add namespace property when providing no namespace for namespaced type`, async () => { - const objects = [generateSavedObject(), generateSavedObject()]; - - mockValidResponse(objects); + describe('migration', () => { + beforeEach(() => { + migrator.migrateDocument.mockImplementation(mockMigrateDocument); + }); - await savedObjectsRepository.bulkUpdate(objects); + it(`waits until migrations are complete before proceeding`, async () => { + migrator.runMigrations = jest.fn(async () => + expect(callAdminCluster).not.toHaveBeenCalled() + ); + await expect( + incrementCounterSuccess(type, id, field, { namespace }) + ).resolves.toBeDefined(); + expect(migrator.runMigrations).toHaveBeenCalledTimes(1); + }); - const [ - , - { - body: [ - { update: firstUpdate }, - { doc: firstUpdateDoc }, - { update: secondUpdate }, - { doc: secondUpdateDoc }, - ], - }, - ] = callAdminCluster.mock.calls[0]; + it(`migrates a document and serializes the migrated doc`, async () => { + const migrationVersion = mockMigrationVersion; + await incrementCounterSuccess(type, id, field, { migrationVersion }); + const attributes = { buildNum: 1 }; // this is added by the incrementCounter function + const doc = { type, id, attributes, migrationVersion, ...mockTimestampFields }; + expectMigrationArgs(doc); - expect(firstUpdate).toMatchObject({ - _id: 'index-pattern:logstash-1', - _index: '.kibana-test', + const migratedDoc = migrator.migrateDocument(doc); + expect(serializer.savedObjectToRaw).toHaveBeenLastCalledWith(migratedDoc); }); + }); - expect(firstUpdateDoc).toMatchObject({ - updated_at: mockTimestamp, - 'index-pattern': { title: 'Testing 1' }, - references: [ - { - name: 'ref_0', - type: 'test', - id: '1', + describe('returns', () => { + it(`formats the ES response`, async () => { + callAdminCluster.mockImplementation((method, params) => ({ + _id: params.id, + ...mockVersionProps, + _index: '.kibana', + get: { + found: true, + _source: { + type: 'config', + ...mockTimestampFields, + config: { + buildNum: 8468, + defaultIndex: 'logstash-*', + }, + }, }, - ], - }); - - expect(secondUpdate).toMatchObject({ - _id: 'index-pattern:logstash-2', - _index: '.kibana-test', - }); + })); - expect(secondUpdateDoc).toMatchObject({ - updated_at: mockTimestamp, - 'index-pattern': { title: 'Testing 2' }, - references: [ + const response = await savedObjectsRepository.incrementCounter( + 'config', + '6.0.0-alpha1', + 'buildNum', { - name: 'ref_0', - type: 'test', - id: '1', + namespace: 'foo-namespace', + } + ); + + expect(response).toEqual({ + type: 'config', + id: '6.0.0-alpha1', + ...mockTimestampFields, + version: mockVersion, + attributes: { + buildNum: 8468, + defaultIndex: 'logstash-*', }, - ], + }); }); }); + }); - it(`doesn't prepend namespace to the id or add namespace property when providing namespace for namespace agnostic type`, async () => { - const objects = [ - generateSavedObject({ - type: 'globaltype', - id: 'foo', - namespace: 'foo-namespace', - }), - ]; + describe('#deleteFromNamespaces', () => { + const id = 'some-id'; + const type = MULTI_NAMESPACE_TYPE; + const namespace1 = 'default'; + const namespace2 = 'foo-namespace'; + const namespace3 = 'bar-namespace'; + + const mockGetResponse = (type, id, namespaces) => { + // mock a document that exists in two namespaces + const mockResponse = getMockGetResponse({ type, id }); + mockResponse._source.namespaces = namespaces; + callAdminCluster.mockResolvedValueOnce(mockResponse); // this._callCluster('get', ...) + }; + + const deleteFromNamespacesSuccess = async ( + type, + id, + namespaces, + currentNamespaces, + options + ) => { + mockGetResponse(type, id, currentNamespaces); // this._callCluster('get', ...) + const isDelete = currentNamespaces.every(namespace => namespaces.includes(namespace)); + callAdminCluster.mockResolvedValue({ + _id: `${type}:${id}`, + ...mockVersionProps, + result: isDelete ? 'deleted' : 'updated', + }); // this._writeToCluster('delete', ...) *or* this._writeToCluster('update', ...) + const result = await savedObjectsRepository.deleteFromNamespaces( + type, + id, + namespaces, + options + ); + expect(callAdminCluster).toHaveBeenCalledTimes(2); + return result; + }; - mockValidResponse(objects); + describe('cluster calls', () => { + describe('delete action', () => { + const deleteFromNamespacesSuccessDelete = async (expectFn, options, _type = type) => { + const test = async namespaces => { + await deleteFromNamespacesSuccess(_type, id, namespaces, namespaces, options); + expectFn(); + callAdminCluster.mockReset(); + }; + await test([namespace1]); + await test([namespace1, namespace2]); + }; + + it(`should use ES get action then delete action if the object has no namespaces remaining`, async () => { + const expectFn = () => expectClusterCalls('get', 'delete'); + await deleteFromNamespacesSuccessDelete(expectFn); + }); - await savedObjectsRepository.bulkUpdate(objects); + it(`formats the ES requests`, async () => { + const expectFn = () => { + expectClusterCallArgs({ id: `${type}:${id}` }, 1); + const versionProperties = { + if_seq_no: mockVersionProps._seq_no, + if_primary_term: mockVersionProps._primary_term, + }; + expectClusterCallArgs({ id: `${type}:${id}`, ...versionProperties }, 2); + }; + await deleteFromNamespacesSuccessDelete(expectFn); + }); - const [ - , - { - body: [{ update }, { doc }], - }, - ] = callAdminCluster.mock.calls[0]; + it(`defaults to a refresh setting of wait_for`, async () => { + await deleteFromNamespacesSuccessDelete(() => + expectClusterCallArgs({ refresh: 'wait_for' }, 2) + ); + }); - expect(update).toMatchObject({ - _id: 'globaltype:foo', - _index: '.kibana-test', - }); + it(`accepts a custom refresh setting`, async () => { + const refresh = 'foo'; + const expectFn = () => expectClusterCallArgs({ refresh }, 2); + await deleteFromNamespacesSuccessDelete(expectFn, { refresh }); + }); - expect(doc).toMatchObject({ - updated_at: mockTimestamp, - globaltype: { title: 'Testing 1' }, - references: [ - { - name: 'ref_0', - type: 'test', - id: '1', - }, - ], + it(`should use default index`, async () => { + const expectFn = () => expectClusterCallArgs({ index: '.kibana-test' }, 2); + await deleteFromNamespacesSuccessDelete(expectFn); + }); + + it(`should use custom index`, async () => { + const expectFn = () => expectClusterCallArgs({ index: 'custom' }, 2); + await deleteFromNamespacesSuccessDelete(expectFn, {}, MULTI_NAMESPACE_CUSTOM_INDEX_TYPE); + }); }); - }); - }); - describe('#incrementCounter', () => { - beforeEach(() => { - callAdminCluster.mockImplementation((method, params) => ({ - _id: params.id, - ...mockVersionProps, - _index: '.kibana', - get: { - found: true, - _source: { - type: 'config', - ...mockTimestampFields, - config: { - buildNum: 8468, - defaultIndex: 'logstash-*', - }, - }, - }, - })); - }); + describe('update action', () => { + const deleteFromNamespacesSuccessUpdate = async (expectFn, options, _type = type) => { + const test = async remaining => { + const currentNamespaces = [namespace1].concat(remaining); + await deleteFromNamespacesSuccess(_type, id, [namespace1], currentNamespaces, options); + expectFn(); + callAdminCluster.mockReset(); + }; + await test([namespace2]); + await test([namespace2, namespace3]); + }; + + it(`should use ES get action then update action if the object has one or more namespaces remaining`, async () => { + await deleteFromNamespacesSuccessUpdate(() => expectClusterCalls('get', 'update')); + }); - it('formats Elasticsearch response', async () => { - callAdminCluster.mockImplementation((method, params) => ({ - _id: params.id, - ...mockVersionProps, - _index: '.kibana', - get: { - found: true, - _source: { - type: 'config', - ...mockTimestampFields, - config: { - buildNum: 8468, - defaultIndex: 'logstash-*', - }, - }, - }, - })); + it(`formats the ES requests`, async () => { + let ctr = 0; + const expectFn = () => { + expectClusterCallArgs({ id: `${type}:${id}` }, 1); + const namespaces = ctr++ === 0 ? [namespace2] : [namespace2, namespace3]; + const versionProperties = { + if_seq_no: mockVersionProps._seq_no, + if_primary_term: mockVersionProps._primary_term, + }; + expectClusterCallArgs( + { + id: `${type}:${id}`, + ...versionProperties, + body: { doc: { ...mockTimestampFields, namespaces } }, + }, + 2 + ); + }; + await deleteFromNamespacesSuccessUpdate(expectFn); + }); - const response = await savedObjectsRepository.incrementCounter( - 'config', - '6.0.0-alpha1', - 'buildNum', - { - namespace: 'foo-namespace', - } - ); + it(`defaults to a refresh setting of wait_for`, async () => { + const expectFn = () => expectClusterCallArgs({ refresh: 'wait_for' }, 2); + await deleteFromNamespacesSuccessUpdate(expectFn); + }); - expect(response).toEqual({ - type: 'config', - id: '6.0.0-alpha1', - ...mockTimestampFields, - version: mockVersion, - attributes: { - buildNum: 8468, - defaultIndex: 'logstash-*', - }, + it(`accepts a custom refresh setting`, async () => { + const refresh = 'foo'; + const expectFn = () => expectClusterCallArgs({ refresh }, 2); + await deleteFromNamespacesSuccessUpdate(expectFn, { refresh }); + }); + + it(`should use default index`, async () => { + const expectFn = () => expectClusterCallArgs({ index: '.kibana-test' }, 2); + await deleteFromNamespacesSuccessUpdate(expectFn); + }); + + it(`should use custom index`, async () => { + const expectFn = () => expectClusterCallArgs({ index: 'custom' }, 2); + await deleteFromNamespacesSuccessUpdate(expectFn, {}, MULTI_NAMESPACE_CUSTOM_INDEX_TYPE); + }); }); }); - it('migrates the doc if an upsert is required', async () => { - migrator.migrateDocument = doc => { - doc.attributes.buildNum = 42; - doc.migrationVersion = { foo: '2.3.4' }; - doc.references = [{ name: 'search_0', type: 'search', id: '123' }]; - return doc; + describe('errors', () => { + const expectNotFoundError = async (type, id, namespaces, options) => { + await expect( + savedObjectsRepository.deleteFromNamespaces(type, id, namespaces, options) + ).rejects.toThrowError(createGenericNotFoundError(type, id)); + }; + const expectBadRequestError = async (type, id, namespaces, message) => { + await expect( + savedObjectsRepository.deleteFromNamespaces(type, id, namespaces) + ).rejects.toThrowError(createBadRequestError(message)); }; - await savedObjectsRepository.incrementCounter('config', 'doesnotexist', 'buildNum', { - namespace: 'foo-namespace', + it(`throws when type is invalid`, async () => { + await expectNotFoundError('unknownType', id, [namespace1, namespace2]); + expect(callAdminCluster).not.toHaveBeenCalled(); }); - expect(callAdminCluster).toHaveBeenCalledTimes(1); - expect(callAdminCluster.mock.calls[0][1]).toMatchObject({ - body: { - upsert: { - config: { buildNum: 42 }, - migrationVersion: { foo: '2.3.4' }, - type: 'config', - ...mockTimestampFields, - references: [{ name: 'search_0', type: 'search', id: '123' }], - }, - }, + it(`throws when type is hidden`, async () => { + await expectNotFoundError(HIDDEN_TYPE, id, [namespace1, namespace2]); + expect(callAdminCluster).not.toHaveBeenCalled(); }); - }); - it('defaults to a refresh setting of `wait_for`', async () => { - await savedObjectsRepository.incrementCounter('config', 'doesnotexist', 'buildNum', { - namespace: 'foo-namespace', + it(`throws when type is not namespace-agnostic`, async () => { + const test = async type => { + const message = `${type} doesn't support multiple namespaces`; + await expectBadRequestError(type, id, [namespace1, namespace2], message); + expect(callAdminCluster).not.toHaveBeenCalled(); + }; + await test('index-pattern'); + await test(NAMESPACE_AGNOSTIC_TYPE); }); - expect(callAdminCluster).toHaveBeenCalledTimes(1); - expect(callAdminCluster.mock.calls[0][1]).toMatchObject({ - refresh: 'wait_for', + it(`throws when namespaces is an empty array`, async () => { + const test = async namespaces => { + const message = 'namespaces must be a non-empty array of strings'; + await expectBadRequestError(type, id, namespaces, message); + expect(callAdminCluster).not.toHaveBeenCalled(); + }; + await test([]); }); - }); - it('accepts a custom refresh setting', async () => { - await savedObjectsRepository.incrementCounter('config', 'doesnotexist', 'buildNum', { - namespace: 'foo-namespace', - refresh: true, + it(`throws when ES is unable to find the document during get`, async () => { + callAdminCluster.mockResolvedValue({ found: false }); // this._callCluster('get', ...) + await expectNotFoundError(type, id, [namespace1, namespace2]); + expectClusterCalls('get'); }); - expect(callAdminCluster).toHaveBeenCalledTimes(1); - expect(callAdminCluster.mock.calls[0][1]).toMatchObject({ - refresh: true, + it(`throws when ES is unable to find the index during get`, async () => { + callAdminCluster.mockResolvedValue({ status: 404 }); // this._callCluster('get', ...) + await expectNotFoundError(type, id, [namespace1, namespace2]); + expectClusterCalls('get'); }); - }); - it(`prepends namespace to the id but doesn't add namespace to body when providing namespace for namespaced type`, async () => { - await savedObjectsRepository.incrementCounter('config', '6.0.0-alpha1', 'buildNum', { - namespace: 'foo-namespace', + it(`throws when the document exists, but not in this namespace`, async () => { + mockGetResponse(type, id, [namespace1]); // this._callCluster('get', ...) + await expectNotFoundError(type, id, [namespace1], { namespace: 'some-other-namespace' }); + expectClusterCalls('get'); }); - expect(callAdminCluster).toHaveBeenCalledTimes(1); + it(`throws when ES is unable to find the document during delete`, async () => { + mockGetResponse(type, id, [namespace1]); // this._callCluster('get', ...) + callAdminCluster.mockResolvedValue({ result: 'not_found' }); // this._writeToCluster('delete', ...) + await expectNotFoundError(type, id, [namespace1]); + expectClusterCalls('get', 'delete'); + }); + + it(`throws when ES is unable to find the index during delete`, async () => { + mockGetResponse(type, id, [namespace1]); // this._callCluster('get', ...) + callAdminCluster.mockResolvedValue({ error: { type: 'index_not_found_exception' } }); // this._writeToCluster('delete', ...) + await expectNotFoundError(type, id, [namespace1]); + expectClusterCalls('get', 'delete'); + }); + + it(`throws when ES returns an unexpected response`, async () => { + mockGetResponse(type, id, [namespace1]); // this._callCluster('get', ...) + callAdminCluster.mockResolvedValue({ result: 'something unexpected' }); // this._writeToCluster('delete', ...) + await expect( + savedObjectsRepository.deleteFromNamespaces(type, id, [namespace1]) + ).rejects.toThrowError('Unexpected Elasticsearch DELETE response'); + expectClusterCalls('get', 'delete'); + }); + + it(`throws when ES is unable to find the document during update`, async () => { + mockGetResponse(type, id, [namespace1, namespace2]); // this._callCluster('get', ...) + callAdminCluster.mockResolvedValue({ status: 404 }); // this._writeToCluster('update', ...) + await expectNotFoundError(type, id, [namespace1]); + expectClusterCalls('get', 'update'); + }); + }); - const requestDoc = callAdminCluster.mock.calls[0][1]; - expect(requestDoc.id).toBe('foo-namespace:config:6.0.0-alpha1'); - expect(requestDoc.body.script.params.type).toBe('config'); - expect(requestDoc.body.upsert.type).toBe('config'); - expect(requestDoc).toHaveProperty('body.upsert.config'); + describe('migration', () => { + it(`waits until migrations are complete before proceeding`, async () => { + let callAdminClusterCount = 0; + migrator.runMigrations = jest.fn(async () => + // runMigrations should resolve before callAdminCluster is initiated + expect(callAdminCluster).toHaveBeenCalledTimes(callAdminClusterCount++) + ); + await expect( + deleteFromNamespacesSuccess(type, id, [namespace1], [namespace1]) + ).resolves.toBeDefined(); + expect(migrator.runMigrations).toHaveReturnedTimes(2); + }); }); - it(`doesn't prepend namespace to the id or add namespace property when providing no namespace for namespaced type`, async () => { - await savedObjectsRepository.incrementCounter('config', '6.0.0-alpha1', 'buildNum'); + describe('returns', () => { + it(`returns an empty object on success (delete)`, async () => { + const test = async namespaces => { + const result = await deleteFromNamespacesSuccess(type, id, namespaces, namespaces); + expect(result).toEqual({}); + callAdminCluster.mockReset(); + }; + await test([namespace1]); + await test([namespace1, namespace2]); + }); - expect(callAdminCluster).toHaveBeenCalledTimes(1); + it(`returns an empty object on success (update)`, async () => { + const test = async remaining => { + const currentNamespaces = [namespace1].concat(remaining); + const result = await deleteFromNamespacesSuccess( + type, + id, + [namespace1], + currentNamespaces + ); + expect(result).toEqual({}); + callAdminCluster.mockReset(); + }; + await test([namespace2]); + await test([namespace2, namespace3]); + }); - const requestDoc = callAdminCluster.mock.calls[0][1]; - expect(requestDoc.id).toBe('config:6.0.0-alpha1'); - expect(requestDoc.body.script.params.type).toBe('config'); - expect(requestDoc.body.upsert.type).toBe('config'); - expect(requestDoc).toHaveProperty('body.upsert.config'); + it(`succeeds when the document doesn't exist in all of the targeted namespaces`, async () => { + const namespaces = [namespace2]; + const currentNamespaces = [namespace1]; + const result = await deleteFromNamespacesSuccess(type, id, namespaces, currentNamespaces); + expect(result).toEqual({}); + }); }); + }); - it(`doesn't prepend namespace to the id or add namespace property when providing namespace for namespace agnostic type`, async () => { - callAdminCluster.mockImplementation((method, params) => ({ - _id: params.id, + describe('#update', () => { + const id = 'logstash-*'; + const type = 'index-pattern'; + const attributes = { title: 'Testing' }; + const namespace = 'foo-namespace'; + const references = [ + { + name: 'ref_0', + type: 'test', + id: '1', + }, + ]; + + const updateSuccess = async (type, id, attributes, options) => { + if (registry.isMultiNamespace(type)) { + const mockGetResponse = getMockGetResponse({ type, id, namespace: options?.namespace }); + callAdminCluster.mockResolvedValueOnce(mockGetResponse); // this._callCluster('get', ...) + } + callAdminCluster.mockResolvedValue({ + _id: `${type}:${id}`, ...mockVersionProps, - _index: '.kibana', - get: { - found: true, - _source: { - type: 'globaltype', - ...mockTimestampFields, - globaltype: { - counter: 1, - }, - }, - }, - })); + result: 'updated', + ...(registry.isMultiNamespace(type) && { + // don't need the rest of the source for test purposes, just the namespaces attribute + get: { _source: { namespaces: [options?.namespace ?? 'default'] } }, + }), + }); // this._writeToCluster('update', ...) + const result = await savedObjectsRepository.update(type, id, attributes, options); + expect(callAdminCluster).toHaveBeenCalledTimes(registry.isMultiNamespace(type) ? 2 : 1); + return result; + }; - await savedObjectsRepository.incrementCounter('globaltype', 'foo', 'counter', { - namespace: 'foo-namespace', + describe('cluster calls', () => { + it(`should use the ES get action then update action when type is multi-namespace`, async () => { + await updateSuccess(MULTI_NAMESPACE_TYPE, id, attributes); + expectClusterCalls('get', 'update'); }); - expect(callAdminCluster).toHaveBeenCalledTimes(1); + it(`should use the ES update action when type is not multi-namespace`, async () => { + await updateSuccess(type, id, attributes); + expectClusterCalls('update'); + }); - const requestDoc = callAdminCluster.mock.calls[0][1]; - expect(requestDoc.id).toBe('globaltype:foo'); - expect(requestDoc.body.script.params.type).toBe('globaltype'); - expect(requestDoc.body.upsert.type).toBe('globaltype'); - expect(requestDoc).toHaveProperty('body.upsert.globaltype'); - }); + it(`defaults to no references array`, async () => { + await updateSuccess(type, id, attributes); + expectClusterCallArgs({ + body: { doc: expect.not.objectContaining({ references: expect.anything() }) }, + }); + }); - it('should assert that the "type" and "counterFieldName" arguments are strings', () => { - expect.assertions(6); - - expect( - savedObjectsRepository.incrementCounter(null, '6.0.0-alpha1', 'buildNum', { - namespace: 'foo-namespace', - }) - ).rejects.toEqual(new Error('"type" argument must be a string')); - - expect( - savedObjectsRepository.incrementCounter(42, '6.0.0-alpha1', 'buildNum', { - namespace: 'foo-namespace', - }) - ).rejects.toEqual(new Error('"type" argument must be a string')); - - expect( - savedObjectsRepository.incrementCounter({}, '6.0.0-alpha1', 'buildNum', { - namespace: 'foo-namespace', - }) - ).rejects.toEqual(new Error('"type" argument must be a string')); - - expect( - savedObjectsRepository.incrementCounter('config', '6.0.0-alpha1', null, { - namespace: 'foo-namespace', - }) - ).rejects.toEqual(new Error('"counterFieldName" argument must be a string')); - - expect( - savedObjectsRepository.incrementCounter('config', '6.0.0-alpha1', 42, { - namespace: 'foo-namespace', - }) - ).rejects.toEqual(new Error('"counterFieldName" argument must be a string')); - - expect( - savedObjectsRepository.incrementCounter( - 'config', - '6.0.0-alpha1', - {}, - { - namespace: 'foo-namespace', - } - ) - ).rejects.toEqual(new Error('"counterFieldName" argument must be a string')); - }); - }); + it(`accepts custom references array`, async () => { + const test = async references => { + await updateSuccess(type, id, attributes, { references }); + expectClusterCallArgs({ + body: { doc: expect.objectContaining({ references }) }, + }); + callAdminCluster.mockReset(); + }; + await test(references); + await test(['string']); + await test([]); + }); + + it(`doesn't accept custom references if not an array`, async () => { + const test = async references => { + await updateSuccess(type, id, attributes, { references }); + expectClusterCallArgs({ + body: { doc: expect.not.objectContaining({ references: expect.anything() }) }, + }); + callAdminCluster.mockReset(); + }; + await test('string'); + await test(123); + await test(true); + await test(null); + }); + + it(`defaults to a refresh setting of wait_for`, async () => { + await updateSuccess(type, id, { foo: 'bar' }); + expectClusterCallArgs({ refresh: 'wait_for' }); + }); + + it(`accepts a custom refresh setting`, async () => { + const refresh = 'foo'; + await updateSuccess(type, id, { foo: 'bar' }, { refresh }); + expectClusterCallArgs({ refresh }); + }); + + it(`defaults to the version of the existing document when type is multi-namespace`, async () => { + await updateSuccess(MULTI_NAMESPACE_TYPE, id, attributes, { references }); + const versionProperties = { + if_seq_no: mockVersionProps._seq_no, + if_primary_term: mockVersionProps._primary_term, + }; + expectClusterCallArgs(versionProperties, 2); + }); + + it(`accepts version`, async () => { + await updateSuccess(type, id, attributes, { + version: encodeHitVersion({ _seq_no: 100, _primary_term: 200 }), + }); + expectClusterCallArgs({ if_seq_no: 100, if_primary_term: 200 }); + }); - describe('types on custom index', () => { - it("should error when attempting to 'update' an unsupported type", async () => { - await expect( - savedObjectsRepository.update('hiddenType', 'bogus', { title: 'some title' }) - ).rejects.toEqual(new Error('Saved object [hiddenType/bogus] not found')); - }); - }); + it(`prepends namespace to the id when providing namespace for single-namespace type`, async () => { + await updateSuccess(type, id, attributes, { namespace }); + expectClusterCallArgs({ id: expect.stringMatching(`${namespace}:${type}:${id}`) }); + }); - describe('unsupported types', () => { - it("should error when attempting to 'update' an unsupported type", async () => { - await expect( - savedObjectsRepository.update('hiddenType', 'bogus', { title: 'some title' }) - ).rejects.toEqual(new Error('Saved object [hiddenType/bogus] not found')); - }); + it(`doesn't prepend namespace to the id when providing no namespace for single-namespace type`, async () => { + await updateSuccess(type, id, attributes, { references }); + expectClusterCallArgs({ id: expect.stringMatching(`${type}:${id}`) }); + }); - it("should error when attempting to 'get' an unsupported type", async () => { - await expect(savedObjectsRepository.get('hiddenType')).rejects.toEqual( - new Error('Not Found') - ); - }); + it(`doesn't prepend namespace to the id when not using single-namespace type`, async () => { + await updateSuccess(NAMESPACE_AGNOSTIC_TYPE, id, attributes, { namespace }); + expectClusterCallArgs({ id: expect.stringMatching(`${NAMESPACE_AGNOSTIC_TYPE}:${id}`) }); - it("should return an error object when attempting to 'create' an unsupported type", async () => { - await expect( - savedObjectsRepository.create('hiddenType', { title: 'some title' }) - ).rejects.toEqual(new Error("Unsupported saved object type: 'hiddenType': Bad Request")); - }); + callAdminCluster.mockReset(); + await updateSuccess(MULTI_NAMESPACE_TYPE, id, attributes, { namespace }); + expectClusterCallArgs({ id: expect.stringMatching(`${MULTI_NAMESPACE_TYPE}:${id}`) }, 2); + }); - it("should not return hidden saved ojects when attempting to 'find' support and unsupported types", async () => { - callAdminCluster.mockReturnValue({ - hits: { - total: 1, - hits: [ - { - _id: 'one', - _source: { - updated_at: mockTimestamp, - type: 'config', - }, - references: [], - }, - ], - }, + it(`includes _sourceIncludes when type is multi-namespace`, async () => { + await updateSuccess(MULTI_NAMESPACE_TYPE, id, attributes); + expectClusterCallArgs({ _sourceIncludes: ['namespaces'] }, 2); }); - const results = await savedObjectsRepository.find({ type: ['hiddenType', 'config'] }); - expect(results).toEqual({ - total: 1, - saved_objects: [ - { - id: 'one', - references: [], - type: 'config', - updated_at: mockTimestamp, - }, - ], - page: 1, - per_page: 20, + + it(`doesn't include _sourceIncludes when type is not multi-namespace`, async () => { + await updateSuccess(type, id, attributes); + expect(callAdminCluster).toHaveBeenLastCalledWith( + expect.any(String), + expect.not.objectContaining({ + _sourceIncludes: expect.anything(), + }) + ); }); }); - it("should return empty results when attempting to 'find' an unsupported type", async () => { - callAdminCluster.mockReturnValue({ - hits: { - total: 0, - hits: [], - }, + describe('errors', () => { + const expectNotFoundError = async (type, id) => { + await expect(savedObjectsRepository.update(type, id)).rejects.toThrowError( + createGenericNotFoundError(type, id) + ); + }; + + it(`throws when type is invalid`, async () => { + await expectNotFoundError('unknownType', id); + expect(callAdminCluster).not.toHaveBeenCalled(); }); - const results = await savedObjectsRepository.find({ type: 'hiddenType' }); - expect(results).toEqual({ - total: 0, - saved_objects: [], - page: 1, - per_page: 20, + + it(`throws when type is hidden`, async () => { + await expectNotFoundError(HIDDEN_TYPE, id); + expect(callAdminCluster).not.toHaveBeenCalled(); }); - }); - it("should return empty results when attempting to 'find' more than one unsupported types", async () => { - const findParams = { type: ['hiddenType', 'hiddenType2'] }; - callAdminCluster.mockReturnValue({ - status: 200, - hits: { - total: 0, - hits: [], - }, + it(`throws when ES is unable to find the document during get`, async () => { + callAdminCluster.mockResolvedValue({ found: false }); // this._callCluster('get', ...) + await expectNotFoundError(MULTI_NAMESPACE_TYPE, id); + expectClusterCalls('get'); }); - const results = await savedObjectsRepository.find(findParams); - expect(results).toEqual({ - total: 0, - saved_objects: [], - page: 1, - per_page: 20, + + it(`throws when ES is unable to find the index during get`, async () => { + callAdminCluster.mockResolvedValue({ status: 404 }); // this._callCluster('get', ...) + await expectNotFoundError(MULTI_NAMESPACE_TYPE, id); + expectClusterCalls('get'); }); - }); - it("should error when attempting to 'delete' hidden types", async () => { - await expect(savedObjectsRepository.delete('hiddenType')).rejects.toEqual( - new Error('Not Found') - ); + it(`throws when type is multi-namespace and the document exists, but not in this namespace`, async () => { + const response = getMockGetResponse({ type: MULTI_NAMESPACE_TYPE, id, namespace }); + callAdminCluster.mockResolvedValue(response); // this._callCluster('get', ...) + await expectNotFoundError(MULTI_NAMESPACE_TYPE, id, { namespace: 'bar-namespace' }); + expectClusterCalls('get'); + }); + + it(`throws when ES is unable to find the document during update`, async () => { + callAdminCluster.mockResolvedValue({ status: 404 }); // this._writeToCluster('update', ...) + await expectNotFoundError(type, id); + expectClusterCalls('update'); + }); }); - it("should error when attempting to 'bulkCreate' an unsupported type", async () => { - callAdminCluster.mockReturnValue({ - items: [ - { - index: { - _id: 'one', - _seq_no: 1, - _primary_term: 1, - _type: 'config', - attributes: { - title: 'Test One', - }, - }, - }, - ], - }); - const results = await savedObjectsRepository.bulkCreate([ - { type: 'config', id: 'one', attributes: { title: 'Test One' } }, - { type: 'hiddenType', id: 'two', attributes: { title: 'Test Two' } }, - ]); - expect(results).toEqual({ - saved_objects: [ - { - type: 'config', - id: 'one', - attributes: { title: 'Test One' }, - references: [], - version: 'WzEsMV0=', - updated_at: mockTimestamp, - }, - { - error: { - error: 'Bad Request', - message: "Unsupported saved object type: 'hiddenType': Bad Request", - statusCode: 400, - }, - id: 'two', - type: 'hiddenType', - }, - ], + describe('migration', () => { + it(`waits until migrations are complete before proceeding`, async () => { + let callAdminClusterCount = 0; + migrator.runMigrations = jest.fn(async () => + // runMigrations should resolve before callAdminCluster is initiated + expect(callAdminCluster).toHaveBeenCalledTimes(callAdminClusterCount++) + ); + await expect(updateSuccess(type, id, attributes)).resolves.toBeDefined(); + expect(migrator.runMigrations).toHaveReturnedTimes(1); }); }); - it("should error when attempting to 'incrementCounter' for an unsupported type", async () => { - await expect( - savedObjectsRepository.incrementCounter('hiddenType', 'doesntmatter', 'fieldArg') - ).rejects.toEqual(new Error("Unsupported saved object type: 'hiddenType': Bad Request")); + describe('returns', () => { + it(`returns _seq_no and _primary_term encoded as version`, async () => { + const result = await updateSuccess(type, id, attributes, { + namespace, + references, + }); + expect(result).toEqual({ + id, + type, + ...mockTimestampFields, + version: mockVersion, + attributes, + references, + }); + }); + + it(`includes namespaces if type is multi-namespace`, async () => { + const result = await updateSuccess(MULTI_NAMESPACE_TYPE, id, attributes); + expect(result).toMatchObject({ + namespaces: expect.any(Array), + }); + }); + + it(`doesn't include namespaces if type is not multi-namespace`, async () => { + const result = await updateSuccess(type, id, attributes); + expect(result).not.toMatchObject({ + namespaces: expect.anything(), + }); + }); }); }); }); diff --git a/src/core/server/saved_objects/service/lib/repository.ts b/src/core/server/saved_objects/service/lib/repository.ts index 72a7867854b60..5f17c11792763 100644 --- a/src/core/server/saved_objects/service/lib/repository.ts +++ b/src/core/server/saved_objects/service/lib/repository.ts @@ -45,6 +45,8 @@ import { SavedObjectsBulkUpdateObject, SavedObjectsBulkUpdateOptions, SavedObjectsDeleteOptions, + SavedObjectsAddToNamespacesOptions, + SavedObjectsDeleteFromNamespacesOptions, } from '../saved_objects_client'; import { SavedObject, @@ -60,20 +62,12 @@ import { validateConvertFilterToKueryNode } from './filter_utils'; // so any breaking changes to this repository are considered breaking changes to the SavedObjectsClient. // eslint-disable-next-line @typescript-eslint/consistent-type-definitions -type Left = { - tag: 'Left'; - error: T; -}; +type Left = { tag: 'Left'; error: Record }; // eslint-disable-next-line @typescript-eslint/consistent-type-definitions -type Right = { - tag: 'Right'; - value: T; -}; - -type Either = Left | Right; -const isLeft = (either: Either): either is Left => { - return either.tag === 'Left'; -}; +type Right = { tag: 'Right'; value: Record }; +type Either = Left | Right; +const isLeft = (either: Either): either is Left => either.tag === 'Left'; +const isRight = (either: Either): either is Right => either.tag === 'Right'; export interface SavedObjectsRepositoryOptions { index: string; @@ -220,8 +214,8 @@ export class SavedObjectsRepository { const { id, migrationVersion, - overwrite = false, namespace, + overwrite = false, references = [], refresh = DEFAULT_REFRESH_SETTING, } = options; @@ -230,22 +224,36 @@ export class SavedObjectsRepository { throw SavedObjectsErrorHelpers.createUnsupportedTypeError(type); } - const method = id && !overwrite ? 'create' : 'index'; const time = this._getCurrentTime(); + let savedObjectNamespace; + let savedObjectNamespaces: string[] | undefined; + + if (this._registry.isSingleNamespace(type) && namespace) { + savedObjectNamespace = namespace; + } else if (this._registry.isMultiNamespace(type)) { + if (id && overwrite) { + // we will overwrite a multi-namespace saved object if it exists; if that happens, ensure we preserve its included namespaces + savedObjectNamespaces = await this.preflightGetNamespaces(type, id, namespace); + } else { + savedObjectNamespaces = getSavedObjectNamespaces(namespace); + } + } try { const migrated = this._migrator.migrateDocument({ id, type, - namespace, + ...(savedObjectNamespace && { namespace: savedObjectNamespace }), + ...(savedObjectNamespaces && { namespaces: savedObjectNamespaces }), attributes, migrationVersion, updated_at: time, - references, + ...(Array.isArray(references) && { references }), }); const raw = this._serializer.savedObjectToRaw(migrated as SavedObjectSanitizedDoc); + const method = id && overwrite ? 'index' : 'create'; const response = await this._writeToCluster(method, { id: raw._id, index: this.getIndexForType(type), @@ -282,10 +290,9 @@ export class SavedObjectsRepository { ): Promise> { const { namespace, overwrite = false, refresh = DEFAULT_REFRESH_SETTING } = options; const time = this._getCurrentTime(); - const bulkCreateParams: object[] = []; - let requestIndexCounter = 0; - const expectedResults: Array> = objects.map(object => { + let bulkGetRequestIndexCounter = 0; + const expectedResults: Either[] = objects.map(object => { if (!this._allowedTypes.includes(object.type)) { return { tag: 'Left' as 'Left', @@ -297,9 +304,73 @@ export class SavedObjectsRepository { }; } - const method = object.id && !overwrite ? 'create' : 'index'; + const method = object.id && overwrite ? 'index' : 'create'; + const requiresNamespacesCheck = + method === 'index' && this._registry.isMultiNamespace(object.type); + + return { + tag: 'Right' as 'Right', + value: { + method, + object, + ...(requiresNamespacesCheck && { esRequestIndex: bulkGetRequestIndexCounter++ }), + }, + }; + }); + + const bulkGetDocs = expectedResults + .filter(isRight) + .filter(({ value }) => value.esRequestIndex !== undefined) + .map(({ value: { object: { type, id } } }) => ({ + _id: this._serializer.generateRawId(namespace, type, id), + _index: this.getIndexForType(type), + _source: ['type', 'namespaces'], + })); + const bulkGetResponse = bulkGetDocs.length + ? await this._callCluster('mget', { + body: { + docs: bulkGetDocs, + }, + ignore: [404], + }) + : undefined; + + let bulkRequestIndexCounter = 0; + const bulkCreateParams: object[] = []; + const expectedBulkResults: Either[] = expectedResults.map(expectedBulkGetResult => { + if (isLeft(expectedBulkGetResult)) { + return expectedBulkGetResult; + } + + let savedObjectNamespace; + let savedObjectNamespaces; + const { esRequestIndex, object, method } = expectedBulkGetResult.value; + if (esRequestIndex !== undefined) { + const indexFound = bulkGetResponse.status !== 404; + const actualResult = indexFound ? bulkGetResponse.docs[esRequestIndex] : undefined; + const docFound = indexFound && actualResult.found === true; + if (docFound && !this.rawDocExistsInNamespace(actualResult, namespace)) { + const { id, type } = object; + return { + tag: 'Left' as 'Left', + error: { + id, + type, + error: SavedObjectsErrorHelpers.createConflictError(type, id).output.payload, + }, + }; + } + savedObjectNamespaces = getSavedObjectNamespaces(namespace, docFound && actualResult); + } else { + if (this._registry.isSingleNamespace(object.type)) { + savedObjectNamespace = namespace; + } else if (this._registry.isMultiNamespace(object.type)) { + savedObjectNamespaces = getSavedObjectNamespaces(namespace); + } + } + const expectedResult = { - esRequestIndex: requestIndexCounter++, + esRequestIndex: bulkRequestIndexCounter++, requestedId: object.id, rawMigratedDoc: this._serializer.savedObjectToRaw( this._migrator.migrateDocument({ @@ -307,7 +378,8 @@ export class SavedObjectsRepository { type: object.type, attributes: object.attributes, migrationVersion: object.migrationVersion, - namespace, + ...(savedObjectNamespace && { namespace: savedObjectNamespace }), + ...(savedObjectNamespaces && { namespaces: savedObjectNamespaces }), updated_at: time, references: object.references || [], }) as SavedObjectSanitizedDoc @@ -327,19 +399,21 @@ export class SavedObjectsRepository { return { tag: 'Right' as 'Right', value: expectedResult }; }); - const esResponse = await this._writeToCluster('bulk', { - refresh, - body: bulkCreateParams, - }); + const bulkResponse = bulkCreateParams.length + ? await this._writeToCluster('bulk', { + refresh, + body: bulkCreateParams, + }) + : undefined; return { - saved_objects: expectedResults.map(expectedResult => { + saved_objects: expectedBulkResults.map(expectedResult => { if (isLeft(expectedResult)) { - return expectedResult.error; + return expectedResult.error as any; } const { requestedId, rawMigratedDoc, esRequestIndex } = expectedResult.value; - const response = esResponse.items[esRequestIndex]; + const response = bulkResponse.items[esRequestIndex]; const { error, _id: responseId, @@ -348,7 +422,7 @@ export class SavedObjectsRepository { } = Object.values(response)[0] as any; const { - _source: { type, [type]: attributes, references = [] }, + _source: { type, [type]: attributes, references = [], namespaces }, } = rawMigratedDoc; const id = requestedId || responseId; @@ -362,6 +436,7 @@ export class SavedObjectsRepository { return { id, type, + ...(namespaces && { namespaces }), updated_at: time, version: encodeVersion(seqNo, primaryTerm), attributes, @@ -382,32 +457,76 @@ export class SavedObjectsRepository { */ async delete(type: string, id: string, options: SavedObjectsDeleteOptions = {}): Promise<{}> { if (!this._allowedTypes.includes(type)) { - throw SavedObjectsErrorHelpers.createGenericNotFoundError(); + throw SavedObjectsErrorHelpers.createGenericNotFoundError(type, id); } const { namespace, refresh = DEFAULT_REFRESH_SETTING } = options; - const response = await this._writeToCluster('delete', { - id: this._serializer.generateRawId(namespace, type, id), + const rawId = this._serializer.generateRawId(namespace, type, id); + let preflightResult: SavedObjectsRawDoc | undefined; + + if (this._registry.isMultiNamespace(type)) { + preflightResult = await this.preflightCheckIncludesNamespace(type, id, namespace); + const existingNamespaces = getSavedObjectNamespaces(undefined, preflightResult); + const remainingNamespaces = existingNamespaces?.filter( + x => x !== getNamespaceString(namespace) + ); + + if (remainingNamespaces?.length) { + // if there is 1 or more namespace remaining, update the saved object + const time = this._getCurrentTime(); + + const doc = { + updated_at: time, + namespaces: remainingNamespaces, + }; + + const updateResponse = await this._writeToCluster('update', { + id: rawId, + index: this.getIndexForType(type), + ...getExpectedVersionProperties(undefined, preflightResult), + refresh, + ignore: [404], + body: { + doc, + }, + }); + + if (updateResponse.status === 404) { + // see "404s from missing index" above + throw SavedObjectsErrorHelpers.createGenericNotFoundError(type, id); + } + return {}; + } + } + + const deleteResponse = await this._writeToCluster('delete', { + id: rawId, index: this.getIndexForType(type), + ...getExpectedVersionProperties(undefined, preflightResult), refresh, ignore: [404], }); - const deleted = response.result === 'deleted'; + const deleted = deleteResponse.result === 'deleted'; if (deleted) { return {}; } - const docNotFound = response.result === 'not_found'; - const indexNotFound = response.error && response.error.type === 'index_not_found_exception'; - if (docNotFound || indexNotFound) { + const deleteDocNotFound = deleteResponse.result === 'not_found'; + const deleteIndexNotFound = + deleteResponse.error && deleteResponse.error.type === 'index_not_found_exception'; + if (deleteDocNotFound || deleteIndexNotFound) { // see "404s from missing index" above throw SavedObjectsErrorHelpers.createGenericNotFoundError(type, id); } throw new Error( - `Unexpected Elasticsearch DELETE response: ${JSON.stringify({ type, id, response })}` + `Unexpected Elasticsearch DELETE response: ${JSON.stringify({ + type, + id, + response: deleteResponse, + })}` ); } @@ -426,25 +545,37 @@ export class SavedObjectsRepository { } const { refresh = DEFAULT_REFRESH_SETTING } = options; - const allTypes = Object.keys(getRootPropertiesObjects(this._mappings)); + const typesToUpdate = allTypes.filter(type => !this._registry.isNamespaceAgnostic(type)); - const typesToDelete = allTypes.filter(type => !this._registry.isNamespaceAgnostic(type)); - - const esOptions = { - index: this.getIndicesForTypes(typesToDelete), + const updateOptions = { + index: this.getIndicesForTypes(typesToUpdate), ignore: [404], refresh, body: { + script: { + source: ` + if (!ctx._source.containsKey('namespaces')) { + ctx.op = "delete"; + } else { + ctx._source['namespaces'].removeAll(Collections.singleton(params['namespace'])); + if (ctx._source['namespaces'].empty) { + ctx.op = "delete"; + } + } + `, + lang: 'painless', + params: { namespace: getNamespaceString(namespace) }, + }, conflicts: 'proceed', ...getSearchDsl(this._mappings, this._registry, { namespace, - type: typesToDelete, + type: typesToUpdate, }), }, }; - return await this._writeToCluster('deleteByQuery', esOptions); + return await this._writeToCluster('updateByQuery', updateOptions); } /** @@ -586,55 +717,77 @@ export class SavedObjectsRepository { return { saved_objects: [] }; } - const unsupportedTypeObjects = objects - .filter(o => !this._allowedTypes.includes(o.type)) - .map(({ type, id }) => { - return ({ - id, - type, - error: SavedObjectsErrorHelpers.createUnsupportedTypeError(type).output.payload, - } as any) as SavedObject; - }); + let bulkGetRequestIndexCounter = 0; + const expectedBulkGetResults: Either[] = objects.map(object => { + const { type, id, fields } = object; - const supportedTypeObjects = objects.filter(o => this._allowedTypes.includes(o.type)); + if (!this._allowedTypes.includes(type)) { + return { + tag: 'Left' as 'Left', + error: { + id, + type, + error: SavedObjectsErrorHelpers.createUnsupportedTypeError(type).output.payload, + }, + }; + } - const response = await this._callCluster('mget', { - body: { - docs: supportedTypeObjects.map(({ type, id, fields }) => { - return { - _id: this._serializer.generateRawId(namespace, type, id), - _index: this.getIndexForType(type), - _source: includedFields(type, fields), - }; - }), - }, + return { + tag: 'Right' as 'Right', + value: { + type, + id, + fields, + esRequestIndex: bulkGetRequestIndexCounter++, + }, + }; }); + const bulkGetDocs = expectedBulkGetResults + .filter(isRight) + .map(({ value: { type, id, fields } }) => ({ + _id: this._serializer.generateRawId(namespace, type, id), + _index: this.getIndexForType(type), + _source: includedFields(type, fields), + })); + const bulkGetResponse = bulkGetDocs.length + ? await this._callCluster('mget', { + body: { + docs: bulkGetDocs, + }, + ignore: [404], + }) + : undefined; + return { - saved_objects: (response.docs as any[]) - .map((doc, i) => { - const { id, type } = supportedTypeObjects[i]; + saved_objects: expectedBulkGetResults.map(expectedResult => { + if (isLeft(expectedResult)) { + return expectedResult.error as any; + } - if (!doc.found) { - return ({ - id, - type, - error: { statusCode: 404, message: 'Not found' }, - } as any) as SavedObject; - } + const { type, id, esRequestIndex } = expectedResult.value; + const doc = bulkGetResponse.docs[esRequestIndex]; - const time = doc._source.updated_at; - return { + if (!doc.found || !this.rawDocExistsInNamespace(doc, namespace)) { + return ({ id, type, - ...(time && { updated_at: time }), - version: encodeHitVersion(doc), - attributes: doc._source[type], - references: doc._source.references || [], - migrationVersion: doc._source.migrationVersion, - }; - }) - .concat(unsupportedTypeObjects), + error: SavedObjectsErrorHelpers.createGenericNotFoundError(type, id).output.payload, + } as any) as SavedObject; + } + + const time = doc._source.updated_at; + return { + id, + type, + ...(doc._source.namespaces && { namespaces: doc._source.namespaces }), + ...(time && { updated_at: time }), + version: encodeHitVersion(doc), + attributes: doc._source[type], + references: doc._source.references || [], + migrationVersion: doc._source.migrationVersion, + }; + }), }; } @@ -666,7 +819,7 @@ export class SavedObjectsRepository { const docNotFound = response.found === false; const indexNotFound = response.status === 404; - if (docNotFound || indexNotFound) { + if (docNotFound || indexNotFound || !this.rawDocExistsInNamespace(response, namespace)) { // see "404s from missing index" above throw SavedObjectsErrorHelpers.createGenericNotFoundError(type, id); } @@ -676,6 +829,7 @@ export class SavedObjectsRepository { return { id, type, + ...(response._source.namespaces && { namespaces: response._source.namespaces }), ...(updatedAt && { updated_at: updatedAt }), version: encodeHitVersion(response), attributes: response._source[type], @@ -707,29 +861,32 @@ export class SavedObjectsRepository { const { version, namespace, references, refresh = DEFAULT_REFRESH_SETTING } = options; + let preflightResult: SavedObjectsRawDoc | undefined; + if (this._registry.isMultiNamespace(type)) { + preflightResult = await this.preflightCheckIncludesNamespace(type, id, namespace); + } + const time = this._getCurrentTime(); const doc = { [type]: attributes, updated_at: time, - references, + ...(Array.isArray(references) && { references }), }; - if (!Array.isArray(doc.references)) { - delete doc.references; - } - const response = await this._writeToCluster('update', { + const updateResponse = await this._writeToCluster('update', { id: this._serializer.generateRawId(namespace, type, id), index: this.getIndexForType(type), - ...(version && decodeRequestVersion(version)), + ...getExpectedVersionProperties(version, preflightResult), refresh, ignore: [404], body: { doc, }, + ...(this._registry.isMultiNamespace(type) && { _sourceIncludes: ['namespaces'] }), }); - if (response.status === 404) { + if (updateResponse.status === 404) { // see "404s from missing index" above throw SavedObjectsErrorHelpers.createGenericNotFoundError(type, id); } @@ -738,12 +895,168 @@ export class SavedObjectsRepository { id, type, updated_at: time, - version: encodeHitVersion(response), + version: encodeHitVersion(updateResponse), + ...(this._registry.isMultiNamespace(type) && { + namespaces: updateResponse.get._source.namespaces, + }), references, attributes, }; } + /** + * Adds one or more namespaces to a given multi-namespace saved object. This method and + * [`deleteFromNamespaces`]{@link SavedObjectsRepository.deleteFromNamespaces} are the only ways to change which Spaces a multi-namespace + * saved object is shared to. + */ + async addToNamespaces( + type: string, + id: string, + namespaces: string[], + options: SavedObjectsAddToNamespacesOptions = {} + ): Promise<{}> { + if (!this._allowedTypes.includes(type)) { + throw SavedObjectsErrorHelpers.createGenericNotFoundError(type, id); + } + + if (!this._registry.isMultiNamespace(type)) { + throw SavedObjectsErrorHelpers.createBadRequestError( + `${type} doesn't support multiple namespaces` + ); + } + + if (!namespaces.length) { + throw SavedObjectsErrorHelpers.createBadRequestError( + 'namespaces must be a non-empty array of strings' + ); + } + + const { version, namespace, refresh = DEFAULT_REFRESH_SETTING } = options; + + const rawId = this._serializer.generateRawId(undefined, type, id); + const preflightResult = await this.preflightCheckIncludesNamespace(type, id, namespace); + const existingNamespaces = getSavedObjectNamespaces(undefined, preflightResult); + // there should never be a case where a multi-namespace object does not have any existing namespaces + // however, it is a possibility if someone manually modifies the document in Elasticsearch + const time = this._getCurrentTime(); + + const doc = { + updated_at: time, + namespaces: existingNamespaces ? unique(existingNamespaces.concat(namespaces)) : namespaces, + }; + + const updateResponse = await this._writeToCluster('update', { + id: rawId, + index: this.getIndexForType(type), + ...getExpectedVersionProperties(version, preflightResult), + refresh, + ignore: [404], + body: { + doc, + }, + }); + + if (updateResponse.status === 404) { + // see "404s from missing index" above + throw SavedObjectsErrorHelpers.createGenericNotFoundError(type, id); + } + + return {}; + } + + /** + * Removes one or more namespaces from a given multi-namespace saved object. If no namespaces remain, the saved object is deleted + * entirely. This method and [`addToNamespaces`]{@link SavedObjectsRepository.addToNamespaces} are the only ways to change which Spaces a + * multi-namespace saved object is shared to. + */ + async deleteFromNamespaces( + type: string, + id: string, + namespaces: string[], + options: SavedObjectsDeleteFromNamespacesOptions = {} + ): Promise<{}> { + if (!this._allowedTypes.includes(type)) { + throw SavedObjectsErrorHelpers.createGenericNotFoundError(type, id); + } + + if (!this._registry.isMultiNamespace(type)) { + throw SavedObjectsErrorHelpers.createBadRequestError( + `${type} doesn't support multiple namespaces` + ); + } + + if (!namespaces.length) { + throw SavedObjectsErrorHelpers.createBadRequestError( + 'namespaces must be a non-empty array of strings' + ); + } + + const { namespace, refresh = DEFAULT_REFRESH_SETTING } = options; + + const rawId = this._serializer.generateRawId(undefined, type, id); + const preflightResult = await this.preflightCheckIncludesNamespace(type, id, namespace); + const existingNamespaces = getSavedObjectNamespaces(undefined, preflightResult); + // if there are somehow no existing namespaces, allow the operation to proceed and delete this saved object + const remainingNamespaces = existingNamespaces?.filter(x => !namespaces.includes(x)); + + if (remainingNamespaces?.length) { + // if there is 1 or more namespace remaining, update the saved object + const time = this._getCurrentTime(); + + const doc = { + updated_at: time, + namespaces: remainingNamespaces, + }; + + const updateResponse = await this._writeToCluster('update', { + id: rawId, + index: this.getIndexForType(type), + ...getExpectedVersionProperties(undefined, preflightResult), + refresh, + ignore: [404], + body: { + doc, + }, + }); + + if (updateResponse.status === 404) { + // see "404s from missing index" above + throw SavedObjectsErrorHelpers.createGenericNotFoundError(type, id); + } + return {}; + } else { + // if there are no namespaces remaining, delete the saved object + const deleteResponse = await this._writeToCluster('delete', { + id: this._serializer.generateRawId(undefined, type, id), + index: this.getIndexForType(type), + ...getExpectedVersionProperties(undefined, preflightResult), + refresh, + ignore: [404], + }); + + const deleted = deleteResponse.result === 'deleted'; + if (deleted) { + return {}; + } + + const deleteDocNotFound = deleteResponse.result === 'not_found'; + const deleteIndexNotFound = + deleteResponse.error && deleteResponse.error.type === 'index_not_found_exception'; + if (deleteDocNotFound || deleteIndexNotFound) { + // see "404s from missing index" above + throw SavedObjectsErrorHelpers.createGenericNotFoundError(type, id); + } + + throw new Error( + `Unexpected Elasticsearch DELETE response: ${JSON.stringify({ + type, + id, + response: deleteResponse, + })}` + ); + } + } + /** * Updates multiple objects in bulk * @@ -757,10 +1070,10 @@ export class SavedObjectsRepository { options: SavedObjectsBulkUpdateOptions = {} ): Promise> { const time = this._getCurrentTime(); - const bulkUpdateParams: object[] = []; + const { namespace } = options; - let requestIndexCounter = 0; - const expectedResults: Array> = objects.map(object => { + let bulkGetRequestIndexCounter = 0; + const expectedBulkGetResults: Either[] = objects.map(object => { const { type, id } = object; if (!this._allowedTypes.includes(type)) { @@ -775,41 +1088,100 @@ export class SavedObjectsRepository { } const { attributes, references, version } = object; - const { namespace } = options; const documentToSave = { [type]: attributes, updated_at: time, - references, + ...(Array.isArray(references) && { references }), }; - if (!Array.isArray(documentToSave.references)) { - delete documentToSave.references; - } + const requiresNamespacesCheck = this._registry.isMultiNamespace(object.type); - const expectedResult = { - type, - id, - esRequestIndex: requestIndexCounter++, - documentToSave, + return { + tag: 'Right' as 'Right', + value: { + type, + id, + version, + documentToSave, + ...(requiresNamespacesCheck && { esRequestIndex: bulkGetRequestIndexCounter++ }), + }, }; + }); - bulkUpdateParams.push( - { - update: { - _id: this._serializer.generateRawId(namespace, type, id), - _index: this.getIndexForType(type), - ...(version && decodeRequestVersion(version)), + const bulkGetDocs = expectedBulkGetResults + .filter(isRight) + .filter(({ value }) => value.esRequestIndex !== undefined) + .map(({ value: { type, id } }) => ({ + _id: this._serializer.generateRawId(namespace, type, id), + _index: this.getIndexForType(type), + _source: ['type', 'namespaces'], + })); + const bulkGetResponse = bulkGetDocs.length + ? await this._callCluster('mget', { + body: { + docs: bulkGetDocs, }, - }, - { doc: documentToSave } - ); + ignore: [404], + }) + : undefined; - return { tag: 'Right' as 'Right', value: expectedResult }; - }); + let bulkUpdateRequestIndexCounter = 0; + const bulkUpdateParams: object[] = []; + const expectedBulkUpdateResults: Either[] = expectedBulkGetResults.map( + expectedBulkGetResult => { + if (isLeft(expectedBulkGetResult)) { + return expectedBulkGetResult; + } + + const { esRequestIndex, id, type, version, documentToSave } = expectedBulkGetResult.value; + let namespaces; + let versionProperties; + if (esRequestIndex !== undefined) { + const indexFound = bulkGetResponse.status !== 404; + const actualResult = indexFound ? bulkGetResponse.docs[esRequestIndex] : undefined; + const docFound = indexFound && actualResult.found === true; + if (!docFound || !this.rawDocExistsInNamespace(actualResult, namespace)) { + return { + tag: 'Left' as 'Left', + error: { + id, + type, + error: SavedObjectsErrorHelpers.createGenericNotFoundError(type, id).output.payload, + }, + }; + } + namespaces = actualResult._source.namespaces; + versionProperties = getExpectedVersionProperties(version, actualResult); + } else { + versionProperties = getExpectedVersionProperties(version); + } + + const expectedResult = { + type, + id, + namespaces, + esRequestIndex: bulkUpdateRequestIndexCounter++, + documentToSave: expectedBulkGetResult.value.documentToSave, + }; + + bulkUpdateParams.push( + { + update: { + _id: this._serializer.generateRawId(namespace, type, id), + _index: this.getIndexForType(type), + ...versionProperties, + }, + }, + { doc: documentToSave } + ); + + return { tag: 'Right' as 'Right', value: expectedResult }; + } + ); const { refresh = DEFAULT_REFRESH_SETTING } = options; - const esResponse = bulkUpdateParams.length + const bulkUpdateResponse = bulkUpdateParams.length ? await this._writeToCluster('bulk', { refresh, body: bulkUpdateParams, @@ -817,13 +1189,13 @@ export class SavedObjectsRepository { : {}; return { - saved_objects: expectedResults.map(expectedResult => { + saved_objects: expectedBulkUpdateResults.map(expectedResult => { if (isLeft(expectedResult)) { - return expectedResult.error; + return expectedResult.error as any; } - const { type, id, documentToSave, esRequestIndex } = expectedResult.value; - const response = esResponse.items[esRequestIndex]; + const { type, id, namespaces, documentToSave, esRequestIndex } = expectedResult.value; + const response = bulkUpdateResponse.items[esRequestIndex]; const { error, _seq_no: seqNo, _primary_term: primaryTerm } = Object.values( response )[0] as any; @@ -839,6 +1211,7 @@ export class SavedObjectsRepository { return { id, type, + ...(namespaces && { namespaces }), updated_at, version: encodeVersion(seqNo, primaryTerm), attributes, @@ -877,10 +1250,20 @@ export class SavedObjectsRepository { const { migrationVersion, namespace, refresh = DEFAULT_REFRESH_SETTING } = options; const time = this._getCurrentTime(); + let savedObjectNamespace; + let savedObjectNamespaces: string[] | undefined; + + if (this._registry.isSingleNamespace(type) && namespace) { + savedObjectNamespace = namespace; + } else if (this._registry.isMultiNamespace(type)) { + savedObjectNamespaces = await this.preflightGetNamespaces(type, id, namespace); + } const migrated = this._migrator.migrateDocument({ id, type, + ...(savedObjectNamespace && { namespace: savedObjectNamespace }), + ...(savedObjectNamespaces && { namespaces: savedObjectNamespaces }), attributes: { [counterFieldName]: 1 }, migrationVersion, updated_at: time, @@ -889,7 +1272,7 @@ export class SavedObjectsRepository { const raw = this._serializer.savedObjectToRaw(migrated as SavedObjectSanitizedDoc); const response = await this._writeToCluster('update', { - id: this._serializer.generateRawId(namespace, type, id), + id: raw._id, index: this.getIndexForType(type), refresh, _source: true, @@ -952,14 +1335,13 @@ export class SavedObjectsRepository { } /** - * Returns an array of indices as specified in `this._schema` for each of the + * Returns an array of indices as specified in `this._registry` for each of the * given `types`. If any of the types don't have an associated index, the * default index `this._index` will be included. * * @param types The types whose indices should be retrieved */ private getIndicesForTypes(types: string[]) { - const unique = (array: string[]) => [...new Set(array)]; return unique(types.map(t => this.getIndexForType(t))); } @@ -975,12 +1357,97 @@ export class SavedObjectsRepository { const savedObject = this._serializer.rawToSavedObject(raw); return omit(savedObject, 'namespace'); } + + /** + * Check to ensure that a raw document exists in a namespace. If the document is not a multi-namespace type, then this returns `true` as + * we rely on the guarantees of the document ID format. If the document is a multi-namespace type, this checks to ensure that the + * document's `namespaces` value includes the string representation of the given namespace. + * + * WARNING: This should only be used for documents that were retrieved from Elasticsearch. Otherwise, the guarantees of the document ID + * format mentioned above do not apply. + */ + private rawDocExistsInNamespace(raw: SavedObjectsRawDoc, namespace?: string) { + const rawDocType = raw._source.type; + + // if the type is namespace isolated, or namespace agnostic, we can continue to rely on the guarantees + // of the document ID format and don't need to check this + if (!this._registry.isMultiNamespace(rawDocType)) { + return true; + } + + const namespaces = raw._source.namespaces; + return namespaces?.includes(getNamespaceString(namespace)) ?? false; + } + + /** + * Pre-flight check to get a multi-namespace saved object's included namespaces. This ensures that, if the saved object exists, it + * includes the target namespace. + * + * @param type The type of the saved object. + * @param id The ID of the saved object. + * @param namespace The target namespace. + * @returns Array of namespaces that this saved object currently includes, or (if the object does not exist yet) the namespaces that a + * newly-created object will include. Value may be undefined if an existing saved object has no namespaces attribute; this should not + * happen in normal operations, but it is possible if the Elasticsearch document is manually modified. + * @throws Will throw an error if the saved object exists and it does not include the target namespace. + */ + private async preflightGetNamespaces(type: string, id: string, namespace?: string) { + if (!this._registry.isMultiNamespace(type)) { + throw new Error(`Cannot make preflight get request for non-multi-namespace type '${type}'.`); + } + + const response = await this._callCluster('get', { + id: this._serializer.generateRawId(undefined, type, id), + index: this.getIndexForType(type), + ignore: [404], + }); + + const indexFound = response.status !== 404; + const docFound = indexFound && response.found === true; + if (docFound) { + if (!this.rawDocExistsInNamespace(response, namespace)) { + throw SavedObjectsErrorHelpers.createConflictError(type, id); + } + return getSavedObjectNamespaces(namespace, response); + } + return getSavedObjectNamespaces(namespace); + } + + /** + * Pre-flight check for a multi-namespace saved object's namespaces. This ensures that, if the saved object exists, it includes the target + * namespace. + * + * @param type The type of the saved object. + * @param id The ID of the saved object. + * @param namespace The target namespace. + * @returns Raw document from Elasticsearch. + * @throws Will throw an error if the saved object is not found, or if it doesn't include the target namespace. + */ + private async preflightCheckIncludesNamespace(type: string, id: string, namespace?: string) { + if (!this._registry.isMultiNamespace(type)) { + throw new Error(`Cannot make preflight get request for non-multi-namespace type '${type}'.`); + } + + const rawId = this._serializer.generateRawId(undefined, type, id); + const response = await this._callCluster('get', { + id: rawId, + index: this.getIndexForType(type), + ignore: [404], + }); + + const indexFound = response.status !== 404; + const docFound = indexFound && response.found === true; + if (!docFound || !this.rawDocExistsInNamespace(response, namespace)) { + throw SavedObjectsErrorHelpers.createGenericNotFoundError(type, id); + } + return response as SavedObjectsRawDoc; + } } function getBulkOperationError(error: { type: string; reason?: string }, type: string, id: string) { switch (error.type) { case 'version_conflict_engine_exception': - return { statusCode: 409, message: 'version conflict, document already exists' }; + return SavedObjectsErrorHelpers.createConflictError(type, id).output.payload; case 'document_missing_exception': return SavedObjectsErrorHelpers.createGenericNotFoundError(type, id).output.payload; default: @@ -989,3 +1456,49 @@ function getBulkOperationError(error: { type: string; reason?: string }, type: s }; } } + +/** + * Returns an object with the expected version properties. This facilitates Elasticsearch's Optimistic Concurrency Control. + * + * @param version Optional version specified by the consumer. + * @param document Optional existing document that was obtained in a preflight operation. + */ +function getExpectedVersionProperties(version?: string, document?: SavedObjectsRawDoc) { + if (version) { + return decodeRequestVersion(version); + } else if (document) { + return { + if_seq_no: document._seq_no, + if_primary_term: document._primary_term, + }; + } + return {}; +} + +/** + * Returns the string representation of a namespace. + * The default namespace is undefined, and is represented by the string 'default'. + */ +function getNamespaceString(namespace?: string) { + return namespace ?? 'default'; +} + +/** + * Returns a string array of namespaces for a given saved object. If the saved object is undefined, the result is an array that contains the + * current namespace. Value may be undefined if an existing saved object has no namespaces attribute; this should not happen in normal + * operations, but it is possible if the Elasticsearch document is manually modified. + * + * @param namespace The current namespace. + * @param document Optional existing saved object that was obtained in a preflight operation. + */ +function getSavedObjectNamespaces( + namespace?: string, + document?: SavedObjectsRawDoc +): string[] | undefined { + if (document) { + return document._source?.namespaces; + } + return [getNamespaceString(namespace)]; +} + +const unique = (array: string[]) => [...new Set(array)]; diff --git a/src/core/server/saved_objects/service/lib/repository_create_repository.test.ts b/src/core/server/saved_objects/service/lib/repository_create_repository.test.ts index a6b580e9b3461..ea881805e1ae6 100644 --- a/src/core/server/saved_objects/service/lib/repository_create_repository.test.ts +++ b/src/core/server/saved_objects/service/lib/repository_create_repository.test.ts @@ -32,7 +32,7 @@ describe('SavedObjectsRepository#createRepository', () => { typeRegistry.registerType({ name: 'nsAgnosticType', hidden: false, - namespaceAgnostic: true, + namespaceType: 'agnostic', mappings: { properties: { name: { type: 'keyword' }, @@ -44,7 +44,7 @@ describe('SavedObjectsRepository#createRepository', () => { typeRegistry.registerType({ name: 'nsType', hidden: false, - namespaceAgnostic: false, + namespaceType: 'single', indexPattern: 'beats', mappings: { properties: { @@ -56,7 +56,7 @@ describe('SavedObjectsRepository#createRepository', () => { typeRegistry.registerType({ name: 'hiddenType', hidden: true, - namespaceAgnostic: true, + namespaceType: 'agnostic', mappings: { properties: { name: { type: 'keyword' }, diff --git a/src/core/server/saved_objects/service/lib/search_dsl/query_params.test.ts b/src/core/server/saved_objects/service/lib/search_dsl/query_params.test.ts index b2129765ee426..a72c21dd5eee4 100644 --- a/src/core/server/saved_objects/service/lib/search_dsl/query_params.test.ts +++ b/src/core/server/saved_objects/service/lib/search_dsl/query_params.test.ts @@ -17,6 +17,9 @@ * under the License. */ +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { esKuery, KueryNode } from '../../../../../../plugins/data/server'; + import { typeRegistryMock } from '../../../saved_objects_type_registry.mock'; import { getQueryParams } from './query_params'; @@ -24,1268 +27,329 @@ const registry = typeRegistryMock.create(); const MAPPINGS = { properties: { - type: { - type: 'keyword', - }, - pending: { - properties: { - title: { - type: 'text', - }, - }, - }, + pending: { properties: { title: { type: 'text' } } }, saved: { properties: { - title: { - type: 'text', - fields: { - raw: { - type: 'keyword', - }, - }, - }, - obj: { - properties: { - key1: { - type: 'text', - }, - }, - }, - }, - }, - global: { - properties: { - name: { - type: 'keyword', - }, + title: { type: 'text', fields: { raw: { type: 'keyword' } } }, + obj: { properties: { key1: { type: 'text' } } }, }, }, + // mock registry returns isMultiNamespace=true for 'shared' type + shared: { properties: { name: { type: 'keyword' } } }, + // mock registry returns isNamespaceAgnostic=true for 'global' type + global: { properties: { name: { type: 'keyword' } } }, }, }; +const ALL_TYPES = Object.keys(MAPPINGS.properties); +// get all possible subsets (combination) of all types +const ALL_TYPE_SUBSETS = ALL_TYPES.reduce( + (subsets, value) => subsets.concat(subsets.map(set => [...set, value])), + [[] as string[]] +) + .filter(x => x.length) // exclude empty set + .map(x => (x.length === 1 ? x[0] : x)); // if a subset is a single string, destructure it -// create a type clause to be used within the "should", if a namespace is specified -// the clause will ensure the namespace matches; otherwise, the clause will ensure -// that there isn't a namespace field. -const createTypeClause = (type: string, namespace?: string) => { - if (namespace) { - return { - bool: { - must: [{ term: { type } }, { term: { namespace } }], - }, - }; - } +/** + * Note: these tests cases are defined in the order they appear in the source code, for readability's sake + */ +describe('#getQueryParams', () => { + const mappings = MAPPINGS; + type Result = ReturnType; - return { - bool: { - must: [{ term: { type } }], - must_not: [{ exists: { field: 'namespace' } }], - }, - }; -}; + describe('kueryNode filter clause (query.bool.filter[...]', () => { + const expectResult = (result: Result, expected: any) => { + expect(result.query.bool.filter).toEqual(expect.arrayContaining([expected])); + }; -describe('searchDsl/queryParams', () => { - describe('no parameters', () => { - it('searches for all known types without a namespace specified', () => { - expect(getQueryParams({ mappings: MAPPINGS, registry })).toEqual({ - query: { - bool: { - filter: [ - { - bool: { - should: [ - createTypeClause('pending'), - createTypeClause('saved'), - createTypeClause('global'), - ], - minimum_should_match: 1, - }, - }, - ], - }, - }, + describe('`kueryNode` parameter', () => { + it('does not include the clause when `kueryNode` is not specified', () => { + const result = getQueryParams({ mappings, registry, kueryNode: undefined }); + expect(result.query.bool.filter).toHaveLength(1); }); - }); - }); - describe('namespace', () => { - it('filters namespaced types for namespace, and ensures namespace agnostic types have no namespace', () => { - expect(getQueryParams({ mappings: MAPPINGS, registry, namespace: 'foo-namespace' })).toEqual({ - query: { - bool: { - filter: [ - { - bool: { - should: [ - createTypeClause('pending', 'foo-namespace'), - createTypeClause('saved', 'foo-namespace'), - createTypeClause('global'), - ], - minimum_should_match: 1, - }, - }, - ], - }, - }, - }); - }); - }); + it('includes the specified Kuery clause', () => { + const test = (kueryNode: KueryNode) => { + const result = getQueryParams({ mappings, registry, kueryNode }); + const expected = esKuery.toElasticsearchQuery(kueryNode); + expect(result.query.bool.filter).toHaveLength(2); + expectResult(result, expected); + }; - describe('type (singular, namespaced)', () => { - it('includes a terms filter for type and namespace not being specified', () => { - expect( - getQueryParams({ mappings: MAPPINGS, registry, namespace: undefined, type: 'saved' }) - ).toEqual({ - query: { - bool: { - filter: [ - { - bool: { - should: [createTypeClause('saved')], - minimum_should_match: 1, - }, - }, - ], - }, - }, - }); - }); - }); + const simpleClause = { + type: 'function', + function: 'is', + arguments: [ + { type: 'literal', value: 'global.name' }, + { type: 'literal', value: 'GLOBAL' }, + { type: 'literal', value: false }, + ], + } as KueryNode; + test(simpleClause); - describe('type (singular, global)', () => { - it('includes a terms filter for type and namespace not being specified', () => { - expect( - getQueryParams({ mappings: MAPPINGS, registry, namespace: undefined, type: 'global' }) - ).toEqual({ - query: { - bool: { - filter: [ - { - bool: { - should: [createTypeClause('global')], - minimum_should_match: 1, + const complexClause = { + type: 'function', + function: 'and', + arguments: [ + simpleClause, + { + type: 'function', + function: 'not', + arguments: [ + { + type: 'function', + function: 'is', + arguments: [ + { type: 'literal', value: 'saved.obj.key1' }, + { type: 'literal', value: 'key' }, + { type: 'literal', value: true }, + ], }, - }, - ], - }, - }, + ], + }, + ], + } as KueryNode; + test(complexClause); }); }); }); - describe('type (plural, namespaced and global)', () => { - it('includes term filters for types and namespace not being specified', () => { - expect( - getQueryParams({ - mappings: MAPPINGS, - registry, - namespace: undefined, - type: ['saved', 'global'], - }) - ).toEqual({ - query: { - bool: { - filter: [ - { - bool: { - should: [createTypeClause('saved'), createTypeClause('global')], - minimum_should_match: 1, - }, - }, - ], - }, - }, - }); - }); - }); + describe('reference filter clause (query.bool.filter[bool.must])', () => { + describe('`hasReference` parameter', () => { + const expectResult = (result: Result, expected: any) => { + expect(result.query.bool.filter).toEqual( + expect.arrayContaining([{ bool: expect.objectContaining({ must: expected }) }]) + ); + }; - describe('namespace, type (plural, namespaced and global)', () => { - it('includes a terms filter for type and namespace not being specified', () => { - expect( - getQueryParams({ - mappings: MAPPINGS, + it('does not include the clause when `hasReference` is not specified', () => { + const result = getQueryParams({ + mappings, registry, - namespace: 'foo-namespace', - type: ['saved', 'global'], - }) - ).toEqual({ - query: { - bool: { - filter: [ - { - bool: { - should: [createTypeClause('saved', 'foo-namespace'), createTypeClause('global')], - minimum_should_match: 1, - }, - }, - ], - }, - }, + hasReference: undefined, + }); + expectResult(result, undefined); }); - }); - }); - describe('search', () => { - it('includes a sqs query and all known types without a namespace specified', () => { - expect( - getQueryParams({ - mappings: MAPPINGS, + it('creates a clause with query for specified reference', () => { + const hasReference = { id: 'foo', type: 'bar' }; + const result = getQueryParams({ + mappings, registry, - namespace: undefined, - type: undefined, - search: 'us*', - }) - ).toEqual({ - query: { - bool: { - filter: [ - { + hasReference, + }); + expectResult(result, [ + { + nested: { + path: 'references', + query: { bool: { - should: [ - createTypeClause('pending'), - createTypeClause('saved'), - createTypeClause('global'), + must: [ + { term: { 'references.id': hasReference.id } }, + { term: { 'references.type': hasReference.type } }, ], - minimum_should_match: 1, - }, - }, - ], - must: [ - { - simple_query_string: { - query: 'us*', - lenient: true, - fields: ['*'], }, }, - ], + }, }, - }, + ]); }); }); }); - describe('namespace, search', () => { - it('includes a sqs query and namespaced types with the namespace and global types without a namespace', () => { - expect( - getQueryParams({ - mappings: MAPPINGS, - registry, - namespace: 'foo-namespace', - type: undefined, - search: 'us*', - }) - ).toEqual({ - query: { - bool: { - filter: [ - { - bool: { - should: [ - createTypeClause('pending', 'foo-namespace'), - createTypeClause('saved', 'foo-namespace'), - createTypeClause('global'), - ], - minimum_should_match: 1, - }, - }, - ], - must: [ - { - simple_query_string: { - query: 'us*', - lenient: true, - fields: ['*'], - }, - }, - ], - }, - }, - }); - }); - }); + describe('type filter clauses (query.bool.filter[bool.should])', () => { + describe('`type` parameter', () => { + const expectResult = (result: Result, ...types: string[]) => { + expect(result.query.bool.filter).toEqual( + expect.arrayContaining([ + { + bool: expect.objectContaining({ + should: types.map(type => ({ + bool: expect.objectContaining({ + must: expect.arrayContaining([{ term: { type } }]), + }), + })), + minimum_should_match: 1, + }), + }, + ]) + ); + }; - describe('type (plural, namespaced and global), search', () => { - it('includes a sqs query and types without a namespace', () => { - expect( - getQueryParams({ - mappings: MAPPINGS, - registry, - namespace: undefined, - type: ['saved', 'global'], - search: 'us*', - }) - ).toEqual({ - query: { - bool: { - filter: [ - { - bool: { - should: [createTypeClause('saved'), createTypeClause('global')], - minimum_should_match: 1, - }, - }, - ], - must: [ - { - simple_query_string: { - query: 'us*', - lenient: true, - fields: ['*'], - }, - }, - ], - }, - }, + it('searches for all known types when `type` is not specified', () => { + const result = getQueryParams({ mappings, registry, type: undefined }); + expectResult(result, ...ALL_TYPES); }); - }); - }); - describe('namespace, type (plural, namespaced and global), search', () => { - it('includes a sqs query and namespace type with a namespace and global type without a namespace', () => { - expect( - getQueryParams({ - mappings: MAPPINGS, - registry, - namespace: 'foo-namespace', - type: ['saved', 'global'], - search: 'us*', - }) - ).toEqual({ - query: { - bool: { - filter: [ - { - bool: { - should: [createTypeClause('saved', 'foo-namespace'), createTypeClause('global')], - minimum_should_match: 1, - }, - }, - ], - must: [ - { - simple_query_string: { - query: 'us*', - lenient: true, - fields: ['*'], - }, - }, - ], - }, - }, + it('searches for specified type/s', () => { + const test = (typeOrTypes: string | string[]) => { + const result = getQueryParams({ + mappings, + registry, + type: typeOrTypes, + }); + const type = Array.isArray(typeOrTypes) ? typeOrTypes : [typeOrTypes]; + expectResult(result, ...type); + }; + for (const typeOrTypes of ALL_TYPE_SUBSETS) { + test(typeOrTypes); + } }); }); - }); - describe('search, searchFields', () => { - it('includes all types for field', () => { - expect( - getQueryParams({ - mappings: MAPPINGS, - registry, - namespace: undefined, - type: undefined, - search: 'y*', - searchFields: ['title'], - }) - ).toEqual({ - query: { - bool: { - filter: [ - { - bool: { - should: [ - createTypeClause('pending'), - createTypeClause('saved'), - createTypeClause('global'), - ], - minimum_should_match: 1, - }, - }, - ], - must: [ - { - simple_query_string: { - query: 'y*', - fields: ['pending.title', 'saved.title', 'global.title'], - }, - }, - ], - }, - }, - }); - }); - it('supports field boosting', () => { - expect( - getQueryParams({ - mappings: MAPPINGS, - registry, - namespace: undefined, - type: undefined, - search: 'y*', - searchFields: ['title^3'], - }) - ).toEqual({ - query: { - bool: { - filter: [ - { - bool: { - should: [ - createTypeClause('pending'), - createTypeClause('saved'), - createTypeClause('global'), - ], - minimum_should_match: 1, - }, - }, - ], - must: [ - { - simple_query_string: { - query: 'y*', - fields: ['pending.title^3', 'saved.title^3', 'global.title^3'], - }, - }, - ], - }, - }, + describe('`namespace` parameter', () => { + const createTypeClause = (type: string, namespace?: string) => { + if (registry.isMultiNamespace(type)) { + return { + bool: { + must: expect.arrayContaining([{ term: { namespaces: namespace ?? 'default' } }]), + must_not: [{ exists: { field: 'namespace' } }], + }, + }; + } else if (namespace && registry.isSingleNamespace(type)) { + return { + bool: { + must: expect.arrayContaining([{ term: { namespace } }]), + must_not: [{ exists: { field: 'namespaces' } }], + }, + }; + } + // isNamespaceAgnostic + return { + bool: expect.objectContaining({ + must_not: [{ exists: { field: 'namespace' } }, { exists: { field: 'namespaces' } }], + }), + }; + }; + + const expectResult = (result: Result, ...typeClauses: any) => { + expect(result.query.bool.filter).toEqual( + expect.arrayContaining([ + { bool: expect.objectContaining({ should: typeClauses, minimum_should_match: 1 }) }, + ]) + ); + }; + + const test = (namespace?: string) => { + for (const typeOrTypes of ALL_TYPE_SUBSETS) { + const result = getQueryParams({ mappings, registry, type: typeOrTypes, namespace }); + const types = Array.isArray(typeOrTypes) ? typeOrTypes : [typeOrTypes]; + expectResult(result, ...types.map(x => createTypeClause(x, namespace))); + } + // also test with no specified type/s + const result = getQueryParams({ mappings, registry, type: undefined, namespace }); + expectResult(result, ...ALL_TYPES.map(x => createTypeClause(x, namespace))); + }; + + it('filters results with "namespace" field when `namespace` is not specified', () => { + test(undefined); }); - }); - it('supports field and multi-field', () => { - expect( - getQueryParams({ - mappings: MAPPINGS, - registry, - namespace: undefined, - type: undefined, - search: 'y*', - searchFields: ['title', 'title.raw'], - }) - ).toEqual({ - query: { - bool: { - filter: [ - { - bool: { - should: [ - createTypeClause('pending'), - createTypeClause('saved'), - createTypeClause('global'), - ], - minimum_should_match: 1, - }, - }, - ], - must: [ - { - simple_query_string: { - query: 'y*', - fields: [ - 'pending.title', - 'saved.title', - 'global.title', - 'pending.title.raw', - 'saved.title.raw', - 'global.title.raw', - ], - }, - }, - ], - }, - }, + + it('filters results for specified namespace for appropriate type/s', () => { + test('foo-namespace'); }); }); }); - describe('namespace, search, searchFields', () => { - it('includes all types for field', () => { - expect( - getQueryParams({ - mappings: MAPPINGS, + describe('search clause (query.bool.must.simple_query_string)', () => { + const search = 'foo*'; + + const expectResult = (result: Result, sqsClause: any) => { + expect(result.query.bool.must).toEqual([{ simple_query_string: sqsClause }]); + }; + + describe('`search` parameter', () => { + it('does not include clause when `search` is not specified', () => { + const result = getQueryParams({ + mappings, registry, - namespace: 'foo-namespace', - type: undefined, - search: 'y*', - searchFields: ['title'], - }) - ).toEqual({ - query: { - bool: { - filter: [ - { - bool: { - should: [ - createTypeClause('pending', 'foo-namespace'), - createTypeClause('saved', 'foo-namespace'), - createTypeClause('global'), - ], - minimum_should_match: 1, - }, - }, - ], - must: [ - { - simple_query_string: { - query: 'y*', - fields: ['pending.title', 'saved.title', 'global.title'], - }, - }, - ], - }, - }, + search: undefined, + }); + expect(result.query.bool.must).toBeUndefined(); }); - }); - it('supports field boosting', () => { - expect( - getQueryParams({ - mappings: MAPPINGS, + + it('creates a clause with query for specified search', () => { + const result = getQueryParams({ + mappings, registry, - namespace: 'foo-namespace', - type: undefined, - search: 'y*', - searchFields: ['title^3'], - }) - ).toEqual({ - query: { - bool: { - filter: [ - { - bool: { - should: [ - createTypeClause('pending', 'foo-namespace'), - createTypeClause('saved', 'foo-namespace'), - createTypeClause('global'), - ], - minimum_should_match: 1, - }, - }, - ], - must: [ - { - simple_query_string: { - query: 'y*', - fields: ['pending.title^3', 'saved.title^3', 'global.title^3'], - }, - }, - ], - }, - }, + search, + }); + expectResult(result, expect.objectContaining({ query: search })); }); }); - it('supports field and multi-field', () => { - expect( - getQueryParams({ - mappings: MAPPINGS, + + describe('`searchFields` parameter', () => { + const getExpectedFields = (searchFields: string[], typeOrTypes: string | string[]) => { + const types = Array.isArray(typeOrTypes) ? typeOrTypes : [typeOrTypes]; + return searchFields.map(x => types.map(y => `${y}.${x}`)).flat(); + }; + + const test = (searchFields: string[]) => { + for (const typeOrTypes of ALL_TYPE_SUBSETS) { + const result = getQueryParams({ + mappings, + registry, + type: typeOrTypes, + search, + searchFields, + }); + const fields = getExpectedFields(searchFields, typeOrTypes); + expectResult(result, expect.objectContaining({ fields })); + } + // also test with no specified type/s + const result = getQueryParams({ + mappings, registry, - namespace: 'foo-namespace', type: undefined, - search: 'y*', - searchFields: ['title', 'title.raw'], - }) - ).toEqual({ - query: { - bool: { - filter: [ - { - bool: { - should: [ - createTypeClause('pending', 'foo-namespace'), - createTypeClause('saved', 'foo-namespace'), - createTypeClause('global'), - ], - minimum_should_match: 1, - }, - }, - ], - must: [ - { - simple_query_string: { - query: 'y*', - fields: [ - 'pending.title', - 'saved.title', - 'global.title', - 'pending.title.raw', - 'saved.title.raw', - 'global.title.raw', - ], - }, - }, - ], - }, - }, - }); - }); - }); + search, + searchFields, + }); + const fields = getExpectedFields(searchFields, ALL_TYPES); + expectResult(result, expect.objectContaining({ fields })); + }; - describe('type (plural, namespaced and global), search, searchFields', () => { - it('includes all types for field', () => { - expect( - getQueryParams({ - mappings: MAPPINGS, - registry, - namespace: undefined, - type: ['saved', 'global'], - search: 'y*', - searchFields: ['title'], - }) - ).toEqual({ - query: { - bool: { - filter: [ - { - bool: { - should: [createTypeClause('saved'), createTypeClause('global')], - minimum_should_match: 1, - }, - }, - ], - must: [ - { - simple_query_string: { - query: 'y*', - fields: ['saved.title', 'global.title'], - }, - }, - ], - }, - }, - }); - }); - it('supports field boosting', () => { - expect( - getQueryParams({ - mappings: MAPPINGS, - registry, - namespace: undefined, - type: ['saved', 'global'], - search: 'y*', - searchFields: ['title^3'], - }) - ).toEqual({ - query: { - bool: { - filter: [ - { - bool: { - should: [createTypeClause('saved'), createTypeClause('global')], - minimum_should_match: 1, - }, - }, - ], - must: [ - { - simple_query_string: { - query: 'y*', - fields: ['saved.title^3', 'global.title^3'], - }, - }, - ], - }, - }, - }); - }); - it('supports field and multi-field', () => { - expect( - getQueryParams({ - mappings: MAPPINGS, + it('includes lenient flag and all fields when `searchFields` is not specified', () => { + const result = getQueryParams({ + mappings, registry, - namespace: undefined, - type: ['saved', 'global'], - search: 'y*', - searchFields: ['title', 'title.raw'], - }) - ).toEqual({ - query: { - bool: { - filter: [ - { - bool: { - should: [createTypeClause('saved'), createTypeClause('global')], - minimum_should_match: 1, - }, - }, - ], - must: [ - { - simple_query_string: { - query: 'y*', - fields: ['saved.title', 'global.title', 'saved.title.raw', 'global.title.raw'], - }, - }, - ], - }, - }, + search, + searchFields: undefined, + }); + expectResult(result, expect.objectContaining({ lenient: true, fields: ['*'] })); }); - }); - }); - describe('namespace, type (plural, namespaced and global), search, searchFields', () => { - it('includes all types for field', () => { - expect( - getQueryParams({ - mappings: MAPPINGS, - registry, - namespace: 'foo-namespace', - type: ['saved', 'global'], - search: 'y*', - searchFields: ['title'], - }) - ).toEqual({ - query: { - bool: { - filter: [ - { - bool: { - should: [createTypeClause('saved', 'foo-namespace'), createTypeClause('global')], - minimum_should_match: 1, - }, - }, - ], - must: [ - { - simple_query_string: { - query: 'y*', - fields: ['saved.title', 'global.title'], - }, - }, - ], - }, - }, - }); - }); - it('supports field boosting', () => { - expect( - getQueryParams({ - mappings: MAPPINGS, - registry, - namespace: 'foo-namespace', - type: ['saved', 'global'], - search: 'y*', - searchFields: ['title^3'], - }) - ).toEqual({ - query: { - bool: { - filter: [ - { - bool: { - should: [createTypeClause('saved', 'foo-namespace'), createTypeClause('global')], - minimum_should_match: 1, - }, - }, - ], - must: [ - { - simple_query_string: { - query: 'y*', - fields: ['saved.title^3', 'global.title^3'], - }, - }, - ], - }, - }, - }); - }); - it('supports field and multi-field', () => { - expect( - getQueryParams({ - mappings: MAPPINGS, - registry, - namespace: 'foo-namespace', - type: ['saved', 'global'], - search: 'y*', - searchFields: ['title', 'title.raw'], - }) - ).toEqual({ - query: { - bool: { - filter: [ - { - bool: { - should: [createTypeClause('saved', 'foo-namespace'), createTypeClause('global')], - minimum_should_match: 1, - }, - }, - ], - must: [ - { - simple_query_string: { - query: 'y*', - fields: ['saved.title', 'global.title', 'saved.title.raw', 'global.title.raw'], - }, - }, - ], - }, - }, + it('includes specified search fields for appropriate type/s', () => { + test(['title']); }); - }); - }); - describe('type (plural, namespaced and global), search, defaultSearchOperator', () => { - it('supports defaultSearchOperator', () => { - expect( - getQueryParams({ - mappings: MAPPINGS, - registry, - namespace: 'foo-namespace', - type: ['saved', 'global'], - search: 'foo', - searchFields: undefined, - defaultSearchOperator: 'AND', - }) - ).toEqual({ - query: { - bool: { - filter: [ - { - bool: { - minimum_should_match: 1, - should: [ - { - bool: { - must: [ - { - term: { - type: 'saved', - }, - }, - { - term: { - namespace: 'foo-namespace', - }, - }, - ], - }, - }, - { - bool: { - must: [ - { - term: { - type: 'global', - }, - }, - ], - must_not: [ - { - exists: { - field: 'namespace', - }, - }, - ], - }, - }, - ], - }, - }, - ], - must: [ - { - simple_query_string: { - lenient: true, - fields: ['*'], - default_operator: 'AND', - query: 'foo', - }, - }, - ], - }, - }, + it('supports boosting', () => { + test(['title^3']); }); - }); - }); - describe('type (plural, namespaced and global), hasReference', () => { - it('supports hasReference', () => { - expect( - getQueryParams({ - mappings: MAPPINGS, - registry, - namespace: 'foo-namespace', - type: ['saved', 'global'], - search: undefined, - searchFields: undefined, - defaultSearchOperator: 'OR', - hasReference: { - type: 'bar', - id: '1', - }, - }) - ).toEqual({ - query: { - bool: { - filter: [ - { - bool: { - must: [ - { - nested: { - path: 'references', - query: { - bool: { - must: [ - { - term: { - 'references.id': '1', - }, - }, - { - term: { - 'references.type': 'bar', - }, - }, - ], - }, - }, - }, - }, - ], - should: [createTypeClause('saved', 'foo-namespace'), createTypeClause('global')], - minimum_should_match: 1, - }, - }, - ], - }, - }, + it('supports multiple fields', () => { + test(['title, title.raw']); }); }); - }); - describe('type filter', () => { - it(' with namespace', () => { - expect( - getQueryParams({ - mappings: MAPPINGS, - registry, - namespace: 'foo-namespace', - kueryNode: { - type: 'function', - function: 'is', - arguments: [ - { type: 'literal', value: 'global.name' }, - { type: 'literal', value: 'GLOBAL' }, - { type: 'literal', value: false }, - ], - }, - }) - ).toEqual({ - query: { - bool: { - filter: [ - { - bool: { - should: [ - { - match: { - 'global.name': 'GLOBAL', - }, - }, - ], - minimum_should_match: 1, - }, - }, - { - bool: { - should: [ - { - bool: { - must: [ - { - term: { - type: 'pending', - }, - }, - { - term: { - namespace: 'foo-namespace', - }, - }, - ], - }, - }, - { - bool: { - must: [ - { - term: { - type: 'saved', - }, - }, - { - term: { - namespace: 'foo-namespace', - }, - }, - ], - }, - }, - { - bool: { - must: [ - { - term: { - type: 'global', - }, - }, - ], - must_not: [ - { - exists: { - field: 'namespace', - }, - }, - ], - }, - }, - ], - minimum_should_match: 1, - }, - }, - ], - }, - }, - }); - }); - it(' with namespace and more complex filter', () => { - expect( - getQueryParams({ - mappings: MAPPINGS, + describe('`defaultSearchOperator` parameter', () => { + it('does not include default_operator when `defaultSearchOperator` is not specified', () => { + const result = getQueryParams({ + mappings, registry, - namespace: 'foo-namespace', - kueryNode: { - type: 'function', - function: 'and', - arguments: [ - { - type: 'function', - function: 'is', - arguments: [ - { type: 'literal', value: 'global.name' }, - { type: 'literal', value: 'GLOBAL' }, - { type: 'literal', value: false }, - ], - }, - { - type: 'function', - function: 'not', - arguments: [ - { - type: 'function', - function: 'is', - arguments: [ - { type: 'literal', value: 'saved.obj.key1' }, - { type: 'literal', value: 'key' }, - { type: 'literal', value: true }, - ], - }, - ], - }, - ], - }, - }) - ).toEqual({ - query: { - bool: { - filter: [ - { - bool: { - filter: [ - { - bool: { - should: [ - { - match: { - 'global.name': 'GLOBAL', - }, - }, - ], - minimum_should_match: 1, - }, - }, - { - bool: { - must_not: { - bool: { - should: [ - { - match_phrase: { - 'saved.obj.key1': 'key', - }, - }, - ], - minimum_should_match: 1, - }, - }, - }, - }, - ], - }, - }, - { - bool: { - should: [ - { - bool: { - must: [ - { - term: { - type: 'pending', - }, - }, - { - term: { - namespace: 'foo-namespace', - }, - }, - ], - }, - }, - { - bool: { - must: [ - { - term: { - type: 'saved', - }, - }, - { - term: { - namespace: 'foo-namespace', - }, - }, - ], - }, - }, - { - bool: { - must: [ - { - term: { - type: 'global', - }, - }, - ], - must_not: [ - { - exists: { - field: 'namespace', - }, - }, - ], - }, - }, - ], - minimum_should_match: 1, - }, - }, - ], - }, - }, + search, + defaultSearchOperator: undefined, + }); + expectResult(result, expect.not.objectContaining({ default_operator: expect.anything() })); }); - }); - it(' with search and searchFields', () => { - expect( - getQueryParams({ - mappings: MAPPINGS, + + it('includes specified default operator', () => { + const defaultSearchOperator = 'AND'; + const result = getQueryParams({ + mappings, registry, - namespace: 'foo-namespace', - search: 'y*', - searchFields: ['title'], - kueryNode: { - type: 'function', - function: 'is', - arguments: [ - { type: 'literal', value: 'global.name' }, - { type: 'literal', value: 'GLOBAL' }, - { type: 'literal', value: false }, - ], - }, - }) - ).toEqual({ - query: { - bool: { - filter: [ - { - bool: { - should: [ - { - match: { - 'global.name': 'GLOBAL', - }, - }, - ], - minimum_should_match: 1, - }, - }, - { - bool: { - should: [ - { - bool: { - must: [ - { - term: { - type: 'pending', - }, - }, - { - term: { - namespace: 'foo-namespace', - }, - }, - ], - }, - }, - { - bool: { - must: [ - { - term: { - type: 'saved', - }, - }, - { - term: { - namespace: 'foo-namespace', - }, - }, - ], - }, - }, - { - bool: { - must: [ - { - term: { - type: 'global', - }, - }, - ], - must_not: [ - { - exists: { - field: 'namespace', - }, - }, - ], - }, - }, - ], - minimum_should_match: 1, - }, - }, - ], - must: [ - { - simple_query_string: { - query: 'y*', - fields: ['pending.title', 'saved.title', 'global.title'], - }, - }, - ], - }, - }, + search, + defaultSearchOperator, + }); + expectResult(result, expect.objectContaining({ default_operator: defaultSearchOperator })); }); }); }); diff --git a/src/core/server/saved_objects/service/lib/search_dsl/query_params.ts b/src/core/server/saved_objects/service/lib/search_dsl/query_params.ts index e6c06208ca1a5..967ce8bceaf84 100644 --- a/src/core/server/saved_objects/service/lib/search_dsl/query_params.ts +++ b/src/core/server/saved_objects/service/lib/search_dsl/query_params.ts @@ -66,18 +66,26 @@ function getClauseForType( namespace: string | undefined, type: string ) { - if (namespace && !registry.isNamespaceAgnostic(type)) { + if (registry.isMultiNamespace(type)) { + return { + bool: { + must: [{ term: { type } }, { term: { namespaces: namespace ?? 'default' } }], + must_not: [{ exists: { field: 'namespace' } }], + }, + }; + } else if (namespace && registry.isSingleNamespace(type)) { return { bool: { must: [{ term: { type } }, { term: { namespace } }], + must_not: [{ exists: { field: 'namespaces' } }], }, }; } - + // isSingleNamespace in the default namespace, or isNamespaceAgnostic return { bool: { must: [{ term: { type } }], - must_not: [{ exists: { field: 'namespace' } }], + must_not: [{ exists: { field: 'namespace' } }, { exists: { field: 'namespaces' } }], }, }; } diff --git a/src/core/server/saved_objects/service/saved_objects_client.mock.ts b/src/core/server/saved_objects/service/saved_objects_client.mock.ts index c6de9fa94291c..b209c9ca54f63 100644 --- a/src/core/server/saved_objects/service/saved_objects_client.mock.ts +++ b/src/core/server/saved_objects/service/saved_objects_client.mock.ts @@ -31,6 +31,8 @@ const create = () => find: jest.fn(), get: jest.fn(), update: jest.fn(), + addToNamespaces: jest.fn(), + deleteFromNamespaces: jest.fn(), } as unknown) as jest.Mocked); export const savedObjectsClientMock = { create }; diff --git a/src/core/server/saved_objects/service/saved_objects_client.test.js b/src/core/server/saved_objects/service/saved_objects_client.test.js index 1794d9ae86c17..53bb31369adbf 100644 --- a/src/core/server/saved_objects/service/saved_objects_client.test.js +++ b/src/core/server/saved_objects/service/saved_objects_client.test.js @@ -147,3 +147,37 @@ test(`#bulkUpdate`, async () => { }); expect(result).toBe(returnValue); }); + +test(`#addToNamespaces`, async () => { + const returnValue = Symbol(); + const mockRepository = { + addToNamespaces: jest.fn().mockResolvedValue(returnValue), + }; + const client = new SavedObjectsClient(mockRepository); + + const type = Symbol(); + const id = Symbol(); + const namespaces = Symbol(); + const options = Symbol(); + const result = await client.addToNamespaces(type, id, namespaces, options); + + expect(mockRepository.addToNamespaces).toHaveBeenCalledWith(type, id, namespaces, options); + expect(result).toBe(returnValue); +}); + +test(`#deleteFromNamespaces`, async () => { + const returnValue = Symbol(); + const mockRepository = { + deleteFromNamespaces: jest.fn().mockResolvedValue(returnValue), + }; + const client = new SavedObjectsClient(mockRepository); + + const type = Symbol(); + const id = Symbol(); + const namespaces = Symbol(); + const options = Symbol(); + const result = await client.deleteFromNamespaces(type, id, namespaces, options); + + expect(mockRepository.deleteFromNamespaces).toHaveBeenCalledWith(type, id, namespaces, options); + expect(result).toBe(returnValue); +}); diff --git a/src/core/server/saved_objects/service/saved_objects_client.ts b/src/core/server/saved_objects/service/saved_objects_client.ts index 70d69374ba8fe..8780f07cc3091 100644 --- a/src/core/server/saved_objects/service/saved_objects_client.ts +++ b/src/core/server/saved_objects/service/saved_objects_client.ts @@ -107,6 +107,26 @@ export interface SavedObjectsUpdateOptions extends SavedObjectsBaseOptions { refresh?: MutatingOperationRefreshSetting; } +/** + * + * @public + */ +export interface SavedObjectsAddToNamespacesOptions extends SavedObjectsBaseOptions { + /** An opaque version number which changes on each successful write operation. Can be used for implementing optimistic concurrency control. */ + version?: string; + /** The Elasticsearch Refresh setting for this operation */ + refresh?: MutatingOperationRefreshSetting; +} + +/** + * + * @public + */ +export interface SavedObjectsDeleteFromNamespacesOptions extends SavedObjectsBaseOptions { + /** The Elasticsearch Refresh setting for this operation */ + refresh?: MutatingOperationRefreshSetting; +} + /** * * @public @@ -270,6 +290,40 @@ export class SavedObjectsClient { return await this._repository.update(type, id, attributes, options); } + /** + * Adds namespaces to a SavedObject + * + * @param type + * @param id + * @param namespaces + * @param options + */ + async addToNamespaces( + type: string, + id: string, + namespaces: string[], + options: SavedObjectsAddToNamespacesOptions = {} + ): Promise<{}> { + return await this._repository.addToNamespaces(type, id, namespaces, options); + } + + /** + * Removes namespaces from a SavedObject + * + * @param type + * @param id + * @param namespaces + * @param options + */ + async deleteFromNamespaces( + type: string, + id: string, + namespaces: string[], + options: SavedObjectsDeleteFromNamespacesOptions = {} + ): Promise<{}> { + return await this._repository.deleteFromNamespaces(type, id, namespaces, options); + } + /** * Bulk Updates multiple SavedObject at once * diff --git a/src/core/server/saved_objects/types.ts b/src/core/server/saved_objects/types.ts index f14e9d9efb5e3..b50c6dc9a1abf 100644 --- a/src/core/server/saved_objects/types.ts +++ b/src/core/server/saved_objects/types.ts @@ -172,6 +172,19 @@ export type MutatingOperationRefreshSetting = boolean | 'wait_for'; */ export type SavedObjectsClientContract = Pick; +/** + * The namespace type dictates how a saved object can be interacted in relation to namespaces. Each type is mutually exclusive: + * * single (default): this type of saved object is namespace-isolated, e.g., it exists in only one namespace. + * * multiple: this type of saved object is shareable, e.g., it can exist in one or more namespaces. + * * agnostic: this type of saved object is global. + * + * Note: do not write logic that uses this value directly; instead, use the appropriate accessors in the + * {@link SavedObjectTypeRegistry | type registry}. + * + * @public + */ +export type SavedObjectsNamespaceType = 'single' | 'multiple' | 'agnostic'; + /** * @remarks This is only internal for now, and will only be public when we expose the registerType API * @@ -190,9 +203,9 @@ export interface SavedObjectsType { */ hidden: boolean; /** - * Is the type global (true), or namespaced (false). + * The {@link SavedObjectsNamespaceType | namespace type} for the type. */ - namespaceAgnostic: boolean; + namespaceType: SavedObjectsNamespaceType; /** * If defined, the type instances will be stored in the given index instead of the default one. */ @@ -329,6 +342,8 @@ export type SavedObjectLegacyMigrationFn = ( */ interface SavedObjectsLegacyTypeSchema { isNamespaceAgnostic?: boolean; + /** Cannot be used in conjunction with `isNamespaceAgnostic` */ + multiNamespace?: boolean; hidden?: boolean; indexPattern?: ((config: LegacyConfig) => string) | string; convertToAliasScript?: string; diff --git a/src/core/server/saved_objects/utils.test.ts b/src/core/server/saved_objects/utils.test.ts index 0719fe7138e8a..033aeea7c018d 100644 --- a/src/core/server/saved_objects/utils.test.ts +++ b/src/core/server/saved_objects/utils.test.ts @@ -84,6 +84,16 @@ describe('convertLegacyTypes', () => { }, { pluginId: 'pluginB', + properties: { + typeB: { + properties: { + fieldB: { type: 'text' }, + }, + }, + }, + }, + { + pluginId: 'pluginC', properties: { typeC: { properties: { @@ -92,6 +102,16 @@ describe('convertLegacyTypes', () => { }, }, }, + { + pluginId: 'pluginD', + properties: { + typeD: { + properties: { + fieldD: { type: 'text' }, + }, + }, + }, + }, ], savedObjectMigrations: {}, savedObjectSchemas: { @@ -100,6 +120,18 @@ describe('convertLegacyTypes', () => { hidden: true, isNamespaceAgnostic: true, }, + typeB: { + indexPattern: 'barBaz', + hidden: false, + multiNamespace: true, + }, + typeD: { + indexPattern: 'bazQux', + hidden: false, + // if both isNamespaceAgnostic and multiNamespace are true, the resulting namespaceType is 'agnostic' + isNamespaceAgnostic: true, + multiNamespace: true, + }, }, savedObjectValidations: {}, savedObjectsManagement: {}, @@ -372,29 +404,42 @@ describe('convertTypesToLegacySchema', () => { { name: 'typeA', hidden: false, - namespaceAgnostic: true, + namespaceType: 'agnostic', mappings: { properties: {} }, convertToAliasScript: 'some script', }, { name: 'typeB', hidden: true, - namespaceAgnostic: false, + namespaceType: 'single', indexPattern: 'myIndex', mappings: { properties: {} }, }, + { + name: 'typeC', + hidden: false, + namespaceType: 'multiple', + mappings: { properties: {} }, + }, ]; expect(convertTypesToLegacySchema(types)).toEqual({ typeA: { hidden: false, isNamespaceAgnostic: true, + multiNamespace: false, convertToAliasScript: 'some script', }, typeB: { hidden: true, isNamespaceAgnostic: false, + multiNamespace: false, indexPattern: 'myIndex', }, + typeC: { + hidden: false, + isNamespaceAgnostic: false, + multiNamespace: true, + }, }); }); }); diff --git a/src/core/server/saved_objects/utils.ts b/src/core/server/saved_objects/utils.ts index ea90efd8b9fbd..af7c08d1fbfcc 100644 --- a/src/core/server/saved_objects/utils.ts +++ b/src/core/server/saved_objects/utils.ts @@ -20,6 +20,7 @@ import { LegacyConfig } from '../legacy'; import { SavedObjectMigrationMap } from './migrations'; import { + SavedObjectsNamespaceType, SavedObjectsType, SavedObjectsLegacyUiExports, SavedObjectLegacyMigrationMap, @@ -48,10 +49,15 @@ export const convertLegacyTypes = ( const schema = savedObjectSchemas[type]; const migrations = savedObjectMigrations[type]; const management = savedObjectsManagement[type]; + const namespaceType = (schema?.isNamespaceAgnostic + ? 'agnostic' + : schema?.multiNamespace + ? 'multiple' + : 'single') as SavedObjectsNamespaceType; return { name: type, hidden: schema?.hidden ?? false, - namespaceAgnostic: schema?.isNamespaceAgnostic ?? false, + namespaceType, mappings, indexPattern: typeof schema?.indexPattern === 'function' @@ -76,7 +82,8 @@ export const convertTypesToLegacySchema = ( return { ...schema, [type.name]: { - isNamespaceAgnostic: type.namespaceAgnostic, + isNamespaceAgnostic: type.namespaceType === 'agnostic', + multiNamespace: type.namespaceType === 'multiple', hidden: type.hidden, indexPattern: type.indexPattern, convertToAliasScript: type.convertToAliasScript, diff --git a/src/core/server/server.api.md b/src/core/server/server.api.md index f3e3b7736d8d3..7ca5c75f19e8f 100644 --- a/src/core/server/server.api.md +++ b/src/core/server/server.api.md @@ -632,7 +632,9 @@ export interface CoreSetup; // (undocumented) - http: HttpServiceSetup; + http: HttpServiceSetup & { + resources: HttpResources; + }; // (undocumented) metrics: MetricsServiceSetup; // (undocumented) @@ -861,6 +863,30 @@ export type Headers = { [header: string]: string | string[] | undefined; }; +// @public +export interface HttpResources { + register: (route: RouteConfig, handler: HttpResourcesRequestHandler) => void; +} + +// @public +export interface HttpResourcesRenderOptions { + headers?: ResponseHeaders; +} + +// @public +export type HttpResourcesRequestHandler

= RequestHandler; + +// @public +export type HttpResourcesResponseOptions = HttpResponseOptions; + +// @public +export interface HttpResourcesServiceToolkit { + renderAnonymousCoreApp: (options?: HttpResourcesRenderOptions) => Promise; + renderCoreApp: (options?: HttpResourcesRenderOptions) => Promise; + renderHtml: (options: HttpResourcesResponseOptions) => IKibanaResponse; + renderJs: (options: HttpResourcesResponseOptions) => IKibanaResponse; +} + // @public export interface HttpResponseOptions { body?: HttpResponsePayload; @@ -989,7 +1015,7 @@ export interface IRouter { // // @internal getRoutes: () => RouterRoute[]; - handleLegacyErrors: (handler: RequestHandler) => RequestHandler; + handleLegacyErrors: RequestHandlerWrapper; patch: RouteRegistrar<'patch'>; post: RouteRegistrar<'post'>; put: RouteRegistrar<'put'>; @@ -1003,16 +1029,11 @@ export type IsAuthenticated = (request: KibanaRequest | LegacyRequest) => boolea export type ISavedObjectsRepository = Pick; // @public -export type ISavedObjectTypeRegistry = Pick; +export type ISavedObjectTypeRegistry = Omit; // @public export type IScopedClusterClient = Pick; -// @public (undocumented) -export interface IScopedRenderingClient { - render(options?: Pick): Promise; -} - // @public export interface IUiSettingsClient { get: (key: string) => Promise; @@ -1150,6 +1171,10 @@ export interface LegacyServiceSetupDeps { core: LegacyCoreSetup; // (undocumented) plugins: Record; + // Warning: (ae-forgotten-export) The symbol "UiPlugins" needs to be exported by the entry point index.d.ts + // + // (undocumented) + uiPlugins: UiPlugins; } // @public @deprecated (undocumented) @@ -1466,12 +1491,6 @@ export type PluginOpaqueId = symbol; export interface PluginsServiceSetup { contracts: Map; initialized: boolean; - // (undocumented) - uiPlugins: { - internal: Map; - public: Map; - browserConfigs: Map>; - }; } // @internal (undocumented) @@ -1496,19 +1515,13 @@ export type RedirectResponseOptions = HttpResponseOptions & { }; }; -// @internal (undocumented) -export interface RenderingServiceSetup { - render(request: R, uiSettings: IUiSettingsClient, options?: IRenderOptions): Promise; -} - // @public -export type RequestHandler

= (context: RequestHandlerContext, request: KibanaRequest, response: KibanaResponseFactory) => IKibanaResponse | Promise>; +export type RequestHandler

= (context: RequestHandlerContext, request: KibanaRequest, response: ResponseFactory) => IKibanaResponse | Promise>; // @public export interface RequestHandlerContext { // (undocumented) core: { - rendering: IScopedRenderingClient; savedObjects: { client: SavedObjectsClientContract; typeRegistry: ISavedObjectTypeRegistry; @@ -1529,6 +1542,9 @@ export type RequestHandlerContextContainer = IContextContainer = IContextProvider, TContextName>; +// @public +export type RequestHandlerWrapper = (handler: RequestHandler) => RequestHandler; + // @public export function resolveSavedObjectsImportErrors({ readStream, objectLimit, retries, savedObjectsClient, supportedTypes, namespace, }: SavedObjectsResolveImportErrorsOptions): Promise; @@ -1542,11 +1558,7 @@ export type ResponseError = string | Error | { export type ResponseErrorAttributes = Record; // @public -export type ResponseHeaders = { - [header in KnownHeaders]?: string | string[]; -} & { - [header: string]: string | string[]; -}; +export type ResponseHeaders = Record | Record; // @public export interface RouteConfig { @@ -1643,6 +1655,7 @@ export interface SavedObject { }; id: string; migrationVersion?: SavedObjectsMigrationVersion; + namespaces?: string[]; references: SavedObjectReference[]; type: string; updated_at?: string; @@ -1687,6 +1700,12 @@ export interface SavedObjectReference { type: string; } +// @public (undocumented) +export interface SavedObjectsAddToNamespacesOptions extends SavedObjectsBaseOptions { + refresh?: MutatingOperationRefreshSetting; + version?: string; +} + // Warning: (ae-forgotten-export) The symbol "SavedObjectDoc" needs to be exported by the entry point index.d.ts // Warning: (ae-forgotten-export) The symbol "Referencable" needs to be exported by the entry point index.d.ts // @@ -1754,11 +1773,13 @@ export interface SavedObjectsBulkUpdateResponse { export class SavedObjectsClient { // @internal constructor(repository: ISavedObjectsRepository); + addToNamespaces(type: string, id: string, namespaces: string[], options?: SavedObjectsAddToNamespacesOptions): Promise<{}>; bulkCreate(objects: Array>, options?: SavedObjectsCreateOptions): Promise>; bulkGet(objects?: SavedObjectsBulkGetObject[], options?: SavedObjectsBaseOptions): Promise>; bulkUpdate(objects: Array>, options?: SavedObjectsBulkUpdateOptions): Promise>; create(type: string, attributes: T, options?: SavedObjectsCreateOptions): Promise>; delete(type: string, id: string, options?: SavedObjectsDeleteOptions): Promise<{}>; + deleteFromNamespaces(type: string, id: string, namespaces: string[], options?: SavedObjectsDeleteFromNamespacesOptions): Promise<{}>; // (undocumented) static errors: typeof SavedObjectsErrorHelpers; // (undocumented) @@ -1839,6 +1860,11 @@ export interface SavedObjectsDeleteByNamespaceOptions extends SavedObjectsBaseOp refresh?: MutatingOperationRefreshSetting; } +// @public (undocumented) +export interface SavedObjectsDeleteFromNamespacesOptions extends SavedObjectsBaseOptions { + refresh?: MutatingOperationRefreshSetting; +} + // @public (undocumented) export interface SavedObjectsDeleteOptions extends SavedObjectsBaseOptions { refresh?: MutatingOperationRefreshSetting; @@ -1849,6 +1875,8 @@ export class SavedObjectsErrorHelpers { // (undocumented) static createBadRequestError(reason?: string): DecoratedError; // (undocumented) + static createConflictError(type: string, id: string): DecoratedError; + // (undocumented) static createEsAutoCreateIndexError(): DecoratedError; // (undocumented) static createGenericNotFoundError(type?: string | null, id?: string | null): DecoratedError; @@ -1861,6 +1889,8 @@ export class SavedObjectsErrorHelpers { // (undocumented) static decorateConflictError(error: Error, reason?: string): DecoratedError; // (undocumented) + static decorateEsCannotExecuteScriptError(error: Error, reason?: string): DecoratedError; + // (undocumented) static decorateEsUnavailableError(error: Error, reason?: string): DecoratedError; // (undocumented) static decorateForbiddenError(error: Error, reason?: string): DecoratedError; @@ -1877,6 +1907,8 @@ export class SavedObjectsErrorHelpers { // (undocumented) static isEsAutoCreateIndexError(error: Error | DecoratedError): boolean; // (undocumented) + static isEsCannotExecuteScriptError(error: Error | DecoratedError): boolean; + // (undocumented) static isEsUnavailableError(error: Error | DecoratedError): boolean; // (undocumented) static isForbiddenError(error: Error | DecoratedError): boolean; @@ -2064,8 +2096,6 @@ export interface SavedObjectsLegacyService { // (undocumented) getScopedSavedObjectsClient: SavedObjectsClientProvider['getClient']; // (undocumented) - importAndExportableTypes: string[]; - // (undocumented) importExport: { objectLimit: number; importSavedObjects(options: SavedObjectsImportOptions): Promise; @@ -2106,6 +2136,9 @@ export interface SavedObjectsMigrationVersion { [pluginName: string]: string; } +// @public +export type SavedObjectsNamespaceType = 'single' | 'multiple' | 'agnostic'; + // @public export interface SavedObjectsRawDoc { // (undocumented) @@ -2124,6 +2157,7 @@ export interface SavedObjectsRawDoc { // @public (undocumented) export class SavedObjectsRepository { + addToNamespaces(type: string, id: string, namespaces: string[], options?: SavedObjectsAddToNamespacesOptions): Promise<{}>; bulkCreate(objects: Array>, options?: SavedObjectsCreateOptions): Promise>; bulkGet(objects?: SavedObjectsBulkGetObject[], options?: SavedObjectsBaseOptions): Promise>; bulkUpdate(objects: Array>, options?: SavedObjectsBulkUpdateOptions): Promise>; @@ -2134,6 +2168,7 @@ export class SavedObjectsRepository { static createRepository(migrator: KibanaMigrator, typeRegistry: SavedObjectTypeRegistry, indexName: string, callCluster: APICaller, extraTypes?: string[], injectedConstructor?: any): ISavedObjectsRepository; delete(type: string, id: string, options?: SavedObjectsDeleteOptions): Promise<{}>; deleteByNamespace(namespace: string, options?: SavedObjectsDeleteByNamespaceOptions): Promise; + deleteFromNamespaces(type: string, id: string, namespaces: string[], options?: SavedObjectsDeleteFromNamespacesOptions): Promise<{}>; // (undocumented) find({ search, defaultSearchOperator, searchFields, hasReference, page, perPage, sortField, sortOrder, fields, namespace, type, filter, }: SavedObjectsFindOptions): Promise>; get(type: string, id: string, options?: SavedObjectsBaseOptions): Promise>; @@ -2175,7 +2210,11 @@ export class SavedObjectsSchema { // (undocumented) isHiddenType(type: string): boolean; // (undocumented) + isMultiNamespace(type: string): boolean; + // (undocumented) isNamespaceAgnostic(type: string): boolean; + // (undocumented) + isSingleNamespace(type: string): boolean; } // @public @@ -2224,7 +2263,7 @@ export interface SavedObjectsType { mappings: SavedObjectsTypeMappingDefinition; migrations?: SavedObjectMigrationMap; name: string; - namespaceAgnostic: boolean; + namespaceType: SavedObjectsNamespaceType; } // @public @@ -2269,7 +2308,9 @@ export class SavedObjectTypeRegistry { getType(type: string): SavedObjectsType | undefined; isHidden(type: string): boolean; isImportableAndExportable(type: string): boolean; + isMultiNamespace(type: string): boolean; isNamespaceAgnostic(type: string): boolean; + isSingleNamespace(type: string): boolean; registerType(type: SavedObjectsType): void; } @@ -2427,12 +2468,11 @@ export const validBodyOutput: readonly ["data", "stream"]; // Warnings were encountered during analysis: // // src/core/server/http/router/response.ts:316:3 - (ae-forgotten-export) The symbol "KibanaResponse" needs to be exported by the entry point index.d.ts -// src/core/server/legacy/types.ts:162:3 - (ae-forgotten-export) The symbol "VarsProvider" needs to be exported by the entry point index.d.ts -// src/core/server/legacy/types.ts:163:3 - (ae-forgotten-export) The symbol "VarsReplacer" needs to be exported by the entry point index.d.ts -// src/core/server/legacy/types.ts:164:3 - (ae-forgotten-export) The symbol "LegacyNavLinkSpec" needs to be exported by the entry point index.d.ts -// src/core/server/legacy/types.ts:165:3 - (ae-forgotten-export) The symbol "LegacyAppSpec" needs to be exported by the entry point index.d.ts -// src/core/server/legacy/types.ts:166:16 - (ae-forgotten-export) The symbol "LegacyPluginSpec" needs to be exported by the entry point index.d.ts -// src/core/server/plugins/plugins_service.ts:47:5 - (ae-forgotten-export) The symbol "InternalPluginInfo" needs to be exported by the entry point index.d.ts +// src/core/server/legacy/types.ts:163:3 - (ae-forgotten-export) The symbol "VarsProvider" needs to be exported by the entry point index.d.ts +// src/core/server/legacy/types.ts:164:3 - (ae-forgotten-export) The symbol "VarsReplacer" needs to be exported by the entry point index.d.ts +// 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:230:3 - (ae-forgotten-export) The symbol "KibanaConfigType" needs to be exported by the entry point index.d.ts // src/core/server/plugins/types.ts:230:3 - (ae-forgotten-export) The symbol "SharedGlobalConfigKeys" needs to be exported by the entry point index.d.ts // src/core/server/plugins/types.ts:232:3 - (ae-forgotten-export) The symbol "PathConfigType" needs to be exported by the entry point index.d.ts diff --git a/src/core/server/server.test.ts b/src/core/server/server.test.ts index 24c41d511180a..1e3e1638cf2a0 100644 --- a/src/core/server/server.test.ts +++ b/src/core/server/server.test.ts @@ -46,7 +46,10 @@ const rawConfigService = rawConfigServiceMock.create({}); beforeEach(() => { mockConfigService.atPath.mockReturnValue(new BehaviorSubject({ autoListen: true })); - mockPluginsService.discover.mockResolvedValue(new Map()); + mockPluginsService.discover.mockResolvedValue({ + pluginTree: new Map(), + uiPlugins: { internal: new Map(), public: new Map(), browserConfigs: new Map() }, + }); }); afterEach(() => { @@ -88,7 +91,10 @@ test('injects legacy dependency to context#setup()', async () => { [pluginA, []], [pluginB, [pluginA]], ]); - mockPluginsService.discover.mockResolvedValue(pluginDependencies); + mockPluginsService.discover.mockResolvedValue({ + pluginTree: pluginDependencies, + uiPlugins: { internal: new Map(), public: new Map(), browserConfigs: new Map() }, + }); await server.setup(); diff --git a/src/core/server/server.ts b/src/core/server/server.ts index 07ea431dd3a0d..d4c0ebcfb7cf2 100644 --- a/src/core/server/server.ts +++ b/src/core/server/server.ts @@ -29,7 +29,8 @@ import { import { CoreApp } from './core_app'; import { ElasticsearchService } from './elasticsearch'; import { HttpService } from './http'; -import { RenderingService, RenderingServiceSetup } from './rendering'; +import { HttpResourcesService } from './http_resources'; +import { RenderingService } from './rendering'; import { LegacyService, ensureValidConfiguration } from './legacy'; import { Logger, LoggerFactory } from './logging'; import { UiSettingsService } from './ui_settings'; @@ -71,6 +72,7 @@ export class Server { private readonly uiSettings: UiSettingsService; private readonly uuid: UuidService; private readonly metrics: MetricsService; + private readonly httpResources: HttpResourcesService; private readonly status: StatusService; private readonly coreApp: CoreApp; @@ -99,13 +101,14 @@ export class Server { this.metrics = new MetricsService(core); this.status = new StatusService(core); this.coreApp = new CoreApp(core); + this.httpResources = new HttpResourcesService(core); } public async setup() { this.log.debug('setting up server'); // Discover any plugins before continuing. This allows other systems to utilize the plugin dependency graph. - const pluginDependencies = await this.plugins.discover(); + const { pluginTree, uiPlugins } = await this.plugins.discover(); const legacyPlugins = await this.legacy.discoverPlugins(); // Immediately terminate in case of invalid configuration @@ -117,10 +120,7 @@ export class Server { // 1) Can access context from any NP plugin // 2) Can register context providers that will only be available to other legacy plugins and will not leak into // New Platform plugins. - pluginDependencies: new Map([ - ...pluginDependencies, - [this.legacy.legacyId, [...pluginDependencies.keys()]], - ]), + pluginDependencies: new Map([...pluginTree, [this.legacy.legacyId, [...pluginTree.keys()]]]), }); const uuidSetup = await this.uuid.setup(); @@ -148,6 +148,17 @@ export class Server { const metricsSetup = await this.metrics.setup({ http: httpSetup }); + const renderingSetup = await this.rendering.setup({ + http: httpSetup, + legacyPlugins, + uiPlugins, + }); + + const httpResourcesSetup = this.httpResources.setup({ + http: httpSetup, + rendering: renderingSetup, + }); + const statusSetup = this.status.setup({ elasticsearch: elasticsearchServiceSetup, savedObjects: savedObjectsSetup, @@ -158,28 +169,25 @@ export class Server { context: contextServiceSetup, elasticsearch: elasticsearchServiceSetup, http: httpSetup, - metrics: metricsSetup, savedObjects: savedObjectsSetup, status: statusSetup, uiSettings: uiSettingsSetup, uuid: uuidSetup, + metrics: metricsSetup, + rendering: renderingSetup, + httpResources: httpResourcesSetup, }; const pluginsSetup = await this.plugins.setup(coreSetup); this.pluginsInitialized = pluginsSetup.initialized; - const renderingSetup = await this.rendering.setup({ - http: httpSetup, - legacyPlugins, - plugins: pluginsSetup, - }); - await this.legacy.setup({ core: { ...coreSetup, plugins: pluginsSetup, rendering: renderingSetup }, plugins: mapToObject(pluginsSetup.contracts), + uiPlugins, }); - this.registerCoreContext(coreSetup, renderingSetup); + this.registerCoreContext(coreSetup); this.coreApp.setup(coreSetup); return coreSetup; @@ -201,7 +209,7 @@ export class Server { uiSettings: uiSettingsStart, }; - const pluginsStart = await this.plugins.start(this.coreStart!); + const pluginsStart = await this.plugins.start(this.coreStart); await this.legacy.start({ core: { @@ -212,7 +220,9 @@ export class Server { }); await this.http.start(); - await this.rendering.start(); + await this.rendering.start({ + legacy: this.legacy, + }); await this.metrics.start(); return this.coreStart; @@ -232,7 +242,7 @@ export class Server { await this.status.stop(); } - private registerCoreContext(coreSetup: InternalCoreSetup, rendering: RenderingServiceSetup) { + private registerCoreContext(coreSetup: InternalCoreSetup) { coreSetup.http.registerRouteHandlerContext( coreId, 'core', @@ -241,13 +251,6 @@ export class Server { const uiSettingsClient = coreSetup.uiSettings.asScopedToClient(savedObjectsClient); return { - rendering: { - render: async (options = {}) => - rendering.render(req, uiSettingsClient, { - ...options, - vars: await this.legacy.legacyInternals!.getVars('core', req), - }), - }, savedObjects: { client: savedObjectsClient, typeRegistry: this.coreStart!.savedObjects.getTypeRegistry(), diff --git a/src/core/server/ui_settings/saved_objects/ui_settings.ts b/src/core/server/ui_settings/saved_objects/ui_settings.ts index 031315bec0dab..1bea65ddee924 100644 --- a/src/core/server/ui_settings/saved_objects/ui_settings.ts +++ b/src/core/server/ui_settings/saved_objects/ui_settings.ts @@ -22,7 +22,7 @@ import { SavedObjectsType } from '../../saved_objects'; export const uiSettingsType: SavedObjectsType = { name: 'config', hidden: false, - namespaceAgnostic: false, + namespaceType: 'single', mappings: { // we don't want to allow `true` in the public `SavedObjectsTypeMappingDefinition` type, however // this is needed for the config that is kinda a special type. To avoid adding additional internal types diff --git a/src/core/types/saved_objects.ts b/src/core/types/saved_objects.ts index d3faab6c557cd..04aaacc3cf31a 100644 --- a/src/core/types/saved_objects.ts +++ b/src/core/types/saved_objects.ts @@ -96,4 +96,6 @@ export interface SavedObject { references: SavedObjectReference[]; /** {@inheritdoc SavedObjectsMigrationVersion} */ migrationVersion?: SavedObjectsMigrationVersion; + /** Namespace(s) that this saved object exists in. This attribute is only used for multi-namespace saved object types. */ + namespaces?: string[]; } diff --git a/src/dev/build/tasks/os_packages/docker_generator/resources/bin/kibana-docker b/src/dev/build/tasks/os_packages/docker_generator/resources/bin/kibana-docker index d4d2e86e1e96b..38acfb15d3ece 100755 --- a/src/dev/build/tasks/os_packages/docker_generator/resources/bin/kibana-docker +++ b/src/dev/build/tasks/os_packages/docker_generator/resources/bin/kibana-docker @@ -195,6 +195,7 @@ kibana_vars=( xpack.reporting.capture.viewport.width xpack.reporting.capture.zoom xpack.reporting.csv.checkForFormulas + xpack.reporting.csv.escapeFormulaValues xpack.reporting.csv.enablePanelActionDownload xpack.reporting.csv.maxSizeBytes xpack.reporting.csv.scroll.duration diff --git a/src/dev/jest/config.js b/src/dev/jest/config.js index a941735c7840e..7da14e0dfe51b 100644 --- a/src/dev/jest/config.js +++ b/src/dev/jest/config.js @@ -62,6 +62,7 @@ export default { '\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$': '/src/dev/jest/mocks/file_mock.js', '\\.(css|less|scss)$': '/src/dev/jest/mocks/style_mock.js', + '\\.ace\\.worker.js$': '/src/dev/jest/mocks/ace_worker_module_mock.js', }, setupFiles: [ '/src/dev/jest/setup/babel_polyfill.js', diff --git a/src/dev/jest/mocks/ace_worker_module_mock.js b/src/dev/jest/mocks/ace_worker_module_mock.js new file mode 100644 index 0000000000000..9d267f494f8bf --- /dev/null +++ b/src/dev/jest/mocks/ace_worker_module_mock.js @@ -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. + */ + +module.exports = ''; diff --git a/src/dev/storybook/aliases.ts b/src/dev/storybook/aliases.ts index 8ed64f004c9be..43114b2edccfc 100644 --- a/src/dev/storybook/aliases.ts +++ b/src/dev/storybook/aliases.ts @@ -24,6 +24,6 @@ export const storybookAliases = { drilldowns: 'x-pack/plugins/drilldowns/scripts/storybook.js', embeddable: 'src/plugins/embeddable/scripts/storybook.js', infra: 'x-pack/legacy/plugins/infra/scripts/storybook.js', - siem: 'x-pack/legacy/plugins/siem/scripts/storybook.js', + siem: 'x-pack/plugins/siem/scripts/storybook.js', ui_actions: 'x-pack/plugins/advanced_ui_actions/scripts/storybook.js', }; diff --git a/src/dev/typescript/projects.ts b/src/dev/typescript/projects.ts index 34756912fc247..01d8a30b598c1 100644 --- a/src/dev/typescript/projects.ts +++ b/src/dev/typescript/projects.ts @@ -27,7 +27,7 @@ export const PROJECTS = [ new Project(resolve(REPO_ROOT, 'test/tsconfig.json'), { name: 'kibana/test' }), new Project(resolve(REPO_ROOT, 'x-pack/tsconfig.json')), new Project(resolve(REPO_ROOT, 'x-pack/test/tsconfig.json'), { name: 'x-pack/test' }), - new Project(resolve(REPO_ROOT, 'x-pack/legacy/plugins/siem/cypress/tsconfig.json'), { + new Project(resolve(REPO_ROOT, 'x-pack/plugins/siem/cypress/tsconfig.json'), { name: 'siem/cypress', }), new Project(resolve(REPO_ROOT, 'x-pack/legacy/plugins/apm/e2e/tsconfig.json'), { @@ -44,6 +44,9 @@ export const PROJECTS = [ ...glob .sync('examples/*/tsconfig.json', { cwd: REPO_ROOT }) .map(path => new Project(resolve(REPO_ROOT, path))), + ...glob + .sync('x-pack/examples/*/tsconfig.json', { cwd: REPO_ROOT }) + .map(path => new Project(resolve(REPO_ROOT, path))), ...glob .sync('test/plugin_functional/plugins/*/tsconfig.json', { cwd: REPO_ROOT }) .map(path => new Project(resolve(REPO_ROOT, path))), diff --git a/src/legacy/core_plugins/kibana/index.js b/src/legacy/core_plugins/kibana/index.js index 1d643418997f5..989583742acd0 100644 --- a/src/legacy/core_plugins/kibana/index.js +++ b/src/legacy/core_plugins/kibana/index.js @@ -54,11 +54,7 @@ export default function(kibana) { }, uiExports: { - hacks: [ - 'plugins/kibana/discover/legacy', - 'plugins/kibana/dev_tools', - 'plugins/kibana/visualize/legacy', - ], + hacks: ['plugins/kibana/discover/legacy', 'plugins/kibana/dev_tools'], app: { id: 'kibana', title: 'Kibana', diff --git a/src/legacy/core_plugins/kibana/inject_vars.js b/src/legacy/core_plugins/kibana/inject_vars.js index 76d1704907ab5..c3b906ee842e3 100644 --- a/src/legacy/core_plugins/kibana/inject_vars.js +++ b/src/legacy/core_plugins/kibana/inject_vars.js @@ -20,10 +20,7 @@ export function injectVars(server) { const serverConfig = server.config(); - const { importAndExportableTypes } = server.savedObjects; - return { - importAndExportableTypes, autocompleteTerminateAfter: serverConfig.get('kibana.autocompleteTerminateAfter'), autocompleteTimeout: serverConfig.get('kibana.autocompleteTimeout'), }; diff --git a/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/discover.html b/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/discover.html index d068e824a3e0a..1221b01657e45 100644 --- a/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/discover.html +++ b/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/discover.html @@ -140,6 +140,7 @@

{{screenTitle}}

position="'top'" > { - angularRouteManager.when(matchAllWithPrefix(legacyAppId), { - resolveRedirectTo: ($location: ILocationService) => { - const url = $location.url(); - return `/${newAppId}${keepPrefix ? url : url.replace(legacyAppId, '')}`; + npStart.plugins.kibanaLegacy.getForwards().forEach(forwardDefinition => { + angularRouteManager.when(matchAllWithPrefix(forwardDefinition.legacyAppId), { + outerAngularWrapperRoute: true, + reloadOnSearch: false, + reloadOnUrl: false, + template: '', + controller($location: ILocationService) { + const newPath = forwardDefinition.rewritePath($location.url()); + npStart.core.application.navigateToApp(forwardDefinition.newAppId, { path: newPath }); }, }); }); + + npStart.plugins.kibanaLegacy + .getLegacyAppAliases() + .forEach(({ legacyAppId, newAppId, keepPrefix }) => { + angularRouteManager.when(matchAllWithPrefix(legacyAppId), { + resolveRedirectTo: ($location: ILocationService) => { + const url = $location.url(); + return `/${newAppId}${keepPrefix ? url : url.replace(legacyAppId, '')}`; + }, + }); + }); } } diff --git a/src/legacy/core_plugins/kibana/public/management/saved_object_registry.ts b/src/legacy/core_plugins/kibana/public/management/saved_object_registry.ts index 705be68a141e7..587a372f91555 100644 --- a/src/legacy/core_plugins/kibana/public/management/saved_object_registry.ts +++ b/src/legacy/core_plugins/kibana/public/management/saved_object_registry.ts @@ -17,66 +17,8 @@ * under the License. */ -import _ from 'lodash'; -import { i18n } from '@kbn/i18n'; -import { npStart } from 'ui/new_platform'; -import { SavedObjectLoader } from '../../../../../plugins/saved_objects/public'; -import { createSavedSearchesLoader } from '../../../../../plugins/discover/public'; +import { npSetup } from 'ui/new_platform'; -/** - * This registry is used for the editing mode of Saved Searches, Visualizations, - * Dashboard and Time Lion saved objects. - */ -interface SavedObjectRegistryEntry { - id: string; - service: SavedObjectLoader; - title: string; -} - -export interface ISavedObjectsManagementRegistry { - register(service: SavedObjectRegistryEntry): void; - all(): SavedObjectRegistryEntry[]; - get(id: string): SavedObjectRegistryEntry | undefined; -} - -const registry: SavedObjectRegistryEntry[] = []; - -export const savedObjectManagementRegistry: ISavedObjectsManagementRegistry = { - register: (service: SavedObjectRegistryEntry) => { - registry.push(service); - }, - all: () => { - return registry; - }, - get: (id: string) => { - return _.find(registry, { id }); - }, -}; - -const services = { - savedObjectsClient: npStart.core.savedObjects.client, - indexPatterns: npStart.plugins.data.indexPatterns, - search: npStart.plugins.data.search, - chrome: npStart.core.chrome, - overlays: npStart.core.overlays, -}; - -savedObjectManagementRegistry.register({ - id: 'savedVisualizations', - service: npStart.plugins.visualizations.savedVisualizationsLoader, - title: 'visualizations', -}); - -savedObjectManagementRegistry.register({ - id: 'savedDashboards', - service: npStart.plugins.dashboard.getSavedDashboardLoader(), - title: i18n.translate('kbn.dashboard.savedDashboardsTitle', { - defaultMessage: 'dashboards', - }), -}); +const registry = npSetup.plugins.savedObjectsManagement?.serviceRegistry; -savedObjectManagementRegistry.register({ - id: 'savedSearches', - service: createSavedSearchesLoader(services), - title: 'searches', -}); +export const savedObjectManagementRegistry = registry!; diff --git a/src/legacy/core_plugins/kibana/public/management/sections/index.js b/src/legacy/core_plugins/kibana/public/management/sections/index.js index 54717ad003ade..adc1741f57263 100644 --- a/src/legacy/core_plugins/kibana/public/management/sections/index.js +++ b/src/legacy/core_plugins/kibana/public/management/sections/index.js @@ -17,5 +17,4 @@ * under the License. */ -import './objects'; import './index_patterns'; diff --git a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/create_index_pattern_wizard/__snapshots__/create_index_pattern_wizard.test.js.snap b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/create_index_pattern_wizard/__snapshots__/create_index_pattern_wizard.test.js.snap deleted file mode 100644 index 59b275c7708a4..0000000000000 --- a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/create_index_pattern_wizard/__snapshots__/create_index_pattern_wizard.test.js.snap +++ /dev/null @@ -1,205 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`CreateIndexPatternWizard defaults to the loading state 1`] = ` - -
-
- -
- -
-`; - -exports[`CreateIndexPatternWizard renders index pattern step when there are indices 1`] = ` - -
-
- -
- -
-`; - -exports[`CreateIndexPatternWizard renders the empty state when there are no indices 1`] = ` - -
-
- -
- -
-`; - -exports[`CreateIndexPatternWizard renders time field step when step is set to 2 1`] = ` - -
-
- -
- -
-`; - -exports[`CreateIndexPatternWizard renders when there are no indices but there are remote clusters 1`] = ` - -
-
- -
- -
-`; - -exports[`CreateIndexPatternWizard shows system indices even if there are no other indices if the include system indices is toggled 1`] = ` - -
-
- -
- -
-`; diff --git a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/create_index_pattern_wizard/__snapshots__/create_index_pattern_wizard.test.tsx.snap b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/create_index_pattern_wizard/__snapshots__/create_index_pattern_wizard.test.tsx.snap new file mode 100644 index 0000000000000..09a06bd8827ce --- /dev/null +++ b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/create_index_pattern_wizard/__snapshots__/create_index_pattern_wizard.test.tsx.snap @@ -0,0 +1,312 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`CreateIndexPatternWizard defaults to the loading state 1`] = ` + +
+
+ +
+ +
+`; + +exports[`CreateIndexPatternWizard renders index pattern step when there are indices 1`] = ` + +
+
+ +
+ +
+`; + +exports[`CreateIndexPatternWizard renders the empty state when there are no indices 1`] = ` + +
+
+ +
+ +
+`; + +exports[`CreateIndexPatternWizard renders time field step when step is set to 2 1`] = ` + +
+
+ +
+ +
+`; + +exports[`CreateIndexPatternWizard renders when there are no indices but there are remote clusters 1`] = ` + +
+
+ +
+ +
+`; + +exports[`CreateIndexPatternWizard shows system indices even if there are no other indices if the include system indices is toggled 1`] = ` + +
+
+ +
+ +
+`; diff --git a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/create_index_pattern_wizard/components/step_index_pattern/step_index_pattern.tsx b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/create_index_pattern_wizard/components/step_index_pattern/step_index_pattern.tsx index 648bf7f8f9738..d8f677b7f6089 100644 --- a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/create_index_pattern_wizard/components/step_index_pattern/step_index_pattern.tsx +++ b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/create_index_pattern_wizard/components/step_index_pattern/step_index_pattern.tsx @@ -48,7 +48,7 @@ interface StepIndexPatternProps { esService: DataPublicPluginStart['search']['__LEGACY']['esClient']; savedObjectsClient: SavedObjectsClient; indexPatternCreationType: IndexPatternCreationConfig; - goToNextStep: () => void; + goToNextStep: (query: string) => void; initialQuery?: string; uiSettings: IUiSettingsClient; } diff --git a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/create_index_pattern_wizard/create_index_pattern_wizard.js b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/create_index_pattern_wizard/create_index_pattern_wizard.js deleted file mode 100644 index 1a93188edd6cc..0000000000000 --- a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/create_index_pattern_wizard/create_index_pattern_wizard.js +++ /dev/null @@ -1,272 +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 React, { Component } from 'react'; -import PropTypes from 'prop-types'; - -import { EuiGlobalToastList } from '@elastic/eui'; -import { FormattedMessage } from '@kbn/i18n/react'; - -import { StepIndexPattern } from './components/step_index_pattern'; -import { StepTimeField } from './components/step_time_field'; -import { Header } from './components/header'; -import { LoadingState } from './components/loading_state'; -import { EmptyState } from './components/empty_state'; - -import { MAX_SEARCH_SIZE } from './constants'; -import { ensureMinimumTime, getIndices } from './lib'; -import { i18n } from '@kbn/i18n'; - -export class CreateIndexPatternWizard extends Component { - static propTypes = { - initialQuery: PropTypes.string, - services: PropTypes.shape({ - es: PropTypes.object.isRequired, - indexPatterns: PropTypes.object.isRequired, - savedObjectsClient: PropTypes.object.isRequired, - indexPatternCreationType: PropTypes.object.isRequired, - config: PropTypes.object.isRequired, - changeUrl: PropTypes.func.isRequired, - openConfirm: PropTypes.func.isRequired, - }).isRequired, - }; - - constructor(props) { - super(props); - this.indexPatternCreationType = this.props.services.indexPatternCreationType; - this.state = { - step: 1, - indexPattern: '', - allIndices: [], - remoteClustersExist: false, - isInitiallyLoadingIndices: true, - isIncludingSystemIndices: false, - toasts: [], - }; - } - - async UNSAFE_componentWillMount() { - this.fetchData(); - } - - catchAndWarn = async (asyncFn, errorValue, errorMsg) => { - try { - return await asyncFn; - } catch (errors) { - this.setState(prevState => ({ - toasts: prevState.toasts.concat([ - { - title: errorMsg, - id: errorMsg.props.id, - color: 'warning', - iconType: 'alert', - }, - ]), - })); - return errorValue; - } - }; - - fetchData = async () => { - const { services } = this.props; - - this.setState({ - allIndices: [], - isInitiallyLoadingIndices: true, - remoteClustersExist: false, - }); - - const indicesFailMsg = ( - - ); - - const clustersFailMsg = ( - - ); - - // query local and remote indices, updating state independently - ensureMinimumTime( - this.catchAndWarn( - getIndices(services.es, this.indexPatternCreationType, `*`, MAX_SEARCH_SIZE), - [], - indicesFailMsg - ) - ).then(allIndices => this.setState({ allIndices, isInitiallyLoadingIndices: false })); - - this.catchAndWarn( - // if we get an error from remote cluster query, supply fallback value that allows user entry. - // ['a'] is fallback value - getIndices(services.es, this.indexPatternCreationType, `*:*`, 1), - ['a'], - clustersFailMsg - ).then(remoteIndices => this.setState({ remoteClustersExist: !!remoteIndices.length })); - }; - - createIndexPattern = async (timeFieldName, indexPatternId) => { - const { services } = this.props; - const { indexPattern } = this.state; - - const emptyPattern = await services.indexPatterns.make(); - - Object.assign(emptyPattern, { - id: indexPatternId, - title: indexPattern, - timeFieldName, - ...this.indexPatternCreationType.getIndexPatternMappings(), - }); - - const createdId = await emptyPattern.create(); - if (!createdId) { - const confirmMessage = i18n.translate('kbn.management.indexPattern.titleExistsLabel', { - values: { title: this.title }, - defaultMessage: "An index pattern with the title '{title}' already exists.", - }); - - const isConfirmed = await services.openConfirm(confirmMessage, { - confirmButtonText: i18n.translate('kbn.management.indexPattern.goToPatternButtonLabel', { - defaultMessage: 'Go to existing pattern', - }), - }); - - if (isConfirmed) { - return services.changeUrl(`/management/kibana/index_patterns/${indexPatternId}`); - } else { - return false; - } - } - - if (!services.config.get('defaultIndex')) { - await services.config.set('defaultIndex', createdId); - } - - services.indexPatterns.clearCache(createdId); - services.changeUrl(`/management/kibana/index_patterns/${createdId}`); - }; - - goToTimeFieldStep = indexPattern => { - this.setState({ step: 2, indexPattern }); - }; - - goToIndexPatternStep = () => { - this.setState({ step: 1 }); - }; - - onChangeIncludingSystemIndices = () => { - this.setState(state => ({ - isIncludingSystemIndices: !state.isIncludingSystemIndices, - })); - }; - - renderHeader() { - const { isIncludingSystemIndices } = this.state; - - return ( -
- ); - } - - renderContent() { - const { - allIndices, - isInitiallyLoadingIndices, - isIncludingSystemIndices, - step, - indexPattern, - remoteClustersExist, - } = this.state; - - if (isInitiallyLoadingIndices) { - return ; - } - - const hasDataIndices = allIndices.some(({ name }) => !name.startsWith('.')); - if (!hasDataIndices && !isIncludingSystemIndices && !remoteClustersExist) { - return ; - } - - if (step === 1) { - const { services, initialQuery } = this.props; - return ( - - ); - } - - if (step === 2) { - const { services } = this.props; - return ( - - ); - } - - return null; - } - - removeToast = removedToast => { - this.setState(prevState => ({ - toasts: prevState.toasts.filter(toast => toast.id !== removedToast.id), - })); - }; - - render() { - const header = this.renderHeader(); - const content = this.renderContent(); - - return ( - -
- {header} - {content} -
- -
- ); - } -} diff --git a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/create_index_pattern_wizard/create_index_pattern_wizard.test.js b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/create_index_pattern_wizard/create_index_pattern_wizard.test.js deleted file mode 100644 index 941f87d4d9fd2..0000000000000 --- a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/create_index_pattern_wizard/create_index_pattern_wizard.test.js +++ /dev/null @@ -1,202 +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 React from 'react'; -import { shallow } from 'enzyme'; - -import { CreateIndexPatternWizard } from './create_index_pattern_wizard'; -const mockIndexPatternCreationType = { - getIndexPatternType: () => 'default', - getIndexPatternName: () => 'name', - getIsBeta: () => false, - checkIndicesForErrors: () => false, - getShowSystemIndices: () => false, - renderPrompt: () => {}, - getIndexPatternMappings: () => { - return {}; - }, -}; -jest.mock('./components/step_index_pattern', () => ({ StepIndexPattern: 'StepIndexPattern' })); -jest.mock('./components/step_time_field', () => ({ StepTimeField: 'StepTimeField' })); -jest.mock('./components/header', () => ({ Header: 'Header' })); -jest.mock('./components/loading_state', () => ({ LoadingState: 'LoadingState' })); -jest.mock('./components/empty_state', () => ({ EmptyState: 'EmptyState' })); -jest.mock('./lib/get_indices', () => ({ - getIndices: () => { - return [{ name: 'kibana' }]; - }, -})); -jest.mock('ui/chrome', () => ({ - addBasePath: () => {}, -})); - -const loadingDataDocUrl = ''; -const initialQuery = ''; -const services = { - es: {}, - indexPatterns: {}, - savedObjectsClient: {}, - config: {}, - changeUrl: () => {}, - scopeApply: () => {}, - - indexPatternCreationType: mockIndexPatternCreationType, -}; - -describe('CreateIndexPatternWizard', () => { - it(`defaults to the loading state`, async () => { - const component = shallow( - - ); - - expect(component).toMatchSnapshot(); - }); - - it('renders the empty state when there are no indices', async () => { - const component = shallow( - - ); - - component.setState({ - isInitiallyLoadingIndices: false, - allIndices: [], - remoteClustersExist: false, - }); - - await component.update(); - expect(component).toMatchSnapshot(); - }); - - it('renders when there are no indices but there are remote clusters', async () => { - const component = shallow( - - ); - - component.setState({ - isInitiallyLoadingIndices: false, - allIndices: [], - remoteClustersExist: true, - }); - - await component.update(); - expect(component).toMatchSnapshot(); - }); - - it('shows system indices even if there are no other indices if the include system indices is toggled', async () => { - const component = shallow( - - ); - - component.setState({ - isInitiallyLoadingIndices: false, - isIncludingSystemIndices: true, - allIndices: [{ name: '.kibana ' }], - }); - - await component.update(); - expect(component).toMatchSnapshot(); - }); - - it('renders index pattern step when there are indices', async () => { - const component = shallow( - - ); - - component.setState({ - isInitiallyLoadingIndices: false, - allIndices: [{ name: 'myIndexPattern' }], - }); - - await component.update(); - expect(component).toMatchSnapshot(); - }); - - it('renders time field step when step is set to 2', async () => { - const component = shallow( - - ); - - component.setState({ - isInitiallyLoadingIndices: false, - allIndices: [{ name: 'myIndexPattern' }], - step: 2, - }); - - await component.update(); - expect(component).toMatchSnapshot(); - }); - - it('invokes the provided services when creating an index pattern', async () => { - const get = jest.fn(); - const set = jest.fn(); - const create = jest.fn().mockImplementation(() => 'id'); - const clear = jest.fn(); - const changeUrl = jest.fn(); - - const component = shallow( - ({ - create, - }), - clearCache: clear, - }, - changeUrl, - indexPatternCreationType: mockIndexPatternCreationType, - }} - /> - ); - - component.setState({ indexPattern: 'foo' }); - await component.instance().createIndexPattern(null, 'id'); - expect(get).toBeCalled(); - expect(create).toBeCalled(); - expect(clear).toBeCalledWith('id'); - expect(changeUrl).toBeCalledWith(`/management/kibana/index_patterns/id`); - }); -}); diff --git a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/create_index_pattern_wizard/create_index_pattern_wizard.test.tsx b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/create_index_pattern_wizard/create_index_pattern_wizard.test.tsx new file mode 100644 index 0000000000000..45af98661eda3 --- /dev/null +++ b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/create_index_pattern_wizard/create_index_pattern_wizard.test.tsx @@ -0,0 +1,171 @@ +/* + * 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 { shallow } from 'enzyme'; + +import { CreateIndexPatternWizard } from './create_index_pattern_wizard'; +import { coreMock } from '../../../../../../../../core/public/mocks'; +import { dataPluginMock } from '../../../../../../../../plugins/data/public/mocks'; +import { IndexPatternCreationConfig } from '../../../../../../../../plugins/index_pattern_management/public'; +import { IndexPattern } from '../../../../../../../../plugins/data/public'; +import { SavedObjectsClient } from '../../../../../../../../core/public'; + +jest.mock('./components/step_index_pattern', () => ({ StepIndexPattern: 'StepIndexPattern' })); +jest.mock('./components/step_time_field', () => ({ StepTimeField: 'StepTimeField' })); +jest.mock('./components/header', () => ({ Header: 'Header' })); +jest.mock('./components/loading_state', () => ({ LoadingState: 'LoadingState' })); +jest.mock('./components/empty_state', () => ({ EmptyState: 'EmptyState' })); +jest.mock('./lib/get_indices', () => ({ + getIndices: () => { + return [{ name: 'kibana' }]; + }, +})); +jest.mock('ui/chrome', () => ({ + addBasePath: () => {}, +})); + +const { savedObjects, overlays, uiSettings } = coreMock.createStart(); +const { indexPatterns, search } = dataPluginMock.createStartContract(); +const mockIndexPatternCreationType = new IndexPatternCreationConfig({ + type: 'default', + name: 'name', +}); + +const initialQuery = ''; +const services = { + es: search.__LEGACY.esClient, + indexPatterns, + savedObjectsClient: savedObjects.client as SavedObjectsClient, + uiSettings, + changeUrl: jest.fn(), + openConfirm: overlays.openConfirm, + indexPatternCreationType: mockIndexPatternCreationType, +}; + +describe('CreateIndexPatternWizard', () => { + test(`defaults to the loading state`, () => { + const component = shallow( + + ); + + expect(component).toMatchSnapshot(); + }); + + test('renders the empty state when there are no indices', async () => { + const component = shallow( + + ); + + component.setState({ + isInitiallyLoadingIndices: false, + allIndices: [], + remoteClustersExist: false, + }); + + await component.update(); + expect(component).toMatchSnapshot(); + }); + + test('renders when there are no indices but there are remote clusters', async () => { + const component = shallow( + + ); + + component.setState({ + isInitiallyLoadingIndices: false, + allIndices: [], + remoteClustersExist: true, + }); + + await component.update(); + expect(component).toMatchSnapshot(); + }); + + test('shows system indices even if there are no other indices if the include system indices is toggled', async () => { + const component = shallow( + + ); + + component.setState({ + isInitiallyLoadingIndices: false, + isIncludingSystemIndices: true, + allIndices: [{ name: '.kibana ' }], + }); + + await component.update(); + expect(component).toMatchSnapshot(); + }); + + test('renders index pattern step when there are indices', async () => { + const component = shallow( + + ); + + component.setState({ + isInitiallyLoadingIndices: false, + allIndices: [{ name: 'myIndexPattern' }], + }); + + await component.update(); + expect(component).toMatchSnapshot(); + }); + + test('renders time field step when step is set to 2', async () => { + const component = shallow( + + ); + + component.setState({ + isInitiallyLoadingIndices: false, + allIndices: [{ name: 'myIndexPattern' }], + step: 2, + }); + + await component.update(); + expect(component).toMatchSnapshot(); + }); + + test('invokes the provided services when creating an index pattern', async () => { + const create = jest.fn().mockImplementation(() => 'id'); + const clear = jest.fn(); + services.indexPatterns.clearCache = clear; + const indexPattern = ({ + id: '1', + title: 'my-fake-index-pattern', + timeFieldName: 'timestamp', + fields: [], + create, + } as unknown) as IndexPattern; + services.indexPatterns.make = async () => { + return indexPattern; + }; + + const component = shallow( + + ); + + component.setState({ indexPattern: 'foo' }); + await component.instance().createIndexPattern(undefined, 'id'); + expect(services.uiSettings.get).toBeCalled(); + expect(create).toBeCalled(); + expect(clear).toBeCalledWith('id'); + expect(services.changeUrl).toBeCalledWith(`/management/kibana/index_patterns/id`); + }); +}); diff --git a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/create_index_pattern_wizard/create_index_pattern_wizard.tsx b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/create_index_pattern_wizard/create_index_pattern_wizard.tsx new file mode 100644 index 0000000000000..4166d48349d35 --- /dev/null +++ b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/create_index_pattern_wizard/create_index_pattern_wizard.tsx @@ -0,0 +1,299 @@ +/* + * 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, { ReactElement, Component } from 'react'; + +import { EuiGlobalToastList, EuiGlobalToastListToast } from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { i18n } from '@kbn/i18n'; + +import { StepIndexPattern } from './components/step_index_pattern'; +import { StepTimeField } from './components/step_time_field'; +import { Header } from './components/header'; +import { LoadingState } from './components/loading_state'; +import { EmptyState } from './components/empty_state'; + +import { MAX_SEARCH_SIZE } from './constants'; +import { ensureMinimumTime, getIndices } from './lib'; +import { + SavedObjectsClient, + IUiSettingsClient, + OverlayStart, +} from '../../../../../../../../core/public'; +import { DataPublicPluginStart } from '../../../../../../../../plugins/data/public'; +import { IndexPatternCreationConfig } from '../../../../../../../../plugins/index_pattern_management/public'; +import { MatchedIndex } from './types'; + +interface CreateIndexPatternWizardProps { + initialQuery: string; + services: { + indexPatternCreationType: IndexPatternCreationConfig; + es: DataPublicPluginStart['search']['__LEGACY']['esClient']; + indexPatterns: DataPublicPluginStart['indexPatterns']; + savedObjectsClient: SavedObjectsClient; + uiSettings: IUiSettingsClient; + changeUrl: (url: string) => void; + openConfirm: OverlayStart['openConfirm']; + }; +} + +interface CreateIndexPatternWizardState { + step: number; + indexPattern: string; + allIndices: MatchedIndex[]; + remoteClustersExist: boolean; + isInitiallyLoadingIndices: boolean; + isIncludingSystemIndices: boolean; + toasts: EuiGlobalToastListToast[]; +} + +export class CreateIndexPatternWizard extends Component< + CreateIndexPatternWizardProps, + CreateIndexPatternWizardState +> { + state = { + step: 1, + indexPattern: '', + allIndices: [], + remoteClustersExist: false, + isInitiallyLoadingIndices: true, + isIncludingSystemIndices: false, + toasts: [], + }; + + async UNSAFE_componentWillMount() { + this.fetchData(); + } + + catchAndWarn = async ( + asyncFn: Promise, + errorValue: [] | string[], + errorMsg: ReactElement + ) => { + try { + return await asyncFn; + } catch (errors) { + this.setState(prevState => ({ + toasts: prevState.toasts.concat([ + { + title: errorMsg, + id: errorMsg.props.id, + color: 'warning', + iconType: 'alert', + }, + ]), + })); + return errorValue; + } + }; + + fetchData = async () => { + const { services } = this.props; + + this.setState({ + allIndices: [], + isInitiallyLoadingIndices: true, + remoteClustersExist: false, + }); + + const indicesFailMsg = ( + + ); + + const clustersFailMsg = ( + + ); + + // query local and remote indices, updating state independently + ensureMinimumTime( + this.catchAndWarn( + getIndices(services.es, services.indexPatternCreationType, `*`, MAX_SEARCH_SIZE), + [], + indicesFailMsg + ) + ).then((allIndices: MatchedIndex[]) => + this.setState({ allIndices, isInitiallyLoadingIndices: false }) + ); + + this.catchAndWarn( + // if we get an error from remote cluster query, supply fallback value that allows user entry. + // ['a'] is fallback value + getIndices(services.es, services.indexPatternCreationType, `*:*`, 1), + ['a'], + clustersFailMsg + ).then((remoteIndices: string[] | MatchedIndex[]) => + this.setState({ remoteClustersExist: !!remoteIndices.length }) + ); + }; + + createIndexPattern = async (timeFieldName: string | undefined, indexPatternId: string) => { + const { services } = this.props; + const { indexPattern } = this.state; + + const emptyPattern = await services.indexPatterns.make(); + + Object.assign(emptyPattern, { + id: indexPatternId, + title: indexPattern, + timeFieldName, + ...services.indexPatternCreationType.getIndexPatternMappings(), + }); + + const createdId = await emptyPattern.create(); + if (!createdId) { + const confirmMessage = i18n.translate('kbn.management.indexPattern.titleExistsLabel', { + values: { title: emptyPattern.title }, + defaultMessage: "An index pattern with the title '{title}' already exists.", + }); + + const isConfirmed = await services.openConfirm(confirmMessage, { + confirmButtonText: i18n.translate('kbn.management.indexPattern.goToPatternButtonLabel', { + defaultMessage: 'Go to existing pattern', + }), + }); + + if (isConfirmed) { + return services.changeUrl(`/management/kibana/index_patterns/${indexPatternId}`); + } else { + return false; + } + } + + if (!services.uiSettings.get('defaultIndex')) { + await services.uiSettings.set('defaultIndex', createdId); + } + + services.indexPatterns.clearCache(createdId); + services.changeUrl(`/management/kibana/index_patterns/${createdId}`); + }; + + goToTimeFieldStep = (indexPattern: string) => { + this.setState({ step: 2, indexPattern }); + }; + + goToIndexPatternStep = () => { + this.setState({ step: 1 }); + }; + + onChangeIncludingSystemIndices = () => { + this.setState(prevState => ({ + isIncludingSystemIndices: !prevState.isIncludingSystemIndices, + })); + }; + + renderHeader() { + const { isIncludingSystemIndices } = this.state; + const { services } = this.props; + + return ( +
+ ); + } + + renderContent() { + const { + allIndices, + isInitiallyLoadingIndices, + isIncludingSystemIndices, + step, + indexPattern, + remoteClustersExist, + } = this.state; + + if (isInitiallyLoadingIndices) { + return ; + } + + const hasDataIndices = allIndices.some(({ name }: MatchedIndex) => !name.startsWith('.')); + if (!hasDataIndices && !isIncludingSystemIndices && !remoteClustersExist) { + return ; + } + + if (step === 1) { + const { services, initialQuery } = this.props; + return ( + + ); + } + + if (step === 2) { + const { services } = this.props; + return ( + + ); + } + + return null; + } + + removeToast = (id: string) => { + this.setState(prevState => ({ + toasts: prevState.toasts.filter(toast => toast.id !== id), + })); + }; + + render() { + const header = this.renderHeader(); + const content = this.renderContent(); + + return ( + +
+ {header} + {content} +
+ { + this.removeToast(id); + }} + toastLifeTimeMs={6000} + /> +
+ ); + } +} diff --git a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/create_index_pattern_wizard/index.js b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/create_index_pattern_wizard/index.js index 47cb773258cb4..ed1fc026c560c 100644 --- a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/create_index_pattern_wizard/index.js +++ b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/create_index_pattern_wizard/index.js @@ -36,17 +36,15 @@ uiRoutes.when('/management/kibana/index_pattern', { $routeParams.type ); const services = { - config: npStart.core.uiSettings, + uiSettings: npStart.core.uiSettings, es: npStart.plugins.data.search.__LEGACY.esClient, indexPatterns: npStart.plugins.data.indexPatterns, - $http: npStart.core.http, savedObjectsClient: npStart.core.savedObjects.client, indexPatternCreationType, changeUrl: url => { $scope.$evalAsync(() => kbnUrl.changePath(url)); }, openConfirm: npStart.core.overlays.openConfirm, - uiSettings: npStart.core.uiSettings, }; const initialQuery = $routeParams.id ? decodeURIComponent($routeParams.id) : undefined; diff --git a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/create_index_pattern_wizard/lib/get_indices.test.ts b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/create_index_pattern_wizard/lib/get_indices.test.ts index 40583af7177fe..b1500f8303b66 100644 --- a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/create_index_pattern_wizard/lib/get_indices.test.ts +++ b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/create_index_pattern_wizard/lib/get_indices.test.ts @@ -20,7 +20,7 @@ import { getIndices } from './get_indices'; import { IndexPatternCreationConfig } from '../../../../../../../../../plugins/index_pattern_management/public'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { LegacyApiCaller } from '../../../../../../../../../plugins/data/public/search'; +import { LegacyApiCaller } from '../../../../../../../../../plugins/data/public/search/legacy'; export const successfulResponse = { hits: { diff --git a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/constants.ts b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/constants.ts new file mode 100644 index 0000000000000..56da031eb4ee8 --- /dev/null +++ b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/constants.ts @@ -0,0 +1,22 @@ +/* + * 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 const TAB_INDEXED_FIELDS = 'indexedFields'; +export const TAB_SCRIPTED_FIELDS = 'scriptedFields'; +export const TAB_SOURCE_FILTERS = 'sourceFilters'; diff --git a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/create_edit_field.html b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/create_edit_field.html new file mode 100644 index 0000000000000..2decaf423183e --- /dev/null +++ b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/create_edit_field.html @@ -0,0 +1,5 @@ + +
+
+
+
diff --git a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/create_edit_field/create_edit_field.html b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/create_edit_field/create_edit_field.html deleted file mode 100644 index fee8525a6af41..0000000000000 --- a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/create_edit_field/create_edit_field.html +++ /dev/null @@ -1,11 +0,0 @@ - -
-
- -
- -
-
-
diff --git a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/create_edit_field/create_edit_field.js b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/create_edit_field/create_edit_field.js deleted file mode 100644 index 0dcf778a5a662..0000000000000 --- a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/create_edit_field/create_edit_field.js +++ /dev/null @@ -1,178 +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 { IndexPatternField } from '../../../../../../../../../plugins/data/public'; -import { RegistryFieldFormatEditorsProvider } from 'ui/registry/field_format_editors'; -import { docTitle } from 'ui/doc_title'; -import { KbnUrlProvider } from 'ui/url'; -import uiRoutes from 'ui/routes'; -import { toastNotifications } from 'ui/notify'; -import { npStart } from 'ui/new_platform'; - -import template from './create_edit_field.html'; -import { getEditFieldBreadcrumbs, getCreateFieldBreadcrumbs } from '../../breadcrumbs'; - -import React from 'react'; -import { render, unmountComponentAtNode } from 'react-dom'; -import { FieldEditor } from 'ui/field_editor'; -import { I18nContext } from 'ui/i18n'; -import { i18n } from '@kbn/i18n'; - -const REACT_FIELD_EDITOR_ID = 'reactFieldEditor'; -const renderFieldEditor = ( - $scope, - indexPattern, - field, - { getConfig, $http, fieldFormatEditors, redirectAway } -) => { - $scope.$$postDigest(() => { - const node = document.getElementById(REACT_FIELD_EDITOR_ID); - if (!node) { - return; - } - - render( - - - , - node - ); - }); -}; - -const destroyFieldEditor = () => { - const node = document.getElementById(REACT_FIELD_EDITOR_ID); - node && unmountComponentAtNode(node); -}; - -uiRoutes - .when('/management/kibana/index_patterns/:indexPatternId/field/:fieldName*', { - mode: 'edit', - k7Breadcrumbs: getEditFieldBreadcrumbs, - }) - .when('/management/kibana/index_patterns/:indexPatternId/create-field/', { - mode: 'create', - k7Breadcrumbs: getCreateFieldBreadcrumbs, - }) - .defaults(/management\/kibana\/index_patterns\/[^\/]+\/(field|create-field)(\/|$)/, { - template, - mapBreadcrumbs($route, breadcrumbs) { - const { indexPattern } = $route.current.locals; - return breadcrumbs.map(crumb => { - if (crumb.id !== indexPattern.id) { - return crumb; - } - - return { - ...crumb, - display: indexPattern.title, - }; - }); - }, - resolve: { - indexPattern: function($route, Promise, redirectWhenMissing) { - const { indexPatterns } = npStart.plugins.data; - return Promise.resolve(indexPatterns.get($route.current.params.indexPatternId)).catch( - redirectWhenMissing('/management/kibana/index_patterns') - ); - }, - }, - controllerAs: 'fieldSettings', - controller: function FieldEditorPageController( - $scope, - $route, - $timeout, - $http, - Private, - config - ) { - const getConfig = (...args) => config.get(...args); - const fieldFormatEditors = Private(RegistryFieldFormatEditorsProvider); - const kbnUrl = Private(KbnUrlProvider); - - this.mode = $route.current.mode; - this.indexPattern = $route.current.locals.indexPattern; - - if (this.mode === 'edit') { - const fieldName = $route.current.params.fieldName; - this.field = this.indexPattern.fields.getByName(fieldName); - - if (!this.field) { - const message = i18n.translate('kbn.management.editIndexPattern.scripted.noFieldLabel', { - defaultMessage: - "'{indexPatternTitle}' index pattern doesn't have a scripted field called '{fieldName}'", - values: { indexPatternTitle: this.indexPattern.title, fieldName }, - }); - toastNotifications.add(message); - - kbnUrl.redirectToRoute(this.indexPattern, 'edit'); - return; - } - } else if (this.mode === 'create') { - this.field = new IndexPatternField(this.indexPattern, { - scripted: true, - type: 'number', - }); - } else { - const errorMessage = i18n.translate( - 'kbn.management.editIndexPattern.scripted.unknownModeErrorMessage', - { - defaultMessage: 'unknown fieldSettings mode {mode}', - values: { mode: this.mode }, - } - ); - throw new Error(errorMessage); - } - - const fieldName = - this.field.name || - i18n.translate('kbn.management.editIndexPattern.scripted.newFieldPlaceholder', { - defaultMessage: 'New Scripted Field', - }); - docTitle.change([fieldName, this.indexPattern.title]); - - renderFieldEditor($scope, this.indexPattern, this.field, { - getConfig, - $http, - fieldFormatEditors, - redirectAway: () => { - $timeout(() => { - kbnUrl.changeToRoute( - this.indexPattern, - this.field.scripted ? 'scriptedFields' : 'indexedFields' - ); - }); - }, - }); - - $scope.$on('$destroy', () => { - destroyFieldEditor(); - }); - }, - }); diff --git a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/create_edit_field/create_edit_field.tsx b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/create_edit_field/create_edit_field.tsx new file mode 100644 index 0000000000000..4839870f0f3c8 --- /dev/null +++ b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/create_edit_field/create_edit_field.tsx @@ -0,0 +1,111 @@ +/* + * 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 { withRouter, RouteComponentProps } from 'react-router-dom'; +// @ts-ignore +import { FieldEditor } from 'ui/field_editor'; + +import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { IndexHeader } from '../index_header'; +import { IndexPattern, IndexPatternField } from '../../../../../../../../../plugins/data/public'; +import { ChromeDocTitle, NotificationsStart } from '../../../../../../../../../core/public'; +import { TAB_SCRIPTED_FIELDS, TAB_INDEXED_FIELDS } from '../constants'; + +interface CreateEditFieldProps extends RouteComponentProps { + indexPattern: IndexPattern; + mode?: string; + fieldName?: string; + fieldFormatEditors: any; + getConfig: (name: string) => any; + services: { + notifications: NotificationsStart; + docTitle: ChromeDocTitle; + http: Function; + }; +} + +const newFieldPlaceholder = i18n.translate( + 'kbn.management.editIndexPattern.scripted.newFieldPlaceholder', + { + defaultMessage: 'New Scripted Field', + } +); + +export const CreateEditField = withRouter( + ({ + indexPattern, + mode, + fieldName, + fieldFormatEditors, + getConfig, + services, + history, + }: CreateEditFieldProps) => { + const field = + mode === 'edit' && fieldName + ? indexPattern.fields.getByName(fieldName) + : new IndexPatternField(indexPattern, { + scripted: true, + type: 'number', + }); + + const url = `/management/kibana/index_patterns/${indexPattern.id}`; + + if (mode === 'edit') { + if (!field) { + const message = i18n.translate('kbn.management.editIndexPattern.scripted.noFieldLabel', { + defaultMessage: + "'{indexPatternTitle}' index pattern doesn't have a scripted field called '{fieldName}'", + values: { indexPatternTitle: indexPattern.title, fieldName }, + }); + services.notifications.toasts.addWarning(message); + history.push(url); + } + } + + const docFieldName = field?.name || newFieldPlaceholder; + + services.docTitle.change([docFieldName, indexPattern.title]); + + const redirectAway = () => { + history.push(`${url}?_a=(tab:${field?.scripted ? TAB_SCRIPTED_FIELDS : TAB_INDEXED_FIELDS})`); + }; + + return ( + + + + + + + + + ); + } +); diff --git a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/create_edit_field/index.js b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/create_edit_field/index.js deleted file mode 100644 index 890a3b2622577..0000000000000 --- a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/create_edit_field/index.js +++ /dev/null @@ -1,20 +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 './create_edit_field'; diff --git a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/create_edit_field/index.ts b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/create_edit_field/index.ts new file mode 100644 index 0000000000000..473a8f5b57c82 --- /dev/null +++ b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/create_edit_field/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 { CreateEditField } from './create_edit_field'; diff --git a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/edit_index_pattern.html b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/edit_index_pattern.html index 625227be3c2d2..0376df6bbdc58 100644 --- a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/edit_index_pattern.html +++ b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/edit_index_pattern.html @@ -7,12 +7,7 @@ aria-label="{{::'kbn.management.editIndexPattern.detailsAria' | i18n: { defaultMessage: 'Index pattern details' } }}" > - +

diff --git a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/edit_index_pattern.js b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/edit_index_pattern.js index 594430ca01f4c..3239a17f109e4 100644 --- a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/edit_index_pattern.js +++ b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/edit_index_pattern.js @@ -18,15 +18,18 @@ */ import _ from 'lodash'; -import './index_header'; -import './create_edit_field'; +import { HashRouter } from 'react-router-dom'; +import { IndexHeader } from './index_header'; +import { CreateEditField } from './create_edit_field'; import { docTitle } from 'ui/doc_title'; import { KbnUrlProvider } from 'ui/url'; import { IndicesEditSectionsProvider } from './edit_sections'; import { fatalError, toastNotifications } from 'ui/notify'; +import { RegistryFieldFormatEditorsProvider } from 'ui/registry/field_format_editors'; import uiRoutes from 'ui/routes'; import { uiModules } from 'ui/modules'; import template from './edit_index_pattern.html'; +import createEditFieldtemplate from './create_edit_field.html'; import { fieldWildcardMatcher } from '../../../../../../../../plugins/kibana_utils/public'; import { subscribeWithScope } from '../../../../../../../../plugins/kibana_legacy/public'; import React from 'react'; @@ -37,17 +40,20 @@ import { ScriptedFieldsTable } from './scripted_fields_table'; import { i18n } from '@kbn/i18n'; import { I18nContext } from 'ui/i18n'; import { npStart } from 'ui/new_platform'; - -import { getEditBreadcrumbs } from '../breadcrumbs'; +import { + getEditBreadcrumbs, + getEditFieldBreadcrumbs, + getCreateFieldBreadcrumbs, +} from '../breadcrumbs'; +import { TAB_INDEXED_FIELDS, TAB_SCRIPTED_FIELDS, TAB_SOURCE_FILTERS } from './constants'; import { createEditIndexPatternPageStateContainer } from './edit_index_pattern_state_container'; const REACT_SOURCE_FILTERS_DOM_ELEMENT_ID = 'reactSourceFiltersTable'; const REACT_INDEXED_FIELDS_DOM_ELEMENT_ID = 'reactIndexedFieldsTable'; const REACT_SCRIPTED_FIELDS_DOM_ELEMENT_ID = 'reactScriptedFieldsTable'; +const REACT_INDEX_HEADER_DOM_ELEMENT_ID = 'reactIndexHeader'; -const TAB_INDEXED_FIELDS = 'indexedFields'; -const TAB_SCRIPTED_FIELDS = 'scriptedFields'; -const TAB_SOURCE_FILTERS = 'sourceFilters'; +const EDIT_FIELD_PATH = '/management/kibana/index_patterns/{{indexPattern.id}}/field/{{name}}'; function updateSourceFiltersTable($scope) { $scope.$$postDigest(() => { @@ -97,8 +103,8 @@ function updateScriptedFieldsTable($scope) { fieldFilter={$scope.fieldFilter} scriptedFieldLanguageFilter={$scope.scriptedFieldLanguageFilter} helpers={{ - redirectToRoute: (obj, route) => { - $scope.kbnUrl.changeToRoute(obj, route); + redirectToRoute: field => { + $scope.kbnUrl.changePath(EDIT_FIELD_PATH, field); $scope.$apply(); }, getRouteHref: (obj, route) => $scope.kbnUrl.getRouteHref(obj, route), @@ -140,8 +146,8 @@ function updateIndexedFieldsTable($scope) { fieldWildcardMatcher={$scope.fieldWildcardMatcher} indexedFieldTypeFilter={$scope.indexedFieldTypeFilter} helpers={{ - redirectToRoute: (obj, route) => { - $scope.kbnUrl.changeToRoute(obj, route); + redirectToRoute: field => { + $scope.kbnUrl.changePath(EDIT_FIELD_PATH, field); $scope.$apply(); }, getFieldInfo: $scope.getFieldInfo, @@ -158,6 +164,33 @@ function destroyIndexedFieldsTable() { node && unmountComponentAtNode(node); } +function destroyIndexHeader() { + const node = document.getElementById(REACT_INDEX_HEADER_DOM_ELEMENT_ID); + node && unmountComponentAtNode(node); +} + +function renderIndexHeader($scope, config) { + $scope.$$postDigest(() => { + const node = document.getElementById(REACT_INDEX_HEADER_DOM_ELEMENT_ID); + if (!node) { + return; + } + + render( + + + , + node + ); + }); +} + function handleTabChange($scope, newTab) { destroyIndexedFieldsTable(); destroySourceFiltersTable(); @@ -389,6 +422,90 @@ uiModules destroyIndexedFieldsTable(); destroyScriptedFieldsTable(); destroySourceFiltersTable(); + destroyIndexHeader(); destroyState(); }); + + renderIndexHeader($scope, config); + }); + +// routes for create edit field. Will be removed after migartion all component to react. +const REACT_FIELD_EDITOR_ID = 'reactFieldEditor'; +const renderCreateEditField = ($scope, $route, getConfig, $http, fieldFormatEditors) => { + $scope.$$postDigest(() => { + const node = document.getElementById(REACT_FIELD_EDITOR_ID); + if (!node) { + return; + } + + render( + + + + + , + node + ); + }); +}; + +const destroyCreateEditField = () => { + const node = document.getElementById(REACT_FIELD_EDITOR_ID); + node && unmountComponentAtNode(node); +}; + +uiRoutes + .when('/management/kibana/index_patterns/:indexPatternId/field/:fieldName*', { + mode: 'edit', + k7Breadcrumbs: getEditFieldBreadcrumbs, + }) + .when('/management/kibana/index_patterns/:indexPatternId/create-field/', { + mode: 'create', + k7Breadcrumbs: getCreateFieldBreadcrumbs, + }) + .defaults(/management\/kibana\/index_patterns\/[^\/]+\/(field|create-field)(\/|$)/, { + template: createEditFieldtemplate, + mapBreadcrumbs($route, breadcrumbs) { + const { indexPattern } = $route.current.locals; + return breadcrumbs.map(crumb => { + if (crumb.id !== indexPattern.id) { + return crumb; + } + + return { + ...crumb, + display: indexPattern.title, + }; + }); + }, + resolve: { + indexPattern: function($route, Promise, redirectWhenMissing) { + const { indexPatterns } = npStart.plugins.data; + return Promise.resolve(indexPatterns.get($route.current.params.indexPatternId)).catch( + redirectWhenMissing('/management/kibana/index_patterns') + ); + }, + }, + controllerAs: 'fieldSettings', + controller: function FieldEditorPageController($scope, $route, $http, Private, config) { + const getConfig = (...args) => config.get(...args); + const fieldFormatEditors = Private(RegistryFieldFormatEditorsProvider); + + renderCreateEditField($scope, $route, getConfig, $http, fieldFormatEditors); + + $scope.$on('$destroy', () => { + destroyCreateEditField(); + }); + }, }); diff --git a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/index_header/index.js b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/index_header/index.js deleted file mode 100644 index 7c288286bd61e..0000000000000 --- a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/index_header/index.js +++ /dev/null @@ -1,20 +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 './index_header'; diff --git a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/index_header/index.ts b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/index_header/index.ts new file mode 100644 index 0000000000000..44c05a55b36f9 --- /dev/null +++ b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/index_header/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 { IndexHeader } from './index_header'; diff --git a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/index_header/index_header.html b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/index_header/index_header.html deleted file mode 100644 index d6b91d96f13d3..0000000000000 --- a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/index_header/index_header.html +++ /dev/null @@ -1,59 +0,0 @@ -

-
- -

- - {{indexPattern.title}} -

-
- -
- - - - - -
-
diff --git a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/index_header/index_header.js b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/index_header/index_header.js deleted file mode 100644 index 87bce06c1146c..0000000000000 --- a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/index_header/index_header.js +++ /dev/null @@ -1,40 +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 { uiModules } from 'ui/modules'; -import template from './index_header.html'; -uiModules.get('apps/management').directive('kbnManagementIndexPatternsHeader', function(config) { - return { - restrict: 'E', - template, - replace: true, - scope: { - indexPattern: '=', - setDefault: '&', - refreshFields: '&', - delete: '&', - }, - link: function($scope, $el, attrs) { - $scope.delete = attrs.delete ? $scope.delete : null; - $scope.setDefault = attrs.setDefault ? $scope.setDefault : null; - $scope.refreshFields = attrs.refreshFields ? $scope.refreshFields : null; - config.bindToScope($scope, 'defaultIndex'); - }, - }; -}); diff --git a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/index_header/index_header.tsx b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/index_header/index_header.tsx new file mode 100644 index 0000000000000..deac85d9a32e9 --- /dev/null +++ b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/index_header/index_header.tsx @@ -0,0 +1,134 @@ +/* + * 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 { + EuiFlexGroup, + EuiToolTip, + EuiFlexItem, + EuiIcon, + EuiTitle, + EuiButtonIcon, +} from '@elastic/eui'; +import { IIndexPattern } from '../../../../../../../../../plugins/data/public'; + +interface IndexHeaderProps { + indexPattern: IIndexPattern; + defaultIndex?: string; + setDefault?: () => void; + refreshFields?: () => void; + deleteIndexPattern?: () => void; +} + +const setDefaultAriaLabel = i18n.translate('kbn.management.editIndexPattern.setDefaultAria', { + defaultMessage: 'Set as default index.', +}); + +const setDefaultTooltip = i18n.translate('kbn.management.editIndexPattern.setDefaultTooltip', { + defaultMessage: 'Set as default index.', +}); + +const refreshAriaLabel = i18n.translate('kbn.management.editIndexPattern.refreshAria', { + defaultMessage: 'Reload field list.', +}); + +const refreshTooltip = i18n.translate('kbn.management.editIndexPattern.refreshTooltip', { + defaultMessage: 'Refresh field list.', +}); + +const removeAriaLabel = i18n.translate('kbn.management.editIndexPattern.removeAria', { + defaultMessage: 'Remove index pattern.', +}); + +const removeTooltip = i18n.translate('kbn.management.editIndexPattern.removeTooltip', { + defaultMessage: 'Remove index pattern.', +}); + +export function IndexHeader({ + defaultIndex, + indexPattern, + setDefault, + refreshFields, + deleteIndexPattern, +}: IndexHeaderProps) { + return ( + + + + {defaultIndex === indexPattern.id && ( + + + + )} + + +

{indexPattern.title}

+
+
+
+
+ + + {setDefault && ( + + + + + + )} + + {refreshFields && ( + + + + + + )} + + {deleteIndexPattern && ( + + + + + + )} + + +
+ ); +} diff --git a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/indexed_fields_table/__jest__/__snapshots__/indexed_fields_table.test.js.snap b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/indexed_fields_table/__jest__/__snapshots__/indexed_fields_table.test.js.snap deleted file mode 100644 index dc77fe6c8a69d..0000000000000 --- a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/indexed_fields_table/__jest__/__snapshots__/indexed_fields_table.test.js.snap +++ /dev/null @@ -1,102 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`IndexedFieldsTable should filter based on the query bar 1`] = ` -
- - -`; - -exports[`IndexedFieldsTable should filter based on the type filter 1`] = ` -
-
- -`; - -exports[`IndexedFieldsTable should render normally 1`] = ` -
-
- -`; diff --git a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/indexed_fields_table/__jest__/indexed_fields_table.test.js b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/indexed_fields_table/__jest__/indexed_fields_table.test.js deleted file mode 100644 index 26e271ea477da..0000000000000 --- a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/indexed_fields_table/__jest__/indexed_fields_table.test.js +++ /dev/null @@ -1,103 +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 React from 'react'; -import { shallow } from 'enzyme'; - -import { IndexedFieldsTable } from '../indexed_fields_table'; - -jest.mock('@elastic/eui', () => ({ - EuiFlexGroup: 'eui-flex-group', - EuiFlexItem: 'eui-flex-item', - EuiIcon: 'eui-icon', - EuiInMemoryTable: 'eui-in-memory-table', -})); - -jest.mock('../components/table', () => ({ - // Note: this seems to fix React complaining about non lowercase attributes - Table: () => { - return 'table'; - }, -})); - -const helpers = { - redirectToRoute: () => {}, -}; - -const fields = [ - { name: 'Elastic', displayName: 'Elastic', searchable: true }, - { name: 'timestamp', displayName: 'timestamp', type: 'date' }, - { name: 'conflictingField', displayName: 'conflictingField', type: 'conflict' }, -]; - -const indexPattern = { - getNonScriptedFields: () => fields, -}; - -describe('IndexedFieldsTable', () => { - it('should render normally', async () => { - const component = shallow( - {}} - /> - ); - - await new Promise(resolve => process.nextTick(resolve)); - component.update(); - - expect(component).toMatchSnapshot(); - }); - - it('should filter based on the query bar', async () => { - const component = shallow( - {}} - /> - ); - - await new Promise(resolve => process.nextTick(resolve)); - component.setProps({ fieldFilter: 'Elast' }); - component.update(); - - expect(component).toMatchSnapshot(); - }); - - it('should filter based on the type filter', async () => { - const component = shallow( - {}} - /> - ); - - await new Promise(resolve => process.nextTick(resolve)); - component.setProps({ indexedFieldTypeFilter: 'date' }); - component.update(); - - expect(component).toMatchSnapshot(); - }); -}); diff --git a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/indexed_fields_table/__snapshots__/indexed_fields_table.test.tsx.snap b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/indexed_fields_table/__snapshots__/indexed_fields_table.test.tsx.snap new file mode 100644 index 0000000000000..db2a032b1e4d9 --- /dev/null +++ b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/indexed_fields_table/__snapshots__/indexed_fields_table.test.tsx.snap @@ -0,0 +1,99 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`IndexedFieldsTable should filter based on the query bar 1`] = ` +
+
+ +`; + +exports[`IndexedFieldsTable should filter based on the type filter 1`] = ` +
+
+ +`; + +exports[`IndexedFieldsTable should render normally 1`] = ` +
+
+ +`; diff --git a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/indexed_fields_table/components/table/__jest__/__snapshots__/table.test.js.snap b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/indexed_fields_table/components/table/__jest__/__snapshots__/table.test.js.snap deleted file mode 100644 index f3aa2c5da4b67..0000000000000 --- a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/indexed_fields_table/components/table/__jest__/__snapshots__/table.test.js.snap +++ /dev/null @@ -1,159 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`Table should render conflicting type 1`] = ` - - conflict - -   - - - -`; - -exports[`Table should render normal field name 1`] = ` - - Elastic - -`; - -exports[`Table should render normal type 1`] = ` - - string - -`; - -exports[`Table should render normally 1`] = ` - -`; - -exports[`Table should render the boolean template (false) 1`] = ``; - -exports[`Table should render the boolean template (true) 1`] = ` -
-`; - -exports[`Table should render timestamp field name 1`] = ` - - timestamp - -   - - - -`; diff --git a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/indexed_fields_table/components/table/__jest__/table.test.js b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/indexed_fields_table/components/table/__jest__/table.test.js deleted file mode 100644 index 4fd9ef7485bdf..0000000000000 --- a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/indexed_fields_table/components/table/__jest__/table.test.js +++ /dev/null @@ -1,110 +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 React from 'react'; -import { shallow } from 'enzyme'; -import { shallowWithI18nProvider } from 'test_utils/enzyme_helpers'; - -import { Table } from '../table'; - -const indexPattern = { - timeFieldName: 'timestamp', -}; - -const items = [ - { name: 'Elastic', displayName: 'Elastic', searchable: true, info: {} }, - { name: 'timestamp', displayName: 'timestamp', type: 'date', info: {} }, - { name: 'conflictingField', displayName: 'conflictingField', type: 'conflict', info: {} }, -]; - -describe('Table', () => { - it('should render normally', async () => { - const component = shallowWithI18nProvider( -
{}} /> - ); - - expect(component).toMatchSnapshot(); - }); - - it('should render normal field name', async () => { - const component = shallowWithI18nProvider( -
{}} /> - ); - - const tableCell = shallow(component.prop('columns')[0].render('Elastic', items[0])); - expect(tableCell).toMatchSnapshot(); - }); - - it('should render timestamp field name', async () => { - const component = shallowWithI18nProvider( -
{}} /> - ); - - const tableCell = shallow(component.prop('columns')[0].render('timestamp', items[1])); - expect(tableCell).toMatchSnapshot(); - }); - - it('should render the boolean template (true)', async () => { - const component = shallowWithI18nProvider( -
{}} /> - ); - - const tableCell = shallow(component.prop('columns')[3].render(true)); - expect(tableCell).toMatchSnapshot(); - }); - - it('should render the boolean template (false)', async () => { - const component = shallowWithI18nProvider( -
{}} /> - ); - - const tableCell = shallow(component.prop('columns')[3].render(false, items[2])); - expect(tableCell).toMatchSnapshot(); - }); - - it('should render normal type', async () => { - const component = shallowWithI18nProvider( -
{}} /> - ); - - const tableCell = shallow(component.prop('columns')[1].render('string')); - expect(tableCell).toMatchSnapshot(); - }); - - it('should render conflicting type', async () => { - const component = shallowWithI18nProvider( -
{}} /> - ); - - const tableCell = shallow(component.prop('columns')[1].render('conflict', true)); - expect(tableCell).toMatchSnapshot(); - }); - - it('should allow edits', () => { - const editField = jest.fn(); - - const component = shallowWithI18nProvider( -
- ); - - // Click the edit button - component.prop('columns')[6].actions[0].onClick(); - expect(editField).toBeCalled(); - }); -}); diff --git a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/indexed_fields_table/components/table/__snapshots__/table.test.tsx.snap b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/indexed_fields_table/components/table/__snapshots__/table.test.tsx.snap new file mode 100644 index 0000000000000..2d51b1722cfb2 --- /dev/null +++ b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/indexed_fields_table/components/table/__snapshots__/table.test.tsx.snap @@ -0,0 +1,166 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Table should render conflicting type 1`] = ` + + conflict + +   + + + +`; + +exports[`Table should render normal field name 1`] = ` + + Elastic + +`; + +exports[`Table should render normal type 1`] = ` + + string + +`; + +exports[`Table should render normally 1`] = ` + +`; + +exports[`Table should render the boolean template (false) 1`] = ``; + +exports[`Table should render the boolean template (true) 1`] = ` +
+`; + +exports[`Table should render timestamp field name 1`] = ` + + timestamp + +   + + + +`; diff --git a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/indexed_fields_table/components/table/index.js b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/indexed_fields_table/components/table/index.ts similarity index 100% rename from src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/indexed_fields_table/components/table/index.js rename to src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/indexed_fields_table/components/table/index.ts diff --git a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/indexed_fields_table/components/table/table.js b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/indexed_fields_table/components/table/table.js deleted file mode 100644 index 29e160cf1c182..0000000000000 --- a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/indexed_fields_table/components/table/table.js +++ /dev/null @@ -1,231 +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 React, { PureComponent } from 'react'; -import PropTypes from 'prop-types'; - -import { EuiIcon, EuiInMemoryTable, EuiIconTip } from '@elastic/eui'; - -import { i18n } from '@kbn/i18n'; - -export class Table extends PureComponent { - static propTypes = { - indexPattern: PropTypes.object.isRequired, - items: PropTypes.array.isRequired, - editField: PropTypes.func.isRequired, - }; - - renderBooleanTemplate(value, label) { - return value ? : ; - } - - renderFieldName(name, field) { - const { indexPattern } = this.props; - - const infoLabel = i18n.translate( - 'kbn.management.editIndexPattern.fields.table.additionalInfoAriaLabel', - { defaultMessage: 'Additional field information' } - ); - const timeLabel = i18n.translate( - 'kbn.management.editIndexPattern.fields.table.primaryTimeAriaLabel', - { defaultMessage: 'Primary time field' } - ); - const timeContent = i18n.translate( - 'kbn.management.editIndexPattern.fields.table.primaryTimeTooltip', - { defaultMessage: 'This field represents the time that events occurred.' } - ); - - return ( - - {name} - {field.info && field.info.length ? ( - -   - ( -
{info}
- ))} - /> -
- ) : null} - {indexPattern.timeFieldName === name ? ( - -   - - - ) : null} -
- ); - } - - renderFieldType(type, isConflict) { - const label = i18n.translate('kbn.management.editIndexPattern.fields.table.multiTypeAria', { - defaultMessage: 'Multiple type field', - }); - const content = i18n.translate( - 'kbn.management.editIndexPattern.fields.table.multiTypeTooltip', - { - defaultMessage: - 'The type of this field changes across indices. It is unavailable for many analysis functions.', - } - ); - - return ( - - {type} - {isConflict ? ( - -   - - - ) : ( - '' - )} - - ); - } - - render() { - const { items, editField } = this.props; - - const pagination = { - initialPageSize: 10, - pageSizeOptions: [5, 10, 25, 50], - }; - - const columns = [ - { - field: 'displayName', - name: i18n.translate('kbn.management.editIndexPattern.fields.table.nameHeader', { - defaultMessage: 'Name', - }), - dataType: 'string', - sortable: true, - render: (value, field) => { - return this.renderFieldName(value, field); - }, - width: '38%', - 'data-test-subj': 'indexedFieldName', - }, - { - field: 'type', - name: i18n.translate('kbn.management.editIndexPattern.fields.table.typeHeader', { - defaultMessage: 'Type', - }), - dataType: 'string', - sortable: true, - render: value => { - return this.renderFieldType(value, value === 'conflict'); - }, - 'data-test-subj': 'indexedFieldType', - }, - { - field: 'format', - name: i18n.translate('kbn.management.editIndexPattern.fields.table.formatHeader', { - defaultMessage: 'Format', - }), - dataType: 'string', - sortable: true, - }, - { - field: 'searchable', - name: i18n.translate('kbn.management.editIndexPattern.fields.table.searchableHeader', { - defaultMessage: 'Searchable', - }), - description: i18n.translate( - 'kbn.management.editIndexPattern.fields.table.searchableDescription', - { defaultMessage: 'These fields can be used in the filter bar' } - ), - dataType: 'boolean', - sortable: true, - render: value => - this.renderBooleanTemplate( - value, - i18n.translate('kbn.management.editIndexPattern.fields.table.isSearchableAria', { - defaultMessage: 'Is searchable', - }) - ), - }, - { - field: 'aggregatable', - name: i18n.translate('kbn.management.editIndexPattern.fields.table.aggregatableLabel', { - defaultMessage: 'Aggregatable', - }), - description: i18n.translate( - 'kbn.management.editIndexPattern.fields.table.aggregatableDescription', - { defaultMessage: 'These fields can be used in visualization aggregations' } - ), - dataType: 'boolean', - sortable: true, - render: value => - this.renderBooleanTemplate( - value, - i18n.translate('kbn.management.editIndexPattern.fields.table.isAggregatableAria', { - defaultMessage: 'Is aggregatable', - }) - ), - }, - { - field: 'excluded', - name: i18n.translate('kbn.management.editIndexPattern.fields.table.excludedLabel', { - defaultMessage: 'Excluded', - }), - description: i18n.translate( - 'kbn.management.editIndexPattern.fields.table.excludedDescription', - { defaultMessage: 'Fields that are excluded from _source when it is fetched' } - ), - dataType: 'boolean', - sortable: true, - render: value => - this.renderBooleanTemplate( - value, - i18n.translate('kbn.management.editIndexPattern.fields.table.isExcludedAria', { - defaultMessage: 'Is excluded', - }) - ), - }, - { - name: '', - actions: [ - { - name: i18n.translate('kbn.management.editIndexPattern.fields.table.editLabel', { - defaultMessage: 'Edit', - }), - description: i18n.translate( - 'kbn.management.editIndexPattern.fields.table.editDescription', - { defaultMessage: 'Edit' } - ), - icon: 'pencil', - onClick: editField, - type: 'icon', - 'data-test-subj': 'editFieldFormat', - }, - ], - width: '40px', - }, - ]; - - return ( - - ); - } -} diff --git a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/indexed_fields_table/components/table/table.test.tsx b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/indexed_fields_table/components/table/table.test.tsx new file mode 100644 index 0000000000000..d0479a9a9e032 --- /dev/null +++ b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/indexed_fields_table/components/table/table.test.tsx @@ -0,0 +1,132 @@ +/* + * 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 { shallow } from 'enzyme'; +import { IIndexPattern } from '../../../../../../../../../../../plugins/data/public'; +import { IndexedFieldItem } from '../../types'; +import { Table } from './table'; + +const indexPattern = { + timeFieldName: 'timestamp', +} as IIndexPattern; + +const items: IndexedFieldItem[] = [ + { + name: 'Elastic', + displayName: 'Elastic', + searchable: true, + info: [], + type: 'name', + excluded: false, + format: '', + }, + { + name: 'timestamp', + displayName: 'timestamp', + type: 'date', + info: [], + excluded: false, + format: 'YYYY-MM-DD', + }, + { + name: 'conflictingField', + displayName: 'conflictingField', + type: 'conflict', + info: [], + excluded: false, + format: '', + }, +]; + +describe('Table', () => { + test('should render normally', () => { + const component = shallow( +
{}} /> + ); + + expect(component).toMatchSnapshot(); + }); + + test('should render normal field name', () => { + const component = shallow( +
{}} /> + ); + + const tableCell = shallow(component.prop('columns')[0].render('Elastic', items[0])); + expect(tableCell).toMatchSnapshot(); + }); + + test('should render timestamp field name', () => { + const component = shallow( +
{}} /> + ); + + const tableCell = shallow(component.prop('columns')[0].render('timestamp', items[1])); + expect(tableCell).toMatchSnapshot(); + }); + + test('should render the boolean template (true)', () => { + const component = shallow( +
{}} /> + ); + + const tableCell = shallow(component.prop('columns')[3].render(true)); + expect(tableCell).toMatchSnapshot(); + }); + + test('should render the boolean template (false)', () => { + const component = shallow( +
{}} /> + ); + + const tableCell = shallow(component.prop('columns')[3].render(false, items[2])); + expect(tableCell).toMatchSnapshot(); + }); + + test('should render normal type', () => { + const component = shallow( +
{}} /> + ); + + const tableCell = shallow(component.prop('columns')[1].render('string')); + expect(tableCell).toMatchSnapshot(); + }); + + test('should render conflicting type', () => { + const component = shallow( +
{}} /> + ); + + const tableCell = shallow(component.prop('columns')[1].render('conflict', true)); + expect(tableCell).toMatchSnapshot(); + }); + + test('should allow edits', () => { + const editField = jest.fn(); + + const component = shallow( +
+ ); + + // Click the edit button + component.prop('columns')[6].actions[0].onClick(); + expect(editField).toBeCalled(); + }); +}); diff --git a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/indexed_fields_table/components/table/table.tsx b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/indexed_fields_table/components/table/table.tsx new file mode 100644 index 0000000000000..aa8e8b8e13a07 --- /dev/null +++ b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/indexed_fields_table/components/table/table.tsx @@ -0,0 +1,281 @@ +/* + * 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, { PureComponent } from 'react'; + +import { EuiIcon, EuiInMemoryTable, EuiIconTip, EuiBasicTableColumn } from '@elastic/eui'; + +import { i18n } from '@kbn/i18n'; + +import { IIndexPattern } from '../../../../../../../../../../../plugins/data/public'; +import { IndexedFieldItem } from '../../types'; + +// localized labels +const additionalInfoAriaLabel = i18n.translate( + 'kbn.management.editIndexPattern.fields.table.additionalInfoAriaLabel', + { defaultMessage: 'Additional field information' } +); + +const primaryTimeAriaLabel = i18n.translate( + 'kbn.management.editIndexPattern.fields.table.primaryTimeAriaLabel', + { defaultMessage: 'Primary time field' } +); + +const primaryTimeTooltip = i18n.translate( + 'kbn.management.editIndexPattern.fields.table.primaryTimeTooltip', + { defaultMessage: 'This field represents the time that events occurred.' } +); + +const multiTypeAriaLabel = i18n.translate( + 'kbn.management.editIndexPattern.fields.table.multiTypeAria', + { + defaultMessage: 'Multiple type field', + } +); + +const multiTypeTooltip = i18n.translate( + 'kbn.management.editIndexPattern.fields.table.multiTypeTooltip', + { + defaultMessage: + 'The type of this field changes across indices. It is unavailable for many analysis functions.', + } +); + +const nameHeader = i18n.translate('kbn.management.editIndexPattern.fields.table.nameHeader', { + defaultMessage: 'Name', +}); + +const typeHeader = i18n.translate('kbn.management.editIndexPattern.fields.table.typeHeader', { + defaultMessage: 'Type', +}); + +const formatHeader = i18n.translate('kbn.management.editIndexPattern.fields.table.formatHeader', { + defaultMessage: 'Format', +}); + +const searchableHeader = i18n.translate( + 'kbn.management.editIndexPattern.fields.table.searchableHeader', + { + defaultMessage: 'Searchable', + } +); + +const searchableDescription = i18n.translate( + 'kbn.management.editIndexPattern.fields.table.searchableDescription', + { defaultMessage: 'These fields can be used in the filter bar' } +); + +const isSearchableAriaLabel = i18n.translate( + 'kbn.management.editIndexPattern.fields.table.isSearchableAria', + { + defaultMessage: 'Is searchable', + } +); + +const aggregatableLabel = i18n.translate( + 'kbn.management.editIndexPattern.fields.table.aggregatableLabel', + { + defaultMessage: 'Aggregatable', + } +); + +const aggregatableDescription = i18n.translate( + 'kbn.management.editIndexPattern.fields.table.aggregatableDescription', + { defaultMessage: 'These fields can be used in visualization aggregations' } +); + +const isAggregatableAriaLabel = i18n.translate( + 'kbn.management.editIndexPattern.fields.table.isAggregatableAria', + { + defaultMessage: 'Is aggregatable', + } +); + +const excludedLabel = i18n.translate('kbn.management.editIndexPattern.fields.table.excludedLabel', { + defaultMessage: 'Excluded', +}); + +const excludedDescription = i18n.translate( + 'kbn.management.editIndexPattern.fields.table.excludedDescription', + { defaultMessage: 'Fields that are excluded from _source when it is fetched' } +); + +const isExcludedAriaLabel = i18n.translate( + 'kbn.management.editIndexPattern.fields.table.isExcludedAria', + { + defaultMessage: 'Is excluded', + } +); + +const editLabel = i18n.translate('kbn.management.editIndexPattern.fields.table.editLabel', { + defaultMessage: 'Edit', +}); + +const editDescription = i18n.translate( + 'kbn.management.editIndexPattern.fields.table.editDescription', + { defaultMessage: 'Edit' } +); + +interface IndexedFieldProps { + indexPattern: IIndexPattern; + items: IndexedFieldItem[]; + editField: (field: IndexedFieldItem) => void; +} + +export class Table extends PureComponent { + renderBooleanTemplate(value: string, arialLabel: string) { + return value ? : ; + } + + renderFieldName(name: string, field: IndexedFieldItem) { + const { indexPattern } = this.props; + + return ( + + {name} + {field.info && field.info.length ? ( + +   + ( +
{info}
+ ))} + /> +
+ ) : null} + {indexPattern.timeFieldName === name ? ( + +   + + + ) : null} +
+ ); + } + + renderFieldType(type: string, isConflict: boolean) { + return ( + + {type} + {isConflict ? ( + +   + + + ) : ( + '' + )} + + ); + } + + render() { + const { items, editField } = this.props; + + const pagination = { + initialPageSize: 10, + pageSizeOptions: [5, 10, 25, 50], + }; + + const columns: Array> = [ + { + field: 'displayName', + name: nameHeader, + dataType: 'string', + sortable: true, + render: (value: string, field: IndexedFieldItem) => { + return this.renderFieldName(value, field); + }, + width: '38%', + 'data-test-subj': 'indexedFieldName', + }, + { + field: 'type', + name: typeHeader, + dataType: 'string', + sortable: true, + render: (value: string) => { + return this.renderFieldType(value, value === 'conflict'); + }, + 'data-test-subj': 'indexedFieldType', + }, + { + field: 'format', + name: formatHeader, + dataType: 'string', + sortable: true, + }, + { + field: 'searchable', + name: searchableHeader, + description: searchableDescription, + dataType: 'boolean', + sortable: true, + render: (value: string) => this.renderBooleanTemplate(value, isSearchableAriaLabel), + }, + { + field: 'aggregatable', + name: aggregatableLabel, + description: aggregatableDescription, + dataType: 'boolean', + sortable: true, + render: (value: string) => this.renderBooleanTemplate(value, isAggregatableAriaLabel), + }, + { + field: 'excluded', + name: excludedLabel, + description: excludedDescription, + dataType: 'boolean', + sortable: true, + render: (value: string) => this.renderBooleanTemplate(value, isExcludedAriaLabel), + }, + { + name: '', + actions: [ + { + name: editLabel, + description: editDescription, + icon: 'pencil', + onClick: editField, + type: 'icon', + 'data-test-subj': 'editFieldFormat', + }, + ], + width: '40px', + }, + ]; + + return ( + + ); + } +} diff --git a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/indexed_fields_table/index.js b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/indexed_fields_table/index.ts similarity index 100% rename from src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/indexed_fields_table/index.js rename to src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/indexed_fields_table/index.ts diff --git a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/indexed_fields_table/indexed_fields_table.js b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/indexed_fields_table/indexed_fields_table.js deleted file mode 100644 index 652efbe98067f..0000000000000 --- a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/indexed_fields_table/indexed_fields_table.js +++ /dev/null @@ -1,112 +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 React, { Component } from 'react'; -import PropTypes from 'prop-types'; -import { createSelector } from 'reselect'; - -import { Table } from './components/table'; -import { getFieldFormat } from './lib'; - -export class IndexedFieldsTable extends Component { - static propTypes = { - fields: PropTypes.array.isRequired, - indexPattern: PropTypes.object.isRequired, - fieldFilter: PropTypes.string, - indexedFieldTypeFilter: PropTypes.string, - helpers: PropTypes.shape({ - redirectToRoute: PropTypes.func.isRequired, - getFieldInfo: PropTypes.func, - }), - fieldWildcardMatcher: PropTypes.func.isRequired, - }; - - constructor(props) { - super(props); - - this.state = { - fields: this.mapFields(this.props.fields), - }; - } - - UNSAFE_componentWillReceiveProps(nextProps) { - if (nextProps.fields !== this.props.fields) { - this.setState({ - fields: this.mapFields(nextProps.fields), - }); - } - } - - mapFields(fields) { - const { indexPattern, fieldWildcardMatcher, helpers } = this.props; - const sourceFilters = - indexPattern.sourceFilters && indexPattern.sourceFilters.map(f => f.value); - const fieldWildcardMatch = fieldWildcardMatcher(sourceFilters || []); - - return ( - (fields && - fields.map(field => { - return { - ...field, - displayName: field.displayName, - routes: field.routes, - indexPattern: field.indexPattern, - format: getFieldFormat(indexPattern, field.name), - excluded: fieldWildcardMatch ? fieldWildcardMatch(field.name) : false, - info: helpers.getFieldInfo && helpers.getFieldInfo(indexPattern, field.name), - }; - })) || - [] - ); - } - - getFilteredFields = createSelector( - state => state.fields, - (state, props) => props.fieldFilter, - (state, props) => props.indexedFieldTypeFilter, - (fields, fieldFilter, indexedFieldTypeFilter) => { - if (fieldFilter) { - const normalizedFieldFilter = fieldFilter.toLowerCase(); - fields = fields.filter(field => field.name.toLowerCase().includes(normalizedFieldFilter)); - } - - if (indexedFieldTypeFilter) { - fields = fields.filter(field => field.type === indexedFieldTypeFilter); - } - - return fields; - } - ); - - render() { - const { indexPattern } = this.props; - - const fields = this.getFilteredFields(this.state, this.props); - - return ( -
-
this.props.helpers.redirectToRoute(field, 'edit')} - /> - - ); - } -} diff --git a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/indexed_fields_table/indexed_fields_table.test.tsx b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/indexed_fields_table/indexed_fields_table.test.tsx new file mode 100644 index 0000000000000..f8b78a92e098e --- /dev/null +++ b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/indexed_fields_table/indexed_fields_table.test.tsx @@ -0,0 +1,121 @@ +/* + * 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 { shallow } from 'enzyme'; +import { IndexPatternField, IIndexPattern } from '../../../../../../../../../plugins/data/public'; +import { IndexedFieldsTable } from './indexed_fields_table'; + +jest.mock('@elastic/eui', () => ({ + EuiFlexGroup: 'eui-flex-group', + EuiFlexItem: 'eui-flex-item', + EuiIcon: 'eui-icon', + EuiInMemoryTable: 'eui-in-memory-table', +})); + +jest.mock('./components/table', () => ({ + // Note: this seems to fix React complaining about non lowercase attributes + Table: () => { + return 'table'; + }, +})); + +const helpers = { + redirectToRoute: (obj: any) => {}, + getFieldInfo: () => [], +}; + +const fields = [ + { + name: 'Elastic', + displayName: 'Elastic', + searchable: true, + type: 'name', + }, + { name: 'timestamp', displayName: 'timestamp', type: 'date' }, + { name: 'conflictingField', displayName: 'conflictingField', type: 'conflict' }, +] as IndexPatternField[]; + +const indexPattern = ({ + getNonScriptedFields: () => fields, +} as unknown) as IIndexPattern; + +describe('IndexedFieldsTable', () => { + test('should render normally', async () => { + const component = shallow( + { + return () => false; + }} + indexedFieldTypeFilter="" + fieldFilter="" + /> + ); + + await new Promise(resolve => process.nextTick(resolve)); + component.update(); + + expect(component).toMatchSnapshot(); + }); + + test('should filter based on the query bar', async () => { + const component = shallow( + { + return () => false; + }} + indexedFieldTypeFilter="" + fieldFilter="" + /> + ); + + await new Promise(resolve => process.nextTick(resolve)); + component.setProps({ fieldFilter: 'Elast' }); + component.update(); + + expect(component).toMatchSnapshot(); + }); + + test('should filter based on the type filter', async () => { + const component = shallow( + { + return () => false; + }} + indexedFieldTypeFilter="" + fieldFilter="" + /> + ); + + await new Promise(resolve => process.nextTick(resolve)); + component.setProps({ indexedFieldTypeFilter: 'date' }); + component.update(); + + expect(component).toMatchSnapshot(); + }); +}); diff --git a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/indexed_fields_table/indexed_fields_table.tsx b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/indexed_fields_table/indexed_fields_table.tsx new file mode 100644 index 0000000000000..7c2bb565615d7 --- /dev/null +++ b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/indexed_fields_table/indexed_fields_table.tsx @@ -0,0 +1,120 @@ +/* + * 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, { Component } from 'react'; +import { createSelector } from 'reselect'; +import { IndexPatternField, IIndexPattern } from '../../../../../../../../../plugins/data/public'; +import { Table } from './components/table'; +import { getFieldFormat } from './lib'; +import { IndexedFieldItem } from './types'; + +interface IndexedFieldsTableProps { + fields: IndexPatternField[]; + indexPattern: IIndexPattern; + fieldFilter?: string; + indexedFieldTypeFilter?: string; + helpers: { + redirectToRoute: (obj: any) => void; + getFieldInfo: (indexPattern: IIndexPattern, field: string) => string[]; + }; + fieldWildcardMatcher: (filters: any[]) => (val: any) => boolean; +} + +interface IndexedFieldsTableState { + fields: IndexedFieldItem[]; +} + +export class IndexedFieldsTable extends Component< + IndexedFieldsTableProps, + IndexedFieldsTableState +> { + constructor(props: IndexedFieldsTableProps) { + super(props); + + this.state = { + fields: this.mapFields(this.props.fields), + }; + } + + UNSAFE_componentWillReceiveProps(nextProps: IndexedFieldsTableProps) { + if (nextProps.fields !== this.props.fields) { + this.setState({ + fields: this.mapFields(nextProps.fields), + }); + } + } + + mapFields(fields: IndexPatternField[]): IndexedFieldItem[] { + const { indexPattern, fieldWildcardMatcher, helpers } = this.props; + const sourceFilters = + indexPattern.sourceFilters && + indexPattern.sourceFilters.map((f: Record) => f.value); + const fieldWildcardMatch = fieldWildcardMatcher(sourceFilters || []); + + return ( + (fields && + fields.map(field => { + return { + ...field, + displayName: field.displayName, + indexPattern: field.indexPattern, + format: getFieldFormat(indexPattern, field.name), + excluded: fieldWildcardMatch ? fieldWildcardMatch(field.name) : false, + info: helpers.getFieldInfo && helpers.getFieldInfo(indexPattern, field.name), + }; + })) || + [] + ); + } + + getFilteredFields = createSelector( + (state: IndexedFieldsTableState) => state.fields, + (state: IndexedFieldsTableState, props: IndexedFieldsTableProps) => props.fieldFilter, + (state: IndexedFieldsTableState, props: IndexedFieldsTableProps) => + props.indexedFieldTypeFilter, + (fields, fieldFilter, indexedFieldTypeFilter) => { + if (fieldFilter) { + const normalizedFieldFilter = fieldFilter.toLowerCase(); + fields = fields.filter(field => field.name.toLowerCase().includes(normalizedFieldFilter)); + } + + if (indexedFieldTypeFilter) { + fields = fields.filter(field => field.type === indexedFieldTypeFilter); + } + + return fields; + } + ); + + render() { + const { indexPattern } = this.props; + + const fields = this.getFilteredFields(this.state, this.props); + + return ( +
+
this.props.helpers.redirectToRoute(field)} + /> + + ); + } +} diff --git a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/indexed_fields_table/lib/__jest__/get_field_format.test.js b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/indexed_fields_table/lib/__jest__/get_field_format.test.js deleted file mode 100644 index 7090f70199919..0000000000000 --- a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/indexed_fields_table/lib/__jest__/get_field_format.test.js +++ /dev/null @@ -1,52 +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 { getFieldFormat } from '../get_field_format'; - -const indexPattern = { - fieldFormatMap: { - Elastic: { - type: { - title: 'string', - }, - }, - }, -}; - -describe('getFieldFormat', () => { - it('should handle no arguments', () => { - expect(getFieldFormat()).toEqual(''); - }); - - it('should handle no field name', () => { - expect(getFieldFormat(indexPattern)).toEqual(''); - }); - - it('should handle empty name', () => { - expect(getFieldFormat(indexPattern, '')).toEqual(''); - }); - - it('should handle undefined field name', () => { - expect(getFieldFormat(indexPattern, 'none')).toEqual(undefined); - }); - - it('should retrieve field format', () => { - expect(getFieldFormat(indexPattern, 'Elastic')).toEqual('string'); - }); -}); diff --git a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/indexed_fields_table/lib/get_field_format.js b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/indexed_fields_table/lib/get_field_format.js deleted file mode 100644 index 9402694bb1371..0000000000000 --- a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/indexed_fields_table/lib/get_field_format.js +++ /dev/null @@ -1,26 +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 { get } from 'lodash'; - -export function getFieldFormat(indexPattern, fieldName) { - return indexPattern && fieldName - ? get(indexPattern, ['fieldFormatMap', fieldName, 'type', 'title']) - : ''; -} diff --git a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/indexed_fields_table/lib/get_field_format.test.ts b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/indexed_fields_table/lib/get_field_format.test.ts new file mode 100644 index 0000000000000..fc7477c074ac2 --- /dev/null +++ b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/indexed_fields_table/lib/get_field_format.test.ts @@ -0,0 +1,53 @@ +/* + * 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 { IIndexPattern } from '../../../../../../../../../../plugins/data/public'; +import { getFieldFormat } from './get_field_format'; + +const indexPattern = ({ + fieldFormatMap: { + Elastic: { + type: { + title: 'string', + }, + }, + }, +} as unknown) as IIndexPattern; + +describe('getFieldFormat', () => { + test('should handle no arguments', () => { + expect(getFieldFormat()).toEqual(''); + }); + + test('should handle no field name', () => { + expect(getFieldFormat(indexPattern)).toEqual(''); + }); + + test('should handle empty name', () => { + expect(getFieldFormat(indexPattern, '')).toEqual(''); + }); + + test('should handle undefined field name', () => { + expect(getFieldFormat(indexPattern, 'none')).toEqual(undefined); + }); + + test('should retrieve field format', () => { + expect(getFieldFormat(indexPattern, 'Elastic')).toEqual('string'); + }); +}); diff --git a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/indexed_fields_table/lib/get_field_format.ts b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/indexed_fields_table/lib/get_field_format.ts new file mode 100644 index 0000000000000..1d6f267430f07 --- /dev/null +++ b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/indexed_fields_table/lib/get_field_format.ts @@ -0,0 +1,27 @@ +/* + * 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 { get } from 'lodash'; +import { IIndexPattern } from '../../../../../../../../../../plugins/data/public'; + +export function getFieldFormat(indexPattern?: IIndexPattern, fieldName?: string): string { + return indexPattern && fieldName + ? get(indexPattern, ['fieldFormatMap', fieldName, 'type', 'title']) + : ''; +} diff --git a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/indexed_fields_table/lib/index.js b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/indexed_fields_table/lib/index.ts similarity index 100% rename from src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/indexed_fields_table/lib/index.js rename to src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/indexed_fields_table/lib/index.ts diff --git a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/indexed_fields_table/types.ts b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/indexed_fields_table/types.ts new file mode 100644 index 0000000000000..f27f4608bf5d5 --- /dev/null +++ b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/indexed_fields_table/types.ts @@ -0,0 +1,25 @@ +/* + * 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 { IFieldType } from '../../../../../../../../../plugins/data/public'; + +export interface IndexedFieldItem extends IFieldType { + info: string[]; + excluded: boolean; +} diff --git a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/scripted_fields_table/__jest__/__snapshots__/scripted_field_table.test.js.snap b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/scripted_fields_table/__jest__/__snapshots__/scripted_field_table.test.js.snap deleted file mode 100644 index a53f4d7f609cb..0000000000000 --- a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/scripted_fields_table/__jest__/__snapshots__/scripted_field_table.test.js.snap +++ /dev/null @@ -1,186 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`ScriptedFieldsTable should filter based on the lang filter 1`] = ` -
-
- - -
- -`; - -exports[`ScriptedFieldsTable should filter based on the query bar 1`] = ` -
-
- - -
- -`; - -exports[`ScriptedFieldsTable should hide the table if there are no scripted fields 1`] = ` -
-
- - -
- -`; - -exports[`ScriptedFieldsTable should render normally 1`] = ` -
-
- - -
- -`; - -exports[`ScriptedFieldsTable should show a delete modal 1`] = ` -
-
- - -
- - - - -`; diff --git a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/scripted_fields_table/__jest__/scripted_field_table.test.js b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/scripted_fields_table/__jest__/scripted_field_table.test.js deleted file mode 100644 index 5be963ad94b7d..0000000000000 --- a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/scripted_fields_table/__jest__/scripted_field_table.test.js +++ /dev/null @@ -1,176 +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 React from 'react'; -import { shallowWithI18nProvider } from 'test_utils/enzyme_helpers'; - -import { ScriptedFieldsTable } from '../scripted_fields_table'; - -jest.mock('@elastic/eui', () => ({ - EuiTitle: 'eui-title', - EuiText: 'eui-text', - EuiHorizontalRule: 'eui-horizontal-rule', - EuiSpacer: 'eui-spacer', - EuiCallOut: 'eui-call-out', - EuiLink: 'eui-link', - EuiOverlayMask: 'eui-overlay-mask', - EuiConfirmModal: 'eui-confirm-modal', - Comparators: { - property: () => {}, - default: () => {}, - }, -})); -jest.mock('../components/header', () => ({ Header: 'header' })); -jest.mock('../components/call_outs', () => ({ CallOuts: 'call-outs' })); -jest.mock('../components/table', () => ({ - // Note: this seems to fix React complaining about non lowercase attributes - Table: () => { - return 'table'; - }, -})); -jest.mock('ui/scripting_languages', () => ({ - getSupportedScriptingLanguages: () => ['painless'], - getDeprecatedScriptingLanguages: () => [], -})); -jest.mock('ui/documentation_links', () => ({ - documentationLinks: { - scriptedFields: { - painless: 'painlessDocs', - }, - }, -})); - -const helpers = { - redirectToRoute: () => {}, - getRouteHref: () => '#', -}; - -const indexPattern = { - getScriptedFields: () => [ - { name: 'ScriptedField', lang: 'painless', script: 'x++' }, - { name: 'JustATest', lang: 'painless', script: 'z++' }, - ], -}; - -describe('ScriptedFieldsTable', () => { - it('should render normally', async () => { - const component = shallowWithI18nProvider( - - ); - - // Allow the componentWillMount code to execute - // https://github.com/airbnb/enzyme/issues/450 - await component.update(); // Fire `componentWillMount()` - await component.update(); // Force update the component post async actions - - expect(component).toMatchSnapshot(); - }); - - it('should filter based on the query bar', async () => { - const component = shallowWithI18nProvider( - - ); - - // Allow the componentWillMount code to execute - // https://github.com/airbnb/enzyme/issues/450 - await component.update(); // Fire `componentWillMount()` - await component.update(); // Force update the component post async actions - - component.setProps({ fieldFilter: 'Just' }); - component.update(); - - expect(component).toMatchSnapshot(); - }); - - it('should filter based on the lang filter', async () => { - const component = shallowWithI18nProvider( - [ - { name: 'ScriptedField', lang: 'painless', script: 'x++' }, - { name: 'JustATest', lang: 'painless', script: 'z++' }, - { name: 'Bad', lang: 'somethingElse', script: 'z++' }, - ], - }} - helpers={helpers} - /> - ); - - // Allow the componentWillMount code to execute - // https://github.com/airbnb/enzyme/issues/450 - await component.update(); // Fire `componentWillMount()` - await component.update(); // Force update the component post async actions - - component.setProps({ scriptedFieldLanguageFilter: 'painless' }); - component.update(); - - expect(component).toMatchSnapshot(); - }); - - it('should hide the table if there are no scripted fields', async () => { - const component = shallowWithI18nProvider( - [], - }} - helpers={helpers} - /> - ); - - // Allow the componentWillMount code to execute - // https://github.com/airbnb/enzyme/issues/450 - await component.update(); // Fire `componentWillMount()` - await component.update(); // Force update the component post async actions - - expect(component).toMatchSnapshot(); - }); - - it('should show a delete modal', async () => { - const component = shallowWithI18nProvider( - - ); - - await component.update(); // Fire `componentWillMount()` - component.instance().startDeleteField({ name: 'ScriptedField' }); - await component.update(); - - // Ensure the modal is visible - expect(component).toMatchSnapshot(); - }); - - it('should delete a field', async () => { - const removeScriptedField = jest.fn(); - const component = shallowWithI18nProvider( - - ); - - await component.update(); // Fire `componentWillMount()` - component.instance().startDeleteField({ name: 'ScriptedField' }); - await component.update(); - await component.instance().deleteField(); - await component.update(); - expect(removeScriptedField).toBeCalled(); - }); -}); diff --git a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/scripted_fields_table/__snapshots__/scripted_field_table.test.tsx.snap b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/scripted_fields_table/__snapshots__/scripted_field_table.test.tsx.snap new file mode 100644 index 0000000000000..569b75c848c52 --- /dev/null +++ b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/scripted_fields_table/__snapshots__/scripted_field_table.test.tsx.snap @@ -0,0 +1,188 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`ScriptedFieldsTable should filter based on the lang filter 1`] = ` + +
+ + +
+ +`; + +exports[`ScriptedFieldsTable should filter based on the query bar 1`] = ` + +
+ + +
+ +`; + +exports[`ScriptedFieldsTable should hide the table if there are no scripted fields 1`] = ` + +
+ + +
+ +`; + +exports[`ScriptedFieldsTable should render normally 1`] = ` + +
+ + +
+ +`; + +exports[`ScriptedFieldsTable should show a delete modal 1`] = ` + +
+ + +
+ + +`; diff --git a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/scripted_fields_table/components/call_outs/__jest__/__snapshots__/call_outs.test.js.snap b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/scripted_fields_table/components/call_outs/__jest__/__snapshots__/call_outs.test.js.snap deleted file mode 100644 index e6f0d6cd819e3..0000000000000 --- a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/scripted_fields_table/components/call_outs/__jest__/__snapshots__/call_outs.test.js.snap +++ /dev/null @@ -1,43 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`CallOuts should render normally 1`] = ` -
- - } - > -

- - - , - } - } - /> -

-
- -
-`; - -exports[`CallOuts should render without any call outs 1`] = `""`; diff --git a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/scripted_fields_table/components/call_outs/__jest__/call_outs.test.js b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/scripted_fields_table/components/call_outs/__jest__/call_outs.test.js deleted file mode 100644 index 12e0ee8839967..0000000000000 --- a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/scripted_fields_table/components/call_outs/__jest__/call_outs.test.js +++ /dev/null @@ -1,44 +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 React from 'react'; -import { shallow } from 'enzyme'; - -import { CallOuts } from '../call_outs'; - -describe('CallOuts', () => { - it('should render normally', async () => { - const component = shallow( - - ); - - expect(component).toMatchSnapshot(); - }); - - it('should render without any call outs', async () => { - const component = shallow( - - ); - - expect(component).toMatchSnapshot(); - }); -}); diff --git a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/scripted_fields_table/components/call_outs/__snapshots__/call_outs.test.tsx.snap b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/scripted_fields_table/components/call_outs/__snapshots__/call_outs.test.tsx.snap new file mode 100644 index 0000000000000..4dfda1b9339b1 --- /dev/null +++ b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/scripted_fields_table/components/call_outs/__snapshots__/call_outs.test.tsx.snap @@ -0,0 +1,43 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`CallOuts should render normally 1`] = ` + + + } + > +

+ + + , + } + } + /> +

+
+ +
+`; + +exports[`CallOuts should render without any call outs 1`] = `""`; diff --git a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/scripted_fields_table/components/call_outs/call_outs.js b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/scripted_fields_table/components/call_outs/call_outs.js deleted file mode 100644 index 0c321c8ba8b01..0000000000000 --- a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/scripted_fields_table/components/call_outs/call_outs.js +++ /dev/null @@ -1,65 +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 React from 'react'; - -import { EuiCallOut, EuiLink, EuiSpacer } from '@elastic/eui'; - -import { FormattedMessage } from '@kbn/i18n/react'; - -export const CallOuts = ({ deprecatedLangsInUse, painlessDocLink }) => { - if (!deprecatedLangsInUse.length) { - return null; - } - - return ( -
- - } - color="danger" - iconType="cross" - > -

- - - - ), - }} - /> -

-
- -
- ); -}; diff --git a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/scripted_fields_table/components/call_outs/call_outs.test.tsx b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/scripted_fields_table/components/call_outs/call_outs.test.tsx new file mode 100644 index 0000000000000..407928931191d --- /dev/null +++ b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/scripted_fields_table/components/call_outs/call_outs.test.tsx @@ -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. + */ + +import React from 'react'; +import { shallow } from 'enzyme'; + +import { CallOuts } from '../call_outs'; + +describe('CallOuts', () => { + test('should render normally', () => { + const component = shallow( + + ); + + expect(component).toMatchSnapshot(); + }); + + test('should render without any call outs', () => { + const component = shallow( + + ); + + expect(component).toMatchSnapshot(); + }); +}); diff --git a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/scripted_fields_table/components/call_outs/call_outs.tsx b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/scripted_fields_table/components/call_outs/call_outs.tsx new file mode 100644 index 0000000000000..8e38b569a32fa --- /dev/null +++ b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/scripted_fields_table/components/call_outs/call_outs.tsx @@ -0,0 +1,69 @@ +/* + * 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 { EuiCallOut, EuiLink, EuiSpacer } from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n/react'; + +interface CallOutsProps { + deprecatedLangsInUse: string[]; + painlessDocLink: string; +} + +export const CallOuts = ({ deprecatedLangsInUse, painlessDocLink }: CallOutsProps) => { + if (!deprecatedLangsInUse.length) { + return null; + } + + return ( + <> + + } + color="danger" + iconType="cross" + > +

+ + + + ), + }} + /> +

+
+ + + ); +}; diff --git a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/scripted_fields_table/components/call_outs/index.js b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/scripted_fields_table/components/call_outs/index.ts similarity index 100% rename from src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/scripted_fields_table/components/call_outs/index.js rename to src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/scripted_fields_table/components/call_outs/index.ts diff --git a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/scripted_fields_table/components/confirmation_modal/__snapshots__/confirmation_modal.test.tsx.snap b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/scripted_fields_table/components/confirmation_modal/__snapshots__/confirmation_modal.test.tsx.snap new file mode 100644 index 0000000000000..2b320782cb163 --- /dev/null +++ b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/scripted_fields_table/components/confirmation_modal/__snapshots__/confirmation_modal.test.tsx.snap @@ -0,0 +1,14 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`DeleteScritpedFieldConfirmationModal should render normally 1`] = ` + + + +`; diff --git a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/scripted_fields_table/components/confirmation_modal/confirmation_modal.test.tsx b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/scripted_fields_table/components/confirmation_modal/confirmation_modal.test.tsx new file mode 100644 index 0000000000000..f3594e7507a6a --- /dev/null +++ b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/scripted_fields_table/components/confirmation_modal/confirmation_modal.test.tsx @@ -0,0 +1,37 @@ +/* + * 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 { shallow } from 'enzyme'; + +import { DeleteScritpedFieldConfirmationModal } from './confirmation_modal'; + +describe('DeleteScritpedFieldConfirmationModal', () => { + test('should render normally', () => { + const component = shallow( + {}} + hideDeleteConfirmationModal={() => {}} + /> + ); + + expect(component).toMatchSnapshot(); + }); +}); diff --git a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/scripted_fields_table/components/confirmation_modal/confirmation_modal.tsx b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/scripted_fields_table/components/confirmation_modal/confirmation_modal.tsx new file mode 100644 index 0000000000000..1e82174f863b0 --- /dev/null +++ b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/scripted_fields_table/components/confirmation_modal/confirmation_modal.tsx @@ -0,0 +1,63 @@ +/* + * 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 { EUI_MODAL_CONFIRM_BUTTON, EuiConfirmModal, EuiOverlayMask } from '@elastic/eui'; + +import { ScriptedFieldItem } from '../../types'; + +interface DeleteScritpedFieldConfirmationModalProps { + field: ScriptedFieldItem; + hideDeleteConfirmationModal: ( + event?: React.KeyboardEvent | React.MouseEvent + ) => void; + deleteField: (event: React.MouseEvent) => void; +} + +export const DeleteScritpedFieldConfirmationModal = ({ + field, + hideDeleteConfirmationModal, + deleteField, +}: DeleteScritpedFieldConfirmationModalProps) => { + const title = i18n.translate('kbn.management.editIndexPattern.scripted.deleteFieldLabel', { + defaultMessage: "Delete scripted field '{fieldName}'?", + values: { fieldName: field.name }, + }); + const cancelButtonText = i18n.translate( + 'kbn.management.editIndexPattern.scripted.deleteField.cancelButton', + { defaultMessage: 'Cancel' } + ); + const confirmButtonText = i18n.translate( + 'kbn.management.editIndexPattern.scripted.deleteField.deleteButton', + { defaultMessage: 'Delete' } + ); + + return ( + + + + ); +}; diff --git a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/scripted_fields_table/components/confirmation_modal/index.ts b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/scripted_fields_table/components/confirmation_modal/index.ts new file mode 100644 index 0000000000000..b87b572333e6f --- /dev/null +++ b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/scripted_fields_table/components/confirmation_modal/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 { DeleteScritpedFieldConfirmationModal } from './confirmation_modal'; diff --git a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/scripted_fields_table/components/header/__jest__/header.test.js b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/scripted_fields_table/components/header/__jest__/header.test.js deleted file mode 100644 index 3e377ccfbdd41..0000000000000 --- a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/scripted_fields_table/components/header/__jest__/header.test.js +++ /dev/null @@ -1,31 +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 React from 'react'; -import { shallow } from 'enzyme'; - -import { Header } from '../header'; - -describe('Header', () => { - it('should render normally', async () => { - const component = shallow(
); - - expect(component).toMatchSnapshot(); - }); -}); diff --git a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/scripted_fields_table/components/header/__jest__/__snapshots__/header.test.js.snap b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/scripted_fields_table/components/header/__snapshots__/header.test.tsx.snap similarity index 100% rename from src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/scripted_fields_table/components/header/__jest__/__snapshots__/header.test.js.snap rename to src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/scripted_fields_table/components/header/__snapshots__/header.test.tsx.snap diff --git a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/scripted_fields_table/components/header/header.js b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/scripted_fields_table/components/header/header.js deleted file mode 100644 index 97c235d82f870..0000000000000 --- a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/scripted_fields_table/components/header/header.js +++ /dev/null @@ -1,62 +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 React from 'react'; -import PropTypes from 'prop-types'; - -import { EuiButton, EuiFlexGroup, EuiFlexItem, EuiText, EuiTitle } from '@elastic/eui'; - -import { FormattedMessage } from '@kbn/i18n/react'; - -export const Header = ({ addScriptedFieldUrl }) => ( - - - -

- -

-
- -

- -

-
-
- - - - - - -
-); - -Header.propTypes = { - addScriptedFieldUrl: PropTypes.string.isRequired, -}; diff --git a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/scripted_fields_table/components/header/header.test.tsx b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/scripted_fields_table/components/header/header.test.tsx new file mode 100644 index 0000000000000..19479de8f2aa4 --- /dev/null +++ b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/scripted_fields_table/components/header/header.test.tsx @@ -0,0 +1,31 @@ +/* + * 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 { shallow } from 'enzyme'; + +import { Header } from './header'; + +describe('Header', () => { + test('should render normally', () => { + const component = shallow(
); + + expect(component).toMatchSnapshot(); + }); +}); diff --git a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/scripted_fields_table/components/header/header.tsx b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/scripted_fields_table/components/header/header.tsx new file mode 100644 index 0000000000000..b8f832dad72af --- /dev/null +++ b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/scripted_fields_table/components/header/header.tsx @@ -0,0 +1,60 @@ +/* + * 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 { EuiButton, EuiFlexGroup, EuiFlexItem, EuiText, EuiTitle } from '@elastic/eui'; + +import { FormattedMessage } from '@kbn/i18n/react'; + +interface HeaderProps { + addScriptedFieldUrl: string; +} + +export const Header = ({ addScriptedFieldUrl }: HeaderProps) => ( + + + +

+ +

+
+ +

+ +

+
+
+ + + + + + +
+); diff --git a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/scripted_fields_table/components/header/index.js b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/scripted_fields_table/components/header/index.ts similarity index 100% rename from src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/scripted_fields_table/components/header/index.js rename to src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/scripted_fields_table/components/header/index.ts diff --git a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/scripted_fields_table/components/index.js b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/scripted_fields_table/components/index.js deleted file mode 100644 index 5c0bb41eab765..0000000000000 --- a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/scripted_fields_table/components/index.js +++ /dev/null @@ -1,22 +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. - */ - -export { Table } from './table'; -export { Header } from './header'; -export { CallOuts } from './call_outs'; diff --git a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/scripted_fields_table/components/index.ts b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/scripted_fields_table/components/index.ts new file mode 100644 index 0000000000000..7d74776fb2bca --- /dev/null +++ b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/scripted_fields_table/components/index.ts @@ -0,0 +1,23 @@ +/* + * 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 { Table } from './table'; +export { Header } from './header'; +export { CallOuts } from './call_outs'; +export { DeleteScritpedFieldConfirmationModal } from './confirmation_modal'; diff --git a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/scripted_fields_table/components/table/__jest__/__snapshots__/table.test.js.snap b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/scripted_fields_table/components/table/__jest__/__snapshots__/table.test.js.snap deleted file mode 100644 index 2da4d84463b29..0000000000000 --- a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/scripted_fields_table/components/table/__jest__/__snapshots__/table.test.js.snap +++ /dev/null @@ -1,87 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`Table should render normally 1`] = ` - -`; - -exports[`Table should render the format 1`] = ` - - string - -`; diff --git a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/scripted_fields_table/components/table/__jest__/table.test.js b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/scripted_fields_table/components/table/__jest__/table.test.js deleted file mode 100644 index 4545bfa8f64db..0000000000000 --- a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/scripted_fields_table/components/table/__jest__/table.test.js +++ /dev/null @@ -1,103 +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 React from 'react'; -import { shallow } from 'enzyme'; -import { shallowWithI18nProvider } from 'test_utils/enzyme_helpers'; - -import { Table } from '../table'; - -const indexPattern = { - fieldFormatMap: { - Elastic: { - type: { - title: 'string', - }, - }, - }, -}; - -const items = [{ id: 1, name: 'Elastic' }]; - -describe('Table', () => { - it('should render normally', async () => { - const component = shallowWithI18nProvider( -
{}} - deleteField={() => {}} - onChange={() => {}} - /> - ); - - expect(component).toMatchSnapshot(); - }); - - it('should render the format', async () => { - const component = shallowWithI18nProvider( -
{}} - deleteField={() => {}} - onChange={() => {}} - /> - ); - - const formatTableCell = shallow(component.prop('columns')[3].render('Elastic')); - expect(formatTableCell).toMatchSnapshot(); - }); - - it('should allow edits', () => { - const editField = jest.fn(); - - const component = shallowWithI18nProvider( -
{}} - onChange={() => {}} - /> - ); - - // Click the delete button - component.prop('columns')[4].actions[0].onClick(); - expect(editField).toBeCalled(); - }); - - it('should allow deletes', () => { - const deleteField = jest.fn(); - - const component = shallowWithI18nProvider( -
{}} - deleteField={deleteField} - onChange={() => {}} - /> - ); - - // Click the delete button - component.prop('columns')[4].actions[1].onClick(); - expect(deleteField).toBeCalled(); - }); -}); diff --git a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/scripted_fields_table/components/table/__snapshots__/table.test.tsx.snap b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/scripted_fields_table/components/table/__snapshots__/table.test.tsx.snap new file mode 100644 index 0000000000000..8439887dd468a --- /dev/null +++ b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/scripted_fields_table/components/table/__snapshots__/table.test.tsx.snap @@ -0,0 +1,90 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Table should render normally 1`] = ` + +`; + +exports[`Table should render the format 1`] = ` + + string + +`; diff --git a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/scripted_fields_table/components/table/index.js b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/scripted_fields_table/components/table/index.ts similarity index 100% rename from src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/scripted_fields_table/components/table/index.js rename to src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/scripted_fields_table/components/table/index.ts diff --git a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/scripted_fields_table/components/table/table.js b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/scripted_fields_table/components/table/table.js deleted file mode 100644 index 5e05dd95827c7..0000000000000 --- a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/scripted_fields_table/components/table/table.js +++ /dev/null @@ -1,140 +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 React, { PureComponent } from 'react'; -import PropTypes from 'prop-types'; - -import { EuiInMemoryTable } from '@elastic/eui'; - -import { i18n } from '@kbn/i18n'; - -export class Table extends PureComponent { - static propTypes = { - indexPattern: PropTypes.object.isRequired, - items: PropTypes.array.isRequired, - editField: PropTypes.func.isRequired, - deleteField: PropTypes.func.isRequired, - }; - - renderFormatCell = value => { - const { indexPattern } = this.props; - - const title = - indexPattern.fieldFormatMap[value] && indexPattern.fieldFormatMap[value].type - ? indexPattern.fieldFormatMap[value].type.title - : ''; - - return {title}; - }; - - render() { - const { items, editField, deleteField } = this.props; - - const columns = [ - { - field: 'displayName', - name: i18n.translate('kbn.management.editIndexPattern.scripted.table.nameHeader', { - defaultMessage: 'Name', - }), - description: i18n.translate( - 'kbn.management.editIndexPattern.scripted.table.nameDescription', - { defaultMessage: 'Name of the field' } - ), - dataType: 'string', - sortable: true, - width: '38%', - }, - { - field: 'lang', - name: i18n.translate('kbn.management.editIndexPattern.scripted.table.langHeader', { - defaultMessage: 'Lang', - }), - description: i18n.translate( - 'kbn.management.editIndexPattern.scripted.table.langDescription', - { defaultMessage: 'Language used for the field' } - ), - dataType: 'string', - sortable: true, - 'data-test-subj': 'scriptedFieldLang', - }, - { - field: 'script', - name: i18n.translate('kbn.management.editIndexPattern.scripted.table.scriptHeader', { - defaultMessage: 'Script', - }), - description: i18n.translate( - 'kbn.management.editIndexPattern.scripted.table.scriptDescription', - { defaultMessage: 'Script for the field' } - ), - dataType: 'string', - sortable: true, - }, - { - field: 'name', - name: i18n.translate('kbn.management.editIndexPattern.scripted.table.formatHeader', { - defaultMessage: 'Format', - }), - description: i18n.translate( - 'kbn.management.editIndexPattern.scripted.table.formatDescription', - { defaultMessage: 'Format used for the field' } - ), - render: this.renderFormatCell, - sortable: false, - }, - { - name: '', - actions: [ - { - name: i18n.translate('kbn.management.editIndexPattern.scripted.table.editHeader', { - defaultMessage: 'Edit', - }), - description: i18n.translate( - 'kbn.management.editIndexPattern.scripted.table.editDescription', - { defaultMessage: 'Edit this field' } - ), - icon: 'pencil', - onClick: editField, - }, - { - name: i18n.translate('kbn.management.editIndexPattern.scripted.table.deleteHeader', { - defaultMessage: 'Delete', - }), - description: i18n.translate( - 'kbn.management.editIndexPattern.scripted.table.deleteDescription', - { defaultMessage: 'Delete this field' } - ), - icon: 'trash', - color: 'danger', - onClick: deleteField, - }, - ], - width: '40px', - }, - ]; - - const pagination = { - initialPageSize: 10, - pageSizeOptions: [5, 10, 25, 50], - }; - - return ( - - ); - } -} diff --git a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/scripted_fields_table/components/table/table.test.tsx b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/scripted_fields_table/components/table/table.test.tsx new file mode 100644 index 0000000000000..13b3875f58687 --- /dev/null +++ b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/scripted_fields_table/components/table/table.test.tsx @@ -0,0 +1,106 @@ +/* + * 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 { shallow } from 'enzyme'; + +import { Table } from '../table'; +import { ScriptedFieldItem } from '../../types'; +import { IIndexPattern } from '../../../../../../../../../../../plugins/data/public'; + +const getIndexPatternMock = (mockedFields: any = {}) => ({ ...mockedFields } as IIndexPattern); + +const items: ScriptedFieldItem[] = [{ name: '1', lang: 'Elastic', script: '' }]; + +describe('Table', () => { + let indexPattern: IIndexPattern; + + beforeEach(() => { + indexPattern = getIndexPatternMock({ + fieldFormatMap: { + Elastic: { + type: { + title: 'string', + }, + }, + }, + }); + }); + + test('should render normally', () => { + const component = shallow
( +
{}} + deleteField={() => {}} + /> + ); + + expect(component).toMatchSnapshot(); + }); + + test('should render the format', () => { + const component = shallow( +
{}} + deleteField={() => {}} + /> + ); + + const formatTableCell = shallow(component.prop('columns')[3].render('Elastic')); + expect(formatTableCell).toMatchSnapshot(); + }); + + test('should allow edits', () => { + const editField = jest.fn(); + + const component = shallow( +
{}} + /> + ); + + // Click the delete button + component.prop('columns')[4].actions[0].onClick(); + expect(editField).toBeCalled(); + }); + + test('should allow deletes', () => { + const deleteField = jest.fn(); + + const component = shallow( +
{}} + deleteField={deleteField} + /> + ); + + // Click the delete button + component.prop('columns')[4].actions[1].onClick(); + expect(deleteField).toBeCalled(); + }); +}); diff --git a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/scripted_fields_table/components/table/table.tsx b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/scripted_fields_table/components/table/table.tsx new file mode 100644 index 0000000000000..14aed11b32203 --- /dev/null +++ b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/scripted_fields_table/components/table/table.tsx @@ -0,0 +1,139 @@ +/* + * 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, { PureComponent } from 'react'; +import { get } from 'lodash'; +import { i18n } from '@kbn/i18n'; +import { EuiInMemoryTable, EuiBasicTableColumn } from '@elastic/eui'; + +import { ScriptedFieldItem } from '../../types'; +import { IIndexPattern } from '../../../../../../../../../../../plugins/data/public'; + +interface TableProps { + indexPattern: IIndexPattern; + items: ScriptedFieldItem[]; + editField: (field: ScriptedFieldItem) => void; + deleteField: (field: ScriptedFieldItem) => void; +} + +export class Table extends PureComponent { + renderFormatCell = (value: string) => { + const { indexPattern } = this.props; + const title = get(indexPattern, ['fieldFormatMap', value, 'type', 'title'], ''); + + return {title}; + }; + + render() { + const { items, editField, deleteField } = this.props; + + const columns: Array> = [ + { + field: 'displayName', + name: i18n.translate('kbn.management.editIndexPattern.scripted.table.nameHeader', { + defaultMessage: 'Name', + }), + description: i18n.translate( + 'kbn.management.editIndexPattern.scripted.table.nameDescription', + { defaultMessage: 'Name of the field' } + ), + dataType: 'string', + sortable: true, + width: '38%', + }, + { + field: 'lang', + name: i18n.translate('kbn.management.editIndexPattern.scripted.table.langHeader', { + defaultMessage: 'Lang', + }), + description: i18n.translate( + 'kbn.management.editIndexPattern.scripted.table.langDescription', + { defaultMessage: 'Language used for the field' } + ), + dataType: 'string', + sortable: true, + 'data-test-subj': 'scriptedFieldLang', + }, + { + field: 'script', + name: i18n.translate('kbn.management.editIndexPattern.scripted.table.scriptHeader', { + defaultMessage: 'Script', + }), + description: i18n.translate( + 'kbn.management.editIndexPattern.scripted.table.scriptDescription', + { defaultMessage: 'Script for the field' } + ), + dataType: 'string', + sortable: true, + }, + { + field: 'name', + name: i18n.translate('kbn.management.editIndexPattern.scripted.table.formatHeader', { + defaultMessage: 'Format', + }), + description: i18n.translate( + 'kbn.management.editIndexPattern.scripted.table.formatDescription', + { defaultMessage: 'Format used for the field' } + ), + render: this.renderFormatCell, + sortable: false, + }, + { + name: '', + actions: [ + { + type: 'icon', + name: i18n.translate('kbn.management.editIndexPattern.scripted.table.editHeader', { + defaultMessage: 'Edit', + }), + description: i18n.translate( + 'kbn.management.editIndexPattern.scripted.table.editDescription', + { defaultMessage: 'Edit this field' } + ), + icon: 'pencil', + onClick: editField, + }, + { + type: 'icon', + name: i18n.translate('kbn.management.editIndexPattern.scripted.table.deleteHeader', { + defaultMessage: 'Delete', + }), + description: i18n.translate( + 'kbn.management.editIndexPattern.scripted.table.deleteDescription', + { defaultMessage: 'Delete this field' } + ), + icon: 'trash', + color: 'danger', + onClick: deleteField, + }, + ], + width: '40px', + }, + ]; + + const pagination = { + initialPageSize: 10, + pageSizeOptions: [5, 10, 25, 50], + }; + + return ( + + ); + } +} diff --git a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/scripted_fields_table/index.js b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/scripted_fields_table/index.ts similarity index 100% rename from src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/scripted_fields_table/index.js rename to src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/scripted_fields_table/index.ts diff --git a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/scripted_fields_table/scripted_field_table.test.tsx b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/scripted_fields_table/scripted_field_table.test.tsx new file mode 100644 index 0000000000000..914d80f9f61d7 --- /dev/null +++ b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/scripted_fields_table/scripted_field_table.test.tsx @@ -0,0 +1,187 @@ +/* + * 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 { shallow } from 'enzyme'; + +import { ScriptedFieldsTable } from '../scripted_fields_table'; +import { IIndexPattern } from '../../../../../../../../../plugins/data/common/index_patterns'; + +jest.mock('@elastic/eui', () => ({ + EuiTitle: 'eui-title', + EuiText: 'eui-text', + EuiHorizontalRule: 'eui-horizontal-rule', + EuiSpacer: 'eui-spacer', + EuiCallOut: 'eui-call-out', + EuiLink: 'eui-link', + EuiOverlayMask: 'eui-overlay-mask', + EuiConfirmModal: 'eui-confirm-modal', + Comparators: { + property: () => {}, + default: () => {}, + }, +})); +jest.mock('./components/header', () => ({ Header: 'header' })); +jest.mock('./components/call_outs', () => ({ CallOuts: 'call-outs' })); +jest.mock('./components/table', () => ({ + // Note: this seems to fix React complaining about non lowercase attributes + Table: () => { + return 'table'; + }, +})); + +jest.mock('ui/scripting_languages', () => ({ + getSupportedScriptingLanguages: () => ['painless'], + getDeprecatedScriptingLanguages: () => [], +})); + +jest.mock('ui/documentation_links', () => ({ + documentationLinks: { + scriptedFields: { + painless: 'painlessDocs', + }, + }, +})); + +const helpers = { + redirectToRoute: () => {}, + getRouteHref: () => '#', +}; + +const getIndexPatternMock = (mockedFields: any = {}) => ({ ...mockedFields } as IIndexPattern); + +describe('ScriptedFieldsTable', () => { + let indexPattern: IIndexPattern; + + beforeEach(() => { + indexPattern = getIndexPatternMock({ + getScriptedFields: () => [ + { name: 'ScriptedField', lang: 'painless', script: 'x++' }, + { name: 'JustATest', lang: 'painless', script: 'z++' }, + ], + }); + }); + + test('should render normally', async () => { + const component = shallow( + + ); + + // Allow the componentWillMount code to execute + // https://github.com/airbnb/enzyme/issues/450 + await component.update(); // Fire `componentWillMount()` + await component.update(); // Force update the component post async actions + + expect(component).toMatchSnapshot(); + }); + + test('should filter based on the query bar', async () => { + const component = shallow( + + ); + + // Allow the componentWillMount code to execute + // https://github.com/airbnb/enzyme/issues/450 + await component.update(); // Fire `componentWillMount()` + await component.update(); // Force update the component post async actions + + component.setProps({ fieldFilter: 'Just' }); + component.update(); + + expect(component).toMatchSnapshot(); + }); + + test('should filter based on the lang filter', async () => { + const component = shallow( + [ + { name: 'ScriptedField', lang: 'painless', script: 'x++' }, + { name: 'JustATest', lang: 'painless', script: 'z++' }, + { name: 'Bad', lang: 'somethingElse', script: 'z++' }, + ], + })} + helpers={helpers} + /> + ); + + // Allow the componentWillMount code to execute + // https://github.com/airbnb/enzyme/issues/450 + await component.update(); // Fire `componentWillMount()` + await component.update(); // Force update the component post async actions + + component.setProps({ scriptedFieldLanguageFilter: 'painless' }); + component.update(); + + expect(component).toMatchSnapshot(); + }); + + test('should hide the table if there are no scripted fields', async () => { + const component = shallow( + [], + })} + helpers={helpers} + /> + ); + + // Allow the componentWillMount code to execute + // https://github.com/airbnb/enzyme/issues/450 + await component.update(); // Fire `componentWillMount()` + await component.update(); // Force update the component post async actions + + expect(component).toMatchSnapshot(); + }); + + test('should show a delete modal', async () => { + const component = shallow( + + ); + + await component.update(); // Fire `componentWillMount()` + component.instance().startDeleteField({ name: 'ScriptedField', lang: '', script: '' }); + await component.update(); + + // Ensure the modal is visible + expect(component).toMatchSnapshot(); + }); + + test('should delete a field', async () => { + const removeScriptedField = jest.fn(); + const component = shallow( + + ); + + await component.update(); // Fire `componentWillMount()` + component.instance().startDeleteField({ name: 'ScriptedField', lang: '', script: '' }); + + await component.update(); + await component.instance().deleteField(); + await component.update(); + + expect(removeScriptedField).toBeCalled(); + }); +}); diff --git a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/scripted_fields_table/scripted_fields_table.js b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/scripted_fields_table/scripted_fields_table.js deleted file mode 100644 index 69343a5175a25..0000000000000 --- a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/scripted_fields_table/scripted_fields_table.js +++ /dev/null @@ -1,190 +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 React, { Component } from 'react'; -import PropTypes from 'prop-types'; -import { - getSupportedScriptingLanguages, - getDeprecatedScriptingLanguages, -} from 'ui/scripting_languages'; -import { documentationLinks } from 'ui/documentation_links'; - -import { EuiSpacer, EuiOverlayMask, EuiConfirmModal, EUI_MODAL_CONFIRM_BUTTON } from '@elastic/eui'; -import { i18n } from '@kbn/i18n'; - -import { Table, Header, CallOuts } from './components'; - -export class ScriptedFieldsTable extends Component { - static propTypes = { - indexPattern: PropTypes.object.isRequired, - fieldFilter: PropTypes.string, - scriptedFieldLanguageFilter: PropTypes.string, - helpers: PropTypes.shape({ - redirectToRoute: PropTypes.func.isRequired, - getRouteHref: PropTypes.func.isRequired, - }), - onRemoveField: PropTypes.func, - }; - - constructor(props) { - super(props); - - this.state = { - deprecatedLangsInUse: [], - fieldToDelete: undefined, - isDeleteConfirmationModalVisible: false, - fields: [], - }; - } - - UNSAFE_componentWillMount() { - this.fetchFields(); - } - - fetchFields = async () => { - const fields = await this.props.indexPattern.getScriptedFields(); - - const deprecatedLangsInUse = []; - const deprecatedLangs = getDeprecatedScriptingLanguages(); - const supportedLangs = getSupportedScriptingLanguages(); - - for (const { lang } of fields) { - if (deprecatedLangs.includes(lang) || !supportedLangs.includes(lang)) { - deprecatedLangsInUse.push(lang); - } - } - - this.setState({ - fields, - deprecatedLangsInUse, - }); - }; - - getFilteredItems = () => { - const { fields } = this.state; - const { fieldFilter, scriptedFieldLanguageFilter } = this.props; - - let languageFilteredFields = fields; - - if (scriptedFieldLanguageFilter) { - languageFilteredFields = fields.filter( - field => field.lang === this.props.scriptedFieldLanguageFilter - ); - } - - let filteredFields = languageFilteredFields; - - if (fieldFilter) { - const normalizedFieldFilter = this.props.fieldFilter.toLowerCase(); - filteredFields = languageFilteredFields.filter(field => - field.name.toLowerCase().includes(normalizedFieldFilter) - ); - } - - return filteredFields; - }; - - renderCallOuts() { - const { deprecatedLangsInUse } = this.state; - - return ( - - ); - } - - startDeleteField = field => { - this.setState({ fieldToDelete: field, isDeleteConfirmationModalVisible: true }); - }; - - hideDeleteConfirmationModal = () => { - this.setState({ fieldToDelete: undefined, isDeleteConfirmationModalVisible: false }); - }; - - deleteField = () => { - const { indexPattern, onRemoveField } = this.props; - const { fieldToDelete } = this.state; - - indexPattern.removeScriptedField(fieldToDelete); - onRemoveField && onRemoveField(); - this.fetchFields(); - this.hideDeleteConfirmationModal(); - }; - - renderDeleteConfirmationModal() { - const { fieldToDelete } = this.state; - - if (!fieldToDelete) { - return null; - } - - const title = i18n.translate('kbn.management.editIndexPattern.scripted.deleteFieldLabel', { - defaultMessage: "Delete scripted field '{fieldName}'?", - values: { fieldName: fieldToDelete.name }, - }); - const cancelButtonText = i18n.translate( - 'kbn.management.editIndexPattern.scripted.deleteField.cancelButton', - { defaultMessage: 'Cancel' } - ); - const confirmButtonText = i18n.translate( - 'kbn.management.editIndexPattern.scripted.deleteField.deleteButton', - { defaultMessage: 'Delete' } - ); - - return ( - - - - ); - } - - render() { - const { helpers, indexPattern } = this.props; - - const items = this.getFilteredItems(); - - return ( -
-
- - {this.renderCallOuts()} - - - -
this.props.helpers.redirectToRoute(field, 'edit')} - deleteField={this.startDeleteField} - /> - - {this.renderDeleteConfirmationModal()} - - ); - } -} diff --git a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/scripted_fields_table/scripted_fields_table.tsx b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/scripted_fields_table/scripted_fields_table.tsx new file mode 100644 index 0000000000000..e8dfbd6496057 --- /dev/null +++ b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/scripted_fields_table/scripted_fields_table.tsx @@ -0,0 +1,172 @@ +/* + * 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, { Component } from 'react'; +import { + getSupportedScriptingLanguages, + getDeprecatedScriptingLanguages, +} from 'ui/scripting_languages'; +import { documentationLinks } from 'ui/documentation_links'; + +import { EuiSpacer } from '@elastic/eui'; + +import { Table, Header, CallOuts, DeleteScritpedFieldConfirmationModal } from './components'; +import { ScriptedFieldItem } from './types'; + +import { IIndexPattern } from '../../../../../../../../../plugins/data/public'; + +interface ScriptedFieldsTableProps { + indexPattern: IIndexPattern; + fieldFilter?: string; + scriptedFieldLanguageFilter?: string; + helpers: { + redirectToRoute: Function; + getRouteHref: Function; + }; + onRemoveField?: () => void; +} + +interface ScriptedFieldsTableState { + deprecatedLangsInUse: string[]; + fieldToDelete: ScriptedFieldItem | undefined; + isDeleteConfirmationModalVisible: boolean; + fields: ScriptedFieldItem[]; +} + +export class ScriptedFieldsTable extends Component< + ScriptedFieldsTableProps, + ScriptedFieldsTableState +> { + constructor(props: ScriptedFieldsTableProps) { + super(props); + + this.state = { + deprecatedLangsInUse: [], + fieldToDelete: undefined, + isDeleteConfirmationModalVisible: false, + fields: [], + }; + } + + UNSAFE_componentWillMount() { + this.fetchFields(); + } + + fetchFields = async () => { + const fields = await this.props.indexPattern.getScriptedFields(); + + const deprecatedLangsInUse = []; + const deprecatedLangs = getDeprecatedScriptingLanguages(); + const supportedLangs = getSupportedScriptingLanguages(); + + for (const field of fields) { + const lang: string = field.lang; + if (deprecatedLangs.includes(lang) || !supportedLangs.includes(lang)) { + deprecatedLangsInUse.push(lang); + } + } + + this.setState({ + fields, + deprecatedLangsInUse, + }); + }; + + getFilteredItems = () => { + const { fields } = this.state; + const { fieldFilter, scriptedFieldLanguageFilter } = this.props; + + let languageFilteredFields = fields; + + if (scriptedFieldLanguageFilter) { + languageFilteredFields = fields.filter( + field => field.lang === this.props.scriptedFieldLanguageFilter + ); + } + + let filteredFields = languageFilteredFields; + + if (fieldFilter) { + const normalizedFieldFilter = fieldFilter.toLowerCase(); + + filteredFields = languageFilteredFields.filter(field => + field.name.toLowerCase().includes(normalizedFieldFilter) + ); + } + + return filteredFields; + }; + + startDeleteField = (field: ScriptedFieldItem) => { + this.setState({ fieldToDelete: field, isDeleteConfirmationModalVisible: true }); + }; + + hideDeleteConfirmationModal = () => { + this.setState({ fieldToDelete: undefined, isDeleteConfirmationModalVisible: false }); + }; + + deleteField = () => { + const { indexPattern, onRemoveField } = this.props; + const { fieldToDelete } = this.state; + + indexPattern.removeScriptedField(fieldToDelete); + + if (onRemoveField) { + onRemoveField(); + } + + this.fetchFields(); + this.hideDeleteConfirmationModal(); + }; + + render() { + const { helpers, indexPattern } = this.props; + const { fieldToDelete, deprecatedLangsInUse } = this.state; + + const items = this.getFilteredItems(); + + return ( + <> +
+ + + + + +
this.props.helpers.redirectToRoute(field)} + deleteField={this.startDeleteField} + /> + + {fieldToDelete && ( + + )} + + ); + } +} diff --git a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/scripted_fields_table/types.ts b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/scripted_fields_table/types.ts new file mode 100644 index 0000000000000..c1227393c561f --- /dev/null +++ b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/scripted_fields_table/types.ts @@ -0,0 +1,25 @@ +/* + * 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. + */ + +/** @internal **/ +export interface ScriptedFieldItem { + name: string; + lang: string; + script: string; +} diff --git a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/source_filters_table/__jest__/__snapshots__/source_filters_table.test.js.snap b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/source_filters_table/__jest__/__snapshots__/source_filters_table.test.js.snap deleted file mode 100644 index 52fccb8607a83..0000000000000 --- a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/source_filters_table/__jest__/__snapshots__/source_filters_table.test.js.snap +++ /dev/null @@ -1,348 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`SourceFiltersTable should add a filter 1`] = ` -
-
- - -
- -`; - -exports[`SourceFiltersTable should filter based on the query bar 1`] = ` -
-
- - -
- -`; - -exports[`SourceFiltersTable should remove a filter 1`] = ` -
-
- - -
- -`; - -exports[`SourceFiltersTable should render normally 1`] = ` -
-
- - -
- -`; - -exports[`SourceFiltersTable should should a loading indicator when saving 1`] = ` -
-
- - -
- -`; - -exports[`SourceFiltersTable should show a delete modal 1`] = ` -
-
- - -
- - - } - confirmButtonText={ - - } - onCancel={[Function]} - onConfirm={[Function]} - title={ - - } - /> - - -`; - -exports[`SourceFiltersTable should update a filter 1`] = ` -
-
- - -
- -`; diff --git a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/source_filters_table/__jest__/source_filters_table.test.js b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/source_filters_table/__jest__/source_filters_table.test.js deleted file mode 100644 index a39958a77abbf..0000000000000 --- a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/source_filters_table/__jest__/source_filters_table.test.js +++ /dev/null @@ -1,159 +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 React from 'react'; -import { shallow } from 'enzyme'; - -import { SourceFiltersTable } from '../source_filters_table'; - -jest.mock('@elastic/eui', () => ({ - EuiButton: 'eui-button', - EuiTitle: 'eui-title', - EuiText: 'eui-text', - EuiButton: 'eui-button', - EuiHorizontalRule: 'eui-horizontal-rule', - EuiSpacer: 'eui-spacer', - EuiCallOut: 'eui-call-out', - EuiLink: 'eui-link', - EuiOverlayMask: 'eui-overlay-mask', - EuiConfirmModal: 'eui-confirm-modal', - EuiLoadingSpinner: 'eui-loading-spinner', - Comparators: { - property: () => {}, - default: () => {}, - }, -})); -jest.mock('../components/header', () => ({ Header: 'header' })); -jest.mock('../components/table', () => ({ - // Note: this seems to fix React complaining about non lowercase attributes - Table: () => { - return 'table'; - }, -})); - -const indexPattern = { - sourceFilters: [{ value: 'time*' }, { value: 'nam*' }, { value: 'age*' }], -}; - -describe('SourceFiltersTable', () => { - it('should render normally', async () => { - const component = shallow( - {}} /> - ); - - expect(component).toMatchSnapshot(); - }); - - it('should filter based on the query bar', async () => { - const component = shallow( - {}} /> - ); - - component.setProps({ filterFilter: 'ti' }); - expect(component).toMatchSnapshot(); - }); - - it('should should a loading indicator when saving', async () => { - const component = shallow( - {}} - /> - ); - - component.setState({ isSaving: true }); - expect(component).toMatchSnapshot(); - }); - - it('should show a delete modal', async () => { - const component = shallow( - {}} - /> - ); - - component.instance().startDeleteFilter({ value: 'tim*' }); - component.update(); // We are not calling `.setState` directly so we need to re-render - expect(component).toMatchSnapshot(); - }); - - it('should remove a filter', async () => { - const save = jest.fn(); - const component = shallow( - {}} - /> - ); - - component.instance().startDeleteFilter({ value: 'tim*' }); - component.update(); // We are not calling `.setState` directly so we need to re-render - await component.instance().deleteFilter(); - component.update(); // We are not calling `.setState` directly so we need to re-render - - expect(save).toBeCalled(); - expect(component).toMatchSnapshot(); - }); - - it('should add a filter', async () => { - const save = jest.fn(); - const component = shallow( - {}} - /> - ); - - await component.instance().onAddFilter('na*'); - component.update(); // We are not calling `.setState` directly so we need to re-render - - expect(save).toBeCalled(); - expect(component).toMatchSnapshot(); - }); - - it('should update a filter', async () => { - const save = jest.fn(); - const component = shallow( - {}} - /> - ); - - await component.instance().saveFilter({ oldFilterValue: 'tim*', newFilterValue: 'ti*' }); - component.update(); // We are not calling `.setState` directly so we need to re-render - - expect(save).toBeCalled(); - expect(component).toMatchSnapshot(); - }); -}); diff --git a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/source_filters_table/__snapshots__/source_filters_table.test.tsx.snap b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/source_filters_table/__snapshots__/source_filters_table.test.tsx.snap new file mode 100644 index 0000000000000..a7b73624c4665 --- /dev/null +++ b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/source_filters_table/__snapshots__/source_filters_table.test.tsx.snap @@ -0,0 +1,313 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`SourceFiltersTable should add a filter 1`] = ` + +
+ + +
+ +`; + +exports[`SourceFiltersTable should filter based on the query bar 1`] = ` + +
+ + +
+ +`; + +exports[`SourceFiltersTable should remove a filter 1`] = ` + +
+ + +
+ +`; + +exports[`SourceFiltersTable should render normally 1`] = ` + +
+ + +
+ +`; + +exports[`SourceFiltersTable should should a loading indicator when saving 1`] = ` + +
+ + +
+ +`; + +exports[`SourceFiltersTable should show a delete modal 1`] = ` + +
+ + +
+ + +`; + +exports[`SourceFiltersTable should update a filter 1`] = ` + +
+ + +
+ +`; diff --git a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/source_filters_table/components/add_filter/__jest__/add_filter.test.js b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/source_filters_table/components/add_filter/__jest__/add_filter.test.js deleted file mode 100644 index 915d9490db045..0000000000000 --- a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/source_filters_table/components/add_filter/__jest__/add_filter.test.js +++ /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 React from 'react'; -import { shallowWithI18nProvider } from 'test_utils/enzyme_helpers'; - -import { AddFilter } from '../add_filter'; - -describe('AddFilter', () => { - it('should render normally', async () => { - const component = shallowWithI18nProvider( {}} />); - - expect(component).toMatchSnapshot(); - }); - - it('should allow adding a filter', async () => { - const onAddFilter = jest.fn(); - const component = shallowWithI18nProvider(); - - // Set a value in the input field - component.setState({ filter: 'tim*' }); - - // Click the button - component.find('EuiButton').simulate('click'); - component.update(); - - expect(onAddFilter).toBeCalledWith('tim*'); - }); - - it('should ignore strings with just spaces', async () => { - const component = shallowWithI18nProvider( {}} />); - - // Set a value in the input field - component.find('EuiFieldText').simulate('keypress', ' '); - component.update(); - - expect(component).toMatchSnapshot(); - }); -}); diff --git a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/source_filters_table/components/add_filter/__jest__/__snapshots__/add_filter.test.js.snap b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/source_filters_table/components/add_filter/__snapshots__/add_filter.test.tsx.snap similarity index 100% rename from src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/source_filters_table/components/add_filter/__jest__/__snapshots__/add_filter.test.js.snap rename to src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/source_filters_table/components/add_filter/__snapshots__/add_filter.test.tsx.snap diff --git a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/source_filters_table/components/add_filter/add_filter.js b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/source_filters_table/components/add_filter/add_filter.js deleted file mode 100644 index 2124b76b3a915..0000000000000 --- a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/source_filters_table/components/add_filter/add_filter.js +++ /dev/null @@ -1,73 +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 React, { Component } from 'react'; -import PropTypes from 'prop-types'; - -import { EuiFlexGroup, EuiFlexItem, EuiFieldText, EuiButton } from '@elastic/eui'; - -import { i18n } from '@kbn/i18n'; -import { FormattedMessage } from '@kbn/i18n/react'; - -export class AddFilter extends Component { - static propTypes = { - onAddFilter: PropTypes.func.isRequired, - }; - - constructor(props) { - super(props); - this.state = { - filter: '', - }; - } - - onAddFilter = () => { - this.props.onAddFilter(this.state.filter); - this.setState({ filter: '' }); - }; - - render() { - const { filter } = this.state; - const placeholder = i18n.translate('kbn.management.editIndexPattern.sourcePlaceholder', { - defaultMessage: - "source filter, accepts wildcards (e.g., `user*` to filter fields starting with 'user')", - }); - - return ( - - - this.setState({ filter: e.target.value.trim() })} - placeholder={placeholder} - /> - - - - - - - - ); - } -} diff --git a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/source_filters_table/components/add_filter/add_filter.test.tsx b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/source_filters_table/components/add_filter/add_filter.test.tsx new file mode 100644 index 0000000000000..1ebaa3eaf89f8 --- /dev/null +++ b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/source_filters_table/components/add_filter/add_filter.test.tsx @@ -0,0 +1,52 @@ +/* + * 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 { shallow } from 'enzyme'; + +import { AddFilter } from './add_filter'; + +describe('AddFilter', () => { + test('should render normally', () => { + const component = shallow( {}} />); + + expect(component).toMatchSnapshot(); + }); + + test('should allow adding a filter', async () => { + const onAddFilter = jest.fn(); + const component = shallow(); + + component.find('EuiFieldText').simulate('change', { target: { value: 'tim*' } }); + component.find('EuiButton').simulate('click'); + component.update(); + + expect(onAddFilter).toBeCalledWith('tim*'); + }); + + test('should ignore strings with just spaces', () => { + const component = shallow( {}} />); + + // Set a value in the input field + component.find('EuiFieldText').simulate('keypress', ' '); + component.update(); + + expect(component).toMatchSnapshot(); + }); +}); diff --git a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/source_filters_table/components/add_filter/add_filter.tsx b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/source_filters_table/components/add_filter/add_filter.tsx new file mode 100644 index 0000000000000..d0f397637de33 --- /dev/null +++ b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/source_filters_table/components/add_filter/add_filter.tsx @@ -0,0 +1,63 @@ +/* + * 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, useCallback } from 'react'; + +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { EuiFlexGroup, EuiFlexItem, EuiFieldText, EuiButton } from '@elastic/eui'; + +interface AddFilterProps { + onAddFilter: (filter: string) => void; +} + +const sourcePlaceholder = i18n.translate('kbn.management.editIndexPattern.sourcePlaceholder', { + defaultMessage: + "source filter, accepts wildcards (e.g., `user*` to filter fields starting with 'user')", +}); + +export const AddFilter = ({ onAddFilter }: AddFilterProps) => { + const [filter, setFilter] = useState(''); + + const onAddButtonClick = useCallback(() => { + onAddFilter(filter); + setFilter(''); + }, [filter, onAddFilter]); + + return ( + + + setFilter(e.target.value.trim())} + placeholder={sourcePlaceholder} + /> + + + + + + + + ); +}; diff --git a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/source_filters_table/components/add_filter/index.js b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/source_filters_table/components/add_filter/index.ts similarity index 100% rename from src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/source_filters_table/components/add_filter/index.js rename to src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/source_filters_table/components/add_filter/index.ts diff --git a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/source_filters_table/components/confirmation_modal/__snapshots__/confirmation_modal.test.tsx.snap b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/source_filters_table/components/confirmation_modal/__snapshots__/confirmation_modal.test.tsx.snap new file mode 100644 index 0000000000000..62376b498d887 --- /dev/null +++ b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/source_filters_table/components/confirmation_modal/__snapshots__/confirmation_modal.test.tsx.snap @@ -0,0 +1,37 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Header should render normally 1`] = ` + + + } + confirmButtonText={ + + } + defaultFocusedButton="confirm" + onCancel={[Function]} + onConfirm={[Function]} + title={ + + } + /> + +`; diff --git a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/source_filters_table/components/confirmation_modal/confirmation_modal.test.tsx b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/source_filters_table/components/confirmation_modal/confirmation_modal.test.tsx new file mode 100644 index 0000000000000..ac7237095e4b3 --- /dev/null +++ b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/source_filters_table/components/confirmation_modal/confirmation_modal.test.tsx @@ -0,0 +1,37 @@ +/* + * 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 { shallow } from 'enzyme'; + +import { DeleteFilterConfirmationModal } from './confirmation_modal'; + +describe('Header', () => { + test('should render normally', () => { + const component = shallow( + {}} + onDeleteFilter={() => {}} + /> + ); + + expect(component).toMatchSnapshot(); + }); +}); diff --git a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/source_filters_table/components/confirmation_modal/confirmation_modal.tsx b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/source_filters_table/components/confirmation_modal/confirmation_modal.tsx new file mode 100644 index 0000000000000..dabfb6d8f275a --- /dev/null +++ b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/source_filters_table/components/confirmation_modal/confirmation_modal.tsx @@ -0,0 +1,76 @@ +/* + * 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 PropTypes from 'prop-types'; + +import { FormattedMessage } from '@kbn/i18n/react'; +import { EuiOverlayMask, EuiConfirmModal, EUI_MODAL_CONFIRM_BUTTON } from '@elastic/eui'; + +interface DeleteFilterConfirmationModalProps { + filterToDeleteValue: string; + onCancelConfirmationModal: ( + event?: React.KeyboardEvent | React.MouseEvent + ) => void; + onDeleteFilter: (event: React.MouseEvent) => void; +} + +export const DeleteFilterConfirmationModal = ({ + filterToDeleteValue, + onCancelConfirmationModal, + onDeleteFilter, +}: DeleteFilterConfirmationModalProps) => { + return ( + + + } + onCancel={onCancelConfirmationModal} + onConfirm={onDeleteFilter} + cancelButtonText={ + + } + buttonColor="danger" + confirmButtonText={ + + } + defaultFocusedButton={EUI_MODAL_CONFIRM_BUTTON} + /> + + ); +}; + +DeleteFilterConfirmationModal.propTypes = { + filterToDeleteValue: PropTypes.string.isRequired, + onCancelConfirmationModal: PropTypes.func.isRequired, + onDeleteFilter: PropTypes.func.isRequired, +}; diff --git a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/source_filters_table/components/confirmation_modal/index.ts b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/source_filters_table/components/confirmation_modal/index.ts new file mode 100644 index 0000000000000..e48e38b7c3dcb --- /dev/null +++ b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/source_filters_table/components/confirmation_modal/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 { DeleteFilterConfirmationModal } from './confirmation_modal'; diff --git a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/source_filters_table/components/header/__jest__/__snapshots__/header.test.js.snap b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/source_filters_table/components/header/__jest__/__snapshots__/header.test.js.snap deleted file mode 100644 index a5be141a18c89..0000000000000 --- a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/source_filters_table/components/header/__jest__/__snapshots__/header.test.js.snap +++ /dev/null @@ -1,36 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`Header should render normally 1`] = ` -
- -

- -

-
- -

- -

-

- -

-
- -
-`; diff --git a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/source_filters_table/components/header/__jest__/header.test.js b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/source_filters_table/components/header/__jest__/header.test.js deleted file mode 100644 index 058bf99fe26fa..0000000000000 --- a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/source_filters_table/components/header/__jest__/header.test.js +++ /dev/null @@ -1,31 +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 React from 'react'; -import { shallow } from 'enzyme'; - -import { Header } from '../header'; - -describe('Header', () => { - it('should render normally', async () => { - const component = shallow(
); - - expect(component).toMatchSnapshot(); - }); -}); diff --git a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/source_filters_table/components/header/__snapshots__/header.test.tsx.snap b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/source_filters_table/components/header/__snapshots__/header.test.tsx.snap new file mode 100644 index 0000000000000..cde0de79caacd --- /dev/null +++ b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/source_filters_table/components/header/__snapshots__/header.test.tsx.snap @@ -0,0 +1,36 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Header should render normally 1`] = ` + + +

+ +

+
+ +

+ +

+

+ +

+
+ +
+`; diff --git a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/source_filters_table/components/header/header.js b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/source_filters_table/components/header/header.js deleted file mode 100644 index 8822ca6236250..0000000000000 --- a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/source_filters_table/components/header/header.js +++ /dev/null @@ -1,56 +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 React from 'react'; - -import { EuiTitle, EuiText, EuiSpacer } from '@elastic/eui'; - -import { FormattedMessage } from '@kbn/i18n/react'; - -export const Header = () => ( -
- -

- -

-
- -

- -

-

- -

-
- -
-); diff --git a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/source_filters_table/components/header/header.test.tsx b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/source_filters_table/components/header/header.test.tsx new file mode 100644 index 0000000000000..869bdeb55cf02 --- /dev/null +++ b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/source_filters_table/components/header/header.test.tsx @@ -0,0 +1,31 @@ +/* + * 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 { shallow } from 'enzyme'; + +import { Header } from '../header'; + +describe('Header', () => { + test('should render normally', () => { + const component = shallow(
); + + expect(component).toMatchSnapshot(); + }); +}); diff --git a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/source_filters_table/components/header/header.tsx b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/source_filters_table/components/header/header.tsx new file mode 100644 index 0000000000000..7b37f75043dd5 --- /dev/null +++ b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/source_filters_table/components/header/header.tsx @@ -0,0 +1,55 @@ +/* + * 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 { EuiTitle, EuiText, EuiSpacer } from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n/react'; + +export const Header = () => ( + <> + +

+ +

+
+ +

+ +

+

+ +

+
+ + +); diff --git a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/source_filters_table/components/header/index.js b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/source_filters_table/components/header/index.ts similarity index 100% rename from src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/source_filters_table/components/header/index.js rename to src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/source_filters_table/components/header/index.ts diff --git a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/source_filters_table/components/index.ts b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/source_filters_table/components/index.ts new file mode 100644 index 0000000000000..87ac13ad15f50 --- /dev/null +++ b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/source_filters_table/components/index.ts @@ -0,0 +1,23 @@ +/* + * 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 { AddFilter } from './add_filter'; +export { DeleteFilterConfirmationModal } from './confirmation_modal'; +export { Header } from './header'; +export { Table } from './table'; diff --git a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/source_filters_table/components/table/__jest__/__snapshots__/table.test.js.snap b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/source_filters_table/components/table/__jest__/__snapshots__/table.test.js.snap deleted file mode 100644 index a0853b8628cc9..0000000000000 --- a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/source_filters_table/components/table/__jest__/__snapshots__/table.test.js.snap +++ /dev/null @@ -1,102 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`Table editing should show a save button 1`] = ` -
- - -
-`; - -exports[`Table editing should show an input field 1`] = ` - - - - - -`; - -exports[`Table editing should update the matches dynamically as input value is changed 1`] = ` -
- - time, value - -
-`; - -exports[`Table should render filter matches 1`] = ` - - time - -`; - -exports[`Table should render normally 1`] = ` - -`; diff --git a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/source_filters_table/components/table/__jest__/table.test.js b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/source_filters_table/components/table/__jest__/table.test.js deleted file mode 100644 index 7fba1fcfe4876..0000000000000 --- a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/source_filters_table/components/table/__jest__/table.test.js +++ /dev/null @@ -1,299 +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 React from 'react'; -import { shallow } from 'enzyme'; -import { shallowWithI18nProvider } from 'test_utils/enzyme_helpers'; - -import { Table } from '../table'; -import { keyCodes } from '@elastic/eui'; - -const indexPattern = {}; -const items = [{ value: 'tim*' }]; - -describe('Table', () => { - it('should render normally', async () => { - const component = shallowWithI18nProvider( -
{}} - fieldWildcardMatcher={() => {}} - saveFilter={() => {}} - isSaving={true} - /> - ); - - expect(component).toMatchSnapshot(); - }); - - it('should render filter matches', async () => { - const component = shallowWithI18nProvider( -
[{ name: 'time' }, { name: 'value' }], - }} - items={items} - deleteFilter={() => {}} - fieldWildcardMatcher={filter => field => field.includes(filter[0])} - saveFilter={() => {}} - isSaving={false} - /> - ); - - const matchesTableCell = shallow(component.prop('columns')[1].render('tim', { clientId: 1 })); - expect(matchesTableCell).toMatchSnapshot(); - }); - - describe('editing', () => { - const saveFilter = jest.fn(); - const clientId = 1; - let component; - - beforeEach(() => { - component = shallowWithI18nProvider( -
{}} - fieldWildcardMatcher={() => {}} - saveFilter={saveFilter} - isSaving={false} - /> - ); - }); - - it('should show an input field', () => { - // Start the editing process - const editingComponent = shallow( - // Wrap in a div because: https://github.com/airbnb/enzyme/issues/1213 -
{component.prop('columns')[2].render({ clientId, value: 'tim*' })}
- ); - editingComponent - .find('EuiButtonIcon') - .at(1) - .simulate('click'); - // Ensure the state change propagates - component.update(); - - // Ensure the table cell switches to an input - const filterNameTableCell = shallow( - component.prop('columns')[0].render('tim*', { clientId }) - ); - expect(filterNameTableCell).toMatchSnapshot(); - }); - - it('should show a save button', () => { - // Start the editing process - const editingComponent = shallow( - // Fixes: Invariant Violation: ReactShallowRenderer render(): Shallow rendering works only with custom components, but the provided element type was `symbol`. -
{component.prop('columns')[2].render({ clientId, value: 'tim*' })}
- ); - editingComponent - .find('EuiButtonIcon') - .at(1) - .simulate('click'); - - // Ensure the state change propagates - component.update(); - - // Verify save button - const saveTableCell = shallow( - // Fixes Invariant Violation: ReactShallowRenderer render(): Shallow rendering works only with custom components, but the provided element type was `symbol`. -
{component.prop('columns')[2].render({ clientId, value: 'tim*' })}
- ); - expect(saveTableCell).toMatchSnapshot(); - }); - - it('should update the matches dynamically as input value is changed', () => { - const localComponent = shallowWithI18nProvider( -
[{ name: 'time' }, { name: 'value' }], - }} - items={items} - deleteFilter={() => {}} - fieldWildcardMatcher={query => () => { - return query.includes('time*'); - }} - saveFilter={saveFilter} - isSaving={false} - /> - ); - - // Start the editing process - const editingComponent = shallow( - // Fixes: Invariant Violation: ReactShallowRenderer render(): Shallow rendering works only with custom components, but the provided element type was `symbol`. -
{localComponent.prop('columns')[2].render({ clientId, value: 'tim*' })}
- ); - editingComponent - .find('EuiButtonIcon') - .at(1) - .simulate('click'); - - // Update the value - localComponent.setState({ editingFilterValue: 'time*' }); - - // Ensure the state change propagates - localComponent.update(); - - // Verify updated matches - const matchesTableCell = shallow( - // Fixes Invariant Violation: ReactShallowRenderer render(): Shallow rendering works only with custom components, but the provided element type was `symbol`. -
{localComponent.prop('columns')[1].render('tim*', { clientId })}
- ); - expect(matchesTableCell).toMatchSnapshot(); - }); - - it('should exit on save', () => { - // Change the value to something else - component.setState({ - editingFilterId: clientId, - editingFilterValue: 'ti*', - }); - - // Click the save button - const editingComponent = shallow( - // Fixes Invariant Violation: ReactShallowRenderer render(): Shallow rendering works only with custom components, but the provided element type was `symbol`. -
{component.prop('columns')[2].render({ clientId, value: 'tim*' })}
- ); - editingComponent - .find('EuiButtonIcon') - .at(0) - .simulate('click'); - - // Ensure we call saveFilter properly - expect(saveFilter).toBeCalledWith({ - filterId: clientId, - newFilterValue: 'ti*', - }); - - // Ensure the state is properly reset - expect(component.state('editingFilterId')).toBe(null); - }); - }); - - it('should allow deletes', () => { - const deleteFilter = jest.fn(); - - const component = shallowWithI18nProvider( -
{}} - saveFilter={() => {}} - isSaving={false} - /> - ); - - // Click the delete button - const deleteCellComponent = shallow( - // Fixes Invariant Violation: ReactShallowRenderer render(): Shallow rendering works only with custom components, but the provided element type was `symbol`. -
{component.prop('columns')[2].render({ clientId: 1, value: 'tim*' })}
- ); - deleteCellComponent - .find('EuiButtonIcon') - .at(0) - .simulate('click'); - expect(deleteFilter).toBeCalled(); - }); - - it('should save when in edit mode and the enter key is pressed', () => { - const saveFilter = jest.fn(); - const clientId = 1; - - const component = shallowWithI18nProvider( -
{}} - fieldWildcardMatcher={() => {}} - saveFilter={saveFilter} - isSaving={false} - /> - ); - - // Start the editing process - const editingComponent = shallow( - // Fixes Invariant Violation: ReactShallowRenderer render(): Shallow rendering works only with custom components, but the provided element type was `symbol`. -
{component.prop('columns')[2].render({ clientId, value: 'tim*' })}
- ); - editingComponent - .find('EuiButtonIcon') - .at(1) - .simulate('click'); - // Ensure the state change propagates - component.update(); - - // Get the rendered input cell - const filterNameTableCell = shallow( - // Fixes Invariant Violation: ReactShallowRenderer render(): Shallow rendering works only with custom components, but the provided element type was `symbol`. -
{component.prop('columns')[0].render('tim*', { clientId })}
- ); - - // Press the enter key - filterNameTableCell.find('EuiFieldText').simulate('keydown', { keyCode: keyCodes.ENTER }); - expect(saveFilter).toBeCalled(); - - // It should reset - expect(component.state('editingFilterId')).toBe(null); - }); - - it('should cancel when in edit mode and the esc key is pressed', () => { - const saveFilter = jest.fn(); - const clientId = 1; - - const component = shallowWithI18nProvider( -
{}} - fieldWildcardMatcher={() => {}} - saveFilter={saveFilter} - isSaving={false} - /> - ); - - // Start the editing process - const editingComponent = shallow( - // Fixes Invariant Violation: ReactShallowRenderer render(): Shallow rendering works only with custom components, but the provided element type was `symbol`. -
{component.prop('columns')[2].render({ clientId, value: 'tim*' })}
- ); - editingComponent - .find('EuiButtonIcon') - .at(1) - .simulate('click'); - // Ensure the state change propagates - component.update(); - - // Get the rendered input cell - const filterNameTableCell = shallow( - // Fixes Invariant Violation: ReactShallowRenderer render(): Shallow rendering works only with custom components, but the provided element type was `symbol`. -
{component.prop('columns')[0].render('tim*', { clientId })}
- ); - - // Press the enter key - filterNameTableCell.find('EuiFieldText').simulate('keydown', { keyCode: keyCodes.ESCAPE }); - expect(saveFilter).not.toBeCalled(); - - // It should reset - expect(component.state('editingFilterId')).toBe(null); - }); -}); diff --git a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/source_filters_table/components/table/__snapshots__/table.test.tsx.snap b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/source_filters_table/components/table/__snapshots__/table.test.tsx.snap new file mode 100644 index 0000000000000..c70d0871bb854 --- /dev/null +++ b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/source_filters_table/components/table/__snapshots__/table.test.tsx.snap @@ -0,0 +1,97 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Table editing should show a save button 1`] = ` +
+ + +
+`; + +exports[`Table editing should show an input field 1`] = ` + + tim* + +`; + +exports[`Table editing should update the matches dynamically as input value is changed 1`] = ` +
+ + + +
+`; + +exports[`Table should render filter matches 1`] = ` + + time + +`; + +exports[`Table should render normally 1`] = ` + +`; diff --git a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/source_filters_table/components/table/index.js b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/source_filters_table/components/table/index.ts similarity index 100% rename from src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/source_filters_table/components/table/index.js rename to src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/source_filters_table/components/table/index.ts diff --git a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/source_filters_table/components/table/table.js b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/source_filters_table/components/table/table.js deleted file mode 100644 index f16663e1cd41a..0000000000000 --- a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/source_filters_table/components/table/table.js +++ /dev/null @@ -1,218 +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 React, { Component, Fragment } from 'react'; -import PropTypes from 'prop-types'; - -import { - EuiInMemoryTable, - EuiFieldText, - EuiButtonIcon, - keyCodes, - RIGHT_ALIGNMENT, -} from '@elastic/eui'; - -import { i18n } from '@kbn/i18n'; -import { FormattedMessage } from '@kbn/i18n/react'; - -export class Table extends Component { - static propTypes = { - indexPattern: PropTypes.object.isRequired, - items: PropTypes.array.isRequired, - deleteFilter: PropTypes.func.isRequired, - fieldWildcardMatcher: PropTypes.func.isRequired, - saveFilter: PropTypes.func.isRequired, - isSaving: PropTypes.bool.isRequired, - }; - - constructor(props) { - super(props); - this.state = { - editingFilterId: null, - editingFilterValue: null, - }; - } - - startEditingFilter = (id, value) => - this.setState({ editingFilterId: id, editingFilterValue: value }); - stopEditingFilter = () => this.setState({ editingFilterId: null }); - onEditingFilterChange = e => this.setState({ editingFilterValue: e.target.value }); - - onEditFieldKeyDown = ({ keyCode }) => { - if (keyCodes.ENTER === keyCode) { - this.props.saveFilter({ - filterId: this.state.editingFilterId, - newFilterValue: this.state.editingFilterValue, - }); - this.stopEditingFilter(); - } - if (keyCodes.ESCAPE === keyCode) { - this.stopEditingFilter(); - } - }; - - getColumns() { - const { deleteFilter, fieldWildcardMatcher, indexPattern, saveFilter } = this.props; - - return [ - { - field: 'value', - name: i18n.translate('kbn.management.editIndexPattern.source.table.filterHeader', { - defaultMessage: 'Filter', - }), - description: i18n.translate( - 'kbn.management.editIndexPattern.source.table.filterDescription', - { defaultMessage: 'Filter name' } - ), - dataType: 'string', - sortable: true, - render: (value, filter) => { - if (this.state.editingFilterId === filter.clientId) { - return ( - - ); - } - - return {value}; - }, - }, - { - field: 'value', - name: i18n.translate('kbn.management.editIndexPattern.source.table.matchesHeader', { - defaultMessage: 'Matches', - }), - description: i18n.translate( - 'kbn.management.editIndexPattern.source.table.matchesDescription', - { defaultMessage: 'Language used for the field' } - ), - dataType: 'string', - sortable: true, - render: (value, filter) => { - const realtimeValue = - this.state.editingFilterId === filter.clientId ? this.state.editingFilterValue : value; - const matcher = fieldWildcardMatcher([realtimeValue]); - const matches = indexPattern - .getNonScriptedFields() - .map(f => f.name) - .filter(matcher) - .sort(); - if (matches.length) { - return {matches.join(', ')}; - } - - return ( - - - - ); - }, - }, - { - name: '', - align: RIGHT_ALIGNMENT, - width: '100', - render: filter => { - if (this.state.editingFilterId === filter.clientId) { - return ( - - { - saveFilter({ - filterId: this.state.editingFilterId, - newFilterValue: this.state.editingFilterValue, - }); - this.stopEditingFilter(); - }} - iconType="checkInCircleFilled" - aria-label={i18n.translate( - 'kbn.management.editIndexPattern.source.table.saveAria', - { defaultMessage: 'Save' } - )} - /> - { - this.stopEditingFilter(); - }} - iconType="cross" - aria-label={i18n.translate( - 'kbn.management.editIndexPattern.source.table.cancelAria', - { defaultMessage: 'Cancel' } - )} - /> - - ); - } - - return ( - - deleteFilter(filter)} - iconType="trash" - aria-label={i18n.translate( - 'kbn.management.editIndexPattern.source.table.deleteAria', - { defaultMessage: 'Delete' } - )} - /> - this.startEditingFilter(filter.clientId, filter.value)} - iconType="pencil" - aria-label={i18n.translate( - 'kbn.management.editIndexPattern.source.table.editAria', - { defaultMessage: 'Edit' } - )} - /> - - ); - }, - }, - ]; - } - - render() { - const { items, isSaving } = this.props; - const columns = this.getColumns(); - const pagination = { - initialPageSize: 10, - pageSizeOptions: [5, 10, 25, 50], - }; - - return ( - - ); - } -} diff --git a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/source_filters_table/components/table/table.test.tsx b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/source_filters_table/components/table/table.test.tsx new file mode 100644 index 0000000000000..4705ecd2d1685 --- /dev/null +++ b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/source_filters_table/components/table/table.test.tsx @@ -0,0 +1,319 @@ +/* + * 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, { ReactElement } from 'react'; +import { shallow, ShallowWrapper } from 'enzyme'; + +import { Table, TableProps, TableState } from './table'; +import { EuiTableFieldDataColumnType, keyCodes } from '@elastic/eui'; +import { IIndexPattern } from '../../../../../../../../../../../plugins/data/public'; +import { SourceFiltersTableFilter } from '../../types'; + +const indexPattern = {} as IIndexPattern; +const items: SourceFiltersTableFilter[] = [{ value: 'tim*', clientId: '' }]; + +const getIndexPatternMock = (mockedFields: any = {}) => ({ ...mockedFields } as IIndexPattern); + +const getTableColumnRender = ( + component: ShallowWrapper, + index: number = 0 +) => { + const columns = component.prop>>( + 'columns' + ); + return { + render: columns[index].render as (...args: any) => ReactElement, + }; +}; + +describe('Table', () => { + test('should render normally', () => { + const component = shallow( +
{}} + fieldWildcardMatcher={() => {}} + saveFilter={() => undefined} + isSaving={true} + /> + ); + + expect(component).toMatchSnapshot(); + }); + + test('should render filter matches', () => { + const component = shallow
( +
[{ name: 'time' }, { name: 'value' }], + })} + items={items} + deleteFilter={() => {}} + fieldWildcardMatcher={(filter: string) => (field: string) => field.includes(filter[0])} + saveFilter={() => undefined} + isSaving={false} + /> + ); + + const matchesTableCell = shallow( + getTableColumnRender(component, 1).render('tim', { clientId: 1 }) + ); + expect(matchesTableCell).toMatchSnapshot(); + }); + + describe('editing', () => { + const saveFilter = jest.fn(); + const clientId = '1'; + let component: ShallowWrapper; + + beforeEach(() => { + component = shallow
( +
{}} + fieldWildcardMatcher={() => {}} + saveFilter={saveFilter} + isSaving={false} + /> + ); + }); + + test('should show an input field', () => { + // Start the editing process + + const editingComponent = shallow( + // Wrap in a div because: https://github.com/airbnb/enzyme/issues/1213 +
{getTableColumnRender(component, 2).render({ clientId, value: 'tim*' })}
+ ); + editingComponent + .find('EuiButtonIcon') + .at(1) + .simulate('click'); + // Ensure the state change propagates + component.update(); + + const cell = getTableColumnRender(component).render('tim*', { clientId }); + const filterNameTableCell = shallow(cell); + + expect(filterNameTableCell).toMatchSnapshot(); + }); + + test('should show a save button', () => { + // Start the editing process + const editingComponent = shallow( + // Fixes: Invariant Violation: ReactShallowRenderer render(): Shallow rendering works only with custom components, but the provided element type was `symbol`. +
{getTableColumnRender(component, 2).render({ clientId, value: 'tim*' })}
+ ); + + editingComponent + .find('EuiButtonIcon') + .at(1) + .simulate('click'); + + // Ensure the state change propagates + component.update(); + + // Verify save button + const saveTableCell = shallow( + // Fixes Invariant Violation: ReactShallowRenderer render(): Shallow rendering works only with custom components, but the provided element type was `symbol`. +
{getTableColumnRender(component, 2).render({ clientId, value: 'tim*' })}
+ ); + expect(saveTableCell).toMatchSnapshot(); + }); + + test('should update the matches dynamically as input value is changed', () => { + const localComponent = shallow( +
[{ name: 'time' }, { name: 'value' }], + })} + items={items} + deleteFilter={() => {}} + fieldWildcardMatcher={(query: string) => () => query.includes('time*')} + saveFilter={saveFilter} + isSaving={false} + /> + ); + + // Start the editing process + const editingComponent = shallow( + // Fixes: Invariant Violation: ReactShallowRenderer render(): Shallow rendering works only with custom components, but the provided element type was `symbol`. +
{localComponent.prop('columns')[2].render({ clientId, value: 'tim*' })}
+ ); + + editingComponent + .find('EuiButtonIcon') + .at(1) + .simulate('click'); + + // Update the value + localComponent.setState({ editingFilterValue: 'time*' }); + + // Ensure the state change propagates + localComponent.update(); + + // Verify updated matches + const matchesTableCell = shallow( + // Fixes Invariant Violation: ReactShallowRenderer render(): Shallow rendering works only with custom components, but the provided element type was `symbol`. +
{localComponent.prop('columns')[1].render('tim*', { clientId })}
+ ); + expect(matchesTableCell).toMatchSnapshot(); + }); + + test('should exit on save', () => { + // Change the value to something else + component.setState({ + editingFilterId: clientId, + editingFilterValue: 'ti*', + }); + + // Click the save button + const editingComponent = shallow( + // Fixes Invariant Violation: ReactShallowRenderer render(): Shallow rendering works only with custom components, but the provided element type was `symbol`. +
{getTableColumnRender(component, 2).render({ clientId, value: 'tim*' })}
+ ); + + editingComponent + .find('EuiButtonIcon') + .at(0) + .simulate('click'); + + editingComponent.update(); + + // Ensure we call saveFilter properly + expect(saveFilter).toBeCalledWith({ + clientId, + value: 'ti*', + }); + + // Ensure the state is properly reset + expect(component.state('editingFilterId')).toBe(''); + }); + }); + + test('should allow deletes', () => { + const deleteFilter = jest.fn(); + + const component = shallow( +
{}} + saveFilter={() => undefined} + isSaving={false} + /> + ); + + // Click the delete button + const deleteCellComponent = shallow( + // Fixes Invariant Violation: ReactShallowRenderer render(): Shallow rendering works only with custom components, but the provided element type was `symbol`. +
{component.prop('columns')[2].render({ clientId: 1, value: 'tim*' })}
+ ); + deleteCellComponent + .find('EuiButtonIcon') + .at(1) + .simulate('click'); + expect(deleteFilter).toBeCalled(); + }); + + test('should save when in edit mode and the enter key is pressed', () => { + const saveFilter = jest.fn(); + + const component = shallow( +
{}} + fieldWildcardMatcher={() => {}} + saveFilter={saveFilter} + isSaving={false} + /> + ); + + // Start the editing process + const editingComponent = shallow( + // Fixes Invariant Violation: ReactShallowRenderer render(): Shallow rendering works only with custom components, but the provided element type was `symbol`. +
{component.prop('columns')[2].render({ clientId: 1, value: 'tim*' })}
+ ); + editingComponent + .find('EuiButtonIcon') + .at(0) + .simulate('click'); + + component.update(); + + // Get the rendered input cell + const filterNameTableCell = shallow( + // Fixes Invariant Violation: ReactShallowRenderer render(): Shallow rendering works only with custom components, but the provided element type was `symbol`. +
{component.prop('columns')[0].render('tim*', { clientId: 1 })}
+ ); + + // Press the enter key + filterNameTableCell.find('EuiFieldText').simulate('keydown', { keyCode: keyCodes.ENTER }); + expect(saveFilter).toBeCalled(); + + // It should reset + expect(component.state('editingFilterId')).toBe(''); + }); + + test('should cancel when in edit mode and the esc key is pressed', () => { + const saveFilter = jest.fn(); + + const component = shallow( +
{}} + fieldWildcardMatcher={() => {}} + saveFilter={saveFilter} + isSaving={false} + /> + ); + + // Start the editing process + const editingComponent = shallow( + // Fixes Invariant Violation: ReactShallowRenderer render(): Shallow rendering works only with custom components, but the provided element type was `symbol`. +
{component.prop('columns')[2].render({ clientId: 1, value: 'tim*' })}
+ ); + + editingComponent + .find('EuiButtonIcon') + .at(0) + .simulate('click'); + + // Ensure the state change propagates + component.update(); + + // Get the rendered input cell + const filterNameTableCell = shallow( + // Fixes Invariant Violation: ReactShallowRenderer render(): Shallow rendering works only with custom components, but the provided element type was `symbol`. +
{component.prop('columns')[0].render('tim*', { clientId: 1 })}
+ ); + + // Press the ESCAPE key + filterNameTableCell.find('EuiFieldText').simulate('keydown', { keyCode: keyCodes.ESCAPE }); + expect(saveFilter).not.toBeCalled(); + + // It should reset + expect(component.state('editingFilterId')).toBe(''); + }); +}); diff --git a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/source_filters_table/components/table/table.tsx b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/source_filters_table/components/table/table.tsx new file mode 100644 index 0000000000000..db2b74bbc9824 --- /dev/null +++ b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/source_filters_table/components/table/table.tsx @@ -0,0 +1,243 @@ +/* + * 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, { Component } from 'react'; + +import { + keyCodes, + EuiBasicTableColumn, + EuiInMemoryTable, + EuiFieldText, + EuiButtonIcon, + RIGHT_ALIGNMENT, +} from '@elastic/eui'; + +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { SourceFiltersTableFilter } from '../../types'; + +import { IIndexPattern } from '../../../../../../../../../../../plugins/data/public'; + +const filterHeader = i18n.translate('kbn.management.editIndexPattern.source.table.filterHeader', { + defaultMessage: 'Filter', +}); + +const filterDescription = i18n.translate( + 'kbn.management.editIndexPattern.source.table.filterDescription', + { defaultMessage: 'Filter name' } +); + +const matchesHeader = i18n.translate('kbn.management.editIndexPattern.source.table.matchesHeader', { + defaultMessage: 'Matches', +}); + +const matchesDescription = i18n.translate( + 'kbn.management.editIndexPattern.source.table.matchesDescription', + { defaultMessage: 'Language used for the field' } +); + +const editAria = i18n.translate('kbn.management.editIndexPattern.source.table.editAria', { + defaultMessage: 'Edit', +}); + +const saveAria = i18n.translate('kbn.management.editIndexPattern.source.table.saveAria', { + defaultMessage: 'Save', +}); + +const deleteAria = i18n.translate('kbn.management.editIndexPattern.source.table.deleteAria', { + defaultMessage: 'Delete', +}); + +const cancelAria = i18n.translate('kbn.management.editIndexPattern.source.table.cancelAria', { + defaultMessage: 'Cancel', +}); + +export interface TableProps { + indexPattern: IIndexPattern; + items: SourceFiltersTableFilter[]; + deleteFilter: Function; + fieldWildcardMatcher: Function; + saveFilter: (filter: SourceFiltersTableFilter) => any; + isSaving: boolean; +} + +export interface TableState { + editingFilterId: string | number; + editingFilterValue: string; +} + +export class Table extends Component { + constructor(props: TableProps) { + super(props); + this.state = { + editingFilterId: '', + editingFilterValue: '', + }; + } + + startEditingFilter = ( + editingFilterId: TableState['editingFilterId'], + editingFilterValue: TableState['editingFilterValue'] + ) => this.setState({ editingFilterId, editingFilterValue }); + + stopEditingFilter = () => this.setState({ editingFilterId: '' }); + onEditingFilterChange = (e: React.ChangeEvent) => + this.setState({ editingFilterValue: e.target.value }); + + onEditFieldKeyDown = ({ keyCode }: React.KeyboardEvent) => { + if (keyCodes.ENTER === keyCode && this.state.editingFilterId && this.state.editingFilterValue) { + this.props.saveFilter({ + clientId: this.state.editingFilterId, + value: this.state.editingFilterValue, + }); + this.stopEditingFilter(); + } + if (keyCodes.ESCAPE === keyCode) { + this.stopEditingFilter(); + } + }; + + getColumns(): Array> { + const { deleteFilter, fieldWildcardMatcher, indexPattern, saveFilter } = this.props; + + return [ + { + field: 'value', + name: filterHeader, + description: filterDescription, + dataType: 'string', + sortable: true, + render: (value, filter) => { + if (this.state.editingFilterId && this.state.editingFilterId === filter.clientId) { + return ( + + ); + } + + return {value}; + }, + }, + { + field: 'value', + name: matchesHeader, + description: matchesDescription, + dataType: 'string', + sortable: true, + render: (value, filter) => { + const wildcardMatcher = fieldWildcardMatcher([ + this.state.editingFilterId === filter.clientId ? this.state.editingFilterValue : value, + ]); + const matches = indexPattern + .getNonScriptedFields() + .map((currentFilter: any) => currentFilter.name) + .filter(wildcardMatcher) + .sort(); + + if (matches.length) { + return {matches.join(', ')}; + } + + return ( + + + + ); + }, + }, + { + name: '', + align: RIGHT_ALIGNMENT, + width: '100', + render: (filter: SourceFiltersTableFilter) => { + if (this.state.editingFilterId === filter.clientId) { + return ( + <> + { + saveFilter({ + clientId: this.state.editingFilterId, + value: this.state.editingFilterValue, + }); + this.stopEditingFilter(); + }} + iconType="checkInCircleFilled" + aria-label={saveAria} + /> + { + this.stopEditingFilter(); + }} + iconType="cross" + aria-label={cancelAria} + /> + + ); + } + + return ( + <> + this.startEditingFilter(filter.clientId, filter.value)} + iconType="pencil" + aria-label={editAria} + /> + deleteFilter(filter)} + iconType="trash" + aria-label={deleteAria} + /> + + ); + }, + }, + ]; + } + + render() { + const { items, isSaving } = this.props; + const columns = this.getColumns(); + const pagination = { + initialPageSize: 10, + pageSizeOptions: [5, 10, 25, 50], + }; + + return ( + + ); + } +} diff --git a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/source_filters_table/index.js b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/source_filters_table/index.ts similarity index 100% rename from src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/source_filters_table/index.js rename to src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/source_filters_table/index.ts diff --git a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/source_filters_table/source_filters_table.js b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/source_filters_table/source_filters_table.js deleted file mode 100644 index 3b485573f3821..0000000000000 --- a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/source_filters_table/source_filters_table.js +++ /dev/null @@ -1,212 +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 React, { Component } from 'react'; -import PropTypes from 'prop-types'; -import { createSelector } from 'reselect'; - -import { EuiSpacer, EuiOverlayMask, EuiConfirmModal, EUI_MODAL_CONFIRM_BUTTON } from '@elastic/eui'; - -import { Table } from './components/table'; -import { Header } from './components/header'; -import { AddFilter } from './components/add_filter'; -import { FormattedMessage } from '@kbn/i18n/react'; - -export class SourceFiltersTable extends Component { - static propTypes = { - indexPattern: PropTypes.object.isRequired, - filterFilter: PropTypes.string, - fieldWildcardMatcher: PropTypes.func.isRequired, - onAddOrRemoveFilter: PropTypes.func, - }; - - constructor(props) { - super(props); - - // Source filters do not have any unique ids, only the value is stored. - // To ensure we can create a consistent and expected UX when managing - // source filters, we are assigning a unique id to each filter on the - // client side only - this.clientSideId = 0; - - this.state = { - filterToDelete: undefined, - isDeleteConfirmationModalVisible: false, - isSaving: false, - filters: [], - }; - } - - UNSAFE_componentWillMount() { - this.updateFilters(); - } - - updateFilters = () => { - const sourceFilters = this.props.indexPattern.sourceFilters || []; - const filters = sourceFilters.map(filter => ({ - ...filter, - clientId: ++this.clientSideId, - })); - - this.setState({ filters }); - }; - - getFilteredFilters = createSelector( - state => state.filters, - (state, props) => props.filterFilter, - (filters, filterFilter) => { - if (filterFilter) { - const filterFilterToLowercase = filterFilter.toLowerCase(); - return filters.filter(filter => - filter.value.toLowerCase().includes(filterFilterToLowercase) - ); - } - - return filters; - } - ); - - startDeleteFilter = filter => { - this.setState({ - filterToDelete: filter, - isDeleteConfirmationModalVisible: true, - }); - }; - - hideDeleteConfirmationModal = () => { - this.setState({ - filterToDelete: undefined, - isDeleteConfirmationModalVisible: false, - }); - }; - - deleteFilter = async () => { - const { indexPattern, onAddOrRemoveFilter } = this.props; - const { filterToDelete, filters } = this.state; - - indexPattern.sourceFilters = filters.filter(filter => { - return filter.clientId !== filterToDelete.clientId; - }); - - this.setState({ isSaving: true }); - await indexPattern.save(); - onAddOrRemoveFilter && onAddOrRemoveFilter(); - this.updateFilters(); - this.setState({ isSaving: false }); - this.hideDeleteConfirmationModal(); - }; - - onAddFilter = async value => { - const { indexPattern, onAddOrRemoveFilter } = this.props; - - indexPattern.sourceFilters = [...(indexPattern.sourceFilters || []), { value }]; - - this.setState({ isSaving: true }); - await indexPattern.save(); - onAddOrRemoveFilter && onAddOrRemoveFilter(); - this.updateFilters(); - this.setState({ isSaving: false }); - }; - - saveFilter = async ({ filterId, newFilterValue }) => { - const { indexPattern } = this.props; - const { filters } = this.state; - - indexPattern.sourceFilters = filters.map(filter => { - if (filter.clientId === filterId) { - return { - value: newFilterValue, - clientId: filter.clientId, - }; - } - return filter; - }); - - this.setState({ isSaving: true }); - await indexPattern.save(); - this.updateFilters(); - this.setState({ isSaving: false }); - }; - - renderDeleteConfirmationModal() { - const { filterToDelete } = this.state; - - if (!filterToDelete) { - return null; - } - - return ( - - - } - onCancel={this.hideDeleteConfirmationModal} - onConfirm={this.deleteFilter} - cancelButtonText={ - - } - buttonColor="danger" - confirmButtonText={ - - } - defaultFocusedButton={EUI_MODAL_CONFIRM_BUTTON} - /> - - ); - } - - render() { - const { indexPattern, fieldWildcardMatcher } = this.props; - - const { isSaving } = this.state; - - const filteredFilters = this.getFilteredFilters(this.state, this.props); - - return ( -
-
- - -
- - {this.renderDeleteConfirmationModal()} - - ); - } -} diff --git a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/source_filters_table/source_filters_table.test.tsx b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/source_filters_table/source_filters_table.test.tsx new file mode 100644 index 0000000000000..1b68dd13566d3 --- /dev/null +++ b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/source_filters_table/source_filters_table.test.tsx @@ -0,0 +1,175 @@ +/* + * 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 { shallow } from 'enzyme'; + +import { SourceFiltersTable } from './source_filters_table'; +import { IIndexPattern } from '../../../../../../../../../plugins/data/public'; + +jest.mock('@elastic/eui', () => ({ + EuiButton: 'eui-button', + EuiTitle: 'eui-title', + EuiText: 'eui-text', + EuiHorizontalRule: 'eui-horizontal-rule', + EuiSpacer: 'eui-spacer', + EuiCallOut: 'eui-call-out', + EuiLink: 'eui-link', + EuiOverlayMask: 'eui-overlay-mask', + EuiConfirmModal: 'eui-confirm-modal', + EuiLoadingSpinner: 'eui-loading-spinner', + Comparators: { + property: () => {}, + default: () => {}, + }, +})); + +jest.mock('./components/header', () => ({ Header: 'header' })); +jest.mock('./components/table', () => ({ + // Note: this seems to fix React complaining about non lowercase attributes + Table: () => { + return 'table'; + }, +})); + +const getIndexPatternMock = (mockedFields: any = {}) => + ({ + sourceFilters: [{ value: 'time*' }, { value: 'nam*' }, { value: 'age*' }], + ...mockedFields, + } as IIndexPattern); + +describe('SourceFiltersTable', () => { + test('should render normally', () => { + const component = shallow( + {}} + filterFilter={''} + /> + ); + + expect(component).toMatchSnapshot(); + }); + + test('should filter based on the query bar', () => { + const component = shallow( + {}} + filterFilter={''} + /> + ); + + component.setProps({ filterFilter: 'ti' }); + expect(component).toMatchSnapshot(); + }); + + test('should should a loading indicator when saving', () => { + const component = shallow( + {}} + /> + ); + + component.setState({ isSaving: true }); + expect(component).toMatchSnapshot(); + }); + + test('should show a delete modal', () => { + const component = shallow( + {}} + /> + ); + + component.instance().startDeleteFilter({ value: 'tim*', clientId: 1 }); + component.update(); // We are not calling `.setState` directly so we need to re-render + expect(component).toMatchSnapshot(); + }); + + test('should remove a filter', async () => { + const save = jest.fn(); + const component = shallow( + {}} + /> + ); + + component.instance().startDeleteFilter({ value: 'tim*', clientId: 1 }); + component.update(); // We are not calling `.setState` directly so we need to re-render + await component.instance().deleteFilter(); + component.update(); // We are not calling `.setState` directly so we need to re-render + + expect(save).toBeCalled(); + expect(component).toMatchSnapshot(); + }); + + test('should add a filter', async () => { + const save = jest.fn(); + const component = shallow( + {}} + /> + ); + + await component.instance().onAddFilter('na*'); + component.update(); // We are not calling `.setState` directly so we need to re-render + + expect(save).toBeCalled(); + expect(component).toMatchSnapshot(); + }); + + test('should update a filter', async () => { + const save = jest.fn(); + const component = shallow( + {}} + /> + ); + + await component.instance().saveFilter({ clientId: 'tim*', value: 'ti*' }); + component.update(); // We are not calling `.setState` directly so we need to re-render + + expect(save).toBeCalled(); + expect(component).toMatchSnapshot(); + }); +}); diff --git a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/source_filters_table/source_filters_table.tsx b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/source_filters_table/source_filters_table.tsx new file mode 100644 index 0000000000000..dcf8ae9e1323f --- /dev/null +++ b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/source_filters_table/source_filters_table.tsx @@ -0,0 +1,192 @@ +/* + * 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, { Component } from 'react'; +import { createSelector } from 'reselect'; + +import { EuiSpacer } from '@elastic/eui'; +import { AddFilter, Table, Header, DeleteFilterConfirmationModal } from './components'; +import { IIndexPattern } from '../../../../../../../../../plugins/data/public'; +import { SourceFiltersTableFilter } from './types'; + +export interface SourceFiltersTableProps { + indexPattern: IIndexPattern; + filterFilter: string; + fieldWildcardMatcher: Function; + onAddOrRemoveFilter?: Function; +} + +export interface SourceFiltersTableState { + filterToDelete: any; + isDeleteConfirmationModalVisible: boolean; + isSaving: boolean; + filters: SourceFiltersTableFilter[]; +} + +export class SourceFiltersTable extends Component< + SourceFiltersTableProps, + SourceFiltersTableState +> { + // Source filters do not have any unique ids, only the value is stored. + // To ensure we can create a consistent and expected UX when managing + // source filters, we are assigning a unique id to each filter on the + // client side only + private clientSideId: number = 0; + + constructor(props: SourceFiltersTableProps) { + super(props); + + this.state = { + filterToDelete: undefined, + isDeleteConfirmationModalVisible: false, + isSaving: false, + filters: [], + }; + } + + UNSAFE_componentWillMount() { + this.updateFilters(); + } + + updateFilters = () => { + const sourceFilters = this.props.indexPattern.sourceFilters; + const filters = (sourceFilters || []).map((sourceFilter: any) => ({ + ...sourceFilter, + clientId: ++this.clientSideId, + })); + + this.setState({ filters }); + }; + + getFilteredFilters = createSelector( + (state: SourceFiltersTableState) => state.filters, + (state: SourceFiltersTableState, props: SourceFiltersTableProps) => props.filterFilter, + (filters, filterFilter) => { + if (filterFilter) { + const filterFilterToLowercase = filterFilter.toLowerCase(); + return filters.filter(filter => + filter.value.toLowerCase().includes(filterFilterToLowercase) + ); + } + + return filters; + } + ); + + startDeleteFilter = (filter: SourceFiltersTableFilter) => { + this.setState({ + filterToDelete: filter, + isDeleteConfirmationModalVisible: true, + }); + }; + + hideDeleteConfirmationModal = () => { + this.setState({ + filterToDelete: undefined, + isDeleteConfirmationModalVisible: false, + }); + }; + + deleteFilter = async () => { + const { indexPattern, onAddOrRemoveFilter } = this.props; + const { filterToDelete, filters } = this.state; + + indexPattern.sourceFilters = filters.filter(filter => { + return filter.clientId !== filterToDelete.clientId; + }); + + this.setState({ isSaving: true }); + await indexPattern.save(); + + if (onAddOrRemoveFilter) { + onAddOrRemoveFilter(); + } + + this.updateFilters(); + this.setState({ isSaving: false }); + this.hideDeleteConfirmationModal(); + }; + + onAddFilter = async (value: string) => { + const { indexPattern, onAddOrRemoveFilter } = this.props; + + indexPattern.sourceFilters = [...(indexPattern.sourceFilters || []), { value }]; + + this.setState({ isSaving: true }); + await indexPattern.save(); + + if (onAddOrRemoveFilter) { + onAddOrRemoveFilter(); + } + + this.updateFilters(); + this.setState({ isSaving: false }); + }; + + saveFilter = async ({ clientId, value }: SourceFiltersTableFilter) => { + const { indexPattern } = this.props; + const { filters } = this.state; + + indexPattern.sourceFilters = filters.map(filter => { + if (filter.clientId === clientId) { + return { + value, + clientId, + }; + } + + return filter; + }); + + this.setState({ isSaving: true }); + await indexPattern.save(); + this.updateFilters(); + this.setState({ isSaving: false }); + }; + + render() { + const { indexPattern, fieldWildcardMatcher } = this.props; + const { isSaving, filterToDelete } = this.state; + const filteredFilters = this.getFilteredFilters(this.state, this.props); + + return ( + <> +
+ + +
+ + {filterToDelete && ( + + )} + + ); + } +} diff --git a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/source_filters_table/types.ts b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/source_filters_table/types.ts new file mode 100644 index 0000000000000..ee3689f017471 --- /dev/null +++ b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/source_filters_table/types.ts @@ -0,0 +1,24 @@ +/* + * 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. + */ + +/** @internal **/ +export interface SourceFiltersTableFilter { + value: string; + clientId: string | number; +} diff --git a/src/legacy/core_plugins/kibana/public/management/sections/objects/_objects.html b/src/legacy/core_plugins/kibana/public/management/sections/objects/_objects.html deleted file mode 100644 index 090fb7b636685..0000000000000 --- a/src/legacy/core_plugins/kibana/public/management/sections/objects/_objects.html +++ /dev/null @@ -1,5 +0,0 @@ - - -
-
-
diff --git a/src/legacy/core_plugins/kibana/public/management/sections/objects/_objects.js b/src/legacy/core_plugins/kibana/public/management/sections/objects/_objects.js deleted file mode 100644 index c5901ca6ee6bf..0000000000000 --- a/src/legacy/core_plugins/kibana/public/management/sections/objects/_objects.js +++ /dev/null @@ -1,104 +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 { savedObjectManagementRegistry } from '../../saved_object_registry'; -import objectIndexHTML from './_objects.html'; -import uiRoutes from 'ui/routes'; -import chrome from 'ui/chrome'; -import { uiModules } from 'ui/modules'; -import React from 'react'; -import { render, unmountComponentAtNode } from 'react-dom'; -import { ObjectsTable } from './components/objects_table'; -import { I18nContext } from 'ui/i18n'; -import { get } from 'lodash'; -import { npStart } from 'ui/new_platform'; -import { getIndexBreadcrumbs } from './breadcrumbs'; - -const REACT_OBJECTS_TABLE_DOM_ELEMENT_ID = 'reactSavedObjectsTable'; - -function updateObjectsTable($scope, $injector) { - const indexPatterns = npStart.plugins.data.indexPatterns; - const $http = $injector.get('$http'); - const kbnUrl = $injector.get('kbnUrl'); - const config = $injector.get('config'); - - const savedObjectsClient = npStart.core.savedObjects.client; - const services = savedObjectManagementRegistry.all().map(obj => obj.service); - const uiCapabilites = npStart.core.application.capabilities; - - $scope.$$postDigest(() => { - const node = document.getElementById(REACT_OBJECTS_TABLE_DOM_ELEMENT_ID); - if (!node) { - return; - } - - render( - - { - if (object.meta.editUrl) { - kbnUrl.change(object.meta.editUrl); - $scope.$apply(); - } - }} - canGoInApp={object => { - const { inAppUrl } = object.meta; - return inAppUrl && get(uiCapabilites, inAppUrl.uiCapabilitiesPath); - }} - /> - , - node - ); - }); -} - -function destroyObjectsTable() { - const node = document.getElementById(REACT_OBJECTS_TABLE_DOM_ELEMENT_ID); - node && unmountComponentAtNode(node); -} - -uiRoutes - .when('/management/kibana/objects', { - template: objectIndexHTML, - k7Breadcrumbs: getIndexBreadcrumbs, - requireUICapability: 'management.kibana.objects', - }) - .when('/management/kibana/objects/:service', { - redirectTo: '/management/kibana/objects', - }); - -uiModules.get('apps/management').directive('kbnManagementObjects', function() { - return { - restrict: 'E', - controllerAs: 'managementObjectsController', - controller: function($scope, $injector) { - updateObjectsTable($scope, $injector); - $scope.$on('$destroy', destroyObjectsTable); - }, - }; -}); diff --git a/src/legacy/core_plugins/kibana/public/management/sections/objects/_view.html b/src/legacy/core_plugins/kibana/public/management/sections/objects/_view.html deleted file mode 100644 index 8bce0aabcd64a..0000000000000 --- a/src/legacy/core_plugins/kibana/public/management/sections/objects/_view.html +++ /dev/null @@ -1,5 +0,0 @@ - - -
-
-
diff --git a/src/legacy/core_plugins/kibana/public/management/sections/objects/_view.js b/src/legacy/core_plugins/kibana/public/management/sections/objects/_view.js deleted file mode 100644 index a847055b40015..0000000000000 --- a/src/legacy/core_plugins/kibana/public/management/sections/objects/_view.js +++ /dev/null @@ -1,85 +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 React from 'react'; -import { render, unmountComponentAtNode } from 'react-dom'; -import 'angular'; -import 'angular-elastic/elastic'; -import uiRoutes from 'ui/routes'; -import { uiModules } from 'ui/modules'; -import { I18nContext } from 'ui/i18n'; -import { npStart } from 'ui/new_platform'; -import objectViewHTML from './_view.html'; -import { getViewBreadcrumbs } from './breadcrumbs'; -import { savedObjectManagementRegistry } from '../../saved_object_registry'; -import { SavedObjectEdition } from './saved_object_view'; - -const REACT_OBJECTS_VIEW_DOM_ELEMENT_ID = 'reactSavedObjectsView'; - -uiRoutes.when('/management/kibana/objects/:service/:id', { - template: objectViewHTML, - k7Breadcrumbs: getViewBreadcrumbs, - requireUICapability: 'management.kibana.objects', -}); - -function createReactView($scope, $routeParams) { - const { service: serviceName, id: objectId, notFound } = $routeParams; - - const { savedObjects, overlays, notifications, application } = npStart.core; - - $scope.$$postDigest(() => { - const node = document.getElementById(REACT_OBJECTS_VIEW_DOM_ELEMENT_ID); - if (!node) { - return; - } - - render( - - - , - node - ); - }); -} - -function destroyReactView() { - const node = document.getElementById(REACT_OBJECTS_VIEW_DOM_ELEMENT_ID); - node && unmountComponentAtNode(node); -} - -uiModules - .get('apps/management', ['monospaced.elastic']) - .directive('kbnManagementObjectsView', function() { - return { - restrict: 'E', - controller: function($scope, $routeParams) { - createReactView($scope, $routeParams); - $scope.$on('$destroy', destroyReactView); - }, - }; - }); diff --git a/src/legacy/core_plugins/kibana/public/management/sections/objects/breadcrumbs.js b/src/legacy/core_plugins/kibana/public/management/sections/objects/breadcrumbs.js deleted file mode 100644 index e9082bfeb680d..0000000000000 --- a/src/legacy/core_plugins/kibana/public/management/sections/objects/breadcrumbs.js +++ /dev/null @@ -1,50 +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 { MANAGEMENT_BREADCRUMB } from 'ui/management'; -import { i18n } from '@kbn/i18n'; - -import { savedObjectManagementRegistry } from '../../saved_object_registry'; - -export function getIndexBreadcrumbs() { - return [ - MANAGEMENT_BREADCRUMB, - { - text: i18n.translate('kbn.management.savedObjects.indexBreadcrumb', { - defaultMessage: 'Saved objects', - }), - href: '#/management/kibana/objects', - }, - ]; -} - -export function getViewBreadcrumbs($routeParams) { - const serviceObj = savedObjectManagementRegistry.get($routeParams.service); - const { service } = serviceObj; - - return [ - ...getIndexBreadcrumbs(), - { - text: i18n.translate('kbn.management.savedObjects.editBreadcrumb', { - defaultMessage: 'Edit {savedObjectType}', - values: { savedObjectType: service.type }, - }), - }, - ]; -} diff --git a/src/legacy/core_plugins/kibana/public/management/sections/objects/components/object_view/__snapshots__/header.test.tsx.snap b/src/legacy/core_plugins/kibana/public/management/sections/objects/components/object_view/__snapshots__/header.test.tsx.snap deleted file mode 100644 index 7e1f7ea12b014..0000000000000 --- a/src/legacy/core_plugins/kibana/public/management/sections/objects/components/object_view/__snapshots__/header.test.tsx.snap +++ /dev/null @@ -1,165 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`Intro component renders correctly 1`] = ` -
- -
- -
- -

- - Edit search - -

-
-
-
- -
- -
- -
- - - - - - -
- - - -
-
-
- -
- -
- -
-`; diff --git a/src/legacy/core_plugins/kibana/public/management/sections/objects/components/object_view/header.tsx b/src/legacy/core_plugins/kibana/public/management/sections/objects/components/object_view/header.tsx deleted file mode 100644 index 641493e0cbaa8..0000000000000 --- a/src/legacy/core_plugins/kibana/public/management/sections/objects/components/object_view/header.tsx +++ /dev/null @@ -1,110 +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 React from 'react'; -import { - EuiFlexGroup, - EuiFlexItem, - EuiTitle, - EuiButton, - EuiPageContentHeader, - EuiPageContentHeaderSection, -} from '@elastic/eui'; -import { FormattedMessage } from '@kbn/i18n/react'; - -interface HeaderProps { - canEdit: boolean; - canDelete: boolean; - canViewInApp: boolean; - type: string; - viewUrl: string; - onDeleteClick: () => void; -} - -export const Header = ({ - canEdit, - canDelete, - canViewInApp, - type, - viewUrl, - onDeleteClick, -}: HeaderProps) => { - return ( - - - - {canEdit ? ( -

- -

- ) : ( -

- -

- )} -
-
- - - {canViewInApp && ( - - - - - - )} - {canDelete && ( - - onDeleteClick()} - data-test-subj="savedObjectEditDelete" - > - - - - )} - - -
- ); -}; diff --git a/src/legacy/core_plugins/kibana/public/management/sections/objects/components/objects_table/__jest__/__snapshots__/objects_table.test.js.snap b/src/legacy/core_plugins/kibana/public/management/sections/objects/components/objects_table/__jest__/__snapshots__/objects_table.test.js.snap deleted file mode 100644 index 2c0a5d8f6b8f1..0000000000000 --- a/src/legacy/core_plugins/kibana/public/management/sections/objects/components/objects_table/__jest__/__snapshots__/objects_table.test.js.snap +++ /dev/null @@ -1,354 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`ObjectsTable delete should show a confirm modal 1`] = ` - - } - confirmButtonText={ - - } - defaultFocusedButton="confirm" - onCancel={[Function]} - onConfirm={[Function]} - title={ - - } -> -

- -

- -
-`; - -exports[`ObjectsTable export should allow the user to choose when exporting all 1`] = ` - - - - - - - - - } - labelType="legend" - > - - - - - } - name="includeReferencesDeep" - onChange={[Function]} - /> - - - - - - - - - - - - - - - - - - - - -`; - -exports[`ObjectsTable import should show the flyout 1`] = ` - -`; - -exports[`ObjectsTable relationships should show the flyout 1`] = ` - -`; - -exports[`ObjectsTable should render normally 1`] = ` - -
- -
- -`; diff --git a/src/legacy/core_plugins/kibana/public/management/sections/objects/components/objects_table/__jest__/objects_table.test.js b/src/legacy/core_plugins/kibana/public/management/sections/objects/components/objects_table/__jest__/objects_table.test.js deleted file mode 100644 index 7b9c17640a0f3..0000000000000 --- a/src/legacy/core_plugins/kibana/public/management/sections/objects/components/objects_table/__jest__/objects_table.test.js +++ /dev/null @@ -1,592 +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 React from 'react'; -import { shallowWithI18nProvider } from 'test_utils/enzyme_helpers'; -import { mockManagementPlugin } from '../../../../../../../../../../plugins/index_pattern_management/public/mocks'; -import { Query } from '@elastic/eui'; - -import { ObjectsTable, POSSIBLE_TYPES } from '../objects_table'; -import { Flyout } from '../components/flyout/'; -import { Relationships } from '../components/relationships/'; -import { findObjects } from '../../../lib'; -import { extractExportDetails } from '../../../lib/extract_export_details'; - -jest.mock('ui/kfetch', () => ({ kfetch: jest.fn() })); - -jest.mock('../../../../../../../../../../plugins/index_pattern_management/public', () => ({ - setup: mockManagementPlugin.createSetupContract(), - start: mockManagementPlugin.createStartContract(), -})); - -jest.mock('../../../lib/find_objects', () => ({ - findObjects: jest.fn(), -})); - -jest.mock('../components/header', () => ({ - Header: () => 'Header', -})); - -jest.mock('ui/chrome', () => ({ - addBasePath: () => '', - getInjected: () => ['index-pattern', 'visualization', 'dashboard', 'search'], -})); - -jest.mock('../../../lib/fetch_export_objects', () => ({ - fetchExportObjects: jest.fn(), -})); - -jest.mock('../../../lib/fetch_export_by_type_and_search', () => ({ - fetchExportByTypeAndSearch: jest.fn(), -})); - -jest.mock('../../../lib/extract_export_details', () => ({ - extractExportDetails: jest.fn(), -})); - -jest.mock('../../../lib/get_saved_object_counts', () => ({ - getSavedObjectCounts: jest.fn().mockImplementation(() => { - return { - 'index-pattern': 0, - visualization: 0, - dashboard: 0, - search: 0, - }; - }), -})); - -jest.mock('@elastic/filesaver', () => ({ - saveAs: jest.fn(), -})); - -jest.mock('../../../lib/get_relationships', () => ({ - getRelationships: jest.fn(), -})); - -jest.mock('ui/notify', () => ({})); - -const allSavedObjects = [ - { - id: '1', - type: 'index-pattern', - attributes: { - title: `MyIndexPattern*`, - }, - }, - { - id: '2', - type: 'search', - attributes: { - title: `MySearch`, - }, - }, - { - id: '3', - type: 'dashboard', - attributes: { - title: `MyDashboard`, - }, - }, - { - id: '4', - type: 'visualization', - attributes: { - title: `MyViz`, - }, - }, -]; - -const $http = () => {}; -$http.post = jest.fn().mockImplementation(() => []); -const defaultProps = { - goInspectObject: () => {}, - confirmModalPromise: jest.fn(), - savedObjectsClient: { - find: jest.fn(), - bulkGet: jest.fn(), - }, - indexPatterns: { - clearCache: jest.fn(), - }, - $http, - basePath: '', - newIndexPatternUrl: '', - kbnIndex: '', - services: [], - uiCapabilities: { - savedObjectsManagement: { - read: true, - edit: false, - delete: false, - }, - }, - canDelete: true, -}; - -beforeEach(() => { - findObjects.mockImplementation(() => ({ - total: 4, - savedObjects: [ - { - id: '1', - type: 'index-pattern', - meta: { - title: `MyIndexPattern*`, - icon: 'indexPatternApp', - editUrl: '#/management/kibana/index_patterns/1', - inAppUrl: { - path: '/management/kibana/index_patterns/1', - uiCapabilitiesPath: 'management.kibana.index_patterns', - }, - }, - }, - { - id: '2', - type: 'search', - meta: { - title: `MySearch`, - icon: 'search', - editUrl: '#/management/kibana/objects/savedSearches/2', - inAppUrl: { - path: '/discover/2', - uiCapabilitiesPath: 'discover.show', - }, - }, - }, - { - id: '3', - type: 'dashboard', - meta: { - title: `MyDashboard`, - icon: 'dashboardApp', - editUrl: '#/management/kibana/objects/savedDashboards/3', - inAppUrl: { - path: '/dashboard/3', - uiCapabilitiesPath: 'dashboard.show', - }, - }, - }, - { - id: '4', - type: 'visualization', - meta: { - title: `MyViz`, - icon: 'visualizeApp', - editUrl: '#/management/kibana/objects/savedVisualizations/4', - inAppUrl: { - path: '/visualize/edit/4', - uiCapabilitiesPath: 'visualize.show', - }, - }, - }, - ], - })); -}); - -let addDangerMock; -let addSuccessMock; -let addWarningMock; - -describe('ObjectsTable', () => { - beforeEach(() => { - defaultProps.savedObjectsClient.find.mockClear(); - extractExportDetails.mockReset(); - // mock _.debounce to fire immediately with no internal timer - require('lodash').debounce = func => { - function debounced(...args) { - return func.apply(this, args); - } - return debounced; - }; - addDangerMock = jest.fn(); - addSuccessMock = jest.fn(); - addWarningMock = jest.fn(); - require('ui/notify').toastNotifications = { - addDanger: addDangerMock, - addSuccess: addSuccessMock, - addWarning: addWarningMock, - }; - }); - - it('should render normally', async () => { - const component = shallowWithI18nProvider( - - ); - - // Ensure all promises resolve - await new Promise(resolve => process.nextTick(resolve)); - // Ensure the state changes are reflected - component.update(); - - expect(component).toMatchSnapshot(); - }); - - it('should add danger toast when find fails', async () => { - findObjects.mockImplementation(() => { - throw new Error('Simulated find error'); - }); - const component = shallowWithI18nProvider( - - ); - - // Ensure all promises resolve - await new Promise(resolve => process.nextTick(resolve)); - // Ensure the state changes are reflected - component.update(); - - expect(addDangerMock).toHaveBeenCalled(); - }); - - describe('export', () => { - it('should export selected objects', async () => { - const mockSelectedSavedObjects = [ - { id: '1', type: 'index-pattern' }, - { id: '3', type: 'dashboard' }, - ]; - - const mockSavedObjects = mockSelectedSavedObjects.map(obj => ({ - _id: obj.id, - _source: {}, - })); - - const mockSavedObjectsClient = { - ...defaultProps.savedObjectsClient, - bulkGet: jest.fn().mockImplementation(() => ({ - savedObjects: mockSavedObjects, - })), - }; - - const { fetchExportObjects } = require('../../../lib/fetch_export_objects'); - - const component = shallowWithI18nProvider( - - ); - - // Ensure all promises resolve - await new Promise(resolve => process.nextTick(resolve)); - // Ensure the state changes are reflected - component.update(); - - // Set some as selected - component.instance().onSelectionChanged(mockSelectedSavedObjects); - - await component.instance().onExport(true); - - expect(fetchExportObjects).toHaveBeenCalledWith(mockSelectedSavedObjects, true); - expect(addSuccessMock).toHaveBeenCalledWith({ - title: 'Your file is downloading in the background', - }); - }); - - it('should display a warning is export contains missing references', async () => { - const mockSelectedSavedObjects = [ - { id: '1', type: 'index-pattern' }, - { id: '3', type: 'dashboard' }, - ]; - - const mockSavedObjects = mockSelectedSavedObjects.map(obj => ({ - _id: obj.id, - _source: {}, - })); - - const mockSavedObjectsClient = { - ...defaultProps.savedObjectsClient, - bulkGet: jest.fn().mockImplementation(() => ({ - savedObjects: mockSavedObjects, - })), - }; - - const { fetchExportObjects } = require('../../../lib/fetch_export_objects'); - extractExportDetails.mockImplementation(() => ({ - exportedCount: 2, - missingRefCount: 1, - missingReferences: [{ id: '7', type: 'visualisation' }], - })); - - const component = shallowWithI18nProvider( - - ); - - // Ensure all promises resolve - await new Promise(resolve => process.nextTick(resolve)); - // Ensure the state changes are reflected - component.update(); - - // Set some as selected - component.instance().onSelectionChanged(mockSelectedSavedObjects); - - await component.instance().onExport(true); - - expect(fetchExportObjects).toHaveBeenCalledWith(mockSelectedSavedObjects, true); - expect(addWarningMock).toHaveBeenCalledWith({ - title: - 'Your file is downloading in the background. ' + - 'Some related objects could not be found. ' + - 'Please see the last line in the exported file for a list of missing objects.', - }); - }); - - it('should allow the user to choose when exporting all', async () => { - const component = shallowWithI18nProvider(); - - // Ensure all promises resolve - await new Promise(resolve => process.nextTick(resolve)); - // Ensure the state changes are reflected - component.update(); - - component.find('Header').prop('onExportAll')(); - component.update(); - - expect(component.find('EuiModal')).toMatchSnapshot(); - }); - - it('should export all', async () => { - const { - fetchExportByTypeAndSearch, - } = require('../../../lib/fetch_export_by_type_and_search'); - const { saveAs } = require('@elastic/filesaver'); - const component = shallowWithI18nProvider(); - - // Ensure all promises resolve - await new Promise(resolve => process.nextTick(resolve)); - // Ensure the state changes are reflected - component.update(); - - // Set up mocks - const blob = new Blob([JSON.stringify(allSavedObjects)], { type: 'application/ndjson' }); - fetchExportByTypeAndSearch.mockImplementation(() => blob); - - await component.instance().onExportAll(); - - expect(fetchExportByTypeAndSearch).toHaveBeenCalledWith(POSSIBLE_TYPES, undefined, true); - expect(saveAs).toHaveBeenCalledWith(blob, 'export.ndjson'); - expect(addSuccessMock).toHaveBeenCalledWith({ - title: 'Your file is downloading in the background', - }); - }); - - it('should export all, accounting for the current search criteria', async () => { - const { - fetchExportByTypeAndSearch, - } = require('../../../lib/fetch_export_by_type_and_search'); - const { saveAs } = require('@elastic/filesaver'); - const component = shallowWithI18nProvider(); - - component.instance().onQueryChange({ - query: Query.parse('test'), - }); - - // Ensure all promises resolve - await new Promise(resolve => process.nextTick(resolve)); - // Ensure the state changes are reflected - component.update(); - - // Set up mocks - const blob = new Blob([JSON.stringify(allSavedObjects)], { type: 'application/ndjson' }); - fetchExportByTypeAndSearch.mockImplementation(() => blob); - - await component.instance().onExportAll(); - - expect(fetchExportByTypeAndSearch).toHaveBeenCalledWith(POSSIBLE_TYPES, 'test*', true); - expect(saveAs).toHaveBeenCalledWith(blob, 'export.ndjson'); - expect(addSuccessMock).toHaveBeenCalledWith({ - title: 'Your file is downloading in the background', - }); - }); - }); - - describe('import', () => { - it('should show the flyout', async () => { - const component = shallowWithI18nProvider(); - - // Ensure all promises resolve - await new Promise(resolve => process.nextTick(resolve)); - // Ensure the state changes are reflected - component.update(); - - component.instance().showImportFlyout(); - component.update(); - - expect(component.find(Flyout)).toMatchSnapshot(); - }); - - it('should hide the flyout', async () => { - const component = shallowWithI18nProvider(); - - // Ensure all promises resolve - await new Promise(resolve => process.nextTick(resolve)); - // Ensure the state changes are reflected - component.update(); - - component.instance().hideImportFlyout(); - component.update(); - - expect(component.find(Flyout).length).toBe(0); - }); - }); - - describe('relationships', () => { - it('should fetch relationships', async () => { - const { getRelationships } = require('../../../lib/get_relationships'); - - const component = shallowWithI18nProvider(); - - // Ensure all promises resolve - await new Promise(resolve => process.nextTick(resolve)); - // Ensure the state changes are reflected - component.update(); - - await component.instance().getRelationships('search', '1'); - const savedObjectTypes = ['index-pattern', 'visualization', 'dashboard', 'search']; - expect(getRelationships).toHaveBeenCalledWith( - 'search', - '1', - savedObjectTypes, - defaultProps.$http, - defaultProps.basePath - ); - }); - - it('should show the flyout', async () => { - const component = shallowWithI18nProvider(); - - // Ensure all promises resolve - await new Promise(resolve => process.nextTick(resolve)); - // Ensure the state changes are reflected - component.update(); - - component.instance().onShowRelationships({ - id: '2', - type: 'search', - meta: { - title: `MySearch`, - icon: 'search', - editUrl: '#/management/kibana/objects/savedSearches/2', - inAppUrl: { - path: '/discover/2', - uiCapabilitiesPath: 'discover.show', - }, - }, - }); - component.update(); - - expect(component.find(Relationships)).toMatchSnapshot(); - expect(component.state('relationshipObject')).toEqual({ - id: '2', - type: 'search', - meta: { - title: 'MySearch', - editUrl: '#/management/kibana/objects/savedSearches/2', - icon: 'search', - inAppUrl: { - path: '/discover/2', - uiCapabilitiesPath: 'discover.show', - }, - }, - }); - }); - - it('should hide the flyout', async () => { - const component = shallowWithI18nProvider(); - - // Ensure all promises resolve - await new Promise(resolve => process.nextTick(resolve)); - // Ensure the state changes are reflected - component.update(); - - component.instance().onHideRelationships(); - component.update(); - - expect(component.find(Relationships).length).toBe(0); - expect(component.state('relationshipId')).toBe(undefined); - expect(component.state('relationshipType')).toBe(undefined); - expect(component.state('relationshipTitle')).toBe(undefined); - }); - }); - - describe('delete', () => { - it('should show a confirm modal', async () => { - const component = shallowWithI18nProvider(); - - const mockSelectedSavedObjects = [ - { id: '1', type: 'index-pattern', title: 'Title 1' }, - { id: '3', type: 'dashboard', title: 'Title 2' }, - ]; - - // Ensure all promises resolve - await new Promise(resolve => process.nextTick(resolve)); - // Ensure the state changes are reflected - component.update(); - - // Set some as selected - component.instance().onSelectionChanged(mockSelectedSavedObjects); - await component.instance().onDelete(); - component.update(); - - expect(component.find('EuiConfirmModal')).toMatchSnapshot(); - }); - - it('should delete selected objects', async () => { - const mockSelectedSavedObjects = [ - { id: '1', type: 'index-pattern' }, - { id: '3', type: 'dashboard' }, - ]; - - const mockSavedObjects = mockSelectedSavedObjects.map(obj => ({ - id: obj.id, - type: obj.type, - source: {}, - })); - - const mockSavedObjectsClient = { - ...defaultProps.savedObjectsClient, - bulkGet: jest.fn().mockImplementation(() => ({ - savedObjects: mockSavedObjects, - })), - delete: jest.fn(), - }; - - const component = shallowWithI18nProvider( - - ); - - // Ensure all promises resolve - await new Promise(resolve => process.nextTick(resolve)); - // Ensure the state changes are reflected - component.update(); - - // Set some as selected - component.instance().onSelectionChanged(mockSelectedSavedObjects); - - await component.instance().delete(); - - expect(defaultProps.indexPatterns.clearCache).toHaveBeenCalled(); - expect(mockSavedObjectsClient.bulkGet).toHaveBeenCalledWith(mockSelectedSavedObjects); - expect(mockSavedObjectsClient.delete).toHaveBeenCalledWith( - mockSavedObjects[0].type, - mockSavedObjects[0].id - ); - expect(mockSavedObjectsClient.delete).toHaveBeenCalledWith( - mockSavedObjects[1].type, - mockSavedObjects[1].id - ); - expect(component.state('selectedSavedObjects').length).toBe(0); - }); - }); -}); diff --git a/src/legacy/core_plugins/kibana/public/management/sections/objects/components/objects_table/components/flyout/__jest__/__snapshots__/flyout.test.js.snap b/src/legacy/core_plugins/kibana/public/management/sections/objects/components/objects_table/components/flyout/__jest__/__snapshots__/flyout.test.js.snap deleted file mode 100644 index 34ce8394232ed..0000000000000 --- a/src/legacy/core_plugins/kibana/public/management/sections/objects/components/objects_table/components/flyout/__jest__/__snapshots__/flyout.test.js.snap +++ /dev/null @@ -1,679 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`Flyout conflicts should allow conflict resolution 1`] = ` - - - -

- -

-
-
- - - - - } - > -

- - - , - } - } - /> -

-
-
- -
- - - - - - - - - - - - - - -
-`; - -exports[`Flyout conflicts should allow conflict resolution 2`] = ` -[MockFunction] { - "calls": Array [ - Array [ - Object { - "getConflictResolutions": [Function], - "state": Object { - "conflictedIndexPatterns": undefined, - "conflictedSavedObjectsLinkedToSavedSearches": undefined, - "conflictedSearchDocs": undefined, - "conflictingRecord": undefined, - "error": undefined, - "failedImports": Array [ - Object { - "error": Object { - "references": Array [ - Object { - "id": "MyIndexPattern*", - "type": "index-pattern", - }, - ], - "type": "missing_references", - }, - "obj": Object { - "id": "1", - "title": "My Visualization", - "type": "visualization", - }, - }, - ], - "file": Object { - "name": "foo.ndjson", - "path": "/home/foo.ndjson", - }, - "importCount": 0, - "indexPatterns": Array [ - Object { - "id": "1", - }, - Object { - "id": "2", - }, - ], - "isLegacyFile": false, - "isOverwriteAllChecked": true, - "loadingMessage": undefined, - "status": "loading", - "unmatchedReferences": Array [ - Object { - "existingIndexPatternId": "MyIndexPattern*", - "list": Array [ - Object { - "id": "1", - "title": "My Visualization", - "type": "visualization", - }, - ], - "newIndexPatternId": "2", - }, - ], - }, - }, - ], - ], - "results": Array [ - Object { - "type": "return", - "value": Object { - "failedImports": Array [], - "importCount": 1, - "status": "success", - }, - }, - ], -} -`; - -exports[`Flyout conflicts should handle errors 1`] = ` - - } -> -

- -

-

- -`; - -exports[`Flyout errors should display unsupported type errors properly 1`] = ` - - } -> -

- -

-

- wigwags [id=1] unsupported type -

-
-`; - -exports[`Flyout legacy conflicts should allow conflict resolution 1`] = ` - - - -

- -

-
-
- - - - - } - > -

- -

-
-
- - - - } - > -

- - - , - } - } - /> -

-
-
- -
- - - - - - - - - - - - - - -
-`; - -exports[`Flyout legacy conflicts should handle errors 1`] = ` -Array [ - - } - > -

- -

-
, - - } - > -

- - - , - } - } - /> -

-
, - - } - > -

- foobar -

-
, -] -`; - -exports[`Flyout should render import step 1`] = ` - - - -

- -

-
-
- - - - } - labelType="label" - > - - } - onChange={[Function]} - /> - - - - } - name="overwriteAll" - onChange={[Function]} - /> - - - - - - - - - - - - - - - - - -
-`; diff --git a/src/legacy/core_plugins/kibana/public/management/sections/objects/components/objects_table/components/flyout/__jest__/flyout.test.js b/src/legacy/core_plugins/kibana/public/management/sections/objects/components/objects_table/components/flyout/__jest__/flyout.test.js deleted file mode 100644 index 0d16e0ae35dd6..0000000000000 --- a/src/legacy/core_plugins/kibana/public/management/sections/objects/components/objects_table/components/flyout/__jest__/flyout.test.js +++ /dev/null @@ -1,566 +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 React from 'react'; -import { shallowWithI18nProvider } from 'test_utils/enzyme_helpers'; -import { mockManagementPlugin } from '../../../../../../../../../../../../plugins/index_pattern_management/public/mocks'; -import { Flyout } from '../flyout'; - -jest.mock('ui/kfetch', () => ({ kfetch: jest.fn() })); - -jest.mock('../../../../../lib/import_file', () => ({ - importFile: jest.fn(), -})); - -jest.mock('../../../../../lib/resolve_import_errors', () => ({ - resolveImportErrors: jest.fn(), -})); - -jest.mock('ui/chrome', () => ({ - addBasePath: () => {}, - getInjected: () => ['index-pattern', 'visualization', 'dashboard', 'search'], -})); - -jest.mock('../../../../../lib/import_legacy_file', () => ({ - importLegacyFile: jest.fn(), -})); - -jest.mock('../../../../../lib/resolve_saved_objects', () => ({ - resolveSavedObjects: jest.fn(), - resolveSavedSearches: jest.fn(), - resolveIndexPatternConflicts: jest.fn(), - saveObjects: jest.fn(), -})); - -jest.mock('../../../../../../../../../../../../plugins/index_pattern_management/public', () => ({ - setup: mockManagementPlugin.createSetupContract(), - start: mockManagementPlugin.createStartContract(), -})); - -jest.mock('ui/notify', () => ({})); - -const defaultProps = { - close: jest.fn(), - done: jest.fn(), - services: [], - newIndexPatternUrl: '', - getConflictResolutions: jest.fn(), - confirmModalPromise: jest.fn(), - indexPatterns: { - getFields: jest.fn().mockImplementation(() => [{ id: '1' }, { id: '2' }]), - }, -}; - -const mockFile = { - name: 'foo.ndjson', - path: '/home/foo.ndjson', -}; -const legacyMockFile = { - name: 'foo.json', - path: '/home/foo.json', -}; - -describe('Flyout', () => { - it('should render import step', async () => { - const component = shallowWithI18nProvider(); - - // Ensure all promises resolve - await new Promise(resolve => process.nextTick(resolve)); - // Ensure the state changes are reflected - component.update(); - - expect(component).toMatchSnapshot(); - }); - - it('should toggle the overwrite all control', async () => { - const component = shallowWithI18nProvider(); - - // Ensure all promises resolve - await new Promise(resolve => process.nextTick(resolve)); - // Ensure the state changes are reflected - component.update(); - - expect(component.state('isOverwriteAllChecked')).toBe(true); - component.find('EuiSwitch').simulate('change'); - expect(component.state('isOverwriteAllChecked')).toBe(false); - }); - - it('should allow picking a file', async () => { - const component = shallowWithI18nProvider(); - - // Ensure all promises resolve - await new Promise(resolve => process.nextTick(resolve)); - // Ensure the state changes are reflected - component.update(); - - expect(component.state('file')).toBe(undefined); - component.find('EuiFilePicker').simulate('change', [mockFile]); - expect(component.state('file')).toBe(mockFile); - }); - - it('should allow removing a file', async () => { - const component = shallowWithI18nProvider(); - - // Ensure all promises resolve - await Promise.resolve(); - // Ensure the state changes are reflected - component.update(); - - expect(component.state('file')).toBe(undefined); - component.find('EuiFilePicker').simulate('change', [mockFile]); - expect(component.state('file')).toBe(mockFile); - component.find('EuiFilePicker').simulate('change', []); - expect(component.state('file')).toBe(undefined); - }); - - it('should handle invalid files', async () => { - const { importLegacyFile } = require('../../../../../lib/import_legacy_file'); - const component = shallowWithI18nProvider(); - - // Ensure all promises resolve - await new Promise(resolve => process.nextTick(resolve)); - // Ensure the state changes are reflected - component.update(); - - importLegacyFile.mockImplementation(() => { - throw new Error('foobar'); - }); - - await component.instance().legacyImport(); - expect(component.state('error')).toBe('The file could not be processed.'); - - importLegacyFile.mockImplementation(() => ({ - invalid: true, - })); - - await component.instance().legacyImport(); - expect(component.state('error')).toBe( - 'Saved objects file format is invalid and cannot be imported.' - ); - }); - - describe('conflicts', () => { - const { importFile } = require('../../../../../lib/import_file'); - const { resolveImportErrors } = require('../../../../../lib/resolve_import_errors'); - - beforeEach(() => { - importFile.mockImplementation(() => ({ - success: false, - successCount: 0, - errors: [ - { - id: '1', - type: 'visualization', - title: 'My Visualization', - error: { - type: 'missing_references', - references: [ - { - id: 'MyIndexPattern*', - type: 'index-pattern', - }, - ], - }, - }, - ], - })); - resolveImportErrors.mockImplementation(() => ({ - status: 'success', - importCount: 1, - failedImports: [], - })); - }); - - it('should figure out unmatchedReferences', async () => { - const component = shallowWithI18nProvider(); - - // Ensure all promises resolve - await new Promise(resolve => process.nextTick(resolve)); - // Ensure the state changes are reflected - component.update(); - - component.setState({ file: mockFile, isLegacyFile: false }); - await component.instance().import(); - - expect(importFile).toHaveBeenCalledWith(mockFile, true); - expect(component.state()).toMatchObject({ - conflictedIndexPatterns: undefined, - conflictedSavedObjectsLinkedToSavedSearches: undefined, - conflictedSearchDocs: undefined, - importCount: 0, - status: 'idle', - error: undefined, - unmatchedReferences: [ - { - existingIndexPatternId: 'MyIndexPattern*', - newIndexPatternId: undefined, - list: [ - { - id: '1', - type: 'visualization', - title: 'My Visualization', - }, - ], - }, - ], - }); - }); - - it('should allow conflict resolution', async () => { - const component = shallowWithI18nProvider(); - - // Ensure all promises resolve - await new Promise(resolve => process.nextTick(resolve)); - // Ensure the state changes are reflected - component.update(); - - component.setState({ file: mockFile, isLegacyFile: false }); - await component.instance().import(); - - // Ensure it looks right - component.update(); - expect(component).toMatchSnapshot(); - - // Ensure we can change the resolution - component.instance().onIndexChanged('MyIndexPattern*', { target: { value: '2' } }); - expect(component.state('unmatchedReferences')[0].newIndexPatternId).toBe('2'); - - // Let's resolve now - await component - .find('EuiButton[data-test-subj="importSavedObjectsConfirmBtn"]') - .simulate('click'); - // Ensure all promises resolve - await new Promise(resolve => process.nextTick(resolve)); - expect(resolveImportErrors).toMatchSnapshot(); - }); - - it('should handle errors', async () => { - const component = shallowWithI18nProvider(); - - // Ensure all promises resolve - await new Promise(resolve => process.nextTick(resolve)); - // Ensure the state changes are reflected - component.update(); - - resolveImportErrors.mockImplementation(() => ({ - status: 'success', - importCount: 0, - failedImports: [ - { - obj: { - type: 'visualization', - id: '1', - }, - error: { - type: 'unknown', - }, - }, - ], - })); - - component.setState({ file: mockFile, isLegacyFile: false }); - - // Go through the import flow - await component.instance().import(); - component.update(); - // Set a resolution - component.instance().onIndexChanged('MyIndexPattern*', { target: { value: '2' } }); - await component - .find('EuiButton[data-test-subj="importSavedObjectsConfirmBtn"]') - .simulate('click'); - // Ensure all promises resolve - await new Promise(resolve => process.nextTick(resolve)); - - expect(component.state('failedImports')).toEqual([ - { - error: { - type: 'unknown', - }, - obj: { - id: '1', - type: 'visualization', - }, - }, - ]); - expect(component.find('EuiFlyoutBody EuiCallOut')).toMatchSnapshot(); - }); - }); - - describe('errors', () => { - const { importFile } = require('../../../../../lib/import_file'); - const { resolveImportErrors } = require('../../../../../lib/resolve_import_errors'); - - it('should display unsupported type errors properly', async () => { - const component = shallowWithI18nProvider(); - - // Ensure all promises resolve - await Promise.resolve(); - // Ensure the state changes are reflected - component.update(); - - importFile.mockImplementation(() => ({ - success: false, - successCount: 0, - errors: [ - { - id: '1', - type: 'wigwags', - title: 'My Title', - error: { - type: 'unsupported_type', - }, - }, - ], - })); - resolveImportErrors.mockImplementation(() => ({ - status: 'success', - importCount: 0, - failedImports: [ - { - error: { - type: 'unsupported_type', - }, - obj: { - id: '1', - type: 'wigwags', - title: 'My Title', - }, - }, - ], - })); - - component.setState({ file: mockFile, isLegacyFile: false }); - - // Go through the import flow - await component.instance().import(); - component.update(); - - // Ensure all promises resolve - await Promise.resolve(); - - expect(component.state('status')).toBe('success'); - expect(component.state('failedImports')).toEqual([ - { - error: { - type: 'unsupported_type', - }, - obj: { - id: '1', - type: 'wigwags', - title: 'My Title', - }, - }, - ]); - expect(component.find('EuiFlyout EuiCallOut')).toMatchSnapshot(); - }); - }); - - describe('legacy conflicts', () => { - const { importLegacyFile } = require('../../../../../lib/import_legacy_file'); - const { - resolveSavedObjects, - resolveSavedSearches, - resolveIndexPatternConflicts, - saveObjects, - } = require('../../../../../lib/resolve_saved_objects'); - - const mockData = [ - { - _id: '1', - _type: 'search', - }, - { - _id: '2', - _type: 'index-pattern', - }, - { - _id: '3', - _type: 'invalid', - }, - ]; - - const mockConflictedIndexPatterns = [ - { - doc: { - _type: 'index-pattern', - _id: '1', - _source: { - title: 'MyIndexPattern*', - }, - }, - obj: { - searchSource: { - getOwnField: field => { - if (field === 'index') { - return 'MyIndexPattern*'; - } - if (field === 'filter') { - return [{ meta: { index: 'filterIndex' } }]; - } - }, - }, - _serialize: () => { - return { references: [{ id: 'MyIndexPattern*' }, { id: 'filterIndex' }] }; - }, - }, - }, - ]; - - const mockConflictedSavedObjectsLinkedToSavedSearches = [2]; - const mockConflictedSearchDocs = [3]; - - beforeEach(() => { - importLegacyFile.mockImplementation(() => mockData); - resolveSavedObjects.mockImplementation(() => ({ - conflictedIndexPatterns: mockConflictedIndexPatterns, - conflictedSavedObjectsLinkedToSavedSearches: mockConflictedSavedObjectsLinkedToSavedSearches, - conflictedSearchDocs: mockConflictedSearchDocs, - importedObjectCount: 2, - confirmModalPromise: () => {}, - })); - }); - - it('should figure out unmatchedReferences', async () => { - const component = shallowWithI18nProvider(); - - // Ensure all promises resolve - await new Promise(resolve => process.nextTick(resolve)); - // Ensure the state changes are reflected - component.update(); - - component.setState({ file: legacyMockFile, isLegacyFile: true }); - await component.instance().legacyImport(); - - expect(importLegacyFile).toHaveBeenCalledWith(legacyMockFile); - // Remove the last element from data since it should be filtered out - expect(resolveSavedObjects).toHaveBeenCalledWith( - mockData.slice(0, 2).map(doc => ({ ...doc, _migrationVersion: {} })), - true, - defaultProps.services, - defaultProps.indexPatterns, - defaultProps.confirmModalPromise - ); - - expect(component.state()).toMatchObject({ - conflictedIndexPatterns: mockConflictedIndexPatterns, - conflictedSavedObjectsLinkedToSavedSearches: mockConflictedSavedObjectsLinkedToSavedSearches, - conflictedSearchDocs: mockConflictedSearchDocs, - importCount: 2, - status: 'idle', - error: undefined, - unmatchedReferences: [ - { - existingIndexPatternId: 'MyIndexPattern*', - newIndexPatternId: undefined, - list: [ - { - id: 'MyIndexPattern*', - title: 'MyIndexPattern*', - type: 'index-pattern', - }, - ], - }, - { - existingIndexPatternId: 'filterIndex', - list: [ - { - id: 'filterIndex', - title: 'MyIndexPattern*', - type: 'index-pattern', - }, - ], - newIndexPatternId: undefined, - }, - ], - }); - }); - - it('should allow conflict resolution', async () => { - const component = shallowWithI18nProvider(); - - // Ensure all promises resolve - await new Promise(resolve => process.nextTick(resolve)); - // Ensure the state changes are reflected - component.update(); - - component.setState({ file: legacyMockFile, isLegacyFile: true }); - await component.instance().legacyImport(); - - // Ensure it looks right - component.update(); - expect(component).toMatchSnapshot(); - - // Ensure we can change the resolution - component.instance().onIndexChanged('MyIndexPattern*', { target: { value: '2' } }); - expect(component.state('unmatchedReferences')[0].newIndexPatternId).toBe('2'); - - // Let's resolve now - await component - .find('EuiButton[data-test-subj="importSavedObjectsConfirmBtn"]') - .simulate('click'); - // Ensure all promises resolve - await new Promise(resolve => process.nextTick(resolve)); - expect(resolveIndexPatternConflicts).toHaveBeenCalledWith( - component.instance().resolutions, - mockConflictedIndexPatterns, - true, - defaultProps.indexPatterns - ); - expect(saveObjects).toHaveBeenCalledWith( - mockConflictedSavedObjectsLinkedToSavedSearches, - true - ); - expect(resolveSavedSearches).toHaveBeenCalledWith( - mockConflictedSearchDocs, - defaultProps.services, - defaultProps.indexPatterns, - true - ); - }); - - it('should handle errors', async () => { - const component = shallowWithI18nProvider(); - - // Ensure all promises resolve - await new Promise(resolve => process.nextTick(resolve)); - // Ensure the state changes are reflected - component.update(); - - resolveIndexPatternConflicts.mockImplementation(() => { - throw new Error('foobar'); - }); - - component.setState({ file: legacyMockFile, isLegacyFile: true }); - - // Go through the import flow - await component.instance().legacyImport(); - component.update(); - // Set a resolution - component.instance().onIndexChanged('MyIndexPattern*', { target: { value: '2' } }); - await component - .find('EuiButton[data-test-subj="importSavedObjectsConfirmBtn"]') - .simulate('click'); - // Ensure all promises resolve - await new Promise(resolve => process.nextTick(resolve)); - - expect(component.state('error')).toEqual('foobar'); - expect(component.find('EuiFlyoutBody EuiCallOut')).toMatchSnapshot(); - }); - }); -}); diff --git a/src/legacy/core_plugins/kibana/public/management/sections/objects/components/objects_table/components/flyout/flyout.js b/src/legacy/core_plugins/kibana/public/management/sections/objects/components/objects_table/components/flyout/flyout.js deleted file mode 100644 index da2221bb54203..0000000000000 --- a/src/legacy/core_plugins/kibana/public/management/sections/objects/components/objects_table/components/flyout/flyout.js +++ /dev/null @@ -1,945 +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 React, { Component, Fragment } from 'react'; -import PropTypes from 'prop-types'; -import { take, get as getField } from 'lodash'; -import { - EuiFlyout, - EuiFlyoutBody, - EuiFlyoutFooter, - EuiFlyoutHeader, - EuiButtonEmpty, - EuiButton, - EuiText, - EuiTitle, - EuiForm, - EuiFormRow, - EuiSwitch, - EuiFilePicker, - EuiInMemoryTable, - EuiSelect, - EuiFlexGroup, - EuiFlexItem, - EuiLoadingKibana, - EuiCallOut, - EuiSpacer, - EuiLink, - EuiConfirmModal, - EuiOverlayMask, - EUI_MODAL_CONFIRM_BUTTON, -} from '@elastic/eui'; -import { i18n } from '@kbn/i18n'; -import { FormattedMessage } from '@kbn/i18n/react'; - -import { - importFile, - importLegacyFile, - resolveImportErrors, - logLegacyImport, - getDefaultTitle, -} from '../../../../lib'; -import { processImportResponse } from '../../../../lib/process_import_response'; -import { - resolveSavedObjects, - resolveSavedSearches, - resolveIndexPatternConflicts, - saveObjects, -} from '../../../../lib/resolve_saved_objects'; -import { POSSIBLE_TYPES } from '../../objects_table'; - -export class Flyout extends Component { - static propTypes = { - close: PropTypes.func.isRequired, - done: PropTypes.func.isRequired, - services: PropTypes.array.isRequired, - newIndexPatternUrl: PropTypes.string.isRequired, - indexPatterns: PropTypes.object.isRequired, - confirmModalPromise: PropTypes.func.isRequired, - }; - - constructor(props) { - super(props); - - this.state = { - conflictedIndexPatterns: undefined, - conflictedSavedObjectsLinkedToSavedSearches: undefined, - conflictedSearchDocs: undefined, - unmatchedReferences: undefined, - conflictingRecord: undefined, - error: undefined, - file: undefined, - importCount: 0, - indexPatterns: undefined, - isOverwriteAllChecked: true, - loadingMessage: undefined, - isLegacyFile: false, - status: 'idle', - }; - } - - componentDidMount() { - this.fetchIndexPatterns(); - } - - fetchIndexPatterns = async () => { - const indexPatterns = await this.props.indexPatterns.getFields(['id', 'title']); - this.setState({ indexPatterns }); - }; - - changeOverwriteAll = () => { - this.setState(state => ({ - isOverwriteAllChecked: !state.isOverwriteAllChecked, - })); - }; - - setImportFile = ([file]) => { - if (!file) { - this.setState({ file: undefined, isLegacyFile: false }); - return; - } - this.setState({ - file, - isLegacyFile: /\.json$/i.test(file.name) || file.type === 'application/json', - }); - }; - - /** - * Import - * - * Does the initial import of a file, resolveImportErrors then handles errors and retries - */ - import = async () => { - const { file, isOverwriteAllChecked } = this.state; - this.setState({ status: 'loading', error: undefined }); - - // Import the file - let response; - try { - response = await importFile(file, isOverwriteAllChecked); - } catch (e) { - this.setState({ - status: 'error', - error: i18n.translate('kbn.management.objects.objectsTable.flyout.importFileErrorMessage', { - defaultMessage: 'The file could not be processed.', - }), - }); - return; - } - - this.setState(processImportResponse(response), () => { - // Resolve import errors right away if there's no index patterns to match - // This will ask about overwriting each object, etc - if (this.state.unmatchedReferences.length === 0) { - this.resolveImportErrors(); - } - }); - }; - - /** - * Get Conflict Resolutions - * - * Function iterates through the objects, displays a modal for each asking the user if they wish to overwrite it or not. - * - * @param {array} objects List of objects to request the user if they wish to overwrite it - * @return {Promise} An object with the key being "type:id" and value the resolution chosen by the user - */ - getConflictResolutions = async objects => { - const resolutions = {}; - for (const { type, id, title } of objects) { - const overwrite = await new Promise(resolve => { - this.setState({ - conflictingRecord: { - id, - type, - title, - done: resolve, - }, - }); - }); - resolutions[`${type}:${id}`] = overwrite; - this.setState({ conflictingRecord: undefined }); - } - return resolutions; - }; - - /** - * Resolve Import Errors - * - * Function goes through the failedImports and tries to resolve the issues. - */ - resolveImportErrors = async () => { - this.setState({ - error: undefined, - status: 'loading', - loadingMessage: undefined, - }); - - try { - const updatedState = await resolveImportErrors({ - state: this.state, - getConflictResolutions: this.getConflictResolutions, - }); - this.setState(updatedState); - } catch (e) { - this.setState({ - status: 'error', - error: i18n.translate( - 'kbn.management.objects.objectsTable.flyout.resolveImportErrorsFileErrorMessage', - { defaultMessage: 'The file could not be processed.' } - ), - }); - } - }; - - legacyImport = async () => { - const { services, indexPatterns, confirmModalPromise } = this.props; - const { file, isOverwriteAllChecked } = this.state; - - this.setState({ status: 'loading', error: undefined }); - - // Log warning on server, don't wait for response - logLegacyImport(); - - let contents; - try { - contents = await importLegacyFile(file); - } catch (e) { - this.setState({ - status: 'error', - error: i18n.translate( - 'kbn.management.objects.objectsTable.flyout.importLegacyFileErrorMessage', - { defaultMessage: 'The file could not be processed.' } - ), - }); - return; - } - - if (!Array.isArray(contents)) { - this.setState({ - status: 'error', - error: i18n.translate( - 'kbn.management.objects.objectsTable.flyout.invalidFormatOfImportedFileErrorMessage', - { defaultMessage: 'Saved objects file format is invalid and cannot be imported.' } - ), - }); - return; - } - - contents = contents - .filter(content => POSSIBLE_TYPES.includes(content._type)) - .map(doc => ({ - ...doc, - // The server assumes that documents with no migrationVersion are up to date. - // That assumption enables Kibana and other API consumers to not have to build - // up migrationVersion prior to creating new objects. But it means that imports - // need to set migrationVersion to something other than undefined, so that imported - // docs are not seen as automatically up-to-date. - _migrationVersion: doc._migrationVersion || {}, - })); - - const { - conflictedIndexPatterns, - conflictedSavedObjectsLinkedToSavedSearches, - conflictedSearchDocs, - importedObjectCount, - failedImports, - } = await resolveSavedObjects( - contents, - isOverwriteAllChecked, - services, - indexPatterns, - confirmModalPromise - ); - - const byId = {}; - conflictedIndexPatterns - .map(({ doc, obj }) => { - return { doc, obj: obj._serialize() }; - }) - .forEach(({ doc, obj }) => - obj.references.forEach(ref => { - byId[ref.id] = byId[ref.id] != null ? byId[ref.id].concat({ doc, obj }) : [{ doc, obj }]; - }) - ); - const unmatchedReferences = Object.entries(byId).reduce( - (accum, [existingIndexPatternId, list]) => { - accum.push({ - existingIndexPatternId, - newIndexPatternId: undefined, - list: list.map(({ doc }) => ({ - id: existingIndexPatternId, - type: doc._type, - title: doc._source.title, - })), - }); - return accum; - }, - [] - ); - - this.setState({ - conflictedIndexPatterns, - conflictedSavedObjectsLinkedToSavedSearches, - conflictedSearchDocs, - failedImports, - unmatchedReferences, - importCount: importedObjectCount, - status: unmatchedReferences.length === 0 ? 'success' : 'idle', - }); - }; - - get hasUnmatchedReferences() { - return this.state.unmatchedReferences && this.state.unmatchedReferences.length > 0; - } - - get resolutions() { - return this.state.unmatchedReferences.reduce( - (accum, { existingIndexPatternId, newIndexPatternId }) => { - if (newIndexPatternId) { - accum.push({ - oldId: existingIndexPatternId, - newId: newIndexPatternId, - }); - } - return accum; - }, - [] - ); - } - - confirmLegacyImport = async () => { - const { - conflictedIndexPatterns, - isOverwriteAllChecked, - conflictedSavedObjectsLinkedToSavedSearches, - conflictedSearchDocs, - failedImports, - } = this.state; - - const { services, indexPatterns } = this.props; - - this.setState({ - error: undefined, - status: 'loading', - loadingMessage: undefined, - }); - - let importCount = this.state.importCount; - - if (this.hasUnmatchedReferences) { - try { - const resolutions = this.resolutions; - - // Do not Promise.all these calls as the order matters - this.setState({ - loadingMessage: i18n.translate( - 'kbn.management.objects.objectsTable.flyout.confirmLegacyImport.resolvingConflictsLoadingMessage', - { defaultMessage: 'Resolving conflicts…' } - ), - }); - if (resolutions.length) { - importCount += await resolveIndexPatternConflicts( - resolutions, - conflictedIndexPatterns, - isOverwriteAllChecked, - this.props.indexPatterns - ); - } - this.setState({ - loadingMessage: i18n.translate( - 'kbn.management.objects.objectsTable.flyout.confirmLegacyImport.savingConflictsLoadingMessage', - { defaultMessage: 'Saving conflicts…' } - ), - }); - importCount += await saveObjects( - conflictedSavedObjectsLinkedToSavedSearches, - isOverwriteAllChecked - ); - this.setState({ - loadingMessage: i18n.translate( - 'kbn.management.objects.objectsTable.flyout.confirmLegacyImport.savedSearchAreLinkedProperlyLoadingMessage', - { defaultMessage: 'Ensure saved searches are linked properly…' } - ), - }); - importCount += await resolveSavedSearches( - conflictedSearchDocs, - services, - indexPatterns, - isOverwriteAllChecked - ); - this.setState({ - loadingMessage: i18n.translate( - 'kbn.management.objects.objectsTable.flyout.confirmLegacyImport.retryingFailedObjectsLoadingMessage', - { defaultMessage: 'Retrying failed objects…' } - ), - }); - importCount += await saveObjects( - failedImports.map(({ obj }) => obj), - isOverwriteAllChecked - ); - } catch (e) { - this.setState({ - error: e.message, - status: 'error', - loadingMessage: undefined, - }); - return; - } - } - - this.setState({ status: 'success', importCount }); - }; - - onIndexChanged = (id, e) => { - const value = e.target.value; - this.setState(state => { - const conflictIndex = state.unmatchedReferences.findIndex( - conflict => conflict.existingIndexPatternId === id - ); - if (conflictIndex === -1) { - return state; - } - - return { - unmatchedReferences: [ - ...state.unmatchedReferences.slice(0, conflictIndex), - { - ...state.unmatchedReferences[conflictIndex], - newIndexPatternId: value, - }, - ...state.unmatchedReferences.slice(conflictIndex + 1), - ], - }; - }); - }; - - renderUnmatchedReferences() { - const { unmatchedReferences } = this.state; - - if (!unmatchedReferences) { - return null; - } - - const columns = [ - { - field: 'existingIndexPatternId', - name: i18n.translate( - 'kbn.management.objects.objectsTable.flyout.renderConflicts.columnIdName', - { defaultMessage: 'ID' } - ), - description: i18n.translate( - 'kbn.management.objects.objectsTable.flyout.renderConflicts.columnIdDescription', - { defaultMessage: 'ID of the index pattern' } - ), - sortable: true, - }, - { - field: 'list', - name: i18n.translate( - 'kbn.management.objects.objectsTable.flyout.renderConflicts.columnCountName', - { defaultMessage: 'Count' } - ), - description: i18n.translate( - 'kbn.management.objects.objectsTable.flyout.renderConflicts.columnCountDescription', - { defaultMessage: 'How many affected objects' } - ), - render: list => { - return {list.length}; - }, - }, - { - field: 'list', - name: i18n.translate( - 'kbn.management.objects.objectsTable.flyout.renderConflicts.columnSampleOfAffectedObjectsName', - { defaultMessage: 'Sample of affected objects' } - ), - description: i18n.translate( - 'kbn.management.objects.objectsTable.flyout.renderConflicts.columnSampleOfAffectedObjectsDescription', - { defaultMessage: 'Sample of affected objects' } - ), - render: list => { - return ( -
    - {take(list, 3).map((obj, key) => ( -
  • {obj.title}
  • - ))} -
- ); - }, - }, - { - field: 'existingIndexPatternId', - name: i18n.translate( - 'kbn.management.objects.objectsTable.flyout.renderConflicts.columnNewIndexPatternName', - { defaultMessage: 'New index pattern' } - ), - render: id => { - const options = this.state.indexPatterns.map(indexPattern => ({ - text: indexPattern.title, - value: indexPattern.id, - ['data-test-subj']: `indexPatternOption-${indexPattern.title}`, - })); - - options.unshift({ - text: '-- Skip Import --', - value: '', - }); - - return ( - this.onIndexChanged(id, e)} - options={options} - /> - ); - }, - }, - ]; - - const pagination = { - pageSizeOptions: [5, 10, 25], - }; - - return ( - - ); - } - - renderError() { - const { error, status } = this.state; - - if (status !== 'error') { - return null; - } - - return ( - - - } - color="danger" - > -

{error}

-
- -
- ); - } - - renderBody() { - const { - status, - loadingMessage, - isOverwriteAllChecked, - importCount, - failedImports = [], - isLegacyFile, - } = this.state; - - if (status === 'loading') { - return ( - - - - - -

{loadingMessage}

-
-
-
- ); - } - - // Kept backwards compatible logic - if ( - failedImports.length && - (!this.hasUnmatchedReferences || (isLegacyFile === false && status === 'success')) - ) { - return ( - - } - color="warning" - iconType="help" - > -

- -

-

- {failedImports - .map(({ error, obj }) => { - if (error.type === 'missing_references') { - return error.references.map(reference => { - return i18n.translate( - 'kbn.management.objects.objectsTable.flyout.importFailedMissingReference', - { - defaultMessage: '{type} [id={id}] could not locate {refType} [id={refId}]', - values: { - id: obj.id, - type: obj.type, - refId: reference.id, - refType: reference.type, - }, - } - ); - }); - } else if (error.type === 'unsupported_type') { - return i18n.translate( - 'kbn.management.objects.objectsTable.flyout.importFailedUnsupportedType', - { - defaultMessage: '{type} [id={id}] unsupported type', - values: { - id: obj.id, - type: obj.type, - }, - } - ); - } - return getField(error, 'body.message', error.message || ''); - }) - .join(' ')} -

-
- ); - } - - if (status === 'success') { - if (importCount === 0) { - return ( - - } - color="primary" - /> - ); - } - - return ( - - } - color="success" - iconType="check" - > -

- -

-
- ); - } - - if (this.hasUnmatchedReferences) { - return this.renderUnmatchedReferences(); - } - - return ( - - - } - > - - } - onChange={this.setImportFile} - /> - - - - } - data-test-subj="importSavedObjectsOverwriteToggle" - checked={isOverwriteAllChecked} - onChange={this.changeOverwriteAll} - /> - - - ); - } - - renderFooter() { - const { status } = this.state; - const { done, close } = this.props; - - let confirmButton; - - if (status === 'success') { - confirmButton = ( - - - - ); - } else if (this.hasUnmatchedReferences) { - confirmButton = ( - - - - ); - } else { - confirmButton = ( - - - - ); - } - - return ( - - - - - - - {confirmButton} - - ); - } - - renderSubheader() { - if (this.state.status === 'loading' || this.state.status === 'success') { - return null; - } - - let legacyFileWarning; - if (this.state.isLegacyFile) { - legacyFileWarning = ( - - } - color="warning" - iconType="help" - > -

- -

-
- ); - } - - let indexPatternConflictsWarning; - if (this.hasUnmatchedReferences) { - indexPatternConflictsWarning = ( - - } - color="warning" - iconType="help" - > -

- - - - ), - }} - /> -

-
- ); - } - - if (!legacyFileWarning && !indexPatternConflictsWarning) { - return null; - } - - return ( - - {legacyFileWarning && ( - - - {legacyFileWarning} - - )} - {indexPatternConflictsWarning && ( - - - {indexPatternConflictsWarning} - - )} - - ); - } - - overwriteConfirmed() { - this.state.conflictingRecord.done(true); - } - - overwriteSkipped() { - this.state.conflictingRecord.done(false); - } - - render() { - const { close } = this.props; - - let confirmOverwriteModal; - if (this.state.conflictingRecord) { - confirmOverwriteModal = ( - - -

- -

-
-
- ); - } - - return ( - - - -

- -

-
-
- - - {this.renderSubheader()} - {this.renderError()} - {this.renderBody()} - - - {this.renderFooter()} - {confirmOverwriteModal} -
- ); - } -} diff --git a/src/legacy/core_plugins/kibana/public/management/sections/objects/components/objects_table/components/flyout/index.js b/src/legacy/core_plugins/kibana/public/management/sections/objects/components/objects_table/components/flyout/index.js deleted file mode 100644 index cdeebdbf7b63a..0000000000000 --- a/src/legacy/core_plugins/kibana/public/management/sections/objects/components/objects_table/components/flyout/index.js +++ /dev/null @@ -1,20 +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. - */ - -export { Flyout } from './flyout'; diff --git a/src/legacy/core_plugins/kibana/public/management/sections/objects/components/objects_table/components/header/__jest__/__snapshots__/header.test.js.snap b/src/legacy/core_plugins/kibana/public/management/sections/objects/components/objects_table/components/header/__jest__/__snapshots__/header.test.js.snap deleted file mode 100644 index 51bd51a5e2e58..0000000000000 --- a/src/legacy/core_plugins/kibana/public/management/sections/objects/components/objects_table/components/header/__jest__/__snapshots__/header.test.js.snap +++ /dev/null @@ -1,106 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`Header should render normally 1`] = ` - - - - -

- -

-
-
- - - - - - - - - - - - - - - - - - - -
- - -

- - - -

-
- -
-`; diff --git a/src/legacy/core_plugins/kibana/public/management/sections/objects/components/objects_table/components/header/__jest__/header.test.js b/src/legacy/core_plugins/kibana/public/management/sections/objects/components/objects_table/components/header/__jest__/header.test.js deleted file mode 100644 index 1f501b5751224..0000000000000 --- a/src/legacy/core_plugins/kibana/public/management/sections/objects/components/objects_table/components/header/__jest__/header.test.js +++ /dev/null @@ -1,39 +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 React from 'react'; -import { shallow } from 'enzyme'; - -import { Header } from '../header'; - -describe('Header', () => { - it('should render normally', () => { - const props = { - onExportAll: () => {}, - onImport: () => {}, - onRefresh: () => {}, - totalCount: 4, - filteredCount: 2, - }; - - const component = shallow(
); - - expect(component).toMatchSnapshot(); - }); -}); diff --git a/src/legacy/core_plugins/kibana/public/management/sections/objects/components/objects_table/components/header/header.js b/src/legacy/core_plugins/kibana/public/management/sections/objects/components/objects_table/components/header/header.js deleted file mode 100644 index 0bec8a0cf2daf..0000000000000 --- a/src/legacy/core_plugins/kibana/public/management/sections/objects/components/objects_table/components/header/header.js +++ /dev/null @@ -1,113 +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 React, { Fragment } from 'react'; -import PropTypes from 'prop-types'; - -import { - EuiSpacer, - EuiTitle, - EuiFlexGroup, - EuiFlexItem, - EuiText, - EuiTextColor, - EuiButtonEmpty, -} from '@elastic/eui'; -import { FormattedMessage } from '@kbn/i18n/react'; - -export const Header = ({ onExportAll, onImport, onRefresh, filteredCount }) => ( - - - - -

- -

-
-
- - - - - - - - - - - - - - - - - - - - -
- - -

- - - -

-
- -
-); - -Header.propTypes = { - onExportAll: PropTypes.func.isRequired, - onImport: PropTypes.func.isRequired, - onRefresh: PropTypes.func.isRequired, - filteredCount: PropTypes.number.isRequired, -}; diff --git a/src/legacy/core_plugins/kibana/public/management/sections/objects/components/objects_table/components/header/index.js b/src/legacy/core_plugins/kibana/public/management/sections/objects/components/objects_table/components/header/index.js deleted file mode 100644 index ac1e7bac06c87..0000000000000 --- a/src/legacy/core_plugins/kibana/public/management/sections/objects/components/objects_table/components/header/index.js +++ /dev/null @@ -1,20 +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. - */ - -export { Header } from './header'; diff --git a/src/legacy/core_plugins/kibana/public/management/sections/objects/components/objects_table/components/relationships/__jest__/__snapshots__/relationships.test.js.snap b/src/legacy/core_plugins/kibana/public/management/sections/objects/components/objects_table/components/relationships/__jest__/__snapshots__/relationships.test.js.snap deleted file mode 100644 index 728944f3ccbfe..0000000000000 --- a/src/legacy/core_plugins/kibana/public/management/sections/objects/components/objects_table/components/relationships/__jest__/__snapshots__/relationships.test.js.snap +++ /dev/null @@ -1,716 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`Relationships should render dashboards normally 1`] = ` - - - -

- - - -    - MyDashboard -

-
-
- -
- -

- Here are the saved objects related to MyDashboard. Deleting this dashboard affects its parent objects, but not its children. -

-
- - -
-
-
-`; - -exports[`Relationships should render errors 1`] = ` - - - -

- - - -    - MyDashboard -

-
-
- - - } - > - foo - - -
-`; - -exports[`Relationships should render index patterns normally 1`] = ` - - - -

- - - -    - MyIndexPattern* -

-
-
- -
- -

- Here are the saved objects related to MyIndexPattern*. Deleting this index-pattern affects its parent objects, but not its children. -

-
- - -
-
-
-`; - -exports[`Relationships should render searches normally 1`] = ` - - - -

- - - -    - MySearch -

-
-
- -
- -

- Here are the saved objects related to MySearch. Deleting this search affects its parent objects, but not its children. -

-
- - -
-
-
-`; - -exports[`Relationships should render visualizations normally 1`] = ` - - - -

- - - -    - MyViz -

-
-
- -
- -

- Here are the saved objects related to MyViz. Deleting this visualization affects its parent objects, but not its children. -

-
- - -
-
-
-`; diff --git a/src/legacy/core_plugins/kibana/public/management/sections/objects/components/objects_table/components/relationships/__jest__/relationships.test.js b/src/legacy/core_plugins/kibana/public/management/sections/objects/components/objects_table/components/relationships/__jest__/relationships.test.js deleted file mode 100644 index 479726e8785d8..0000000000000 --- a/src/legacy/core_plugins/kibana/public/management/sections/objects/components/objects_table/components/relationships/__jest__/relationships.test.js +++ /dev/null @@ -1,324 +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 React from 'react'; -import { shallowWithI18nProvider } from 'test_utils/enzyme_helpers'; - -jest.mock('ui/kfetch', () => ({ kfetch: jest.fn() })); - -jest.mock('ui/chrome', () => ({ - addBasePath: () => '', -})); - -jest.mock('../../../../../lib/fetch_export_by_type_and_search', () => ({ - fetchExportByTypeAndSearch: jest.fn(), -})); - -jest.mock('../../../../../lib/fetch_export_objects', () => ({ - fetchExportObjects: jest.fn(), -})); - -import { Relationships } from '../relationships'; - -describe('Relationships', () => { - it('should render index patterns normally', async () => { - const props = { - goInspectObject: () => {}, - getRelationships: jest.fn().mockImplementation(() => [ - { - type: 'search', - id: '1', - relationship: 'parent', - meta: { - editUrl: '/management/kibana/objects/savedSearches/1', - icon: 'search', - inAppUrl: { - path: '/app/kibana#/discover/1', - uiCapabilitiesPath: 'discover.show', - }, - title: 'My Search Title', - }, - }, - { - type: 'visualization', - id: '2', - relationship: 'parent', - meta: { - editUrl: '/management/kibana/objects/savedVisualizations/2', - icon: 'visualizeApp', - inAppUrl: { - path: '/app/kibana#/visualize/edit/2', - uiCapabilitiesPath: 'visualize.show', - }, - title: 'My Visualization Title', - }, - }, - ]), - savedObject: { - id: '1', - type: 'index-pattern', - meta: { - title: 'MyIndexPattern*', - icon: 'indexPatternApp', - editUrl: '#/management/kibana/index_patterns/1', - inAppUrl: { - path: '/management/kibana/index_patterns/1', - uiCapabilitiesPath: 'management.kibana.index_patterns', - }, - }, - }, - close: jest.fn(), - }; - - const component = shallowWithI18nProvider(); - - // Make sure we are showing loading - expect(component.find('EuiLoadingKibana').length).toBe(1); - - // Ensure all promises resolve - await new Promise(resolve => process.nextTick(resolve)); - // Ensure the state changes are reflected - component.update(); - - expect(props.getRelationships).toHaveBeenCalled(); - expect(component).toMatchSnapshot(); - }); - - it('should render searches normally', async () => { - const props = { - goInspectObject: () => {}, - getRelationships: jest.fn().mockImplementation(() => [ - { - type: 'index-pattern', - id: '1', - relationship: 'child', - meta: { - editUrl: '/management/kibana/index_patterns/1', - icon: 'indexPatternApp', - inAppUrl: { - path: '/app/kibana#/management/kibana/index_patterns/1', - uiCapabilitiesPath: 'management.kibana.index_patterns', - }, - title: 'My Index Pattern', - }, - }, - { - type: 'visualization', - id: '2', - relationship: 'parent', - meta: { - editUrl: '/management/kibana/objects/savedVisualizations/2', - icon: 'visualizeApp', - inAppUrl: { - path: '/app/kibana#/visualize/edit/2', - uiCapabilitiesPath: 'visualize.show', - }, - title: 'My Visualization Title', - }, - }, - ]), - savedObject: { - id: '1', - type: 'search', - meta: { - title: 'MySearch', - icon: 'search', - editUrl: '#/management/kibana/objects/savedSearches/1', - inAppUrl: { - path: '/discover/1', - uiCapabilitiesPath: 'discover.show', - }, - }, - }, - close: jest.fn(), - }; - - const component = shallowWithI18nProvider(); - - // Make sure we are showing loading - expect(component.find('EuiLoadingKibana').length).toBe(1); - - // Ensure all promises resolve - await new Promise(resolve => process.nextTick(resolve)); - // Ensure the state changes are reflected - component.update(); - - expect(props.getRelationships).toHaveBeenCalled(); - expect(component).toMatchSnapshot(); - }); - - it('should render visualizations normally', async () => { - const props = { - goInspectObject: () => {}, - getRelationships: jest.fn().mockImplementation(() => [ - { - type: 'dashboard', - id: '1', - relationship: 'parent', - meta: { - editUrl: '/management/kibana/objects/savedDashboards/1', - icon: 'dashboardApp', - inAppUrl: { - path: '/app/kibana#/dashboard/1', - uiCapabilitiesPath: 'dashboard.show', - }, - title: 'My Dashboard 1', - }, - }, - { - type: 'dashboard', - id: '2', - relationship: 'parent', - meta: { - editUrl: '/management/kibana/objects/savedDashboards/2', - icon: 'dashboardApp', - inAppUrl: { - path: '/app/kibana#/dashboard/2', - uiCapabilitiesPath: 'dashboard.show', - }, - title: 'My Dashboard 2', - }, - }, - ]), - savedObject: { - id: '1', - type: 'visualization', - meta: { - title: 'MyViz', - icon: 'visualizeApp', - editUrl: '#/management/kibana/objects/savedVisualizations/1', - inAppUrl: { - path: '/visualize/edit/1', - uiCapabilitiesPath: 'visualize.show', - }, - }, - }, - close: jest.fn(), - }; - - const component = shallowWithI18nProvider(); - - // Make sure we are showing loading - expect(component.find('EuiLoadingKibana').length).toBe(1); - - // Ensure all promises resolve - await new Promise(resolve => process.nextTick(resolve)); - // Ensure the state changes are reflected - component.update(); - - expect(props.getRelationships).toHaveBeenCalled(); - expect(component).toMatchSnapshot(); - }); - - it('should render dashboards normally', async () => { - const props = { - goInspectObject: () => {}, - getRelationships: jest.fn().mockImplementation(() => [ - { - type: 'visualization', - id: '1', - relationship: 'child', - meta: { - editUrl: '/management/kibana/objects/savedVisualizations/1', - icon: 'visualizeApp', - inAppUrl: { - path: '/app/kibana#/visualize/edit/1', - uiCapabilitiesPath: 'visualize.show', - }, - title: 'My Visualization Title 1', - }, - }, - { - type: 'visualization', - id: '2', - relationship: 'child', - meta: { - editUrl: '/management/kibana/objects/savedVisualizations/2', - icon: 'visualizeApp', - inAppUrl: { - path: '/app/kibana#/visualize/edit/2', - uiCapabilitiesPath: 'visualize.show', - }, - title: 'My Visualization Title 2', - }, - }, - ]), - savedObject: { - id: '1', - type: 'dashboard', - meta: { - title: 'MyDashboard', - icon: 'dashboardApp', - editUrl: '#/management/kibana/objects/savedDashboards/1', - inAppUrl: { - path: '/dashboard/1', - uiCapabilitiesPath: 'dashboard.show', - }, - }, - }, - close: jest.fn(), - }; - - const component = shallowWithI18nProvider(); - - // Make sure we are showing loading - expect(component.find('EuiLoadingKibana').length).toBe(1); - - // Ensure all promises resolve - await new Promise(resolve => process.nextTick(resolve)); - // Ensure the state changes are reflected - component.update(); - - expect(props.getRelationships).toHaveBeenCalled(); - expect(component).toMatchSnapshot(); - }); - - it('should render errors', async () => { - const props = { - goInspectObject: () => {}, - getRelationships: jest.fn().mockImplementation(() => { - throw new Error('foo'); - }), - savedObject: { - id: '1', - type: 'dashboard', - meta: { - title: 'MyDashboard', - icon: 'dashboardApp', - editUrl: '#/management/kibana/objects/savedDashboards/1', - inAppUrl: { - path: '/dashboard/1', - uiCapabilitiesPath: 'dashboard.show', - }, - }, - }, - close: jest.fn(), - }; - - const component = shallowWithI18nProvider(); - - // Ensure all promises resolve - await new Promise(resolve => process.nextTick(resolve)); - // Ensure the state changes are reflected - component.update(); - - expect(props.getRelationships).toHaveBeenCalled(); - expect(component).toMatchSnapshot(); - }); -}); diff --git a/src/legacy/core_plugins/kibana/public/management/sections/objects/components/objects_table/components/relationships/index.js b/src/legacy/core_plugins/kibana/public/management/sections/objects/components/objects_table/components/relationships/index.js deleted file mode 100644 index 522b1ce83a6b6..0000000000000 --- a/src/legacy/core_plugins/kibana/public/management/sections/objects/components/objects_table/components/relationships/index.js +++ /dev/null @@ -1,20 +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. - */ - -export { Relationships } from './relationships'; diff --git a/src/legacy/core_plugins/kibana/public/management/sections/objects/components/objects_table/components/relationships/relationships.js b/src/legacy/core_plugins/kibana/public/management/sections/objects/components/objects_table/components/relationships/relationships.js deleted file mode 100644 index ce3415ad2f0e7..0000000000000 --- a/src/legacy/core_plugins/kibana/public/management/sections/objects/components/objects_table/components/relationships/relationships.js +++ /dev/null @@ -1,340 +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 React, { Component } from 'react'; -import PropTypes from 'prop-types'; - -import { - EuiTitle, - EuiFlyout, - EuiFlyoutBody, - EuiFlyoutHeader, - EuiLink, - EuiIcon, - EuiCallOut, - EuiLoadingKibana, - EuiInMemoryTable, - EuiToolTip, - EuiText, - EuiSpacer, -} from '@elastic/eui'; -import chrome from 'ui/chrome'; -import { i18n } from '@kbn/i18n'; -import { FormattedMessage } from '@kbn/i18n/react'; -import { getDefaultTitle, getSavedObjectLabel } from '../../../../lib'; - -export class Relationships extends Component { - static propTypes = { - getRelationships: PropTypes.func.isRequired, - savedObject: PropTypes.object.isRequired, - close: PropTypes.func.isRequired, - goInspectObject: PropTypes.func.isRequired, - canGoInApp: PropTypes.func.isRequired, - }; - - constructor(props) { - super(props); - - this.state = { - relationships: undefined, - isLoading: false, - error: undefined, - }; - } - - UNSAFE_componentWillMount() { - this.getRelationshipData(); - } - - UNSAFE_componentWillReceiveProps(nextProps) { - if (nextProps.savedObject.id !== this.props.savedObject.id) { - this.getRelationshipData(); - } - } - - async getRelationshipData() { - const { savedObject, getRelationships } = this.props; - - this.setState({ isLoading: true }); - - try { - const relationships = await getRelationships(savedObject.type, savedObject.id); - this.setState({ relationships, isLoading: false, error: undefined }); - } catch (err) { - this.setState({ error: err.message, isLoading: false }); - } - } - - renderError() { - const { error } = this.state; - - if (!error) { - return null; - } - - return ( - - } - color="danger" - > - {error} - - ); - } - - renderRelationships() { - const { goInspectObject, savedObject } = this.props; - const { relationships, isLoading, error } = this.state; - - if (error) { - return this.renderError(); - } - - if (isLoading) { - return ; - } - - const columns = [ - { - field: 'type', - name: i18n.translate('kbn.management.objects.objectsTable.relationships.columnTypeName', { - defaultMessage: 'Type', - }), - width: '50px', - align: 'center', - description: i18n.translate( - 'kbn.management.objects.objectsTable.relationships.columnTypeDescription', - { defaultMessage: 'Type of the saved object' } - ), - sortable: false, - render: (type, object) => { - return ( - - - - ); - }, - }, - { - field: 'relationship', - name: i18n.translate( - 'kbn.management.objects.objectsTable.relationships.columnRelationshipName', - { defaultMessage: 'Direct relationship' } - ), - dataType: 'string', - sortable: false, - width: '125px', - 'data-test-subj': 'directRelationship', - render: relationship => { - if (relationship === 'parent') { - return ( - - - - ); - } - if (relationship === 'child') { - return ( - - - - ); - } - }, - }, - { - field: 'meta.title', - name: i18n.translate('kbn.management.objects.objectsTable.relationships.columnTitleName', { - defaultMessage: 'Title', - }), - description: i18n.translate( - 'kbn.management.objects.objectsTable.relationships.columnTitleDescription', - { defaultMessage: 'Title of the saved object' } - ), - dataType: 'string', - sortable: false, - render: (title, object) => { - const { path } = object.meta.inAppUrl || {}; - const canGoInApp = this.props.canGoInApp(object); - if (!canGoInApp) { - return ( - - {title || getDefaultTitle(object)} - - ); - } - return ( - - {title || getDefaultTitle(object)} - - ); - }, - }, - { - name: i18n.translate( - 'kbn.management.objects.objectsTable.relationships.columnActionsName', - { defaultMessage: 'Actions' } - ), - actions: [ - { - name: i18n.translate( - 'kbn.management.objects.objectsTable.relationships.columnActions.inspectActionName', - { defaultMessage: 'Inspect' } - ), - description: i18n.translate( - 'kbn.management.objects.objectsTable.relationships.columnActions.inspectActionDescription', - { defaultMessage: 'Inspect this saved object' } - ), - type: 'icon', - icon: 'inspect', - 'data-test-subj': 'relationshipsTableAction-inspect', - onClick: object => goInspectObject(object), - available: object => !!object.meta.editUrl, - }, - ], - }, - ]; - - const filterTypesMap = new Map( - relationships.map(relationship => [ - relationship.type, - { - value: relationship.type, - name: relationship.type, - view: relationship.type, - }, - ]) - ); - - const search = { - filters: [ - { - type: 'field_value_selection', - field: 'relationship', - name: i18n.translate( - 'kbn.management.objects.objectsTable.relationships.search.filters.relationship.name', - { defaultMessage: 'Direct relationship' } - ), - multiSelect: 'or', - options: [ - { - value: 'parent', - name: 'parent', - view: i18n.translate( - 'kbn.management.objects.objectsTable.relationships.search.filters.relationship.parentAsValue.view', - { defaultMessage: 'Parent' } - ), - }, - { - value: 'child', - name: 'child', - view: i18n.translate( - 'kbn.management.objects.objectsTable.relationships.search.filters.relationship.childAsValue.view', - { defaultMessage: 'Child' } - ), - }, - ], - }, - { - type: 'field_value_selection', - field: 'type', - name: i18n.translate( - 'kbn.management.objects.objectsTable.relationships.search.filters.type.name', - { defaultMessage: 'Type' } - ), - multiSelect: 'or', - options: [...filterTypesMap.values()], - }, - ], - }; - - return ( -
- -

- {i18n.translate( - 'kbn.management.objects.objectsTable.relationships.relationshipsTitle', - { - defaultMessage: - 'Here are the saved objects related to {title}. ' + - 'Deleting this {type} affects its parent objects, but not its children.', - values: { - type: savedObject.type, - title: savedObject.meta.title || getDefaultTitle(savedObject), - }, - } - )} -

-
- - ({ - 'data-test-subj': `relationshipsTableRow`, - })} - /> -
- ); - } - - render() { - const { close, savedObject } = this.props; - - return ( - - - -

- - - -    - {savedObject.meta.title || getDefaultTitle(savedObject)} -

-
-
- - {this.renderRelationships()} -
- ); - } -} diff --git a/src/legacy/core_plugins/kibana/public/management/sections/objects/components/objects_table/components/table/__jest__/__snapshots__/table.test.js.snap b/src/legacy/core_plugins/kibana/public/management/sections/objects/components/objects_table/components/table/__jest__/__snapshots__/table.test.js.snap deleted file mode 100644 index a4dcfb9c38184..0000000000000 --- a/src/legacy/core_plugins/kibana/public/management/sections/objects/components/objects_table/components/table/__jest__/__snapshots__/table.test.js.snap +++ /dev/null @@ -1,428 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`Table prevents saved objects from being deleted 1`] = ` - - - - , - - - - } - closePopover={[Function]} - display="inlineBlock" - hasArrow={true} - isOpen={false} - ownFocus={false} - panelPaddingSize="m" - > - - } - labelType="label" - > - - } - name="includeReferencesDeep" - onChange={[Function]} - /> - - - - - - - , - ] - } - /> - -
- -
-
-`; - -exports[`Table should render normally 1`] = ` - - - - , - - - - } - closePopover={[Function]} - display="inlineBlock" - hasArrow={true} - isOpen={false} - ownFocus={false} - panelPaddingSize="m" - > - - } - labelType="label" - > - - } - name="includeReferencesDeep" - onChange={[Function]} - /> - - - - - - - , - ] - } - /> - -
- -
-
-`; diff --git a/src/legacy/core_plugins/kibana/public/management/sections/objects/components/objects_table/components/table/__jest__/table.test.js b/src/legacy/core_plugins/kibana/public/management/sections/objects/components/objects_table/components/table/__jest__/table.test.js deleted file mode 100644 index 9b3e2314c9f84..0000000000000 --- a/src/legacy/core_plugins/kibana/public/management/sections/objects/components/objects_table/components/table/__jest__/table.test.js +++ /dev/null @@ -1,129 +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 React from 'react'; -import { shallowWithI18nProvider, mountWithI18nProvider } from 'test_utils/enzyme_helpers'; -import { findTestSubject } from '@elastic/eui/lib/test'; -import { keyCodes } from '@elastic/eui/lib/services'; -import { npSetup as mockNpSetup } from '../../../../../../../../../../../ui/public/new_platform/__mocks__'; - -jest.mock('ui/kfetch', () => ({ kfetch: jest.fn() })); - -jest.mock('ui/chrome', () => ({ - addBasePath: () => '', -})); - -jest.mock('ui/new_platform', () => ({ - npSetup: mockNpSetup, -})); - -import { Table } from '../table'; - -const defaultProps = { - selectedSavedObjects: [ - { - id: '1', - type: 'index-pattern', - meta: { - title: `MyIndexPattern*`, - icon: 'indexPatternApp', - editUrl: '#/management/kibana/index_patterns/1', - inAppUrl: { - path: '/management/kibana/index_patterns/1', - uiCapabilitiesPath: 'management.kibana.index_patterns', - }, - }, - }, - ], - selectionConfig: { - onSelectionChange: () => {}, - }, - filterOptions: [{ value: 2 }], - onDelete: () => {}, - onExport: () => {}, - goInspectObject: () => {}, - canGoInApp: () => {}, - pageIndex: 1, - pageSize: 2, - items: [ - { - id: '1', - type: 'index-pattern', - meta: { - title: `MyIndexPattern*`, - icon: 'indexPatternApp', - editUrl: '#/management/kibana/index_patterns/1', - inAppUrl: { - path: '/management/kibana/index_patterns/1', - uiCapabilitiesPath: 'management.kibana.index_patterns', - }, - }, - }, - ], - itemId: 'id', - totalItemCount: 3, - onQueryChange: () => {}, - onTableChange: () => {}, - isSearching: false, - onShowRelationships: () => {}, - canDelete: true, -}; - -describe('Table', () => { - it('should render normally', () => { - const component = shallowWithI18nProvider(
); - - expect(component).toMatchSnapshot(); - }); - - it('should handle query parse error', () => { - const onQueryChangeMock = jest.fn(); - const customizedProps = { - ...defaultProps, - onQueryChange: onQueryChangeMock, - }; - - const component = mountWithI18nProvider(
); - const searchBar = findTestSubject(component, 'savedObjectSearchBar'); - - // Send invalid query - searchBar.simulate('keyup', { keyCode: keyCodes.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' } }); - expect(onQueryChangeMock).toHaveBeenCalledTimes(1); - expect(component.state().isSearchTextValid).toBe(true); - }); - - it(`prevents saved objects from being deleted`, () => { - const selectedSavedObjects = [ - { type: 'visualization' }, - { type: 'search' }, - { type: 'index-pattern' }, - ]; - const customizedProps = { ...defaultProps, selectedSavedObjects, canDelete: false }; - const component = shallowWithI18nProvider(
); - - expect(component).toMatchSnapshot(); - }); -}); diff --git a/src/legacy/core_plugins/kibana/public/management/sections/objects/components/objects_table/components/table/index.js b/src/legacy/core_plugins/kibana/public/management/sections/objects/components/objects_table/components/table/index.js deleted file mode 100644 index e1195c6edfe31..0000000000000 --- a/src/legacy/core_plugins/kibana/public/management/sections/objects/components/objects_table/components/table/index.js +++ /dev/null @@ -1,20 +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. - */ - -export { Table } from './table'; diff --git a/src/legacy/core_plugins/kibana/public/management/sections/objects/components/objects_table/components/table/table.js b/src/legacy/core_plugins/kibana/public/management/sections/objects/components/objects_table/components/table/table.js deleted file mode 100644 index 132fa1e691c1c..0000000000000 --- a/src/legacy/core_plugins/kibana/public/management/sections/objects/components/objects_table/components/table/table.js +++ /dev/null @@ -1,389 +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 chrome from 'ui/chrome'; -import { npSetup } from 'ui/new_platform'; -import React, { PureComponent, Fragment } from 'react'; -import PropTypes from 'prop-types'; - -import { - EuiSearchBar, - EuiBasicTable, - EuiButton, - EuiIcon, - EuiLink, - EuiSpacer, - EuiToolTip, - EuiFormErrorText, - EuiPopover, - EuiSwitch, - EuiFormRow, - EuiText, -} from '@elastic/eui'; -import { getDefaultTitle, getSavedObjectLabel } from '../../../../lib'; -import { i18n } from '@kbn/i18n'; -import { FormattedMessage } from '@kbn/i18n/react'; - -export class Table extends PureComponent { - static propTypes = { - selectedSavedObjects: PropTypes.array.isRequired, - selectionConfig: PropTypes.shape({ - selectable: PropTypes.func, - selectableMessage: PropTypes.func, - onSelectionChange: PropTypes.func.isRequired, - }).isRequired, - filterOptions: PropTypes.array.isRequired, - canDelete: PropTypes.bool.isRequired, - onDelete: PropTypes.func.isRequired, - onExport: PropTypes.func.isRequired, - goInspectObject: PropTypes.func.isRequired, - - pageIndex: PropTypes.number.isRequired, - pageSize: PropTypes.number.isRequired, - items: PropTypes.array.isRequired, - itemId: PropTypes.oneOfType([ - PropTypes.string, // the name of the item id property - PropTypes.func, // (item) => string - ]), - totalItemCount: PropTypes.number.isRequired, - onQueryChange: PropTypes.func.isRequired, - onTableChange: PropTypes.func.isRequired, - isSearching: PropTypes.bool.isRequired, - - onShowRelationships: PropTypes.func.isRequired, - }; - - state = { - isSearchTextValid: true, - parseErrorMessage: null, - isExportPopoverOpen: false, - isIncludeReferencesDeepChecked: true, - activeAction: null, - }; - - constructor(props) { - super(props); - this.extraActions = npSetup.plugins.savedObjectsManagement.actionRegistry.getAll(); - } - - onChange = ({ query, error }) => { - if (error) { - this.setState({ - isSearchTextValid: false, - parseErrorMessage: error.message, - }); - return; - } - - this.setState({ - isSearchTextValid: true, - parseErrorMessage: null, - }); - this.props.onQueryChange({ query }); - }; - - closeExportPopover = () => { - this.setState({ isExportPopoverOpen: false }); - }; - - toggleExportPopoverVisibility = () => { - this.setState(state => ({ - isExportPopoverOpen: !state.isExportPopoverOpen, - })); - }; - - toggleIsIncludeReferencesDeepChecked = () => { - this.setState(state => ({ - isIncludeReferencesDeepChecked: !state.isIncludeReferencesDeepChecked, - })); - }; - - onExportClick = () => { - const { onExport } = this.props; - const { isIncludeReferencesDeepChecked } = this.state; - onExport(isIncludeReferencesDeepChecked); - this.setState({ isExportPopoverOpen: false }); - }; - - render() { - const { - pageIndex, - pageSize, - itemId, - items, - totalItemCount, - isSearching, - filterOptions, - selectionConfig: selection, - onDelete, - selectedSavedObjects, - onTableChange, - goInspectObject, - onShowRelationships, - } = this.props; - - const pagination = { - pageIndex: pageIndex, - pageSize: pageSize, - totalItemCount: totalItemCount, - pageSizeOptions: [5, 10, 20, 50], - }; - - const filters = [ - { - type: 'field_value_selection', - field: 'type', - name: i18n.translate('kbn.management.objects.objectsTable.table.typeFilterName', { - defaultMessage: 'Type', - }), - multiSelect: 'or', - options: filterOptions, - }, - // Add this back in once we have tag support - // { - // type: 'field_value_selection', - // field: 'tag', - // name: 'Tags', - // multiSelect: 'or', - // options: [], - // }, - ]; - - const columns = [ - { - field: 'type', - name: i18n.translate('kbn.management.objects.objectsTable.table.columnTypeName', { - defaultMessage: 'Type', - }), - width: '50px', - align: 'center', - description: i18n.translate( - 'kbn.management.objects.objectsTable.table.columnTypeDescription', - { defaultMessage: 'Type of the saved object' } - ), - sortable: false, - 'data-test-subj': 'savedObjectsTableRowType', - render: (type, object) => { - return ( - - - - ); - }, - }, - { - field: 'meta.title', - name: i18n.translate('kbn.management.objects.objectsTable.table.columnTitleName', { - defaultMessage: 'Title', - }), - description: i18n.translate( - 'kbn.management.objects.objectsTable.table.columnTitleDescription', - { defaultMessage: 'Title of the saved object' } - ), - dataType: 'string', - sortable: false, - 'data-test-subj': 'savedObjectsTableRowTitle', - render: (title, object) => { - const { path } = object.meta.inAppUrl || {}; - const canGoInApp = this.props.canGoInApp(object); - if (!canGoInApp) { - return {title || getDefaultTitle(object)}; - } - return ( - {title || getDefaultTitle(object)} - ); - }, - }, - { - name: i18n.translate('kbn.management.objects.objectsTable.table.columnActionsName', { - defaultMessage: 'Actions', - }), - actions: [ - { - name: i18n.translate( - 'kbn.management.objects.objectsTable.table.columnActions.inspectActionName', - { defaultMessage: 'Inspect' } - ), - description: i18n.translate( - 'kbn.management.objects.objectsTable.table.columnActions.inspectActionDescription', - { defaultMessage: 'Inspect this saved object' } - ), - type: 'icon', - icon: 'inspect', - onClick: object => goInspectObject(object), - available: object => !!object.meta.editUrl, - 'data-test-subj': 'savedObjectsTableAction-inspect', - }, - { - name: i18n.translate( - 'kbn.management.objects.objectsTable.table.columnActions.viewRelationshipsActionName', - { defaultMessage: 'Relationships' } - ), - description: i18n.translate( - 'kbn.management.objects.objectsTable.table.columnActions.viewRelationshipsActionDescription', - { - defaultMessage: - 'View the relationships this saved object has to other saved objects', - } - ), - type: 'icon', - icon: 'kqlSelector', - onClick: object => onShowRelationships(object), - 'data-test-subj': 'savedObjectsTableAction-relationships', - }, - ...this.extraActions.map(action => { - return { - ...action.euiAction, - 'data-test-subj': `savedObjectsTableAction-${action.id}`, - onClick: object => { - this.setState({ - activeAction: action, - }); - - action.registerOnFinishCallback(() => { - this.setState({ - activeAction: null, - }); - }); - - action.euiAction.onClick(object); - }, - }; - }), - ], - }, - ]; - - let queryParseError; - if (!this.state.isSearchTextValid) { - const parseErrorMsg = i18n.translate( - 'kbn.management.objects.objectsTable.searchBar.unableToParseQueryErrorMessage', - { defaultMessage: 'Unable to parse query' } - ); - queryParseError = ( - {`${parseErrorMsg}. ${this.state.parseErrorMessage}`} - ); - } - - const button = ( - - - - ); - - const activeActionContents = this.state.activeAction ? this.state.activeAction.render() : null; - - return ( - - {activeActionContents} - - - , - - - } - > - - } - checked={this.state.isIncludeReferencesDeepChecked} - onChange={this.toggleIsIncludeReferencesDeepChecked} - /> - - - - - - - , - ]} - /> - {queryParseError} - -
- ({ - 'data-test-subj': `savedObjectsTableRow row-${item.id}`, - })} - /> -
-
- ); - } -} diff --git a/src/legacy/core_plugins/kibana/public/management/sections/objects/components/objects_table/index.js b/src/legacy/core_plugins/kibana/public/management/sections/objects/components/objects_table/index.js deleted file mode 100644 index 601dea544361c..0000000000000 --- a/src/legacy/core_plugins/kibana/public/management/sections/objects/components/objects_table/index.js +++ /dev/null @@ -1,20 +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. - */ - -export { ObjectsTable } from './objects_table'; diff --git a/src/legacy/core_plugins/kibana/public/management/sections/objects/components/objects_table/objects_table.js b/src/legacy/core_plugins/kibana/public/management/sections/objects/components/objects_table/objects_table.js deleted file mode 100644 index 188762f165b24..0000000000000 --- a/src/legacy/core_plugins/kibana/public/management/sections/objects/components/objects_table/objects_table.js +++ /dev/null @@ -1,722 +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 chrome from 'ui/chrome'; -import { saveAs } from '@elastic/filesaver'; -import React, { Component } from 'react'; -import PropTypes from 'prop-types'; -import { debounce } from 'lodash'; -import { Header } from './components/header'; -import { Flyout } from './components/flyout'; -import { Relationships } from './components/relationships'; -import { Table } from './components/table'; -import { toastNotifications } from 'ui/notify'; - -import { - EuiSpacer, - Query, - EuiInMemoryTable, - EuiIcon, - EuiConfirmModal, - EuiLoadingKibana, - EuiOverlayMask, - EUI_MODAL_CONFIRM_BUTTON, - EuiCheckboxGroup, - EuiToolTip, - EuiPageContent, - EuiSwitch, - EuiModal, - EuiModalHeader, - EuiModalBody, - EuiModalFooter, - EuiButtonEmpty, - EuiButton, - EuiModalHeaderTitle, - EuiFormRow, - EuiFlexGroup, - EuiFlexItem, -} from '@elastic/eui'; -import { i18n } from '@kbn/i18n'; -import { FormattedMessage } from '@kbn/i18n/react'; - -import { - parseQuery, - getSavedObjectCounts, - getRelationships, - getSavedObjectLabel, - fetchExportObjects, - fetchExportByTypeAndSearch, - findObjects, -} from '../../lib'; -import { extractExportDetails } from '../../lib/extract_export_details'; - -export const POSSIBLE_TYPES = chrome.getInjected('importAndExportableTypes'); - -export class ObjectsTable extends Component { - static propTypes = { - savedObjectsClient: PropTypes.object.isRequired, - indexPatterns: PropTypes.object.isRequired, - $http: PropTypes.func.isRequired, - basePath: PropTypes.string.isRequired, - perPageConfig: PropTypes.number, - newIndexPatternUrl: PropTypes.string.isRequired, - confirmModalPromise: PropTypes.func.isRequired, - services: PropTypes.array.isRequired, - uiCapabilities: PropTypes.object.isRequired, - goInspectObject: PropTypes.func.isRequired, - canGoInApp: PropTypes.func.isRequired, - }; - - constructor(props) { - super(props); - this.savedObjectTypes = POSSIBLE_TYPES; - - this.state = { - totalCount: 0, - page: 0, - perPage: props.perPageConfig || 50, - savedObjects: [], - savedObjectCounts: this.savedObjectTypes.reduce((typeToCountMap, type) => { - typeToCountMap[type] = 0; - return typeToCountMap; - }, {}), - activeQuery: Query.parse(''), - selectedSavedObjects: [], - isShowingImportFlyout: false, - isSearching: false, - filteredItemCount: 0, - isShowingRelationships: false, - relationshipObject: undefined, - isShowingDeleteConfirmModal: false, - isShowingExportAllOptionsModal: false, - isDeleting: false, - exportAllOptions: [], - exportAllSelectedOptions: {}, - isIncludeReferencesDeepChecked: true, - }; - } - - componentDidMount() { - this._isMounted = true; - this.fetchSavedObjects(); - this.fetchCounts(); - } - - componentWillUnmount() { - this._isMounted = false; - this.debouncedFetch.cancel(); - } - - fetchCounts = async () => { - const { queryText, visibleTypes } = parseQuery(this.state.activeQuery); - - const filteredTypes = this.savedObjectTypes.filter( - type => !visibleTypes || visibleTypes.includes(type) - ); - - // These are the saved objects visible in the table. - const filteredSavedObjectCounts = await getSavedObjectCounts( - this.props.$http, - filteredTypes, - queryText - ); - - const exportAllOptions = []; - const exportAllSelectedOptions = {}; - - Object.keys(filteredSavedObjectCounts).forEach(id => { - // Add this type as a bulk-export option. - exportAllOptions.push({ - id, - label: `${id} (${filteredSavedObjectCounts[id] || 0})`, - }); - - // Select it by defayult. - exportAllSelectedOptions[id] = true; - }); - - // Fetch all the saved objects that exist so we can accurately populate the counts within - // the table filter dropdown. - const savedObjectCounts = await getSavedObjectCounts( - this.props.$http, - this.savedObjectTypes, - queryText - ); - - this.setState(state => ({ - ...state, - savedObjectCounts, - exportAllOptions, - exportAllSelectedOptions, - })); - }; - - fetchSavedObjects = () => { - this.setState( - { - isSearching: true, - }, - this.debouncedFetch - ); - }; - - debouncedFetch = debounce(async () => { - const { activeQuery: query, page, perPage } = this.state; - const { queryText, visibleTypes } = parseQuery(query); - // "searchFields" is missing from the "findOptions" but gets injected via the API. - // The API extracts the fields from each uiExports.savedObjectsManagement "defaultSearchField" attribute - const findOptions = { - search: queryText ? `${queryText}*` : undefined, - perPage, - page: page + 1, - fields: ['id'], - type: this.savedObjectTypes.filter(type => !visibleTypes || visibleTypes.includes(type)), - }; - if (findOptions.type.length > 1) { - findOptions.sortField = 'type'; - } - - let resp; - try { - resp = await findObjects(findOptions); - } catch (error) { - if (this._isMounted) { - this.setState({ - isSearching: false, - }); - } - toastNotifications.addDanger({ - title: i18n.translate( - 'kbn.management.objects.objectsTable.unableFindSavedObjectsNotificationMessage', - { defaultMessage: 'Unable find saved objects' } - ), - text: `${error}`, - }); - return; - } - - if (!this._isMounted) { - return; - } - - this.setState(({ activeQuery }) => { - // ignore results for old requests - if (activeQuery.text !== query.text) { - return {}; - } - - return { - savedObjects: resp.savedObjects, - filteredItemCount: resp.total, - isSearching: false, - }; - }); - }, 300); - - refreshData = async () => { - await Promise.all([this.fetchSavedObjects(), this.fetchCounts()]); - }; - - onSelectionChanged = selection => { - this.setState({ selectedSavedObjects: selection }); - }; - - onQueryChange = ({ query }) => { - // TODO: Use isSameQuery to compare new query with state.activeQuery to avoid re-fetching the - // same data we already have. - this.setState( - { - activeQuery: query, - page: 0, // Reset this on each query change - selectedSavedObjects: [], - }, - () => { - this.fetchSavedObjects(); - this.fetchCounts(); - } - ); - }; - - onTableChange = async table => { - const { index: page, size: perPage } = table.page || {}; - - this.setState( - { - page, - perPage, - selectedSavedObjects: [], - }, - this.fetchSavedObjects - ); - }; - - onShowRelationships = object => { - this.setState({ - isShowingRelationships: true, - relationshipObject: object, - }); - }; - - onHideRelationships = () => { - this.setState({ - isShowingRelationships: false, - relationshipObject: undefined, - }); - }; - - onExport = async includeReferencesDeep => { - const { selectedSavedObjects } = this.state; - const objectsToExport = selectedSavedObjects.map(obj => ({ id: obj.id, type: obj.type })); - - let blob; - try { - blob = await fetchExportObjects(objectsToExport, includeReferencesDeep); - } catch (e) { - toastNotifications.addDanger({ - title: i18n.translate('kbn.management.objects.objectsTable.export.dangerNotification', { - defaultMessage: 'Unable to generate export', - }), - }); - throw e; - } - - saveAs(blob, 'export.ndjson'); - - const exportDetails = await extractExportDetails(blob); - this.showExportSuccessMessage(exportDetails); - }; - - onExportAll = async () => { - const { exportAllSelectedOptions, isIncludeReferencesDeepChecked, activeQuery } = this.state; - const { queryText } = parseQuery(activeQuery); - const exportTypes = Object.entries(exportAllSelectedOptions).reduce((accum, [id, selected]) => { - if (selected) { - accum.push(id); - } - return accum; - }, []); - - let blob; - try { - blob = await fetchExportByTypeAndSearch( - exportTypes, - queryText ? `${queryText}*` : undefined, - isIncludeReferencesDeepChecked - ); - } catch (e) { - toastNotifications.addDanger({ - title: i18n.translate('kbn.management.objects.objectsTable.export.dangerNotification', { - defaultMessage: 'Unable to generate export', - }), - }); - throw e; - } - - saveAs(blob, 'export.ndjson'); - - const exportDetails = await extractExportDetails(blob); - this.showExportSuccessMessage(exportDetails); - this.setState({ isShowingExportAllOptionsModal: false }); - }; - - showExportSuccessMessage = exportDetails => { - if (exportDetails && exportDetails.missingReferences.length > 0) { - toastNotifications.addWarning({ - title: i18n.translate( - 'kbn.management.objects.objectsTable.export.successWithMissingRefsNotification', - { - defaultMessage: - 'Your file is downloading in the background. ' + - 'Some related objects could not be found. ' + - 'Please see the last line in the exported file for a list of missing objects.', - } - ), - }); - } else { - toastNotifications.addSuccess({ - title: i18n.translate('kbn.management.objects.objectsTable.export.successNotification', { - defaultMessage: 'Your file is downloading in the background', - }), - }); - } - }; - - finishImport = () => { - this.hideImportFlyout(); - this.fetchSavedObjects(); - this.fetchCounts(); - }; - - showImportFlyout = () => { - this.setState({ isShowingImportFlyout: true }); - }; - - hideImportFlyout = () => { - this.setState({ isShowingImportFlyout: false }); - }; - - onDelete = () => { - this.setState({ isShowingDeleteConfirmModal: true }); - }; - - delete = async () => { - const { savedObjectsClient } = this.props; - const { selectedSavedObjects, isDeleting } = this.state; - - if (isDeleting) { - return; - } - - this.setState({ isDeleting: true }); - - const indexPatterns = selectedSavedObjects.filter(object => object.type === 'index-pattern'); - if (indexPatterns.length) { - await this.props.indexPatterns.clearCache(); - } - - const objects = await savedObjectsClient.bulkGet(selectedSavedObjects); - const deletes = objects.savedObjects.map(object => - savedObjectsClient.delete(object.type, object.id) - ); - await Promise.all(deletes); - - // Unset this - this.setState({ - selectedSavedObjects: [], - }); - - // Fetching all data - await this.fetchSavedObjects(); - await this.fetchCounts(); - - // Allow the user to interact with the table once the saved objects have been re-fetched. - this.setState({ - isShowingDeleteConfirmModal: false, - isDeleting: false, - }); - }; - - getRelationships = async (type, id) => { - return await getRelationships( - type, - id, - this.savedObjectTypes, - this.props.$http, - this.props.basePath - ); - }; - - renderFlyout() { - if (!this.state.isShowingImportFlyout) { - return null; - } - - return ( - - ); - } - - renderRelationships() { - if (!this.state.isShowingRelationships) { - return null; - } - - return ( - - ); - } - - renderDeleteConfirmModal() { - const { isShowingDeleteConfirmModal, isDeleting, selectedSavedObjects } = this.state; - - if (!isShowingDeleteConfirmModal) { - return null; - } - - let modal; - - if (isDeleting) { - // Block the user from interacting with the table while its contents are being deleted. - modal = ; - } else { - const onCancel = () => { - this.setState({ isShowingDeleteConfirmModal: false }); - }; - - const onConfirm = () => { - this.delete(); - }; - - modal = ( - - } - onCancel={onCancel} - onConfirm={onConfirm} - buttonColor="danger" - cancelButtonText={ - - } - confirmButtonText={ - isDeleting ? ( - - ) : ( - - ) - } - defaultFocusedButton={EUI_MODAL_CONFIRM_BUTTON} - > -

- -

- ( - - - - ), - }, - { - field: 'id', - name: i18n.translate( - 'kbn.management.objects.objectsTable.deleteSavedObjectsConfirmModal.idColumnName', - { defaultMessage: 'Id' } - ), - }, - { - field: 'meta.title', - name: i18n.translate( - 'kbn.management.objects.objectsTable.deleteSavedObjectsConfirmModal.titleColumnName', - { defaultMessage: 'Title' } - ), - }, - ]} - pagination={true} - sorting={false} - /> -
- ); - } - - return {modal}; - } - - changeIncludeReferencesDeep = () => { - this.setState(state => ({ - isIncludeReferencesDeepChecked: !state.isIncludeReferencesDeepChecked, - })); - }; - - closeExportAllModal = () => { - this.setState({ isShowingExportAllOptionsModal: false }); - }; - - renderExportAllOptionsModal() { - const { - isShowingExportAllOptionsModal, - filteredItemCount, - exportAllOptions, - exportAllSelectedOptions, - isIncludeReferencesDeepChecked, - } = this.state; - - if (!isShowingExportAllOptionsModal) { - return null; - } - - return ( - - - - - - - - - - } - labelType="legend" - > - { - const newExportAllSelectedOptions = { - ...exportAllSelectedOptions, - ...{ - [optionId]: !exportAllSelectedOptions[optionId], - }, - }; - - this.setState({ - exportAllSelectedOptions: newExportAllSelectedOptions, - }); - }} - /> - - - - } - checked={isIncludeReferencesDeepChecked} - onChange={this.changeIncludeReferencesDeep} - /> - - - - - - - - - - - - - - - - - - - - - - ); - } - - render() { - const { - selectedSavedObjects, - page, - perPage, - savedObjects, - filteredItemCount, - isSearching, - savedObjectCounts, - } = this.state; - - const selectionConfig = { - onSelectionChange: this.onSelectionChanged, - }; - - const filterOptions = this.savedObjectTypes.map(type => ({ - value: type, - name: type, - view: `${type} (${savedObjectCounts[type] || 0})`, - })); - - return ( - - {this.renderFlyout()} - {this.renderRelationships()} - {this.renderDeleteConfirmModal()} - {this.renderExportAllOptionsModal()} -
this.setState({ isShowingExportAllOptionsModal: true })} - onImport={this.showImportFlyout} - onRefresh={this.refreshData} - filteredCount={filteredItemCount} - /> - -
- - ); - } -} diff --git a/src/legacy/core_plugins/kibana/public/management/sections/objects/index.js b/src/legacy/core_plugins/kibana/public/management/sections/objects/index.js deleted file mode 100644 index 3965c42ac088d..0000000000000 --- a/src/legacy/core_plugins/kibana/public/management/sections/objects/index.js +++ /dev/null @@ -1,36 +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 { i18n } from '@kbn/i18n'; -import { management } from 'ui/management'; -import './_view'; -import './_objects'; -import 'ace'; -import { uiModules } from 'ui/modules'; - -// add the module deps to this module -uiModules.get('apps/management'); - -management.getSection('kibana').register('objects', { - display: i18n.translate('kbn.management.objects.savedObjectsSectionLabel', { - defaultMessage: 'Saved Objects', - }), - order: 10, - url: '#/management/kibana/objects', -}); diff --git a/src/legacy/core_plugins/kibana/public/management/sections/objects/lib/find_objects.ts b/src/legacy/core_plugins/kibana/public/management/sections/objects/lib/find_objects.ts deleted file mode 100644 index 24e08f0524f62..0000000000000 --- a/src/legacy/core_plugins/kibana/public/management/sections/objects/lib/find_objects.ts +++ /dev/null @@ -1,32 +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 { kfetch } from 'ui/kfetch'; -import { SavedObjectsFindOptions } from 'src/core/public'; -import { keysToCamelCaseShallow } from './case_conversion'; - -export async function findObjects(findOptions: SavedObjectsFindOptions) { - const response = await kfetch({ - method: 'GET', - pathname: '/api/kibana/management/saved_objects/_find', - query: findOptions as Record, - }); - - return keysToCamelCaseShallow(response); -} diff --git a/src/legacy/core_plugins/kibana/public/management/sections/objects/lib/get_relationships.test.ts b/src/legacy/core_plugins/kibana/public/management/sections/objects/lib/get_relationships.test.ts deleted file mode 100644 index b45b51b4de293..0000000000000 --- a/src/legacy/core_plugins/kibana/public/management/sections/objects/lib/get_relationships.test.ts +++ /dev/null @@ -1,60 +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 { getRelationships } from './get_relationships'; - -describe('getRelationships', () => { - it('should make an http request', async () => { - const $http = jest.fn() as any; - const basePath = 'test'; - - await getRelationships('dashboard', '1', ['search', 'index-pattern'], $http, basePath); - expect($http.mock.calls.length).toBe(1); - }); - - it('should handle successful responses', async () => { - const $http = jest.fn().mockImplementation(() => ({ data: [1, 2] })) as any; - const basePath = 'test'; - - const response = await getRelationships( - 'dashboard', - '1', - ['search', 'index-pattern'], - $http, - basePath - ); - expect(response).toEqual([1, 2]); - }); - - it('should handle errors', async () => { - const $http = jest.fn().mockImplementation(() => { - const err = new Error(); - (err as any).data = { - error: 'Test error', - statusCode: 500, - }; - throw err; - }) as any; - const basePath = 'test'; - - await expect( - getRelationships('dashboard', '1', ['search', 'index-pattern'], $http, basePath) - ).rejects.toThrowErrorMatchingInlineSnapshot(`"Test error"`); - }); -}); diff --git a/src/legacy/core_plugins/kibana/public/management/sections/objects/lib/get_relationships.ts b/src/legacy/core_plugins/kibana/public/management/sections/objects/lib/get_relationships.ts deleted file mode 100644 index 07bdf2db68fa2..0000000000000 --- a/src/legacy/core_plugins/kibana/public/management/sections/objects/lib/get_relationships.ts +++ /dev/null @@ -1,54 +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 { IHttpService } from 'angular'; -import { get } from 'lodash'; -import { SavedObjectRelation } from '../types'; - -export async function getRelationships( - type: string, - id: string, - savedObjectTypes: string[], - $http: IHttpService, - basePath: string -): Promise { - const url = `${basePath}/api/kibana/management/saved_objects/relationships/${encodeURIComponent( - type - )}/${encodeURIComponent(id)}`; - const options = { - method: 'GET', - url, - params: { - savedObjectTypes, - }, - }; - - try { - const response = await $http(options); - return response?.data; - } catch (resp) { - const respBody = get(resp, 'data', {}) as any; - const err = new Error(respBody.message || respBody.error || `${resp.status} Response`); - - (err as any).statusCode = respBody.statusCode || resp.status; - (err as any).body = respBody; - - throw err; - } -} diff --git a/src/legacy/core_plugins/kibana/public/management/sections/objects/lib/get_saved_object_counts.ts b/src/legacy/core_plugins/kibana/public/management/sections/objects/lib/get_saved_object_counts.ts deleted file mode 100644 index d4dda1190bc43..0000000000000 --- a/src/legacy/core_plugins/kibana/public/management/sections/objects/lib/get_saved_object_counts.ts +++ /dev/null @@ -1,34 +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 { IHttpService } from 'angular'; -import chrome from 'ui/chrome'; - -const apiBase = chrome.addBasePath('/api/kibana/management/saved_objects/scroll'); -export async function getSavedObjectCounts( - $http: IHttpService, - typesToInclude: string[], - searchString: string -): Promise> { - const results = await $http.post>(`${apiBase}/counts`, { - typesToInclude, - searchString, - }); - return results.data; -} diff --git a/src/legacy/core_plugins/kibana/public/management/sections/objects/lib/index.ts b/src/legacy/core_plugins/kibana/public/management/sections/objects/lib/index.ts deleted file mode 100644 index ecdfa6549a54e..0000000000000 --- a/src/legacy/core_plugins/kibana/public/management/sections/objects/lib/index.ts +++ /dev/null @@ -1,45 +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. - */ - -export { fetchExportByTypeAndSearch } from './fetch_export_by_type_and_search'; -export { fetchExportObjects } from './fetch_export_objects'; -export { canViewInApp } from './in_app_url'; -export { getRelationships } from './get_relationships'; -export { getSavedObjectCounts } from './get_saved_object_counts'; -export { getSavedObjectLabel } from './get_saved_object_label'; -export { importFile } from './import_file'; -export { importLegacyFile } from './import_legacy_file'; -export { parseQuery } from './parse_query'; -export { resolveImportErrors } from './resolve_import_errors'; -export { - resolveIndexPatternConflicts, - resolveSavedObjects, - resolveSavedSearches, - saveObject, - saveObjects, -} from './resolve_saved_objects'; -export { logLegacyImport } from './log_legacy_import'; -export { - processImportResponse, - ProcessedImportResponse, - FailedImport, -} from './process_import_response'; -export { getDefaultTitle } from './get_default_title'; -export { findObjects } from './find_objects'; -export { extractExportDetails, SavedObjectsExportResultDetails } from './extract_export_details'; diff --git a/src/legacy/core_plugins/kibana/public/management/sections/objects/types.ts b/src/legacy/core_plugins/kibana/public/management/sections/objects/types.ts deleted file mode 100644 index 6a89142bc9798..0000000000000 --- a/src/legacy/core_plugins/kibana/public/management/sections/objects/types.ts +++ /dev/null @@ -1,56 +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 { SavedObject, SavedObjectReference } from 'src/core/public'; - -export interface SavedObjectMetadata { - icon?: string; - title?: string; - editUrl?: string; - inAppUrl?: { path: string; uiCapabilitiesPath: string }; -} - -export type SavedObjectWithMetadata = SavedObject & { - meta: SavedObjectMetadata; -}; - -export interface SavedObjectRelation { - id: string; - type: string; - relationship: 'child' | 'parent'; - meta: SavedObjectMetadata; -} - -export interface ObjectField { - type: FieldType; - name: string; - value: any; -} - -export type FieldType = 'text' | 'number' | 'boolean' | 'array' | 'json'; - -export interface FieldState { - value?: any; - invalid?: boolean; -} - -export interface SubmittedFormData { - attributes: any; - references: SavedObjectReference[]; -} diff --git a/src/legacy/core_plugins/kibana/public/visualize/_index.scss b/src/legacy/core_plugins/kibana/public/visualize/_index.scss deleted file mode 100644 index 079d82936bb57..0000000000000 --- a/src/legacy/core_plugins/kibana/public/visualize/_index.scss +++ /dev/null @@ -1,5 +0,0 @@ -// Visualize plugin styles -@import 'np_ready/index'; - -// should be removed while moving the visualize into NP -@import '../../../../../plugins/vis_default_editor/public/index' diff --git a/src/legacy/core_plugins/kibana/public/visualize/index.ts b/src/legacy/core_plugins/kibana/public/visualize/index.ts deleted file mode 100644 index c3ae39d9fde25..0000000000000 --- a/src/legacy/core_plugins/kibana/public/visualize/index.ts +++ /dev/null @@ -1,29 +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 { PluginInitializerContext } from 'kibana/public'; -import { VisualizePlugin } from './plugin'; - -export * from './np_ready/visualize_constants'; -export { VisualizeConstants, createVisualizeEditUrl } from './np_ready/visualize_constants'; - -// Core will be looking for this when loading our plugin in the new platform -export const plugin = (context: PluginInitializerContext) => { - return new VisualizePlugin(context); -}; diff --git a/src/legacy/core_plugins/kibana/public/visualize/kibana_services.ts b/src/legacy/core_plugins/kibana/public/visualize/kibana_services.ts deleted file mode 100644 index d5440c4677d8a..0000000000000 --- a/src/legacy/core_plugins/kibana/public/visualize/kibana_services.ts +++ /dev/null @@ -1,83 +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 { - ChromeStart, - CoreStart, - SavedObjectsClientContract, - ToastsStart, - IUiSettingsClient, - I18nStart, - PluginInitializerContext, -} from 'kibana/public'; - -import { NavigationPublicPluginStart as NavigationStart } from '../../../../../plugins/navigation/public'; -import { Storage } from '../../../../../plugins/kibana_utils/public'; -import { EmbeddableStart } from '../../../../../plugins/embeddable/public'; -import { SharePluginStart } from '../../../../../plugins/share/public'; -import { DataPublicPluginStart, IndexPatternsContract } from '../../../../../plugins/data/public'; -import { VisualizationsStart } from '../../../../../plugins/visualizations/public'; -import { SavedVisualizations } from './np_ready/types'; -import { UsageCollectionSetup } from '../../../../../plugins/usage_collection/public'; -import { KibanaLegacyStart } from '../../../../../plugins/kibana_legacy/public'; -import { DefaultEditorController } from '../../../../../plugins/vis_default_editor/public'; - -export interface VisualizeKibanaServices { - pluginInitializerContext: PluginInitializerContext; - addBasePath: (url: string) => string; - chrome: ChromeStart; - core: CoreStart; - data: DataPublicPluginStart; - embeddable: EmbeddableStart; - indexPatterns: IndexPatternsContract; - localStorage: Storage; - navigation: NavigationStart; - toastNotifications: ToastsStart; - savedObjectsClient: SavedObjectsClientContract; - savedQueryService: DataPublicPluginStart['query']['savedQueries']; - savedVisualizations: SavedVisualizations; - share: SharePluginStart; - uiSettings: IUiSettingsClient; - config: KibanaLegacyStart['config']; - visualizeCapabilities: any; - visualizations: VisualizationsStart; - usageCollection?: UsageCollectionSetup; - I18nContext: I18nStart['Context']; - setActiveUrl: (newUrl: string) => void; - DefaultVisualizationEditor: typeof DefaultEditorController; - createVisEmbeddableFromObject: VisualizationsStart['__LEGACY']['createVisEmbeddableFromObject']; -} - -let services: VisualizeKibanaServices | null = null; -export function setServices(newServices: VisualizeKibanaServices) { - services = newServices; -} - -export function getServices() { - if (!services) { - throw new Error( - 'Kibana services not set - are you trying to import this module from outside of the visualize app?' - ); - } - return services; -} - -export function clearServices() { - services = null; -} diff --git a/src/legacy/core_plugins/kibana/public/visualize/legacy.ts b/src/legacy/core_plugins/kibana/public/visualize/legacy.ts deleted file mode 100644 index 4ef2c93689714..0000000000000 --- a/src/legacy/core_plugins/kibana/public/visualize/legacy.ts +++ /dev/null @@ -1,28 +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 { PluginInitializerContext } from 'kibana/public'; -import { npSetup, npStart } from 'ui/new_platform'; -import { plugin } from './index'; - -const instance = plugin({ - env: npSetup.plugins.kibanaLegacy.env, -} as PluginInitializerContext); -instance.setup(npSetup.core, npSetup.plugins); -instance.start(npStart.core, npStart.plugins); diff --git a/src/legacy/core_plugins/kibana/public/visualize/legacy_imports.ts b/src/legacy/core_plugins/kibana/public/visualize/legacy_imports.ts deleted file mode 100644 index f6d73b987912d..0000000000000 --- a/src/legacy/core_plugins/kibana/public/visualize/legacy_imports.ts +++ /dev/null @@ -1,36 +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. - */ - -/** - * The imports in this file are static functions and types which still live in legacy folders and are used - * within dashboard. To consolidate them all in one place, they are re-exported from this file. Eventually - * this list should become empty. Imports from the top level of shimmed or moved plugins can be imported - * directly where they are needed. - */ - -export { DashboardConstants } from '../../../../../plugins/dashboard/public'; -export { - VisSavedObject, - VISUALIZE_EMBEDDABLE_TYPE, -} from '../../../../../plugins/visualizations/public/'; -export { - configureAppAngularModule, - migrateLegacyQuery, - subscribeWithScope, -} from '../../../../../plugins/kibana_legacy/public'; diff --git a/src/legacy/core_plugins/kibana/public/visualize/np_ready/editor/lib/url_helper.test.ts b/src/legacy/core_plugins/kibana/public/visualize/np_ready/editor/lib/url_helper.test.ts deleted file mode 100644 index e6974af9af832..0000000000000 --- a/src/legacy/core_plugins/kibana/public/visualize/np_ready/editor/lib/url_helper.test.ts +++ /dev/null @@ -1,48 +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 { addEmbeddableToDashboardUrl } from './url_helper'; - -jest.mock('../../../legacy_imports', () => ({ - DashboardConstants: { - ADD_EMBEDDABLE_ID: 'addEmbeddableId', - ADD_EMBEDDABLE_TYPE: 'addEmbeddableType', - CREATE_NEW_DASHBOARD_URL: '/dashboard', - }, - VISUALIZE_EMBEDDABLE_TYPE: 'visualization', -})); - -describe('', () => { - it('addEmbeddableToDashboardUrl when dashboard is not saved', () => { - const id = '123eb456cd'; - const url = - "/pep/app/kibana#/dashboard?_g=(refreshInterval:(pause:!t,value:0),time:(from:now-15m,to:now))&_a=(description:'',filters:!())"; - expect(addEmbeddableToDashboardUrl(url, id)).toEqual( - `/dashboard?_a=%28description%3A%27%27%2Cfilters%3A%21%28%29%29&_g=%28refreshInterval%3A%28pause%3A%21t%2Cvalue%3A0%29%2Ctime%3A%28from%3Anow-15m%2Cto%3Anow%29%29&addEmbeddableId=${id}&addEmbeddableType=visualization` - ); - }); - it('addEmbeddableToDashboardUrl when dashboard is saved', () => { - const id = '123eb456cd'; - const url = - "/pep/app/kibana#/dashboard/9b780cd0-3dd3-11e8-b2b9-5d5dc1715159?_g=(refreshInterval:(pause:!t,value:0),time:(from:now-15m,to:now))&_a=(description:'',filters:!())"; - expect(addEmbeddableToDashboardUrl(url, id)).toEqual( - `/dashboard/9b780cd0-3dd3-11e8-b2b9-5d5dc1715159?_a=%28description%3A%27%27%2Cfilters%3A%21%28%29%29&_g=%28refreshInterval%3A%28pause%3A%21t%2Cvalue%3A0%29%2Ctime%3A%28from%3Anow-15m%2Cto%3Anow%29%29&addEmbeddableId=${id}&addEmbeddableType=visualization` - ); - }); -}); diff --git a/src/legacy/core_plugins/kibana/public/visualize/np_ready/editor/lib/url_helper.ts b/src/legacy/core_plugins/kibana/public/visualize/np_ready/editor/lib/url_helper.ts deleted file mode 100644 index c7937c856184a..0000000000000 --- a/src/legacy/core_plugins/kibana/public/visualize/np_ready/editor/lib/url_helper.ts +++ /dev/null @@ -1,39 +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 { parseUrl, stringify } from 'query-string'; -import { DashboardConstants, VISUALIZE_EMBEDDABLE_TYPE } from '../../../legacy_imports'; - -/** * - * Returns relative dashboard URL with added embeddableType and embeddableId query params - * eg. - * input: url: lol/app/kibana#/dashboard?_g=(refreshInterval:(pause:!t,value:0),time:(from:now-15m,to:now)), embeddableId: 12345 - * output: /dashboard?addEmbeddableType=visualization&addEmbeddableId=12345&_g=(refreshInterval:(pause:!t,value:0),time:(from:now-15m,to:now)) - * @param url dasbhoard absolute url - * @param embeddableId id of the saved visualization - */ -export function addEmbeddableToDashboardUrl(dashboardUrl: string, embeddableId: string) { - const { url, query } = parseUrl(dashboardUrl); - const [, dashboardId] = url.split(DashboardConstants.CREATE_NEW_DASHBOARD_URL); - - query[DashboardConstants.ADD_EMBEDDABLE_TYPE] = VISUALIZE_EMBEDDABLE_TYPE; - query[DashboardConstants.ADD_EMBEDDABLE_ID] = embeddableId; - - return `${DashboardConstants.CREATE_NEW_DASHBOARD_URL}${dashboardId}?${stringify(query)}`; -} diff --git a/src/legacy/core_plugins/kibana/public/visualize/np_ready/types.d.ts b/src/legacy/core_plugins/kibana/public/visualize/np_ready/types.d.ts deleted file mode 100644 index e376b4f2bbacf..0000000000000 --- a/src/legacy/core_plugins/kibana/public/visualize/np_ready/types.d.ts +++ /dev/null @@ -1,78 +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 { - TimeRange, - Query, - Filter, - DataPublicPluginStart, - SavedQuery, -} from 'src/plugins/data/public'; -import { EmbeddableStart } from 'src/plugins/embeddable/public'; -import { PersistedState } from 'src/plugins/visualizations/public'; -import { LegacyCoreStart } from 'kibana/public'; -import { VisSavedObject } from '../legacy_imports'; -import { SavedVisState } from '../../../../../../plugins/visualizations/public'; -import { SavedSearch } from '../../../../../../plugins/discover/public'; - -export type PureVisState = SavedVisState; - -export interface VisualizeAppState { - filters: Filter[]; - uiState: PersistedState; - vis: PureVisState; - query: Query; - savedQuery?: string; - linked: boolean; -} - -export interface VisualizeAppStateTransitions { - set: ( - state: VisualizeAppState - ) => ( - prop: T, - value: VisualizeAppState[T] - ) => VisualizeAppState; - setVis: (state: VisualizeAppState) => (vis: Partial) => VisualizeAppState; - removeSavedQuery: (state: VisualizeAppState) => (defaultQuery: Query) => VisualizeAppState; - unlinkSavedSearch: ( - state: VisualizeAppState - ) => ({ query, parentFilters }: { query?: Query; parentFilters?: Filter[] }) => VisualizeAppState; - updateVisState: (state: VisualizeAppState) => (vis: PureVisState) => VisualizeAppState; - updateFromSavedQuery: (state: VisualizeAppState) => (savedQuery: SavedQuery) => VisualizeAppState; -} - -export interface EditorRenderProps { - core: LegacyCoreStart; - data: DataPublicPluginStart; - filters: Filter[]; - timeRange: TimeRange; - query?: Query; - savedSearch?: SavedSearch; - uiState: PersistedState; - /** - * Flag to determine if visualiztion is linked to the saved search - */ - linked: boolean; -} - -export interface SavedVisualizations { - urlFor: (id: string) => string; - get: (id: string) => Promise; -} diff --git a/src/legacy/core_plugins/kibana/public/visualize/plugin.ts b/src/legacy/core_plugins/kibana/public/visualize/plugin.ts deleted file mode 100644 index 4ffbc307c69a8..0000000000000 --- a/src/legacy/core_plugins/kibana/public/visualize/plugin.ts +++ /dev/null @@ -1,205 +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 { BehaviorSubject } from 'rxjs'; -import { i18n } from '@kbn/i18n'; -import { filter, map } from 'rxjs/operators'; - -import { - AppMountParameters, - CoreSetup, - CoreStart, - Plugin, - PluginInitializerContext, - SavedObjectsClientContract, -} from 'kibana/public'; - -import { Storage, createKbnUrlTracker } from '../../../../../plugins/kibana_utils/public'; -import { - DataPublicPluginStart, - DataPublicPluginSetup, - esFilters, -} from '../../../../../plugins/data/public'; -import { EmbeddableStart } from '../../../../../plugins/embeddable/public'; -import { NavigationPublicPluginStart as NavigationStart } from '../../../../../plugins/navigation/public'; -import { SharePluginStart } from '../../../../../plugins/share/public'; -import { - KibanaLegacySetup, - AngularRenderedAppUpdater, -} from '../../../../../plugins/kibana_legacy/public'; -import { VisualizationsStart } from '../../../../../plugins/visualizations/public'; -import { VisualizeConstants } from './np_ready/visualize_constants'; -import { setServices, VisualizeKibanaServices } from './kibana_services'; -import { - FeatureCatalogueCategory, - HomePublicPluginSetup, -} from '../../../../../plugins/home/public'; -import { UsageCollectionSetup } from '../../../../../plugins/usage_collection/public'; -import { DefaultEditorController } from '../../../../../plugins/vis_default_editor/public'; - -export interface VisualizePluginStartDependencies { - data: DataPublicPluginStart; - embeddable: EmbeddableStart; - navigation: NavigationStart; - share: SharePluginStart; - visualizations: VisualizationsStart; -} - -export interface VisualizePluginSetupDependencies { - home: HomePublicPluginSetup; - kibanaLegacy: KibanaLegacySetup; - usageCollection?: UsageCollectionSetup; - data: DataPublicPluginSetup; -} - -export class VisualizePlugin implements Plugin { - private startDependencies: { - data: DataPublicPluginStart; - embeddable: EmbeddableStart; - navigation: NavigationStart; - savedObjectsClient: SavedObjectsClientContract; - share: SharePluginStart; - visualizations: VisualizationsStart; - } | null = null; - private appStateUpdater = new BehaviorSubject(() => ({})); - private stopUrlTracking: (() => void) | undefined = undefined; - - constructor(private initializerContext: PluginInitializerContext) {} - - public async setup( - core: CoreSetup, - { home, kibanaLegacy, usageCollection, data }: VisualizePluginSetupDependencies - ) { - const { appMounted, appUnMounted, stop: stopUrlTracker, setActiveUrl } = createKbnUrlTracker({ - baseUrl: core.http.basePath.prepend('/app/kibana'), - defaultSubUrl: '#/visualize', - storageKey: `lastUrl:${core.http.basePath.get()}:visualize`, - navLinkUpdater$: this.appStateUpdater, - toastNotifications: core.notifications.toasts, - stateParams: [ - { - kbnUrlKey: '_g', - stateUpdate$: data.query.state$.pipe( - filter( - ({ changes }) => !!(changes.globalFilters || changes.time || changes.refreshInterval) - ), - map(({ state }) => ({ - ...state, - filters: state.filters?.filter(esFilters.isFilterPinned), - })) - ), - }, - ], - }); - this.stopUrlTracking = () => { - stopUrlTracker(); - }; - - kibanaLegacy.registerLegacyApp({ - id: 'visualize', - title: 'Visualize', - updater$: this.appStateUpdater.asObservable(), - navLinkId: 'kibana:visualize', - mount: async (params: AppMountParameters) => { - const [coreStart] = await core.getStartServices(); - - if (this.startDependencies === null) { - throw new Error('not started yet'); - } - - appMounted(); - const { - savedObjectsClient, - embeddable, - navigation, - visualizations, - data: dataStart, - share, - } = this.startDependencies; - - const deps: VisualizeKibanaServices = { - pluginInitializerContext: this.initializerContext, - addBasePath: coreStart.http.basePath.prepend, - core: coreStart, - chrome: coreStart.chrome, - data: dataStart, - embeddable, - indexPatterns: dataStart.indexPatterns, - localStorage: new Storage(localStorage), - navigation, - savedObjectsClient, - savedVisualizations: visualizations.savedVisualizationsLoader, - savedQueryService: dataStart.query.savedQueries, - share, - toastNotifications: coreStart.notifications.toasts, - uiSettings: coreStart.uiSettings, - config: kibanaLegacy.config, - visualizeCapabilities: coreStart.application.capabilities.visualize, - visualizations, - usageCollection, - I18nContext: coreStart.i18n.Context, - setActiveUrl, - DefaultVisualizationEditor: DefaultEditorController, - createVisEmbeddableFromObject: visualizations.__LEGACY.createVisEmbeddableFromObject, - }; - setServices(deps); - - const { renderApp } = await import('./np_ready/application'); - const unmount = renderApp(params.element, params.appBasePath, deps); - return () => { - unmount(); - appUnMounted(); - }; - }, - }); - - home.featureCatalogue.register({ - id: 'visualize', - title: 'Visualize', - description: i18n.translate('kbn.visualize.visualizeDescription', { - defaultMessage: - 'Create visualizations and aggregate data stores in your Elasticsearch indices.', - }), - icon: 'visualizeApp', - path: `/app/kibana#${VisualizeConstants.LANDING_PAGE_PATH}`, - showOnHomePage: true, - category: FeatureCatalogueCategory.DATA, - }); - } - - public start( - core: CoreStart, - { embeddable, navigation, data, share, visualizations }: VisualizePluginStartDependencies - ) { - this.startDependencies = { - data, - embeddable, - navigation, - savedObjectsClient: core.savedObjects.client, - share, - visualizations, - }; - } - - stop() { - if (this.stopUrlTracking) { - this.stopUrlTracking(); - } - } -} diff --git a/src/legacy/core_plugins/region_map/public/__tests__/region_map_visualization.js b/src/legacy/core_plugins/region_map/public/__tests__/region_map_visualization.js index 3880f42d52561..6e1b0b7160941 100644 --- a/src/legacy/core_plugins/region_map/public/__tests__/region_map_visualization.js +++ b/src/legacy/core_plugins/region_map/public/__tests__/region_map_visualization.js @@ -23,12 +23,18 @@ import _ from 'lodash'; import ChoroplethLayer from '../choropleth_layer'; import { ImageComparator } from 'test_utils/image_comparator'; import worldJson from './world.json'; -import EMS_CATALOGUE from '../../../../ui/public/vis/__tests__/map/ems_mocks/sample_manifest.json'; -import EMS_FILES from '../../../../ui/public/vis/__tests__/map/ems_mocks/sample_files.json'; -import EMS_TILES from '../../../../ui/public/vis/__tests__/map/ems_mocks/sample_tiles.json'; -import EMS_STYLE_ROAD_MAP_BRIGHT from '../../../../ui/public/vis/__tests__/map/ems_mocks/sample_style_bright'; -import EMS_STYLE_ROAD_MAP_DESATURATED from '../../../../ui/public/vis/__tests__/map/ems_mocks/sample_style_desaturated'; -import EMS_STYLE_DARK_MAP from '../../../../ui/public/vis/__tests__/map/ems_mocks/sample_style_dark'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import EMS_CATALOGUE from '../../../../../plugins/maps_legacy/public/__tests__/map/ems_mocks/sample_manifest.json'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import EMS_FILES from '../../../../../plugins/maps_legacy/public/__tests__/map/ems_mocks/sample_files.json'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import EMS_TILES from '../../../../../plugins/maps_legacy/public/__tests__/map/ems_mocks/sample_tiles.json'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import EMS_STYLE_ROAD_MAP_BRIGHT from '../../../../../plugins/maps_legacy/public/__tests__/map/ems_mocks/sample_style_bright'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import EMS_STYLE_ROAD_MAP_DESATURATED from '../../../../../plugins/maps_legacy/public/__tests__/map/ems_mocks/sample_style_desaturated'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import EMS_STYLE_DARK_MAP from '../../../../../plugins/maps_legacy/public/__tests__/map/ems_mocks/sample_style_dark'; import initialPng from './initial.png'; import toiso3Png from './toiso3.png'; @@ -44,6 +50,10 @@ import { createRegionMapTypeDefinition } from '../region_map_type'; import { ExprVis } from '../../../../../plugins/visualizations/public/expressions/vis'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths import { BaseVisType } from '../../../../../plugins/visualizations/public/vis_types/base_vis_type'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { setInjectedVarFunc } from '../../../../../plugins/maps_legacy/public/kibana_services'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { ServiceSettings } from '../../../../../plugins/maps_legacy/public/map/service_settings'; const THRESHOLD = 0.45; const PIXEL_DIFF = 96; @@ -92,7 +102,31 @@ describe('RegionMapsVisualizationTests', function() { let getManifestStub; beforeEach( ngMock.inject((Private, $injector) => { - const serviceSettings = $injector.get('serviceSettings'); + setInjectedVarFunc(injectedVar => { + switch (injectedVar) { + case 'mapConfig': + return { + emsFileApiUrl: '', + emsTileApiUrl: '', + emsLandingPageUrl: '', + }; + case 'tilemapsConfig': + return { + deprecated: { + config: { + options: { + attribution: '123', + }, + }, + }, + }; + case 'version': + return '123'; + default: + return 'not found'; + } + }); + const serviceSettings = new ServiceSettings(); const uiSettings = $injector.get('config'); const regionmapsConfig = { includeElasticMapsService: true, diff --git a/src/legacy/core_plugins/region_map/public/choropleth_layer.js b/src/legacy/core_plugins/region_map/public/choropleth_layer.js index e637a217bfbc3..4ea9cc1f7bfbf 100644 --- a/src/legacy/core_plugins/region_map/public/choropleth_layer.js +++ b/src/legacy/core_plugins/region_map/public/choropleth_layer.js @@ -22,11 +22,9 @@ import L from 'leaflet'; import _ from 'lodash'; import d3 from 'd3'; import { i18n } from '@kbn/i18n'; -import { KibanaMapLayer } from 'ui/vis/map/kibana_map_layer'; import * as topojson from 'topojson-client'; import { toastNotifications } from 'ui/notify'; -import * as colorUtil from 'ui/vis/map/color_util'; - +import { colorUtil, KibanaMapLayer } from '../../../../plugins/maps_legacy/public'; import { truncatedColorMaps } from '../../../../plugins/charts/public'; const EMPTY_STYLE = { diff --git a/src/legacy/core_plugins/region_map/public/components/region_map_options.tsx b/src/legacy/core_plugins/region_map/public/components/region_map_options.tsx index 61cfbf00ded9e..31a27c4da7fcf 100644 --- a/src/legacy/core_plugins/region_map/public/components/region_map_options.tsx +++ b/src/legacy/core_plugins/region_map/public/components/region_map_options.tsx @@ -21,10 +21,17 @@ import React, { useCallback, useMemo } from 'react'; import { EuiIcon, EuiLink, EuiPanel, EuiSpacer, EuiText, EuiTitle } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; - -import { FileLayerField, VectorLayer, ServiceSettings } from 'ui/vis/map/service_settings'; import { VisOptionsProps } from 'src/plugins/vis_default_editor/public'; -import { NumberInputOption, SelectOption, SwitchOption } from '../../../vis_type_vislib/public'; +import { + FileLayerField, + VectorLayer, + IServiceSettings, +} from '../../../../../plugins/maps_legacy/public'; +import { + NumberInputOption, + SelectOption, + SwitchOption, +} from '../../../../../plugins/charts/public'; import { WmsOptions } from '../../../tile_map/public/components/wms_options'; import { RegionMapVisParams } from '../types'; @@ -39,7 +46,7 @@ const mapFieldForOption = ({ description, name }: FileLayerField) => ({ }); export type RegionMapOptionsProps = { - serviceSettings: ServiceSettings; + serviceSettings: IServiceSettings; } & VisOptionsProps; function RegionMapOptions(props: RegionMapOptionsProps) { diff --git a/src/legacy/core_plugins/region_map/public/legacy.ts b/src/legacy/core_plugins/region_map/public/legacy.ts index 08615946affa2..b0cc767a044e8 100644 --- a/src/legacy/core_plugins/region_map/public/legacy.ts +++ b/src/legacy/core_plugins/region_map/public/legacy.ts @@ -20,21 +20,18 @@ import { PluginInitializerContext } from 'kibana/public'; import { npSetup, npStart } from 'ui/new_platform'; -import { RegionMapPluginSetupDependencies, RegionMapsConfig } from './plugin'; +import { RegionMapPluginSetupDependencies } from './plugin'; import { LegacyDependenciesPlugin } from './shim'; import { plugin } from '.'; -const regionmapsConfig = npSetup.core.injectedMetadata.getInjectedVar( - 'regionmap' -) as RegionMapsConfig; - const plugins: Readonly = { expressions: npSetup.plugins.expressions, visualizations: npSetup.plugins.visualizations, + mapsLegacy: npSetup.plugins.mapsLegacy, // Temporary solution // It will be removed when all dependent services are migrated to the new platform. - __LEGACY: new LegacyDependenciesPlugin(regionmapsConfig), + __LEGACY: new LegacyDependenciesPlugin(), }; const pluginInstance = plugin({} as PluginInitializerContext); diff --git a/src/legacy/core_plugins/region_map/public/plugin.ts b/src/legacy/core_plugins/region_map/public/plugin.ts index cae569f8fd26d..1453c2155e2d6 100644 --- a/src/legacy/core_plugins/region_map/public/plugin.ts +++ b/src/legacy/core_plugins/region_map/public/plugin.ts @@ -32,10 +32,14 @@ import { LegacyDependenciesPlugin, LegacyDependenciesPluginSetup } from './shim' import { createRegionMapFn } from './region_map_fn'; // @ts-ignore import { createRegionMapTypeDefinition } from './region_map_type'; +import { IServiceSettings, MapsLegacyPluginSetup } from '../../../../plugins/maps_legacy/public'; /** @private */ interface RegionMapVisualizationDependencies extends LegacyDependenciesPluginSetup { uiSettings: IUiSettingsClient; + regionmapsConfig: RegionMapsConfig; + serviceSettings: IServiceSettings; + notificationService: any; } /** @internal */ @@ -43,6 +47,7 @@ export interface RegionMapPluginSetupDependencies { expressions: ReturnType; visualizations: VisualizationsSetup; __LEGACY: LegacyDependenciesPlugin; + mapsLegacy: MapsLegacyPluginSetup; } /** @internal */ @@ -61,10 +66,13 @@ export class RegionMapPlugin implements Plugin, void> { public async setup( core: CoreSetup, - { expressions, visualizations, __LEGACY }: RegionMapPluginSetupDependencies + { expressions, visualizations, mapsLegacy, __LEGACY }: RegionMapPluginSetupDependencies ) { const visualizationDependencies: Readonly = { uiSettings: core.uiSettings, + regionmapsConfig: core.injectedMetadata.getInjectedVar('regionmap') as RegionMapsConfig, + serviceSettings: mapsLegacy.serviceSettings, + notificationService: core.notifications.toasts, ...(await __LEGACY.setup()), }; diff --git a/src/legacy/core_plugins/region_map/public/region_map_visualization.js b/src/legacy/core_plugins/region_map/public/region_map_visualization.js index 72f9d66e7d2bf..f08d53ee35c8d 100644 --- a/src/legacy/core_plugins/region_map/public/region_map_visualization.js +++ b/src/legacy/core_plugins/region_map/public/region_map_visualization.js @@ -28,8 +28,16 @@ import { truncatedColorMaps } from '../../../../plugins/charts/public'; // TODO: reference to TILE_MAP plugin should be removed import { BaseMapsVisualizationProvider } from '../../tile_map/public/base_maps_visualization'; -export function createRegionMapVisualization({ serviceSettings, $injector, uiSettings }) { - const BaseMapsVisualization = new BaseMapsVisualizationProvider(serviceSettings); +export function createRegionMapVisualization({ + serviceSettings, + $injector, + uiSettings, + notificationService, +}) { + const BaseMapsVisualization = new BaseMapsVisualizationProvider( + serviceSettings, + notificationService + ); const tooltipFormatter = new TileMapTooltipFormatter($injector); return class RegionMapsVisualization extends BaseMapsVisualization { diff --git a/src/legacy/core_plugins/region_map/public/shim/legacy_dependencies_plugin.ts b/src/legacy/core_plugins/region_map/public/shim/legacy_dependencies_plugin.ts index c47fc40fbacd7..3a7615e83f281 100644 --- a/src/legacy/core_plugins/region_map/public/shim/legacy_dependencies_plugin.ts +++ b/src/legacy/core_plugins/region_map/public/shim/legacy_dependencies_plugin.ts @@ -19,31 +19,20 @@ import chrome from 'ui/chrome'; import { CoreStart, Plugin } from 'kibana/public'; -import 'ui/vis/map/service_settings'; -import { RegionMapsConfig } from '../plugin'; /** @internal */ export interface LegacyDependenciesPluginSetup { $injector: any; serviceSettings: any; - regionmapsConfig: RegionMapsConfig; } export class LegacyDependenciesPlugin implements Plugin, void> { - constructor(private readonly regionmapsConfig: RegionMapsConfig) {} - public async setup() { const $injector = await chrome.dangerouslyGetActiveInjector(); return { $injector, - regionmapsConfig: this.regionmapsConfig, - // Settings for EMSClient. - // EMSClient, which currently lives in the tile_map vis, - // will probably end up being exposed from the future vis_type_maps plugin, - // which would register both the tile_map and the region_map vis plugins. - serviceSettings: $injector.get('serviceSettings'), } as LegacyDependenciesPluginSetup; } diff --git a/src/legacy/core_plugins/region_map/public/types.ts b/src/legacy/core_plugins/region_map/public/types.ts index 2097aebd27ce0..8585bf720e0cf 100644 --- a/src/legacy/core_plugins/region_map/public/types.ts +++ b/src/legacy/core_plugins/region_map/public/types.ts @@ -17,7 +17,7 @@ * under the License. */ -import { VectorLayer, FileLayerField } from 'ui/vis/map/service_settings'; +import { VectorLayer, FileLayerField } from '../../../../plugins/maps_legacy/public'; import { WMSOptions } from '../../tile_map/public/types'; export interface RegionMapVisParams { diff --git a/src/legacy/core_plugins/region_map/public/util.ts b/src/legacy/core_plugins/region_map/public/util.ts index 69a7a1815bc8e..24c721da1f31a 100644 --- a/src/legacy/core_plugins/region_map/public/util.ts +++ b/src/legacy/core_plugins/region_map/public/util.ts @@ -17,7 +17,7 @@ * under the License. */ -import { FileLayer, VectorLayer } from 'ui/vis/map/service_settings'; +import { FileLayer, VectorLayer } from '../../../../plugins/maps_legacy/public'; // TODO: reference to TILE_MAP plugin should be removed import { ORIGIN } from '../../../../legacy/core_plugins/tile_map/common/origin'; diff --git a/src/legacy/core_plugins/tile_map/public/__tests__/coordinate_maps_visualization.js b/src/legacy/core_plugins/tile_map/public/__tests__/coordinate_maps_visualization.js index 2c142b19d9096..3904c43707906 100644 --- a/src/legacy/core_plugins/tile_map/public/__tests__/coordinate_maps_visualization.js +++ b/src/legacy/core_plugins/tile_map/public/__tests__/coordinate_maps_visualization.js @@ -25,12 +25,18 @@ import initial from './initial.png'; import blues from './blues.png'; import shadedGeohashGrid from './shadedGeohashGrid.png'; import heatmapRaw from './heatmap_raw.png'; -import EMS_CATALOGUE from '../../../../ui/public/vis/__tests__/map/ems_mocks/sample_manifest.json'; -import EMS_FILES from '../../../../ui/public/vis/__tests__/map/ems_mocks/sample_files.json'; -import EMS_TILES from '../../../../ui/public/vis/__tests__/map/ems_mocks/sample_tiles.json'; -import EMS_STYLE_ROAD_MAP_BRIGHT from '../../../../ui/public/vis/__tests__/map/ems_mocks/sample_style_bright'; -import EMS_STYLE_ROAD_MAP_DESATURATED from '../../../../ui/public/vis/__tests__/map/ems_mocks/sample_style_desaturated'; -import EMS_STYLE_DARK_MAP from '../../../../ui/public/vis/__tests__/map/ems_mocks/sample_style_dark'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import EMS_CATALOGUE from '../../../../../plugins/maps_legacy/public/__tests__/map/ems_mocks/sample_manifest.json'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import EMS_FILES from '../../../../../plugins/maps_legacy/public/__tests__/map/ems_mocks/sample_files.json'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import EMS_TILES from '../../../../../plugins/maps_legacy/public/__tests__/map/ems_mocks/sample_tiles.json'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import EMS_STYLE_ROAD_MAP_BRIGHT from '../../../../../plugins/maps_legacy/public/__tests__/map/ems_mocks/sample_style_bright'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import EMS_STYLE_ROAD_MAP_DESATURATED from '../../../../../plugins/maps_legacy/public/__tests__/map/ems_mocks/sample_style_desaturated'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import EMS_STYLE_DARK_MAP from '../../../../../plugins/maps_legacy/public/__tests__/map/ems_mocks/sample_style_dark'; import { createTileMapVisualization } from '../tile_map_visualization'; import { createTileMapTypeDefinition } from '../tile_map_type'; @@ -38,6 +44,15 @@ import { createTileMapTypeDefinition } from '../tile_map_type'; import { ExprVis } from '../../../../../plugins/visualizations/public/expressions/vis'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths import { BaseVisType } from '../../../../../plugins/visualizations/public/vis_types/base_vis_type'; +import { + getPrecision, + getZoomPrecision, + // eslint-disable-next-line @kbn/eslint/no-restricted-paths +} from '../../../../../plugins/maps_legacy/public/map/precision'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { ServiceSettings } from '../../../../../plugins/maps_legacy/public/map/service_settings'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { setInjectedVarFunc } from '../../../../../plugins/maps_legacy/public/kibana_services'; function mockRawData() { const stack = [dummyESResponse]; @@ -75,13 +90,39 @@ describe('CoordinateMapsVisualizationTest', function() { beforeEach(ngMock.module('kibana')); beforeEach( ngMock.inject((Private, $injector) => { - const serviceSettings = $injector.get('serviceSettings'); + setInjectedVarFunc(injectedVar => { + switch (injectedVar) { + case 'mapConfig': + return { + emsFileApiUrl: '', + emsTileApiUrl: '', + emsLandingPageUrl: '', + }; + case 'tilemapsConfig': + return { + deprecated: { + config: { + options: { + attribution: '123', + }, + }, + }, + }; + case 'version': + return '123'; + default: + return 'not found'; + } + }); + const serviceSettings = new ServiceSettings(); const uiSettings = $injector.get('config'); dependencies = { serviceSettings, uiSettings, $injector, + getPrecision, + getZoomPrecision, }; visType = new BaseVisType(createTileMapTypeDefinition(dependencies)); diff --git a/src/legacy/core_plugins/tile_map/public/__tests__/geohash_layer.js b/src/legacy/core_plugins/tile_map/public/__tests__/geohash_layer.js index 857432079e376..fc029d6bccb6e 100644 --- a/src/legacy/core_plugins/tile_map/public/__tests__/geohash_layer.js +++ b/src/legacy/core_plugins/tile_map/public/__tests__/geohash_layer.js @@ -18,13 +18,13 @@ */ import expect from '@kbn/expect'; -import { KibanaMap } from 'ui/vis/map/kibana_map'; import { GeohashLayer } from '../geohash_layer'; // import heatmapPng from './heatmap.png'; import scaledCircleMarkersPng from './scaledCircleMarkers.png'; // import shadedCircleMarkersPng from './shadedCircleMarkers.png'; import { ImageComparator } from 'test_utils/image_comparator'; import GeoHashSampleData from './dummy_es_response.json'; +import { KibanaMap } from '../../../../../plugins/maps_legacy/public'; describe('geohash_layer', function() { let domNode; diff --git a/src/legacy/core_plugins/tile_map/public/base_maps_visualization.js b/src/legacy/core_plugins/tile_map/public/base_maps_visualization.js index d38159c91ef9f..1dac4607280cc 100644 --- a/src/legacy/core_plugins/tile_map/public/base_maps_visualization.js +++ b/src/legacy/core_plugins/tile_map/public/base_maps_visualization.js @@ -19,22 +19,25 @@ import _ from 'lodash'; import { i18n } from '@kbn/i18n'; -import { KibanaMap } from 'ui/vis/map/kibana_map'; +import { KibanaMap } from '../../../../plugins/maps_legacy/public'; import * as Rx from 'rxjs'; import { filter, first } from 'rxjs/operators'; -import 'ui/vis/map/service_settings'; import { toastNotifications } from 'ui/notify'; import chrome from 'ui/chrome'; const WMS_MINZOOM = 0; const WMS_MAXZOOM = 22; //increase this to 22. Better for WMS -export function BaseMapsVisualizationProvider(serviceSettings) { +export function BaseMapsVisualizationProvider(mapServiceSettings, notificationService) { /** * Abstract base class for a visualization consisting of a map with a single baselayer. * @class BaseMapsVisualization * @constructor */ + + const serviceSettings = mapServiceSettings; + const toastService = notificationService; + return class BaseMapsVisualization { constructor(element, vis) { this.vis = vis; @@ -94,8 +97,9 @@ export function BaseMapsVisualizationProvider(serviceSettings) { const centerFromUIState = uiState.get('mapCenter'); options.zoom = !isNaN(zoomFromUiState) ? zoomFromUiState : this.vis.params.mapZoom; options.center = centerFromUIState ? centerFromUIState : this.vis.params.mapCenter; + const services = { toastService }; - this._kibanaMap = new KibanaMap(this._container, options); + this._kibanaMap = new KibanaMap(this._container, options, services); this._kibanaMap.setMinZoom(WMS_MINZOOM); //use a default this._kibanaMap.setMaxZoom(WMS_MAXZOOM); //use a default diff --git a/src/legacy/core_plugins/tile_map/public/components/tile_map_options.tsx b/src/legacy/core_plugins/tile_map/public/components/tile_map_options.tsx index 9169647aa2aef..9ca42fe3e4074 100644 --- a/src/legacy/core_plugins/tile_map/public/components/tile_map_options.tsx +++ b/src/legacy/core_plugins/tile_map/public/components/tile_map_options.tsx @@ -27,7 +27,7 @@ import { RangeOption, SelectOption, SwitchOption, -} from '../../../vis_type_vislib/public'; +} from '../../../../../plugins/charts/public'; import { WmsOptions } from './wms_options'; import { TileMapVisParams } from '../types'; import { MapTypes } from '../map_types'; diff --git a/src/legacy/core_plugins/tile_map/public/components/wms_internal_options.tsx b/src/legacy/core_plugins/tile_map/public/components/wms_internal_options.tsx index b81667400303d..47f5b8f31e62b 100644 --- a/src/legacy/core_plugins/tile_map/public/components/wms_internal_options.tsx +++ b/src/legacy/core_plugins/tile_map/public/components/wms_internal_options.tsx @@ -21,7 +21,7 @@ import React from 'react'; import { EuiLink, EuiSpacer, EuiText, EuiScreenReaderOnly } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; -import { TextInputOption } from '../../../vis_type_vislib/public'; +import { TextInputOption } from '../../../../../plugins/charts/public'; import { WMSOptions } from '../types'; interface WmsInternalOptions { diff --git a/src/legacy/core_plugins/tile_map/public/components/wms_options.tsx b/src/legacy/core_plugins/tile_map/public/components/wms_options.tsx index 27127b781cd4d..e74c260d3b8e5 100644 --- a/src/legacy/core_plugins/tile_map/public/components/wms_options.tsx +++ b/src/legacy/core_plugins/tile_map/public/components/wms_options.tsx @@ -21,11 +21,10 @@ import React, { useMemo } from 'react'; import { EuiPanel, EuiSpacer, EuiTitle } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; - -import { TmsLayer } from 'ui/vis/map/service_settings'; +import { TmsLayer } from '../../../../../plugins/maps_legacy/public'; import { Vis } from '../../../../../plugins/visualizations/public'; import { RegionMapVisParams } from '../../../region_map/public/types'; -import { SelectOption, SwitchOption } from '../../../vis_type_vislib/public'; +import { SelectOption, SwitchOption } from '../../../../../plugins/charts/public'; import { WmsInternalOptions } from './wms_internal_options'; import { WMSOptions, TileMapVisParams } from '../types'; diff --git a/src/legacy/core_plugins/tile_map/public/geohash_layer.js b/src/legacy/core_plugins/tile_map/public/geohash_layer.js index a604e02be7c8c..b9acf1a15208f 100644 --- a/src/legacy/core_plugins/tile_map/public/geohash_layer.js +++ b/src/legacy/core_plugins/tile_map/public/geohash_layer.js @@ -20,8 +20,7 @@ import L from 'leaflet'; import { min, isEqual } from 'lodash'; import { i18n } from '@kbn/i18n'; - -import { KibanaMapLayer } from 'ui/vis/map/kibana_map_layer'; +import { KibanaMapLayer } from '../../../../plugins/maps_legacy/public'; import { HeatmapMarkers } from './markers/heatmap'; import { ScaledCirclesMarkers } from './markers/scaled_circles'; import { ShadedCirclesMarkers } from './markers/shaded_circles'; diff --git a/src/legacy/core_plugins/tile_map/public/legacy.ts b/src/legacy/core_plugins/tile_map/public/legacy.ts index 7b1f916076f61..741e118750f32 100644 --- a/src/legacy/core_plugins/tile_map/public/legacy.ts +++ b/src/legacy/core_plugins/tile_map/public/legacy.ts @@ -27,6 +27,7 @@ import { plugin } from '.'; const plugins: Readonly = { expressions: npSetup.plugins.expressions, visualizations: npSetup.plugins.visualizations, + mapsLegacy: npSetup.plugins.mapsLegacy, // Temporary solution // It will be removed when all dependent services are migrated to the new platform. diff --git a/src/legacy/core_plugins/tile_map/public/markers/scaled_circles.js b/src/legacy/core_plugins/tile_map/public/markers/scaled_circles.js index 88d6db82946c7..f39de6ca7d179 100644 --- a/src/legacy/core_plugins/tile_map/public/markers/scaled_circles.js +++ b/src/legacy/core_plugins/tile_map/public/markers/scaled_circles.js @@ -22,8 +22,7 @@ import _ from 'lodash'; import d3 from 'd3'; import $ from 'jquery'; import { EventEmitter } from 'events'; -import * as colorUtil from 'ui/vis/map/color_util'; - +import { colorUtil } from '../../../../../plugins/maps_legacy/public'; import { truncatedColorMaps } from '../../../../../plugins/charts/public'; export class ScaledCirclesMarkers extends EventEmitter { diff --git a/src/legacy/core_plugins/tile_map/public/plugin.ts b/src/legacy/core_plugins/tile_map/public/plugin.ts index f2addbe3ab872..2b97407b17b38 100644 --- a/src/legacy/core_plugins/tile_map/public/plugin.ts +++ b/src/legacy/core_plugins/tile_map/public/plugin.ts @@ -32,16 +32,22 @@ import { LegacyDependenciesPlugin, LegacyDependenciesPluginSetup } from './shim' import { createTileMapFn } from './tile_map_fn'; // @ts-ignore import { createTileMapTypeDefinition } from './tile_map_type'; +import { IServiceSettings, MapsLegacyPluginSetup } from '../../../../plugins/maps_legacy/public'; /** @private */ interface TileMapVisualizationDependencies extends LegacyDependenciesPluginSetup { + serviceSettings: IServiceSettings; uiSettings: IUiSettingsClient; + getZoomPrecision: any; + getPrecision: any; + notificationService: any; } /** @internal */ export interface TileMapPluginSetupDependencies { expressions: ReturnType; visualizations: VisualizationsSetup; + mapsLegacy: MapsLegacyPluginSetup; __LEGACY: LegacyDependenciesPlugin; } @@ -55,9 +61,14 @@ export class TileMapPlugin implements Plugin, void> { public async setup( core: CoreSetup, - { expressions, visualizations, __LEGACY }: TileMapPluginSetupDependencies + { expressions, visualizations, mapsLegacy, __LEGACY }: TileMapPluginSetupDependencies ) { + const { getZoomPrecision, getPrecision, serviceSettings } = mapsLegacy; const visualizationDependencies: Readonly = { + serviceSettings, + getZoomPrecision, + getPrecision, + notificationService: core.notifications.toasts, uiSettings: core.uiSettings, ...(await __LEGACY.setup()), }; diff --git a/src/legacy/core_plugins/tile_map/public/shim/legacy_dependencies_plugin.ts b/src/legacy/core_plugins/tile_map/public/shim/legacy_dependencies_plugin.ts index 063b12bf0a2db..5296e98b09efe 100644 --- a/src/legacy/core_plugins/tile_map/public/shim/legacy_dependencies_plugin.ts +++ b/src/legacy/core_plugins/tile_map/public/shim/legacy_dependencies_plugin.ts @@ -18,12 +18,12 @@ */ import chrome from 'ui/chrome'; -import 'ui/vis/map/service_settings'; import { CoreStart, Plugin } from 'kibana/public'; +// TODO: Determine why visualizations don't populate without this +import 'angular-sanitize'; /** @internal */ export interface LegacyDependenciesPluginSetup { - serviceSettings: any; $injector: any; } @@ -34,11 +34,6 @@ export class LegacyDependenciesPlugin return { $injector, - // Settings for EMSClient. - // EMSClient, which currently lives in the tile_map vis, - // will probably end up being exposed from the future vis_type_maps plugin, - // which would register both the tile_map and the region_map vis plugins. - serviceSettings: $injector.get('serviceSettings'), } as LegacyDependenciesPluginSetup; } diff --git a/src/legacy/core_plugins/tile_map/public/tile_map_fn.js b/src/legacy/core_plugins/tile_map/public/tile_map_fn.js index 2f54d23590c33..5ad4a2c33db25 100644 --- a/src/legacy/core_plugins/tile_map/public/tile_map_fn.js +++ b/src/legacy/core_plugins/tile_map/public/tile_map_fn.js @@ -16,7 +16,7 @@ * specific language governing permissions and limitations * under the License. */ -import { convertToGeoJson } from 'ui/vis/map/convert_to_geojson'; +import { convertToGeoJson } from '../../../../plugins/maps_legacy/public'; import { i18n } from '@kbn/i18n'; export const createTileMapFn = () => ({ diff --git a/src/legacy/core_plugins/tile_map/public/tile_map_type.js b/src/legacy/core_plugins/tile_map/public/tile_map_type.js index fe82ad5c7352b..ae3a839b600e9 100644 --- a/src/legacy/core_plugins/tile_map/public/tile_map_type.js +++ b/src/legacy/core_plugins/tile_map/public/tile_map_type.js @@ -19,9 +19,7 @@ import React from 'react'; import { i18n } from '@kbn/i18n'; - -import { convertToGeoJson } from 'ui/vis/map/convert_to_geojson'; - +import { convertToGeoJson } from '../../../../plugins/maps_legacy/public'; import { Schemas } from '../../../../plugins/vis_default_editor/public'; import { createTileMapVisualization } from './tile_map_visualization'; import { TileMapOptions } from './components/tile_map_options'; diff --git a/src/legacy/core_plugins/tile_map/public/tile_map_visualization.js b/src/legacy/core_plugins/tile_map/public/tile_map_visualization.js index 910def8a0c78e..fdce8bc51fe86 100644 --- a/src/legacy/core_plugins/tile_map/public/tile_map_visualization.js +++ b/src/legacy/core_plugins/tile_map/public/tile_map_visualization.js @@ -23,15 +23,19 @@ import { BaseMapsVisualizationProvider } from './base_maps_visualization'; import { TileMapTooltipFormatterProvider } from './editors/_tooltip_formatter'; import { npStart } from 'ui/new_platform'; import { getFormat } from '../../../ui/public/visualize/loader/pipeline_helpers/utilities'; -import { - scaleBounds, - zoomPrecision, - getPrecision, - geoContains, -} from '../../../ui/public/vis/map/decode_geo_hash'; +import { scaleBounds, geoContains } from '../../../../plugins/maps_legacy/public'; -export const createTileMapVisualization = ({ serviceSettings, $injector }) => { - const BaseMapsVisualization = new BaseMapsVisualizationProvider(serviceSettings); +export const createTileMapVisualization = ({ + serviceSettings, + $injector, + getZoomPrecision, + getPrecision, + notificationService, +}) => { + const BaseMapsVisualization = new BaseMapsVisualizationProvider( + serviceSettings, + notificationService + ); const tooltipFormatter = new TileMapTooltipFormatterProvider($injector); return class CoordinateMapsVisualization extends BaseMapsVisualization { @@ -59,6 +63,7 @@ export const createTileMapVisualization = ({ serviceSettings, $injector }) => { updateVarsObject.data.boundingBox = geohashAgg.aggConfigParams.boundingBox; } // todo: autoPrecision should be vis parameter, not aggConfig one + const zoomPrecision = getZoomPrecision(); updateVarsObject.data.precision = geohashAgg.aggConfigParams.autoPrecision ? zoomPrecision[this.vis.getUiState().get('mapZoom')] : getPrecision(geohashAgg.aggConfigParams.precision); diff --git a/src/legacy/core_plugins/tile_map/public/tilemap_fn.test.js b/src/legacy/core_plugins/tile_map/public/tilemap_fn.test.js index 0913d6fc92e8a..6da37f4c5ef86 100644 --- a/src/legacy/core_plugins/tile_map/public/tilemap_fn.test.js +++ b/src/legacy/core_plugins/tile_map/public/tilemap_fn.test.js @@ -22,7 +22,7 @@ import { functionWrapper } from '../../../../plugins/expressions/common/expressi import { createTileMapFn } from './tile_map_fn'; jest.mock('ui/new_platform'); -jest.mock('ui/vis/map/convert_to_geojson', () => ({ +jest.mock('../../../../plugins/maps_legacy/public', () => ({ convertToGeoJson: jest.fn().mockReturnValue({ featureCollection: { type: 'FeatureCollection', @@ -37,7 +37,7 @@ jest.mock('ui/vis/map/convert_to_geojson', () => ({ }), })); -import { convertToGeoJson } from 'ui/vis/map/convert_to_geojson'; +import { convertToGeoJson } from '../../../../plugins/maps_legacy/public'; describe('interpreter/functions#tilemap', () => { const fn = functionWrapper(createTileMapFn()); diff --git a/src/legacy/core_plugins/tile_map/public/types.ts b/src/legacy/core_plugins/tile_map/public/types.ts index 5f1c3f9b03c9e..e1b4c27319123 100644 --- a/src/legacy/core_plugins/tile_map/public/types.ts +++ b/src/legacy/core_plugins/tile_map/public/types.ts @@ -17,7 +17,7 @@ * under the License. */ -import { TmsLayer } from 'ui/vis/map/service_settings'; +import { TmsLayer } from '../../../../plugins/maps_legacy/public'; import { MapTypes } from './map_types'; export interface WMSOptions { diff --git a/src/legacy/core_plugins/timelion/public/app.js b/src/legacy/core_plugins/timelion/public/app.js index c15318d29e761..7f5c7d4664af8 100644 --- a/src/legacy/core_plugins/timelion/public/app.js +++ b/src/legacy/core_plugins/timelion/public/app.js @@ -27,10 +27,11 @@ import { fatalError, toastNotifications } from 'ui/notify'; import { timefilter } from 'ui/timefilter'; import { npStart } from 'ui/new_platform'; import { getSavedSheetBreadcrumbs, getCreateBreadcrumbs } from './breadcrumbs'; -import { getTimezone } from '../../vis_type_timelion/public'; +import { getTimezone } from '../../../../plugins/vis_type_timelion/public'; import 'uiExports/savedObjectTypes'; +require('ui/i18n'); require('ui/autoload/all'); // TODO: remove ui imports completely (move to plugins) @@ -57,7 +58,7 @@ require('plugins/timelion/directives/timelion_options_sheet'); document.title = 'Timelion - Kibana'; -const app = require('ui/modules').get('apps/timelion', []); +const app = require('ui/modules').get('apps/timelion', ['i18n', 'ngSanitize']); require('ui/routes').enable(); 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 57262fda55e48..35ac883e5d99c 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 @@ -43,7 +43,7 @@ import _ from 'lodash'; import $ from 'jquery'; import PEG from 'pegjs'; -import grammar from 'raw-loader!../../../../../plugins/timelion/common/chain.peg'; +import grammar from 'raw-loader!../../../../../plugins/vis_type_timelion/common/chain.peg'; import timelionExpressionInputTemplate from './timelion_expression_input.html'; import { SUGGESTION_TYPE, @@ -52,7 +52,7 @@ import { insertAtLocation, } from './timelion_expression_input_helpers'; import { comboBoxKeyCodes } from '@elastic/eui'; -import { getArgValueSuggestions } from '../../../vis_type_timelion/public/helpers/arg_value_suggestions'; +import { npStart } from 'ui/new_platform'; const Parser = PEG.generate(grammar); @@ -68,7 +68,7 @@ export function TimelionExpInput($http, $timeout) { replace: true, template: timelionExpressionInputTemplate, link: function(scope, elem) { - const argValueSuggestions = getArgValueSuggestions(); + const argValueSuggestions = npStart.plugins.visTypeTimelion.getArgValueSuggestions(); const expressionInput = elem.find('[data-expression-input]'); const functionReference = {}; let suggestibleFunctionLocation = {}; diff --git a/src/legacy/core_plugins/timelion/public/panels/timechart/schema.ts b/src/legacy/core_plugins/timelion/public/panels/timechart/schema.ts index cd40cbfa89ffe..34b389f5ff4ce 100644 --- a/src/legacy/core_plugins/timelion/public/panels/timechart/schema.ts +++ b/src/legacy/core_plugins/timelion/public/panels/timechart/schema.ts @@ -17,7 +17,8 @@ * under the License. */ -import '../../../../vis_type_timelion/public/flot'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import '../../../../../../plugins/vis_type_timelion/public/flot'; import _ from 'lodash'; import $ from 'jquery'; import moment from 'moment-timezone'; @@ -28,11 +29,14 @@ import { calculateInterval, DEFAULT_TIME_FORMAT, // @ts-ignore -} from '../../../../../../plugins/timelion/common/lib'; -import { tickFormatters } from '../../../../vis_type_timelion/public/helpers/tick_formatters'; +} from '../../../../../../plugins/vis_type_timelion/common/lib'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { tickFormatters } from '../../../../../../plugins/vis_type_timelion/public/helpers/tick_formatters'; import { TimelionVisualizationDependencies } from '../../plugin'; -import { xaxisFormatterProvider } from '../../../../vis_type_timelion/public/helpers/xaxis_formatter'; -import { generateTicksProvider } from '../../../../vis_type_timelion/public/helpers/tick_generator'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { xaxisFormatterProvider } from '../../../../../../plugins/vis_type_timelion/public/helpers/xaxis_formatter'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { generateTicksProvider } from '../../../../../../plugins/vis_type_timelion/public/helpers/tick_generator'; const DEBOUNCE_DELAY = 50; diff --git a/src/legacy/core_plugins/timelion/public/shim/timelion_legacy_module.ts b/src/legacy/core_plugins/timelion/public/shim/timelion_legacy_module.ts index 9de8477e3978c..8fadf223e1807 100644 --- a/src/legacy/core_plugins/timelion/public/shim/timelion_legacy_module.ts +++ b/src/legacy/core_plugins/timelion/public/shim/timelion_legacy_module.ts @@ -21,7 +21,6 @@ import 'ngreact'; import 'brace/mode/hjson'; import 'brace/ext/searchbox'; import 'ui/accessibility/kbn_ui_ace_keyboard_mode'; -import 'ui/vis/map/service_settings'; import { once } from 'lodash'; // @ts-ignore diff --git a/src/legacy/core_plugins/vis_type_markdown/index.ts b/src/legacy/core_plugins/vis_type_markdown/index.ts deleted file mode 100644 index 3c00420e57d55..0000000000000 --- a/src/legacy/core_plugins/vis_type_markdown/index.ts +++ /dev/null @@ -1,44 +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 { resolve } from 'path'; -import { Legacy } from 'kibana'; - -import { LegacyPluginApi, LegacyPluginInitializer } from '../../../../src/legacy/types'; - -const markdownPluginInitializer: LegacyPluginInitializer = ({ Plugin }: LegacyPluginApi) => - new Plugin({ - id: 'markdown_vis', - require: ['kibana', 'elasticsearch'], - publicDir: resolve(__dirname, 'public'), - uiExports: { - styleSheetPaths: resolve(__dirname, 'public/index.scss'), - hacks: [resolve(__dirname, 'public/legacy')], - injectDefaultVars: server => ({}), - }, - init: (server: Legacy.Server) => ({}), - config(Joi: any) { - return Joi.object({ - enabled: Joi.boolean().default(true), - }).default(); - }, - } as Legacy.PluginSpecOptions); - -// eslint-disable-next-line import/no-default-export -export default markdownPluginInitializer; diff --git a/src/legacy/core_plugins/vis_type_markdown/package.json b/src/legacy/core_plugins/vis_type_markdown/package.json deleted file mode 100644 index 5c233d82fe506..0000000000000 --- a/src/legacy/core_plugins/vis_type_markdown/package.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "name": "markdown_vis", - "version": "kibana" -} diff --git a/src/legacy/core_plugins/vis_type_markdown/public/index.scss b/src/legacy/core_plugins/vis_type_markdown/public/index.scss deleted file mode 100644 index ebae2d1936a9e..0000000000000 --- a/src/legacy/core_plugins/vis_type_markdown/public/index.scss +++ /dev/null @@ -1,10 +0,0 @@ -@import 'src/legacy/ui/public/styles/styling_constants'; - -// Prefix all styles with "mkd" to avoid conflicts. -// Examples -// mkdChart -// mkdChart__legend -// mkdChart__legend--small -// mkdChart__legend-isLoading - -@import './markdown_vis'; diff --git a/src/legacy/core_plugins/vis_type_markdown/public/index.ts b/src/legacy/core_plugins/vis_type_markdown/public/index.ts deleted file mode 100644 index 22dd61b6bda9c..0000000000000 --- a/src/legacy/core_plugins/vis_type_markdown/public/index.ts +++ /dev/null @@ -1,25 +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 { PluginInitializerContext } from '../../../../core/public'; -import { MarkdownPlugin as Plugin } from './plugin'; - -export function plugin(initializerContext: PluginInitializerContext) { - return new Plugin(initializerContext); -} diff --git a/src/legacy/core_plugins/vis_type_markdown/public/legacy.ts b/src/legacy/core_plugins/vis_type_markdown/public/legacy.ts deleted file mode 100644 index 1cfc583f6e005..0000000000000 --- a/src/legacy/core_plugins/vis_type_markdown/public/legacy.ts +++ /dev/null @@ -1,33 +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 { PluginInitializerContext } from 'kibana/public'; -import { npSetup, npStart } from 'ui/new_platform'; -import { MarkdownPluginSetupDependencies } from './plugin'; -import { plugin } from '.'; - -const plugins: Readonly = { - expressions: npSetup.plugins.expressions, - visualizations: npSetup.plugins.visualizations, -}; - -const pluginInstance = plugin({} as PluginInitializerContext); - -export const setup = pluginInstance.setup(npSetup.core, plugins); -export const start = pluginInstance.start(npStart.core); diff --git a/src/legacy/core_plugins/vis_type_markdown/public/plugin.ts b/src/legacy/core_plugins/vis_type_markdown/public/plugin.ts deleted file mode 100644 index 0445d270c9330..0000000000000 --- a/src/legacy/core_plugins/vis_type_markdown/public/plugin.ts +++ /dev/null @@ -1,49 +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 { PluginInitializerContext, CoreSetup, CoreStart, Plugin } from '../../../../core/public'; -import { Plugin as ExpressionsPublicPlugin } from '../../../../plugins/expressions/public'; -import { VisualizationsSetup } from '../../../../plugins/visualizations/public'; - -import { markdownVisDefinition } from './markdown_vis'; -import { createMarkdownVisFn } from './markdown_fn'; - -/** @internal */ -export interface MarkdownPluginSetupDependencies { - expressions: ReturnType; - visualizations: VisualizationsSetup; -} - -/** @internal */ -export class MarkdownPlugin implements Plugin { - initializerContext: PluginInitializerContext; - - constructor(initializerContext: PluginInitializerContext) { - this.initializerContext = initializerContext; - } - - public setup(core: CoreSetup, { expressions, visualizations }: MarkdownPluginSetupDependencies) { - visualizations.createReactVisualization(markdownVisDefinition); - expressions.registerFunction(createMarkdownVisFn); - } - - public start(core: CoreStart) { - // nothing to do here yet - } -} diff --git a/src/legacy/core_plugins/vis_type_metric/index.ts b/src/legacy/core_plugins/vis_type_metric/index.ts deleted file mode 100644 index 8e6654e40f0fc..0000000000000 --- a/src/legacy/core_plugins/vis_type_metric/index.ts +++ /dev/null @@ -1,44 +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 { resolve } from 'path'; -import { Legacy } from 'kibana'; - -import { LegacyPluginApi, LegacyPluginInitializer } from '../../../../src/legacy/types'; - -const metricPluginInitializer: LegacyPluginInitializer = ({ Plugin }: LegacyPluginApi) => - new Plugin({ - id: 'metric_vis', - require: ['kibana', 'elasticsearch'], - publicDir: resolve(__dirname, 'public'), - uiExports: { - styleSheetPaths: resolve(__dirname, 'public/index.scss'), - hacks: [resolve(__dirname, 'public/legacy')], - injectDefaultVars: server => ({}), - }, - init: (server: Legacy.Server) => ({}), - config(Joi: any) { - return Joi.object({ - enabled: Joi.boolean().default(true), - }).default(); - }, - } as Legacy.PluginSpecOptions); - -// eslint-disable-next-line import/no-default-export -export default metricPluginInitializer; diff --git a/src/legacy/core_plugins/vis_type_metric/package.json b/src/legacy/core_plugins/vis_type_metric/package.json deleted file mode 100644 index e570261fc30dd..0000000000000 --- a/src/legacy/core_plugins/vis_type_metric/package.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "name": "metric_vis", - "version": "kibana" -} diff --git a/src/legacy/core_plugins/vis_type_metric/public/index.scss b/src/legacy/core_plugins/vis_type_metric/public/index.scss deleted file mode 100644 index e4116b69bf0f1..0000000000000 --- a/src/legacy/core_plugins/vis_type_metric/public/index.scss +++ /dev/null @@ -1,10 +0,0 @@ -@import 'src/legacy/ui/public/styles/styling_constants'; - -// Prefix all styles with "mtr" to avoid conflicts. -// Examples -// mtrChart -// mtrChart__legend -// mtrChart__legend--small -// mtrChart__legend-isLoading - -@import './metric_vis'; diff --git a/src/legacy/core_plugins/vis_type_metric/public/index.ts b/src/legacy/core_plugins/vis_type_metric/public/index.ts deleted file mode 100644 index 7499babef58a7..0000000000000 --- a/src/legacy/core_plugins/vis_type_metric/public/index.ts +++ /dev/null @@ -1,25 +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 { PluginInitializerContext } from '../../../../core/public'; -import { MetricVisPlugin as Plugin } from './plugin'; - -export function plugin(initializerContext: PluginInitializerContext) { - return new Plugin(initializerContext); -} diff --git a/src/legacy/core_plugins/vis_type_metric/public/legacy.ts b/src/legacy/core_plugins/vis_type_metric/public/legacy.ts deleted file mode 100644 index ba883601e5d65..0000000000000 --- a/src/legacy/core_plugins/vis_type_metric/public/legacy.ts +++ /dev/null @@ -1,34 +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 { PluginInitializerContext } from 'kibana/public'; -import { npSetup, npStart } from 'ui/new_platform'; -import { MetricVisPluginSetupDependencies } from './plugin'; -import { plugin } from '.'; - -const plugins: Readonly = { - expressions: npSetup.plugins.expressions, - visualizations: npSetup.plugins.visualizations, - charts: npSetup.plugins.charts, -}; - -const pluginInstance = plugin({} as PluginInitializerContext); - -export const setup = pluginInstance.setup(npSetup.core, plugins); -export const start = pluginInstance.start(npStart.core, { data: npStart.plugins.data }); diff --git a/src/legacy/core_plugins/vis_type_metric/public/plugin.ts b/src/legacy/core_plugins/vis_type_metric/public/plugin.ts deleted file mode 100644 index cb65d5cafbdd2..0000000000000 --- a/src/legacy/core_plugins/vis_type_metric/public/plugin.ts +++ /dev/null @@ -1,61 +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 { PluginInitializerContext, CoreSetup, CoreStart, Plugin } from '../../../../core/public'; -import { Plugin as ExpressionsPublicPlugin } from '../../../../plugins/expressions/public'; -import { VisualizationsSetup } from '../../../../plugins/visualizations/public'; - -import { createMetricVisFn } from './metric_vis_fn'; -import { createMetricVisTypeDefinition } from './metric_vis_type'; -import { ChartsPluginSetup } from '../../../../plugins/charts/public'; -import { DataPublicPluginStart } from '../../../../plugins/data/public'; -import { setFormatService } from './services'; - -/** @internal */ -export interface MetricVisPluginSetupDependencies { - expressions: ReturnType; - visualizations: VisualizationsSetup; - charts: ChartsPluginSetup; -} - -/** @internal */ -export interface MetricVisPluginStartDependencies { - data: DataPublicPluginStart; -} - -/** @internal */ -export class MetricVisPlugin implements Plugin { - initializerContext: PluginInitializerContext; - - constructor(initializerContext: PluginInitializerContext) { - this.initializerContext = initializerContext; - } - - public setup( - core: CoreSetup, - { expressions, visualizations, charts }: MetricVisPluginSetupDependencies - ) { - expressions.registerFunction(createMetricVisFn); - visualizations.createReactVisualization(createMetricVisTypeDefinition()); - } - - public start(core: CoreStart, { data }: MetricVisPluginStartDependencies) { - setFormatService(data.fieldFormats); - } -} diff --git a/src/legacy/core_plugins/vis_type_metric/public/services.ts b/src/legacy/core_plugins/vis_type_metric/public/services.ts deleted file mode 100644 index b303ccd5aeed2..0000000000000 --- a/src/legacy/core_plugins/vis_type_metric/public/services.ts +++ /dev/null @@ -1,25 +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 { createGetterSetter } from '../../../../plugins/kibana_utils/public'; -import { DataPublicPluginStart } from '../../../../plugins/data/public'; - -export const [getFormatService, setFormatService] = createGetterSetter< - DataPublicPluginStart['fieldFormats'] ->('metric data.fieldFormats'); diff --git a/src/legacy/core_plugins/vis_type_metric/public/types.ts b/src/legacy/core_plugins/vis_type_metric/public/types.ts deleted file mode 100644 index cae18dd8a2ab1..0000000000000 --- a/src/legacy/core_plugins/vis_type_metric/public/types.ts +++ /dev/null @@ -1,58 +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 { Range } from '../../../../plugins/expressions/public'; -import { SchemaConfig } from '../../../../plugins/visualizations/public'; -import { ColorModes, Labels, Style } from '../../vis_type_vislib/public'; -import { ColorSchemas } from '../../../../plugins/charts/public'; - -export const visType = 'metric'; - -export interface DimensionsVisParam { - metrics: SchemaConfig[]; - bucket?: SchemaConfig; -} - -export interface MetricVisParam { - percentageMode: boolean; - useRanges: boolean; - colorSchema: ColorSchemas; - metricColorMode: ColorModes; - colorsRange: Range[]; - labels: Labels; - invertColors: boolean; - style: Style; -} - -export interface VisParams { - addTooltip: boolean; - addLegend: boolean; - dimensions: DimensionsVisParam; - metric: MetricVisParam; - type: typeof visType; -} - -export interface MetricVisMetric { - value: any; - label: string; - color?: string; - bgColor?: string; - lightText: boolean; - rowIndex: number; -} diff --git a/src/legacy/core_plugins/vis_type_table/public/components/table_vis_options.tsx b/src/legacy/core_plugins/vis_type_table/public/components/table_vis_options.tsx index d01ab31e0a843..265528f33f9cd 100644 --- a/src/legacy/core_plugins/vis_type_table/public/components/table_vis_options.tsx +++ b/src/legacy/core_plugins/vis_type_table/public/components/table_vis_options.tsx @@ -25,7 +25,11 @@ import { FormattedMessage } from '@kbn/i18n/react'; import { VisOptionsProps } from 'src/plugins/vis_default_editor/public'; import { search } from '../../../../../plugins/data/public'; -import { NumberInputOption, SwitchOption, SelectOption } from '../../../vis_type_vislib/public'; +import { + SwitchOption, + SelectOption, + NumberInputOption, +} from '../../../../../plugins/charts/public'; import { TableVisParams } from '../types'; import { totalAggregations } from './utils'; diff --git a/src/legacy/core_plugins/vis_type_tagcloud/public/components/tag_cloud_options.tsx b/src/legacy/core_plugins/vis_type_tagcloud/public/components/tag_cloud_options.tsx index 80e4e1de7ddab..7a64549edd747 100644 --- a/src/legacy/core_plugins/vis_type_tagcloud/public/components/tag_cloud_options.tsx +++ b/src/legacy/core_plugins/vis_type_tagcloud/public/components/tag_cloud_options.tsx @@ -22,7 +22,7 @@ import { EuiPanel } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { VisOptionsProps } from 'src/plugins/vis_default_editor/public'; import { ValidatedDualRange } from '../../../../../../src/plugins/kibana_react/public'; -import { SelectOption, SwitchOption } from '../../../vis_type_vislib/public'; +import { SelectOption, SwitchOption } from '../../../../../plugins/charts/public'; import { TagCloudVisParams } from '../types'; function TagCloudOptions({ stateParams, setValue, vis }: VisOptionsProps) { diff --git a/src/legacy/core_plugins/vis_type_timelion/index.ts b/src/legacy/core_plugins/vis_type_timelion/index.ts deleted file mode 100644 index 7bca5154c84fd..0000000000000 --- a/src/legacy/core_plugins/vis_type_timelion/index.ts +++ /dev/null @@ -1,44 +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 { resolve } from 'path'; -import { Legacy } from 'kibana'; - -import { LegacyPluginApi, LegacyPluginInitializer } from '../../../../src/legacy/types'; - -const timelionVisPluginInitializer: LegacyPluginInitializer = ({ Plugin }: LegacyPluginApi) => - new Plugin({ - id: 'timelion_vis', - require: ['kibana', 'elasticsearch'], - publicDir: resolve(__dirname, 'public'), - uiExports: { - styleSheetPaths: resolve(__dirname, 'public/index.scss'), - hacks: [resolve(__dirname, 'public/legacy')], - injectDefaultVars: server => ({}), - }, - init: (server: Legacy.Server) => ({}), - config(Joi: any) { - return Joi.object({ - enabled: Joi.boolean().default(true), - }).default(); - }, - }); - -// eslint-disable-next-line import/no-default-export -export default timelionVisPluginInitializer; diff --git a/src/legacy/core_plugins/vis_type_timelion/package.json b/src/legacy/core_plugins/vis_type_timelion/package.json deleted file mode 100644 index 9b09f98ce6caf..0000000000000 --- a/src/legacy/core_plugins/vis_type_timelion/package.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "name": "timelion_vis", - "version": "kibana" -} diff --git a/src/legacy/core_plugins/vis_type_timelion/public/components/_index.scss b/src/legacy/core_plugins/vis_type_timelion/public/components/_index.scss deleted file mode 100644 index 1d887f43ff9a1..0000000000000 --- a/src/legacy/core_plugins/vis_type_timelion/public/components/_index.scss +++ /dev/null @@ -1,2 +0,0 @@ -@import './panel'; -@import './timelion_expression_input'; diff --git a/src/legacy/core_plugins/vis_type_timelion/public/flot.js b/src/legacy/core_plugins/vis_type_timelion/public/flot.js deleted file mode 100644 index d6ca6d96c34ef..0000000000000 --- a/src/legacy/core_plugins/vis_type_timelion/public/flot.js +++ /dev/null @@ -1,26 +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. - */ - -require('jquery.flot'); -require('jquery.flot.time'); -require('jquery.flot.symbol'); -require('jquery.flot.crosshair'); -require('jquery.flot.selection'); -require('jquery.flot.stack'); -require('jquery.flot.axislabels'); diff --git a/src/legacy/core_plugins/vis_type_timelion/public/index.scss b/src/legacy/core_plugins/vis_type_timelion/public/index.scss deleted file mode 100644 index 313f14a8acf69..0000000000000 --- a/src/legacy/core_plugins/vis_type_timelion/public/index.scss +++ /dev/null @@ -1,5 +0,0 @@ -@import 'src/legacy/ui/public/styles/styling_constants'; - -@import './timelion_vis'; -@import './timelion_editor'; -@import './components/index'; diff --git a/src/legacy/core_plugins/vis_type_timelion/public/index.ts b/src/legacy/core_plugins/vis_type_timelion/public/index.ts deleted file mode 100644 index 6292e2ad3eb08..0000000000000 --- a/src/legacy/core_plugins/vis_type_timelion/public/index.ts +++ /dev/null @@ -1,27 +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 { PluginInitializerContext } from '../../../../core/public'; -import { TimelionVisPlugin as Plugin } from './plugin'; - -export function plugin(initializerContext: PluginInitializerContext) { - return new Plugin(initializerContext); -} - -export { getTimezone } from './helpers/get_timezone'; diff --git a/src/legacy/core_plugins/vis_type_timelion/public/legacy.ts b/src/legacy/core_plugins/vis_type_timelion/public/legacy.ts deleted file mode 100644 index f8de9f94dcedf..0000000000000 --- a/src/legacy/core_plugins/vis_type_timelion/public/legacy.ts +++ /dev/null @@ -1,35 +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 { PluginInitializerContext } from 'kibana/public'; - -import { npSetup, npStart } from './legacy_imports'; -import { TimelionVisSetupDependencies } from './plugin'; -import { plugin } from '.'; - -const setupPlugins: Readonly = { - expressions: npSetup.plugins.expressions, - data: npSetup.plugins.data, - visualizations: npSetup.plugins.visualizations, -}; - -const pluginInstance = plugin({} as PluginInitializerContext); - -export const setup = pluginInstance.setup(npSetup.core, setupPlugins); -export const start = pluginInstance.start(npStart.core, npStart.plugins); diff --git a/src/legacy/core_plugins/vis_type_timelion/public/legacy_imports.ts b/src/legacy/core_plugins/vis_type_timelion/public/legacy_imports.ts deleted file mode 100644 index e7612b288fb24..0000000000000 --- a/src/legacy/core_plugins/vis_type_timelion/public/legacy_imports.ts +++ /dev/null @@ -1,21 +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. - */ - -export { npSetup, npStart } from 'ui/new_platform'; -export { PluginsStart } from 'ui/new_platform/new_platform'; diff --git a/src/legacy/core_plugins/vis_type_timelion/public/plugin.ts b/src/legacy/core_plugins/vis_type_timelion/public/plugin.ts deleted file mode 100644 index b5aa64db19aa4..0000000000000 --- a/src/legacy/core_plugins/vis_type_timelion/public/plugin.ts +++ /dev/null @@ -1,76 +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 { - CoreSetup, - CoreStart, - Plugin, - PluginInitializerContext, - IUiSettingsClient, - HttpSetup, -} from 'kibana/public'; -import { Plugin as ExpressionsPlugin } from 'src/plugins/expressions/public'; -import { DataPublicPluginSetup, TimefilterContract } from 'src/plugins/data/public'; - -import { PluginsStart } from './legacy_imports'; -import { VisualizationsSetup } from '../../../../plugins/visualizations/public'; - -import { getTimelionVisualizationConfig } from './timelion_vis_fn'; -import { getTimelionVisDefinition } from './timelion_vis_type'; -import { setIndexPatterns, setSavedObjectsClient } from './helpers/plugin_services'; - -type TimelionVisCoreSetup = CoreSetup; - -/** @internal */ -export interface TimelionVisDependencies extends Partial { - uiSettings: IUiSettingsClient; - http: HttpSetup; - timefilter: TimefilterContract; -} - -/** @internal */ -export interface TimelionVisSetupDependencies { - expressions: ReturnType; - visualizations: VisualizationsSetup; - data: DataPublicPluginSetup; -} - -/** @internal */ -export class TimelionVisPlugin implements Plugin { - constructor(public initializerContext: PluginInitializerContext) {} - - public async setup( - core: TimelionVisCoreSetup, - { expressions, visualizations, data }: TimelionVisSetupDependencies - ) { - const dependencies: TimelionVisDependencies = { - uiSettings: core.uiSettings, - http: core.http, - timefilter: data.query.timefilter.timefilter, - }; - - expressions.registerFunction(() => getTimelionVisualizationConfig(dependencies)); - visualizations.createReactVisualization(getTimelionVisDefinition(dependencies)); - } - - public start(core: CoreStart, plugins: PluginsStart) { - setIndexPatterns(plugins.data.indexPatterns); - setSavedObjectsClient(core.savedObjects.client); - } -} diff --git a/src/legacy/core_plugins/vis_type_timeseries/index.ts b/src/legacy/core_plugins/vis_type_timeseries/index.ts deleted file mode 100644 index 596fd5b581a71..0000000000000 --- a/src/legacy/core_plugins/vis_type_timeseries/index.ts +++ /dev/null @@ -1,45 +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 { resolve } from 'path'; -import { Legacy } from 'kibana'; - -import { LegacyPluginApi, LegacyPluginInitializer } from '../../../../src/legacy/types'; - -const metricsPluginInitializer: LegacyPluginInitializer = ({ Plugin }: LegacyPluginApi) => - new Plugin({ - id: 'metrics', - require: ['kibana', 'elasticsearch'], - publicDir: resolve(__dirname, 'public'), - uiExports: { - styleSheetPaths: resolve(__dirname, 'public/index.scss'), - hacks: [resolve(__dirname, 'public/legacy')], - injectDefaultVars: server => ({}), - }, - config(Joi: any) { - return Joi.object({ - enabled: Joi.boolean().default(true), - chartResolution: Joi.number().default(150), - minimumBucketSize: Joi.number().default(10), - }).default(); - }, - } as Legacy.PluginSpecOptions); - -// eslint-disable-next-line import/no-default-export -export default metricsPluginInitializer; diff --git a/src/legacy/core_plugins/vis_type_timeseries/package.json b/src/legacy/core_plugins/vis_type_timeseries/package.json deleted file mode 100644 index 6b4874dfe6a68..0000000000000 --- a/src/legacy/core_plugins/vis_type_timeseries/package.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "author": "Chris Cowan", - "name": "metrics", - "version": "kibana" -} - diff --git a/src/legacy/core_plugins/vis_type_timeseries/public/components/vis_types/table/vis.js b/src/legacy/core_plugins/vis_type_timeseries/public/components/vis_types/table/vis.js deleted file mode 100644 index 1fe9358cbfea9..0000000000000 --- a/src/legacy/core_plugins/vis_type_timeseries/public/components/vis_types/table/vis.js +++ /dev/null @@ -1,257 +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 _, { isArray, last, get } from 'lodash'; -import React, { Component } from 'react'; -import PropTypes from 'prop-types'; -import { createTickFormatter } from '../../lib/tick_formatter'; -import { calculateLabel } from '../../../../../../../plugins/vis_type_timeseries/common/calculate_label'; -import { isSortable } from './is_sortable'; -import { EuiToolTip, EuiIcon } from '@elastic/eui'; -import { replaceVars } from '../../lib/replace_vars'; -import { fieldFormats } from '../../../../../../../plugins/data/public'; -import { FormattedMessage } from '@kbn/i18n/react'; -import { getFieldFormats } from '../../../services'; - -import { METRIC_TYPES } from '../../../../../../../plugins/vis_type_timeseries/common/metric_types'; - -function getColor(rules, colorKey, value) { - let color; - if (rules) { - rules.forEach(rule => { - if (rule.operator && rule.value != null) { - if (_[rule.operator](value, rule.value)) { - color = rule[colorKey]; - } - } - }); - } - return color; -} - -export class TableVis extends Component { - constructor(props) { - super(props); - - const fieldFormatsService = getFieldFormats(); - const DateFormat = fieldFormatsService.getType(fieldFormats.FIELD_FORMAT_IDS.DATE); - - this.dateFormatter = new DateFormat({}, this.props.getConfig); - } - - get visibleSeries() { - return get(this.props, 'model.series', []).filter(series => !series.hidden); - } - - renderRow = row => { - const { model } = this.props; - let rowDisplay = model.pivot_type === 'date' ? this.dateFormatter.convert(row.key) : row.key; - if (model.drilldown_url) { - const url = replaceVars(model.drilldown_url, {}, { key: row.key }); - rowDisplay = {rowDisplay}; - } - const columns = row.series - .filter(item => item) - .map(item => { - const column = this.visibleSeries.find(c => c.id === item.id); - if (!column) return null; - const formatter = createTickFormatter( - column.formatter, - column.value_template, - this.props.getConfig - ); - const value = formatter(item.last); - let trend; - if (column.trend_arrows) { - const trendIcon = item.slope > 0 ? 'sortUp' : 'sortDown'; - trend = ( - -   - - ); - } - const style = { color: getColor(column.color_rules, 'text', item.last) }; - return ( - - ); - }); - return ( - - - {columns} - - ); - }; - - renderHeader() { - const { model, uiState, onUiState } = this.props; - const stateKey = `${model.type}.sort`; - const sort = uiState.get(stateKey, { - column: '_default_', - order: 'asc', - }); - - const calculateHeaderLabel = (metric, item) => { - const defaultLabel = item.label || calculateLabel(metric, item.metrics); - - switch (metric.type) { - case METRIC_TYPES.PERCENTILE: - return `${defaultLabel} (${last(metric.percentiles).value || 0})`; - case METRIC_TYPES.PERCENTILE_RANK: - return `${defaultLabel} (${last(metric.values) || 0})`; - default: - return defaultLabel; - } - }; - - const columns = this.visibleSeries.map(item => { - const metric = last(item.metrics); - const label = calculateHeaderLabel(metric, item); - - const handleClick = () => { - if (!isSortable(metric)) return; - let order; - if (sort.column === item.id) { - order = sort.order === 'asc' ? 'desc' : 'asc'; - } else { - order = 'asc'; - } - onUiState(stateKey, { column: item.id, order }); - }; - let sortComponent; - if (isSortable(metric)) { - let sortIcon; - if (sort.column === item.id) { - sortIcon = sort.order === 'asc' ? 'sortUp' : 'sortDown'; - } else { - sortIcon = 'empty'; - } - sortComponent = ; - } - let headerContent = ( - - {label} {sortComponent} - - ); - if (!isSortable(metric)) { - headerContent = ( - - } - > - {headerContent} - - ); - } - - return ( - - ); - }); - const label = model.pivot_label || model.pivot_field || model.pivot_id; - let sortIcon; - if (sort.column === '_default_') { - sortIcon = sort.order === 'asc' ? 'sortUp' : 'sortDown'; - } else { - sortIcon = 'empty'; - } - const sortComponent = ; - const handleSortClick = () => { - let order; - if (sort.column === '_default_') { - order = sort.order === 'asc' ? 'desc' : 'asc'; - } else { - order = 'asc'; - } - onUiState(stateKey, { column: '_default_', order }); - }; - return ( - - - {columns} - - ); - } - - render() { - const { visData, model } = this.props; - const header = this.renderHeader(); - let rows; - - if (isArray(visData.series) && visData.series.length) { - rows = visData.series.map(this.renderRow); - } else { - const message = model.pivot_id ? ( - - ) : ( - - ); - rows = ( - - - - ); - } - return ( -
-
- {value} - {trend} -
{rowDisplay}
- {headerContent} -
- {label} {sortComponent} -
{message}
- {header} - {rows} -
-
- ); - } -} - -TableVis.defaultProps = { - sort: {}, -}; - -TableVis.propTypes = { - visData: PropTypes.object, - model: PropTypes.object, - backgroundColor: PropTypes.string, - onPaginate: PropTypes.func, - onUiState: PropTypes.func, - uiState: PropTypes.object, - pageNumber: PropTypes.number, - getConfig: PropTypes.func, -}; diff --git a/src/legacy/core_plugins/vis_type_timeseries/public/components/vis_types/timeseries/config.js b/src/legacy/core_plugins/vis_type_timeseries/public/components/vis_types/timeseries/config.js deleted file mode 100644 index 024f59c3abb1c..0000000000000 --- a/src/legacy/core_plugins/vis_type_timeseries/public/components/vis_types/timeseries/config.js +++ /dev/null @@ -1,583 +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 PropTypes from 'prop-types'; -import React from 'react'; -import { DataFormatPicker } from '../../data_format_picker'; -import { createSelectHandler } from '../../lib/create_select_handler'; -import { YesNo } from '../../yes_no'; -import { createTextHandler } from '../../lib/create_text_handler'; -import { IndexPattern } from '../../index_pattern'; -import { - htmlIdGenerator, - EuiComboBox, - EuiFlexGroup, - EuiFlexItem, - EuiFieldText, - EuiFormRow, - EuiCode, - EuiHorizontalRule, - EuiFieldNumber, - EuiFormLabel, - EuiSpacer, -} from '@elastic/eui'; -import { FormattedMessage, injectI18n } from '@kbn/i18n/react'; -import { getDefaultQueryLanguage } from '../../lib/get_default_query_language'; -import { QueryBarWrapper } from '../../query_bar_wrapper'; - -import { isPercentDisabled } from '../../lib/stacked'; -import { STACKED_OPTIONS } from '../../../visualizations/constants/chart'; - -export const TimeseriesConfig = injectI18n(function(props) { - const handleSelectChange = createSelectHandler(props.onChange); - const handleTextChange = createTextHandler(props.onChange); - const defaults = { - fill: '', - line_width: '', - point_size: '', - value_template: '{{value}}', - offset_time: '', - split_color_mode: 'gradient', - axis_min: '', - axis_max: '', - stacked: STACKED_OPTIONS.NONE, - steps: 0, - }; - const model = { ...defaults, ...props.model }; - const htmlId = htmlIdGenerator(); - const { intl } = props; - const stackedOptions = [ - { - label: intl.formatMessage({ - id: 'visTypeTimeseries.timeSeries.noneLabel', - defaultMessage: 'None', - }), - value: STACKED_OPTIONS.NONE, - }, - { - label: intl.formatMessage({ - id: 'visTypeTimeseries.timeSeries.stackedLabel', - defaultMessage: 'Stacked', - }), - value: STACKED_OPTIONS.STACKED, - }, - { - label: intl.formatMessage({ - id: 'visTypeTimeseries.timeSeries.stackedWithinSeriesLabel', - defaultMessage: 'Stacked within series', - }), - value: STACKED_OPTIONS.STACKED_WITHIN_SERIES, - }, - { - label: intl.formatMessage({ - id: 'visTypeTimeseries.timeSeries.percentLabel', - defaultMessage: 'Percent', - }), - value: STACKED_OPTIONS.PERCENT, - disabled: isPercentDisabled(props.seriesQuantity[model.id]), - }, - ]; - const selectedStackedOption = stackedOptions.find(option => { - return model.stacked === option.value; - }); - - const positionOptions = [ - { - label: intl.formatMessage({ - id: 'visTypeTimeseries.timeSeries.rightLabel', - defaultMessage: 'Right', - }), - value: 'right', - }, - { - label: intl.formatMessage({ - id: 'visTypeTimeseries.timeSeries.leftLabel', - defaultMessage: 'Left', - }), - value: 'left', - }, - ]; - const selectedAxisPosOption = positionOptions.find(option => { - return model.axis_position === option.value; - }); - - const chartTypeOptions = [ - { - label: intl.formatMessage({ - id: 'visTypeTimeseries.timeSeries.barLabel', - defaultMessage: 'Bar', - }), - value: 'bar', - }, - { - label: intl.formatMessage({ - id: 'visTypeTimeseries.timeSeries.lineLabel', - defaultMessage: 'Line', - }), - value: 'line', - }, - ]; - const selectedChartTypeOption = chartTypeOptions.find(option => { - return model.chart_type === option.value; - }); - - const splitColorOptions = [ - { - label: intl.formatMessage({ - id: 'visTypeTimeseries.timeSeries.gradientLabel', - defaultMessage: 'Gradient', - }), - value: 'gradient', - }, - { - label: intl.formatMessage({ - id: 'visTypeTimeseries.timeSeries.rainbowLabel', - defaultMessage: 'Rainbow', - }), - value: 'rainbow', - }, - ]; - const selectedSplitColorOption = splitColorOptions.find(option => { - return model.split_color_mode === option.value; - }); - - let type; - - if (model.chart_type === 'line') { - type = ( - - - - } - > - - - - - - } - > - - - - - - } - > - - - - - - } - > - - - - - - } - > - - - - - - - - - - - - ); - } - if (model.chart_type === 'bar') { - type = ( - - - - } - > - - - - - - } - > - - - - - - } - > - - - - - - } - > - - - - - ); - } - - const disableSeparateYaxis = model.separate_axis ? false : true; - - const seriesIndexPattern = - props.model.override_index_pattern && props.model.series_index_pattern - ? props.model.series_index_pattern - : props.indexPatternForQuery; - - return ( -
- - - - - - - } - helpText={ - - {'{{value}}/s'} }} - /> - - } - fullWidth - > - - - - - - - - - } - fullWidth - > - props.onChange({ filter })} - indexPatterns={[seriesIndexPattern]} - /> - - - - - {type} - - - - - - - } - > - - - - - - - - - - - - - } - > - - - - - - - - - - - - - - - - - - } - > - {/* - EUITODO: The following input couldn't be converted to EUI because of type mis-match. - It accepts a null value, but is passed a empty string. - */} - - - - - - } - > - {/* - EUITODO: The following input couldn't be converted to EUI because of type mis-match. - It accepts a null value, but is passed a empty string. - */} - - - - - - } - > - - - - - - - - - - - - - - - - - - - -
- ); -}); - -TimeseriesConfig.propTypes = { - fields: PropTypes.object, - model: PropTypes.object, - onChange: PropTypes.func, - indexPatternForQuery: PropTypes.string, - seriesQuantity: PropTypes.object, -}; diff --git a/src/legacy/core_plugins/vis_type_timeseries/public/components/vis_types/timeseries/vis.js b/src/legacy/core_plugins/vis_type_timeseries/public/components/vis_types/timeseries/vis.js deleted file mode 100644 index f559bc38b6c58..0000000000000 --- a/src/legacy/core_plugins/vis_type_timeseries/public/components/vis_types/timeseries/vis.js +++ /dev/null @@ -1,260 +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 PropTypes from 'prop-types'; -import React, { Component } from 'react'; -import reactCSS from 'reactcss'; - -import { startsWith, get, cloneDeep, map } from 'lodash'; -import { htmlIdGenerator } from '@elastic/eui'; -import { ScaleType } from '@elastic/charts'; - -import { createTickFormatter } from '../../lib/tick_formatter'; -import { TimeSeries } from '../../../visualizations/views/timeseries'; -import { MarkdownSimple } from '../../../../../../../plugins/kibana_react/public'; -import { replaceVars } from '../../lib/replace_vars'; -import { getAxisLabelString } from '../../lib/get_axis_label_string'; -import { getInterval } from '../../lib/get_interval'; -import { areFieldsDifferent } from '../../lib/charts'; -import { createXaxisFormatter } from '../../lib/create_xaxis_formatter'; -import { STACKED_OPTIONS } from '../../../visualizations/constants'; -import { getCoreStart, getUISettings } from '../../../services'; - -export class TimeseriesVisualization extends Component { - static propTypes = { - model: PropTypes.object, - onBrush: PropTypes.func, - visData: PropTypes.object, - dateFormat: PropTypes.string, - getConfig: PropTypes.func, - }; - - xAxisFormatter = interval => val => { - const { scaledDataFormat, dateFormat } = this.props.visData; - - if (!scaledDataFormat || !dateFormat) { - return val; - } - - const formatter = createXaxisFormatter(interval, scaledDataFormat, dateFormat); - - return formatter(val); - }; - - yAxisStackedByPercentFormatter = val => { - const n = Number(val) * 100; - - return `${(Number.isNaN(n) ? 0 : n).toFixed(0)}%`; - }; - - applyDocTo = template => doc => { - const vars = replaceVars(template, null, doc); - - if (vars instanceof Error) { - this.showToastNotification = vars.error.caused_by; - - return template; - } - - return vars; - }; - - static getYAxisDomain = model => { - const axisMin = get(model, 'axis_min', '').toString(); - const axisMax = get(model, 'axis_max', '').toString(); - const fit = model.series - ? model.series.filter(({ hidden }) => !hidden).every(({ fill }) => fill === '0') - : model.fill === '0'; - - return { - min: axisMin.length ? Number(axisMin) : undefined, - max: axisMax.length ? Number(axisMax) : undefined, - fit, - }; - }; - - static addYAxis = (yAxis, { id, groupId, position, tickFormatter, domain, hide }) => { - yAxis.push({ - id, - groupId, - position, - tickFormatter, - domain, - hide, - }); - }; - - static getAxisScaleType = model => - get(model, 'axis_scale') === 'log' ? ScaleType.Log : ScaleType.Linear; - - static getTickFormatter = (model, getConfig) => - createTickFormatter(get(model, 'formatter'), get(model, 'value_template'), getConfig); - - componentDidUpdate() { - const toastNotifications = getCoreStart().notifications.toasts; - if ( - this.showToastNotification && - this.notificationReason !== this.showToastNotification.reason - ) { - if (this.notification) { - toastNotifications.remove(this.notification); - } - - this.notificationReason = this.showToastNotification.reason; - this.notification = toastNotifications.addDanger({ - title: this.showToastNotification.title, - text: {this.showToastNotification.reason}, - }); - } - - if (!this.showToastNotification && this.notification) { - toastNotifications.remove(this.notification); - this.notificationReason = null; - this.notification = null; - } - } - - prepareAnnotations = () => { - const { model, visData } = this.props; - - return map(model.annotations, ({ id, color, icon, template }) => { - const annotationData = get(visData, `${model.id}.annotations.${id}`, []); - const applyDocToTemplate = this.applyDocTo(template); - - return { - id, - color, - icon, - data: annotationData.map(({ docs, ...rest }) => ({ - ...rest, - docs: docs.map(applyDocToTemplate), - })), - }; - }); - }; - - render() { - const { model, visData, onBrush } = this.props; - const styles = reactCSS({ - default: { - tvbVis: { - backgroundColor: get(model, 'background_color'), - }, - }, - }); - const series = get(visData, `${model.id}.series`, []); - const interval = getInterval(visData, model); - const yAxisIdGenerator = htmlIdGenerator('yaxis'); - const mainAxisGroupId = yAxisIdGenerator('main_group'); - - const seriesModel = model.series.filter(s => !s.hidden).map(s => cloneDeep(s)); - const enableHistogramMode = areFieldsDifferent('chart_type')(seriesModel); - const firstSeries = seriesModel.find(s => s.formatter && !s.separate_axis); - - const mainAxisScaleType = TimeseriesVisualization.getAxisScaleType(model); - const mainAxisDomain = TimeseriesVisualization.getYAxisDomain(model); - const tickFormatter = TimeseriesVisualization.getTickFormatter( - firstSeries, - this.props.getConfig - ); - const yAxis = []; - let mainDomainAdded = false; - - this.showToastNotification = null; - - seriesModel.forEach(seriesGroup => { - const isStackedWithinSeries = seriesGroup.stacked === STACKED_OPTIONS.STACKED_WITHIN_SERIES; - const hasSeparateAxis = Boolean(seriesGroup.separate_axis); - const groupId = hasSeparateAxis || isStackedWithinSeries ? seriesGroup.id : mainAxisGroupId; - const domain = hasSeparateAxis - ? TimeseriesVisualization.getYAxisDomain(seriesGroup) - : undefined; - const isCustomDomain = groupId !== mainAxisGroupId; - const seriesGroupTickFormatter = TimeseriesVisualization.getTickFormatter( - seriesGroup, - this.props.getConfig - ); - const yScaleType = hasSeparateAxis - ? TimeseriesVisualization.getAxisScaleType(seriesGroup) - : mainAxisScaleType; - - if (seriesGroup.stacked === STACKED_OPTIONS.PERCENT) { - seriesGroup.separate_axis = true; - seriesGroup.axisFormatter = 'percent'; - seriesGroup.axis_min = seriesGroup.axis_min || 0; - seriesGroup.axis_max = seriesGroup.axis_max || 1; - seriesGroup.axis_position = model.axis_position; - } - - series - .filter(r => startsWith(r.id, seriesGroup.id)) - .forEach(seriesDataRow => { - seriesDataRow.tickFormatter = seriesGroupTickFormatter; - seriesDataRow.groupId = groupId; - seriesDataRow.yScaleType = yScaleType; - seriesDataRow.hideInLegend = Boolean(seriesGroup.hide_in_legend); - seriesDataRow.useDefaultGroupDomain = !isCustomDomain; - }); - - if (isCustomDomain) { - TimeseriesVisualization.addYAxis(yAxis, { - domain, - groupId, - id: yAxisIdGenerator(seriesGroup.id), - position: seriesGroup.axis_position, - hide: isStackedWithinSeries, - tickFormatter: - seriesGroup.stacked === STACKED_OPTIONS.PERCENT - ? this.yAxisStackedByPercentFormatter - : seriesGroupTickFormatter, - }); - } else if (!mainDomainAdded) { - TimeseriesVisualization.addYAxis(yAxis, { - tickFormatter, - id: yAxisIdGenerator('main'), - groupId: mainAxisGroupId, - position: model.axis_position, - domain: mainAxisDomain, - }); - - mainDomainAdded = true; - } - }); - - const darkMode = getUISettings().get('theme:darkMode'); - return ( -
- -
- ); - } -} diff --git a/src/legacy/core_plugins/vis_type_timeseries/public/index.scss b/src/legacy/core_plugins/vis_type_timeseries/public/index.scss deleted file mode 100644 index 86fbfb52dbe64..0000000000000 --- a/src/legacy/core_plugins/vis_type_timeseries/public/index.scss +++ /dev/null @@ -1,20 +0,0 @@ -@import 'src/legacy/ui/public/styles/styling_constants'; - -// Prefix all styles with "tvb" to avoid conflicts. -// Examples -// tvbChart -// tvbChart__legend -// tvbChart__legend--small -// tvbChart__legend-isLoading - -@import './variables'; -@import './mixins'; - -// Library overrides -@import './tvb_editor'; - -// Components -@import './components/index'; - -// Visualizations -@import './visualizations/views/index'; diff --git a/src/legacy/core_plugins/vis_type_timeseries/public/index.ts b/src/legacy/core_plugins/vis_type_timeseries/public/index.ts deleted file mode 100644 index 16b099ba16ae9..0000000000000 --- a/src/legacy/core_plugins/vis_type_timeseries/public/index.ts +++ /dev/null @@ -1,25 +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 { PluginInitializerContext } from '../../../../core/public'; -import { MetricsPlugin as Plugin } from './plugin'; - -export function plugin(initializerContext: PluginInitializerContext) { - return new Plugin(initializerContext); -} diff --git a/src/legacy/core_plugins/vis_type_timeseries/public/legacy.ts b/src/legacy/core_plugins/vis_type_timeseries/public/legacy.ts deleted file mode 100644 index 42f116701be51..0000000000000 --- a/src/legacy/core_plugins/vis_type_timeseries/public/legacy.ts +++ /dev/null @@ -1,33 +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 { PluginInitializerContext } from 'kibana/public'; -import { npSetup, npStart } from 'ui/new_platform'; -import { MetricsPluginSetupDependencies } from './plugin'; -import { plugin } from '.'; - -const plugins: Readonly = { - expressions: npSetup.plugins.expressions, - visualizations: npSetup.plugins.visualizations, -}; - -const pluginInstance = plugin({} as PluginInitializerContext); - -export const setup = pluginInstance.setup(npSetup.core, plugins); -export const start = pluginInstance.start(npStart.core, npStart.plugins); diff --git a/src/legacy/core_plugins/vis_type_timeseries/public/plugin.ts b/src/legacy/core_plugins/vis_type_timeseries/public/plugin.ts deleted file mode 100644 index 0310ecf6cfd87..0000000000000 --- a/src/legacy/core_plugins/vis_type_timeseries/public/plugin.ts +++ /dev/null @@ -1,70 +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 { PluginInitializerContext, CoreSetup, CoreStart, Plugin } from 'kibana/public'; -import { Plugin as ExpressionsPublicPlugin } from '../../../../plugins/expressions/public'; -import { VisualizationsSetup } from '../../../../plugins/visualizations/public'; - -import { createMetricsFn } from './metrics_fn'; -import { metricsVisDefinition } from './metrics_type'; -import { - setSavedObjectsClient, - setUISettings, - setI18n, - setFieldFormats, - setCoreStart, - setDataStart, -} from './services'; -import { DataPublicPluginStart } from '../../../../plugins/data/public'; - -/** @internal */ -export interface MetricsPluginSetupDependencies { - expressions: ReturnType; - visualizations: VisualizationsSetup; -} - -/** @internal */ -export interface MetricsPluginStartDependencies { - data: DataPublicPluginStart; -} - -/** @internal */ -export class MetricsPlugin implements Plugin, void> { - initializerContext: PluginInitializerContext; - - constructor(initializerContext: PluginInitializerContext) { - this.initializerContext = initializerContext; - } - - public async setup( - core: CoreSetup, - { expressions, visualizations }: MetricsPluginSetupDependencies - ) { - expressions.registerFunction(createMetricsFn); - setUISettings(core.uiSettings); - visualizations.createReactVisualization(metricsVisDefinition); - } - - public start(core: CoreStart, { data }: MetricsPluginStartDependencies) { - setSavedObjectsClient(core.savedObjects); - setI18n(core.i18n); - setFieldFormats(data.fieldFormats); - setDataStart(data); - setCoreStart(core); - } -} diff --git a/src/legacy/core_plugins/vis_type_timeseries/public/services.ts b/src/legacy/core_plugins/vis_type_timeseries/public/services.ts deleted file mode 100644 index 64ee897247b89..0000000000000 --- a/src/legacy/core_plugins/vis_type_timeseries/public/services.ts +++ /dev/null @@ -1,38 +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 { I18nStart, SavedObjectsStart, IUiSettingsClient, CoreStart } from 'src/core/public'; -import { createGetterSetter } from '../../../../plugins/kibana_utils/public'; -import { DataPublicPluginStart } from '../../../../plugins/data/public'; - -export const [getUISettings, setUISettings] = createGetterSetter('UISettings'); - -export const [getFieldFormats, setFieldFormats] = createGetterSetter< - DataPublicPluginStart['fieldFormats'] ->('FieldFormats'); - -export const [getSavedObjectsClient, setSavedObjectsClient] = createGetterSetter( - 'SavedObjectsClient' -); - -export const [getCoreStart, setCoreStart] = createGetterSetter('CoreStart'); - -export const [getDataStart, setDataStart] = createGetterSetter('DataStart'); - -export const [getI18n, setI18n] = createGetterSetter('I18n'); diff --git a/src/legacy/core_plugins/vis_type_timeseries/public/visualizations/views/timeseries/index.js b/src/legacy/core_plugins/vis_type_timeseries/public/visualizations/views/timeseries/index.js deleted file mode 100644 index 3ce3aae2649e1..0000000000000 --- a/src/legacy/core_plugins/vis_type_timeseries/public/visualizations/views/timeseries/index.js +++ /dev/null @@ -1,267 +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 React, { useEffect, useRef } from 'react'; -import PropTypes from 'prop-types'; -import classNames from 'classnames'; - -import { - Axis, - Chart, - Position, - Settings, - AnnotationDomainTypes, - LineAnnotation, - TooltipType, -} from '@elastic/charts'; -import { EuiIcon } from '@elastic/eui'; -import { getTimezone } from '../../../lib/get_timezone'; -import { eventBus, ACTIVE_CURSOR } from '../../lib/active_cursor'; -import { getUISettings } from '../../../services'; -import { GRID_LINE_CONFIG, ICON_TYPES_MAP, STACKED_OPTIONS } from '../../constants'; -import { AreaSeriesDecorator } from './decorators/area_decorator'; -import { BarSeriesDecorator } from './decorators/bar_decorator'; -import { getStackAccessors } from './utils/stack_format'; -import { getTheme, getChartClasses } from './utils/theme'; - -const generateAnnotationData = (values, formatter) => - values.map(({ key, docs }) => ({ - dataValue: key, - details: docs[0], - header: formatter({ - value: key, - }), - })); - -const decorateFormatter = formatter => ({ value }) => formatter(value); - -const handleCursorUpdate = cursor => { - eventBus.trigger(ACTIVE_CURSOR, cursor); -}; - -export const TimeSeries = ({ - darkMode, - backgroundColor, - showGrid, - legend, - legendPosition, - xAxisLabel, - series, - yAxis, - onBrush, - xAxisFormatter, - annotations, - enableHistogramMode, -}) => { - const chartRef = useRef(); - const updateCursor = (_, cursor) => { - if (chartRef.current) { - chartRef.current.dispatchExternalPointerEvent(cursor); - } - }; - - useEffect(() => { - eventBus.on(ACTIVE_CURSOR, updateCursor); - - return () => { - eventBus.off(ACTIVE_CURSOR, undefined, updateCursor); - }; - }, []); // eslint-disable-line - - const tooltipFormatter = decorateFormatter(xAxisFormatter); - const uiSettings = getUISettings(); - const timeZone = getTimezone(uiSettings); - const hasBarChart = series.some(({ bars }) => bars.show); - - // compute the theme based on the bg color - const theme = getTheme(darkMode, backgroundColor); - // apply legend style change if bgColor is configured - const classes = classNames('tvbVisTimeSeries', getChartClasses(backgroundColor)); - - return ( - - - - {annotations.map(({ id, data, icon, color }) => { - const dataValues = generateAnnotationData(data, tooltipFormatter); - const style = { line: { stroke: color } }; - - return ( - } - hideLinesTooltips={true} - style={style} - /> - ); - })} - - {series.map( - ( - { - id, - label, - bars, - lines, - data, - hideInLegend, - xScaleType, - yScaleType, - groupId, - color, - stack, - points, - useDefaultGroupDomain, - y1AccessorFormat, - y0AccessorFormat, - }, - sortIndex - ) => { - const stackAccessors = getStackAccessors(stack); - const isPercentage = stack === STACKED_OPTIONS.PERCENT; - const key = `${id}-${label}`; - - if (bars.show) { - return ( - - ); - } - - if (lines.show) { - return ( - - ); - } - - return null; - } - )} - - {yAxis.map(({ id, groupId, position, tickFormatter, domain, hide }) => ( - - ))} - - - - ); -}; - -TimeSeries.defaultProps = { - showGrid: true, - legend: true, - legendPosition: 'right', -}; - -TimeSeries.propTypes = { - darkMode: PropTypes.bool, - backgroundColor: PropTypes.string, - showGrid: PropTypes.bool, - legend: PropTypes.bool, - legendPosition: PropTypes.string, - xAxisLabel: PropTypes.string, - series: PropTypes.array, - yAxis: PropTypes.array, - onBrush: PropTypes.func, - xAxisFormatter: PropTypes.func, - annotations: PropTypes.array, - enableHistogramMode: PropTypes.bool.isRequired, -}; diff --git a/src/legacy/core_plugins/vis_type_vega/public/__tests__/vega_visualization.js b/src/legacy/core_plugins/vis_type_vega/public/__tests__/vega_visualization.js index c7fbc0815b07c..6412d8a569b2a 100644 --- a/src/legacy/core_plugins/vis_type_vega/public/__tests__/vega_visualization.js +++ b/src/legacy/core_plugins/vis_type_vega/public/__tests__/vega_visualization.js @@ -49,6 +49,10 @@ import { BaseVisType } from '../../../../../plugins/visualizations/public/vis_ty // eslint-disable-next-line @kbn/eslint/no-restricted-paths import { ExprVis } from '../../../../../plugins/visualizations/public/expressions/vis'; import { setInjectedVars } from '../services'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { setInjectedVarFunc } from '../../../../../plugins/maps_legacy/public/kibana_services'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { ServiceSettings } from '../../../../../plugins/maps_legacy/public/map/service_settings'; const THRESHOLD = 0.1; const PIXEL_DIFF = 30; @@ -69,9 +73,34 @@ describe('VegaVisualizations', () => { beforeEach(ngMock.module('kibana')); beforeEach( - ngMock.inject($injector => { + ngMock.inject(() => { + setInjectedVarFunc(injectedVar => { + switch (injectedVar) { + case 'mapConfig': + return { + emsFileApiUrl: '', + emsTileApiUrl: '', + emsLandingPageUrl: '', + }; + case 'tilemapsConfig': + return { + deprecated: { + config: { + options: { + attribution: '123', + }, + }, + }, + }; + case 'version': + return '123'; + default: + return 'not found'; + } + }); + const serviceSettings = new ServiceSettings(); vegaVisualizationDependencies = { - serviceSettings: $injector.get('serviceSettings'), + serviceSettings, core: { uiSettings: npStart.core.uiSettings, }, diff --git a/src/legacy/core_plugins/vis_type_vega/public/legacy.ts b/src/legacy/core_plugins/vis_type_vega/public/legacy.ts index b2c73894d978d..450af4a6f253e 100644 --- a/src/legacy/core_plugins/vis_type_vega/public/legacy.ts +++ b/src/legacy/core_plugins/vis_type_vega/public/legacy.ts @@ -20,16 +20,12 @@ import { PluginInitializerContext } from 'kibana/public'; import { npSetup, npStart } from 'ui/new_platform'; import { VegaPluginSetupDependencies, VegaPluginStartDependencies } from './plugin'; -import { LegacyDependenciesPlugin } from './shim'; import { plugin } from '.'; const setupPlugins: Readonly = { ...npSetup.plugins, visualizations: npSetup.plugins.visualizations, - - // Temporary solution - // It will be removed when all dependent services are migrated to the new platform. - __LEGACY: new LegacyDependenciesPlugin(), + mapsLegacy: npSetup.plugins.mapsLegacy, }; const startPlugins: Readonly = { diff --git a/src/legacy/core_plugins/vis_type_vega/public/legacy_imports.ts b/src/legacy/core_plugins/vis_type_vega/public/legacy_imports.ts deleted file mode 100644 index b868321d6310f..0000000000000 --- a/src/legacy/core_plugins/vis_type_vega/public/legacy_imports.ts +++ /dev/null @@ -1,23 +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. - */ - -// @ts-ignore -export { KibanaMapLayer } from 'ui/vis/map/kibana_map_layer'; -// @ts-ignore -export { KibanaMap } from 'ui/vis/map/kibana_map'; diff --git a/src/legacy/core_plugins/vis_type_vega/public/plugin.ts b/src/legacy/core_plugins/vis_type_vega/public/plugin.ts index 38b92a40cd99a..9fa77d28fbbfa 100644 --- a/src/legacy/core_plugins/vis_type_vega/public/plugin.ts +++ b/src/legacy/core_plugins/vis_type_vega/public/plugin.ts @@ -17,7 +17,6 @@ * under the License. */ import { PluginInitializerContext, CoreSetup, CoreStart, Plugin } from '../../../../core/public'; -import { LegacyDependenciesPlugin, LegacyDependenciesPluginSetup } from './shim'; import { Plugin as ExpressionsPublicPlugin } from '../../../../plugins/expressions/public'; import { Plugin as DataPublicPlugin } from '../../../../plugins/data/public'; import { VisualizationsSetup } from '../../../../plugins/visualizations/public'; @@ -32,13 +31,15 @@ import { import { createVegaFn } from './vega_fn'; import { createVegaTypeDefinition } from './vega_type'; import { VisTypeVegaSetup } from '../../../../plugins/vis_type_vega/public'; +import { IServiceSettings } from '../../../../plugins/maps_legacy/public'; /** @internal */ -export interface VegaVisualizationDependencies extends LegacyDependenciesPluginSetup { +export interface VegaVisualizationDependencies { core: CoreSetup; plugins: { data: ReturnType; }; + serviceSettings: IServiceSettings; } /** @internal */ @@ -47,7 +48,7 @@ export interface VegaPluginSetupDependencies { visualizations: VisualizationsSetup; data: ReturnType; visTypeVega: VisTypeVegaSetup; - __LEGACY: LegacyDependenciesPlugin; + mapsLegacy: any; } /** @internal */ @@ -65,7 +66,7 @@ export class VegaPlugin implements Plugin, void> { public async setup( core: CoreSetup, - { data, expressions, visualizations, visTypeVega, __LEGACY }: VegaPluginSetupDependencies + { data, expressions, visualizations, visTypeVega, mapsLegacy }: VegaPluginSetupDependencies ) { setInjectedVars({ enableExternalUrls: visTypeVega.config.enableExternalUrls, @@ -79,7 +80,7 @@ export class VegaPlugin implements Plugin, void> { plugins: { data, }, - ...(await __LEGACY.setup()), + serviceSettings: mapsLegacy.serviceSettings, }; expressions.registerFunction(() => createVegaFn(visualizationDependencies)); diff --git a/src/legacy/core_plugins/vis_type_vega/public/shim/index.ts b/src/legacy/core_plugins/vis_type_vega/public/shim/index.ts deleted file mode 100644 index cfc7b62ff4f86..0000000000000 --- a/src/legacy/core_plugins/vis_type_vega/public/shim/index.ts +++ /dev/null @@ -1,20 +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. - */ - -export * from './legacy_dependencies_plugin'; diff --git a/src/legacy/core_plugins/vis_type_vega/public/shim/legacy_dependencies_plugin.ts b/src/legacy/core_plugins/vis_type_vega/public/shim/legacy_dependencies_plugin.ts deleted file mode 100644 index 8925f76cffa43..0000000000000 --- a/src/legacy/core_plugins/vis_type_vega/public/shim/legacy_dependencies_plugin.ts +++ /dev/null @@ -1,49 +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. - */ - -// TODO remove this file as soon as serviceSettings is exposed in the new platform -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import chrome from 'ui/chrome'; -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import 'ui/vis/map/service_settings'; -import { CoreStart, Plugin } from 'kibana/public'; - -/** @internal */ -export interface LegacyDependenciesPluginSetup { - serviceSettings: any; -} - -export class LegacyDependenciesPlugin - implements Plugin, void> { - public async setup() { - const $injector = await chrome.dangerouslyGetActiveInjector(); - - return { - // Settings for EMSClient. - // EMSClient, which currently lives in the tile_map vis, - // will probably end up being exposed from the future vis_type_maps plugin, - // which would register both the tile_map and the region_map vis plugins. - serviceSettings: $injector.get('serviceSettings'), - } as LegacyDependenciesPluginSetup; - } - - public start(core: CoreStart) { - // nothing to do here yet - } -} diff --git a/src/legacy/core_plugins/vis_type_vega/public/vega_view/vega_map_layer.js b/src/legacy/core_plugins/vis_type_vega/public/vega_view/vega_map_layer.js index 38540e9f218fb..d43eb9c3351ea 100644 --- a/src/legacy/core_plugins/vis_type_vega/public/vega_view/vega_map_layer.js +++ b/src/legacy/core_plugins/vis_type_vega/public/vega_view/vega_map_layer.js @@ -19,7 +19,7 @@ import L from 'leaflet'; import 'leaflet-vega'; -import { KibanaMapLayer } from '../legacy_imports'; +import { KibanaMapLayer } from '../../../../../plugins/maps_legacy/public'; export class VegaMapLayer extends KibanaMapLayer { constructor(spec, options) { diff --git a/src/legacy/core_plugins/vis_type_vega/public/vega_view/vega_map_view.js b/src/legacy/core_plugins/vis_type_vega/public/vega_view/vega_map_view.js index 487c90d01ada3..03aef29dc5739 100644 --- a/src/legacy/core_plugins/vis_type_vega/public/vega_view/vega_map_view.js +++ b/src/legacy/core_plugins/vis_type_vega/public/vega_view/vega_map_view.js @@ -21,10 +21,15 @@ import * as vega from 'vega-lib'; import { i18n } from '@kbn/i18n'; import { VegaBaseView } from './vega_base_view'; import { VegaMapLayer } from './vega_map_layer'; -import { KibanaMap } from '../legacy_imports'; +import { KibanaMap } from '../../../../../plugins/maps_legacy/public'; import { getEmsTileLayerId, getUISettings } from '../services'; export class VegaMapView extends VegaBaseView { + constructor(opts, services) { + super(opts); + this.services = services; + } + async _initViewCustomizations() { const mapConfig = this._parser.mapConfig; let baseMapOpts; @@ -102,14 +107,18 @@ export class VegaMapView extends VegaBaseView { // maxBounds = L.latLngBounds(L.latLng(b[1], b[0]), L.latLng(b[3], b[2])); // } - this._kibanaMap = new KibanaMap(this._$container.get(0), { - zoom, - minZoom, - maxZoom, - center: [mapConfig.latitude, mapConfig.longitude], - zoomControl: mapConfig.zoomControl, - scrollWheelZoom: mapConfig.scrollWheelZoom, - }); + this._kibanaMap = new KibanaMap( + this._$container.get(0), + { + zoom, + minZoom, + maxZoom, + center: [mapConfig.latitude, mapConfig.longitude], + zoomControl: mapConfig.zoomControl, + scrollWheelZoom: mapConfig.scrollWheelZoom, + }, + this.services + ); if (baseMapOpts) { this._kibanaMap.setBaseLayer({ diff --git a/src/legacy/core_plugins/vis_type_vega/public/vega_visualization.js b/src/legacy/core_plugins/vis_type_vega/public/vega_visualization.js index 96835ef3b10bc..a6e911de7f0cb 100644 --- a/src/legacy/core_plugins/vis_type_vega/public/vega_visualization.js +++ b/src/legacy/core_plugins/vis_type_vega/public/vega_visualization.js @@ -116,7 +116,8 @@ export const createVegaVisualization = ({ serviceSettings }) => }; if (vegaParser.useMap) { - this._vegaView = new VegaMapView(vegaViewParams); + const services = { toastService: getNotifications().toasts }; + this._vegaView = new VegaMapView(vegaViewParams, services); } else { this._vegaView = new VegaView(vegaViewParams); } diff --git a/src/legacy/core_plugins/vis_type_vislib/public/area.ts b/src/legacy/core_plugins/vis_type_vislib/public/area.ts index 68decacaaa040..8a196da64fc4b 100644 --- a/src/legacy/core_plugins/vis_type_vislib/public/area.ts +++ b/src/legacy/core_plugins/vis_type_vislib/public/area.ts @@ -33,13 +33,13 @@ import { AxisTypes, ScaleTypes, AxisModes, - Rotates, ThresholdLineStyles, getConfigCollections, } from './utils/collections'; import { getAreaOptionTabs, countLabel } from './utils/common_config'; import { createVislibVisController } from './vis_controller'; import { VisTypeVislibDependencies } from './plugin'; +import { Rotates } from '../../../../plugins/charts/public'; export const createAreaVisTypeDefinition = (deps: VisTypeVislibDependencies) => ({ name: 'area', diff --git a/src/legacy/core_plugins/vis_type_vislib/public/components/common/index.ts b/src/legacy/core_plugins/vis_type_vislib/public/components/common/index.ts index 05972d101f576..f0bec3167cb7c 100644 --- a/src/legacy/core_plugins/vis_type_vislib/public/components/common/index.ts +++ b/src/legacy/core_plugins/vis_type_vislib/public/components/common/index.ts @@ -17,13 +17,5 @@ * under the License. */ -export { BasicOptions } from './basic_options'; -export { ColorRanges } from './color_ranges'; -export { ColorSchemaOptions, SetColorSchemaOptionsValue } from './color_schema'; -export { NumberInputOption } from './number_input'; -export { RangeOption } from './range'; -export { SelectOption } from './select'; -export { SwitchOption } from './switch'; -export { TextInputOption } from './text_input'; export { TruncateLabelsOption } from './truncate_labels'; export { ValidationWrapper, ValidationVisOptionsProps } from './validation_wrapper'; diff --git a/src/legacy/core_plugins/vis_type_vislib/public/components/common/utils.ts b/src/legacy/core_plugins/vis_type_vislib/public/components/common/utils.ts deleted file mode 100644 index d51631106dda7..0000000000000 --- a/src/legacy/core_plugins/vis_type_vislib/public/components/common/utils.ts +++ /dev/null @@ -1,34 +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 { useEffect } from 'react'; - -function useValidation( - setValidity: (paramName: ParamName, isValid: boolean) => void, - paramName: ParamName, - isValid: boolean -) { - useEffect(() => { - setValidity(paramName, isValid); - - return () => setValidity(paramName, true); - }, [isValid, paramName, setValidity]); -} - -export { useValidation }; diff --git a/src/legacy/core_plugins/vis_type_vislib/public/components/options/gauge/labels_panel.tsx b/src/legacy/core_plugins/vis_type_vislib/public/components/options/gauge/labels_panel.tsx index b9bae1cad35e7..3fca9dc8adc08 100644 --- a/src/legacy/core_plugins/vis_type_vislib/public/components/options/gauge/labels_panel.tsx +++ b/src/legacy/core_plugins/vis_type_vislib/public/components/options/gauge/labels_panel.tsx @@ -22,7 +22,7 @@ import { EuiPanel, EuiSpacer, EuiTitle } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; -import { SwitchOption, TextInputOption } from '../../common'; +import { SwitchOption, TextInputOption } from '../../../../../../../plugins/charts/public'; import { GaugeOptionsInternalProps } from '.'; function LabelsPanel({ stateParams, setValue, setGaugeValue }: GaugeOptionsInternalProps) { diff --git a/src/legacy/core_plugins/vis_type_vislib/public/components/options/gauge/ranges_panel.tsx b/src/legacy/core_plugins/vis_type_vislib/public/components/options/gauge/ranges_panel.tsx index 7de64e5888096..433cc4edeb47b 100644 --- a/src/legacy/core_plugins/vis_type_vislib/public/components/options/gauge/ranges_panel.tsx +++ b/src/legacy/core_plugins/vis_type_vislib/public/components/options/gauge/ranges_panel.tsx @@ -22,12 +22,16 @@ import { EuiPanel, EuiSpacer, EuiTitle } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; -import { ColorRanges, ColorSchemaOptions, SwitchOption } from '../../common'; +import { + ColorRanges, + ColorSchemaOptions, + ColorSchemaParams, + SetColorRangeValue, + SwitchOption, + ColorSchemas, +} from '../../../../../../../plugins/charts/public'; import { GaugeOptionsInternalProps } from '.'; -import { ColorSchemaVislibParams } from '../../../types'; import { Gauge } from '../../../gauge'; -import { SetColorRangeValue } from '../../common/color_ranges'; -import { ColorSchemas } from '../../../../../../../plugins/charts/public'; function RangesPanel({ setGaugeValue, @@ -39,7 +43,7 @@ function RangesPanel({ vis, }: GaugeOptionsInternalProps) { const setColorSchemaOptions = useCallback( - (paramName: T, value: ColorSchemaVislibParams[T]) => { + (paramName: T, value: ColorSchemaParams[T]) => { setGaugeValue(paramName, value as Gauge[T]); // set outline if color schema is changed to greys // if outline wasn't set explicitly yet diff --git a/src/legacy/core_plugins/vis_type_vislib/public/components/options/gauge/style_panel.tsx b/src/legacy/core_plugins/vis_type_vislib/public/components/options/gauge/style_panel.tsx index 9254c3c18347c..48711de7d171a 100644 --- a/src/legacy/core_plugins/vis_type_vislib/public/components/options/gauge/style_panel.tsx +++ b/src/legacy/core_plugins/vis_type_vislib/public/components/options/gauge/style_panel.tsx @@ -22,7 +22,7 @@ import { EuiPanel, EuiSpacer, EuiTitle } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; -import { SelectOption } from '../../common'; +import { SelectOption } from '../../../../../../../plugins/charts/public'; import { GaugeOptionsInternalProps } from '.'; import { AggGroupNames } from '../../../../../../../plugins/data/public'; diff --git a/src/legacy/core_plugins/vis_type_vislib/public/components/options/heatmap/index.tsx b/src/legacy/core_plugins/vis_type_vislib/public/components/options/heatmap/index.tsx index 715b5902b69da..dc207ad89286f 100644 --- a/src/legacy/core_plugins/vis_type_vislib/public/components/options/heatmap/index.tsx +++ b/src/legacy/core_plugins/vis_type_vislib/public/components/options/heatmap/index.tsx @@ -31,12 +31,12 @@ import { NumberInputOption, SelectOption, SwitchOption, -} from '../../common'; -import { SetColorSchemaOptionsValue } from '../../common/color_schema'; + SetColorSchemaOptionsValue, + SetColorRangeValue, +} from '../../../../../../../plugins/charts/public'; import { HeatmapVisParams } from '../../../heatmap'; import { ValueAxis } from '../../../types'; import { LabelsPanel } from './labels_panel'; -import { SetColorRangeValue } from '../../common/color_ranges'; function HeatmapOptions(props: VisOptionsProps) { const { stateParams, vis, uiState, setValue, setValidity, setTouched } = props; diff --git a/src/legacy/core_plugins/vis_type_vislib/public/components/options/heatmap/labels_panel.tsx b/src/legacy/core_plugins/vis_type_vislib/public/components/options/heatmap/labels_panel.tsx index 38811bd836459..3d1629740df2c 100644 --- a/src/legacy/core_plugins/vis_type_vislib/public/components/options/heatmap/labels_panel.tsx +++ b/src/legacy/core_plugins/vis_type_vislib/public/components/options/heatmap/labels_panel.tsx @@ -26,7 +26,7 @@ import { FormattedMessage } from '@kbn/i18n/react'; import { VisOptionsProps } from 'src/plugins/vis_default_editor/public'; import { ValueAxis } from '../../../types'; import { HeatmapVisParams } from '../../../heatmap'; -import { SwitchOption } from '../../common'; +import { SwitchOption } from '../../../../../../../plugins/charts/public'; const VERTICAL_ROTATION = 270; diff --git a/src/legacy/core_plugins/vis_type_vislib/public/components/options/metrics_axes/category_axis_panel.tsx b/src/legacy/core_plugins/vis_type_vislib/public/components/options/metrics_axes/category_axis_panel.tsx index 915885388640c..246c20a14807c 100644 --- a/src/legacy/core_plugins/vis_type_vislib/public/components/options/metrics_axes/category_axis_panel.tsx +++ b/src/legacy/core_plugins/vis_type_vislib/public/components/options/metrics_axes/category_axis_panel.tsx @@ -25,7 +25,7 @@ import { FormattedMessage } from '@kbn/i18n/react'; import { VisOptionsProps } from 'src/plugins/vis_default_editor/public'; import { Axis } from '../../../types'; -import { SelectOption, SwitchOption } from '../../common'; +import { SelectOption, SwitchOption } from '../../../../../../../plugins/charts/public'; import { LabelOptions, SetAxisLabel } from './label_options'; import { Positions } from '../../../utils/collections'; diff --git a/src/legacy/core_plugins/vis_type_vislib/public/components/options/metrics_axes/chart_options.tsx b/src/legacy/core_plugins/vis_type_vislib/public/components/options/metrics_axes/chart_options.tsx index ec7a325ba43d1..89aab3a19c589 100644 --- a/src/legacy/core_plugins/vis_type_vislib/public/components/options/metrics_axes/chart_options.tsx +++ b/src/legacy/core_plugins/vis_type_vislib/public/components/options/metrics_axes/chart_options.tsx @@ -25,7 +25,7 @@ import { EuiFlexGroup, EuiFlexItem, EuiSpacer } from '@elastic/eui'; import { Vis } from '../../../../../../../plugins/visualizations/public'; import { SeriesParam, ValueAxis } from '../../../types'; import { ChartTypes } from '../../../utils/collections'; -import { SelectOption } from '../../common'; +import { SelectOption } from '../../../../../../../plugins/charts/public'; import { LineOptions } from './line_options'; import { SetParamByIndex, ChangeValueAxis } from './'; diff --git a/src/legacy/core_plugins/vis_type_vislib/public/components/options/metrics_axes/custom_extents_options.tsx b/src/legacy/core_plugins/vis_type_vislib/public/components/options/metrics_axes/custom_extents_options.tsx index 53b2ffa55a941..a3a97df9e35ae 100644 --- a/src/legacy/core_plugins/vis_type_vislib/public/components/options/metrics_axes/custom_extents_options.tsx +++ b/src/legacy/core_plugins/vis_type_vislib/public/components/options/metrics_axes/custom_extents_options.tsx @@ -21,7 +21,7 @@ import React, { useCallback, useEffect } from 'react'; import { i18n } from '@kbn/i18n'; import { ValueAxis } from '../../../types'; -import { NumberInputOption, SwitchOption } from '../../common'; +import { NumberInputOption, SwitchOption } from '../../../../../../../plugins/charts/public'; import { YExtents } from './y_extents'; import { SetScale } from './value_axis_options'; diff --git a/src/legacy/core_plugins/vis_type_vislib/public/components/options/metrics_axes/label_options.tsx b/src/legacy/core_plugins/vis_type_vislib/public/components/options/metrics_axes/label_options.tsx index b6b54193e9f4a..bc687e56646f6 100644 --- a/src/legacy/core_plugins/vis_type_vislib/public/components/options/metrics_axes/label_options.tsx +++ b/src/legacy/core_plugins/vis_type_vislib/public/components/options/metrics_axes/label_options.tsx @@ -24,8 +24,9 @@ import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; import { Axis } from '../../../types'; -import { SelectOption, SwitchOption, TruncateLabelsOption } from '../../common'; +import { TruncateLabelsOption } from '../../common'; import { getRotateOptions } from '../../../utils/collections'; +import { SelectOption, SwitchOption } from '../../../../../../../plugins/charts/public'; export type SetAxisLabel = ( paramName: T, diff --git a/src/legacy/core_plugins/vis_type_vislib/public/components/options/metrics_axes/line_options.test.tsx b/src/legacy/core_plugins/vis_type_vislib/public/components/options/metrics_axes/line_options.test.tsx index 1d29d39bfcb7f..5354bc9c033e6 100644 --- a/src/legacy/core_plugins/vis_type_vislib/public/components/options/metrics_axes/line_options.test.tsx +++ b/src/legacy/core_plugins/vis_type_vislib/public/components/options/metrics_axes/line_options.test.tsx @@ -20,7 +20,7 @@ import React from 'react'; import { shallow } from 'enzyme'; import { LineOptions, LineOptionsParams } from './line_options'; -import { NumberInputOption } from '../../common'; +import { NumberInputOption } from '../../../../../../../plugins/charts/public'; import { seriesParam, vis } from './mocks'; jest.mock('ui/new_platform'); diff --git a/src/legacy/core_plugins/vis_type_vislib/public/components/options/metrics_axes/line_options.tsx b/src/legacy/core_plugins/vis_type_vislib/public/components/options/metrics_axes/line_options.tsx index 01a69a6fac70b..76f95bd93caf8 100644 --- a/src/legacy/core_plugins/vis_type_vislib/public/components/options/metrics_axes/line_options.tsx +++ b/src/legacy/core_plugins/vis_type_vislib/public/components/options/metrics_axes/line_options.tsx @@ -24,7 +24,11 @@ import { EuiFlexGroup, EuiFlexItem, EuiSpacer } from '@elastic/eui'; import { Vis } from '../../../../../../../plugins/visualizations/public'; import { SeriesParam } from '../../../types'; -import { NumberInputOption, SelectOption, SwitchOption } from '../../common'; +import { + NumberInputOption, + SelectOption, + SwitchOption, +} from '../../../../../../../plugins/charts/public'; import { SetChart } from './chart_options'; export interface LineOptionsParams { diff --git a/src/legacy/core_plugins/vis_type_vislib/public/components/options/metrics_axes/mocks.ts b/src/legacy/core_plugins/vis_type_vislib/public/components/options/metrics_axes/mocks.ts index 0d9fa8c25a4f7..a296281375dac 100644 --- a/src/legacy/core_plugins/vis_type_vislib/public/components/options/metrics_axes/mocks.ts +++ b/src/legacy/core_plugins/vis_type_vislib/public/components/options/metrics_axes/mocks.ts @@ -18,7 +18,7 @@ */ import { Vis } from '../../../../../../../plugins/visualizations/public'; -import { Axis, ValueAxis, SeriesParam, Style } from '../../../types'; +import { Axis, ValueAxis, SeriesParam } from '../../../types'; import { ChartTypes, ChartModes, @@ -31,6 +31,7 @@ import { getPositions, getInterpolationModes, } from '../../../utils/collections'; +import { Style } from '../../../../../../../plugins/charts/public'; const defaultValueAxisId = 'ValueAxis-1'; diff --git a/src/legacy/core_plugins/vis_type_vislib/public/components/options/metrics_axes/value_axis_options.test.tsx b/src/legacy/core_plugins/vis_type_vislib/public/components/options/metrics_axes/value_axis_options.test.tsx index 955867e66d09f..876a6917ee0b4 100644 --- a/src/legacy/core_plugins/vis_type_vislib/public/components/options/metrics_axes/value_axis_options.test.tsx +++ b/src/legacy/core_plugins/vis_type_vislib/public/components/options/metrics_axes/value_axis_options.test.tsx @@ -21,7 +21,7 @@ import React from 'react'; import { shallow } from 'enzyme'; import { ValueAxisOptions, ValueAxisOptionsParams } from './value_axis_options'; import { ValueAxis } from '../../../types'; -import { TextInputOption } from '../../common'; +import { TextInputOption } from '../../../../../../../plugins/charts/public'; import { LabelOptions } from './label_options'; import { ScaleTypes, Positions } from '../../../utils/collections'; import { valueAxis, vis } from './mocks'; diff --git a/src/legacy/core_plugins/vis_type_vislib/public/components/options/metrics_axes/value_axis_options.tsx b/src/legacy/core_plugins/vis_type_vislib/public/components/options/metrics_axes/value_axis_options.tsx index 8f0327e78c7ab..1b89a766d0591 100644 --- a/src/legacy/core_plugins/vis_type_vislib/public/components/options/metrics_axes/value_axis_options.tsx +++ b/src/legacy/core_plugins/vis_type_vislib/public/components/options/metrics_axes/value_axis_options.tsx @@ -24,7 +24,11 @@ import { EuiSpacer, EuiAccordion, EuiHorizontalRule } from '@elastic/eui'; import { Vis } from '../../../../../../../plugins/visualizations/public'; import { ValueAxis } from '../../../types'; import { Positions } from '../../../utils/collections'; -import { SelectOption, SwitchOption, TextInputOption } from '../../common'; +import { + SelectOption, + SwitchOption, + TextInputOption, +} from '../../../../../../../plugins/charts/public'; import { LabelOptions, SetAxisLabel } from './label_options'; import { CustomExtentsOptions } from './custom_extents_options'; import { isAxisHorizontal } from './utils'; diff --git a/src/legacy/core_plugins/vis_type_vislib/public/components/options/metrics_axes/y_extents.test.tsx b/src/legacy/core_plugins/vis_type_vislib/public/components/options/metrics_axes/y_extents.test.tsx index 17c47b35b20dc..b5ed475ca8e31 100644 --- a/src/legacy/core_plugins/vis_type_vislib/public/components/options/metrics_axes/y_extents.test.tsx +++ b/src/legacy/core_plugins/vis_type_vislib/public/components/options/metrics_axes/y_extents.test.tsx @@ -21,7 +21,7 @@ import React from 'react'; import { mount, shallow } from 'enzyme'; import { YExtents, YExtentsProps } from './y_extents'; import { ScaleTypes } from '../../../utils/collections'; -import { NumberInputOption } from '../../common'; +import { NumberInputOption } from '../../../../../../../plugins/charts/public'; jest.mock('ui/new_platform'); diff --git a/src/legacy/core_plugins/vis_type_vislib/public/components/options/metrics_axes/y_extents.tsx b/src/legacy/core_plugins/vis_type_vislib/public/components/options/metrics_axes/y_extents.tsx index c0db58a612e71..faeb6069b5126 100644 --- a/src/legacy/core_plugins/vis_type_vislib/public/components/options/metrics_axes/y_extents.tsx +++ b/src/legacy/core_plugins/vis_type_vislib/public/components/options/metrics_axes/y_extents.tsx @@ -23,7 +23,7 @@ import { i18n } from '@kbn/i18n'; import { Scale } from '../../../types'; import { ScaleTypes } from '../../../utils/collections'; -import { NumberInputOption } from '../../common'; +import { NumberInputOption } from '../../../../../../../plugins/charts/public'; import { SetScale } from './value_axis_options'; const rangeError = i18n.translate('visTypeVislib.controls.pointSeries.valueAxes.minErrorMessage', { diff --git a/src/legacy/core_plugins/vis_type_vislib/public/components/options/pie.tsx b/src/legacy/core_plugins/vis_type_vislib/public/components/options/pie.tsx index 4c0be456aad64..f6be9cd0bd8fe 100644 --- a/src/legacy/core_plugins/vis_type_vislib/public/components/options/pie.tsx +++ b/src/legacy/core_plugins/vis_type_vislib/public/components/options/pie.tsx @@ -23,7 +23,8 @@ import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; import { VisOptionsProps } from 'src/plugins/vis_default_editor/public'; -import { BasicOptions, TruncateLabelsOption, SwitchOption } from '../common'; +import { TruncateLabelsOption } from '../common'; +import { BasicOptions, SwitchOption } from '../../../../../../plugins/charts/public'; import { PieVisParams } from '../../pie'; function PieOptions(props: VisOptionsProps) { diff --git a/src/legacy/core_plugins/vis_type_vislib/public/components/options/point_series/grid_panel.tsx b/src/legacy/core_plugins/vis_type_vislib/public/components/options/point_series/grid_panel.tsx index bb2b3f8fddb49..392d180d2c5f2 100644 --- a/src/legacy/core_plugins/vis_type_vislib/public/components/options/point_series/grid_panel.tsx +++ b/src/legacy/core_plugins/vis_type_vislib/public/components/options/point_series/grid_panel.tsx @@ -23,7 +23,7 @@ import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; import { VisOptionsProps } from 'src/plugins/vis_default_editor/public'; -import { SelectOption, SwitchOption } from '../../common'; +import { SelectOption, SwitchOption } from '../../../../../../../plugins/charts/public'; import { BasicVislibParams, ValueAxis } from '../../../types'; function GridPanel({ stateParams, setValue, hasHistogramAgg }: VisOptionsProps) { diff --git a/src/legacy/core_plugins/vis_type_vislib/public/components/options/point_series/point_series.tsx b/src/legacy/core_plugins/vis_type_vislib/public/components/options/point_series/point_series.tsx index b9872ab94bd0b..903c1917751d9 100644 --- a/src/legacy/core_plugins/vis_type_vislib/public/components/options/point_series/point_series.tsx +++ b/src/legacy/core_plugins/vis_type_vislib/public/components/options/point_series/point_series.tsx @@ -21,7 +21,8 @@ import { EuiPanel, EuiTitle, EuiSpacer } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; -import { BasicOptions, SwitchOption, ValidationVisOptionsProps } from '../../common'; +import { ValidationVisOptionsProps } from '../../common'; +import { BasicOptions, SwitchOption } from '../../../../../../../plugins/charts/public'; import { GridPanel } from './grid_panel'; import { ThresholdPanel } from './threshold_panel'; import { BasicVislibParams } from '../../../types'; diff --git a/src/legacy/core_plugins/vis_type_vislib/public/components/options/point_series/threshold_panel.tsx b/src/legacy/core_plugins/vis_type_vislib/public/components/options/point_series/threshold_panel.tsx index 7866ad74ede7f..12f058ec7dd1f 100644 --- a/src/legacy/core_plugins/vis_type_vislib/public/components/options/point_series/threshold_panel.tsx +++ b/src/legacy/core_plugins/vis_type_vislib/public/components/options/point_series/threshold_panel.tsx @@ -21,8 +21,12 @@ import { EuiPanel, EuiTitle, EuiColorPicker, EuiFormRow, EuiSpacer } from '@elas import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; -import { SelectOption, SwitchOption, ValidationVisOptionsProps } from '../../common'; -import { NumberInputOption } from '../../common/required_number_input'; +import { ValidationVisOptionsProps } from '../../common'; +import { + SelectOption, + SwitchOption, + RequiredNumberInputOption, +} from '../../../../../../../plugins/charts/public'; import { BasicVislibParams } from '../../../types'; function ThresholdPanel({ @@ -73,7 +77,7 @@ function ThresholdPanel({ {stateParams.thresholdLine.show && ( <> - - ({ name: 'histogram', diff --git a/src/legacy/core_plugins/vis_type_vislib/public/horizontal_bar.ts b/src/legacy/core_plugins/vis_type_vislib/public/horizontal_bar.ts index dc47252ccd44f..6f73271726660 100644 --- a/src/legacy/core_plugins/vis_type_vislib/public/horizontal_bar.ts +++ b/src/legacy/core_plugins/vis_type_vislib/public/horizontal_bar.ts @@ -32,13 +32,13 @@ import { AxisTypes, ScaleTypes, AxisModes, - Rotates, ThresholdLineStyles, getConfigCollections, } from './utils/collections'; import { getAreaOptionTabs, countLabel } from './utils/common_config'; import { createVislibVisController } from './vis_controller'; import { VisTypeVislibDependencies } from './plugin'; +import { Rotates } from '../../../../plugins/charts/public'; export const createHorizontalBarVisTypeDefinition = (deps: VisTypeVislibDependencies) => ({ name: 'horizontal_bar', diff --git a/src/legacy/core_plugins/vis_type_vislib/public/index.ts b/src/legacy/core_plugins/vis_type_vislib/public/index.ts index 1f773c4efcb02..4d7091ffb204b 100644 --- a/src/legacy/core_plugins/vis_type_vislib/public/index.ts +++ b/src/legacy/core_plugins/vis_type_vislib/public/index.ts @@ -24,18 +24,4 @@ export function plugin(initializerContext: PluginInitializerContext) { return new Plugin(initializerContext); } -export { - BasicOptions, - RangeOption, - ColorRanges, - SelectOption, - SetColorSchemaOptionsValue, - ColorSchemaOptions, - NumberInputOption, - SwitchOption, - TextInputOption, -} from './components'; - -export { ColorModes } from './utils/collections'; - export * from './types'; diff --git a/src/legacy/core_plugins/vis_type_vislib/public/legacy.ts b/src/legacy/core_plugins/vis_type_vislib/public/legacy.ts index aa11e0ef41fba..579caa1cb88f6 100644 --- a/src/legacy/core_plugins/vis_type_vislib/public/legacy.ts +++ b/src/legacy/core_plugins/vis_type_vislib/public/legacy.ts @@ -30,6 +30,7 @@ const setupPlugins: Readonly = { expressions: npSetup.plugins.expressions, visualizations: npSetup.plugins.visualizations, charts: npSetup.plugins.charts, + visTypeXy: npSetup.plugins.visTypeXy, }; const startPlugins: Readonly = { diff --git a/src/legacy/core_plugins/vis_type_vislib/public/line.ts b/src/legacy/core_plugins/vis_type_vislib/public/line.ts index 885ab295d11e1..1f9a8d77398e6 100644 --- a/src/legacy/core_plugins/vis_type_vislib/public/line.ts +++ b/src/legacy/core_plugins/vis_type_vislib/public/line.ts @@ -32,7 +32,6 @@ import { AxisTypes, ScaleTypes, AxisModes, - Rotates, ThresholdLineStyles, InterpolationModes, getConfigCollections, @@ -40,6 +39,7 @@ import { import { getAreaOptionTabs, countLabel } from './utils/common_config'; import { createVislibVisController } from './vis_controller'; import { VisTypeVislibDependencies } from './plugin'; +import { Rotates } from '../../../../plugins/charts/public'; export const createLineVisTypeDefinition = (deps: VisTypeVislibDependencies) => ({ name: 'line', diff --git a/src/legacy/core_plugins/vis_type_vislib/public/plugin.ts b/src/legacy/core_plugins/vis_type_vislib/public/plugin.ts index 2731fb6f5fbe6..ef3f664252856 100644 --- a/src/legacy/core_plugins/vis_type_vislib/public/plugin.ts +++ b/src/legacy/core_plugins/vis_type_vislib/public/plugin.ts @@ -24,6 +24,7 @@ import { PluginInitializerContext, } from 'kibana/public'; +import { VisTypeXyPluginSetup } from 'src/plugins/vis_type_xy/public'; import { Plugin as ExpressionsPublicPlugin } from '../../../../plugins/expressions/public'; import { VisualizationsSetup } from '../../../../plugins/visualizations/public'; import { createVisTypeVislibVisFn } from './vis_type_vislib_vis_fn'; @@ -39,7 +40,6 @@ import { createGoalVisTypeDefinition, } from './vis_type_vislib_vis_types'; import { ChartsPluginSetup } from '../../../../plugins/charts/public'; -import { ConfigSchema as VisTypeXyConfigSchema } from '../../vis_type_xy'; import { DataPublicPluginStart } from '../../../../plugins/data/public'; import { setFormatService, setDataActions } from './services'; @@ -53,6 +53,7 @@ export interface VisTypeVislibPluginSetupDependencies { expressions: ReturnType; visualizations: VisualizationsSetup; charts: ChartsPluginSetup; + visTypeXy?: VisTypeXyPluginSetup; } /** @internal */ @@ -68,7 +69,7 @@ export class VisTypeVislibPlugin implements Plugin { public async setup( core: VisTypeVislibCoreSetup, - { expressions, visualizations, charts }: VisTypeVislibPluginSetupDependencies + { expressions, visualizations, charts, visTypeXy }: VisTypeVislibPluginSetupDependencies ) { const visualizationDependencies: Readonly = { uiSettings: core.uiSettings, @@ -86,12 +87,8 @@ export class VisTypeVislibPlugin implements Plugin { ]; const vislibFns = [createVisTypeVislibVisFn(), createPieVisFn()]; - const visTypeXy = core.injectedMetadata.getInjectedVar('visTypeXy') as - | VisTypeXyConfigSchema['visTypeXy'] - | undefined; - // if visTypeXy plugin is disabled it's config will be undefined - if (!visTypeXy || !visTypeXy.enabled) { + if (!visTypeXy) { const convertedTypes: any[] = []; const convertedFns: any[] = []; diff --git a/src/legacy/core_plugins/vis_type_vislib/public/types.ts b/src/legacy/core_plugins/vis_type_vislib/public/types.ts index f33b42483c53e..25c6ae5439fe8 100644 --- a/src/legacy/core_plugins/vis_type_vislib/public/types.ts +++ b/src/legacy/core_plugins/vis_type_vislib/public/types.ts @@ -25,39 +25,16 @@ import { AxisModes, AxisTypes, InterpolationModes, - Rotates, ScaleTypes, ThresholdLineStyles, } from './utils/collections'; -import { ColorSchemas } from '../../../../plugins/charts/public'; +import { Labels, Style } from '../../../../plugins/charts/public'; export interface CommonVislibParams { addTooltip: boolean; legendPosition: Positions; } -export interface ColorSchemaVislibParams { - colorSchema: ColorSchemas; - invertColors: boolean; -} - -export interface Labels { - color?: string; - filter?: boolean; - overwriteColor?: boolean; - rotate?: Rotates; - show: boolean; - truncate?: number | null; -} - -export interface Style { - bgFill: string; - bgColor: boolean; - labelColor: boolean; - subText: string; - fontSize: number; -} - export interface Scale { boundsMargin?: number | ''; defaultYExtents?: boolean; diff --git a/src/legacy/core_plugins/vis_type_vislib/public/utils/collections.ts b/src/legacy/core_plugins/vis_type_vislib/public/utils/collections.ts index f32b765cd6e57..2024c43dd1c8b 100644 --- a/src/legacy/core_plugins/vis_type_vislib/public/utils/collections.ts +++ b/src/legacy/core_plugins/vis_type_vislib/public/utils/collections.ts @@ -20,7 +20,7 @@ import { i18n } from '@kbn/i18n'; import { $Values } from '@kbn/utility-types'; -import { colorSchemas } from '../../../../../plugins/charts/public'; +import { colorSchemas, Rotates } from '../../../../../plugins/charts/public'; export const Positions = Object.freeze({ RIGHT: 'right' as 'right', @@ -203,13 +203,6 @@ const getAxisModes = () => [ }, ]; -export const Rotates = Object.freeze({ - HORIZONTAL: 0, - VERTICAL: 90, - ANGLED: 75, -}); -export type Rotates = $Values; - export const ThresholdLineStyles = Object.freeze({ FULL: 'full' as 'full', DASHED: 'dashed' as 'dashed', @@ -265,13 +258,6 @@ export const GaugeTypes = Object.freeze({ }); export type GaugeTypes = $Values; -export const ColorModes = Object.freeze({ - BACKGROUND: 'Background' as 'Background', - LABELS: 'Labels' as 'Labels', - NONE: 'None' as 'None', -}); -export type ColorModes = $Values; - const getGaugeTypes = () => [ { text: i18n.translate('visTypeVislib.gauge.gaugeTypes.arcText', { diff --git a/src/legacy/core_plugins/vis_type_xy/index.ts b/src/legacy/core_plugins/vis_type_xy/index.ts deleted file mode 100644 index 58d2e425eef40..0000000000000 --- a/src/legacy/core_plugins/vis_type_xy/index.ts +++ /dev/null @@ -1,56 +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 { resolve } from 'path'; -import { Legacy } from 'kibana'; - -import { LegacyPluginApi, LegacyPluginInitializer } from '../../types'; - -export interface ConfigSchema { - visTypeXy: { - enabled: boolean; - }; -} - -const visTypeXyPluginInitializer: LegacyPluginInitializer = ({ Plugin }: LegacyPluginApi) => - new Plugin({ - id: 'visTypeXy', - require: ['kibana', 'elasticsearch', 'visualizations', 'interpreter'], - publicDir: resolve(__dirname, 'public'), - uiExports: { - hacks: [resolve(__dirname, 'public/legacy')], - injectDefaultVars(server): ConfigSchema { - const config = server.config(); - - return { - visTypeXy: { - enabled: config.get('visTypeXy.enabled') as boolean, - }, - }; - }, - }, - config(Joi: any) { - return Joi.object({ - enabled: Joi.boolean().default(false), - }).default(); - }, - } as Legacy.PluginSpecOptions); - -// eslint-disable-next-line import/no-default-export -export default visTypeXyPluginInitializer; diff --git a/src/legacy/core_plugins/vis_type_xy/package.json b/src/legacy/core_plugins/vis_type_xy/package.json deleted file mode 100644 index 920f7dcb44e87..0000000000000 --- a/src/legacy/core_plugins/vis_type_xy/package.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "name": "visTypeXy", - "version": "kibana" -} diff --git a/src/legacy/core_plugins/vis_type_xy/public/index.ts b/src/legacy/core_plugins/vis_type_xy/public/index.ts deleted file mode 100644 index 218dc8aa8a683..0000000000000 --- a/src/legacy/core_plugins/vis_type_xy/public/index.ts +++ /dev/null @@ -1,25 +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 { PluginInitializerContext } from '../../../../core/public'; -import { VisTypeXyPlugin as Plugin } from './plugin'; - -export function plugin(initializerContext: PluginInitializerContext) { - return new Plugin(initializerContext); -} diff --git a/src/legacy/core_plugins/vis_type_xy/public/legacy.ts b/src/legacy/core_plugins/vis_type_xy/public/legacy.ts deleted file mode 100644 index 740ceeaac6a7d..0000000000000 --- a/src/legacy/core_plugins/vis_type_xy/public/legacy.ts +++ /dev/null @@ -1,40 +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 { npSetup, npStart } from 'ui/new_platform'; -import { PluginInitializerContext } from 'kibana/public'; - -import { plugin } from '.'; -import { VisTypeXyPluginSetupDependencies, VisTypeXyPluginStartDependencies } from './plugin'; - -const setupPlugins: Readonly = { - expressions: npSetup.plugins.expressions, - visualizations: npSetup.plugins.visualizations, - charts: npSetup.plugins.charts, -}; - -const startPlugins: Readonly = { - expressions: npStart.plugins.expressions, - visualizations: npStart.plugins.visualizations, -}; - -const pluginInstance = plugin({} as PluginInitializerContext); - -export const setup = pluginInstance.setup(npSetup.core, setupPlugins); -export const start = pluginInstance.start(npStart.core, startPlugins); diff --git a/src/legacy/core_plugins/vis_type_xy/public/plugin.ts b/src/legacy/core_plugins/vis_type_xy/public/plugin.ts deleted file mode 100644 index ab01b6b3153fb..0000000000000 --- a/src/legacy/core_plugins/vis_type_xy/public/plugin.ts +++ /dev/null @@ -1,85 +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 { - CoreSetup, - CoreStart, - Plugin, - IUiSettingsClient, - PluginInitializerContext, -} from 'kibana/public'; - -import { Plugin as ExpressionsPublicPlugin } from '../../../../plugins/expressions/public'; -import { - VisualizationsSetup, - VisualizationsStart, -} from '../../../../plugins/visualizations/public'; -import { ChartsPluginSetup } from '../../../../plugins/charts/public'; - -export interface VisTypeXyDependencies { - uiSettings: IUiSettingsClient; - charts: ChartsPluginSetup; -} - -/** @internal */ -export interface VisTypeXyPluginSetupDependencies { - expressions: ReturnType; - visualizations: VisualizationsSetup; - charts: ChartsPluginSetup; -} - -/** @internal */ -export interface VisTypeXyPluginStartDependencies { - expressions: ReturnType; - visualizations: VisualizationsStart; -} - -type VisTypeXyCoreSetup = CoreSetup; - -/** @internal */ -export class VisTypeXyPlugin implements Plugin { - constructor(public initializerContext: PluginInitializerContext) {} - - public async setup( - core: VisTypeXyCoreSetup, - { expressions, visualizations, charts }: VisTypeXyPluginSetupDependencies - ) { - // eslint-disable-next-line no-console - console.warn( - 'The visTypeXy plugin is enabled\n\n', - 'This may negatively alter existing vislib visualization configurations if saved.' - ); - const visualizationDependencies: Readonly = { - uiSettings: core.uiSettings, - charts, - }; - - const visTypeDefinitions: any[] = []; - const visFunctions: any = []; - - visFunctions.forEach((fn: any) => expressions.registerFunction(fn)); - visTypeDefinitions.forEach((vis: any) => - visualizations.createBaseVisualization(vis(visualizationDependencies)) - ); - } - - public start(core: CoreStart, deps: VisTypeXyPluginStartDependencies) { - // nothing to do here - } -} diff --git a/src/legacy/server/kbn_server.d.ts b/src/legacy/server/kbn_server.d.ts index a9b8c29374854..0d2f3528c9019 100644 --- a/src/legacy/server/kbn_server.d.ts +++ b/src/legacy/server/kbn_server.d.ts @@ -41,10 +41,11 @@ import { // eslint-disable-next-line @kbn/eslint/no-restricted-paths import { LegacyConfig, ILegacyService, ILegacyInternals } from '../../core/server/legacy'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { UiPlugins } from '../../core/server/plugins'; import { ApmOssPlugin } from '../core_plugins/apm_oss'; import { CallClusterWithRequest, ElasticsearchPlugin } from '../core_plugins/elasticsearch'; import { UsageCollectionSetup } from '../../plugins/usage_collection/server'; -import { Capabilities } from '../../core/server'; import { UiSettingsServiceFactoryOptions } from '../../legacy/ui/ui_settings/ui_settings_service_factory'; import { HomeServerPluginSetup } from '../../plugins/home/server'; @@ -111,7 +112,7 @@ export interface KibanaCore { kibanaMigrator: LegacyServiceStartDeps['core']['savedObjects']['migrator']; legacy: ILegacyInternals; rendering: LegacyServiceSetupDeps['core']['rendering']; - uiPlugins: LegacyServiceSetupDeps['core']['plugins']['uiPlugins']; + uiPlugins: UiPlugins; uiSettings: LegacyServiceSetupDeps['core']['uiSettings']; savedObjectsClientProvider: LegacyServiceStartDeps['core']['savedObjects']['clientProvider']; }; diff --git a/src/legacy/server/logging/log_reporter.js b/src/legacy/server/logging/log_reporter.js index 6e62a5ee284e3..b784d03a5b86e 100644 --- a/src/legacy/server/logging/log_reporter.js +++ b/src/legacy/server/logging/log_reporter.js @@ -30,7 +30,7 @@ import { LogInterceptor } from './log_interceptor'; // thrown every time we start the server. // In order to keep using the legacy logger until we remove it I'm just adding // a new hard limit here. -process.stdout.setMaxListeners(15); +process.stdout.setMaxListeners(25); export function getLoggerStream({ events, config }) { const squeeze = new Squeeze(events); diff --git a/src/legacy/server/saved_objects/saved_objects_mixin.js b/src/legacy/server/saved_objects/saved_objects_mixin.js index bcf766231dc9c..3e71e1989ae7a 100644 --- a/src/legacy/server/saved_objects/saved_objects_mixin.js +++ b/src/legacy/server/saved_objects/saved_objects_mixin.js @@ -78,13 +78,8 @@ export function savedObjectsMixin(kbnServer, server) { const provider = kbnServer.newPlatform.__internals.savedObjectsClientProvider; - const importAndExportableTypes = typeRegistry - .getImportableAndExportableTypes() - .map(type => type.name); - const service = { types: visibleTypes, - importAndExportableTypes, SavedObjectsClient, SavedObjectsRepository, getSavedObjectsRepository: createRepository, diff --git a/src/legacy/ui/public/_index.scss b/src/legacy/ui/public/_index.scss index 87006d9347de4..aaed52f8b120a 100644 --- a/src/legacy/ui/public/_index.scss +++ b/src/legacy/ui/public/_index.scss @@ -17,9 +17,3 @@ @import './field_editor/index'; @import './style_compile/index'; @import '../../../plugins/management/public/components/index'; - -// The following are prefixed with "vis" - -// Can't import vis folder here because of cascading issues, it's imported in core_plugins/kibana -// @import './vis/index'; -@import './visualize/index'; diff --git a/src/legacy/ui/public/field_editor/field_editor.js b/src/legacy/ui/public/field_editor/field_editor.js index 43461c4c689be..e90cb110ac304 100644 --- a/src/legacy/ui/public/field_editor/field_editor.js +++ b/src/legacy/ui/public/field_editor/field_editor.js @@ -66,7 +66,7 @@ import { ScriptingHelpFlyout } from './components/scripting_help'; import { FieldFormatEditor } from './components/field_format_editor'; import { FIELD_TYPES_BY_LANG, DEFAULT_FIELD_TYPES } from './constants'; -import { copyField, executeScript, isScriptValid } from './lib'; +import { executeScript, isScriptValid } from './lib'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; @@ -100,7 +100,6 @@ export class FieldEditor extends PureComponent { indexPattern: PropTypes.object.isRequired, field: PropTypes.object.isRequired, helpers: PropTypes.shape({ - Field: PropTypes.func.isRequired, getConfig: PropTypes.func.isRequired, $http: PropTypes.func.isRequired, fieldFormatEditors: PropTypes.object.isRequired, @@ -111,11 +110,7 @@ export class FieldEditor extends PureComponent { constructor(props) { super(props); - const { - field, - indexPattern, - helpers: { Field }, - } = props; + const { field, indexPattern } = props; this.state = { isReady: false, @@ -125,7 +120,7 @@ export class FieldEditor extends PureComponent { fieldTypes: [], fieldTypeFormats: [], existingFieldNames: indexPattern.fields.map(f => f.name), - field: copyField(field, indexPattern, Field), + field: { ...field, format: field.format }, fieldFormatId: undefined, fieldFormatParams: {}, showScriptingHelp: false, @@ -730,7 +725,7 @@ export class FieldEditor extends PureComponent { }; saveField = async () => { - const field = this.state.field.toActualField(); + const field = this.state.field; const { indexPattern } = this.props; const { fieldFormatId } = this.state; diff --git a/src/legacy/ui/public/field_editor/lib/__tests__/copy_field.test.js b/src/legacy/ui/public/field_editor/lib/__tests__/copy_field.test.js deleted file mode 100644 index 2cee45742ab81..0000000000000 --- a/src/legacy/ui/public/field_editor/lib/__tests__/copy_field.test.js +++ /dev/null @@ -1,48 +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 { copyField } from '../copy_field'; - -const field = { - name: 'test_field', - scripted: true, - type: 'number', - lang: 'painless', -}; - -describe('copyField', () => { - it('should copy a field', () => { - const copiedField = copyField(field, {}, {}); - copiedField.name = 'test_name_change'; - - // Check that copied field has `toActualField()` method - expect(typeof copiedField.toActualField).toEqual('function'); - - // Check that we did not modify the original field object when - // modifying copied field - expect(field.toActualField).toEqual(undefined); - expect(field.name).toEqual('test_field'); - - expect(copiedField).not.toEqual(field); - expect(copiedField.name).toEqual('test_name_change'); - expect(copiedField.scripted).toEqual(field.scripted); - expect(copiedField.type).toEqual(field.type); - expect(copiedField.lang).toEqual(field.lang); - }); -}); diff --git a/src/legacy/ui/public/field_editor/lib/copy_field.js b/src/legacy/ui/public/field_editor/lib/copy_field.js deleted file mode 100644 index bfc1cb8480d5d..0000000000000 --- a/src/legacy/ui/public/field_editor/lib/copy_field.js +++ /dev/null @@ -1,82 +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 { has } from 'lodash'; - -/** - * Fully clones a Field object, so that modifications can be performed on - * the copy without affecting original field. Field objects contain - * enumerable and non-eumerable properties that may or may not be writable. - * The function copies all properties as property descriptors into - * `newFieldProps`, overrides getter and setter, and returns a new object - * created from that. - * - * @param {object} field - Field object to copy - * @param {object} indexPattern - index pattern object the field belongs to - * @param {object} Field - Field object type - * @return {object} the cloned object - */ -export const copyField = (field, indexPattern, Field) => { - const changes = {}; - const newFieldProps = { - // When we are ready to save the copied field back into the index pattern, - // we use `toActualField()` to retrieve an actual `Field` type object, using - // its original properties with our "changes" applied. - toActualField: { - value: () => { - return new Field(indexPattern, { - ...field.$$spec, - ...changes, - }); - }, - }, - }; - - // Index pattern `Field` objects are created with custom property - // descriptors using `ObjDefine`. - // - // Each property of a `Field` type object could be enumerable/non-enumerable, - // writable/not writable, configurable/not configurable, and have custom - // getter and setter. We can't use the original `field` object directly for - // creating a new field or editing a new field, since we need all the - // properties to be editable. - // - // A normal copy of `field` (i.e. `const newField = { ...field }`) will only - // copy enumerable properties and copy each property's descriptors (not - // writable, etc). - // - // So we copy `field`'s **property descriptors** into `newFieldProps` - // and modify them so that they are "writable" with a getter/setter that - // stores and retrieves changes into/from another object (`changes`). - Object.getOwnPropertyNames(field).forEach(function(prop) { - const desc = Object.getOwnPropertyDescriptor(field, prop); - - newFieldProps[prop] = { - enumerable: desc.enumerable, - get: function() { - return has(changes, prop) ? changes[prop] : field[prop]; - }, - set: function(v) { - changes[prop] = v; - }, - }; - }); - - return Object.create(null, newFieldProps); -}; diff --git a/src/legacy/ui/public/field_editor/lib/index.js b/src/legacy/ui/public/field_editor/lib/index.js index c74bb0cc2ef8a..c9dd9d03b74f7 100644 --- a/src/legacy/ui/public/field_editor/lib/index.js +++ b/src/legacy/ui/public/field_editor/lib/index.js @@ -17,5 +17,4 @@ * under the License. */ -export { copyField } from './copy_field'; export { executeScript, isScriptValid } from './validate_script'; diff --git a/src/legacy/ui/public/new_platform/new_platform.karma_mock.js b/src/legacy/ui/public/new_platform/new_platform.karma_mock.js index 0779d6472671c..f14f26613ef01 100644 --- a/src/legacy/ui/public/new_platform/new_platform.karma_mock.js +++ b/src/legacy/ui/public/new_platform/new_platform.karma_mock.js @@ -242,6 +242,7 @@ export const npSetup = { }, kibanaLegacy: { registerLegacyApp: () => {}, + registerLegacyAppAlias: () => {}, forwardApp: () => {}, config: { defaultAppId: 'home', @@ -309,6 +310,12 @@ export const npSetup = { registerAlias: sinon.fake(), hideTypes: sinon.fake(), }, + + mapsLegacy: { + serviceSettings: sinon.fake(), + getPrecision: sinon.fake(), + getZoomPrecision: sinon.fake(), + }, }, }; @@ -356,6 +363,7 @@ export const npStart = { kibanaLegacy: { getApps: () => [], getForwards: () => [], + getLegacyAppAliases: () => [], config: { defaultAppId: 'home', }, @@ -447,6 +455,7 @@ export const npStart = { createAggConfigs: (indexPattern, configStates = []) => { return new AggConfigs(indexPattern, configStates, { typesRegistry: aggTypesRegistry.start(), + fieldFormats: getFieldFormatsRegistry(mockCoreStart), }); }, types: aggTypesRegistry.start(), diff --git a/src/legacy/ui/public/new_platform/new_platform.ts b/src/legacy/ui/public/new_platform/new_platform.ts index cdd7e1a994912..5ae2e2348aaa1 100644 --- a/src/legacy/ui/public/new_platform/new_platform.ts +++ b/src/legacy/ui/public/new_platform/new_platform.ts @@ -22,6 +22,7 @@ import { IScope } from 'angular'; import { UiActionsStart, UiActionsSetup } from 'src/plugins/ui_actions/public'; import { EmbeddableStart, EmbeddableSetup } from 'src/plugins/embeddable/public'; import { createBrowserHistory } from 'history'; +import { VisTypeXyPluginSetup } from 'src/plugins/vis_type_xy/public'; import { DashboardStart } from '../../../../plugins/dashboard/public'; import { setSetupServices, setStartServices } from './set_services'; import { @@ -68,6 +69,8 @@ import { VisualizationsSetup, VisualizationsStart, } from '../../../../plugins/visualizations/public'; +import { VisTypeTimelionPluginStart } from '../../../../plugins/vis_type_timelion/public'; +import { MapsLegacyPluginSetup } from '../../../../plugins/maps_legacy/public'; export interface PluginsSetup { bfetch: BfetchPublicSetup; @@ -90,7 +93,9 @@ export interface PluginsSetup { visualizations: VisualizationsSetup; telemetry?: TelemetryPluginSetup; savedObjectsManagement: SavedObjectsManagementPluginSetup; + mapsLegacy: MapsLegacyPluginSetup; indexPatternManagement: IndexPatternManagementSetup; + visTypeXy?: VisTypeXyPluginSetup; } export interface PluginsStart { @@ -112,6 +117,7 @@ export interface PluginsStart { telemetry?: TelemetryPluginStart; dashboard: DashboardStart; savedObjectsManagement: SavedObjectsManagementPluginStart; + visTypeTimelion: VisTypeTimelionPluginStart; indexPatternManagement: IndexPatternManagementStart; } diff --git a/src/legacy/ui/public/scripting_languages/index.js b/src/legacy/ui/public/scripting_languages/index.js deleted file mode 100644 index 2f43a44d66068..0000000000000 --- a/src/legacy/ui/public/scripting_languages/index.js +++ /dev/null @@ -1,46 +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 chrome from '../chrome'; -import { toastNotifications } from '../notify'; -import { i18n } from '@kbn/i18n'; - -export function getSupportedScriptingLanguages() { - return ['painless']; -} - -export function getDeprecatedScriptingLanguages() { - return []; -} - -export function GetEnabledScriptingLanguagesProvider($http) { - return () => { - return $http - .get(chrome.addBasePath('/api/kibana/scripts/languages')) - .then(res => res.data) - .catch(() => { - toastNotifications.addDanger( - i18n.translate('common.ui.scriptingLanguages.errorFetchingToastDescription', { - defaultMessage: 'Error getting available scripting languages from Elasticsearch', - }) - ); - return []; - }); - }; -} diff --git a/src/legacy/ui/public/scripting_languages/index.ts b/src/legacy/ui/public/scripting_languages/index.ts new file mode 100644 index 0000000000000..283a3273a2a5d --- /dev/null +++ b/src/legacy/ui/public/scripting_languages/index.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. + */ + +import { IHttpService } from 'angular'; +import { i18n } from '@kbn/i18n'; + +import chrome from '../chrome'; +import { toastNotifications } from '../notify'; + +export function getSupportedScriptingLanguages(): string[] { + return ['painless']; +} + +export function getDeprecatedScriptingLanguages(): string[] { + return []; +} + +export function GetEnabledScriptingLanguagesProvider($http: IHttpService) { + return () => { + return $http + .get(chrome.addBasePath('/api/kibana/scripts/languages')) + .then((res: any) => res.data) + .catch(() => { + toastNotifications.addDanger( + i18n.translate('common.ui.scriptingLanguages.errorFetchingToastDescription', { + defaultMessage: 'Error getting available scripting languages from Elasticsearch', + }) + ); + return []; + }); + }; +} diff --git a/src/legacy/ui/public/vis/__tests__/map/service_settings.js b/src/legacy/ui/public/vis/__tests__/map/service_settings.js deleted file mode 100644 index 61925760457c6..0000000000000 --- a/src/legacy/ui/public/vis/__tests__/map/service_settings.js +++ /dev/null @@ -1,348 +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 expect from '@kbn/expect'; -import ngMock from 'ng_mock'; -import url from 'url'; - -import EMS_FILES from './ems_mocks/sample_files.json'; -import EMS_TILES from './ems_mocks/sample_tiles.json'; -import EMS_STYLE_ROAD_MAP_BRIGHT from './ems_mocks/sample_style_bright'; -import EMS_STYLE_ROAD_MAP_DESATURATED from './ems_mocks/sample_style_desaturated'; -import EMS_STYLE_DARK_MAP from './ems_mocks/sample_style_dark'; -import { ORIGIN } from '../../../../../core_plugins/tile_map/common/origin'; - -describe('service_settings (FKA tilemaptest)', function() { - let serviceSettings; - let mapConfig; - let tilemapsConfig; - - const emsFileApiUrl = 'https://files.foobar'; - const emsTileApiUrl = 'https://tiles.foobar'; - - const emsTileApiUrl2 = 'https://tiles_override.foobar'; - const emsFileApiUrl2 = 'https://files_override.foobar'; - - beforeEach( - ngMock.module('kibana', $provide => { - $provide.decorator('mapConfig', () => { - return { - emsFileApiUrl, - emsTileApiUrl, - includeElasticMapsService: true, - emsTileLayerId: { - bright: 'road_map', - desaturated: 'road_map_desaturated', - dark: 'dark_map', - }, - }; - }); - }) - ); - - let emsTileApiUrlOriginal; - let emsFileApiUrlOriginal; - let tilemapsConfigDeprecatedOriginal; - let getManifestStub; - beforeEach( - ngMock.inject(function($injector, $rootScope) { - serviceSettings = $injector.get('serviceSettings'); - getManifestStub = serviceSettings.__debugStubManifestCalls(async url => { - //simulate network calls - if (url.startsWith('https://tiles.foobar')) { - if (url.includes('/manifest')) { - return EMS_TILES; - } else if (url.includes('osm-bright-desaturated.json')) { - return EMS_STYLE_ROAD_MAP_DESATURATED; - } else if (url.includes('osm-bright.json')) { - return EMS_STYLE_ROAD_MAP_BRIGHT; - } else if (url.includes('dark-matter.json')) { - return EMS_STYLE_DARK_MAP; - } - } else if (url.startsWith('https://files.foobar')) { - return EMS_FILES; - } - }); - mapConfig = $injector.get('mapConfig'); - tilemapsConfig = $injector.get('tilemapsConfig'); - - emsTileApiUrlOriginal = mapConfig.emsTileApiUrl; - emsFileApiUrlOriginal = mapConfig.emsFileApiUrl; - - tilemapsConfigDeprecatedOriginal = tilemapsConfig.deprecated; - $rootScope.$digest(); - }) - ); - - afterEach(function() { - getManifestStub.removeStub(); - mapConfig.emsTileApiUrl = emsTileApiUrlOriginal; - mapConfig.emsFileApiUrl = emsFileApiUrlOriginal; - tilemapsConfig.deprecated = tilemapsConfigDeprecatedOriginal; - }); - - describe('TMS', function() { - it('should NOT get url from the config', async function() { - const tmsServices = await serviceSettings.getTMSServices(); - const tmsService = tmsServices[0]; - expect(typeof tmsService.url === 'undefined').to.equal(true); - }); - - it('should get url by resolving dynamically', async function() { - const tmsServices = await serviceSettings.getTMSServices(); - const tmsService = tmsServices[0]; - expect(typeof tmsService.url === 'undefined').to.equal(true); - - const attrs = await serviceSettings.getAttributesForTMSLayer(tmsService); - expect(attrs.url).to.contain('{x}'); - expect(attrs.url).to.contain('{y}'); - expect(attrs.url).to.contain('{z}'); - - const urlObject = url.parse(attrs.url, true); - expect(urlObject.hostname).to.be('tiles.foobar'); - expect(urlObject.query).to.have.property('my_app_name', 'kibana'); - expect(urlObject.query).to.have.property('elastic_tile_service_tos', 'agree'); - expect(urlObject.query).to.have.property('my_app_version'); - }); - - it('should get options', async function() { - const tmsServices = await serviceSettings.getTMSServices(); - const tmsService = tmsServices[0]; - expect(tmsService).to.have.property('minZoom'); - expect(tmsService).to.have.property('maxZoom'); - expect(tmsService) - .to.have.property('attribution') - .contain('OpenStreetMap'); - }); - - describe('modify - url', function() { - let tilemapServices; - - async function assertQuery(expected) { - const attrs = await serviceSettings.getAttributesForTMSLayer(tilemapServices[0]); - const urlObject = url.parse(attrs.url, true); - Object.keys(expected).forEach(key => { - expect(urlObject.query).to.have.property(key, expected[key]); - }); - } - - it('accepts an object', async () => { - serviceSettings.addQueryParams({ foo: 'bar' }); - tilemapServices = await serviceSettings.getTMSServices(); - await assertQuery({ foo: 'bar' }); - }); - - it('merged additions with previous values', async () => { - // ensure that changes are always additive - serviceSettings.addQueryParams({ foo: 'bar' }); - serviceSettings.addQueryParams({ bar: 'stool' }); - tilemapServices = await serviceSettings.getTMSServices(); - await assertQuery({ foo: 'bar', bar: 'stool' }); - }); - - it('overwrites conflicting previous values', async () => { - // ensure that conflicts are overwritten - serviceSettings.addQueryParams({ foo: 'bar' }); - serviceSettings.addQueryParams({ bar: 'stool' }); - serviceSettings.addQueryParams({ foo: 'tstool' }); - tilemapServices = await serviceSettings.getTMSServices(); - await assertQuery({ foo: 'tstool', bar: 'stool' }); - }); - - it('when overridden, should continue to work', async () => { - mapConfig.emsFileApiUrl = emsFileApiUrl2; - mapConfig.emsTileApiUrl = emsTileApiUrl2; - serviceSettings.addQueryParams({ foo: 'bar' }); - tilemapServices = await serviceSettings.getTMSServices(); - await assertQuery({ foo: 'bar' }); - }); - - it('should merge in tilemap url', async () => { - tilemapsConfig.deprecated = { - isOverridden: true, - config: { - url: 'https://a.tile.openstreetmap.org/{z}/{x}/{y}.png', - options: { minZoom: 0, maxZoom: 20 }, - }, - }; - - tilemapServices = await serviceSettings.getTMSServices(); - const expected = [ - { - attribution: '', - url: 'https://a.tile.openstreetmap.org/{z}/{x}/{y}.png', - id: 'TMS in config/kibana.yml', - }, - { - id: 'road_map', - name: 'Road Map - Bright', - url: - 'https://tiles.foobar/raster/styles/osm-bright/{z}/{x}/{y}.png?elastic_tile_service_tos=agree&my_app_name=kibana&my_app_version=1.2.3', - minZoom: 0, - maxZoom: 10, - attribution: - 'OpenStreetMap contributors | OpenMapTiles | MapTiler | Elastic Maps Service', - subdomains: [], - }, - ]; - - const assertions = tilemapServices.map(async (actualService, index) => { - const expectedService = expected[index]; - expect(actualService.id).to.equal(expectedService.id); - expect(actualService.attribution).to.equal(expectedService.attribution); - const attrs = await serviceSettings.getAttributesForTMSLayer(actualService); - expect(attrs.url).to.equal(expectedService.url); - }); - - return Promise.all(assertions); - }); - - it('should load appropriate EMS attributes for desaturated and dark theme', async () => { - tilemapServices = await serviceSettings.getTMSServices(); - const roadMapService = tilemapServices.find(service => service.id === 'road_map'); - - const desaturationFalse = await serviceSettings.getAttributesForTMSLayer( - roadMapService, - false, - false - ); - const desaturationTrue = await serviceSettings.getAttributesForTMSLayer( - roadMapService, - true, - false - ); - const darkThemeDesaturationFalse = await serviceSettings.getAttributesForTMSLayer( - roadMapService, - false, - true - ); - const darkThemeDesaturationTrue = await serviceSettings.getAttributesForTMSLayer( - roadMapService, - true, - true - ); - - expect(desaturationFalse.url).to.equal( - 'https://tiles.foobar/raster/styles/osm-bright/{z}/{x}/{y}.png?elastic_tile_service_tos=agree&my_app_name=kibana&my_app_version=1.2.3' - ); - expect(desaturationFalse.maxZoom).to.equal(10); - expect(desaturationTrue.url).to.equal( - 'https://tiles.foobar/raster/styles/osm-bright-desaturated/{z}/{x}/{y}.png?elastic_tile_service_tos=agree&my_app_name=kibana&my_app_version=1.2.3' - ); - expect(desaturationTrue.maxZoom).to.equal(18); - expect(darkThemeDesaturationFalse.url).to.equal( - 'https://tiles.foobar/raster/styles/dark-matter/{z}/{x}/{y}.png?elastic_tile_service_tos=agree&my_app_name=kibana&my_app_version=1.2.3' - ); - expect(darkThemeDesaturationFalse.maxZoom).to.equal(22); - expect(darkThemeDesaturationTrue.url).to.equal( - 'https://tiles.foobar/raster/styles/dark-matter/{z}/{x}/{y}.png?elastic_tile_service_tos=agree&my_app_name=kibana&my_app_version=1.2.3' - ); - expect(darkThemeDesaturationTrue.maxZoom).to.equal(22); - }); - - it('should exclude EMS', async () => { - tilemapsConfig.deprecated = { - isOverridden: true, - config: { - url: 'https://a.tile.openstreetmap.org/{z}/{x}/{y}.png', - options: { minZoom: 0, maxZoom: 20 }, - }, - }; - mapConfig.includeElasticMapsService = false; - - tilemapServices = await serviceSettings.getTMSServices(); - const expected = [ - { - attribution: '', - url: 'https://a.tile.openstreetmap.org/{z}/{x}/{y}.png', - id: 'TMS in config/kibana.yml', - }, - ]; - expect(tilemapServices.length).to.eql(1); - expect(tilemapServices[0].attribution).to.eql(expected[0].attribution); - expect(tilemapServices[0].id).to.eql(expected[0].id); - const attrs = await serviceSettings.getAttributesForTMSLayer(tilemapServices[0]); - expect(attrs.url).to.equal(expected[0].url); - }); - - it('should exclude all when not configured', async () => { - mapConfig.includeElasticMapsService = false; - tilemapServices = await serviceSettings.getTMSServices(); - const expected = []; - expect(tilemapServices).to.eql(expected); - }); - }); - }); - - describe('File layers', function() { - it('should load manifest (all props)', async function() { - serviceSettings.addQueryParams({ foo: 'bar' }); - const fileLayers = await serviceSettings.getFileLayers(); - expect(fileLayers.length).to.be(18); - const assertions = fileLayers.map(async function(fileLayer) { - expect(fileLayer.origin).to.be(ORIGIN.EMS); - const fileUrl = await serviceSettings.getUrlForRegionLayer(fileLayer); - const urlObject = url.parse(fileUrl, true); - Object.keys({ foo: 'bar', elastic_tile_service_tos: 'agree' }).forEach(key => { - expect(urlObject.query).to.have.property(key); - }); - }); - - return Promise.all(assertions); - }); - - it('should load manifest (individual props)', async () => { - const expected = { - attribution: - 'Made with NaturalEarth | Elastic Maps Service', - format: 'geojson', - fields: [ - { type: 'id', name: 'iso2', description: 'ISO 3166-1 alpha-2 code' }, - { type: 'id', name: 'iso3', description: 'ISO 3166-1 alpha-3 code' }, - { type: 'property', name: 'name', description: 'name' }, - ], - created_at: '2017-04-26T17:12:15.978370', //not present in 6.6 - name: 'World Countries', - }; - - const fileLayers = await serviceSettings.getFileLayers(); - const actual = fileLayers[0]; - - expect(expected.attribution).to.eql(actual.attribution); - expect(expected.format).to.eql(actual.format); - expect(expected.fields).to.eql(actual.fields); - expect(expected.name).to.eql(actual.name); - - expect(expected.created_at).to.eql(actual.created_at); - }); - - it('should exclude all when not configured', async () => { - mapConfig.includeElasticMapsService = false; - const fileLayers = await serviceSettings.getFileLayers(); - const expected = []; - expect(fileLayers).to.eql(expected); - }); - - it('should get hotlink', async () => { - const fileLayers = await serviceSettings.getFileLayers(); - const hotlink = await serviceSettings.getEMSHotLink(fileLayers[0]); - expect(hotlink).to.eql('?locale=en#file/world_countries'); //url host undefined becuase emsLandingPageUrl is set at kibana-load - }); - }); -}); diff --git a/src/legacy/ui/public/vis/map/service_settings.js b/src/legacy/ui/public/vis/map/service_settings.js deleted file mode 100644 index a014aeb182c67..0000000000000 --- a/src/legacy/ui/public/vis/map/service_settings.js +++ /dev/null @@ -1,256 +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 { uiModules } from '../../modules'; -import _ from 'lodash'; -import MarkdownIt from 'markdown-it'; -import { ORIGIN } from '../../../../core_plugins/tile_map/common/origin'; -import { EMSClient } from '@elastic/ems-client'; -import { i18n } from '@kbn/i18n'; -import 'angular-sanitize'; - -const markdownIt = new MarkdownIt({ - html: false, - linkify: true, -}); - -const TMS_IN_YML_ID = 'TMS in config/kibana.yml'; - -uiModules - .get('kibana', ['ngSanitize']) - .service('serviceSettings', function($sanitize, mapConfig, tilemapsConfig, kbnVersion) { - const attributionFromConfig = $sanitize( - markdownIt.render(tilemapsConfig.deprecated.config.options.attribution || '') - ); - const tmsOptionsFromConfig = _.assign({}, tilemapsConfig.deprecated.config.options, { - attribution: attributionFromConfig, - }); - - class ServiceSettings { - constructor() { - this._showZoomMessage = true; - this._emsClient = new EMSClient({ - language: i18n.getLocale(), - appVersion: kbnVersion, - appName: 'kibana', - fileApiUrl: mapConfig.emsFileApiUrl, - tileApiUrl: mapConfig.emsTileApiUrl, - htmlSanitizer: $sanitize, - landingPageUrl: mapConfig.emsLandingPageUrl, - // Wrap to avoid errors passing window fetch - fetchFunction: function(...args) { - return fetch(...args); - }, - }); - } - - shouldShowZoomMessage({ origin }) { - return origin === ORIGIN.EMS && this._showZoomMessage; - } - - disableZoomMessage() { - this._showZoomMessage = false; - } - - __debugStubManifestCalls(manifestRetrieval) { - const oldGetManifest = this._emsClient.getManifest; - this._emsClient.getManifest = manifestRetrieval; - return { - removeStub: () => { - delete this._emsClient.getManifest; - //not strictly necessary since this is prototype method - if (this._emsClient.getManifest !== oldGetManifest) { - this._emsClient.getManifest = oldGetManifest; - } - }, - }; - } - - async getFileLayers() { - if (!mapConfig.includeElasticMapsService) { - return []; - } - - const fileLayers = await this._emsClient.getFileLayers(); - return fileLayers.map(fileLayer => { - //backfill to older settings - const format = fileLayer.getDefaultFormatType(); - const meta = fileLayer.getDefaultFormatMeta(); - - return { - name: fileLayer.getDisplayName(), - origin: fileLayer.getOrigin(), - id: fileLayer.getId(), - created_at: fileLayer.getCreatedAt(), - attribution: fileLayer.getHTMLAttribution(), - fields: fileLayer.getFieldsInLanguage(), - format: format, //legacy: format and meta are split up - meta: meta, //legacy, format and meta are split up - }; - }); - } - - /** - * Returns all the services published by EMS (if configures) - * It also includes the service configured in tilemap (override) - */ - async getTMSServices() { - let allServices = []; - if (tilemapsConfig.deprecated.isOverridden) { - //use tilemap.* settings from yml - const tmsService = _.cloneDeep(tmsOptionsFromConfig); - tmsService.id = TMS_IN_YML_ID; - tmsService.origin = ORIGIN.KIBANA_YML; - allServices.push(tmsService); - } - - if (mapConfig.includeElasticMapsService) { - const servicesFromManifest = await this._emsClient.getTMSServices(); - const strippedServiceFromManifest = await Promise.all( - servicesFromManifest - .filter(tmsService => tmsService.getId() === mapConfig.emsTileLayerId.bright) - .map(async tmsService => { - //shim for compatibility - const shim = { - origin: tmsService.getOrigin(), - id: tmsService.getId(), - minZoom: await tmsService.getMinZoom(), - maxZoom: await tmsService.getMaxZoom(), - attribution: tmsService.getHTMLAttribution(), - }; - return shim; - }) - ); - allServices = allServices.concat(strippedServiceFromManifest); - } - - return allServices; - } - - /** - * Add optional query-parameters to all requests - * - * @param additionalQueryParams - */ - addQueryParams(additionalQueryParams) { - this._emsClient.addQueryParams(additionalQueryParams); - } - - async getEMSHotLink(fileLayerConfig) { - const fileLayers = await this._emsClient.getFileLayers(); - const layer = fileLayers.find(fileLayer => { - const hasIdByName = fileLayer.hasId(fileLayerConfig.name); //legacy - const hasIdById = fileLayer.hasId(fileLayerConfig.id); - return hasIdByName || hasIdById; - }); - return layer ? layer.getEMSHotLink() : null; - } - - async _getAttributesForEMSTMSLayer(isDesaturated, isDarkMode) { - const tmsServices = await this._emsClient.getTMSServices(); - const emsTileLayerId = mapConfig.emsTileLayerId; - let serviceId; - if (isDarkMode) { - serviceId = emsTileLayerId.dark; - } else { - if (isDesaturated) { - serviceId = emsTileLayerId.desaturated; - } else { - serviceId = emsTileLayerId.bright; - } - } - const tmsService = tmsServices.find(service => { - return service.getId() === serviceId; - }); - return { - url: await tmsService.getUrlTemplate(), - minZoom: await tmsService.getMinZoom(), - maxZoom: await tmsService.getMaxZoom(), - attribution: await tmsService.getHTMLAttribution(), - origin: ORIGIN.EMS, - }; - } - - async getAttributesForTMSLayer(tmsServiceConfig, isDesaturated, isDarkMode) { - if (tmsServiceConfig.origin === ORIGIN.EMS) { - return this._getAttributesForEMSTMSLayer(isDesaturated, isDarkMode); - } else if (tmsServiceConfig.origin === ORIGIN.KIBANA_YML) { - const config = tilemapsConfig.deprecated.config; - const attrs = _.pick(config, ['url', 'minzoom', 'maxzoom', 'attribution']); - return { ...attrs, ...{ origin: ORIGIN.KIBANA_YML } }; - } else { - //this is an older config. need to resolve this dynamically. - if (tmsServiceConfig.id === TMS_IN_YML_ID) { - const config = tilemapsConfig.deprecated.config; - const attrs = _.pick(config, ['url', 'minzoom', 'maxzoom', 'attribution']); - return { ...attrs, ...{ origin: ORIGIN.KIBANA_YML } }; - } else { - //assume ems - return this._getAttributesForEMSTMSLayer(isDesaturated, isDarkMode); - } - } - } - - async _getFileUrlFromEMS(fileLayerConfig) { - const fileLayers = await this._emsClient.getFileLayers(); - const layer = fileLayers.find(fileLayer => { - const hasIdByName = fileLayer.hasId(fileLayerConfig.name); //legacy - const hasIdById = fileLayer.hasId(fileLayerConfig.id); - return hasIdByName || hasIdById; - }); - - if (layer) { - return layer.getDefaultFormatUrl(); - } else { - throw new Error(`File ${fileLayerConfig.name} not recognized`); - } - } - - async getUrlForRegionLayer(fileLayerConfig) { - let url; - if (fileLayerConfig.origin === ORIGIN.EMS) { - url = this._getFileUrlFromEMS(fileLayerConfig); - } else if ( - fileLayerConfig.layerId && - fileLayerConfig.layerId.startsWith(`${ORIGIN.EMS}.`) - ) { - //fallback for older saved objects - url = this._getFileUrlFromEMS(fileLayerConfig); - } else if ( - fileLayerConfig.layerId && - fileLayerConfig.layerId.startsWith(`${ORIGIN.KIBANA_YML}.`) - ) { - //fallback for older saved objects - url = fileLayerConfig.url; - } else { - //generic fallback - url = fileLayerConfig.url; - } - return url; - } - - async getJsonForRegionLayer(fileLayerConfig) { - const url = await this.getUrlForRegionLayer(fileLayerConfig); - const response = await fetch(url); - return await response.json(); - } - } - - return new ServiceSettings(); - }); diff --git a/src/legacy/ui/public/visualize/_index.scss b/src/legacy/ui/public/visualize/_index.scss deleted file mode 100644 index d9761f741353b..0000000000000 --- a/src/legacy/ui/public/visualize/_index.scss +++ /dev/null @@ -1 +0,0 @@ -@import '../../../../plugins/visualizations/public/components/index'; diff --git a/src/legacy/core_plugins/vis_type_vislib/public/components/common/basic_options.tsx b/src/plugins/charts/public/static/components/basic_options.tsx similarity index 90% rename from src/legacy/core_plugins/vis_type_vislib/public/components/common/basic_options.tsx rename to src/plugins/charts/public/static/components/basic_options.tsx index baf3e8ecd1b28..cac4c8d70d796 100644 --- a/src/legacy/core_plugins/vis_type_vislib/public/components/common/basic_options.tsx +++ b/src/plugins/charts/public/static/components/basic_options.tsx @@ -37,7 +37,7 @@ function BasicOptions({ return ( <> ({ setValue={setValue} /> ; + +export const Rotates = Object.freeze({ + HORIZONTAL: 0, + VERTICAL: 90, + ANGLED: 75, +}); +export type Rotates = $Values; diff --git a/src/legacy/core_plugins/vis_type_vislib/public/components/common/color_ranges.tsx b/src/plugins/charts/public/static/components/color_ranges.tsx similarity index 93% rename from src/legacy/core_plugins/vis_type_vislib/public/components/common/color_ranges.tsx rename to src/plugins/charts/public/static/components/color_ranges.tsx index 84c70f10b12da..a9b05d7d91c7c 100644 --- a/src/legacy/core_plugins/vis_type_vislib/public/components/common/color_ranges.tsx +++ b/src/plugins/charts/public/static/components/color_ranges.tsx @@ -22,10 +22,7 @@ import { last } from 'lodash'; import { i18n } from '@kbn/i18n'; -import { - RangeValues, - RangesParamEditor, -} from '../../../../../../plugins/vis_default_editor/public'; +import { RangeValues, RangesParamEditor } from '../../../../vis_default_editor/public'; export type SetColorRangeValue = (paramName: string, value: RangeValues[]) => void; @@ -74,7 +71,7 @@ function ColorRanges({ return ( ( +export type SetColorSchemaOptionsValue = ( paramName: T, - value: ColorSchemaVislibParams[T] + value: ColorSchemaParams[T] ) => void; -interface ColorSchemaOptionsProps extends ColorSchemaVislibParams { +interface ColorSchemaOptionsProps extends ColorSchemaParams { disabled?: boolean; colorSchemas: ColorSchema[]; uiState: VisOptionsProps['uiState']; @@ -67,7 +67,7 @@ function ColorSchemaOptions({ }} > @@ -80,11 +80,11 @@ function ColorSchemaOptions({ disabled={disabled} helpText={ showHelpText && - i18n.translate('visTypeVislib.controls.colorSchema.howToChangeColorsDescription', { + i18n.translate('charts.controls.colorSchema.howToChangeColorsDescription', { defaultMessage: 'Individual colors can be changed in the legend.', }) } - label={i18n.translate('visTypeVislib.controls.colorSchema.colorSchemaLabel', { + label={i18n.translate('charts.controls.colorSchema.colorSchemaLabel', { defaultMessage: 'Color schema', })} labelAppend={isCustomColors && resetColorsButton} @@ -96,7 +96,7 @@ function ColorSchemaOptions({ ({ const [stateValue, setStateValue] = useState(value); const [isValidState, setIsValidState] = useState(true); - const error = i18n.translate('visTypeVislib.controls.rangeErrorMessage', { + const error = i18n.translate('charts.controls.rangeErrorMessage', { defaultMessage: 'Values must be on or between {min} and {max}', values: { min, max }, }); diff --git a/src/legacy/core_plugins/vis_type_vislib/public/components/common/required_number_input.tsx b/src/plugins/charts/public/static/components/required_number_input.tsx similarity index 87% rename from src/legacy/core_plugins/vis_type_vislib/public/components/common/required_number_input.tsx rename to src/plugins/charts/public/static/components/required_number_input.tsx index 7b62016c4e502..7594c775b07ad 100644 --- a/src/legacy/core_plugins/vis_type_vislib/public/components/common/required_number_input.tsx +++ b/src/plugins/charts/public/static/components/required_number_input.tsx @@ -17,9 +17,8 @@ * under the License. */ -import React, { ReactNode, useCallback, ChangeEvent } from 'react'; +import React, { ReactNode, useCallback, ChangeEvent, useEffect } from 'react'; import { EuiFormRow, EuiFieldNumber } from '@elastic/eui'; -import { useValidation } from './utils'; interface NumberInputOptionProps { disabled?: boolean; @@ -42,7 +41,7 @@ interface NumberInputOptionProps { * * @param {number} props.value Should be numeric only */ -function NumberInputOption({ +function RequiredNumberInputOption({ disabled, error, isInvalid, @@ -57,7 +56,12 @@ function NumberInputOption({ 'data-test-subj': dataTestSubj, }: NumberInputOptionProps) { const isValid = value !== null; - useValidation(setValidity, paramName, isValid); + + useEffect(() => { + setValidity(paramName, isValid); + + return () => setValidity(paramName, true); + }, [isValid, paramName, setValidity]); const onChange = useCallback( (ev: ChangeEvent) => @@ -84,4 +88,4 @@ function NumberInputOption({ ); } -export { NumberInputOption }; +export { RequiredNumberInputOption }; diff --git a/src/legacy/core_plugins/vis_type_vislib/public/components/common/select.tsx b/src/plugins/charts/public/static/components/select.tsx similarity index 100% rename from src/legacy/core_plugins/vis_type_vislib/public/components/common/select.tsx rename to src/plugins/charts/public/static/components/select.tsx diff --git a/src/legacy/core_plugins/vis_type_vislib/public/components/common/switch.tsx b/src/plugins/charts/public/static/components/switch.tsx similarity index 100% rename from src/legacy/core_plugins/vis_type_vislib/public/components/common/switch.tsx rename to src/plugins/charts/public/static/components/switch.tsx diff --git a/src/legacy/core_plugins/vis_type_vislib/public/components/common/text_input.tsx b/src/plugins/charts/public/static/components/text_input.tsx similarity index 100% rename from src/legacy/core_plugins/vis_type_vislib/public/components/common/text_input.tsx rename to src/plugins/charts/public/static/components/text_input.tsx diff --git a/src/plugins/charts/public/static/components/types.ts b/src/plugins/charts/public/static/components/types.ts new file mode 100644 index 0000000000000..196eb60b06aec --- /dev/null +++ b/src/plugins/charts/public/static/components/types.ts @@ -0,0 +1,43 @@ +/* + * 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 { ColorSchemas } from '../color_maps'; +import { Rotates } from './collections'; + +export interface ColorSchemaParams { + colorSchema: ColorSchemas; + invertColors: boolean; +} + +export interface Labels { + color?: string; + filter?: boolean; + overwriteColor?: boolean; + rotate?: Rotates; + show: boolean; + truncate?: number | null; +} + +export interface Style { + bgFill: string; + bgColor: boolean; + labelColor: boolean; + subText: string; + fontSize: number; +} diff --git a/src/plugins/charts/public/static/index.ts b/src/plugins/charts/public/static/index.ts index bee58e4f1e3e1..6fc097d05467f 100644 --- a/src/plugins/charts/public/static/index.ts +++ b/src/plugins/charts/public/static/index.ts @@ -18,3 +18,4 @@ */ export * from './color_maps'; +export * from './components'; diff --git a/src/plugins/console/public/application/containers/editor/legacy/console_editor/editor_output.tsx b/src/plugins/console/public/application/containers/editor/legacy/console_editor/editor_output.tsx index 36d90bb6bff1a..8510aaebfaa1e 100644 --- a/src/plugins/console/public/application/containers/editor/legacy/console_editor/editor_output.tsx +++ b/src/plugins/console/public/application/containers/editor/legacy/console_editor/editor_output.tsx @@ -20,7 +20,7 @@ import { EuiScreenReaderOnly } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import React, { useEffect, useRef } from 'react'; -import { expandLiteralStrings } from '../../../../../../../es_ui_shared/console_lang/lib'; +import { expandLiteralStrings } from '../../../../../../../es_ui_shared/public'; import { useEditorReadContext, useRequestReadContext, diff --git a/src/plugins/console/public/application/hooks/use_send_current_request_to_es/send_request_to_es.ts b/src/plugins/console/public/application/hooks/use_send_current_request_to_es/send_request_to_es.ts index 102f90a9feb6f..cfbd5691bc22b 100644 --- a/src/plugins/console/public/application/hooks/use_send_current_request_to_es/send_request_to_es.ts +++ b/src/plugins/console/public/application/hooks/use_send_current_request_to_es/send_request_to_es.ts @@ -18,7 +18,7 @@ */ import { extractDeprecationMessages } from '../../../lib/utils'; -import { collapseLiteralStrings } from '../../../../../es_ui_shared/console_lang/lib'; +import { collapseLiteralStrings } from '../../../../../es_ui_shared/public'; // @ts-ignore import * as es from '../../../lib/es/es'; import { BaseResponseType } from '../../../types'; diff --git a/src/plugins/console/public/application/models/legacy_core_editor/__tests__/output_tokenization.test.js b/src/plugins/console/public/application/models/legacy_core_editor/__tests__/output_tokenization.test.js index 6a93d007a6cdc..5c86b0a1d2092 100644 --- a/src/plugins/console/public/application/models/legacy_core_editor/__tests__/output_tokenization.test.js +++ b/src/plugins/console/public/application/models/legacy_core_editor/__tests__/output_tokenization.test.js @@ -16,6 +16,7 @@ * specific language governing permissions and limitations * under the License. */ +import '../legacy_core_editor.test.mocks'; const $ = require('jquery'); import RowParser from '../../../../lib/row_parser'; import ace from 'brace'; diff --git a/src/plugins/console/public/application/models/legacy_core_editor/mode/input_highlight_rules.js b/src/plugins/console/public/application/models/legacy_core_editor/mode/input_highlight_rules.js index 2c1b30f806f95..29f192f4ea858 100644 --- a/src/plugins/console/public/application/models/legacy_core_editor/mode/input_highlight_rules.js +++ b/src/plugins/console/public/application/models/legacy_core_editor/mode/input_highlight_rules.js @@ -18,7 +18,7 @@ */ const ace = require('brace'); -import { addXJsonToRules } from '../../../../../../es_ui_shared/console_lang'; +import { addXJsonToRules } from '../../../../../../es_ui_shared/public'; export function addEOL(tokens, reg, nextIfEOL, normalNext) { if (typeof reg === 'object') { diff --git a/src/plugins/console/public/application/models/legacy_core_editor/mode/output_highlight_rules.js b/src/plugins/console/public/application/models/legacy_core_editor/mode/output_highlight_rules.js index e27222ebd65e9..c9d538ab6ceb1 100644 --- a/src/plugins/console/public/application/models/legacy_core_editor/mode/output_highlight_rules.js +++ b/src/plugins/console/public/application/models/legacy_core_editor/mode/output_highlight_rules.js @@ -19,7 +19,7 @@ const ace = require('brace'); import 'brace/mode/json'; -import { addXJsonToRules } from '../../../../../../es_ui_shared/console_lang'; +import { addXJsonToRules } from '../../../../../../es_ui_shared/public'; const oop = ace.acequire('ace/lib/oop'); const JsonHighlightRules = ace.acequire('ace/mode/json_highlight_rules').JsonHighlightRules; diff --git a/src/plugins/console/public/application/models/legacy_core_editor/mode/script.js b/src/plugins/console/public/application/models/legacy_core_editor/mode/script.js index 13ae329380221..89adea8921c07 100644 --- a/src/plugins/console/public/application/models/legacy_core_editor/mode/script.js +++ b/src/plugins/console/public/application/models/legacy_core_editor/mode/script.js @@ -18,7 +18,7 @@ */ import ace from 'brace'; -import { ScriptHighlightRules } from '../../../../../../es_ui_shared/console_lang'; +import { ScriptHighlightRules } from '../../../../../../es_ui_shared/public'; const oop = ace.acequire('ace/lib/oop'); const TextMode = ace.acequire('ace/mode/text').Mode; diff --git a/src/plugins/console/public/application/models/sense_editor/__tests__/editor_input1.txt b/src/plugins/console/public/application/models/sense_editor/__tests__/editor_input1.txt index f9a4bcb85034d..398a0fdeab61f 100644 --- a/src/plugins/console/public/application/models/sense_editor/__tests__/editor_input1.txt +++ b/src/plugins/console/public/application/models/sense_editor/__tests__/editor_input1.txt @@ -25,3 +25,9 @@ GET index_1/type1/1/_source?_source_include=f DELETE index_2 + +POST /_sql?format=txt +{ + "query": "SELECT prenom FROM claude_index WHERE prenom = 'claude' ", + "fetch_size": 1 +} diff --git a/src/plugins/console/public/application/models/sense_editor/__tests__/sense_editor.test.js b/src/plugins/console/public/application/models/sense_editor/__tests__/sense_editor.test.js index 63f97345bc9ff..34b4cad7fbb6b 100644 --- a/src/plugins/console/public/application/models/sense_editor/__tests__/sense_editor.test.js +++ b/src/plugins/console/public/application/models/sense_editor/__tests__/sense_editor.test.js @@ -22,7 +22,7 @@ import $ from 'jquery'; import _ from 'lodash'; import { create } from '../create'; -import { collapseLiteralStrings } from '../../../../../../es_ui_shared/console_lang/lib'; +import { collapseLiteralStrings } from '../../../../../../es_ui_shared/public'; const editorInput1 = require('./editor_input1.txt'); describe('Editor', () => { @@ -470,6 +470,18 @@ curl -XGET "http://localhost:9200/_stats?level=shards" curl -XPUT "http://localhost:9200/index_1/type1/1" -H 'Content-Type: application/json' -d' { "f": 1 +}'`.trim() + ); + + multiReqCopyAsCurlTest( + 'with single quotes', + editorInput1, + { start: { lineNumber: 29 }, end: { lineNumber: 33 } }, + ` +curl -XPOST "http://localhost:9200/_sql?format=txt" -H 'Content-Type: application/json' -d' +{ + "query": "SELECT prenom FROM claude_index WHERE prenom = '\\''claude'\\'' ", + "fetch_size": 1 }'`.trim() ); }); diff --git a/src/plugins/console/public/application/models/sense_editor/sense_editor.ts b/src/plugins/console/public/application/models/sense_editor/sense_editor.ts index b1444bdf2bbab..d326543bbe00b 100644 --- a/src/plugins/console/public/application/models/sense_editor/sense_editor.ts +++ b/src/plugins/console/public/application/models/sense_editor/sense_editor.ts @@ -19,7 +19,7 @@ import _ from 'lodash'; import RowParser from '../../../lib/row_parser'; -import { collapseLiteralStrings } from '../../../../../es_ui_shared/console_lang/lib'; +import { collapseLiteralStrings } from '../../../../../es_ui_shared/public'; import * as utils from '../../../lib/utils'; // @ts-ignore @@ -484,8 +484,9 @@ export class SenseEditor { if (esData && esData.length) { ret += " -H 'Content-Type: application/json' -d'\n"; const dataAsString = collapseLiteralStrings(esData.join('\n')); - // since Sense doesn't allow single quote json string any single qoute is within a string. - ret += dataAsString.replace(/'/g, '\\"'); + + // We escape single quoted strings that that are wrapped in single quoted strings + ret += dataAsString.replace(/'/g, "'\\''"); if (esData.length > 1) { ret += '\n'; } // end with a new line diff --git a/src/plugins/console/public/lib/utils/index.ts b/src/plugins/console/public/lib/utils/index.ts index f66c952dd3af7..0ebea0f9d4055 100644 --- a/src/plugins/console/public/lib/utils/index.ts +++ b/src/plugins/console/public/lib/utils/index.ts @@ -18,10 +18,7 @@ */ import _ from 'lodash'; -import { - expandLiteralStrings, - collapseLiteralStrings, -} from '../../../../es_ui_shared/console_lang/lib'; +import { expandLiteralStrings, collapseLiteralStrings } from '../../../../es_ui_shared/public'; export function textFromRequest(request: any) { let data = request.data; diff --git a/src/plugins/console/server/__tests__/proxy_route/proxy_fallback.test.ts b/src/plugins/console/server/__tests__/proxy_route/proxy_fallback.test.ts new file mode 100644 index 0000000000000..b226bad11a01a --- /dev/null +++ b/src/plugins/console/server/__tests__/proxy_route/proxy_fallback.test.ts @@ -0,0 +1,64 @@ +/* + * 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 { duration } from 'moment'; +import { getProxyRouteHandlerDeps } from './mocks'; + +import { kibanaResponseFactory } from '../../../../../core/server'; +import { createHandler } from '../../routes/api/console/proxy/create_handler'; +import * as requestModule from '../../lib/proxy_request'; + +describe('Console Proxy Route', () => { + afterEach(async () => { + jest.resetAllMocks(); + }); + + describe('fallback behaviour', () => { + it('falls back to all configured endpoints regardless of error', async () => { + // Describe a situation where all three configured nodes reject + (requestModule.proxyRequest as jest.Mock).mockRejectedValueOnce(new Error('ECONNREFUSED')); + (requestModule.proxyRequest as jest.Mock).mockRejectedValueOnce(new Error('EHOSTUNREACH')); + (requestModule.proxyRequest as jest.Mock).mockRejectedValueOnce(new Error('ESOCKETTIMEDOUT')); + + const handler = createHandler( + getProxyRouteHandlerDeps({ + readLegacyESConfig: () => ({ + requestTimeout: duration(30000), + customHeaders: {}, + requestHeadersWhitelist: [], + hosts: ['http://localhost:9201', 'http://localhost:9202', 'http://localhost:9203'], + }), + }) + ); + + const response = await handler( + {} as any, + { + headers: {}, + query: { method: 'get', path: 'test' }, + } as any, + kibanaResponseFactory + ); + + expect(response.status).toBe(502); + // Return the message from the ES node we attempted last. + expect(response.payload.message).toBe('ESOCKETTIMEDOUT'); + }); + }); +}); diff --git a/src/plugins/console/server/lib/spec_definitions/js/ingest.ts b/src/plugins/console/server/lib/spec_definitions/js/ingest.ts index 1182dc075f42f..20dbeda5e0b3d 100644 --- a/src/plugins/console/server/lib/spec_definitions/js/ingest.ts +++ b/src/plugins/console/server/lib/spec_definitions/js/ingest.ts @@ -57,6 +57,49 @@ const bytesProcessorDefinition = { }, }; +// Based on https://www.elastic.co/guide/en/elasticsearch/reference/master/ingest-circle-processor.html +const circleProcessorDefinition = { + circle: { + __template: { + field: '', + error_distance: '', + shape_type: '', + }, + field: '', + target_field: '', + error_distance: '', + shape_type: { + __one_of: ['geo_shape', 'shape'], + }, + ignore_missing: { + __one_of: [false, true], + }, + ...commonPipelineParams, + }, +}; + +// Based on https://www.elastic.co/guide/en/elasticsearch/reference/master/csv-processor.html +const csvProcessorDefinition = { + csv: { + __template: { + field: '', + target_fields: [''], + }, + field: '', + target_fields: [''], + separator: '', + quote: '', + empty_value: '', + trim: { + __one_of: [true, false], + }, + ignore_missing: { + __one_of: [false, true], + }, + ...commonPipelineParams, + }, +}; + // Based on https://www.elastic.co/guide/en/elasticsearch/reference/master/convert-processor.html const convertProcessorDefinition = { convert: { @@ -174,6 +217,25 @@ const foreachProcessorDefinition = { }, }; +// Based on https://www.elastic.co/guide/en/elasticsearch/reference/master/geoip-processor.html +const geoipProcessorDefinition = { + geoip: { + __template: { + field: '', + }, + field: '', + target_field: '', + database_file: '', + properties: [''], + ignore_missing: { + __one_of: [false, true], + }, + first_only: { + __one_of: [false, true], + }, + }, +}; + // Based on https://www.elastic.co/guide/en/elasticsearch/reference/master/grok-processor.html const grokProcessorDefinition = { grok: { @@ -209,6 +271,37 @@ const gsubProcessorDefinition = { }, }; +// Based on https://www.elastic.co/guide/en/elasticsearch/reference/master/htmlstrip-processor.html +const htmlStripProcessorDefinition = { + html_strip: { + __template: { + field: '', + }, + field: '', + target_field: '', + ignore_missing: { + __one_of: [false, true], + }, + ...commonPipelineParams, + }, +}; + +// Based on https://www.elastic.co/guide/en/elasticsearch/reference/master/inference-processor.html +const inferenceProcessorDefinition = { + inference: { + __template: { + model_id: '', + field_map: {}, + inference_config: {}, + }, + model_id: '', + field_map: {}, + inference_config: {}, + target_field: '', + ...commonPipelineParams, + }, +}; + // Based on https://www.elastic.co/guide/en/elasticsearch/reference/master/join-processor.html const joinProcessorDefinition = { join: { @@ -338,6 +431,18 @@ const setProcessorDefinition = { }, }; +// Based on https://www.elastic.co/guide/en/elasticsearch/reference/master/ingest-node-set-security-user-processor.html +const setSecurityUserProcessorDefinition = { + set_security_user: { + __template: { + field: '', + }, + field: '', + properties: [''], + ...commonPipelineParams, + }, +}; + // Based on https://www.elastic.co/guide/en/elasticsearch/reference/master/split-processor.html const splitProcessorDefinition = { split: { @@ -394,10 +499,43 @@ const uppercaseProcessorDefinition = { }, }; +// Based on https://www.elastic.co/guide/en/elasticsearch/reference/master/urldecode-processor.html +const urlDecodeProcessorDefinition = { + urldecode: { + __template: { + field: '', + }, + field: '', + target_field: '', + ignore_missing: { + __one_of: [false, true], + }, + ...commonPipelineParams, + }, +}; + +// Based on https://www.elastic.co/guide/en/elasticsearch/reference/master/user-agent-processor.html +const userAgentProcessorDefinition = { + user_agent: { + __template: { + field: '', + }, + field: '', + target_field: '', + regex_file: '', + properties: [''], + ignore_missing: { + __one_of: [false, true], + }, + }, +}; + const processorDefinition = { __one_of: [ appendProcessorDefinition, bytesProcessorDefinition, + csvProcessorDefinition, + circleProcessorDefinition, convertProcessorDefinition, dateProcessorDefinition, dateIndexNameProcessorDefinition, @@ -406,8 +544,11 @@ const processorDefinition = { dropProcessorDefinition, failProcessorDefinition, foreachProcessorDefinition, + geoipProcessorDefinition, grokProcessorDefinition, gsubProcessorDefinition, + htmlStripProcessorDefinition, + inferenceProcessorDefinition, joinProcessorDefinition, jsonProcessorDefinition, kvProcessorDefinition, @@ -417,10 +558,13 @@ const processorDefinition = { renameProcessorDefinition, scriptProcessorDefinition, setProcessorDefinition, + setSecurityUserProcessorDefinition, splitProcessorDefinition, sortProcessorDefinition, trimProcessorDefinition, uppercaseProcessorDefinition, + urlDecodeProcessorDefinition, + userAgentProcessorDefinition, ], }; diff --git a/src/plugins/console/server/lib/spec_definitions/json/overrides/cluster.put_settings.json b/src/plugins/console/server/lib/spec_definitions/json/overrides/cluster.put_settings.json index 408b01c4cb8f5..0da2c130b47cf 100644 --- a/src/plugins/console/server/lib/spec_definitions/json/overrides/cluster.put_settings.json +++ b/src/plugins/console/server/lib/spec_definitions/json/overrides/cluster.put_settings.json @@ -59,7 +59,61 @@ } }, "transient": { - "__scope_link": ".persistent" + "cluster": { + "routing": { + "allocation.enable": { + "__one_of": ["all", "primaries", "new_primaries", "none"] + }, + "allocation.disk.threshold_enabled": { "__one_of": [false, true] }, + "allocation.disk.watermark.low": "85%", + "allocation.disk.watermark.high": "90%", + "allocation.disk.reroute_interval": "60s", + "allocation.exclude": { + "_ip": "", + "_name": "", + "_host": "", + "_id": "" + }, + "allocation.include": { + "_ip": "", + "_name": "", + "_host": "", + "_id": "" + }, + "allocation.require": { + "_ip": "", + "_name": "", + "_host": "", + "_id": "" + }, + "allocation.awareness.attributes": [], + "allocation.awareness.force": { + "*": { + "values": [] + } + }, + "allocation.allow_rebalance": { + "__one_of": [ + "always", + "indices_primaries_active", + "indices_all_active" + ] + }, + "allocation.cluster_concurrent_rebalance": 2, + "allocation.node_initial_primaries_recoveries": 4, + "allocation.node_concurrent_recoveries": 2, + "allocation.same_shard.host": { "__one_of": [false, true] } + } + }, + "indices": { + "breaker": { + "total.limit": "70%", + "fielddata.limit": "60%", + "fielddata.overhead": 1.03, + "request.limit": "40%", + "request.overhead": 1.0 + } + } } } } diff --git a/src/plugins/console/server/routes/api/console/proxy/create_handler.ts b/src/plugins/console/server/routes/api/console/proxy/create_handler.ts index 50a9fcf03c209..9446289ff03ea 100644 --- a/src/plugins/console/server/routes/api/console/proxy/create_handler.ts +++ b/src/plugins/console/server/routes/api/console/proxy/create_handler.ts @@ -175,10 +175,9 @@ export const createHandler = ({ break; } catch (e) { + // If we reached here it means we hit a lower level network issue than just, for e.g., a 500. + // We try contacting another node in that case. log.error(e); - if (e.code !== 'ECONNREFUSED') { - return response.internalError(e); - } if (idx === hosts.length - 1) { log.warn(`Could not connect to any configured ES node [${hosts.join(', ')}]`); return response.customError({ diff --git a/src/plugins/dashboard/public/application/__snapshots__/dashboard_empty_screen.test.tsx.snap b/src/plugins/dashboard/public/application/__snapshots__/dashboard_empty_screen.test.tsx.snap index e71e4f1b15134..7210879c5eacc 100644 --- a/src/plugins/dashboard/public/application/__snapshots__/dashboard_empty_screen.test.tsx.snap +++ b/src/plugins/dashboard/public/application/__snapshots__/dashboard_empty_screen.test.tsx.snap @@ -667,14 +667,13 @@ exports[`DashboardEmptyScreen renders correctly with visualize paragraph 1`] = ` + + + + + + + + + +
+`; diff --git a/src/legacy/core_plugins/kibana/public/management/sections/objects/components/object_view/__snapshots__/intro.test.tsx.snap b/src/plugins/saved_objects_management/public/management_section/object_view/components/__snapshots__/intro.test.tsx.snap similarity index 90% rename from src/legacy/core_plugins/kibana/public/management/sections/objects/components/object_view/__snapshots__/intro.test.tsx.snap rename to src/plugins/saved_objects_management/public/management_section/object_view/components/__snapshots__/intro.test.tsx.snap index 812031b4b363c..ff179401e6f71 100644 --- a/src/legacy/core_plugins/kibana/public/management/sections/objects/components/object_view/__snapshots__/intro.test.tsx.snap +++ b/src/plugins/saved_objects_management/public/management_section/object_view/components/__snapshots__/intro.test.tsx.snap @@ -8,7 +8,7 @@ exports[`Intro component renders correctly 1`] = ` title={ } @@ -37,7 +37,7 @@ exports[`Intro component renders correctly 1`] = ` > Proceed with caution! @@ -53,7 +53,7 @@ exports[`Intro component renders correctly 1`] = `
Modifying objects is for advanced users only. Object properties are not validated and invalid objects could cause errors, data loss, or worse. Unless someone with intimate knowledge of the code told you to be in here, you probably shouldn’t be. diff --git a/src/legacy/core_plugins/kibana/public/management/sections/objects/components/object_view/__snapshots__/not_found_errors.test.tsx.snap b/src/plugins/saved_objects_management/public/management_section/object_view/components/__snapshots__/not_found_errors.test.tsx.snap similarity index 89% rename from src/legacy/core_plugins/kibana/public/management/sections/objects/components/object_view/__snapshots__/not_found_errors.test.tsx.snap rename to src/plugins/saved_objects_management/public/management_section/object_view/components/__snapshots__/not_found_errors.test.tsx.snap index ac565a000813e..d5372fd5b18d9 100644 --- a/src/legacy/core_plugins/kibana/public/management/sections/objects/components/object_view/__snapshots__/not_found_errors.test.tsx.snap +++ b/src/plugins/saved_objects_management/public/management_section/object_view/components/__snapshots__/not_found_errors.test.tsx.snap @@ -10,7 +10,7 @@ exports[`NotFoundErrors component renders correctly for index-pattern type 1`] = title={ } @@ -39,7 +39,7 @@ exports[`NotFoundErrors component renders correctly for index-pattern type 1`] = > There is a problem with this saved object @@ -55,7 +55,7 @@ exports[`NotFoundErrors component renders correctly for index-pattern type 1`] =
The index pattern associated with this object no longer exists. @@ -64,7 +64,7 @@ exports[`NotFoundErrors component renders correctly for index-pattern type 1`] =
If you know what this error means, go ahead and fix it — otherwise click the delete button above. @@ -87,7 +87,7 @@ exports[`NotFoundErrors component renders correctly for index-pattern-field type title={ } @@ -116,7 +116,7 @@ exports[`NotFoundErrors component renders correctly for index-pattern-field type > There is a problem with this saved object @@ -132,7 +132,7 @@ exports[`NotFoundErrors component renders correctly for index-pattern-field type
A field associated with this object no longer exists in the index pattern. @@ -141,7 +141,7 @@ exports[`NotFoundErrors component renders correctly for index-pattern-field type
If you know what this error means, go ahead and fix it — otherwise click the delete button above. @@ -164,7 +164,7 @@ exports[`NotFoundErrors component renders correctly for search type 1`] = ` title={ } @@ -193,7 +193,7 @@ exports[`NotFoundErrors component renders correctly for search type 1`] = ` > There is a problem with this saved object @@ -209,7 +209,7 @@ exports[`NotFoundErrors component renders correctly for search type 1`] = `
The saved search associated with this object no longer exists. @@ -218,7 +218,7 @@ exports[`NotFoundErrors component renders correctly for search type 1`] = `
If you know what this error means, go ahead and fix it — otherwise click the delete button above. @@ -241,7 +241,7 @@ exports[`NotFoundErrors component renders correctly for unknown type 1`] = ` title={ } @@ -270,7 +270,7 @@ exports[`NotFoundErrors component renders correctly for unknown type 1`] = ` > There is a problem with this saved object @@ -287,7 +287,7 @@ exports[`NotFoundErrors component renders correctly for unknown type 1`] = `
If you know what this error means, go ahead and fix it — otherwise click the delete button above. diff --git a/src/legacy/core_plugins/kibana/public/management/sections/objects/components/object_view/field.test.tsx b/src/plugins/saved_objects_management/public/management_section/object_view/components/field.test.tsx similarity index 100% rename from src/legacy/core_plugins/kibana/public/management/sections/objects/components/object_view/field.test.tsx rename to src/plugins/saved_objects_management/public/management_section/object_view/components/field.test.tsx diff --git a/src/legacy/core_plugins/kibana/public/management/sections/objects/components/object_view/field.tsx b/src/plugins/saved_objects_management/public/management_section/object_view/components/field.tsx similarity index 97% rename from src/legacy/core_plugins/kibana/public/management/sections/objects/components/object_view/field.tsx rename to src/plugins/saved_objects_management/public/management_section/object_view/components/field.tsx index 1ed0b57e400b8..1b69eb4240d68 100644 --- a/src/legacy/core_plugins/kibana/public/management/sections/objects/components/object_view/field.tsx +++ b/src/plugins/saved_objects_management/public/management_section/object_view/components/field.tsx @@ -104,9 +104,9 @@ export class Field extends PureComponent { id={this.fieldId} label={ !!currentValue ? ( - + ) : ( - + ) } checked={!!currentValue} diff --git a/src/legacy/core_plugins/kibana/public/management/sections/objects/components/object_view/form.tsx b/src/plugins/saved_objects_management/public/management_section/object_view/components/form.tsx similarity index 89% rename from src/legacy/core_plugins/kibana/public/management/sections/objects/components/object_view/form.tsx rename to src/plugins/saved_objects_management/public/management_section/object_view/components/form.tsx index 7270d41eef529..04be7ee3ce207 100644 --- a/src/legacy/core_plugins/kibana/public/management/sections/objects/components/object_view/form.tsx +++ b/src/plugins/saved_objects_management/public/management_section/object_view/components/form.tsx @@ -29,15 +29,11 @@ import { import { cloneDeep, set } from 'lodash'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; -import { - SimpleSavedObject, - SavedObjectsClientContract, -} from '../../../../../../../../../core/public'; - -import { SavedObjectLoader } from '../../../../../../../../../plugins/saved_objects/public'; +import { SimpleSavedObject, SavedObjectsClientContract } from '../../../../../../core/public'; +import { SavedObjectLoader } from '../../../../../saved_objects/public'; import { Field } from './field'; import { ObjectField, FieldState, SubmittedFormData } from '../../types'; -import { createFieldList } from '../../lib/create_field_list'; +import { createFieldList } from '../../../lib'; interface FormProps { object: SimpleSavedObject; @@ -96,7 +92,7 @@ export class Form extends Component { { data-test-subj="savedObjectEditSave" > @@ -117,14 +113,14 @@ export class Form extends Component { diff --git a/src/legacy/core_plugins/kibana/public/management/sections/objects/components/object_view/header.test.tsx b/src/plugins/saved_objects_management/public/management_section/object_view/components/header.test.tsx similarity index 100% rename from src/legacy/core_plugins/kibana/public/management/sections/objects/components/object_view/header.test.tsx rename to src/plugins/saved_objects_management/public/management_section/object_view/components/header.test.tsx diff --git a/src/plugins/saved_objects_management/public/management_section/object_view/components/header.tsx b/src/plugins/saved_objects_management/public/management_section/object_view/components/header.tsx new file mode 100644 index 0000000000000..305d953c4990b --- /dev/null +++ b/src/plugins/saved_objects_management/public/management_section/object_view/components/header.tsx @@ -0,0 +1,110 @@ +/* + * 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 { + EuiFlexGroup, + EuiFlexItem, + EuiTitle, + EuiButton, + EuiPageContentHeader, + EuiPageContentHeaderSection, +} from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n/react'; + +interface HeaderProps { + canEdit: boolean; + canDelete: boolean; + canViewInApp: boolean; + type: string; + viewUrl: string; + onDeleteClick: () => void; +} + +export const Header = ({ + canEdit, + canDelete, + canViewInApp, + type, + viewUrl, + onDeleteClick, +}: HeaderProps) => { + return ( + + + + {canEdit ? ( +

+ +

+ ) : ( +

+ +

+ )} +
+
+ + + {canViewInApp && ( + + + + + + )} + {canDelete && ( + + onDeleteClick()} + data-test-subj="savedObjectEditDelete" + > + + + + )} + + +
+ ); +}; diff --git a/src/legacy/core_plugins/kibana/public/management/sections/objects/components/object_view/index.ts b/src/plugins/saved_objects_management/public/management_section/object_view/components/index.ts similarity index 100% rename from src/legacy/core_plugins/kibana/public/management/sections/objects/components/object_view/index.ts rename to src/plugins/saved_objects_management/public/management_section/object_view/components/index.ts diff --git a/src/legacy/core_plugins/kibana/public/management/sections/objects/components/object_view/intro.test.tsx b/src/plugins/saved_objects_management/public/management_section/object_view/components/intro.test.tsx similarity index 100% rename from src/legacy/core_plugins/kibana/public/management/sections/objects/components/object_view/intro.test.tsx rename to src/plugins/saved_objects_management/public/management_section/object_view/components/intro.test.tsx diff --git a/src/legacy/core_plugins/kibana/public/management/sections/objects/components/object_view/intro.tsx b/src/plugins/saved_objects_management/public/management_section/object_view/components/intro.tsx similarity index 92% rename from src/legacy/core_plugins/kibana/public/management/sections/objects/components/object_view/intro.tsx rename to src/plugins/saved_objects_management/public/management_section/object_view/components/intro.tsx index 098ad71345d49..920a5fcbcb02e 100644 --- a/src/legacy/core_plugins/kibana/public/management/sections/objects/components/object_view/intro.tsx +++ b/src/plugins/saved_objects_management/public/management_section/object_view/components/intro.tsx @@ -26,7 +26,7 @@ export const Intro = () => { } @@ -35,7 +35,7 @@ export const Intro = () => { >
diff --git a/src/legacy/core_plugins/kibana/public/management/sections/objects/components/object_view/not_found_errors.test.tsx b/src/plugins/saved_objects_management/public/management_section/object_view/components/not_found_errors.test.tsx similarity index 100% rename from src/legacy/core_plugins/kibana/public/management/sections/objects/components/object_view/not_found_errors.test.tsx rename to src/plugins/saved_objects_management/public/management_section/object_view/components/not_found_errors.test.tsx diff --git a/src/legacy/core_plugins/kibana/public/management/sections/objects/components/object_view/not_found_errors.tsx b/src/plugins/saved_objects_management/public/management_section/object_view/components/not_found_errors.tsx similarity index 87% rename from src/legacy/core_plugins/kibana/public/management/sections/objects/components/object_view/not_found_errors.tsx rename to src/plugins/saved_objects_management/public/management_section/object_view/components/not_found_errors.tsx index c3d18855f6c9a..1a63f7eaf4819 100644 --- a/src/legacy/core_plugins/kibana/public/management/sections/objects/components/object_view/not_found_errors.tsx +++ b/src/plugins/saved_objects_management/public/management_section/object_view/components/not_found_errors.tsx @@ -31,21 +31,21 @@ export const NotFoundErrors = ({ type }: NotFoundErrors) => { case 'search': return ( ); case 'index-pattern': return ( ); case 'index-pattern-field': return ( ); @@ -58,7 +58,7 @@ export const NotFoundErrors = ({ type }: NotFoundErrors) => { } @@ -68,7 +68,7 @@ export const NotFoundErrors = ({ type }: NotFoundErrors) => {
{getMessage()}
diff --git a/src/plugins/saved_objects_management/public/management_section/object_view/index.ts b/src/plugins/saved_objects_management/public/management_section/object_view/index.ts new file mode 100644 index 0000000000000..a823923536d31 --- /dev/null +++ b/src/plugins/saved_objects_management/public/management_section/object_view/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 { SavedObjectEdition } from './saved_object_view'; diff --git a/src/legacy/core_plugins/kibana/public/management/sections/objects/saved_object_view.tsx b/src/plugins/saved_objects_management/public/management_section/object_view/saved_object_view.tsx similarity index 89% rename from src/legacy/core_plugins/kibana/public/management/sections/objects/saved_object_view.tsx rename to src/plugins/saved_objects_management/public/management_section/object_view/saved_object_view.tsx index 4984fe3e6d6b8..f714970a5cac3 100644 --- a/src/legacy/core_plugins/kibana/public/management/sections/objects/saved_object_view.tsx +++ b/src/plugins/saved_objects_management/public/management_section/object_view/saved_object_view.tsx @@ -26,16 +26,16 @@ import { OverlayStart, NotificationsStart, SimpleSavedObject, -} from '../../../../../../../core/public'; -import { ISavedObjectsManagementRegistry } from '../../saved_object_registry'; -import { Header, NotFoundErrors, Intro, Form } from './components/object_view'; -import { canViewInApp } from './lib/in_app_url'; -import { SubmittedFormData } from './types'; +} from '../../../../../core/public'; +import { ISavedObjectsManagementServiceRegistry } from '../../services'; +import { Header, NotFoundErrors, Intro, Form } from './components'; +import { canViewInApp } from '../../lib'; +import { SubmittedFormData } from '../types'; interface SavedObjectEditionProps { id: string; serviceName: string; - serviceRegistry: ISavedObjectsManagementRegistry; + serviceRegistry: ISavedObjectsManagementServiceRegistry; capabilities: Capabilities; overlays: OverlayStart; notifications: NotificationsStart; @@ -135,17 +135,17 @@ export class SavedObjectEdition extends Component< const { type, object } = this.state; const confirmed = await overlays.openConfirm( - i18n.translate('kbn.management.objects.confirmModalOptions.modalDescription', { + i18n.translate('savedObjectsManagement.deleteConfirm.modalDescription', { defaultMessage: 'This action permanently removes the object from Kibana.', }), { confirmButtonText: i18n.translate( - 'kbn.management.objects.confirmModalOptions.deleteButtonLabel', + 'savedObjectsManagement.deleteConfirm.modalDeleteButtonLabel', { defaultMessage: 'Delete', } ), - title: i18n.translate('kbn.management.objects.confirmModalOptions.modalTitle', { + title: i18n.translate('savedObjectsManagement.deleteConfirm.modalTitle', { defaultMessage: `Delete '{title}'?`, values: { title: object?.attributes?.title || 'saved Kibana object', diff --git a/src/plugins/saved_objects_management/public/management_section/objects_table/__snapshots__/saved_objects_table.test.tsx.snap b/src/plugins/saved_objects_management/public/management_section/objects_table/__snapshots__/saved_objects_table.test.tsx.snap new file mode 100644 index 0000000000000..fe64df6ff51d1 --- /dev/null +++ b/src/plugins/saved_objects_management/public/management_section/objects_table/__snapshots__/saved_objects_table.test.tsx.snap @@ -0,0 +1,432 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`SavedObjectsTable delete should show a confirm modal 1`] = ` + + } + confirmButtonText={ + + } + defaultFocusedButton="confirm" + onCancel={[Function]} + onConfirm={[Function]} + title={ + + } +> +

+ +

+ +
+`; + +exports[`SavedObjectsTable export should allow the user to choose when exporting all 1`] = ` + + + + + + + + + } + labelType="legend" + > + + + + + } + name="includeReferencesDeep" + onChange={[Function]} + /> + + + + + + + + + + + + + + + + + + + + +`; + +exports[`SavedObjectsTable import should show the flyout 1`] = ` + +`; + +exports[`SavedObjectsTable relationships should show the flyout 1`] = ` + +`; + +exports[`SavedObjectsTable should render normally 1`] = ` + +
+ + + +`; diff --git a/src/plugins/saved_objects_management/public/management_section/objects_table/components/__snapshots__/flyout.test.tsx.snap b/src/plugins/saved_objects_management/public/management_section/objects_table/components/__snapshots__/flyout.test.tsx.snap new file mode 100644 index 0000000000000..4721d166c65e5 --- /dev/null +++ b/src/plugins/saved_objects_management/public/management_section/objects_table/components/__snapshots__/flyout.test.tsx.snap @@ -0,0 +1,703 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Flyout conflicts should allow conflict resolution 1`] = ` + + + +

+ +

+
+
+ + + + + } + > +

+ + + , + } + } + /> +

+
+
+ +
+ + + + + + + + + + + + + + +
+`; + +exports[`Flyout conflicts should allow conflict resolution 2`] = ` +[MockFunction] { + "calls": Array [ + Array [ + Object { + "getConflictResolutions": [Function], + "http": Object { + "addLoadingCountSource": [MockFunction], + "anonymousPaths": Object { + "isAnonymous": [MockFunction], + "register": [MockFunction], + }, + "basePath": BasePath { + "basePath": "", + "get": [Function], + "prepend": [Function], + "remove": [Function], + "serverBasePath": "", + }, + "delete": [MockFunction], + "fetch": [MockFunction], + "get": [MockFunction], + "getLoadingCount$": [MockFunction], + "head": [MockFunction], + "intercept": [MockFunction], + "options": [MockFunction], + "patch": [MockFunction], + "post": [MockFunction], + "put": [MockFunction], + }, + "state": Object { + "conflictedIndexPatterns": undefined, + "conflictedSavedObjectsLinkedToSavedSearches": undefined, + "conflictedSearchDocs": undefined, + "conflictingRecord": undefined, + "error": undefined, + "failedImports": Array [ + Object { + "error": Object { + "references": Array [ + Object { + "id": "MyIndexPattern*", + "type": "index-pattern", + }, + ], + "type": "missing_references", + }, + "obj": Object { + "id": "1", + "title": "My Visualization", + "type": "visualization", + }, + }, + ], + "file": Object { + "name": "foo.ndjson", + "path": "/home/foo.ndjson", + }, + "importCount": 0, + "indexPatterns": Array [ + Object { + "id": "1", + }, + Object { + "id": "2", + }, + ], + "isLegacyFile": false, + "isOverwriteAllChecked": true, + "loadingMessage": undefined, + "status": "loading", + "unmatchedReferences": Array [ + Object { + "existingIndexPatternId": "MyIndexPattern*", + "list": Array [ + Object { + "id": "1", + "title": "My Visualization", + "type": "visualization", + }, + ], + "newIndexPatternId": "2", + }, + ], + }, + }, + ], + ], + "results": Array [ + Object { + "type": "return", + "value": Object { + "failedImports": Array [], + "importCount": 1, + "status": "success", + }, + }, + ], +} +`; + +exports[`Flyout conflicts should handle errors 1`] = ` + + } +> +

+ +

+

+ +`; + +exports[`Flyout errors should display unsupported type errors properly 1`] = ` + + } +> +

+ +

+

+ wigwags [id=1] unsupported type +

+
+`; + +exports[`Flyout legacy conflicts should allow conflict resolution 1`] = ` + + + +

+ +

+
+
+ + + + + } + > +

+ +

+
+
+ + + + } + > +

+ + + , + } + } + /> +

+
+
+ +
+ + + + + + + + + + + + + + +
+`; + +exports[`Flyout legacy conflicts should handle errors 1`] = ` +Array [ + + } + > +

+ +

+
, + + } + > +

+ + + , + } + } + /> +

+
, + + } + > +

+ foobar +

+
, +] +`; + +exports[`Flyout should render import step 1`] = ` + + + +

+ +

+
+
+ + + + } + labelType="label" + > + + } + onChange={[Function]} + /> + + + + } + name="overwriteAll" + onChange={[Function]} + /> + + + + + + + + + + + + + + + + + +
+`; diff --git a/src/plugins/saved_objects_management/public/management_section/objects_table/components/__snapshots__/header.test.tsx.snap b/src/plugins/saved_objects_management/public/management_section/objects_table/components/__snapshots__/header.test.tsx.snap new file mode 100644 index 0000000000000..642a5030e4ec0 --- /dev/null +++ b/src/plugins/saved_objects_management/public/management_section/objects_table/components/__snapshots__/header.test.tsx.snap @@ -0,0 +1,106 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Header should render normally 1`] = ` + + + + +

+ +

+
+
+ + + + + + + + + + + + + + + + + + + +
+ + +

+ + + +

+
+ +
+`; diff --git a/src/plugins/saved_objects_management/public/management_section/objects_table/components/__snapshots__/relationships.test.tsx.snap b/src/plugins/saved_objects_management/public/management_section/objects_table/components/__snapshots__/relationships.test.tsx.snap new file mode 100644 index 0000000000000..a8bb691cd54e9 --- /dev/null +++ b/src/plugins/saved_objects_management/public/management_section/objects_table/components/__snapshots__/relationships.test.tsx.snap @@ -0,0 +1,716 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Relationships should render dashboards normally 1`] = ` + + + +

+ + + +    + MyDashboard +

+
+
+ +
+ +

+ Here are the saved objects related to MyDashboard. Deleting this dashboard affects its parent objects, but not its children. +

+
+ + +
+
+
+`; + +exports[`Relationships should render errors 1`] = ` + + + +

+ + + +    + MyDashboard +

+
+
+ + + } + > + foo + + +
+`; + +exports[`Relationships should render index patterns normally 1`] = ` + + + +

+ + + +    + MyIndexPattern* +

+
+
+ +
+ +

+ Here are the saved objects related to MyIndexPattern*. Deleting this index-pattern affects its parent objects, but not its children. +

+
+ + +
+
+
+`; + +exports[`Relationships should render searches normally 1`] = ` + + + +

+ + + +    + MySearch +

+
+
+ +
+ +

+ Here are the saved objects related to MySearch. Deleting this search affects its parent objects, but not its children. +

+
+ + +
+
+
+`; + +exports[`Relationships should render visualizations normally 1`] = ` + + + +

+ + + +    + MyViz +

+
+
+ +
+ +

+ Here are the saved objects related to MyViz. Deleting this visualization affects its parent objects, but not its children. +

+
+ + +
+
+
+`; diff --git a/src/plugins/saved_objects_management/public/management_section/objects_table/components/__snapshots__/table.test.tsx.snap b/src/plugins/saved_objects_management/public/management_section/objects_table/components/__snapshots__/table.test.tsx.snap new file mode 100644 index 0000000000000..d09dd6f8b868b --- /dev/null +++ b/src/plugins/saved_objects_management/public/management_section/objects_table/components/__snapshots__/table.test.tsx.snap @@ -0,0 +1,432 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Table prevents saved objects from being deleted 1`] = ` + + + + , + + + + } + closePopover={[Function]} + display="inlineBlock" + hasArrow={true} + isOpen={false} + ownFocus={false} + panelPaddingSize="m" + > + + } + labelType="label" + > + + } + name="includeReferencesDeep" + onChange={[Function]} + /> + + + + + + + , + ] + } + /> + +
+ +
+
+`; + +exports[`Table should render normally 1`] = ` + + + + , + + + + } + closePopover={[Function]} + display="inlineBlock" + hasArrow={true} + isOpen={false} + ownFocus={false} + panelPaddingSize="m" + > + + } + labelType="label" + > + + } + name="includeReferencesDeep" + onChange={[Function]} + /> + + + + + + + , + ] + } + /> + +
+ +
+
+`; diff --git a/src/plugins/saved_objects_management/public/management_section/objects_table/components/flyout.test.mocks.ts b/src/plugins/saved_objects_management/public/management_section/objects_table/components/flyout.test.mocks.ts new file mode 100644 index 0000000000000..b5361d212954f --- /dev/null +++ b/src/plugins/saved_objects_management/public/management_section/objects_table/components/flyout.test.mocks.ts @@ -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. + */ + +export const importFileMock = jest.fn(); +jest.doMock('../../../lib/import_file', () => ({ + importFile: importFileMock, +})); + +export const resolveImportErrorsMock = jest.fn(); +jest.doMock('../../../lib/resolve_import_errors', () => ({ + resolveImportErrors: resolveImportErrorsMock, +})); + +export const importLegacyFileMock = jest.fn(); +jest.doMock('../../../lib/import_legacy_file', () => ({ + importLegacyFile: importLegacyFileMock, +})); + +export const resolveSavedObjectsMock = jest.fn(); +export const resolveSavedSearchesMock = jest.fn(); +export const resolveIndexPatternConflictsMock = jest.fn(); +export const saveObjectsMock = jest.fn(); +jest.doMock('../../../lib/resolve_saved_objects', () => ({ + resolveSavedObjects: resolveSavedObjectsMock, + resolveSavedSearches: resolveSavedSearchesMock, + resolveIndexPatternConflicts: resolveIndexPatternConflictsMock, + saveObjects: saveObjectsMock, +})); diff --git a/src/plugins/saved_objects_management/public/management_section/objects_table/components/flyout.test.tsx b/src/plugins/saved_objects_management/public/management_section/objects_table/components/flyout.test.tsx new file mode 100644 index 0000000000000..5d713ff044f24 --- /dev/null +++ b/src/plugins/saved_objects_management/public/management_section/objects_table/components/flyout.test.tsx @@ -0,0 +1,545 @@ +/* + * 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 { + importFileMock, + importLegacyFileMock, + resolveImportErrorsMock, + resolveIndexPatternConflictsMock, + resolveSavedObjectsMock, + resolveSavedSearchesMock, + saveObjectsMock, +} from './flyout.test.mocks'; + +import React from 'react'; +import { shallowWithI18nProvider } from 'test_utils/enzyme_helpers'; +import { coreMock } from '../../../../../../core/public/mocks'; +import { serviceRegistryMock } from '../../../services/service_registry.mock'; +import { Flyout, FlyoutProps, FlyoutState } from './flyout'; +import { ShallowWrapper } from 'enzyme'; + +const mockFile = ({ + name: 'foo.ndjson', + path: '/home/foo.ndjson', +} as unknown) as File; +const legacyMockFile = ({ + name: 'foo.json', + path: '/home/foo.json', +} as unknown) as File; + +describe('Flyout', () => { + let defaultProps: FlyoutProps; + + const shallowRender = (props: FlyoutProps) => { + return (shallowWithI18nProvider() as unknown) as ShallowWrapper< + FlyoutProps, + FlyoutState, + Flyout + >; + }; + + beforeEach(() => { + const { http, overlays } = coreMock.createStart(); + + defaultProps = { + close: jest.fn(), + done: jest.fn(), + newIndexPatternUrl: '', + indexPatterns: { + getFields: jest.fn().mockImplementation(() => [{ id: '1' }, { id: '2' }]), + } as any, + overlays, + http, + allowedTypes: ['search', 'index-pattern', 'visualization'], + serviceRegistry: serviceRegistryMock.create(), + }; + }); + + it('should render import step', async () => { + const component = shallowRender(defaultProps); + + // Ensure all promises resolve + await new Promise(resolve => process.nextTick(resolve)); + // Ensure the state changes are reflected + component.update(); + + expect(component).toMatchSnapshot(); + }); + + it('should toggle the overwrite all control', async () => { + const component = shallowRender(defaultProps); + + // Ensure all promises resolve + await new Promise(resolve => process.nextTick(resolve)); + // Ensure the state changes are reflected + component.update(); + + expect(component.state('isOverwriteAllChecked')).toBe(true); + component.find('EuiSwitch').simulate('change'); + expect(component.state('isOverwriteAllChecked')).toBe(false); + }); + + it('should allow picking a file', async () => { + const component = shallowRender(defaultProps); + + // Ensure all promises resolve + await new Promise(resolve => process.nextTick(resolve)); + // Ensure the state changes are reflected + component.update(); + + expect(component.state('file')).toBe(undefined); + component.find('EuiFilePicker').simulate('change', [mockFile]); + expect(component.state('file')).toBe(mockFile); + }); + + it('should allow removing a file', async () => { + const component = shallowRender(defaultProps); + + // Ensure all promises resolve + await Promise.resolve(); + // Ensure the state changes are reflected + component.update(); + + expect(component.state('file')).toBe(undefined); + component.find('EuiFilePicker').simulate('change', [mockFile]); + expect(component.state('file')).toBe(mockFile); + component.find('EuiFilePicker').simulate('change', []); + expect(component.state('file')).toBe(undefined); + }); + + it('should handle invalid files', async () => { + const component = shallowRender(defaultProps); + + // Ensure all promises resolve + await new Promise(resolve => process.nextTick(resolve)); + // Ensure the state changes are reflected + component.update(); + + importLegacyFileMock.mockImplementation(() => { + throw new Error('foobar'); + }); + + await component.instance().legacyImport(); + expect(component.state('error')).toBe('The file could not be processed.'); + + importLegacyFileMock.mockImplementation(() => ({ + invalid: true, + })); + + await component.instance().legacyImport(); + expect(component.state('error')).toBe( + 'Saved objects file format is invalid and cannot be imported.' + ); + }); + + describe('conflicts', () => { + beforeEach(() => { + importFileMock.mockImplementation(() => ({ + success: false, + successCount: 0, + errors: [ + { + id: '1', + type: 'visualization', + title: 'My Visualization', + error: { + type: 'missing_references', + references: [ + { + id: 'MyIndexPattern*', + type: 'index-pattern', + }, + ], + }, + }, + ], + })); + resolveImportErrorsMock.mockImplementation(() => ({ + status: 'success', + importCount: 1, + failedImports: [], + })); + }); + + it('should figure out unmatchedReferences', async () => { + const component = shallowRender(defaultProps); + + // Ensure all promises resolve + await new Promise(resolve => process.nextTick(resolve)); + // Ensure the state changes are reflected + component.update(); + + component.setState({ file: mockFile, isLegacyFile: false }); + await component.instance().import(); + + expect(importFileMock).toHaveBeenCalledWith(defaultProps.http, mockFile, true); + expect(component.state()).toMatchObject({ + conflictedIndexPatterns: undefined, + conflictedSavedObjectsLinkedToSavedSearches: undefined, + conflictedSearchDocs: undefined, + importCount: 0, + status: 'idle', + error: undefined, + unmatchedReferences: [ + { + existingIndexPatternId: 'MyIndexPattern*', + newIndexPatternId: undefined, + list: [ + { + id: '1', + type: 'visualization', + title: 'My Visualization', + }, + ], + }, + ], + }); + }); + + it('should allow conflict resolution', async () => { + const component = shallowRender(defaultProps); + + // Ensure all promises resolve + await new Promise(resolve => process.nextTick(resolve)); + // Ensure the state changes are reflected + component.update(); + + component.setState({ file: mockFile, isLegacyFile: false }); + await component.instance().import(); + + // Ensure it looks right + component.update(); + expect(component).toMatchSnapshot(); + + // Ensure we can change the resolution + component.instance().onIndexChanged('MyIndexPattern*', { target: { value: '2' } }); + expect(component.state('unmatchedReferences')![0].newIndexPatternId).toBe('2'); + + // Let's resolve now + await component + .find('EuiButton[data-test-subj="importSavedObjectsConfirmBtn"]') + .simulate('click'); + // Ensure all promises resolve + await new Promise(resolve => process.nextTick(resolve)); + expect(resolveImportErrorsMock).toMatchSnapshot(); + }); + + it('should handle errors', async () => { + const component = shallowRender(defaultProps); + + // Ensure all promises resolve + await new Promise(resolve => process.nextTick(resolve)); + // Ensure the state changes are reflected + component.update(); + + resolveImportErrorsMock.mockImplementation(() => ({ + status: 'success', + importCount: 0, + failedImports: [ + { + obj: { + type: 'visualization', + id: '1', + }, + error: { + type: 'unknown', + }, + }, + ], + })); + + component.setState({ file: mockFile, isLegacyFile: false }); + + // Go through the import flow + await component.instance().import(); + component.update(); + // Set a resolution + component.instance().onIndexChanged('MyIndexPattern*', { target: { value: '2' } }); + await component + .find('EuiButton[data-test-subj="importSavedObjectsConfirmBtn"]') + .simulate('click'); + // Ensure all promises resolve + await new Promise(resolve => process.nextTick(resolve)); + + expect(component.state('failedImports')).toEqual([ + { + error: { + type: 'unknown', + }, + obj: { + id: '1', + type: 'visualization', + }, + }, + ]); + expect(component.find('EuiFlyoutBody EuiCallOut')).toMatchSnapshot(); + }); + }); + + describe('errors', () => { + it('should display unsupported type errors properly', async () => { + const component = shallowRender(defaultProps); + + // Ensure all promises resolve + await Promise.resolve(); + // Ensure the state changes are reflected + component.update(); + + importFileMock.mockImplementation(() => ({ + success: false, + successCount: 0, + errors: [ + { + id: '1', + type: 'wigwags', + title: 'My Title', + error: { + type: 'unsupported_type', + }, + }, + ], + })); + resolveImportErrorsMock.mockImplementation(() => ({ + status: 'success', + importCount: 0, + failedImports: [ + { + error: { + type: 'unsupported_type', + }, + obj: { + id: '1', + type: 'wigwags', + title: 'My Title', + }, + }, + ], + })); + + component.setState({ file: mockFile, isLegacyFile: false }); + + // Go through the import flow + await component.instance().import(); + component.update(); + + // Ensure all promises resolve + await Promise.resolve(); + + expect(component.state('status')).toBe('success'); + expect(component.state('failedImports')).toEqual([ + { + error: { + type: 'unsupported_type', + }, + obj: { + id: '1', + type: 'wigwags', + title: 'My Title', + }, + }, + ]); + expect(component.find('EuiFlyout EuiCallOut')).toMatchSnapshot(); + }); + }); + + describe('legacy conflicts', () => { + const mockData = [ + { + _id: '1', + _type: 'search', + }, + { + _id: '2', + _type: 'index-pattern', + }, + { + _id: '3', + _type: 'invalid', + }, + ]; + + const mockConflictedIndexPatterns = [ + { + doc: { + _type: 'index-pattern', + _id: '1', + _source: { + title: 'MyIndexPattern*', + }, + }, + obj: { + searchSource: { + getOwnField: (field: string) => { + if (field === 'index') { + return 'MyIndexPattern*'; + } + if (field === 'filter') { + return [{ meta: { index: 'filterIndex' } }]; + } + }, + }, + _serialize: () => { + return { references: [{ id: 'MyIndexPattern*' }, { id: 'filterIndex' }] }; + }, + }, + }, + ]; + + const mockConflictedSavedObjectsLinkedToSavedSearches = [2]; + const mockConflictedSearchDocs = [3]; + + beforeEach(() => { + importLegacyFileMock.mockImplementation(() => mockData); + resolveSavedObjectsMock.mockImplementation(() => ({ + conflictedIndexPatterns: mockConflictedIndexPatterns, + conflictedSavedObjectsLinkedToSavedSearches: mockConflictedSavedObjectsLinkedToSavedSearches, + conflictedSearchDocs: mockConflictedSearchDocs, + importedObjectCount: 2, + confirmModalPromise: () => {}, + })); + }); + + it('should figure out unmatchedReferences', async () => { + const component = shallowRender(defaultProps); + + // Ensure all promises resolve + await new Promise(resolve => process.nextTick(resolve)); + // Ensure the state changes are reflected + component.update(); + + component.setState({ file: legacyMockFile, isLegacyFile: true }); + await component.instance().legacyImport(); + + expect(importLegacyFileMock).toHaveBeenCalledWith(legacyMockFile); + // Remove the last element from data since it should be filtered out + expect(resolveSavedObjectsMock).toHaveBeenCalledWith( + mockData.slice(0, 2).map(doc => ({ ...doc, _migrationVersion: {} })), + true, + defaultProps.serviceRegistry.all().map(s => s.service), + defaultProps.indexPatterns, + defaultProps.overlays.openConfirm + ); + + expect(component.state()).toMatchObject({ + conflictedIndexPatterns: mockConflictedIndexPatterns, + conflictedSavedObjectsLinkedToSavedSearches: mockConflictedSavedObjectsLinkedToSavedSearches, + conflictedSearchDocs: mockConflictedSearchDocs, + importCount: 2, + status: 'idle', + error: undefined, + unmatchedReferences: [ + { + existingIndexPatternId: 'MyIndexPattern*', + newIndexPatternId: undefined, + list: [ + { + id: 'MyIndexPattern*', + title: 'MyIndexPattern*', + type: 'index-pattern', + }, + ], + }, + { + existingIndexPatternId: 'filterIndex', + list: [ + { + id: 'filterIndex', + title: 'MyIndexPattern*', + type: 'index-pattern', + }, + ], + newIndexPatternId: undefined, + }, + ], + }); + }); + + it('should allow conflict resolution', async () => { + const component = shallowRender(defaultProps); + + // Ensure all promises resolve + await new Promise(resolve => process.nextTick(resolve)); + // Ensure the state changes are reflected + component.update(); + + component.setState({ file: legacyMockFile, isLegacyFile: true }); + await component.instance().legacyImport(); + + // Ensure it looks right + component.update(); + expect(component).toMatchSnapshot(); + + // Ensure we can change the resolution + component.instance().onIndexChanged('MyIndexPattern*', { target: { value: '2' } }); + expect(component.state('unmatchedReferences')![0].newIndexPatternId).toBe('2'); + + // Let's resolve now + await component + .find('EuiButton[data-test-subj="importSavedObjectsConfirmBtn"]') + .simulate('click'); + // Ensure all promises resolve + await new Promise(resolve => process.nextTick(resolve)); + expect(resolveIndexPatternConflictsMock).toHaveBeenCalledWith( + component.instance().resolutions, + mockConflictedIndexPatterns, + true, + defaultProps.indexPatterns + ); + expect(saveObjectsMock).toHaveBeenCalledWith( + mockConflictedSavedObjectsLinkedToSavedSearches, + true + ); + expect(resolveSavedSearchesMock).toHaveBeenCalledWith( + mockConflictedSearchDocs, + defaultProps.serviceRegistry.all().map(s => s.service), + defaultProps.indexPatterns, + true + ); + }); + + it('should handle errors', async () => { + const component = shallowRender(defaultProps); + + // Ensure all promises resolve + await new Promise(resolve => process.nextTick(resolve)); + // Ensure the state changes are reflected + component.update(); + + resolveIndexPatternConflictsMock.mockImplementation(() => { + throw new Error('foobar'); + }); + + component.setState({ file: legacyMockFile, isLegacyFile: true }); + + // Go through the import flow + await component.instance().legacyImport(); + component.update(); + // Set a resolution + component.instance().onIndexChanged('MyIndexPattern*', { target: { value: '2' } }); + await component + .find('EuiButton[data-test-subj="importSavedObjectsConfirmBtn"]') + .simulate('click'); + // Ensure all promises resolve + await new Promise(resolve => process.nextTick(resolve)); + + expect(component.state('error')).toEqual('foobar'); + expect(component.find('EuiFlyoutBody EuiCallOut')).toMatchSnapshot(); + }); + }); +}); diff --git a/src/plugins/saved_objects_management/public/management_section/objects_table/components/flyout.tsx b/src/plugins/saved_objects_management/public/management_section/objects_table/components/flyout.tsx new file mode 100644 index 0000000000000..45788dcb601ae --- /dev/null +++ b/src/plugins/saved_objects_management/public/management_section/objects_table/components/flyout.tsx @@ -0,0 +1,981 @@ +/* + * 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, { Component, Fragment } from 'react'; +import { take, get as getField } from 'lodash'; +import { + EuiFlyout, + EuiFlyoutBody, + EuiFlyoutFooter, + EuiFlyoutHeader, + EuiButtonEmpty, + EuiButton, + EuiText, + EuiTitle, + EuiForm, + EuiFormRow, + EuiSwitch, + // @ts-ignore + EuiFilePicker, + EuiInMemoryTable, + EuiSelect, + EuiFlexGroup, + EuiFlexItem, + EuiLoadingKibana, + EuiCallOut, + EuiSpacer, + EuiLink, + EuiConfirmModal, + EuiOverlayMask, + EUI_MODAL_CONFIRM_BUTTON, +} from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { OverlayStart, HttpStart } from 'src/core/public'; +import { IndexPatternsContract, IIndexPattern } from '../../../../../data/public'; +import { + importFile, + importLegacyFile, + resolveImportErrors, + logLegacyImport, + getDefaultTitle, + processImportResponse, + ProcessedImportResponse, +} from '../../../lib'; +import { + resolveSavedObjects, + resolveSavedSearches, + resolveIndexPatternConflicts, + saveObjects, +} from '../../../lib/resolve_saved_objects'; +import { ISavedObjectsManagementServiceRegistry } from '../../../services'; + +export interface FlyoutProps { + serviceRegistry: ISavedObjectsManagementServiceRegistry; + allowedTypes: string[]; + close: () => void; + done: () => void; + newIndexPatternUrl: string; + indexPatterns: IndexPatternsContract; + overlays: OverlayStart; + http: HttpStart; +} + +export interface FlyoutState { + conflictedIndexPatterns?: any[]; + conflictedSavedObjectsLinkedToSavedSearches?: any[]; + conflictedSearchDocs?: any[]; + unmatchedReferences?: ProcessedImportResponse['unmatchedReferences']; + failedImports?: ProcessedImportResponse['failedImports']; + conflictingRecord?: ConflictingRecord; + error?: string; + file?: File; + importCount: number; + indexPatterns?: IIndexPattern[]; + isOverwriteAllChecked: boolean; + loadingMessage?: string; + isLegacyFile: boolean; + status: string; +} + +interface ConflictingRecord { + id: string; + type: string; + title: string; + done: (success: boolean) => void; +} + +export class Flyout extends Component { + constructor(props: FlyoutProps) { + super(props); + + this.state = { + conflictedIndexPatterns: undefined, + conflictedSavedObjectsLinkedToSavedSearches: undefined, + conflictedSearchDocs: undefined, + unmatchedReferences: undefined, + conflictingRecord: undefined, + error: undefined, + file: undefined, + importCount: 0, + indexPatterns: undefined, + isOverwriteAllChecked: true, + loadingMessage: undefined, + isLegacyFile: false, + status: 'idle', + }; + } + + componentDidMount() { + this.fetchIndexPatterns(); + } + + fetchIndexPatterns = async () => { + const indexPatterns = await this.props.indexPatterns.getFields(['id', 'title']); + this.setState({ indexPatterns } as any); + }; + + changeOverwriteAll = () => { + this.setState(state => ({ + isOverwriteAllChecked: !state.isOverwriteAllChecked, + })); + }; + + setImportFile = (files: FileList | null) => { + if (!files || !files[0]) { + this.setState({ file: undefined, isLegacyFile: false }); + return; + } + const file = files[0]; + this.setState({ + file, + isLegacyFile: /\.json$/i.test(file.name) || file.type === 'application/json', + }); + }; + + /** + * Import + * + * Does the initial import of a file, resolveImportErrors then handles errors and retries + */ + import = async () => { + const { http } = this.props; + const { file, isOverwriteAllChecked } = this.state; + this.setState({ status: 'loading', error: undefined }); + + // Import the file + try { + const response = await importFile(http, file!, isOverwriteAllChecked); + this.setState(processImportResponse(response), () => { + // Resolve import errors right away if there's no index patterns to match + // This will ask about overwriting each object, etc + if (this.state.unmatchedReferences?.length === 0) { + this.resolveImportErrors(); + } + }); + } catch (e) { + this.setState({ + status: 'error', + error: i18n.translate('savedObjectsManagement.objectsTable.flyout.importFileErrorMessage', { + defaultMessage: 'The file could not be processed.', + }), + }); + return; + } + }; + + /** + * Get Conflict Resolutions + * + * Function iterates through the objects, displays a modal for each asking the user if they wish to overwrite it or not. + * + * @param {array} objects List of objects to request the user if they wish to overwrite it + * @return {Promise} An object with the key being "type:id" and value the resolution chosen by the user + */ + getConflictResolutions = async (objects: any[]) => { + const resolutions: Record = {}; + for (const { type, id, title } of objects) { + const overwrite = await new Promise(resolve => { + this.setState({ + conflictingRecord: { + id, + type, + title, + done: resolve, + }, + }); + }); + resolutions[`${type}:${id}`] = overwrite; + this.setState({ conflictingRecord: undefined }); + } + return resolutions; + }; + + /** + * Resolve Import Errors + * + * Function goes through the failedImports and tries to resolve the issues. + */ + resolveImportErrors = async () => { + this.setState({ + error: undefined, + status: 'loading', + loadingMessage: undefined, + }); + + try { + const updatedState = await resolveImportErrors({ + http: this.props.http, + state: this.state, + getConflictResolutions: this.getConflictResolutions, + }); + this.setState(updatedState); + } catch (e) { + this.setState({ + status: 'error', + error: i18n.translate( + 'savedObjectsManagement.objectsTable.flyout.resolveImportErrorsFileErrorMessage', + { defaultMessage: 'The file could not be processed.' } + ), + }); + } + }; + + legacyImport = async () => { + const { serviceRegistry, indexPatterns, overlays, http, allowedTypes } = this.props; + const { file, isOverwriteAllChecked } = this.state; + + this.setState({ status: 'loading', error: undefined }); + + // Log warning on server, don't wait for response + logLegacyImport(http); + + let contents; + try { + contents = await importLegacyFile(file!); + } catch (e) { + this.setState({ + status: 'error', + error: i18n.translate( + 'savedObjectsManagement.objectsTable.flyout.importLegacyFileErrorMessage', + { defaultMessage: 'The file could not be processed.' } + ), + }); + return; + } + + if (!Array.isArray(contents)) { + this.setState({ + status: 'error', + error: i18n.translate( + 'savedObjectsManagement.objectsTable.flyout.invalidFormatOfImportedFileErrorMessage', + { defaultMessage: 'Saved objects file format is invalid and cannot be imported.' } + ), + }); + return; + } + + contents = contents + .filter(content => allowedTypes.includes(content._type)) + .map(doc => ({ + ...doc, + // The server assumes that documents with no migrationVersion are up to date. + // That assumption enables Kibana and other API consumers to not have to build + // up migrationVersion prior to creating new objects. But it means that imports + // need to set migrationVersion to something other than undefined, so that imported + // docs are not seen as automatically up-to-date. + _migrationVersion: doc._migrationVersion || {}, + })); + + const { + conflictedIndexPatterns, + conflictedSavedObjectsLinkedToSavedSearches, + conflictedSearchDocs, + importedObjectCount, + failedImports, + } = await resolveSavedObjects( + contents, + isOverwriteAllChecked, + serviceRegistry.all().map(e => e.service), + indexPatterns, + overlays.openConfirm + ); + + const byId: Record = {}; + conflictedIndexPatterns + .map(({ doc, obj }) => { + return { doc, obj: obj._serialize() }; + }) + .forEach(({ doc, obj }) => + obj.references.forEach((ref: Record) => { + byId[ref.id] = byId[ref.id] != null ? byId[ref.id].concat({ doc, obj }) : [{ doc, obj }]; + }) + ); + const unmatchedReferences = Object.entries(byId).reduce( + (accum, [existingIndexPatternId, list]) => { + accum.push({ + existingIndexPatternId, + newIndexPatternId: undefined, + list: list.map(({ doc }) => ({ + id: existingIndexPatternId, + type: doc._type, + title: doc._source.title, + })), + }); + return accum; + }, + [] as any[] + ); + + this.setState({ + conflictedIndexPatterns, + conflictedSavedObjectsLinkedToSavedSearches, + conflictedSearchDocs, + failedImports, + unmatchedReferences, + importCount: importedObjectCount, + status: unmatchedReferences.length === 0 ? 'success' : 'idle', + }); + }; + + public get hasUnmatchedReferences() { + return this.state.unmatchedReferences && this.state.unmatchedReferences.length > 0; + } + + public get resolutions() { + return this.state.unmatchedReferences!.reduce( + (accum, { existingIndexPatternId, newIndexPatternId }) => { + if (newIndexPatternId) { + accum.push({ + oldId: existingIndexPatternId, + newId: newIndexPatternId, + }); + } + return accum; + }, + [] as Array<{ oldId: string; newId: string }> + ); + } + + confirmLegacyImport = async () => { + const { + conflictedIndexPatterns, + isOverwriteAllChecked, + conflictedSavedObjectsLinkedToSavedSearches, + conflictedSearchDocs, + failedImports, + } = this.state; + + const { serviceRegistry, indexPatterns } = this.props; + + this.setState({ + error: undefined, + status: 'loading', + loadingMessage: undefined, + }); + + let importCount = this.state.importCount; + + if (this.hasUnmatchedReferences) { + try { + const resolutions = this.resolutions; + + // Do not Promise.all these calls as the order matters + this.setState({ + loadingMessage: i18n.translate( + 'savedObjectsManagement.objectsTable.flyout.confirmLegacyImport.resolvingConflictsLoadingMessage', + { defaultMessage: 'Resolving conflicts…' } + ), + }); + if (resolutions.length) { + importCount += await resolveIndexPatternConflicts( + resolutions, + conflictedIndexPatterns!, + isOverwriteAllChecked, + indexPatterns + ); + } + this.setState({ + loadingMessage: i18n.translate( + 'savedObjectsManagement.objectsTable.flyout.confirmLegacyImport.savingConflictsLoadingMessage', + { defaultMessage: 'Saving conflicts…' } + ), + }); + importCount += await saveObjects( + conflictedSavedObjectsLinkedToSavedSearches!, + isOverwriteAllChecked + ); + this.setState({ + loadingMessage: i18n.translate( + 'savedObjectsManagement.objectsTable.flyout.confirmLegacyImport.savedSearchAreLinkedProperlyLoadingMessage', + { defaultMessage: 'Ensure saved searches are linked properly…' } + ), + }); + importCount += await resolveSavedSearches( + conflictedSearchDocs!, + serviceRegistry.all().map(e => e.service), + indexPatterns, + isOverwriteAllChecked + ); + this.setState({ + loadingMessage: i18n.translate( + 'savedObjectsManagement.objectsTable.flyout.confirmLegacyImport.retryingFailedObjectsLoadingMessage', + { defaultMessage: 'Retrying failed objects…' } + ), + }); + importCount += await saveObjects( + failedImports!.map(({ obj }) => obj) as any[], + isOverwriteAllChecked + ); + } catch (e) { + this.setState({ + error: e.message, + status: 'error', + loadingMessage: undefined, + }); + return; + } + } + + this.setState({ status: 'success', importCount }); + }; + + onIndexChanged = (id: string, e: any) => { + const value = e.target.value; + this.setState(state => { + const conflictIndex = state.unmatchedReferences?.findIndex( + conflict => conflict.existingIndexPatternId === id + ); + if (conflictIndex === undefined || conflictIndex === -1) { + return state; + } + + return { + unmatchedReferences: [ + ...state.unmatchedReferences!.slice(0, conflictIndex), + { + ...state.unmatchedReferences![conflictIndex], + newIndexPatternId: value, + }, + ...state.unmatchedReferences!.slice(conflictIndex + 1), + ], + } as any; + }); + }; + + renderUnmatchedReferences() { + const { unmatchedReferences } = this.state; + + if (!unmatchedReferences) { + return null; + } + + const columns = [ + { + field: 'existingIndexPatternId', + name: i18n.translate( + 'savedObjectsManagement.objectsTable.flyout.renderConflicts.columnIdName', + { defaultMessage: 'ID' } + ), + description: i18n.translate( + 'savedObjectsManagement.objectsTable.flyout.renderConflicts.columnIdDescription', + { defaultMessage: 'ID of the index pattern' } + ), + sortable: true, + }, + { + field: 'list', + name: i18n.translate( + 'savedObjectsManagement.objectsTable.flyout.renderConflicts.columnCountName', + { defaultMessage: 'Count' } + ), + description: i18n.translate( + 'savedObjectsManagement.objectsTable.flyout.renderConflicts.columnCountDescription', + { defaultMessage: 'How many affected objects' } + ), + render: (list: any[]) => { + return {list.length}; + }, + }, + { + field: 'list', + name: i18n.translate( + 'savedObjectsManagement.objectsTable.flyout.renderConflicts.columnSampleOfAffectedObjectsName', + { defaultMessage: 'Sample of affected objects' } + ), + description: i18n.translate( + 'savedObjectsManagement.objectsTable.flyout.renderConflicts.columnSampleOfAffectedObjectsDescription', + { defaultMessage: 'Sample of affected objects' } + ), + render: (list: any[]) => { + return ( +
    + {take(list, 3).map((obj, key) => ( +
  • {obj.title}
  • + ))} +
+ ); + }, + }, + { + field: 'existingIndexPatternId', + name: i18n.translate( + 'savedObjectsManagement.objectsTable.flyout.renderConflicts.columnNewIndexPatternName', + { defaultMessage: 'New index pattern' } + ), + render: (id: string) => { + const options = this.state.indexPatterns!.map( + indexPattern => + ({ + text: indexPattern.title, + value: indexPattern.id, + 'data-test-subj': `indexPatternOption-${indexPattern.title}`, + } as { text: string; value: string; 'data-test-subj'?: string }) + ); + + options.unshift({ + text: '-- Skip Import --', + value: '', + }); + + return ( + this.onIndexChanged(id, e)} + options={options} + /> + ); + }, + }, + ]; + + const pagination = { + pageSizeOptions: [5, 10, 25], + }; + + return ( + + ); + } + + renderError() { + const { error, status } = this.state; + + if (status !== 'error') { + return null; + } + + return ( + + + } + color="danger" + > +

{error}

+
+ +
+ ); + } + + renderBody() { + const { + status, + loadingMessage, + isOverwriteAllChecked, + importCount, + failedImports = [], + isLegacyFile, + } = this.state; + + if (status === 'loading') { + return ( + + + + + +

{loadingMessage}

+
+
+
+ ); + } + + // Kept backwards compatible logic + if ( + failedImports.length && + (!this.hasUnmatchedReferences || (isLegacyFile === false && status === 'success')) + ) { + return ( + + } + color="warning" + iconType="help" + > +

+ +

+

+ {failedImports + .map(({ error, obj }) => { + if (error.type === 'missing_references') { + return error.references.map(reference => { + return i18n.translate( + 'savedObjectsManagement.objectsTable.flyout.importFailedMissingReference', + { + defaultMessage: '{type} [id={id}] could not locate {refType} [id={refId}]', + values: { + id: obj.id, + type: obj.type, + refId: reference.id, + refType: reference.type, + }, + } + ); + }); + } else if (error.type === 'unsupported_type') { + return i18n.translate( + 'savedObjectsManagement.objectsTable.flyout.importFailedUnsupportedType', + { + defaultMessage: '{type} [id={id}] unsupported type', + values: { + id: obj.id, + type: obj.type, + }, + } + ); + } + return getField(error, 'body.message', (error as any).message ?? ''); + }) + .join(' ')} +

+
+ ); + } + + if (status === 'success') { + if (importCount === 0) { + return ( + + } + color="primary" + /> + ); + } + + return ( + + } + color="success" + iconType="check" + > +

+ +

+
+ ); + } + + if (this.hasUnmatchedReferences) { + return this.renderUnmatchedReferences(); + } + + return ( + + + } + > + + } + onChange={this.setImportFile} + /> + + + + } + data-test-subj="importSavedObjectsOverwriteToggle" + checked={isOverwriteAllChecked} + onChange={this.changeOverwriteAll} + /> + + + ); + } + + renderFooter() { + const { status } = this.state; + const { done, close } = this.props; + + let confirmButton; + + if (status === 'success') { + confirmButton = ( + + + + ); + } else if (this.hasUnmatchedReferences) { + confirmButton = ( + + + + ); + } else { + confirmButton = ( + + + + ); + } + + return ( + + + + + + + {confirmButton} + + ); + } + + renderSubheader() { + if (this.state.status === 'loading' || this.state.status === 'success') { + return null; + } + + let legacyFileWarning; + if (this.state.isLegacyFile) { + legacyFileWarning = ( + + } + color="warning" + iconType="help" + > +

+ +

+
+ ); + } + + let indexPatternConflictsWarning; + if (this.hasUnmatchedReferences) { + indexPatternConflictsWarning = ( + + } + color="warning" + iconType="help" + > +

+ + + + ), + }} + /> +

+
+ ); + } + + if (!legacyFileWarning && !indexPatternConflictsWarning) { + return null; + } + + return ( + + {legacyFileWarning && ( + + + {legacyFileWarning} + + )} + {indexPatternConflictsWarning && ( + + + {indexPatternConflictsWarning} + + )} + + ); + } + + overwriteConfirmed() { + this.state.conflictingRecord!.done(true); + } + + overwriteSkipped() { + this.state.conflictingRecord!.done(false); + } + + render() { + const { close } = this.props; + + let confirmOverwriteModal; + if (this.state.conflictingRecord) { + confirmOverwriteModal = ( + + +

+ +

+
+
+ ); + } + + return ( + + + +

+ +

+
+
+ + + {this.renderSubheader()} + {this.renderError()} + {this.renderBody()} + + + {this.renderFooter()} + {confirmOverwriteModal} +
+ ); + } +} diff --git a/src/plugins/saved_objects_management/public/management_section/objects_table/components/header.test.tsx b/src/plugins/saved_objects_management/public/management_section/objects_table/components/header.test.tsx new file mode 100644 index 0000000000000..891190d0bb24b --- /dev/null +++ b/src/plugins/saved_objects_management/public/management_section/objects_table/components/header.test.tsx @@ -0,0 +1,38 @@ +/* + * 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 { shallow } from 'enzyme'; +import { Header } from './header'; + +describe('Header', () => { + it('should render normally', () => { + const props = { + onExportAll: () => {}, + onImport: () => {}, + onRefresh: () => {}, + totalCount: 4, + filteredCount: 2, + }; + + const component = shallow(
); + + expect(component).toMatchSnapshot(); + }); +}); diff --git a/src/plugins/saved_objects_management/public/management_section/objects_table/components/header.tsx b/src/plugins/saved_objects_management/public/management_section/objects_table/components/header.tsx new file mode 100644 index 0000000000000..7a9584f08d632 --- /dev/null +++ b/src/plugins/saved_objects_management/public/management_section/objects_table/components/header.tsx @@ -0,0 +1,114 @@ +/* + * 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, { Fragment } from 'react'; +import { + EuiSpacer, + EuiTitle, + EuiFlexGroup, + EuiFlexItem, + EuiText, + EuiTextColor, + EuiButtonEmpty, +} from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n/react'; + +export const Header = ({ + onExportAll, + onImport, + onRefresh, + filteredCount, +}: { + onExportAll: () => void; + onImport: () => void; + onRefresh: () => void; + filteredCount: number; +}) => ( + + + + +

+ +

+
+
+ + + + + + + + + + + + + + + + + + + + +
+ + +

+ + + +

+
+ +
+); diff --git a/src/plugins/saved_objects_management/public/management_section/objects_table/components/index.ts b/src/plugins/saved_objects_management/public/management_section/objects_table/components/index.ts new file mode 100644 index 0000000000000..9c8736a9011eb --- /dev/null +++ b/src/plugins/saved_objects_management/public/management_section/objects_table/components/index.ts @@ -0,0 +1,23 @@ +/* + * 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 { Header } from './header'; +export { Table } from './table'; +export { Flyout } from './flyout'; +export { Relationships } from './relationships'; diff --git a/src/plugins/saved_objects_management/public/management_section/objects_table/components/relationships.test.tsx b/src/plugins/saved_objects_management/public/management_section/objects_table/components/relationships.test.tsx new file mode 100644 index 0000000000000..347f2d977015c --- /dev/null +++ b/src/plugins/saved_objects_management/public/management_section/objects_table/components/relationships.test.tsx @@ -0,0 +1,338 @@ +/* + * 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 { shallowWithI18nProvider } from 'test_utils/enzyme_helpers'; +import { httpServiceMock } from '../../../../../../core/public/mocks'; +import { Relationships, RelationshipsProps } from './relationships'; + +jest.mock('../../../lib/fetch_export_by_type_and_search', () => ({ + fetchExportByTypeAndSearch: jest.fn(), +})); + +jest.mock('../../../lib/fetch_export_objects', () => ({ + fetchExportObjects: jest.fn(), +})); + +describe('Relationships', () => { + it('should render index patterns normally', async () => { + const props: RelationshipsProps = { + goInspectObject: () => {}, + canGoInApp: () => true, + basePath: httpServiceMock.createSetupContract().basePath, + getRelationships: jest.fn().mockImplementation(() => [ + { + type: 'search', + id: '1', + relationship: 'parent', + meta: { + editUrl: '/management/kibana/objects/savedSearches/1', + icon: 'search', + inAppUrl: { + path: '/app/kibana#/discover/1', + uiCapabilitiesPath: 'discover.show', + }, + title: 'My Search Title', + }, + }, + { + type: 'visualization', + id: '2', + relationship: 'parent', + meta: { + editUrl: '/management/kibana/objects/savedVisualizations/2', + icon: 'visualizeApp', + inAppUrl: { + path: '/app/kibana#/visualize/edit/2', + uiCapabilitiesPath: 'visualize.show', + }, + title: 'My Visualization Title', + }, + }, + ]), + savedObject: { + id: '1', + type: 'index-pattern', + attributes: {}, + references: [], + meta: { + title: 'MyIndexPattern*', + icon: 'indexPatternApp', + editUrl: '#/management/kibana/index_patterns/1', + inAppUrl: { + path: '/management/kibana/index_patterns/1', + uiCapabilitiesPath: 'management.kibana.index_patterns', + }, + }, + }, + close: jest.fn(), + }; + + const component = shallowWithI18nProvider(); + + // Make sure we are showing loading + expect(component.find('EuiLoadingKibana').length).toBe(1); + + // Ensure all promises resolve + await new Promise(resolve => process.nextTick(resolve)); + // Ensure the state changes are reflected + component.update(); + + expect(props.getRelationships).toHaveBeenCalled(); + expect(component).toMatchSnapshot(); + }); + + it('should render searches normally', async () => { + const props: RelationshipsProps = { + goInspectObject: () => {}, + canGoInApp: () => true, + basePath: httpServiceMock.createSetupContract().basePath, + getRelationships: jest.fn().mockImplementation(() => [ + { + type: 'index-pattern', + id: '1', + relationship: 'child', + meta: { + editUrl: '/management/kibana/index_patterns/1', + icon: 'indexPatternApp', + inAppUrl: { + path: '/app/kibana#/management/kibana/index_patterns/1', + uiCapabilitiesPath: 'management.kibana.index_patterns', + }, + title: 'My Index Pattern', + }, + }, + { + type: 'visualization', + id: '2', + relationship: 'parent', + meta: { + editUrl: '/management/kibana/objects/savedVisualizations/2', + icon: 'visualizeApp', + inAppUrl: { + path: '/app/kibana#/visualize/edit/2', + uiCapabilitiesPath: 'visualize.show', + }, + title: 'My Visualization Title', + }, + }, + ]), + savedObject: { + id: '1', + type: 'search', + attributes: {}, + references: [], + meta: { + title: 'MySearch', + icon: 'search', + editUrl: '#/management/kibana/objects/savedSearches/1', + inAppUrl: { + path: '/discover/1', + uiCapabilitiesPath: 'discover.show', + }, + }, + }, + close: jest.fn(), + }; + + const component = shallowWithI18nProvider(); + + // Make sure we are showing loading + expect(component.find('EuiLoadingKibana').length).toBe(1); + + // Ensure all promises resolve + await new Promise(resolve => process.nextTick(resolve)); + // Ensure the state changes are reflected + component.update(); + + expect(props.getRelationships).toHaveBeenCalled(); + expect(component).toMatchSnapshot(); + }); + + it('should render visualizations normally', async () => { + const props: RelationshipsProps = { + goInspectObject: () => {}, + canGoInApp: () => true, + basePath: httpServiceMock.createSetupContract().basePath, + getRelationships: jest.fn().mockImplementation(() => [ + { + type: 'dashboard', + id: '1', + relationship: 'parent', + meta: { + editUrl: '/management/kibana/objects/savedDashboards/1', + icon: 'dashboardApp', + inAppUrl: { + path: '/app/kibana#/dashboard/1', + uiCapabilitiesPath: 'dashboard.show', + }, + title: 'My Dashboard 1', + }, + }, + { + type: 'dashboard', + id: '2', + relationship: 'parent', + meta: { + editUrl: '/management/kibana/objects/savedDashboards/2', + icon: 'dashboardApp', + inAppUrl: { + path: '/app/kibana#/dashboard/2', + uiCapabilitiesPath: 'dashboard.show', + }, + title: 'My Dashboard 2', + }, + }, + ]), + savedObject: { + id: '1', + type: 'visualization', + attributes: {}, + references: [], + meta: { + title: 'MyViz', + icon: 'visualizeApp', + editUrl: '#/management/kibana/objects/savedVisualizations/1', + inAppUrl: { + path: '/visualize/edit/1', + uiCapabilitiesPath: 'visualize.show', + }, + }, + }, + close: jest.fn(), + }; + + const component = shallowWithI18nProvider(); + + // Make sure we are showing loading + expect(component.find('EuiLoadingKibana').length).toBe(1); + + // Ensure all promises resolve + await new Promise(resolve => process.nextTick(resolve)); + // Ensure the state changes are reflected + component.update(); + + expect(props.getRelationships).toHaveBeenCalled(); + expect(component).toMatchSnapshot(); + }); + + it('should render dashboards normally', async () => { + const props: RelationshipsProps = { + goInspectObject: () => {}, + canGoInApp: () => true, + basePath: httpServiceMock.createSetupContract().basePath, + getRelationships: jest.fn().mockImplementation(() => [ + { + type: 'visualization', + id: '1', + relationship: 'child', + meta: { + editUrl: '/management/kibana/objects/savedVisualizations/1', + icon: 'visualizeApp', + inAppUrl: { + path: '/app/kibana#/visualize/edit/1', + uiCapabilitiesPath: 'visualize.show', + }, + title: 'My Visualization Title 1', + }, + }, + { + type: 'visualization', + id: '2', + relationship: 'child', + meta: { + editUrl: '/management/kibana/objects/savedVisualizations/2', + icon: 'visualizeApp', + inAppUrl: { + path: '/app/kibana#/visualize/edit/2', + uiCapabilitiesPath: 'visualize.show', + }, + title: 'My Visualization Title 2', + }, + }, + ]), + savedObject: { + id: '1', + type: 'dashboard', + attributes: {}, + references: [], + meta: { + title: 'MyDashboard', + icon: 'dashboardApp', + editUrl: '#/management/kibana/objects/savedDashboards/1', + inAppUrl: { + path: '/dashboard/1', + uiCapabilitiesPath: 'dashboard.show', + }, + }, + }, + close: jest.fn(), + }; + + const component = shallowWithI18nProvider(); + + // Make sure we are showing loading + expect(component.find('EuiLoadingKibana').length).toBe(1); + + // Ensure all promises resolve + await new Promise(resolve => process.nextTick(resolve)); + // Ensure the state changes are reflected + component.update(); + + expect(props.getRelationships).toHaveBeenCalled(); + expect(component).toMatchSnapshot(); + }); + + it('should render errors', async () => { + const props: RelationshipsProps = { + goInspectObject: () => {}, + canGoInApp: () => true, + basePath: httpServiceMock.createSetupContract().basePath, + getRelationships: jest.fn().mockImplementation(() => { + throw new Error('foo'); + }), + savedObject: { + id: '1', + type: 'dashboard', + attributes: {}, + references: [], + meta: { + title: 'MyDashboard', + icon: 'dashboardApp', + editUrl: '#/management/kibana/objects/savedDashboards/1', + inAppUrl: { + path: '/dashboard/1', + uiCapabilitiesPath: 'dashboard.show', + }, + }, + }, + close: jest.fn(), + }; + + const component = shallowWithI18nProvider(); + + // Ensure all promises resolve + await new Promise(resolve => process.nextTick(resolve)); + // Ensure the state changes are reflected + component.update(); + + expect(props.getRelationships).toHaveBeenCalled(); + expect(component).toMatchSnapshot(); + }); +}); diff --git a/src/plugins/saved_objects_management/public/management_section/objects_table/components/relationships.tsx b/src/plugins/saved_objects_management/public/management_section/objects_table/components/relationships.tsx new file mode 100644 index 0000000000000..ddb262138d565 --- /dev/null +++ b/src/plugins/saved_objects_management/public/management_section/objects_table/components/relationships.tsx @@ -0,0 +1,347 @@ +/* + * 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, { Component } from 'react'; +import { + EuiTitle, + EuiFlyout, + EuiFlyoutBody, + EuiFlyoutHeader, + EuiLink, + EuiIcon, + EuiCallOut, + EuiLoadingKibana, + EuiInMemoryTable, + EuiToolTip, + EuiText, + EuiSpacer, +} from '@elastic/eui'; +import { FilterConfig } from '@elastic/eui/src/components/search_bar/filters/filters'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { IBasePath } from 'src/core/public'; +import { getDefaultTitle, getSavedObjectLabel } from '../../../lib'; +import { SavedObjectWithMetadata, SavedObjectRelation } from '../../../types'; + +export interface RelationshipsProps { + basePath: IBasePath; + getRelationships: (type: string, id: string) => Promise; + savedObject: SavedObjectWithMetadata; + close: () => void; + goInspectObject: (obj: SavedObjectWithMetadata) => void; + canGoInApp: (obj: SavedObjectWithMetadata) => boolean; +} + +export interface RelationshipsState { + relationships: SavedObjectRelation[]; + isLoading: boolean; + error?: string; +} + +export class Relationships extends Component { + constructor(props: RelationshipsProps) { + super(props); + + this.state = { + relationships: [], + isLoading: false, + error: undefined, + }; + } + + UNSAFE_componentWillMount() { + this.getRelationshipData(); + } + + UNSAFE_componentWillReceiveProps(nextProps: RelationshipsProps) { + if (nextProps.savedObject.id !== this.props.savedObject.id) { + this.getRelationshipData(); + } + } + + async getRelationshipData() { + const { savedObject, getRelationships } = this.props; + + this.setState({ isLoading: true }); + + try { + const relationships = await getRelationships(savedObject.type, savedObject.id); + this.setState({ relationships, isLoading: false, error: undefined }); + } catch (err) { + this.setState({ error: err.message, isLoading: false }); + } + } + + renderError() { + const { error } = this.state; + + if (!error) { + return null; + } + + return ( + + } + color="danger" + > + {error} + + ); + } + + renderRelationships() { + const { goInspectObject, savedObject, basePath } = this.props; + const { relationships, isLoading, error } = this.state; + + if (error) { + return this.renderError(); + } + + if (isLoading) { + return ; + } + + const columns = [ + { + field: 'type', + name: i18n.translate('savedObjectsManagement.objectsTable.relationships.columnTypeName', { + defaultMessage: 'Type', + }), + width: '50px', + align: 'center', + description: i18n.translate( + 'savedObjectsManagement.objectsTable.relationships.columnTypeDescription', + { defaultMessage: 'Type of the saved object' } + ), + sortable: false, + render: (type: string, object: SavedObjectWithMetadata) => { + return ( + + + + ); + }, + }, + { + field: 'relationship', + name: i18n.translate( + 'savedObjectsManagement.objectsTable.relationships.columnRelationshipName', + { defaultMessage: 'Direct relationship' } + ), + dataType: 'string', + sortable: false, + width: '125px', + 'data-test-subj': 'directRelationship', + render: (relationship: string) => { + if (relationship === 'parent') { + return ( + + + + ); + } + if (relationship === 'child') { + return ( + + + + ); + } + }, + }, + { + field: 'meta.title', + name: i18n.translate('savedObjectsManagement.objectsTable.relationships.columnTitleName', { + defaultMessage: 'Title', + }), + description: i18n.translate( + 'savedObjectsManagement.objectsTable.relationships.columnTitleDescription', + { defaultMessage: 'Title of the saved object' } + ), + dataType: 'string', + sortable: false, + render: (title: string, object: SavedObjectWithMetadata) => { + const { path = '' } = object.meta.inAppUrl || {}; + const canGoInApp = this.props.canGoInApp(object); + if (!canGoInApp) { + return ( + + {title || getDefaultTitle(object)} + + ); + } + return ( + + {title || getDefaultTitle(object)} + + ); + }, + }, + { + name: i18n.translate( + 'savedObjectsManagement.objectsTable.relationships.columnActionsName', + { defaultMessage: 'Actions' } + ), + actions: [ + { + name: i18n.translate( + 'savedObjectsManagement.objectsTable.relationships.columnActions.inspectActionName', + { defaultMessage: 'Inspect' } + ), + description: i18n.translate( + 'savedObjectsManagement.objectsTable.relationships.columnActions.inspectActionDescription', + { defaultMessage: 'Inspect this saved object' } + ), + type: 'icon', + icon: 'inspect', + 'data-test-subj': 'relationshipsTableAction-inspect', + onClick: (object: SavedObjectWithMetadata) => goInspectObject(object), + available: (object: SavedObjectWithMetadata) => !!object.meta.editUrl, + }, + ], + }, + ]; + + const filterTypesMap = new Map( + relationships.map(relationship => [ + relationship.type, + { + value: relationship.type, + name: relationship.type, + view: relationship.type, + }, + ]) + ); + + const search = { + filters: [ + { + type: 'field_value_selection', + field: 'relationship', + name: i18n.translate( + 'savedObjectsManagement.objectsTable.relationships.search.filters.relationship.name', + { defaultMessage: 'Direct relationship' } + ), + multiSelect: 'or', + options: [ + { + value: 'parent', + name: 'parent', + view: i18n.translate( + 'savedObjectsManagement.objectsTable.relationships.search.filters.relationship.parentAsValue.view', + { defaultMessage: 'Parent' } + ), + }, + { + value: 'child', + name: 'child', + view: i18n.translate( + 'savedObjectsManagement.objectsTable.relationships.search.filters.relationship.childAsValue.view', + { defaultMessage: 'Child' } + ), + }, + ], + }, + { + type: 'field_value_selection', + field: 'type', + name: i18n.translate( + 'savedObjectsManagement.objectsTable.relationships.search.filters.type.name', + { defaultMessage: 'Type' } + ), + multiSelect: 'or', + options: [...filterTypesMap.values()], + }, + ] as FilterConfig[], + }; + + return ( +
+ +

+ {i18n.translate( + 'savedObjectsManagement.objectsTable.relationships.relationshipsTitle', + { + defaultMessage: + 'Here are the saved objects related to {title}. ' + + 'Deleting this {type} affects its parent objects, but not its children.', + values: { + type: savedObject.type, + title: savedObject.meta.title || getDefaultTitle(savedObject), + }, + } + )} +

+
+ + ({ + 'data-test-subj': `relationshipsTableRow`, + })} + /> +
+ ); + } + + render() { + const { close, savedObject } = this.props; + + return ( + + + +

+ + + +    + {savedObject.meta.title || getDefaultTitle(savedObject)} +

+
+
+ + {this.renderRelationships()} +
+ ); + } +} 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 new file mode 100644 index 0000000000000..356f227773610 --- /dev/null +++ b/src/plugins/saved_objects_management/public/management_section/objects_table/components/table.test.tsx @@ -0,0 +1,126 @@ +/* + * 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 { shallowWithI18nProvider, mountWithI18nProvider } from 'test_utils/enzyme_helpers'; +// @ts-ignore +import { findTestSubject } from '@elastic/eui/lib/test'; +import { keyCodes } from '@elastic/eui'; +import { httpServiceMock } from '../../../../../../core/public/mocks'; +import { actionServiceMock } from '../../../services/action_service.mock'; +import { Table, TableProps } from './table'; + +const defaultProps: TableProps = { + basePath: httpServiceMock.createSetupContract().basePath, + actionRegistry: actionServiceMock.createStart(), + selectedSavedObjects: [ + { + id: '1', + type: 'index-pattern', + attributes: {}, + references: [], + meta: { + title: `MyIndexPattern*`, + icon: 'indexPatternApp', + editUrl: '#/management/kibana/index_patterns/1', + inAppUrl: { + path: '/management/kibana/index_patterns/1', + uiCapabilitiesPath: 'management.kibana.index_patterns', + }, + }, + }, + ], + selectionConfig: { + onSelectionChange: () => {}, + }, + filterOptions: [{ value: 2 }], + onDelete: () => {}, + onExport: () => {}, + goInspectObject: () => {}, + canGoInApp: () => true, + pageIndex: 1, + pageSize: 2, + items: [ + { + id: '1', + type: 'index-pattern', + attributes: {}, + references: [], + meta: { + title: `MyIndexPattern*`, + icon: 'indexPatternApp', + editUrl: '#/management/kibana/index_patterns/1', + inAppUrl: { + path: '/management/kibana/index_patterns/1', + uiCapabilitiesPath: 'management.kibana.index_patterns', + }, + }, + }, + ], + itemId: 'id', + totalItemCount: 3, + onQueryChange: () => {}, + onTableChange: () => {}, + isSearching: false, + onShowRelationships: () => {}, + canDelete: true, +}; + +describe('Table', () => { + it('should render normally', () => { + const component = shallowWithI18nProvider(
); + + expect(component).toMatchSnapshot(); + }); + + it('should handle query parse error', () => { + const onQueryChangeMock = jest.fn(); + const customizedProps = { + ...defaultProps, + onQueryChange: onQueryChangeMock, + }; + + const component = mountWithI18nProvider(
); + const searchBar = findTestSubject(component, 'savedObjectSearchBar'); + + // Send invalid query + searchBar.simulate('keyup', { keyCode: keyCodes.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' } }); + expect(onQueryChangeMock).toHaveBeenCalledTimes(1); + expect(component.state().isSearchTextValid).toBe(true); + }); + + it(`prevents saved objects from being deleted`, () => { + const selectedSavedObjects = [ + { type: 'visualization' }, + { type: 'search' }, + { type: 'index-pattern' }, + ] as any; + const customizedProps = { ...defaultProps, selectedSavedObjects, canDelete: false }; + const component = shallowWithI18nProvider(
); + + expect(component).toMatchSnapshot(); + }); +}); diff --git a/src/plugins/saved_objects_management/public/management_section/objects_table/components/table.tsx b/src/plugins/saved_objects_management/public/management_section/objects_table/components/table.tsx new file mode 100644 index 0000000000000..5b574e4b3d331 --- /dev/null +++ b/src/plugins/saved_objects_management/public/management_section/objects_table/components/table.tsx @@ -0,0 +1,401 @@ +/* + * 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 { IBasePath } from 'src/core/public'; +import React, { PureComponent, Fragment } from 'react'; +import { + // @ts-ignore + EuiSearchBar, + EuiBasicTable, + EuiButton, + EuiIcon, + EuiLink, + EuiSpacer, + EuiToolTip, + EuiFormErrorText, + EuiPopover, + EuiSwitch, + EuiFormRow, + EuiText, + EuiTableFieldDataColumnType, + EuiTableActionsColumnType, +} from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { getDefaultTitle, getSavedObjectLabel } from '../../../lib'; +import { SavedObjectWithMetadata } from '../../../types'; +import { + SavedObjectsManagementActionServiceStart, + SavedObjectsManagementAction, +} from '../../../services'; + +export interface TableProps { + basePath: IBasePath; + actionRegistry: SavedObjectsManagementActionServiceStart; + selectedSavedObjects: SavedObjectWithMetadata[]; + selectionConfig: { + onSelectionChange: (selection: SavedObjectWithMetadata[]) => void; + }; + filterOptions: any[]; + canDelete: boolean; + onDelete: () => void; + onExport: (includeReferencesDeep: boolean) => void; + goInspectObject: (obj: SavedObjectWithMetadata) => void; + pageIndex: number; + pageSize: number; + items: SavedObjectWithMetadata[]; + itemId: string | (() => string); + totalItemCount: number; + onQueryChange: (query: any) => void; + onTableChange: (table: any) => void; + isSearching: boolean; + onShowRelationships: (object: SavedObjectWithMetadata) => void; + canGoInApp: (obj: SavedObjectWithMetadata) => boolean; +} + +interface TableState { + isSearchTextValid: boolean; + parseErrorMessage: any; + isExportPopoverOpen: boolean; + isIncludeReferencesDeepChecked: boolean; + activeAction?: SavedObjectsManagementAction; +} + +export class Table extends PureComponent { + state: TableState = { + isSearchTextValid: true, + parseErrorMessage: null, + isExportPopoverOpen: false, + isIncludeReferencesDeepChecked: true, + activeAction: undefined, + }; + + constructor(props: TableProps) { + super(props); + } + + onChange = ({ query, error }: any) => { + if (error) { + this.setState({ + isSearchTextValid: false, + parseErrorMessage: error.message, + }); + return; + } + + this.setState({ + isSearchTextValid: true, + parseErrorMessage: null, + }); + this.props.onQueryChange({ query }); + }; + + closeExportPopover = () => { + this.setState({ isExportPopoverOpen: false }); + }; + + toggleExportPopoverVisibility = () => { + this.setState(state => ({ + isExportPopoverOpen: !state.isExportPopoverOpen, + })); + }; + + toggleIsIncludeReferencesDeepChecked = () => { + this.setState(state => ({ + isIncludeReferencesDeepChecked: !state.isIncludeReferencesDeepChecked, + })); + }; + + onExportClick = () => { + const { onExport } = this.props; + const { isIncludeReferencesDeepChecked } = this.state; + onExport(isIncludeReferencesDeepChecked); + this.setState({ isExportPopoverOpen: false }); + }; + + render() { + const { + pageIndex, + pageSize, + itemId, + items, + totalItemCount, + isSearching, + filterOptions, + selectionConfig: selection, + onDelete, + selectedSavedObjects, + onTableChange, + goInspectObject, + onShowRelationships, + basePath, + actionRegistry, + } = this.props; + + const pagination = { + pageIndex, + pageSize, + totalItemCount, + pageSizeOptions: [5, 10, 20, 50], + }; + + const filters = [ + { + type: 'field_value_selection', + field: 'type', + name: i18n.translate('savedObjectsManagement.objectsTable.table.typeFilterName', { + defaultMessage: 'Type', + }), + multiSelect: 'or', + options: filterOptions, + }, + // Add this back in once we have tag support + // { + // type: 'field_value_selection', + // field: 'tag', + // name: 'Tags', + // multiSelect: 'or', + // options: [], + // }, + ]; + + const columns = [ + { + field: 'type', + name: i18n.translate('savedObjectsManagement.objectsTable.table.columnTypeName', { + defaultMessage: 'Type', + }), + width: '50px', + align: 'center', + description: i18n.translate( + 'savedObjectsManagement.objectsTable.table.columnTypeDescription', + { defaultMessage: 'Type of the saved object' } + ), + sortable: false, + 'data-test-subj': 'savedObjectsTableRowType', + render: (type: string, object: SavedObjectWithMetadata) => { + return ( + + + + ); + }, + } as EuiTableFieldDataColumnType>, + { + field: 'meta.title', + name: i18n.translate('savedObjectsManagement.objectsTable.table.columnTitleName', { + defaultMessage: 'Title', + }), + description: i18n.translate( + 'savedObjectsManagement.objectsTable.table.columnTitleDescription', + { defaultMessage: 'Title of the saved object' } + ), + dataType: 'string', + sortable: false, + 'data-test-subj': 'savedObjectsTableRowTitle', + render: (title: string, object: SavedObjectWithMetadata) => { + const { path = '' } = object.meta.inAppUrl || {}; + const canGoInApp = this.props.canGoInApp(object); + if (!canGoInApp) { + return {title || getDefaultTitle(object)}; + } + return ( + {title || getDefaultTitle(object)} + ); + }, + } as EuiTableFieldDataColumnType>, + { + name: i18n.translate('savedObjectsManagement.objectsTable.table.columnActionsName', { + defaultMessage: 'Actions', + }), + actions: [ + { + name: i18n.translate( + 'savedObjectsManagement.objectsTable.table.columnActions.inspectActionName', + { defaultMessage: 'Inspect' } + ), + description: i18n.translate( + 'savedObjectsManagement.objectsTable.table.columnActions.inspectActionDescription', + { defaultMessage: 'Inspect this saved object' } + ), + type: 'icon', + icon: 'inspect', + onClick: object => goInspectObject(object), + available: object => !!object.meta.editUrl, + 'data-test-subj': 'savedObjectsTableAction-inspect', + }, + { + name: i18n.translate( + 'savedObjectsManagement.objectsTable.table.columnActions.viewRelationshipsActionName', + { defaultMessage: 'Relationships' } + ), + description: i18n.translate( + 'savedObjectsManagement.objectsTable.table.columnActions.viewRelationshipsActionDescription', + { + defaultMessage: + 'View the relationships this saved object has to other saved objects', + } + ), + type: 'icon', + icon: 'kqlSelector', + onClick: object => onShowRelationships(object), + 'data-test-subj': 'savedObjectsTableAction-relationships', + }, + ...actionRegistry.getAll().map(action => { + return { + ...action.euiAction, + 'data-test-subj': `savedObjectsTableAction-${action.id}`, + onClick: (object: SavedObjectWithMetadata) => { + this.setState({ + activeAction: action, + }); + + action.registerOnFinishCallback(() => { + this.setState({ + activeAction: undefined, + }); + }); + + if (action.euiAction.onClick) { + action.euiAction.onClick(object as any); + } + }, + }; + }), + ], + } as EuiTableActionsColumnType, + ]; + + let queryParseError; + if (!this.state.isSearchTextValid) { + const parseErrorMsg = i18n.translate( + 'savedObjectsManagement.objectsTable.searchBar.unableToParseQueryErrorMessage', + { defaultMessage: 'Unable to parse query' } + ); + queryParseError = ( + {`${parseErrorMsg}. ${this.state.parseErrorMessage}`} + ); + } + + const button = ( + + + + ); + + const activeActionContents = this.state.activeAction?.render() ?? null; + + return ( + + {activeActionContents} + + + , + + + } + > + + } + checked={this.state.isIncludeReferencesDeepChecked} + onChange={this.toggleIsIncludeReferencesDeepChecked} + /> + + + + + + + , + ]} + /> + {queryParseError} + +
+ ({ + 'data-test-subj': `savedObjectsTableRow row-${item.id}`, + })} + /> +
+
+ ); + } +} diff --git a/src/plugins/saved_objects_management/public/management_section/objects_table/index.ts b/src/plugins/saved_objects_management/public/management_section/objects_table/index.ts new file mode 100644 index 0000000000000..8777b15389690 --- /dev/null +++ b/src/plugins/saved_objects_management/public/management_section/objects_table/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 { SavedObjectsTable } from './saved_objects_table'; diff --git a/src/plugins/saved_objects_management/public/management_section/objects_table/saved_objects_table.test.mocks.ts b/src/plugins/saved_objects_management/public/management_section/objects_table/saved_objects_table.test.mocks.ts new file mode 100644 index 0000000000000..6b4659a6b5a13 --- /dev/null +++ b/src/plugins/saved_objects_management/public/management_section/objects_table/saved_objects_table.test.mocks.ts @@ -0,0 +1,67 @@ +/* + * 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 const saveAsMock = jest.fn(); +jest.doMock('@elastic/filesaver', () => ({ + saveAs: saveAsMock, +})); + +jest.doMock('lodash', () => ({ + ...jest.requireActual('lodash'), + debounce: (func: Function) => { + function debounced(this: any, ...args: any[]) { + return func.apply(this, args); + } + return debounced; + }, +})); + +export const findObjectsMock = jest.fn(); +jest.doMock('../../lib/find_objects', () => ({ + findObjects: findObjectsMock, +})); + +export const fetchExportObjectsMock = jest.fn(); +jest.doMock('../../lib/fetch_export_objects', () => ({ + fetchExportObjects: fetchExportObjectsMock, +})); + +export const fetchExportByTypeAndSearchMock = jest.fn(); +jest.doMock('../../lib/fetch_export_by_type_and_search', () => ({ + fetchExportByTypeAndSearch: fetchExportByTypeAndSearchMock, +})); + +export const extractExportDetailsMock = jest.fn(); +jest.doMock('../../lib/extract_export_details', () => ({ + extractExportDetails: extractExportDetailsMock, +})); + +jest.doMock('./components/header', () => ({ + Header: () => 'Header', +})); + +export const getSavedObjectCountsMock = jest.fn(); +jest.doMock('../../lib/get_saved_object_counts', () => ({ + getSavedObjectCounts: getSavedObjectCountsMock, +})); + +export const getRelationshipsMock = jest.fn(); +jest.doMock('../../lib/get_relationships', () => ({ + getRelationships: getRelationshipsMock, +})); diff --git a/src/plugins/saved_objects_management/public/management_section/objects_table/saved_objects_table.test.tsx b/src/plugins/saved_objects_management/public/management_section/objects_table/saved_objects_table.test.tsx new file mode 100644 index 0000000000000..342fdc4784b09 --- /dev/null +++ b/src/plugins/saved_objects_management/public/management_section/objects_table/saved_objects_table.test.tsx @@ -0,0 +1,551 @@ +/* + * 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 { + extractExportDetailsMock, + fetchExportByTypeAndSearchMock, + fetchExportObjectsMock, + findObjectsMock, + getRelationshipsMock, + getSavedObjectCountsMock, + saveAsMock, +} from './saved_objects_table.test.mocks'; + +import React from 'react'; +import { Query } from '@elastic/eui'; +import { ShallowWrapper } from 'enzyme'; +import { shallowWithI18nProvider } from 'test_utils/enzyme_helpers'; +import { + httpServiceMock, + overlayServiceMock, + notificationServiceMock, + savedObjectsServiceMock, + applicationServiceMock, +} from '../../../../../core/public/mocks'; +import { dataPluginMock } from '../../../../data/public/mocks'; +import { serviceRegistryMock } from '../../services/service_registry.mock'; +import { actionServiceMock } from '../../services/action_service.mock'; +import { + SavedObjectsTable, + SavedObjectsTableProps, + SavedObjectsTableState, +} from './saved_objects_table'; +import { Flyout, Relationships } from './components'; +import { SavedObjectWithMetadata } from '../../types'; + +const allowedTypes = ['index-pattern', 'visualization', 'dashboard', 'search']; + +const allSavedObjects = [ + { + id: '1', + type: 'index-pattern', + attributes: { + title: `MyIndexPattern*`, + }, + }, + { + id: '2', + type: 'search', + attributes: { + title: `MySearch`, + }, + }, + { + id: '3', + type: 'dashboard', + attributes: { + title: `MyDashboard`, + }, + }, + { + id: '4', + type: 'visualization', + attributes: { + title: `MyViz`, + }, + }, +]; + +describe('SavedObjectsTable', () => { + let defaultProps: SavedObjectsTableProps; + let http: ReturnType; + let overlays: ReturnType; + let notifications: ReturnType; + let savedObjects: ReturnType; + + const shallowRender = (overrides: Partial = {}) => { + return (shallowWithI18nProvider( + + ) as unknown) as ShallowWrapper< + SavedObjectsTableProps, + SavedObjectsTableState, + SavedObjectsTable + >; + }; + + beforeEach(() => { + extractExportDetailsMock.mockReset(); + + http = httpServiceMock.createStartContract(); + overlays = overlayServiceMock.createStartContract(); + notifications = notificationServiceMock.createStartContract(); + savedObjects = savedObjectsServiceMock.createStartContract(); + + const applications = applicationServiceMock.createStartContract(); + applications.capabilities = { + navLinks: {}, + management: {}, + catalogue: {}, + savedObjectsManagement: { + read: true, + edit: false, + delete: false, + }, + }; + + http.post.mockResolvedValue([]); + + getSavedObjectCountsMock.mockReturnValue({ + 'index-pattern': 0, + visualization: 0, + dashboard: 0, + search: 0, + }); + + defaultProps = { + allowedTypes, + serviceRegistry: serviceRegistryMock.create(), + actionRegistry: actionServiceMock.createStart(), + savedObjectsClient: savedObjects.client, + indexPatterns: dataPluginMock.createStartContract().indexPatterns, + http, + overlays, + notifications, + applications, + perPageConfig: 15, + goInspectObject: () => {}, + canGoInApp: () => true, + }; + + findObjectsMock.mockImplementation(() => ({ + total: 4, + savedObjects: [ + { + id: '1', + type: 'index-pattern', + meta: { + title: `MyIndexPattern*`, + icon: 'indexPatternApp', + editUrl: '#/management/kibana/index_patterns/1', + inAppUrl: { + path: '/management/kibana/index_patterns/1', + uiCapabilitiesPath: 'management.kibana.index_patterns', + }, + }, + }, + { + id: '2', + type: 'search', + meta: { + title: `MySearch`, + icon: 'search', + editUrl: '#/management/kibana/objects/savedSearches/2', + inAppUrl: { + path: '/discover/2', + uiCapabilitiesPath: 'discover.show', + }, + }, + }, + { + id: '3', + type: 'dashboard', + meta: { + title: `MyDashboard`, + icon: 'dashboardApp', + editUrl: '#/management/kibana/objects/savedDashboards/3', + inAppUrl: { + path: '/dashboard/3', + uiCapabilitiesPath: 'dashboard.show', + }, + }, + }, + { + id: '4', + type: 'visualization', + meta: { + title: `MyViz`, + icon: 'visualizeApp', + editUrl: '#/management/kibana/objects/savedVisualizations/4', + inAppUrl: { + path: '/visualize/edit/4', + uiCapabilitiesPath: 'visualize.show', + }, + }, + }, + ], + })); + }); + + it('should render normally', async () => { + const component = shallowRender({ perPageConfig: 15 }); + + // Ensure all promises resolve + await new Promise(resolve => process.nextTick(resolve)); + // Ensure the state changes are reflected + component.update(); + + expect(component).toMatchSnapshot(); + }); + + it('should add danger toast when find fails', async () => { + findObjectsMock.mockImplementation(() => { + throw new Error('Simulated find error'); + }); + const component = shallowRender({ perPageConfig: 15 }); + + // Ensure all promises resolve + await new Promise(resolve => process.nextTick(resolve)); + // Ensure the state changes are reflected + component.update(); + + expect(notifications.toasts.addDanger).toHaveBeenCalled(); + }); + + describe('export', () => { + it('should export selected objects', async () => { + const mockSelectedSavedObjects = [ + { id: '1', type: 'index-pattern' }, + { id: '3', type: 'dashboard' }, + ] as SavedObjectWithMetadata[]; + + const mockSavedObjects = mockSelectedSavedObjects.map(obj => ({ + _id: obj.id, + _source: {}, + })); + + const mockSavedObjectsClient = { + ...defaultProps.savedObjectsClient, + bulkGet: jest.fn().mockImplementation(() => ({ + savedObjects: mockSavedObjects, + })), + }; + + const component = shallowRender({ savedObjectsClient: mockSavedObjectsClient }); + + // Ensure all promises resolve + await new Promise(resolve => process.nextTick(resolve)); + // Ensure the state changes are reflected + component.update(); + + // Set some as selected + component.instance().onSelectionChanged(mockSelectedSavedObjects); + + await component.instance().onExport(true); + + expect(fetchExportObjectsMock).toHaveBeenCalledWith(http, mockSelectedSavedObjects, true); + expect(notifications.toasts.addSuccess).toHaveBeenCalledWith({ + title: 'Your file is downloading in the background', + }); + }); + + it('should display a warning is export contains missing references', async () => { + const mockSelectedSavedObjects = [ + { id: '1', type: 'index-pattern' }, + { id: '3', type: 'dashboard' }, + ] as SavedObjectWithMetadata[]; + + const mockSavedObjects = mockSelectedSavedObjects.map(obj => ({ + _id: obj.id, + _source: {}, + })); + + const mockSavedObjectsClient = { + ...defaultProps.savedObjectsClient, + bulkGet: jest.fn().mockImplementation(() => ({ + savedObjects: mockSavedObjects, + })), + }; + + extractExportDetailsMock.mockImplementation(() => ({ + exportedCount: 2, + missingRefCount: 1, + missingReferences: [{ id: '7', type: 'visualisation' }], + })); + + const component = shallowRender({ savedObjectsClient: mockSavedObjectsClient }); + + // Ensure all promises resolve + await new Promise(resolve => process.nextTick(resolve)); + // Ensure the state changes are reflected + component.update(); + + // Set some as selected + component.instance().onSelectionChanged(mockSelectedSavedObjects); + + await component.instance().onExport(true); + + expect(fetchExportObjectsMock).toHaveBeenCalledWith(http, mockSelectedSavedObjects, true); + expect(notifications.toasts.addWarning).toHaveBeenCalledWith({ + title: + 'Your file is downloading in the background. ' + + 'Some related objects could not be found. ' + + 'Please see the last line in the exported file for a list of missing objects.', + }); + }); + + it('should allow the user to choose when exporting all', async () => { + const component = shallowRender(); + + // Ensure all promises resolve + await new Promise(resolve => process.nextTick(resolve)); + // Ensure the state changes are reflected + component.update(); + + (component.find('Header') as any).prop('onExportAll')(); + component.update(); + + expect(component.find('EuiModal')).toMatchSnapshot(); + }); + + it('should export all', async () => { + const component = shallowRender(); + + // Ensure all promises resolve + await new Promise(resolve => process.nextTick(resolve)); + // Ensure the state changes are reflected + component.update(); + + // Set up mocks + const blob = new Blob([JSON.stringify(allSavedObjects)], { type: 'application/ndjson' }); + fetchExportByTypeAndSearchMock.mockImplementation(() => blob); + + await component.instance().onExportAll(); + + expect(fetchExportByTypeAndSearchMock).toHaveBeenCalledWith( + http, + allowedTypes, + undefined, + true + ); + expect(saveAsMock).toHaveBeenCalledWith(blob, 'export.ndjson'); + expect(notifications.toasts.addSuccess).toHaveBeenCalledWith({ + title: 'Your file is downloading in the background', + }); + }); + + it('should export all, accounting for the current search criteria', async () => { + const component = shallowRender(); + + component.instance().onQueryChange({ + query: Query.parse('test'), + }); + + // Ensure all promises resolve + await new Promise(resolve => process.nextTick(resolve)); + // Ensure the state changes are reflected + component.update(); + + // Set up mocks + const blob = new Blob([JSON.stringify(allSavedObjects)], { type: 'application/ndjson' }); + fetchExportByTypeAndSearchMock.mockImplementation(() => blob); + + await component.instance().onExportAll(); + + expect(fetchExportByTypeAndSearchMock).toHaveBeenCalledWith( + http, + allowedTypes, + 'test*', + true + ); + expect(saveAsMock).toHaveBeenCalledWith(blob, 'export.ndjson'); + expect(notifications.toasts.addSuccess).toHaveBeenCalledWith({ + title: 'Your file is downloading in the background', + }); + }); + }); + + describe('import', () => { + it('should show the flyout', async () => { + const component = shallowRender(); + + // Ensure all promises resolve + await new Promise(resolve => process.nextTick(resolve)); + // Ensure the state changes are reflected + component.update(); + + component.instance().showImportFlyout(); + component.update(); + + expect(component.find(Flyout)).toMatchSnapshot(); + }); + + it('should hide the flyout', async () => { + const component = shallowRender(); + + // Ensure all promises resolve + await new Promise(resolve => process.nextTick(resolve)); + // Ensure the state changes are reflected + component.update(); + + component.instance().hideImportFlyout(); + component.update(); + + expect(component.find(Flyout).length).toBe(0); + }); + }); + + describe('relationships', () => { + it('should fetch relationships', async () => { + const component = shallowRender(); + + // Ensure all promises resolve + await new Promise(resolve => process.nextTick(resolve)); + // Ensure the state changes are reflected + component.update(); + + await component.instance().getRelationships('search', '1'); + const savedObjectTypes = ['index-pattern', 'visualization', 'dashboard', 'search']; + expect(getRelationshipsMock).toHaveBeenCalledWith(http, 'search', '1', savedObjectTypes); + }); + + it('should show the flyout', async () => { + const component = shallowRender(); + + // Ensure all promises resolve + await new Promise(resolve => process.nextTick(resolve)); + // Ensure the state changes are reflected + component.update(); + + component.instance().onShowRelationships({ + id: '2', + type: 'search', + meta: { + title: `MySearch`, + icon: 'search', + editUrl: '#/management/kibana/objects/savedSearches/2', + inAppUrl: { + path: '/discover/2', + uiCapabilitiesPath: 'discover.show', + }, + }, + } as SavedObjectWithMetadata); + component.update(); + + expect(component.find(Relationships)).toMatchSnapshot(); + expect(component.state('relationshipObject')).toEqual({ + id: '2', + type: 'search', + meta: { + title: 'MySearch', + editUrl: '#/management/kibana/objects/savedSearches/2', + icon: 'search', + inAppUrl: { + path: '/discover/2', + uiCapabilitiesPath: 'discover.show', + }, + }, + }); + }); + + it('should hide the flyout', async () => { + const component = shallowRender(); + + // Ensure all promises resolve + await new Promise(resolve => process.nextTick(resolve)); + // Ensure the state changes are reflected + component.update(); + + component.instance().onHideRelationships(); + component.update(); + + expect(component.find(Relationships).length).toBe(0); + expect(component.state('relationshipId')).toBe(undefined); + expect(component.state('relationshipType')).toBe(undefined); + expect(component.state('relationshipTitle')).toBe(undefined); + }); + }); + + describe('delete', () => { + it('should show a confirm modal', async () => { + const component = shallowRender(); + + const mockSelectedSavedObjects = [ + { id: '1', type: 'index-pattern' }, + { id: '3', type: 'dashboard' }, + ] as SavedObjectWithMetadata[]; + + // Ensure all promises resolve + await new Promise(resolve => process.nextTick(resolve)); + // Ensure the state changes are reflected + component.update(); + + // Set some as selected + component.instance().onSelectionChanged(mockSelectedSavedObjects); + await component.instance().onDelete(); + component.update(); + + expect(component.find('EuiConfirmModal')).toMatchSnapshot(); + }); + + it('should delete selected objects', async () => { + const mockSelectedSavedObjects = [ + { id: '1', type: 'index-pattern' }, + { id: '3', type: 'dashboard' }, + ] as SavedObjectWithMetadata[]; + + const mockSavedObjects = mockSelectedSavedObjects.map(obj => ({ + id: obj.id, + type: obj.type, + source: {}, + })); + + const mockSavedObjectsClient = { + ...defaultProps.savedObjectsClient, + bulkGet: jest.fn().mockImplementation(() => ({ + savedObjects: mockSavedObjects, + })), + delete: jest.fn(), + }; + + const component = shallowRender({ savedObjectsClient: mockSavedObjectsClient }); + + // Ensure all promises resolve + await new Promise(resolve => process.nextTick(resolve)); + // Ensure the state changes are reflected + component.update(); + + // Set some as selected + component.instance().onSelectionChanged(mockSelectedSavedObjects); + + await component.instance().delete(); + + expect(defaultProps.indexPatterns.clearCache).toHaveBeenCalled(); + expect(mockSavedObjectsClient.bulkGet).toHaveBeenCalledWith(mockSelectedSavedObjects); + expect(mockSavedObjectsClient.delete).toHaveBeenCalledWith( + mockSavedObjects[0].type, + mockSavedObjects[0].id + ); + expect(mockSavedObjectsClient.delete).toHaveBeenCalledWith( + mockSavedObjects[1].type, + mockSavedObjects[1].id + ); + expect(component.state('selectedSavedObjects').length).toBe(0); + }); + }); +}); diff --git a/src/plugins/saved_objects_management/public/management_section/objects_table/saved_objects_table.tsx b/src/plugins/saved_objects_management/public/management_section/objects_table/saved_objects_table.tsx new file mode 100644 index 0000000000000..c76fea5a0fb29 --- /dev/null +++ b/src/plugins/saved_objects_management/public/management_section/objects_table/saved_objects_table.tsx @@ -0,0 +1,756 @@ +/* + * 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, { Component } from 'react'; +import { debounce } from 'lodash'; +// @ts-ignore +import { saveAs } from '@elastic/filesaver'; +import { + EuiSpacer, + Query, + EuiInMemoryTable, + EuiIcon, + EuiConfirmModal, + EuiLoadingKibana, + EuiOverlayMask, + EUI_MODAL_CONFIRM_BUTTON, + EuiCheckboxGroup, + EuiToolTip, + EuiPageContent, + EuiSwitch, + EuiModal, + EuiModalHeader, + EuiModalBody, + EuiModalFooter, + EuiButtonEmpty, + EuiButton, + EuiModalHeaderTitle, + EuiFormRow, + EuiFlexGroup, + EuiFlexItem, +} from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { + SavedObjectsClientContract, + SavedObjectsFindOptions, + HttpStart, + OverlayStart, + NotificationsStart, + ApplicationStart, +} from 'src/core/public'; +import { IndexPatternsContract } from '../../../../data/public'; +import { + parseQuery, + getSavedObjectCounts, + getRelationships, + getSavedObjectLabel, + fetchExportObjects, + fetchExportByTypeAndSearch, + findObjects, + extractExportDetails, + SavedObjectsExportResultDetails, +} from '../../lib'; +import { SavedObjectWithMetadata } from '../../types'; +import { + ISavedObjectsManagementServiceRegistry, + SavedObjectsManagementActionServiceStart, +} from '../../services'; +import { Header, Table, Flyout, Relationships } from './components'; + +interface ExportAllOption { + id: string; + label: string; +} + +export interface SavedObjectsTableProps { + allowedTypes: string[]; + serviceRegistry: ISavedObjectsManagementServiceRegistry; + actionRegistry: SavedObjectsManagementActionServiceStart; + savedObjectsClient: SavedObjectsClientContract; + indexPatterns: IndexPatternsContract; + http: HttpStart; + overlays: OverlayStart; + notifications: NotificationsStart; + applications: ApplicationStart; + perPageConfig: number; + goInspectObject: (obj: SavedObjectWithMetadata) => void; + canGoInApp: (obj: SavedObjectWithMetadata) => boolean; +} + +export interface SavedObjectsTableState { + totalCount: number; + page: number; + perPage: number; + savedObjects: SavedObjectWithMetadata[]; + savedObjectCounts: Record; + activeQuery: Query; + selectedSavedObjects: SavedObjectWithMetadata[]; + isShowingImportFlyout: boolean; + isSearching: boolean; + filteredItemCount: number; + isShowingRelationships: boolean; + relationshipObject?: SavedObjectWithMetadata; + isShowingDeleteConfirmModal: boolean; + isShowingExportAllOptionsModal: boolean; + isDeleting: boolean; + exportAllOptions: ExportAllOption[]; + exportAllSelectedOptions: Record; + isIncludeReferencesDeepChecked: boolean; +} + +export class SavedObjectsTable extends Component { + private _isMounted = false; + + constructor(props: SavedObjectsTableProps) { + super(props); + + this.state = { + totalCount: 0, + page: 0, + perPage: props.perPageConfig || 50, + savedObjects: [], + savedObjectCounts: props.allowedTypes.reduce((typeToCountMap, type) => { + typeToCountMap[type] = 0; + return typeToCountMap; + }, {} as Record), + activeQuery: Query.parse(''), + selectedSavedObjects: [], + isShowingImportFlyout: false, + isSearching: false, + filteredItemCount: 0, + isShowingRelationships: false, + relationshipObject: undefined, + isShowingDeleteConfirmModal: false, + isShowingExportAllOptionsModal: false, + isDeleting: false, + exportAllOptions: [], + exportAllSelectedOptions: {}, + isIncludeReferencesDeepChecked: true, + }; + } + + componentDidMount() { + this._isMounted = true; + this.fetchSavedObjects(); + this.fetchCounts(); + } + + componentWillUnmount() { + this._isMounted = false; + this.debouncedFetch.cancel(); + } + + fetchCounts = async () => { + const { allowedTypes } = this.props; + const { queryText, visibleTypes } = parseQuery(this.state.activeQuery); + + const filteredTypes = allowedTypes.filter(type => !visibleTypes || visibleTypes.includes(type)); + + // These are the saved objects visible in the table. + const filteredSavedObjectCounts = await getSavedObjectCounts( + this.props.http, + filteredTypes, + queryText + ); + + const exportAllOptions: ExportAllOption[] = []; + const exportAllSelectedOptions: Record = {}; + + Object.keys(filteredSavedObjectCounts).forEach(id => { + // Add this type as a bulk-export option. + exportAllOptions.push({ + id, + label: `${id} (${filteredSavedObjectCounts[id] || 0})`, + }); + + // Select it by default. + exportAllSelectedOptions[id] = true; + }); + + // Fetch all the saved objects that exist so we can accurately populate the counts within + // the table filter dropdown. + const savedObjectCounts = await getSavedObjectCounts(this.props.http, allowedTypes, queryText); + + this.setState(state => ({ + ...state, + savedObjectCounts, + exportAllOptions, + exportAllSelectedOptions, + })); + }; + + fetchSavedObjects = () => { + this.setState( + { + isSearching: true, + }, + this.debouncedFetch + ); + }; + + debouncedFetch = debounce(async () => { + const { activeQuery: query, page, perPage } = this.state; + const { notifications, http, allowedTypes } = this.props; + const { queryText, visibleTypes } = parseQuery(query); + // "searchFields" is missing from the "findOptions" but gets injected via the API. + // The API extracts the fields from each uiExports.savedObjectsManagement "defaultSearchField" attribute + const findOptions: SavedObjectsFindOptions = { + search: queryText ? `${queryText}*` : undefined, + perPage, + page: page + 1, + fields: ['id'], + type: allowedTypes.filter(type => !visibleTypes || visibleTypes.includes(type)), + }; + if (findOptions.type.length > 1) { + findOptions.sortField = 'type'; + } + + try { + const resp = await findObjects(http, findOptions); + if (!this._isMounted) { + return; + } + + this.setState(({ activeQuery }) => { + // ignore results for old requests + if (activeQuery.text !== query.text) { + return null; + } + + return { + savedObjects: resp.savedObjects, + filteredItemCount: resp.total, + isSearching: false, + }; + }); + } catch (error) { + if (this._isMounted) { + this.setState({ + isSearching: false, + }); + } + notifications.toasts.addDanger({ + title: i18n.translate( + 'savedObjectsManagement.objectsTable.unableFindSavedObjectsNotificationMessage', + { defaultMessage: 'Unable find saved objects' } + ), + text: `${error}`, + }); + } + }, 300); + + refreshData = async () => { + await Promise.all([this.fetchSavedObjects(), this.fetchCounts()]); + }; + + onSelectionChanged = (selection: SavedObjectWithMetadata[]) => { + this.setState({ selectedSavedObjects: selection }); + }; + + onQueryChange = ({ query }: { query: Query }) => { + // TODO: Use isSameQuery to compare new query with state.activeQuery to avoid re-fetching the + // same data we already have. + this.setState( + { + activeQuery: query, + page: 0, // Reset this on each query change + selectedSavedObjects: [], + }, + () => { + this.fetchSavedObjects(); + this.fetchCounts(); + } + ); + }; + + onTableChange = async (table: any) => { + const { index: page, size: perPage } = table.page || {}; + + this.setState( + { + page, + perPage, + selectedSavedObjects: [], + }, + this.fetchSavedObjects + ); + }; + + onShowRelationships = (object: SavedObjectWithMetadata) => { + this.setState({ + isShowingRelationships: true, + relationshipObject: object, + }); + }; + + onHideRelationships = () => { + this.setState({ + isShowingRelationships: false, + relationshipObject: undefined, + }); + }; + + onExport = async (includeReferencesDeep: boolean) => { + const { selectedSavedObjects } = this.state; + const { notifications, http } = this.props; + const objectsToExport = selectedSavedObjects.map(obj => ({ id: obj.id, type: obj.type })); + + let blob; + try { + blob = await fetchExportObjects(http, objectsToExport, includeReferencesDeep); + } catch (e) { + notifications.toasts.addDanger({ + title: i18n.translate('savedObjectsManagement.objectsTable.export.dangerNotification', { + defaultMessage: 'Unable to generate export', + }), + }); + throw e; + } + + saveAs(blob, 'export.ndjson'); + + const exportDetails = await extractExportDetails(blob); + this.showExportSuccessMessage(exportDetails); + }; + + onExportAll = async () => { + const { exportAllSelectedOptions, isIncludeReferencesDeepChecked, activeQuery } = this.state; + const { notifications, http } = this.props; + const { queryText } = parseQuery(activeQuery); + const exportTypes = Object.entries(exportAllSelectedOptions).reduce((accum, [id, selected]) => { + if (selected) { + accum.push(id); + } + return accum; + }, [] as string[]); + + let blob; + try { + blob = await fetchExportByTypeAndSearch( + http, + exportTypes, + queryText ? `${queryText}*` : undefined, + isIncludeReferencesDeepChecked + ); + } catch (e) { + notifications.toasts.addDanger({ + title: i18n.translate('savedObjectsManagement.objectsTable.export.dangerNotification', { + defaultMessage: 'Unable to generate export', + }), + }); + throw e; + } + + saveAs(blob, 'export.ndjson'); + + const exportDetails = await extractExportDetails(blob); + this.showExportSuccessMessage(exportDetails); + this.setState({ isShowingExportAllOptionsModal: false }); + }; + + showExportSuccessMessage = (exportDetails: SavedObjectsExportResultDetails | undefined) => { + const { notifications } = this.props; + if (exportDetails && exportDetails.missingReferences.length > 0) { + notifications.toasts.addWarning({ + title: i18n.translate( + 'savedObjectsManagement.objectsTable.export.successWithMissingRefsNotification', + { + defaultMessage: + 'Your file is downloading in the background. ' + + 'Some related objects could not be found. ' + + 'Please see the last line in the exported file for a list of missing objects.', + } + ), + }); + } else { + notifications.toasts.addSuccess({ + title: i18n.translate('savedObjectsManagement.objectsTable.export.successNotification', { + defaultMessage: 'Your file is downloading in the background', + }), + }); + } + }; + + finishImport = () => { + this.hideImportFlyout(); + this.fetchSavedObjects(); + this.fetchCounts(); + }; + + showImportFlyout = () => { + this.setState({ isShowingImportFlyout: true }); + }; + + hideImportFlyout = () => { + this.setState({ isShowingImportFlyout: false }); + }; + + onDelete = () => { + this.setState({ isShowingDeleteConfirmModal: true }); + }; + + delete = async () => { + const { savedObjectsClient } = this.props; + const { selectedSavedObjects, isDeleting } = this.state; + + if (isDeleting) { + return; + } + + this.setState({ isDeleting: true }); + + const indexPatterns = selectedSavedObjects.filter(object => object.type === 'index-pattern'); + if (indexPatterns.length) { + await this.props.indexPatterns.clearCache(); + } + + const objects = await savedObjectsClient.bulkGet(selectedSavedObjects); + const deletes = objects.savedObjects.map(object => + savedObjectsClient.delete(object.type, object.id) + ); + await Promise.all(deletes); + + // Unset this + this.setState({ + selectedSavedObjects: [], + }); + + // Fetching all data + await this.fetchSavedObjects(); + await this.fetchCounts(); + + // Allow the user to interact with the table once the saved objects have been re-fetched. + this.setState({ + isShowingDeleteConfirmModal: false, + isDeleting: false, + }); + }; + + getRelationships = async (type: string, id: string) => { + const { allowedTypes, http } = this.props; + return await getRelationships(http, type, id, allowedTypes); + }; + + renderFlyout() { + if (!this.state.isShowingImportFlyout) { + return null; + } + const { applications } = this.props; + const newIndexPatternUrl = applications.getUrlForApp('kibana', { + path: '#/management/kibana/index_pattern', + }); + + return ( + + ); + } + + renderRelationships() { + if (!this.state.isShowingRelationships) { + return null; + } + + return ( + + ); + } + + renderDeleteConfirmModal() { + const { isShowingDeleteConfirmModal, isDeleting, selectedSavedObjects } = this.state; + + if (!isShowingDeleteConfirmModal) { + return null; + } + + let modal; + + if (isDeleting) { + // Block the user from interacting with the table while its contents are being deleted. + modal = ; + } else { + const onCancel = () => { + this.setState({ isShowingDeleteConfirmModal: false }); + }; + + const onConfirm = () => { + this.delete(); + }; + + modal = ( + + } + onCancel={onCancel} + onConfirm={onConfirm} + buttonColor="danger" + cancelButtonText={ + + } + confirmButtonText={ + isDeleting ? ( + + ) : ( + + ) + } + defaultFocusedButton={EUI_MODAL_CONFIRM_BUTTON} + > +

+ +

+ ( + + + + ), + }, + { + field: 'id', + name: i18n.translate( + 'savedObjectsManagement.objectsTable.deleteSavedObjectsConfirmModal.idColumnName', + { defaultMessage: 'Id' } + ), + }, + { + field: 'meta.title', + name: i18n.translate( + 'savedObjectsManagement.objectsTable.deleteSavedObjectsConfirmModal.titleColumnName', + { defaultMessage: 'Title' } + ), + }, + ]} + pagination={true} + sorting={false} + /> +
+ ); + } + + return {modal}; + } + + changeIncludeReferencesDeep = () => { + this.setState(state => ({ + isIncludeReferencesDeepChecked: !state.isIncludeReferencesDeepChecked, + })); + }; + + closeExportAllModal = () => { + this.setState({ isShowingExportAllOptionsModal: false }); + }; + + renderExportAllOptionsModal() { + const { + isShowingExportAllOptionsModal, + filteredItemCount, + exportAllOptions, + exportAllSelectedOptions, + isIncludeReferencesDeepChecked, + } = this.state; + + if (!isShowingExportAllOptionsModal) { + return null; + } + + return ( + + + + + + + + + + } + labelType="legend" + > + { + const newExportAllSelectedOptions = { + ...exportAllSelectedOptions, + ...{ + [optionId]: !exportAllSelectedOptions[optionId], + }, + }; + + this.setState({ + exportAllSelectedOptions: newExportAllSelectedOptions, + }); + }} + /> + + + + } + checked={isIncludeReferencesDeepChecked} + onChange={this.changeIncludeReferencesDeep} + /> + + + + + + + + + + + + + + + + + + + + + + ); + } + + render() { + const { + selectedSavedObjects, + page, + perPage, + savedObjects, + filteredItemCount, + isSearching, + savedObjectCounts, + } = this.state; + const { http, allowedTypes, applications } = this.props; + + const selectionConfig = { + onSelectionChange: this.onSelectionChanged, + }; + + const filterOptions = allowedTypes.map(type => ({ + value: type, + name: type, + view: `${type} (${savedObjectCounts[type] || 0})`, + })); + + return ( + + {this.renderFlyout()} + {this.renderRelationships()} + {this.renderDeleteConfirmModal()} + {this.renderExportAllOptionsModal()} +
this.setState({ isShowingExportAllOptionsModal: true })} + onImport={this.showImportFlyout} + onRefresh={this.refreshData} + filteredCount={filteredItemCount} + /> + +
+ + ); + } +} diff --git a/src/plugins/saved_objects_management/public/management_section/types.ts b/src/plugins/saved_objects_management/public/management_section/types.ts new file mode 100644 index 0000000000000..541746534b84a --- /dev/null +++ b/src/plugins/saved_objects_management/public/management_section/types.ts @@ -0,0 +1,38 @@ +/* + * 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 { SavedObjectReference } from '../../../../core/types'; + +export interface ObjectField { + type: FieldType; + name: string; + value: any; +} + +export type FieldType = 'text' | 'number' | 'boolean' | 'array' | 'json'; + +export interface FieldState { + value?: any; + invalid?: boolean; +} + +export interface SubmittedFormData { + attributes: any; + references: SavedObjectReference[]; +} diff --git a/src/plugins/saved_objects_management/public/mocks.ts b/src/plugins/saved_objects_management/public/mocks.ts index 8cf23afe87907..1de3de8e85302 100644 --- a/src/plugins/saved_objects_management/public/mocks.ts +++ b/src/plugins/saved_objects_management/public/mocks.ts @@ -17,23 +17,27 @@ * under the License. */ -import { actionRegistryMock } from './services/action_registry.mock'; +import { actionServiceMock } from './services/action_service.mock'; +import { serviceRegistryMock } from './services/service_registry.mock'; import { SavedObjectsManagementPluginSetup, SavedObjectsManagementPluginStart } from './plugin'; const createSetupContractMock = (): jest.Mocked => { const mock = { - actionRegistry: actionRegistryMock.create(), + actions: actionServiceMock.createSetup(), + serviceRegistry: serviceRegistryMock.create(), }; return mock; }; const createStartContractMock = (): jest.Mocked => { - const mock = {}; + const mock = { + actions: actionServiceMock.createStart(), + }; return mock; }; export const savedObjectsManagementPluginMock = { - createActionRegistry: actionRegistryMock.create, + createServiceRegistry: serviceRegistryMock.create, createSetupContract: createSetupContractMock, createStartContract: createStartContractMock, }; diff --git a/src/plugins/saved_objects_management/public/plugin.test.ts b/src/plugins/saved_objects_management/public/plugin.test.ts index 1cafbb235ad5b..09080f46a6869 100644 --- a/src/plugins/saved_objects_management/public/plugin.test.ts +++ b/src/plugins/saved_objects_management/public/plugin.test.ts @@ -20,6 +20,9 @@ import { coreMock } from '../../../core/public/mocks'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths import { homePluginMock } from '../../home/public/mocks'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { managementPluginMock } from '../../management/public/mocks'; +import { dataPluginMock } from '../../data/public/mocks'; import { SavedObjectsManagementPlugin } from './plugin'; describe('SavedObjectsManagementPlugin', () => { @@ -31,10 +34,13 @@ describe('SavedObjectsManagementPlugin', () => { describe('#setup', () => { it('registers the saved_objects feature to the home plugin', async () => { - const coreSetup = coreMock.createSetup(); + const coreSetup = coreMock.createSetup({ + pluginStartDeps: { data: dataPluginMock.createStartContract() }, + }); const homeSetup = homePluginMock.createSetupContract(); + const managementSetup = managementPluginMock.createSetupContract(); - await plugin.setup(coreSetup, { home: homeSetup }); + await plugin.setup(coreSetup, { home: homeSetup, management: managementSetup }); expect(homeSetup.featureCatalogue.register).toHaveBeenCalledTimes(1); expect(homeSetup.featureCatalogue.register).toHaveBeenCalledWith( diff --git a/src/plugins/saved_objects_management/public/plugin.ts b/src/plugins/saved_objects_management/public/plugin.ts index 3f2e9c166058e..c8dede3da9263 100644 --- a/src/plugins/saved_objects_management/public/plugin.ts +++ b/src/plugins/saved_objects_management/public/plugin.ts @@ -19,37 +19,59 @@ import { i18n } from '@kbn/i18n'; import { CoreSetup, CoreStart, Plugin } from 'src/core/public'; +import { ManagementSetup } from '../../management/public'; +import { DataPublicPluginStart } from '../../data/public'; +import { DashboardStart } from '../../dashboard/public'; +import { DiscoverStart } from '../../discover/public'; import { HomePublicPluginSetup, FeatureCatalogueCategory } from '../../home/public'; +import { VisualizationsStart } from '../../visualizations/public'; import { - SavedObjectsManagementActionRegistry, - ISavedObjectsManagementActionRegistry, + SavedObjectsManagementActionService, + SavedObjectsManagementActionServiceSetup, + SavedObjectsManagementActionServiceStart, + SavedObjectsManagementServiceRegistry, + ISavedObjectsManagementServiceRegistry, } from './services'; +import { registerServices } from './register_services'; export interface SavedObjectsManagementPluginSetup { - actionRegistry: ISavedObjectsManagementActionRegistry; + actions: SavedObjectsManagementActionServiceSetup; + serviceRegistry: ISavedObjectsManagementServiceRegistry; } -// eslint-disable-next-line @typescript-eslint/no-empty-interface -export interface SavedObjectsManagementPluginStart {} +export interface SavedObjectsManagementPluginStart { + actions: SavedObjectsManagementActionServiceStart; +} export interface SetupDependencies { + management: ManagementSetup; home: HomePublicPluginSetup; } +export interface StartDependencies { + data: DataPublicPluginStart; + dashboard?: DashboardStart; + visualizations?: VisualizationsStart; + discover?: DiscoverStart; +} + export class SavedObjectsManagementPlugin implements Plugin< SavedObjectsManagementPluginSetup, SavedObjectsManagementPluginStart, SetupDependencies, - {} + StartDependencies > { - private actionRegistry = new SavedObjectsManagementActionRegistry(); + private actionService = new SavedObjectsManagementActionService(); + private serviceRegistry = new SavedObjectsManagementServiceRegistry(); public setup( - core: CoreSetup<{}>, - { home }: SetupDependencies + core: CoreSetup, + { home, management }: SetupDependencies ): SavedObjectsManagementPluginSetup { + const actionSetup = this.actionService.setup(); + home.featureCatalogue.register({ id: 'saved_objects', title: i18n.translate('savedObjectsManagement.objects.savedObjectsTitle', { @@ -65,12 +87,39 @@ export class SavedObjectsManagementPlugin category: FeatureCatalogueCategory.ADMIN, }); + const kibanaSection = management.sections.getSection('kibana'); + if (!kibanaSection) { + throw new Error('`kibana` management section not found.'); + } + kibanaSection.registerApp({ + id: 'objects', + title: i18n.translate('savedObjectsManagement.managementSectionLabel', { + defaultMessage: 'Saved Objects', + }), + order: 10, + mount: async mountParams => { + const { mountManagementSection } = await import('./management_section'); + return mountManagementSection({ + core, + serviceRegistry: this.serviceRegistry, + mountParams, + }); + }, + }); + + // depends on `getStartServices`, should not be awaited + registerServices(this.serviceRegistry, core.getStartServices); + return { - actionRegistry: this.actionRegistry, + actions: actionSetup, + serviceRegistry: this.serviceRegistry, }; } public start(core: CoreStart) { - return {}; + const actionStart = this.actionService.start(); + return { + actions: actionStart, + }; } } diff --git a/src/plugins/saved_objects_management/public/register_services.ts b/src/plugins/saved_objects_management/public/register_services.ts new file mode 100644 index 0000000000000..a34b632b78f6c --- /dev/null +++ b/src/plugins/saved_objects_management/public/register_services.ts @@ -0,0 +1,59 @@ +/* + * 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 { StartServicesAccessor } from '../../../core/public'; +import { SavedObjectsManagementPluginStart, StartDependencies } from './plugin'; +import { ISavedObjectsManagementServiceRegistry } from './services'; + +export const registerServices = async ( + registry: ISavedObjectsManagementServiceRegistry, + getStartServices: StartServicesAccessor +) => { + const [coreStart, { dashboard, data, visualizations, discover }] = await getStartServices(); + + if (dashboard) { + registry.register({ + id: 'savedDashboards', + title: 'dashboards', + service: dashboard.getSavedDashboardLoader(), + }); + } + + if (visualizations) { + registry.register({ + id: 'savedVisualizations', + title: 'visualizations', + service: visualizations.savedVisualizationsLoader, + }); + } + + if (discover) { + registry.register({ + id: 'savedSearches', + title: 'searches', + service: discover.savedSearches.createLoader({ + savedObjectsClient: coreStart.savedObjects.client, + indexPatterns: data.indexPatterns, + search: data.search, + chrome: coreStart.chrome, + overlays: coreStart.overlays, + }), + }); + } +}; diff --git a/src/plugins/saved_objects_management/public/services/action_registry.mock.ts b/src/plugins/saved_objects_management/public/services/action_registry.mock.ts deleted file mode 100644 index a9093ad42d0ac..0000000000000 --- a/src/plugins/saved_objects_management/public/services/action_registry.mock.ts +++ /dev/null @@ -1,37 +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 { ISavedObjectsManagementActionRegistry } from './action_registry'; - -const createRegistryMock = (): jest.Mocked => { - const mock = { - register: jest.fn(), - has: jest.fn(), - getAll: jest.fn(), - }; - - mock.has.mockReturnValue(true); - mock.getAll.mockReturnValue([]); - - return mock; -}; - -export const actionRegistryMock = { - create: createRegistryMock, -}; diff --git a/src/plugins/saved_objects_management/public/services/action_registry.test.ts b/src/plugins/saved_objects_management/public/services/action_registry.test.ts deleted file mode 100644 index eb3bda00f4196..0000000000000 --- a/src/plugins/saved_objects_management/public/services/action_registry.test.ts +++ /dev/null @@ -1,76 +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 { SavedObjectsManagementActionRegistry } from './action_registry'; -import { SavedObjectsManagementAction } from './action_types'; - -class DummyAction extends SavedObjectsManagementAction { - constructor(public id: string) { - super(); - } - - public euiAction = { - name: 'name', - description: 'description', - icon: 'icon', - type: 'type', - }; - - public render = () => ''; -} - -describe('SavedObjectsManagementActionRegistry', () => { - let registry: SavedObjectsManagementActionRegistry; - - const createAction = (id: string): SavedObjectsManagementAction => { - return new DummyAction(id); - }; - - beforeEach(() => { - registry = new SavedObjectsManagementActionRegistry(); - }); - - describe('#register', () => { - it('allows actions to be registered and retrieved', () => { - const action = createAction('foo'); - registry.register(action); - expect(registry.getAll()).toContain(action); - }); - - it('does not allow actions with duplicate ids to be registered', () => { - const action = createAction('my-action'); - registry.register(action); - expect(() => registry.register(action)).toThrowErrorMatchingInlineSnapshot( - `"Saved Objects Management Action with id 'my-action' already exists"` - ); - }); - }); - - describe('#has', () => { - it('returns true when an action with a matching ID exists', () => { - const action = createAction('existing-action'); - registry.register(action); - expect(registry.has('existing-action')).toEqual(true); - }); - - it(`returns false when an action doesn't exist`, () => { - expect(registry.has('missing-action')).toEqual(false); - }); - }); -}); diff --git a/src/plugins/saved_objects_management/public/services/action_registry.ts b/src/plugins/saved_objects_management/public/services/action_registry.ts deleted file mode 100644 index 8bf77231dd73f..0000000000000 --- a/src/plugins/saved_objects_management/public/services/action_registry.ts +++ /dev/null @@ -1,52 +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 { SavedObjectsManagementAction } from './action_types'; - -export type ISavedObjectsManagementActionRegistry = PublicMethodsOf< - SavedObjectsManagementActionRegistry ->; - -export class SavedObjectsManagementActionRegistry { - private readonly actions = new Map(); - - /** - * register given action in the registry. - */ - register(action: SavedObjectsManagementAction) { - if (this.actions.has(action.id)) { - throw new Error(`Saved Objects Management Action with id '${action.id}' already exists`); - } - this.actions.set(action.id, action); - } - - /** - * return true if the registry contains given action, false otherwise. - */ - has(actionId: string) { - return this.actions.has(actionId); - } - - /** - * return all {@link SavedObjectsManagementAction | actions} currently registered. - */ - getAll() { - return [...this.actions.values()]; - } -} diff --git a/src/plugins/saved_objects_management/public/services/action_service.mock.ts b/src/plugins/saved_objects_management/public/services/action_service.mock.ts new file mode 100644 index 0000000000000..97c95a589b925 --- /dev/null +++ b/src/plugins/saved_objects_management/public/services/action_service.mock.ts @@ -0,0 +1,57 @@ +/* + * 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 { + SavedObjectsManagementActionService, + SavedObjectsManagementActionServiceSetup, + SavedObjectsManagementActionServiceStart, +} from './action_service'; + +const createSetupMock = (): jest.Mocked => { + const mock = { + register: jest.fn(), + }; + return mock; +}; + +const createStartMock = (): jest.Mocked => { + const mock = { + has: jest.fn(), + getAll: jest.fn(), + }; + + mock.has.mockReturnValue(true); + mock.getAll.mockReturnValue([]); + + return mock; +}; + +const createServiceMock = (): jest.Mocked> => { + const mock = { + setup: jest.fn().mockReturnValue(createSetupMock()), + start: jest.fn().mockReturnValue(createStartMock()), + }; + return mock; +}; + +export const actionServiceMock = { + create: createServiceMock, + createSetup: createSetupMock, + createStart: createStartMock, +}; diff --git a/src/plugins/saved_objects_management/public/services/action_service.test.ts b/src/plugins/saved_objects_management/public/services/action_service.test.ts new file mode 100644 index 0000000000000..107554589f83d --- /dev/null +++ b/src/plugins/saved_objects_management/public/services/action_service.test.ts @@ -0,0 +1,84 @@ +/* + * 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 { + SavedObjectsManagementActionService, + SavedObjectsManagementActionServiceSetup, +} from './action_service'; +import { SavedObjectsManagementAction } from './types'; + +class DummyAction extends SavedObjectsManagementAction { + constructor(public id: string) { + super(); + } + + public euiAction = { + name: 'name', + description: 'description', + icon: 'icon', + type: 'type', + }; + + public render = () => ''; +} + +describe('SavedObjectsManagementActionRegistry', () => { + let service: SavedObjectsManagementActionService; + let setup: SavedObjectsManagementActionServiceSetup; + + const createAction = (id: string): SavedObjectsManagementAction => { + return new DummyAction(id); + }; + + beforeEach(() => { + service = new SavedObjectsManagementActionService(); + setup = service.setup(); + }); + + describe('#register', () => { + it('allows actions to be registered and retrieved', () => { + const action = createAction('foo'); + setup.register(action); + const start = service.start(); + expect(start.getAll()).toContain(action); + }); + + it('does not allow actions with duplicate ids to be registered', () => { + const action = createAction('my-action'); + setup.register(action); + expect(() => setup.register(action)).toThrowErrorMatchingInlineSnapshot( + `"Saved Objects Management Action with id 'my-action' already exists"` + ); + }); + }); + + describe('#has', () => { + it('returns true when an action with a matching ID exists', () => { + const action = createAction('existing-action'); + setup.register(action); + const start = service.start(); + expect(start.has('existing-action')).toEqual(true); + }); + + it(`returns false when an action doesn't exist`, () => { + const start = service.start(); + expect(start.has('missing-action')).toEqual(false); + }); + }); +}); diff --git a/src/plugins/saved_objects_management/public/services/action_service.ts b/src/plugins/saved_objects_management/public/services/action_service.ts new file mode 100644 index 0000000000000..2b0b4cf5431e5 --- /dev/null +++ b/src/plugins/saved_objects_management/public/services/action_service.ts @@ -0,0 +1,60 @@ +/* + * 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 { SavedObjectsManagementAction } from './types'; + +export interface SavedObjectsManagementActionServiceSetup { + /** + * register given action in the registry. + */ + register: (action: SavedObjectsManagementAction) => void; +} + +export interface SavedObjectsManagementActionServiceStart { + /** + * return true if the registry contains given action, false otherwise. + */ + has: (actionId: string) => boolean; + /** + * return all {@link SavedObjectsManagementAction | actions} currently registered. + */ + getAll: () => SavedObjectsManagementAction[]; +} + +export class SavedObjectsManagementActionService { + private readonly actions = new Map(); + + setup(): SavedObjectsManagementActionServiceSetup { + return { + register: action => { + if (this.actions.has(action.id)) { + throw new Error(`Saved Objects Management Action with id '${action.id}' already exists`); + } + this.actions.set(action.id, action); + }, + }; + } + + start(): SavedObjectsManagementActionServiceStart { + return { + has: actionId => this.actions.has(actionId), + getAll: () => [...this.actions.values()], + }; + } +} diff --git a/src/plugins/saved_objects_management/public/services/index.ts b/src/plugins/saved_objects_management/public/services/index.ts index d6353576b8e11..a59ad9012c402 100644 --- a/src/plugins/saved_objects_management/public/services/index.ts +++ b/src/plugins/saved_objects_management/public/services/index.ts @@ -18,7 +18,13 @@ */ export { - SavedObjectsManagementActionRegistry, - ISavedObjectsManagementActionRegistry, -} from './action_registry'; -export { SavedObjectsManagementAction, SavedObjectsManagementRecord } from './action_types'; + SavedObjectsManagementActionService, + SavedObjectsManagementActionServiceStart, + SavedObjectsManagementActionServiceSetup, +} from './action_service'; +export { + SavedObjectsManagementServiceRegistry, + ISavedObjectsManagementServiceRegistry, + SavedObjectsManagementServiceRegistryEntry, +} from './service_registry'; +export { SavedObjectsManagementAction, SavedObjectsManagementRecord } from './types'; diff --git a/src/plugins/saved_objects_management/public/services/service_registry.mock.ts b/src/plugins/saved_objects_management/public/services/service_registry.mock.ts new file mode 100644 index 0000000000000..2e671c781928f --- /dev/null +++ b/src/plugins/saved_objects_management/public/services/service_registry.mock.ts @@ -0,0 +1,36 @@ +/* + * 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 { ISavedObjectsManagementServiceRegistry } from './service_registry'; + +const createRegistryMock = (): jest.Mocked => { + const mock = { + register: jest.fn(), + all: jest.fn(), + get: jest.fn(), + }; + + mock.all.mockReturnValue([]); + + return mock; +}; + +export const serviceRegistryMock = { + create: createRegistryMock, +}; diff --git a/src/plugins/saved_objects_management/public/services/service_registry.ts b/src/plugins/saved_objects_management/public/services/service_registry.ts new file mode 100644 index 0000000000000..2d6ec0b92047a --- /dev/null +++ b/src/plugins/saved_objects_management/public/services/service_registry.ts @@ -0,0 +1,49 @@ +/* + * 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 { SavedObjectLoader } from '../../../saved_objects/public'; + +export interface SavedObjectsManagementServiceRegistryEntry { + id: string; + service: SavedObjectLoader; + title: string; +} + +export type ISavedObjectsManagementServiceRegistry = PublicMethodsOf< + SavedObjectsManagementServiceRegistry +>; + +export class SavedObjectsManagementServiceRegistry { + private readonly registry = new Map(); + + public register(entry: SavedObjectsManagementServiceRegistryEntry) { + if (this.registry.has(entry.id)) { + throw new Error(''); + } + this.registry.set(entry.id, entry); + } + + public all(): SavedObjectsManagementServiceRegistryEntry[] { + return [...this.registry.values()]; + } + + public get(id: string): SavedObjectsManagementServiceRegistryEntry | undefined { + return this.registry.get(id); + } +} diff --git a/src/plugins/saved_objects_management/public/services/action_types.ts b/src/plugins/saved_objects_management/public/services/types.ts similarity index 100% rename from src/plugins/saved_objects_management/public/services/action_types.ts rename to src/plugins/saved_objects_management/public/services/types.ts diff --git a/src/plugins/saved_objects_management/public/types.ts b/src/plugins/saved_objects_management/public/types.ts new file mode 100644 index 0000000000000..e91b5d253b55f --- /dev/null +++ b/src/plugins/saved_objects_management/public/types.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 { SavedObjectMetadata, SavedObjectWithMetadata, SavedObjectRelation } from '../common'; diff --git a/src/plugins/saved_objects_management/server/routes/get_allowed_types.ts b/src/plugins/saved_objects_management/server/routes/get_allowed_types.ts new file mode 100644 index 0000000000000..ab5bec6678946 --- /dev/null +++ b/src/plugins/saved_objects_management/server/routes/get_allowed_types.ts @@ -0,0 +1,40 @@ +/* + * 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 { IRouter } from 'src/core/server'; + +export const registerGetAllowedTypesRoute = (router: IRouter) => { + router.get( + { + path: '/api/kibana/management/saved_objects/_allowed_types', + validate: false, + }, + async (context, req, res) => { + const allowedTypes = context.core.savedObjects.typeRegistry + .getImportableAndExportableTypes() + .map(type => type.name); + + return res.ok({ + body: { + types: allowedTypes, + }, + }); + } + ); +}; diff --git a/src/plugins/saved_objects_management/server/routes/index.test.ts b/src/plugins/saved_objects_management/server/routes/index.test.ts index f183972953dce..237760444f04e 100644 --- a/src/plugins/saved_objects_management/server/routes/index.test.ts +++ b/src/plugins/saved_objects_management/server/routes/index.test.ts @@ -34,7 +34,7 @@ describe('registerRoutes', () => { }); expect(httpSetup.createRouter).toHaveBeenCalledTimes(1); - expect(router.get).toHaveBeenCalledTimes(2); + expect(router.get).toHaveBeenCalledTimes(3); expect(router.post).toHaveBeenCalledTimes(2); expect(router.get).toHaveBeenCalledWith( @@ -49,6 +49,12 @@ describe('registerRoutes', () => { }), expect.any(Function) ); + expect(router.get).toHaveBeenCalledWith( + expect.objectContaining({ + path: '/api/kibana/management/saved_objects/_allowed_types', + }), + expect.any(Function) + ); expect(router.post).toHaveBeenCalledWith( expect.objectContaining({ path: '/api/kibana/management/saved_objects/scroll/counts', diff --git a/src/plugins/saved_objects_management/server/routes/index.ts b/src/plugins/saved_objects_management/server/routes/index.ts index 2c6adb71ed3ce..0929de56b215e 100644 --- a/src/plugins/saved_objects_management/server/routes/index.ts +++ b/src/plugins/saved_objects_management/server/routes/index.ts @@ -23,6 +23,7 @@ import { registerFindRoute } from './find'; import { registerScrollForCountRoute } from './scroll_count'; import { registerScrollForExportRoute } from './scroll_export'; import { registerRelationshipsRoute } from './relationships'; +import { registerGetAllowedTypesRoute } from './get_allowed_types'; interface RegisterRouteOptions { http: HttpServiceSetup; @@ -35,4 +36,5 @@ export function registerRoutes({ http, managementServicePromise }: RegisterRoute registerScrollForCountRoute(router); registerScrollForExportRoute(router); registerRelationshipsRoute(router, managementServicePromise); + registerGetAllowedTypesRoute(router); } diff --git a/src/plugins/saved_objects_management/server/services/management.test.ts b/src/plugins/saved_objects_management/server/services/management.test.ts index 6b95048749fae..3625a3f913444 100644 --- a/src/plugins/saved_objects_management/server/services/management.test.ts +++ b/src/plugins/saved_objects_management/server/services/management.test.ts @@ -28,7 +28,7 @@ describe('SavedObjectsManagement', () => { registry.registerType({ name: 'unknown', hidden: false, - namespaceAgnostic: false, + namespaceType: 'single', mappings: { properties: {} }, migrations: {}, ...type, diff --git a/src/plugins/saved_objects_management/server/types.ts b/src/plugins/saved_objects_management/server/types.ts index 5c4763d357e87..bd17d6a19ae70 100644 --- a/src/plugins/saved_objects_management/server/types.ts +++ b/src/plugins/saved_objects_management/server/types.ts @@ -17,38 +17,10 @@ * under the License. */ -import { SavedObject } from 'src/core/server'; - // eslint-disable-next-line @typescript-eslint/no-empty-interface export interface SavedObjectsManagementPluginSetup {} // eslint-disable-next-line @typescript-eslint/no-empty-interface export interface SavedObjectsManagementPluginStart {} -/** - * The metadata injected into a {@link SavedObject | saved object} when returning - * {@link SavedObjectWithMetadata | enhanced objects} from the plugin API endpoints. - */ -export interface SavedObjectMetadata { - icon?: string; - title?: string; - editUrl?: string; - inAppUrl?: { path: string; uiCapabilitiesPath: string }; -} - -/** - * A {@link SavedObject | saved object} enhanced with meta properties used by the client-side plugin. - */ -export type SavedObjectWithMetadata = SavedObject & { - meta: SavedObjectMetadata; -}; - -/** - * Represents a relation between two {@link SavedObject | saved object} - */ -export interface SavedObjectRelation { - id: string; - type: string; - relationship: 'child' | 'parent'; - meta: SavedObjectMetadata; -} +export { SavedObjectMetadata, SavedObjectWithMetadata, SavedObjectRelation } from '../common'; diff --git a/src/plugins/share/server/routes/goto.ts b/src/plugins/share/server/routes/goto.ts index 0c5b74915e58a..747af3b9e57df 100644 --- a/src/plugins/share/server/routes/goto.ts +++ b/src/plugins/share/server/routes/goto.ts @@ -34,7 +34,7 @@ export const createGotoRoute = ({ shortUrlLookup: ShortUrlLookupService; http: CoreSetup['http']; }) => { - router.get( + http.resources.register( { path: getGotoPath('{urlId}'), validate: { @@ -63,14 +63,8 @@ export const createGotoRoute = ({ }, }); } - const body = await context.core.rendering.render(); - return response.ok({ - headers: { - 'content-security-policy': http.csp.header, - }, - body, - }); + return response.renderCoreApp(); }) ); }; diff --git a/src/plugins/telemetry/server/collectors/application_usage/saved_objects_types.ts b/src/plugins/telemetry/server/collectors/application_usage/saved_objects_types.ts index 9f997ab7b5df3..a0de79da565e6 100644 --- a/src/plugins/telemetry/server/collectors/application_usage/saved_objects_types.ts +++ b/src/plugins/telemetry/server/collectors/application_usage/saved_objects_types.ts @@ -33,7 +33,7 @@ export function registerMappings(registerType: SavedObjectsServiceSetup['registe registerType({ name: 'application_usage_totals', hidden: false, - namespaceAgnostic: true, + namespaceType: 'agnostic', mappings: { properties: { appId: { type: 'keyword' }, @@ -46,7 +46,7 @@ export function registerMappings(registerType: SavedObjectsServiceSetup['registe registerType({ name: 'application_usage_transactional', hidden: false, - namespaceAgnostic: true, + namespaceType: 'agnostic', mappings: { properties: { timestamp: { type: 'date' }, diff --git a/src/plugins/telemetry/server/collectors/ui_metric/telemetry_ui_metric_collector.ts b/src/plugins/telemetry/server/collectors/ui_metric/telemetry_ui_metric_collector.ts index 3f6e1836cac7d..603742f612a6b 100644 --- a/src/plugins/telemetry/server/collectors/ui_metric/telemetry_ui_metric_collector.ts +++ b/src/plugins/telemetry/server/collectors/ui_metric/telemetry_ui_metric_collector.ts @@ -38,7 +38,7 @@ export function registerUiMetricUsageCollector( registerType({ name: 'ui-metric', hidden: false, - namespaceAgnostic: true, + namespaceType: 'agnostic', mappings: { properties: { count: { diff --git a/src/plugins/telemetry/server/config.ts b/src/plugins/telemetry/server/config.ts index 9621a8b5619b2..99dde0c3b3d96 100644 --- a/src/plugins/telemetry/server/config.ts +++ b/src/plugins/telemetry/server/config.ts @@ -36,8 +36,8 @@ export const configSchema = schema.object({ config: schema.string({ defaultValue: getConfigPath() }), banner: schema.boolean({ defaultValue: true }), url: schema.conditional( - schema.contextRef('dev'), - schema.literal(true), + schema.contextRef('dist'), + schema.literal(false), // Point to staging if it's not a distributable release schema.string({ defaultValue: `https://telemetry-staging.elastic.co/xpack/${ENDPOINT_VERSION}/send`, }), @@ -46,8 +46,8 @@ export const configSchema = schema.object({ }) ), optInStatusUrl: schema.conditional( - schema.contextRef('dev'), - schema.literal(true), + schema.contextRef('dist'), + schema.literal(false), // Point to staging if it's not a distributable release schema.string({ defaultValue: `https://telemetry-staging.elastic.co/opt_in_status/${ENDPOINT_VERSION}/send`, }), diff --git a/src/plugins/telemetry/server/plugin.ts b/src/plugins/telemetry/server/plugin.ts index 1df6a665e4d76..d1530c272027a 100644 --- a/src/plugins/telemetry/server/plugin.ts +++ b/src/plugins/telemetry/server/plugin.ts @@ -125,7 +125,7 @@ export class TelemetryPlugin implements Plugin { registerType({ name: 'telemetry', hidden: false, - namespaceAgnostic: true, + namespaceType: 'agnostic', mappings: { properties: { enabled: { diff --git a/src/plugins/telemetry_collection_manager/server/encryption/encrypt.test.mocks.ts b/src/plugins/telemetry_collection_manager/server/encryption/encrypt.test.mocks.ts new file mode 100644 index 0000000000000..9a7cb8ba28d04 --- /dev/null +++ b/src/plugins/telemetry_collection_manager/server/encryption/encrypt.test.mocks.ts @@ -0,0 +1,27 @@ +/* + * 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 const mockEncrypt = jest.fn(); +export const createRequestEncryptor = jest.fn().mockResolvedValue({ + encrypt: mockEncrypt, +}); + +jest.doMock('@elastic/request-crypto', () => ({ + createRequestEncryptor, +})); diff --git a/src/plugins/telemetry_collection_manager/server/encryption/encrypt.test.ts b/src/plugins/telemetry_collection_manager/server/encryption/encrypt.test.ts index 4a4ba7aa1f321..6d64268569e06 100644 --- a/src/plugins/telemetry_collection_manager/server/encryption/encrypt.test.ts +++ b/src/plugins/telemetry_collection_manager/server/encryption/encrypt.test.ts @@ -16,35 +16,44 @@ * specific language governing permissions and limitations * under the License. */ - +import { createRequestEncryptor, mockEncrypt } from './encrypt.test.mocks'; import { telemetryJWKS } from './telemetry_jwks'; import { encryptTelemetry, getKID } from './encrypt'; -import { createRequestEncryptor } from '@elastic/request-crypto'; - -jest.mock('@elastic/request-crypto', () => ({ - createRequestEncryptor: jest.fn().mockResolvedValue({ - encrypt: jest.fn(), - }), -})); describe('getKID', () => { it(`returns 'kibana_dev' kid for development`, async () => { - const isProd = false; - const kid = getKID(isProd); + const useProdKey = false; + const kid = getKID(useProdKey); expect(kid).toBe('kibana_dev'); }); it(`returns 'kibana_prod' kid for development`, async () => { - const isProd = true; - const kid = getKID(isProd); + const useProdKey = true; + const kid = getKID(useProdKey); expect(kid).toBe('kibana'); }); }); describe('encryptTelemetry', () => { + afterEach(() => { + mockEncrypt.mockReset(); + }); + it('encrypts payload', async () => { const payload = { some: 'value' }; - await encryptTelemetry(payload, true); + await encryptTelemetry(payload, { useProdKey: true }); expect(createRequestEncryptor).toBeCalledWith(telemetryJWKS); }); + + it('uses kibana kid on { useProdKey: true }', async () => { + const payload = { some: 'value' }; + await encryptTelemetry(payload, { useProdKey: true }); + expect(mockEncrypt).toBeCalledWith('kibana', payload); + }); + + it('uses kibana_dev kid on { useProdKey: false }', async () => { + const payload = { some: 'value' }; + await encryptTelemetry(payload, { useProdKey: false }); + expect(mockEncrypt).toBeCalledWith('kibana_dev', payload); + }); }); diff --git a/src/plugins/telemetry_collection_manager/server/encryption/encrypt.ts b/src/plugins/telemetry_collection_manager/server/encryption/encrypt.ts index c20f4b768b7dc..89f34d794f059 100644 --- a/src/plugins/telemetry_collection_manager/server/encryption/encrypt.ts +++ b/src/plugins/telemetry_collection_manager/server/encryption/encrypt.ts @@ -20,12 +20,15 @@ import { createRequestEncryptor } from '@elastic/request-crypto'; import { telemetryJWKS } from './telemetry_jwks'; -export function getKID(isProd = false): string { - return isProd ? 'kibana' : 'kibana_dev'; +export function getKID(useProdKey = false): string { + return useProdKey ? 'kibana' : 'kibana_dev'; } -export async function encryptTelemetry(payload: any, isProd = false): Promise { - const kid = getKID(isProd); +export async function encryptTelemetry( + payload: any, + { useProdKey = false } = {} +): Promise { + const kid = getKID(useProdKey); const encryptor = await createRequestEncryptor(telemetryJWKS); const clusters = [].concat(payload); return Promise.all(clusters.map((cluster: any) => encryptor.encrypt(kid, cluster))); diff --git a/src/plugins/telemetry_collection_manager/server/plugin.ts b/src/plugins/telemetry_collection_manager/server/plugin.ts index 7e8dff9e0aec1..0b57fae83c0fb 100644 --- a/src/plugins/telemetry_collection_manager/server/plugin.ts +++ b/src/plugins/telemetry_collection_manager/server/plugin.ts @@ -50,12 +50,12 @@ export class TelemetryCollectionManagerPlugin private readonly collections: Array> = []; private usageGetterMethodPriority = -1; private usageCollection?: UsageCollectionSetup; - private readonly isDev: boolean; + private readonly isDistributable: boolean; private readonly version: string; constructor(initializerContext: PluginInitializerContext) { this.logger = initializerContext.logger.get(); - this.isDev = initializerContext.env.mode.dev; + this.isDistributable = initializerContext.env.packageInfo.dist; this.version = initializerContext.env.packageInfo.version; } @@ -158,7 +158,7 @@ export class TelemetryCollectionManagerPlugin if (config.unencrypted) { return optInStats; } - return encryptTelemetry(optInStats, this.isDev); + return encryptTelemetry(optInStats, { useProdKey: this.isDistributable }); } } catch (err) { this.logger.debug(`Failed to collect any opt in stats with registered collections.`); @@ -176,7 +176,6 @@ export class TelemetryCollectionManagerPlugin ) => { const context: StatsCollectionContext = { logger: this.logger.get(collection.title), - isDev: this.isDev, version: this.version, ...collection.customContext, }; @@ -205,7 +204,8 @@ export class TelemetryCollectionManagerPlugin if (config.unencrypted) { return usageData; } - return encryptTelemetry(usageData, this.isDev); + + return encryptTelemetry(usageData, { useProdKey: this.isDistributable }); } } catch (err) { this.logger.debug( @@ -224,7 +224,6 @@ export class TelemetryCollectionManagerPlugin ): Promise { const context: StatsCollectionContext = { logger: this.logger.get(collection.title), - isDev: this.isDev, version: this.version, ...collection.customContext, }; diff --git a/src/plugins/telemetry_collection_manager/server/types.ts b/src/plugins/telemetry_collection_manager/server/types.ts index e23d6a4c388f4..d3a47694d38a7 100644 --- a/src/plugins/telemetry_collection_manager/server/types.ts +++ b/src/plugins/telemetry_collection_manager/server/types.ts @@ -101,7 +101,6 @@ export interface ESLicense { export interface StatsCollectionContext { logger: Logger; - isDev: boolean; version: string; } diff --git a/src/plugins/timelion/kibana.json b/src/plugins/timelion/kibana.json deleted file mode 100644 index dddfd6c67e655..0000000000000 --- a/src/plugins/timelion/kibana.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "id": "timelion", - "version": "8.0.0", - "kibanaVersion": "kibana", - "configPath": ["timelion"], - "server": true, - "ui": true -} diff --git a/src/plugins/timelion/public/index.ts b/src/plugins/timelion/public/index.ts deleted file mode 100644 index b05c4f8a30b22..0000000000000 --- a/src/plugins/timelion/public/index.ts +++ /dev/null @@ -1,30 +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 { CoreStart, PluginInitializerContext } from 'kibana/public'; -import { ConfigSchema } from '../config'; - -export const plugin = (initializerContext: PluginInitializerContext) => ({ - setup() {}, - start(core: CoreStart) { - if (initializerContext.config.get().ui.enabled === false) { - core.chrome.navLinks.update('timelion', { hidden: true }); - } - }, -}); diff --git a/src/plugins/timelion/server/index.ts b/src/plugins/timelion/server/index.ts deleted file mode 100644 index 5d420327f961e..0000000000000 --- a/src/plugins/timelion/server/index.ts +++ /dev/null @@ -1,35 +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 { PluginInitializerContext } from '../../../../src/core/server'; -import { configSchema } from '../config'; -import { Plugin } from './plugin'; - -export { PluginSetupContract } from './plugin'; - -export const config = { - schema: configSchema, - exposeToBrowser: { - ui: { - enabled: true, - }, - }, -}; -export const plugin = (initializerContext: PluginInitializerContext) => - new Plugin(initializerContext); diff --git a/src/plugins/ui_actions/public/actions/action.ts b/src/plugins/ui_actions/public/actions/action.ts index f532c2c8aa219..feaa1f6a60e2f 100644 --- a/src/plugins/ui_actions/public/actions/action.ts +++ b/src/plugins/ui_actions/public/actions/action.ts @@ -63,9 +63,11 @@ export interface Action { isCompatible(context: Context): Promise; /** - * If this returns something truthy, this is used in addition to the `execute` method when clicked. + * If this returns something truthy, this will be used as [href] attribute on a link if possible (e.g. in context menu item) + * to support right click -> open in a new tab behavior. + * For regular click navigation is prevented and `execute()` takes control. */ - getHref?(context: Context): string | undefined; + getHref?(context: Context): Promise; /** * Executes the action. diff --git a/src/plugins/ui_actions/public/actions/action_definition.ts b/src/plugins/ui_actions/public/actions/action_definition.ts index 3eaa13572a826..79fda78401abd 100644 --- a/src/plugins/ui_actions/public/actions/action_definition.ts +++ b/src/plugins/ui_actions/public/actions/action_definition.ts @@ -63,7 +63,7 @@ export interface ActionDefinition { /** * If this returns something truthy, this is used in addition to the `execute` method when clicked. */ - getHref?(context: ActionContextMapping[T]): string | undefined; + getHref?(context: ActionContextMapping[T]): Promise; /** * Executes the action. diff --git a/src/plugins/ui_actions/public/actions/create_action.ts b/src/plugins/ui_actions/public/actions/create_action.ts index 90a9415c0b497..cc66f221e4082 100644 --- a/src/plugins/ui_actions/public/actions/create_action.ts +++ b/src/plugins/ui_actions/public/actions/create_action.ts @@ -28,7 +28,6 @@ export function createAction(action: ActionDefinition): id: action.type, isCompatible: () => Promise.resolve(true), getDisplayName: () => '', - getHref: () => undefined, ...action, }; } diff --git a/src/plugins/ui_actions/public/context_menu/build_eui_context_menu_panels.tsx b/src/plugins/ui_actions/public/context_menu/build_eui_context_menu_panels.tsx index 3dce2c1f4c257..d26740ffdf033 100644 --- a/src/plugins/ui_actions/public/context_menu/build_eui_context_menu_panels.tsx +++ b/src/plugins/ui_actions/public/context_menu/build_eui_context_menu_panels.tsx @@ -71,7 +71,7 @@ async function buildEuiContextMenuPanelItems({ } items.push( - convertPanelActionToContextMenuItem({ + await convertPanelActionToContextMenuItem({ action, actionContext, closeMenu, @@ -88,9 +88,9 @@ async function buildEuiContextMenuPanelItems({ * * @param {ContextMenuAction} action * @param {Embeddable} embeddable - * @return {EuiContextMenuPanelItemDescriptor} + * @return {Promise} */ -function convertPanelActionToContextMenuItem({ +async function convertPanelActionToContextMenuItem({ action, actionContext, closeMenu, @@ -98,7 +98,7 @@ function convertPanelActionToContextMenuItem({ action: Action; actionContext: A; closeMenu: () => void; -}): EuiContextMenuPanelItemDescriptor { +}): Promise { const menuPanelItem: EuiContextMenuPanelItemDescriptor = { name: action.MenuItem ? React.createElement(uiToReactComponent(action.MenuItem), { @@ -110,13 +110,33 @@ function convertPanelActionToContextMenuItem({ 'data-test-subj': `embeddablePanelAction-${action.id}`, }; - menuPanelItem.onClick = () => { - action.execute(actionContext); + menuPanelItem.onClick = event => { + if (event.currentTarget instanceof HTMLAnchorElement) { + // from react-router's + if ( + !event.defaultPrevented && // onClick prevented default + event.button === 0 && // ignore everything but left clicks + (!event.currentTarget.target || event.currentTarget.target === '_self') && // let browser handle "target=_blank" etc. + !(event.metaKey || event.altKey || event.ctrlKey || event.shiftKey) // ignore clicks with modifier keys + ) { + event.preventDefault(); + action.execute(actionContext); + } else { + // let browser handle navigation + } + } else { + // not a link + action.execute(actionContext); + } + closeMenu(); }; - if (action.getHref && action.getHref(actionContext)) { - menuPanelItem.href = action.getHref(actionContext); + if (action.getHref) { + const href = await action.getHref(actionContext); + if (href) { + menuPanelItem.href = href; + } } return menuPanelItem; diff --git a/src/plugins/ui_actions/public/triggers/trigger_internal.ts b/src/plugins/ui_actions/public/triggers/trigger_internal.ts index 5b670df354f78..1fc92d7c0cb1b 100644 --- a/src/plugins/ui_actions/public/triggers/trigger_internal.ts +++ b/src/plugins/ui_actions/public/triggers/trigger_internal.ts @@ -55,13 +55,6 @@ export class TriggerInternal { action: Action, context: TriggerContextMapping[T] ) { - const href = action.getHref && action.getHref(context); - - if (href) { - window.location.href = href; - return; - } - await action.execute(context); } diff --git a/src/plugins/vis_default_editor/public/components/controls/components/number_list/utils.ts b/src/plugins/vis_default_editor/public/components/controls/components/number_list/utils.ts index c2ac63c98cbea..dac86249ebbb9 100644 --- a/src/plugins/vis_default_editor/public/components/controls/components/number_list/utils.ts +++ b/src/plugins/vis_default_editor/public/components/controls/components/number_list/utils.ts @@ -92,7 +92,7 @@ function validateValueUnique( isDuplicate: false, }; - if (inputValue && list.indexOf(inputValue) !== index) { + if (inputValue !== EMPTY_STRING && list.indexOf(inputValue) !== index) { result.isDuplicate = true; result.error = i18n.translate( 'visDefaultEditor.controls.numberList.duplicateValueErrorMessage', diff --git a/src/plugins/vis_default_editor/public/default_editor.tsx b/src/plugins/vis_default_editor/public/default_editor.tsx index f1963b94dcf95..1c2ddbc314f99 100644 --- a/src/plugins/vis_default_editor/public/default_editor.tsx +++ b/src/plugins/vis_default_editor/public/default_editor.tsx @@ -19,7 +19,7 @@ import React, { useEffect, useRef, useState, useCallback } from 'react'; -import { EditorRenderProps } from 'src/legacy/core_plugins/kibana/public/visualize/np_ready/types'; +import { EditorRenderProps } from 'src/plugins/visualize/public'; import { PanelsContainer, Panel } from '../../kibana_react/public'; import './vis_type_agg_filter'; diff --git a/src/plugins/vis_default_editor/public/default_editor_controller.tsx b/src/plugins/vis_default_editor/public/default_editor_controller.tsx index 798da09f8e30b..014c69f50d558 100644 --- a/src/plugins/vis_default_editor/public/default_editor_controller.tsx +++ b/src/plugins/vis_default_editor/public/default_editor_controller.tsx @@ -22,7 +22,7 @@ import { render, unmountComponentAtNode } from 'react-dom'; import { i18n } from '@kbn/i18n'; import { EventEmitter } from 'events'; -import { EditorRenderProps } from 'src/legacy/core_plugins/kibana/public/visualize/np_ready/types'; +import { EditorRenderProps } from 'src/plugins/visualize/public'; import { Vis, VisualizeEmbeddableContract } from 'src/plugins/visualizations/public'; import { Storage } from '../../kibana_utils/public'; import { KibanaContextProvider } from '../../kibana_react/public'; diff --git a/src/plugins/vis_type_markdown/config.ts b/src/plugins/vis_type_markdown/config.ts new file mode 100644 index 0000000000000..6749bd83de39f --- /dev/null +++ b/src/plugins/vis_type_markdown/config.ts @@ -0,0 +1,26 @@ +/* + * 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 { schema, TypeOf } from '@kbn/config-schema'; + +export const configSchema = schema.object({ + enabled: schema.boolean({ defaultValue: true }), +}); + +export type ConfigSchema = TypeOf; diff --git a/src/plugins/vis_type_markdown/kibana.json b/src/plugins/vis_type_markdown/kibana.json new file mode 100644 index 0000000000000..d52e22118ccf0 --- /dev/null +++ b/src/plugins/vis_type_markdown/kibana.json @@ -0,0 +1,7 @@ +{ + "id": "visTypeMarkdown", + "version": "kibana", + "ui": true, + "server": true, + "requiredPlugins": ["expressions", "visualizations"] +} diff --git a/src/legacy/core_plugins/vis_type_markdown/public/__snapshots__/markdown_fn.test.ts.snap b/src/plugins/vis_type_markdown/public/__snapshots__/markdown_fn.test.ts.snap similarity index 100% rename from src/legacy/core_plugins/vis_type_markdown/public/__snapshots__/markdown_fn.test.ts.snap rename to src/plugins/vis_type_markdown/public/__snapshots__/markdown_fn.test.ts.snap diff --git a/src/legacy/core_plugins/vis_type_markdown/public/_markdown_vis.scss b/src/plugins/vis_type_markdown/public/_markdown_vis.scss similarity index 100% rename from src/legacy/core_plugins/vis_type_markdown/public/_markdown_vis.scss rename to src/plugins/vis_type_markdown/public/_markdown_vis.scss diff --git a/src/plugins/vis_type_markdown/public/index.scss b/src/plugins/vis_type_markdown/public/index.scss new file mode 100644 index 0000000000000..ddb7fe3a6b0d9 --- /dev/null +++ b/src/plugins/vis_type_markdown/public/index.scss @@ -0,0 +1,8 @@ +// Prefix all styles with "mkd" to avoid conflicts. +// Examples +// mkdChart +// mkdChart__legend +// mkdChart__legend--small +// mkdChart__legend-isLoading + +@import './markdown_vis'; diff --git a/src/plugins/vis_type_markdown/public/index.ts b/src/plugins/vis_type_markdown/public/index.ts new file mode 100644 index 0000000000000..bc3b6ff7bf105 --- /dev/null +++ b/src/plugins/vis_type_markdown/public/index.ts @@ -0,0 +1,25 @@ +/* + * 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 { PluginInitializerContext } from '../../../core/public'; +import { MarkdownPlugin as Plugin } from './plugin'; + +export function plugin(initializerContext: PluginInitializerContext) { + return new Plugin(initializerContext); +} diff --git a/src/legacy/core_plugins/vis_type_markdown/public/markdown_fn.test.ts b/src/plugins/vis_type_markdown/public/markdown_fn.test.ts similarity index 89% rename from src/legacy/core_plugins/vis_type_markdown/public/markdown_fn.test.ts rename to src/plugins/vis_type_markdown/public/markdown_fn.test.ts index 5f41840bac99b..d6085804e74ab 100644 --- a/src/legacy/core_plugins/vis_type_markdown/public/markdown_fn.test.ts +++ b/src/plugins/vis_type_markdown/public/markdown_fn.test.ts @@ -17,8 +17,7 @@ * under the License. */ -// eslint-disable-next-line -import { functionWrapper } from '../../../../plugins/expressions/common/expression_functions/specs/tests/utils'; +import { functionWrapper } from '../../expressions/common/expression_functions/specs/tests/utils'; import { createMarkdownVisFn } from './markdown_fn'; describe('interpreter/functions#markdown', () => { diff --git a/src/legacy/core_plugins/vis_type_markdown/public/markdown_fn.ts b/src/plugins/vis_type_markdown/public/markdown_fn.ts similarity index 95% rename from src/legacy/core_plugins/vis_type_markdown/public/markdown_fn.ts rename to src/plugins/vis_type_markdown/public/markdown_fn.ts index bbf2b7844c73f..9f0809109e465 100644 --- a/src/legacy/core_plugins/vis_type_markdown/public/markdown_fn.ts +++ b/src/plugins/vis_type_markdown/public/markdown_fn.ts @@ -18,7 +18,7 @@ */ import { i18n } from '@kbn/i18n'; -import { ExpressionFunctionDefinition, Render } from '../../../../plugins/expressions/public'; +import { ExpressionFunctionDefinition, Render } from '../../expressions/public'; import { Arguments, MarkdownVisParams } from './types'; interface RenderValue { diff --git a/src/legacy/core_plugins/vis_type_markdown/public/markdown_options.tsx b/src/plugins/vis_type_markdown/public/markdown_options.tsx similarity index 100% rename from src/legacy/core_plugins/vis_type_markdown/public/markdown_options.tsx rename to src/plugins/vis_type_markdown/public/markdown_options.tsx diff --git a/src/legacy/core_plugins/vis_type_markdown/public/markdown_vis.ts b/src/plugins/vis_type_markdown/public/markdown_vis.ts similarity index 96% rename from src/legacy/core_plugins/vis_type_markdown/public/markdown_vis.ts rename to src/plugins/vis_type_markdown/public/markdown_vis.ts index 57ea6d9c9bb3d..b84d9638eb973 100644 --- a/src/legacy/core_plugins/vis_type_markdown/public/markdown_vis.ts +++ b/src/plugins/vis_type_markdown/public/markdown_vis.ts @@ -22,7 +22,7 @@ import { i18n } from '@kbn/i18n'; import { MarkdownVisWrapper } from './markdown_vis_controller'; import { MarkdownOptions } from './markdown_options'; import { SettingsOptions } from './settings_options'; -import { DefaultEditorSize } from '../../../../plugins/vis_default_editor/public'; +import { DefaultEditorSize } from '../../vis_default_editor/public'; export const markdownVisDefinition = { name: 'markdown', diff --git a/src/legacy/core_plugins/vis_type_markdown/public/markdown_vis_controller.test.tsx b/src/plugins/vis_type_markdown/public/markdown_vis_controller.test.tsx similarity index 100% rename from src/legacy/core_plugins/vis_type_markdown/public/markdown_vis_controller.test.tsx rename to src/plugins/vis_type_markdown/public/markdown_vis_controller.test.tsx diff --git a/src/legacy/core_plugins/vis_type_markdown/public/markdown_vis_controller.tsx b/src/plugins/vis_type_markdown/public/markdown_vis_controller.tsx similarity index 97% rename from src/legacy/core_plugins/vis_type_markdown/public/markdown_vis_controller.tsx rename to src/plugins/vis_type_markdown/public/markdown_vis_controller.tsx index 3260e9f7d8091..4e77bb196b713 100644 --- a/src/legacy/core_plugins/vis_type_markdown/public/markdown_vis_controller.tsx +++ b/src/plugins/vis_type_markdown/public/markdown_vis_controller.tsx @@ -18,7 +18,7 @@ */ import React from 'react'; -import { Markdown } from '../../../../plugins/kibana_react/public'; +import { Markdown } from '../../kibana_react/public'; import { MarkdownVisParams } from './types'; interface MarkdownVisComponentProps extends MarkdownVisParams { diff --git a/src/plugins/vis_type_markdown/public/plugin.ts b/src/plugins/vis_type_markdown/public/plugin.ts new file mode 100644 index 0000000000000..9365017a31adc --- /dev/null +++ b/src/plugins/vis_type_markdown/public/plugin.ts @@ -0,0 +1,52 @@ +/* + * 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 { PluginInitializerContext, CoreSetup, CoreStart, Plugin } from '../../../core/public'; +import { Plugin as ExpressionsPublicPlugin } from '../../expressions/public'; +import { VisualizationsSetup } from '../../visualizations/public'; + +import { markdownVisDefinition } from './markdown_vis'; +import { createMarkdownVisFn } from './markdown_fn'; +import { ConfigSchema } from '../config'; + +import './index.scss'; + +/** @internal */ +export interface MarkdownPluginSetupDependencies { + expressions: ReturnType; + visualizations: VisualizationsSetup; +} + +/** @internal */ +export class MarkdownPlugin implements Plugin { + initializerContext: PluginInitializerContext; + + constructor(initializerContext: PluginInitializerContext) { + this.initializerContext = initializerContext; + } + + public setup(core: CoreSetup, { expressions, visualizations }: MarkdownPluginSetupDependencies) { + visualizations.createReactVisualization(markdownVisDefinition); + expressions.registerFunction(createMarkdownVisFn); + } + + public start(core: CoreStart) { + // nothing to do here yet + } +} diff --git a/src/legacy/core_plugins/vis_type_markdown/public/settings_options.tsx b/src/plugins/vis_type_markdown/public/settings_options.tsx similarity index 96% rename from src/legacy/core_plugins/vis_type_markdown/public/settings_options.tsx rename to src/plugins/vis_type_markdown/public/settings_options.tsx index 552fd63373554..6f6a80564ce07 100644 --- a/src/legacy/core_plugins/vis_type_markdown/public/settings_options.tsx +++ b/src/plugins/vis_type_markdown/public/settings_options.tsx @@ -22,7 +22,7 @@ import { EuiPanel } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { VisOptionsProps } from 'src/plugins/vis_default_editor/public'; -import { RangeOption, SwitchOption } from '../../vis_type_vislib/public'; +import { RangeOption, SwitchOption } from '../../charts/public'; import { MarkdownVisParams } from './types'; function SettingsOptions({ stateParams, setValue }: VisOptionsProps) { diff --git a/src/legacy/core_plugins/vis_type_markdown/public/types.ts b/src/plugins/vis_type_markdown/public/types.ts similarity index 100% rename from src/legacy/core_plugins/vis_type_markdown/public/types.ts rename to src/plugins/vis_type_markdown/public/types.ts diff --git a/src/plugins/vis_type_markdown/server/index.ts b/src/plugins/vis_type_markdown/server/index.ts new file mode 100644 index 0000000000000..73e1712353b1a --- /dev/null +++ b/src/plugins/vis_type_markdown/server/index.ts @@ -0,0 +1,34 @@ +/* + * 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 { PluginConfigDescriptor } from 'kibana/server'; + +import { configSchema, ConfigSchema } from '../config'; + +export const config: PluginConfigDescriptor = { + schema: configSchema, + deprecations: ({ renameFromRoot }) => [ + renameFromRoot('markdown_vis.enabled', 'vis_type_markdown.enabled'), + ], +}; + +export const plugin = () => ({ + setup() {}, + start() {}, +}); diff --git a/src/plugins/vis_type_metric/config.ts b/src/plugins/vis_type_metric/config.ts new file mode 100644 index 0000000000000..6749bd83de39f --- /dev/null +++ b/src/plugins/vis_type_metric/config.ts @@ -0,0 +1,26 @@ +/* + * 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 { schema, TypeOf } from '@kbn/config-schema'; + +export const configSchema = schema.object({ + enabled: schema.boolean({ defaultValue: true }), +}); + +export type ConfigSchema = TypeOf; diff --git a/src/plugins/vis_type_metric/kibana.json b/src/plugins/vis_type_metric/kibana.json new file mode 100644 index 0000000000000..24135d257b317 --- /dev/null +++ b/src/plugins/vis_type_metric/kibana.json @@ -0,0 +1,8 @@ +{ + "id": "visTypeMetric", + "version": "8.0.0", + "kibanaVersion": "kibana", + "server": true, + "ui": true, + "requiredPlugins": ["data", "visualizations", "charts","expressions"] +} diff --git a/src/legacy/core_plugins/vis_type_metric/public/__snapshots__/metric_vis_fn.test.ts.snap b/src/plugins/vis_type_metric/public/__snapshots__/metric_vis_fn.test.ts.snap similarity index 100% rename from src/legacy/core_plugins/vis_type_metric/public/__snapshots__/metric_vis_fn.test.ts.snap rename to src/plugins/vis_type_metric/public/__snapshots__/metric_vis_fn.test.ts.snap diff --git a/src/legacy/core_plugins/vis_type_metric/public/_metric_vis.scss b/src/plugins/vis_type_metric/public/_metric_vis.scss similarity index 100% rename from src/legacy/core_plugins/vis_type_metric/public/_metric_vis.scss rename to src/plugins/vis_type_metric/public/_metric_vis.scss diff --git a/src/legacy/core_plugins/vis_type_metric/public/components/__snapshots__/metric_vis_component.test.tsx.snap b/src/plugins/vis_type_metric/public/components/__snapshots__/metric_vis_component.test.tsx.snap similarity index 100% rename from src/legacy/core_plugins/vis_type_metric/public/components/__snapshots__/metric_vis_component.test.tsx.snap rename to src/plugins/vis_type_metric/public/components/__snapshots__/metric_vis_component.test.tsx.snap diff --git a/src/legacy/core_plugins/vis_type_metric/public/components/metric_vis_component.test.tsx b/src/plugins/vis_type_metric/public/components/metric_vis_component.test.tsx similarity index 97% rename from src/legacy/core_plugins/vis_type_metric/public/components/metric_vis_component.test.tsx rename to src/plugins/vis_type_metric/public/components/metric_vis_component.test.tsx index 2bd423656b0f0..3969b28d75414 100644 --- a/src/legacy/core_plugins/vis_type_metric/public/components/metric_vis_component.test.tsx +++ b/src/plugins/vis_type_metric/public/components/metric_vis_component.test.tsx @@ -21,7 +21,7 @@ import React from 'react'; import { shallow } from 'enzyme'; import { MetricVisComponent, MetricVisComponentProps } from './metric_vis_component'; -import { ExprVis } from '../../../../../plugins/visualizations/public'; +import { ExprVis } from '../../../visualizations/public'; jest.mock('../services', () => ({ getFormatService: () => ({ diff --git a/src/legacy/core_plugins/vis_type_metric/public/components/metric_vis_component.tsx b/src/plugins/vis_type_metric/public/components/metric_vis_component.tsx similarity index 96% rename from src/legacy/core_plugins/vis_type_metric/public/components/metric_vis_component.tsx rename to src/plugins/vis_type_metric/public/components/metric_vis_component.tsx index de2cc66a99c79..eb3986b6388fe 100644 --- a/src/legacy/core_plugins/vis_type_metric/public/components/metric_vis_component.tsx +++ b/src/plugins/vis_type_metric/public/components/metric_vis_component.tsx @@ -22,12 +22,12 @@ import React, { Component } from 'react'; import { isColorDark } from '@elastic/eui'; import { MetricVisValue } from './metric_vis_value'; import { Input } from '../metric_vis_fn'; -import { FieldFormatsContentType, IFieldFormat } from '../../../../../plugins/data/public'; -import { KibanaDatatable } from '../../../../../plugins/expressions/public'; -import { getHeatmapColors } from '../../../../../plugins/charts/public'; +import { FieldFormatsContentType, IFieldFormat } from '../../../data/public'; +import { KibanaDatatable } from '../../../expressions/public'; +import { getHeatmapColors } from '../../../charts/public'; import { VisParams, MetricVisMetric } from '../types'; import { getFormatService } from '../services'; -import { SchemaConfig, ExprVis } from '../../../../../plugins/visualizations/public'; +import { SchemaConfig, ExprVis } from '../../../visualizations/public'; export interface MetricVisComponentProps { visParams: VisParams; diff --git a/src/legacy/core_plugins/vis_type_metric/public/components/metric_vis_options.tsx b/src/plugins/vis_type_metric/public/components/metric_vis_options.tsx similarity index 97% rename from src/legacy/core_plugins/vis_type_metric/public/components/metric_vis_options.tsx rename to src/plugins/vis_type_metric/public/components/metric_vis_options.tsx index 5c3032511f09a..009d63ded39b4 100644 --- a/src/legacy/core_plugins/vis_type_metric/public/components/metric_vis_options.tsx +++ b/src/plugins/vis_type_metric/public/components/metric_vis_options.tsx @@ -37,9 +37,9 @@ import { SwitchOption, RangeOption, SetColorSchemaOptionsValue, -} from '../../../vis_type_vislib/public'; + SetColorRangeValue, +} from '../../../charts/public'; import { MetricVisParam, VisParams } from '../types'; -import { SetColorRangeValue } from '../../../vis_type_vislib/public/components/common/color_ranges'; function MetricVisOptions({ stateParams, diff --git a/src/legacy/core_plugins/vis_type_metric/public/components/metric_vis_value.test.tsx b/src/plugins/vis_type_metric/public/components/metric_vis_value.test.tsx similarity index 100% rename from src/legacy/core_plugins/vis_type_metric/public/components/metric_vis_value.test.tsx rename to src/plugins/vis_type_metric/public/components/metric_vis_value.test.tsx diff --git a/src/legacy/core_plugins/vis_type_metric/public/components/metric_vis_value.tsx b/src/plugins/vis_type_metric/public/components/metric_vis_value.tsx similarity index 100% rename from src/legacy/core_plugins/vis_type_metric/public/components/metric_vis_value.tsx rename to src/plugins/vis_type_metric/public/components/metric_vis_value.tsx diff --git a/src/plugins/vis_type_metric/public/index.scss b/src/plugins/vis_type_metric/public/index.scss new file mode 100644 index 0000000000000..638f9ac1ef93a --- /dev/null +++ b/src/plugins/vis_type_metric/public/index.scss @@ -0,0 +1,8 @@ +// Prefix all styles with "mtr" to avoid conflicts. +// Examples +// mtrChart +// mtrChart__legend +// mtrChart__legend--small +// mtrChart__legend-isLoading + +@import 'metric_vis'; diff --git a/src/plugins/vis_type_metric/public/index.ts b/src/plugins/vis_type_metric/public/index.ts new file mode 100644 index 0000000000000..3d3e1879a51d9 --- /dev/null +++ b/src/plugins/vis_type_metric/public/index.ts @@ -0,0 +1,25 @@ +/* + * 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 './index.scss'; +import { PluginInitializerContext } from 'kibana/public'; +import { MetricVisPlugin as Plugin } from './plugin'; + +export function plugin(initializerContext: PluginInitializerContext) { + return new Plugin(initializerContext); +} diff --git a/src/legacy/core_plugins/vis_type_metric/public/metric_vis_fn.test.ts b/src/plugins/vis_type_metric/public/metric_vis_fn.test.ts similarity index 90% rename from src/legacy/core_plugins/vis_type_metric/public/metric_vis_fn.test.ts rename to src/plugins/vis_type_metric/public/metric_vis_fn.test.ts index 4094cd4eff060..3ed8f8f79a83f 100644 --- a/src/legacy/core_plugins/vis_type_metric/public/metric_vis_fn.test.ts +++ b/src/plugins/vis_type_metric/public/metric_vis_fn.test.ts @@ -18,10 +18,7 @@ */ import { createMetricVisFn } from './metric_vis_fn'; -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { functionWrapper } from '../../../../plugins/expressions/common/expression_functions/specs/tests/utils'; - -jest.mock('ui/new_platform'); +import { functionWrapper } from '../../expressions/common/expression_functions/specs/tests/utils'; describe('interpreter/functions#metric', () => { const fn = functionWrapper(createMetricVisFn()); diff --git a/src/legacy/core_plugins/vis_type_metric/public/metric_vis_fn.ts b/src/plugins/vis_type_metric/public/metric_vis_fn.ts similarity index 96% rename from src/legacy/core_plugins/vis_type_metric/public/metric_vis_fn.ts rename to src/plugins/vis_type_metric/public/metric_vis_fn.ts index 03b412c6fff15..3d16fed0fa385 100644 --- a/src/legacy/core_plugins/vis_type_metric/public/metric_vis_fn.ts +++ b/src/plugins/vis_type_metric/public/metric_vis_fn.ts @@ -25,10 +25,9 @@ import { Range, Render, Style, -} from '../../../../plugins/expressions/public'; -import { ColorModes } from '../../vis_type_vislib/public'; +} from '../../expressions/public'; import { visType, DimensionsVisParam, VisParams } from './types'; -import { ColorSchemas, vislibColorMaps } from '../../../../plugins/charts/public'; +import { ColorSchemas, vislibColorMaps, ColorModes } from '../../charts/public'; export type Input = KibanaDatatable; diff --git a/src/legacy/core_plugins/vis_type_metric/public/metric_vis_type.test.ts b/src/plugins/vis_type_metric/public/metric_vis_type.test.ts similarity index 97% rename from src/legacy/core_plugins/vis_type_metric/public/metric_vis_type.test.ts rename to src/plugins/vis_type_metric/public/metric_vis_type.test.ts index 706693eff1007..636118c692aaa 100644 --- a/src/legacy/core_plugins/vis_type_metric/public/metric_vis_type.test.ts +++ b/src/plugins/vis_type_metric/public/metric_vis_type.test.ts @@ -20,8 +20,6 @@ import { createMetricVisTypeDefinition } from './metric_vis_type'; import { MetricVisComponent } from './components/metric_vis_component'; -jest.mock('ui/new_platform'); - describe('metric_vis - createMetricVisTypeDefinition', () => { it('has metric vis component set', () => { const def = createMetricVisTypeDefinition(); diff --git a/src/legacy/core_plugins/vis_type_metric/public/metric_vis_type.ts b/src/plugins/vis_type_metric/public/metric_vis_type.ts similarity index 92% rename from src/legacy/core_plugins/vis_type_metric/public/metric_vis_type.ts rename to src/plugins/vis_type_metric/public/metric_vis_type.ts index 3bbb8964122e5..b7e9213283bee 100644 --- a/src/legacy/core_plugins/vis_type_metric/public/metric_vis_type.ts +++ b/src/plugins/vis_type_metric/public/metric_vis_type.ts @@ -21,10 +21,9 @@ import { i18n } from '@kbn/i18n'; import { MetricVisComponent } from './components/metric_vis_component'; import { MetricVisOptions } from './components/metric_vis_options'; -import { ColorModes } from '../../vis_type_vislib/public'; -import { ColorSchemas, colorSchemas } from '../../../../plugins/charts/public'; -import { AggGroupNames } from '../../../../plugins/data/public'; -import { Schemas } from '../../../../plugins/vis_default_editor/public'; +import { ColorSchemas, colorSchemas, ColorModes } from '../../charts/public'; +import { AggGroupNames } from '../../data/public'; +import { Schemas } from '../../vis_default_editor/public'; export const createMetricVisTypeDefinition = () => ({ name: 'metric', diff --git a/src/plugins/vis_type_metric/public/plugin.ts b/src/plugins/vis_type_metric/public/plugin.ts new file mode 100644 index 0000000000000..a3951fa46c21c --- /dev/null +++ b/src/plugins/vis_type_metric/public/plugin.ts @@ -0,0 +1,62 @@ +/* + * 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 { PluginInitializerContext, CoreSetup, CoreStart, Plugin } from 'kibana/public'; +import { Plugin as ExpressionsPublicPlugin } from '../../expressions/public'; +import { VisualizationsSetup } from '../../visualizations/public'; + +import { createMetricVisFn } from './metric_vis_fn'; +import { createMetricVisTypeDefinition } from './metric_vis_type'; +import { ChartsPluginSetup } from '../../charts/public'; +import { DataPublicPluginStart } from '../../data/public'; +import { setFormatService } from './services'; +import { ConfigSchema } from '../config'; + +/** @internal */ +export interface MetricVisPluginSetupDependencies { + expressions: ReturnType; + visualizations: VisualizationsSetup; + charts: ChartsPluginSetup; +} + +/** @internal */ +export interface MetricVisPluginStartDependencies { + data: DataPublicPluginStart; +} + +/** @internal */ +export class MetricVisPlugin implements Plugin { + initializerContext: PluginInitializerContext; + + constructor(initializerContext: PluginInitializerContext) { + this.initializerContext = initializerContext; + } + + public setup( + core: CoreSetup, + { expressions, visualizations, charts }: MetricVisPluginSetupDependencies + ) { + expressions.registerFunction(createMetricVisFn); + visualizations.createReactVisualization(createMetricVisTypeDefinition()); + } + + public start(core: CoreStart, { data }: MetricVisPluginStartDependencies) { + setFormatService(data.fieldFormats); + } +} diff --git a/src/plugins/vis_type_metric/public/services.ts b/src/plugins/vis_type_metric/public/services.ts new file mode 100644 index 0000000000000..681afbaf0b268 --- /dev/null +++ b/src/plugins/vis_type_metric/public/services.ts @@ -0,0 +1,25 @@ +/* + * 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 { createGetterSetter } from '../../kibana_utils/common'; +import { DataPublicPluginStart } from '../../data/public'; + +export const [getFormatService, setFormatService] = createGetterSetter< + DataPublicPluginStart['fieldFormats'] +>('metric data.fieldFormats'); diff --git a/src/plugins/vis_type_metric/public/types.ts b/src/plugins/vis_type_metric/public/types.ts new file mode 100644 index 0000000000000..e1f2c7721a426 --- /dev/null +++ b/src/plugins/vis_type_metric/public/types.ts @@ -0,0 +1,57 @@ +/* + * 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 { Range } from '../../expressions/public'; +import { SchemaConfig } from '../../visualizations/public'; +import { ColorModes, Labels, Style, ColorSchemas } from '../../charts/public'; + +export const visType = 'metric'; + +export interface DimensionsVisParam { + metrics: SchemaConfig[]; + bucket?: SchemaConfig; +} + +export interface MetricVisParam { + percentageMode: boolean; + useRanges: boolean; + colorSchema: ColorSchemas; + metricColorMode: ColorModes; + colorsRange: Range[]; + labels: Labels; + invertColors: boolean; + style: Style; +} + +export interface VisParams { + addTooltip: boolean; + addLegend: boolean; + dimensions: DimensionsVisParam; + metric: MetricVisParam; + type: typeof visType; +} + +export interface MetricVisMetric { + value: any; + label: string; + color?: string; + bgColor?: string; + lightText: boolean; + rowIndex: number; +} diff --git a/src/plugins/vis_type_metric/server/index.ts b/src/plugins/vis_type_metric/server/index.ts new file mode 100644 index 0000000000000..ce550dc81dd85 --- /dev/null +++ b/src/plugins/vis_type_metric/server/index.ts @@ -0,0 +1,34 @@ +/* + * 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 { PluginConfigDescriptor } from 'kibana/server'; + +import { configSchema, ConfigSchema } from '../config'; + +export const config: PluginConfigDescriptor = { + schema: configSchema, + deprecations: ({ renameFromRoot }) => [ + renameFromRoot('metric_vis.enabled', 'vis_type_metric.enabled'), + ], +}; + +export const plugin = () => ({ + setup() {}, + start() {}, +}); diff --git a/src/legacy/core_plugins/vis_type_timelion/README.md b/src/plugins/vis_type_timelion/README.md similarity index 100% rename from src/legacy/core_plugins/vis_type_timelion/README.md rename to src/plugins/vis_type_timelion/README.md diff --git a/src/plugins/timelion/common/chain.peg b/src/plugins/vis_type_timelion/common/chain.peg similarity index 100% rename from src/plugins/timelion/common/chain.peg rename to src/plugins/vis_type_timelion/common/chain.peg diff --git a/src/plugins/timelion/common/lib/calculate_interval.test.ts b/src/plugins/vis_type_timelion/common/lib/calculate_interval.test.ts similarity index 100% rename from src/plugins/timelion/common/lib/calculate_interval.test.ts rename to src/plugins/vis_type_timelion/common/lib/calculate_interval.test.ts diff --git a/src/plugins/timelion/common/lib/calculate_interval.ts b/src/plugins/vis_type_timelion/common/lib/calculate_interval.ts similarity index 100% rename from src/plugins/timelion/common/lib/calculate_interval.ts rename to src/plugins/vis_type_timelion/common/lib/calculate_interval.ts diff --git a/src/plugins/timelion/common/lib/index.ts b/src/plugins/vis_type_timelion/common/lib/index.ts similarity index 100% rename from src/plugins/timelion/common/lib/index.ts rename to src/plugins/vis_type_timelion/common/lib/index.ts diff --git a/src/plugins/timelion/common/lib/to_milliseconds.ts b/src/plugins/vis_type_timelion/common/lib/to_milliseconds.ts similarity index 100% rename from src/plugins/timelion/common/lib/to_milliseconds.ts rename to src/plugins/vis_type_timelion/common/lib/to_milliseconds.ts diff --git a/src/plugins/timelion/common/types.ts b/src/plugins/vis_type_timelion/common/types.ts similarity index 100% rename from src/plugins/timelion/common/types.ts rename to src/plugins/vis_type_timelion/common/types.ts diff --git a/src/plugins/timelion/config.ts b/src/plugins/vis_type_timelion/config.ts similarity index 100% rename from src/plugins/timelion/config.ts rename to src/plugins/vis_type_timelion/config.ts diff --git a/src/plugins/vis_type_timelion/kibana.json b/src/plugins/vis_type_timelion/kibana.json new file mode 100644 index 0000000000000..85c282c51a2e7 --- /dev/null +++ b/src/plugins/vis_type_timelion/kibana.json @@ -0,0 +1,8 @@ +{ + "id": "visTypeTimelion", + "version": "8.0.0", + "kibanaVersion": "kibana", + "server": true, + "ui": true, + "requiredPlugins": ["visualizations", "data", "expressions"] +} diff --git a/src/legacy/core_plugins/vis_type_timelion/public/_generated_/chain.js b/src/plugins/vis_type_timelion/public/_generated_/chain.js similarity index 100% rename from src/legacy/core_plugins/vis_type_timelion/public/_generated_/chain.js rename to src/plugins/vis_type_timelion/public/_generated_/chain.js diff --git a/src/legacy/core_plugins/vis_type_timelion/public/_timelion_editor.scss b/src/plugins/vis_type_timelion/public/_timelion_editor.scss similarity index 100% rename from src/legacy/core_plugins/vis_type_timelion/public/_timelion_editor.scss rename to src/plugins/vis_type_timelion/public/_timelion_editor.scss diff --git a/src/legacy/core_plugins/vis_type_timelion/public/_timelion_vis.scss b/src/plugins/vis_type_timelion/public/_timelion_vis.scss similarity index 100% rename from src/legacy/core_plugins/vis_type_timelion/public/_timelion_vis.scss rename to src/plugins/vis_type_timelion/public/_timelion_vis.scss diff --git a/src/plugins/vis_type_timelion/public/components/_index.scss b/src/plugins/vis_type_timelion/public/components/_index.scss new file mode 100644 index 0000000000000..707c9dafebe2b --- /dev/null +++ b/src/plugins/vis_type_timelion/public/components/_index.scss @@ -0,0 +1,2 @@ +@import 'panel'; +@import 'timelion_expression_input'; diff --git a/src/legacy/core_plugins/vis_type_timelion/public/components/_panel.scss b/src/plugins/vis_type_timelion/public/components/_panel.scss similarity index 100% rename from src/legacy/core_plugins/vis_type_timelion/public/components/_panel.scss rename to src/plugins/vis_type_timelion/public/components/_panel.scss diff --git a/src/legacy/core_plugins/vis_type_timelion/public/components/_timelion_expression_input.scss b/src/plugins/vis_type_timelion/public/components/_timelion_expression_input.scss similarity index 100% rename from src/legacy/core_plugins/vis_type_timelion/public/components/_timelion_expression_input.scss rename to src/plugins/vis_type_timelion/public/components/_timelion_expression_input.scss diff --git a/src/legacy/core_plugins/vis_type_timelion/public/components/chart.tsx b/src/plugins/vis_type_timelion/public/components/chart.tsx similarity index 100% rename from src/legacy/core_plugins/vis_type_timelion/public/components/chart.tsx rename to src/plugins/vis_type_timelion/public/components/chart.tsx diff --git a/src/legacy/core_plugins/vis_type_timelion/public/components/index.ts b/src/plugins/vis_type_timelion/public/components/index.ts similarity index 100% rename from src/legacy/core_plugins/vis_type_timelion/public/components/index.ts rename to src/plugins/vis_type_timelion/public/components/index.ts diff --git a/src/legacy/core_plugins/vis_type_timelion/public/components/panel.tsx b/src/plugins/vis_type_timelion/public/components/panel.tsx similarity index 98% rename from src/legacy/core_plugins/vis_type_timelion/public/components/panel.tsx rename to src/plugins/vis_type_timelion/public/components/panel.tsx index 3b42fa7dfcbb8..8f796526e8520 100644 --- a/src/legacy/core_plugins/vis_type_timelion/public/components/panel.tsx +++ b/src/plugins/vis_type_timelion/public/components/panel.tsx @@ -22,9 +22,9 @@ import $ from 'jquery'; import moment from 'moment-timezone'; import { debounce, compact, get, each, cloneDeep, last, map } from 'lodash'; -import { useKibana } from '../../../../../plugins/kibana_react/public'; +import { useKibana } from '../../../kibana_react/public'; import '../flot'; -import { DEFAULT_TIME_FORMAT } from '../../../../../plugins/timelion/common/lib'; +import { DEFAULT_TIME_FORMAT } from '../../common/lib'; import { buildSeriesData, diff --git a/src/legacy/core_plugins/vis_type_timelion/public/components/timelion_expression_input.tsx b/src/plugins/vis_type_timelion/public/components/timelion_expression_input.tsx similarity index 96% rename from src/legacy/core_plugins/vis_type_timelion/public/components/timelion_expression_input.tsx rename to src/plugins/vis_type_timelion/public/components/timelion_expression_input.tsx index c317451b8201e..999409ef35063 100644 --- a/src/legacy/core_plugins/vis_type_timelion/public/components/timelion_expression_input.tsx +++ b/src/plugins/vis_type_timelion/public/components/timelion_expression_input.tsx @@ -22,13 +22,10 @@ import { EuiFormLabel } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; import { monaco } from '@kbn/ui-shared-deps/monaco'; -import { CodeEditor, useKibana } from '../../../../../plugins/kibana_react/public'; +import { CodeEditor, useKibana } from '../../../kibana_react/public'; import { suggest, getSuggestion } from './timelion_expression_input_helpers'; import { getArgValueSuggestions } from '../helpers/arg_value_suggestions'; -import { - ITimelionFunction, - TimelionFunctionArgs, -} from '../../../../../plugins/timelion/common/types'; +import { ITimelionFunction, TimelionFunctionArgs } from '../../common/types'; const LANGUAGE_ID = 'timelion_expression'; monaco.languages.register({ id: LANGUAGE_ID }); diff --git a/src/legacy/core_plugins/vis_type_timelion/public/components/timelion_expression_input_helpers.test.ts b/src/plugins/vis_type_timelion/public/components/timelion_expression_input_helpers.test.ts similarity index 99% rename from src/legacy/core_plugins/vis_type_timelion/public/components/timelion_expression_input_helpers.test.ts rename to src/plugins/vis_type_timelion/public/components/timelion_expression_input_helpers.test.ts index 2f99256e2a192..2ff6809d1c83d 100644 --- a/src/legacy/core_plugins/vis_type_timelion/public/components/timelion_expression_input_helpers.test.ts +++ b/src/plugins/vis_type_timelion/public/components/timelion_expression_input_helpers.test.ts @@ -22,7 +22,7 @@ import { getArgValueSuggestions } from '../helpers/arg_value_suggestions'; import { setIndexPatterns, setSavedObjectsClient } from '../helpers/plugin_services'; import { IndexPatternsContract } from 'src/plugins/data/public'; import { SavedObjectsClient } from 'kibana/public'; -import { ITimelionFunction } from '../../../../../plugins/timelion/common/types'; +import { ITimelionFunction } from '../../common/types'; describe('Timelion expression suggestions', () => { setIndexPatterns({} as IndexPatternsContract); diff --git a/src/legacy/core_plugins/vis_type_timelion/public/components/timelion_expression_input_helpers.ts b/src/plugins/vis_type_timelion/public/components/timelion_expression_input_helpers.ts similarity index 98% rename from src/legacy/core_plugins/vis_type_timelion/public/components/timelion_expression_input_helpers.ts rename to src/plugins/vis_type_timelion/public/components/timelion_expression_input_helpers.ts index 6f23c864419eb..04cb54306c90e 100644 --- a/src/legacy/core_plugins/vis_type_timelion/public/components/timelion_expression_input_helpers.ts +++ b/src/plugins/vis_type_timelion/public/components/timelion_expression_input_helpers.ts @@ -27,10 +27,7 @@ import { Parser } from 'pegjs'; import { parse } from '../_generated_/chain'; import { ArgValueSuggestions, FunctionArg, Location } from '../helpers/arg_value_suggestions'; -import { - ITimelionFunction, - TimelionFunctionArgs, -} from '../../../../../plugins/timelion/common/types'; +import { ITimelionFunction, TimelionFunctionArgs } from '../../common/types'; export enum SUGGESTION_TYPE { ARGUMENTS = 'arguments', diff --git a/src/legacy/core_plugins/vis_type_timelion/public/components/timelion_interval.tsx b/src/plugins/vis_type_timelion/public/components/timelion_interval.tsx similarity index 96% rename from src/legacy/core_plugins/vis_type_timelion/public/components/timelion_interval.tsx rename to src/plugins/vis_type_timelion/public/components/timelion_interval.tsx index 8a8e1b22fb78d..985ecaeaf3e5a 100644 --- a/src/legacy/core_plugins/vis_type_timelion/public/components/timelion_interval.tsx +++ b/src/plugins/vis_type_timelion/public/components/timelion_interval.tsx @@ -21,9 +21,9 @@ import React, { useMemo, useCallback } from 'react'; import { EuiFormRow, EuiComboBox, EuiComboBoxOptionOption } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import { search } from '../../../../../plugins/data/public'; +import { search } from '../../../data/public'; const { isValidEsInterval } = search.aggs; -import { useValidation } from '../../../../../plugins/vis_default_editor/public'; +import { useValidation } from '../../../vis_default_editor/public'; const intervalOptions = [ { diff --git a/src/legacy/core_plugins/vis_type_timelion/public/components/timelion_vis.tsx b/src/plugins/vis_type_timelion/public/components/timelion_vis.tsx similarity index 95% rename from src/legacy/core_plugins/vis_type_timelion/public/components/timelion_vis.tsx rename to src/plugins/vis_type_timelion/public/components/timelion_vis.tsx index 0fad0a164bf0b..4bb07fe74ee82 100644 --- a/src/legacy/core_plugins/vis_type_timelion/public/components/timelion_vis.tsx +++ b/src/plugins/vis_type_timelion/public/components/timelion_vis.tsx @@ -23,7 +23,7 @@ import { IUiSettingsClient } from 'kibana/public'; import { ChartComponent } from './chart'; import { VisParams } from '../timelion_vis_fn'; import { TimelionSuccessResponse } from '../helpers/timelion_request_handler'; -import { ExprVis } from '../../../../../plugins/visualizations/public'; +import { ExprVis } from '../../../visualizations/public'; export interface TimelionVisComponentProp { config: IUiSettingsClient; diff --git a/src/plugins/vis_type_timelion/public/flot.js b/src/plugins/vis_type_timelion/public/flot.js new file mode 100644 index 0000000000000..1ccb40c93a3d6 --- /dev/null +++ b/src/plugins/vis_type_timelion/public/flot.js @@ -0,0 +1,26 @@ +/* + * 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 './webpackShims/jquery.flot'; +import './webpackShims/jquery.flot.time'; +import './webpackShims/jquery.flot.symbol'; +import './webpackShims/jquery.flot.crosshair'; +import './webpackShims/jquery.flot.selection'; +import './webpackShims/jquery.flot.stack'; +import './webpackShims/jquery.flot.axislabels'; diff --git a/src/legacy/core_plugins/vis_type_timelion/public/helpers/arg_value_suggestions.ts b/src/plugins/vis_type_timelion/public/helpers/arg_value_suggestions.ts similarity index 97% rename from src/legacy/core_plugins/vis_type_timelion/public/helpers/arg_value_suggestions.ts rename to src/plugins/vis_type_timelion/public/helpers/arg_value_suggestions.ts index ea9532964d6fe..76c25b9b9e8de 100644 --- a/src/legacy/core_plugins/vis_type_timelion/public/helpers/arg_value_suggestions.ts +++ b/src/plugins/vis_type_timelion/public/helpers/arg_value_suggestions.ts @@ -19,11 +19,8 @@ import { get } from 'lodash'; import { getIndexPatterns, getSavedObjectsClient } from './plugin_services'; -import { TimelionFunctionArgs } from '../../../../../plugins/timelion/common/types'; -import { - indexPatterns as indexPatternsUtils, - IndexPatternAttributes, -} from '../../../../../plugins/data/public'; +import { TimelionFunctionArgs } from '../../common/types'; +import { indexPatterns as indexPatternsUtils, IndexPatternAttributes } from '../../../data/public'; export interface Location { min: number; diff --git a/src/legacy/core_plugins/vis_type_timelion/public/helpers/get_timezone.ts b/src/plugins/vis_type_timelion/public/helpers/get_timezone.ts similarity index 100% rename from src/legacy/core_plugins/vis_type_timelion/public/helpers/get_timezone.ts rename to src/plugins/vis_type_timelion/public/helpers/get_timezone.ts diff --git a/src/legacy/core_plugins/vis_type_timelion/public/helpers/panel_utils.ts b/src/plugins/vis_type_timelion/public/helpers/panel_utils.ts similarity index 98% rename from src/legacy/core_plugins/vis_type_timelion/public/helpers/panel_utils.ts rename to src/plugins/vis_type_timelion/public/helpers/panel_utils.ts index f932e5ee4b2f4..db29d9112be8e 100644 --- a/src/legacy/core_plugins/vis_type_timelion/public/helpers/panel_utils.ts +++ b/src/plugins/vis_type_timelion/public/helpers/panel_utils.ts @@ -23,7 +23,7 @@ import moment, { Moment } from 'moment-timezone'; import { TimefilterContract } from 'src/plugins/data/public'; import { IUiSettingsClient } from 'kibana/public'; -import { calculateInterval } from '../../../../../plugins/timelion/common/lib'; +import { calculateInterval } from '../../common/lib'; import { xaxisFormatterProvider } from './xaxis_formatter'; import { Series } from './timelion_request_handler'; diff --git a/src/legacy/core_plugins/vis_type_timelion/public/helpers/plugin_services.ts b/src/plugins/vis_type_timelion/public/helpers/plugin_services.ts similarity index 93% rename from src/legacy/core_plugins/vis_type_timelion/public/helpers/plugin_services.ts rename to src/plugins/vis_type_timelion/public/helpers/plugin_services.ts index 5ba4ee5e47983..b055626934eea 100644 --- a/src/legacy/core_plugins/vis_type_timelion/public/helpers/plugin_services.ts +++ b/src/plugins/vis_type_timelion/public/helpers/plugin_services.ts @@ -19,7 +19,7 @@ import { IndexPatternsContract } from 'src/plugins/data/public'; import { SavedObjectsClientContract } from 'kibana/public'; -import { createGetterSetter } from '../../../../../plugins/kibana_utils/public'; +import { createGetterSetter } from '../../../kibana_utils/public'; export const [getIndexPatterns, setIndexPatterns] = createGetterSetter( 'IndexPatterns' diff --git a/src/legacy/core_plugins/vis_type_timelion/public/helpers/tick_formatters.test.ts b/src/plugins/vis_type_timelion/public/helpers/tick_formatters.test.ts similarity index 100% rename from src/legacy/core_plugins/vis_type_timelion/public/helpers/tick_formatters.test.ts rename to src/plugins/vis_type_timelion/public/helpers/tick_formatters.test.ts diff --git a/src/legacy/core_plugins/vis_type_timelion/public/helpers/tick_formatters.ts b/src/plugins/vis_type_timelion/public/helpers/tick_formatters.ts similarity index 100% rename from src/legacy/core_plugins/vis_type_timelion/public/helpers/tick_formatters.ts rename to src/plugins/vis_type_timelion/public/helpers/tick_formatters.ts diff --git a/src/legacy/core_plugins/vis_type_timelion/public/helpers/tick_generator.test.ts b/src/plugins/vis_type_timelion/public/helpers/tick_generator.test.ts similarity index 100% rename from src/legacy/core_plugins/vis_type_timelion/public/helpers/tick_generator.test.ts rename to src/plugins/vis_type_timelion/public/helpers/tick_generator.test.ts diff --git a/src/legacy/core_plugins/vis_type_timelion/public/helpers/tick_generator.ts b/src/plugins/vis_type_timelion/public/helpers/tick_generator.ts similarity index 100% rename from src/legacy/core_plugins/vis_type_timelion/public/helpers/tick_generator.ts rename to src/plugins/vis_type_timelion/public/helpers/tick_generator.ts diff --git a/src/legacy/core_plugins/vis_type_timelion/public/helpers/timelion_request_handler.ts b/src/plugins/vis_type_timelion/public/helpers/timelion_request_handler.ts similarity index 95% rename from src/legacy/core_plugins/vis_type_timelion/public/helpers/timelion_request_handler.ts rename to src/plugins/vis_type_timelion/public/helpers/timelion_request_handler.ts index 61e31420f73ba..a654f7935af5f 100644 --- a/src/legacy/core_plugins/vis_type_timelion/public/helpers/timelion_request_handler.ts +++ b/src/plugins/vis_type_timelion/public/helpers/timelion_request_handler.ts @@ -19,8 +19,8 @@ import { i18n } from '@kbn/i18n'; import { KIBANA_CONTEXT_NAME } from 'src/plugins/expressions/public'; -import { VisParams } from '../../../../../plugins/visualizations/public'; -import { TimeRange, Filter, esQuery, Query } from '../../../../../plugins/data/public'; +import { VisParams } from '../../../visualizations/public'; +import { TimeRange, Filter, esQuery, Query } from '../../../data/public'; import { TimelionVisDependencies } from '../plugin'; import { getTimezone } from './get_timezone'; diff --git a/src/legacy/core_plugins/vis_type_timelion/public/helpers/xaxis_formatter.ts b/src/plugins/vis_type_timelion/public/helpers/xaxis_formatter.ts similarity index 100% rename from src/legacy/core_plugins/vis_type_timelion/public/helpers/xaxis_formatter.ts rename to src/plugins/vis_type_timelion/public/helpers/xaxis_formatter.ts diff --git a/src/plugins/vis_type_timelion/public/index.scss b/src/plugins/vis_type_timelion/public/index.scss new file mode 100644 index 0000000000000..00e9a88520961 --- /dev/null +++ b/src/plugins/vis_type_timelion/public/index.scss @@ -0,0 +1,3 @@ +@import './timelion_vis'; +@import './timelion_editor'; +@import './components/index'; diff --git a/src/plugins/vis_type_timelion/public/index.ts b/src/plugins/vis_type_timelion/public/index.ts new file mode 100644 index 0000000000000..0aa5f3a810033 --- /dev/null +++ b/src/plugins/vis_type_timelion/public/index.ts @@ -0,0 +1,29 @@ +/* + * 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 { PluginInitializerContext } from 'kibana/public'; +import { TimelionVisPlugin as Plugin } from './plugin'; + +export function plugin(initializerContext: PluginInitializerContext) { + return new Plugin(initializerContext); +} + +export { getTimezone } from './helpers/get_timezone'; + +export { VisTypeTimelionPluginStart } from './plugin'; diff --git a/src/plugins/vis_type_timelion/public/plugin.ts b/src/plugins/vis_type_timelion/public/plugin.ts new file mode 100644 index 0000000000000..060fec04deb3f --- /dev/null +++ b/src/plugins/vis_type_timelion/public/plugin.ts @@ -0,0 +1,105 @@ +/* + * 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 { + CoreSetup, + CoreStart, + Plugin, + PluginInitializerContext, + IUiSettingsClient, + HttpSetup, +} from 'kibana/public'; +import { Plugin as ExpressionsPlugin } from 'src/plugins/expressions/public'; +import { + DataPublicPluginSetup, + DataPublicPluginStart, + TimefilterContract, +} from 'src/plugins/data/public'; + +import { VisualizationsSetup } from '../../visualizations/public'; + +import { getTimelionVisualizationConfig } from './timelion_vis_fn'; +import { getTimelionVisDefinition } from './timelion_vis_type'; +import { setIndexPatterns, setSavedObjectsClient } from './helpers/plugin_services'; +import { ConfigSchema } from '../config'; + +import './index.scss'; +import { getArgValueSuggestions } from './helpers/arg_value_suggestions'; + +/** @internal */ +export interface TimelionVisDependencies extends Partial { + uiSettings: IUiSettingsClient; + http: HttpSetup; + timefilter: TimefilterContract; +} + +/** @internal */ +export interface TimelionVisSetupDependencies { + expressions: ReturnType; + visualizations: VisualizationsSetup; + data: DataPublicPluginSetup; +} + +/** @internal */ +export interface TimelionVisStartDependencies { + data: DataPublicPluginStart; +} + +/** @public */ +export interface VisTypeTimelionPluginStart { + getArgValueSuggestions: typeof getArgValueSuggestions; +} + +/** @internal */ +export class TimelionVisPlugin + implements + Plugin< + void, + VisTypeTimelionPluginStart, + TimelionVisSetupDependencies, + TimelionVisStartDependencies + > { + constructor(public initializerContext: PluginInitializerContext) {} + + public setup( + core: CoreSetup, + { expressions, visualizations, data }: TimelionVisSetupDependencies + ) { + const dependencies: TimelionVisDependencies = { + uiSettings: core.uiSettings, + http: core.http, + timefilter: data.query.timefilter.timefilter, + }; + + expressions.registerFunction(() => getTimelionVisualizationConfig(dependencies)); + visualizations.createReactVisualization(getTimelionVisDefinition(dependencies)); + } + + public start(core: CoreStart, plugins: TimelionVisStartDependencies) { + setIndexPatterns(plugins.data.indexPatterns); + setSavedObjectsClient(core.savedObjects.client); + if (this.initializerContext.config.get().ui.enabled === false) { + core.chrome.navLinks.update('timelion', { hidden: true }); + } + + return { + getArgValueSuggestions, + }; + } +} diff --git a/src/legacy/core_plugins/vis_type_timelion/public/timelion_options.tsx b/src/plugins/vis_type_timelion/public/timelion_options.tsx similarity index 95% rename from src/legacy/core_plugins/vis_type_timelion/public/timelion_options.tsx rename to src/plugins/vis_type_timelion/public/timelion_options.tsx index afffcf7ccaf7a..dfe017d3a273f 100644 --- a/src/legacy/core_plugins/vis_type_timelion/public/timelion_options.tsx +++ b/src/plugins/vis_type_timelion/public/timelion_options.tsx @@ -24,7 +24,9 @@ import { VisOptionsProps } from 'src/plugins/vis_default_editor/public'; import { VisParams } from './timelion_vis_fn'; import { TimelionInterval, TimelionExpressionInput } from './components'; -function TimelionOptions({ stateParams, setValue, setValidity }: VisOptionsProps) { +export type TimelionOptionsProps = VisOptionsProps; + +function TimelionOptions({ stateParams, setValue, setValidity }: TimelionOptionsProps) { const setInterval = useCallback((value: VisParams['interval']) => setValue('interval', value), [ setValue, ]); diff --git a/src/legacy/core_plugins/vis_type_timelion/public/timelion_vis_fn.ts b/src/plugins/vis_type_timelion/public/timelion_vis_fn.ts similarity index 100% rename from src/legacy/core_plugins/vis_type_timelion/public/timelion_vis_fn.ts rename to src/plugins/vis_type_timelion/public/timelion_vis_fn.ts diff --git a/src/legacy/core_plugins/vis_type_timelion/public/timelion_vis_type.tsx b/src/plugins/vis_type_timelion/public/timelion_vis_type.tsx similarity index 84% rename from src/legacy/core_plugins/vis_type_timelion/public/timelion_vis_type.tsx rename to src/plugins/vis_type_timelion/public/timelion_vis_type.tsx index 5be77b3e51a6a..52addb3c2d9d2 100644 --- a/src/legacy/core_plugins/vis_type_timelion/public/timelion_vis_type.tsx +++ b/src/plugins/vis_type_timelion/public/timelion_vis_type.tsx @@ -20,11 +20,11 @@ import React from 'react'; import { i18n } from '@kbn/i18n'; -import { KibanaContextProvider } from '../../../../plugins/kibana_react/public'; -import { DefaultEditorSize } from '../../../../plugins/vis_default_editor/public'; +import { KibanaContextProvider } from '../../kibana_react/public'; +import { DefaultEditorSize } from '../../vis_default_editor/public'; import { getTimelionRequestHandler } from './helpers/timelion_request_handler'; import { TimelionVisComponent, TimelionVisComponentProp } from './components'; -import { TimelionOptions } from './timelion_options'; +import { TimelionOptions, TimelionOptionsProps } from './timelion_options'; import { TimelionVisDependencies } from './plugin'; export const TIMELION_VIS_NAME = 'timelion'; @@ -53,7 +53,11 @@ export function getTimelionVisDefinition(dependencies: TimelionVisDependencies) ), }, editorConfig: { - optionsTemplate: TimelionOptions, + optionsTemplate: (props: TimelionOptionsProps) => ( + + + + ), defaultSize: DefaultEditorSize.MEDIUM, }, requestHandler: timelionRequestHandler, diff --git a/src/legacy/core_plugins/vis_type_timelion/public/webpackShims/jquery.flot.axislabels.js b/src/plugins/vis_type_timelion/public/webpackShims/jquery.flot.axislabels.js similarity index 100% rename from src/legacy/core_plugins/vis_type_timelion/public/webpackShims/jquery.flot.axislabels.js rename to src/plugins/vis_type_timelion/public/webpackShims/jquery.flot.axislabels.js diff --git a/src/legacy/core_plugins/vis_type_timelion/public/webpackShims/jquery.flot.crosshair.js b/src/plugins/vis_type_timelion/public/webpackShims/jquery.flot.crosshair.js similarity index 100% rename from src/legacy/core_plugins/vis_type_timelion/public/webpackShims/jquery.flot.crosshair.js rename to src/plugins/vis_type_timelion/public/webpackShims/jquery.flot.crosshair.js diff --git a/src/legacy/core_plugins/vis_type_timelion/public/webpackShims/jquery.flot.js b/src/plugins/vis_type_timelion/public/webpackShims/jquery.flot.js similarity index 100% rename from src/legacy/core_plugins/vis_type_timelion/public/webpackShims/jquery.flot.js rename to src/plugins/vis_type_timelion/public/webpackShims/jquery.flot.js diff --git a/src/legacy/core_plugins/vis_type_timelion/public/webpackShims/jquery.flot.selection.js b/src/plugins/vis_type_timelion/public/webpackShims/jquery.flot.selection.js similarity index 100% rename from src/legacy/core_plugins/vis_type_timelion/public/webpackShims/jquery.flot.selection.js rename to src/plugins/vis_type_timelion/public/webpackShims/jquery.flot.selection.js diff --git a/src/legacy/core_plugins/vis_type_timelion/public/webpackShims/jquery.flot.stack.js b/src/plugins/vis_type_timelion/public/webpackShims/jquery.flot.stack.js similarity index 100% rename from src/legacy/core_plugins/vis_type_timelion/public/webpackShims/jquery.flot.stack.js rename to src/plugins/vis_type_timelion/public/webpackShims/jquery.flot.stack.js diff --git a/src/legacy/core_plugins/vis_type_timelion/public/webpackShims/jquery.flot.symbol.js b/src/plugins/vis_type_timelion/public/webpackShims/jquery.flot.symbol.js similarity index 100% rename from src/legacy/core_plugins/vis_type_timelion/public/webpackShims/jquery.flot.symbol.js rename to src/plugins/vis_type_timelion/public/webpackShims/jquery.flot.symbol.js diff --git a/src/legacy/core_plugins/vis_type_timelion/public/webpackShims/jquery.flot.time.js b/src/plugins/vis_type_timelion/public/webpackShims/jquery.flot.time.js similarity index 100% rename from src/legacy/core_plugins/vis_type_timelion/public/webpackShims/jquery.flot.time.js rename to src/plugins/vis_type_timelion/public/webpackShims/jquery.flot.time.js diff --git a/src/plugins/timelion/server/fit_functions/average.js b/src/plugins/vis_type_timelion/server/fit_functions/average.js similarity index 100% rename from src/plugins/timelion/server/fit_functions/average.js rename to src/plugins/vis_type_timelion/server/fit_functions/average.js diff --git a/src/plugins/timelion/server/fit_functions/average.test.js b/src/plugins/vis_type_timelion/server/fit_functions/average.test.js similarity index 100% rename from src/plugins/timelion/server/fit_functions/average.test.js rename to src/plugins/vis_type_timelion/server/fit_functions/average.test.js diff --git a/src/plugins/timelion/server/fit_functions/carry.js b/src/plugins/vis_type_timelion/server/fit_functions/carry.js similarity index 100% rename from src/plugins/timelion/server/fit_functions/carry.js rename to src/plugins/vis_type_timelion/server/fit_functions/carry.js diff --git a/src/plugins/timelion/server/fit_functions/carry.test.js b/src/plugins/vis_type_timelion/server/fit_functions/carry.test.js similarity index 100% rename from src/plugins/timelion/server/fit_functions/carry.test.js rename to src/plugins/vis_type_timelion/server/fit_functions/carry.test.js diff --git a/src/plugins/timelion/server/fit_functions/nearest.js b/src/plugins/vis_type_timelion/server/fit_functions/nearest.js similarity index 100% rename from src/plugins/timelion/server/fit_functions/nearest.js rename to src/plugins/vis_type_timelion/server/fit_functions/nearest.js diff --git a/src/plugins/timelion/server/fit_functions/none.js b/src/plugins/vis_type_timelion/server/fit_functions/none.js similarity index 100% rename from src/plugins/timelion/server/fit_functions/none.js rename to src/plugins/vis_type_timelion/server/fit_functions/none.js diff --git a/src/plugins/timelion/server/fit_functions/scale.js b/src/plugins/vis_type_timelion/server/fit_functions/scale.js similarity index 100% rename from src/plugins/timelion/server/fit_functions/scale.js rename to src/plugins/vis_type_timelion/server/fit_functions/scale.js diff --git a/src/plugins/timelion/server/handlers/chain_runner.js b/src/plugins/vis_type_timelion/server/handlers/chain_runner.js similarity index 100% rename from src/plugins/timelion/server/handlers/chain_runner.js rename to src/plugins/vis_type_timelion/server/handlers/chain_runner.js diff --git a/src/plugins/timelion/server/handlers/lib/arg_type.js b/src/plugins/vis_type_timelion/server/handlers/lib/arg_type.js similarity index 100% rename from src/plugins/timelion/server/handlers/lib/arg_type.js rename to src/plugins/vis_type_timelion/server/handlers/lib/arg_type.js diff --git a/src/plugins/timelion/server/handlers/lib/index_arguments.js b/src/plugins/vis_type_timelion/server/handlers/lib/index_arguments.js similarity index 100% rename from src/plugins/timelion/server/handlers/lib/index_arguments.js rename to src/plugins/vis_type_timelion/server/handlers/lib/index_arguments.js diff --git a/src/plugins/timelion/server/handlers/lib/parse_sheet.js b/src/plugins/vis_type_timelion/server/handlers/lib/parse_sheet.js similarity index 100% rename from src/plugins/timelion/server/handlers/lib/parse_sheet.js rename to src/plugins/vis_type_timelion/server/handlers/lib/parse_sheet.js diff --git a/src/plugins/timelion/server/handlers/lib/parse_sheet.test.js b/src/plugins/vis_type_timelion/server/handlers/lib/parse_sheet.test.js similarity index 100% rename from src/plugins/timelion/server/handlers/lib/parse_sheet.test.js rename to src/plugins/vis_type_timelion/server/handlers/lib/parse_sheet.test.js diff --git a/src/plugins/timelion/server/handlers/lib/preprocess_chain.js b/src/plugins/vis_type_timelion/server/handlers/lib/preprocess_chain.js similarity index 100% rename from src/plugins/timelion/server/handlers/lib/preprocess_chain.js rename to src/plugins/vis_type_timelion/server/handlers/lib/preprocess_chain.js diff --git a/src/plugins/timelion/server/handlers/lib/reposition_arguments.js b/src/plugins/vis_type_timelion/server/handlers/lib/reposition_arguments.js similarity index 100% rename from src/plugins/timelion/server/handlers/lib/reposition_arguments.js rename to src/plugins/vis_type_timelion/server/handlers/lib/reposition_arguments.js diff --git a/src/plugins/timelion/server/handlers/lib/tl_config.js b/src/plugins/vis_type_timelion/server/handlers/lib/tl_config.js similarity index 100% rename from src/plugins/timelion/server/handlers/lib/tl_config.js rename to src/plugins/vis_type_timelion/server/handlers/lib/tl_config.js diff --git a/src/plugins/timelion/server/handlers/lib/validate_arg.js b/src/plugins/vis_type_timelion/server/handlers/lib/validate_arg.js similarity index 100% rename from src/plugins/timelion/server/handlers/lib/validate_arg.js rename to src/plugins/vis_type_timelion/server/handlers/lib/validate_arg.js diff --git a/src/plugins/timelion/server/handlers/lib/validate_time.js b/src/plugins/vis_type_timelion/server/handlers/lib/validate_time.js similarity index 100% rename from src/plugins/timelion/server/handlers/lib/validate_time.js rename to src/plugins/vis_type_timelion/server/handlers/lib/validate_time.js diff --git a/src/plugins/vis_type_timelion/server/index.ts b/src/plugins/vis_type_timelion/server/index.ts new file mode 100644 index 0000000000000..b40ab2af2b0d7 --- /dev/null +++ b/src/plugins/vis_type_timelion/server/index.ts @@ -0,0 +1,39 @@ +/* + * 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 { PluginConfigDescriptor, PluginInitializerContext } from '../../../../src/core/server'; +import { configSchema, ConfigSchema } from '../config'; +import { Plugin } from './plugin'; + +export { PluginSetupContract } from './plugin'; + +export const config: PluginConfigDescriptor = { + schema: configSchema, + exposeToBrowser: { + ui: true, + }, + deprecations: ({ renameFromRoot }) => [ + renameFromRoot('timelion_vis.enabled', 'vis_type_timelion.enabled'), + renameFromRoot('timelion.enabled', 'vis_type_timelion.enabled'), + renameFromRoot('timelion.graphiteUrls', 'vis_type_timelion.graphiteUrls'), + renameFromRoot('timelion.ui.enabled', 'vis_type_timelion.ui.enabled'), + ], +}; +export const plugin = (initializerContext: PluginInitializerContext) => + new Plugin(initializerContext); diff --git a/src/plugins/timelion/server/lib/alter.js b/src/plugins/vis_type_timelion/server/lib/alter.js similarity index 100% rename from src/plugins/timelion/server/lib/alter.js rename to src/plugins/vis_type_timelion/server/lib/alter.js diff --git a/src/plugins/timelion/server/lib/as_sorted.js b/src/plugins/vis_type_timelion/server/lib/as_sorted.js similarity index 100% rename from src/plugins/timelion/server/lib/as_sorted.js rename to src/plugins/vis_type_timelion/server/lib/as_sorted.js diff --git a/src/plugins/timelion/server/lib/build_target.js b/src/plugins/vis_type_timelion/server/lib/build_target.js similarity index 100% rename from src/plugins/timelion/server/lib/build_target.js rename to src/plugins/vis_type_timelion/server/lib/build_target.js diff --git a/src/plugins/timelion/server/lib/classes/chainable.js b/src/plugins/vis_type_timelion/server/lib/classes/chainable.js similarity index 100% rename from src/plugins/timelion/server/lib/classes/chainable.js rename to src/plugins/vis_type_timelion/server/lib/classes/chainable.js diff --git a/src/plugins/timelion/server/lib/classes/datasource.js b/src/plugins/vis_type_timelion/server/lib/classes/datasource.js similarity index 100% rename from src/plugins/timelion/server/lib/classes/datasource.js rename to src/plugins/vis_type_timelion/server/lib/classes/datasource.js diff --git a/src/plugins/timelion/server/lib/classes/timelion_function.d.ts b/src/plugins/vis_type_timelion/server/lib/classes/timelion_function.d.ts similarity index 100% rename from src/plugins/timelion/server/lib/classes/timelion_function.d.ts rename to src/plugins/vis_type_timelion/server/lib/classes/timelion_function.d.ts diff --git a/src/plugins/timelion/server/lib/classes/timelion_function.js b/src/plugins/vis_type_timelion/server/lib/classes/timelion_function.js similarity index 100% rename from src/plugins/timelion/server/lib/classes/timelion_function.js rename to src/plugins/vis_type_timelion/server/lib/classes/timelion_function.js diff --git a/src/plugins/timelion/server/lib/config_manager.ts b/src/plugins/vis_type_timelion/server/lib/config_manager.ts similarity index 100% rename from src/plugins/timelion/server/lib/config_manager.ts rename to src/plugins/vis_type_timelion/server/lib/config_manager.ts diff --git a/src/plugins/timelion/server/lib/functions_md.js b/src/plugins/vis_type_timelion/server/lib/functions_md.js similarity index 100% rename from src/plugins/timelion/server/lib/functions_md.js rename to src/plugins/vis_type_timelion/server/lib/functions_md.js diff --git a/src/plugins/timelion/server/lib/get_namespaced_settings.js b/src/plugins/vis_type_timelion/server/lib/get_namespaced_settings.js similarity index 100% rename from src/plugins/timelion/server/lib/get_namespaced_settings.js rename to src/plugins/vis_type_timelion/server/lib/get_namespaced_settings.js diff --git a/src/plugins/timelion/server/lib/load_functions.d.ts b/src/plugins/vis_type_timelion/server/lib/load_functions.d.ts similarity index 100% rename from src/plugins/timelion/server/lib/load_functions.d.ts rename to src/plugins/vis_type_timelion/server/lib/load_functions.d.ts diff --git a/src/plugins/timelion/server/lib/load_functions.js b/src/plugins/vis_type_timelion/server/lib/load_functions.js similarity index 100% rename from src/plugins/timelion/server/lib/load_functions.js rename to src/plugins/vis_type_timelion/server/lib/load_functions.js diff --git a/src/plugins/timelion/server/lib/load_functions.test.js b/src/plugins/vis_type_timelion/server/lib/load_functions.test.js similarity index 94% rename from src/plugins/timelion/server/lib/load_functions.test.js rename to src/plugins/vis_type_timelion/server/lib/load_functions.test.js index ebe1a04532e05..b4f83611a7773 100644 --- a/src/plugins/timelion/server/lib/load_functions.test.js +++ b/src/plugins/vis_type_timelion/server/lib/load_functions.test.js @@ -17,7 +17,7 @@ * under the License. */ -const fn = require(`src/plugins/timelion/server/lib/load_functions`); +const fn = require(`src/plugins/vis_type_timelion/server/lib/load_functions`); const expect = require('chai').expect; diff --git a/src/plugins/timelion/server/lib/offset_time.js b/src/plugins/vis_type_timelion/server/lib/offset_time.js similarity index 100% rename from src/plugins/timelion/server/lib/offset_time.js rename to src/plugins/vis_type_timelion/server/lib/offset_time.js diff --git a/src/plugins/timelion/server/lib/offset_time.test.js b/src/plugins/vis_type_timelion/server/lib/offset_time.test.js similarity index 100% rename from src/plugins/timelion/server/lib/offset_time.test.js rename to src/plugins/vis_type_timelion/server/lib/offset_time.test.js diff --git a/src/plugins/timelion/server/lib/process_function_definition.js b/src/plugins/vis_type_timelion/server/lib/process_function_definition.js similarity index 100% rename from src/plugins/timelion/server/lib/process_function_definition.js rename to src/plugins/vis_type_timelion/server/lib/process_function_definition.js diff --git a/src/plugins/timelion/server/lib/reduce.js b/src/plugins/vis_type_timelion/server/lib/reduce.js similarity index 100% rename from src/plugins/timelion/server/lib/reduce.js rename to src/plugins/vis_type_timelion/server/lib/reduce.js diff --git a/src/plugins/timelion/server/lib/split_interval.js b/src/plugins/vis_type_timelion/server/lib/split_interval.js similarity index 100% rename from src/plugins/timelion/server/lib/split_interval.js rename to src/plugins/vis_type_timelion/server/lib/split_interval.js diff --git a/src/plugins/timelion/server/lib/unzip_pairs.js b/src/plugins/vis_type_timelion/server/lib/unzip_pairs.js similarity index 100% rename from src/plugins/timelion/server/lib/unzip_pairs.js rename to src/plugins/vis_type_timelion/server/lib/unzip_pairs.js diff --git a/src/plugins/timelion/server/plugin.ts b/src/plugins/vis_type_timelion/server/plugin.ts similarity index 100% rename from src/plugins/timelion/server/plugin.ts rename to src/plugins/vis_type_timelion/server/plugin.ts diff --git a/src/plugins/timelion/server/routes/functions.ts b/src/plugins/vis_type_timelion/server/routes/functions.ts similarity index 100% rename from src/plugins/timelion/server/routes/functions.ts rename to src/plugins/vis_type_timelion/server/routes/functions.ts diff --git a/src/plugins/timelion/server/routes/run.ts b/src/plugins/vis_type_timelion/server/routes/run.ts similarity index 100% rename from src/plugins/timelion/server/routes/run.ts rename to src/plugins/vis_type_timelion/server/routes/run.ts diff --git a/src/plugins/timelion/server/routes/validate_es.ts b/src/plugins/vis_type_timelion/server/routes/validate_es.ts similarity index 100% rename from src/plugins/timelion/server/routes/validate_es.ts rename to src/plugins/vis_type_timelion/server/routes/validate_es.ts diff --git a/src/plugins/timelion/server/series_functions/abs.js b/src/plugins/vis_type_timelion/server/series_functions/abs.js similarity index 100% rename from src/plugins/timelion/server/series_functions/abs.js rename to src/plugins/vis_type_timelion/server/series_functions/abs.js diff --git a/src/plugins/timelion/server/series_functions/abs.test.js b/src/plugins/vis_type_timelion/server/series_functions/abs.test.js similarity index 100% rename from src/plugins/timelion/server/series_functions/abs.test.js rename to src/plugins/vis_type_timelion/server/series_functions/abs.test.js diff --git a/src/plugins/timelion/server/series_functions/aggregate/aggregate.test.js b/src/plugins/vis_type_timelion/server/series_functions/aggregate/aggregate.test.js similarity index 100% rename from src/plugins/timelion/server/series_functions/aggregate/aggregate.test.js rename to src/plugins/vis_type_timelion/server/series_functions/aggregate/aggregate.test.js diff --git a/src/plugins/timelion/server/series_functions/aggregate/avg.js b/src/plugins/vis_type_timelion/server/series_functions/aggregate/avg.js similarity index 100% rename from src/plugins/timelion/server/series_functions/aggregate/avg.js rename to src/plugins/vis_type_timelion/server/series_functions/aggregate/avg.js diff --git a/src/plugins/timelion/server/series_functions/aggregate/cardinality.js b/src/plugins/vis_type_timelion/server/series_functions/aggregate/cardinality.js similarity index 100% rename from src/plugins/timelion/server/series_functions/aggregate/cardinality.js rename to src/plugins/vis_type_timelion/server/series_functions/aggregate/cardinality.js diff --git a/src/plugins/timelion/server/series_functions/aggregate/first.js b/src/plugins/vis_type_timelion/server/series_functions/aggregate/first.js similarity index 100% rename from src/plugins/timelion/server/series_functions/aggregate/first.js rename to src/plugins/vis_type_timelion/server/series_functions/aggregate/first.js diff --git a/src/plugins/timelion/server/series_functions/aggregate/index.js b/src/plugins/vis_type_timelion/server/series_functions/aggregate/index.js similarity index 100% rename from src/plugins/timelion/server/series_functions/aggregate/index.js rename to src/plugins/vis_type_timelion/server/series_functions/aggregate/index.js diff --git a/src/plugins/timelion/server/series_functions/aggregate/last.js b/src/plugins/vis_type_timelion/server/series_functions/aggregate/last.js similarity index 100% rename from src/plugins/timelion/server/series_functions/aggregate/last.js rename to src/plugins/vis_type_timelion/server/series_functions/aggregate/last.js diff --git a/src/plugins/timelion/server/series_functions/aggregate/max.js b/src/plugins/vis_type_timelion/server/series_functions/aggregate/max.js similarity index 100% rename from src/plugins/timelion/server/series_functions/aggregate/max.js rename to src/plugins/vis_type_timelion/server/series_functions/aggregate/max.js diff --git a/src/plugins/timelion/server/series_functions/aggregate/min.js b/src/plugins/vis_type_timelion/server/series_functions/aggregate/min.js similarity index 100% rename from src/plugins/timelion/server/series_functions/aggregate/min.js rename to src/plugins/vis_type_timelion/server/series_functions/aggregate/min.js diff --git a/src/plugins/timelion/server/series_functions/aggregate/sum.js b/src/plugins/vis_type_timelion/server/series_functions/aggregate/sum.js similarity index 100% rename from src/plugins/timelion/server/series_functions/aggregate/sum.js rename to src/plugins/vis_type_timelion/server/series_functions/aggregate/sum.js diff --git a/src/plugins/timelion/server/series_functions/bars.js b/src/plugins/vis_type_timelion/server/series_functions/bars.js similarity index 100% rename from src/plugins/timelion/server/series_functions/bars.js rename to src/plugins/vis_type_timelion/server/series_functions/bars.js diff --git a/src/plugins/timelion/server/series_functions/bars.test.js b/src/plugins/vis_type_timelion/server/series_functions/bars.test.js similarity index 100% rename from src/plugins/timelion/server/series_functions/bars.test.js rename to src/plugins/vis_type_timelion/server/series_functions/bars.test.js diff --git a/src/plugins/timelion/server/series_functions/color.js b/src/plugins/vis_type_timelion/server/series_functions/color.js similarity index 100% rename from src/plugins/timelion/server/series_functions/color.js rename to src/plugins/vis_type_timelion/server/series_functions/color.js diff --git a/src/plugins/timelion/server/series_functions/color.test.js b/src/plugins/vis_type_timelion/server/series_functions/color.test.js similarity index 100% rename from src/plugins/timelion/server/series_functions/color.test.js rename to src/plugins/vis_type_timelion/server/series_functions/color.test.js diff --git a/src/plugins/timelion/server/series_functions/condition.js b/src/plugins/vis_type_timelion/server/series_functions/condition.js similarity index 100% rename from src/plugins/timelion/server/series_functions/condition.js rename to src/plugins/vis_type_timelion/server/series_functions/condition.js diff --git a/src/plugins/timelion/server/series_functions/condition.test.js b/src/plugins/vis_type_timelion/server/series_functions/condition.test.js similarity index 100% rename from src/plugins/timelion/server/series_functions/condition.test.js rename to src/plugins/vis_type_timelion/server/series_functions/condition.test.js diff --git a/src/plugins/timelion/server/series_functions/cusum.js b/src/plugins/vis_type_timelion/server/series_functions/cusum.js similarity index 100% rename from src/plugins/timelion/server/series_functions/cusum.js rename to src/plugins/vis_type_timelion/server/series_functions/cusum.js diff --git a/src/plugins/timelion/server/series_functions/cusum.test.js b/src/plugins/vis_type_timelion/server/series_functions/cusum.test.js similarity index 100% rename from src/plugins/timelion/server/series_functions/cusum.test.js rename to src/plugins/vis_type_timelion/server/series_functions/cusum.test.js diff --git a/src/plugins/timelion/server/series_functions/derivative.js b/src/plugins/vis_type_timelion/server/series_functions/derivative.js similarity index 100% rename from src/plugins/timelion/server/series_functions/derivative.js rename to src/plugins/vis_type_timelion/server/series_functions/derivative.js diff --git a/src/plugins/timelion/server/series_functions/derivative.test.js b/src/plugins/vis_type_timelion/server/series_functions/derivative.test.js similarity index 100% rename from src/plugins/timelion/server/series_functions/derivative.test.js rename to src/plugins/vis_type_timelion/server/series_functions/derivative.test.js diff --git a/src/plugins/timelion/server/series_functions/divide.js b/src/plugins/vis_type_timelion/server/series_functions/divide.js similarity index 100% rename from src/plugins/timelion/server/series_functions/divide.js rename to src/plugins/vis_type_timelion/server/series_functions/divide.js diff --git a/src/plugins/timelion/server/series_functions/divide.test.js b/src/plugins/vis_type_timelion/server/series_functions/divide.test.js similarity index 100% rename from src/plugins/timelion/server/series_functions/divide.test.js rename to src/plugins/vis_type_timelion/server/series_functions/divide.test.js diff --git a/src/plugins/timelion/server/series_functions/es/es.test.js b/src/plugins/vis_type_timelion/server/series_functions/es/es.test.js similarity index 100% rename from src/plugins/timelion/server/series_functions/es/es.test.js rename to src/plugins/vis_type_timelion/server/series_functions/es/es.test.js diff --git a/src/plugins/timelion/server/series_functions/es/index.js b/src/plugins/vis_type_timelion/server/series_functions/es/index.js similarity index 100% rename from src/plugins/timelion/server/series_functions/es/index.js rename to src/plugins/vis_type_timelion/server/series_functions/es/index.js diff --git a/src/plugins/timelion/server/series_functions/es/lib/agg_body.js b/src/plugins/vis_type_timelion/server/series_functions/es/lib/agg_body.js similarity index 100% rename from src/plugins/timelion/server/series_functions/es/lib/agg_body.js rename to src/plugins/vis_type_timelion/server/series_functions/es/lib/agg_body.js diff --git a/src/plugins/timelion/server/series_functions/es/lib/agg_response_to_series_list.js b/src/plugins/vis_type_timelion/server/series_functions/es/lib/agg_response_to_series_list.js similarity index 100% rename from src/plugins/timelion/server/series_functions/es/lib/agg_response_to_series_list.js rename to src/plugins/vis_type_timelion/server/series_functions/es/lib/agg_response_to_series_list.js diff --git a/src/plugins/timelion/server/series_functions/es/lib/build_request.js b/src/plugins/vis_type_timelion/server/series_functions/es/lib/build_request.js similarity index 100% rename from src/plugins/timelion/server/series_functions/es/lib/build_request.js rename to src/plugins/vis_type_timelion/server/series_functions/es/lib/build_request.js diff --git a/src/plugins/timelion/server/series_functions/es/lib/create_date_agg.js b/src/plugins/vis_type_timelion/server/series_functions/es/lib/create_date_agg.js similarity index 100% rename from src/plugins/timelion/server/series_functions/es/lib/create_date_agg.js rename to src/plugins/vis_type_timelion/server/series_functions/es/lib/create_date_agg.js diff --git a/src/plugins/timelion/server/series_functions/first.js b/src/plugins/vis_type_timelion/server/series_functions/first.js similarity index 100% rename from src/plugins/timelion/server/series_functions/first.js rename to src/plugins/vis_type_timelion/server/series_functions/first.js diff --git a/src/plugins/timelion/server/series_functions/first.test.js b/src/plugins/vis_type_timelion/server/series_functions/first.test.js similarity index 100% rename from src/plugins/timelion/server/series_functions/first.test.js rename to src/plugins/vis_type_timelion/server/series_functions/first.test.js diff --git a/src/plugins/timelion/server/series_functions/fit.js b/src/plugins/vis_type_timelion/server/series_functions/fit.js similarity index 100% rename from src/plugins/timelion/server/series_functions/fit.js rename to src/plugins/vis_type_timelion/server/series_functions/fit.js diff --git a/src/plugins/timelion/server/series_functions/fit.test.js b/src/plugins/vis_type_timelion/server/series_functions/fit.test.js similarity index 98% rename from src/plugins/timelion/server/series_functions/fit.test.js rename to src/plugins/vis_type_timelion/server/series_functions/fit.test.js index 75eaa2a50ea72..6622259a1fd87 100644 --- a/src/plugins/timelion/server/series_functions/fit.test.js +++ b/src/plugins/vis_type_timelion/server/series_functions/fit.test.js @@ -17,7 +17,7 @@ * under the License. */ -const fn = require(`src/plugins/timelion/server/series_functions/fit`); +const fn = require(`src/plugins/vis_type_timelion/server/series_functions/fit`); import moment from 'moment'; const expect = require('chai').expect; import invoke from './helpers/invoke_series_fn.js'; diff --git a/src/plugins/timelion/server/series_functions/fixtures/bucket_list.js b/src/plugins/vis_type_timelion/server/series_functions/fixtures/bucket_list.js similarity index 100% rename from src/plugins/timelion/server/series_functions/fixtures/bucket_list.js rename to src/plugins/vis_type_timelion/server/series_functions/fixtures/bucket_list.js diff --git a/src/plugins/timelion/server/series_functions/fixtures/es_response.js b/src/plugins/vis_type_timelion/server/series_functions/fixtures/es_response.js similarity index 100% rename from src/plugins/timelion/server/series_functions/fixtures/es_response.js rename to src/plugins/vis_type_timelion/server/series_functions/fixtures/es_response.js diff --git a/src/plugins/timelion/server/series_functions/fixtures/series_list.js b/src/plugins/vis_type_timelion/server/series_functions/fixtures/series_list.js similarity index 100% rename from src/plugins/timelion/server/series_functions/fixtures/series_list.js rename to src/plugins/vis_type_timelion/server/series_functions/fixtures/series_list.js diff --git a/src/plugins/timelion/server/series_functions/fixtures/tl_config.js b/src/plugins/vis_type_timelion/server/series_functions/fixtures/tl_config.js similarity index 100% rename from src/plugins/timelion/server/series_functions/fixtures/tl_config.js rename to src/plugins/vis_type_timelion/server/series_functions/fixtures/tl_config.js diff --git a/src/plugins/timelion/server/series_functions/graphite.js b/src/plugins/vis_type_timelion/server/series_functions/graphite.js similarity index 100% rename from src/plugins/timelion/server/series_functions/graphite.js rename to src/plugins/vis_type_timelion/server/series_functions/graphite.js diff --git a/src/plugins/timelion/server/series_functions/graphite.test.js b/src/plugins/vis_type_timelion/server/series_functions/graphite.test.js similarity index 100% rename from src/plugins/timelion/server/series_functions/graphite.test.js rename to src/plugins/vis_type_timelion/server/series_functions/graphite.test.js diff --git a/src/plugins/timelion/server/series_functions/helpers/get_series.js b/src/plugins/vis_type_timelion/server/series_functions/helpers/get_series.js similarity index 100% rename from src/plugins/timelion/server/series_functions/helpers/get_series.js rename to src/plugins/vis_type_timelion/server/series_functions/helpers/get_series.js diff --git a/src/plugins/timelion/server/series_functions/helpers/get_series_list.js b/src/plugins/vis_type_timelion/server/series_functions/helpers/get_series_list.js similarity index 100% rename from src/plugins/timelion/server/series_functions/helpers/get_series_list.js rename to src/plugins/vis_type_timelion/server/series_functions/helpers/get_series_list.js diff --git a/src/plugins/timelion/server/series_functions/helpers/get_single_series_list.js b/src/plugins/vis_type_timelion/server/series_functions/helpers/get_single_series_list.js similarity index 100% rename from src/plugins/timelion/server/series_functions/helpers/get_single_series_list.js rename to src/plugins/vis_type_timelion/server/series_functions/helpers/get_single_series_list.js diff --git a/src/plugins/timelion/server/series_functions/helpers/invoke_series_fn.js b/src/plugins/vis_type_timelion/server/series_functions/helpers/invoke_series_fn.js similarity index 100% rename from src/plugins/timelion/server/series_functions/helpers/invoke_series_fn.js rename to src/plugins/vis_type_timelion/server/series_functions/helpers/invoke_series_fn.js diff --git a/src/plugins/timelion/server/series_functions/hide.js b/src/plugins/vis_type_timelion/server/series_functions/hide.js similarity index 100% rename from src/plugins/timelion/server/series_functions/hide.js rename to src/plugins/vis_type_timelion/server/series_functions/hide.js diff --git a/src/plugins/timelion/server/series_functions/hide.test.js b/src/plugins/vis_type_timelion/server/series_functions/hide.test.js similarity index 100% rename from src/plugins/timelion/server/series_functions/hide.test.js rename to src/plugins/vis_type_timelion/server/series_functions/hide.test.js diff --git a/src/plugins/timelion/server/series_functions/holt/index.js b/src/plugins/vis_type_timelion/server/series_functions/holt/index.js similarity index 100% rename from src/plugins/timelion/server/series_functions/holt/index.js rename to src/plugins/vis_type_timelion/server/series_functions/holt/index.js diff --git a/src/plugins/timelion/server/series_functions/holt/lib/des.js b/src/plugins/vis_type_timelion/server/series_functions/holt/lib/des.js similarity index 100% rename from src/plugins/timelion/server/series_functions/holt/lib/des.js rename to src/plugins/vis_type_timelion/server/series_functions/holt/lib/des.js diff --git a/src/plugins/timelion/server/series_functions/holt/lib/ses.js b/src/plugins/vis_type_timelion/server/series_functions/holt/lib/ses.js similarity index 100% rename from src/plugins/timelion/server/series_functions/holt/lib/ses.js rename to src/plugins/vis_type_timelion/server/series_functions/holt/lib/ses.js diff --git a/src/plugins/timelion/server/series_functions/holt/lib/tes.js b/src/plugins/vis_type_timelion/server/series_functions/holt/lib/tes.js similarity index 100% rename from src/plugins/timelion/server/series_functions/holt/lib/tes.js rename to src/plugins/vis_type_timelion/server/series_functions/holt/lib/tes.js diff --git a/src/plugins/timelion/server/series_functions/label.js b/src/plugins/vis_type_timelion/server/series_functions/label.js similarity index 100% rename from src/plugins/timelion/server/series_functions/label.js rename to src/plugins/vis_type_timelion/server/series_functions/label.js diff --git a/src/plugins/timelion/server/series_functions/label.test.js b/src/plugins/vis_type_timelion/server/series_functions/label.test.js similarity index 100% rename from src/plugins/timelion/server/series_functions/label.test.js rename to src/plugins/vis_type_timelion/server/series_functions/label.test.js diff --git a/src/plugins/timelion/server/series_functions/legend.js b/src/plugins/vis_type_timelion/server/series_functions/legend.js similarity index 100% rename from src/plugins/timelion/server/series_functions/legend.js rename to src/plugins/vis_type_timelion/server/series_functions/legend.js diff --git a/src/plugins/timelion/server/series_functions/legend.test.js b/src/plugins/vis_type_timelion/server/series_functions/legend.test.js similarity index 100% rename from src/plugins/timelion/server/series_functions/legend.test.js rename to src/plugins/vis_type_timelion/server/series_functions/legend.test.js diff --git a/src/plugins/timelion/server/series_functions/lines.js b/src/plugins/vis_type_timelion/server/series_functions/lines.js similarity index 100% rename from src/plugins/timelion/server/series_functions/lines.js rename to src/plugins/vis_type_timelion/server/series_functions/lines.js diff --git a/src/plugins/timelion/server/series_functions/lines.test.js b/src/plugins/vis_type_timelion/server/series_functions/lines.test.js similarity index 100% rename from src/plugins/timelion/server/series_functions/lines.test.js rename to src/plugins/vis_type_timelion/server/series_functions/lines.test.js diff --git a/src/plugins/timelion/server/series_functions/log.js b/src/plugins/vis_type_timelion/server/series_functions/log.js similarity index 100% rename from src/plugins/timelion/server/series_functions/log.js rename to src/plugins/vis_type_timelion/server/series_functions/log.js diff --git a/src/plugins/timelion/server/series_functions/log.test.js b/src/plugins/vis_type_timelion/server/series_functions/log.test.js similarity index 100% rename from src/plugins/timelion/server/series_functions/log.test.js rename to src/plugins/vis_type_timelion/server/series_functions/log.test.js diff --git a/src/plugins/timelion/server/series_functions/max.js b/src/plugins/vis_type_timelion/server/series_functions/max.js similarity index 100% rename from src/plugins/timelion/server/series_functions/max.js rename to src/plugins/vis_type_timelion/server/series_functions/max.js diff --git a/src/plugins/timelion/server/series_functions/max.test.js b/src/plugins/vis_type_timelion/server/series_functions/max.test.js similarity index 100% rename from src/plugins/timelion/server/series_functions/max.test.js rename to src/plugins/vis_type_timelion/server/series_functions/max.test.js diff --git a/src/plugins/timelion/server/series_functions/min.js b/src/plugins/vis_type_timelion/server/series_functions/min.js similarity index 100% rename from src/plugins/timelion/server/series_functions/min.js rename to src/plugins/vis_type_timelion/server/series_functions/min.js diff --git a/src/plugins/timelion/server/series_functions/min.test.js b/src/plugins/vis_type_timelion/server/series_functions/min.test.js similarity index 100% rename from src/plugins/timelion/server/series_functions/min.test.js rename to src/plugins/vis_type_timelion/server/series_functions/min.test.js diff --git a/src/plugins/timelion/server/series_functions/movingaverage.js b/src/plugins/vis_type_timelion/server/series_functions/movingaverage.js similarity index 100% rename from src/plugins/timelion/server/series_functions/movingaverage.js rename to src/plugins/vis_type_timelion/server/series_functions/movingaverage.js diff --git a/src/plugins/timelion/server/series_functions/movingaverage.test.js b/src/plugins/vis_type_timelion/server/series_functions/movingaverage.test.js similarity index 100% rename from src/plugins/timelion/server/series_functions/movingaverage.test.js rename to src/plugins/vis_type_timelion/server/series_functions/movingaverage.test.js diff --git a/src/plugins/timelion/server/series_functions/movingstd.js b/src/plugins/vis_type_timelion/server/series_functions/movingstd.js similarity index 100% rename from src/plugins/timelion/server/series_functions/movingstd.js rename to src/plugins/vis_type_timelion/server/series_functions/movingstd.js diff --git a/src/plugins/timelion/server/series_functions/movingstd.test.js b/src/plugins/vis_type_timelion/server/series_functions/movingstd.test.js similarity index 100% rename from src/plugins/timelion/server/series_functions/movingstd.test.js rename to src/plugins/vis_type_timelion/server/series_functions/movingstd.test.js diff --git a/src/plugins/timelion/server/series_functions/multiply.js b/src/plugins/vis_type_timelion/server/series_functions/multiply.js similarity index 100% rename from src/plugins/timelion/server/series_functions/multiply.js rename to src/plugins/vis_type_timelion/server/series_functions/multiply.js diff --git a/src/plugins/timelion/server/series_functions/multiply.test.js b/src/plugins/vis_type_timelion/server/series_functions/multiply.test.js similarity index 100% rename from src/plugins/timelion/server/series_functions/multiply.test.js rename to src/plugins/vis_type_timelion/server/series_functions/multiply.test.js diff --git a/src/plugins/timelion/server/series_functions/points.js b/src/plugins/vis_type_timelion/server/series_functions/points.js similarity index 100% rename from src/plugins/timelion/server/series_functions/points.js rename to src/plugins/vis_type_timelion/server/series_functions/points.js diff --git a/src/plugins/timelion/server/series_functions/points.test.js b/src/plugins/vis_type_timelion/server/series_functions/points.test.js similarity index 100% rename from src/plugins/timelion/server/series_functions/points.test.js rename to src/plugins/vis_type_timelion/server/series_functions/points.test.js diff --git a/src/plugins/timelion/server/series_functions/precision.js b/src/plugins/vis_type_timelion/server/series_functions/precision.js similarity index 100% rename from src/plugins/timelion/server/series_functions/precision.js rename to src/plugins/vis_type_timelion/server/series_functions/precision.js diff --git a/src/plugins/timelion/server/series_functions/precision.test.js b/src/plugins/vis_type_timelion/server/series_functions/precision.test.js similarity index 100% rename from src/plugins/timelion/server/series_functions/precision.test.js rename to src/plugins/vis_type_timelion/server/series_functions/precision.test.js diff --git a/src/plugins/timelion/server/series_functions/props.js b/src/plugins/vis_type_timelion/server/series_functions/props.js similarity index 100% rename from src/plugins/timelion/server/series_functions/props.js rename to src/plugins/vis_type_timelion/server/series_functions/props.js diff --git a/src/plugins/timelion/server/series_functions/quandl.js b/src/plugins/vis_type_timelion/server/series_functions/quandl.js similarity index 100% rename from src/plugins/timelion/server/series_functions/quandl.js rename to src/plugins/vis_type_timelion/server/series_functions/quandl.js diff --git a/src/plugins/timelion/server/series_functions/quandl.test.js b/src/plugins/vis_type_timelion/server/series_functions/quandl.test.js similarity index 100% rename from src/plugins/timelion/server/series_functions/quandl.test.js rename to src/plugins/vis_type_timelion/server/series_functions/quandl.test.js diff --git a/src/plugins/timelion/server/series_functions/range.js b/src/plugins/vis_type_timelion/server/series_functions/range.js similarity index 100% rename from src/plugins/timelion/server/series_functions/range.js rename to src/plugins/vis_type_timelion/server/series_functions/range.js diff --git a/src/plugins/timelion/server/series_functions/range.test.js b/src/plugins/vis_type_timelion/server/series_functions/range.test.js similarity index 100% rename from src/plugins/timelion/server/series_functions/range.test.js rename to src/plugins/vis_type_timelion/server/series_functions/range.test.js diff --git a/src/plugins/timelion/server/series_functions/scale_interval.js b/src/plugins/vis_type_timelion/server/series_functions/scale_interval.js similarity index 100% rename from src/plugins/timelion/server/series_functions/scale_interval.js rename to src/plugins/vis_type_timelion/server/series_functions/scale_interval.js diff --git a/src/plugins/timelion/server/series_functions/scale_interval.test.js b/src/plugins/vis_type_timelion/server/series_functions/scale_interval.test.js similarity index 100% rename from src/plugins/timelion/server/series_functions/scale_interval.test.js rename to src/plugins/vis_type_timelion/server/series_functions/scale_interval.test.js diff --git a/src/plugins/timelion/server/series_functions/static.js b/src/plugins/vis_type_timelion/server/series_functions/static.js similarity index 100% rename from src/plugins/timelion/server/series_functions/static.js rename to src/plugins/vis_type_timelion/server/series_functions/static.js diff --git a/src/plugins/timelion/server/series_functions/static.test.js b/src/plugins/vis_type_timelion/server/series_functions/static.test.js similarity index 100% rename from src/plugins/timelion/server/series_functions/static.test.js rename to src/plugins/vis_type_timelion/server/series_functions/static.test.js diff --git a/src/plugins/timelion/server/series_functions/subtract.js b/src/plugins/vis_type_timelion/server/series_functions/subtract.js similarity index 100% rename from src/plugins/timelion/server/series_functions/subtract.js rename to src/plugins/vis_type_timelion/server/series_functions/subtract.js diff --git a/src/plugins/timelion/server/series_functions/subtract.test.js b/src/plugins/vis_type_timelion/server/series_functions/subtract.test.js similarity index 100% rename from src/plugins/timelion/server/series_functions/subtract.test.js rename to src/plugins/vis_type_timelion/server/series_functions/subtract.test.js diff --git a/src/plugins/timelion/server/series_functions/sum.js b/src/plugins/vis_type_timelion/server/series_functions/sum.js similarity index 100% rename from src/plugins/timelion/server/series_functions/sum.js rename to src/plugins/vis_type_timelion/server/series_functions/sum.js diff --git a/src/plugins/timelion/server/series_functions/sum.test.js b/src/plugins/vis_type_timelion/server/series_functions/sum.test.js similarity index 100% rename from src/plugins/timelion/server/series_functions/sum.test.js rename to src/plugins/vis_type_timelion/server/series_functions/sum.test.js diff --git a/src/plugins/timelion/server/series_functions/title.js b/src/plugins/vis_type_timelion/server/series_functions/title.js similarity index 100% rename from src/plugins/timelion/server/series_functions/title.js rename to src/plugins/vis_type_timelion/server/series_functions/title.js diff --git a/src/plugins/timelion/server/series_functions/title.test.js b/src/plugins/vis_type_timelion/server/series_functions/title.test.js similarity index 100% rename from src/plugins/timelion/server/series_functions/title.test.js rename to src/plugins/vis_type_timelion/server/series_functions/title.test.js diff --git a/src/plugins/timelion/server/series_functions/trend/index.js b/src/plugins/vis_type_timelion/server/series_functions/trend/index.js similarity index 100% rename from src/plugins/timelion/server/series_functions/trend/index.js rename to src/plugins/vis_type_timelion/server/series_functions/trend/index.js diff --git a/src/plugins/timelion/server/series_functions/trend/lib/regress.js b/src/plugins/vis_type_timelion/server/series_functions/trend/lib/regress.js similarity index 100% rename from src/plugins/timelion/server/series_functions/trend/lib/regress.js rename to src/plugins/vis_type_timelion/server/series_functions/trend/lib/regress.js diff --git a/src/plugins/timelion/server/series_functions/trim.js b/src/plugins/vis_type_timelion/server/series_functions/trim.js similarity index 100% rename from src/plugins/timelion/server/series_functions/trim.js rename to src/plugins/vis_type_timelion/server/series_functions/trim.js diff --git a/src/plugins/timelion/server/series_functions/trim.test.js b/src/plugins/vis_type_timelion/server/series_functions/trim.test.js similarity index 100% rename from src/plugins/timelion/server/series_functions/trim.test.js rename to src/plugins/vis_type_timelion/server/series_functions/trim.test.js diff --git a/src/plugins/timelion/server/series_functions/worldbank.js b/src/plugins/vis_type_timelion/server/series_functions/worldbank.js similarity index 100% rename from src/plugins/timelion/server/series_functions/worldbank.js rename to src/plugins/vis_type_timelion/server/series_functions/worldbank.js diff --git a/src/plugins/timelion/server/series_functions/worldbank_indicators.js b/src/plugins/vis_type_timelion/server/series_functions/worldbank_indicators.js similarity index 100% rename from src/plugins/timelion/server/series_functions/worldbank_indicators.js rename to src/plugins/vis_type_timelion/server/series_functions/worldbank_indicators.js diff --git a/src/plugins/timelion/server/series_functions/yaxis.js b/src/plugins/vis_type_timelion/server/series_functions/yaxis.js similarity index 100% rename from src/plugins/timelion/server/series_functions/yaxis.js rename to src/plugins/vis_type_timelion/server/series_functions/yaxis.js diff --git a/src/plugins/timelion/server/series_functions/yaxis.test.js b/src/plugins/vis_type_timelion/server/series_functions/yaxis.test.js similarity index 100% rename from src/plugins/timelion/server/series_functions/yaxis.test.js rename to src/plugins/vis_type_timelion/server/series_functions/yaxis.test.js diff --git a/src/plugins/timelion/server/timelion.json b/src/plugins/vis_type_timelion/server/timelion.json similarity index 100% rename from src/plugins/timelion/server/timelion.json rename to src/plugins/vis_type_timelion/server/timelion.json diff --git a/src/plugins/timelion/server/types.ts b/src/plugins/vis_type_timelion/server/types.ts similarity index 100% rename from src/plugins/timelion/server/types.ts rename to src/plugins/vis_type_timelion/server/types.ts diff --git a/src/plugins/vis_type_timeseries/common/agg_lookup.js b/src/plugins/vis_type_timeseries/common/agg_lookup.js index 4dfdc83dcfabb..432da03e3d45d 100644 --- a/src/plugins/vis_type_timeseries/common/agg_lookup.js +++ b/src/plugins/vis_type_timeseries/common/agg_lookup.js @@ -97,6 +97,9 @@ export const lookup = { defaultMessage: 'Static Value', }), top_hit: i18n.translate('visTypeTimeseries.aggLookup.topHitLabel', { defaultMessage: 'Top Hit' }), + positive_rate: i18n.translate('visTypeTimeseries.aggLookup.positiveRateLabel', { + defaultMessage: 'Positive Rate', + }), }; const pipeline = [ diff --git a/src/plugins/vis_type_timeseries/common/calculate_label.js b/src/plugins/vis_type_timeseries/common/calculate_label.js index 756d6e57a83e8..71aa0aed7dc11 100644 --- a/src/plugins/vis_type_timeseries/common/calculate_label.js +++ b/src/plugins/vis_type_timeseries/common/calculate_label.js @@ -70,6 +70,12 @@ export function calculateLabel(metric, metrics) { defaultMessage: 'Filter Ratio', }); } + if (metric.type === 'positive_rate') { + return i18n.translate('visTypeTimeseries.calculateLabel.positiveRateLabel', { + defaultMessage: 'Positive Rate of {field}', + values: { field: metric.field }, + }); + } if (metric.type === 'static') { return i18n.translate('visTypeTimeseries.calculateLabel.staticValueLabel', { defaultMessage: 'Static Value of {metricValue}', diff --git a/src/plugins/vis_type_timeseries/kibana.json b/src/plugins/vis_type_timeseries/kibana.json index d77f4ac92da16..9053d2543e0d0 100644 --- a/src/plugins/vis_type_timeseries/kibana.json +++ b/src/plugins/vis_type_timeseries/kibana.json @@ -1,7 +1,9 @@ { - "id": "metrics", + "id": "visTypeTimeseries", "version": "8.0.0", "kibanaVersion": "kibana", "server": true, + "ui": true, + "requiredPlugins": ["charts", "data", "expressions", "visualizations"], "optionalPlugins": ["usageCollection"] } diff --git a/src/legacy/core_plugins/vis_type_timeseries/public/_mixins.scss b/src/plugins/vis_type_timeseries/public/application/_mixins.scss similarity index 100% rename from src/legacy/core_plugins/vis_type_timeseries/public/_mixins.scss rename to src/plugins/vis_type_timeseries/public/application/_mixins.scss diff --git a/src/legacy/core_plugins/vis_type_timeseries/public/_tvb_editor.scss b/src/plugins/vis_type_timeseries/public/application/_tvb_editor.scss similarity index 100% rename from src/legacy/core_plugins/vis_type_timeseries/public/_tvb_editor.scss rename to src/plugins/vis_type_timeseries/public/application/_tvb_editor.scss diff --git a/src/legacy/core_plugins/vis_type_timeseries/public/_variables.scss b/src/plugins/vis_type_timeseries/public/application/_variables.scss similarity index 100% rename from src/legacy/core_plugins/vis_type_timeseries/public/_variables.scss rename to src/plugins/vis_type_timeseries/public/application/_variables.scss diff --git a/src/legacy/core_plugins/vis_type_timeseries/public/components/_annotations_editor.scss b/src/plugins/vis_type_timeseries/public/application/components/_annotations_editor.scss similarity index 100% rename from src/legacy/core_plugins/vis_type_timeseries/public/components/_annotations_editor.scss rename to src/plugins/vis_type_timeseries/public/application/components/_annotations_editor.scss diff --git a/src/legacy/core_plugins/vis_type_timeseries/public/components/_color_picker.scss b/src/plugins/vis_type_timeseries/public/application/components/_color_picker.scss similarity index 100% rename from src/legacy/core_plugins/vis_type_timeseries/public/components/_color_picker.scss rename to src/plugins/vis_type_timeseries/public/application/components/_color_picker.scss diff --git a/src/legacy/core_plugins/vis_type_timeseries/public/components/_color_rules.scss b/src/plugins/vis_type_timeseries/public/application/components/_color_rules.scss similarity index 100% rename from src/legacy/core_plugins/vis_type_timeseries/public/components/_color_rules.scss rename to src/plugins/vis_type_timeseries/public/application/components/_color_rules.scss diff --git a/src/legacy/core_plugins/vis_type_timeseries/public/components/_custom_color_picker.scss b/src/plugins/vis_type_timeseries/public/application/components/_custom_color_picker.scss similarity index 100% rename from src/legacy/core_plugins/vis_type_timeseries/public/components/_custom_color_picker.scss rename to src/plugins/vis_type_timeseries/public/application/components/_custom_color_picker.scss diff --git a/src/legacy/core_plugins/vis_type_timeseries/public/components/_error.scss b/src/plugins/vis_type_timeseries/public/application/components/_error.scss similarity index 100% rename from src/legacy/core_plugins/vis_type_timeseries/public/components/_error.scss rename to src/plugins/vis_type_timeseries/public/application/components/_error.scss diff --git a/src/legacy/core_plugins/vis_type_timeseries/public/components/_index.scss b/src/plugins/vis_type_timeseries/public/application/components/_index.scss similarity index 100% rename from src/legacy/core_plugins/vis_type_timeseries/public/components/_index.scss rename to src/plugins/vis_type_timeseries/public/application/components/_index.scss diff --git a/src/legacy/core_plugins/vis_type_timeseries/public/components/_markdown_editor.scss b/src/plugins/vis_type_timeseries/public/application/components/_markdown_editor.scss similarity index 100% rename from src/legacy/core_plugins/vis_type_timeseries/public/components/_markdown_editor.scss rename to src/plugins/vis_type_timeseries/public/application/components/_markdown_editor.scss diff --git a/src/legacy/core_plugins/vis_type_timeseries/public/components/_no_data.scss b/src/plugins/vis_type_timeseries/public/application/components/_no_data.scss similarity index 100% rename from src/legacy/core_plugins/vis_type_timeseries/public/components/_no_data.scss rename to src/plugins/vis_type_timeseries/public/application/components/_no_data.scss diff --git a/src/legacy/core_plugins/vis_type_timeseries/public/components/_series_editor.scss b/src/plugins/vis_type_timeseries/public/application/components/_series_editor.scss similarity index 100% rename from src/legacy/core_plugins/vis_type_timeseries/public/components/_series_editor.scss rename to src/plugins/vis_type_timeseries/public/application/components/_series_editor.scss diff --git a/src/legacy/core_plugins/vis_type_timeseries/public/components/_vis_editor.scss b/src/plugins/vis_type_timeseries/public/application/components/_vis_editor.scss similarity index 100% rename from src/legacy/core_plugins/vis_type_timeseries/public/components/_vis_editor.scss rename to src/plugins/vis_type_timeseries/public/application/components/_vis_editor.scss diff --git a/src/legacy/core_plugins/vis_type_timeseries/public/components/_vis_editor_visualization.scss b/src/plugins/vis_type_timeseries/public/application/components/_vis_editor_visualization.scss similarity index 100% rename from src/legacy/core_plugins/vis_type_timeseries/public/components/_vis_editor_visualization.scss rename to src/plugins/vis_type_timeseries/public/application/components/_vis_editor_visualization.scss diff --git a/src/legacy/core_plugins/vis_type_timeseries/public/components/_vis_picker.scss b/src/plugins/vis_type_timeseries/public/application/components/_vis_picker.scss similarity index 100% rename from src/legacy/core_plugins/vis_type_timeseries/public/components/_vis_picker.scss rename to src/plugins/vis_type_timeseries/public/application/components/_vis_picker.scss diff --git a/src/legacy/core_plugins/vis_type_timeseries/public/components/_vis_with_splits.scss b/src/plugins/vis_type_timeseries/public/application/components/_vis_with_splits.scss similarity index 100% rename from src/legacy/core_plugins/vis_type_timeseries/public/components/_vis_with_splits.scss rename to src/plugins/vis_type_timeseries/public/application/components/_vis_with_splits.scss diff --git a/src/legacy/core_plugins/vis_type_timeseries/public/components/add_delete_buttons.js b/src/plugins/vis_type_timeseries/public/application/components/add_delete_buttons.js similarity index 100% rename from src/legacy/core_plugins/vis_type_timeseries/public/components/add_delete_buttons.js rename to src/plugins/vis_type_timeseries/public/application/components/add_delete_buttons.js diff --git a/src/legacy/core_plugins/vis_type_timeseries/public/components/add_delete_buttons.test.js b/src/plugins/vis_type_timeseries/public/application/components/add_delete_buttons.test.js similarity index 100% rename from src/legacy/core_plugins/vis_type_timeseries/public/components/add_delete_buttons.test.js rename to src/plugins/vis_type_timeseries/public/application/components/add_delete_buttons.test.js diff --git a/src/legacy/core_plugins/vis_type_timeseries/public/components/aggs/_agg_row.scss b/src/plugins/vis_type_timeseries/public/application/components/aggs/_agg_row.scss similarity index 100% rename from src/legacy/core_plugins/vis_type_timeseries/public/components/aggs/_agg_row.scss rename to src/plugins/vis_type_timeseries/public/application/components/aggs/_agg_row.scss diff --git a/src/legacy/core_plugins/vis_type_timeseries/public/components/aggs/_index.scss b/src/plugins/vis_type_timeseries/public/application/components/aggs/_index.scss similarity index 100% rename from src/legacy/core_plugins/vis_type_timeseries/public/components/aggs/_index.scss rename to src/plugins/vis_type_timeseries/public/application/components/aggs/_index.scss diff --git a/src/legacy/core_plugins/vis_type_timeseries/public/components/aggs/agg.js b/src/plugins/vis_type_timeseries/public/application/components/aggs/agg.js similarity index 100% rename from src/legacy/core_plugins/vis_type_timeseries/public/components/aggs/agg.js rename to src/plugins/vis_type_timeseries/public/application/components/aggs/agg.js diff --git a/src/legacy/core_plugins/vis_type_timeseries/public/components/aggs/agg_row.js b/src/plugins/vis_type_timeseries/public/application/components/aggs/agg_row.js similarity index 100% rename from src/legacy/core_plugins/vis_type_timeseries/public/components/aggs/agg_row.js rename to src/plugins/vis_type_timeseries/public/application/components/aggs/agg_row.js diff --git a/src/legacy/core_plugins/vis_type_timeseries/public/components/aggs/agg_select.js b/src/plugins/vis_type_timeseries/public/application/components/aggs/agg_select.js similarity index 98% rename from src/legacy/core_plugins/vis_type_timeseries/public/components/aggs/agg_select.js rename to src/plugins/vis_type_timeseries/public/application/components/aggs/agg_select.js index f93dee14d0eed..8607ff184dfaa 100644 --- a/src/legacy/core_plugins/vis_type_timeseries/public/components/aggs/agg_select.js +++ b/src/plugins/vis_type_timeseries/public/application/components/aggs/agg_select.js @@ -49,6 +49,12 @@ const metricAggs = [ }), value: 'filter_ratio', }, + { + label: i18n.translate('visTypeTimeseries.aggSelect.metricsAggs.positiveRateLabel', { + defaultMessage: 'Positive Rate', + }), + value: 'positive_rate', + }, { label: i18n.translate('visTypeTimeseries.aggSelect.metricsAggs.maxLabel', { defaultMessage: 'Max', diff --git a/src/legacy/core_plugins/vis_type_timeseries/public/components/aggs/aggs.js b/src/plugins/vis_type_timeseries/public/application/components/aggs/aggs.js similarity index 100% rename from src/legacy/core_plugins/vis_type_timeseries/public/components/aggs/aggs.js rename to src/plugins/vis_type_timeseries/public/application/components/aggs/aggs.js diff --git a/src/legacy/core_plugins/vis_type_timeseries/public/components/aggs/calculation.js b/src/plugins/vis_type_timeseries/public/application/components/aggs/calculation.js similarity index 100% rename from src/legacy/core_plugins/vis_type_timeseries/public/components/aggs/calculation.js rename to src/plugins/vis_type_timeseries/public/application/components/aggs/calculation.js diff --git a/src/legacy/core_plugins/vis_type_timeseries/public/components/aggs/cumulative_sum.js b/src/plugins/vis_type_timeseries/public/application/components/aggs/cumulative_sum.js similarity index 100% rename from src/legacy/core_plugins/vis_type_timeseries/public/components/aggs/cumulative_sum.js rename to src/plugins/vis_type_timeseries/public/application/components/aggs/cumulative_sum.js diff --git a/src/legacy/core_plugins/vis_type_timeseries/public/components/aggs/derivative.js b/src/plugins/vis_type_timeseries/public/application/components/aggs/derivative.js similarity index 100% rename from src/legacy/core_plugins/vis_type_timeseries/public/components/aggs/derivative.js rename to src/plugins/vis_type_timeseries/public/application/components/aggs/derivative.js diff --git a/src/legacy/core_plugins/vis_type_timeseries/public/components/aggs/field_select.js b/src/plugins/vis_type_timeseries/public/application/components/aggs/field_select.js similarity index 100% rename from src/legacy/core_plugins/vis_type_timeseries/public/components/aggs/field_select.js rename to src/plugins/vis_type_timeseries/public/application/components/aggs/field_select.js diff --git a/src/legacy/core_plugins/vis_type_timeseries/public/components/aggs/filter_ratio.js b/src/plugins/vis_type_timeseries/public/application/components/aggs/filter_ratio.js similarity index 100% rename from src/legacy/core_plugins/vis_type_timeseries/public/components/aggs/filter_ratio.js rename to src/plugins/vis_type_timeseries/public/application/components/aggs/filter_ratio.js diff --git a/src/legacy/core_plugins/vis_type_timeseries/public/components/aggs/math.js b/src/plugins/vis_type_timeseries/public/application/components/aggs/math.js similarity index 100% rename from src/legacy/core_plugins/vis_type_timeseries/public/components/aggs/math.js rename to src/plugins/vis_type_timeseries/public/application/components/aggs/math.js diff --git a/src/legacy/core_plugins/vis_type_timeseries/public/components/aggs/metric_select.js b/src/plugins/vis_type_timeseries/public/application/components/aggs/metric_select.js similarity index 100% rename from src/legacy/core_plugins/vis_type_timeseries/public/components/aggs/metric_select.js rename to src/plugins/vis_type_timeseries/public/application/components/aggs/metric_select.js diff --git a/src/legacy/core_plugins/vis_type_timeseries/public/components/aggs/moving_average.js b/src/plugins/vis_type_timeseries/public/application/components/aggs/moving_average.js similarity index 100% rename from src/legacy/core_plugins/vis_type_timeseries/public/components/aggs/moving_average.js rename to src/plugins/vis_type_timeseries/public/application/components/aggs/moving_average.js diff --git a/src/legacy/core_plugins/vis_type_timeseries/public/components/aggs/percentile.js b/src/plugins/vis_type_timeseries/public/application/components/aggs/percentile.js similarity index 100% rename from src/legacy/core_plugins/vis_type_timeseries/public/components/aggs/percentile.js rename to src/plugins/vis_type_timeseries/public/application/components/aggs/percentile.js diff --git a/src/legacy/core_plugins/vis_type_timeseries/public/components/aggs/percentile_rank/index.js b/src/plugins/vis_type_timeseries/public/application/components/aggs/percentile_rank/index.js similarity index 100% rename from src/legacy/core_plugins/vis_type_timeseries/public/components/aggs/percentile_rank/index.js rename to src/plugins/vis_type_timeseries/public/application/components/aggs/percentile_rank/index.js diff --git a/src/legacy/core_plugins/vis_type_timeseries/public/components/aggs/percentile_rank/multi_value_row.js b/src/plugins/vis_type_timeseries/public/application/components/aggs/percentile_rank/multi_value_row.js similarity index 100% rename from src/legacy/core_plugins/vis_type_timeseries/public/components/aggs/percentile_rank/multi_value_row.js rename to src/plugins/vis_type_timeseries/public/application/components/aggs/percentile_rank/multi_value_row.js diff --git a/src/legacy/core_plugins/vis_type_timeseries/public/components/aggs/percentile_rank/percentile_rank.js b/src/plugins/vis_type_timeseries/public/application/components/aggs/percentile_rank/percentile_rank.js similarity index 100% rename from src/legacy/core_plugins/vis_type_timeseries/public/components/aggs/percentile_rank/percentile_rank.js rename to src/plugins/vis_type_timeseries/public/application/components/aggs/percentile_rank/percentile_rank.js diff --git a/src/legacy/core_plugins/vis_type_timeseries/public/components/aggs/percentile_rank/percentile_rank_values.js b/src/plugins/vis_type_timeseries/public/application/components/aggs/percentile_rank/percentile_rank_values.js similarity index 100% rename from src/legacy/core_plugins/vis_type_timeseries/public/components/aggs/percentile_rank/percentile_rank_values.js rename to src/plugins/vis_type_timeseries/public/application/components/aggs/percentile_rank/percentile_rank_values.js diff --git a/src/legacy/core_plugins/vis_type_timeseries/public/components/aggs/percentile_ui.js b/src/plugins/vis_type_timeseries/public/application/components/aggs/percentile_ui.js similarity index 100% rename from src/legacy/core_plugins/vis_type_timeseries/public/components/aggs/percentile_ui.js rename to src/plugins/vis_type_timeseries/public/application/components/aggs/percentile_ui.js diff --git a/src/legacy/core_plugins/vis_type_timeseries/public/components/aggs/positive_only.js b/src/plugins/vis_type_timeseries/public/application/components/aggs/positive_only.js similarity index 100% rename from src/legacy/core_plugins/vis_type_timeseries/public/components/aggs/positive_only.js rename to src/plugins/vis_type_timeseries/public/application/components/aggs/positive_only.js diff --git a/src/plugins/vis_type_timeseries/public/application/components/aggs/positive_rate.js b/src/plugins/vis_type_timeseries/public/application/components/aggs/positive_rate.js new file mode 100644 index 0000000000000..39558fa3a9224 --- /dev/null +++ b/src/plugins/vis_type_timeseries/public/application/components/aggs/positive_rate.js @@ -0,0 +1,191 @@ +/* + * 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 PropTypes from 'prop-types'; +import React from 'react'; +import { AggSelect } from './agg_select'; +import { FieldSelect } from './field_select'; +import { AggRow } from './agg_row'; +import { createChangeHandler } from '../lib/create_change_handler'; +import { createSelectHandler } from '../lib/create_select_handler'; +import { + htmlIdGenerator, + EuiFlexGroup, + EuiFlexItem, + EuiFormLabel, + EuiFormRow, + EuiSpacer, + EuiText, + EuiLink, + EuiComboBox, +} from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { i18n } from '@kbn/i18n'; +import { KBN_FIELD_TYPES } from '../../../../../../plugins/data/public'; + +const UNIT_OPTIONS = [ + { + label: i18n.translate('visTypeTimeseries.units.auto', { defaultMessage: 'auto' }), + value: '', + }, + { + label: i18n.translate('visTypeTimeseries.units.perMillisecond', { + defaultMessage: 'per millisecond', + }), + value: '1ms', + }, + { + label: i18n.translate('visTypeTimeseries.units.perSecond', { defaultMessage: 'per second' }), + value: '1s', + }, + { + label: i18n.translate('visTypeTimeseries.units.perMinute', { defaultMessage: 'per minute' }), + value: '1m', + }, + { + label: i18n.translate('visTypeTimeseries.units.perHour', { defaultMessage: 'per hour' }), + value: '1h', + }, + { + label: i18n.translate('visTypeTimeseries.units.perDay', { defaultMessage: 'per day' }), + value: '1d', + }, +]; + +export const PositiveRateAgg = props => { + const defaults = { unit: '' }; + const model = { ...defaults, ...props.model }; + + const handleChange = createChangeHandler(props.onChange, model); + const handleSelectChange = createSelectHandler(handleChange); + + const htmlId = htmlIdGenerator(); + const indexPattern = + (props.series.override_index_pattern && props.series.series_index_pattern) || + props.panel.index_pattern; + + const selectedUnitOptions = UNIT_OPTIONS.filter(o => o.value === model.unit); + + return ( + + + + + + + + + + + + } + fullWidth + > + + + + + + } + fullWidth + > + + + + + + +

+ + + + ), + }} + /> +

+
+
+ ); +}; + +PositiveRateAgg.propTypes = { + disableDelete: PropTypes.bool, + fields: PropTypes.object, + model: PropTypes.object, + onAdd: PropTypes.func, + onChange: PropTypes.func, + onDelete: PropTypes.func, + panel: PropTypes.object, + series: PropTypes.object, + siblings: PropTypes.array, +}; diff --git a/src/legacy/core_plugins/vis_type_timeseries/public/components/aggs/serial_diff.js b/src/plugins/vis_type_timeseries/public/application/components/aggs/serial_diff.js similarity index 100% rename from src/legacy/core_plugins/vis_type_timeseries/public/components/aggs/serial_diff.js rename to src/plugins/vis_type_timeseries/public/application/components/aggs/serial_diff.js diff --git a/src/legacy/core_plugins/vis_type_timeseries/public/components/aggs/series_agg.js b/src/plugins/vis_type_timeseries/public/application/components/aggs/series_agg.js similarity index 100% rename from src/legacy/core_plugins/vis_type_timeseries/public/components/aggs/series_agg.js rename to src/plugins/vis_type_timeseries/public/application/components/aggs/series_agg.js diff --git a/src/legacy/core_plugins/vis_type_timeseries/public/components/aggs/static.js b/src/plugins/vis_type_timeseries/public/application/components/aggs/static.js similarity index 100% rename from src/legacy/core_plugins/vis_type_timeseries/public/components/aggs/static.js rename to src/plugins/vis_type_timeseries/public/application/components/aggs/static.js diff --git a/src/legacy/core_plugins/vis_type_timeseries/public/components/aggs/std_agg.js b/src/plugins/vis_type_timeseries/public/application/components/aggs/std_agg.js similarity index 100% rename from src/legacy/core_plugins/vis_type_timeseries/public/components/aggs/std_agg.js rename to src/plugins/vis_type_timeseries/public/application/components/aggs/std_agg.js diff --git a/src/legacy/core_plugins/vis_type_timeseries/public/components/aggs/std_deviation.js b/src/plugins/vis_type_timeseries/public/application/components/aggs/std_deviation.js similarity index 100% rename from src/legacy/core_plugins/vis_type_timeseries/public/components/aggs/std_deviation.js rename to src/plugins/vis_type_timeseries/public/application/components/aggs/std_deviation.js diff --git a/src/legacy/core_plugins/vis_type_timeseries/public/components/aggs/std_sibling.js b/src/plugins/vis_type_timeseries/public/application/components/aggs/std_sibling.js similarity index 100% rename from src/legacy/core_plugins/vis_type_timeseries/public/components/aggs/std_sibling.js rename to src/plugins/vis_type_timeseries/public/application/components/aggs/std_sibling.js diff --git a/src/legacy/core_plugins/vis_type_timeseries/public/components/aggs/temporary_unsupported_agg.js b/src/plugins/vis_type_timeseries/public/application/components/aggs/temporary_unsupported_agg.js similarity index 100% rename from src/legacy/core_plugins/vis_type_timeseries/public/components/aggs/temporary_unsupported_agg.js rename to src/plugins/vis_type_timeseries/public/application/components/aggs/temporary_unsupported_agg.js diff --git a/src/legacy/core_plugins/vis_type_timeseries/public/components/aggs/top_hit.js b/src/plugins/vis_type_timeseries/public/application/components/aggs/top_hit.js similarity index 100% rename from src/legacy/core_plugins/vis_type_timeseries/public/components/aggs/top_hit.js rename to src/plugins/vis_type_timeseries/public/application/components/aggs/top_hit.js diff --git a/src/legacy/core_plugins/vis_type_timeseries/public/components/aggs/unsupported_agg.js b/src/plugins/vis_type_timeseries/public/application/components/aggs/unsupported_agg.js similarity index 100% rename from src/legacy/core_plugins/vis_type_timeseries/public/components/aggs/unsupported_agg.js rename to src/plugins/vis_type_timeseries/public/application/components/aggs/unsupported_agg.js diff --git a/src/legacy/core_plugins/vis_type_timeseries/public/components/aggs/vars.js b/src/plugins/vis_type_timeseries/public/application/components/aggs/vars.js similarity index 100% rename from src/legacy/core_plugins/vis_type_timeseries/public/components/aggs/vars.js rename to src/plugins/vis_type_timeseries/public/application/components/aggs/vars.js diff --git a/src/legacy/core_plugins/vis_type_timeseries/public/components/annotations_editor.js b/src/plugins/vis_type_timeseries/public/application/components/annotations_editor.js similarity index 100% rename from src/legacy/core_plugins/vis_type_timeseries/public/components/annotations_editor.js rename to src/plugins/vis_type_timeseries/public/application/components/annotations_editor.js diff --git a/src/legacy/core_plugins/vis_type_timeseries/public/components/color_picker.js b/src/plugins/vis_type_timeseries/public/application/components/color_picker.js similarity index 100% rename from src/legacy/core_plugins/vis_type_timeseries/public/components/color_picker.js rename to src/plugins/vis_type_timeseries/public/application/components/color_picker.js diff --git a/src/legacy/core_plugins/vis_type_timeseries/public/components/color_picker.test.js b/src/plugins/vis_type_timeseries/public/application/components/color_picker.test.js similarity index 100% rename from src/legacy/core_plugins/vis_type_timeseries/public/components/color_picker.test.js rename to src/plugins/vis_type_timeseries/public/application/components/color_picker.test.js diff --git a/src/legacy/core_plugins/vis_type_timeseries/public/components/color_rules.js b/src/plugins/vis_type_timeseries/public/application/components/color_rules.js similarity index 100% rename from src/legacy/core_plugins/vis_type_timeseries/public/components/color_rules.js rename to src/plugins/vis_type_timeseries/public/application/components/color_rules.js diff --git a/src/legacy/core_plugins/vis_type_timeseries/public/components/color_rules.test.js b/src/plugins/vis_type_timeseries/public/application/components/color_rules.test.js similarity index 100% rename from src/legacy/core_plugins/vis_type_timeseries/public/components/color_rules.test.js rename to src/plugins/vis_type_timeseries/public/application/components/color_rules.test.js diff --git a/src/legacy/core_plugins/vis_type_timeseries/public/components/custom_color_picker.js b/src/plugins/vis_type_timeseries/public/application/components/custom_color_picker.js similarity index 100% rename from src/legacy/core_plugins/vis_type_timeseries/public/components/custom_color_picker.js rename to src/plugins/vis_type_timeseries/public/application/components/custom_color_picker.js diff --git a/src/legacy/core_plugins/vis_type_timeseries/public/components/data_format_picker.js b/src/plugins/vis_type_timeseries/public/application/components/data_format_picker.js similarity index 100% rename from src/legacy/core_plugins/vis_type_timeseries/public/components/data_format_picker.js rename to src/plugins/vis_type_timeseries/public/application/components/data_format_picker.js diff --git a/src/legacy/core_plugins/vis_type_timeseries/public/components/error.js b/src/plugins/vis_type_timeseries/public/application/components/error.js similarity index 100% rename from src/legacy/core_plugins/vis_type_timeseries/public/components/error.js rename to src/plugins/vis_type_timeseries/public/application/components/error.js diff --git a/src/legacy/core_plugins/vis_type_timeseries/public/components/icon_select/__snapshots__/icon_select.test.js.snap b/src/plugins/vis_type_timeseries/public/application/components/icon_select/__snapshots__/icon_select.test.js.snap similarity index 100% rename from src/legacy/core_plugins/vis_type_timeseries/public/components/icon_select/__snapshots__/icon_select.test.js.snap rename to src/plugins/vis_type_timeseries/public/application/components/icon_select/__snapshots__/icon_select.test.js.snap diff --git a/src/legacy/core_plugins/vis_type_timeseries/public/components/icon_select/icon_select.js b/src/plugins/vis_type_timeseries/public/application/components/icon_select/icon_select.js similarity index 100% rename from src/legacy/core_plugins/vis_type_timeseries/public/components/icon_select/icon_select.js rename to src/plugins/vis_type_timeseries/public/application/components/icon_select/icon_select.js diff --git a/src/legacy/core_plugins/vis_type_timeseries/public/components/icon_select/icon_select.test.js b/src/plugins/vis_type_timeseries/public/application/components/icon_select/icon_select.test.js similarity index 100% rename from src/legacy/core_plugins/vis_type_timeseries/public/components/icon_select/icon_select.test.js rename to src/plugins/vis_type_timeseries/public/application/components/icon_select/icon_select.test.js diff --git a/src/legacy/core_plugins/vis_type_timeseries/public/components/index_pattern.js b/src/plugins/vis_type_timeseries/public/application/components/index_pattern.js similarity index 100% rename from src/legacy/core_plugins/vis_type_timeseries/public/components/index_pattern.js rename to src/plugins/vis_type_timeseries/public/application/components/index_pattern.js diff --git a/src/legacy/core_plugins/vis_type_timeseries/public/components/lib/agg_to_component.js b/src/plugins/vis_type_timeseries/public/application/components/lib/agg_to_component.js similarity index 96% rename from src/legacy/core_plugins/vis_type_timeseries/public/components/lib/agg_to_component.js rename to src/plugins/vis_type_timeseries/public/application/components/lib/agg_to_component.js index ca40d60f20848..a53192afafdcc 100644 --- a/src/legacy/core_plugins/vis_type_timeseries/public/components/lib/agg_to_component.js +++ b/src/plugins/vis_type_timeseries/public/application/components/lib/agg_to_component.js @@ -33,6 +33,7 @@ import { PercentileRankAgg } from '../aggs/percentile_rank'; import { Static } from '../aggs/static'; import { MathAgg } from '../aggs/math'; import { TopHitAgg } from '../aggs/top_hit'; +import { PositiveRateAgg } from '../aggs/positive_rate'; export const aggToComponent = { count: StandardAgg, @@ -65,4 +66,5 @@ export const aggToComponent = { static: Static, math: MathAgg, top_hit: TopHitAgg, + positive_rate: PositiveRateAgg, }; diff --git a/src/legacy/core_plugins/vis_type_timeseries/public/components/lib/calculate_siblings.js b/src/plugins/vis_type_timeseries/public/application/components/lib/calculate_siblings.js similarity index 100% rename from src/legacy/core_plugins/vis_type_timeseries/public/components/lib/calculate_siblings.js rename to src/plugins/vis_type_timeseries/public/application/components/lib/calculate_siblings.js diff --git a/src/legacy/core_plugins/vis_type_timeseries/public/components/lib/calculate_siblings.test.js b/src/plugins/vis_type_timeseries/public/application/components/lib/calculate_siblings.test.js similarity index 100% rename from src/legacy/core_plugins/vis_type_timeseries/public/components/lib/calculate_siblings.test.js rename to src/plugins/vis_type_timeseries/public/application/components/lib/calculate_siblings.test.js diff --git a/src/legacy/core_plugins/vis_type_timeseries/public/components/lib/charts.js b/src/plugins/vis_type_timeseries/public/application/components/lib/charts.js similarity index 100% rename from src/legacy/core_plugins/vis_type_timeseries/public/components/lib/charts.js rename to src/plugins/vis_type_timeseries/public/application/components/lib/charts.js diff --git a/src/legacy/core_plugins/vis_type_timeseries/public/components/lib/collection_actions.js b/src/plugins/vis_type_timeseries/public/application/components/lib/collection_actions.js similarity index 82% rename from src/legacy/core_plugins/vis_type_timeseries/public/components/lib/collection_actions.js rename to src/plugins/vis_type_timeseries/public/application/components/lib/collection_actions.js index 79cbe98b3d3db..3eae18111bb4d 100644 --- a/src/legacy/core_plugins/vis_type_timeseries/public/components/lib/collection_actions.js +++ b/src/plugins/vis_type_timeseries/public/application/components/lib/collection_actions.js @@ -18,7 +18,6 @@ */ import uuid from 'uuid'; -import _ from 'lodash'; const newFn = () => ({ id: uuid.v1() }); @@ -30,9 +29,7 @@ export function handleChange(props, doc) { if (row.id === doc.id) return doc; return row; }); - if (_.isFunction(props.onChange)) { - props.onChange(_.assign({}, model, part)); - } + props.onChange?.({ ...model, ...part }); } export function handleDelete(props, doc) { @@ -40,20 +37,15 @@ export function handleDelete(props, doc) { const collection = model[name] || []; const part = {}; part[name] = collection.filter(row => row.id !== doc.id); - if (_.isFunction(props.onChange)) { - props.onChange(_.assign({}, model, part)); - } + props.onChange?.({ ...model, ...part }); } export function handleAdd(props, fn = newFn) { - if (!_.isFunction(fn)) fn = newFn; const { model, name } = props; const collection = model[name] || []; const part = {}; part[name] = collection.concat([fn()]); - if (_.isFunction(props.onChange)) { - props.onChange(_.assign({}, model, part)); - } + props.onChange?.({ ...model, ...part }); } export const collectionActions = { handleAdd, handleDelete, handleChange }; diff --git a/src/legacy/core_plugins/vis_type_timeseries/public/components/lib/collection_actions.test.js b/src/plugins/vis_type_timeseries/public/application/components/lib/collection_actions.test.js similarity index 100% rename from src/legacy/core_plugins/vis_type_timeseries/public/components/lib/collection_actions.test.js rename to src/plugins/vis_type_timeseries/public/application/components/lib/collection_actions.test.js diff --git a/src/legacy/core_plugins/vis_type_timeseries/public/components/lib/convert_series_to_vars.js b/src/plugins/vis_type_timeseries/public/application/components/lib/convert_series_to_vars.js similarity index 100% rename from src/legacy/core_plugins/vis_type_timeseries/public/components/lib/convert_series_to_vars.js rename to src/plugins/vis_type_timeseries/public/application/components/lib/convert_series_to_vars.js diff --git a/src/legacy/core_plugins/vis_type_timeseries/public/components/lib/convert_series_to_vars.test.js b/src/plugins/vis_type_timeseries/public/application/components/lib/convert_series_to_vars.test.js similarity index 100% rename from src/legacy/core_plugins/vis_type_timeseries/public/components/lib/convert_series_to_vars.test.js rename to src/plugins/vis_type_timeseries/public/application/components/lib/convert_series_to_vars.test.js diff --git a/src/legacy/core_plugins/vis_type_timeseries/public/components/lib/create_change_handler.js b/src/plugins/vis_type_timeseries/public/application/components/lib/create_change_handler.js similarity index 100% rename from src/legacy/core_plugins/vis_type_timeseries/public/components/lib/create_change_handler.js rename to src/plugins/vis_type_timeseries/public/application/components/lib/create_change_handler.js diff --git a/src/legacy/core_plugins/vis_type_timeseries/public/components/lib/create_number_handler.js b/src/plugins/vis_type_timeseries/public/application/components/lib/create_number_handler.js similarity index 92% rename from src/legacy/core_plugins/vis_type_timeseries/public/components/lib/create_number_handler.js rename to src/plugins/vis_type_timeseries/public/application/components/lib/create_number_handler.js index fd432add43295..3bc8fd87f73dd 100644 --- a/src/legacy/core_plugins/vis_type_timeseries/public/components/lib/create_number_handler.js +++ b/src/plugins/vis_type_timeseries/public/application/components/lib/create_number_handler.js @@ -25,8 +25,6 @@ export const createNumberHandler = handleChange => { if (!detectIE() || e.keyCode === 13) e.preventDefault(); const value = Number(_.get(e, 'target.value', defaultValue)); - if (_.isFunction(handleChange)) { - return handleChange({ [name]: value }); - } + return handleChange?.({ [name]: value }); }; }; diff --git a/src/legacy/core_plugins/vis_type_timeseries/public/components/lib/create_number_handler.test.js b/src/plugins/vis_type_timeseries/public/application/components/lib/create_number_handler.test.js similarity index 100% rename from src/legacy/core_plugins/vis_type_timeseries/public/components/lib/create_number_handler.test.js rename to src/plugins/vis_type_timeseries/public/application/components/lib/create_number_handler.test.js diff --git a/src/legacy/core_plugins/vis_type_timeseries/public/components/lib/create_select_handler.js b/src/plugins/vis_type_timeseries/public/application/components/lib/create_select_handler.js similarity index 86% rename from src/legacy/core_plugins/vis_type_timeseries/public/components/lib/create_select_handler.js rename to src/plugins/vis_type_timeseries/public/application/components/lib/create_select_handler.js index 30937b97d353f..cfc6a4dc57871 100644 --- a/src/legacy/core_plugins/vis_type_timeseries/public/components/lib/create_select_handler.js +++ b/src/plugins/vis_type_timeseries/public/application/components/lib/create_select_handler.js @@ -21,10 +21,8 @@ import _ from 'lodash'; export const createSelectHandler = handleChange => { return name => selectedOptions => { - if (_.isFunction(handleChange)) { - return handleChange({ - [name]: _.get(selectedOptions, '[0].value', null), - }); - } + return handleChange?.({ + [name]: _.get(selectedOptions, '[0].value', null), + }); }; }; diff --git a/src/legacy/core_plugins/vis_type_timeseries/public/components/lib/create_select_handler.test.js b/src/plugins/vis_type_timeseries/public/application/components/lib/create_select_handler.test.js similarity index 100% rename from src/legacy/core_plugins/vis_type_timeseries/public/components/lib/create_select_handler.test.js rename to src/plugins/vis_type_timeseries/public/application/components/lib/create_select_handler.test.js diff --git a/src/legacy/core_plugins/vis_type_timeseries/public/components/lib/create_text_handler.js b/src/plugins/vis_type_timeseries/public/application/components/lib/create_text_handler.js similarity index 92% rename from src/legacy/core_plugins/vis_type_timeseries/public/components/lib/create_text_handler.js rename to src/plugins/vis_type_timeseries/public/application/components/lib/create_text_handler.js index 8e2624488d954..82cc071b59d51 100644 --- a/src/legacy/core_plugins/vis_type_timeseries/public/components/lib/create_text_handler.js +++ b/src/plugins/vis_type_timeseries/public/application/components/lib/create_text_handler.js @@ -26,8 +26,6 @@ export const createTextHandler = handleChange => { if (!detectIE() || e.keyCode === 13) e.preventDefault(); const value = _.get(e, 'target.value', defaultValue); - if (_.isFunction(handleChange)) { - return handleChange({ [name]: value }); - } + return handleChange?.({ [name]: value }); }; }; diff --git a/src/legacy/core_plugins/vis_type_timeseries/public/components/lib/create_text_handler.test.js b/src/plugins/vis_type_timeseries/public/application/components/lib/create_text_handler.test.js similarity index 100% rename from src/legacy/core_plugins/vis_type_timeseries/public/components/lib/create_text_handler.test.js rename to src/plugins/vis_type_timeseries/public/application/components/lib/create_text_handler.test.js diff --git a/src/legacy/core_plugins/vis_type_timeseries/public/components/lib/create_xaxis_formatter.js b/src/plugins/vis_type_timeseries/public/application/components/lib/create_xaxis_formatter.js similarity index 100% rename from src/legacy/core_plugins/vis_type_timeseries/public/components/lib/create_xaxis_formatter.js rename to src/plugins/vis_type_timeseries/public/application/components/lib/create_xaxis_formatter.js diff --git a/src/legacy/core_plugins/vis_type_timeseries/public/components/lib/detect_ie.js b/src/plugins/vis_type_timeseries/public/application/components/lib/detect_ie.js similarity index 100% rename from src/legacy/core_plugins/vis_type_timeseries/public/components/lib/detect_ie.js rename to src/plugins/vis_type_timeseries/public/application/components/lib/detect_ie.js diff --git a/src/legacy/core_plugins/vis_type_timeseries/public/components/lib/durations.js b/src/plugins/vis_type_timeseries/public/application/components/lib/durations.js similarity index 100% rename from src/legacy/core_plugins/vis_type_timeseries/public/components/lib/durations.js rename to src/plugins/vis_type_timeseries/public/application/components/lib/durations.js diff --git a/src/legacy/core_plugins/vis_type_timeseries/public/components/lib/durations.test.js b/src/plugins/vis_type_timeseries/public/application/components/lib/durations.test.js similarity index 100% rename from src/legacy/core_plugins/vis_type_timeseries/public/components/lib/durations.test.js rename to src/plugins/vis_type_timeseries/public/application/components/lib/durations.test.js diff --git a/src/legacy/core_plugins/vis_type_timeseries/public/components/lib/get_axis_label_string.js b/src/plugins/vis_type_timeseries/public/application/components/lib/get_axis_label_string.js similarity index 100% rename from src/legacy/core_plugins/vis_type_timeseries/public/components/lib/get_axis_label_string.js rename to src/plugins/vis_type_timeseries/public/application/components/lib/get_axis_label_string.js diff --git a/src/legacy/core_plugins/vis_type_timeseries/public/components/lib/get_axis_label_string.test.js b/src/plugins/vis_type_timeseries/public/application/components/lib/get_axis_label_string.test.js similarity index 100% rename from src/legacy/core_plugins/vis_type_timeseries/public/components/lib/get_axis_label_string.test.js rename to src/plugins/vis_type_timeseries/public/application/components/lib/get_axis_label_string.test.js diff --git a/src/legacy/core_plugins/vis_type_timeseries/public/components/lib/get_default_query_language.js b/src/plugins/vis_type_timeseries/public/application/components/lib/get_default_query_language.js similarity index 94% rename from src/legacy/core_plugins/vis_type_timeseries/public/components/lib/get_default_query_language.js rename to src/plugins/vis_type_timeseries/public/application/components/lib/get_default_query_language.js index 26723da5ab5c9..972f937ad109d 100644 --- a/src/legacy/core_plugins/vis_type_timeseries/public/components/lib/get_default_query_language.js +++ b/src/plugins/vis_type_timeseries/public/application/components/lib/get_default_query_language.js @@ -17,7 +17,7 @@ * under the License. */ -import { getUISettings } from '../../services'; +import { getUISettings } from '../../../services'; export function getDefaultQueryLanguage() { return getUISettings().get('search:queryLanguage'); diff --git a/src/legacy/core_plugins/vis_type_timeseries/public/components/lib/get_display_name.js b/src/plugins/vis_type_timeseries/public/application/components/lib/get_display_name.js similarity index 100% rename from src/legacy/core_plugins/vis_type_timeseries/public/components/lib/get_display_name.js rename to src/plugins/vis_type_timeseries/public/application/components/lib/get_display_name.js diff --git a/src/legacy/core_plugins/vis_type_timeseries/public/components/lib/get_interval.js b/src/plugins/vis_type_timeseries/public/application/components/lib/get_interval.js similarity index 100% rename from src/legacy/core_plugins/vis_type_timeseries/public/components/lib/get_interval.js rename to src/plugins/vis_type_timeseries/public/application/components/lib/get_interval.js diff --git a/src/legacy/core_plugins/vis_type_timeseries/public/components/lib/new_metric_agg_fn.js b/src/plugins/vis_type_timeseries/public/application/components/lib/new_metric_agg_fn.js similarity index 100% rename from src/legacy/core_plugins/vis_type_timeseries/public/components/lib/new_metric_agg_fn.js rename to src/plugins/vis_type_timeseries/public/application/components/lib/new_metric_agg_fn.js diff --git a/src/legacy/core_plugins/vis_type_timeseries/public/components/lib/new_series_fn.js b/src/plugins/vis_type_timeseries/public/application/components/lib/new_series_fn.js similarity index 100% rename from src/legacy/core_plugins/vis_type_timeseries/public/components/lib/new_series_fn.js rename to src/plugins/vis_type_timeseries/public/application/components/lib/new_series_fn.js diff --git a/src/legacy/core_plugins/vis_type_timeseries/public/components/lib/re_id_series.js b/src/plugins/vis_type_timeseries/public/application/components/lib/re_id_series.js similarity index 100% rename from src/legacy/core_plugins/vis_type_timeseries/public/components/lib/re_id_series.js rename to src/plugins/vis_type_timeseries/public/application/components/lib/re_id_series.js diff --git a/src/legacy/core_plugins/vis_type_timeseries/public/components/lib/re_id_series.test.js b/src/plugins/vis_type_timeseries/public/application/components/lib/re_id_series.test.js similarity index 100% rename from src/legacy/core_plugins/vis_type_timeseries/public/components/lib/re_id_series.test.js rename to src/plugins/vis_type_timeseries/public/application/components/lib/re_id_series.test.js diff --git a/src/legacy/core_plugins/vis_type_timeseries/public/components/lib/reorder.js b/src/plugins/vis_type_timeseries/public/application/components/lib/reorder.js similarity index 100% rename from src/legacy/core_plugins/vis_type_timeseries/public/components/lib/reorder.js rename to src/plugins/vis_type_timeseries/public/application/components/lib/reorder.js diff --git a/src/legacy/core_plugins/vis_type_timeseries/public/components/lib/replace_vars.js b/src/plugins/vis_type_timeseries/public/application/components/lib/replace_vars.js similarity index 100% rename from src/legacy/core_plugins/vis_type_timeseries/public/components/lib/replace_vars.js rename to src/plugins/vis_type_timeseries/public/application/components/lib/replace_vars.js diff --git a/src/legacy/core_plugins/vis_type_timeseries/public/components/lib/replace_vars.test.js b/src/plugins/vis_type_timeseries/public/application/components/lib/replace_vars.test.js similarity index 100% rename from src/legacy/core_plugins/vis_type_timeseries/public/components/lib/replace_vars.test.js rename to src/plugins/vis_type_timeseries/public/application/components/lib/replace_vars.test.js diff --git a/src/legacy/core_plugins/vis_type_timeseries/public/components/lib/series_change_handler.js b/src/plugins/vis_type_timeseries/public/application/components/lib/series_change_handler.js similarity index 100% rename from src/legacy/core_plugins/vis_type_timeseries/public/components/lib/series_change_handler.js rename to src/plugins/vis_type_timeseries/public/application/components/lib/series_change_handler.js diff --git a/src/legacy/core_plugins/vis_type_timeseries/public/components/lib/stacked.js b/src/plugins/vis_type_timeseries/public/application/components/lib/stacked.js similarity index 100% rename from src/legacy/core_plugins/vis_type_timeseries/public/components/lib/stacked.js rename to src/plugins/vis_type_timeseries/public/application/components/lib/stacked.js diff --git a/src/legacy/core_plugins/vis_type_timeseries/public/components/lib/tick_formatter.js b/src/plugins/vis_type_timeseries/public/application/components/lib/tick_formatter.js similarity index 97% rename from src/legacy/core_plugins/vis_type_timeseries/public/components/lib/tick_formatter.js rename to src/plugins/vis_type_timeseries/public/application/components/lib/tick_formatter.js index 3ab8e0f6b885e..fd316e66a16fb 100644 --- a/src/legacy/core_plugins/vis_type_timeseries/public/components/lib/tick_formatter.js +++ b/src/plugins/vis_type_timeseries/public/application/components/lib/tick_formatter.js @@ -20,7 +20,7 @@ import handlebars from 'handlebars/dist/handlebars'; import { isNumber } from 'lodash'; import { inputFormats, outputFormats, isDuration } from '../lib/durations'; -import { getFieldFormats } from '../../services'; +import { getFieldFormats } from '../../../services'; export const createTickFormatter = (format = '0,0.[00]', template, getConfig = null) => { const fieldFormats = getFieldFormats(); diff --git a/src/legacy/core_plugins/vis_type_timeseries/public/components/lib/tick_formatter.test.js b/src/plugins/vis_type_timeseries/public/application/components/lib/tick_formatter.test.js similarity index 98% rename from src/legacy/core_plugins/vis_type_timeseries/public/components/lib/tick_formatter.test.js rename to src/plugins/vis_type_timeseries/public/application/components/lib/tick_formatter.test.js index e87cba126bb46..ee10b254a9e15 100644 --- a/src/legacy/core_plugins/vis_type_timeseries/public/components/lib/tick_formatter.test.js +++ b/src/plugins/vis_type_timeseries/public/application/components/lib/tick_formatter.test.js @@ -19,7 +19,7 @@ import { createTickFormatter } from './tick_formatter'; import { getFieldFormatsRegistry } from '../../../../../../test_utils/public/stub_field_formats'; -import { setFieldFormats } from '../../services'; +import { setFieldFormats } from '../../../services'; const mockUiSettings = { get: item => { diff --git a/src/legacy/core_plugins/vis_type_timeseries/public/components/markdown_editor.js b/src/plugins/vis_type_timeseries/public/application/components/markdown_editor.js similarity index 100% rename from src/legacy/core_plugins/vis_type_timeseries/public/components/markdown_editor.js rename to src/plugins/vis_type_timeseries/public/application/components/markdown_editor.js diff --git a/src/legacy/core_plugins/vis_type_timeseries/public/components/no_data.js b/src/plugins/vis_type_timeseries/public/application/components/no_data.js similarity index 100% rename from src/legacy/core_plugins/vis_type_timeseries/public/components/no_data.js rename to src/plugins/vis_type_timeseries/public/application/components/no_data.js diff --git a/src/legacy/core_plugins/vis_type_timeseries/public/components/panel_config.js b/src/plugins/vis_type_timeseries/public/application/components/panel_config.js similarity index 100% rename from src/legacy/core_plugins/vis_type_timeseries/public/components/panel_config.js rename to src/plugins/vis_type_timeseries/public/application/components/panel_config.js diff --git a/src/legacy/core_plugins/vis_type_timeseries/public/components/panel_config/_index.scss b/src/plugins/vis_type_timeseries/public/application/components/panel_config/_index.scss similarity index 100% rename from src/legacy/core_plugins/vis_type_timeseries/public/components/panel_config/_index.scss rename to src/plugins/vis_type_timeseries/public/application/components/panel_config/_index.scss diff --git a/src/legacy/core_plugins/vis_type_timeseries/public/components/panel_config/_panel_config.scss b/src/plugins/vis_type_timeseries/public/application/components/panel_config/_panel_config.scss similarity index 100% rename from src/legacy/core_plugins/vis_type_timeseries/public/components/panel_config/_panel_config.scss rename to src/plugins/vis_type_timeseries/public/application/components/panel_config/_panel_config.scss diff --git a/src/legacy/core_plugins/vis_type_timeseries/public/components/panel_config/gauge.js b/src/plugins/vis_type_timeseries/public/application/components/panel_config/gauge.js similarity index 100% rename from src/legacy/core_plugins/vis_type_timeseries/public/components/panel_config/gauge.js rename to src/plugins/vis_type_timeseries/public/application/components/panel_config/gauge.js diff --git a/src/legacy/core_plugins/vis_type_timeseries/public/components/panel_config/gauge.test.js b/src/plugins/vis_type_timeseries/public/application/components/panel_config/gauge.test.js similarity index 100% rename from src/legacy/core_plugins/vis_type_timeseries/public/components/panel_config/gauge.test.js rename to src/plugins/vis_type_timeseries/public/application/components/panel_config/gauge.test.js diff --git a/src/legacy/core_plugins/vis_type_timeseries/public/components/panel_config/markdown.js b/src/plugins/vis_type_timeseries/public/application/components/panel_config/markdown.js similarity index 100% rename from src/legacy/core_plugins/vis_type_timeseries/public/components/panel_config/markdown.js rename to src/plugins/vis_type_timeseries/public/application/components/panel_config/markdown.js diff --git a/src/legacy/core_plugins/vis_type_timeseries/public/components/panel_config/metric.js b/src/plugins/vis_type_timeseries/public/application/components/panel_config/metric.js similarity index 100% rename from src/legacy/core_plugins/vis_type_timeseries/public/components/panel_config/metric.js rename to src/plugins/vis_type_timeseries/public/application/components/panel_config/metric.js diff --git a/src/legacy/core_plugins/vis_type_timeseries/public/components/panel_config/table.js b/src/plugins/vis_type_timeseries/public/application/components/panel_config/table.js similarity index 100% rename from src/legacy/core_plugins/vis_type_timeseries/public/components/panel_config/table.js rename to src/plugins/vis_type_timeseries/public/application/components/panel_config/table.js diff --git a/src/legacy/core_plugins/vis_type_timeseries/public/components/panel_config/timeseries.js b/src/plugins/vis_type_timeseries/public/application/components/panel_config/timeseries.js similarity index 100% rename from src/legacy/core_plugins/vis_type_timeseries/public/components/panel_config/timeseries.js rename to src/plugins/vis_type_timeseries/public/application/components/panel_config/timeseries.js diff --git a/src/legacy/core_plugins/vis_type_timeseries/public/components/panel_config/top_n.js b/src/plugins/vis_type_timeseries/public/application/components/panel_config/top_n.js similarity index 100% rename from src/legacy/core_plugins/vis_type_timeseries/public/components/panel_config/top_n.js rename to src/plugins/vis_type_timeseries/public/application/components/panel_config/top_n.js diff --git a/src/legacy/core_plugins/vis_type_timeseries/public/components/query_bar_wrapper.js b/src/plugins/vis_type_timeseries/public/application/components/query_bar_wrapper.js similarity index 100% rename from src/legacy/core_plugins/vis_type_timeseries/public/components/query_bar_wrapper.js rename to src/plugins/vis_type_timeseries/public/application/components/query_bar_wrapper.js diff --git a/src/legacy/core_plugins/vis_type_timeseries/public/components/series.js b/src/plugins/vis_type_timeseries/public/application/components/series.js similarity index 100% rename from src/legacy/core_plugins/vis_type_timeseries/public/components/series.js rename to src/plugins/vis_type_timeseries/public/application/components/series.js diff --git a/src/legacy/core_plugins/vis_type_timeseries/public/components/series_config.js b/src/plugins/vis_type_timeseries/public/application/components/series_config.js similarity index 100% rename from src/legacy/core_plugins/vis_type_timeseries/public/components/series_config.js rename to src/plugins/vis_type_timeseries/public/application/components/series_config.js diff --git a/src/legacy/core_plugins/vis_type_timeseries/public/components/series_drag_handler.js b/src/plugins/vis_type_timeseries/public/application/components/series_drag_handler.js similarity index 100% rename from src/legacy/core_plugins/vis_type_timeseries/public/components/series_drag_handler.js rename to src/plugins/vis_type_timeseries/public/application/components/series_drag_handler.js diff --git a/src/legacy/core_plugins/vis_type_timeseries/public/components/series_editor.js b/src/plugins/vis_type_timeseries/public/application/components/series_editor.js similarity index 100% rename from src/legacy/core_plugins/vis_type_timeseries/public/components/series_editor.js rename to src/plugins/vis_type_timeseries/public/application/components/series_editor.js diff --git a/src/legacy/core_plugins/vis_type_timeseries/public/components/split.js b/src/plugins/vis_type_timeseries/public/application/components/split.js similarity index 100% rename from src/legacy/core_plugins/vis_type_timeseries/public/components/split.js rename to src/plugins/vis_type_timeseries/public/application/components/split.js diff --git a/src/legacy/core_plugins/vis_type_timeseries/public/components/splits/__snapshots__/terms.test.js.snap b/src/plugins/vis_type_timeseries/public/application/components/splits/__snapshots__/terms.test.js.snap similarity index 100% rename from src/legacy/core_plugins/vis_type_timeseries/public/components/splits/__snapshots__/terms.test.js.snap rename to src/plugins/vis_type_timeseries/public/application/components/splits/__snapshots__/terms.test.js.snap diff --git a/src/legacy/core_plugins/vis_type_timeseries/public/components/splits/everything.js b/src/plugins/vis_type_timeseries/public/application/components/splits/everything.js similarity index 100% rename from src/legacy/core_plugins/vis_type_timeseries/public/components/splits/everything.js rename to src/plugins/vis_type_timeseries/public/application/components/splits/everything.js diff --git a/src/legacy/core_plugins/vis_type_timeseries/public/components/splits/filter.js b/src/plugins/vis_type_timeseries/public/application/components/splits/filter.js similarity index 100% rename from src/legacy/core_plugins/vis_type_timeseries/public/components/splits/filter.js rename to src/plugins/vis_type_timeseries/public/application/components/splits/filter.js diff --git a/src/legacy/core_plugins/vis_type_timeseries/public/components/splits/filter_items.js b/src/plugins/vis_type_timeseries/public/application/components/splits/filter_items.js similarity index 100% rename from src/legacy/core_plugins/vis_type_timeseries/public/components/splits/filter_items.js rename to src/plugins/vis_type_timeseries/public/application/components/splits/filter_items.js diff --git a/src/legacy/core_plugins/vis_type_timeseries/public/components/splits/filters.js b/src/plugins/vis_type_timeseries/public/application/components/splits/filters.js similarity index 100% rename from src/legacy/core_plugins/vis_type_timeseries/public/components/splits/filters.js rename to src/plugins/vis_type_timeseries/public/application/components/splits/filters.js diff --git a/src/legacy/core_plugins/vis_type_timeseries/public/components/splits/group_by_select.js b/src/plugins/vis_type_timeseries/public/application/components/splits/group_by_select.js similarity index 100% rename from src/legacy/core_plugins/vis_type_timeseries/public/components/splits/group_by_select.js rename to src/plugins/vis_type_timeseries/public/application/components/splits/group_by_select.js diff --git a/src/legacy/core_plugins/vis_type_timeseries/public/components/splits/terms.js b/src/plugins/vis_type_timeseries/public/application/components/splits/terms.js similarity index 100% rename from src/legacy/core_plugins/vis_type_timeseries/public/components/splits/terms.js rename to src/plugins/vis_type_timeseries/public/application/components/splits/terms.js diff --git a/src/legacy/core_plugins/vis_type_timeseries/public/components/splits/terms.test.js b/src/plugins/vis_type_timeseries/public/application/components/splits/terms.test.js similarity index 100% rename from src/legacy/core_plugins/vis_type_timeseries/public/components/splits/terms.test.js rename to src/plugins/vis_type_timeseries/public/application/components/splits/terms.test.js diff --git a/src/legacy/core_plugins/vis_type_timeseries/public/components/splits/unsupported_split.js b/src/plugins/vis_type_timeseries/public/application/components/splits/unsupported_split.js similarity index 100% rename from src/legacy/core_plugins/vis_type_timeseries/public/components/splits/unsupported_split.js rename to src/plugins/vis_type_timeseries/public/application/components/splits/unsupported_split.js diff --git a/src/legacy/core_plugins/vis_type_timeseries/public/components/svg/bomb_icon.js b/src/plugins/vis_type_timeseries/public/application/components/svg/bomb_icon.js similarity index 100% rename from src/legacy/core_plugins/vis_type_timeseries/public/components/svg/bomb_icon.js rename to src/plugins/vis_type_timeseries/public/application/components/svg/bomb_icon.js diff --git a/src/legacy/core_plugins/vis_type_timeseries/public/components/svg/fire_icon.js b/src/plugins/vis_type_timeseries/public/application/components/svg/fire_icon.js similarity index 100% rename from src/legacy/core_plugins/vis_type_timeseries/public/components/svg/fire_icon.js rename to src/plugins/vis_type_timeseries/public/application/components/svg/fire_icon.js diff --git a/src/legacy/core_plugins/vis_type_timeseries/public/components/vis_editor.js b/src/plugins/vis_type_timeseries/public/application/components/vis_editor.js similarity index 99% rename from src/legacy/core_plugins/vis_type_timeseries/public/components/vis_editor.js rename to src/plugins/vis_type_timeseries/public/application/components/vis_editor.js index b4845696fc8c0..7075e86eb56bf 100644 --- a/src/legacy/core_plugins/vis_type_timeseries/public/components/vis_editor.js +++ b/src/plugins/vis_type_timeseries/public/application/components/vis_editor.js @@ -30,7 +30,7 @@ import { createBrushHandler } from '../lib/create_brush_handler'; import { fetchFields } from '../lib/fetch_fields'; import { extractIndexPatterns } from '../../../../../plugins/vis_type_timeseries/common/extract_index_patterns'; import { esKuery } from '../../../../../plugins/data/public'; -import { getSavedObjectsClient, getUISettings, getDataStart, getCoreStart } from '../services'; +import { getSavedObjectsClient, getUISettings, getDataStart, getCoreStart } from '../../services'; import { CoreStartContextProvider } from '../contexts/query_input_bar_context'; import { KibanaContextProvider } from '../../../../../plugins/kibana_react/public'; @@ -96,7 +96,7 @@ export class VisEditor extends Component { return true; }; - handleChange = async partialModel => { + handleChange = partialModel => { if (isEmpty(partialModel)) { return; } diff --git a/src/legacy/core_plugins/vis_type_timeseries/public/components/vis_editor_visualization.js b/src/plugins/vis_type_timeseries/public/application/components/vis_editor_visualization.js similarity index 100% rename from src/legacy/core_plugins/vis_type_timeseries/public/components/vis_editor_visualization.js rename to src/plugins/vis_type_timeseries/public/application/components/vis_editor_visualization.js diff --git a/src/legacy/core_plugins/vis_type_timeseries/public/components/vis_picker.js b/src/plugins/vis_type_timeseries/public/application/components/vis_picker.js similarity index 100% rename from src/legacy/core_plugins/vis_type_timeseries/public/components/vis_picker.js rename to src/plugins/vis_type_timeseries/public/application/components/vis_picker.js diff --git a/src/legacy/core_plugins/vis_type_timeseries/public/components/vis_types/_index.scss b/src/plugins/vis_type_timeseries/public/application/components/vis_types/_index.scss similarity index 100% rename from src/legacy/core_plugins/vis_type_timeseries/public/components/vis_types/_index.scss rename to src/plugins/vis_type_timeseries/public/application/components/vis_types/_index.scss diff --git a/src/legacy/core_plugins/vis_type_timeseries/public/components/vis_types/_vis_types.scss b/src/plugins/vis_type_timeseries/public/application/components/vis_types/_vis_types.scss similarity index 100% rename from src/legacy/core_plugins/vis_type_timeseries/public/components/vis_types/_vis_types.scss rename to src/plugins/vis_type_timeseries/public/application/components/vis_types/_vis_types.scss diff --git a/src/legacy/core_plugins/vis_type_timeseries/public/components/vis_types/gauge/series.js b/src/plugins/vis_type_timeseries/public/application/components/vis_types/gauge/series.js similarity index 100% rename from src/legacy/core_plugins/vis_type_timeseries/public/components/vis_types/gauge/series.js rename to src/plugins/vis_type_timeseries/public/application/components/vis_types/gauge/series.js diff --git a/src/legacy/core_plugins/vis_type_timeseries/public/components/vis_types/gauge/series.test.js b/src/plugins/vis_type_timeseries/public/application/components/vis_types/gauge/series.test.js similarity index 100% rename from src/legacy/core_plugins/vis_type_timeseries/public/components/vis_types/gauge/series.test.js rename to src/plugins/vis_type_timeseries/public/application/components/vis_types/gauge/series.test.js diff --git a/src/legacy/core_plugins/vis_type_timeseries/public/components/vis_types/gauge/vis.js b/src/plugins/vis_type_timeseries/public/application/components/vis_types/gauge/vis.js similarity index 100% rename from src/legacy/core_plugins/vis_type_timeseries/public/components/vis_types/gauge/vis.js rename to src/plugins/vis_type_timeseries/public/application/components/vis_types/gauge/vis.js diff --git a/src/legacy/core_plugins/vis_type_timeseries/public/components/vis_types/markdown/_markdown.scss b/src/plugins/vis_type_timeseries/public/application/components/vis_types/markdown/_markdown.scss similarity index 100% rename from src/legacy/core_plugins/vis_type_timeseries/public/components/vis_types/markdown/_markdown.scss rename to src/plugins/vis_type_timeseries/public/application/components/vis_types/markdown/_markdown.scss diff --git a/src/legacy/core_plugins/vis_type_timeseries/public/components/vis_types/markdown/series.js b/src/plugins/vis_type_timeseries/public/application/components/vis_types/markdown/series.js similarity index 100% rename from src/legacy/core_plugins/vis_type_timeseries/public/components/vis_types/markdown/series.js rename to src/plugins/vis_type_timeseries/public/application/components/vis_types/markdown/series.js diff --git a/src/legacy/core_plugins/vis_type_timeseries/public/components/vis_types/markdown/vis.js b/src/plugins/vis_type_timeseries/public/application/components/vis_types/markdown/vis.js similarity index 100% rename from src/legacy/core_plugins/vis_type_timeseries/public/components/vis_types/markdown/vis.js rename to src/plugins/vis_type_timeseries/public/application/components/vis_types/markdown/vis.js diff --git a/src/legacy/core_plugins/vis_type_timeseries/public/components/vis_types/metric/series.js b/src/plugins/vis_type_timeseries/public/application/components/vis_types/metric/series.js similarity index 100% rename from src/legacy/core_plugins/vis_type_timeseries/public/components/vis_types/metric/series.js rename to src/plugins/vis_type_timeseries/public/application/components/vis_types/metric/series.js diff --git a/src/legacy/core_plugins/vis_type_timeseries/public/components/vis_types/metric/series.test.js b/src/plugins/vis_type_timeseries/public/application/components/vis_types/metric/series.test.js similarity index 100% rename from src/legacy/core_plugins/vis_type_timeseries/public/components/vis_types/metric/series.test.js rename to src/plugins/vis_type_timeseries/public/application/components/vis_types/metric/series.test.js diff --git a/src/legacy/core_plugins/vis_type_timeseries/public/components/vis_types/metric/vis.js b/src/plugins/vis_type_timeseries/public/application/components/vis_types/metric/vis.js similarity index 100% rename from src/legacy/core_plugins/vis_type_timeseries/public/components/vis_types/metric/vis.js rename to src/plugins/vis_type_timeseries/public/application/components/vis_types/metric/vis.js diff --git a/src/legacy/core_plugins/vis_type_timeseries/public/components/vis_types/table/config.js b/src/plugins/vis_type_timeseries/public/application/components/vis_types/table/config.js similarity index 100% rename from src/legacy/core_plugins/vis_type_timeseries/public/components/vis_types/table/config.js rename to src/plugins/vis_type_timeseries/public/application/components/vis_types/table/config.js diff --git a/src/legacy/core_plugins/vis_type_timeseries/public/components/vis_types/table/is_sortable.js b/src/plugins/vis_type_timeseries/public/application/components/vis_types/table/is_sortable.js similarity index 100% rename from src/legacy/core_plugins/vis_type_timeseries/public/components/vis_types/table/is_sortable.js rename to src/plugins/vis_type_timeseries/public/application/components/vis_types/table/is_sortable.js diff --git a/src/legacy/core_plugins/vis_type_timeseries/public/components/vis_types/table/series.js b/src/plugins/vis_type_timeseries/public/application/components/vis_types/table/series.js similarity index 100% rename from src/legacy/core_plugins/vis_type_timeseries/public/components/vis_types/table/series.js rename to src/plugins/vis_type_timeseries/public/application/components/vis_types/table/series.js diff --git a/src/plugins/vis_type_timeseries/public/application/components/vis_types/table/vis.js b/src/plugins/vis_type_timeseries/public/application/components/vis_types/table/vis.js new file mode 100644 index 0000000000000..c6f1db149928c --- /dev/null +++ b/src/plugins/vis_type_timeseries/public/application/components/vis_types/table/vis.js @@ -0,0 +1,257 @@ +/* + * 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 _, { isArray, last, get } from 'lodash'; +import React, { Component } from 'react'; +import PropTypes from 'prop-types'; +import { createTickFormatter } from '../../lib/tick_formatter'; +import { calculateLabel } from '../../../../../../../plugins/vis_type_timeseries/common/calculate_label'; +import { isSortable } from './is_sortable'; +import { EuiToolTip, EuiIcon } from '@elastic/eui'; +import { replaceVars } from '../../lib/replace_vars'; +import { fieldFormats } from '../../../../../../../plugins/data/public'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { getFieldFormats } from '../../../../services'; + +import { METRIC_TYPES } from '../../../../../../../plugins/vis_type_timeseries/common/metric_types'; + +function getColor(rules, colorKey, value) { + let color; + if (rules) { + rules.forEach(rule => { + if (rule.operator && rule.value != null) { + if (_[rule.operator](value, rule.value)) { + color = rule[colorKey]; + } + } + }); + } + return color; +} + +export class TableVis extends Component { + constructor(props) { + super(props); + + const fieldFormatsService = getFieldFormats(); + const DateFormat = fieldFormatsService.getType(fieldFormats.FIELD_FORMAT_IDS.DATE); + + this.dateFormatter = new DateFormat({}, this.props.getConfig); + } + + get visibleSeries() { + return get(this.props, 'model.series', []).filter(series => !series.hidden); + } + + renderRow = row => { + const { model } = this.props; + let rowDisplay = model.pivot_type === 'date' ? this.dateFormatter.convert(row.key) : row.key; + if (model.drilldown_url) { + const url = replaceVars(model.drilldown_url, {}, { key: row.key }); + rowDisplay =
{rowDisplay}; + } + const columns = row.series + .filter(item => item) + .map(item => { + const column = this.visibleSeries.find(c => c.id === item.id); + if (!column) return null; + const formatter = createTickFormatter( + column.formatter, + column.value_template, + this.props.getConfig + ); + const value = formatter(item.last); + let trend; + if (column.trend_arrows) { + const trendIcon = item.slope > 0 ? 'sortUp' : 'sortDown'; + trend = ( + +   + + ); + } + const style = { color: getColor(column.color_rules, 'text', item.last) }; + return ( +
+ ); + }); + return ( + + + {columns} + + ); + }; + + renderHeader() { + const { model, uiState, onUiState } = this.props; + const stateKey = `${model.type}.sort`; + const sort = uiState.get(stateKey, { + column: '_default_', + order: 'asc', + }); + + const calculateHeaderLabel = (metric, item) => { + const defaultLabel = item.label || calculateLabel(metric, item.metrics); + + switch (metric.type) { + case METRIC_TYPES.PERCENTILE: + return `${defaultLabel} (${last(metric.percentiles).value || 0})`; + case METRIC_TYPES.PERCENTILE_RANK: + return `${defaultLabel} (${last(metric.values) || 0})`; + default: + return defaultLabel; + } + }; + + const columns = this.visibleSeries.map(item => { + const metric = last(item.metrics); + const label = calculateHeaderLabel(metric, item); + + const handleClick = () => { + if (!isSortable(metric)) return; + let order; + if (sort.column === item.id) { + order = sort.order === 'asc' ? 'desc' : 'asc'; + } else { + order = 'asc'; + } + onUiState(stateKey, { column: item.id, order }); + }; + let sortComponent; + if (isSortable(metric)) { + let sortIcon; + if (sort.column === item.id) { + sortIcon = sort.order === 'asc' ? 'sortUp' : 'sortDown'; + } else { + sortIcon = 'empty'; + } + sortComponent = ; + } + let headerContent = ( + + {label} {sortComponent} + + ); + if (!isSortable(metric)) { + headerContent = ( + + } + > + {headerContent} + + ); + } + + return ( + + ); + }); + const label = model.pivot_label || model.pivot_field || model.pivot_id; + let sortIcon; + if (sort.column === '_default_') { + sortIcon = sort.order === 'asc' ? 'sortUp' : 'sortDown'; + } else { + sortIcon = 'empty'; + } + const sortComponent = ; + const handleSortClick = () => { + let order; + if (sort.column === '_default_') { + order = sort.order === 'asc' ? 'desc' : 'asc'; + } else { + order = 'asc'; + } + onUiState(stateKey, { column: '_default_', order }); + }; + return ( + + + {columns} + + ); + } + + render() { + const { visData, model } = this.props; + const header = this.renderHeader(); + let rows; + + if (isArray(visData.series) && visData.series.length) { + rows = visData.series.map(this.renderRow); + } else { + const message = model.pivot_id ? ( + + ) : ( + + ); + rows = ( + + + + ); + } + return ( +
+
+ {value} + {trend} +
{rowDisplay}
+ {headerContent} +
+ {label} {sortComponent} +
{message}
+ {header} + {rows} +
+
+ ); + } +} + +TableVis.defaultProps = { + sort: {}, +}; + +TableVis.propTypes = { + visData: PropTypes.object, + model: PropTypes.object, + backgroundColor: PropTypes.string, + onPaginate: PropTypes.func, + onUiState: PropTypes.func, + uiState: PropTypes.object, + pageNumber: PropTypes.number, + getConfig: PropTypes.func, +}; diff --git a/src/plugins/vis_type_timeseries/public/application/components/vis_types/timeseries/config.js b/src/plugins/vis_type_timeseries/public/application/components/vis_types/timeseries/config.js new file mode 100644 index 0000000000000..d29b795b10ec8 --- /dev/null +++ b/src/plugins/vis_type_timeseries/public/application/components/vis_types/timeseries/config.js @@ -0,0 +1,590 @@ +/* + * 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 PropTypes from 'prop-types'; +import React from 'react'; +import { DataFormatPicker } from '../../data_format_picker'; +import { createSelectHandler } from '../../lib/create_select_handler'; +import { YesNo } from '../../yes_no'; +import { createTextHandler } from '../../lib/create_text_handler'; +import { IndexPattern } from '../../index_pattern'; +import { + htmlIdGenerator, + EuiComboBox, + EuiFlexGroup, + EuiFlexItem, + EuiFieldText, + EuiFormRow, + EuiCode, + EuiHorizontalRule, + EuiFieldNumber, + EuiFormLabel, + EuiSpacer, +} from '@elastic/eui'; +import { FormattedMessage, injectI18n } from '@kbn/i18n/react'; +import { getDefaultQueryLanguage } from '../../lib/get_default_query_language'; +import { QueryBarWrapper } from '../../query_bar_wrapper'; + +import { isPercentDisabled } from '../../lib/stacked'; +import { STACKED_OPTIONS } from '../../../visualizations/constants/chart'; + +export const TimeseriesConfig = injectI18n(function(props) { + const handleSelectChange = createSelectHandler(props.onChange); + const handleTextChange = createTextHandler(props.onChange); + const defaults = { + fill: '', + line_width: '', + point_size: '', + value_template: '{{value}}', + offset_time: '', + split_color_mode: 'kibana', + axis_min: '', + axis_max: '', + stacked: STACKED_OPTIONS.NONE, + steps: 0, + }; + const model = { ...defaults, ...props.model }; + const htmlId = htmlIdGenerator(); + const { intl } = props; + const stackedOptions = [ + { + label: intl.formatMessage({ + id: 'visTypeTimeseries.timeSeries.noneLabel', + defaultMessage: 'None', + }), + value: STACKED_OPTIONS.NONE, + }, + { + label: intl.formatMessage({ + id: 'visTypeTimeseries.timeSeries.stackedLabel', + defaultMessage: 'Stacked', + }), + value: STACKED_OPTIONS.STACKED, + }, + { + label: intl.formatMessage({ + id: 'visTypeTimeseries.timeSeries.stackedWithinSeriesLabel', + defaultMessage: 'Stacked within series', + }), + value: STACKED_OPTIONS.STACKED_WITHIN_SERIES, + }, + { + label: intl.formatMessage({ + id: 'visTypeTimeseries.timeSeries.percentLabel', + defaultMessage: 'Percent', + }), + value: STACKED_OPTIONS.PERCENT, + disabled: isPercentDisabled(props.seriesQuantity[model.id]), + }, + ]; + const selectedStackedOption = stackedOptions.find(option => { + return model.stacked === option.value; + }); + + const positionOptions = [ + { + label: intl.formatMessage({ + id: 'visTypeTimeseries.timeSeries.rightLabel', + defaultMessage: 'Right', + }), + value: 'right', + }, + { + label: intl.formatMessage({ + id: 'visTypeTimeseries.timeSeries.leftLabel', + defaultMessage: 'Left', + }), + value: 'left', + }, + ]; + const selectedAxisPosOption = positionOptions.find(option => { + return model.axis_position === option.value; + }); + + const chartTypeOptions = [ + { + label: intl.formatMessage({ + id: 'visTypeTimeseries.timeSeries.barLabel', + defaultMessage: 'Bar', + }), + value: 'bar', + }, + { + label: intl.formatMessage({ + id: 'visTypeTimeseries.timeSeries.lineLabel', + defaultMessage: 'Line', + }), + value: 'line', + }, + ]; + const selectedChartTypeOption = chartTypeOptions.find(option => { + return model.chart_type === option.value; + }); + + const splitColorOptions = [ + { + label: intl.formatMessage({ + id: 'visTypeTimeseries.timeSeries.defaultPaletteLabel', + defaultMessage: 'Default palette', + }), + value: 'kibana', + }, + { + label: intl.formatMessage({ + id: 'visTypeTimeseries.timeSeries.rainbowLabel', + defaultMessage: 'Rainbow', + }), + value: 'rainbow', + }, + { + label: intl.formatMessage({ + id: 'visTypeTimeseries.timeSeries.gradientLabel', + defaultMessage: 'Gradient', + }), + value: 'gradient', + }, + ]; + const selectedSplitColorOption = splitColorOptions.find(option => { + return model.split_color_mode === option.value; + }); + + let type; + + if (model.chart_type === 'line') { + type = ( + + + + } + > + + + + + + } + > + + + + + + } + > + + + + + + } + > + + + + + + } + > + + + + + + + + + + + + ); + } + if (model.chart_type === 'bar') { + type = ( + + + + } + > + + + + + + } + > + + + + + + } + > + + + + + + } + > + + + + + ); + } + + const disableSeparateYaxis = model.separate_axis ? false : true; + + const seriesIndexPattern = + props.model.override_index_pattern && props.model.series_index_pattern + ? props.model.series_index_pattern + : props.indexPatternForQuery; + + return ( +
+ + + + + + + } + helpText={ + + {'{{value}}/s'} }} + /> + + } + fullWidth + > + + + + + + + + + } + fullWidth + > + props.onChange({ filter })} + indexPatterns={[seriesIndexPattern]} + /> + + + + + {type} + + + + + + + } + > + + + + + + + + + + + + + } + > + + + + + + + + + + + + + + + + + + } + > + {/* + EUITODO: The following input couldn't be converted to EUI because of type mis-match. + It accepts a null value, but is passed a empty string. + */} + + + + + + } + > + {/* + EUITODO: The following input couldn't be converted to EUI because of type mis-match. + It accepts a null value, but is passed a empty string. + */} + + + + + + } + > + + + + + + + + + + + + + + + + + + + +
+ ); +}); + +TimeseriesConfig.propTypes = { + fields: PropTypes.object, + model: PropTypes.object, + onChange: PropTypes.func, + indexPatternForQuery: PropTypes.string, + seriesQuantity: PropTypes.object, +}; diff --git a/src/legacy/core_plugins/vis_type_timeseries/public/components/vis_types/timeseries/series.js b/src/plugins/vis_type_timeseries/public/application/components/vis_types/timeseries/series.js similarity index 100% rename from src/legacy/core_plugins/vis_type_timeseries/public/components/vis_types/timeseries/series.js rename to src/plugins/vis_type_timeseries/public/application/components/vis_types/timeseries/series.js diff --git a/src/plugins/vis_type_timeseries/public/application/components/vis_types/timeseries/vis.js b/src/plugins/vis_type_timeseries/public/application/components/vis_types/timeseries/vis.js new file mode 100644 index 0000000000000..1004f7ee96a54 --- /dev/null +++ b/src/plugins/vis_type_timeseries/public/application/components/vis_types/timeseries/vis.js @@ -0,0 +1,260 @@ +/* + * 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 PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import reactCSS from 'reactcss'; + +import { startsWith, get, cloneDeep, map } from 'lodash'; +import { htmlIdGenerator } from '@elastic/eui'; +import { ScaleType } from '@elastic/charts'; + +import { createTickFormatter } from '../../lib/tick_formatter'; +import { TimeSeries } from '../../../visualizations/views/timeseries'; +import { MarkdownSimple } from '../../../../../../../plugins/kibana_react/public'; +import { replaceVars } from '../../lib/replace_vars'; +import { getAxisLabelString } from '../../lib/get_axis_label_string'; +import { getInterval } from '../../lib/get_interval'; +import { areFieldsDifferent } from '../../lib/charts'; +import { createXaxisFormatter } from '../../lib/create_xaxis_formatter'; +import { STACKED_OPTIONS } from '../../../visualizations/constants'; +import { getCoreStart, getUISettings } from '../../../../services'; + +export class TimeseriesVisualization extends Component { + static propTypes = { + model: PropTypes.object, + onBrush: PropTypes.func, + visData: PropTypes.object, + dateFormat: PropTypes.string, + getConfig: PropTypes.func, + }; + + xAxisFormatter = interval => val => { + const { scaledDataFormat, dateFormat } = this.props.visData; + + if (!scaledDataFormat || !dateFormat) { + return val; + } + + const formatter = createXaxisFormatter(interval, scaledDataFormat, dateFormat); + + return formatter(val); + }; + + yAxisStackedByPercentFormatter = val => { + const n = Number(val) * 100; + + return `${(Number.isNaN(n) ? 0 : n).toFixed(0)}%`; + }; + + applyDocTo = template => doc => { + const vars = replaceVars(template, null, doc); + + if (vars instanceof Error) { + this.showToastNotification = vars.error.caused_by; + + return template; + } + + return vars; + }; + + static getYAxisDomain = model => { + const axisMin = get(model, 'axis_min', '').toString(); + const axisMax = get(model, 'axis_max', '').toString(); + const fit = model.series + ? model.series.filter(({ hidden }) => !hidden).every(({ fill }) => fill === '0') + : model.fill === '0'; + + return { + min: axisMin.length ? Number(axisMin) : undefined, + max: axisMax.length ? Number(axisMax) : undefined, + fit, + }; + }; + + static addYAxis = (yAxis, { id, groupId, position, tickFormatter, domain, hide }) => { + yAxis.push({ + id, + groupId, + position, + tickFormatter, + domain, + hide, + }); + }; + + static getAxisScaleType = model => + get(model, 'axis_scale') === 'log' ? ScaleType.Log : ScaleType.Linear; + + static getTickFormatter = (model, getConfig) => + createTickFormatter(get(model, 'formatter'), get(model, 'value_template'), getConfig); + + componentDidUpdate() { + const toastNotifications = getCoreStart().notifications.toasts; + if ( + this.showToastNotification && + this.notificationReason !== this.showToastNotification.reason + ) { + if (this.notification) { + toastNotifications.remove(this.notification); + } + + this.notificationReason = this.showToastNotification.reason; + this.notification = toastNotifications.addDanger({ + title: this.showToastNotification.title, + text: {this.showToastNotification.reason}, + }); + } + + if (!this.showToastNotification && this.notification) { + toastNotifications.remove(this.notification); + this.notificationReason = null; + this.notification = null; + } + } + + prepareAnnotations = () => { + const { model, visData } = this.props; + + return map(model.annotations, ({ id, color, icon, template }) => { + const annotationData = get(visData, `${model.id}.annotations.${id}`, []); + const applyDocToTemplate = this.applyDocTo(template); + + return { + id, + color, + icon, + data: annotationData.map(({ docs, ...rest }) => ({ + ...rest, + docs: docs.map(applyDocToTemplate), + })), + }; + }); + }; + + render() { + const { model, visData, onBrush } = this.props; + const styles = reactCSS({ + default: { + tvbVis: { + backgroundColor: get(model, 'background_color'), + }, + }, + }); + const series = get(visData, `${model.id}.series`, []); + const interval = getInterval(visData, model); + const yAxisIdGenerator = htmlIdGenerator('yaxis'); + const mainAxisGroupId = yAxisIdGenerator('main_group'); + + const seriesModel = model.series.filter(s => !s.hidden).map(s => cloneDeep(s)); + const enableHistogramMode = areFieldsDifferent('chart_type')(seriesModel); + const firstSeries = seriesModel.find(s => s.formatter && !s.separate_axis); + + const mainAxisScaleType = TimeseriesVisualization.getAxisScaleType(model); + const mainAxisDomain = TimeseriesVisualization.getYAxisDomain(model); + const tickFormatter = TimeseriesVisualization.getTickFormatter( + firstSeries, + this.props.getConfig + ); + const yAxis = []; + let mainDomainAdded = false; + + this.showToastNotification = null; + + seriesModel.forEach(seriesGroup => { + const isStackedWithinSeries = seriesGroup.stacked === STACKED_OPTIONS.STACKED_WITHIN_SERIES; + const hasSeparateAxis = Boolean(seriesGroup.separate_axis); + const groupId = hasSeparateAxis || isStackedWithinSeries ? seriesGroup.id : mainAxisGroupId; + const domain = hasSeparateAxis + ? TimeseriesVisualization.getYAxisDomain(seriesGroup) + : undefined; + const isCustomDomain = groupId !== mainAxisGroupId; + const seriesGroupTickFormatter = TimeseriesVisualization.getTickFormatter( + seriesGroup, + this.props.getConfig + ); + const yScaleType = hasSeparateAxis + ? TimeseriesVisualization.getAxisScaleType(seriesGroup) + : mainAxisScaleType; + + if (seriesGroup.stacked === STACKED_OPTIONS.PERCENT) { + seriesGroup.separate_axis = true; + seriesGroup.axisFormatter = 'percent'; + seriesGroup.axis_min = seriesGroup.axis_min || 0; + seriesGroup.axis_max = seriesGroup.axis_max || 1; + seriesGroup.axis_position = model.axis_position; + } + + series + .filter(r => startsWith(r.id, seriesGroup.id)) + .forEach(seriesDataRow => { + seriesDataRow.tickFormatter = seriesGroupTickFormatter; + seriesDataRow.groupId = groupId; + seriesDataRow.yScaleType = yScaleType; + seriesDataRow.hideInLegend = Boolean(seriesGroup.hide_in_legend); + seriesDataRow.useDefaultGroupDomain = !isCustomDomain; + }); + + if (isCustomDomain) { + TimeseriesVisualization.addYAxis(yAxis, { + domain, + groupId, + id: yAxisIdGenerator(seriesGroup.id), + position: seriesGroup.axis_position, + hide: isStackedWithinSeries, + tickFormatter: + seriesGroup.stacked === STACKED_OPTIONS.PERCENT + ? this.yAxisStackedByPercentFormatter + : seriesGroupTickFormatter, + }); + } else if (!mainDomainAdded) { + TimeseriesVisualization.addYAxis(yAxis, { + tickFormatter, + id: yAxisIdGenerator('main'), + groupId: mainAxisGroupId, + position: model.axis_position, + domain: mainAxisDomain, + }); + + mainDomainAdded = true; + } + }); + + const darkMode = getUISettings().get('theme:darkMode'); + return ( +
+ +
+ ); + } +} diff --git a/src/legacy/core_plugins/vis_type_timeseries/public/components/vis_types/top_n/series.js b/src/plugins/vis_type_timeseries/public/application/components/vis_types/top_n/series.js similarity index 100% rename from src/legacy/core_plugins/vis_type_timeseries/public/components/vis_types/top_n/series.js rename to src/plugins/vis_type_timeseries/public/application/components/vis_types/top_n/series.js diff --git a/src/legacy/core_plugins/vis_type_timeseries/public/components/vis_types/top_n/vis.js b/src/plugins/vis_type_timeseries/public/application/components/vis_types/top_n/vis.js similarity index 100% rename from src/legacy/core_plugins/vis_type_timeseries/public/components/vis_types/top_n/vis.js rename to src/plugins/vis_type_timeseries/public/application/components/vis_types/top_n/vis.js diff --git a/src/legacy/core_plugins/vis_type_timeseries/public/components/vis_with_splits.js b/src/plugins/vis_type_timeseries/public/application/components/vis_with_splits.js similarity index 100% rename from src/legacy/core_plugins/vis_type_timeseries/public/components/vis_with_splits.js rename to src/plugins/vis_type_timeseries/public/application/components/vis_with_splits.js diff --git a/src/legacy/core_plugins/vis_type_timeseries/public/components/visualization.js b/src/plugins/vis_type_timeseries/public/application/components/visualization.js similarity index 100% rename from src/legacy/core_plugins/vis_type_timeseries/public/components/visualization.js rename to src/plugins/vis_type_timeseries/public/application/components/visualization.js diff --git a/src/legacy/core_plugins/vis_type_timeseries/public/components/yes_no.js b/src/plugins/vis_type_timeseries/public/application/components/yes_no.js similarity index 100% rename from src/legacy/core_plugins/vis_type_timeseries/public/components/yes_no.js rename to src/plugins/vis_type_timeseries/public/application/components/yes_no.js diff --git a/src/legacy/core_plugins/vis_type_timeseries/public/components/yes_no.test.js b/src/plugins/vis_type_timeseries/public/application/components/yes_no.test.js similarity index 100% rename from src/legacy/core_plugins/vis_type_timeseries/public/components/yes_no.test.js rename to src/plugins/vis_type_timeseries/public/application/components/yes_no.test.js diff --git a/src/legacy/core_plugins/vis_type_timeseries/public/contexts/form_validation_context.js b/src/plugins/vis_type_timeseries/public/application/contexts/form_validation_context.js similarity index 100% rename from src/legacy/core_plugins/vis_type_timeseries/public/contexts/form_validation_context.js rename to src/plugins/vis_type_timeseries/public/application/contexts/form_validation_context.js diff --git a/src/legacy/core_plugins/vis_type_timeseries/public/contexts/query_input_bar_context.ts b/src/plugins/vis_type_timeseries/public/application/contexts/query_input_bar_context.ts similarity index 100% rename from src/legacy/core_plugins/vis_type_timeseries/public/contexts/query_input_bar_context.ts rename to src/plugins/vis_type_timeseries/public/application/contexts/query_input_bar_context.ts diff --git a/src/legacy/core_plugins/vis_type_timeseries/public/contexts/vis_data_context.js b/src/plugins/vis_type_timeseries/public/application/contexts/vis_data_context.js similarity index 100% rename from src/legacy/core_plugins/vis_type_timeseries/public/contexts/vis_data_context.js rename to src/plugins/vis_type_timeseries/public/application/contexts/vis_data_context.js diff --git a/src/legacy/core_plugins/vis_type_timeseries/public/editor_controller.js b/src/plugins/vis_type_timeseries/public/application/editor_controller.js similarity index 94% rename from src/legacy/core_plugins/vis_type_timeseries/public/editor_controller.js rename to src/plugins/vis_type_timeseries/public/application/editor_controller.js index 16a6348712065..af50d3a06d1fc 100644 --- a/src/legacy/core_plugins/vis_type_timeseries/public/editor_controller.js +++ b/src/plugins/vis_type_timeseries/public/application/editor_controller.js @@ -20,7 +20,8 @@ import React from 'react'; import { render, unmountComponentAtNode } from 'react-dom'; import { fetchIndexPatternFields } from './lib/fetch_fields'; -import { getSavedObjectsClient, getUISettings, getI18n } from './services'; +import { getSavedObjectsClient, getUISettings, getI18n } from '../services'; +import { VisEditor } from './components/vis_editor'; export class EditorController { constructor(el, vis, eventEmitter, embeddableHandler) { @@ -55,19 +56,14 @@ export class EditorController { this.state.isLoaded = true; }; - getComponent = () => { - return this.state.vis.type.editorConfig.component; - }; - async render(params) { - const Component = this.getComponent(); const I18nContext = getI18n().Context; !this.state.isLoaded && (await this.fetchDefaultParams()); render( - getUISettings().get('theme:darkMode'); diff --git a/src/legacy/core_plugins/vis_type_timeseries/public/lib/validate_interval.js b/src/plugins/vis_type_timeseries/public/application/lib/validate_interval.js similarity index 100% rename from src/legacy/core_plugins/vis_type_timeseries/public/lib/validate_interval.js rename to src/plugins/vis_type_timeseries/public/application/lib/validate_interval.js diff --git a/src/legacy/core_plugins/vis_type_timeseries/public/visualizations/constants/chart.js b/src/plugins/vis_type_timeseries/public/application/visualizations/constants/chart.js similarity index 100% rename from src/legacy/core_plugins/vis_type_timeseries/public/visualizations/constants/chart.js rename to src/plugins/vis_type_timeseries/public/application/visualizations/constants/chart.js diff --git a/src/legacy/core_plugins/vis_type_timeseries/public/visualizations/constants/icons.js b/src/plugins/vis_type_timeseries/public/application/visualizations/constants/icons.js similarity index 100% rename from src/legacy/core_plugins/vis_type_timeseries/public/visualizations/constants/icons.js rename to src/plugins/vis_type_timeseries/public/application/visualizations/constants/icons.js diff --git a/src/legacy/core_plugins/vis_type_timeseries/public/visualizations/constants/index.js b/src/plugins/vis_type_timeseries/public/application/visualizations/constants/index.js similarity index 100% rename from src/legacy/core_plugins/vis_type_timeseries/public/visualizations/constants/index.js rename to src/plugins/vis_type_timeseries/public/application/visualizations/constants/index.js diff --git a/src/legacy/core_plugins/vis_type_timeseries/public/visualizations/lib/active_cursor.js b/src/plugins/vis_type_timeseries/public/application/visualizations/lib/active_cursor.js similarity index 100% rename from src/legacy/core_plugins/vis_type_timeseries/public/visualizations/lib/active_cursor.js rename to src/plugins/vis_type_timeseries/public/application/visualizations/lib/active_cursor.js diff --git a/src/legacy/core_plugins/vis_type_timeseries/public/visualizations/lib/calc_dimensions.js b/src/plugins/vis_type_timeseries/public/application/visualizations/lib/calc_dimensions.js similarity index 100% rename from src/legacy/core_plugins/vis_type_timeseries/public/visualizations/lib/calc_dimensions.js rename to src/plugins/vis_type_timeseries/public/application/visualizations/lib/calc_dimensions.js diff --git a/src/legacy/core_plugins/vis_type_timeseries/public/visualizations/lib/calculate_coordinates.js b/src/plugins/vis_type_timeseries/public/application/visualizations/lib/calculate_coordinates.js similarity index 100% rename from src/legacy/core_plugins/vis_type_timeseries/public/visualizations/lib/calculate_coordinates.js rename to src/plugins/vis_type_timeseries/public/application/visualizations/lib/calculate_coordinates.js diff --git a/src/legacy/core_plugins/vis_type_timeseries/public/visualizations/lib/get_value_by.js b/src/plugins/vis_type_timeseries/public/application/visualizations/lib/get_value_by.js similarity index 100% rename from src/legacy/core_plugins/vis_type_timeseries/public/visualizations/lib/get_value_by.js rename to src/plugins/vis_type_timeseries/public/application/visualizations/lib/get_value_by.js diff --git a/src/legacy/core_plugins/vis_type_timeseries/public/visualizations/views/_annotation.scss b/src/plugins/vis_type_timeseries/public/application/visualizations/views/_annotation.scss similarity index 100% rename from src/legacy/core_plugins/vis_type_timeseries/public/visualizations/views/_annotation.scss rename to src/plugins/vis_type_timeseries/public/application/visualizations/views/_annotation.scss diff --git a/src/legacy/core_plugins/vis_type_timeseries/public/visualizations/views/_gauge.scss b/src/plugins/vis_type_timeseries/public/application/visualizations/views/_gauge.scss similarity index 100% rename from src/legacy/core_plugins/vis_type_timeseries/public/visualizations/views/_gauge.scss rename to src/plugins/vis_type_timeseries/public/application/visualizations/views/_gauge.scss diff --git a/src/legacy/core_plugins/vis_type_timeseries/public/visualizations/views/_index.scss b/src/plugins/vis_type_timeseries/public/application/visualizations/views/_index.scss similarity index 100% rename from src/legacy/core_plugins/vis_type_timeseries/public/visualizations/views/_index.scss rename to src/plugins/vis_type_timeseries/public/application/visualizations/views/_index.scss diff --git a/src/legacy/core_plugins/vis_type_timeseries/public/visualizations/views/_metric.scss b/src/plugins/vis_type_timeseries/public/application/visualizations/views/_metric.scss similarity index 100% rename from src/legacy/core_plugins/vis_type_timeseries/public/visualizations/views/_metric.scss rename to src/plugins/vis_type_timeseries/public/application/visualizations/views/_metric.scss diff --git a/src/legacy/core_plugins/vis_type_timeseries/public/visualizations/views/_top_n.scss b/src/plugins/vis_type_timeseries/public/application/visualizations/views/_top_n.scss similarity index 100% rename from src/legacy/core_plugins/vis_type_timeseries/public/visualizations/views/_top_n.scss rename to src/plugins/vis_type_timeseries/public/application/visualizations/views/_top_n.scss diff --git a/src/legacy/core_plugins/vis_type_timeseries/public/visualizations/views/annotation.js b/src/plugins/vis_type_timeseries/public/application/visualizations/views/annotation.js similarity index 100% rename from src/legacy/core_plugins/vis_type_timeseries/public/visualizations/views/annotation.js rename to src/plugins/vis_type_timeseries/public/application/visualizations/views/annotation.js diff --git a/src/legacy/core_plugins/vis_type_timeseries/public/visualizations/views/gauge.js b/src/plugins/vis_type_timeseries/public/application/visualizations/views/gauge.js similarity index 100% rename from src/legacy/core_plugins/vis_type_timeseries/public/visualizations/views/gauge.js rename to src/plugins/vis_type_timeseries/public/application/visualizations/views/gauge.js diff --git a/src/legacy/core_plugins/vis_type_timeseries/public/visualizations/views/gauge_vis.js b/src/plugins/vis_type_timeseries/public/application/visualizations/views/gauge_vis.js similarity index 100% rename from src/legacy/core_plugins/vis_type_timeseries/public/visualizations/views/gauge_vis.js rename to src/plugins/vis_type_timeseries/public/application/visualizations/views/gauge_vis.js diff --git a/src/legacy/core_plugins/vis_type_timeseries/public/visualizations/views/metric.js b/src/plugins/vis_type_timeseries/public/application/visualizations/views/metric.js similarity index 100% rename from src/legacy/core_plugins/vis_type_timeseries/public/visualizations/views/metric.js rename to src/plugins/vis_type_timeseries/public/application/visualizations/views/metric.js diff --git a/src/legacy/core_plugins/vis_type_timeseries/public/visualizations/views/timeseries/__mocks__/@elastic/charts.js b/src/plugins/vis_type_timeseries/public/application/visualizations/views/timeseries/__mocks__/@elastic/charts.js similarity index 100% rename from src/legacy/core_plugins/vis_type_timeseries/public/visualizations/views/timeseries/__mocks__/@elastic/charts.js rename to src/plugins/vis_type_timeseries/public/application/visualizations/views/timeseries/__mocks__/@elastic/charts.js diff --git a/src/legacy/core_plugins/vis_type_timeseries/public/visualizations/views/timeseries/decorators/__snapshots__/area_decorator.test.js.snap b/src/plugins/vis_type_timeseries/public/application/visualizations/views/timeseries/decorators/__snapshots__/area_decorator.test.js.snap similarity index 100% rename from src/legacy/core_plugins/vis_type_timeseries/public/visualizations/views/timeseries/decorators/__snapshots__/area_decorator.test.js.snap rename to src/plugins/vis_type_timeseries/public/application/visualizations/views/timeseries/decorators/__snapshots__/area_decorator.test.js.snap diff --git a/src/legacy/core_plugins/vis_type_timeseries/public/visualizations/views/timeseries/decorators/__snapshots__/bar_decorator.test.js.snap b/src/plugins/vis_type_timeseries/public/application/visualizations/views/timeseries/decorators/__snapshots__/bar_decorator.test.js.snap similarity index 100% rename from src/legacy/core_plugins/vis_type_timeseries/public/visualizations/views/timeseries/decorators/__snapshots__/bar_decorator.test.js.snap rename to src/plugins/vis_type_timeseries/public/application/visualizations/views/timeseries/decorators/__snapshots__/bar_decorator.test.js.snap diff --git a/src/legacy/core_plugins/vis_type_timeseries/public/visualizations/views/timeseries/decorators/area_decorator.js b/src/plugins/vis_type_timeseries/public/application/visualizations/views/timeseries/decorators/area_decorator.js similarity index 100% rename from src/legacy/core_plugins/vis_type_timeseries/public/visualizations/views/timeseries/decorators/area_decorator.js rename to src/plugins/vis_type_timeseries/public/application/visualizations/views/timeseries/decorators/area_decorator.js diff --git a/src/legacy/core_plugins/vis_type_timeseries/public/visualizations/views/timeseries/decorators/area_decorator.test.js b/src/plugins/vis_type_timeseries/public/application/visualizations/views/timeseries/decorators/area_decorator.test.js similarity index 100% rename from src/legacy/core_plugins/vis_type_timeseries/public/visualizations/views/timeseries/decorators/area_decorator.test.js rename to src/plugins/vis_type_timeseries/public/application/visualizations/views/timeseries/decorators/area_decorator.test.js diff --git a/src/legacy/core_plugins/vis_type_timeseries/public/visualizations/views/timeseries/decorators/bar_decorator.js b/src/plugins/vis_type_timeseries/public/application/visualizations/views/timeseries/decorators/bar_decorator.js similarity index 100% rename from src/legacy/core_plugins/vis_type_timeseries/public/visualizations/views/timeseries/decorators/bar_decorator.js rename to src/plugins/vis_type_timeseries/public/application/visualizations/views/timeseries/decorators/bar_decorator.js diff --git a/src/legacy/core_plugins/vis_type_timeseries/public/visualizations/views/timeseries/decorators/bar_decorator.test.js b/src/plugins/vis_type_timeseries/public/application/visualizations/views/timeseries/decorators/bar_decorator.test.js similarity index 100% rename from src/legacy/core_plugins/vis_type_timeseries/public/visualizations/views/timeseries/decorators/bar_decorator.test.js rename to src/plugins/vis_type_timeseries/public/application/visualizations/views/timeseries/decorators/bar_decorator.test.js diff --git a/src/plugins/vis_type_timeseries/public/application/visualizations/views/timeseries/index.js b/src/plugins/vis_type_timeseries/public/application/visualizations/views/timeseries/index.js new file mode 100644 index 0000000000000..5cf1619150e5c --- /dev/null +++ b/src/plugins/vis_type_timeseries/public/application/visualizations/views/timeseries/index.js @@ -0,0 +1,275 @@ +/* + * 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, { useEffect, useRef } from 'react'; +import PropTypes from 'prop-types'; +import classNames from 'classnames'; + +import { + Axis, + Chart, + Position, + Settings, + AnnotationDomainTypes, + LineAnnotation, + TooltipType, +} from '@elastic/charts'; +import { EuiIcon } from '@elastic/eui'; +import { getTimezone } from '../../../lib/get_timezone'; +import { eventBus, ACTIVE_CURSOR } from '../../lib/active_cursor'; +import { getUISettings, getChartsSetup } from '../../../../services'; +import { GRID_LINE_CONFIG, ICON_TYPES_MAP, STACKED_OPTIONS } from '../../constants'; +import { AreaSeriesDecorator } from './decorators/area_decorator'; +import { BarSeriesDecorator } from './decorators/bar_decorator'; +import { getStackAccessors } from './utils/stack_format'; +import { getTheme, getChartClasses } from './utils/theme'; + +const generateAnnotationData = (values, formatter) => + values.map(({ key, docs }) => ({ + dataValue: key, + details: docs[0], + header: formatter({ + value: key, + }), + })); + +const decorateFormatter = formatter => ({ value }) => formatter(value); + +const handleCursorUpdate = cursor => { + eventBus.trigger(ACTIVE_CURSOR, cursor); +}; + +export const TimeSeries = ({ + darkMode, + backgroundColor, + showGrid, + legend, + legendPosition, + xAxisLabel, + series, + yAxis, + onBrush, + xAxisFormatter, + annotations, + enableHistogramMode, +}) => { + const chartRef = useRef(); + const updateCursor = (_, cursor) => { + if (chartRef.current) { + chartRef.current.dispatchExternalPointerEvent(cursor); + } + }; + + useEffect(() => { + eventBus.on(ACTIVE_CURSOR, updateCursor); + + return () => { + eventBus.off(ACTIVE_CURSOR, undefined, updateCursor); + }; + }, []); // eslint-disable-line + + const tooltipFormatter = decorateFormatter(xAxisFormatter); + const uiSettings = getUISettings(); + const timeZone = getTimezone(uiSettings); + const hasBarChart = series.some(({ bars }) => bars.show); + + // compute the theme based on the bg color + const theme = getTheme(darkMode, backgroundColor); + // apply legend style change if bgColor is configured + const classes = classNames('tvbVisTimeSeries', getChartClasses(backgroundColor)); + + // If the color isn't configured by the user, use the color mapping service + // to assign a color from the Kibana palette. Colors will be shared across the + // session, including dashboards. + const { colors } = getChartsSetup(); + colors.mappedColors.mapKeys(series.filter(({ color }) => !color).map(({ label }) => label)); + + return ( + + + + {annotations.map(({ id, data, icon, color }) => { + const dataValues = generateAnnotationData(data, tooltipFormatter); + const style = { line: { stroke: color } }; + + return ( + } + hideLinesTooltips={true} + style={style} + /> + ); + })} + + {series.map( + ( + { + id, + label, + bars, + lines, + data, + hideInLegend, + xScaleType, + yScaleType, + groupId, + color, + stack, + points, + useDefaultGroupDomain, + y1AccessorFormat, + y0AccessorFormat, + }, + sortIndex + ) => { + const stackAccessors = getStackAccessors(stack); + const isPercentage = stack === STACKED_OPTIONS.PERCENT; + const key = `${id}-${label}`; + // Only use color mapping if there is no color from the server + const finalColor = color ?? colors.mappedColors.mapping[label]; + + if (bars.show) { + return ( + + ); + } + + if (lines.show) { + return ( + + ); + } + + return null; + } + )} + + {yAxis.map(({ id, groupId, position, tickFormatter, domain, hide }) => ( + + ))} + + + + ); +}; + +TimeSeries.defaultProps = { + showGrid: true, + legend: true, + legendPosition: 'right', +}; + +TimeSeries.propTypes = { + darkMode: PropTypes.bool, + backgroundColor: PropTypes.string, + showGrid: PropTypes.bool, + legend: PropTypes.bool, + legendPosition: PropTypes.string, + xAxisLabel: PropTypes.string, + series: PropTypes.array, + yAxis: PropTypes.array, + onBrush: PropTypes.func, + xAxisFormatter: PropTypes.func, + annotations: PropTypes.array, + enableHistogramMode: PropTypes.bool.isRequired, +}; diff --git a/src/legacy/core_plugins/vis_type_timeseries/public/visualizations/views/timeseries/model/__snapshots__/charts.test.js.snap b/src/plugins/vis_type_timeseries/public/application/visualizations/views/timeseries/model/__snapshots__/charts.test.js.snap similarity index 100% rename from src/legacy/core_plugins/vis_type_timeseries/public/visualizations/views/timeseries/model/__snapshots__/charts.test.js.snap rename to src/plugins/vis_type_timeseries/public/application/visualizations/views/timeseries/model/__snapshots__/charts.test.js.snap diff --git a/src/legacy/core_plugins/vis_type_timeseries/public/visualizations/views/timeseries/model/charts.js b/src/plugins/vis_type_timeseries/public/application/visualizations/views/timeseries/model/charts.js similarity index 100% rename from src/legacy/core_plugins/vis_type_timeseries/public/visualizations/views/timeseries/model/charts.js rename to src/plugins/vis_type_timeseries/public/application/visualizations/views/timeseries/model/charts.js diff --git a/src/legacy/core_plugins/vis_type_timeseries/public/visualizations/views/timeseries/model/charts.test.js b/src/plugins/vis_type_timeseries/public/application/visualizations/views/timeseries/model/charts.test.js similarity index 100% rename from src/legacy/core_plugins/vis_type_timeseries/public/visualizations/views/timeseries/model/charts.test.js rename to src/plugins/vis_type_timeseries/public/application/visualizations/views/timeseries/model/charts.test.js diff --git a/src/legacy/core_plugins/vis_type_timeseries/public/visualizations/views/timeseries/utils/__snapshots__/series_styles.test.js.snap b/src/plugins/vis_type_timeseries/public/application/visualizations/views/timeseries/utils/__snapshots__/series_styles.test.js.snap similarity index 100% rename from src/legacy/core_plugins/vis_type_timeseries/public/visualizations/views/timeseries/utils/__snapshots__/series_styles.test.js.snap rename to src/plugins/vis_type_timeseries/public/application/visualizations/views/timeseries/utils/__snapshots__/series_styles.test.js.snap diff --git a/src/legacy/core_plugins/vis_type_timeseries/public/visualizations/views/timeseries/utils/series_styles.js b/src/plugins/vis_type_timeseries/public/application/visualizations/views/timeseries/utils/series_styles.js similarity index 100% rename from src/legacy/core_plugins/vis_type_timeseries/public/visualizations/views/timeseries/utils/series_styles.js rename to src/plugins/vis_type_timeseries/public/application/visualizations/views/timeseries/utils/series_styles.js diff --git a/src/legacy/core_plugins/vis_type_timeseries/public/visualizations/views/timeseries/utils/series_styles.test.js b/src/plugins/vis_type_timeseries/public/application/visualizations/views/timeseries/utils/series_styles.test.js similarity index 100% rename from src/legacy/core_plugins/vis_type_timeseries/public/visualizations/views/timeseries/utils/series_styles.test.js rename to src/plugins/vis_type_timeseries/public/application/visualizations/views/timeseries/utils/series_styles.test.js diff --git a/src/legacy/core_plugins/vis_type_timeseries/public/visualizations/views/timeseries/utils/stack_format.js b/src/plugins/vis_type_timeseries/public/application/visualizations/views/timeseries/utils/stack_format.js similarity index 100% rename from src/legacy/core_plugins/vis_type_timeseries/public/visualizations/views/timeseries/utils/stack_format.js rename to src/plugins/vis_type_timeseries/public/application/visualizations/views/timeseries/utils/stack_format.js diff --git a/src/legacy/core_plugins/vis_type_timeseries/public/visualizations/views/timeseries/utils/stack_format.test.js b/src/plugins/vis_type_timeseries/public/application/visualizations/views/timeseries/utils/stack_format.test.js similarity index 100% rename from src/legacy/core_plugins/vis_type_timeseries/public/visualizations/views/timeseries/utils/stack_format.test.js rename to src/plugins/vis_type_timeseries/public/application/visualizations/views/timeseries/utils/stack_format.test.js diff --git a/src/legacy/core_plugins/vis_type_timeseries/public/visualizations/views/timeseries/utils/theme.test.ts b/src/plugins/vis_type_timeseries/public/application/visualizations/views/timeseries/utils/theme.test.ts similarity index 100% rename from src/legacy/core_plugins/vis_type_timeseries/public/visualizations/views/timeseries/utils/theme.test.ts rename to src/plugins/vis_type_timeseries/public/application/visualizations/views/timeseries/utils/theme.test.ts diff --git a/src/legacy/core_plugins/vis_type_timeseries/public/visualizations/views/timeseries/utils/theme.ts b/src/plugins/vis_type_timeseries/public/application/visualizations/views/timeseries/utils/theme.ts similarity index 100% rename from src/legacy/core_plugins/vis_type_timeseries/public/visualizations/views/timeseries/utils/theme.ts rename to src/plugins/vis_type_timeseries/public/application/visualizations/views/timeseries/utils/theme.ts diff --git a/src/legacy/core_plugins/vis_type_timeseries/public/visualizations/views/top_n.js b/src/plugins/vis_type_timeseries/public/application/visualizations/views/top_n.js similarity index 100% rename from src/legacy/core_plugins/vis_type_timeseries/public/visualizations/views/top_n.js rename to src/plugins/vis_type_timeseries/public/application/visualizations/views/top_n.js diff --git a/src/plugins/vis_type_timeseries/public/index.ts b/src/plugins/vis_type_timeseries/public/index.ts new file mode 100644 index 0000000000000..fbf4a81b6ad1b --- /dev/null +++ b/src/plugins/vis_type_timeseries/public/index.ts @@ -0,0 +1,25 @@ +/* + * 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 { PluginInitializerContext } from '../../../core/public'; +import { MetricsPlugin as Plugin } from './plugin'; + +export function plugin(initializerContext: PluginInitializerContext) { + return new Plugin(initializerContext); +} diff --git a/src/legacy/core_plugins/vis_type_timeseries/public/metrics_fn.ts b/src/plugins/vis_type_timeseries/public/metrics_fn.ts similarity index 92% rename from src/legacy/core_plugins/vis_type_timeseries/public/metrics_fn.ts rename to src/plugins/vis_type_timeseries/public/metrics_fn.ts index 1f9cbecc2a354..008b13cce6565 100644 --- a/src/legacy/core_plugins/vis_type_timeseries/public/metrics_fn.ts +++ b/src/plugins/vis_type_timeseries/public/metrics_fn.ts @@ -19,12 +19,8 @@ import { get } from 'lodash'; import { i18n } from '@kbn/i18n'; -import { - ExpressionFunctionDefinition, - KibanaContext, - Render, -} from '../../../../plugins/expressions/public'; -import { PersistedState } from '../../../../plugins/visualizations/public'; +import { ExpressionFunctionDefinition, KibanaContext, Render } from '../../expressions/public'; +import { PersistedState } from '../../visualizations/public'; // @ts-ignore import { metricsRequestHandler } from './request_handler'; diff --git a/src/legacy/core_plugins/vis_type_timeseries/public/metrics_type.ts b/src/plugins/vis_type_timeseries/public/metrics_type.ts similarity index 85% rename from src/legacy/core_plugins/vis_type_timeseries/public/metrics_type.ts rename to src/plugins/vis_type_timeseries/public/metrics_type.ts index 1db35c406eb13..c525ce7fa0b3b 100644 --- a/src/legacy/core_plugins/vis_type_timeseries/public/metrics_type.ts +++ b/src/plugins/vis_type_timeseries/public/metrics_type.ts @@ -21,11 +21,10 @@ import { i18n } from '@kbn/i18n'; // @ts-ignore import { metricsRequestHandler } from './request_handler'; +import { EditorController } from './application'; // @ts-ignore -import { EditorController } from './editor_controller'; -// @ts-ignore -import { PANEL_TYPES } from '../../../../plugins/vis_type_timeseries/common/panel_types'; -import { defaultFeedbackMessage } from '../../../../plugins/kibana_utils/public'; +import { PANEL_TYPES } from '../common/panel_types'; +import { defaultFeedbackMessage } from '../../kibana_utils/public'; export const metricsVisDefinition = { name: 'metrics', @@ -44,6 +43,7 @@ export const metricsVisDefinition = { id: '61ca57f1-469d-11e7-af02-69e470af7417', color: '#68BC00', split_mode: 'everything', + split_color_mode: 'kibana', metrics: [ { id: '61ca57f2-469d-11e7-af02-69e470af7417', @@ -69,12 +69,9 @@ export const metricsVisDefinition = { show_legend: 1, show_grid: 1, }, - component: require('./components/vis_editor').VisEditor, + component: require('./application/components/vis_editor').VisEditor, }, editor: EditorController, - editorConfig: { - component: require('./components/vis_editor').VisEditor, - }, options: { showQueryBar: false, showFilterBar: false, diff --git a/src/plugins/vis_type_timeseries/public/plugin.ts b/src/plugins/vis_type_timeseries/public/plugin.ts new file mode 100644 index 0000000000000..d98e55bdb340c --- /dev/null +++ b/src/plugins/vis_type_timeseries/public/plugin.ts @@ -0,0 +1,77 @@ +/* + * 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 './application/index.scss'; + +import { PluginInitializerContext, CoreSetup, CoreStart, Plugin } from 'kibana/public'; +import { Plugin as ExpressionsPublicPlugin } from '../../expressions/public'; +import { VisualizationsSetup } from '../../visualizations/public'; + +import { createMetricsFn } from './metrics_fn'; +import { metricsVisDefinition } from './metrics_type'; +import { + setSavedObjectsClient, + setUISettings, + setI18n, + setFieldFormats, + setCoreStart, + setDataStart, + setChartsSetup, +} from './services'; +import { DataPublicPluginStart } from '../../data/public'; +import { ChartsPluginSetup } from '../../charts/public'; + +/** @internal */ +export interface MetricsPluginSetupDependencies { + expressions: ReturnType; + visualizations: VisualizationsSetup; + charts: ChartsPluginSetup; +} + +/** @internal */ +export interface MetricsPluginStartDependencies { + data: DataPublicPluginStart; +} + +/** @internal */ +export class MetricsPlugin implements Plugin, void> { + initializerContext: PluginInitializerContext; + + constructor(initializerContext: PluginInitializerContext) { + this.initializerContext = initializerContext; + } + + public async setup( + core: CoreSetup, + { expressions, visualizations, charts }: MetricsPluginSetupDependencies + ) { + expressions.registerFunction(createMetricsFn); + setUISettings(core.uiSettings); + setChartsSetup(charts); + visualizations.createReactVisualization(metricsVisDefinition); + } + + public start(core: CoreStart, { data }: MetricsPluginStartDependencies) { + setSavedObjectsClient(core.savedObjects); + setI18n(core.i18n); + setFieldFormats(data.fieldFormats); + setDataStart(data); + setCoreStart(core); + } +} diff --git a/src/legacy/core_plugins/vis_type_timeseries/public/request_handler.js b/src/plugins/vis_type_timeseries/public/request_handler.js similarity index 95% rename from src/legacy/core_plugins/vis_type_timeseries/public/request_handler.js rename to src/plugins/vis_type_timeseries/public/request_handler.js index 2cac1567a6eb7..bd6c6d9553930 100644 --- a/src/legacy/core_plugins/vis_type_timeseries/public/request_handler.js +++ b/src/plugins/vis_type_timeseries/public/request_handler.js @@ -17,8 +17,7 @@ * under the License. */ -import { validateInterval } from './lib/validate_interval'; -import { getTimezone } from './lib/get_timezone'; +import { getTimezone, validateInterval } from './application'; import { getUISettings, getDataStart, getCoreStart } from './services'; export const metricsRequestHandler = async ({ diff --git a/src/plugins/vis_type_timeseries/public/services.ts b/src/plugins/vis_type_timeseries/public/services.ts new file mode 100644 index 0000000000000..9aa84478fb78b --- /dev/null +++ b/src/plugins/vis_type_timeseries/public/services.ts @@ -0,0 +1,43 @@ +/* + * 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 { I18nStart, SavedObjectsStart, IUiSettingsClient, CoreStart } from 'src/core/public'; +import { createGetterSetter } from '../../kibana_utils/public'; +import { ChartsPluginSetup } from '../../charts/public'; +import { DataPublicPluginStart } from '../../data/public'; + +export const [getUISettings, setUISettings] = createGetterSetter('UISettings'); + +export const [getFieldFormats, setFieldFormats] = createGetterSetter< + DataPublicPluginStart['fieldFormats'] +>('FieldFormats'); + +export const [getSavedObjectsClient, setSavedObjectsClient] = createGetterSetter( + 'SavedObjectsClient' +); + +export const [getCoreStart, setCoreStart] = createGetterSetter('CoreStart'); + +export const [getDataStart, setDataStart] = createGetterSetter('DataStart'); + +export const [getI18n, setI18n] = createGetterSetter('I18n'); + +export const [getChartsSetup, setChartsSetup] = createGetterSetter( + 'ChartsPluginSetup' +); diff --git a/src/plugins/vis_type_timeseries/server/config.ts b/src/plugins/vis_type_timeseries/server/config.ts new file mode 100644 index 0000000000000..f4668eff8fa04 --- /dev/null +++ b/src/plugins/vis_type_timeseries/server/config.ts @@ -0,0 +1,31 @@ +/* + * 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 { schema, TypeOf } from '@kbn/config-schema'; + +export const config = schema.object({ + enabled: schema.boolean({ defaultValue: true }), + + /** @deprecated **/ + chartResolution: schema.number({ defaultValue: 150 }), + /** @deprecated **/ + minimumBucketSize: schema.number({ defaultValue: 10 }), +}); + +export type VisTypeTimeseriesConfig = TypeOf; diff --git a/src/plugins/vis_type_timeseries/server/index.ts b/src/plugins/vis_type_timeseries/server/index.ts index fa74b6e965971..f460257caf5e3 100644 --- a/src/plugins/vis_type_timeseries/server/index.ts +++ b/src/plugins/vis_type_timeseries/server/index.ts @@ -17,18 +17,25 @@ * under the License. */ -import { schema, TypeOf } from '@kbn/config-schema'; -import { PluginInitializerContext } from 'src/core/server'; +import { PluginInitializerContext, PluginConfigDescriptor } from 'src/core/server'; +import { VisTypeTimeseriesConfig, config as configSchema } from './config'; import { VisTypeTimeseriesPlugin } from './plugin'; + export { VisTypeTimeseriesSetup, Framework } from './plugin'; -export const config = { - schema: schema.object({ - enabled: schema.boolean({ defaultValue: true }), - }), -}; +export const config: PluginConfigDescriptor = { + deprecations: ({ unused, renameFromRoot }) => [ + // In Kibana v7.8 plugin id was renamed from 'metrics' to 'vis_type_timeseries': + renameFromRoot('metrics.enabled', 'vis_type_timeseries.enabled', true), + renameFromRoot('metrics.chartResolution', 'vis_type_timeseries.chartResolution', true), + renameFromRoot('metrics.minimumBucketSize', 'vis_type_timeseries.minimumBucketSize', true), -export type VisTypeTimeseriesConfig = TypeOf; + // Unused properties which should be removed after releasing Kibana v8.0: + unused('chartResolution'), + unused('minimumBucketSize'), + ], + schema: configSchema, +}; export { ValidationTelemetryServiceSetup } from './validation_telemetry'; diff --git a/src/plugins/vis_type_timeseries/server/lib/vis_data/helpers/get_split_colors.js b/src/plugins/vis_type_timeseries/server/lib/vis_data/helpers/get_split_colors.js index ff8d9077b0871..cad8c8f2025a1 100644 --- a/src/plugins/vis_type_timeseries/server/lib/vis_data/helpers/get_split_colors.js +++ b/src/plugins/vis_type_timeseries/server/lib/vis_data/helpers/get_split_colors.js @@ -19,7 +19,7 @@ import Color from 'color'; -export function getSplitColors(inputColor, size = 10, style = 'gradient') { +export function getSplitColors(inputColor, size = 10, style = 'kibana') { const color = new Color(inputColor); const colors = []; let workingColor = Color.hsl(color.hsl().object()); @@ -49,7 +49,7 @@ export function getSplitColors(inputColor, size = 10, style = 'gradient') { '#0F1419', '#666666', ]; - } else { + } else if (style === 'gradient') { colors.push(color.string()); const rotateBy = color.luminosity() / (size - 1); for (let i = 0; i < size - 1; i++) { diff --git a/src/plugins/vis_type_timeseries/server/lib/vis_data/helpers/get_splits.test.js b/src/plugins/vis_type_timeseries/server/lib/vis_data/helpers/get_splits.test.js index 0874d944033f5..376d32d0da13f 100644 --- a/src/plugins/vis_type_timeseries/server/lib/vis_data/helpers/get_splits.test.js +++ b/src/plugins/vis_type_timeseries/server/lib/vis_data/helpers/get_splits.test.js @@ -106,7 +106,7 @@ describe('getSplits(resp, panel, series)', () => { ]); }); - test('should return a splits for terms group bys', () => { + describe('terms group bys', () => { const resp = { aggregations: { SERIES: { @@ -126,38 +126,89 @@ describe('getSplits(resp, panel, series)', () => { }, }, }; - const series = { - id: 'SERIES', - color: '#F00', - split_mode: 'terms', - terms_field: 'beat.hostname', - terms_size: 10, - metrics: [ - { id: 'AVG', type: 'avg', field: 'cpu' }, - { id: 'SIBAGG', type: 'avg_bucket', field: 'AVG' }, - ], - }; - const panel = { type: 'timeseries' }; - expect(getSplits(resp, panel, series)).toEqual([ - { - id: 'SERIES:example-01', - key: 'example-01', - label: 'example-01', - meta: { bucketSize: 10 }, - color: 'rgb(255, 0, 0)', - timeseries: { buckets: [] }, - SIBAGG: { value: 1 }, - }, - { - id: 'SERIES:example-02', - key: 'example-02', - label: 'example-02', - meta: { bucketSize: 10 }, - color: 'rgb(147, 0, 0)', - timeseries: { buckets: [] }, - SIBAGG: { value: 2 }, - }, - ]); + + test('should return a splits with no color', () => { + const series = { + id: 'SERIES', + color: '#F00', + split_mode: 'terms', + terms_field: 'beat.hostname', + terms_size: 10, + metrics: [ + { id: 'AVG', type: 'avg', field: 'cpu' }, + { id: 'SIBAGG', type: 'avg_bucket', field: 'AVG' }, + ], + }; + const panel = { type: 'timeseries' }; + expect(getSplits(resp, panel, series)).toEqual([ + { + id: 'SERIES:example-01', + key: 'example-01', + label: 'example-01', + meta: { bucketSize: 10 }, + color: undefined, + timeseries: { buckets: [] }, + SIBAGG: { value: 1 }, + }, + { + id: 'SERIES:example-02', + key: 'example-02', + label: 'example-02', + meta: { bucketSize: 10 }, + color: undefined, + timeseries: { buckets: [] }, + SIBAGG: { value: 2 }, + }, + ]); + }); + + test('should return gradient color', () => { + const series = { + id: 'SERIES', + color: '#F00', + split_mode: 'terms', + split_color_mode: 'gradient', + terms_field: 'beat.hostname', + terms_size: 10, + metrics: [ + { id: 'AVG', type: 'avg', field: 'cpu' }, + { id: 'SIBAGG', type: 'avg_bucket', field: 'AVG' }, + ], + }; + const panel = { type: 'timeseries' }; + expect(getSplits(resp, panel, series)).toEqual([ + expect.objectContaining({ + color: 'rgb(255, 0, 0)', + }), + expect.objectContaining({ + color: 'rgb(147, 0, 0)', + }), + ]); + }); + + test('should return rainbow color', () => { + const series = { + id: 'SERIES', + color: '#F00', + split_mode: 'terms', + split_color_mode: 'rainbow', + terms_field: 'beat.hostname', + terms_size: 10, + metrics: [ + { id: 'AVG', type: 'avg', field: 'cpu' }, + { id: 'SIBAGG', type: 'avg_bucket', field: 'AVG' }, + ], + }; + const panel = { type: 'timeseries' }; + expect(getSplits(resp, panel, series)).toEqual([ + expect.objectContaining({ + color: '#68BC00', + }), + expect.objectContaining({ + color: '#009CE0', + }), + ]); + }); }); test('should return a splits for filters group bys', () => { diff --git a/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/series/index.js b/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/series/index.js index 4b0b8f33716a2..c727a3131f5df 100644 --- a/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/series/index.js +++ b/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/series/index.js @@ -26,6 +26,7 @@ import { dateHistogram } from './date_histogram'; import { metricBuckets } from './metric_buckets'; import { siblingBuckets } from './sibling_buckets'; import { ratios as filterRatios } from './filter_ratios'; +import { positiveRate } from './positive_rate'; import { normalizeQuery } from './normalize_query'; export const processors = [ @@ -38,5 +39,6 @@ export const processors = [ metricBuckets, siblingBuckets, filterRatios, + positiveRate, normalizeQuery, ]; diff --git a/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/series/positive_rate.js b/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/series/positive_rate.js new file mode 100644 index 0000000000000..1ff548cc19e02 --- /dev/null +++ b/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/series/positive_rate.js @@ -0,0 +1,68 @@ +/* + * 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 { getBucketSize } from '../../helpers/get_bucket_size'; +import { getIntervalAndTimefield } from '../../get_interval_and_timefield'; +import { bucketTransform } from '../../helpers/bucket_transform'; +import { set } from 'lodash'; + +export const filter = metric => metric.type === 'positive_rate'; + +export const createPositiveRate = (doc, intervalString, aggRoot) => metric => { + const maxFn = bucketTransform.max; + const derivativeFn = bucketTransform.derivative; + const positiveOnlyFn = bucketTransform.positive_only; + + const maxMetric = { id: `${metric.id}-positive-rate-max`, type: 'max', field: metric.field }; + const derivativeMetric = { + id: `${metric.id}-positive-rate-derivative`, + type: 'derivative', + field: `${metric.id}-positive-rate-max`, + unit: metric.unit, + }; + const positiveOnlyMetric = { + id: metric.id, + type: 'positive_only', + field: `${metric.id}-positive-rate-derivative`, + }; + + const fakeSeriesMetrics = [maxMetric, derivativeMetric, positiveOnlyMetric]; + + const maxBucket = maxFn(maxMetric, fakeSeriesMetrics, intervalString); + const derivativeBucket = derivativeFn(derivativeMetric, fakeSeriesMetrics, intervalString); + const positiveOnlyBucket = positiveOnlyFn(positiveOnlyMetric, fakeSeriesMetrics, intervalString); + + set(doc, `${aggRoot}.timeseries.aggs.${metric.id}-positive-rate-max`, maxBucket); + set(doc, `${aggRoot}.timeseries.aggs.${metric.id}-positive-rate-derivative`, derivativeBucket); + set(doc, `${aggRoot}.timeseries.aggs.${metric.id}`, positiveOnlyBucket); +}; + +export function positiveRate(req, panel, series, esQueryConfig, indexPatternObject, capabilities) { + return next => doc => { + const { interval } = getIntervalAndTimefield(panel, series, indexPatternObject); + const { intervalString } = getBucketSize(req, interval, capabilities); + if (series.metrics.some(filter)) { + series.metrics + .filter(filter) + .forEach(createPositiveRate(doc, intervalString, `aggs.${series.id}.aggs`)); + return next(doc); + } + return next(doc); + }; +} diff --git a/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/series/positive_rate.test.js b/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/series/positive_rate.test.js new file mode 100644 index 0000000000000..946884c05c722 --- /dev/null +++ b/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/series/positive_rate.test.js @@ -0,0 +1,95 @@ +/* + * 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 { positiveRate } from './positive_rate'; +describe('positiveRate(req, panel, series)', () => { + let panel; + let series; + let req; + beforeEach(() => { + panel = { + time_field: 'timestamp', + }; + series = { + id: 'test', + split_mode: 'terms', + terms_size: 10, + terms_field: 'host', + metrics: [ + { + id: 'metric-1', + type: 'positive_rate', + field: 'system.network.out.bytes', + unit: '1s', + }, + ], + }; + req = { + payload: { + timerange: { + min: '2017-01-01T00:00:00Z', + max: '2017-01-01T01:00:00Z', + }, + }, + }; + }); + + test('calls next when finished', () => { + const next = jest.fn(); + positiveRate(req, panel, series)(next)({}); + expect(next.mock.calls.length).toEqual(1); + }); + + test('returns positive rate aggs', () => { + const next = doc => doc; + const doc = positiveRate(req, panel, series)(next)({}); + expect(doc).toEqual({ + aggs: { + test: { + aggs: { + timeseries: { + aggs: { + 'metric-1-positive-rate-max': { + max: { field: 'system.network.out.bytes' }, + }, + 'metric-1-positive-rate-derivative': { + derivative: { + buckets_path: 'metric-1-positive-rate-max', + gap_policy: 'skip', + unit: '1s', + }, + }, + 'metric-1': { + bucket_script: { + buckets_path: { value: 'metric-1-positive-rate-derivative[normalized_value]' }, + script: { + source: 'params.value > 0.0 ? params.value : 0.0', + lang: 'painless', + }, + gap_policy: 'skip', + }, + }, + }, + }, + }, + }, + }, + }); + }); +}); diff --git a/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/table/index.js b/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/table/index.js index a62533ae7a37c..5864d2538005d 100644 --- a/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/table/index.js +++ b/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/table/index.js @@ -26,6 +26,7 @@ import { metricBuckets } from './metric_buckets'; import { siblingBuckets } from './sibling_buckets'; import { ratios as filterRatios } from './filter_ratios'; import { normalizeQuery } from './normalize_query'; +import { positiveRate } from './positive_rate'; export const processors = [ query, @@ -36,5 +37,6 @@ export const processors = [ metricBuckets, siblingBuckets, filterRatios, + positiveRate, normalizeQuery, ]; diff --git a/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/table/positive_rate.js b/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/table/positive_rate.js new file mode 100644 index 0000000000000..da4b834822d70 --- /dev/null +++ b/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/table/positive_rate.js @@ -0,0 +1,35 @@ +/* + * 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 { getBucketSize } from '../../helpers/get_bucket_size'; +import { getIntervalAndTimefield } from '../../get_interval_and_timefield'; +import { calculateAggRoot } from './calculate_agg_root'; +import { createPositiveRate, filter } from '../series/positive_rate'; + +export function positiveRate(req, panel, esQueryConfig, indexPatternObject) { + return next => doc => { + const { interval } = getIntervalAndTimefield(panel, {}, indexPatternObject); + const { intervalString } = getBucketSize(req, interval); + panel.series.forEach(column => { + const aggRoot = calculateAggRoot(doc, column); + column.metrics.filter(filter).forEach(createPositiveRate(doc, intervalString, aggRoot)); + }); + return next(doc); + }; +} diff --git a/src/plugins/vis_type_timeseries/server/plugin.ts b/src/plugins/vis_type_timeseries/server/plugin.ts index 6ef6362c6e37b..05257cb79a75c 100644 --- a/src/plugins/vis_type_timeseries/server/plugin.ts +++ b/src/plugins/vis_type_timeseries/server/plugin.ts @@ -29,7 +29,7 @@ import { } from 'src/core/server'; import { Observable } from 'rxjs'; import { Server } from 'hapi'; -import { VisTypeTimeseriesConfig } from '.'; +import { VisTypeTimeseriesConfig } from './config'; import { getVisData, GetVisData, GetVisDataOptions } from './lib/get_vis_data'; import { ValidationTelemetryService } from './validation_telemetry'; import { UsageCollectionSetup } from '../../usage_collection/server'; diff --git a/src/plugins/vis_type_timeseries/server/validation_telemetry/saved_object_type.ts b/src/plugins/vis_type_timeseries/server/validation_telemetry/saved_object_type.ts index 77b49e824334f..f18fa1e4cc2fa 100644 --- a/src/plugins/vis_type_timeseries/server/validation_telemetry/saved_object_type.ts +++ b/src/plugins/vis_type_timeseries/server/validation_telemetry/saved_object_type.ts @@ -31,7 +31,7 @@ const resetCount: SavedObjectMigrationFn = doc => ({ export const tsvbTelemetrySavedObjectType: SavedObjectsType = { name: 'tsvb-validation-telemetry', hidden: false, - namespaceAgnostic: true, + namespaceType: 'agnostic', mappings: { properties: { failedRequests: { diff --git a/src/plugins/vis_type_xy/kibana.json b/src/plugins/vis_type_xy/kibana.json new file mode 100644 index 0000000000000..ca02da45e9112 --- /dev/null +++ b/src/plugins/vis_type_xy/kibana.json @@ -0,0 +1,7 @@ +{ + "id": "visTypeXy", + "version": "kibana", + "server": true, + "ui": true, + "requiredPlugins": ["charts", "expressions", "visualizations"] +} diff --git a/src/plugins/vis_type_xy/public/index.ts b/src/plugins/vis_type_xy/public/index.ts new file mode 100644 index 0000000000000..9af75ce9059e9 --- /dev/null +++ b/src/plugins/vis_type_xy/public/index.ts @@ -0,0 +1,27 @@ +/* + * 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 { PluginInitializerContext } from '../../../core/public'; +import { VisTypeXyPlugin as Plugin } from './plugin'; + +export { VisTypeXyPluginSetup } from './plugin'; + +export function plugin(initializerContext: PluginInitializerContext) { + return new Plugin(initializerContext); +} diff --git a/src/plugins/vis_type_xy/public/plugin.ts b/src/plugins/vis_type_xy/public/plugin.ts new file mode 100644 index 0000000000000..667018c1e6e30 --- /dev/null +++ b/src/plugins/vis_type_xy/public/plugin.ts @@ -0,0 +1,87 @@ +/* + * 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 { + CoreSetup, + CoreStart, + Plugin, + IUiSettingsClient, + PluginInitializerContext, +} from 'kibana/public'; + +import { Plugin as ExpressionsPublicPlugin } from '../../expressions/public'; +import { VisualizationsSetup, VisualizationsStart } from '../../visualizations/public'; +import { ChartsPluginSetup } from '../../charts/public'; + +export interface VisTypeXyDependencies { + uiSettings: IUiSettingsClient; + charts: ChartsPluginSetup; +} + +// eslint-disable-next-line @typescript-eslint/no-empty-interface +export interface VisTypeXyPluginSetup {} + +/** @internal */ +export interface VisTypeXyPluginSetupDependencies { + expressions: ReturnType; + visualizations: VisualizationsSetup; + charts: ChartsPluginSetup; +} + +/** @internal */ +export interface VisTypeXyPluginStartDependencies { + expressions: ReturnType; + visualizations: VisualizationsStart; +} + +type VisTypeXyCoreSetup = CoreSetup; + +/** @internal */ +export class VisTypeXyPlugin implements Plugin { + constructor(public initializerContext: PluginInitializerContext) {} + + public async setup( + core: VisTypeXyCoreSetup, + { expressions, visualizations, charts }: VisTypeXyPluginSetupDependencies + ) { + // eslint-disable-next-line no-console + console.warn( + 'The visTypeXy plugin is enabled\n\n', + 'This may negatively alter existing vislib visualization configurations if saved.' + ); + const visualizationDependencies: Readonly = { + uiSettings: core.uiSettings, + charts, + }; + + const visTypeDefinitions: any[] = []; + const visFunctions: any = []; + + visFunctions.forEach((fn: any) => expressions.registerFunction(fn)); + visTypeDefinitions.forEach((vis: any) => + visualizations.createBaseVisualization(vis(visualizationDependencies)) + ); + + return {}; + } + + public start(core: CoreStart, deps: VisTypeXyPluginStartDependencies) { + // nothing to do here + } +} diff --git a/src/plugins/vis_type_xy/server/index.ts b/src/plugins/vis_type_xy/server/index.ts new file mode 100644 index 0000000000000..afc879dc9c845 --- /dev/null +++ b/src/plugins/vis_type_xy/server/index.ts @@ -0,0 +1,29 @@ +/* + * 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 { schema } from '@kbn/config-schema'; + +export const config = { + schema: schema.object({ enabled: schema.boolean({ defaultValue: false }) }), +}; + +export const plugin = () => ({ + setup() {}, + start() {}, +}); diff --git a/src/plugins/visualizations/kibana.json b/src/plugins/visualizations/kibana.json index cd22b1375ae1b..f3f9cbd8341ec 100644 --- a/src/plugins/visualizations/kibana.json +++ b/src/plugins/visualizations/kibana.json @@ -3,5 +3,5 @@ "version": "kibana", "server": true, "ui": true, - "requiredPlugins": ["data", "expressions", "uiActions", "embeddable", "usageCollection"] + "requiredPlugins": ["data", "expressions", "uiActions", "embeddable", "usageCollection", "inspector"] } diff --git a/src/plugins/visualizations/public/embeddable/create_vis_embeddable_from_object.ts b/src/plugins/visualizations/public/embeddable/create_vis_embeddable_from_object.ts index bf2d174f594b2..8e51bd4ac5d4f 100644 --- a/src/plugins/visualizations/public/embeddable/create_vis_embeddable_from_object.ts +++ b/src/plugins/visualizations/public/embeddable/create_vis_embeddable_from_object.ts @@ -28,8 +28,9 @@ import { getTimeFilter, getCapabilities, } from '../services'; +import { VisualizeEmbeddableFactoryDeps } from './visualize_embeddable_factory'; -export const createVisEmbeddableFromObject = async ( +export const createVisEmbeddableFromObject = (deps: VisualizeEmbeddableFactoryDeps) => async ( vis: Vis, input: Partial & { id: string }, parent?: IContainer @@ -58,6 +59,7 @@ export const createVisEmbeddableFromObject = async ( indexPatterns, editUrl, editable, + deps, }, input, parent diff --git a/src/plugins/visualizations/public/embeddable/visualize_embeddable.ts b/src/plugins/visualizations/public/embeddable/visualize_embeddable.ts index e64d200251797..ffb028ff131b3 100644 --- a/src/plugins/visualizations/public/embeddable/visualize_embeddable.ts +++ b/src/plugins/visualizations/public/embeddable/visualize_embeddable.ts @@ -42,6 +42,7 @@ import { buildPipeline } from '../legacy/build_pipeline'; import { Vis } from '../vis'; import { getExpressions, getUiActions } from '../services'; import { VIS_EVENT_TO_TRIGGER } from './events'; +import { VisualizeEmbeddableFactoryDeps } from './visualize_embeddable_factory'; const getKeys = (o: T): Array => Object.keys(o) as Array; @@ -50,6 +51,7 @@ export interface VisualizeEmbeddableConfiguration { indexPatterns?: IIndexPattern[]; editUrl: string; editable: boolean; + deps: VisualizeEmbeddableFactoryDeps; } export interface VisualizeInput extends EmbeddableInput { @@ -84,10 +86,11 @@ export class VisualizeEmbeddable extends Embeddable { - if (this.handler) { - return this.handler.openInspector(this.getTitle() || ''); - } + if (!this.handler) return; + + const adapters = this.handler.inspect(); + if (!adapters) return; + + this.deps.start().plugins.inspector.open(adapters, { + title: this.getTitle() || '', + }); }; /** diff --git a/src/plugins/visualizations/public/embeddable/visualize_embeddable_factory.tsx b/src/plugins/visualizations/public/embeddable/visualize_embeddable_factory.tsx index 4b7d01ae3b246..6ab1c98645988 100644 --- a/src/plugins/visualizations/public/embeddable/visualize_embeddable_factory.tsx +++ b/src/plugins/visualizations/public/embeddable/visualize_embeddable_factory.tsx @@ -25,7 +25,7 @@ import { EmbeddableOutput, ErrorEmbeddable, IContainer, -} from '../../../../plugins/embeddable/public'; +} from '../../../embeddable/public'; import { DisabledLabEmbeddable } from './disabled_lab_embeddable'; import { VisualizeEmbeddable, VisualizeInput, VisualizeOutput } from './visualize_embeddable'; import { VISUALIZE_EMBEDDABLE_TYPE } from './constants'; @@ -39,11 +39,17 @@ import { import { showNewVisModal } from '../wizard'; import { convertToSerializedVis } from '../saved_visualizations/_saved_vis'; import { createVisEmbeddableFromObject } from './create_vis_embeddable_from_object'; +import { StartServicesGetter } from '../../../kibana_utils/public'; +import { VisualizationsStartDeps } from '../plugin'; interface VisualizationAttributes extends SavedObjectAttributes { visState: string; } +export interface VisualizeEmbeddableFactoryDeps { + start: StartServicesGetter>; +} + export class VisualizeEmbeddableFactory implements EmbeddableFactoryDefinition< @@ -79,7 +85,8 @@ export class VisualizeEmbeddableFactory return visType.stage !== 'experimental'; }, }; - constructor() {} + + constructor(private readonly deps: VisualizeEmbeddableFactoryDeps) {} public async isEditable() { return getCapabilities().visualize.save as boolean; @@ -101,7 +108,7 @@ export class VisualizeEmbeddableFactory try { const savedObject = await savedVisualizations.get(savedObjectId); const vis = new Vis(savedObject.visState.type, await convertToSerializedVis(savedObject)); - return createVisEmbeddableFromObject(vis, input, parent); + return createVisEmbeddableFromObject(this.deps)(vis, input, parent); } catch (e) { console.error(e); // eslint-disable-line no-console return new ErrorEmbeddable(e, input, parent); diff --git a/src/plugins/visualizations/public/index.scss b/src/plugins/visualizations/public/index.scss index eada763b63c4d..2b61535f3e7f2 100644 --- a/src/plugins/visualizations/public/index.scss +++ b/src/plugins/visualizations/public/index.scss @@ -1,2 +1,3 @@ @import 'wizard/index'; @import 'embeddable/index'; +@import 'components/index'; diff --git a/src/plugins/visualizations/public/mocks.ts b/src/plugins/visualizations/public/mocks.ts index 2aa346423297a..d6eeffdb01459 100644 --- a/src/plugins/visualizations/public/mocks.ts +++ b/src/plugins/visualizations/public/mocks.ts @@ -26,6 +26,7 @@ import { expressionsPluginMock } from '../../../plugins/expressions/public/mocks import { dataPluginMock } from '../../../plugins/data/public/mocks'; import { usageCollectionPluginMock } from '../../../plugins/usage_collection/public/mocks'; import { uiActionsPluginMock } from '../../../plugins/ui_actions/public/mocks'; +import { inspectorPluginMock } from '../../../plugins/inspector/public/mocks'; const createSetupContract = (): VisualizationsSetup => ({ createBaseVisualization: jest.fn(), @@ -53,14 +54,16 @@ const createInstance = async () => { const setup = plugin.setup(coreMock.createSetup(), { data: dataPluginMock.createSetupContract(), - expressions: expressionsPluginMock.createSetupContract(), embeddable: embeddablePluginMock.createSetupContract(), + expressions: expressionsPluginMock.createSetupContract(), + inspector: inspectorPluginMock.createSetupContract(), usageCollection: usageCollectionPluginMock.createSetupContract(), }); const doStart = () => plugin.start(coreMock.createStart(), { data: dataPluginMock.createStartContract(), expressions: expressionsPluginMock.createStartContract(), + inspector: inspectorPluginMock.createStartContract(), uiActions: uiActionsPluginMock.createStartContract(), }); diff --git a/src/plugins/visualizations/public/plugin.ts b/src/plugins/visualizations/public/plugin.ts index 8fcb84b19a9be..b3e8c9b5b61b3 100644 --- a/src/plugins/visualizations/public/plugin.ts +++ b/src/plugins/visualizations/public/plugin.ts @@ -43,18 +43,23 @@ import { VisualizeEmbeddableFactory, createVisEmbeddableFromObject, } from './embeddable'; -import { ExpressionsSetup, ExpressionsStart } from '../../../plugins/expressions/public'; -import { EmbeddableSetup } from '../../../plugins/embeddable/public'; +import { ExpressionsSetup, ExpressionsStart } from '../../expressions/public'; +import { EmbeddableSetup } from '../../embeddable/public'; import { visualization as visualizationFunction } from './expressions/visualization_function'; import { visualization as visualizationRenderer } from './expressions/visualization_renderer'; import { range as rangeExpressionFunction } from './expression_functions/range'; import { visDimension as visDimensionExpressionFunction } from './expression_functions/vis_dimension'; import { DataPublicPluginSetup, DataPublicPluginStart } from '../../../plugins/data/public'; -import { UsageCollectionSetup } from '../../../plugins/usage_collection/public'; +import { + Setup as InspectorSetup, + Start as InspectorStart, +} from '../../../plugins/inspector/public'; +import { UsageCollectionSetup } from '../../usage_collection/public'; +import { createStartServicesGetter, StartServicesGetter } from '../../kibana_utils/public'; import { createSavedVisLoader, SavedVisualizationsLoader } from './saved_visualizations'; import { SerializedVis, Vis } from './vis'; import { showNewVisModal } from './wizard'; -import { UiActionsStart } from '../../../plugins/ui_actions/public'; +import { UiActionsStart } from '../../ui_actions/public'; import { convertFromSerializedVis, convertToSerializedVis, @@ -74,19 +79,21 @@ export interface VisualizationsStart extends TypesStart { convertToSerializedVis: typeof convertToSerializedVis; convertFromSerializedVis: typeof convertFromSerializedVis; showNewVisModal: typeof showNewVisModal; - __LEGACY: { createVisEmbeddableFromObject: typeof createVisEmbeddableFromObject }; + __LEGACY: { createVisEmbeddableFromObject: ReturnType }; } export interface VisualizationsSetupDeps { - expressions: ExpressionsSetup; + data: DataPublicPluginSetup; embeddable: EmbeddableSetup; + expressions: ExpressionsSetup; + inspector: InspectorSetup; usageCollection: UsageCollectionSetup; - data: DataPublicPluginSetup; } export interface VisualizationsStartDeps { data: DataPublicPluginStart; expressions: ExpressionsStart; + inspector: InspectorStart; uiActions: UiActionsStart; } @@ -107,13 +114,16 @@ export class VisualizationsPlugin VisualizationsStartDeps > { private readonly types: TypesService = new TypesService(); + private getStartServicesOrDie?: StartServicesGetter; constructor(initializerContext: PluginInitializerContext) {} public setup( - core: CoreSetup, + core: CoreSetup, { expressions, embeddable, usageCollection, data }: VisualizationsSetupDeps ): VisualizationsSetup { + const start = (this.getStartServicesOrDie = createStartServicesGetter(core.getStartServices)); + setUISettings(core.uiSettings); setUsageCollector(usageCollection); @@ -122,7 +132,7 @@ export class VisualizationsPlugin expressions.registerFunction(rangeExpressionFunction); expressions.registerFunction(visDimensionExpressionFunction); - const embeddableFactory = new VisualizeEmbeddableFactory(); + const embeddableFactory = new VisualizeEmbeddableFactory({ start }); embeddable.registerEmbeddableFactory(VISUALIZE_EMBEDDABLE_TYPE, embeddableFactory); return { @@ -171,7 +181,11 @@ export class VisualizationsPlugin convertToSerializedVis, convertFromSerializedVis, savedVisualizationsLoader, - __LEGACY: { createVisEmbeddableFromObject }, + __LEGACY: { + createVisEmbeddableFromObject: createVisEmbeddableFromObject({ + start: this.getStartServicesOrDie!, + }), + }, }; } diff --git a/src/plugins/visualizations/server/saved_objects/visualization.ts b/src/plugins/visualizations/server/saved_objects/visualization.ts index 9f4782f3ec730..cd2211c185530 100644 --- a/src/plugins/visualizations/server/saved_objects/visualization.ts +++ b/src/plugins/visualizations/server/saved_objects/visualization.ts @@ -23,7 +23,7 @@ import { visualizationSavedObjectTypeMigrations } from './visualization_migratio export const visualizationSavedObjectType: SavedObjectsType = { name: 'visualization', hidden: false, - namespaceAgnostic: false, + namespaceType: 'single', management: { icon: 'visualizeApp', defaultSearchField: 'title', diff --git a/src/plugins/visualizations/server/saved_objects/visualization_migrations.test.ts b/src/plugins/visualizations/server/saved_objects/visualization_migrations.test.ts index 26f8278cd3d43..83d53d27e41fd 100644 --- a/src/plugins/visualizations/server/saved_objects/visualization_migrations.test.ts +++ b/src/plugins/visualizations/server/saved_objects/visualization_migrations.test.ts @@ -1460,4 +1460,62 @@ describe('migration visualization', () => { expect(migratedParams.gauge_color_rules[1]).toEqual(params.gauge_color_rules[1]); }); }); + + describe('7.8.0 tsvb split_color_mode', () => { + const migrate = (doc: any) => + visualizationSavedObjectTypeMigrations['7.8.0']( + doc as Parameters[0], + savedObjectMigrationContext + ); + + const generateDoc = (params: any) => ({ + attributes: { + title: 'My Vis', + type: 'visualization', + description: 'This is my super cool vis.', + visState: JSON.stringify(params), + uiStateJSON: '{}', + version: 1, + kibanaSavedObjectMeta: { + searchSourceJSON: '{}', + }, + }, + }); + + it('should change a missing split_color_mode to gradient', () => { + const params = { type: 'metrics', params: { series: [{}] } }; + const testDoc1 = generateDoc(params); + const migratedTestDoc1 = migrate(testDoc1); + const series = JSON.parse(migratedTestDoc1.attributes.visState).params.series; + + expect(series[0].split_color_mode).toEqual('gradient'); + }); + + it('should not change the color mode if it is set', () => { + const params = { type: 'metrics', params: { series: [{ split_color_mode: 'gradient' }] } }; + const testDoc1 = generateDoc(params); + const migratedTestDoc1 = migrate(testDoc1); + const series = JSON.parse(migratedTestDoc1.attributes.visState).params.series; + + expect(series[0].split_color_mode).toEqual('gradient'); + }); + + it('should not change the color mode if it is non-default', () => { + const params = { type: 'metrics', params: { series: [{ split_color_mode: 'rainbow' }] } }; + const testDoc1 = generateDoc(params); + const migratedTestDoc1 = migrate(testDoc1); + const series = JSON.parse(migratedTestDoc1.attributes.visState).params.series; + + expect(series[0].split_color_mode).toEqual('rainbow'); + }); + + it('should not migrate a visualization of unknown type', () => { + const params = { type: 'unknown', params: { series: [{}] } }; + const doc = generateDoc(params); + const migratedDoc = migrate(doc); + const series = JSON.parse(migratedDoc.attributes.visState).params.series; + + expect(series[0].split_color_mode).toBeUndefined(); + }); + }); }); diff --git a/src/plugins/visualizations/server/saved_objects/visualization_migrations.ts b/src/plugins/visualizations/server/saved_objects/visualization_migrations.ts index 80783e41863ea..94473e35a942d 100644 --- a/src/plugins/visualizations/server/saved_objects/visualization_migrations.ts +++ b/src/plugins/visualizations/server/saved_objects/visualization_migrations.ts @@ -602,7 +602,39 @@ const migrateMatchAllQuery: SavedObjectMigrationFn = doc => { }; } } + return doc; +}; + +// [TSVB] Default color palette is changing, keep the default for older viz +const migrateTsvbDefaultColorPalettes: SavedObjectMigrationFn = doc => { + const visStateJSON = get(doc, 'attributes.visState'); + let visState; + + if (visStateJSON) { + try { + visState = JSON.parse(visStateJSON); + } catch (e) { + // Let it go, the data is invalid and we'll leave it as is + } + if (visState && visState.type === 'metrics') { + const series: any[] = get(visState, 'params.series') || []; + series.forEach(part => { + // The default value was not saved before + if (!part.split_color_mode) { + part.split_color_mode = 'gradient'; + } + }); + + return { + ...doc, + attributes: { + ...doc.attributes, + visState: JSON.stringify(visState), + }, + }; + } + } return doc; }; @@ -639,4 +671,5 @@ export const visualizationSavedObjectTypeMigrations = { '7.3.1': flow(migrateFiltersAggQueryStringQueries), '7.4.2': flow(transformSplitFiltersStringToQueryObject), '7.7.0': flow(migrateOperatorKeyTypo), + '7.8.0': flow(migrateTsvbDefaultColorPalettes), }; diff --git a/src/plugins/visualize/kibana.json b/src/plugins/visualize/kibana.json new file mode 100644 index 0000000000000..a7afa0697a5eb --- /dev/null +++ b/src/plugins/visualize/kibana.json @@ -0,0 +1,17 @@ +{ + "id": "visualize", + "version": "kibana", + "server": false, + "ui": true, + "requiredPlugins": [ + "data", + "kibanaLegacy", + "navigation", + "savedObjects", + "visualizations" + ], + "optionalPlugins": [ + "home", + "share" + ] +} diff --git a/src/legacy/core_plugins/kibana/public/visualize/np_ready/application.ts b/src/plugins/visualize/public/application/application.ts similarity index 95% rename from src/legacy/core_plugins/kibana/public/visualize/np_ready/application.ts rename to src/plugins/visualize/public/application/application.ts index 241397884c8fe..9d8a1b98ef023 100644 --- a/src/legacy/core_plugins/kibana/public/visualize/np_ready/application.ts +++ b/src/plugins/visualize/public/application/application.ts @@ -17,16 +17,18 @@ * under the License. */ +import './index.scss'; + import angular, { IModule } from 'angular'; import { i18nDirective, i18nFilter, I18nProvider } from '@kbn/i18n/angular'; import { AppMountContext } from 'kibana/public'; -import { configureAppAngularModule } from '../legacy_imports'; -import { NavigationPublicPluginStart as NavigationStart } from '../../../../../../plugins/navigation/public'; +import { NavigationPublicPluginStart as NavigationStart } from 'src/plugins/navigation/public'; import { + configureAppAngularModule, createTopNavDirective, createTopNavHelper, -} from '../../../../../../plugins/kibana_legacy/public'; +} from '../../../kibana_legacy/public'; // @ts-ignore import { initVisualizeApp } from './legacy_app'; diff --git a/src/legacy/core_plugins/kibana/public/visualize/np_ready/breadcrumbs.ts b/src/plugins/visualize/public/application/breadcrumbs.ts similarity index 86% rename from src/legacy/core_plugins/kibana/public/visualize/np_ready/breadcrumbs.ts rename to src/plugins/visualize/public/application/breadcrumbs.ts index b6a63d50b205b..972bdc1462b2c 100644 --- a/src/legacy/core_plugins/kibana/public/visualize/np_ready/breadcrumbs.ts +++ b/src/plugins/visualize/public/application/breadcrumbs.ts @@ -24,7 +24,7 @@ import { VisualizeConstants } from './visualize_constants'; export function getLandingBreadcrumbs() { return [ { - text: i18n.translate('kbn.visualize.listing.breadcrumb', { + text: i18n.translate('visualize.listing.breadcrumb', { defaultMessage: 'Visualize', }), href: `#${VisualizeConstants.LANDING_PAGE_PATH}`, @@ -36,7 +36,7 @@ export function getWizardStep1Breadcrumbs() { return [ ...getLandingBreadcrumbs(), { - text: i18n.translate('kbn.visualize.wizard.step1Breadcrumb', { + text: i18n.translate('visualize.wizard.step1Breadcrumb', { defaultMessage: 'Create', }), }, @@ -47,7 +47,7 @@ export function getWizardStep2Breadcrumbs() { return [ ...getLandingBreadcrumbs(), { - text: i18n.translate('kbn.visualize.wizard.step2Breadcrumb', { + text: i18n.translate('visualize.wizard.step2Breadcrumb', { defaultMessage: 'Create', }), }, @@ -58,7 +58,7 @@ export function getCreateBreadcrumbs() { return [ ...getLandingBreadcrumbs(), { - text: i18n.translate('kbn.visualize.editor.createBreadcrumb', { + text: i18n.translate('visualize.editor.createBreadcrumb', { defaultMessage: 'Create', }), }, diff --git a/src/legacy/core_plugins/kibana/public/visualize/np_ready/editor/_editor.scss b/src/plugins/visualize/public/application/editor/_editor.scss similarity index 100% rename from src/legacy/core_plugins/kibana/public/visualize/np_ready/editor/_editor.scss rename to src/plugins/visualize/public/application/editor/_editor.scss diff --git a/src/legacy/core_plugins/kibana/public/visualize/np_ready/editor/_index.scss b/src/plugins/visualize/public/application/editor/_index.scss similarity index 100% rename from src/legacy/core_plugins/kibana/public/visualize/np_ready/editor/_index.scss rename to src/plugins/visualize/public/application/editor/_index.scss diff --git a/src/legacy/core_plugins/kibana/public/visualize/np_ready/editor/editor.html b/src/plugins/visualize/public/application/editor/editor.html similarity index 98% rename from src/legacy/core_plugins/kibana/public/visualize/np_ready/editor/editor.html rename to src/plugins/visualize/public/application/editor/editor.html index 0dcacd30fba4e..a031d70ef9a83 100644 --- a/src/legacy/core_plugins/kibana/public/visualize/np_ready/editor/editor.html +++ b/src/plugins/visualize/public/application/editor/editor.html @@ -80,7 +80,7 @@

) : null; @@ -229,10 +220,10 @@ function VisualizeAppController($scope, $route, $injector, $timeout, kbnUrlState : []), { id: 'share', - label: i18n.translate('kbn.topNavMenu.shareVisualizationButtonLabel', { + label: i18n.translate('visualize.topNavMenu.shareVisualizationButtonLabel', { defaultMessage: 'share', }), - description: i18n.translate('kbn.visualize.topNavMenu.shareVisualizationButtonAriaLabel', { + description: i18n.translate('visualize.topNavMenu.shareVisualizationButtonAriaLabel', { defaultMessage: 'Share Visualization', }), testId: 'shareTopNavButton', @@ -252,13 +243,15 @@ function VisualizeAppController($scope, $route, $injector, $timeout, kbnUrlState isDirty: hasUnappliedChanges || hasUnsavedChanges, }); }, + // disable the Share button if no action specified + disableButton: !share, }, { id: 'inspector', - label: i18n.translate('kbn.topNavMenu.openInspectorButtonLabel', { + label: i18n.translate('visualize.topNavMenu.openInspectorButtonLabel', { defaultMessage: 'inspect', }), - description: i18n.translate('kbn.visualize.topNavMenu.openInspectorButtonAriaLabel', { + description: i18n.translate('visualize.topNavMenu.openInspectorButtonAriaLabel', { defaultMessage: 'Open Inspector for visualization', }), testId: 'openInspectorButton', @@ -274,23 +267,12 @@ function VisualizeAppController($scope, $route, $injector, $timeout, kbnUrlState }, tooltip() { if (!embeddableHandler.hasInspector || !embeddableHandler.hasInspector()) { - return i18n.translate('kbn.visualize.topNavMenu.openInspectorDisabledButtonTooltip', { + return i18n.translate('visualize.topNavMenu.openInspectorDisabledButtonTooltip', { defaultMessage: `This visualization doesn't support any inspectors.`, }); } }, }, - { - id: 'refresh', - label: i18n.translate('kbn.topNavMenu.refreshButtonLabel', { defaultMessage: 'refresh' }), - description: i18n.translate('kbn.visualize.topNavMenu.refreshButtonAriaLabel', { - defaultMessage: 'Refresh', - }), - run: function() { - embeddableHandler.reload(); - }, - testId: 'visualizeRefreshButton', - }, ]; if (savedVis.id) { @@ -330,7 +312,7 @@ function VisualizeAppController($scope, $route, $injector, $timeout, kbnUrlState stopAllSyncing(); toastNotifications.addWarning({ - title: i18n.translate('kbn.visualize.visualizationTypeInvalidNotificationMessage', { + title: i18n.translate('visualize.visualizationTypeInvalidNotificationMessage', { defaultMessage: 'Invalid visualization type', }), text: toMountPoint({error.message}), @@ -370,7 +352,7 @@ function VisualizeAppController($scope, $route, $injector, $timeout, kbnUrlState return; } - savedQueryService.getSavedQuery(savedQueryId).then(savedQuery => { + queryService.savedQueries.getSavedQuery(savedQueryId).then(savedQuery => { $scope.$evalAsync(() => { $scope.updateSavedQuery(savedQuery); }); @@ -633,7 +615,7 @@ function VisualizeAppController($scope, $route, $injector, $timeout, kbnUrlState if (id) { toastNotifications.addSuccess({ title: i18n.translate( - 'kbn.visualize.topNavMenu.saveVisualization.successNotificationText', + 'visualize.topNavMenu.saveVisualization.successNotificationText', { defaultMessage: `Saved '{visTitle}'`, values: { @@ -672,15 +654,12 @@ function VisualizeAppController($scope, $route, $injector, $timeout, kbnUrlState // eslint-disable-next-line console.error(error); toastNotifications.addDanger({ - title: i18n.translate( - 'kbn.visualize.topNavMenu.saveVisualization.failureNotificationText', - { - defaultMessage: `Error on saving '{visTitle}'`, - values: { - visTitle: savedVis.title, - }, - } - ), + title: i18n.translate('visualize.topNavMenu.saveVisualization.failureNotificationText', { + defaultMessage: `Error on saving '{visTitle}'`, + values: { + visTitle: savedVis.title, + }, + }), text: error.message, 'data-test-subj': 'saveVisualizationError', }); @@ -703,7 +682,7 @@ function VisualizeAppController($scope, $route, $injector, $timeout, kbnUrlState }); toastNotifications.addSuccess( - i18n.translate('kbn.visualize.linkedToSearch.unlinkSuccessNotificationText', { + i18n.translate('visualize.linkedToSearch.unlinkSuccessNotificationText', { defaultMessage: `Unlinked from saved search '{searchTitle}'`, values: { searchTitle: savedSearch.title, @@ -715,7 +694,7 @@ function VisualizeAppController($scope, $route, $injector, $timeout, kbnUrlState $scope.getAdditionalMessage = () => { return ( '' + - i18n.translate('kbn.visualize.experimentalVisInfoText', { + i18n.translate('visualize.experimentalVisInfoText', { defaultMessage: 'This visualization is marked as experimental.', }) + ' ' + diff --git a/src/legacy/core_plugins/kibana/public/visualize/np_ready/editor/lib/index.ts b/src/plugins/visualize/public/application/editor/lib/index.ts similarity index 100% rename from src/legacy/core_plugins/kibana/public/visualize/np_ready/editor/lib/index.ts rename to src/plugins/visualize/public/application/editor/lib/index.ts diff --git a/src/legacy/core_plugins/kibana/public/visualize/np_ready/editor/lib/make_stateful.ts b/src/plugins/visualize/public/application/editor/lib/make_stateful.ts similarity index 91% rename from src/legacy/core_plugins/kibana/public/visualize/np_ready/editor/lib/make_stateful.ts rename to src/plugins/visualize/public/application/editor/lib/make_stateful.ts index 8384585108a59..d071df314d99c 100644 --- a/src/legacy/core_plugins/kibana/public/visualize/np_ready/editor/lib/make_stateful.ts +++ b/src/plugins/visualize/public/application/editor/lib/make_stateful.ts @@ -17,8 +17,8 @@ * under the License. */ -import { PersistedState } from '../../../../../../../../plugins/visualizations/public'; -import { ReduxLikeStateContainer } from '../../../../../../../../plugins/kibana_utils/public'; +import { PersistedState } from '../../../../../visualizations/public'; +import { ReduxLikeStateContainer } from '../../../../../kibana_utils/public'; import { VisualizeAppState, VisualizeAppStateTransitions } from '../../types'; /** diff --git a/src/legacy/core_plugins/kibana/public/visualize/np_ready/editor/lib/migrate_app_state.ts b/src/plugins/visualize/public/application/editor/lib/migrate_app_state.ts similarity index 100% rename from src/legacy/core_plugins/kibana/public/visualize/np_ready/editor/lib/migrate_app_state.ts rename to src/plugins/visualize/public/application/editor/lib/migrate_app_state.ts diff --git a/src/plugins/visualize/public/application/editor/lib/url_helper.test.ts b/src/plugins/visualize/public/application/editor/lib/url_helper.test.ts new file mode 100644 index 0000000000000..09609e3d7e362 --- /dev/null +++ b/src/plugins/visualize/public/application/editor/lib/url_helper.test.ts @@ -0,0 +1,39 @@ +/* + * 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 { addEmbeddableToDashboardUrl } from './url_helper'; + +describe('', () => { + it('addEmbeddableToDashboardUrl when dashboard is not saved', () => { + const id = '123eb456cd'; + const url = + "/pep/app/kibana#/dashboard?_g=(refreshInterval:(pause:!t,value:0),time:(from:now-15m,to:now))&_a=(description:'',filters:!())"; + expect(addEmbeddableToDashboardUrl(url, id)).toEqual( + `/dashboard?_a=%28description%3A%27%27%2Cfilters%3A%21%28%29%29&_g=%28refreshInterval%3A%28pause%3A%21t%2Cvalue%3A0%29%2Ctime%3A%28from%3Anow-15m%2Cto%3Anow%29%29&addEmbeddableId=${id}&addEmbeddableType=visualization` + ); + }); + it('addEmbeddableToDashboardUrl when dashboard is saved', () => { + const id = '123eb456cd'; + const url = + "/pep/app/kibana#/dashboard/9b780cd0-3dd3-11e8-b2b9-5d5dc1715159?_g=(refreshInterval:(pause:!t,value:0),time:(from:now-15m,to:now))&_a=(description:'',filters:!())"; + expect(addEmbeddableToDashboardUrl(url, id)).toEqual( + `/dashboard/9b780cd0-3dd3-11e8-b2b9-5d5dc1715159?_a=%28description%3A%27%27%2Cfilters%3A%21%28%29%29&_g=%28refreshInterval%3A%28pause%3A%21t%2Cvalue%3A0%29%2Ctime%3A%28from%3Anow-15m%2Cto%3Anow%29%29&addEmbeddableId=${id}&addEmbeddableType=visualization` + ); + }); +}); diff --git a/src/plugins/visualize/public/application/editor/lib/url_helper.ts b/src/plugins/visualize/public/application/editor/lib/url_helper.ts new file mode 100644 index 0000000000000..84e1ef9687cd0 --- /dev/null +++ b/src/plugins/visualize/public/application/editor/lib/url_helper.ts @@ -0,0 +1,40 @@ +/* + * 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 { parseUrl, stringify } from 'query-string'; +import { DashboardConstants } from '../../../../../dashboard/public'; +import { VISUALIZE_EMBEDDABLE_TYPE } from '../../../../../visualizations/public'; + +/** * + * Returns relative dashboard URL with added embeddableType and embeddableId query params + * eg. + * input: url: lol/app/kibana#/dashboard?_g=(refreshInterval:(pause:!t,value:0),time:(from:now-15m,to:now)), embeddableId: 12345 + * output: /dashboard?addEmbeddableType=visualization&addEmbeddableId=12345&_g=(refreshInterval:(pause:!t,value:0),time:(from:now-15m,to:now)) + * @param url dasbhoard absolute url + * @param embeddableId id of the saved visualization + */ +export function addEmbeddableToDashboardUrl(dashboardUrl: string, embeddableId: string) { + const { url, query } = parseUrl(dashboardUrl); + const [, dashboardId] = url.split(DashboardConstants.CREATE_NEW_DASHBOARD_URL); + + query[DashboardConstants.ADD_EMBEDDABLE_TYPE] = VISUALIZE_EMBEDDABLE_TYPE; + query[DashboardConstants.ADD_EMBEDDABLE_ID] = embeddableId; + + return `${DashboardConstants.CREATE_NEW_DASHBOARD_URL}${dashboardId}?${stringify(query)}`; +} diff --git a/src/legacy/core_plugins/kibana/public/visualize/np_ready/editor/lib/visualize_app_state.ts b/src/plugins/visualize/public/application/editor/lib/visualize_app_state.ts similarity index 98% rename from src/legacy/core_plugins/kibana/public/visualize/np_ready/editor/lib/visualize_app_state.ts rename to src/plugins/visualize/public/application/editor/lib/visualize_app_state.ts index 86f39ea76dd3a..fe2a19b7315c3 100644 --- a/src/legacy/core_plugins/kibana/public/visualize/np_ready/editor/lib/visualize_app_state.ts +++ b/src/plugins/visualize/public/application/editor/lib/visualize_app_state.ts @@ -24,7 +24,7 @@ import { createStateContainer, syncState, IKbnUrlStateStorage, -} from '../../../../../../../../plugins/kibana_utils/public'; +} from '../../../../../kibana_utils/public'; import { PureVisState, VisualizeAppState, VisualizeAppStateTransitions } from '../../types'; const STATE_STORAGE_KEY = '_a'; diff --git a/src/legacy/core_plugins/kibana/public/visualize/np_ready/editor/visualization.js b/src/plugins/visualize/public/application/editor/visualization.js similarity index 100% rename from src/legacy/core_plugins/kibana/public/visualize/np_ready/editor/visualization.js rename to src/plugins/visualize/public/application/editor/visualization.js diff --git a/src/legacy/core_plugins/kibana/public/visualize/np_ready/editor/visualization_editor.js b/src/plugins/visualize/public/application/editor/visualization_editor.js similarity index 98% rename from src/legacy/core_plugins/kibana/public/visualize/np_ready/editor/visualization_editor.js rename to src/plugins/visualize/public/application/editor/visualization_editor.js index ef174dbaa5865..874b69532ec11 100644 --- a/src/legacy/core_plugins/kibana/public/visualize/np_ready/editor/visualization_editor.js +++ b/src/plugins/visualize/public/application/editor/visualization_editor.js @@ -44,7 +44,6 @@ export function initVisEditorDirective(app, deps) { editor.render({ core: deps.core, data: deps.data, - embeddable: deps.embeddable, uiState: $scope.uiState, timeRange: $scope.timeRange, filters: $scope.filters, diff --git a/src/legacy/core_plugins/kibana/public/visualize/np_ready/help_menu/help_menu_util.js b/src/plugins/visualize/public/application/help_menu/help_menu_util.js similarity index 94% rename from src/legacy/core_plugins/kibana/public/visualize/np_ready/help_menu/help_menu_util.js rename to src/plugins/visualize/public/application/help_menu/help_menu_util.js index 9c00947d7663c..c297326f2e264 100644 --- a/src/legacy/core_plugins/kibana/public/visualize/np_ready/help_menu/help_menu_util.js +++ b/src/plugins/visualize/public/application/help_menu/help_menu_util.js @@ -21,7 +21,7 @@ import { i18n } from '@kbn/i18n'; export function addHelpMenuToAppChrome(chrome, docLinks) { chrome.setHelpExtension({ - appName: i18n.translate('kbn.visualize.helpMenu.appName', { + appName: i18n.translate('visualize.helpMenu.appName', { defaultMessage: 'Visualize', }), links: [ diff --git a/src/legacy/core_plugins/kibana/public/visualize/np_ready/_index.scss b/src/plugins/visualize/public/application/index.scss similarity index 100% rename from src/legacy/core_plugins/kibana/public/visualize/np_ready/_index.scss rename to src/plugins/visualize/public/application/index.scss diff --git a/src/legacy/core_plugins/kibana/public/visualize/np_ready/legacy_app.js b/src/plugins/visualize/public/application/legacy_app.js similarity index 94% rename from src/legacy/core_plugins/kibana/public/visualize/np_ready/legacy_app.js rename to src/plugins/visualize/public/application/legacy_app.js index a710d3e318749..7c5e3ce9408f0 100644 --- a/src/legacy/core_plugins/kibana/public/visualize/np_ready/legacy_app.js +++ b/src/plugins/visualize/public/application/legacy_app.js @@ -25,7 +25,8 @@ import { createKbnUrlStateStorage, redirectWhenMissing, ensureDefaultIndexPattern, -} from '../../../../../../plugins/kibana_utils/public'; +} from '../../../kibana_utils/public'; +import { createSavedSearchesLoader } from '../../../discover/public'; import editorTemplate from './editor/editor.html'; import visualizeListingTemplate from './listing/visualize_listing.html'; @@ -40,7 +41,6 @@ import { getCreateBreadcrumbs, getEditBreadcrumbs, } from './breadcrumbs'; -import { createSavedSearchesLoader } from '../../../../../../plugins/discover/public'; const getResolvedResults = deps => { const { core, data, visualizations, createVisEmbeddableFromObject } = deps; @@ -93,7 +93,7 @@ export function initVisualizeApp(app, deps) { app.factory('kbnUrlStateStorage', history => createKbnUrlStateStorage({ history, - useHash: deps.uiSettings.get('state:storeInSessionStorage'), + useHash: deps.core.uiSettings.get('state:storeInSessionStorage'), }) ); @@ -107,10 +107,10 @@ export function initVisualizeApp(app, deps) { } return { - text: i18n.translate('kbn.visualize.badge.readOnly.text', { + text: i18n.translate('visualize.badge.readOnly.text', { defaultMessage: 'Read only', }), - tooltip: i18n.translate('kbn.visualize.badge.readOnly.tooltip', { + tooltip: i18n.translate('visualize.badge.readOnly.tooltip', { defaultMessage: 'Unable to save visualizations', }), iconType: 'glasses', @@ -156,7 +156,7 @@ export function initVisualizeApp(app, deps) { if (shouldHaveIndex && !hasIndex) { throw new Error( i18n.translate( - 'kbn.visualize.createVisualization.noIndexPatternOrSavedSearchIdErrorMessage', + 'visualize.createVisualization.noIndexPatternOrSavedSearchIdErrorMessage', { defaultMessage: 'You must provide either an indexPattern or a savedSearchId', } diff --git a/src/legacy/core_plugins/kibana/public/visualize/np_ready/listing/_index.scss b/src/plugins/visualize/public/application/listing/_index.scss similarity index 100% rename from src/legacy/core_plugins/kibana/public/visualize/np_ready/listing/_index.scss rename to src/plugins/visualize/public/application/listing/_index.scss diff --git a/src/legacy/core_plugins/kibana/public/visualize/np_ready/listing/_listing.scss b/src/plugins/visualize/public/application/listing/_listing.scss similarity index 100% rename from src/legacy/core_plugins/kibana/public/visualize/np_ready/listing/_listing.scss rename to src/plugins/visualize/public/application/listing/_listing.scss diff --git a/src/legacy/core_plugins/kibana/public/visualize/np_ready/listing/visualize_listing.html b/src/plugins/visualize/public/application/listing/visualize_listing.html similarity index 100% rename from src/legacy/core_plugins/kibana/public/visualize/np_ready/listing/visualize_listing.html rename to src/plugins/visualize/public/application/listing/visualize_listing.html diff --git a/src/legacy/core_plugins/kibana/public/visualize/np_ready/listing/visualize_listing.js b/src/plugins/visualize/public/application/listing/visualize_listing.js similarity index 93% rename from src/legacy/core_plugins/kibana/public/visualize/np_ready/listing/visualize_listing.js rename to src/plugins/visualize/public/application/listing/visualize_listing.js index 098633d046062..900c17fa394de 100644 --- a/src/legacy/core_plugins/kibana/public/visualize/np_ready/listing/visualize_listing.js +++ b/src/plugins/visualize/public/application/listing/visualize_listing.js @@ -24,7 +24,7 @@ import { VisualizeConstants } from '../visualize_constants'; import { i18n } from '@kbn/i18n'; import { getServices } from '../../kibana_services'; -import { syncQueryStateWithUrl } from '../../../../../../../plugins/data/public'; +import { syncQueryStateWithUrl } from '../../../../data/public'; export function initListingDirective(app, I18nContext) { app.directive('visualizeListingTable', reactDirective => @@ -40,9 +40,8 @@ export function VisualizeListingController($scope, createNewVis, kbnUrlStateStor savedVisualizations, data: { query }, toastNotifications, - uiSettings, visualizations, - core: { docLinks, savedObjects }, + core: { docLinks, savedObjects, uiSettings }, } = getServices(); // syncs `_g` portion of url with query services @@ -110,7 +109,7 @@ export function VisualizeListingController($scope, createNewVis, kbnUrlStateStor }) ).catch(error => { toastNotifications.addError(error, { - title: i18n.translate('kbn.visualize.visualizeListingDeleteErrorTitle', { + title: i18n.translate('visualize.visualizeListingDeleteErrorTitle', { defaultMessage: 'Error deleting visualization', }), }); @@ -119,7 +118,7 @@ export function VisualizeListingController($scope, createNewVis, kbnUrlStateStor chrome.setBreadcrumbs([ { - text: i18n.translate('kbn.visualize.visualizeListingBreadcrumbsTitle', { + text: i18n.translate('visualize.visualizeListingBreadcrumbsTitle', { defaultMessage: 'Visualize', }), }, diff --git a/src/legacy/core_plugins/kibana/public/visualize/np_ready/listing/visualize_listing_table.js b/src/plugins/visualize/public/application/listing/visualize_listing_table.js similarity index 83% rename from src/legacy/core_plugins/kibana/public/visualize/np_ready/listing/visualize_listing_table.js rename to src/plugins/visualize/public/application/listing/visualize_listing_table.js index 932ac8996e97e..100becdd3a80f 100644 --- a/src/legacy/core_plugins/kibana/public/visualize/np_ready/listing/visualize_listing_table.js +++ b/src/plugins/visualize/public/application/listing/visualize_listing_table.js @@ -21,7 +21,7 @@ import React, { Component, Fragment } from 'react'; import PropTypes from 'prop-types'; import { FormattedMessage } from '@kbn/i18n/react'; import { i18n } from '@kbn/i18n'; -import { TableListView } from '../../../../../../../plugins/kibana_react/public'; +import { TableListView } from '../../../../kibana_react/public'; import { EuiIcon, EuiBetaBadge, EuiLink, EuiButton, EuiEmptyPrompt } from '@elastic/eui'; @@ -33,7 +33,7 @@ class VisualizeListingTable extends Component { } render() { - const { visualizeCapabilities, uiSettings, toastNotifications } = getServices(); + const { visualizeCapabilities, core, toastNotifications } = getServices(); return ( item.canDelete} initialFilter={''} noItemsFragment={this.getNoItemsMessage()} - entityName={i18n.translate('kbn.visualize.listing.table.entityName', { + entityName={i18n.translate('visualize.listing.table.entityName', { defaultMessage: 'visualization', })} - entityNamePlural={i18n.translate('kbn.visualize.listing.table.entityNamePlural', { + entityNamePlural={i18n.translate('visualize.listing.table.entityNamePlural', { defaultMessage: 'visualizations', })} - tableListTitle={i18n.translate('kbn.visualize.listing.table.listTitle', { + tableListTitle={i18n.translate('visualize.listing.table.listTitle', { defaultMessage: 'Visualizations', })} toastNotifications={toastNotifications} - uiSettings={uiSettings} + uiSettings={core.uiSettings} /> ); } @@ -67,7 +67,7 @@ class VisualizeListingTable extends Component { const tableColumns = [ { field: 'title', - name: i18n.translate('kbn.visualize.listing.table.titleColumnName', { + name: i18n.translate('visualize.listing.table.titleColumnName', { defaultMessage: 'Title', }), sortable: true, @@ -82,7 +82,7 @@ class VisualizeListingTable extends Component { }, { field: 'typeTitle', - name: i18n.translate('kbn.visualize.listing.table.typeColumnName', { + name: i18n.translate('visualize.listing.table.typeColumnName', { defaultMessage: 'Type', }), sortable: true, @@ -96,7 +96,7 @@ class VisualizeListingTable extends Component { }, { field: 'description', - name: i18n.translate('kbn.dashboard.listing.table.descriptionColumnName', { + name: i18n.translate('visualize.listing.table.descriptionColumnName', { defaultMessage: 'Description', }), sortable: true, @@ -116,7 +116,7 @@ class VisualizeListingTable extends Component { title={

@@ -133,7 +133,7 @@ class VisualizeListingTable extends Component { title={

@@ -142,7 +142,7 @@ class VisualizeListingTable extends Component {

@@ -156,7 +156,7 @@ class VisualizeListingTable extends Component { data-test-subj="createVisualizationPromptButton" > @@ -192,10 +192,10 @@ class VisualizeListingTable extends Component { ( + prop: T, + value: VisualizeAppState[T] + ) => VisualizeAppState; + setVis: (state: VisualizeAppState) => (vis: Partial) => VisualizeAppState; + removeSavedQuery: (state: VisualizeAppState) => (defaultQuery: Query) => VisualizeAppState; + unlinkSavedSearch: ( + state: VisualizeAppState + ) => ({ query, parentFilters }: { query?: Query; parentFilters?: Filter[] }) => VisualizeAppState; + updateVisState: (state: VisualizeAppState) => (vis: PureVisState) => VisualizeAppState; + updateFromSavedQuery: (state: VisualizeAppState) => (savedQuery: SavedQuery) => VisualizeAppState; +} + +export interface EditorRenderProps { + core: CoreStart; + data: DataPublicPluginStart; + filters: Filter[]; + timeRange: TimeRange; + query?: Query; + savedSearch?: SavedSearch; + uiState: PersistedState; + /** + * Flag to determine if visualiztion is linked to the saved search + */ + linked: boolean; +} + +export interface SavedVisualizations { + urlFor: (id: string) => string; + get: (id: string) => Promise; +} diff --git a/src/legacy/core_plugins/kibana/public/visualize/np_ready/visualize_app.ts b/src/plugins/visualize/public/application/visualize_app.ts similarity index 95% rename from src/legacy/core_plugins/kibana/public/visualize/np_ready/visualize_app.ts rename to src/plugins/visualize/public/application/visualize_app.ts index a4afac23f4842..1a4e1534db9e2 100644 --- a/src/legacy/core_plugins/kibana/public/visualize/np_ready/visualize_app.ts +++ b/src/plugins/visualize/public/application/visualize_app.ts @@ -27,5 +27,5 @@ import { initListingDirective } from './listing/visualize_listing'; export function initVisualizeAppDirective(app: IModule, deps: VisualizeKibanaServices) { initEditorDirective(app, deps); - initListingDirective(app, deps.core.i18n.Context); + initListingDirective(app, deps.I18nContext); } diff --git a/src/legacy/core_plugins/kibana/public/visualize/np_ready/visualize_constants.ts b/src/plugins/visualize/public/application/visualize_constants.ts similarity index 100% rename from src/legacy/core_plugins/kibana/public/visualize/np_ready/visualize_constants.ts rename to src/plugins/visualize/public/application/visualize_constants.ts diff --git a/src/plugins/visualize/public/index.ts b/src/plugins/visualize/public/index.ts new file mode 100644 index 0000000000000..f7bbcb1456da4 --- /dev/null +++ b/src/plugins/visualize/public/index.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 { PluginInitializerContext } from 'kibana/public'; +import { VisualizePlugin } from './plugin'; + +export { EditorRenderProps } from './application/types'; +export { VisualizeConstants, createVisualizeEditUrl } from './application/visualize_constants'; + +export const plugin = (context: PluginInitializerContext) => { + return new VisualizePlugin(context); +}; diff --git a/src/plugins/visualize/public/kibana_services.ts b/src/plugins/visualize/public/kibana_services.ts new file mode 100644 index 0000000000000..765e9a82ae899 --- /dev/null +++ b/src/plugins/visualize/public/kibana_services.ts @@ -0,0 +1,75 @@ +/* + * 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 { + ChromeStart, + CoreStart, + SavedObjectsClientContract, + ToastsStart, + PluginInitializerContext, + I18nStart, +} from 'kibana/public'; + +import { NavigationPublicPluginStart as NavigationStart } from '../../navigation/public'; +import { Storage } from '../../kibana_utils/public'; +import { SharePluginStart } from '../../share/public'; +import { DataPublicPluginStart } from '../../data/public'; +import { VisualizationsStart } from '../../visualizations/public'; +import { SavedVisualizations } from './application/types'; +import { KibanaLegacyStart } from '../../kibana_legacy/public'; +import { DefaultEditorController } from '../../vis_default_editor/public'; + +export interface VisualizeKibanaServices { + pluginInitializerContext: PluginInitializerContext; + addBasePath: (url: string) => string; + chrome: ChromeStart; + core: CoreStart; + data: DataPublicPluginStart; + localStorage: Storage; + navigation: NavigationStart; + toastNotifications: ToastsStart; + savedObjectsClient: SavedObjectsClientContract; + savedVisualizations: SavedVisualizations; + share?: SharePluginStart; + config: KibanaLegacyStart['config']; + visualizeCapabilities: any; + visualizations: VisualizationsStart; + I18nContext: I18nStart['Context']; + setActiveUrl: (newUrl: string) => void; + DefaultVisualizationEditor: typeof DefaultEditorController; + createVisEmbeddableFromObject: VisualizationsStart['__LEGACY']['createVisEmbeddableFromObject']; +} + +let services: VisualizeKibanaServices | null = null; +export function setServices(newServices: VisualizeKibanaServices) { + services = newServices; +} + +export function getServices() { + if (!services) { + throw new Error( + 'Kibana services not set - are you trying to import this module from outside of the visualize app?' + ); + } + return services; +} + +export function clearServices() { + services = null; +} diff --git a/src/plugins/visualize/public/plugin.ts b/src/plugins/visualize/public/plugin.ts new file mode 100644 index 0000000000000..ab64e083a553d --- /dev/null +++ b/src/plugins/visualize/public/plugin.ts @@ -0,0 +1,158 @@ +/* + * 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 { BehaviorSubject } from 'rxjs'; +import { i18n } from '@kbn/i18n'; +import { filter, map } from 'rxjs/operators'; + +import { + AppMountParameters, + CoreSetup, + CoreStart, + Plugin, + PluginInitializerContext, +} from 'kibana/public'; + +import { Storage, createKbnUrlTracker } from '../../kibana_utils/public'; +import { DataPublicPluginStart, DataPublicPluginSetup, esFilters } from '../../data/public'; +import { NavigationPublicPluginStart as NavigationStart } from '../../navigation/public'; +import { SharePluginStart } from '../../share/public'; +import { KibanaLegacySetup, AngularRenderedAppUpdater } from '../../kibana_legacy/public'; +import { VisualizationsStart } from '../../visualizations/public'; +import { VisualizeConstants } from './application/visualize_constants'; +import { setServices, VisualizeKibanaServices } from './kibana_services'; +import { FeatureCatalogueCategory, HomePublicPluginSetup } from '../../home/public'; +import { DefaultEditorController } from '../../vis_default_editor/public'; + +export interface VisualizePluginStartDependencies { + data: DataPublicPluginStart; + navigation: NavigationStart; + share?: SharePluginStart; + visualizations: VisualizationsStart; +} + +export interface VisualizePluginSetupDependencies { + home?: HomePublicPluginSetup; + kibanaLegacy: KibanaLegacySetup; + data: DataPublicPluginSetup; +} + +export class VisualizePlugin + implements + Plugin { + private appStateUpdater = new BehaviorSubject(() => ({})); + private stopUrlTracking: (() => void) | undefined = undefined; + + constructor(private initializerContext: PluginInitializerContext) {} + + public async setup( + core: CoreSetup, + { home, kibanaLegacy, data }: VisualizePluginSetupDependencies + ) { + const { appMounted, appUnMounted, stop: stopUrlTracker, setActiveUrl } = createKbnUrlTracker({ + baseUrl: core.http.basePath.prepend('/app/kibana'), + defaultSubUrl: '#/visualize', + storageKey: `lastUrl:${core.http.basePath.get()}:visualize`, + navLinkUpdater$: this.appStateUpdater, + toastNotifications: core.notifications.toasts, + stateParams: [ + { + kbnUrlKey: '_g', + stateUpdate$: data.query.state$.pipe( + filter( + ({ changes }) => !!(changes.globalFilters || changes.time || changes.refreshInterval) + ), + map(({ state }) => ({ + ...state, + filters: state.filters?.filter(esFilters.isFilterPinned), + })) + ), + }, + ], + }); + this.stopUrlTracking = () => { + stopUrlTracker(); + }; + + kibanaLegacy.registerLegacyApp({ + id: 'visualize', + title: 'Visualize', + updater$: this.appStateUpdater.asObservable(), + navLinkId: 'kibana:visualize', + mount: async (params: AppMountParameters) => { + const [coreStart, pluginsStart] = await core.getStartServices(); + + appMounted(); + + const deps: VisualizeKibanaServices = { + pluginInitializerContext: this.initializerContext, + addBasePath: coreStart.http.basePath.prepend, + core: coreStart, + config: kibanaLegacy.config, + chrome: coreStart.chrome, + data: pluginsStart.data, + localStorage: new Storage(localStorage), + navigation: pluginsStart.navigation, + savedObjectsClient: coreStart.savedObjects.client, + savedVisualizations: pluginsStart.visualizations.savedVisualizationsLoader, + share: pluginsStart.share, + toastNotifications: coreStart.notifications.toasts, + visualizeCapabilities: coreStart.application.capabilities.visualize, + visualizations: pluginsStart.visualizations, + I18nContext: coreStart.i18n.Context, + setActiveUrl, + DefaultVisualizationEditor: DefaultEditorController, + createVisEmbeddableFromObject: + pluginsStart.visualizations.__LEGACY.createVisEmbeddableFromObject, + }; + setServices(deps); + + const { renderApp } = await import('./application/application'); + const unmount = renderApp(params.element, params.appBasePath, deps); + return () => { + unmount(); + appUnMounted(); + }; + }, + }); + + if (home) { + home.featureCatalogue.register({ + id: 'visualize', + title: 'Visualize', + description: i18n.translate('visualize.visualizeDescription', { + defaultMessage: + 'Create visualizations and aggregate data stores in your Elasticsearch indices.', + }), + icon: 'visualizeApp', + path: `/app/kibana#${VisualizeConstants.LANDING_PAGE_PATH}`, + showOnHomePage: true, + category: FeatureCatalogueCategory.DATA, + }); + } + } + + public start(core: CoreStart, plugins: VisualizePluginStartDependencies) {} + + stop() { + if (this.stopUrlTracking) { + this.stopUrlTracking(); + } + } +} diff --git a/tasks/config/peg.js b/tasks/config/peg.js index 1d8667840faba..2de6ff4b5cff9 100644 --- a/tasks/config/peg.js +++ b/tasks/config/peg.js @@ -26,7 +26,7 @@ module.exports = { }, }, timelion_chain: { - src: 'src/legacy/core_plugins/vis_type_timelion/public/chain.peg', - dest: 'src/legacy/core_plugins/vis_type_timelion/public/_generated_/chain.js', + src: 'src/plugins/vis_type_timelion/public/chain.peg', + dest: 'src/plugins/vis_type_timelion/public/_generated_/chain.js', }, }; diff --git a/test/api_integration/apis/saved_objects/bulk_create.js b/test/api_integration/apis/saved_objects/bulk_create.js index afa5153336ff7..2d77fdf266793 100644 --- a/test/api_integration/apis/saved_objects/bulk_create.js +++ b/test/api_integration/apis/saved_objects/bulk_create.js @@ -58,7 +58,9 @@ export default function({ getService }) { type: 'visualization', id: 'dd7caf20-9efd-11e7-acb3-3dab96693fab', error: { - message: 'version conflict, document already exists', + error: 'Conflict', + message: + 'Saved object [visualization/dd7caf20-9efd-11e7-acb3-3dab96693fab] conflict', statusCode: 409, }, }, diff --git a/test/api_integration/apis/saved_objects/bulk_get.js b/test/api_integration/apis/saved_objects/bulk_get.js index 984ac781d3e46..67e511f2bf548 100644 --- a/test/api_integration/apis/saved_objects/bulk_get.js +++ b/test/api_integration/apis/saved_objects/bulk_get.js @@ -80,8 +80,9 @@ export default function({ getService }) { id: 'does not exist', type: 'dashboard', error: { + error: 'Not Found', + message: 'Saved object [dashboard/does not exist] not found', statusCode: 404, - message: 'Not found', }, }, { @@ -123,24 +124,28 @@ export default function({ getService }) { id: 'dd7caf20-9efd-11e7-acb3-3dab96693fab', type: 'visualization', error: { + error: 'Not Found', + message: + 'Saved object [visualization/dd7caf20-9efd-11e7-acb3-3dab96693fab] not found', statusCode: 404, - message: 'Not found', }, }, { id: 'does not exist', type: 'dashboard', error: { + error: 'Not Found', + message: 'Saved object [dashboard/does not exist] not found', statusCode: 404, - message: 'Not found', }, }, { id: '7.0.0-alpha1', type: 'config', error: { + error: 'Not Found', + message: 'Saved object [config/7.0.0-alpha1] not found', statusCode: 404, - message: 'Not found', }, }, ], diff --git a/test/api_integration/apis/saved_objects/export.js b/test/api_integration/apis/saved_objects/export.js index fc9ab8140869c..9558e82880deb 100644 --- a/test/api_integration/apis/saved_objects/export.js +++ b/test/api_integration/apis/saved_objects/export.js @@ -170,8 +170,9 @@ export default function({ getService }) { id: '1', type: 'dashboard', error: { + error: 'Not Found', + message: 'Saved object [dashboard/1] not found', statusCode: 404, - message: 'Not found', }, }, ], diff --git a/test/api_integration/apis/saved_objects/migrations.js b/test/api_integration/apis/saved_objects/migrations.js index bd207ccb41b20..9d908b95ca575 100644 --- a/test/api_integration/apis/saved_objects/migrations.js +++ b/test/api_integration/apis/saved_objects/migrations.js @@ -383,7 +383,7 @@ function migrationsToTypes(migrations) { return Object.entries(migrations).map(([type, migrations]) => ({ name: type, hidden: false, - namespaceAgnostic: false, + namespaceType: 'single', mappings: { properties: {} }, migrations: { ...migrations }, })); diff --git a/test/common/config.js b/test/common/config.js index faf8cef027170..ca80dfb01012f 100644 --- a/test/common/config.js +++ b/test/common/config.js @@ -56,6 +56,10 @@ export default function() { `--elasticsearch.password=${kibanaServerTestUser.password}`, `--home.disableWelcomeScreen=true`, '--telemetry.banner=false', + '--telemetry.optIn=false', + // These are *very* important to have them pointing to staging + '--telemetry.url=https://telemetry-staging.elastic.co/xpack/v2/send', + '--telemetry.optInStatusUrl=https://telemetry-staging.elastic.co/opt_in_status/v2/send', `--server.maxPayloadBytes=1679958`, // newsfeed mock service `--plugin-path=${path.join(__dirname, 'fixtures', 'plugins', 'newsfeed')}`, diff --git a/test/examples/embeddables/adding_children.ts b/test/examples/embeddables/adding_children.ts index 110b8ce573332..9ec4b6cffd31a 100644 --- a/test/examples/embeddables/adding_children.ts +++ b/test/examples/embeddables/adding_children.ts @@ -23,8 +23,10 @@ import { PluginFunctionalProviderContext } from 'test/plugin_functional/services // eslint-disable-next-line import/no-default-export export default function({ getService }: PluginFunctionalProviderContext) { const testSubjects = getService('testSubjects'); + const flyout = getService('flyout'); - describe('creating and adding children', () => { + // FLAKY: https://github.com/elastic/kibana/issues/58692 + describe.skip('creating and adding children', () => { before(async () => { await testSubjects.click('embeddablePanelExamplae'); }); @@ -39,5 +41,15 @@ export default function({ getService }: PluginFunctionalProviderContext) { const tasks = await testSubjects.getVisibleTextAll('todoEmbeddableTask'); expect(tasks).to.eql(['Goes out on Wednesdays!', 'new task']); }); + + it('Can add a child backed off a saved object', async () => { + await testSubjects.click('embeddablePanelToggleMenuIcon'); + await testSubjects.click('embeddablePanelAction-ACTION_ADD_PANEL'); + await testSubjects.click('savedObjectTitleGarbage'); + await testSubjects.moveMouseTo('euiFlyoutCloseButton'); + await flyout.ensureClosed('dashboardAddPanel'); + const tasks = await testSubjects.getVisibleTextAll('todoEmbeddableTask'); + expect(tasks).to.eql(['Goes out on Wednesdays!', 'new task', 'Take the garbage out']); + }); }); } diff --git a/test/functional/apps/dashboard/create_and_add_embeddables.js b/test/functional/apps/dashboard/create_and_add_embeddables.js index 3ce8e353e61fc..410acdcb5680d 100644 --- a/test/functional/apps/dashboard/create_and_add_embeddables.js +++ b/test/functional/apps/dashboard/create_and_add_embeddables.js @@ -19,7 +19,7 @@ import expect from '@kbn/expect'; -import { VisualizeConstants } from '../../../../src/legacy/core_plugins/kibana/public/visualize/np_ready/visualize_constants'; +import { VisualizeConstants } from '../../../../src/plugins/visualize/public/application/visualize_constants'; export default function({ getService, getPageObjects }) { const retry = getService('retry'); diff --git a/test/functional/apps/dashboard/dashboard_back_button.ts b/test/functional/apps/dashboard/dashboard_back_button.ts new file mode 100644 index 0000000000000..8a488c1780fcc --- /dev/null +++ b/test/functional/apps/dashboard/dashboard_back_button.ts @@ -0,0 +1,47 @@ +/* + * 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 expect from '@kbn/expect'; +import { FtrProviderContext } from '../../ftr_provider_context'; + +export default function({ getService, getPageObjects }: FtrProviderContext) { + const esArchiver = getService('esArchiver'); + const kibanaServer = getService('kibanaServer'); + const PageObjects = getPageObjects(['dashboard', 'header', 'common', 'visualize', 'timePicker']); + const browser = getService('browser'); + + describe('dashboard back button', () => { + before(async () => { + await esArchiver.loadIfNeeded('dashboard/current/kibana'); + await kibanaServer.uiSettings.replace({ + defaultIndex: '0bf35f60-3dc9-11e8-8660-4d65aa086b3c', + }); + await PageObjects.common.navigateToApp('dashboard'); + await PageObjects.dashboard.preserveCrossAppState(); + }); + + it('after navigation from listing page to dashboard back button works', async () => { + await PageObjects.dashboard.gotoDashboardLandingPage(); + await PageObjects.dashboard.loadSavedDashboard('dashboard with everything'); + await PageObjects.dashboard.waitForRenderComplete(); + await browser.goBack(); + expect(await PageObjects.dashboard.onDashboardLandingPage()).to.be(true); + }); + }); +} diff --git a/test/functional/apps/dashboard/dashboard_filter_bar.js b/test/functional/apps/dashboard/dashboard_filter_bar.js index 6d2a30fa85325..f6089871010c3 100644 --- a/test/functional/apps/dashboard/dashboard_filter_bar.js +++ b/test/functional/apps/dashboard/dashboard_filter_bar.js @@ -27,6 +27,7 @@ export default function({ getService, getPageObjects }) { const pieChart = getService('pieChart'); const esArchiver = getService('esArchiver'); const kibanaServer = getService('kibanaServer'); + const browser = getService('browser'); const PageObjects = getPageObjects(['common', 'dashboard', 'header', 'visualize', 'timePicker']); describe('dashboard filter bar', () => { @@ -126,9 +127,46 @@ export default function({ getService, getPageObjects }) { const filterCount = await filterBar.getFilterCount(); expect(filterCount).to.equal(1); + await pieChart.expectPieSliceCount(1); + }); + it("restoring filters doesn't break back button", async () => { + await browser.goBack(); + await PageObjects.dashboard.expectExistsDashboardLandingPage(); + await browser.goForward(); + await PageObjects.header.waitUntilLoadingHasFinished(); + await PageObjects.dashboard.waitForRenderComplete(); await pieChart.expectPieSliceCount(1); }); + + it("saving with pinned filter doesn't unpin them", async () => { + const filterKey = 'bytes'; + await filterBar.toggleFilterPinned(filterKey); + await PageObjects.dashboard.switchToEditMode(); + await PageObjects.dashboard.saveDashboard('saved with pinned filters', { + saveAsNew: true, + }); + expect(await filterBar.isFilterPinned(filterKey)).to.be(true); + await pieChart.expectPieSliceCount(1); + }); + + it("navigating to a dashboard with global filter doesn't unpin it if same filter is saved with dashboard", async () => { + await PageObjects.dashboard.preserveCrossAppState(); + await PageObjects.dashboard.gotoDashboardLandingPage(); + await PageObjects.dashboard.loadSavedDashboard('with filters'); + await PageObjects.header.waitUntilLoadingHasFinished(); + expect(await filterBar.isFilterPinned('bytes')).to.be(true); + await pieChart.expectPieSliceCount(1); + }); + + it("pinned filters aren't saved", async () => { + await filterBar.removeFilter('bytes'); + await PageObjects.dashboard.gotoDashboardLandingPage(); + await PageObjects.dashboard.loadSavedDashboard('saved with pinned filters'); + await PageObjects.header.waitUntilLoadingHasFinished(); + expect(await filterBar.getFilterCount()).to.be(0); + await pieChart.expectPieSliceCount(5); + }); }); describe('saved search filtering', function() { diff --git a/test/functional/apps/dashboard/index.js b/test/functional/apps/dashboard/index.js index 5e96a55b19014..6666ccc57d584 100644 --- a/test/functional/apps/dashboard/index.js +++ b/test/functional/apps/dashboard/index.js @@ -55,6 +55,7 @@ export default function({ getService, loadTestFile }) { loadTestFile(require.resolve('./dashboard_options')); loadTestFile(require.resolve('./data_shared_attributes')); loadTestFile(require.resolve('./embed_mode')); + loadTestFile(require.resolve('./dashboard_back_button')); // Note: This one must be last because it unloads some data for one of its tests! // No, this isn't ideal, but loading/unloading takes so much time and these are all bunched diff --git a/test/functional/apps/dashboard/panel_controls.js b/test/functional/apps/dashboard/panel_controls.js index f30f58913bd97..6e24b9f3570a3 100644 --- a/test/functional/apps/dashboard/panel_controls.js +++ b/test/functional/apps/dashboard/panel_controls.js @@ -24,7 +24,7 @@ import { AREA_CHART_VIS_NAME, LINE_CHART_VIS_NAME, } from '../../page_objects/dashboard_page'; -import { VisualizeConstants } from '../../../../src/legacy/core_plugins/kibana/public/visualize/np_ready/visualize_constants'; +import { VisualizeConstants } from '../../../../src/plugins/visualize/public/application/visualize_constants'; export default function({ getService, getPageObjects }) { const browser = getService('browser'); @@ -113,6 +113,50 @@ export default function({ getService, getPageObjects }) { }); }); + describe('panel cloning', function() { + before(async () => { + await PageObjects.dashboard.clickNewDashboard(); + await PageObjects.timePicker.setHistoricalDataRange(); + await dashboardAddPanel.addVisualization(PIE_CHART_VIS_NAME); + }); + + after(async function() { + await PageObjects.dashboard.gotoDashboardLandingPage(); + }); + + it('clones a panel', async () => { + const initialPanelTitles = await PageObjects.dashboard.getPanelTitles(); + await dashboardPanelActions.clonePanelByTitle(PIE_CHART_VIS_NAME); + await PageObjects.header.waitUntilLoadingHasFinished(); + await PageObjects.dashboard.waitForRenderComplete(); + const postPanelTitles = await PageObjects.dashboard.getPanelTitles(); + expect(postPanelTitles.length).to.equal(initialPanelTitles.length + 1); + }); + + it('appends a clone title tag', async () => { + const panelTitles = await PageObjects.dashboard.getPanelTitles(); + expect(panelTitles[1]).to.equal(PIE_CHART_VIS_NAME + ' (copy)'); + }); + + it('retains original panel dimensions', async () => { + const panelDimensions = await PageObjects.dashboard.getPanelDimensions(); + expect(panelDimensions[0]).to.eql(panelDimensions[1]); + }); + + it('gives a correct title to the clone of a clone', async () => { + const initialPanelTitles = await PageObjects.dashboard.getPanelTitles(); + const clonedPanelName = initialPanelTitles[initialPanelTitles.length - 1]; + await dashboardPanelActions.clonePanelByTitle(clonedPanelName); + await PageObjects.header.waitUntilLoadingHasFinished(); + await PageObjects.dashboard.waitForRenderComplete(); + const postPanelTitles = await PageObjects.dashboard.getPanelTitles(); + expect(postPanelTitles.length).to.equal(initialPanelTitles.length + 1); + expect(postPanelTitles[postPanelTitles.length - 1]).to.equal( + PIE_CHART_VIS_NAME + ' (copy 1)' + ); + }); + }); + describe('panel edit controls', function() { before(async () => { await PageObjects.dashboard.clickNewDashboard(); @@ -137,6 +181,7 @@ export default function({ getService, getPageObjects }) { await dashboardPanelActions.expectExistsEditPanelAction(); await dashboardPanelActions.expectExistsReplacePanelAction(); + await dashboardPanelActions.expectExistsDuplicatePanelAction(); await dashboardPanelActions.expectExistsRemovePanelAction(); }); @@ -151,6 +196,7 @@ export default function({ getService, getPageObjects }) { await dashboardPanelActions.openContextMenu(); await dashboardPanelActions.expectExistsEditPanelAction(); await dashboardPanelActions.expectExistsReplacePanelAction(); + await dashboardPanelActions.expectExistsDuplicatePanelAction(); await dashboardPanelActions.expectExistsRemovePanelAction(); // Get rid of the timestamp in the url. @@ -166,6 +212,7 @@ export default function({ getService, getPageObjects }) { await dashboardPanelActions.openContextMenu(); await dashboardPanelActions.expectMissingEditPanelAction(); await dashboardPanelActions.expectMissingReplacePanelAction(); + await dashboardPanelActions.expectMissingDuplicatePanelAction(); await dashboardPanelActions.expectMissingRemovePanelAction(); }); @@ -174,6 +221,7 @@ export default function({ getService, getPageObjects }) { await dashboardPanelActions.openContextMenu(); await dashboardPanelActions.expectExistsEditPanelAction(); await dashboardPanelActions.expectExistsReplacePanelAction(); + await dashboardPanelActions.expectExistsDuplicatePanelAction(); await dashboardPanelActions.expectMissingRemovePanelAction(); await dashboardPanelActions.clickExpandPanelToggle(); }); diff --git a/test/functional/apps/discover/_discover_histogram.js b/test/functional/apps/discover/_discover_histogram.js index f815c505a8c27..eeef3333aab0f 100644 --- a/test/functional/apps/discover/_discover_histogram.js +++ b/test/functional/apps/discover/_discover_histogram.js @@ -66,9 +66,8 @@ export default function({ getService, getPageObjects }) { }); it('should visualize monthly data with different day intervals', async () => { - //Nov 1, 2017 @ 01:00:00.000 - Mar 21, 2018 @ 02:00:00.000 - const fromTime = '2017-11-01 00:00:00.000'; - const toTime = '2018-03-21 00:00:00.000'; + const fromTime = 'Nov 01, 2017 @ 00:00:00.000'; + const toTime = 'Mar 21, 2018 @ 00:00:00.000'; await PageObjects.timePicker.setAbsoluteRange(fromTime, toTime); await PageObjects.discover.setChartInterval('Monthly'); await PageObjects.header.waitUntilLoadingHasFinished(); @@ -76,24 +75,25 @@ export default function({ getService, getPageObjects }) { expect(chartCanvasExist).to.be(true); }); it('should visualize weekly data with within DST changes', async () => { - //Nov 1, 2017 @ 01:00:00.000 - Mar 21, 2018 @ 02:00:00.000 - const fromTime = '2018-03-01 00:00:00.000'; - const toTime = '2018-05-01 00:00:00.000'; + const fromTime = 'Mar 01, 2018 @ 00:00:00.000'; + const toTime = 'May 01, 2018 @ 00:00:00.000'; await PageObjects.timePicker.setAbsoluteRange(fromTime, toTime); await PageObjects.discover.setChartInterval('Weekly'); await PageObjects.header.waitUntilLoadingHasFinished(); const chartCanvasExist = await elasticChart.canvasExists(); expect(chartCanvasExist).to.be(true); }); - it('should visualize monthly data with different years Scaled to 30d', async () => { - //Nov 1, 2017 @ 01:00:00.000 - Mar 21, 2018 @ 02:00:00.000 - const fromTime = '2010-01-01 00:00:00.000'; - const toTime = '2018-03-21 00:00:00.000'; + it('should visualize monthly data with different years Scaled to 30 days', async () => { + const fromTime = 'Jan 01, 2010 @ 00:00:00.000'; + const toTime = 'Mar 21, 2019 @ 00:00:00.000'; + await PageObjects.timePicker.setAbsoluteRange(fromTime, toTime); await PageObjects.discover.setChartInterval('Daily'); await PageObjects.header.waitUntilLoadingHasFinished(); const chartCanvasExist = await elasticChart.canvasExists(); expect(chartCanvasExist).to.be(true); + const chartIntervalScaledDesc = await PageObjects.discover.getChartIntervalScaledToDesc(); + expect(chartIntervalScaledDesc).to.be('Scaled to 30 days'); }); }); } diff --git a/test/functional/apps/discover/_saved_queries.js b/test/functional/apps/discover/_saved_queries.js index 3cdaccf32cdc3..76f3a3aea365f 100644 --- a/test/functional/apps/discover/_saved_queries.js +++ b/test/functional/apps/discover/_saved_queries.js @@ -147,6 +147,25 @@ export default function({ getService, getPageObjects }) { await savedQueryManagementComponent.clearCurrentlyLoadedQuery(); 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 + await queryBar.expectQueryLanguageOrFail('lucene'); // make sure lucene is remembered after refresh (comes from localstorage) + await savedQueryManagementComponent.loadSavedQuery('OkResponse'); + await queryBar.expectQueryLanguageOrFail('kql'); + await savedQueryManagementComponent.clearCurrentlyLoadedQuery(); + 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 () => { + 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 efc0dad394464..2c927e9a2f4c7 100644 --- a/test/functional/apps/home/_navigation.ts +++ b/test/functional/apps/home/_navigation.ts @@ -52,6 +52,7 @@ export default function({ getService, getPageObjects }: FtrProviderContext) { 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 }); diff --git a/test/functional/apps/visualize/_tsvb_time_series.ts b/test/functional/apps/visualize/_tsvb_time_series.ts index fa79190a5bf94..ac89c2b55e514 100644 --- a/test/functional/apps/visualize/_tsvb_time_series.ts +++ b/test/functional/apps/visualize/_tsvb_time_series.ts @@ -25,7 +25,6 @@ export default function({ getPageObjects, getService }: FtrProviderContext) { const retry = getService('retry'); const log = getService('log'); const kibanaServer = getService('kibanaServer'); - const testSubjects = getService('testSubjects'); describe('visual builder', function describeIndexTests() { beforeEach(async () => { @@ -126,20 +125,18 @@ export default function({ getPageObjects, getService }: FtrProviderContext) { expect(actualCountMin).to.be('3 hours'); }); - // --reversed class is not implemented in @elastic\chart - describe.skip('Dark mode', () => { + describe('Dark mode', () => { before(async () => { await kibanaServer.uiSettings.update({ 'theme:darkMode': true, }); }); - it(`viz should have 'reversed' class when background color is white`, async () => { + it(`viz should have light class when background color is white`, async () => { await visualBuilder.clickPanelOptions('timeSeries'); await visualBuilder.setBackgroundColor('#FFFFFF'); - const classNames = await testSubjects.getAttribute('timeseriesChart', 'class'); - expect(classNames.includes('tvbVisTimeSeries--reversed')).to.be(true); + expect(await visualBuilder.checkTimeSeriesIsLight()).to.be(true); }); after(async () => { diff --git a/test/functional/page_objects/common_page.ts b/test/functional/page_objects/common_page.ts index f06baeb7a4167..862e5127bb670 100644 --- a/test/functional/page_objects/common_page.ts +++ b/test/functional/page_objects/common_page.ts @@ -247,25 +247,11 @@ export function CommonPageProvider({ getService, getPageObjects }: FtrProviderCo } currentUrl = (await browser.getCurrentUrl()).replace(/\/\/\w+:\w+@/, '//'); - const maxAdditionalLengthOnNavUrl = 230; - - // On several test failures at the end of the TileMap test we try to navigate back to - // Visualize so we can create the next Vertical Bar Chart, but we can see from the - // logging and the screenshot that it's still on the TileMap page. Why didn't the "get" - // with a new timestamped URL go? I thought that sleep(700) between the get and the - // refresh would solve the problem but didn't seem to always work. - // So this hack fails the navSuccessful check if the currentUrl doesn't match the - // appUrl plus up to 230 other chars. - // Navigating to Settings when there is a default index pattern has a URL length of 196 - // (from debug output). Some other tabs may also be long. But a rather simple configured - // visualization is about 1000 chars long. So at least we catch that case. - - // Browsers don't show the ':port' if it's 80 or 443 so we have to - // remove that part so we can get a match in the tests. - const navSuccessful = new RegExp( - appUrl.replace(':80/', '/').replace(':443/', '/') + - `.{0,${maxAdditionalLengthOnNavUrl}}$` - ).test(currentUrl); + + const navSuccessful = currentUrl + .replace(':80/', '/') + .replace(':443/', '/') + .startsWith(appUrl); if (!navSuccessful) { const msg = `App failed to load: ${appName} in ${defaultFindTimeout}ms appUrl=${appUrl} currentUrl=${currentUrl}`; diff --git a/test/functional/page_objects/discover_page.ts b/test/functional/page_objects/discover_page.ts index 00bf87621864a..e43a774940391 100644 --- a/test/functional/page_objects/discover_page.ts +++ b/test/functional/page_objects/discover_page.ts @@ -162,6 +162,11 @@ export function DiscoverPageProvider({ getService, getPageObjects }: FtrProvider return selectedOption.getVisibleText(); } + public async getChartIntervalScaledToDesc() { + await header.waitUntilLoadingHasFinished(); + return await testSubjects.getVisibleText('discoverIntervalSelectScaledToDesc'); + } + public async setChartInterval(interval: string) { const optionElement = await find.byCssSelector(`option[label="${interval}"]`, 5000); await optionElement.click(); diff --git a/test/functional/page_objects/visual_builder_page.ts b/test/functional/page_objects/visual_builder_page.ts index b8e6c812b46bd..12962b3a5cdef 100644 --- a/test/functional/page_objects/visual_builder_page.ts +++ b/test/functional/page_objects/visual_builder_page.ts @@ -71,6 +71,10 @@ export function VisualBuilderPageProvider({ getService, getPageObjects }: FtrPro } } + public async checkTimeSeriesIsLight() { + return await find.existsByCssSelector('.tvbVisTimeSeriesLight'); + } + public async checkTimeSeriesLegendIsPresent() { const isPresent = await find.existsByCssSelector('.echLegend'); if (!isPresent) { diff --git a/test/functional/page_objects/visualize_page.ts b/test/functional/page_objects/visualize_page.ts index 3b63fa68d71ee..220c2d8f6b363 100644 --- a/test/functional/page_objects/visualize_page.ts +++ b/test/functional/page_objects/visualize_page.ts @@ -18,7 +18,7 @@ */ import { FtrProviderContext } from '../ftr_provider_context'; -import { VisualizeConstants } from '../../../src/legacy/core_plugins/kibana/public/visualize/np_ready/visualize_constants'; +import { VisualizeConstants } from '../../../src/plugins/visualize/public/application/visualize_constants'; export function VisualizePageProvider({ getService, getPageObjects }: FtrProviderContext) { const testSubjects = getService('testSubjects'); diff --git a/test/functional/services/dashboard/panel_actions.js b/test/functional/services/dashboard/panel_actions.js index baea2a52208c1..b155d747f3b93 100644 --- a/test/functional/services/dashboard/panel_actions.js +++ b/test/functional/services/dashboard/panel_actions.js @@ -20,6 +20,7 @@ const REMOVE_PANEL_DATA_TEST_SUBJ = 'embeddablePanelAction-deletePanel'; const EDIT_PANEL_DATA_TEST_SUBJ = 'embeddablePanelAction-editPanel'; const REPLACE_PANEL_DATA_TEST_SUBJ = 'embeddablePanelAction-replacePanel'; +const CLONE_PANEL_DATA_TEST_SUBJ = 'embeddablePanelAction-clonePanel'; const TOGGLE_EXPAND_PANEL_DATA_TEST_SUBJ = 'embeddablePanelAction-togglePanel'; const CUSTOMIZE_PANEL_DATA_TEST_SUBJ = 'embeddablePanelAction-ACTION_CUSTOMIZE_PANEL'; const OPEN_CONTEXT_MENU_ICON_DATA_TEST_SUBJ = 'embeddablePanelToggleMenuIcon'; @@ -97,6 +98,16 @@ export function DashboardPanelActionsProvider({ getService, getPageObjects }) { await testSubjects.click(REPLACE_PANEL_DATA_TEST_SUBJ); } + async clonePanelByTitle(title) { + log.debug(`clonePanel(${title})`); + let panelOptions = null; + if (title) { + panelOptions = await this.getPanelHeading(title); + } + await this.openContextMenu(panelOptions); + await testSubjects.click(CLONE_PANEL_DATA_TEST_SUBJ); + } + async openInspectorByTitle(title) { const header = await this.getPanelHeading(title); await this.openInspector(header); @@ -123,7 +134,12 @@ export function DashboardPanelActionsProvider({ getService, getPageObjects }) { } async expectExistsReplacePanelAction() { - log.debug('expectExistsEditPanelAction'); + log.debug('expectExistsReplacePanelAction'); + await testSubjects.existOrFail(REPLACE_PANEL_DATA_TEST_SUBJ); + } + + async expectExistsDuplicatePanelAction() { + log.debug('expectExistsDuplicatePanelAction'); await testSubjects.existOrFail(REPLACE_PANEL_DATA_TEST_SUBJ); } @@ -133,7 +149,12 @@ export function DashboardPanelActionsProvider({ getService, getPageObjects }) { } async expectMissingReplacePanelAction() { - log.debug('expectMissingEditPanelAction'); + log.debug('expectMissingReplacePanelAction'); + await testSubjects.missingOrFail(REPLACE_PANEL_DATA_TEST_SUBJ); + } + + async expectMissingDuplicatePanelAction() { + log.debug('expectMissingDuplicatePanelAction'); await testSubjects.missingOrFail(REPLACE_PANEL_DATA_TEST_SUBJ); } diff --git a/test/functional/services/filter_bar.ts b/test/functional/services/filter_bar.ts index 9d494b1e6d950..a463a593e9e04 100644 --- a/test/functional/services/filter_bar.ts +++ b/test/functional/services/filter_bar.ts @@ -32,10 +32,16 @@ export function FilterBarProvider({ getService, getPageObjects }: FtrProviderCon * @param value filter value * @param enabled filter status */ - public async hasFilter(key: string, value: string, enabled: boolean = true): Promise { + public async hasFilter( + key: string, + value: string, + enabled: boolean = true, + pinned: boolean = false + ): Promise { const filterActivationState = enabled ? 'enabled' : 'disabled'; + const filterPinnedState = pinned ? 'pinned' : 'unpinned'; return testSubjects.exists( - `filter filter-${filterActivationState} filter-key-${key} filter-value-${value}`, + `filter filter-${filterActivationState} filter-key-${key} filter-value-${value} filter-${filterPinnedState}`, { allowHidden: true, } @@ -80,6 +86,11 @@ export function FilterBarProvider({ getService, getPageObjects }: FtrProviderCon await PageObjects.header.awaitGlobalLoadingIndicatorHidden(); } + public async isFilterPinned(key: string): Promise { + const filter = await testSubjects.find(`~filter & ~filter-key-${key}`); + return (await filter.getAttribute('data-test-subj')).includes('filter-pinned'); + } + public async getFilterCount(): Promise { const filters = await testSubjects.findAll('~filter'); return filters.length; diff --git a/test/functional/services/query_bar.ts b/test/functional/services/query_bar.ts index ace8b97155c09..7c7fd2d81f170 100644 --- a/test/functional/services/query_bar.ts +++ b/test/functional/services/query_bar.ts @@ -17,6 +17,7 @@ * under the License. */ +import expect from '@kbn/expect'; import { FtrProviderContext } from '../ftr_provider_context'; export function QueryBarProvider({ getService, getPageObjects }: FtrProviderContext) { @@ -25,6 +26,7 @@ export function QueryBarProvider({ getService, getPageObjects }: FtrProviderCont const log = getService('log'); const PageObjects = getPageObjects(['header', 'common']); const find = getService('find'); + const browser = getService('browser'); class QueryBar { async getQueryString(): Promise { @@ -62,6 +64,24 @@ export function QueryBarProvider({ getService, getPageObjects }: FtrProviderCont public async clickQuerySubmitButton(): Promise { await testSubjects.click('querySubmitButton'); } + + public async switchQueryLanguage(lang: 'kql' | 'lucene'): Promise { + await testSubjects.click('switchQueryLanguageButton'); + const kqlToggle = await testSubjects.find('languageToggle'); + const currentLang = + (await kqlToggle.getAttribute('aria-checked')) === 'true' ? 'kql' : 'lucene'; + if (lang !== currentLang) { + await kqlToggle.click(); + } + + await browser.pressKeys(browser.keys.ESCAPE); // close popover + await this.expectQueryLanguageOrFail(lang); // make sure lang is switched + } + + public async expectQueryLanguageOrFail(lang: 'kql' | 'lucene'): Promise { + const queryLanguageButton = await testSubjects.find('switchQueryLanguageButton'); + expect((await queryLanguageButton.getVisibleText()).toLowerCase()).to.eql(lang); + } } return new QueryBar(); diff --git a/test/functional/services/remote/remote.ts b/test/functional/services/remote/remote.ts index b0724488cb5db..933b08f7681e8 100644 --- a/test/functional/services/remote/remote.ts +++ b/test/functional/services/remote/remote.ts @@ -64,13 +64,10 @@ export async function RemoteProvider({ getService }: FtrProviderContext) { lifecycle, config.get('browser.logPollingMs') ); - const isW3CEnabled = (driver as any).executor_.w3c; const caps = await driver.getCapabilities(); - const browserVersion = caps.get( - isW3CEnabled || browserType === Browsers.ChromiumEdge ? 'browserVersion' : 'version' - ); + const browserVersion = caps.get(isW3CEnabled ? 'browserVersion' : 'version'); log.info( `Remote initialized: ${caps.get( diff --git a/test/functional/services/remote/webdriver.ts b/test/functional/services/remote/webdriver.ts index fc0b5bbb787c8..1b7ef2c1855d0 100644 --- a/test/functional/services/remote/webdriver.ts +++ b/test/functional/services/remote/webdriver.ts @@ -30,13 +30,13 @@ import geckoDriver from 'geckodriver'; import { Builder, Capabilities, By, logging, until } from 'selenium-webdriver'; import chrome from 'selenium-webdriver/chrome'; import firefox from 'selenium-webdriver/firefox'; -// @ts-ignore internal modules are not typed import edge from 'selenium-webdriver/edge'; -import { installDriver } from 'ms-chromium-edge-driver'; // @ts-ignore internal modules are not typed import { Executor } from 'selenium-webdriver/lib/http'; // @ts-ignore internal modules are not typed import { getLogger } from 'selenium-webdriver/lib/logging'; +import { installDriver } from 'ms-chromium-edge-driver'; + import { pollForLogEntry$ } from './poll_for_log_entry'; import { createStdoutSocket } from './create_stdout_stream'; import { preventParallelCalls } from './prevent_parallel_calls'; @@ -77,46 +77,6 @@ async function attemptToCreateCommand( const buildDriverInstance = async () => { switch (browserType) { - case 'msedge': { - if (edgePaths && edgePaths.browserPath && edgePaths.driverPath) { - const edgeOptions = new edge.Options(); - if (headlessBrowser === '1') { - // @ts-ignore internal modules are not typed - edgeOptions.headless(); - } - // @ts-ignore internal modules are not typed - edgeOptions.setEdgeChromium(true); - // @ts-ignore internal modules are not typed - edgeOptions.setBinaryPath(edgePaths.browserPath); - const session = await new Builder() - .forBrowser('MicrosoftEdge') - .setEdgeOptions(edgeOptions) - .setEdgeService(new edge.ServiceBuilder(edgePaths.driverPath)) - .build(); - return { - session, - consoleLog$: pollForLogEntry$( - session, - logging.Type.BROWSER, - logPollingMs, - lifecycle.cleanup.after$ - ).pipe( - takeUntil(lifecycle.cleanup.after$), - map(({ message, level: { name: level } }) => ({ - message: message.replace(/\\n/g, '\n'), - level, - })) - ), - }; - } else { - throw new Error( - `Chromium Edge session requires browser or driver path to be defined: ${JSON.stringify( - edgePaths - )}` - ); - } - } - case 'chrome': { const chromeCapabilities = Capabilities.chrome(); const chromeOptions = [ @@ -179,6 +139,46 @@ async function attemptToCreateCommand( }; } + case 'msedge': { + if (edgePaths && edgePaths.browserPath && edgePaths.driverPath) { + const edgeOptions = new edge.Options(); + if (headlessBrowser === '1') { + // @ts-ignore internal modules are not typed + edgeOptions.headless(); + } + // @ts-ignore internal modules are not typed + edgeOptions.setEdgeChromium(true); + // @ts-ignore internal modules are not typed + edgeOptions.setBinaryPath(edgePaths.browserPath); + const session = await new Builder() + .forBrowser('MicrosoftEdge') + .setEdgeOptions(edgeOptions) + .setEdgeService(new edge.ServiceBuilder(edgePaths.driverPath)) + .build(); + return { + session, + consoleLog$: pollForLogEntry$( + session, + logging.Type.BROWSER, + logPollingMs, + lifecycle.cleanup.after$ + ).pipe( + takeUntil(lifecycle.cleanup.after$), + map(({ message, level: { name: level } }) => ({ + message: message.replace(/\\n/g, '\n'), + level, + })) + ), + }; + } else { + throw new Error( + `Chromium Edge session requires browser or driver path to be defined: ${JSON.stringify( + edgePaths + )}` + ); + } + } + case 'firefox': { const firefoxOptions = new firefox.Options(); // Firefox 65+ supports logging console output to stdout diff --git a/test/plugin_functional/plugins/core_plugin_a/public/application.tsx b/test/plugin_functional/plugins/core_plugin_a/public/application.tsx index abea970749cbc..159bb54f50903 100644 --- a/test/plugin_functional/plugins/core_plugin_a/public/application.tsx +++ b/test/plugin_functional/plugins/core_plugin_a/public/application.tsx @@ -95,7 +95,7 @@ const Nav = withRouter(({ history, navigateToApp }: NavProps) => ( { id: 'home', name: 'Home', - onClick: () => history.push('/'), + onClick: () => history.push(''), 'data-test-subj': 'fooNavHome', }, { diff --git a/test/plugin_functional/plugins/core_plugin_b/public/application.tsx b/test/plugin_functional/plugins/core_plugin_b/public/application.tsx index 447307920c04c..01a63f9782563 100644 --- a/test/plugin_functional/plugins/core_plugin_b/public/application.tsx +++ b/test/plugin_functional/plugins/core_plugin_b/public/application.tsx @@ -102,7 +102,7 @@ const Nav = withRouter(({ history, navigateToApp }: NavProps) => ( { id: 'home', name: 'Home', - onClick: () => navigateToApp('bar', { path: '/' }), + onClick: () => navigateToApp('bar', { path: '' }), 'data-test-subj': 'barNavHome', }, { diff --git a/test/plugin_functional/plugins/kbn_sample_panel_action/public/sample_panel_link.ts b/test/plugin_functional/plugins/kbn_sample_panel_action/public/sample_panel_link.ts index b0f1219a815a3..faa774b8485b1 100644 --- a/test/plugin_functional/plugins/kbn_sample_panel_action/public/sample_panel_link.ts +++ b/test/plugin_functional/plugins/kbn_sample_panel_action/public/sample_panel_link.ts @@ -26,6 +26,8 @@ export const createSamplePanelLink = (): Action => createAction({ type: SAMPLE_PANEL_LINK, getDisplayName: () => 'Sample panel Link', - execute: async () => {}, - getHref: () => 'https://example.com/kibana/test', + execute: async () => { + window.location.href = 'https://example.com/kibana/test'; + }, + getHref: async () => 'https://example.com/kibana/test', }); diff --git a/test/plugin_functional/plugins/kbn_tp_embeddable_explorer/public/np_ready/public/app/dashboard_input.ts b/test/plugin_functional/plugins/kbn_tp_embeddable_explorer/public/np_ready/public/app/dashboard_input.ts index bb8951680be35..37ef8cad948cb 100644 --- a/test/plugin_functional/plugins/kbn_tp_embeddable_explorer/public/np_ready/public/app/dashboard_input.ts +++ b/test/plugin_functional/plugins/kbn_tp_embeddable_explorer/public/np_ready/public/app/dashboard_input.ts @@ -47,7 +47,7 @@ export const dashboardInput: DashboardContainerInput = { explicitInput: { id: '2', firstName: 'Sue', - } as any, + }, }, '822cd0f0-ce7c-419d-aeaa-1171cf452745': { gridData: { @@ -60,8 +60,8 @@ export const dashboardInput: DashboardContainerInput = { type: 'visualization', explicitInput: { id: '822cd0f0-ce7c-419d-aeaa-1171cf452745', + savedObjectId: '3fe22200-3dcb-11e8-8660-4d65aa086b3c', }, - savedObjectId: '3fe22200-3dcb-11e8-8660-4d65aa086b3c', }, '66f0a265-7b06-4974-accd-d05f74f7aa82': { gridData: { @@ -74,8 +74,8 @@ export const dashboardInput: DashboardContainerInput = { type: 'visualization', explicitInput: { id: '66f0a265-7b06-4974-accd-d05f74f7aa82', + savedObjectId: '4c0f47e0-3dcd-11e8-8660-4d65aa086b3c', }, - savedObjectId: '4c0f47e0-3dcd-11e8-8660-4d65aa086b3c', }, 'b2861741-40b9-4dc8-b82b-080c6e29a551': { gridData: { @@ -88,8 +88,8 @@ export const dashboardInput: DashboardContainerInput = { type: 'search', explicitInput: { id: 'b2861741-40b9-4dc8-b82b-080c6e29a551', + savedObjectId: 'be5accf0-3dca-11e8-8660-4d65aa086b3c', }, - savedObjectId: 'be5accf0-3dca-11e8-8660-4d65aa086b3c', }, }, isFullScreenMode: false, diff --git a/test/plugin_functional/plugins/kbn_tp_embeddable_explorer/tsconfig.json b/test/plugin_functional/plugins/kbn_tp_embeddable_explorer/tsconfig.json index d8096d9aab27a..544c27241f5cb 100644 --- a/test/plugin_functional/plugins/kbn_tp_embeddable_explorer/tsconfig.json +++ b/test/plugin_functional/plugins/kbn_tp_embeddable_explorer/tsconfig.json @@ -6,7 +6,8 @@ "types": [ "node", "jest", - "react" + "react", + "flot" ] }, "include": [ @@ -16,4 +17,4 @@ "../../../../typings/**/*", ], "exclude": [] -} \ No newline at end of file +} diff --git a/test/plugin_functional/plugins/rendering_plugin/server/plugin.ts b/test/plugin_functional/plugins/rendering_plugin/server/plugin.ts index 3f6a8e8773e04..f0b1cde24c6fe 100644 --- a/test/plugin_functional/plugins/rendering_plugin/server/plugin.ts +++ b/test/plugin_functional/plugins/rendering_plugin/server/plugin.ts @@ -17,15 +17,13 @@ * under the License. */ -import { Plugin, CoreSetup, IRenderOptions } from 'kibana/server'; +import { Plugin, CoreSetup } from 'kibana/server'; import { schema } from '@kbn/config-schema'; export class RenderingPlugin implements Plugin { public setup(core: CoreSetup) { - const router = core.http.createRouter(); - - router.get( + core.http.resources.register( { path: '/render/{id}', validate: { @@ -41,18 +39,12 @@ export class RenderingPlugin implements Plugin { }, }, async (context, req, res) => { - const { id } = req.params; const { includeUserSettings } = req.query; - const app = { getId: () => id! }; - const options: Partial = { app, includeUserSettings }; - const body = await context.core.rendering.render(options); - return res.ok({ - body, - headers: { - 'content-security-policy': core.http.csp.header, - }, - }); + if (includeUserSettings) { + return res.renderCoreApp(); + } + return res.renderAnonymousCoreApp(); } ); } diff --git a/test/scripts/jenkins_ci_group.sh b/test/scripts/jenkins_ci_group.sh index 23807a6e98dc2..c77ffe40db553 100755 --- a/test/scripts/jenkins_ci_group.sh +++ b/test/scripts/jenkins_ci_group.sh @@ -22,7 +22,7 @@ else echo " -> running tests from the clone folder" #yarn run grunt "run:functionalTests_ciGroup${CI_GROUP}"; - node scripts/functional_tests --debug --include-tag "ciGroup$CI_GROUP" --config test/functional/config.coverage.js || true; + node scripts/functional_tests --debug --include-tag "ciGroup$CI_GROUP" --exclude-tag "skipCoverage" || true; if [[ -d target/kibana-coverage/functional ]]; then echo " -> replacing kibana${CI_GROUP} with kibana in json files" diff --git a/test/scripts/jenkins_xpack.sh b/test/scripts/jenkins_xpack.sh index 5055997df642a..67d88b308ed91 100755 --- a/test/scripts/jenkins_xpack.sh +++ b/test/scripts/jenkins_xpack.sh @@ -17,7 +17,7 @@ if [[ -z "$CODE_COVERAGE" ]] ; then echo " -> Running SIEM cyclic dependency test" cd "$XPACK_DIR" - checks-reporter-with-killswitch "X-Pack SIEM cyclic dependency test" node legacy/plugins/siem/scripts/check_circular_deps + checks-reporter-with-killswitch "X-Pack SIEM cyclic dependency test" node plugins/siem/scripts/check_circular_deps echo "" echo "" @@ -39,7 +39,7 @@ else # build runtime for canvas echo "NODE_ENV=$NODE_ENV" node ./legacy/plugins/canvas/scripts/shareable_runtime - node scripts/jest --ci --verbose --coverage + node --max-old-space-size=6144 scripts/jest --ci --verbose --coverage # rename file in order to be unique one test -f ../target/kibana-coverage/jest/coverage-final.json \ && mv ../target/kibana-coverage/jest/coverage-final.json \ diff --git a/test/scripts/jenkins_xpack_ci_group.sh b/test/scripts/jenkins_xpack_ci_group.sh index 01b13293c10ba..a6e600630364e 100755 --- a/test/scripts/jenkins_xpack_ci_group.sh +++ b/test/scripts/jenkins_xpack_ci_group.sh @@ -23,7 +23,7 @@ else cd "kibana${CI_GROUP}/x-pack" echo " -> running tests from the clone folder" - node scripts/functional_tests --debug --include-tag "ciGroup$CI_GROUP" --config test/functional/config.coverage.js || true; + node scripts/functional_tests --debug --include-tag "ciGroup$CI_GROUP" --exclude-tag "skipCoverage" || true; if [[ -d ../target/kibana-coverage/functional ]]; then echo " -> replacing kibana${CI_GROUP} with kibana in json files" diff --git a/test/tsconfig.json b/test/tsconfig.json index 285d3db64a874..5a3716e620fed 100644 --- a/test/tsconfig.json +++ b/test/tsconfig.json @@ -3,7 +3,8 @@ "compilerOptions": { "types": [ "node", - "mocha" + "mocha", + "flot" ], "lib": [ "esnext", diff --git a/test/typings/rison_node.d.ts b/test/typings/rison_node.d.ts index 2592c36e8ae9a..a0497f421c3fe 100644 --- a/test/typings/rison_node.d.ts +++ b/test/typings/rison_node.d.ts @@ -18,7 +18,7 @@ */ declare module 'rison-node' { - export type RisonValue = null | boolean | number | string | RisonObject | RisonArray; + export type RisonValue = undefined | null | boolean | number | string | RisonObject | RisonArray; // eslint-disable-next-line @typescript-eslint/no-empty-interface export interface RisonArray extends Array {} diff --git a/typings/rison_node.d.ts b/typings/rison_node.d.ts index 2592c36e8ae9a..a0497f421c3fe 100644 --- a/typings/rison_node.d.ts +++ b/typings/rison_node.d.ts @@ -18,7 +18,7 @@ */ declare module 'rison-node' { - export type RisonValue = null | boolean | number | string | RisonObject | RisonArray; + export type RisonValue = undefined | null | boolean | number | string | RisonObject | RisonArray; // eslint-disable-next-line @typescript-eslint/no-empty-interface export interface RisonArray extends Array {} diff --git a/x-pack/.i18nrc.json b/x-pack/.i18nrc.json index ae8d61769b14c..c8715ac3447bd 100644 --- a/x-pack/.i18nrc.json +++ b/x-pack/.i18nrc.json @@ -18,13 +18,13 @@ "xpack.graph": ["legacy/plugins/graph", "plugins/graph"], "xpack.grokDebugger": "plugins/grokdebugger", "xpack.idxMgmt": "plugins/index_management", - "xpack.indexLifecycleMgmt": "legacy/plugins/index_lifecycle_management", + "xpack.indexLifecycleMgmt": "plugins/index_lifecycle_management", "xpack.infra": "plugins/infra", "xpack.ingestManager": "plugins/ingest_manager", - "xpack.lens": "legacy/plugins/lens", + "xpack.lens": "plugins/lens", "xpack.licenseMgmt": "plugins/license_management", "xpack.licensing": "plugins/licensing", - "xpack.logstash": "legacy/plugins/logstash", + "xpack.logstash": ["plugins/logstash", "legacy/plugins/logstash"], "xpack.main": "legacy/plugins/xpack_main", "xpack.maps": ["plugins/maps", "legacy/plugins/maps"], "xpack.ml": ["plugins/ml", "legacy/plugins/ml"], @@ -32,11 +32,11 @@ "xpack.remoteClusters": "plugins/remote_clusters", "xpack.painlessLab": "plugins/painless_lab", "xpack.reporting": ["plugins/reporting", "legacy/plugins/reporting"], - "xpack.rollupJobs": "legacy/plugins/rollup", + "xpack.rollupJobs": ["legacy/plugins/rollup", "plugins/rollup"], "xpack.searchProfiler": "plugins/searchprofiler", "xpack.security": ["legacy/plugins/security", "plugins/security"], "xpack.server": "legacy/server", - "xpack.siem": "legacy/plugins/siem", + "xpack.siem": ["plugins/siem", "legacy/plugins/siem"], "xpack.snapshotRestore": "plugins/snapshot_restore", "xpack.spaces": ["legacy/plugins/spaces", "plugins/spaces"], "xpack.taskManager": "legacy/plugins/task_manager", diff --git a/x-pack/dev-tools/jest/create_jest_config.js b/x-pack/dev-tools/jest/create_jest_config.js index 1f4311ae9bcf6..3068cdd0daa5b 100644 --- a/x-pack/dev-tools/jest/create_jest_config.js +++ b/x-pack/dev-tools/jest/create_jest_config.js @@ -28,6 +28,7 @@ export function createJestConfig({ kibanaDirectory, xPackKibanaDirectory }) { '\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$': fileMockPath, '\\.module.(css|scss)$': `${kibanaDirectory}/src/dev/jest/mocks/css_module_mock.js`, '\\.(css|less|scss)$': `${kibanaDirectory}/src/dev/jest/mocks/style_mock.js`, + '\\.ace\\.worker.js$': `${kibanaDirectory}/src/dev/jest/mocks/ace_worker_module_mock.js`, '^test_utils/enzyme_helpers': `${xPackKibanaDirectory}/test_utils/enzyme_helpers.tsx`, '^test_utils/find_test_subject': `${xPackKibanaDirectory}/test_utils/find_test_subject.ts`, '^test_utils/stub_web_worker': `${xPackKibanaDirectory}/test_utils/stub_web_worker.ts`, diff --git a/x-pack/examples/README.md b/x-pack/examples/README.md new file mode 100644 index 0000000000000..babf744f9777d --- /dev/null +++ b/x-pack/examples/README.md @@ -0,0 +1,7 @@ +## Example plugins + +This folder contains X-Pack example plugins. To run the plugins in this folder, use the `--run-examples` flag, via + +``` +yarn start --run-examples +``` diff --git a/x-pack/examples/ui_actions_enhanced_examples/README.md b/x-pack/examples/ui_actions_enhanced_examples/README.md new file mode 100644 index 0000000000000..c9f53137d8687 --- /dev/null +++ b/x-pack/examples/ui_actions_enhanced_examples/README.md @@ -0,0 +1,3 @@ +## Ui actions enhanced examples + +To run this example, use the command `yarn start --run-examples`. diff --git a/x-pack/examples/ui_actions_enhanced_examples/kibana.json b/x-pack/examples/ui_actions_enhanced_examples/kibana.json new file mode 100644 index 0000000000000..f75852edced5c --- /dev/null +++ b/x-pack/examples/ui_actions_enhanced_examples/kibana.json @@ -0,0 +1,10 @@ +{ + "id": "uiActionsEnhancedExamples", + "version": "0.0.1", + "kibanaVersion": "kibana", + "configPath": ["ui_actions_enhanced_examples"], + "server": false, + "ui": true, + "requiredPlugins": ["uiActions", "data"], + "optionalPlugins": [] +} diff --git a/x-pack/examples/ui_actions_enhanced_examples/package.json b/x-pack/examples/ui_actions_enhanced_examples/package.json new file mode 100644 index 0000000000000..a9f004b075cec --- /dev/null +++ b/x-pack/examples/ui_actions_enhanced_examples/package.json @@ -0,0 +1,17 @@ +{ + "name": "ui_actions_enhanced_examples", + "version": "1.0.0", + "main": "target/examples/ui_actions_enhanced_examples", + "kibana": { + "version": "kibana", + "templateVersion": "1.0.0" + }, + "license": "Apache-2.0", + "scripts": { + "kbn": "node ../../scripts/kbn.js", + "build": "rm -rf './target' && tsc" + }, + "devDependencies": { + "typescript": "3.7.2" + } +} diff --git a/x-pack/examples/ui_actions_enhanced_examples/public/index.ts b/x-pack/examples/ui_actions_enhanced_examples/public/index.ts new file mode 100644 index 0000000000000..7f3f36089d576 --- /dev/null +++ b/x-pack/examples/ui_actions_enhanced_examples/public/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. + */ + +import { UiActionsEnhancedExamplesPlugin } from './plugin'; + +export const plugin = () => new UiActionsEnhancedExamplesPlugin(); diff --git a/x-pack/examples/ui_actions_enhanced_examples/public/plugin.ts b/x-pack/examples/ui_actions_enhanced_examples/public/plugin.ts new file mode 100644 index 0000000000000..a4c43753c8247 --- /dev/null +++ b/x-pack/examples/ui_actions_enhanced_examples/public/plugin.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 { Plugin, CoreSetup, CoreStart } from '../../../../src/core/public'; +import { UiActionsSetup, UiActionsStart } from '../../../../src/plugins/ui_actions/public'; +import { DataPublicPluginSetup, DataPublicPluginStart } from '../../../../src/plugins/data/public'; + +export interface SetupDependencies { + data: DataPublicPluginSetup; + uiActions: UiActionsSetup; +} + +export interface StartDependencies { + data: DataPublicPluginStart; + uiActions: UiActionsStart; +} + +export class UiActionsEnhancedExamplesPlugin + implements Plugin { + public setup(core: CoreSetup, plugins: SetupDependencies) { + // eslint-disable-next-line + console.log('ui_actions_enhanced_examples'); + } + + public start(core: CoreStart, plugins: StartDependencies) {} + + public stop() {} +} diff --git a/x-pack/examples/ui_actions_enhanced_examples/tsconfig.json b/x-pack/examples/ui_actions_enhanced_examples/tsconfig.json new file mode 100644 index 0000000000000..d508076b33199 --- /dev/null +++ b/x-pack/examples/ui_actions_enhanced_examples/tsconfig.json @@ -0,0 +1,15 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "outDir": "./target", + "skipLibCheck": true + }, + "include": [ + "index.ts", + "public/**/*.ts", + "public/**/*.tsx", + "server/**/*.ts", + "../../typings/**/*", + ], + "exclude": [] +} diff --git a/x-pack/index.js b/x-pack/index.js index 6fab13d726fa6..7fbd992120ea6 100644 --- a/x-pack/index.js +++ b/x-pack/index.js @@ -9,14 +9,12 @@ import { graph } from './legacy/plugins/graph'; import { monitoring } from './legacy/plugins/monitoring'; import { reporting } from './legacy/plugins/reporting'; import { security } from './legacy/plugins/security'; -import { tilemap } from './legacy/plugins/tilemap'; import { dashboardMode } from './legacy/plugins/dashboard_mode'; import { logstash } from './legacy/plugins/logstash'; import { beats } from './legacy/plugins/beats_management'; import { apm } from './legacy/plugins/apm'; import { maps } from './legacy/plugins/maps'; import { indexManagement } from './legacy/plugins/index_management'; -import { indexLifecycleManagement } from './legacy/plugins/index_lifecycle_management'; import { spaces } from './legacy/plugins/spaces'; import { canvas } from './legacy/plugins/canvas'; import { infra } from './legacy/plugins/infra'; @@ -30,7 +28,6 @@ import { uptime } from './legacy/plugins/uptime'; import { encryptedSavedObjects } from './legacy/plugins/encrypted_saved_objects'; import { actions } from './legacy/plugins/actions'; import { alerting } from './legacy/plugins/alerting'; -import { lens } from './legacy/plugins/lens'; import { ingestManager } from './legacy/plugins/ingest_manager'; import { triggersActionsUI } from './legacy/plugins/triggers_actions_ui'; @@ -42,7 +39,6 @@ module.exports = function(kibana) { reporting(kibana), spaces(kibana), security(kibana), - tilemap(kibana), dashboardMode(kibana), logstash(kibana), beats(kibana), @@ -50,7 +46,6 @@ module.exports = function(kibana) { maps(kibana), canvas(kibana), indexManagement(kibana), - indexLifecycleManagement(kibana), infra(kibana), taskManager(kibana), rollup(kibana), @@ -60,7 +55,6 @@ module.exports = function(kibana) { upgradeAssistant(kibana), uptime(kibana), encryptedSavedObjects(kibana), - lens(kibana), actions(kibana), alerting(kibana), ingestManager(kibana), diff --git a/x-pack/legacy/plugins/apm/public/components/app/ServiceDetails/ServiceIntegrations/MachineLearningFlyout/index.tsx b/x-pack/legacy/plugins/apm/public/components/app/ServiceDetails/ServiceIntegrations/MachineLearningFlyout/index.tsx index a1462c7637358..cc5c62e25b491 100644 --- a/x-pack/legacy/plugins/apm/public/components/app/ServiceDetails/ServiceIntegrations/MachineLearningFlyout/index.tsx +++ b/x-pack/legacy/plugins/apm/public/components/app/ServiceDetails/ServiceIntegrations/MachineLearningFlyout/index.tsx @@ -57,7 +57,8 @@ export class MachineLearningFlyout extends Component { }; public addErrorToast = () => { - const core = this.context; + const { core } = this.context; + const { urlParams } = this.props; const { serviceName } = urlParams; diff --git a/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/Cytoscape.stories.tsx b/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/Cytoscape.stories.tsx index a8d3b843a1f3d..31c227d8bbcab 100644 --- a/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/Cytoscape.stories.tsx +++ b/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/Cytoscape.stories.tsx @@ -85,6 +85,7 @@ storiesOf('app/ServiceMap/Cytoscape', module) } }, { data: { id: 'external', 'span.type': 'external' } }, + { data: { id: 'ext', 'span.type': 'ext' } }, { data: { id: 'messaging', 'span.type': 'messaging' } }, { data: { diff --git a/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/Cytoscape.tsx b/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/Cytoscape.tsx index e4b656ae8160d..53c86f92ee557 100644 --- a/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/Cytoscape.tsx +++ b/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/Cytoscape.tsx @@ -211,7 +211,9 @@ export function Cytoscape({ resetConnectedEdgeStyle(event.target); }; const unselectHandler: cytoscape.EventHandler = event => { - resetConnectedEdgeStyle(); + resetConnectedEdgeStyle( + serviceName ? event.cy.getElementById(serviceName) : undefined + ); }; const debugHandler: cytoscape.EventHandler = event => { const debugEnabled = sessionStorage.getItem('apm_debug') === 'true'; diff --git a/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/Popover/Contents.tsx b/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/Popover/Contents.tsx index 52263878ca915..491ebdc5aad15 100644 --- a/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/Popover/Contents.tsx +++ b/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/Popover/Contents.tsx @@ -27,6 +27,30 @@ interface ContentsProps { selectedNodeServiceName: string; } +// IE 11 does not handle flex properties as expected. With browser detection, +// we can use regular div elements to render contents that are almost identical. +// +// This method of detecting IE is from a Stack Overflow answer: +// https://stackoverflow.com/a/21825207 +// +// @ts-ignore `documentMode` is not recognized as a valid property of `document`. +const isIE11 = !!window.MSInputMethodContext && !!document.documentMode; + +const FlexColumnGroup = (props: { + children: React.ReactNode; + style: React.CSSProperties; + direction: 'column'; + gutterSize: 's'; +}) => { + if (isIE11) { + const { direction, gutterSize, ...rest } = props; + return
; + } + return ; +}; +const FlexColumnItem = (props: { children: React.ReactNode }) => + isIE11 ?
: ; + export function Contents({ selectedNodeData, isService, @@ -36,18 +60,18 @@ export function Contents({ }: ContentsProps) { const frameworkName = selectedNodeData[SERVICE_FRAMEWORK_NAME]; return ( - - +

{label}

-
- + + {isService ? ( )} - + {isService && ( )} -
+ ); } diff --git a/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/cytoscapeOptions.ts b/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/cytoscapeOptions.ts index 92f66f698f044..bf0e052b951ae 100644 --- a/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/cytoscapeOptions.ts +++ b/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/cytoscapeOptions.ts @@ -12,6 +12,17 @@ import { } from '../../../../../../../plugins/apm/common/elasticsearch_fieldnames'; import { defaultIcon, iconForNode } from './icons'; +// IE 11 does not properly load some SVGs or draw certain shapes. This causes +// a runtime error and the map fails work at all. We would prefer to do some +// kind of feature detection rather than browser detection, but some of these +// limitations are not well documented for older browsers. +// +// This method of detecting IE is from a Stack Overflow answer: +// https://stackoverflow.com/a/21825207 +// +// @ts-ignore `documentMode` is not recognized as a valid property of `document`. +const isIE11 = !!window.MSInputMethodContext && !!document.documentMode; + export const animationOptions: cytoscape.AnimationOptions = { duration: parseInt(theme.euiAnimSpeedNormal, 10), // @ts-ignore The cubic-bezier options here are not recognized by the cytoscape types @@ -37,8 +48,9 @@ const style: cytoscape.Stylesheet[] = [ // used here. // // @ts-ignore - 'background-image': (el: cytoscape.NodeSingular) => - iconForNode(el) ?? defaultIcon, + 'background-image': isIE11 + ? undefined + : (el: cytoscape.NodeSingular) => iconForNode(el) ?? defaultIcon, 'background-height': (el: cytoscape.NodeSingular) => isService(el) ? '60%' : '40%', 'background-width': (el: cytoscape.NodeSingular) => @@ -65,7 +77,7 @@ const style: cytoscape.Stylesheet[] = [ 'min-zoomed-font-size': parseInt(theme.euiSizeL, 10), 'overlay-opacity': 0, shape: (el: cytoscape.NodeSingular) => - isService(el) ? 'ellipse' : 'diamond', + isService(el) ? (isIE11 ? 'rectangle' : 'ellipse') : 'diamond', 'text-background-color': theme.euiColorLightestShade, 'text-background-opacity': 0, 'text-background-padding': theme.paddingSizes.xs, @@ -87,12 +99,12 @@ const style: cytoscape.Stylesheet[] = [ 'line-color': lineColor, 'overlay-opacity': 0, 'target-arrow-color': lineColor, - 'target-arrow-shape': 'triangle', + 'target-arrow-shape': isIE11 ? 'none' : 'triangle', // The DefinitelyTyped definitions don't specify this property since it's // fairly new. // // @ts-ignore - 'target-distance-from-node': theme.paddingSizes.xs, + 'target-distance-from-node': isIE11 ? undefined : theme.paddingSizes.xs, width: 1, 'source-arrow-shape': 'none', 'z-index': zIndexEdge @@ -101,12 +113,16 @@ const style: cytoscape.Stylesheet[] = [ { selector: 'edge[bidirectional]', style: { - 'source-arrow-shape': 'triangle', + 'source-arrow-shape': isIE11 ? 'none' : 'triangle', 'source-arrow-color': lineColor, - 'target-arrow-shape': 'triangle', + 'target-arrow-shape': isIE11 ? 'none' : 'triangle', // @ts-ignore - 'source-distance-from-node': parseInt(theme.paddingSizes.xs, 10), - 'target-distance-from-node': parseInt(theme.paddingSizes.xs, 10) + 'source-distance-from-node': isIE11 + ? undefined + : parseInt(theme.paddingSizes.xs, 10), + 'target-distance-from-node': isIE11 + ? undefined + : parseInt(theme.paddingSizes.xs, 10) } }, // @ts-ignore DefinitelyTyped says visibility is "none" but it's diff --git a/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/icons.ts b/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/icons.ts index dd9b48d312725..095c2d9250e27 100644 --- a/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/icons.ts +++ b/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/icons.ts @@ -32,6 +32,7 @@ export const defaultIcon = defaultIconImport; const icons: { [key: string]: string } = { cache: databaseIcon, db: databaseIcon, + ext: globeIcon, external: globeIcon, messaging: documentsIcon, resource: globeIcon diff --git a/x-pack/legacy/plugins/apm/public/components/app/Settings/AgentConfigurations/AgentConfigurationCreateEdit/SettingsPage/SettingFormRow.tsx b/x-pack/legacy/plugins/apm/public/components/app/Settings/AgentConfigurations/AgentConfigurationCreateEdit/SettingsPage/SettingFormRow.tsx index 30c772bf5f634..baab600145b81 100644 --- a/x-pack/legacy/plugins/apm/public/components/app/Settings/AgentConfigurations/AgentConfigurationCreateEdit/SettingsPage/SettingFormRow.tsx +++ b/x-pack/legacy/plugins/apm/public/components/app/Settings/AgentConfigurations/AgentConfigurationCreateEdit/SettingsPage/SettingFormRow.tsx @@ -73,7 +73,10 @@ function FormRow({ return ( onChange(setting.key, e.target.value)} /> @@ -105,7 +108,7 @@ function FormRow({ defaultMessage: 'Select unit' })} value={unit} - options={setting.units?.map(text => ({ text }))} + options={setting.units?.map(text => ({ text, value: text }))} onChange={e => onChange( setting.key, diff --git a/x-pack/legacy/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/CustomLinkFlyout/DeleteButton.tsx b/x-pack/legacy/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/CustomLinkFlyout/DeleteButton.tsx index 2b3a5cbe87992..87cb171518ea4 100644 --- a/x-pack/legacy/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/CustomLinkFlyout/DeleteButton.tsx +++ b/x-pack/legacy/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/CustomLinkFlyout/DeleteButton.tsx @@ -8,6 +8,7 @@ import { EuiButtonEmpty } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { NotificationsStart } from 'kibana/public'; import React, { useState } from 'react'; +import { px, unit } from '../../../../../../style/variables'; import { callApmApi } from '../../../../../../services/rest/createCallApmApi'; import { useApmPluginContext } from '../../../../../../hooks/useApmPluginContext'; @@ -31,6 +32,7 @@ export function DeleteButton({ onDelete, customLinkId }: Props) { setIsDeleting(false); onDelete(); }} + style={{ marginRight: px(unit) }} > {i18n.translate('xpack.apm.settings.customizeUI.customLink.delete', { defaultMessage: 'Delete' diff --git a/x-pack/legacy/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/CustomLinkFlyout/FlyoutFooter.tsx b/x-pack/legacy/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/CustomLinkFlyout/FlyoutFooter.tsx index cb27221309812..96505d639bcdd 100644 --- a/x-pack/legacy/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/CustomLinkFlyout/FlyoutFooter.tsx +++ b/x-pack/legacy/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/CustomLinkFlyout/FlyoutFooter.tsx @@ -40,29 +40,23 @@ export const FlyoutFooter = ({ )}
- - - {customLinkId && ( - - - + + {customLinkId && ( + + )} + + {i18n.translate( + 'xpack.apm.settings.customizeUI.customLink.flyout.save', + { + defaultMessage: 'Save' + } )} - - - {i18n.translate( - 'xpack.apm.settings.customizeUI.customLink.flyout.save', - { - defaultMessage: 'Save' - } - )} - - - + diff --git a/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/waterfall_helpers/waterfall_helpers.test.ts b/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/waterfall_helpers/waterfall_helpers.test.ts index 416eb879d5974..d844ac8b5988d 100644 --- a/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/waterfall_helpers/waterfall_helpers.test.ts +++ b/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/waterfall_helpers/waterfall_helpers.test.ts @@ -166,6 +166,212 @@ describe('waterfall_helpers', () => { expect(waterfall.errorsCount).toEqual(0); expect(waterfall).toMatchSnapshot(); }); + it('should reparent spans', () => { + const traceItems = [ + { + processor: { event: 'transaction' }, + trace: { id: 'myTraceId' }, + service: { name: 'opbeans-node' }, + transaction: { + duration: { us: 49660 }, + name: 'GET /api', + id: 'myTransactionId1' + }, + timestamp: { us: 1549324795784006 } + } as Transaction, + { + parent: { id: 'mySpanIdD' }, + processor: { event: 'span' }, + trace: { id: 'myTraceId' }, + service: { name: 'opbeans-ruby' }, + transaction: { id: 'myTransactionId1' }, + timestamp: { us: 1549324795825633 }, + span: { + duration: { us: 481 }, + name: 'SELECT FROM products', + id: 'mySpanIdB' + }, + child_ids: ['mySpanIdA', 'mySpanIdC'] + } as Span, + { + parent: { id: 'mySpanIdD' }, + processor: { event: 'span' }, + trace: { id: 'myTraceId' }, + service: { name: 'opbeans-ruby' }, + transaction: { id: 'myTransactionId1' }, + span: { + duration: { us: 6161 }, + name: 'Api::ProductsController#index', + id: 'mySpanIdA' + }, + timestamp: { us: 1549324795824504 } + } as Span, + { + parent: { id: 'mySpanIdD' }, + processor: { event: 'span' }, + trace: { id: 'myTraceId' }, + service: { name: 'opbeans-ruby' }, + transaction: { id: 'myTransactionId1' }, + span: { + duration: { us: 532 }, + name: 'SELECT FROM product', + id: 'mySpanIdC' + }, + timestamp: { us: 1549324795827905 } + } as Span, + { + parent: { id: 'myTransactionId1' }, + processor: { event: 'span' }, + trace: { id: 'myTraceId' }, + service: { name: 'opbeans-node' }, + transaction: { id: 'myTransactionId1' }, + span: { + duration: { us: 47557 }, + name: 'GET opbeans-ruby:3000/api/products', + id: 'mySpanIdD' + }, + timestamp: { us: 1549324795785760 } + } as Span + ]; + const entryTransactionId = 'myTransactionId1'; + const waterfall = getWaterfall( + { + trace: { items: traceItems, errorDocs: [], exceedsMax: false }, + errorsPerTransaction: {} + }, + entryTransactionId + ); + const getIdAndParentId = (item: IWaterfallItem) => ({ + id: item.id, + parentId: item.parent?.id + }); + expect(waterfall.items.length).toBe(5); + expect(getIdAndParentId(waterfall.items[0])).toEqual({ + id: 'myTransactionId1', + parentId: undefined + }); + expect(getIdAndParentId(waterfall.items[1])).toEqual({ + id: 'mySpanIdD', + parentId: 'myTransactionId1' + }); + expect(getIdAndParentId(waterfall.items[2])).toEqual({ + id: 'mySpanIdB', + parentId: 'mySpanIdD' + }); + expect(getIdAndParentId(waterfall.items[3])).toEqual({ + id: 'mySpanIdA', + parentId: 'mySpanIdB' + }); + expect(getIdAndParentId(waterfall.items[4])).toEqual({ + id: 'mySpanIdC', + parentId: 'mySpanIdB' + }); + expect(waterfall.errorItems.length).toBe(0); + expect(waterfall.errorsCount).toEqual(0); + }); + it("shouldn't reparent spans when child id isn't found", () => { + const traceItems = [ + { + processor: { event: 'transaction' }, + trace: { id: 'myTraceId' }, + service: { name: 'opbeans-node' }, + transaction: { + duration: { us: 49660 }, + name: 'GET /api', + id: 'myTransactionId1' + }, + timestamp: { us: 1549324795784006 } + } as Transaction, + { + parent: { id: 'mySpanIdD' }, + processor: { event: 'span' }, + trace: { id: 'myTraceId' }, + service: { name: 'opbeans-ruby' }, + transaction: { id: 'myTransactionId1' }, + timestamp: { us: 1549324795825633 }, + span: { + duration: { us: 481 }, + name: 'SELECT FROM products', + id: 'mySpanIdB' + }, + child_ids: ['incorrectId', 'mySpanIdC'] + } as Span, + { + parent: { id: 'mySpanIdD' }, + processor: { event: 'span' }, + trace: { id: 'myTraceId' }, + service: { name: 'opbeans-ruby' }, + transaction: { id: 'myTransactionId1' }, + span: { + duration: { us: 6161 }, + name: 'Api::ProductsController#index', + id: 'mySpanIdA' + }, + timestamp: { us: 1549324795824504 } + } as Span, + { + parent: { id: 'mySpanIdD' }, + processor: { event: 'span' }, + trace: { id: 'myTraceId' }, + service: { name: 'opbeans-ruby' }, + transaction: { id: 'myTransactionId1' }, + span: { + duration: { us: 532 }, + name: 'SELECT FROM product', + id: 'mySpanIdC' + }, + timestamp: { us: 1549324795827905 } + } as Span, + { + parent: { id: 'myTransactionId1' }, + processor: { event: 'span' }, + trace: { id: 'myTraceId' }, + service: { name: 'opbeans-node' }, + transaction: { id: 'myTransactionId1' }, + span: { + duration: { us: 47557 }, + name: 'GET opbeans-ruby:3000/api/products', + id: 'mySpanIdD' + }, + timestamp: { us: 1549324795785760 } + } as Span + ]; + const entryTransactionId = 'myTransactionId1'; + const waterfall = getWaterfall( + { + trace: { items: traceItems, errorDocs: [], exceedsMax: false }, + errorsPerTransaction: {} + }, + entryTransactionId + ); + const getIdAndParentId = (item: IWaterfallItem) => ({ + id: item.id, + parentId: item.parent?.id + }); + expect(waterfall.items.length).toBe(5); + expect(getIdAndParentId(waterfall.items[0])).toEqual({ + id: 'myTransactionId1', + parentId: undefined + }); + expect(getIdAndParentId(waterfall.items[1])).toEqual({ + id: 'mySpanIdD', + parentId: 'myTransactionId1' + }); + expect(getIdAndParentId(waterfall.items[2])).toEqual({ + id: 'mySpanIdA', + parentId: 'mySpanIdD' + }); + expect(getIdAndParentId(waterfall.items[3])).toEqual({ + id: 'mySpanIdB', + parentId: 'mySpanIdD' + }); + expect(getIdAndParentId(waterfall.items[4])).toEqual({ + id: 'mySpanIdC', + parentId: 'mySpanIdB' + }); + expect(waterfall.errorItems.length).toBe(0); + expect(waterfall.errorsCount).toEqual(0); + }); }); describe('getWaterfallItems', () => { diff --git a/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/waterfall_helpers/waterfall_helpers.ts b/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/waterfall_helpers/waterfall_helpers.ts index 58d9134c4d787..8a873b2ddf1c9 100644 --- a/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/waterfall_helpers/waterfall_helpers.ts +++ b/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/waterfall_helpers/waterfall_helpers.ts @@ -236,6 +236,29 @@ const getWaterfallItems = (items: TraceAPIResponse['trace']['items']) => } }); +/** + * Changes the parent_id of items based on the child_ids property. + * Solves the problem of Inferred spans that are created as child of trace spans + * when it actually should be its parent. + * @param waterfallItems + */ +const reparentSpans = (waterfallItems: IWaterfallItem[]) => { + return waterfallItems.map(waterfallItem => { + if (waterfallItem.docType === 'span') { + const { child_ids: childIds } = waterfallItem.doc; + if (childIds) { + childIds.forEach(childId => { + const item = waterfallItems.find(_item => _item.id === childId); + if (item) { + item.parentId = waterfallItem.id; + } + }); + } + } + return waterfallItem; + }); +}; + const getChildrenGroupedByParentId = (waterfallItems: IWaterfallItem[]) => groupBy(waterfallItems, item => (item.parentId ? item.parentId : ROOT_ID)); @@ -306,7 +329,9 @@ export function getWaterfall( const waterfallItems: IWaterfallItem[] = getWaterfallItems(trace.items); - const childrenByParentId = getChildrenGroupedByParentId(waterfallItems); + const childrenByParentId = getChildrenGroupedByParentId( + reparentSpans(waterfallItems) + ); const entryWaterfallTransaction = getEntryWaterfallTransaction( entryTransactionId, diff --git a/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/WaterfallContainer.stories.tsx b/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/WaterfallContainer.stories.tsx index 938962cc9dd18..f681f4dfc675a 100644 --- a/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/WaterfallContainer.stories.tsx +++ b/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/WaterfallContainer.stories.tsx @@ -14,7 +14,8 @@ import { urlParams, simpleTrace, traceWithErrors, - traceChildStartBeforeParent + traceChildStartBeforeParent, + inferredSpans } from './waterfallContainer.stories.data'; import { getWaterfall } from './Waterfall/waterfall_helpers/waterfall_helpers'; @@ -74,3 +75,22 @@ storiesOf('app/TransactionDetails/Waterfall', module).add( }, { info: { source: false } } ); + +storiesOf('app/TransactionDetails/Waterfall', module).add( + 'inferred spans', + () => { + const waterfall = getWaterfall( + inferredSpans as TraceAPIResponse, + 'f2387d37260d00bd' + ); + return ( + + ); + }, + { info: { source: false } } +); diff --git a/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/waterfallContainer.stories.data.ts b/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/waterfallContainer.stories.data.ts index 835183e73b298..306c8e4f3fedb 100644 --- a/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/waterfallContainer.stories.data.ts +++ b/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/waterfallContainer.stories.data.ts @@ -1645,3 +1645,667 @@ export const traceChildStartBeforeParent = { }, errorsPerTransaction: {} }; + +export const inferredSpans = { + trace: { + items: [ + { + container: { + id: 'fc2ae281f56fb84728bc9b5e6c17f3d13bbb7f4efd461158558e5c38e655abad' + }, + agent: { + name: 'java', + ephemeral_id: '1cb5c830-c677-4b13-b340-ab1502f527c3', + version: '1.15.1-SNAPSHOT' + }, + process: { + pid: 6, + title: '/opt/java/openjdk/bin/java', + ppid: 1 + }, + source: { + ip: '172.18.0.8' + }, + processor: { + name: 'transaction', + event: 'transaction' + }, + url: { + path: '/api/products/2', + scheme: 'http', + port: 3000, + domain: '172.18.0.7', + full: 'http://172.18.0.7:3000/api/products/2' + }, + observer: { + hostname: '7189f754b5a3', + id: 'f32d8d9f-a9f9-4355-8370-548dfd8024dc', + ephemeral_id: 'bff20764-0195-4f78-aa84-d799fc47b954', + type: 'apm-server', + version: '8.0.0', + version_major: 8 + }, + trace: { + id: '3b0dc77f3754e5bcb9da0e4c15e0db97' + }, + '@timestamp': '2020-04-09T11:36:00.786Z', + ecs: { + version: '1.5.0' + }, + service: { + node: { + name: + 'fc2ae281f56fb84728bc9b5e6c17f3d13bbb7f4efd461158558e5c38e655abad' + }, + environment: 'production', + name: 'opbeans-java', + runtime: { + name: 'Java', + version: '11.0.6' + }, + language: { + name: 'Java', + version: '11.0.6' + }, + version: 'None' + }, + host: { + hostname: 'fc2ae281f56f', + os: { + platform: 'Linux' + }, + ip: '172.18.0.7', + name: 'fc2ae281f56f', + architecture: 'amd64' + }, + client: { + ip: '172.18.0.8' + }, + http: { + request: { + headers: { + Accept: ['*/*'], + 'User-Agent': ['Python/3.7 aiohttp/3.3.2'], + Host: ['172.18.0.7:3000'], + 'Accept-Encoding': ['gzip, deflate'] + }, + method: 'get', + socket: { + encrypted: false, + remote_address: '172.18.0.8' + } + }, + response: { + headers: { + 'Transfer-Encoding': ['chunked'], + Date: ['Thu, 09 Apr 2020 11:36:01 GMT'], + 'Content-Type': ['application/json;charset=UTF-8'] + }, + status_code: 200, + finished: true, + headers_sent: true + }, + version: '1.1' + }, + user_agent: { + original: 'Python/3.7 aiohttp/3.3.2', + name: 'Other', + device: { + name: 'Other' + } + }, + transaction: { + duration: { + us: 237537 + }, + result: 'HTTP 2xx', + name: 'APIRestController#product', + span_count: { + dropped: 0, + started: 3 + }, + id: 'f2387d37260d00bd', + type: 'request', + sampled: true + }, + timestamp: { + us: 1586432160786001 + } + }, + { + container: { + id: 'fc2ae281f56fb84728bc9b5e6c17f3d13bbb7f4efd461158558e5c38e655abad' + }, + parent: { + id: 'f2387d37260d00bd' + }, + agent: { + name: 'java', + ephemeral_id: '1cb5c830-c677-4b13-b340-ab1502f527c3', + version: '1.15.1-SNAPSHOT' + }, + process: { + pid: 6, + title: '/opt/java/openjdk/bin/java', + ppid: 1 + }, + processor: { + name: 'transaction', + event: 'span' + }, + observer: { + hostname: '7189f754b5a3', + id: 'f32d8d9f-a9f9-4355-8370-548dfd8024dc', + ephemeral_id: 'bff20764-0195-4f78-aa84-d799fc47b954', + type: 'apm-server', + version: '8.0.0', + version_major: 8 + }, + trace: { + id: '3b0dc77f3754e5bcb9da0e4c15e0db97' + }, + '@timestamp': '2020-04-09T11:36:00.810Z', + ecs: { + version: '1.5.0' + }, + service: { + node: { + name: + 'fc2ae281f56fb84728bc9b5e6c17f3d13bbb7f4efd461158558e5c38e655abad' + }, + environment: 'production', + name: 'opbeans-java', + runtime: { + name: 'Java', + version: '11.0.6' + }, + language: { + name: 'Java', + version: '11.0.6' + }, + version: 'None' + }, + host: { + hostname: 'fc2ae281f56f', + os: { + platform: 'Linux' + }, + ip: '172.18.0.7', + name: 'fc2ae281f56f', + architecture: 'amd64' + }, + transaction: { + id: 'f2387d37260d00bd' + }, + span: { + duration: { + us: 204574 + }, + subtype: 'inferred', + name: 'ServletInvocableHandlerMethod#invokeAndHandle', + id: 'a5df600bd7bd5e38', + type: 'app' + }, + timestamp: { + us: 1586432160810441 + } + }, + { + container: { + id: 'fc2ae281f56fb84728bc9b5e6c17f3d13bbb7f4efd461158558e5c38e655abad' + }, + parent: { + id: 'a5df600bd7bd5e38' + }, + agent: { + name: 'java', + ephemeral_id: '1cb5c830-c677-4b13-b340-ab1502f527c3', + version: '1.15.1-SNAPSHOT' + }, + process: { + pid: 6, + title: '/opt/java/openjdk/bin/java', + ppid: 1 + }, + processor: { + name: 'transaction', + event: 'span' + }, + observer: { + hostname: '7189f754b5a3', + id: 'f32d8d9f-a9f9-4355-8370-548dfd8024dc', + type: 'apm-server', + ephemeral_id: 'bff20764-0195-4f78-aa84-d799fc47b954', + version: '8.0.0', + version_major: 8 + }, + trace: { + id: '3b0dc77f3754e5bcb9da0e4c15e0db97' + }, + '@timestamp': '2020-04-09T11:36:00.810Z', + ecs: { + version: '1.5.0' + }, + service: { + node: { + name: + 'fc2ae281f56fb84728bc9b5e6c17f3d13bbb7f4efd461158558e5c38e655abad' + }, + environment: 'production', + name: 'opbeans-java', + runtime: { + name: 'Java', + version: '11.0.6' + }, + language: { + name: 'Java', + version: '11.0.6' + }, + version: 'None' + }, + host: { + hostname: 'fc2ae281f56f', + os: { + platform: 'Linux' + }, + ip: '172.18.0.7', + name: 'fc2ae281f56f', + architecture: 'amd64' + }, + transaction: { + id: 'f2387d37260d00bd' + }, + timestamp: { + us: 1586432160810441 + }, + span: { + duration: { + us: 102993 + }, + stacktrace: [ + { + library_frame: true, + exclude_from_grouping: false, + filename: 'InvocableHandlerMethod.java', + line: { + number: -1 + }, + function: 'doInvoke' + }, + { + exclude_from_grouping: false, + library_frame: true, + filename: 'InvocableHandlerMethod.java', + line: { + number: -1 + }, + function: 'invokeForRequest' + } + ], + subtype: 'inferred', + name: 'APIRestController#product', + id: '808dc34fc41ce522', + type: 'app' + } + }, + { + container: { + id: 'fc2ae281f56fb84728bc9b5e6c17f3d13bbb7f4efd461158558e5c38e655abad' + }, + parent: { + id: 'f2387d37260d00bd' + }, + agent: { + name: 'java', + ephemeral_id: '1cb5c830-c677-4b13-b340-ab1502f527c3', + version: '1.15.1-SNAPSHOT' + }, + process: { + pid: 6, + title: '/opt/java/openjdk/bin/java', + ppid: 1 + }, + processor: { + name: 'transaction', + event: 'span' + }, + labels: { + productId: '2' + }, + observer: { + hostname: '7189f754b5a3', + id: 'f32d8d9f-a9f9-4355-8370-548dfd8024dc', + ephemeral_id: 'bff20764-0195-4f78-aa84-d799fc47b954', + type: 'apm-server', + version: '8.0.0', + version_major: 8 + }, + trace: { + id: '3b0dc77f3754e5bcb9da0e4c15e0db97' + }, + '@timestamp': '2020-04-09T11:36:00.832Z', + ecs: { + version: '1.5.0' + }, + service: { + node: { + name: + 'fc2ae281f56fb84728bc9b5e6c17f3d13bbb7f4efd461158558e5c38e655abad' + }, + environment: 'production', + name: 'opbeans-java', + runtime: { + name: 'Java', + version: '11.0.6' + }, + language: { + name: 'Java', + version: '11.0.6' + }, + version: 'None' + }, + host: { + hostname: 'fc2ae281f56f', + os: { + platform: 'Linux' + }, + ip: '172.18.0.7', + name: 'fc2ae281f56f', + architecture: 'amd64' + }, + transaction: { + id: 'f2387d37260d00bd' + }, + timestamp: { + us: 1586432160832300 + }, + span: { + duration: { + us: 99295 + }, + name: 'OpenTracing product span', + id: '41226ae63af4f235', + type: 'unknown' + }, + child_ids: ['8d80de06aa11a6fc'] + }, + { + container: { + id: 'fc2ae281f56fb84728bc9b5e6c17f3d13bbb7f4efd461158558e5c38e655abad' + }, + parent: { + id: '808dc34fc41ce522' + }, + process: { + pid: 6, + title: '/opt/java/openjdk/bin/java', + ppid: 1 + }, + agent: { + name: 'java', + ephemeral_id: '1cb5c830-c677-4b13-b340-ab1502f527c3', + version: '1.15.1-SNAPSHOT' + }, + processor: { + name: 'transaction', + event: 'span' + }, + observer: { + hostname: '7189f754b5a3', + id: 'f32d8d9f-a9f9-4355-8370-548dfd8024dc', + ephemeral_id: 'bff20764-0195-4f78-aa84-d799fc47b954', + type: 'apm-server', + version: '8.0.0', + version_major: 8 + }, + trace: { + id: '3b0dc77f3754e5bcb9da0e4c15e0db97' + }, + '@timestamp': '2020-04-09T11:36:00.859Z', + ecs: { + version: '1.5.0' + }, + service: { + node: { + name: + 'fc2ae281f56fb84728bc9b5e6c17f3d13bbb7f4efd461158558e5c38e655abad' + }, + environment: 'production', + name: 'opbeans-java', + runtime: { + name: 'Java', + version: '11.0.6' + }, + language: { + name: 'Java', + version: '11.0.6' + }, + version: 'None' + }, + host: { + hostname: 'fc2ae281f56f', + os: { + platform: 'Linux' + }, + ip: '172.18.0.7', + name: 'fc2ae281f56f', + architecture: 'amd64' + }, + transaction: { + id: 'f2387d37260d00bd' + }, + timestamp: { + us: 1586432160859600 + }, + span: { + duration: { + us: 53835 + }, + subtype: 'inferred', + name: 'Loader#executeQueryStatement', + id: '8d80de06aa11a6fc', + type: 'app' + } + }, + { + container: { + id: 'fc2ae281f56fb84728bc9b5e6c17f3d13bbb7f4efd461158558e5c38e655abad' + }, + parent: { + id: '41226ae63af4f235' + }, + agent: { + name: 'java', + ephemeral_id: '1cb5c830-c677-4b13-b340-ab1502f527c3', + version: '1.15.1-SNAPSHOT' + }, + process: { + pid: 6, + title: '/opt/java/openjdk/bin/java', + ppid: 1 + }, + destination: { + address: 'postgres', + port: 5432 + }, + processor: { + name: 'transaction', + event: 'span' + }, + observer: { + hostname: '7189f754b5a3', + id: 'f32d8d9f-a9f9-4355-8370-548dfd8024dc', + ephemeral_id: 'bff20764-0195-4f78-aa84-d799fc47b954', + type: 'apm-server', + version: '8.0.0', + version_major: 8 + }, + trace: { + id: '3b0dc77f3754e5bcb9da0e4c15e0db97' + }, + '@timestamp': '2020-04-09T11:36:00.903Z', + ecs: { + version: '1.5.0' + }, + service: { + node: { + name: + 'fc2ae281f56fb84728bc9b5e6c17f3d13bbb7f4efd461158558e5c38e655abad' + }, + environment: 'production', + name: 'opbeans-java', + runtime: { + name: 'Java', + version: '11.0.6' + }, + language: { + name: 'Java', + version: '11.0.6' + }, + version: 'None' + }, + host: { + hostname: 'fc2ae281f56f', + os: { + platform: 'Linux' + }, + ip: '172.18.0.7', + name: 'fc2ae281f56f', + architecture: 'amd64' + }, + transaction: { + id: 'f2387d37260d00bd' + }, + timestamp: { + us: 1586432160903236 + }, + span: { + duration: { + us: 10211 + }, + subtype: 'postgresql', + destination: { + service: { + resource: 'postgresql', + name: 'postgresql', + type: 'db' + } + }, + name: 'SELECT FROM products', + action: 'query', + id: '3708d5623658182f', + type: 'db', + db: { + statement: + 'select product0_.id as col_0_0_, product0_.sku as col_1_0_, product0_.name as col_2_0_, product0_.description as col_3_0_, product0_.cost as col_4_0_, product0_.selling_price as col_5_0_, product0_.stock as col_6_0_, producttyp1_.id as col_7_0_, producttyp1_.name as col_8_0_, (select sum(orderline2_.amount) from order_lines orderline2_ where orderline2_.product_id=product0_.id) as col_9_0_ from products product0_ left outer join product_types producttyp1_ on product0_.type_id=producttyp1_.id where product0_.id=?', + type: 'sql', + user: { + name: 'postgres' + } + } + } + }, + { + container: { + id: 'fc2ae281f56fb84728bc9b5e6c17f3d13bbb7f4efd461158558e5c38e655abad' + }, + parent: { + id: '41226ae63af4f235' + }, + process: { + pid: 6, + title: '/opt/java/openjdk/bin/java', + ppid: 1 + }, + agent: { + name: 'java', + ephemeral_id: '1cb5c830-c677-4b13-b340-ab1502f527c3', + version: '1.15.1-SNAPSHOT' + }, + destination: { + address: 'postgres', + port: 5432 + }, + processor: { + name: 'transaction', + event: 'span' + }, + observer: { + hostname: '7189f754b5a3', + id: 'f32d8d9f-a9f9-4355-8370-548dfd8024dc', + ephemeral_id: 'bff20764-0195-4f78-aa84-d799fc47b954', + type: 'apm-server', + version: '8.0.0', + version_major: 8 + }, + trace: { + id: '3b0dc77f3754e5bcb9da0e4c15e0db97' + }, + '@timestamp': '2020-04-09T11:36:00.859Z', + ecs: { + version: '1.5.0' + }, + service: { + node: { + name: + 'fc2ae281f56fb84728bc9b5e6c17f3d13bbb7f4efd461158558e5c38e655abad' + }, + environment: 'production', + name: 'opbeans-java', + runtime: { + name: 'Java', + version: '11.0.6' + }, + language: { + name: 'Java', + version: '11.0.6' + }, + version: 'None' + }, + host: { + hostname: 'fc2ae281f56f', + os: { + platform: 'Linux' + }, + ip: '172.18.0.7', + name: 'fc2ae281f56f', + architecture: 'amd64' + }, + transaction: { + id: 'f2387d37260d00bd' + }, + timestamp: { + us: 1586432160859508 + }, + span: { + duration: { + us: 4503 + }, + subtype: 'postgresql', + destination: { + service: { + resource: 'postgresql', + name: 'postgresql', + type: 'db' + } + }, + name: 'empty query', + action: 'query', + id: '9871cfd612368932', + type: 'db', + db: { + rows_affected: 0, + statement: '(empty query)', + type: 'sql', + user: { + name: 'postgres' + } + } + } + } + ], + exceedsMax: false, + errorDocs: [] + }, + errorsPerTransaction: {} +}; diff --git a/x-pack/legacy/plugins/apm/public/components/shared/TransactionActionMenu/CustomLink/CustomLinkPopover.tsx b/x-pack/legacy/plugins/apm/public/components/shared/TransactionActionMenu/CustomLink/CustomLinkPopover.tsx index a20bc7e21cfc5..3aed1b7ac2953 100644 --- a/x-pack/legacy/plugins/apm/public/components/shared/TransactionActionMenu/CustomLink/CustomLinkPopover.tsx +++ b/x-pack/legacy/plugins/apm/public/components/shared/TransactionActionMenu/CustomLink/CustomLinkPopover.tsx @@ -19,6 +19,7 @@ import { ManageCustomLink } from './ManageCustomLink'; import { px } from '../../../../style/variables'; const ScrollableContainer = styled.div` + -ms-overflow-style: none; max-height: ${px(535)}; overflow: scroll; `; diff --git a/x-pack/legacy/plugins/apm/public/components/shared/TransactionActionMenu/TransactionActionMenu.tsx b/x-pack/legacy/plugins/apm/public/components/shared/TransactionActionMenu/TransactionActionMenu.tsx index e3fbcf8485d54..7ebfe26b83630 100644 --- a/x-pack/legacy/plugins/apm/public/components/shared/TransactionActionMenu/TransactionActionMenu.tsx +++ b/x-pack/legacy/plugins/apm/public/components/shared/TransactionActionMenu/TransactionActionMenu.tsx @@ -124,7 +124,7 @@ export const TransactionActionMenu: FunctionComponent = ({ setIsActionPopoverOpen(true)} /> } > -
+
{isCustomLinksPopoverOpen ? ( (http, { method: 'POST', pathname: `/api/ml/modules/setup/apm_transaction`, diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/common/saved_map.ts b/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/common/saved_map.ts index 4b045b0c5edcf..cba19ce7da80f 100644 --- a/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/common/saved_map.ts +++ b/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/common/saved_map.ts @@ -5,8 +5,6 @@ */ import { ExpressionFunctionDefinition } from 'src/plugins/expressions/common'; -import { TimeRange, Filter as DataFilter } from 'src/plugins/data/public'; -import { EmbeddableInput } from 'src/plugins/embeddable/public'; import { getQueryFilters } from '../../../public/lib/build_embeddable_filters'; import { Filter, MapCenter, TimeRange as TimeRangeArg } from '../../../types'; import { @@ -15,6 +13,7 @@ import { EmbeddableExpression, } from '../../expression_types'; import { getFunctionHelp } from '../../../i18n'; +import { MapEmbeddableInput } from '../../../../../plugins/maps/public'; interface Arguments { id: string; @@ -24,32 +23,12 @@ interface Arguments { timerange: TimeRangeArg | null; } -// Map embeddable is missing proper typings, so type is just to document what we -// are expecting to pass to the embeddable -export type SavedMapInput = EmbeddableInput & { - id: string; - isLayerTOCOpen: boolean; - timeRange?: TimeRange; - refreshConfig: { - isPaused: boolean; - interval: number; - }; - hideFilterActions: true; - filters: DataFilter[]; - mapCenter?: { - lat: number; - lon: number; - zoom: number; - }; - hiddenLayers?: string[]; -}; - const defaultTimeRange = { from: 'now-15m', to: 'now', }; -type Output = EmbeddableExpression; +type Output = EmbeddableExpression; export function savedMap(): ExpressionFunctionDefinition< 'savedMap', @@ -108,8 +87,8 @@ export function savedMap(): ExpressionFunctionDefinition< filters: getQueryFilters(filters), timeRange: args.timerange || defaultTimeRange, refreshConfig: { - isPaused: false, - interval: 0, + pause: false, + value: 0, }, mapCenter: center, diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/plugin.ts b/x-pack/legacy/plugins/canvas/canvas_plugin_src/plugin.ts index 7cd1efe9e27c8..a654c6b28b350 100644 --- a/x-pack/legacy/plugins/canvas/canvas_plugin_src/plugin.ts +++ b/x-pack/legacy/plugins/canvas/canvas_plugin_src/plugin.ts @@ -6,11 +6,14 @@ import { CoreSetup, CoreStart, Plugin } from 'src/core/public'; import { CanvasSetup } from '../public'; +import { EmbeddableStart } from '../../../../../src/plugins/embeddable/public'; +import { UiActionsStart } from '../../../../../src/plugins/ui_actions/public'; +import { Start as InspectorStart } from '../../../../../src/plugins/inspector/public'; import { functions } from './functions/browser'; import { typeFunctions } from './expression_types'; // @ts-ignore: untyped local -import { renderFunctions } from './renderers'; +import { renderFunctions, renderFunctionFactories } from './renderers'; import { elementSpecs } from './elements'; // @ts-ignore Untyped Local @@ -30,13 +33,26 @@ interface SetupDeps { canvas: CanvasSetup; } +export interface StartDeps { + embeddable: EmbeddableStart; + uiActions: UiActionsStart; + inspector: InspectorStart; +} + /** @internal */ -export class CanvasSrcPlugin implements Plugin<{}, {}, SetupDeps, {}> { - public setup(core: CoreSetup, plugins: SetupDeps) { +export class CanvasSrcPlugin implements Plugin { + public setup(core: CoreSetup, plugins: SetupDeps) { plugins.canvas.addFunctions(functions); plugins.canvas.addTypes(typeFunctions); + plugins.canvas.addRenderers(renderFunctions); + core.getStartServices().then(([coreStart, depsStart]) => { + plugins.canvas.addRenderers( + renderFunctionFactories.map((factory: any) => factory(coreStart, depsStart)) + ); + }); + plugins.canvas.addElements(elementSpecs); plugins.canvas.addDatasourceUIs(datasourceSpecs); plugins.canvas.addModelUIs(modelSpecs); @@ -45,11 +61,7 @@ export class CanvasSrcPlugin implements Plugin<{}, {}, SetupDeps, {}> { plugins.canvas.addTagUIs(tagSpecs); plugins.canvas.addTemplates(templateSpecs); plugins.canvas.addTransformUIs(transformSpecs); - - return {}; } - public start(core: CoreStart, plugins: {}) { - return {}; - } + public start(core: CoreStart, plugins: StartDeps) {} } diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/renderers/embeddable/embeddable.scss b/x-pack/legacy/plugins/canvas/canvas_plugin_src/renderers/embeddable/embeddable.scss index 04f2f393d1e80..ae26a1bee99a6 100644 --- a/x-pack/legacy/plugins/canvas/canvas_plugin_src/renderers/embeddable/embeddable.scss +++ b/x-pack/legacy/plugins/canvas/canvas_plugin_src/renderers/embeddable/embeddable.scss @@ -26,8 +26,4 @@ .euiTable { background: none; } - - .lnsExpressionRenderer { - @include euiScrollBar; - } -} \ No newline at end of file +} diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/renderers/embeddable/embeddable.tsx b/x-pack/legacy/plugins/canvas/canvas_plugin_src/renderers/embeddable/embeddable.tsx index 817be6e144fc8..a1096d50c1653 100644 --- a/x-pack/legacy/plugins/canvas/canvas_plugin_src/renderers/embeddable/embeddable.tsx +++ b/x-pack/legacy/plugins/canvas/canvas_plugin_src/renderers/embeddable/embeddable.tsx @@ -7,7 +7,8 @@ import React from 'react'; import ReactDOM from 'react-dom'; import { I18nContext } from 'ui/i18n'; -import { npStart } from 'ui/new_platform'; +import { CoreStart } from '../../../../../../../src/core/public'; +import { StartDeps } from '../../plugin'; import { IEmbeddable, EmbeddableFactory, @@ -28,86 +29,88 @@ const embeddablesRegistry: { [key: string]: IEmbeddable; } = {}; -const renderEmbeddable = (embeddableObject: IEmbeddable, domNode: HTMLElement) => { - return ( -
- - - -
- ); +const renderEmbeddableFactory = (core: CoreStart, plugins: StartDeps) => { + return (embeddableObject: IEmbeddable, domNode: HTMLElement) => { + return ( +
+ + + +
+ ); + }; }; -const embeddable = () => ({ - name: 'embeddable', - displayName: strings.getDisplayName(), - help: strings.getHelpDescription(), - reuseDomNode: true, - render: async ( - domNode: HTMLElement, - { input, embeddableType }: EmbeddableExpression, - handlers: RendererHandlers - ) => { - const uniqueId = handlers.getElementId(); - - if (!embeddablesRegistry[uniqueId]) { - const factory = Array.from(npStart.plugins.embeddable.getEmbeddableFactories()).find( - embeddableFactory => embeddableFactory.type === embeddableType - ) as EmbeddableFactory; - - if (!factory) { - handlers.done(); - throw new EmbeddableFactoryNotFoundError(embeddableType); - } - - const embeddableObject = await factory.createFromSavedObject(input.id, input); +export const embeddableRendererFactory = (core: CoreStart, plugins: StartDeps) => { + const renderEmbeddable = renderEmbeddableFactory(core, plugins); + return () => ({ + name: 'embeddable', + displayName: strings.getDisplayName(), + help: strings.getHelpDescription(), + reuseDomNode: true, + render: async ( + domNode: HTMLElement, + { input, embeddableType }: EmbeddableExpression, + handlers: RendererHandlers + ) => { + const uniqueId = handlers.getElementId(); + + if (!embeddablesRegistry[uniqueId]) { + const factory = Array.from(plugins.embeddable.getEmbeddableFactories()).find( + embeddableFactory => embeddableFactory.type === embeddableType + ) as EmbeddableFactory; + + if (!factory) { + handlers.done(); + throw new EmbeddableFactoryNotFoundError(embeddableType); + } - embeddablesRegistry[uniqueId] = embeddableObject; - ReactDOM.unmountComponentAtNode(domNode); + const embeddableObject = await factory.createFromSavedObject(input.id, input); - const subscription = embeddableObject.getInput$().subscribe(function(updatedInput) { - const updatedExpression = embeddableInputToExpression(updatedInput, embeddableType); + embeddablesRegistry[uniqueId] = embeddableObject; + ReactDOM.unmountComponentAtNode(domNode); - if (updatedExpression) { - handlers.onEmbeddableInputChange(updatedExpression); - } - }); + const subscription = embeddableObject.getInput$().subscribe(function(updatedInput) { + const updatedExpression = embeddableInputToExpression(updatedInput, embeddableType); - ReactDOM.render(renderEmbeddable(embeddableObject, domNode), domNode, () => handlers.done()); + if (updatedExpression) { + handlers.onEmbeddableInputChange(updatedExpression); + } + }); - handlers.onResize(() => { ReactDOM.render(renderEmbeddable(embeddableObject, domNode), domNode, () => handlers.done() ); - }); - handlers.onDestroy(() => { - subscription.unsubscribe(); - handlers.onEmbeddableDestroyed(); + handlers.onResize(() => { + ReactDOM.render(renderEmbeddable(embeddableObject, domNode), domNode, () => + handlers.done() + ); + }); - delete embeddablesRegistry[uniqueId]; + handlers.onDestroy(() => { + subscription.unsubscribe(); + handlers.onEmbeddableDestroyed(); - return ReactDOM.unmountComponentAtNode(domNode); - }); - } else { - embeddablesRegistry[uniqueId].updateInput(input); - } - }, -}); + delete embeddablesRegistry[uniqueId]; -export { embeddable }; + return ReactDOM.unmountComponentAtNode(domNode); + }); + } else { + embeddablesRegistry[uniqueId].updateInput(input); + } + }, + }); +}; diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/renderers/embeddable/input_type_to_expression/map.test.ts b/x-pack/legacy/plugins/canvas/canvas_plugin_src/renderers/embeddable/input_type_to_expression/map.test.ts index 4c294fb37c2db..f9ff94ee7d8f1 100644 --- a/x-pack/legacy/plugins/canvas/canvas_plugin_src/renderers/embeddable/input_type_to_expression/map.test.ts +++ b/x-pack/legacy/plugins/canvas/canvas_plugin_src/renderers/embeddable/input_type_to_expression/map.test.ts @@ -5,7 +5,7 @@ */ import { toExpression } from './map'; -import { SavedMapInput } from '../../../functions/common/saved_map'; +import { MapEmbeddableInput } from '../../../../../maps/public'; import { fromExpression, Ast } from '@kbn/interpreter/common'; const baseSavedMapInput = { @@ -13,15 +13,15 @@ const baseSavedMapInput = { filters: [], isLayerTOCOpen: false, refreshConfig: { - isPaused: true, - interval: 0, + pause: true, + value: 0, }, hideFilterActions: true as true, }; describe('toExpression', () => { it('converts to a savedMap expression', () => { - const input: SavedMapInput = { + const input: MapEmbeddableInput = { ...baseSavedMapInput, }; @@ -39,7 +39,7 @@ describe('toExpression', () => { }); it('includes optional input values', () => { - const input: SavedMapInput = { + const input: MapEmbeddableInput = { ...baseSavedMapInput, mapCenter: { lat: 1, diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/renderers/embeddable/input_type_to_expression/map.ts b/x-pack/legacy/plugins/canvas/canvas_plugin_src/renderers/embeddable/input_type_to_expression/map.ts index e3f9eca61ae28..e0cb71c17774c 100644 --- a/x-pack/legacy/plugins/canvas/canvas_plugin_src/renderers/embeddable/input_type_to_expression/map.ts +++ b/x-pack/legacy/plugins/canvas/canvas_plugin_src/renderers/embeddable/input_type_to_expression/map.ts @@ -4,9 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ -import { SavedMapInput } from '../../../functions/common/saved_map'; +import { MapEmbeddableInput } from '../../../../../maps/public'; -export function toExpression(input: SavedMapInput): string { +export function toExpression(input: MapEmbeddableInput): string { const expressionParts = [] as string[]; expressionParts.push('savedMap'); diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/renderers/index.js b/x-pack/legacy/plugins/canvas/canvas_plugin_src/renderers/index.js index 48364be06e539..84f92f5149893 100644 --- a/x-pack/legacy/plugins/canvas/canvas_plugin_src/renderers/index.js +++ b/x-pack/legacy/plugins/canvas/canvas_plugin_src/renderers/index.js @@ -7,7 +7,7 @@ import { advancedFilter } from './advanced_filter'; import { debug } from './debug'; import { dropdownFilter } from './dropdown_filter'; -import { embeddable } from './embeddable/embeddable'; +import { embeddableRendererFactory } from './embeddable/embeddable'; import { error } from './error'; import { image } from './image'; import { markdown } from './markdown'; @@ -26,7 +26,6 @@ export const renderFunctions = [ advancedFilter, debug, dropdownFilter, - embeddable, error, image, markdown, @@ -41,3 +40,5 @@ export const renderFunctions = [ text, timeFilter, ]; + +export const renderFunctionFactories = [embeddableRendererFactory]; diff --git a/x-pack/legacy/plugins/canvas/index.js b/x-pack/legacy/plugins/canvas/index.js index 489b9600f200e..a1d4b35826b00 100644 --- a/x-pack/legacy/plugins/canvas/index.js +++ b/x-pack/legacy/plugins/canvas/index.js @@ -7,9 +7,7 @@ import { resolve } from 'path'; import { DEFAULT_APP_CATEGORIES } from '../../../../src/core/utils'; import { init } from './init'; -import { mappings } from './server/mappings'; import { CANVAS_APP, CANVAS_TYPE, CUSTOM_ELEMENT_TYPE } from './common/lib'; -import { migrations } from './migrations'; export function canvas(kibana) { return new kibana.Plugin({ @@ -33,8 +31,6 @@ export function canvas(kibana) { 'plugins/canvas/lib/window_error_handler.js', ], home: ['plugins/canvas/legacy_register_feature'], - mappings, - migrations, savedObjectsManagement: { [CANVAS_TYPE]: { icon: 'canvasApp', diff --git a/x-pack/legacy/plugins/canvas/migrations.js b/x-pack/legacy/plugins/canvas/migrations.js deleted file mode 100644 index d5b3d3fb1ce2a..0000000000000 --- a/x-pack/legacy/plugins/canvas/migrations.js +++ /dev/null @@ -1,18 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { CANVAS_TYPE } from './common/lib'; - -export const migrations = { - [CANVAS_TYPE]: { - '7.0.0': doc => { - if (doc.attributes) { - delete doc.attributes.id; - } - return doc; - }, - }, -}; diff --git a/x-pack/legacy/plugins/canvas/migrations.test.js b/x-pack/legacy/plugins/canvas/migrations.test.js deleted file mode 100644 index 182ef3b18cce7..0000000000000 --- a/x-pack/legacy/plugins/canvas/migrations.test.js +++ /dev/null @@ -1,37 +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 { migrations } from './migrations'; -import { CANVAS_TYPE } from './common/lib'; - -describe(`${CANVAS_TYPE}`, () => { - describe('7.0.0', () => { - const migrate = doc => migrations[CANVAS_TYPE]['7.0.0'](doc); - - it('does not throw error on empty object', () => { - const migratedDoc = migrate({}); - expect(migratedDoc).toMatchInlineSnapshot(`Object {}`); - }); - - it('removes id from "attributes"', () => { - const migratedDoc = migrate({ - foo: true, - attributes: { - id: '123', - bar: true, - }, - }); - expect(migratedDoc).toMatchInlineSnapshot(` -Object { - "attributes": Object { - "bar": true, - }, - "foo": true, -} -`); - }); - }); -}); diff --git a/x-pack/legacy/plugins/canvas/public/application.tsx b/x-pack/legacy/plugins/canvas/public/application.tsx index 79b3918fef99b..f75b3b427c41b 100644 --- a/x-pack/legacy/plugins/canvas/public/application.tsx +++ b/x-pack/legacy/plugins/canvas/public/application.tsx @@ -104,8 +104,9 @@ export const initializeCanvas = async ( href: getDocumentationLinks().canvas, }, ], - content: domNode => () => { + content: domNode => { ReactDOM.render(, domNode); + return () => ReactDOM.unmountComponentAtNode(domNode); }, }); diff --git a/x-pack/legacy/plugins/canvas/public/components/embeddable_flyout/flyout.tsx b/x-pack/legacy/plugins/canvas/public/components/embeddable_flyout/flyout.tsx index 08cd3084c35cf..4916a27fcbe60 100644 --- a/x-pack/legacy/plugins/canvas/public/components/embeddable_flyout/flyout.tsx +++ b/x-pack/legacy/plugins/canvas/public/components/embeddable_flyout/flyout.tsx @@ -5,7 +5,6 @@ */ import React from 'react'; -import { npStart } from 'ui/new_platform'; import { EuiFlyout, EuiFlyoutHeader, EuiFlyoutBody, EuiTitle } from '@elastic/eui'; import { SavedObjectFinderUi, @@ -13,6 +12,7 @@ import { } from '../../../../../../../src/plugins/saved_objects/public/'; import { ComponentStrings } from '../../../i18n'; import { CoreStart } from '../../../../../../../src/core/public'; +import { CanvasStartDeps } from '../../plugin'; const { AddEmbeddableFlyout: strings } = ComponentStrings; @@ -22,11 +22,12 @@ export interface Props { availableEmbeddables: string[]; savedObjects: CoreStart['savedObjects']; uiSettings: CoreStart['uiSettings']; + getEmbeddableFactories: CanvasStartDeps['embeddable']['getEmbeddableFactories']; } export class AddEmbeddableFlyout extends React.Component { onAddPanel = (id: string, savedObjectType: string, name: string) => { - const embeddableFactories = npStart.plugins.embeddable.getEmbeddableFactories(); + const embeddableFactories = this.props.getEmbeddableFactories(); // Find the embeddable type from the saved object type const found = Array.from(embeddableFactories).find(embeddableFactory => { @@ -42,7 +43,7 @@ export class AddEmbeddableFlyout extends React.Component { }; render() { - const embeddableFactories = npStart.plugins.embeddable.getEmbeddableFactories(); + const embeddableFactories = this.props.getEmbeddableFactories(); const availableSavedObjects = Array.from(embeddableFactories) .filter(factory => { diff --git a/x-pack/legacy/plugins/canvas/public/components/embeddable_flyout/index.tsx b/x-pack/legacy/plugins/canvas/public/components/embeddable_flyout/index.tsx index a86784d374f49..c13cbfd042237 100644 --- a/x-pack/legacy/plugins/canvas/public/components/embeddable_flyout/index.tsx +++ b/x-pack/legacy/plugins/canvas/public/components/embeddable_flyout/index.tsx @@ -105,6 +105,7 @@ export class EmbeddableFlyoutPortal extends React.Component, this.el ); diff --git a/x-pack/legacy/plugins/canvas/public/components/workpad_loader/workpad_loader.js b/x-pack/legacy/plugins/canvas/public/components/workpad_loader/workpad_loader.js index 9b30b3e1ec7ca..30d4ded8571c5 100644 --- a/x-pack/legacy/plugins/canvas/public/components/workpad_loader/workpad_loader.js +++ b/x-pack/legacy/plugins/canvas/public/components/workpad_loader/workpad_loader.js @@ -266,11 +266,17 @@ export class WorkpadLoader extends React.PureComponent { data-test-subj="canvasWorkpadLoaderTable" /> - - - - - + {rows.length > 0 && ( + + + + + + )} ); diff --git a/x-pack/legacy/plugins/canvas/public/components/workpad_templates/workpad_templates.js b/x-pack/legacy/plugins/canvas/public/components/workpad_templates/workpad_templates.js index c80db544bf370..a9a157f5675f8 100644 --- a/x-pack/legacy/plugins/canvas/public/components/workpad_templates/workpad_templates.js +++ b/x-pack/legacy/plugins/canvas/public/components/workpad_templates/workpad_templates.js @@ -113,11 +113,13 @@ export class WorkpadTemplates extends React.PureComponent { className="canvasWorkpad__dropzoneTable canvasWorkpad__dropzoneTable--tags" /> - - - - - + {rows.length > 0 && ( + + + + + + )} ); }; diff --git a/x-pack/legacy/plugins/canvas/public/legacy.ts b/x-pack/legacy/plugins/canvas/public/legacy.ts index a6caa1985325e..4af7c9b2bd057 100644 --- a/x-pack/legacy/plugins/canvas/public/legacy.ts +++ b/x-pack/legacy/plugins/canvas/public/legacy.ts @@ -26,7 +26,9 @@ const shimSetupPlugins: CanvasSetupDeps = { }; const shimStartPlugins: CanvasStartDeps = { ...npStart.plugins, + embeddable: npStart.plugins.embeddable, expressions: npStart.plugins.expressions, + inspector: npStart.plugins.inspector, uiActions: npStart.plugins.uiActions, __LEGACY: { // ToDo: Copy directly into canvas diff --git a/x-pack/legacy/plugins/canvas/public/plugin.tsx b/x-pack/legacy/plugins/canvas/public/plugin.tsx index d9e5e6b4b084b..3ea3ce625ca71 100644 --- a/x-pack/legacy/plugins/canvas/public/plugin.tsx +++ b/x-pack/legacy/plugins/canvas/public/plugin.tsx @@ -11,6 +11,8 @@ import { initLoadingIndicator } from './lib/loading_indicator'; import { featureCatalogueEntry } from './feature_catalogue_entry'; import { ExpressionsSetup, ExpressionsStart } from '../../../../../src/plugins/expressions/public'; import { UiActionsStart } from '../../../../../src/plugins/ui_actions/public'; +import { EmbeddableStart } from '../../../../../src/plugins/embeddable/public'; +import { Start as InspectorStart } from '../../../../../src/plugins/inspector/public'; // @ts-ignore untyped local import { argTypeSpecs } from './expression_types/arg_types'; import { transitions } from './transitions'; @@ -31,7 +33,9 @@ export interface CanvasSetupDeps { } export interface CanvasStartDeps { + embeddable: EmbeddableStart; expressions: ExpressionsStart; + inspector: InspectorStart; uiActions: UiActionsStart; __LEGACY: { absoluteToParsedUrl: (url: string, basePath: string) => any; @@ -48,14 +52,19 @@ export interface CanvasStartDeps { // These interfaces are empty for now but will be populate as we need to export // things for other plugins to use at startup or runtime export type CanvasSetup = CanvasApi; -export interface CanvasStart {} // eslint-disable-line @typescript-eslint/no-empty-interface +export type CanvasStart = void; /** @internal */ export class CanvasPlugin implements Plugin { + // TODO: Do we want to completely move canvas_plugin_src into it's own plugin? + private srcPlugin = new CanvasSrcPlugin(); + public setup(core: CoreSetup, plugins: CanvasSetupDeps) { const { api: canvasApi, registries } = getPluginApi(plugins.expressions); + this.srcPlugin.setup(core, { canvas: canvasApi }); + core.application.register({ id: 'canvas', title: 'Canvas App', @@ -84,10 +93,6 @@ export class CanvasPlugin canvasApi.addElements(legacyRegistries.elements.getOriginalFns()); canvasApi.addTypes(legacyRegistries.types.getOriginalFns()); - // TODO: Do we want to completely move canvas_plugin_src into it's own plugin? - const srcPlugin = new CanvasSrcPlugin(); - srcPlugin.setup(core, { canvas: canvasApi }); - // Register core canvas stuff canvasApi.addFunctions(initFunctions({ typesRegistry: plugins.expressions.__LEGACY.types })); canvasApi.addArgumentUIs(argTypeSpecs); @@ -99,8 +104,7 @@ export class CanvasPlugin } public start(core: CoreStart, plugins: CanvasStartDeps) { + this.srcPlugin.start(core, plugins); initLoadingIndicator(core.http.addLoadingCountSource); - - return {}; } } diff --git a/x-pack/legacy/plugins/canvas/scripts/jest.js b/x-pack/legacy/plugins/canvas/scripts/jest.js index cce1b8d355846..133f775c7192f 100644 --- a/x-pack/legacy/plugins/canvas/scripts/jest.js +++ b/x-pack/legacy/plugins/canvas/scripts/jest.js @@ -36,6 +36,14 @@ run( `!${path}/**/__tests__/**/*`, '--collectCoverageFrom', // Ignore coverage on example files `!${path}/**/__examples__/**/*`, + '--collectCoverageFrom', // Ignore flot files + `!${path}/**/flot-charts/**`, + '--collectCoverageFrom', // Ignore coverage files + `!${path}/**/coverage/**`, + '--collectCoverageFrom', // Ignore scripts + `!${path}/**/scripts/**`, + '--collectCoverageFrom', // Ignore mock files + `!${path}/**/mocks/**`, '--collectCoverageFrom', // Include JS files `${path}/**/*.js`, '--collectCoverageFrom', // Include TS/X files @@ -76,7 +84,7 @@ run( --all Runs all tests and snapshots. Slower. --storybook Runs Storybook Snapshot tests only. --update Updates Storybook Snapshot tests. - --path Runs any tests at a given path. + --path Runs any tests at a given path. --coverage Collect coverage statistics. `, }, diff --git a/x-pack/legacy/plugins/canvas/server/mappings.ts b/x-pack/legacy/plugins/canvas/server/mappings.ts deleted file mode 100644 index bf2be51882b1a..0000000000000 --- a/x-pack/legacy/plugins/canvas/server/mappings.ts +++ /dev/null @@ -1,44 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -// @ts-ignore converting /libs/constants to TS breaks CI -import { CANVAS_TYPE, CUSTOM_ELEMENT_TYPE } from '../common/lib/constants'; - -export const mappings = { - [CANVAS_TYPE]: { - dynamic: false, - properties: { - name: { - type: 'text', - fields: { - keyword: { - type: 'keyword', - }, - }, - }, - '@timestamp': { type: 'date' }, - '@created': { type: 'date' }, - }, - }, - [CUSTOM_ELEMENT_TYPE]: { - dynamic: false, - properties: { - name: { - type: 'text', - fields: { - keyword: { - type: 'keyword', - }, - }, - }, - help: { type: 'text' }, - content: { type: 'text' }, - image: { type: 'text' }, - '@timestamp': { type: 'date' }, - '@created': { type: 'date' }, - }, - }, -}; diff --git a/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/services/api.js b/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/services/api.js index b50c36aa8df9f..24bc7e17356e2 100644 --- a/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/services/api.js +++ b/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/services/api.js @@ -145,9 +145,35 @@ export const updateFollowerIndex = (id, followerIndex) => { if (isUsingAdvancedSettings) { uiMetrics.push(UIM_FOLLOWER_INDEX_USE_ADVANCED_OPTIONS); } + + const { + maxReadRequestOperationCount, + maxOutstandingReadRequests, + maxReadRequestSize, + maxWriteRequestOperationCount, + maxWriteRequestSize, + maxOutstandingWriteRequests, + maxWriteBufferCount, + maxWriteBufferSize, + maxRetryDelay, + readPollTimeout, + } = followerIndex; + const request = httpClient.put(`${API_BASE_PATH}/follower_indices/${encodeURIComponent(id)}`, { - body: JSON.stringify(followerIndex), + body: JSON.stringify({ + maxReadRequestOperationCount, + maxOutstandingReadRequests, + maxReadRequestSize, + maxWriteRequestOperationCount, + maxWriteRequestSize, + maxOutstandingWriteRequests, + maxWriteBufferCount, + maxWriteBufferSize, + maxRetryDelay, + readPollTimeout, + }), }); + return trackUserRequest(request, uiMetrics); }; diff --git a/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/extend_index_management.ts b/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/extend_index_management.ts index 01c6250383fb8..4ffe0db4e3c4e 100644 --- a/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/extend_index_management.ts +++ b/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/extend_index_management.ts @@ -6,7 +6,7 @@ import { i18n } from '@kbn/i18n'; import { get } from 'lodash'; -import { IndexMgmtSetup } from '../../../../../plugins/index_management/public'; +import { IndexManagementPluginSetup } from '../../../../../plugins/index_management/public'; const propertyPath = 'isFollowerIndex'; @@ -21,7 +21,7 @@ const followerBadgeExtension = { filterExpression: 'isFollowerIndex:true', }; -export const extendIndexManagement = (indexManagement?: IndexMgmtSetup) => { +export const extendIndexManagement = (indexManagement?: IndexManagementPluginSetup) => { if (indexManagement) { indexManagement.extensionsService.addBadge(followerBadgeExtension); } diff --git a/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/plugin.ts b/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/plugin.ts index f7651cbb210a7..46259c698b282 100644 --- a/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/plugin.ts +++ b/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/plugin.ts @@ -11,7 +11,7 @@ import { DocLinksStart, } from 'src/core/public'; -import { IndexMgmtSetup } from '../../../../../plugins/index_management/public'; +import { IndexManagementPluginSetup } from '../../../../../plugins/index_management/public'; // @ts-ignore; import { setHttpClient } from './app/services/api'; @@ -21,7 +21,7 @@ import { setNotifications } from './app/services/notifications'; import { extendIndexManagement } from './extend_index_management'; interface PluginDependencies { - indexManagement: IndexMgmtSetup; + indexManagement: IndexManagementPluginSetup; __LEGACY: { chrome: any; MANAGEMENT_BREADCRUMB: ChromeBreadcrumb; diff --git a/x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/plugin.ts b/x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/plugin.ts index 1012c07af3d2a..829de10ad0177 100644 --- a/x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/plugin.ts +++ b/x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/plugin.ts @@ -6,7 +6,7 @@ import { Plugin, PluginInitializerContext, CoreSetup } from 'src/core/server'; -import { IndexMgmtSetup } from '../../../../../plugins/index_management/server'; +import { IndexManagementPluginSetup } from '../../../../../plugins/index_management/server'; // @ts-ignore import { registerLicenseChecker } from './lib/register_license_checker'; @@ -15,7 +15,7 @@ import { registerRoutes } from './routes/register_routes'; import { ccrDataEnricher } from './cross_cluster_replication_data'; interface PluginDependencies { - indexManagement: IndexMgmtSetup; + indexManagement: IndexManagementPluginSetup; __LEGACY: { server: any; ccrUIEnabled: boolean; diff --git a/x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/routes/api/follower_index.ts b/x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/routes/api/follower_index.ts index 3896e1c02c915..1d7dacf4a8688 100644 --- a/x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/routes/api/follower_index.ts +++ b/x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/routes/api/follower_index.ts @@ -164,6 +164,18 @@ export const registerFollowerIndexRoutes = ({ router, __LEGACY }: RouteDependenc path: `${API_BASE_PATH}/follower_indices/{id}`, validate: { params: schema.object({ id: schema.string() }), + body: schema.object({ + maxReadRequestOperationCount: schema.maybe(schema.number()), + maxOutstandingReadRequests: schema.maybe(schema.number()), + maxReadRequestSize: schema.maybe(schema.string()), // byte value + maxWriteRequestOperationCount: schema.maybe(schema.number()), + maxWriteRequestSize: schema.maybe(schema.string()), // byte value + maxOutstandingWriteRequests: schema.maybe(schema.number()), + maxWriteBufferCount: schema.maybe(schema.number()), + maxWriteBufferSize: schema.maybe(schema.string()), // byte value + maxRetryDelay: schema.maybe(schema.string()), // time value + readPollTimeout: schema.maybe(schema.string()), // time value + }), }, }, licensePreRoutingFactory({ diff --git a/x-pack/legacy/plugins/index_lifecycle_management/common/constants/index.ts b/x-pack/legacy/plugins/index_lifecycle_management/common/constants/index.ts deleted file mode 100644 index 9193efb561a0f..0000000000000 --- a/x-pack/legacy/plugins/index_lifecycle_management/common/constants/index.ts +++ /dev/null @@ -1,16 +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 { i18n } from '@kbn/i18n'; - -export const PLUGIN = { - ID: 'index_lifecycle_management', - TITLE: i18n.translate('xpack.indexLifecycleMgmt.appTitle', { - defaultMessage: 'Index Lifecycle Policies', - }), -}; - -export const BASE_PATH = '/management/elasticsearch/index_lifecycle_management/'; diff --git a/x-pack/legacy/plugins/index_lifecycle_management/index.ts b/x-pack/legacy/plugins/index_lifecycle_management/index.ts deleted file mode 100644 index 9b14b7143bf44..0000000000000 --- a/x-pack/legacy/plugins/index_lifecycle_management/index.ts +++ /dev/null @@ -1,62 +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 { Legacy } from 'kibana'; -import { resolve } from 'path'; -import { PLUGIN } from './common/constants'; -import { Plugin as IndexLifecycleManagementPlugin } from './plugin'; -import { createShim } from './shim'; - -export function indexLifecycleManagement(kibana: any) { - return new kibana.Plugin({ - id: PLUGIN.ID, - configPrefix: 'xpack.ilm', - publicDir: resolve(__dirname, 'public'), - require: ['kibana', 'elasticsearch', 'xpack_main', 'index_management'], - uiExports: { - styleSheetPaths: resolve(__dirname, 'public/np_ready/application/index.scss'), - managementSections: ['plugins/index_lifecycle_management/legacy'], - injectDefaultVars(server: Legacy.Server) { - const config = server.config(); - return { - ilmUiEnabled: config.get('xpack.ilm.ui.enabled'), - }; - }, - }, - config: (Joi: any) => { - return Joi.object({ - // display menu item - ui: Joi.object({ - enabled: Joi.boolean().default(true), - }).default(), - - // enable plugin - enabled: Joi.boolean().default(true), - - filteredNodeAttributes: Joi.array() - .items(Joi.string()) - .default([]), - }).default(); - }, - isEnabled(config: any) { - return ( - config.get('xpack.ilm.enabled') && - config.has('xpack.index_management.enabled') && - config.get('xpack.index_management.enabled') - ); - }, - init(server: Legacy.Server) { - const core = server.newPlatform.setup.core; - const plugins = {}; - const __LEGACY = createShim(server); - - const indexLifecycleManagementPlugin = new IndexLifecycleManagementPlugin(); - - // Set up plugin. - indexLifecycleManagementPlugin.setup(core, plugins, __LEGACY); - }, - }); -} diff --git a/x-pack/legacy/plugins/index_lifecycle_management/plugin.ts b/x-pack/legacy/plugins/index_lifecycle_management/plugin.ts deleted file mode 100644 index 38d1bea45ce07..0000000000000 --- a/x-pack/legacy/plugins/index_lifecycle_management/plugin.ts +++ /dev/null @@ -1,54 +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 { CoreSetup } from 'kibana/server'; -import { LegacySetup } from './shim'; -import { registerLicenseChecker } from './server/lib/register_license_checker'; -import { registerIndexRoutes } from './server/routes/api/index'; -import { registerNodesRoutes } from './server/routes/api/nodes'; -import { registerPoliciesRoutes } from './server/routes/api/policies'; -import { registerTemplatesRoutes } from './server/routes/api/templates'; - -const indexLifecycleDataEnricher = async (indicesList: any, callWithRequest: any) => { - if (!indicesList || !indicesList.length) { - return; - } - const params = { - path: '/*/_ilm/explain', - method: 'GET', - }; - const { indices: ilmIndicesData } = await callWithRequest('transport.request', params); - return indicesList.map((index: any): any => { - return { - ...index, - ilm: { ...(ilmIndicesData[index.name] || {}) }, - }; - }); -}; - -export class Plugin { - public setup(core: CoreSetup, plugins: any, __LEGACY: LegacySetup): void { - const { server } = __LEGACY; - - registerLicenseChecker(server); - - // Register routes. - registerIndexRoutes(server); - registerNodesRoutes(server); - registerPoliciesRoutes(server); - registerTemplatesRoutes(server); - - const serverPlugins = server.newPlatform.setup.plugins as any; - - if ( - server.config().get('xpack.ilm.ui.enabled') && - serverPlugins.indexManagement && - serverPlugins.indexManagement.indexDataEnricher - ) { - serverPlugins.indexManagement.indexDataEnricher.add(indexLifecycleDataEnricher); - } - } -} diff --git a/x-pack/legacy/plugins/index_lifecycle_management/public/legacy.ts b/x-pack/legacy/plugins/index_lifecycle_management/public/legacy.ts deleted file mode 100644 index 006e5f6098f2b..0000000000000 --- a/x-pack/legacy/plugins/index_lifecycle_management/public/legacy.ts +++ /dev/null @@ -1,109 +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 { App } from 'src/core/public'; - -/* Legacy Imports */ -import { npSetup, npStart } from 'ui/new_platform'; -import chrome from 'ui/chrome'; -import routes from 'ui/routes'; -import { management } from 'ui/management'; -import { createUiStatsReporter } from '../../../../../src/legacy/core_plugins/ui_metric/public'; - -import { PLUGIN, BASE_PATH } from '../common/constants'; -import { createPlugin } from './np_ready'; -import { addAllExtensions } from './np_ready/extend_index_management'; - -if (chrome.getInjected('ilmUiEnabled')) { - // We have to initialize this outside of the NP lifecycle, otherwise these extensions won't - // be available in Index Management unless the user visits ILM first. - if ((npSetup.plugins as any).indexManagement) { - addAllExtensions((npSetup.plugins as any).indexManagement.extensionsService); - } - - // This method handles the cleanup needed when route is scope is destroyed. It also prevents Angular - // from destroying scope when route changes and both old route and new route are this same route. - const manageAngularLifecycle = ($scope: any, $route: any, unmount: () => void) => { - const lastRoute = $route.current; - const deregister = $scope.$on('$locationChangeSuccess', () => { - const currentRoute = $route.current; - // if templates are the same we are on the same route - if (lastRoute.$$route.template === currentRoute.$$route.template) { - // this prevents angular from destroying scope - $route.current = lastRoute; - } - }); - $scope.$on('$destroy', () => { - if (deregister) { - deregister(); - } - unmount(); - }); - }; - - // Once this app no longer depends upon Angular's routing (e.g. for the "redirect" service), we can - // use the Management plugin's API to register this app within the Elasticsearch section. - const esSection = management.getSection('elasticsearch'); - esSection.register('index_lifecycle_policies', { - visible: true, - display: PLUGIN.TITLE, - order: 2, - url: `#${BASE_PATH}policies`, - }); - - const REACT_ROOT_ID = 'indexLifecycleManagementReactRoot'; - - const template = ` -
- - `; - - routes.when(`${BASE_PATH}:view?/:action?/:id?`, { - template, - controllerAs: 'indexLifecycleManagement', - controller: class IndexLifecycleManagementController { - constructor($scope: any, $route: any, kbnUrl: any, $rootScope: any) { - $scope.$$postDigest(() => { - const element = document.getElementById(REACT_ROOT_ID)!; - const { core } = npSetup; - - const coreDependencies = { - ...core, - application: { - ...core.application, - async register(app: App) { - const unmountApp = await app.mount({ ...npStart } as any, { - element, - appBasePath: '', - onAppLeave: () => undefined, - // TODO: adapt to use Core's ScopedHistory - history: {} as any, - }); - manageAngularLifecycle($scope, $route, unmountApp as any); - }, - }, - }; - - // The Plugin interface won't allow us to pass __LEGACY as a third argument, so we'll just - // sneak it inside of the plugins argument for now. - const pluginDependencies = { - __LEGACY: { - redirect: (path: string) => { - $scope.$evalAsync(() => { - kbnUrl.redirect(path); - }); - }, - createUiStatsReporter, - }, - }; - - const plugin = createPlugin({} as any); - plugin.setup(coreDependencies, pluginDependencies); - }); - } - } as any, - } as any); -} diff --git a/x-pack/legacy/plugins/index_lifecycle_management/public/np_ready/application/_index_lifecycle_management.scss b/x-pack/legacy/plugins/index_lifecycle_management/public/np_ready/application/_index_lifecycle_management.scss deleted file mode 100644 index 96c6d1a938c61..0000000000000 --- a/x-pack/legacy/plugins/index_lifecycle_management/public/np_ready/application/_index_lifecycle_management.scss +++ /dev/null @@ -1,17 +0,0 @@ -.policyTable__horizontalScrollContainer { - overflow-x: auto; - max-width: 100%; -} - -.policyTable__horizontalScroll { - min-width: 800px; - width: 100%; -} - -.policyTable__link { - font-weight: 400; -} - -.ilmEditPolicyPageContent { - max-width: 1200px !important; -} diff --git a/x-pack/legacy/plugins/index_lifecycle_management/public/np_ready/application/app.tsx b/x-pack/legacy/plugins/index_lifecycle_management/public/np_ready/application/app.tsx deleted file mode 100644 index 6738d7caa4444..0000000000000 --- a/x-pack/legacy/plugins/index_lifecycle_management/public/np_ready/application/app.tsx +++ /dev/null @@ -1,29 +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, { useEffect } from 'react'; -import { HashRouter, Switch, Route, Redirect } from 'react-router-dom'; -import { METRIC_TYPE } from '@kbn/analytics'; - -import { BASE_PATH } from '../../../common/constants'; -import { UIM_APP_LOAD } from './constants'; -import { EditPolicy } from './sections/edit_policy'; -import { PolicyTable } from './sections/policy_table'; -import { trackUiMetric } from './services/ui_metric'; - -export const App = () => { - useEffect(() => trackUiMetric(METRIC_TYPE.LOADED, UIM_APP_LOAD), []); - - return ( - - - - - - - - ); -}; diff --git a/x-pack/legacy/plugins/index_lifecycle_management/public/np_ready/application/index.scss b/x-pack/legacy/plugins/index_lifecycle_management/public/np_ready/application/index.scss deleted file mode 100644 index 53e90e2aae35b..0000000000000 --- a/x-pack/legacy/plugins/index_lifecycle_management/public/np_ready/application/index.scss +++ /dev/null @@ -1,3 +0,0 @@ -// Import the EUI global scope so we can use EUI constants -@import 'src/legacy/ui/public/styles/_styling_constants'; -@import 'index_lifecycle_management'; \ No newline at end of file diff --git a/x-pack/legacy/plugins/index_lifecycle_management/public/np_ready/application/index.tsx b/x-pack/legacy/plugins/index_lifecycle_management/public/np_ready/application/index.tsx deleted file mode 100644 index b87a633d65c9c..0000000000000 --- a/x-pack/legacy/plugins/index_lifecycle_management/public/np_ready/application/index.tsx +++ /dev/null @@ -1,63 +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 from 'react'; -import { render, unmountComponentAtNode } from 'react-dom'; -import { Provider } from 'react-redux'; -import { DocLinksStart, ToastsSetup, HttpSetup, FatalErrorsSetup } from 'src/core/public'; - -import { App } from './app'; -import { indexLifecycleManagementStore } from './store'; -import { init as initHttp } from './services/http'; -import { init as initNavigation } from './services/navigation'; -import { init as initDocumentation } from './services/documentation'; -import { init as initUiMetric } from './services/ui_metric'; -import { init as initNotification } from './services/notification'; - -export interface LegacySetup { - redirect: any; - createUiStatsReporter: any; -} - -interface AppDependencies { - legacy: LegacySetup; - I18nContext: any; - http: HttpSetup; - toasts: ToastsSetup; - fatalErrors: FatalErrorsSetup; - docLinks: DocLinksStart; - element: HTMLElement; -} - -export const renderApp = (appDependencies: AppDependencies) => { - const { - legacy: { redirect, createUiStatsReporter }, - I18nContext, - http, - toasts, - fatalErrors, - docLinks: { ELASTIC_WEBSITE_URL, DOC_LINK_VERSION }, - element, - } = appDependencies; - - // Initialize services - initHttp(http); - initNavigation(redirect); - initDocumentation(`${ELASTIC_WEBSITE_URL}guide/en/elasticsearch/reference/${DOC_LINK_VERSION}/`); - initUiMetric(createUiStatsReporter); - initNotification(toasts, fatalErrors); - - render( - - - - - , - element - ); - - return () => unmountComponentAtNode(element); -}; diff --git a/x-pack/legacy/plugins/index_lifecycle_management/public/np_ready/application/services/api.js b/x-pack/legacy/plugins/index_lifecycle_management/public/np_ready/application/services/api.js deleted file mode 100644 index f13bbcb6162b8..0000000000000 --- a/x-pack/legacy/plugins/index_lifecycle_management/public/np_ready/application/services/api.js +++ /dev/null @@ -1,81 +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 { - UIM_POLICY_DELETE, - UIM_POLICY_ATTACH_INDEX, - UIM_POLICY_ATTACH_INDEX_TEMPLATE, - UIM_POLICY_DETACH_INDEX, - UIM_INDEX_RETRY_STEP, -} from '../constants'; - -import { trackUiMetric } from './ui_metric'; -import { sendGet, sendPost, sendDelete } from './http'; - -// The extend_index_management module that we support an injected httpClient here. - -export async function loadNodes(httpClient) { - return await sendGet(`nodes/list`, httpClient); -} - -export async function loadNodeDetails(selectedNodeAttrs, httpClient) { - return await sendGet(`nodes/${selectedNodeAttrs}/details`, httpClient); -} - -export async function loadIndexTemplates(httpClient) { - return await sendGet(`templates`, httpClient); -} - -export async function loadIndexTemplate(templateName, httpClient) { - if (!templateName) { - return {}; - } - return await sendGet(`templates/${templateName}`, httpClient); -} - -export async function loadPolicies(withIndices, httpClient) { - const query = withIndices ? '?withIndices=true' : ''; - return await sendGet('policies', query, httpClient); -} - -export async function savePolicy(policy, httpClient) { - return await sendPost(`policies`, policy, httpClient); -} - -export async function deletePolicy(policyName, httpClient) { - const response = await sendDelete(`policies/${encodeURIComponent(policyName)}`, httpClient); - // Only track successful actions. - trackUiMetric('count', UIM_POLICY_DELETE); - return response; -} - -export const retryLifecycleForIndex = async (indexNames, httpClient) => { - const response = await sendPost(`index/retry`, { indexNames }, httpClient); - // Only track successful actions. - trackUiMetric('count', UIM_INDEX_RETRY_STEP); - return response; -}; - -export const removeLifecycleForIndex = async (indexNames, httpClient) => { - const response = await sendPost(`index/remove`, { indexNames }, httpClient); - // Only track successful actions. - trackUiMetric('count', UIM_POLICY_DETACH_INDEX); - return response; -}; - -export const addLifecyclePolicyToIndex = async (body, httpClient) => { - const response = await sendPost(`index/add`, body, httpClient); - // Only track successful actions. - trackUiMetric('count', UIM_POLICY_ATTACH_INDEX); - return response; -}; - -export const addLifecyclePolicyToTemplate = async (body, httpClient) => { - const response = await sendPost(`template`, body, httpClient); - // Only track successful actions. - trackUiMetric('count', UIM_POLICY_ATTACH_INDEX_TEMPLATE); - return response; -}; diff --git a/x-pack/legacy/plugins/index_lifecycle_management/public/np_ready/application/services/http.ts b/x-pack/legacy/plugins/index_lifecycle_management/public/np_ready/application/services/http.ts deleted file mode 100644 index bbda1ebd2e0e5..0000000000000 --- a/x-pack/legacy/plugins/index_lifecycle_management/public/np_ready/application/services/http.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. - */ - -let _httpClient: any; - -export function init(httpClient: any): void { - _httpClient = httpClient; -} - -function getFullPath(path: string): string { - const apiPrefix = '/api/index_lifecycle_management'; - - if (path) { - return `${apiPrefix}/${path}`; - } - - return apiPrefix; -} - -// The extend_index_management module requires that we support an injected httpClient here. - -export function sendPost(path: string, payload: any, httpClient = _httpClient): any { - return httpClient.post(getFullPath(path), { body: JSON.stringify(payload) }); -} - -export function sendGet(path: string, query: any, httpClient = _httpClient): any { - return httpClient.get(getFullPath(path), { query }); -} - -export function sendDelete(path: string, httpClient = _httpClient): any { - return httpClient.delete(getFullPath(path)); -} diff --git a/x-pack/legacy/plugins/index_lifecycle_management/public/np_ready/application/services/navigation.ts b/x-pack/legacy/plugins/index_lifecycle_management/public/np_ready/application/services/navigation.ts deleted file mode 100644 index 943f9a49d0ab6..0000000000000 --- a/x-pack/legacy/plugins/index_lifecycle_management/public/np_ready/application/services/navigation.ts +++ /dev/null @@ -1,23 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { BASE_PATH } from '../../../../common/constants'; - -// This depends upon Angular, which is why we use this provider pattern to access it within -// our React app. -let _redirect: any; - -export function init(redirect: any) { - _redirect = redirect; -} - -export const goToPolicyList = () => { - _redirect(`${BASE_PATH}policies`); -}; - -export const getPolicyPath = (policyName: string): string => { - return encodeURI(`#${BASE_PATH}policies/edit/${encodeURIComponent(policyName)}`); -}; diff --git a/x-pack/legacy/plugins/index_lifecycle_management/public/np_ready/extend_index_management/index.js b/x-pack/legacy/plugins/index_lifecycle_management/public/np_ready/extend_index_management/index.js deleted file mode 100644 index 69658d31695bc..0000000000000 --- a/x-pack/legacy/plugins/index_lifecycle_management/public/np_ready/extend_index_management/index.js +++ /dev/null @@ -1,248 +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 from 'react'; -import { get, every, any } from 'lodash'; -import { i18n } from '@kbn/i18n'; -import { EuiSearchBar } from '@elastic/eui'; - -import { init as initUiMetric } from '../application/services/ui_metric'; -import { init as initNotification } from '../application/services/notification'; -import { retryLifecycleForIndex } from '../application/services/api'; -import { IndexLifecycleSummary } from './components/index_lifecycle_summary'; -import { AddLifecyclePolicyConfirmModal } from './components/add_lifecycle_confirm_modal'; -import { RemoveLifecyclePolicyConfirmModal } from './components/remove_lifecycle_confirm_modal'; - -const stepPath = 'ilm.step'; - -export const retryLifecycleActionExtension = ({ - indices, - usageCollection, - toasts, - fatalErrors, -}) => { - // These are hacks that we can remove once the New Platform migration is done. They're needed here - // because API requests and API errors require them. - const getLegacyReporter = appName => (type, name) => { - usageCollection.reportUiStats(appName, type, name); - }; - - initUiMetric(getLegacyReporter); - initNotification(toasts, fatalErrors); - - const allHaveErrors = every(indices, index => { - return index.ilm && index.ilm.failed_step; - }); - if (!allHaveErrors) { - return null; - } - const indexNames = indices.map(({ name }) => name); - return { - requestMethod: retryLifecycleForIndex, - icon: 'play', - indexNames: [indexNames], - buttonLabel: i18n.translate('xpack.indexLifecycleMgmt.retryIndexLifecycleActionButtonLabel', { - defaultMessage: 'Retry lifecycle step', - }), - successMessage: i18n.translate( - 'xpack.indexLifecycleMgmt.retryIndexLifecycleAction.retriedLifecycleMessage', - { - defaultMessage: 'Called retry lifecycle step for: {indexNames}', - values: { indexNames: indexNames.map(indexName => `"${indexName}"`).join(', ') }, - } - ), - }; -}; - -export const removeLifecyclePolicyActionExtension = ({ - indices, - reloadIndices, - createUiStatsReporter, - toasts, - fatalErrors, - httpClient, -}) => { - // These are hacks that we can remove once the New Platform migration is done. They're needed here - // because API requests and API errors require them. - initUiMetric(createUiStatsReporter); - initNotification(toasts, fatalErrors); - - const allHaveIlm = every(indices, index => { - return index.ilm && index.ilm.managed; - }); - if (!allHaveIlm) { - return null; - } - const indexNames = indices.map(({ name }) => name); - return { - renderConfirmModal: closeModal => { - return ( - - ); - }, - icon: 'stopFilled', - indexNames: [indexNames], - buttonLabel: i18n.translate('xpack.indexLifecycleMgmt.removeIndexLifecycleActionButtonLabel', { - defaultMessage: 'Remove lifecycle policy', - }), - }; -}; - -export const addLifecyclePolicyActionExtension = ({ - indices, - reloadIndices, - createUiStatsReporter, - toasts, - fatalErrors, - httpClient, -}) => { - // These are hacks that we can remove once the New Platform migration is done. They're needed here - // because API requests and API errors require them. - initUiMetric(createUiStatsReporter); - initNotification(toasts, fatalErrors); - - if (indices.length !== 1) { - return null; - } - const index = indices[0]; - const hasIlm = index.ilm && index.ilm.managed; - - if (hasIlm) { - return null; - } - const indexName = index.name; - return { - renderConfirmModal: closeModal => { - return ( - - ); - }, - icon: 'plusInCircle', - buttonLabel: i18n.translate('xpack.indexLifecycleMgmt.addLifecyclePolicyActionButtonLabel', { - defaultMessage: 'Add lifecycle policy', - }), - }; -}; - -export const ilmBannerExtension = indices => { - const { Query } = EuiSearchBar; - if (!indices.length) { - return null; - } - const indicesWithLifecycleErrors = indices.filter(index => { - return get(index, stepPath) === 'ERROR'; - }); - const numIndicesWithLifecycleErrors = indicesWithLifecycleErrors.length; - if (!numIndicesWithLifecycleErrors) { - return null; - } - return { - type: 'warning', - filter: Query.parse(`${stepPath}:ERROR`), - filterLabel: i18n.translate('xpack.indexLifecycleMgmt.indexMgmtBanner.filterLabel', { - defaultMessage: 'Show errors', - }), - title: i18n.translate('xpack.indexLifecycleMgmt.indexMgmtBanner.errorMessage', { - defaultMessage: `{ numIndicesWithLifecycleErrors, number} - {numIndicesWithLifecycleErrors, plural, one {index has} other {indices have} } - lifecycle errors`, - values: { numIndicesWithLifecycleErrors }, - }), - }; -}; - -export const ilmSummaryExtension = index => { - return ; -}; - -export const ilmFilterExtension = indices => { - const hasIlm = any(indices, index => index.ilm && index.ilm.managed); - if (!hasIlm) { - return []; - } else { - return [ - { - type: 'field_value_selection', - name: i18n.translate('xpack.indexLifecycleMgmt.indexMgmtFilter.lifecycleStatusLabel', { - defaultMessage: 'Lifecycle status', - }), - multiSelect: false, - field: 'ilm.managed', - options: [ - { - value: true, - view: i18n.translate('xpack.indexLifecycleMgmt.indexMgmtFilter.managedLabel', { - defaultMessage: 'Managed', - }), - }, - { - value: false, - view: i18n.translate('xpack.indexLifecycleMgmt.indexMgmtFilter.unmanagedLabel', { - defaultMessage: 'Unmanaged', - }), - }, - ], - }, - { - type: 'field_value_selection', - field: 'ilm.phase', - name: i18n.translate('xpack.indexLifecycleMgmt.indexMgmtFilter.lifecyclePhaseLabel', { - defaultMessage: 'Lifecycle phase', - }), - multiSelect: 'or', - options: [ - { - value: 'hot', - view: i18n.translate('xpack.indexLifecycleMgmt.indexMgmtFilter.hotLabel', { - defaultMessage: 'Hot', - }), - }, - { - value: 'warm', - view: i18n.translate('xpack.indexLifecycleMgmt.indexMgmtFilter.warmLabel', { - defaultMessage: 'Warm', - }), - }, - { - value: 'cold', - view: i18n.translate('xpack.indexLifecycleMgmt.indexMgmtFilter.coldLabel', { - defaultMessage: 'Cold', - }), - }, - { - value: 'delete', - view: i18n.translate('xpack.indexLifecycleMgmt.indexMgmtFilter.deleteLabel', { - defaultMessage: 'Delete', - }), - }, - ], - }, - ]; - } -}; - -export const addAllExtensions = extensionsService => { - extensionsService.addAction(retryLifecycleActionExtension); - extensionsService.addAction(removeLifecyclePolicyActionExtension); - extensionsService.addAction(addLifecyclePolicyActionExtension); - - extensionsService.addBanner(ilmBannerExtension); - extensionsService.addSummary(ilmSummaryExtension); - extensionsService.addFilter(ilmFilterExtension); -}; diff --git a/x-pack/legacy/plugins/index_lifecycle_management/public/np_ready/index.ts b/x-pack/legacy/plugins/index_lifecycle_management/public/np_ready/index.ts deleted file mode 100644 index 1af0b697a9283..0000000000000 --- a/x-pack/legacy/plugins/index_lifecycle_management/public/np_ready/index.ts +++ /dev/null @@ -1,10 +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 { PluginInitializerContext } from 'src/core/public'; -import { IndexLifecycleManagementPlugin } from './plugin'; - -export const createPlugin = (ctx: PluginInitializerContext) => new IndexLifecycleManagementPlugin(); diff --git a/x-pack/legacy/plugins/index_lifecycle_management/public/np_ready/plugin.tsx b/x-pack/legacy/plugins/index_lifecycle_management/public/np_ready/plugin.tsx deleted file mode 100644 index e2897f09fa892..0000000000000 --- a/x-pack/legacy/plugins/index_lifecycle_management/public/np_ready/plugin.tsx +++ /dev/null @@ -1,58 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { CoreSetup, CoreStart, Plugin } from 'src/core/public'; -import { PLUGIN } from '../../common/constants'; -import { LegacySetup } from './application'; - -interface PluginsSetup { - __LEGACY: LegacySetup; -} - -export class IndexLifecycleManagementPlugin implements Plugin { - setup(core: CoreSetup, plugins: PluginsSetup) { - // Extract individual core dependencies. - const { - application, - notifications: { toasts }, - fatalErrors, - http, - } = core; - - // The Plugin interface won't allow us to pass __LEGACY as a third argument, so we'll just - // sneak it inside of the plugins parameter for now. - const { __LEGACY } = plugins; - - application.register({ - id: PLUGIN.ID, - title: PLUGIN.TITLE, - async mount(config, mountPoint) { - const { - core: { - docLinks, - i18n: { Context: I18nContext }, - }, - } = config; - - const { element } = mountPoint; - const { renderApp } = await import('./application'); - - // Inject all dependencies into our app. - return renderApp({ - legacy: { ...__LEGACY }, - I18nContext, - http, - toasts, - fatalErrors, - docLinks, - element, - }); - }, - }); - } - start(core: CoreStart, plugins: any) {} - stop() {} -} diff --git a/x-pack/legacy/plugins/index_lifecycle_management/server/lib/call_with_request_factory/call_with_request_factory.ts b/x-pack/legacy/plugins/index_lifecycle_management/server/lib/call_with_request_factory/call_with_request_factory.ts deleted file mode 100644 index 1b28dc4fde4f7..0000000000000 --- a/x-pack/legacy/plugins/index_lifecycle_management/server/lib/call_with_request_factory/call_with_request_factory.ts +++ /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 { once } from 'lodash'; -import { Legacy } from 'kibana'; - -const callWithRequest = once((server: Legacy.Server): any => { - const cluster = server.plugins.elasticsearch.getCluster('data'); - return cluster.callWithRequest; -}); - -export const callWithRequestFactory = (server: Legacy.Server, request: any) => { - return (...args: any[]) => { - return callWithRequest(server)(request, ...args); - }; -}; diff --git a/x-pack/legacy/plugins/index_lifecycle_management/server/lib/call_with_request_factory/index.ts b/x-pack/legacy/plugins/index_lifecycle_management/server/lib/call_with_request_factory/index.ts deleted file mode 100644 index 787814d87dff9..0000000000000 --- a/x-pack/legacy/plugins/index_lifecycle_management/server/lib/call_with_request_factory/index.ts +++ /dev/null @@ -1,7 +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. - */ - -export { callWithRequestFactory } from './call_with_request_factory'; diff --git a/x-pack/legacy/plugins/index_lifecycle_management/server/lib/check_license/__tests__/check_license.js b/x-pack/legacy/plugins/index_lifecycle_management/server/lib/check_license/__tests__/check_license.js deleted file mode 100644 index 933fda01c055d..0000000000000 --- a/x-pack/legacy/plugins/index_lifecycle_management/server/lib/check_license/__tests__/check_license.js +++ /dev/null @@ -1,145 +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 expect from '@kbn/expect'; -import { set } from 'lodash'; -import { checkLicense } from '../check_license'; - -describe('check_license', function() { - let mockLicenseInfo; - beforeEach(() => (mockLicenseInfo = {})); - - describe('license information is undefined', () => { - beforeEach(() => (mockLicenseInfo = undefined)); - - it('should set isAvailable to false', () => { - expect(checkLicense(mockLicenseInfo).isAvailable).to.be(false); - }); - - it('should set showLinks to true', () => { - expect(checkLicense(mockLicenseInfo).showLinks).to.be(true); - }); - - it('should set enableLinks to false', () => { - expect(checkLicense(mockLicenseInfo).enableLinks).to.be(false); - }); - - it('should set a message', () => { - expect(checkLicense(mockLicenseInfo).message).to.not.be(undefined); - }); - }); - - describe('license information is not available', () => { - beforeEach(() => (mockLicenseInfo.isAvailable = () => false)); - - it('should set isAvailable to false', () => { - expect(checkLicense(mockLicenseInfo).isAvailable).to.be(false); - }); - - it('should set showLinks to true', () => { - expect(checkLicense(mockLicenseInfo).showLinks).to.be(true); - }); - - it('should set enableLinks to false', () => { - expect(checkLicense(mockLicenseInfo).enableLinks).to.be(false); - }); - - it('should set a message', () => { - expect(checkLicense(mockLicenseInfo).message).to.not.be(undefined); - }); - }); - - describe('license information is available', () => { - beforeEach(() => { - mockLicenseInfo.isAvailable = () => true; - set(mockLicenseInfo, 'license.getType', () => 'basic'); - }); - - describe('& license is > basic', () => { - beforeEach(() => set(mockLicenseInfo, 'license.isOneOf', () => true)); - - describe('& license is active', () => { - beforeEach(() => set(mockLicenseInfo, 'license.isActive', () => true)); - - it('should set isAvailable to true', () => { - expect(checkLicense(mockLicenseInfo).isAvailable).to.be(true); - }); - - it('should set showLinks to true', () => { - expect(checkLicense(mockLicenseInfo).showLinks).to.be(true); - }); - - it('should set enableLinks to true', () => { - expect(checkLicense(mockLicenseInfo).enableLinks).to.be(true); - }); - - it('should not set a message', () => { - expect(checkLicense(mockLicenseInfo).message).to.be(undefined); - }); - }); - - describe('& license is expired', () => { - beforeEach(() => set(mockLicenseInfo, 'license.isActive', () => false)); - - it('should set isAvailable to false', () => { - expect(checkLicense(mockLicenseInfo).isAvailable).to.be(false); - }); - - it('should set showLinks to true', () => { - expect(checkLicense(mockLicenseInfo).showLinks).to.be(true); - }); - - it('should set enableLinks to false', () => { - expect(checkLicense(mockLicenseInfo).enableLinks).to.be(false); - }); - - it('should set a message', () => { - expect(checkLicense(mockLicenseInfo).message).to.not.be(undefined); - }); - }); - }); - - describe('& license is basic', () => { - beforeEach(() => set(mockLicenseInfo, 'license.isOneOf', () => true)); - - describe('& license is active', () => { - beforeEach(() => set(mockLicenseInfo, 'license.isActive', () => true)); - - it('should set isAvailable to true', () => { - expect(checkLicense(mockLicenseInfo).isAvailable).to.be(true); - }); - - it('should set showLinks to true', () => { - expect(checkLicense(mockLicenseInfo).showLinks).to.be(true); - }); - - it('should set enableLinks to true', () => { - expect(checkLicense(mockLicenseInfo).enableLinks).to.be(true); - }); - - it('should not set a message', () => { - expect(checkLicense(mockLicenseInfo).message).to.be(undefined); - }); - }); - - describe('& license is expired', () => { - beforeEach(() => set(mockLicenseInfo, 'license.isActive', () => false)); - - it('should set isAvailable to false', () => { - expect(checkLicense(mockLicenseInfo).isAvailable).to.be(false); - }); - - it('should set showLinks to true', () => { - expect(checkLicense(mockLicenseInfo).showLinks).to.be(true); - }); - - it('should set a message', () => { - expect(checkLicense(mockLicenseInfo).message).to.not.be(undefined); - }); - }); - }); - }); -}); diff --git a/x-pack/legacy/plugins/index_lifecycle_management/server/lib/check_license/check_license.ts b/x-pack/legacy/plugins/index_lifecycle_management/server/lib/check_license/check_license.ts deleted file mode 100644 index b35ab14964d55..0000000000000 --- a/x-pack/legacy/plugins/index_lifecycle_management/server/lib/check_license/check_license.ts +++ /dev/null @@ -1,66 +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 { i18n } from '@kbn/i18n'; - -export function checkLicense(xpackLicenseInfo: any): any { - const pluginName = 'Index Lifecycle Policies'; - - // If, for some reason, we cannot get the license information - // from Elasticsearch, assume worst case and disable - if (!xpackLicenseInfo || !xpackLicenseInfo.isAvailable()) { - return { - isAvailable: false, - showLinks: true, - enableLinks: false, - message: i18n.translate('xpack.indexLifecycleMgmt.checkLicense.errorUnavailableMessage', { - defaultMessage: - 'You cannot use {pluginName} because license information is not available at this time.', - values: { pluginName }, - }), - }; - } - - const VALID_LICENSE_MODES = ['basic', 'standard', 'gold', 'platinum', 'enterprise', 'trial']; - - const isLicenseModeValid = xpackLicenseInfo.license.isOneOf(VALID_LICENSE_MODES); - const isLicenseActive = xpackLicenseInfo.license.isActive(); - const licenseType = xpackLicenseInfo.license.getType(); - - // License is not valid - if (!isLicenseModeValid) { - return { - isAvailable: false, - showLinks: false, - message: i18n.translate('xpack.indexLifecycleMgmt.checkLicense.errorUnsupportedMessage', { - defaultMessage: - 'Your {licenseType} license does not support {pluginName}. Please upgrade your license.', - values: { licenseType, pluginName }, - }), - }; - } - - // License is valid but not active - if (!isLicenseActive) { - return { - isAvailable: false, - showLinks: true, - enableLinks: false, - message: i18n.translate('xpack.indexLifecycleMgmt.checkLicense.errorExpiredMessage', { - defaultMessage: - 'You cannot use {pluginName} because your {licenseType} license has expired.', - values: { pluginName, licenseType }, - }), - }; - } - - // License is valid and active - return { - isAvailable: true, - showLinks: true, - enableLinks: true, - }; -} diff --git a/x-pack/legacy/plugins/index_lifecycle_management/server/lib/error_wrappers/__tests__/wrap_custom_error.js b/x-pack/legacy/plugins/index_lifecycle_management/server/lib/error_wrappers/__tests__/wrap_custom_error.js deleted file mode 100644 index f9c102be7a1ff..0000000000000 --- a/x-pack/legacy/plugins/index_lifecycle_management/server/lib/error_wrappers/__tests__/wrap_custom_error.js +++ /dev/null @@ -1,21 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import expect from '@kbn/expect'; -import { wrapCustomError } from '../wrap_custom_error'; - -describe('wrap_custom_error', () => { - describe('#wrapCustomError', () => { - it('should return a Boom object', () => { - const originalError = new Error('I am an error'); - const statusCode = 404; - const wrappedError = wrapCustomError(originalError, statusCode); - - expect(wrappedError.isBoom).to.be(true); - expect(wrappedError.output.statusCode).to.equal(statusCode); - }); - }); -}); diff --git a/x-pack/legacy/plugins/index_lifecycle_management/server/lib/error_wrappers/__tests__/wrap_es_error.js b/x-pack/legacy/plugins/index_lifecycle_management/server/lib/error_wrappers/__tests__/wrap_es_error.js deleted file mode 100644 index fe2b6cce652f1..0000000000000 --- a/x-pack/legacy/plugins/index_lifecycle_management/server/lib/error_wrappers/__tests__/wrap_es_error.js +++ /dev/null @@ -1,38 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import expect from '@kbn/expect'; -import { wrapEsError } from '../wrap_es_error'; - -describe('wrap_es_error', () => { - describe('#wrapEsError', () => { - let originalError; - beforeEach(() => { - originalError = new Error('I am an error'); - originalError.statusCode = 404; - }); - - it('should return a Boom object', () => { - const wrappedError = wrapEsError(originalError); - - expect(wrappedError.isBoom).to.be(true); - }); - - it('should return the correct Boom object', () => { - const wrappedError = wrapEsError(originalError); - - expect(wrappedError.output.statusCode).to.be(originalError.statusCode); - expect(wrappedError.output.payload.message).to.be(originalError.message); - }); - - it('should return the correct Boom object with custom message', () => { - const wrappedError = wrapEsError(originalError, { 404: 'No encontrado!' }); - - expect(wrappedError.output.statusCode).to.be(originalError.statusCode); - expect(wrappedError.output.payload.message).to.be('No encontrado!'); - }); - }); -}); diff --git a/x-pack/legacy/plugins/index_lifecycle_management/server/lib/error_wrappers/__tests__/wrap_unknown_error.js b/x-pack/legacy/plugins/index_lifecycle_management/server/lib/error_wrappers/__tests__/wrap_unknown_error.js deleted file mode 100644 index 85e0b2b3033ad..0000000000000 --- a/x-pack/legacy/plugins/index_lifecycle_management/server/lib/error_wrappers/__tests__/wrap_unknown_error.js +++ /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 expect from '@kbn/expect'; -import { wrapUnknownError } from '../wrap_unknown_error'; - -describe('wrap_unknown_error', () => { - describe('#wrapUnknownError', () => { - it('should return a Boom object', () => { - const originalError = new Error('I am an error'); - const wrappedError = wrapUnknownError(originalError); - - expect(wrappedError.isBoom).to.be(true); - }); - }); -}); diff --git a/x-pack/legacy/plugins/index_lifecycle_management/server/lib/error_wrappers/index.ts b/x-pack/legacy/plugins/index_lifecycle_management/server/lib/error_wrappers/index.ts deleted file mode 100644 index f275f15637091..0000000000000 --- a/x-pack/legacy/plugins/index_lifecycle_management/server/lib/error_wrappers/index.ts +++ /dev/null @@ -1,9 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -export { wrapCustomError } from './wrap_custom_error'; -export { wrapEsError } from './wrap_es_error'; -export { wrapUnknownError } from './wrap_unknown_error'; diff --git a/x-pack/legacy/plugins/index_lifecycle_management/server/lib/error_wrappers/wrap_custom_error.ts b/x-pack/legacy/plugins/index_lifecycle_management/server/lib/error_wrappers/wrap_custom_error.ts deleted file mode 100644 index c5780e7c83fb5..0000000000000 --- a/x-pack/legacy/plugins/index_lifecycle_management/server/lib/error_wrappers/wrap_custom_error.ts +++ /dev/null @@ -1,18 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import Boom from 'boom'; - -/** - * Wraps a custom error into a Boom error response and returns it - * - * @param err Object error - * @param statusCode Error status code - * @return Object Boom error response - */ -export function wrapCustomError(err: any, statusCode: any): any { - return Boom.boomify(err, { statusCode }); -} diff --git a/x-pack/legacy/plugins/index_lifecycle_management/server/lib/error_wrappers/wrap_es_error.ts b/x-pack/legacy/plugins/index_lifecycle_management/server/lib/error_wrappers/wrap_es_error.ts deleted file mode 100644 index 6980a5afa5eac..0000000000000 --- a/x-pack/legacy/plugins/index_lifecycle_management/server/lib/error_wrappers/wrap_es_error.ts +++ /dev/null @@ -1,29 +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 Boom from 'boom'; - -/** - * Wraps an error thrown by the ES JS client into a Boom error response and returns it - * - * @param err Object Error thrown by ES JS client - * @param statusCodeToMessageMap Object Optional map of HTTP status codes => error messages - * @return Object Boom error response - */ -export function wrapEsError(err: any, statusCodeToMessageMap: any = {}): any { - const statusCode = err.statusCode; - - // If no custom message if specified for the error's status code, just - // wrap the error as a Boom error response and return it - if (!statusCodeToMessageMap[statusCode]) { - return Boom.boomify(err, { statusCode }); - } - - // Otherwise, use the custom message to create a Boom error response and - // return it - const message = statusCodeToMessageMap[statusCode]; - return new Boom(message, { statusCode }); -} diff --git a/x-pack/legacy/plugins/index_lifecycle_management/server/lib/error_wrappers/wrap_unknown_error.ts b/x-pack/legacy/plugins/index_lifecycle_management/server/lib/error_wrappers/wrap_unknown_error.ts deleted file mode 100644 index ede1baec286f3..0000000000000 --- a/x-pack/legacy/plugins/index_lifecycle_management/server/lib/error_wrappers/wrap_unknown_error.ts +++ /dev/null @@ -1,17 +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 Boom from 'boom'; - -/** - * Wraps an unknown error into a Boom error response and returns it - * - * @param err Object Unknown error - * @return Object Boom error response - */ -export function wrapUnknownError(err: any): any { - return Boom.boomify(err); -} diff --git a/x-pack/legacy/plugins/index_lifecycle_management/server/lib/is_es_error/index.ts b/x-pack/legacy/plugins/index_lifecycle_management/server/lib/is_es_error/index.ts deleted file mode 100644 index a9a3c61472d8c..0000000000000 --- a/x-pack/legacy/plugins/index_lifecycle_management/server/lib/is_es_error/index.ts +++ /dev/null @@ -1,7 +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. - */ - -export { isEsError } from './is_es_error'; diff --git a/x-pack/legacy/plugins/index_lifecycle_management/server/lib/license_pre_routing_factory/__tests__/license_pre_routing_factory.js b/x-pack/legacy/plugins/index_lifecycle_management/server/lib/license_pre_routing_factory/__tests__/license_pre_routing_factory.js deleted file mode 100644 index 4d3b33f8b3af3..0000000000000 --- a/x-pack/legacy/plugins/index_lifecycle_management/server/lib/license_pre_routing_factory/__tests__/license_pre_routing_factory.js +++ /dev/null @@ -1,69 +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 expect from '@kbn/expect'; -import { licensePreRoutingFactory } from '../license_pre_routing_factory'; - -describe('license_pre_routing_factory', () => { - describe('#reportingFeaturePreRoutingFactory', () => { - let mockServer; - let mockLicenseCheckResults; - - beforeEach(() => { - mockServer = { - plugins: { - xpack_main: { - info: { - feature: () => ({ - getLicenseCheckResults: () => mockLicenseCheckResults, - }), - }, - }, - }, - }; - }); - - it('only instantiates one instance per server', () => { - const firstInstance = licensePreRoutingFactory(mockServer); - const secondInstance = licensePreRoutingFactory(mockServer); - - expect(firstInstance).to.be(secondInstance); - }); - - describe('isAvailable is false', () => { - beforeEach(() => { - mockLicenseCheckResults = { - isAvailable: false, - }; - }); - - it('replies with 403', () => { - const licensePreRouting = licensePreRoutingFactory(mockServer); - const stubRequest = {}; - expect(() => licensePreRouting(stubRequest)).to.throwException(response => { - expect(response).to.be.an(Error); - expect(response.isBoom).to.be(true); - expect(response.output.statusCode).to.be(403); - }); - }); - }); - - describe('isAvailable is true', () => { - beforeEach(() => { - mockLicenseCheckResults = { - isAvailable: true, - }; - }); - - it('replies with nothing', () => { - const licensePreRouting = licensePreRoutingFactory(mockServer); - const stubRequest = {}; - const response = licensePreRouting(stubRequest); - expect(response).to.be(null); - }); - }); - }); -}); diff --git a/x-pack/legacy/plugins/index_lifecycle_management/server/lib/license_pre_routing_factory/index.ts b/x-pack/legacy/plugins/index_lifecycle_management/server/lib/license_pre_routing_factory/index.ts deleted file mode 100644 index 0743e443955f4..0000000000000 --- a/x-pack/legacy/plugins/index_lifecycle_management/server/lib/license_pre_routing_factory/index.ts +++ /dev/null @@ -1,7 +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. - */ - -export { licensePreRoutingFactory } from './license_pre_routing_factory'; diff --git a/x-pack/legacy/plugins/index_lifecycle_management/server/lib/license_pre_routing_factory/license_pre_routing_factory.ts b/x-pack/legacy/plugins/index_lifecycle_management/server/lib/license_pre_routing_factory/license_pre_routing_factory.ts deleted file mode 100644 index e348125967c14..0000000000000 --- a/x-pack/legacy/plugins/index_lifecycle_management/server/lib/license_pre_routing_factory/license_pre_routing_factory.ts +++ /dev/null @@ -1,29 +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 { once } from 'lodash'; -import { Legacy } from 'kibana'; - -import { PLUGIN } from '../../../common/constants'; -import { wrapCustomError } from '../error_wrappers'; - -export const licensePreRoutingFactory = once((server: Legacy.Server) => { - const xpackMainPlugin = server.plugins.xpack_main; - - // License checking and enable/disable logic - function licensePreRouting() { - const licenseCheckResults = xpackMainPlugin.info.feature(PLUGIN.ID).getLicenseCheckResults(); - if (!licenseCheckResults.isAvailable) { - const error = new Error(licenseCheckResults.message); - const statusCode = 403; - throw wrapCustomError(error, statusCode); - } - - return null; - } - - return licensePreRouting; -}); diff --git a/x-pack/legacy/plugins/index_lifecycle_management/server/lib/register_license_checker/index.ts b/x-pack/legacy/plugins/index_lifecycle_management/server/lib/register_license_checker/index.ts deleted file mode 100644 index 7b0f97c38d129..0000000000000 --- a/x-pack/legacy/plugins/index_lifecycle_management/server/lib/register_license_checker/index.ts +++ /dev/null @@ -1,7 +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. - */ - -export { registerLicenseChecker } from './register_license_checker'; diff --git a/x-pack/legacy/plugins/index_lifecycle_management/server/lib/register_license_checker/register_license_checker.ts b/x-pack/legacy/plugins/index_lifecycle_management/server/lib/register_license_checker/register_license_checker.ts deleted file mode 100644 index 8e3b89fa20e33..0000000000000 --- a/x-pack/legacy/plugins/index_lifecycle_management/server/lib/register_license_checker/register_license_checker.ts +++ /dev/null @@ -1,23 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { Legacy } from 'kibana'; -// @ts-ignore -import { mirrorPluginStatus } from '../../../../../server/lib/mirror_plugin_status'; -import { PLUGIN } from '../../../common/constants'; -import { checkLicense } from '../check_license'; - -export function registerLicenseChecker(server: Legacy.Server) { - const xpackMainPlugin = server.plugins.xpack_main as any; - const ilmPlugin = (server.plugins as any).index_lifecycle_management; - - mirrorPluginStatus(xpackMainPlugin, ilmPlugin); - xpackMainPlugin.status.once('green', () => { - // Register a function that is called whenever the xpack info changes, - // to re-compute the license check results for this plugin - xpackMainPlugin.info.feature(PLUGIN.ID).registerLicenseCheckResultsGenerator(checkLicense); - }); -} diff --git a/x-pack/legacy/plugins/index_lifecycle_management/server/routes/api/index/index.ts b/x-pack/legacy/plugins/index_lifecycle_management/server/routes/api/index/index.ts deleted file mode 100644 index 82fb2e3b2a372..0000000000000 --- a/x-pack/legacy/plugins/index_lifecycle_management/server/routes/api/index/index.ts +++ /dev/null @@ -1,7 +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. - */ - -export { registerIndexRoutes } from './register_index_routes'; diff --git a/x-pack/legacy/plugins/index_lifecycle_management/server/routes/api/index/register_add_policy_route.ts b/x-pack/legacy/plugins/index_lifecycle_management/server/routes/api/index/register_add_policy_route.ts deleted file mode 100644 index c3e235220931c..0000000000000 --- a/x-pack/legacy/plugins/index_lifecycle_management/server/routes/api/index/register_add_policy_route.ts +++ /dev/null @@ -1,58 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { callWithRequestFactory } from '../../../lib/call_with_request_factory'; -import { isEsError } from '../../../lib/is_es_error'; -import { wrapEsError, wrapUnknownError } from '../../../lib/error_wrappers'; -import { licensePreRoutingFactory } from '../../../lib/license_pre_routing_factory'; - -async function addLifecyclePolicy( - callWithRequest: any, - indexName: string, - policyName: string, - alias: string -) { - const body = { - lifecycle: { - name: policyName, - rollover_alias: alias, - }, - }; - - const params = { - method: 'PUT', - path: `/${encodeURIComponent(indexName)}/_settings`, - body, - }; - - return callWithRequest('transport.request', params); -} - -export function registerAddPolicyRoute(server: any) { - const licensePreRouting = licensePreRoutingFactory(server); - - server.route({ - path: '/api/index_lifecycle_management/index/add', - method: 'POST', - handler: async (request: any) => { - const callWithRequest = callWithRequestFactory(server, request); - const { indexName, policyName, alias } = request.payload as any; - try { - const response = await addLifecyclePolicy(callWithRequest, indexName, policyName, alias); - return response; - } catch (err) { - if (isEsError(err)) { - return wrapEsError(err); - } - - return wrapUnknownError(err); - } - }, - config: { - pre: [licensePreRouting], - }, - }); -} diff --git a/x-pack/legacy/plugins/index_lifecycle_management/server/routes/api/index/register_index_routes.ts b/x-pack/legacy/plugins/index_lifecycle_management/server/routes/api/index/register_index_routes.ts deleted file mode 100644 index 74eb1a86a93ba..0000000000000 --- a/x-pack/legacy/plugins/index_lifecycle_management/server/routes/api/index/register_index_routes.ts +++ /dev/null @@ -1,15 +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 { registerRetryRoute } from './register_retry_route'; -import { registerRemoveRoute } from './register_remove_route'; -import { registerAddPolicyRoute } from './register_add_policy_route'; - -export function registerIndexRoutes(server: any) { - registerRetryRoute(server); - registerRemoveRoute(server); - registerAddPolicyRoute(server); -} diff --git a/x-pack/legacy/plugins/index_lifecycle_management/server/routes/api/index/register_remove_route.ts b/x-pack/legacy/plugins/index_lifecycle_management/server/routes/api/index/register_remove_route.ts deleted file mode 100644 index ed3b5a97a3b42..0000000000000 --- a/x-pack/legacy/plugins/index_lifecycle_management/server/routes/api/index/register_remove_route.ts +++ /dev/null @@ -1,51 +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 { callWithRequestFactory } from '../../../lib/call_with_request_factory'; -import { isEsError } from '../../../lib/is_es_error'; -import { wrapEsError, wrapUnknownError } from '../../../lib/error_wrappers'; -import { licensePreRoutingFactory } from '../../../lib/license_pre_routing_factory'; - -async function removeLifecycle(callWithRequest: any, indexNames: string[]) { - const responses = []; - for (let i = 0; i < indexNames.length; i++) { - const indexName = indexNames[i]; - const params = { - method: 'POST', - path: `/${encodeURIComponent(indexName)}/_ilm/remove`, - ignore: [404], - }; - - responses.push(callWithRequest('transport.request', params)); - } - return Promise.all(responses); -} - -export function registerRemoveRoute(server: any) { - const licensePreRouting = licensePreRoutingFactory(server); - - server.route({ - path: '/api/index_lifecycle_management/index/remove', - method: 'POST', - handler: async (request: any) => { - const callWithRequest = callWithRequestFactory(server, request); - - try { - const response = await removeLifecycle(callWithRequest, request.payload.indexNames); - return response; - } catch (err) { - if (isEsError(err)) { - return wrapEsError(err); - } - - return wrapUnknownError(err); - } - }, - config: { - pre: [licensePreRouting], - }, - }); -} diff --git a/x-pack/legacy/plugins/index_lifecycle_management/server/routes/api/index/register_retry_route.ts b/x-pack/legacy/plugins/index_lifecycle_management/server/routes/api/index/register_retry_route.ts deleted file mode 100644 index 89278edbecea2..0000000000000 --- a/x-pack/legacy/plugins/index_lifecycle_management/server/routes/api/index/register_retry_route.ts +++ /dev/null @@ -1,51 +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 { callWithRequestFactory } from '../../../lib/call_with_request_factory'; -import { isEsError } from '../../../lib/is_es_error'; -import { wrapEsError, wrapUnknownError } from '../../../lib/error_wrappers'; -import { licensePreRoutingFactory } from '../../../lib/license_pre_routing_factory'; - -async function retryLifecycle(callWithRequest: any, indexNames: string[]) { - const responses = []; - for (let i = 0; i < indexNames.length; i++) { - const indexName = indexNames[i]; - const params = { - method: 'POST', - path: `/${encodeURIComponent(indexName)}/_ilm/retry`, - ignore: [404], - }; - - responses.push(callWithRequest('transport.request', params)); - } - return Promise.all(responses); -} - -export function registerRetryRoute(server: any) { - const licensePreRouting = licensePreRoutingFactory(server); - - server.route({ - path: '/api/index_lifecycle_management/index/retry', - method: 'POST', - handler: async (request: any) => { - const callWithRequest = callWithRequestFactory(server, request); - - try { - const response = await retryLifecycle(callWithRequest, request.payload.indexNames); - return response; - } catch (err) { - if (isEsError(err)) { - return wrapEsError(err); - } - - return wrapUnknownError(err); - } - }, - config: { - pre: [licensePreRouting], - }, - }); -} diff --git a/x-pack/legacy/plugins/index_lifecycle_management/server/routes/api/nodes/constants.ts b/x-pack/legacy/plugins/index_lifecycle_management/server/routes/api/nodes/constants.ts deleted file mode 100644 index 4392dacac8fa4..0000000000000 --- a/x-pack/legacy/plugins/index_lifecycle_management/server/routes/api/nodes/constants.ts +++ /dev/null @@ -1,15 +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. - */ - -export const NODE_ATTRS_KEYS_TO_IGNORE: string[] = [ - 'ml.enabled', - 'ml.machine_memory', - 'ml.max_open_jobs', - // Used by ML to identify nodes that have transform enabled: - // https://github.com/elastic/elasticsearch/pull/52712/files#diff-225cc2c1291b4c60a8c3412a619094e1R147 - 'transform.node', - 'xpack.installed', -]; diff --git a/x-pack/legacy/plugins/index_lifecycle_management/server/routes/api/nodes/index.ts b/x-pack/legacy/plugins/index_lifecycle_management/server/routes/api/nodes/index.ts deleted file mode 100644 index ef0ac271ae60e..0000000000000 --- a/x-pack/legacy/plugins/index_lifecycle_management/server/routes/api/nodes/index.ts +++ /dev/null @@ -1,7 +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. - */ - -export { registerNodesRoutes } from './register_nodes_routes'; diff --git a/x-pack/legacy/plugins/index_lifecycle_management/server/routes/api/nodes/register_details_route.ts b/x-pack/legacy/plugins/index_lifecycle_management/server/routes/api/nodes/register_details_route.ts deleted file mode 100644 index c2c3f8bf07028..0000000000000 --- a/x-pack/legacy/plugins/index_lifecycle_management/server/routes/api/nodes/register_details_route.ts +++ /dev/null @@ -1,61 +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 { callWithRequestFactory } from '../../../lib/call_with_request_factory'; -import { isEsError } from '../../../lib/is_es_error'; -import { wrapEsError, wrapUnknownError } from '../../../lib/error_wrappers'; -import { licensePreRoutingFactory } from '../../../lib/license_pre_routing_factory'; - -function findMatchingNodes(stats: any, nodeAttrs: string): any { - return Object.entries(stats.nodes).reduce((accum: any[], [nodeId, nodeStats]: [any, any]) => { - const attributes = nodeStats.attributes || {}; - for (const [key, value] of Object.entries(attributes)) { - if (`${key}:${value}` === nodeAttrs) { - accum.push({ - nodeId, - stats: nodeStats, - }); - break; - } - } - return accum; - }, []); -} - -async function fetchNodeStats(callWithRequest: any): Promise { - const params = { - format: 'json', - }; - - return await callWithRequest('nodes.stats', params); -} - -export function registerDetailsRoute(server: any) { - const licensePreRouting = licensePreRoutingFactory(server); - - server.route({ - path: '/api/index_lifecycle_management/nodes/{nodeAttrs}/details', - method: 'GET', - handler: async (request: any) => { - const callWithRequest = callWithRequestFactory(server, request); - - try { - const stats = await fetchNodeStats(callWithRequest); - const response = findMatchingNodes(stats, request.params.nodeAttrs); - return response; - } catch (err) { - if (isEsError(err)) { - return wrapEsError(err); - } - - return wrapUnknownError(err); - } - }, - config: { - pre: [licensePreRouting], - }, - }); -} diff --git a/x-pack/legacy/plugins/index_lifecycle_management/server/routes/api/nodes/register_list_route.ts b/x-pack/legacy/plugins/index_lifecycle_management/server/routes/api/nodes/register_list_route.ts deleted file mode 100644 index edbe4289ed83c..0000000000000 --- a/x-pack/legacy/plugins/index_lifecycle_management/server/routes/api/nodes/register_list_route.ts +++ /dev/null @@ -1,63 +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 { callWithRequestFactory } from '../../../lib/call_with_request_factory'; -import { isEsError } from '../../../lib/is_es_error'; -import { wrapEsError, wrapUnknownError } from '../../../lib/error_wrappers'; -import { licensePreRoutingFactory } from '../../../lib/license_pre_routing_factory'; -import { NODE_ATTRS_KEYS_TO_IGNORE } from './constants'; - -function convertStatsIntoList(stats: any, attributesToBeFiltered: string[]): any { - return Object.entries(stats.nodes).reduce((accum: any, [nodeId, nodeStats]: [any, any]) => { - const attributes = nodeStats.attributes || {}; - for (const [key, value] of Object.entries(attributes)) { - if (!attributesToBeFiltered.includes(key)) { - const attributeString = `${key}:${value}`; - accum[attributeString] = accum[attributeString] || []; - accum[attributeString].push(nodeId); - } - } - return accum; - }, {}); -} - -async function fetchNodeStats(callWithRequest: any): Promise { - const params = { - format: 'json', - }; - - return await callWithRequest('nodes.stats', params); -} - -export function registerListRoute(server: any) { - const config = server.config(); - const filteredNodeAttributes = config.get('xpack.ilm.filteredNodeAttributes'); - const attributesToBeFiltered = [...NODE_ATTRS_KEYS_TO_IGNORE, ...filteredNodeAttributes]; - const licensePreRouting = licensePreRoutingFactory(server); - - server.route({ - path: '/api/index_lifecycle_management/nodes/list', - method: 'GET', - handler: async (request: any) => { - const callWithRequest = callWithRequestFactory(server, request); - - try { - const stats = await fetchNodeStats(callWithRequest); - const response = convertStatsIntoList(stats, attributesToBeFiltered); - return response; - } catch (err) { - if (isEsError(err)) { - return wrapEsError(err); - } - - return wrapUnknownError(err); - } - }, - config: { - pre: [licensePreRouting], - }, - }); -} diff --git a/x-pack/legacy/plugins/index_lifecycle_management/server/routes/api/nodes/register_nodes_routes.ts b/x-pack/legacy/plugins/index_lifecycle_management/server/routes/api/nodes/register_nodes_routes.ts deleted file mode 100644 index 4486d97038657..0000000000000 --- a/x-pack/legacy/plugins/index_lifecycle_management/server/routes/api/nodes/register_nodes_routes.ts +++ /dev/null @@ -1,13 +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 { registerListRoute } from './register_list_route'; -import { registerDetailsRoute } from './register_details_route'; - -export function registerNodesRoutes(server: any) { - registerListRoute(server); - registerDetailsRoute(server); -} diff --git a/x-pack/legacy/plugins/index_lifecycle_management/server/routes/api/policies/index.ts b/x-pack/legacy/plugins/index_lifecycle_management/server/routes/api/policies/index.ts deleted file mode 100644 index 7c6103a3389ab..0000000000000 --- a/x-pack/legacy/plugins/index_lifecycle_management/server/routes/api/policies/index.ts +++ /dev/null @@ -1,7 +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. - */ - -export { registerPoliciesRoutes } from './register_policies_routes'; diff --git a/x-pack/legacy/plugins/index_lifecycle_management/server/routes/api/policies/register_create_route.ts b/x-pack/legacy/plugins/index_lifecycle_management/server/routes/api/policies/register_create_route.ts deleted file mode 100644 index f6bc96dd498a4..0000000000000 --- a/x-pack/legacy/plugins/index_lifecycle_management/server/routes/api/policies/register_create_route.ts +++ /dev/null @@ -1,52 +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 { callWithRequestFactory } from '../../../lib/call_with_request_factory'; -import { isEsError } from '../../../lib/is_es_error'; -import { wrapEsError, wrapUnknownError } from '../../../lib/error_wrappers'; -import { licensePreRoutingFactory } from '../../../lib/license_pre_routing_factory'; - -async function createPolicy(callWithRequest: any, policy: any): Promise { - const body = { - policy: { - phases: policy.phases, - }, - }; - const params = { - method: 'PUT', - path: `/_ilm/policy/${encodeURIComponent(policy.name)}`, - ignore: [404], - body, - }; - - return await callWithRequest('transport.request', params); -} - -export function registerCreateRoute(server: any) { - const licensePreRouting = licensePreRoutingFactory(server); - - server.route({ - path: '/api/index_lifecycle_management/policies', - method: 'POST', - handler: async (request: any) => { - const callWithRequest = callWithRequestFactory(server, request); - - try { - const response = await createPolicy(callWithRequest, request.payload); - return response; - } catch (err) { - if (isEsError(err)) { - return wrapEsError(err); - } - - return wrapUnknownError(err); - } - }, - config: { - pre: [licensePreRouting], - }, - }); -} diff --git a/x-pack/legacy/plugins/index_lifecycle_management/server/routes/api/policies/register_delete_route.ts b/x-pack/legacy/plugins/index_lifecycle_management/server/routes/api/policies/register_delete_route.ts deleted file mode 100644 index c84f2efd92d8f..0000000000000 --- a/x-pack/legacy/plugins/index_lifecycle_management/server/routes/api/policies/register_delete_route.ts +++ /dev/null @@ -1,46 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { callWithRequestFactory } from '../../../lib/call_with_request_factory'; -import { isEsError } from '../../../lib/is_es_error'; -import { wrapEsError, wrapUnknownError } from '../../../lib/error_wrappers'; -import { licensePreRoutingFactory } from '../../../lib/license_pre_routing_factory'; - -async function deletePolicies(policyNames: string, callWithRequest: any): Promise { - const params = { - method: 'DELETE', - path: `/_ilm/policy/${encodeURIComponent(policyNames)}`, - // we allow 404 since they may have no policies - ignore: [404], - }; - - return await callWithRequest('transport.request', params); -} - -export function registerDeleteRoute(server: any) { - const licensePreRouting = licensePreRoutingFactory(server); - - server.route({ - path: '/api/index_lifecycle_management/policies/{policyNames}', - method: 'DELETE', - handler: async (request: any) => { - const callWithRequest = callWithRequestFactory(server, request); - const { policyNames } = request.params; - try { - await deletePolicies(policyNames, callWithRequest); - return {}; - } catch (err) { - if (isEsError(err)) { - return wrapEsError(err); - } - return wrapUnknownError(err); - } - }, - config: { - pre: [licensePreRouting], - }, - }); -} diff --git a/x-pack/legacy/plugins/index_lifecycle_management/server/routes/api/policies/register_fetch_route.ts b/x-pack/legacy/plugins/index_lifecycle_management/server/routes/api/policies/register_fetch_route.ts deleted file mode 100644 index c65f849a47d87..0000000000000 --- a/x-pack/legacy/plugins/index_lifecycle_management/server/routes/api/policies/register_fetch_route.ts +++ /dev/null @@ -1,84 +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 { callWithRequestFactory } from '../../../lib/call_with_request_factory'; -import { isEsError } from '../../../lib/is_es_error'; -import { wrapEsError, wrapUnknownError } from '../../../lib/error_wrappers'; -import { licensePreRoutingFactory } from '../../../lib/license_pre_routing_factory'; - -function formatPolicies(policiesMap: any): any { - if (policiesMap.status === 404) { - return []; - } - - return Object.keys(policiesMap).reduce((accum: any[], lifecycleName: string) => { - const policyEntry = policiesMap[lifecycleName]; - accum.push({ - ...policyEntry, - name: lifecycleName, - }); - return accum; - }, []); -} - -async function fetchPolicies(callWithRequest: any): Promise { - const params = { - method: 'GET', - path: '/_ilm/policy', - // we allow 404 since they may have no policies - ignore: [404], - }; - - return await callWithRequest('transport.request', params); -} -async function addLinkedIndices(policiesMap: any, callWithRequest: any) { - if (policiesMap.status === 404) { - return policiesMap; - } - const params = { - method: 'GET', - path: '/*/_ilm/explain', - // we allow 404 since they may have no policies - ignore: [404], - }; - - const policyExplanation: any = await callWithRequest('transport.request', params); - Object.entries(policyExplanation.indices).forEach(([indexName, { policy }]: [string, any]) => { - if (policy && policiesMap[policy]) { - policiesMap[policy].linkedIndices = policiesMap[policy].linkedIndices || []; - policiesMap[policy].linkedIndices.push(indexName); - } - }); -} - -export function registerFetchRoute(server: any) { - const licensePreRouting = licensePreRoutingFactory(server); - - server.route({ - path: '/api/index_lifecycle_management/policies', - method: 'GET', - handler: async (request: any) => { - const callWithRequest = callWithRequestFactory(server, request); - const { withIndices } = request.query; - try { - const policiesMap = await fetchPolicies(callWithRequest); - if (withIndices) { - await addLinkedIndices(policiesMap, callWithRequest); - } - return formatPolicies(policiesMap); - } catch (err) { - if (isEsError(err)) { - return wrapEsError(err); - } - - return wrapUnknownError(err); - } - }, - config: { - pre: [licensePreRouting], - }, - }); -} diff --git a/x-pack/legacy/plugins/index_lifecycle_management/server/routes/api/policies/register_policies_routes.ts b/x-pack/legacy/plugins/index_lifecycle_management/server/routes/api/policies/register_policies_routes.ts deleted file mode 100644 index 279b016da178f..0000000000000 --- a/x-pack/legacy/plugins/index_lifecycle_management/server/routes/api/policies/register_policies_routes.ts +++ /dev/null @@ -1,15 +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 { registerFetchRoute } from './register_fetch_route'; -import { registerCreateRoute } from './register_create_route'; -import { registerDeleteRoute } from './register_delete_route'; - -export function registerPoliciesRoutes(server: any) { - registerFetchRoute(server); - registerCreateRoute(server); - registerDeleteRoute(server); -} diff --git a/x-pack/legacy/plugins/index_lifecycle_management/server/routes/api/templates/index.ts b/x-pack/legacy/plugins/index_lifecycle_management/server/routes/api/templates/index.ts deleted file mode 100644 index dc9a0acaaf09b..0000000000000 --- a/x-pack/legacy/plugins/index_lifecycle_management/server/routes/api/templates/index.ts +++ /dev/null @@ -1,7 +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. - */ - -export { registerTemplatesRoutes } from './register_templates_routes'; diff --git a/x-pack/legacy/plugins/index_lifecycle_management/server/routes/api/templates/register_add_policy_route.ts b/x-pack/legacy/plugins/index_lifecycle_management/server/routes/api/templates/register_add_policy_route.ts deleted file mode 100644 index 57e5a91f60f5b..0000000000000 --- a/x-pack/legacy/plugins/index_lifecycle_management/server/routes/api/templates/register_add_policy_route.ts +++ /dev/null @@ -1,67 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { merge } from 'lodash'; - -import { callWithRequestFactory } from '../../../lib/call_with_request_factory'; -import { isEsError } from '../../../lib/is_es_error'; -import { wrapEsError, wrapUnknownError } from '../../../lib/error_wrappers'; -import { licensePreRoutingFactory } from '../../../lib/license_pre_routing_factory'; - -async function getIndexTemplate(callWithRequest: any, templateName: string): Promise { - const response = await callWithRequest('indices.getTemplate', { name: templateName }); - return response[templateName]; -} - -async function updateIndexTemplate(callWithRequest: any, indexTemplatePatch: any): Promise { - // Fetch existing template - const template = await getIndexTemplate(callWithRequest, indexTemplatePatch.templateName); - merge(template, { - settings: { - index: { - lifecycle: { - name: indexTemplatePatch.policyName, - rollover_alias: indexTemplatePatch.aliasName, - }, - }, - }, - }); - - const params = { - method: 'PUT', - path: `/_template/${encodeURIComponent(indexTemplatePatch.templateName)}`, - ignore: [404], - body: template, - }; - - return await callWithRequest('transport.request', params); -} - -export function registerAddPolicyRoute(server: any) { - const licensePreRouting = licensePreRoutingFactory(server); - - server.route({ - path: '/api/index_lifecycle_management/template', - method: 'POST', - handler: async (request: any) => { - const callWithRequest = callWithRequestFactory(server, request); - - try { - const response = await updateIndexTemplate(callWithRequest, request.payload); - return response; - } catch (err) { - if (isEsError(err)) { - return wrapEsError(err); - } - - return wrapUnknownError(err); - } - }, - config: { - pre: [licensePreRouting], - }, - }); -} diff --git a/x-pack/legacy/plugins/index_lifecycle_management/server/routes/api/templates/register_fetch_route.ts b/x-pack/legacy/plugins/index_lifecycle_management/server/routes/api/templates/register_fetch_route.ts deleted file mode 100644 index fd58f471d69bb..0000000000000 --- a/x-pack/legacy/plugins/index_lifecycle_management/server/routes/api/templates/register_fetch_route.ts +++ /dev/null @@ -1,86 +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 { callWithRequestFactory } from '../../../lib/call_with_request_factory'; -import { isEsError } from '../../../lib/is_es_error'; -import { wrapEsError, wrapUnknownError } from '../../../lib/error_wrappers'; -import { licensePreRoutingFactory } from '../../../lib/license_pre_routing_factory'; - -/** - * We don't want to output system template (whose name starts with a ".") which don't - * have a time base index pattern (with a wildcard in it) as those templates are already - * assigned to a single index. - * - * @param {String} templateName The index template - * @param {Array} indexPatterns Index patterns - */ -function isReservedSystemTemplate(templateName: string, indexPatterns: string[]): boolean { - return ( - templateName.startsWith('kibana_index_template') || - (templateName.startsWith('.') && - indexPatterns.every(pattern => { - return !pattern.includes('*'); - })) - ); -} - -function filterAndFormatTemplates(templates: any): any { - const formattedTemplates = []; - const templateNames = Object.keys(templates); - for (const templateName of templateNames) { - const { settings, index_patterns } = templates[templateName]; // eslint-disable-line camelcase - if (isReservedSystemTemplate(templateName, index_patterns)) { - continue; - } - const formattedTemplate = { - index_lifecycle_name: - settings.index && settings.index.lifecycle ? settings.index.lifecycle.name : undefined, - index_patterns, - allocation_rules: - settings.index && settings.index.routing ? settings.index.routing : undefined, - settings, - name: templateName, - }; - formattedTemplates.push(formattedTemplate); - } - return formattedTemplates; -} - -async function fetchTemplates(callWithRequest: any): Promise { - const params = { - method: 'GET', - path: '/_template', - // we allow 404 incase the user shutdown security in-between the check and now - ignore: [404], - }; - - return await callWithRequest('transport.request', params); -} -export function registerFetchRoute(server: any) { - const licensePreRouting = licensePreRoutingFactory(server); - - server.route({ - path: '/api/index_lifecycle_management/templates', - method: 'GET', - handler: async (request: any) => { - const callWithRequest = callWithRequestFactory(server, request); - - try { - const templates = await fetchTemplates(callWithRequest); - return filterAndFormatTemplates(templates); - } catch (err) { - if (isEsError(err)) { - return wrapEsError(err); - } - - return wrapUnknownError(err); - } - }, - config: { - pre: [licensePreRouting], - }, - }); -} diff --git a/x-pack/legacy/plugins/index_lifecycle_management/server/routes/api/templates/register_get_route.ts b/x-pack/legacy/plugins/index_lifecycle_management/server/routes/api/templates/register_get_route.ts deleted file mode 100644 index 3edaea6e15818..0000000000000 --- a/x-pack/legacy/plugins/index_lifecycle_management/server/routes/api/templates/register_get_route.ts +++ /dev/null @@ -1,48 +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 { callWithRequestFactory } from '../../../lib/call_with_request_factory'; -import { isEsError } from '../../../lib/is_es_error'; -import { wrapEsError, wrapUnknownError } from '../../../lib/error_wrappers'; -import { licensePreRoutingFactory } from '../../../lib/license_pre_routing_factory'; - -async function fetchTemplate(callWithRequest: any, templateName: string): Promise { - const params = { - method: 'GET', - path: `/_template/${encodeURIComponent(templateName)}`, - // we allow 404 incase the user shutdown security in-between the check and now - ignore: [404], - }; - - return await callWithRequest('transport.request', params); -} - -export function registerGetRoute(server: any) { - const licensePreRouting = licensePreRoutingFactory(server); - - server.route({ - path: '/api/index_lifecycle_management/templates/{templateName}', - method: 'GET', - handler: async (request: any) => { - const callWithRequest = callWithRequestFactory(server, request); - const templateName = request.params.templateName; - - try { - const template = await fetchTemplate(callWithRequest, templateName); - return template[templateName]; - } catch (err) { - if (isEsError(err)) { - return wrapEsError(err); - } - - return wrapUnknownError(err); - } - }, - config: { - pre: [licensePreRouting], - }, - }); -} diff --git a/x-pack/legacy/plugins/index_lifecycle_management/server/routes/api/templates/register_templates_routes.ts b/x-pack/legacy/plugins/index_lifecycle_management/server/routes/api/templates/register_templates_routes.ts deleted file mode 100644 index 424b2d36b1ba2..0000000000000 --- a/x-pack/legacy/plugins/index_lifecycle_management/server/routes/api/templates/register_templates_routes.ts +++ /dev/null @@ -1,15 +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 { registerFetchRoute } from './register_fetch_route'; -import { registerGetRoute } from './register_get_route'; -import { registerAddPolicyRoute } from './register_add_policy_route'; - -export function registerTemplatesRoutes(server: any) { - registerFetchRoute(server); - registerGetRoute(server); - registerAddPolicyRoute(server); -} diff --git a/x-pack/legacy/plugins/index_lifecycle_management/shim.ts b/x-pack/legacy/plugins/index_lifecycle_management/shim.ts deleted file mode 100644 index 18b3d9ef28b6a..0000000000000 --- a/x-pack/legacy/plugins/index_lifecycle_management/shim.ts +++ /dev/null @@ -1,17 +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 { Legacy } from 'kibana'; - -export interface LegacySetup { - server: Legacy.Server; -} - -export function createShim(server: Legacy.Server): LegacySetup { - return { - server, - }; -} diff --git a/x-pack/legacy/plugins/index_management/index.ts b/x-pack/legacy/plugins/index_management/index.ts index 9eba98a526d2b..afca15203b970 100644 --- a/x-pack/legacy/plugins/index_management/index.ts +++ b/x-pack/legacy/plugins/index_management/index.ts @@ -4,6 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ +// TODO: Remove this once CCR is migrated to the plugins directory. export function indexManagement(kibana: any) { return new kibana.Plugin({ id: 'index_management', diff --git a/x-pack/legacy/plugins/lens/index.ts b/x-pack/legacy/plugins/lens/index.ts deleted file mode 100644 index e9a901c58cd90..0000000000000 --- a/x-pack/legacy/plugins/lens/index.ts +++ /dev/null @@ -1,38 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import * as Joi from 'joi'; -import { resolve } from 'path'; -import { LegacyPluginInitializer } from 'src/legacy/types'; -import { PLUGIN_ID, NOT_INTERNATIONALIZED_PRODUCT_NAME } from '../../../plugins/lens/common'; - -export const lens: LegacyPluginInitializer = kibana => { - return new kibana.Plugin({ - id: PLUGIN_ID, - configPrefix: `xpack.${PLUGIN_ID}`, - // task_manager could be required, but is only used for telemetry - require: ['kibana', 'elasticsearch', 'xpack_main', 'interpreter'], - publicDir: resolve(__dirname, 'public'), - - uiExports: { - app: { - title: NOT_INTERNATIONALIZED_PRODUCT_NAME, - description: 'Explore and visualize data.', - main: `plugins/${PLUGIN_ID}/redirect`, - listed: false, - }, - visualize: [`plugins/${PLUGIN_ID}/legacy`], - embeddableFactories: [`plugins/${PLUGIN_ID}/legacy`], - styleSheetPaths: resolve(__dirname, 'public/index.scss'), - }, - - config: () => { - return Joi.object({ - enabled: Joi.boolean().default(true), - }).default(); - }, - }); -}; diff --git a/x-pack/legacy/plugins/lens/public/app_plugin/_index.scss b/x-pack/legacy/plugins/lens/public/app_plugin/_index.scss deleted file mode 100644 index 2ac86f0e58a61..0000000000000 --- a/x-pack/legacy/plugins/lens/public/app_plugin/_index.scss +++ /dev/null @@ -1 +0,0 @@ -@import './app'; diff --git a/x-pack/legacy/plugins/lens/public/app_plugin/app.tsx b/x-pack/legacy/plugins/lens/public/app_plugin/app.tsx deleted file mode 100644 index dfea2e39fcbc5..0000000000000 --- a/x-pack/legacy/plugins/lens/public/app_plugin/app.tsx +++ /dev/null @@ -1,417 +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 _ from 'lodash'; -import React, { useState, useEffect, useCallback } from 'react'; -import { I18nProvider } from '@kbn/i18n/react'; -import { i18n } from '@kbn/i18n'; -import { Query, DataPublicPluginStart } from 'src/plugins/data/public'; -import { AppMountContext, NotificationsStart } from 'src/core/public'; -import { IStorageWrapper } from 'src/plugins/kibana_utils/public'; -import { npStart } from 'ui/new_platform'; -import { FormattedMessage } from '@kbn/i18n/react'; -import { KibanaContextProvider } from '../../../../../../src/plugins/kibana_react/public'; -import { SavedObjectSaveModal } from '../../../../../../src/plugins/saved_objects/public'; -import { Document, SavedObjectStore } from '../persistence'; -import { EditorFrameInstance } from '../types'; -import { NativeRenderer } from '../native_renderer'; -import { trackUiEvent } from '../lens_ui_telemetry'; -import { - esFilters, - Filter, - IndexPattern as IndexPatternInstance, - IndexPatternsContract, - SavedQuery, -} from '../../../../../../src/plugins/data/public'; - -interface State { - isLoading: boolean; - isSaveModalVisible: boolean; - indexPatternsForTopNav: IndexPatternInstance[]; - persistedDoc?: Document; - lastKnownDoc?: Document; - - // Properties needed to interface with TopNav - dateRange: { - fromDate: string; - toDate: string; - }; - query: Query; - filters: Filter[]; - savedQuery?: SavedQuery; -} - -export function App({ - editorFrame, - data, - core, - storage, - docId, - docStorage, - redirectTo, - addToDashboardMode, -}: { - editorFrame: EditorFrameInstance; - data: DataPublicPluginStart; - core: AppMountContext['core']; - storage: IStorageWrapper; - docId?: string; - docStorage: SavedObjectStore; - redirectTo: (id?: string) => void; - addToDashboardMode?: boolean; -}) { - const language = - storage.get('kibana.userQueryLanguage') || core.uiSettings.get('search:queryLanguage'); - - const [state, setState] = useState(() => { - const currentRange = data.query.timefilter.timefilter.getTime(); - return { - isLoading: !!docId, - isSaveModalVisible: false, - indexPatternsForTopNav: [], - query: { query: '', language }, - dateRange: { - fromDate: currentRange.from, - toDate: currentRange.to, - }, - filters: [], - }; - }); - - const { lastKnownDoc } = state; - - useEffect(() => { - // Clear app-specific filters when navigating to Lens. Necessary because Lens - // can be loaded without a full page refresh - data.query.filterManager.setAppFilters([]); - - const filterSubscription = data.query.filterManager.getUpdates$().subscribe({ - next: () => { - setState(s => ({ ...s, filters: data.query.filterManager.getFilters() })); - trackUiEvent('app_filters_updated'); - }, - }); - - const timeSubscription = data.query.timefilter.timefilter.getTimeUpdate$().subscribe({ - next: () => { - const currentRange = data.query.timefilter.timefilter.getTime(); - setState(s => ({ - ...s, - dateRange: { - fromDate: currentRange.from, - toDate: currentRange.to, - }, - })); - }, - }); - - return () => { - filterSubscription.unsubscribe(); - timeSubscription.unsubscribe(); - }; - }, []); - - // Sync Kibana breadcrumbs any time the saved document's title changes - useEffect(() => { - core.chrome.setBreadcrumbs([ - { - href: core.http.basePath.prepend(`/app/kibana#/visualize`), - text: i18n.translate('xpack.lens.breadcrumbsTitle', { - defaultMessage: 'Visualize', - }), - }, - { - text: state.persistedDoc - ? state.persistedDoc.title - : i18n.translate('xpack.lens.breadcrumbsCreate', { defaultMessage: 'Create' }), - }, - ]); - }, [state.persistedDoc && state.persistedDoc.title]); - - useEffect(() => { - if (docId && (!state.persistedDoc || state.persistedDoc.id !== docId)) { - setState(s => ({ ...s, isLoading: true })); - docStorage - .load(docId) - .then(doc => { - getAllIndexPatterns( - doc.state.datasourceMetaData.filterableIndexPatterns, - data.indexPatterns, - core.notifications - ) - .then(indexPatterns => { - // Don't overwrite any pinned filters - data.query.filterManager.setAppFilters(doc.state.filters); - setState(s => ({ - ...s, - isLoading: false, - persistedDoc: doc, - lastKnownDoc: doc, - query: doc.state.query, - indexPatternsForTopNav: indexPatterns, - })); - }) - .catch(() => { - setState(s => ({ ...s, isLoading: false })); - - redirectTo(); - }); - }) - .catch(() => { - setState(s => ({ ...s, isLoading: false })); - - core.notifications.toasts.addDanger( - i18n.translate('xpack.lens.app.docLoadingError', { - defaultMessage: 'Error loading saved document', - }) - ); - - redirectTo(); - }); - } - }, [docId]); - - const isSaveable = - lastKnownDoc && - lastKnownDoc.expression && - lastKnownDoc.expression.length > 0 && - core.application.capabilities.visualize.save; - - const onError = useCallback( - (e: { message: string }) => - core.notifications.toasts.addDanger({ - title: e.message, - }), - [] - ); - - const { TopNavMenu } = npStart.plugins.navigation.ui; - - const confirmButton = addToDashboardMode ? ( - - ) : null; - - return ( - - -
-
- { - if (isSaveable && lastKnownDoc) { - setState(s => ({ ...s, isSaveModalVisible: true })); - } - }, - testId: 'lnsApp_saveButton', - disableButton: !isSaveable, - }, - ]} - data-test-subj="lnsApp_topNav" - screenTitle={'lens'} - onQuerySubmit={payload => { - const { dateRange, query } = payload; - - if ( - dateRange.from !== state.dateRange.fromDate || - dateRange.to !== state.dateRange.toDate - ) { - data.query.timefilter.timefilter.setTime(dateRange); - trackUiEvent('app_date_change'); - } else { - trackUiEvent('app_query_change'); - } - - setState(s => ({ - ...s, - dateRange: { - fromDate: dateRange.from, - toDate: dateRange.to, - }, - query: query || s.query, - })); - }} - appName={'lens'} - indexPatterns={state.indexPatternsForTopNav} - showSearchBar={true} - showDatePicker={true} - showQueryBar={true} - showFilterBar={true} - showSaveQuery={core.application.capabilities.visualize.saveQuery as boolean} - savedQuery={state.savedQuery} - onSaved={savedQuery => { - setState(s => ({ ...s, savedQuery })); - }} - onSavedQueryUpdated={savedQuery => { - const savedQueryFilters = savedQuery.attributes.filters || []; - const globalFilters = data.query.filterManager.getGlobalFilters(); - data.query.filterManager.setFilters([...globalFilters, ...savedQueryFilters]); - setState(s => ({ - ...s, - savedQuery: { ...savedQuery }, // Shallow query for reference issues - dateRange: savedQuery.attributes.timefilter - ? { - fromDate: savedQuery.attributes.timefilter.from, - toDate: savedQuery.attributes.timefilter.to, - } - : s.dateRange, - })); - }} - onClearSavedQuery={() => { - data.query.filterManager.setFilters(data.query.filterManager.getGlobalFilters()); - setState(s => ({ - ...s, - savedQuery: undefined, - filters: data.query.filterManager.getGlobalFilters(), - query: { - query: '', - language: - storage.get('kibana.userQueryLanguage') || - core.uiSettings.get('search:queryLanguage'), - }, - })); - }} - query={state.query} - dateRangeFrom={state.dateRange.fromDate} - dateRangeTo={state.dateRange.toDate} - /> -
- - {(!state.isLoading || state.persistedDoc) && ( - { - if (!_.isEqual(state.persistedDoc, doc)) { - setState(s => ({ ...s, lastKnownDoc: doc })); - } - - // Update the cached index patterns if the user made a change to any of them - if ( - state.indexPatternsForTopNav.length !== filterableIndexPatterns.length || - filterableIndexPatterns.find( - ({ id }) => - !state.indexPatternsForTopNav.find(indexPattern => indexPattern.id === id) - ) - ) { - getAllIndexPatterns( - filterableIndexPatterns, - data.indexPatterns, - core.notifications - ).then(indexPatterns => { - if (indexPatterns) { - setState(s => ({ ...s, indexPatternsForTopNav: indexPatterns })); - } - }); - } - }, - }} - /> - )} -
- {lastKnownDoc && state.isSaveModalVisible && ( - { - const [pinnedFilters, appFilters] = _.partition( - lastKnownDoc.state?.filters, - esFilters.isFilterPinned - ); - const lastDocWithoutPinned = pinnedFilters?.length - ? { - ...lastKnownDoc, - state: { - ...lastKnownDoc.state, - filters: appFilters, - }, - } - : lastKnownDoc; - - const doc = { - ...lastDocWithoutPinned, - id: props.newCopyOnSave ? undefined : lastKnownDoc.id, - title: props.newTitle, - }; - - docStorage - .save(doc) - .then(({ id }) => { - // Prevents unnecessary network request and disables save button - const newDoc = { ...doc, id }; - setState(s => ({ - ...s, - isSaveModalVisible: false, - persistedDoc: newDoc, - lastKnownDoc: newDoc, - })); - if (docId !== id) { - redirectTo(id); - } - }) - .catch(e => { - // eslint-disable-next-line no-console - console.dir(e); - trackUiEvent('save_failed'); - core.notifications.toasts.addDanger( - i18n.translate('xpack.lens.app.docSavingError', { - defaultMessage: 'Error saving document', - }) - ); - setState(s => ({ ...s, isSaveModalVisible: false })); - }); - }} - onClose={() => setState(s => ({ ...s, isSaveModalVisible: false }))} - title={lastKnownDoc.title || ''} - showCopyOnSave={!addToDashboardMode} - objectType={i18n.translate('xpack.lens.app.saveModalType', { - defaultMessage: 'Lens visualization', - })} - showDescription={false} - confirmButtonLabel={confirmButton} - /> - )} -
-
- ); -} - -export async function getAllIndexPatterns( - ids: Array<{ id: string }>, - indexPatternsService: IndexPatternsContract, - notifications: NotificationsStart -): Promise { - try { - return await Promise.all(ids.map(({ id }) => indexPatternsService.get(id))); - } catch (e) { - notifications.toasts.addDanger( - i18n.translate('xpack.lens.app.indexPatternLoadingError', { - defaultMessage: 'Error loading index patterns', - }) - ); - - throw new Error(e); - } -} diff --git a/x-pack/legacy/plugins/lens/public/datatable_visualization/_index.scss b/x-pack/legacy/plugins/lens/public/datatable_visualization/_index.scss deleted file mode 100644 index 99c357b53952f..0000000000000 --- a/x-pack/legacy/plugins/lens/public/datatable_visualization/_index.scss +++ /dev/null @@ -1 +0,0 @@ -@import './visualization'; diff --git a/x-pack/legacy/plugins/lens/public/datatable_visualization/index.ts b/x-pack/legacy/plugins/lens/public/datatable_visualization/index.ts deleted file mode 100644 index 7a54a5daa3095..0000000000000 --- a/x-pack/legacy/plugins/lens/public/datatable_visualization/index.ts +++ /dev/null @@ -1,31 +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 { CoreSetup } from 'src/core/public'; -import { datatableVisualization } from './visualization'; -import { ExpressionsSetup } from '../../../../../../src/plugins/expressions/public'; -import { datatable, datatableColumns, getDatatableRenderer } from './expression'; -import { EditorFrameSetup, FormatFactory } from '../types'; - -export interface DatatableVisualizationPluginSetupPlugins { - expressions: ExpressionsSetup; - formatFactory: Promise; - editorFrame: EditorFrameSetup; -} - -export class DatatableVisualization { - constructor() {} - - setup( - _core: CoreSetup | null, - { expressions, formatFactory, editorFrame }: DatatableVisualizationPluginSetupPlugins - ) { - expressions.registerFunction(() => datatableColumns); - expressions.registerFunction(() => datatable); - expressions.registerRenderer(() => getDatatableRenderer(formatFactory)); - editorFrame.registerVisualization(datatableVisualization); - } -} diff --git a/x-pack/legacy/plugins/lens/public/drag_drop/_index.scss b/x-pack/legacy/plugins/lens/public/drag_drop/_index.scss deleted file mode 100644 index 1b3d0cf0a3c2a..0000000000000 --- a/x-pack/legacy/plugins/lens/public/drag_drop/_index.scss +++ /dev/null @@ -1 +0,0 @@ -@import './drag_drop' diff --git a/x-pack/legacy/plugins/lens/public/editor_frame_service/_index.scss b/x-pack/legacy/plugins/lens/public/editor_frame_service/_index.scss deleted file mode 100644 index 4d7e054ff03c3..0000000000000 --- a/x-pack/legacy/plugins/lens/public/editor_frame_service/_index.scss +++ /dev/null @@ -1 +0,0 @@ -@import './editor_frame/index'; diff --git a/x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/index.scss b/x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/index.scss deleted file mode 100644 index 6c6a63c8c7eb6..0000000000000 --- a/x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/index.scss +++ /dev/null @@ -1,8 +0,0 @@ -@import './chart_switch'; -@import './config_panel_wrapper'; -@import './data_panel_wrapper'; -@import './expression_renderer'; -@import './frame_layout'; -@import './suggestion_panel'; -@import './workspace_panel_wrapper'; - diff --git a/x-pack/legacy/plugins/lens/public/help_menu_util.tsx b/x-pack/legacy/plugins/lens/public/help_menu_util.tsx deleted file mode 100644 index 9ead31690e854..0000000000000 --- a/x-pack/legacy/plugins/lens/public/help_menu_util.tsx +++ /dev/null @@ -1,25 +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 { ELASTIC_WEBSITE_URL, DOC_LINK_VERSION } from 'ui/documentation_links'; -import { ChromeStart } from 'kibana/public'; - -export function addHelpMenuToAppChrome(chrome: ChromeStart) { - chrome.setHelpExtension({ - appName: 'Lens', - links: [ - { - linkType: 'documentation', - href: `${ELASTIC_WEBSITE_URL}guide/en/kibana/${DOC_LINK_VERSION}/lens.html`, - }, - { - linkType: 'github', - title: '[Lens]', - labels: ['Feature:Lens'], - }, - ], - }); -} diff --git a/x-pack/legacy/plugins/lens/public/helpers/url_helper.test.ts b/x-pack/legacy/plugins/lens/public/helpers/url_helper.test.ts deleted file mode 100644 index ef960fb52952b..0000000000000 --- a/x-pack/legacy/plugins/lens/public/helpers/url_helper.test.ts +++ /dev/null @@ -1,49 +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. - */ - -jest.mock('../../../../../../src/plugins/dashboard/public', () => ({ - DashboardConstants: { - ADD_EMBEDDABLE_ID: 'addEmbeddableId', - ADD_EMBEDDABLE_TYPE: 'addEmbeddableType', - }, -})); - -import { addEmbeddableToDashboardUrl, getUrlVars } from './url_helper'; - -describe('Dashboard URL Helper', () => { - it('addEmbeddableToDashboardUrl', () => { - const id = '123eb456cd'; - const urlVars = { - x: '1', - y: '2', - z: '3', - }; - const url = - "/pep/app/kibana#/dashboard?_g=(refreshInterval:(pause:!t,value:0),time:(from:now-15m,to:now))&_a=(description:'',filters:!())"; - expect(addEmbeddableToDashboardUrl(url, id, urlVars)).toEqual( - `/pep/app/kibana#/dashboard?_a=%28description%3A%27%27%2Cfilters%3A%21%28%29%29&_g=%28refreshInterval%3A%28pause%3A%21t%2Cvalue%3A0%29%2Ctime%3A%28from%3Anow-15m%2Cto%3Anow%29%29&addEmbeddableId=${id}&addEmbeddableType=lens&x=1&y=2&z=3` - ); - }); - - it('getUrlVars', () => { - let url = - "http://localhost:5601/app/kibana#/dashboard?_g=(refreshInterval:(pause:!t,value:0),time:(from:now-15m,to:now))&_a=(description:'',filters:!()"; - expect(getUrlVars(url)).toEqual({ - _g: '(refreshInterval:(pause:!t,value:0),time:(from:now-15m,to:now))', - _a: "(description:'',filters:!()", - }); - url = 'http://mybusiness.mydomain.com/app/kibana#/dashboard?x=y&y=z'; - expect(getUrlVars(url)).toEqual({ - x: 'y', - y: 'z', - }); - url = 'http://localhost:5601/app/kibana#/dashboard/777182'; - expect(getUrlVars(url)).toEqual({}); - url = - 'http://localhost:5601/app/kibana#/dashboard/777182?title=Some%20Dashboard%20With%20Spaces'; - expect(getUrlVars(url)).toEqual({ title: 'Some Dashboard With Spaces' }); - }); -}); diff --git a/x-pack/legacy/plugins/lens/public/helpers/url_helper.ts b/x-pack/legacy/plugins/lens/public/helpers/url_helper.ts deleted file mode 100644 index 3495c15118ce7..0000000000000 --- a/x-pack/legacy/plugins/lens/public/helpers/url_helper.ts +++ /dev/null @@ -1,45 +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 { parseUrl, stringify } from 'query-string'; -import { DashboardConstants } from '../../../../../../src/plugins/dashboard/public'; - -type UrlVars = Record; - -/** - * Return query params from URL - * @param url given url - */ -export function getUrlVars(url: string): Record { - const vars: UrlVars = {}; - for (const [, key, value] of url.matchAll(/[?&]+([^=&]+)=([^&]*)/gi)) { - vars[key] = decodeURIComponent(value); - } - return vars; -} - -/** * - * Returns dashboard URL with added embeddableType and embeddableId query params - * eg. - * input: url: /lol/app/kibana#/dashboard?_g=(refreshInterval:(pause:!t,value:0),time:(from:now-15m,to:now)), embeddableId: 12345 - * output: /lol/app/kibana#/dashboard?addEmbeddableType=lens&addEmbeddableId=12345&_g=(refreshInterval:(pause:!t,value:0),time:(from:now-15m,to:now)) - * @param url dasbhoard absolute url - * @param embeddableId id of the saved visualization - * @param urlVars url query params - */ -export function addEmbeddableToDashboardUrl(url: string, embeddableId: string, urlVars: UrlVars) { - const dashboardParsedUrl = parseUrl(url); - const keys = Object.keys(urlVars).sort(); - - keys.forEach(key => { - dashboardParsedUrl.query[key] = urlVars[key]; - }); - dashboardParsedUrl.query[DashboardConstants.ADD_EMBEDDABLE_TYPE] = 'lens'; - dashboardParsedUrl.query[DashboardConstants.ADD_EMBEDDABLE_ID] = embeddableId; - const query = stringify(dashboardParsedUrl.query); - - return `${dashboardParsedUrl.url}?${query}`; -} diff --git a/x-pack/legacy/plugins/lens/public/index.scss b/x-pack/legacy/plugins/lens/public/index.scss deleted file mode 100644 index 2f91d14c397c7..0000000000000 --- a/x-pack/legacy/plugins/lens/public/index.scss +++ /dev/null @@ -1,13 +0,0 @@ -// Import the EUI global scope so we can use EUI constants -@import 'src/legacy/ui/public/styles/_styling_constants'; - -@import './variables'; -@import './mixins'; - -@import './app_plugin/index'; -@import 'datatable_visualization/index'; -@import './drag_drop/index'; -@import 'editor_frame_service/index'; -@import 'indexpattern_datasource/index'; -@import 'xy_visualization/index'; -@import 'metric_visualization/index'; diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_datasource/_index.scss b/x-pack/legacy/plugins/lens/public/indexpattern_datasource/_index.scss deleted file mode 100644 index a283198d6cf73..0000000000000 --- a/x-pack/legacy/plugins/lens/public/indexpattern_datasource/_index.scss +++ /dev/null @@ -1,4 +0,0 @@ -@import './datapanel'; -@import './field_item'; - -@import './dimension_panel/index'; diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_datasource/dimension_panel/_index.scss b/x-pack/legacy/plugins/lens/public/indexpattern_datasource/dimension_panel/_index.scss deleted file mode 100644 index 26f805fe735f0..0000000000000 --- a/x-pack/legacy/plugins/lens/public/indexpattern_datasource/dimension_panel/_index.scss +++ /dev/null @@ -1,2 +0,0 @@ -@import './field_select'; -@import './popover'; diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_datasource/index.ts b/x-pack/legacy/plugins/lens/public/indexpattern_datasource/index.ts deleted file mode 100644 index 8a5c562ebd455..0000000000000 --- a/x-pack/legacy/plugins/lens/public/indexpattern_datasource/index.ts +++ /dev/null @@ -1,49 +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 { CoreSetup } from 'src/core/public'; -import { Storage } from '../../../../../../src/plugins/kibana_utils/public'; -import { getIndexPatternDatasource } from './indexpattern'; -import { renameColumns } from './rename_columns'; -import { getAutoDate } from './auto_date'; -import { ExpressionsSetup } from '../../../../../../src/plugins/expressions/public'; -import { - DataPublicPluginSetup, - DataPublicPluginStart, -} from '../../../../../../src/plugins/data/public'; -import { Datasource, EditorFrameSetup } from '../types'; - -export interface IndexPatternDatasourceSetupPlugins { - expressions: ExpressionsSetup; - data: DataPublicPluginSetup; - editorFrame: EditorFrameSetup; -} - -export interface IndexPatternDatasourceStartPlugins { - data: DataPublicPluginStart; -} - -export class IndexPatternDatasource { - constructor() {} - - setup( - core: CoreSetup, - { data: dataSetup, expressions, editorFrame }: IndexPatternDatasourceSetupPlugins - ) { - expressions.registerFunction(renameColumns); - expressions.registerFunction(getAutoDate({ data: dataSetup })); - - editorFrame.registerDatasource( - core.getStartServices().then(([coreStart, { data }]) => - getIndexPatternDatasource({ - core: coreStart, - storage: new Storage(localStorage), - data, - }) - ) as Promise - ); - } -} diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_datasource/operations/definitions/index.ts b/x-pack/legacy/plugins/lens/public/indexpattern_datasource/operations/definitions/index.ts deleted file mode 100644 index 2db5296905000..0000000000000 --- a/x-pack/legacy/plugins/lens/public/indexpattern_datasource/operations/definitions/index.ts +++ /dev/null @@ -1,198 +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 { IUiSettingsClient, SavedObjectsClientContract, HttpSetup } from 'src/core/public'; -import { IStorageWrapper } from 'src/plugins/kibana_utils/public'; -import { termsOperation } from './terms'; -import { cardinalityOperation } from './cardinality'; -import { minOperation, averageOperation, sumOperation, maxOperation } from './metrics'; -import { dateHistogramOperation } from './date_histogram'; -import { countOperation } from './count'; -import { DimensionPriority, StateSetter, OperationMetadata } from '../../../types'; -import { BaseIndexPatternColumn } from './column_types'; -import { IndexPatternPrivateState, IndexPattern, IndexPatternField } from '../../types'; -import { DateRange } from '../../../../../../../plugins/lens/common'; -import { DataPublicPluginStart } from '../../../../../../../../src/plugins/data/public'; - -// List of all operation definitions registered to this data source. -// If you want to implement a new operation, add it to this array and -// its type will get propagated to everything else -const internalOperationDefinitions = [ - termsOperation, - dateHistogramOperation, - minOperation, - maxOperation, - averageOperation, - cardinalityOperation, - sumOperation, - countOperation, -]; - -export { termsOperation } from './terms'; -export { dateHistogramOperation } from './date_histogram'; -export { minOperation, averageOperation, sumOperation, maxOperation } from './metrics'; -export { countOperation } from './count'; - -/** - * Properties passed to the operation-specific part of the popover editor - */ -export interface ParamEditorProps { - currentColumn: C; - state: IndexPatternPrivateState; - setState: StateSetter; - columnId: string; - layerId: string; - uiSettings: IUiSettingsClient; - storage: IStorageWrapper; - savedObjectsClient: SavedObjectsClientContract; - http: HttpSetup; - dateRange: DateRange; - data: DataPublicPluginStart; -} - -interface BaseOperationDefinitionProps { - type: C['operationType']; - /** - * The priority of the operation. If multiple operations are possible in - * a given scenario (e.g. the user dragged a field into the workspace), - * the operation with the highest priority is picked. - */ - priority?: number; - /** - * The name of the operation shown to the user (e.g. in the popover editor). - * Should be i18n-ified. - */ - displayName: string; - /** - * This function is called if another column in the same layer changed or got removed. - * Can be used to update references to other columns (e.g. for sorting). - * Based on the current column and the other updated columns, this function has to - * return an updated column. If not implemented, the `id` function is used instead. - */ - onOtherColumnChanged?: ( - currentColumn: C, - columns: Partial> - ) => C; - /** - * React component for operation specific settings shown in the popover editor - */ - paramEditor?: React.ComponentType>; - /** - * Function turning a column into an agg config passed to the `esaggs` function - * together with the agg configs returned from other columns. - */ - toEsAggsConfig: (column: C, columnId: string) => unknown; - /** - * Returns true if the `column` can also be used on `newIndexPattern`. - * If this function returns false, the column is removed when switching index pattern - * for a layer - */ - isTransferable: (column: C, newIndexPattern: IndexPattern) => boolean; - /** - * Transfering a column to another index pattern. This can be used to - * adjust operation specific settings such as reacting to aggregation restrictions - * present on the new index pattern. - */ - transfer?: (column: C, newIndexPattern: IndexPattern) => C; -} - -interface BaseBuildColumnArgs { - suggestedPriority: DimensionPriority | undefined; - layerId: string; - columns: Partial>; - indexPattern: IndexPattern; -} - -interface FieldBasedOperationDefinition - extends BaseOperationDefinitionProps { - /** - * Returns the meta data of the operation if applied to the given field. Undefined - * if the field is not applicable to the operation. - */ - getPossibleOperationForField: (field: IndexPatternField) => OperationMetadata | undefined; - /** - * Builds the column object for the given parameters. Should include default p - */ - buildColumn: ( - arg: BaseBuildColumnArgs & { - field: IndexPatternField; - previousColumn?: C; - } - ) => C; - /** - * This method will be called if the user changes the field of an operation. - * You must implement it and return the new column after the field change. - * The most simple implementation will just change the field on the column, and keep - * the rest the same. Some implementations might want to change labels, or their parameters - * when changing the field. - * - * This will only be called for switching the field, not for initially selecting a field. - * - * See {@link OperationDefinition#transfer} for controlling column building when switching an - * index pattern not just a field. - * - * @param oldColumn The column before the user changed the field. - * @param indexPattern The index pattern that field is on. - * @param field The field that the user changed to. - */ - onFieldChange: (oldColumn: C, indexPattern: IndexPattern, field: IndexPatternField) => C; -} - -/** - * Shape of an operation definition. If the type parameter of the definition - * indicates a field based column, `getPossibleOperationForField` has to be - * specified, otherwise `getPossibleOperationForDocument` has to be defined. - */ -export type OperationDefinition = FieldBasedOperationDefinition< - C ->; - -// Helper to to infer the column type out of the operation definition. -// This is done to avoid it to have to list out the column types along with -// the operation definition types -type ColumnFromOperationDefinition = D extends OperationDefinition ? C : never; - -/** - * A union type of all available column types. If a column is of an unknown type somewhere - * withing the indexpattern data source it should be typed as `IndexPatternColumn` to make - * typeguards possible that consider all available column types. - */ -export type IndexPatternColumn = ColumnFromOperationDefinition< - typeof internalOperationDefinitions[number] ->; - -/** - * A union type of all available operation types. The operation type is a unique id of an operation. - * Each column is assigned to exactly one operation type. - */ -export type OperationType = typeof internalOperationDefinitions[number]['type']; - -/** - * This is an operation definition of an unspecified column out of all possible - * column types. - */ -export type GenericOperationDefinition = FieldBasedOperationDefinition; - -/** - * List of all available operation definitions - */ -export const operationDefinitions = internalOperationDefinitions as GenericOperationDefinition[]; - -/** - * Map of all operation visible to consumers (e.g. the dimension panel). - * This simplifies the type of the map and makes it a simple list of unspecified - * operations definitions, because typescript can't infer the type correctly in most - * situations. - * - * If you need a specifically typed version of an operation (e.g. explicitly working with terms), - * you should import the definition directly from this file - * (e.g. `import { termsOperation } from './operations/definitions'`). This map is - * intended to be used in situations where the operation type is not known during compile time. - */ -export const operationDefinitionMap = internalOperationDefinitions.reduce( - (definitionMap, definition) => ({ ...definitionMap, [definition.type]: definition }), - {} -) as Record; diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_datasource/to_expression.ts b/x-pack/legacy/plugins/lens/public/indexpattern_datasource/to_expression.ts deleted file mode 100644 index 3747deaa6059b..0000000000000 --- a/x-pack/legacy/plugins/lens/public/indexpattern_datasource/to_expression.ts +++ /dev/null @@ -1,81 +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 _ from 'lodash'; -import { IndexPatternColumn } from './indexpattern'; -import { operationDefinitionMap } from './operations'; -import { IndexPattern, IndexPatternPrivateState } from './types'; -import { OriginalColumn } from './rename_columns'; - -function getExpressionForLayer( - indexPattern: IndexPattern, - columns: Record, - columnOrder: string[] -) { - if (columnOrder.length === 0) { - return null; - } - - function getEsAggsConfig(column: C, columnId: string) { - return operationDefinitionMap[column.operationType].toEsAggsConfig(column, columnId); - } - - const columnEntries = columnOrder.map(colId => [colId, columns[colId]] as const); - - if (columnEntries.length) { - const aggs = columnEntries.map(([colId, col]) => { - return getEsAggsConfig(col, colId); - }); - - const idMap = columnEntries.reduce((currentIdMap, [colId], index) => { - return { - ...currentIdMap, - [`col-${index}-${colId}`]: { - ...columns[colId], - id: colId, - }, - }; - }, {} as Record); - - const formatterOverrides = columnEntries - .map(([id, col]) => { - const format = col.params && 'format' in col.params ? col.params.format : undefined; - if (!format) { - return null; - } - const base = `| lens_format_column format="${format.id}" columnId="${id}"`; - if (typeof format.params?.decimals === 'number') { - return base + ` decimals=${format.params.decimals}`; - } - return base; - }) - .filter(expr => !!expr) - .join(' '); - - return `esaggs - index="${indexPattern.id}" - metricsAtAllLevels=false - partialRows=false - includeFormatHints=true - aggConfigs={lens_auto_date aggConfigs='${JSON.stringify( - aggs - )}'} | lens_rename_columns idMap='${JSON.stringify(idMap)}' ${formatterOverrides}`; - } - - return null; -} - -export function toExpression(state: IndexPatternPrivateState, layerId: string) { - if (state.layers[layerId]) { - return getExpressionForLayer( - state.indexPatterns[state.layers[layerId].indexPatternId], - state.layers[layerId].columns, - state.layers[layerId].columnOrder - ); - } - - return null; -} diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_datasource/types.ts b/x-pack/legacy/plugins/lens/public/indexpattern_datasource/types.ts deleted file mode 100644 index 3820ff3b387bb..0000000000000 --- a/x-pack/legacy/plugins/lens/public/indexpattern_datasource/types.ts +++ /dev/null @@ -1,60 +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 { IndexPatternColumn } from './operations'; -import { IndexPatternAggRestrictions } from '../../../../../../src/plugins/data/public'; - -export interface IndexPattern { - id: string; - fields: IndexPatternField[]; - title: string; - timeFieldName?: string | null; - fieldFormatMap?: Record< - string, - { - id: string; - params: unknown; - } - >; -} - -export interface IndexPatternField { - name: string; - type: string; - esTypes?: string[]; - aggregatable: boolean; - scripted?: boolean; - searchable: boolean; - aggregationRestrictions?: Partial; -} - -export interface IndexPatternLayer { - columnOrder: string[]; - columns: Record; - // Each layer is tied to the index pattern that created it - indexPatternId: string; -} - -export interface IndexPatternPersistedState { - currentIndexPatternId: string; - layers: Record; -} - -export type IndexPatternPrivateState = IndexPatternPersistedState & { - indexPatternRefs: IndexPatternRef[]; - indexPatterns: Record; - - /** - * indexPatternId -> fieldName -> boolean - */ - existingFields: Record>; - showEmptyFields: boolean; -}; - -export interface IndexPatternRef { - id: string; - title: string; -} diff --git a/x-pack/legacy/plugins/lens/public/legacy.ts b/x-pack/legacy/plugins/lens/public/legacy.ts deleted file mode 100644 index 3b7b6a7a1b510..0000000000000 --- a/x-pack/legacy/plugins/lens/public/legacy.ts +++ /dev/null @@ -1,17 +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 { npSetup, npStart } from 'ui/new_platform'; - -export * from './types'; - -import { plugin } from './index'; - -const pluginInstance = plugin(); -pluginInstance.setup(npSetup.core, { - ...npSetup.plugins, -}); -pluginInstance.start(npStart.core, npStart.plugins); diff --git a/x-pack/legacy/plugins/lens/public/lens_ui_telemetry/factory.ts b/x-pack/legacy/plugins/lens/public/lens_ui_telemetry/factory.ts deleted file mode 100644 index 73750a65c50b8..0000000000000 --- a/x-pack/legacy/plugins/lens/public/lens_ui_telemetry/factory.ts +++ /dev/null @@ -1,124 +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 moment from 'moment'; -import { HttpSetup } from 'src/core/public'; - -import { IStorageWrapper } from 'src/plugins/kibana_utils/public'; -import { BASE_API_URL } from '../../../../../plugins/lens/common'; - -const STORAGE_KEY = 'lens-ui-telemetry'; - -let reportManager: LensReportManager; - -export function setReportManager(newManager: LensReportManager) { - if (reportManager) { - reportManager.stop(); - } - reportManager = newManager; -} - -export function stopReportManager() { - if (reportManager) { - reportManager.stop(); - } -} - -export function trackUiEvent(name: string) { - if (reportManager) { - reportManager.trackEvent(name); - } -} - -export function trackSuggestionEvent(name: string) { - if (reportManager) { - reportManager.trackSuggestionEvent(name); - } -} - -export class LensReportManager { - private events: Record> = {}; - private suggestionEvents: Record> = {}; - - private storage: IStorageWrapper; - private http: HttpSetup; - private timer: ReturnType; - - constructor({ storage, http }: { storage: IStorageWrapper; http: HttpSetup }) { - this.storage = storage; - this.http = http; - - this.readFromStorage(); - - this.timer = setInterval(() => { - this.postToServer(); - }, 10000); - } - - public trackEvent(name: string) { - this.readFromStorage(); - this.trackTo(this.events, name); - } - - public trackSuggestionEvent(name: string) { - this.readFromStorage(); - this.trackTo(this.suggestionEvents, name); - } - - public stop() { - if (this.timer) { - clearInterval(this.timer); - } - } - - private readFromStorage() { - const data = this.storage.get(STORAGE_KEY); - if (data && typeof data.events === 'object' && typeof data.suggestionEvents === 'object') { - this.events = data.events; - this.suggestionEvents = data.suggestionEvents; - } - } - - private async postToServer() { - this.readFromStorage(); - if (Object.keys(this.events).length || Object.keys(this.suggestionEvents).length) { - try { - await this.http.post(`${BASE_API_URL}/telemetry`, { - body: JSON.stringify({ - events: this.events, - suggestionEvents: this.suggestionEvents, - }), - }); - this.events = {}; - this.suggestionEvents = {}; - this.write(); - } catch (e) { - // Silent error because events will be reported during the next timer - } - } - } - - private trackTo(target: Record>, name: string) { - const date = moment() - .utc() - .format('YYYY-MM-DD'); - if (!target[date]) { - target[date] = { - [name]: 1, - }; - } else if (!target[date][name]) { - target[date][name] = 1; - } else { - target[date][name] += 1; - } - - this.write(); - } - - private write() { - this.storage.set(STORAGE_KEY, { events: this.events, suggestionEvents: this.suggestionEvents }); - } -} diff --git a/x-pack/legacy/plugins/lens/public/metric_visualization/index.ts b/x-pack/legacy/plugins/lens/public/metric_visualization/index.ts deleted file mode 100644 index 65f064258a5e2..0000000000000 --- a/x-pack/legacy/plugins/lens/public/metric_visualization/index.ts +++ /dev/null @@ -1,32 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { CoreSetup } from 'src/core/public'; -import { metricVisualization } from './metric_visualization'; -import { ExpressionsSetup } from '../../../../../../src/plugins/expressions/public'; -import { metricChart, getMetricChartRenderer } from './metric_expression'; -import { EditorFrameSetup, FormatFactory } from '../types'; - -export interface MetricVisualizationPluginSetupPlugins { - expressions: ExpressionsSetup; - formatFactory: Promise; - editorFrame: EditorFrameSetup; -} - -export class MetricVisualization { - constructor() {} - - setup( - _core: CoreSetup | null, - { expressions, formatFactory, editorFrame }: MetricVisualizationPluginSetupPlugins - ) { - expressions.registerFunction(() => metricChart); - - expressions.registerRenderer(() => getMetricChartRenderer(formatFactory)); - - editorFrame.registerVisualization(metricVisualization); - } -} diff --git a/x-pack/legacy/plugins/lens/public/plugin.tsx b/x-pack/legacy/plugins/lens/public/plugin.tsx deleted file mode 100644 index b426a12d07f9b..0000000000000 --- a/x-pack/legacy/plugins/lens/public/plugin.tsx +++ /dev/null @@ -1,203 +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 from 'react'; -import { FormattedMessage, I18nProvider } from '@kbn/i18n/react'; -import { HashRouter, Route, RouteComponentProps, Switch } from 'react-router-dom'; -import { render, unmountComponentAtNode } from 'react-dom'; -import rison, { RisonObject, RisonValue } from 'rison-node'; -import { isObject } from 'lodash'; - -import { AppMountParameters, CoreSetup, CoreStart } from 'src/core/public'; -import { DataPublicPluginSetup, DataPublicPluginStart } from 'src/plugins/data/public'; -import { EmbeddableSetup, EmbeddableStart } from 'src/plugins/embeddable/public'; -import { ExpressionsSetup, ExpressionsStart } from 'src/plugins/expressions/public'; -import { VisualizationsSetup } from 'src/plugins/visualizations/public'; -import { KibanaLegacySetup } from 'src/plugins/kibana_legacy/public'; -import { DashboardConstants } from '../../../../../src/plugins/dashboard/public'; -import { Storage } from '../../../../../src/plugins/kibana_utils/public'; -import { EditorFrameService } from './editor_frame_service'; -import { IndexPatternDatasource } from './indexpattern_datasource'; -import { addHelpMenuToAppChrome } from './help_menu_util'; -import { SavedObjectIndexStore } from './persistence'; -import { XyVisualization } from './xy_visualization'; -import { MetricVisualization } from './metric_visualization'; -import { DatatableVisualization } from './datatable_visualization'; -import { App } from './app_plugin'; -import { - LensReportManager, - setReportManager, - stopReportManager, - trackUiEvent, -} from './lens_ui_telemetry'; - -import { UiActionsStart } from '../../../../../src/plugins/ui_actions/public'; -import { NOT_INTERNATIONALIZED_PRODUCT_NAME } from '../../../../plugins/lens/common'; -import { addEmbeddableToDashboardUrl, getUrlVars } from './helpers'; -import { EditorFrameStart } from './types'; -import { getLensAliasConfig } from './vis_type_alias'; - -export interface LensPluginSetupDependencies { - kibanaLegacy: KibanaLegacySetup; - expressions: ExpressionsSetup; - data: DataPublicPluginSetup; - embeddable: EmbeddableSetup; - visualizations: VisualizationsSetup; -} - -export interface LensPluginStartDependencies { - data: DataPublicPluginStart; - embeddable: EmbeddableStart; - expressions: ExpressionsStart; - uiActions: UiActionsStart; -} - -export const isRisonObject = (value: RisonValue): value is RisonObject => { - return isObject(value); -}; -export class LensPlugin { - private datatableVisualization: DatatableVisualization; - private editorFrameService: EditorFrameService; - private createEditorFrame: EditorFrameStart['createInstance'] | null = null; - private indexpatternDatasource: IndexPatternDatasource; - private xyVisualization: XyVisualization; - private metricVisualization: MetricVisualization; - - constructor() { - this.datatableVisualization = new DatatableVisualization(); - this.editorFrameService = new EditorFrameService(); - this.indexpatternDatasource = new IndexPatternDatasource(); - this.xyVisualization = new XyVisualization(); - this.metricVisualization = new MetricVisualization(); - } - - setup( - core: CoreSetup, - { kibanaLegacy, expressions, data, embeddable, visualizations }: LensPluginSetupDependencies - ) { - const editorFrameSetupInterface = this.editorFrameService.setup(core, { - data, - embeddable, - expressions, - }); - const dependencies = { - expressions, - data, - editorFrame: editorFrameSetupInterface, - formatFactory: core - .getStartServices() - .then(([_, { data: dataStart }]) => dataStart.fieldFormats.deserialize), - }; - this.indexpatternDatasource.setup(core, dependencies); - this.xyVisualization.setup(core, dependencies); - this.datatableVisualization.setup(core, dependencies); - this.metricVisualization.setup(core, dependencies); - - visualizations.registerAlias(getLensAliasConfig()); - - kibanaLegacy.registerLegacyApp({ - id: 'lens', - title: NOT_INTERNATIONALIZED_PRODUCT_NAME, - mount: async (params: AppMountParameters) => { - const [coreStart, startDependencies] = await core.getStartServices(); - const dataStart = startDependencies.data; - const savedObjectsClient = coreStart.savedObjects.client; - addHelpMenuToAppChrome(coreStart.chrome); - - const instance = await this.createEditorFrame!(); - - setReportManager( - new LensReportManager({ - storage: new Storage(localStorage), - http: core.http, - }) - ); - const updateUrlTime = (urlVars: Record): void => { - const decoded = rison.decode(urlVars._g); - if (!isRisonObject(decoded)) { - return; - } - // @ts-ignore - decoded.time = dataStart.query.timefilter.timefilter.getTime(); - urlVars._g = rison.encode(decoded); - }; - const redirectTo = ( - routeProps: RouteComponentProps<{ id?: string }>, - addToDashboardMode: boolean, - id?: string - ) => { - if (!id) { - routeProps.history.push('/lens'); - } else if (!addToDashboardMode) { - routeProps.history.push(`/lens/edit/${id}`); - } else if (addToDashboardMode && id) { - routeProps.history.push(`/lens/edit/${id}`); - const lastDashboardLink = coreStart.chrome.navLinks.get('kibana:dashboard'); - if (!lastDashboardLink || !lastDashboardLink.url) { - throw new Error('Cannot get last dashboard url'); - } - const urlVars = getUrlVars(lastDashboardLink.url); - updateUrlTime(urlVars); // we need to pass in timerange in query params directly - const dashboardUrl = addEmbeddableToDashboardUrl(lastDashboardLink.url, id, urlVars); - window.history.pushState({}, '', dashboardUrl); - } - }; - - const renderEditor = (routeProps: RouteComponentProps<{ id?: string }>) => { - trackUiEvent('loaded'); - const addToDashboardMode = - !!routeProps.location.search && - routeProps.location.search.includes( - DashboardConstants.ADD_VISUALIZATION_TO_DASHBOARD_MODE_PARAM - ); - return ( - redirectTo(routeProps, addToDashboardMode, id)} - addToDashboardMode={addToDashboardMode} - /> - ); - }; - - function NotFound() { - trackUiEvent('loaded_404'); - return ; - } - - render( - - - - - - - - - , - params.element - ); - return () => { - instance.unmount(); - unmountComponentAtNode(params.element); - }; - }, - }); - } - - start(core: CoreStart, startDependencies: LensPluginStartDependencies) { - this.createEditorFrame = this.editorFrameService.start(core, startDependencies).createInstance; - this.xyVisualization.start(core, startDependencies); - } - - stop() { - stopReportManager(); - } -} diff --git a/x-pack/legacy/plugins/lens/public/redirect.ts b/x-pack/legacy/plugins/lens/public/redirect.ts deleted file mode 100644 index 25b0188214c5e..0000000000000 --- a/x-pack/legacy/plugins/lens/public/redirect.ts +++ /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. - */ - -// This file redirects lens urls starting with app/lens#... to their counterpart on app/kibana#lens/... to -// make sure it's compatible with the 7.5 release - -import { npSetup } from 'ui/new_platform'; -import chrome from 'ui/chrome'; - -chrome.setRootController('lens', () => { - // prefix the path in the hash with lens/ - const prefixedHashRoute = window.location.hash.replace(/^#\//, '#/lens/'); - - // redirect to the new lens url `app/kibana#/lens/...` - window.location.href = npSetup.core.http.basePath.prepend('/app/kibana' + prefixedHashRoute); -}); diff --git a/x-pack/legacy/plugins/lens/public/types.ts b/x-pack/legacy/plugins/lens/public/types.ts deleted file mode 100644 index 3d67b7b2da460..0000000000000 --- a/x-pack/legacy/plugins/lens/public/types.ts +++ /dev/null @@ -1,425 +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 { Ast } from '@kbn/interpreter/common'; -import { IconType } from '@elastic/eui/src/components/icon/icon'; -import { CoreSetup } from 'src/core/public'; -import { - KibanaDatatable, - SerializedFieldFormat, -} from '../../../../../src/plugins/expressions/public'; -import { DragContextState } from './drag_drop'; -import { Document } from './persistence'; -import { DateRange } from '../../../../plugins/lens/common'; -import { Query, Filter, SavedQuery, IFieldFormat } from '../../../../../src/plugins/data/public'; - -export type ErrorCallback = (e: { message: string }) => void; - -export type FormatFactory = (mapping?: SerializedFieldFormat) => IFieldFormat; - -export interface PublicAPIProps { - state: T; - layerId: string; - dateRange: DateRange; -} - -export interface EditorFrameProps { - onError: ErrorCallback; - doc?: Document; - dateRange: DateRange; - query: Query; - filters: Filter[]; - savedQuery?: SavedQuery; - - // Frame loader (app or embeddable) is expected to call this when it loads and updates - // This should be replaced with a top-down state - onChange: (newState: { - filterableIndexPatterns: DatasourceMetaData['filterableIndexPatterns']; - doc: Document; - }) => void; -} -export interface EditorFrameInstance { - mount: (element: Element, props: EditorFrameProps) => void; - unmount: () => void; -} - -export interface EditorFrameSetup { - // generic type on the API functions to pull the "unknown vs. specific type" error into the implementation - registerDatasource: (datasource: Datasource | Promise>) => void; - registerVisualization: ( - visualization: Visualization | Promise> - ) => void; -} - -export interface EditorFrameStart { - createInstance: () => Promise; -} - -// Hints the default nesting to the data source. 0 is the highest priority -export type DimensionPriority = 0 | 1 | 2; - -export interface TableSuggestionColumn { - columnId: string; - operation: Operation; -} - -/** - * A possible table a datasource can create. This object is passed to the visualization - * which tries to build a meaningful visualization given the shape of the table. If this - * is possible, the visualization returns a `VisualizationSuggestion` object - */ -export interface TableSuggestion { - /** - * Flag indicating whether the table will include more than one column. - * This is not the case for example for a single metric aggregation - * */ - isMultiRow: boolean; - /** - * The columns of the table. Each column has to be mapped to a dimension in a chart. If a visualization - * can't use all columns of a suggestion, it should not return a `VisualizationSuggestion` based on it - * because there would be unreferenced columns - */ - columns: TableSuggestionColumn[]; - /** - * The layer this table will replace. This is only relevant if the visualization this suggestion is passed - * is currently active and has multiple layers configured. If this suggestion is applied, the table of this - * layer will be replaced by the columns specified in this suggestion - */ - layerId: string; - /** - * A label describing the table. This can be used to provide a title for the `VisualizationSuggestion`, - * but the visualization can also decide to overwrite it. - */ - label?: string; - /** - * The change type indicates what was changed in this table compared to the currently active table of this layer. - */ - changeType: TableChangeType; -} - -/** - * Indicates what was changed in this table compared to the currently active table of this layer. - * * `initial` means the layer associated with this table does not exist in the current configuration - * * `unchanged` means the table is the same in the currently active configuration - * * `reduced` means the table is a reduced version of the currently active table (some columns dropped, but not all of them) - * * `extended` means the table is an extended version of the currently active table (added one or multiple additional columns) - * * `layers` means the change is a change to the layer structure, not to the table - */ -export type TableChangeType = 'initial' | 'unchanged' | 'reduced' | 'extended' | 'layers'; - -export interface DatasourceSuggestion { - state: T; - table: TableSuggestion; - keptLayerIds: string[]; -} - -export interface DatasourceMetaData { - filterableIndexPatterns: Array<{ id: string; title: string }>; -} - -export type StateSetter = (newState: T | ((prevState: T) => T)) => void; - -/** - * Interface for the datasource registry - */ -export interface Datasource { - id: string; - - // For initializing, either from an empty state or from persisted state - // Because this will be called at runtime, state might have a type of `any` and - // datasources should validate their arguments - initialize: (state?: P) => Promise; - - // Given the current state, which parts should be saved? - getPersistableState: (state: T) => P; - - insertLayer: (state: T, newLayerId: string) => T; - removeLayer: (state: T, layerId: string) => T; - clearLayer: (state: T, layerId: string) => T; - getLayers: (state: T) => string[]; - removeColumn: (props: { prevState: T; layerId: string; columnId: string }) => T; - - renderDataPanel: (domElement: Element, props: DatasourceDataPanelProps) => void; - renderDimensionTrigger: (domElement: Element, props: DatasourceDimensionTriggerProps) => void; - renderDimensionEditor: (domElement: Element, props: DatasourceDimensionEditorProps) => void; - renderLayerPanel: (domElement: Element, props: DatasourceLayerPanelProps) => void; - canHandleDrop: (props: DatasourceDimensionDropProps) => boolean; - onDrop: (props: DatasourceDimensionDropHandlerProps) => boolean; - - toExpression: (state: T, layerId: string) => Ast | string | null; - - getMetaData: (state: T) => DatasourceMetaData; - - getDatasourceSuggestionsForField: (state: T, field: unknown) => Array>; - getDatasourceSuggestionsFromCurrentState: (state: T) => Array>; - - getPublicAPI: (props: PublicAPIProps) => DatasourcePublicAPI; -} - -/** - * This is an API provided to visualizations by the frame, which calls the publicAPI on the datasource - */ -export interface DatasourcePublicAPI { - datasourceId: string; - getTableSpec: () => Array<{ columnId: string }>; - getOperationForColumnId: (columnId: string) => Operation | null; -} - -export interface DatasourceDataPanelProps { - state: T; - dragDropContext: DragContextState; - setState: StateSetter; - core: Pick; - query: Query; - dateRange: DateRange; - filters: Filter[]; -} - -interface SharedDimensionProps { - /** Visualizations can restrict operations based on their own rules. - * For example, limiting to only bucketed or only numeric operations. - */ - filterOperations: (operation: OperationMetadata) => boolean; - - /** Visualizations can hint at the role this dimension would play, which - * affects the default ordering of the query - */ - suggestedPriority?: DimensionPriority; - - /** Some dimension editors will allow users to change the operation grouping - * from the panel, and this lets the visualization hint that it doesn't want - * users to have that level of control - */ - hideGrouping?: boolean; -} - -export type DatasourceDimensionProps = SharedDimensionProps & { - layerId: string; - columnId: string; - onRemove?: (accessor: string) => void; - state: T; -}; - -// The only way a visualization has to restrict the query building -export type DatasourceDimensionEditorProps = DatasourceDimensionProps & { - setState: StateSetter; - core: Pick; - dateRange: DateRange; -}; - -export type DatasourceDimensionTriggerProps = DatasourceDimensionProps & { - dragDropContext: DragContextState; - togglePopover: () => void; -}; - -export interface DatasourceLayerPanelProps { - layerId: string; - state: T; - setState: StateSetter; -} - -export type DatasourceDimensionDropProps = SharedDimensionProps & { - layerId: string; - columnId: string; - state: T; - setState: StateSetter; - dragDropContext: DragContextState; -}; - -export type DatasourceDimensionDropHandlerProps = DatasourceDimensionDropProps & { - droppedItem: unknown; -}; - -export type DataType = 'document' | 'string' | 'number' | 'date' | 'boolean' | 'ip'; - -// An operation represents a column in a table, not any information -// about how the column was created such as whether it is a sum or average. -// Visualizations are able to filter based on the output, not based on the -// underlying data -export interface Operation extends OperationMetadata { - // User-facing label for the operation - label: string; -} - -export interface OperationMetadata { - // The output of this operation will have this data type - dataType: DataType; - // A bucketed operation is grouped by duplicate values, otherwise each row is - // treated as unique - isBucketed: boolean; - scale?: 'ordinal' | 'interval' | 'ratio'; - // Extra meta-information like cardinality, color - // TODO currently it's not possible to differentiate between a field from a raw - // document and an aggregated metric which might be handy in some cases. Once we - // introduce a raw document datasource, this should be considered here. -} - -export interface LensMultiTable { - type: 'lens_multitable'; - tables: Record; - dateRange?: { - fromDate: Date; - toDate: Date; - }; -} - -export interface VisualizationConfigProps { - layerId: string; - frame: FramePublicAPI; - state: T; -} - -export type VisualizationLayerWidgetProps = VisualizationConfigProps & { - setState: (newState: T) => void; -}; - -type VisualizationDimensionGroupConfig = SharedDimensionProps & { - groupLabel: string; - - /** ID is passed back to visualization. For example, `x` */ - groupId: string; - accessors: string[]; - supportsMoreColumns: boolean; - /** If required, a warning will appear if accessors are empty */ - required?: boolean; - dataTestSubj?: string; -}; - -interface VisualizationDimensionChangeProps { - layerId: string; - columnId: string; - prevState: T; -} - -/** - * Object passed to `getSuggestions` of a visualization. - * It contains a possible table the current datasource could - * provide and the state of the visualization if it is currently active. - * - * If the current datasource suggests multiple tables, `getSuggestions` - * is called multiple times with separate `SuggestionRequest` objects. - */ -export interface SuggestionRequest { - /** - * A table configuration the datasource could provide. - */ - table: TableSuggestion; - /** - * State is only passed if the visualization is active. - */ - state?: T; - /** - * The visualization needs to know which table is being suggested - */ - keptLayerIds: string[]; -} - -/** - * A possible configuration of a given visualization. It is based on a `TableSuggestion`. - * Suggestion might be shown in the UI to be chosen by the user directly, but they are - * also applied directly under some circumstances (dragging in the first field from the data - * panel or switching to another visualization in the chart switcher). - */ -export interface VisualizationSuggestion { - /** - * The score of a suggestion should indicate how valuable the suggestion is. It is used - * to rank multiple suggestions of multiple visualizations. The number should be between 0 and 1 - */ - score: number; - /** - * Flag indicating whether this suggestion should not be advertised to the user. It is still - * considered in scenarios where the available suggestion with the highest suggestion is applied - * directly. - */ - hide?: boolean; - /** - * Descriptive title of the suggestion. Should be as short as possible. This title is shown if - * the suggestion is advertised to the user and will also show either the `previewExpression` or - * the `previewIcon` - */ - title: string; - /** - * The new state of the visualization if this suggestion is applied. - */ - state: T; - /** - * An EUI icon type shown instead of the preview expression. - */ - previewIcon: IconType; -} - -export interface FramePublicAPI { - datasourceLayers: Record; - - dateRange: DateRange; - query: Query; - filters: Filter[]; - - // Adds a new layer. This has a side effect of updating the datasource state - addNewLayer: () => string; - removeLayers: (layerIds: string[]) => void; -} - -export interface VisualizationType { - id: string; - icon?: IconType; - largeIcon?: IconType; - label: string; -} - -export interface Visualization { - id: string; - - visualizationTypes: VisualizationType[]; - - getLayerIds: (state: T) => string[]; - clearLayer: (state: T, layerId: string) => T; - removeLayer?: (state: T, layerId: string) => T; - appendLayer?: (state: T, layerId: string) => T; - - // Layer context menu is used by visualizations for styling the entire layer - // For example, the XY visualization uses this to have multiple chart types - getLayerContextMenuIcon?: (opts: { state: T; layerId: string }) => IconType | undefined; - renderLayerContextMenu?: (domElement: Element, props: VisualizationLayerWidgetProps) => void; - - getConfiguration: ( - props: VisualizationConfigProps - ) => { groups: VisualizationDimensionGroupConfig[] }; - - getDescription: ( - state: T - ) => { - icon?: IconType; - label: string; - }; - - switchVisualizationType?: (visualizationTypeId: string, state: T) => T; - - // For initializing from saved object - initialize: (frame: FramePublicAPI, state?: P) => T; - - getPersistableState: (state: T) => P; - - // Actions triggered by the frame which tell the datasource that a dimension is being changed - setDimension: ( - props: VisualizationDimensionChangeProps & { - groupId: string; - } - ) => T; - removeDimension: (props: VisualizationDimensionChangeProps) => T; - - toExpression: (state: T, frame: FramePublicAPI) => Ast | string | null; - - /** - * Epression to render a preview version of the chart in very constraint space. - * If there is no expression provided, the preview icon is used. - */ - toPreviewExpression?: (state: T, frame: FramePublicAPI) => Ast | string | null; - - // The frame will call this function on all visualizations when the table changes, or when - // rendering additional ways of using the data - getSuggestions: (context: SuggestionRequest) => Array>; -} diff --git a/x-pack/legacy/plugins/lens/public/xy_visualization/_index.scss b/x-pack/legacy/plugins/lens/public/xy_visualization/_index.scss deleted file mode 100644 index 794ed4aed82ec..0000000000000 --- a/x-pack/legacy/plugins/lens/public/xy_visualization/_index.scss +++ /dev/null @@ -1 +0,0 @@ -@import './_xy_expression'; diff --git a/x-pack/legacy/plugins/lens/public/xy_visualization/index.ts b/x-pack/legacy/plugins/lens/public/xy_visualization/index.ts deleted file mode 100644 index 8cc5abb44d6e1..0000000000000 --- a/x-pack/legacy/plugins/lens/public/xy_visualization/index.ts +++ /dev/null @@ -1,64 +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 { EUI_CHARTS_THEME_DARK, EUI_CHARTS_THEME_LIGHT } from '@elastic/eui/dist/eui_charts_theme'; -import { CoreSetup, IUiSettingsClient, CoreStart } from 'src/core/public'; -import moment from 'moment-timezone'; -import { ExpressionsSetup } from '../../../../../../src/plugins/expressions/public'; -import { xyVisualization } from './xy_visualization'; -import { xyChart, getXyChartRenderer } from './xy_expression'; -import { legendConfig, xConfig, layerConfig } from './types'; -import { EditorFrameSetup, FormatFactory } from '../types'; -import { UiActionsStart } from '../../../../../../src/plugins/ui_actions/public'; -import { setExecuteTriggerActions } from './services'; - -export interface XyVisualizationPluginSetupPlugins { - expressions: ExpressionsSetup; - formatFactory: Promise; - editorFrame: EditorFrameSetup; -} - -interface XyVisualizationPluginStartPlugins { - uiActions: UiActionsStart; -} - -function getTimeZone(uiSettings: IUiSettingsClient) { - const configuredTimeZone = uiSettings.get('dateFormat:tz'); - if (configuredTimeZone === 'Browser') { - return moment.tz.guess(); - } - - return configuredTimeZone; -} - -export class XyVisualization { - constructor() {} - - setup( - core: CoreSetup, - { expressions, formatFactory, editorFrame }: XyVisualizationPluginSetupPlugins - ) { - expressions.registerFunction(() => legendConfig); - expressions.registerFunction(() => xConfig); - expressions.registerFunction(() => layerConfig); - expressions.registerFunction(() => xyChart); - - expressions.registerRenderer( - getXyChartRenderer({ - formatFactory, - chartTheme: core.uiSettings.get('theme:darkMode') - ? EUI_CHARTS_THEME_DARK.theme - : EUI_CHARTS_THEME_LIGHT.theme, - timeZone: getTimeZone(core.uiSettings), - }) - ); - - editorFrame.registerVisualization(xyVisualization); - } - start(core: CoreStart, { uiActions }: XyVisualizationPluginStartPlugins) { - setExecuteTriggerActions(uiActions.executeTriggerActions); - } -} diff --git a/x-pack/legacy/plugins/lens/public/xy_visualization/services.ts b/x-pack/legacy/plugins/lens/public/xy_visualization/services.ts deleted file mode 100644 index af683efb86534..0000000000000 --- a/x-pack/legacy/plugins/lens/public/xy_visualization/services.ts +++ /dev/null @@ -1,12 +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 { createGetterSetter } from '../../../../../../src/plugins/kibana_utils/public'; -import { UiActionsStart } from '../../../../../../src/plugins/ui_actions/public'; - -export const [getExecuteTriggerActions, setExecuteTriggerActions] = createGetterSetter< - UiActionsStart['executeTriggerActions'] ->('executeTriggerActions'); diff --git a/x-pack/legacy/plugins/lens/public/xy_visualization/types.ts b/x-pack/legacy/plugins/lens/public/xy_visualization/types.ts deleted file mode 100644 index f7b4afc76ec4b..0000000000000 --- a/x-pack/legacy/plugins/lens/public/xy_visualization/types.ts +++ /dev/null @@ -1,282 +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 { Position } from '@elastic/charts'; -import { i18n } from '@kbn/i18n'; -import { ArgumentType, ExpressionFunctionDefinition } from 'src/plugins/expressions/common'; -import chartAreaSVG from '../assets/chart_area.svg'; -import chartAreaStackedSVG from '../assets/chart_area_stacked.svg'; -import chartBarSVG from '../assets/chart_bar.svg'; -import chartBarStackedSVG from '../assets/chart_bar_stacked.svg'; -import chartBarHorizontalSVG from '../assets/chart_bar_horizontal.svg'; -import chartBarHorizontalStackedSVG from '../assets/chart_bar_horizontal_stacked.svg'; -import chartLineSVG from '../assets/chart_line.svg'; - -import { VisualizationType } from '..'; - -export interface LegendConfig { - isVisible: boolean; - position: Position; -} - -type LegendConfigResult = LegendConfig & { type: 'lens_xy_legendConfig' }; - -export const legendConfig: ExpressionFunctionDefinition< - 'lens_xy_legendConfig', - null, - LegendConfig, - LegendConfigResult -> = { - name: 'lens_xy_legendConfig', - aliases: [], - type: 'lens_xy_legendConfig', - help: `Configure the xy chart's legend`, - inputTypes: ['null'], - args: { - isVisible: { - types: ['boolean'], - help: i18n.translate('xpack.lens.xyChart.isVisible.help', { - defaultMessage: 'Specifies whether or not the legend is visible.', - }), - }, - position: { - types: ['string'], - options: [Position.Top, Position.Right, Position.Bottom, Position.Left], - help: i18n.translate('xpack.lens.xyChart.position.help', { - defaultMessage: 'Specifies the legend position.', - }), - }, - }, - fn: function fn(input: unknown, args: LegendConfig) { - return { - type: 'lens_xy_legendConfig', - ...args, - }; - }, -}; - -interface AxisConfig { - title: string; - hide?: boolean; -} - -const axisConfig: { [key in keyof AxisConfig]: ArgumentType } = { - title: { - types: ['string'], - help: i18n.translate('xpack.lens.xyChart.title.help', { - defaultMessage: 'The axis title', - }), - }, - hide: { - types: ['boolean'], - default: false, - help: 'Show / hide axis', - }, -}; - -export interface YState extends AxisConfig { - accessors: string[]; -} - -export interface XConfig extends AxisConfig { - accessor: string; -} - -type XConfigResult = XConfig & { type: 'lens_xy_xConfig' }; - -export const xConfig: ExpressionFunctionDefinition< - 'lens_xy_xConfig', - null, - XConfig, - XConfigResult -> = { - name: 'lens_xy_xConfig', - aliases: [], - type: 'lens_xy_xConfig', - help: `Configure the xy chart's x axis`, - inputTypes: ['null'], - args: { - ...axisConfig, - accessor: { - types: ['string'], - help: 'The column to display on the x axis.', - }, - }, - fn: function fn(input: unknown, args: XConfig) { - return { - type: 'lens_xy_xConfig', - ...args, - }; - }, -}; - -type LayerConfigResult = LayerArgs & { type: 'lens_xy_layer' }; - -export const layerConfig: ExpressionFunctionDefinition< - 'lens_xy_layer', - null, - LayerArgs, - LayerConfigResult -> = { - name: 'lens_xy_layer', - aliases: [], - type: 'lens_xy_layer', - help: `Configure a layer in the xy chart`, - inputTypes: ['null'], - args: { - ...axisConfig, - layerId: { - types: ['string'], - help: '', - }, - xAccessor: { - types: ['string'], - help: '', - }, - seriesType: { - types: ['string'], - options: ['bar', 'line', 'area', 'bar_stacked', 'area_stacked'], - help: 'The type of chart to display.', - }, - xScaleType: { - options: ['ordinal', 'linear', 'time'], - help: 'The scale type of the x axis', - default: 'ordinal', - }, - isHistogram: { - types: ['boolean'], - default: false, - help: 'Whether to layout the chart as a histogram', - }, - yScaleType: { - options: ['log', 'sqrt', 'linear', 'time'], - help: 'The scale type of the y axes', - default: 'linear', - }, - splitAccessor: { - types: ['string'], - help: 'The column to split by', - multi: false, - }, - accessors: { - types: ['string'], - help: 'The columns to display on the y axis.', - multi: true, - }, - columnToLabel: { - types: ['string'], - help: 'JSON key-value pairs of column ID to label', - }, - }, - fn: function fn(input: unknown, args: LayerArgs) { - return { - type: 'lens_xy_layer', - ...args, - }; - }, -}; - -export type SeriesType = - | 'bar' - | 'bar_horizontal' - | 'line' - | 'area' - | 'bar_stacked' - | 'bar_horizontal_stacked' - | 'area_stacked'; - -export interface LayerConfig { - hide?: boolean; - layerId: string; - xAccessor?: string; - accessors: string[]; - seriesType: SeriesType; - splitAccessor?: string; -} - -export type LayerArgs = LayerConfig & { - columnToLabel?: string; // Actually a JSON key-value pair - yScaleType: 'time' | 'linear' | 'log' | 'sqrt'; - xScaleType: 'time' | 'linear' | 'ordinal'; - isHistogram: boolean; -}; - -// Arguments to XY chart expression, with computed properties -export interface XYArgs { - xTitle: string; - yTitle: string; - legend: LegendConfig & { type: 'lens_xy_legendConfig' }; - layers: LayerArgs[]; -} - -// Persisted parts of the state -export interface XYState { - preferredSeriesType: SeriesType; - legend: LegendConfig; - layers: LayerConfig[]; -} - -export type State = XYState; -export type PersistableState = XYState; - -export const visualizationTypes: VisualizationType[] = [ - { - id: 'bar', - icon: 'visBarVertical', - largeIcon: chartBarSVG, - label: i18n.translate('xpack.lens.xyVisualization.barLabel', { - defaultMessage: 'Bar', - }), - }, - { - id: 'bar_horizontal', - icon: 'visBarHorizontal', - largeIcon: chartBarHorizontalSVG, - label: i18n.translate('xpack.lens.xyVisualization.barHorizontalLabel', { - defaultMessage: 'Horizontal bar', - }), - }, - { - id: 'bar_stacked', - icon: 'visBarVerticalStacked', - largeIcon: chartBarStackedSVG, - label: i18n.translate('xpack.lens.xyVisualization.stackedBarLabel', { - defaultMessage: 'Stacked bar', - }), - }, - { - id: 'bar_horizontal_stacked', - icon: 'visBarHorizontalStacked', - largeIcon: chartBarHorizontalStackedSVG, - label: i18n.translate('xpack.lens.xyVisualization.stackedBarHorizontalLabel', { - defaultMessage: 'Stacked horizontal bar', - }), - }, - { - id: 'line', - icon: 'visLine', - largeIcon: chartLineSVG, - label: i18n.translate('xpack.lens.xyVisualization.lineLabel', { - defaultMessage: 'Line', - }), - }, - { - id: 'area', - icon: 'visArea', - largeIcon: chartAreaSVG, - label: i18n.translate('xpack.lens.xyVisualization.areaLabel', { - defaultMessage: 'Area', - }), - }, - { - id: 'area_stacked', - icon: 'visAreaStacked', - largeIcon: chartAreaStackedSVG, - label: i18n.translate('xpack.lens.xyVisualization.stackedAreaLabel', { - defaultMessage: 'Stacked area', - }), - }, -]; diff --git a/x-pack/legacy/plugins/lens/public/xy_visualization/xy_expression.test.tsx b/x-pack/legacy/plugins/lens/public/xy_visualization/xy_expression.test.tsx deleted file mode 100644 index 54abc2c2bb667..0000000000000 --- a/x-pack/legacy/plugins/lens/public/xy_visualization/xy_expression.test.tsx +++ /dev/null @@ -1,815 +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 { - AreaSeries, - Axis, - BarSeries, - Position, - LineSeries, - Settings, - ScaleType, - GeometryValue, - XYChartSeriesIdentifier, - SeriesNameFn, -} from '@elastic/charts'; -import { xyChart, XYChart } from './xy_expression'; -import { LensMultiTable } from '../types'; -import { - KibanaDatatable, - KibanaDatatableRow, -} from '../../../../../../src/plugins/expressions/public'; -import React from 'react'; -import { shallow } from 'enzyme'; -import { XYArgs, LegendConfig, legendConfig, layerConfig, LayerArgs } from './types'; -import { createMockExecutionContext } from '../../../../../../src/plugins/expressions/common/mocks'; -import { mountWithIntl } from 'test_utils/enzyme_helpers'; - -const executeTriggerActions = jest.fn(); - -const createSampleDatatableWithRows = (rows: KibanaDatatableRow[]): KibanaDatatable => ({ - type: 'kibana_datatable', - columns: [ - { - id: 'a', - name: 'a', - formatHint: { id: 'number', params: { pattern: '0,0.000' } }, - }, - { id: 'b', name: 'b', formatHint: { id: 'number', params: { pattern: '000,0' } } }, - { - id: 'c', - name: 'c', - formatHint: { id: 'string' }, - meta: { type: 'date-histogram', aggConfigParams: { interval: '10s' } }, - }, - { id: 'd', name: 'ColD', formatHint: { id: 'string' } }, - ], - rows, -}); - -const sampleLayer: LayerArgs = { - layerId: 'first', - seriesType: 'line', - xAccessor: 'c', - accessors: ['a', 'b'], - splitAccessor: 'd', - columnToLabel: '{"a": "Label A", "b": "Label B", "d": "Label D"}', - xScaleType: 'ordinal', - yScaleType: 'linear', - isHistogram: false, -}; - -const createArgsWithLayers = (layers: LayerArgs[] = [sampleLayer]): XYArgs => ({ - xTitle: '', - yTitle: '', - legend: { - type: 'lens_xy_legendConfig', - isVisible: false, - position: Position.Top, - }, - layers, -}); - -function sampleArgs() { - const data: LensMultiTable = { - type: 'lens_multitable', - tables: { - first: createSampleDatatableWithRows([ - { a: 1, b: 2, c: 'I', d: 'Foo' }, - { a: 1, b: 5, c: 'J', d: 'Bar' }, - ]), - }, - }; - - const args: XYArgs = createArgsWithLayers(); - - return { data, args }; -} - -describe('xy_expression', () => { - describe('configs', () => { - test('legendConfig produces the correct arguments', () => { - const args: LegendConfig = { - isVisible: true, - position: Position.Left, - }; - - const result = legendConfig.fn(null, args, createMockExecutionContext()); - - expect(result).toEqual({ - type: 'lens_xy_legendConfig', - ...args, - }); - }); - - test('layerConfig produces the correct arguments', () => { - const args: LayerArgs = { - layerId: 'first', - seriesType: 'line', - xAccessor: 'c', - accessors: ['a', 'b'], - splitAccessor: 'd', - xScaleType: 'linear', - yScaleType: 'linear', - isHistogram: false, - }; - - const result = layerConfig.fn(null, args, createMockExecutionContext()); - - expect(result).toEqual({ - type: 'lens_xy_layer', - ...args, - }); - }); - }); - - describe('xyChart', () => { - test('it renders with the specified data and args', () => { - const { data, args } = sampleArgs(); - const result = xyChart.fn(data, args, createMockExecutionContext()); - - expect(result).toEqual({ - type: 'render', - as: 'lens_xy_chart_renderer', - value: { data, args }, - }); - }); - }); - - describe('XYChart component', () => { - let getFormatSpy: jest.Mock; - let convertSpy: jest.Mock; - - beforeEach(() => { - convertSpy = jest.fn(x => x); - getFormatSpy = jest.fn(); - getFormatSpy.mockReturnValue({ convert: convertSpy }); - }); - - test('it renders line', () => { - const { data, args } = sampleArgs(); - - const component = shallow( - - ); - expect(component).toMatchSnapshot(); - expect(component.find(LineSeries)).toHaveLength(1); - }); - - describe('date range', () => { - const timeSampleLayer: LayerArgs = { - layerId: 'first', - seriesType: 'line', - xAccessor: 'c', - accessors: ['a', 'b'], - splitAccessor: 'd', - columnToLabel: '{"a": "Label A", "b": "Label B", "d": "Label D"}', - xScaleType: 'time', - yScaleType: 'linear', - isHistogram: false, - }; - const multiLayerArgs = createArgsWithLayers([ - timeSampleLayer, - { - ...timeSampleLayer, - layerId: 'second', - seriesType: 'bar', - xScaleType: 'time', - }, - ]); - test('it uses the full date range', () => { - const { data, args } = sampleArgs(); - - const component = shallow( - - ); - expect(component.find(Settings).prop('xDomain')).toMatchInlineSnapshot(` - Object { - "max": 1546491600000, - "min": 1546405200000, - "minInterval": undefined, - } - `); - }); - - test('it generates correct xDomain for a layer with single value and a layer with no data (1-0) ', () => { - const data: LensMultiTable = { - type: 'lens_multitable', - tables: { - first: createSampleDatatableWithRows([{ a: 1, b: 2, c: 'I', d: 'Foo' }]), - second: createSampleDatatableWithRows([]), - }, - }; - - const component = shallow( - - ); - - expect(component.find(Settings).prop('xDomain')).toMatchInlineSnapshot(` - Object { - "max": 1546491600000, - "min": 1546405200000, - "minInterval": 10000, - } - `); - }); - - test('it generates correct xDomain for two layers with single value(1-1)', () => { - const data: LensMultiTable = { - type: 'lens_multitable', - tables: { - first: createSampleDatatableWithRows([{ a: 1, b: 2, c: 'I', d: 'Foo' }]), - second: createSampleDatatableWithRows([{ a: 10, b: 5, c: 'J', d: 'Bar' }]), - }, - }; - const component = shallow( - - ); - - expect(component.find(Settings).prop('xDomain')).toMatchInlineSnapshot(` - Object { - "max": 1546491600000, - "min": 1546405200000, - "minInterval": 10000, - } - `); - }); - test('it generates correct xDomain for a layer with single value and layer with multiple value data (1-n)', () => { - const data: LensMultiTable = { - type: 'lens_multitable', - tables: { - first: createSampleDatatableWithRows([{ a: 1, b: 2, c: 'I', d: 'Foo' }]), - second: createSampleDatatableWithRows([ - { a: 10, b: 5, c: 'J', d: 'Bar' }, - { a: 8, b: 5, c: 'K', d: 'Buzz' }, - ]), - }, - }; - const component = shallow( - - ); - - expect(component.find(Settings).prop('xDomain')).toMatchInlineSnapshot(` - Object { - "max": 1546491600000, - "min": 1546405200000, - "minInterval": undefined, - } - `); - }); - - test('it generates correct xDomain for 2 layers with multiple value data (n-n)', () => { - const data: LensMultiTable = { - type: 'lens_multitable', - tables: { - first: createSampleDatatableWithRows([ - { a: 1, b: 2, c: 'I', d: 'Foo' }, - { a: 8, b: 5, c: 'K', d: 'Buzz' }, - { a: 9, b: 7, c: 'L', d: 'Bar' }, - { a: 10, b: 2, c: 'G', d: 'Bear' }, - ]), - second: createSampleDatatableWithRows([ - { a: 10, b: 5, c: 'J', d: 'Bar' }, - { a: 8, b: 4, c: 'K', d: 'Fi' }, - { a: 1, b: 8, c: 'O', d: 'Pi' }, - ]), - }, - }; - const component = shallow( - - ); - - expect(component.find(Settings).prop('xDomain')).toMatchInlineSnapshot(` - Object { - "max": 1546491600000, - "min": 1546405200000, - "minInterval": undefined, - } - `); - }); - }); - - test('it does not use date range if the x is not a time scale', () => { - const { data, args } = sampleArgs(); - - const component = shallow( - - ); - expect(component.find(Settings).prop('xDomain')).toBeUndefined(); - }); - - test('it renders bar', () => { - const { data, args } = sampleArgs(); - const component = shallow( - - ); - expect(component).toMatchSnapshot(); - expect(component.find(BarSeries)).toHaveLength(1); - }); - - test('it renders area', () => { - const { data, args } = sampleArgs(); - const component = shallow( - - ); - expect(component).toMatchSnapshot(); - expect(component.find(AreaSeries)).toHaveLength(1); - }); - - test('it renders horizontal bar', () => { - const { data, args } = sampleArgs(); - const component = shallow( - - ); - expect(component).toMatchSnapshot(); - expect(component.find(BarSeries)).toHaveLength(1); - expect(component.find(Settings).prop('rotation')).toEqual(90); - }); - - test('onElementClick returns correct context data', () => { - const geometry: GeometryValue = { x: 5, y: 1, accessor: 'y1' }; - const series = { - key: 'spec{d}yAccessor{d}splitAccessors{b-2}', - specId: 'd', - yAccessor: 'd', - splitAccessors: {}, - seriesKeys: [2, 'd'], - }; - - const { args, data } = sampleArgs(); - - const wrapper = mountWithIntl( - - ); - - wrapper - .find(Settings) - .first() - .prop('onElementClick')!([[geometry, series as XYChartSeriesIdentifier]]); - - expect(executeTriggerActions).toHaveBeenCalledWith('VALUE_CLICK_TRIGGER', { - data: { - data: [ - { - column: 1, - row: 1, - table: data.tables.first, - value: 5, - }, - { - column: 1, - row: 0, - table: data.tables.first, - value: 2, - }, - ], - }, - }); - }); - - test('it renders stacked bar', () => { - const { data, args } = sampleArgs(); - const component = shallow( - - ); - expect(component).toMatchSnapshot(); - expect(component.find(BarSeries)).toHaveLength(1); - expect(component.find(BarSeries).prop('stackAccessors')).toHaveLength(1); - }); - - test('it renders stacked area', () => { - const { data, args } = sampleArgs(); - const component = shallow( - - ); - expect(component).toMatchSnapshot(); - expect(component.find(AreaSeries)).toHaveLength(1); - expect(component.find(AreaSeries).prop('stackAccessors')).toHaveLength(1); - }); - - test('it renders stacked horizontal bar', () => { - const { data, args } = sampleArgs(); - const component = shallow( - - ); - expect(component).toMatchSnapshot(); - expect(component.find(BarSeries)).toHaveLength(1); - expect(component.find(BarSeries).prop('stackAccessors')).toHaveLength(1); - expect(component.find(Settings).prop('rotation')).toEqual(90); - }); - - test('it passes time zone to the series', () => { - const { data, args } = sampleArgs(); - const component = shallow( - - ); - expect(component.find(LineSeries).prop('timeZone')).toEqual('CEST'); - }); - - test('it applies histogram mode to the series for single series', () => { - const { data, args } = sampleArgs(); - const firstLayer: LayerArgs = { ...args.layers[0], seriesType: 'bar', isHistogram: true }; - delete firstLayer.splitAccessor; - const component = shallow( - - ); - expect(component.find(BarSeries).prop('enableHistogramMode')).toEqual(true); - }); - - test('it applies histogram mode to the series for stacked series', () => { - const { data, args } = sampleArgs(); - const component = shallow( - - ); - expect(component.find(BarSeries).prop('enableHistogramMode')).toEqual(true); - }); - - test('it does not apply histogram mode for splitted series', () => { - const { data, args } = sampleArgs(); - const component = shallow( - - ); - expect(component.find(BarSeries).prop('enableHistogramMode')).toEqual(false); - }); - - test('it names the series for multiple accessors', () => { - const { data, args } = sampleArgs(); - - const component = shallow( - - ); - const nameFn = component.find(LineSeries).prop('name') as SeriesNameFn; - - expect( - nameFn( - { - seriesKeys: ['a', 'b', 'c', 'd'], - key: '', - specId: 'a', - yAccessor: '', - splitAccessors: new Map(), - }, - false - ) - ).toEqual('Label A - Label B - c - Label D'); - }); - - test('it names the series for a single accessor', () => { - const { data, args } = sampleArgs(); - - const component = shallow( - - ); - const nameFn = component.find(LineSeries).prop('name') as SeriesNameFn; - - expect( - nameFn( - { - seriesKeys: ['a', 'b', 'c', 'd'], - key: '', - specId: 'a', - yAccessor: '', - splitAccessors: new Map(), - }, - false - ) - ).toEqual('Label A'); - }); - - test('it set the scale of the x axis according to the args prop', () => { - const { data, args } = sampleArgs(); - - const component = shallow( - - ); - expect(component.find(LineSeries).prop('xScaleType')).toEqual(ScaleType.Ordinal); - }); - - test('it set the scale of the y axis according to the args prop', () => { - const { data, args } = sampleArgs(); - - const component = shallow( - - ); - expect(component.find(LineSeries).prop('yScaleType')).toEqual(ScaleType.Sqrt); - }); - - test('it gets the formatter for the x axis', () => { - const { data, args } = sampleArgs(); - - shallow( - - ); - - expect(getFormatSpy).toHaveBeenCalledWith({ id: 'string' }); - }); - - test('it gets a default formatter for y if there are multiple y accessors', () => { - const { data, args } = sampleArgs(); - - shallow( - - ); - - expect(getFormatSpy).toHaveBeenCalledWith({ id: 'number' }); - }); - - test('it gets the formatter for the y axis if there is only one accessor', () => { - const { data, args } = sampleArgs(); - - shallow( - - ); - expect(getFormatSpy).toHaveBeenCalledWith({ - id: 'number', - params: { pattern: '0,0.000' }, - }); - }); - - test('it should pass the formatter function to the axis', () => { - const { data, args } = sampleArgs(); - - const instance = shallow( - - ); - - const tickFormatter = instance - .find(Axis) - .first() - .prop('tickFormat'); - - if (!tickFormatter) { - throw new Error('tickFormatter prop not found'); - } - - tickFormatter('I'); - - expect(convertSpy).toHaveBeenCalledWith('I'); - }); - }); -}); diff --git a/x-pack/legacy/plugins/logstash/index.js b/x-pack/legacy/plugins/logstash/index.js index ae8571d1c19c3..29f01032f3413 100755 --- a/x-pack/legacy/plugins/logstash/index.js +++ b/x-pack/legacy/plugins/logstash/index.js @@ -5,12 +5,8 @@ */ import { resolve } from 'path'; -import { registerLogstashPipelinesRoutes } from './server/routes/api/pipelines'; -import { registerLogstashPipelineRoutes } from './server/routes/api/pipeline'; -import { registerLogstashUpgradeRoutes } from './server/routes/api/upgrade'; -import { registerLogstashClusterRoutes } from './server/routes/api/cluster'; import { registerLicenseChecker } from './server/lib/register_license_checker'; -import { PLUGIN } from './common/constants'; +import { PLUGIN } from '../../../plugins/logstash/common/constants'; export const logstash = kibana => new kibana.Plugin({ @@ -32,9 +28,5 @@ export const logstash = kibana => }, init: server => { registerLicenseChecker(server); - registerLogstashPipelinesRoutes(server); - registerLogstashPipelineRoutes(server); - registerLogstashUpgradeRoutes(server); - registerLogstashClusterRoutes(server); }, }); diff --git a/x-pack/legacy/plugins/logstash/public/components/pipeline_editor/pipeline_editor.js b/x-pack/legacy/plugins/logstash/public/components/pipeline_editor/pipeline_editor.js index 43ca656e0827c..5e430ccbd8ceb 100644 --- a/x-pack/legacy/plugins/logstash/public/components/pipeline_editor/pipeline_editor.js +++ b/x-pack/legacy/plugins/logstash/public/components/pipeline_editor/pipeline_editor.js @@ -13,7 +13,7 @@ import 'brace/mode/plain_text'; import 'brace/theme/github'; import { isEmpty } from 'lodash'; -import { TOOLTIPS } from '../../../common/constants/tooltips'; +import { TOOLTIPS } from '../../../../../../plugins/logstash/common/constants/tooltips'; import { EuiButton, EuiButtonEmpty, diff --git a/x-pack/legacy/plugins/logstash/public/lib/register_home_feature.ts b/x-pack/legacy/plugins/logstash/public/lib/register_home_feature.ts index e943656120d5e..2e1ee2afb9ce6 100644 --- a/x-pack/legacy/plugins/logstash/public/lib/register_home_feature.ts +++ b/x-pack/legacy/plugins/logstash/public/lib/register_home_feature.ts @@ -10,7 +10,7 @@ import { npSetup } from 'ui/new_platform'; import { xpackInfo } from 'plugins/xpack_main/services/xpack_info'; import { FeatureCatalogueCategory } from '../../../../../../src/plugins/home/public'; // @ts-ignore -import { PLUGIN } from '../../common/constants'; +import { PLUGIN } from '../../../../../plugins/logstash/common/constants'; const { plugins: { home }, diff --git a/x-pack/legacy/plugins/logstash/public/models/pipeline_list_item/pipeline_list_item.js b/x-pack/legacy/plugins/logstash/public/models/pipeline_list_item/pipeline_list_item.js index 14900fdaa7cdc..06d01a05bac27 100755 --- a/x-pack/legacy/plugins/logstash/public/models/pipeline_list_item/pipeline_list_item.js +++ b/x-pack/legacy/plugins/logstash/public/models/pipeline_list_item/pipeline_list_item.js @@ -8,7 +8,7 @@ import { pick, capitalize } from 'lodash'; import { getSearchValue } from 'plugins/logstash/lib/get_search_value'; import { getMoment } from 'plugins/logstash/../common/lib/get_moment'; -import { PIPELINE } from '../../../common/constants'; +import { PIPELINE } from '../../../../../../plugins/logstash/common/constants'; /** * Represents the model for listing pipelines in the UI diff --git a/x-pack/legacy/plugins/logstash/public/services/cluster/cluster_service.js b/x-pack/legacy/plugins/logstash/public/services/cluster/cluster_service.js index 4bad4f48cc61d..e89c2fe7d11bf 100755 --- a/x-pack/legacy/plugins/logstash/public/services/cluster/cluster_service.js +++ b/x-pack/legacy/plugins/logstash/public/services/cluster/cluster_service.js @@ -5,7 +5,7 @@ */ import chrome from 'ui/chrome'; -import { ROUTES } from '../../../common/constants'; +import { ROUTES } from '../../../../../../plugins/logstash/common/constants'; import { Cluster } from 'plugins/logstash/models/cluster'; export class ClusterService { diff --git a/x-pack/legacy/plugins/logstash/public/services/license/logstash_license_service.js b/x-pack/legacy/plugins/logstash/public/services/license/logstash_license_service.js index 97b336ec0728b..69cc8614a6ae2 100755 --- a/x-pack/legacy/plugins/logstash/public/services/license/logstash_license_service.js +++ b/x-pack/legacy/plugins/logstash/public/services/license/logstash_license_service.js @@ -7,7 +7,7 @@ import React from 'react'; import { toastNotifications } from 'ui/notify'; import { MarkdownSimple } from '../../../../../../../src/plugins/kibana_react/public'; -import { PLUGIN } from '../../../common/constants'; +import { PLUGIN } from '../../../../../../plugins/logstash/common/constants'; export class LogstashLicenseService { constructor(xpackInfoService, kbnUrlService, $timeout) { diff --git a/x-pack/legacy/plugins/logstash/public/services/monitoring/monitoring_service.js b/x-pack/legacy/plugins/logstash/public/services/monitoring/monitoring_service.js index 8a267e38db738..6103e730c2171 100755 --- a/x-pack/legacy/plugins/logstash/public/services/monitoring/monitoring_service.js +++ b/x-pack/legacy/plugins/logstash/public/services/monitoring/monitoring_service.js @@ -6,7 +6,7 @@ import moment from 'moment'; import chrome from 'ui/chrome'; -import { ROUTES, MONITORING } from '../../../common/constants'; +import { ROUTES, MONITORING } from '../../../../../../plugins/logstash/common/constants'; import { PipelineListItem } from 'plugins/logstash/models/pipeline_list_item'; export class MonitoringService { diff --git a/x-pack/legacy/plugins/logstash/public/services/pipeline/pipeline_service.js b/x-pack/legacy/plugins/logstash/public/services/pipeline/pipeline_service.js index 0696bf9d83256..b5d0dbeb852d5 100755 --- a/x-pack/legacy/plugins/logstash/public/services/pipeline/pipeline_service.js +++ b/x-pack/legacy/plugins/logstash/public/services/pipeline/pipeline_service.js @@ -5,7 +5,7 @@ */ import chrome from 'ui/chrome'; -import { ROUTES } from '../../../common/constants'; +import { ROUTES } from '../../../../../../plugins/logstash/common/constants'; import { Pipeline } from 'plugins/logstash/models/pipeline'; export class PipelineService { diff --git a/x-pack/legacy/plugins/logstash/public/services/pipelines/pipelines_service.js b/x-pack/legacy/plugins/logstash/public/services/pipelines/pipelines_service.js index 5a43cf07eba41..d70c8be06fde4 100755 --- a/x-pack/legacy/plugins/logstash/public/services/pipelines/pipelines_service.js +++ b/x-pack/legacy/plugins/logstash/public/services/pipelines/pipelines_service.js @@ -5,7 +5,7 @@ */ import chrome from 'ui/chrome'; -import { ROUTES, MONITORING } from '../../../common/constants'; +import { ROUTES, MONITORING } from '../../../../../../plugins/logstash/common/constants'; import { PipelineListItem } from 'plugins/logstash/models/pipeline_list_item'; const RECENTLY_DELETED_PIPELINE_IDS_STORAGE_KEY = 'xpack.logstash.recentlyDeletedPipelines'; diff --git a/x-pack/legacy/plugins/logstash/public/services/upgrade/upgrade_service.js b/x-pack/legacy/plugins/logstash/public/services/upgrade/upgrade_service.js index 7870a495d07a3..2019bdc1bf1aa 100755 --- a/x-pack/legacy/plugins/logstash/public/services/upgrade/upgrade_service.js +++ b/x-pack/legacy/plugins/logstash/public/services/upgrade/upgrade_service.js @@ -5,7 +5,7 @@ */ import chrome from 'ui/chrome'; -import { ROUTES } from '../../../common/constants'; +import { ROUTES } from '../../../../../../plugins/logstash/common/constants'; export class UpgradeService { constructor($http) { diff --git a/x-pack/legacy/plugins/logstash/server/lib/call_with_request_factory/call_with_request_factory.js b/x-pack/legacy/plugins/logstash/server/lib/call_with_request_factory/call_with_request_factory.js deleted file mode 100755 index 8dc09d394e973..0000000000000 --- a/x-pack/legacy/plugins/logstash/server/lib/call_with_request_factory/call_with_request_factory.js +++ /dev/null @@ -1,18 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { once } from 'lodash'; - -const callWithRequest = once(server => { - const cluster = server.plugins.elasticsearch.createCluster('logstash'); - return cluster.callWithRequest; -}); - -export const callWithRequestFactory = (server, request) => { - return (...args) => { - return callWithRequest(server)(request, ...args); - }; -}; diff --git a/x-pack/legacy/plugins/logstash/server/lib/call_with_request_factory/index.js b/x-pack/legacy/plugins/logstash/server/lib/call_with_request_factory/index.js deleted file mode 100755 index 787814d87dff9..0000000000000 --- a/x-pack/legacy/plugins/logstash/server/lib/call_with_request_factory/index.js +++ /dev/null @@ -1,7 +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. - */ - -export { callWithRequestFactory } from './call_with_request_factory'; diff --git a/x-pack/legacy/plugins/logstash/server/lib/error_wrappers/__tests__/wrap_custom_error.js b/x-pack/legacy/plugins/logstash/server/lib/error_wrappers/__tests__/wrap_custom_error.js deleted file mode 100755 index f9c102be7a1ff..0000000000000 --- a/x-pack/legacy/plugins/logstash/server/lib/error_wrappers/__tests__/wrap_custom_error.js +++ /dev/null @@ -1,21 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import expect from '@kbn/expect'; -import { wrapCustomError } from '../wrap_custom_error'; - -describe('wrap_custom_error', () => { - describe('#wrapCustomError', () => { - it('should return a Boom object', () => { - const originalError = new Error('I am an error'); - const statusCode = 404; - const wrappedError = wrapCustomError(originalError, statusCode); - - expect(wrappedError.isBoom).to.be(true); - expect(wrappedError.output.statusCode).to.equal(statusCode); - }); - }); -}); diff --git a/x-pack/legacy/plugins/logstash/server/lib/error_wrappers/__tests__/wrap_es_error.js b/x-pack/legacy/plugins/logstash/server/lib/error_wrappers/__tests__/wrap_es_error.js deleted file mode 100755 index cab25cd0b1b10..0000000000000 --- a/x-pack/legacy/plugins/logstash/server/lib/error_wrappers/__tests__/wrap_es_error.js +++ /dev/null @@ -1,42 +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 expect from '@kbn/expect'; -import { wrapEsError } from '../wrap_es_error'; - -describe('wrap_es_error', () => { - describe('#wrapEsError', () => { - let originalError; - beforeEach(() => { - originalError = new Error('I am an error'); - originalError.statusCode = 404; - }); - - it('should return a Boom object', () => { - const wrappedError = wrapEsError(originalError); - - expect(wrappedError.isBoom).to.be(true); - }); - - it('should return the correct Boom object', () => { - const wrappedError = wrapEsError(originalError); - - expect(wrappedError.output.statusCode).to.be(originalError.statusCode); - expect(wrappedError.output.payload.message).to.be(originalError.message); - }); - - it('should return invalid permissions message for 403 errors', () => { - const securityError = new Error('I am an error'); - securityError.statusCode = 403; - const wrappedError = wrapEsError(securityError); - - expect(wrappedError.isBoom).to.be(true); - expect(wrappedError.message).to.be( - 'Insufficient user permissions for managing Logstash pipelines' - ); - }); - }); -}); diff --git a/x-pack/legacy/plugins/logstash/server/lib/error_wrappers/__tests__/wrap_unknown_error.js b/x-pack/legacy/plugins/logstash/server/lib/error_wrappers/__tests__/wrap_unknown_error.js deleted file mode 100755 index 85e0b2b3033ad..0000000000000 --- a/x-pack/legacy/plugins/logstash/server/lib/error_wrappers/__tests__/wrap_unknown_error.js +++ /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 expect from '@kbn/expect'; -import { wrapUnknownError } from '../wrap_unknown_error'; - -describe('wrap_unknown_error', () => { - describe('#wrapUnknownError', () => { - it('should return a Boom object', () => { - const originalError = new Error('I am an error'); - const wrappedError = wrapUnknownError(originalError); - - expect(wrappedError.isBoom).to.be(true); - }); - }); -}); diff --git a/x-pack/legacy/plugins/logstash/server/lib/error_wrappers/index.js b/x-pack/legacy/plugins/logstash/server/lib/error_wrappers/index.js deleted file mode 100755 index f275f15637091..0000000000000 --- a/x-pack/legacy/plugins/logstash/server/lib/error_wrappers/index.js +++ /dev/null @@ -1,9 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -export { wrapCustomError } from './wrap_custom_error'; -export { wrapEsError } from './wrap_es_error'; -export { wrapUnknownError } from './wrap_unknown_error'; diff --git a/x-pack/legacy/plugins/logstash/server/lib/error_wrappers/wrap_custom_error.js b/x-pack/legacy/plugins/logstash/server/lib/error_wrappers/wrap_custom_error.js deleted file mode 100755 index 3295113d38ee5..0000000000000 --- a/x-pack/legacy/plugins/logstash/server/lib/error_wrappers/wrap_custom_error.js +++ /dev/null @@ -1,18 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import Boom from 'boom'; - -/** - * Wraps a custom error into a Boom error response and returns it - * - * @param err Object error - * @param statusCode Error status code - * @return Object Boom error response - */ -export function wrapCustomError(err, statusCode) { - return Boom.boomify(err, { statusCode }); -} diff --git a/x-pack/legacy/plugins/logstash/server/lib/error_wrappers/wrap_es_error.js b/x-pack/legacy/plugins/logstash/server/lib/error_wrappers/wrap_es_error.js deleted file mode 100755 index 41819179bde55..0000000000000 --- a/x-pack/legacy/plugins/logstash/server/lib/error_wrappers/wrap_es_error.js +++ /dev/null @@ -1,27 +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 Boom from 'boom'; -import { i18n } from '@kbn/i18n'; - -/** - * Wraps ES errors into a Boom error response and returns it - * This also handles the permissions issue gracefully - * - * @param err Object ES error - * @return Object Boom error response - */ -export function wrapEsError(err) { - const statusCode = err.statusCode; - if (statusCode === 403) { - return Boom.forbidden( - i18n.translate('xpack.logstash.insufficientUserPermissionsDescription', { - defaultMessage: 'Insufficient user permissions for managing Logstash pipelines', - }) - ); - } - return Boom.boomify(err, { statusCode: err.statusCode }); -} diff --git a/x-pack/legacy/plugins/logstash/server/lib/error_wrappers/wrap_unknown_error.js b/x-pack/legacy/plugins/logstash/server/lib/error_wrappers/wrap_unknown_error.js deleted file mode 100755 index ffd915c513362..0000000000000 --- a/x-pack/legacy/plugins/logstash/server/lib/error_wrappers/wrap_unknown_error.js +++ /dev/null @@ -1,17 +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 Boom from 'boom'; - -/** - * Wraps an unknown error into a Boom error response and returns it - * - * @param err Object Unknown error - * @return Object Boom error response - */ -export function wrapUnknownError(err) { - return Boom.boomify(err); -} diff --git a/x-pack/legacy/plugins/logstash/server/lib/fetch_all_from_scroll/__tests__/fetch_all_from_scroll.js b/x-pack/legacy/plugins/logstash/server/lib/fetch_all_from_scroll/__tests__/fetch_all_from_scroll.js deleted file mode 100755 index b1593fb1ba355..0000000000000 --- a/x-pack/legacy/plugins/logstash/server/lib/fetch_all_from_scroll/__tests__/fetch_all_from_scroll.js +++ /dev/null @@ -1,88 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import expect from '@kbn/expect'; -import sinon from 'sinon'; -import { fetchAllFromScroll } from '../fetch_all_from_scroll'; -import { set } from 'lodash'; - -describe('fetch_all_from_scroll', () => { - let mockResponse; - let stubCallWithRequest; - - beforeEach(() => { - mockResponse = {}; - - stubCallWithRequest = sinon.stub(); - stubCallWithRequest.onCall(0).returns( - new Promise(resolve => { - const mockInnerResponse = { - hits: { - hits: ['newhit'], - }, - _scroll_id: 'newScrollId', - }; - return resolve(mockInnerResponse); - }) - ); - - stubCallWithRequest.onCall(1).returns( - new Promise(resolve => { - const mockInnerResponse = { - hits: { - hits: [], - }, - }; - return resolve(mockInnerResponse); - }) - ); - }); - - describe('#fetchAllFromScroll', () => { - describe('when the passed-in response has no hits', () => { - beforeEach(() => { - set(mockResponse, 'hits.hits', []); - }); - - it('should return an empty array of hits', () => { - return fetchAllFromScroll(mockResponse).then(hits => { - expect(hits).to.eql([]); - }); - }); - - it('should not call callWithRequest', () => { - return fetchAllFromScroll(mockResponse, stubCallWithRequest).then(() => { - expect(stubCallWithRequest.called).to.be(false); - }); - }); - }); - - describe('when the passed-in response has some hits', () => { - beforeEach(() => { - set(mockResponse, 'hits.hits', ['foo', 'bar']); - set(mockResponse, '_scroll_id', 'originalScrollId'); - }); - - it('should return the hits from the response', () => { - return fetchAllFromScroll(mockResponse, stubCallWithRequest).then(hits => { - expect(hits).to.eql(['foo', 'bar', 'newhit']); - }); - }); - - it('should call callWithRequest', () => { - return fetchAllFromScroll(mockResponse, stubCallWithRequest).then(() => { - expect(stubCallWithRequest.calledTwice).to.be(true); - - const firstCallWithRequestCallArgs = stubCallWithRequest.args[0]; - expect(firstCallWithRequestCallArgs[1].body.scroll_id).to.eql('originalScrollId'); - - const secondCallWithRequestCallArgs = stubCallWithRequest.args[1]; - expect(secondCallWithRequestCallArgs[1].body.scroll_id).to.eql('newScrollId'); - }); - }); - }); - }); -}); diff --git a/x-pack/legacy/plugins/logstash/server/lib/fetch_all_from_scroll/fetch_all_from_scroll.js b/x-pack/legacy/plugins/logstash/server/lib/fetch_all_from_scroll/fetch_all_from_scroll.js deleted file mode 100755 index 835ef0090a5d2..0000000000000 --- a/x-pack/legacy/plugins/logstash/server/lib/fetch_all_from_scroll/fetch_all_from_scroll.js +++ /dev/null @@ -1,28 +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 { get } from 'lodash'; -import { ES_SCROLL_SETTINGS } from '../../../common/constants'; - -export function fetchAllFromScroll(response, callWithRequest, hits = []) { - const newHits = get(response, 'hits.hits', []); - const scrollId = get(response, '_scroll_id'); - - if (newHits.length > 0) { - hits.push(...newHits); - - return callWithRequest('scroll', { - body: { - scroll: ES_SCROLL_SETTINGS.KEEPALIVE, - scroll_id: scrollId, - }, - }).then(innerResponse => { - return fetchAllFromScroll(innerResponse, callWithRequest, hits); - }); - } - - return Promise.resolve(hits); -} diff --git a/x-pack/legacy/plugins/logstash/server/lib/license_pre_routing_factory/__tests__/license_pre_routing_factory.js b/x-pack/legacy/plugins/logstash/server/lib/license_pre_routing_factory/__tests__/license_pre_routing_factory.js deleted file mode 100755 index 1dc1df922acf7..0000000000000 --- a/x-pack/legacy/plugins/logstash/server/lib/license_pre_routing_factory/__tests__/license_pre_routing_factory.js +++ /dev/null @@ -1,69 +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 expect from '@kbn/expect'; -import { licensePreRoutingFactory } from '../license_pre_routing_factory'; - -describe('license_pre_routing_factory', () => { - describe('#logstashFeaturePreRoutingFactory', () => { - let mockServer; - let mockLicenseCheckResults; - - beforeEach(() => { - mockServer = { - plugins: { - xpack_main: { - info: { - feature: () => ({ - getLicenseCheckResults: () => mockLicenseCheckResults, - }), - }, - }, - }, - }; - }); - - it('only instantiates one instance per server', () => { - const firstInstance = licensePreRoutingFactory(mockServer); - const secondInstance = licensePreRoutingFactory(mockServer); - - expect(firstInstance).to.be(secondInstance); - }); - - describe('isAvailable is false', () => { - beforeEach(() => { - mockLicenseCheckResults = { - isAvailable: false, - }; - }); - - it('replies with 403', () => { - const licensePreRouting = licensePreRoutingFactory(mockServer); - const stubRequest = {}; - expect(() => licensePreRouting(stubRequest)).to.throwException(response => { - expect(response).to.be.an(Error); - expect(response.isBoom).to.be(true); - expect(response.output.statusCode).to.be(403); - }); - }); - }); - - describe('isAvailable is true', () => { - beforeEach(() => { - mockLicenseCheckResults = { - isAvailable: true, - }; - }); - - it('replies with nothing', () => { - const licensePreRouting = licensePreRoutingFactory(mockServer); - const stubRequest = {}; - const response = licensePreRouting(stubRequest); - expect(response).to.be(null); - }); - }); - }); -}); diff --git a/x-pack/legacy/plugins/logstash/server/lib/license_pre_routing_factory/index.js b/x-pack/legacy/plugins/logstash/server/lib/license_pre_routing_factory/index.js deleted file mode 100755 index 0743e443955f4..0000000000000 --- a/x-pack/legacy/plugins/logstash/server/lib/license_pre_routing_factory/index.js +++ /dev/null @@ -1,7 +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. - */ - -export { licensePreRoutingFactory } from './license_pre_routing_factory'; diff --git a/x-pack/legacy/plugins/logstash/server/lib/license_pre_routing_factory/license_pre_routing_factory.js b/x-pack/legacy/plugins/logstash/server/lib/license_pre_routing_factory/license_pre_routing_factory.js deleted file mode 100755 index 05402a56a52d8..0000000000000 --- a/x-pack/legacy/plugins/logstash/server/lib/license_pre_routing_factory/license_pre_routing_factory.js +++ /dev/null @@ -1,27 +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 { once } from 'lodash'; -import { wrapCustomError } from '../error_wrappers'; -import { PLUGIN } from '../../../common/constants'; - -export const licensePreRoutingFactory = once(server => { - const xpackMainPlugin = server.plugins.xpack_main; - - // License checking and enable/disable logic - function licensePreRouting() { - const licenseCheckResults = xpackMainPlugin.info.feature(PLUGIN.ID).getLicenseCheckResults(); - if (!licenseCheckResults.isAvailable) { - const error = new Error(licenseCheckResults.message); - const statusCode = 403; - throw wrapCustomError(error, statusCode); - } - - return null; - } - - return licensePreRouting; -}); diff --git a/x-pack/legacy/plugins/logstash/server/lib/register_license_checker/register_license_checker.js b/x-pack/legacy/plugins/logstash/server/lib/register_license_checker/register_license_checker.js index 8a17fb2eea497..a0d06e77b410d 100755 --- a/x-pack/legacy/plugins/logstash/server/lib/register_license_checker/register_license_checker.js +++ b/x-pack/legacy/plugins/logstash/server/lib/register_license_checker/register_license_checker.js @@ -6,7 +6,7 @@ import { mirrorPluginStatus } from '../../../../../server/lib/mirror_plugin_status'; import { checkLicense } from '../check_license'; -import { PLUGIN } from '../../../common/constants'; +import { PLUGIN } from '../../../../../../plugins/logstash/common/constants'; export function registerLicenseChecker(server) { const xpackMainPlugin = server.plugins.xpack_main; diff --git a/x-pack/legacy/plugins/logstash/server/models/cluster/__tests__/cluster.js b/x-pack/legacy/plugins/logstash/server/models/cluster/__tests__/cluster.js deleted file mode 100755 index 08a447a160a1a..0000000000000 --- a/x-pack/legacy/plugins/logstash/server/models/cluster/__tests__/cluster.js +++ /dev/null @@ -1,23 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import expect from '@kbn/expect'; -import { Cluster } from '../cluster'; - -describe('cluster', () => { - describe('Cluster', () => { - describe('fromUpstreamJSON factory method', () => { - const upstreamJSON = { - cluster_uuid: 'S-S4NNZDRV-g9c-JrIhx6A', - }; - - it('returns correct Cluster instance', () => { - const cluster = Cluster.fromUpstreamJSON(upstreamJSON); - expect(cluster.uuid).to.be(upstreamJSON.cluster_uuid); - }); - }); - }); -}); diff --git a/x-pack/legacy/plugins/logstash/server/models/cluster/cluster.js b/x-pack/legacy/plugins/logstash/server/models/cluster/cluster.js deleted file mode 100755 index b114162fb0986..0000000000000 --- a/x-pack/legacy/plugins/logstash/server/models/cluster/cluster.js +++ /dev/null @@ -1,30 +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 { get } from 'lodash'; - -/** - * This model deals with a cluster object from ES and converts it to Kibana downstream - */ -export class Cluster { - constructor(props) { - this.uuid = props.uuid; - } - - get downstreamJSON() { - const json = { - uuid: this.uuid, - }; - - return json; - } - - // generate Pipeline object from elasticsearch response - static fromUpstreamJSON(upstreamCluster) { - const uuid = get(upstreamCluster, 'cluster_uuid'); - return new Cluster({ uuid }); - } -} diff --git a/x-pack/legacy/plugins/logstash/server/models/pipeline/__tests__/pipeline.js b/x-pack/legacy/plugins/logstash/server/models/pipeline/__tests__/pipeline.js deleted file mode 100755 index 41869c22271f0..0000000000000 --- a/x-pack/legacy/plugins/logstash/server/models/pipeline/__tests__/pipeline.js +++ /dev/null @@ -1,76 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import expect from '@kbn/expect'; -import { Pipeline } from '../pipeline'; - -describe('pipeline', () => { - describe('Pipeline', () => { - describe('fromUpstreamJSON factory method', () => { - const upstreamJSON = { - _id: 'apache', - _source: { - description: 'this is an apache pipeline', - pipeline_metadata: { - version: 1, - type: 'logstash_pipeline', - }, - username: 'elastic', - pipeline: 'input {} filter { grok {} }\n output {}', - }, - }; - - it('returns correct Pipeline instance', () => { - const pipeline = Pipeline.fromUpstreamJSON(upstreamJSON); - expect(pipeline.id).to.be(upstreamJSON._id); - expect(pipeline.description).to.be(upstreamJSON._source.description); - expect(pipeline.username).to.be(upstreamJSON._source.username); - expect(pipeline.pipeline).to.be(upstreamJSON._source.pipeline); - }); - - it('throws if pipeline argument does not contain an id property', () => { - const badJSON = { - // no _id - _source: upstreamJSON._source, - }; - const testFromUpstreamJsonError = () => { - return Pipeline.fromUpstreamJSON(badJSON); - }; - expect(testFromUpstreamJsonError).to.throwError( - /upstreamPipeline argument must contain an id property/i - ); - }); - }); - - describe('upstreamJSON getter method', () => { - it('returns the upstream JSON', () => { - const downstreamJSON = { - id: 'apache', - description: 'this is an apache pipeline', - username: 'elastic', - pipeline: 'input {} filter { grok {} }\n output {}', - }; - const pipeline = new Pipeline(downstreamJSON); - const expectedUpstreamJSON = { - description: 'this is an apache pipeline', - pipeline_metadata: { - type: 'logstash_pipeline', - version: 1, - }, - username: 'elastic', - pipeline: 'input {} filter { grok {} }\n output {}', - }; - // can't do an object level comparison because modified field is always `now` - expect(pipeline.upstreamJSON.last_modified).to.be.a('string'); - expect(pipeline.upstreamJSON.description).to.be(expectedUpstreamJSON.description); - expect(pipeline.upstreamJSON.pipeline_metadata).to.eql( - expectedUpstreamJSON.pipeline_metadata - ); - expect(pipeline.upstreamJSON.pipeline).to.be(expectedUpstreamJSON.pipeline); - }); - }); - }); -}); diff --git a/x-pack/legacy/plugins/logstash/server/models/pipeline/pipeline.js b/x-pack/legacy/plugins/logstash/server/models/pipeline/pipeline.js deleted file mode 100755 index f02d303cb0380..0000000000000 --- a/x-pack/legacy/plugins/logstash/server/models/pipeline/pipeline.js +++ /dev/null @@ -1,93 +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 moment from 'moment'; -import { badRequest } from 'boom'; -import { get } from 'lodash'; -import { i18n } from '@kbn/i18n'; - -/** - * This model deals with a pipeline object from ES and converts it to Kibana downstream - */ -export class Pipeline { - constructor(props) { - this.id = props.id; - this.description = props.description; - this.username = props.username; - this.pipeline = props.pipeline; - this.settings = props.settings || {}; - } - - get downstreamJSON() { - const json = { - id: this.id, - description: this.description, - username: this.username, - pipeline: this.pipeline, - settings: this.settings, - }; - - return json; - } - - /** - * Returns the JSON schema for the pipeline doc that Elasticsearch expects - * For now, we hard code pipeline_metadata since we don't use it yet - * pipeline_metadata.version is the version of the Logstash config stored in - * pipeline field. - * pipeline_metadata.type is the Logstash config type (future: LIR, json, etc) - * @return {[JSON]} [Elasticsearch JSON] - */ - get upstreamJSON() { - return { - description: this.description, - last_modified: moment().toISOString(), - pipeline_metadata: { - version: 1, - type: 'logstash_pipeline', - }, - username: this.username, - pipeline: this.pipeline, - pipeline_settings: this.settings, - }; - } - - // generate Pipeline object from kibana response - static fromDownstreamJSON(downstreamPipeline, pipelineId, username) { - const opts = { - id: pipelineId, - description: downstreamPipeline.description, - username, - pipeline: downstreamPipeline.pipeline, - settings: downstreamPipeline.settings, - }; - - return new Pipeline(opts); - } - - // generate Pipeline object from elasticsearch response - static fromUpstreamJSON(upstreamPipeline) { - if (!upstreamPipeline._id) { - throw badRequest( - i18n.translate( - 'xpack.logstash.upstreamPipelineArgumentMustContainAnIdPropertyErrorMessage', - { - defaultMessage: 'upstreamPipeline argument must contain an id property', - } - ) - ); - } - const id = get(upstreamPipeline, '_id'); - const description = get(upstreamPipeline, '_source.description'); - const username = get(upstreamPipeline, '_source.username'); - const pipeline = get(upstreamPipeline, '_source.pipeline'); - const settings = get(upstreamPipeline, '_source.pipeline_settings'); - - const opts = { id, description, username, pipeline, settings }; - - return new Pipeline(opts); - } -} diff --git a/x-pack/legacy/plugins/logstash/server/models/pipeline_list_item/__tests__/pipeline_list_item.js b/x-pack/legacy/plugins/logstash/server/models/pipeline_list_item/__tests__/pipeline_list_item.js deleted file mode 100755 index 4f3a447f030c7..0000000000000 --- a/x-pack/legacy/plugins/logstash/server/models/pipeline_list_item/__tests__/pipeline_list_item.js +++ /dev/null @@ -1,49 +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 expect from '@kbn/expect'; -import { PipelineListItem } from '../pipeline_list_item'; - -describe('pipeline_list_item', () => { - describe('PipelineListItem', () => { - const upstreamJSON = { - _id: 'apache', - _source: { - description: 'this is an apache pipeline', - last_modified: '2017-05-14T02:50:51.250Z', - pipeline_metadata: { - type: 'logstash_pipeline', - version: 1, - }, - username: 'elastic', - pipeline: 'input {} filter { grok {} }\n output {}', - }, - }; - - describe('fromUpstreamJSON factory method', () => { - it('returns correct PipelineListItem instance', () => { - const pipelineListItem = PipelineListItem.fromUpstreamJSON(upstreamJSON); - expect(pipelineListItem.id).to.be(upstreamJSON._id); - expect(pipelineListItem.description).to.be(upstreamJSON._source.description); - expect(pipelineListItem.username).to.be(upstreamJSON._source.username); - expect(pipelineListItem.last_modified).to.be(upstreamJSON._source.last_modified); - }); - }); - - describe('downstreamJSON getter method', () => { - it('returns the downstreamJSON JSON', () => { - const pipelineListItem = PipelineListItem.fromUpstreamJSON(upstreamJSON); - const expectedDownstreamJSON = { - id: 'apache', - description: 'this is an apache pipeline', - username: 'elastic', - last_modified: '2017-05-14T02:50:51.250Z', - }; - expect(pipelineListItem.downstreamJSON).to.eql(expectedDownstreamJSON); - }); - }); - }); -}); diff --git a/x-pack/legacy/plugins/logstash/server/models/pipeline_list_item/pipeline_list_item.js b/x-pack/legacy/plugins/logstash/server/models/pipeline_list_item/pipeline_list_item.js deleted file mode 100755 index bbb506766897e..0000000000000 --- a/x-pack/legacy/plugins/logstash/server/models/pipeline_list_item/pipeline_list_item.js +++ /dev/null @@ -1,42 +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 { get } from 'lodash'; - -export class PipelineListItem { - constructor(props) { - this.id = props.id; - this.description = props.description; - this.last_modified = props.last_modified; - this.username = props.username; - } - - get downstreamJSON() { - const json = { - id: this.id, - description: this.description, - last_modified: this.last_modified, - username: this.username, - }; - - return json; - } - - /** - * Takes the json GET response from ES and constructs a pipeline model to be used - * in Kibana downstream - */ - static fromUpstreamJSON(pipeline) { - const opts = { - id: pipeline._id, - description: get(pipeline, '_source.description'), - last_modified: get(pipeline, '_source.last_modified'), - username: get(pipeline, '_source.username'), - }; - - return new PipelineListItem(opts); - } -} diff --git a/x-pack/legacy/plugins/logstash/server/routes/api/cluster/index.js b/x-pack/legacy/plugins/logstash/server/routes/api/cluster/index.js deleted file mode 100755 index b129d8524b573..0000000000000 --- a/x-pack/legacy/plugins/logstash/server/routes/api/cluster/index.js +++ /dev/null @@ -1,7 +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. - */ - -export { registerLogstashClusterRoutes } from './register_cluster_routes'; diff --git a/x-pack/legacy/plugins/logstash/server/routes/api/cluster/register_cluster_routes.js b/x-pack/legacy/plugins/logstash/server/routes/api/cluster/register_cluster_routes.js deleted file mode 100755 index 86e18b02ddce2..0000000000000 --- a/x-pack/legacy/plugins/logstash/server/routes/api/cluster/register_cluster_routes.js +++ /dev/null @@ -1,11 +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 { registerLoadRoute } from './register_load_route'; - -export function registerLogstashClusterRoutes(server) { - registerLoadRoute(server); -} diff --git a/x-pack/legacy/plugins/logstash/server/routes/api/cluster/register_load_route.js b/x-pack/legacy/plugins/logstash/server/routes/api/cluster/register_load_route.js deleted file mode 100755 index 663b60cc8c1d1..0000000000000 --- a/x-pack/legacy/plugins/logstash/server/routes/api/cluster/register_load_route.js +++ /dev/null @@ -1,40 +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 Boom from 'boom'; -import { callWithRequestFactory } from '../../../lib/call_with_request_factory'; -import { Cluster } from '../../../models/cluster'; -import { licensePreRoutingFactory } from '../../../lib/license_pre_routing_factory'; - -function fetchCluster(callWithRequest) { - return callWithRequest('info'); -} - -export function registerLoadRoute(server) { - const licensePreRouting = licensePreRoutingFactory(server); - - server.route({ - path: '/api/logstash/cluster', - method: 'GET', - handler: (request, h) => { - const callWithRequest = callWithRequestFactory(server, request); - - return fetchCluster(callWithRequest) - .then(responseFromES => ({ - cluster: Cluster.fromUpstreamJSON(responseFromES).downstreamJSON, - })) - .catch(e => { - if (e.status === 403) { - return h.response(); - } - throw Boom.internal(e); - }); - }, - config: { - pre: [licensePreRouting], - }, - }); -} diff --git a/x-pack/legacy/plugins/logstash/server/routes/api/pipeline/index.js b/x-pack/legacy/plugins/logstash/server/routes/api/pipeline/index.js deleted file mode 100755 index 643a405ced919..0000000000000 --- a/x-pack/legacy/plugins/logstash/server/routes/api/pipeline/index.js +++ /dev/null @@ -1,7 +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. - */ - -export { registerLogstashPipelineRoutes } from './register_pipeline_routes'; diff --git a/x-pack/legacy/plugins/logstash/server/routes/api/pipeline/register_delete_route.js b/x-pack/legacy/plugins/logstash/server/routes/api/pipeline/register_delete_route.js deleted file mode 100755 index 232ee4207541c..0000000000000 --- a/x-pack/legacy/plugins/logstash/server/routes/api/pipeline/register_delete_route.js +++ /dev/null @@ -1,38 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { callWithRequestFactory } from '../../../lib/call_with_request_factory'; -import { wrapEsError } from '../../../lib/error_wrappers'; -import { INDEX_NAMES } from '../../../../common/constants'; -import { licensePreRoutingFactory } from '../../../lib/license_pre_routing_factory'; - -function deletePipeline(callWithRequest, pipelineId) { - return callWithRequest('delete', { - index: INDEX_NAMES.PIPELINES, - id: pipelineId, - refresh: 'wait_for', - }); -} - -export function registerDeleteRoute(server) { - const licensePreRouting = licensePreRoutingFactory(server); - - server.route({ - path: '/api/logstash/pipeline/{id}', - method: 'DELETE', - handler: (request, h) => { - const callWithRequest = callWithRequestFactory(server, request); - const pipelineId = request.params.id; - - return deletePipeline(callWithRequest, pipelineId) - .then(() => h.response().code(204)) - .catch(e => wrapEsError(e)); - }, - config: { - pre: [licensePreRouting], - }, - }); -} diff --git a/x-pack/legacy/plugins/logstash/server/routes/api/pipeline/register_load_route.js b/x-pack/legacy/plugins/logstash/server/routes/api/pipeline/register_load_route.js deleted file mode 100755 index 796bf939d747f..0000000000000 --- a/x-pack/legacy/plugins/logstash/server/routes/api/pipeline/register_load_route.js +++ /dev/null @@ -1,47 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import Boom from 'boom'; -import { INDEX_NAMES } from '../../../../common/constants'; -import { callWithRequestFactory } from '../../../lib/call_with_request_factory'; -import { Pipeline } from '../../../models/pipeline'; -import { licensePreRoutingFactory } from '../../../lib/license_pre_routing_factory'; - -function fetchPipeline(callWithRequest, pipelineId) { - return callWithRequest('get', { - index: INDEX_NAMES.PIPELINES, - id: pipelineId, - _source: ['description', 'username', 'pipeline', 'pipeline_settings'], - ignore: [404], - }); -} - -export function registerLoadRoute(server) { - const licensePreRouting = licensePreRoutingFactory(server); - - server.route({ - path: '/api/logstash/pipeline/{id}', - method: 'GET', - handler: request => { - const callWithRequest = callWithRequestFactory(server, request); - const pipelineId = request.params.id; - - return fetchPipeline(callWithRequest, pipelineId) - .then(pipelineResponseFromES => { - if (!pipelineResponseFromES.found) { - throw Boom.notFound(); - } - - const pipeline = Pipeline.fromUpstreamJSON(pipelineResponseFromES); - return pipeline.downstreamJSON; - }) - .catch(e => Boom.boomify(e)); - }, - config: { - pre: [licensePreRouting], - }, - }); -} diff --git a/x-pack/legacy/plugins/logstash/server/routes/api/pipeline/register_pipeline_routes.js b/x-pack/legacy/plugins/logstash/server/routes/api/pipeline/register_pipeline_routes.js deleted file mode 100755 index 9966cd2ca2139..0000000000000 --- a/x-pack/legacy/plugins/logstash/server/routes/api/pipeline/register_pipeline_routes.js +++ /dev/null @@ -1,15 +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 { registerLoadRoute } from './register_load_route'; -import { registerDeleteRoute } from './register_delete_route'; -import { registerSaveRoute } from './register_save_route'; - -export function registerLogstashPipelineRoutes(server) { - registerLoadRoute(server); - registerDeleteRoute(server); - registerSaveRoute(server); -} diff --git a/x-pack/legacy/plugins/logstash/server/routes/api/pipeline/register_save_route.js b/x-pack/legacy/plugins/logstash/server/routes/api/pipeline/register_save_route.js deleted file mode 100755 index 50f62dc0a0ddd..0000000000000 --- a/x-pack/legacy/plugins/logstash/server/routes/api/pipeline/register_save_route.js +++ /dev/null @@ -1,48 +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 { get } from 'lodash'; -import { wrapEsError } from '../../../lib/error_wrappers'; -import { INDEX_NAMES } from '../../../../common/constants'; -import { callWithRequestFactory } from '../../../lib/call_with_request_factory'; -import { Pipeline } from '../../../models/pipeline'; -import { licensePreRoutingFactory } from '../../../lib/license_pre_routing_factory'; - -function savePipeline(callWithRequest, pipelineId, pipelineBody) { - return callWithRequest('index', { - index: INDEX_NAMES.PIPELINES, - id: pipelineId, - body: pipelineBody, - refresh: 'wait_for', - }); -} - -export function registerSaveRoute(server) { - const licensePreRouting = licensePreRoutingFactory(server); - - server.route({ - path: '/api/logstash/pipeline/{id}', - method: 'PUT', - handler: async (request, h) => { - let username; - if (server.plugins.security) { - const user = await server.plugins.security.getUser(request); - username = get(user, 'username'); - } - - const callWithRequest = callWithRequestFactory(server, request); - const pipelineId = request.params.id; - - const pipeline = Pipeline.fromDownstreamJSON(request.payload, pipelineId, username); - return savePipeline(callWithRequest, pipeline.id, pipeline.upstreamJSON) - .then(() => h.response().code(204)) - .catch(e => wrapEsError(e)); - }, - config: { - pre: [licensePreRouting], - }, - }); -} diff --git a/x-pack/legacy/plugins/logstash/server/routes/api/pipelines/index.js b/x-pack/legacy/plugins/logstash/server/routes/api/pipelines/index.js deleted file mode 100755 index db275b5a3ea79..0000000000000 --- a/x-pack/legacy/plugins/logstash/server/routes/api/pipelines/index.js +++ /dev/null @@ -1,7 +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. - */ - -export { registerLogstashPipelinesRoutes } from './register_pipelines_routes'; diff --git a/x-pack/legacy/plugins/logstash/server/routes/api/pipelines/register_delete_route.js b/x-pack/legacy/plugins/logstash/server/routes/api/pipelines/register_delete_route.js deleted file mode 100755 index 8ccd792d5a876..0000000000000 --- a/x-pack/legacy/plugins/logstash/server/routes/api/pipelines/register_delete_route.js +++ /dev/null @@ -1,51 +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 { callWithRequestFactory } from '../../../lib/call_with_request_factory'; -import { wrapUnknownError } from '../../../lib/error_wrappers'; -import { INDEX_NAMES } from '../../../../common/constants'; -import { licensePreRoutingFactory } from '../../../lib/license_pre_routing_factory'; - -function deletePipelines(callWithRequest, pipelineIds) { - const deletePromises = pipelineIds.map(pipelineId => { - return callWithRequest('delete', { - index: INDEX_NAMES.PIPELINES, - id: pipelineId, - refresh: 'wait_for', - }) - .then(success => ({ success })) - .catch(error => ({ error })); - }); - - return Promise.all(deletePromises).then(results => { - const successes = results.filter(result => Boolean(result.success)); - const errors = results.filter(result => Boolean(result.error)); - - return { - numSuccesses: successes.length, - numErrors: errors.length, - }; - }); -} - -export function registerDeleteRoute(server) { - const licensePreRouting = licensePreRoutingFactory(server); - - server.route({ - path: '/api/logstash/pipelines/delete', - method: 'POST', - handler: request => { - const callWithRequest = callWithRequestFactory(server, request); - - return deletePipelines(callWithRequest, request.payload.pipelineIds) - .then(results => ({ results })) - .catch(err => wrapUnknownError(err)); - }, - config: { - pre: [licensePreRouting], - }, - }); -} diff --git a/x-pack/legacy/plugins/logstash/server/routes/api/pipelines/register_list_route.js b/x-pack/legacy/plugins/logstash/server/routes/api/pipelines/register_list_route.js deleted file mode 100755 index 43ce1c3e8f6f6..0000000000000 --- a/x-pack/legacy/plugins/logstash/server/routes/api/pipelines/register_list_route.js +++ /dev/null @@ -1,52 +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 { wrapEsError } from '../../../lib/error_wrappers'; -import { callWithRequestFactory } from '../../../lib/call_with_request_factory'; -import { fetchAllFromScroll } from '../../../lib/fetch_all_from_scroll'; -import { INDEX_NAMES, ES_SCROLL_SETTINGS } from '../../../../common/constants'; -import { PipelineListItem } from '../../../models/pipeline_list_item'; -import { licensePreRoutingFactory } from '../../../lib/license_pre_routing_factory'; - -function fetchPipelines(callWithRequest) { - const params = { - index: INDEX_NAMES.PIPELINES, - scroll: ES_SCROLL_SETTINGS.KEEPALIVE, - body: { - size: ES_SCROLL_SETTINGS.PAGE_SIZE, - }, - ignore: [404], - }; - - return callWithRequest('search', params).then(response => - fetchAllFromScroll(response, callWithRequest) - ); -} - -export function registerListRoute(server) { - const licensePreRouting = licensePreRoutingFactory(server); - - server.route({ - path: '/api/logstash/pipelines', - method: 'GET', - handler: request => { - const callWithRequest = callWithRequestFactory(server, request); - - return fetchPipelines(callWithRequest) - .then((pipelinesHits = []) => { - const pipelines = pipelinesHits.map(pipeline => { - return PipelineListItem.fromUpstreamJSON(pipeline).downstreamJSON; - }); - - return { pipelines }; - }) - .catch(e => wrapEsError(e)); - }, - config: { - pre: [licensePreRouting], - }, - }); -} diff --git a/x-pack/legacy/plugins/logstash/server/routes/api/pipelines/register_pipelines_routes.js b/x-pack/legacy/plugins/logstash/server/routes/api/pipelines/register_pipelines_routes.js deleted file mode 100755 index 6d25f3acb9bf9..0000000000000 --- a/x-pack/legacy/plugins/logstash/server/routes/api/pipelines/register_pipelines_routes.js +++ /dev/null @@ -1,13 +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 { registerListRoute } from './register_list_route'; -import { registerDeleteRoute } from './register_delete_route'; - -export function registerLogstashPipelinesRoutes(server) { - registerListRoute(server); - registerDeleteRoute(server); -} diff --git a/x-pack/legacy/plugins/logstash/server/routes/api/upgrade/index.js b/x-pack/legacy/plugins/logstash/server/routes/api/upgrade/index.js deleted file mode 100755 index d616349dd6566..0000000000000 --- a/x-pack/legacy/plugins/logstash/server/routes/api/upgrade/index.js +++ /dev/null @@ -1,7 +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. - */ - -export { registerLogstashUpgradeRoutes } from './register_upgrade_routes'; diff --git a/x-pack/legacy/plugins/logstash/server/routes/api/upgrade/register_execute_route.js b/x-pack/legacy/plugins/logstash/server/routes/api/upgrade/register_execute_route.js deleted file mode 100755 index 16f97930ae25e..0000000000000 --- a/x-pack/legacy/plugins/logstash/server/routes/api/upgrade/register_execute_route.js +++ /dev/null @@ -1,56 +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 { callWithRequestFactory } from '../../../lib/call_with_request_factory'; -import { wrapUnknownError } from '../../../lib/error_wrappers'; -import { INDEX_NAMES } from '../../../../common/constants'; -import { licensePreRoutingFactory } from '../../../lib/license_pre_routing_factory'; - -function doesIndexExist(callWithRequest) { - return callWithRequest('indices.exists', { - index: INDEX_NAMES.PIPELINES, - }); -} - -async function executeUpgrade(callWithRequest) { - // If index doesn't exist yet, there is no mapping to upgrade - if (!(await doesIndexExist(callWithRequest))) { - return; - } - - return callWithRequest('indices.putMapping', { - index: INDEX_NAMES.PIPELINES, - body: { - properties: { - pipeline_settings: { - dynamic: false, - type: 'object', - }, - }, - }, - }); -} - -export function registerExecuteRoute(server) { - const licensePreRouting = licensePreRoutingFactory(server); - - server.route({ - path: '/api/logstash/upgrade', - method: 'POST', - handler: async request => { - const callWithRequest = callWithRequestFactory(server, request); - try { - await executeUpgrade(callWithRequest); - return { is_upgraded: true }; - } catch (err) { - throw wrapUnknownError(err); - } - }, - config: { - pre: [licensePreRouting], - }, - }); -} diff --git a/x-pack/legacy/plugins/logstash/server/routes/api/upgrade/register_upgrade_routes.js b/x-pack/legacy/plugins/logstash/server/routes/api/upgrade/register_upgrade_routes.js deleted file mode 100755 index a198f82613e37..0000000000000 --- a/x-pack/legacy/plugins/logstash/server/routes/api/upgrade/register_upgrade_routes.js +++ /dev/null @@ -1,11 +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 { registerExecuteRoute } from './register_execute_route'; - -export function registerLogstashUpgradeRoutes(server) { - registerExecuteRoute(server); -} diff --git a/x-pack/legacy/plugins/maps/common/get_join_key.ts b/x-pack/legacy/plugins/maps/common/get_join_key.ts new file mode 100644 index 0000000000000..004f12ca08d2e --- /dev/null +++ b/x-pack/legacy/plugins/maps/common/get_join_key.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. + */ + +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +export * from '../../../../plugins/maps/common/get_join_key'; diff --git a/x-pack/legacy/plugins/maps/common/migrations/ems_raster_tile_to_ems_vector_tile.js b/x-pack/legacy/plugins/maps/common/migrations/ems_raster_tile_to_ems_vector_tile.js index 94f4018bbdbb7..091cfd8605cb6 100644 --- a/x-pack/legacy/plugins/maps/common/migrations/ems_raster_tile_to_ems_vector_tile.js +++ b/x-pack/legacy/plugins/maps/common/migrations/ems_raster_tile_to_ems_vector_tile.js @@ -5,11 +5,11 @@ */ import _ from 'lodash'; -import { EMS_TMS, LAYER_TYPE } from '../constants'; +import { SOURCE_TYPES, LAYER_TYPE } from '../constants'; function isEmsTileSource(layerDescriptor) { const sourceType = _.get(layerDescriptor, 'sourceDescriptor.type'); - return sourceType === EMS_TMS; + return sourceType === SOURCE_TYPES.EMS_TMS; } function isTileLayer(layerDescriptor) { diff --git a/x-pack/legacy/plugins/maps/common/migrations/join_agg_key.test.ts b/x-pack/legacy/plugins/maps/common/migrations/join_agg_key.test.ts new file mode 100644 index 0000000000000..d92bf06541433 --- /dev/null +++ b/x-pack/legacy/plugins/maps/common/migrations/join_agg_key.test.ts @@ -0,0 +1,140 @@ +/* + * 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 { LAYER_TYPE } from '../constants'; +import { migrateJoinAggKey } from './join_agg_key'; + +describe('migrateJoinAggKey', () => { + const joins = [ + { + leftField: 'machine.os', + right: { + id: '9055b4aa-136a-4b6d-90ab-9f94ccfe5eb5', + indexPatternTitle: 'kibana_sample_data_logs', + term: 'machine.os.keyword', + metrics: [ + { + type: 'avg', + field: 'bytes', + }, + { + type: 'count', + }, + ], + whereQuery: { + query: 'bytes > 10000', + language: 'kuery', + }, + indexPatternRefName: 'layer_1_join_0_index_pattern', + }, + }, + { + leftField: 'machine.os', + right: { + id: '9a7f4e71-9500-4512-82f1-b7eaee3d87ff', + indexPatternTitle: 'kibana_sample_data_logs', + term: 'machine.os.keyword', + whereQuery: { + query: 'bytes < 10000', + language: 'kuery', + }, + metrics: [ + { + type: 'avg', + field: 'bytes', + }, + ], + indexPatternRefName: 'layer_1_join_1_index_pattern', + }, + }, + ]; + + test('Should handle missing layerListJSON attribute', () => { + const attributes = { + title: 'my map', + }; + expect(migrateJoinAggKey({ attributes })).toEqual({ + title: 'my map', + }); + }); + + test('Should migrate vector styles from legacy join agg key to new join agg key', () => { + const layerListJSON = JSON.stringify([ + { + type: LAYER_TYPE.VECTOR, + joins, + style: { + properties: { + fillColor: { + type: 'DYNAMIC', + options: { + color: 'Blues', + colorCategory: 'palette_0', + field: { + name: + '__kbnjoin__avg_of_bytes_groupby_kibana_sample_data_logs.machine.os.keyword', + origin: 'join', + }, + fieldMetaOptions: { + isEnabled: true, + sigma: 3, + }, + type: 'ORDINAL', + }, + }, + lineColor: { + type: 'DYNAMIC', + options: { + color: 'Blues', + colorCategory: 'palette_0', + field: { + name: '__kbnjoin__count_groupby_kibana_sample_data_logs.machine.os.keyword', + origin: 'join', + }, + fieldMetaOptions: { + isEnabled: true, + sigma: 3, + }, + type: 'ORDINAL', + }, + }, + lineWidth: { + type: 'DYNAMIC', + options: { + color: 'Blues', + colorCategory: 'palette_0', + field: { + name: 'mySourceField', + origin: 'source', + }, + fieldMetaOptions: { + isEnabled: true, + sigma: 3, + }, + type: 'ORDINAL', + }, + }, + }, + }, + }, + ]); + const attributes = { + title: 'my map', + layerListJSON, + }; + const { layerListJSON: migratedLayerListJSON } = migrateJoinAggKey({ attributes }); + const migratedLayerList = JSON.parse(migratedLayerListJSON!); + expect(migratedLayerList[0].style.properties.fillColor.options.field.name).toBe( + '__kbnjoin__avg_of_bytes__9055b4aa-136a-4b6d-90ab-9f94ccfe5eb5' + ); + expect(migratedLayerList[0].style.properties.lineColor.options.field.name).toBe( + '__kbnjoin__count__9055b4aa-136a-4b6d-90ab-9f94ccfe5eb5' + ); + expect(migratedLayerList[0].style.properties.lineWidth.options.field.name).toBe( + 'mySourceField' + ); + }); +}); diff --git a/x-pack/legacy/plugins/maps/common/migrations/join_agg_key.ts b/x-pack/legacy/plugins/maps/common/migrations/join_agg_key.ts new file mode 100644 index 0000000000000..29661aedb550c --- /dev/null +++ b/x-pack/legacy/plugins/maps/common/migrations/join_agg_key.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 _ from 'lodash'; +import { + AGG_DELIMITER, + AGG_TYPE, + FIELD_ORIGIN, + JOIN_FIELD_NAME_PREFIX, + LAYER_TYPE, + VECTOR_STYLES, +} from '../constants'; +import { getJoinAggKey } from '../get_join_key'; +import { + AggDescriptor, + JoinDescriptor, + LayerDescriptor, + VectorLayerDescriptor, +} from '../descriptor_types'; +import { MapSavedObjectAttributes } from '../../../../../plugins/maps/common/map_saved_object_type'; + +const GROUP_BY_DELIMITER = '_groupby_'; + +function getLegacyAggKey({ + aggType, + aggFieldName, + indexPatternTitle, + termFieldName, +}: { + aggType: AGG_TYPE; + aggFieldName?: string; + indexPatternTitle: string; + termFieldName: string; +}): string { + const metricKey = + aggType !== AGG_TYPE.COUNT ? `${aggType}${AGG_DELIMITER}${aggFieldName}` : aggType; + return `${JOIN_FIELD_NAME_PREFIX}${metricKey}${GROUP_BY_DELIMITER}${indexPatternTitle}.${termFieldName}`; +} + +function parseLegacyAggKey(legacyAggKey: string): { aggType: AGG_TYPE; aggFieldName?: string } { + const groupBySplit = legacyAggKey + .substring(JOIN_FIELD_NAME_PREFIX.length) + .split(GROUP_BY_DELIMITER); + const metricKey = groupBySplit[0]; + const metricKeySplit = metricKey.split(AGG_DELIMITER); + return { + aggType: metricKeySplit[0] as AGG_TYPE, + aggFieldName: metricKeySplit.length === 2 ? metricKeySplit[1] : undefined, + }; +} + +export function migrateJoinAggKey({ + attributes, +}: { + attributes: MapSavedObjectAttributes; +}): MapSavedObjectAttributes { + if (!attributes || !attributes.layerListJSON) { + return attributes; + } + + const layerList: LayerDescriptor[] = JSON.parse(attributes.layerListJSON); + layerList.forEach((layerDescriptor: LayerDescriptor) => { + if ( + layerDescriptor.type === LAYER_TYPE.VECTOR || + layerDescriptor.type === LAYER_TYPE.BLENDED_VECTOR + ) { + const vectorLayerDescriptor = layerDescriptor as VectorLayerDescriptor; + + if ( + !vectorLayerDescriptor.style || + !vectorLayerDescriptor.joins || + vectorLayerDescriptor.joins.length === 0 + ) { + return; + } + + const legacyJoinFields = new Map(); + vectorLayerDescriptor.joins.forEach((joinDescriptor: JoinDescriptor) => { + _.get(joinDescriptor, 'right.metrics', []).forEach((aggDescriptor: AggDescriptor) => { + const legacyAggKey = getLegacyAggKey({ + aggType: aggDescriptor.type, + aggFieldName: aggDescriptor.field, + indexPatternTitle: _.get(joinDescriptor, 'right.indexPatternTitle', ''), + termFieldName: _.get(joinDescriptor, 'right.term', ''), + }); + // The legacy getAggKey implemenation has a naming collision bug where + // aggType, aggFieldName, indexPatternTitle, and termFieldName would result in the identical aggKey. + // The VectorStyle implemenation used the first matching join descriptor + // so, in the event of a name collision, the first join descriptor will be used here as well. + if (!legacyJoinFields.has(legacyAggKey)) { + legacyJoinFields.set(legacyAggKey, joinDescriptor); + } + }); + }); + + Object.keys(vectorLayerDescriptor.style.properties).forEach(key => { + const style: any = vectorLayerDescriptor.style!.properties[key as VECTOR_STYLES]; + if (_.get(style, 'options.field.origin') === FIELD_ORIGIN.JOIN) { + const joinDescriptor = legacyJoinFields.get(style.options.field.name); + if (joinDescriptor) { + const { aggType, aggFieldName } = parseLegacyAggKey(style.options.field.name); + // Update legacy join agg key to new join agg key + style.options.field.name = getJoinAggKey({ + aggType, + aggFieldName, + rightSourceId: joinDescriptor.right.id!, + }); + } + } + }); + } + }); + + return { + ...attributes, + layerListJSON: JSON.stringify(layerList), + }; +} diff --git a/x-pack/legacy/plugins/maps/common/migrations/move_apply_global_query.js b/x-pack/legacy/plugins/maps/common/migrations/move_apply_global_query.js index 490e760d8c003..0d6b0052d2b0d 100644 --- a/x-pack/legacy/plugins/maps/common/migrations/move_apply_global_query.js +++ b/x-pack/legacy/plugins/maps/common/migrations/move_apply_global_query.js @@ -5,11 +5,13 @@ */ import _ from 'lodash'; -import { ES_GEO_GRID, ES_PEW_PEW, ES_SEARCH } from '../constants'; +import { SOURCE_TYPES } from '../constants'; function isEsSource(layerDescriptor) { const sourceType = _.get(layerDescriptor, 'sourceDescriptor.type'); - return [ES_GEO_GRID, ES_PEW_PEW, ES_SEARCH].includes(sourceType); + return [SOURCE_TYPES.ES_GEO_GRID, SOURCE_TYPES.ES_PEW_PEW, SOURCE_TYPES.ES_SEARCH].includes( + sourceType + ); } // Migration to move applyGlobalQuery from layer to sources. diff --git a/x-pack/legacy/plugins/maps/common/migrations/references.js b/x-pack/legacy/plugins/maps/common/migrations/references.js index a96af700da37c..3980705fd7cfa 100644 --- a/x-pack/legacy/plugins/maps/common/migrations/references.js +++ b/x-pack/legacy/plugins/maps/common/migrations/references.js @@ -7,11 +7,15 @@ // Can not use public Layer classes to extract references since this logic must run in both client and server. import _ from 'lodash'; -import { ES_GEO_GRID, ES_SEARCH, ES_PEW_PEW } from '../constants'; +import { SOURCE_TYPES } from '../constants'; function doesSourceUseIndexPattern(layerDescriptor) { const sourceType = _.get(layerDescriptor, 'sourceDescriptor.type'); - return sourceType === ES_GEO_GRID || sourceType === ES_SEARCH || sourceType === ES_PEW_PEW; + return ( + sourceType === SOURCE_TYPES.ES_GEO_GRID || + sourceType === SOURCE_TYPES.ES_SEARCH || + sourceType === SOURCE_TYPES.ES_PEW_PEW + ); } export function extractReferences({ attributes, references = [] }) { diff --git a/x-pack/legacy/plugins/maps/common/migrations/references.test.js b/x-pack/legacy/plugins/maps/common/migrations/references.test.js index 40f6fd72a48d7..50a45c81339dc 100644 --- a/x-pack/legacy/plugins/maps/common/migrations/references.test.js +++ b/x-pack/legacy/plugins/maps/common/migrations/references.test.js @@ -5,16 +5,16 @@ */ import { extractReferences, injectReferences } from './references'; -import { ES_GEO_GRID, ES_SEARCH, ES_PEW_PEW } from '../constants'; +import { SOURCE_TYPES } from '../constants'; const layerListJSON = { esSearchSource: { - withIndexPatternId: `[{\"sourceDescriptor\":{\"type\":\"${ES_SEARCH}\",\"indexPatternId\":\"c698b940-e149-11e8-a35a-370a8516603a\"}}]`, - withIndexPatternRef: `[{\"sourceDescriptor\":{\"type\":\"${ES_SEARCH}\",\"indexPatternRefName\":\"layer_0_source_index_pattern\"}}]`, + withIndexPatternId: `[{\"sourceDescriptor\":{\"type\":\"${SOURCE_TYPES.ES_SEARCH}\",\"indexPatternId\":\"c698b940-e149-11e8-a35a-370a8516603a\"}}]`, + withIndexPatternRef: `[{\"sourceDescriptor\":{\"type\":\"${SOURCE_TYPES.ES_SEARCH}\",\"indexPatternRefName\":\"layer_0_source_index_pattern\"}}]`, }, esGeoGridSource: { - withIndexPatternId: `[{\"sourceDescriptor\":{\"type\":\"${ES_GEO_GRID}\",\"indexPatternId\":\"c698b940-e149-11e8-a35a-370a8516603a\"}}]`, - withIndexPatternRef: `[{\"sourceDescriptor\":{\"type\":\"${ES_GEO_GRID}\",\"indexPatternRefName\":\"layer_0_source_index_pattern\"}}]`, + withIndexPatternId: `[{\"sourceDescriptor\":{\"type\":\"${SOURCE_TYPES.ES_GEO_GRID}\",\"indexPatternId\":\"c698b940-e149-11e8-a35a-370a8516603a\"}}]`, + withIndexPatternRef: `[{\"sourceDescriptor\":{\"type\":\"${SOURCE_TYPES.ES_GEO_GRID}\",\"indexPatternRefName\":\"layer_0_source_index_pattern\"}}]`, }, join: { withIndexPatternId: @@ -23,8 +23,8 @@ const layerListJSON = { '[{"joins":[{"right":{"indexPatternRefName":"layer_0_join_0_index_pattern"}}]}]', }, pewPewSource: { - withIndexPatternId: `[{\"sourceDescriptor\":{\"type\":\"${ES_PEW_PEW}\",\"indexPatternId\":\"c698b940-e149-11e8-a35a-370a8516603a\"}}]`, - withIndexPatternRef: `[{\"sourceDescriptor\":{\"type\":\"${ES_PEW_PEW}\",\"indexPatternRefName\":\"layer_0_source_index_pattern\"}}]`, + withIndexPatternId: `[{\"sourceDescriptor\":{\"type\":\"${SOURCE_TYPES.ES_PEW_PEW}\",\"indexPatternId\":\"c698b940-e149-11e8-a35a-370a8516603a\"}}]`, + withIndexPatternRef: `[{\"sourceDescriptor\":{\"type\":\"${SOURCE_TYPES.ES_PEW_PEW}\",\"indexPatternRefName\":\"layer_0_source_index_pattern\"}}]`, }, }; diff --git a/x-pack/legacy/plugins/maps/common/migrations/scaling_type.ts b/x-pack/legacy/plugins/maps/common/migrations/scaling_type.ts index 5823ddd6b42e3..551975fbacea5 100644 --- a/x-pack/legacy/plugins/maps/common/migrations/scaling_type.ts +++ b/x-pack/legacy/plugins/maps/common/migrations/scaling_type.ts @@ -5,13 +5,13 @@ */ import _ from 'lodash'; -import { ES_SEARCH, SCALING_TYPES } from '../constants'; +import { SOURCE_TYPES, SCALING_TYPES } from '../constants'; import { LayerDescriptor, ESSearchSourceDescriptor } from '../descriptor_types'; import { MapSavedObjectAttributes } from '../../../../../plugins/maps/common/map_saved_object_type'; function isEsDocumentSource(layerDescriptor: LayerDescriptor) { const sourceType = _.get(layerDescriptor, 'sourceDescriptor.type'); - return sourceType === ES_SEARCH; + return sourceType === SOURCE_TYPES.ES_SEARCH; } export function migrateUseTopHitsToScalingType({ diff --git a/x-pack/legacy/plugins/maps/common/migrations/top_hits_time_to_sort.js b/x-pack/legacy/plugins/maps/common/migrations/top_hits_time_to_sort.js index 7392dfa71bf3a..055c867486f6c 100644 --- a/x-pack/legacy/plugins/maps/common/migrations/top_hits_time_to_sort.js +++ b/x-pack/legacy/plugins/maps/common/migrations/top_hits_time_to_sort.js @@ -5,11 +5,11 @@ */ import _ from 'lodash'; -import { ES_SEARCH, SORT_ORDER } from '../constants'; +import { SOURCE_TYPES, SORT_ORDER } from '../constants'; function isEsDocumentSource(layerDescriptor) { const sourceType = _.get(layerDescriptor, 'sourceDescriptor.type'); - return sourceType === ES_SEARCH; + return sourceType === SOURCE_TYPES.ES_SEARCH; } export function topHitsTimeToSort({ attributes }) { diff --git a/x-pack/legacy/plugins/maps/index.js b/x-pack/legacy/plugins/maps/index.js index 1a7f478d3bbad..f4e01efc05f45 100644 --- a/x-pack/legacy/plugins/maps/index.js +++ b/x-pack/legacy/plugins/maps/index.js @@ -38,6 +38,7 @@ export function maps(kibana) { return { showMapVisualizationTypes: serverConfig.get('xpack.maps.showMapVisualizationTypes'), showMapsInspectorAdapter: serverConfig.get('xpack.maps.showMapsInspectorAdapter'), + enableVectorTiles: serverConfig.get('xpack.maps.enableVectorTiles'), preserveDrawingBuffer: serverConfig.get('xpack.maps.preserveDrawingBuffer'), isEmsEnabled: mapConfig.includeElasticMapsService, emsFontLibraryUrl: mapConfig.emsFontLibraryUrl, @@ -85,6 +86,7 @@ export function maps(kibana) { showMapVisualizationTypes: Joi.boolean().default(false), showMapsInspectorAdapter: Joi.boolean().default(false), // flag used in functional testing preserveDrawingBuffer: Joi.boolean().default(false), // flag used in functional testing + enableVectorTiles: Joi.boolean().default(false), // flag used to enable/disable vector-tiles }).default(); }, diff --git a/x-pack/legacy/plugins/maps/migrations.js b/x-pack/legacy/plugins/maps/migrations.js index 6a1f5bc937497..a8e69eef7a02f 100644 --- a/x-pack/legacy/plugins/maps/migrations.js +++ b/x-pack/legacy/plugins/maps/migrations.js @@ -11,6 +11,7 @@ import { moveApplyGlobalQueryToSources } from './common/migrations/move_apply_gl import { addFieldMetaOptions } from './common/migrations/add_field_meta_options'; import { migrateSymbolStyleDescriptor } from './common/migrations/migrate_symbol_style_descriptor'; import { migrateUseTopHitsToScalingType } from './common/migrations/scaling_type'; +import { migrateJoinAggKey } from './common/migrations/join_agg_key'; export const migrations = { map: { @@ -57,5 +58,13 @@ export const migrations = { attributes: attributesPhase2, }; }, + '7.8.0': doc => { + const attributes = migrateJoinAggKey(doc); + + return { + ...doc, + attributes, + }; + }, }, }; diff --git a/x-pack/legacy/plugins/maps/public/actions/map_actions.js b/x-pack/legacy/plugins/maps/public/actions/map_actions.js index aa55cf0808ef2..7bfbf5761c5b8 100644 --- a/x-pack/legacy/plugins/maps/public/actions/map_actions.js +++ b/x-pack/legacy/plugins/maps/public/actions/map_actions.js @@ -125,9 +125,21 @@ async function syncDataForAllLayers(dispatch, getState, dataFilters) { export function cancelAllInFlightRequests() { return (dispatch, getState) => { getLayerList(getState()).forEach(layer => { - layer.getInFlightRequestTokens().forEach(requestToken => { - dispatch(cancelRequest(requestToken)); - }); + dispatch(clearDataRequests(layer)); + }); + }; +} + +function clearDataRequests(layer) { + return dispatch => { + layer.getInFlightRequestTokens().forEach(requestToken => { + dispatch(cancelRequest(requestToken)); + }); + dispatch({ + type: UPDATE_LAYER_PROP, + id: layer.getId(), + propName: '__dataRequests', + newValue: [], }); }; } @@ -663,13 +675,31 @@ export function updateSourceProp(layerId, propName, value, newLayerType) { layerId, propName, value, - newLayerType, }); + if (newLayerType) { + dispatch(updateLayerType(layerId, newLayerType)); + } await dispatch(clearMissingStyleProperties(layerId)); dispatch(syncDataForLayer(layerId)); }; } +function updateLayerType(layerId, newLayerType) { + return (dispatch, getState) => { + const layer = getLayerById(layerId, getState()); + if (!layer || layer.getType() === newLayerType) { + return; + } + dispatch(clearDataRequests(layer)); + dispatch({ + type: UPDATE_LAYER_PROP, + id: layerId, + propName: 'type', + newValue: newLayerType, + }); + }; +} + export function syncDataForLayer(layerId) { return async (dispatch, getState) => { const targetLayer = getLayerById(layerId, getState()); diff --git a/x-pack/legacy/plugins/maps/public/actions/map_actions.test.js b/x-pack/legacy/plugins/maps/public/actions/map_actions.test.js index c280b8af7ab80..7e2a3c827fa88 100644 --- a/x-pack/legacy/plugins/maps/public/actions/map_actions.test.js +++ b/x-pack/legacy/plugins/maps/public/actions/map_actions.test.js @@ -5,7 +5,7 @@ */ jest.mock('../selectors/map_selectors', () => ({})); -jest.mock('../kibana_services', () => ({})); +jest.mock('../../../../../plugins/maps/public/kibana_services', () => ({})); import { mapExtentChanged, setMouseCoordinates } from './map_actions'; diff --git a/x-pack/legacy/plugins/maps/public/angular/get_initial_layers.js b/x-pack/legacy/plugins/maps/public/angular/get_initial_layers.js index 5e497ff0736b2..686259aeaaba4 100644 --- a/x-pack/legacy/plugins/maps/public/angular/get_initial_layers.js +++ b/x-pack/legacy/plugins/maps/public/angular/get_initial_layers.js @@ -4,11 +4,28 @@ * you may not use this file except in compliance with the Elastic License. */ import _ from 'lodash'; +// Import each layer type, even those not used, to init in registry +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import '../../../../../plugins/maps/public/layers/sources/wms_source'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import '../../../../../plugins/maps/public/layers/sources/ems_file_source'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import '../../../../../plugins/maps/public/layers/sources/es_search_source'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import '../../../../../plugins/maps/public/layers/sources/es_pew_pew_source/es_pew_pew_source'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import '../../../../../plugins/maps/public/layers/sources/kibana_regionmap_source'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import '../../../../../plugins/maps/public/layers/sources/es_geo_grid_source'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import '../../../../../plugins/maps/public/layers/sources/xyz_tms_source'; + // eslint-disable-next-line @kbn/eslint/no-restricted-paths import { KibanaTilemapSource } from '../../../../../plugins/maps/public/layers/sources/kibana_tilemap_source'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths import { EMSTMSSource } from '../../../../../plugins/maps/public/layers/sources/ems_tms_source'; -import { getInjectedVarFunc } from '../kibana_services'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { getInjectedVarFunc } from '../../../../../plugins/maps/public/kibana_services'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths import { getKibanaTileMap } from '../../../../../plugins/maps/public/meta'; diff --git a/x-pack/legacy/plugins/maps/public/angular/get_initial_layers.test.js b/x-pack/legacy/plugins/maps/public/angular/get_initial_layers.test.js index 5334beaaf714a..8c9185a16ea0e 100644 --- a/x-pack/legacy/plugins/maps/public/angular/get_initial_layers.test.js +++ b/x-pack/legacy/plugins/maps/public/angular/get_initial_layers.test.js @@ -7,7 +7,7 @@ jest.mock('../../../../../plugins/maps/public/meta', () => { return {}; }); -jest.mock('../kibana_services'); +jest.mock('../../../../../plugins/maps/public/kibana_services'); import { getInitialLayers } from './get_initial_layers'; @@ -15,7 +15,8 @@ const layerListNotProvided = undefined; describe('Saved object has layer list', () => { beforeEach(() => { - require('../kibana_services').getInjectedVarFunc = () => jest.fn(); + require('../../../../../plugins/maps/public/kibana_services').getInjectedVarFunc = () => + jest.fn(); }); it('Should get initial layers from saved object', () => { @@ -65,7 +66,7 @@ describe('EMS is enabled', () => { require('../../../../../plugins/maps/public/meta').getKibanaTileMap = () => { return null; }; - require('../kibana_services').getInjectedVarFunc = () => key => { + require('../../../../../plugins/maps/public/kibana_services').getInjectedVarFunc = () => key => { switch (key) { case 'emsTileLayerId': return { @@ -110,7 +111,7 @@ describe('EMS is not enabled', () => { return null; }; - require('../kibana_services').getInjectedVarFunc = () => key => { + require('../../../../../plugins/maps/public/kibana_services').getInjectedVarFunc = () => key => { switch (key) { case 'isEmsEnabled': return false; diff --git a/x-pack/legacy/plugins/maps/public/angular/get_initial_query.js b/x-pack/legacy/plugins/maps/public/angular/get_initial_query.js index fc8305b252cc3..c50ecb2b05dc0 100644 --- a/x-pack/legacy/plugins/maps/public/angular/get_initial_query.js +++ b/x-pack/legacy/plugins/maps/public/angular/get_initial_query.js @@ -4,11 +4,12 @@ * you may not use this file except in compliance with the Elastic License. */ -import chrome from 'ui/chrome'; - -const settings = chrome.getUiSettingsClient(); +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { getUiSettings } from '../../../../../plugins/maps/public/kibana_services'; export function getInitialQuery({ mapStateJSON, appState = {}, userQueryLanguage }) { + const settings = getUiSettings(); + if (appState.query) { return appState.query; } diff --git a/x-pack/legacy/plugins/maps/public/angular/get_initial_refresh_config.js b/x-pack/legacy/plugins/maps/public/angular/get_initial_refresh_config.js index 10a2580b78e6d..8735d45debfc4 100644 --- a/x-pack/legacy/plugins/maps/public/angular/get_initial_refresh_config.js +++ b/x-pack/legacy/plugins/maps/public/angular/get_initial_refresh_config.js @@ -3,11 +3,12 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -import chrome from 'ui/chrome'; - -const uiSettings = chrome.getUiSettingsClient(); +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { getUiSettings } from '../../../../../plugins/maps/public/kibana_services'; export function getInitialRefreshConfig({ mapStateJSON, globalState = {} }) { + const uiSettings = getUiSettings(); + if (mapStateJSON) { const mapState = JSON.parse(mapStateJSON); if (mapState.refreshConfig) { diff --git a/x-pack/legacy/plugins/maps/public/angular/get_initial_time_filters.js b/x-pack/legacy/plugins/maps/public/angular/get_initial_time_filters.js index 82439175841b2..74fbf603e99f5 100644 --- a/x-pack/legacy/plugins/maps/public/angular/get_initial_time_filters.js +++ b/x-pack/legacy/plugins/maps/public/angular/get_initial_time_filters.js @@ -3,9 +3,8 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -import chrome from 'ui/chrome'; - -const uiSettings = chrome.getUiSettingsClient(); +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { getUiSettings } from '../../../../../plugins/maps/public/kibana_services'; export function getInitialTimeFilters({ mapStateJSON, globalState = {} }) { if (mapStateJSON) { @@ -15,6 +14,6 @@ export function getInitialTimeFilters({ mapStateJSON, globalState = {} }) { } } - const defaultTime = uiSettings.get('timepicker:timeDefaults'); + const defaultTime = getUiSettings().get('timepicker:timeDefaults'); return { ...defaultTime, ...globalState.time }; } diff --git a/x-pack/legacy/plugins/maps/public/angular/map.html b/x-pack/legacy/plugins/maps/public/angular/map.html index 2f34ffa660d6e..7d7dcf6f9c9a9 100644 --- a/x-pack/legacy/plugins/maps/public/angular/map.html +++ b/x-pack/legacy/plugins/maps/public/angular/map.html @@ -3,11 +3,11 @@
{ - const { filterManager } = npStart.plugins.data.query; + const savedQueryService = getData().query.savedQueries; + const { filterManager } = getData().query; const savedMap = $route.current.locals.map; $scope.screenTitle = savedMap.title; let unsubscribe; @@ -115,6 +129,14 @@ app.controller( return _.get($state, 'filters', []); } + const visibleSubscription = getCoreChrome() + .getIsVisible$() + .subscribe(isVisible => { + $scope.$evalAsync(() => { + $scope.isVisible = isVisible; + }); + }); + $scope.$listen(globalState, 'fetch_with_changes', diff => { if (diff.includes('time') || diff.includes('filters')) { onQueryChange({ @@ -169,10 +191,10 @@ app.controller( }); /* Saved Queries */ - $scope.showSaveQuery = capabilities.get().maps.saveQuery; + $scope.showSaveQuery = getMapsCapabilities().saveQuery; $scope.$watch( - () => capabilities.get().maps.saveQuery, + () => getMapsCapabilities().saveQuery, newCapability => { $scope.showSaveQuery = newCapability; } @@ -342,7 +364,7 @@ app.controller( // clear old UI state store.dispatch(setSelectedLayer(null)); store.dispatch(updateFlyout(FLYOUT_STATE.NONE)); - store.dispatch(setReadOnly(!capabilities.get().maps.save)); + store.dispatch(setReadOnly(!getMapsCapabilities().save)); handleStoreChanges(store); unsubscribe = store.subscribe(() => { @@ -446,6 +468,8 @@ app.controller( $scope.$on('$destroy', () => { window.removeEventListener('beforeunload', beforeUnload); + visibleSubscription.unsubscribe(); + getCoreChrome().setIsVisible(true); if (unsubscribe) { unsubscribe(); @@ -457,7 +481,7 @@ app.controller( }); const updateBreadcrumbs = () => { - chrome.breadcrumbs.set([ + getCoreChrome().setBreadcrumbs([ { text: i18n.translate('xpack.maps.mapController.mapsBreadcrumbLabel', { defaultMessage: 'Maps', @@ -482,7 +506,7 @@ app.controller( }; updateBreadcrumbs(); - addHelpMenuToAppChrome(chrome); + addHelpMenuToAppChrome(); async function doSave(saveOptions) { await store.dispatch(clearTransientLayerStateAndCloseFlyout()); @@ -491,9 +515,9 @@ app.controller( try { id = await savedMap.save(saveOptions); - docTitle.change(savedMap.title); + getCoreChrome().docTitle.change(savedMap.title); } catch (err) { - toastNotifications.addDanger({ + getToasts().addDanger({ title: i18n.translate('xpack.maps.mapController.saveErrorMessage', { defaultMessage: `Error on saving '{title}'`, values: { title: savedMap.title }, @@ -505,7 +529,7 @@ app.controller( } if (id) { - toastNotifications.addSuccess({ + getToasts().addSuccess({ title: i18n.translate('xpack.maps.mapController.saveSuccessMessage', { defaultMessage: `Saved '{title}'`, values: { title: savedMap.title }, @@ -539,6 +563,7 @@ app.controller( }), testId: 'mapsFullScreenMode', run() { + getCoreChrome().setIsVisible(false); store.dispatch(enableFullScreen()); }, }, @@ -556,7 +581,7 @@ app.controller( getInspector().open(inspectorAdapters, {}); }, }, - ...(capabilities.get().maps.save + ...(getMapsCapabilities().save ? [ { id: 'save', @@ -611,7 +636,7 @@ app.controller( showDescription={false} /> ); - showSaveModal(saveModal, npStart.core.i18n.Context); + showSaveModal(saveModal, getCoreI18n().Context); }, }, ] diff --git a/x-pack/legacy/plugins/maps/public/angular/services/gis_map_saved_object_loader.js b/x-pack/legacy/plugins/maps/public/angular/services/gis_map_saved_object_loader.js index bc636c0b200f8..710997a9c0d7f 100644 --- a/x-pack/legacy/plugins/maps/public/angular/services/gis_map_saved_object_loader.js +++ b/x-pack/legacy/plugins/maps/public/angular/services/gis_map_saved_object_loader.js @@ -4,24 +4,27 @@ * you may not use this file except in compliance with the Elastic License. */ +import _ from 'lodash'; import { createSavedGisMapClass } from './saved_gis_map'; -import { uiModules } from 'ui/modules'; import { SavedObjectLoader } from '../../../../../../../src/plugins/saved_objects/public'; -import { npStart } from '../../../../../../../src/legacy/ui/public/new_platform'; +import { + getCoreChrome, + getSavedObjectsClient, + getIndexPatternService, + getCoreOverlays, + getData, + // eslint-disable-next-line @kbn/eslint/no-restricted-paths +} from '../../../../../../plugins/maps/public/kibana_services'; -const module = uiModules.get('app/maps'); - -// This is the only thing that gets injected into controllers -module.service('gisMapSavedObjectLoader', function() { - const savedObjectsClient = npStart.core.savedObjects.client; +export const getMapsSavedObjectLoader = _.once(function() { const services = { - savedObjectsClient, - indexPatterns: npStart.plugins.data.indexPatterns, - search: npStart.plugins.data.search, - chrome: npStart.core.chrome, - overlays: npStart.core.overlays, + savedObjectsClient: getSavedObjectsClient(), + indexPatterns: getIndexPatternService(), + search: getData().search, + chrome: getCoreChrome(), + overlays: getCoreOverlays(), }; const SavedGisMap = createSavedGisMapClass(services); - return new SavedObjectLoader(SavedGisMap, npStart.core.savedObjects.client, npStart.core.chrome); + return new SavedObjectLoader(SavedGisMap, getSavedObjectsClient(), getCoreChrome()); }); diff --git a/x-pack/legacy/plugins/maps/public/components/map_listing.js b/x-pack/legacy/plugins/maps/public/components/map_listing.js index 6fb5930e81a20..ef1d524cb91dd 100644 --- a/x-pack/legacy/plugins/maps/public/components/map_listing.js +++ b/x-pack/legacy/plugins/maps/public/components/map_listing.js @@ -7,7 +7,8 @@ import React from 'react'; import PropTypes from 'prop-types'; import _ from 'lodash'; -import { toastNotifications } from 'ui/notify'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { getToasts } from '../../../../../plugins/maps/public/kibana_services'; import { EuiTitle, EuiFieldSearch, @@ -27,7 +28,6 @@ import { import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; import { addHelpMenuToAppChrome } from '../help_menu_util'; -import chrome from 'ui/chrome'; export const EMPTY_FILTER = ''; @@ -55,7 +55,7 @@ export class MapListing extends React.Component { componentDidMount() { this.fetchItems(); - addHelpMenuToAppChrome(chrome); + addHelpMenuToAppChrome(); } debouncedFetch = _.debounce(async filter => { @@ -91,7 +91,7 @@ export class MapListing extends React.Component { try { await this.props.delete(this.state.selectedIds); } catch (error) { - toastNotifications.addDanger({ + getToasts().addDanger({ title: i18n.translate('xpack.maps.mapListing.unableToDeleteToastTitle', { defaultMessage: `Unable to delete map(s)`, }), diff --git a/x-pack/legacy/plugins/maps/public/connected_components/gis_map/index.js b/x-pack/legacy/plugins/maps/public/connected_components/gis_map/index.js index 39cb2c469e054..2d8265bae9387 100644 --- a/x-pack/legacy/plugins/maps/public/connected_components/gis_map/index.js +++ b/x-pack/legacy/plugins/maps/public/connected_components/gis_map/index.js @@ -18,6 +18,8 @@ import { getQueryableUniqueIndexPatternIds, isToolbarOverlayHidden, } from '../../selectors/map_selectors'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { getCoreChrome } from '../../../../../../plugins/maps/public/kibana_services'; function mapStateToProps(state = {}) { const flyoutDisplay = getFlyoutDisplay(state); @@ -37,7 +39,10 @@ function mapStateToProps(state = {}) { function mapDispatchToProps(dispatch) { return { triggerRefreshTimer: () => dispatch(triggerRefreshTimer()), - exitFullScreen: () => dispatch(exitFullScreen()), + exitFullScreen: () => { + dispatch(exitFullScreen()); + getCoreChrome().setIsVisible(true); + }, cancelAllInFlightRequests: () => dispatch(cancelAllInFlightRequests()), }; } diff --git a/x-pack/legacy/plugins/maps/public/connected_components/gis_map/view.js b/x-pack/legacy/plugins/maps/public/connected_components/gis_map/view.js index 358313b8f5b6d..06097ebea1900 100644 --- a/x-pack/legacy/plugins/maps/public/connected_components/gis_map/view.js +++ b/x-pack/legacy/plugins/maps/public/connected_components/gis_map/view.js @@ -12,7 +12,8 @@ import { ToolbarOverlay } from '../toolbar_overlay/index'; import { LayerPanel } from '../layer_panel/index'; import { AddLayerPanel } from '../layer_addpanel/index'; import { EuiFlexGroup, EuiFlexItem, EuiCallOut } from '@elastic/eui'; -import { ExitFullScreenButton } from 'ui/exit_full_screen'; +import { ExitFullScreenButton } from '../../../../../../../src/plugins/kibana_react/public'; + // eslint-disable-next-line @kbn/eslint/no-restricted-paths import { getIndexPatternsFromIds } from '../../../../../../plugins/maps/public/index_pattern_util'; import { ES_GEO_FIELD_TYPE } from '../../../common/constants'; diff --git a/x-pack/legacy/plugins/maps/public/connected_components/layer_addpanel/view.js b/x-pack/legacy/plugins/maps/public/connected_components/layer_addpanel/view.js index a54df69471aa0..92fcf01f3901f 100644 --- a/x-pack/legacy/plugins/maps/public/connected_components/layer_addpanel/view.js +++ b/x-pack/legacy/plugins/maps/public/connected_components/layer_addpanel/view.js @@ -63,13 +63,13 @@ export class AddLayerPanel extends Component { return; } - const style = + const styleDescriptor = this.state.layer && this.state.layer.getCurrentStyle() ? this.state.layer.getCurrentStyle().getDescriptor() : null; const layerInitProps = { ...options, - style: style, + style: styleDescriptor, }; const newLayer = source.createDefaultLayer(layerInitProps, this.props.mapColors); if (!this._isMounted) { diff --git a/x-pack/legacy/plugins/maps/public/connected_components/layer_panel/filter_editor/filter_editor.js b/x-pack/legacy/plugins/maps/public/connected_components/layer_panel/filter_editor/filter_editor.js index f6bcac0dfc339..40fdac38493d4 100644 --- a/x-pack/legacy/plugins/maps/public/connected_components/layer_panel/filter_editor/filter_editor.js +++ b/x-pack/legacy/plugins/maps/public/connected_components/layer_panel/filter_editor/filter_editor.js @@ -20,12 +20,14 @@ import { import { FormattedMessage } from '@kbn/i18n/react'; import { i18n } from '@kbn/i18n'; -import { getIndexPatternService } from '../../../kibana_services'; +import { + getIndexPatternService, + getUiSettings, + getData, + // eslint-disable-next-line @kbn/eslint/no-restricted-paths +} from '../../../../../../../plugins/maps/public/kibana_services'; import { GlobalFilterCheckbox } from '../../../components/global_filter_checkbox'; -import { npStart } from 'ui/new_platform'; -const { SearchBar } = npStart.plugins.data.ui; - export class FilterEditor extends Component { state = { isPopoverOpen: false, @@ -84,7 +86,8 @@ export class FilterEditor extends Component { _renderQueryPopover() { const layerQuery = this.props.layer.getQuery(); - const { uiSettings } = npStart.core; + const uiSettings = getUiSettings(); + const { SearchBar } = getData().ui; return ( { const label = event.target.value; @@ -22,8 +20,8 @@ export function LayerSettings(props) { }; const onZoomChange = ([min, max]) => { - props.updateMinZoom(props.layerId, Math.max(MIN_ZOOM, parseInt(min, 10))); - props.updateMaxZoom(props.layerId, Math.min(MAX_ZOOM, parseInt(max, 10))); + props.updateMinZoom(props.layerId, Math.max(props.minVisibilityZoom, parseInt(min, 10))); + props.updateMaxZoom(props.layerId, Math.min(props.maxVisibilityZoom, parseInt(max, 10))); }; const onAlphaChange = alpha => { @@ -38,8 +36,8 @@ export function LayerSettings(props) { defaultMessage: 'Visibility', })} formRowDisplay="columnCompressed" - min={MIN_ZOOM} - max={MAX_ZOOM} + min={props.minVisibilityZoom} + max={props.maxVisibilityZoom} value={[props.minZoom, props.maxZoom]} showInput="inputWithPopover" showRange diff --git a/x-pack/legacy/plugins/maps/public/connected_components/layer_panel/style_settings/_style_settings.scss b/x-pack/legacy/plugins/maps/public/connected_components/layer_panel/style_settings/_style_settings.scss index 249b6dfca5c76..f9ad412f7e48a 100644 --- a/x-pack/legacy/plugins/maps/public/connected_components/layer_panel/style_settings/_style_settings.scss +++ b/x-pack/legacy/plugins/maps/public/connected_components/layer_panel/style_settings/_style_settings.scss @@ -1,3 +1,3 @@ .mapStyleSettings__fixedBox { - width: $euiSize * 7.5; -} \ No newline at end of file + width: $euiSize * 7.5; +} diff --git a/x-pack/legacy/plugins/maps/public/connected_components/layer_panel/view.js b/x-pack/legacy/plugins/maps/public/connected_components/layer_panel/view.js index 1b269e388bea0..2521318f0b3c9 100644 --- a/x-pack/legacy/plugins/maps/public/connected_components/layer_panel/view.js +++ b/x-pack/legacy/plugins/maps/public/connected_components/layer_panel/view.js @@ -30,12 +30,11 @@ import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; import { KibanaContextProvider } from '../../../../../../../src/plugins/kibana_react/public'; import { Storage } from '../../../../../../../src/plugins/kibana_utils/public'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { getData, getCore } from '../../../../../../plugins/maps/public/kibana_services'; const localStorage = new Storage(window.localStorage); -// This import will eventually become a dependency injected by the fully deangularized NP plugin. -import { npStart } from 'ui/new_platform'; - export class LayerPanel extends React.Component { state = { displayName: '', @@ -168,8 +167,8 @@ export class LayerPanel extends React.Component { services={{ appName: 'maps', storage: localStorage, - data: npStart.plugins.data, - ...npStart.core, + data: getData(), + ...getCore(), }} > diff --git a/x-pack/legacy/plugins/maps/public/connected_components/map/features_tooltip/feature_geometry_filter_form.js b/x-pack/legacy/plugins/maps/public/connected_components/map/features_tooltip/feature_geometry_filter_form.js index 7063c50edad6a..15824b82965e8 100644 --- a/x-pack/legacy/plugins/maps/public/connected_components/map/features_tooltip/feature_geometry_filter_form.js +++ b/x-pack/legacy/plugins/maps/public/connected_components/map/features_tooltip/feature_geometry_filter_form.js @@ -12,7 +12,8 @@ import { i18n } from '@kbn/i18n'; import { createSpatialFilterWithGeometry } from '../../../../../../../plugins/maps/public/elasticsearch_geo_utils'; import { GEO_JSON_TYPE } from '../../../../common/constants'; import { GeometryFilterForm } from '../../../components/geometry_filter_form'; -import { UrlOverflowService } from 'ui/error_url_overflow'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { UrlOverflowService } from '../../../../../../../../src/plugins/kibana_legacy/public'; import rison from 'rison-node'; // over estimated and imprecise value to ensure filter has additional room for any meta keys added when filter is mapped. diff --git a/x-pack/legacy/plugins/maps/public/connected_components/map/mb/draw_control/draw_control.js b/x-pack/legacy/plugins/maps/public/connected_components/map/mb/draw_control/draw_control.js index df2988d399c5b..cc0e665525036 100644 --- a/x-pack/legacy/plugins/maps/public/connected_components/map/mb/draw_control/draw_control.js +++ b/x-pack/legacy/plugins/maps/public/connected_components/map/mb/draw_control/draw_control.js @@ -12,7 +12,6 @@ import DrawRectangle from 'mapbox-gl-draw-rectangle-mode'; import { DrawCircle } from './draw_circle'; import { createDistanceFilterWithMeta, - createSpatialFilterWithBoundingBox, createSpatialFilterWithGeometry, getBoundingBoxGeometry, roundCoordinates, @@ -84,23 +83,17 @@ export class DrawControl extends React.Component { roundCoordinates(geometry.coordinates); try { - const options = { + const filter = createSpatialFilterWithGeometry({ + geometry: + this.props.drawState.drawType === DRAW_TYPE.BOUNDS + ? getBoundingBoxGeometry(geometry) + : geometry, indexPatternId: this.props.drawState.indexPatternId, geoFieldName: this.props.drawState.geoFieldName, geoFieldType: this.props.drawState.geoFieldType, geometryLabel: this.props.drawState.geometryLabel, relation: this.props.drawState.relation, - }; - const filter = - this.props.drawState.drawType === DRAW_TYPE.BOUNDS - ? createSpatialFilterWithBoundingBox({ - ...options, - geometry: getBoundingBoxGeometry(geometry), - }) - : createSpatialFilterWithGeometry({ - ...options, - geometry, - }); + }); this.props.addFilters([filter]); } catch (error) { // TODO notify user why filter was not created diff --git a/x-pack/legacy/plugins/maps/public/connected_components/map/mb/view.js b/x-pack/legacy/plugins/maps/public/connected_components/map/mb/view.js index fedc1902d80a2..a36e1d7048e92 100644 --- a/x-pack/legacy/plugins/maps/public/connected_components/map/mb/view.js +++ b/x-pack/legacy/plugins/maps/public/connected_components/map/mb/view.js @@ -14,11 +14,15 @@ import { } from './utils'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths import { getGlyphUrl, isRetina } from '../../../../../../../plugins/maps/public/meta'; -import { DECIMAL_DEGREES_PRECISION, ZOOM_PRECISION } from '../../../../common/constants'; +import { + DECIMAL_DEGREES_PRECISION, + MAX_ZOOM, + MIN_ZOOM, + ZOOM_PRECISION, +} from '../../../../common/constants'; import mapboxgl from 'mapbox-gl/dist/mapbox-gl-csp'; import mbWorkerUrl from '!!file-loader!mapbox-gl/dist/mapbox-gl-csp-worker'; import mbRtlPlugin from '!!file-loader!@mapbox/mapbox-gl-rtl-text/mapbox-gl-rtl-text.min.js'; -import chrome from 'ui/chrome'; import { spritesheet } from '@elastic/maki'; import sprites1 from '@elastic/maki/dist/sprite@1.png'; import sprites2 from '@elastic/maki/dist/sprite@2.png'; @@ -29,6 +33,8 @@ import { clampToLonBounds, // eslint-disable-next-line @kbn/eslint/no-restricted-paths } from '../../../../../../../plugins/maps/public/elasticsearch_geo_utils'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { getInjectedVarFunc } from '../../../../../../../plugins/maps/public/kibana_services'; mapboxgl.workerUrl = mbWorkerUrl; mapboxgl.setRTLTextPlugin(mbRtlPlugin); @@ -129,8 +135,10 @@ export class MBMapContainer extends React.Component { container: this.refs.mapContainer, style: mbStyle, scrollZoom: this.props.scrollZoom, - preserveDrawingBuffer: chrome.getInjected('preserveDrawingBuffer', false), + preserveDrawingBuffer: getInjectedVarFunc()('preserveDrawingBuffer', false), interactive: !this.props.disableInteractive, + minZoom: MIN_ZOOM, + maxZoom: MAX_ZOOM, }; const initialView = _.get(this.props.goto, 'center'); if (initialView) { diff --git a/x-pack/legacy/plugins/maps/public/embeddable/map_embeddable.tsx b/x-pack/legacy/plugins/maps/public/embeddable/map_embeddable.tsx index bdd2d863e6920..b8e4c84ad56a1 100644 --- a/x-pack/legacy/plugins/maps/public/embeddable/map_embeddable.tsx +++ b/x-pack/legacy/plugins/maps/public/embeddable/map_embeddable.tsx @@ -11,7 +11,6 @@ import { render, unmountComponentAtNode } from 'react-dom'; import 'mapbox-gl/dist/mapbox-gl.css'; import { I18nContext } from 'ui/i18n'; -import { npStart } from 'ui/new_platform'; import { Subscription } from 'rxjs'; import { Unsubscribe } from 'redux'; import { @@ -59,6 +58,8 @@ import { getMapCenter, getMapZoom, getHiddenLayerIds } from '../selectors/map_se import { MAP_SAVED_OBJECT_TYPE } from '../../common/constants'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths import { RenderToolTipContent } from '../../../../../plugins/maps/public/layers/tooltips/tooltip_property'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { getUiActions } from '../../../../../plugins/maps/public/kibana_services'; interface MapEmbeddableConfig { editUrl?: string; @@ -269,7 +270,7 @@ export class MapEmbeddable extends Embeddable { - npStart.plugins.uiActions.executeTriggerActions(APPLY_FILTER_TRIGGER, { + getUiActions().executeTriggerActions(APPLY_FILTER_TRIGGER, { embeddable: this, filters, }); diff --git a/x-pack/legacy/plugins/maps/public/embeddable/map_embeddable_factory.ts b/x-pack/legacy/plugins/maps/public/embeddable/map_embeddable_factory.ts index 5deb3057a449e..96c3baf634a83 100644 --- a/x-pack/legacy/plugins/maps/public/embeddable/map_embeddable_factory.ts +++ b/x-pack/legacy/plugins/maps/public/embeddable/map_embeddable_factory.ts @@ -5,14 +5,18 @@ */ import _ from 'lodash'; -import chrome from 'ui/chrome'; -import { capabilities } from 'ui/capabilities'; import { i18n } from '@kbn/i18n'; import { npSetup, npStart } from 'ui/new_platform'; -import { SavedObjectLoader } from 'src/plugins/saved_objects/public'; import { IIndexPattern } from 'src/plugins/data/public'; +// @ts-ignore +import { getMapsSavedObjectLoader } from '../angular/services/gis_map_saved_object_loader'; import { MapEmbeddable, MapEmbeddableInput } from './map_embeddable'; -import { getIndexPatternService } from '../kibana_services'; +import { + getIndexPatternService, + getHttp, + getMapsCapabilities, + // eslint-disable-next-line @kbn/eslint/no-restricted-paths +} from '../../../../../plugins/maps/public/kibana_services'; import { EmbeddableFactoryDefinition, IContainer, @@ -26,7 +30,6 @@ import { getQueryableUniqueIndexPatternIds } from '../selectors/map_selectors'; import { getInitialLayers } from '../angular/get_initial_layers'; import { mergeInputWithSavedMap } from './merge_input_with_saved_map'; import '../angular/services/gis_map_saved_object_loader'; -import { bindSetupCoreAndPlugins, bindStartCoreAndPlugins } from '../plugin'; // @ts-ignore import { bindSetupCoreAndPlugins as bindNpSetupCoreAndPlugins, @@ -44,14 +47,12 @@ export class MapEmbeddableFactory implements EmbeddableFactoryDefinition { }; constructor() { // Init required services. Necessary while in legacy - bindSetupCoreAndPlugins(npSetup.core, npSetup.plugins); bindNpSetupCoreAndPlugins(npSetup.core, npSetup.plugins); - bindStartCoreAndPlugins(npStart.core, npStart.plugins); bindNpStartCoreAndPlugins(npStart.core, npStart.plugins); } async isEditable() { - return capabilities.get().maps.save as boolean; + return getMapsCapabilities().save as boolean; } // Not supported yet for maps types. @@ -96,8 +97,7 @@ export class MapEmbeddableFactory implements EmbeddableFactoryDefinition { } async _fetchSavedMap(savedObjectId: string) { - const $injector = await chrome.dangerouslyGetActiveInjector(); - const savedObjectLoader = $injector.get('gisMapSavedObjectLoader'); + const savedObjectLoader = getMapsSavedObjectLoader(); return await savedObjectLoader.get(savedObjectId); } @@ -114,7 +114,7 @@ export class MapEmbeddableFactory implements EmbeddableFactoryDefinition { { layerList, title: savedMap.title, - editUrl: chrome.addBasePath(createMapPath(savedObjectId)), + editUrl: getHttp().basePath.prepend(createMapPath(savedObjectId)), indexPatterns, editable: await this.isEditable(), }, diff --git a/x-pack/legacy/plugins/maps/public/help_menu_util.js b/x-pack/legacy/plugins/maps/public/help_menu_util.js index 72d51cc180eb3..70b9340b562cd 100644 --- a/x-pack/legacy/plugins/maps/public/help_menu_util.js +++ b/x-pack/legacy/plugins/maps/public/help_menu_util.js @@ -3,10 +3,13 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -import { ELASTIC_WEBSITE_URL, DOC_LINK_VERSION } from 'ui/documentation_links'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { getDocLinks, getCoreChrome } from '../../../../plugins/maps/public/kibana_services'; -export function addHelpMenuToAppChrome(chrome) { - chrome.helpExtension.set({ +export function addHelpMenuToAppChrome() { + const { ELASTIC_WEBSITE_URL, DOC_LINK_VERSION } = getDocLinks(); + + getCoreChrome().setHelpExtension({ appName: 'Maps', links: [ { diff --git a/x-pack/legacy/plugins/maps/public/index.ts b/x-pack/legacy/plugins/maps/public/index.ts index b69485e251be4..8555594e909d3 100644 --- a/x-pack/legacy/plugins/maps/public/index.ts +++ b/x-pack/legacy/plugins/maps/public/index.ts @@ -4,7 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ -import './kibana_services'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import '../../../../plugins/maps/public/kibana_services'; // import the uiExports that we want to "use" import 'uiExports/inspectorViews'; diff --git a/x-pack/legacy/plugins/maps/public/kibana_services.d.ts b/x-pack/legacy/plugins/maps/public/kibana_services.d.ts deleted file mode 100644 index 89b1fee1aa842..0000000000000 --- a/x-pack/legacy/plugins/maps/public/kibana_services.d.ts +++ /dev/null @@ -1,23 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { IIndexPattern } from 'src/plugins/data/public'; - -export function getIndexPatternService(): { - get: (id: string) => IIndexPattern | undefined; -}; - -export function setLicenseId(args: unknown): void; -export function setInspector(args: unknown): void; -export function setFileUpload(args: unknown): void; -export function setIndexPatternSelect(args: unknown): void; -export function setHttp(args: unknown): void; -export function setTimeFilter(args: unknown): void; -export function setUiSettings(args: unknown): void; -export function setInjectedVarFunc(args: unknown): void; -export function setToasts(args: unknown): void; -export function setIndexPatternService(args: unknown): void; -export function setAutocompleteService(args: unknown): void; diff --git a/x-pack/legacy/plugins/maps/public/kibana_services.js b/x-pack/legacy/plugins/maps/public/kibana_services.js deleted file mode 100644 index a6491fe1aa6d4..0000000000000 --- a/x-pack/legacy/plugins/maps/public/kibana_services.js +++ /dev/null @@ -1,29 +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. - */ - -let indexPatternService; -export const setIndexPatternService = dataIndexPatterns => - (indexPatternService = dataIndexPatterns); -export const getIndexPatternService = () => indexPatternService; - -let inspector; -export const setInspector = newInspector => (inspector = newInspector); -export const getInspector = () => { - return inspector; -}; - -let getInjectedVar; -export const setInjectedVarFunc = getInjectedVarFunc => (getInjectedVar = getInjectedVarFunc); -export const getInjectedVarFunc = () => getInjectedVar; - -let indexPatternSelectComponent; -export const setIndexPatternSelect = indexPatternSelect => - (indexPatternSelectComponent = indexPatternSelect); -export const getIndexPatternSelectComponent = () => indexPatternSelectComponent; - -let dataTimeFilter; -export const setTimeFilter = timeFilter => (dataTimeFilter = timeFilter); -export const getTimeFilter = () => dataTimeFilter; diff --git a/x-pack/legacy/plugins/maps/public/plugin.ts b/x-pack/legacy/plugins/maps/public/plugin.ts index 0fa7e1106a6df..71f1a30c1fbef 100644 --- a/x-pack/legacy/plugins/maps/public/plugin.ts +++ b/x-pack/legacy/plugins/maps/public/plugin.ts @@ -4,28 +4,13 @@ * you may not use this file except in compliance with the Elastic License. */ -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import '../../../../plugins/maps/public/layers/layer_wizard_registry'; -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import '../../../../plugins/maps/public/layers/sources/source_registry'; -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import '../../../../plugins/maps/public/layers/load_layer_wizards'; - import { Plugin, CoreStart, CoreSetup } from 'src/core/public'; // @ts-ignore -import { wrapInI18nContext } from 'ui/i18n'; -// @ts-ignore import { Start as InspectorStartContract } from 'src/plugins/inspector/public'; // @ts-ignore +import { wrapInI18nContext } from 'ui/i18n'; +// @ts-ignore import { MapListing } from './components/map_listing'; -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { - setInspector, - setIndexPatternSelect, - setTimeFilter, - setInjectedVarFunc, - setIndexPatternService, -} from './kibana_services'; // @ts-ignore import { bindSetupCoreAndPlugins as bindNpSetupCoreAndPlugins, @@ -62,20 +47,6 @@ interface MapsPluginStartDependencies { // file_upload TODO: Export type from file upload and use here } -export const bindSetupCoreAndPlugins = (core: CoreSetup, plugins: any) => { - const { injectedMetadata } = core; - setInjectedVarFunc(injectedMetadata.getInjectedVar); - setInjectedVarFunc(core.injectedMetadata.getInjectedVar); -}; - -export const bindStartCoreAndPlugins = (core: CoreStart, plugins: any) => { - const { data, inspector } = plugins; - setInspector(inspector); - setIndexPatternSelect(data.ui.IndexPatternSelect); - setTimeFilter(data.query.timefilter.timefilter); - setIndexPatternService(data.indexPatterns); -}; - /** @internal */ export class MapsPlugin implements Plugin { public setup(core: CoreSetup, { __LEGACY: { uiModules }, np }: MapsPluginSetupDependencies) { @@ -85,14 +56,12 @@ export class MapsPlugin implements Plugin { return reactDirective(wrapInI18nContext(MapListing)); }); - bindSetupCoreAndPlugins(core, np); bindNpSetupCoreAndPlugins(core, np); np.home.featureCatalogue.register(featureCatalogueEntry); } public start(core: CoreStart, plugins: MapsPluginStartDependencies) { - bindStartCoreAndPlugins(core, plugins); bindNpStartCoreAndPlugins(core, plugins); } } diff --git a/x-pack/legacy/plugins/maps/public/register_vis_type_alias.js b/x-pack/legacy/plugins/maps/public/register_vis_type_alias.js index 64a42173098ee..9dc07bcb5dc0e 100644 --- a/x-pack/legacy/plugins/maps/public/register_vis_type_alias.js +++ b/x-pack/legacy/plugins/maps/public/register_vis_type_alias.js @@ -4,12 +4,20 @@ * you may not use this file except in compliance with the Elastic License. */ -import chrome from 'ui/chrome'; -import { npSetup } from '../../../../../src/legacy/ui/public/new_platform'; import { i18n } from '@kbn/i18n'; import { APP_ID, APP_ICON, MAP_BASE_URL } from '../common/constants'; +import { + getInjectedVarFunc, + getVisualizations, + // eslint-disable-next-line @kbn/eslint/no-restricted-paths +} from '../../../../plugins/maps/public/kibana_services'; +import { npSetup } from 'ui/new_platform'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { bindSetupCoreAndPlugins } from '../../../../plugins/maps/public/plugin'; -const showMapVisualizationTypes = chrome.getInjected('showMapVisualizationTypes', false); +bindSetupCoreAndPlugins(npSetup.core, npSetup.plugins); + +const showMapVisualizationTypes = getInjectedVarFunc()('showMapVisualizationTypes', false); const description = i18n.translate('xpack.maps.visTypeAlias.description', { defaultMessage: 'Create and style maps with multiple layers and indices.', @@ -23,7 +31,7 @@ The Maps app offers more functionality and is easier to use.`, } ); -npSetup.plugins.visualizations.registerAlias({ +getVisualizations().registerAlias({ aliasUrl: MAP_BASE_URL, name: APP_ID, title: i18n.translate('xpack.maps.visTypeAlias.title', { @@ -37,5 +45,5 @@ npSetup.plugins.visualizations.registerAlias({ }); if (!showMapVisualizationTypes) { - npSetup.plugins.visualizations.hideTypes(['region_map', 'tile_map']); + getVisualizations().hideTypes(['region_map', 'tile_map']); } diff --git a/x-pack/legacy/plugins/maps/public/routes.js b/x-pack/legacy/plugins/maps/public/routes.js index 49705acb417e2..c082e0e1352c0 100644 --- a/x-pack/legacy/plugins/maps/public/routes.js +++ b/x-pack/legacy/plugins/maps/public/routes.js @@ -5,20 +5,23 @@ */ import { i18n } from '@kbn/i18n'; -import { capabilities } from 'ui/capabilities'; -import chrome from 'ui/chrome'; import routes from 'ui/routes'; -import { docTitle } from 'ui/doc_title'; import listingTemplate from './angular/listing_ng_wrapper.html'; import mapTemplate from './angular/map.html'; -import { npStart } from 'ui/new_platform'; +import { + getSavedObjectsClient, + getCoreChrome, + getMapsCapabilities, + // eslint-disable-next-line @kbn/eslint/no-restricted-paths +} from '../../../../plugins/maps/public/kibana_services'; +import { getMapsSavedObjectLoader } from './angular/services/gis_map_saved_object_loader'; routes.enable(); routes .defaults(/.*/, { - badge: uiCapabilities => { - if (uiCapabilities.maps.save) { + badge: () => { + if (getMapsCapabilities().save) { return undefined; } @@ -35,7 +38,8 @@ routes }) .when('/', { template: listingTemplate, - controller($scope, gisMapSavedObjectLoader, config) { + controller($scope, config) { + const gisMapSavedObjectLoader = getMapsSavedObjectLoader(); $scope.listingLimit = config.get('savedObjects:listingLimit'); $scope.find = search => { return gisMapSavedObjectLoader.find(search, $scope.listingLimit); @@ -43,19 +47,17 @@ routes $scope.delete = ids => { return gisMapSavedObjectLoader.delete(ids); }; - $scope.readOnly = !capabilities.get().maps.save; + $scope.readOnly = !getMapsCapabilities().save; }, resolve: { hasMaps: function(kbnUrl) { - chrome - .getSavedObjectsClient() + getSavedObjectsClient() .find({ type: 'map', perPage: 1 }) .then(resp => { // Do not show empty listing page, just redirect to a new map if (resp.savedObjects.length === 0) { kbnUrl.redirect('/map'); } - return true; }); }, @@ -65,7 +67,8 @@ routes template: mapTemplate, controller: 'GisMapController', resolve: { - map: function(gisMapSavedObjectLoader, redirectWhenMissing) { + map: function(redirectWhenMissing) { + const gisMapSavedObjectLoader = getMapsSavedObjectLoader(); return gisMapSavedObjectLoader.get().catch( redirectWhenMissing({ map: '/', @@ -78,13 +81,14 @@ routes template: mapTemplate, controller: 'GisMapController', resolve: { - map: function(gisMapSavedObjectLoader, redirectWhenMissing, $route) { + map: function(redirectWhenMissing, $route) { + const gisMapSavedObjectLoader = getMapsSavedObjectLoader(); const id = $route.current.params.id; return gisMapSavedObjectLoader .get(id) .then(savedMap => { - npStart.core.chrome.recentlyAccessed.add(savedMap.getFullPath(), savedMap.title, id); - docTitle.change(savedMap.title); + getCoreChrome().recentlyAccessed.add(savedMap.getFullPath(), savedMap.title, id); + getCoreChrome().docTitle.change(savedMap.title); return savedMap; }) .catch( diff --git a/x-pack/legacy/plugins/maps/public/selectors/map_selectors.js b/x-pack/legacy/plugins/maps/public/selectors/map_selectors.js index 59346e4c6fb98..1e71025935519 100644 --- a/x-pack/legacy/plugins/maps/public/selectors/map_selectors.js +++ b/x-pack/legacy/plugins/maps/public/selectors/map_selectors.js @@ -16,7 +16,10 @@ import { VectorLayer } from '../../../../../plugins/maps/public/layers/vector_la import { HeatmapLayer } from '../../../../../plugins/maps/public/layers/heatmap_layer'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths import { BlendedVectorLayer } from '../../../../../plugins/maps/public/layers/blended_vector_layer'; -import { getTimeFilter } from '../kibana_services'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { getTimeFilter } from '../../../../../plugins/maps/public/kibana_services'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { TiledVectorLayer } from '../../../../../plugins/maps/public/layers/tiled_vector_layer'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths import { getInspectorAdapters } from '../../../../../plugins/maps/public/reducers/non_serializable_instances'; import { @@ -50,6 +53,8 @@ function createLayerInstance(layerDescriptor, inspectorAdapters) { return new HeatmapLayer({ layerDescriptor, source }); case BlendedVectorLayer.type: return new BlendedVectorLayer({ layerDescriptor, source }); + case TiledVectorLayer.type: + return new TiledVectorLayer({ layerDescriptor, source }); default: throw new Error(`Unrecognized layerType ${layerDescriptor.type}`); } diff --git a/x-pack/legacy/plugins/maps/public/selectors/map_selectors.test.js b/x-pack/legacy/plugins/maps/public/selectors/map_selectors.test.js index 77bd29259647c..72cc748617540 100644 --- a/x-pack/legacy/plugins/maps/public/selectors/map_selectors.test.js +++ b/x-pack/legacy/plugins/maps/public/selectors/map_selectors.test.js @@ -5,6 +5,7 @@ */ jest.mock('../../../../../plugins/maps/public/layers/vector_layer', () => {}); +jest.mock('../../../../../plugins/maps/public/layers/tiled_vector_layer', () => {}); jest.mock('../../../../../plugins/maps/public/layers/blended_vector_layer', () => {}); jest.mock('../../../../../plugins/maps/public/layers/heatmap_layer', () => {}); jest.mock('../../../../../plugins/maps/public/layers/vector_tile_layer', () => {}); @@ -14,7 +15,7 @@ jest.mock('../../../../../plugins/maps/public/reducers/non_serializable_instance return {}; }, })); -jest.mock('../kibana_services', () => ({ +jest.mock('../../../../../plugins/maps/public/kibana_services', () => ({ getTimeFilter: () => ({ getTime: () => { return { diff --git a/x-pack/legacy/plugins/maps/server/maps_telemetry/maps_telemetry.ts b/x-pack/legacy/plugins/maps/server/maps_telemetry/maps_telemetry.ts index 5657e14622e9b..27c0211446e85 100644 --- a/x-pack/legacy/plugins/maps/server/maps_telemetry/maps_telemetry.ts +++ b/x-pack/legacy/plugins/maps/server/maps_telemetry/maps_telemetry.ts @@ -12,7 +12,7 @@ import { } from 'src/core/server'; import { IFieldType, IIndexPattern } from 'src/plugins/data/public'; import { - EMS_FILE, + SOURCE_TYPES, ES_GEO_FIELD_TYPE, MAP_SAVED_OBJECT_TYPE, TELEMETRY_TYPE, @@ -87,6 +87,8 @@ export function buildMapsTelemetry({ const mapsCount = layerLists.length; const dataSourcesCount = layerLists.map(lList => { + // todo: not every source-descriptor has an id + // @ts-ignore const sourceIdList = lList.map((layer: LayerDescriptor) => layer.sourceDescriptor.id); return _.uniq(sourceIdList).length; }); @@ -98,7 +100,7 @@ export function buildMapsTelemetry({ const emsLayersCount = layerLists.map(lList => _(lList) .countBy((layer: LayerDescriptor) => { - const isEmsFile = _.get(layer, 'sourceDescriptor.type') === EMS_FILE; + const isEmsFile = _.get(layer, 'sourceDescriptor.type') === SOURCE_TYPES.EMS_FILE; return isEmsFile && _.get(layer, 'sourceDescriptor.id'); }) .pick((val, key) => key !== 'false') diff --git a/x-pack/legacy/plugins/monitoring/public/components/metricbeat_migration/flyout/__snapshots__/flyout.test.js.snap b/x-pack/legacy/plugins/monitoring/public/components/metricbeat_migration/flyout/__snapshots__/flyout.test.js.snap index eb1c65c6a696d..4a7537166bd8a 100644 --- a/x-pack/legacy/plugins/monitoring/public/components/metricbeat_migration/flyout/__snapshots__/flyout.test.js.snap +++ b/x-pack/legacy/plugins/monitoring/public/components/metricbeat_migration/flyout/__snapshots__/flyout.test.js.snap @@ -161,7 +161,7 @@ exports[`Flyout apm part two should show instructions to migrate to metricbeat 1 "children":

", - "maxScreenshotDimension": 1950, - "proxy": Object { - "enabled": false, - }, - }, - "type": "chromium", - }, - "concurrency": 4, - "loadDelay": 3000, - "maxAttempts": 1, - "networkPolicy": Object { - "enabled": true, - "rules": Array [ - Object { - "allow": true, - "protocol": "http:", - }, - Object { - "allow": true, - "protocol": "https:", - }, - Object { - "allow": true, - "protocol": "ws:", - }, - Object { - "allow": true, - "protocol": "wss:", - }, - Object { - "allow": true, - "protocol": "data:", - }, - Object { - "allow": false, - }, - ], - }, - "settleTime": 1000, - "timeout": 20000, - "timeouts": Object { - "openUrl": 30000, - "renderComplete": 30000, - "waitForElements": 30000, - }, - "viewport": Object { - "height": 1200, - "width": 1950, - }, - "zoom": 2, - }, - "csv": Object { - "checkForFormulas": true, - "enablePanelActionDownload": true, - "maxSizeBytes": 10485760, - "scroll": Object { - "duration": "30s", - "size": 500, - }, - "useByteOrderMarkEncoding": false, - }, - "enabled": true, - "encryptionKey": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", - "index": ".reporting", - "kibanaServer": Object {}, - "poll": Object { - "jobCompletionNotifier": Object { - "interval": 10000, - "intervalErrorMultiplier": 5, - }, - "jobsRefresh": Object { - "interval": 5000, - "intervalErrorMultiplier": 5, - }, - }, - "queue": Object { - "indexInterval": "week", - "pollEnabled": true, - "pollInterval": 3000, - "pollIntervalErrorMultiplier": 10, - "timeout": 120000, - }, - "roles": Object { - "allow": Array [ - "reporting_user", - ], - }, -} -`; - -exports[`config schema with context {"dev":false,"dist":true} produces correct config 1`] = ` -Object { - "capture": Object { - "browser": Object { - "autoDownload": false, - "chromium": Object { - "disableSandbox": "", - "maxScreenshotDimension": 1950, - "proxy": Object { - "enabled": false, - }, - }, - "type": "chromium", - }, - "concurrency": 4, - "loadDelay": 3000, - "maxAttempts": 3, - "networkPolicy": Object { - "enabled": true, - "rules": Array [ - Object { - "allow": true, - "protocol": "http:", - }, - Object { - "allow": true, - "protocol": "https:", - }, - Object { - "allow": true, - "protocol": "ws:", - }, - Object { - "allow": true, - "protocol": "wss:", - }, - Object { - "allow": true, - "protocol": "data:", - }, - Object { - "allow": false, - }, - ], - }, - "settleTime": 1000, - "timeout": 20000, - "timeouts": Object { - "openUrl": 30000, - "renderComplete": 30000, - "waitForElements": 30000, - }, - "viewport": Object { - "height": 1200, - "width": 1950, - }, - "zoom": 2, - }, - "csv": Object { - "checkForFormulas": true, - "enablePanelActionDownload": true, - "maxSizeBytes": 10485760, - "scroll": Object { - "duration": "30s", - "size": 500, - }, - "useByteOrderMarkEncoding": false, - }, - "enabled": true, - "index": ".reporting", - "kibanaServer": Object {}, - "poll": Object { - "jobCompletionNotifier": Object { - "interval": 10000, - "intervalErrorMultiplier": 5, - }, - "jobsRefresh": Object { - "interval": 5000, - "intervalErrorMultiplier": 5, - }, - }, - "queue": Object { - "indexInterval": "week", - "pollEnabled": true, - "pollInterval": 3000, - "pollIntervalErrorMultiplier": 10, - "timeout": 120000, - }, - "roles": Object { - "allow": Array [ - "reporting_user", - ], - }, -} -`; - -exports[`config schema with context {"dev":true,"dist":false} produces correct config 1`] = ` -Object { - "capture": Object { - "browser": Object { - "autoDownload": true, - "chromium": Object { - "disableSandbox": "", - "maxScreenshotDimension": 1950, - "proxy": Object { - "enabled": false, - }, - }, - "type": "chromium", - }, - "concurrency": 4, - "loadDelay": 3000, - "maxAttempts": 1, - "networkPolicy": Object { - "enabled": true, - "rules": Array [ - Object { - "allow": true, - "protocol": "http:", - }, - Object { - "allow": true, - "protocol": "https:", - }, - Object { - "allow": true, - "protocol": "ws:", - }, - Object { - "allow": true, - "protocol": "wss:", - }, - Object { - "allow": true, - "protocol": "data:", - }, - Object { - "allow": false, - }, - ], - }, - "settleTime": 1000, - "timeout": 20000, - "timeouts": Object { - "openUrl": 30000, - "renderComplete": 30000, - "waitForElements": 30000, - }, - "viewport": Object { - "height": 1200, - "width": 1950, - }, - "zoom": 2, - }, - "csv": Object { - "checkForFormulas": true, - "enablePanelActionDownload": true, - "maxSizeBytes": 10485760, - "scroll": Object { - "duration": "30s", - "size": 500, - }, - "useByteOrderMarkEncoding": false, - }, - "enabled": true, - "encryptionKey": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", - "index": ".reporting", - "kibanaServer": Object {}, - "poll": Object { - "jobCompletionNotifier": Object { - "interval": 10000, - "intervalErrorMultiplier": 5, - }, - "jobsRefresh": Object { - "interval": 5000, - "intervalErrorMultiplier": 5, - }, - }, - "queue": Object { - "indexInterval": "week", - "pollEnabled": true, - "pollInterval": 3000, - "pollIntervalErrorMultiplier": 10, - "timeout": 120000, - }, - "roles": Object { - "allow": Array [ - "reporting_user", - ], - }, -} -`; - -exports[`config schema with context {"dev":true,"dist":true} produces correct config 1`] = ` -Object { - "capture": Object { - "browser": Object { - "autoDownload": false, - "chromium": Object { - "disableSandbox": "", - "maxScreenshotDimension": 1950, - "proxy": Object { - "enabled": false, - }, - }, - "type": "chromium", - }, - "concurrency": 4, - "loadDelay": 3000, - "maxAttempts": 3, - "networkPolicy": Object { - "enabled": true, - "rules": Array [ - Object { - "allow": true, - "protocol": "http:", - }, - Object { - "allow": true, - "protocol": "https:", - }, - Object { - "allow": true, - "protocol": "ws:", - }, - Object { - "allow": true, - "protocol": "wss:", - }, - Object { - "allow": true, - "protocol": "data:", - }, - Object { - "allow": false, - }, - ], - }, - "settleTime": 1000, - "timeout": 20000, - "timeouts": Object { - "openUrl": 30000, - "renderComplete": 30000, - "waitForElements": 30000, - }, - "viewport": Object { - "height": 1200, - "width": 1950, - }, - "zoom": 2, - }, - "csv": Object { - "checkForFormulas": true, - "enablePanelActionDownload": true, - "maxSizeBytes": 10485760, - "scroll": Object { - "duration": "30s", - "size": 500, - }, - "useByteOrderMarkEncoding": false, - }, - "enabled": true, - "index": ".reporting", - "kibanaServer": Object {}, - "poll": Object { - "jobCompletionNotifier": Object { - "interval": 10000, - "intervalErrorMultiplier": 5, - }, - "jobsRefresh": Object { - "interval": 5000, - "intervalErrorMultiplier": 5, - }, - }, - "queue": Object { - "indexInterval": "week", - "pollEnabled": true, - "pollInterval": 3000, - "pollIntervalErrorMultiplier": 10, - "timeout": 120000, - }, - "roles": Object { - "allow": Array [ - "reporting_user", - ], - }, -} -`; diff --git a/x-pack/legacy/plugins/reporting/common/constants.ts b/x-pack/legacy/plugins/reporting/common/constants.ts index e3d6a4274e7df..f30a7cc87f318 100644 --- a/x-pack/legacy/plugins/reporting/common/constants.ts +++ b/x-pack/legacy/plugins/reporting/common/constants.ts @@ -20,6 +20,7 @@ export const API_GENERATE_IMMEDIATE = `${API_BASE_URL_V1}/generate/immediate/csv export const CONTENT_TYPE_CSV = 'text/csv'; export const CSV_REPORTING_ACTION = 'downloadCsvReport'; export const CSV_BOM_CHARS = '\ufeff'; +export const CSV_FORMULA_CHARS = ['=', '+', '-', '@']; export const WHITELISTED_JOB_CONTENT_TYPES = [ 'application/json', diff --git a/x-pack/legacy/plugins/reporting/config.ts b/x-pack/legacy/plugins/reporting/config.ts deleted file mode 100644 index 5eceb84c83e43..0000000000000 --- a/x-pack/legacy/plugins/reporting/config.ts +++ /dev/null @@ -1,183 +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 { BROWSER_TYPE } from './common/constants'; -// @ts-ignore untyped module -import { config as appConfig } from './server/config/config'; -import { getDefaultChromiumSandboxDisabled } from './server/browsers'; - -export async function config(Joi: any) { - return Joi.object({ - enabled: Joi.boolean().default(true), - kibanaServer: Joi.object({ - protocol: Joi.string().valid(['http', 'https']), - hostname: Joi.string().invalid('0'), - port: Joi.number().integer(), - }).default(), - queue: Joi.object({ - indexInterval: Joi.string().default('week'), - pollEnabled: Joi.boolean().default(true), - pollInterval: Joi.number() - .integer() - .default(3000), - pollIntervalErrorMultiplier: Joi.number() - .integer() - .default(10), - timeout: Joi.number() - .integer() - .default(120000), - }).default(), - capture: Joi.object({ - timeouts: Joi.object({ - openUrl: Joi.number() - .integer() - .default(30000), - waitForElements: Joi.number() - .integer() - .default(30000), - renderComplete: Joi.number() - .integer() - .default(30000), - }).default(), - networkPolicy: Joi.object({ - enabled: Joi.boolean().default(true), - rules: Joi.array() - .items( - Joi.object({ - allow: Joi.boolean().required(), - protocol: Joi.string(), - host: Joi.string(), - }) - ) - .default([ - { allow: true, protocol: 'http:' }, - { allow: true, protocol: 'https:' }, - { allow: true, protocol: 'ws:' }, - { allow: true, protocol: 'wss:' }, - { allow: true, protocol: 'data:' }, - { allow: false }, // Default action is to deny! - ]), - }).default(), - zoom: Joi.number() - .integer() - .default(2), - viewport: Joi.object({ - width: Joi.number() - .integer() - .default(1950), - height: Joi.number() - .integer() - .default(1200), - }).default(), - timeout: Joi.number() - .integer() - .default(20000), // deprecated - loadDelay: Joi.number() - .integer() - .default(3000), - settleTime: Joi.number() - .integer() - .default(1000), // deprecated - concurrency: Joi.number() - .integer() - .default(appConfig.concurrency), // deprecated - browser: Joi.object({ - type: Joi.any() - .valid(BROWSER_TYPE) - .default(BROWSER_TYPE), - autoDownload: Joi.boolean().when('$dist', { - is: true, - then: Joi.default(false), - otherwise: Joi.default(true), - }), - chromium: Joi.object({ - inspect: Joi.boolean() - .when('$dev', { - is: false, - then: Joi.valid(false), - else: Joi.default(false), - }) - .default(), - disableSandbox: Joi.boolean().default(await getDefaultChromiumSandboxDisabled()), - proxy: Joi.object({ - enabled: Joi.boolean().default(false), - server: Joi.string() - .uri({ scheme: ['http', 'https'] }) - .when('enabled', { - is: Joi.valid(false), - then: Joi.valid(null), - else: Joi.required(), - }), - bypass: Joi.array() - .items(Joi.string().regex(/^[^\s]+$/)) - .when('enabled', { - is: Joi.valid(false), - then: Joi.valid(null), - else: Joi.default([]), - }), - }).default(), - maxScreenshotDimension: Joi.number() - .integer() - .default(1950), - }).default(), - }).default(), - maxAttempts: Joi.number() - .integer() - .greater(0) - .when('$dist', { - is: true, - then: Joi.default(3), - otherwise: Joi.default(1), - }) - .default(), - }).default(), - csv: Joi.object({ - useByteOrderMarkEncoding: Joi.boolean().default(false), - checkForFormulas: Joi.boolean().default(true), - enablePanelActionDownload: Joi.boolean().default(true), - maxSizeBytes: Joi.number() - .integer() - .default(1024 * 1024 * 10), // bytes in a kB * kB in a mB * 10 - scroll: Joi.object({ - duration: Joi.string() - .regex(/^[0-9]+(d|h|m|s|ms|micros|nanos)$/, { name: 'DurationString' }) - .default('30s'), - size: Joi.number() - .integer() - .default(500), - }).default(), - }).default(), - encryptionKey: Joi.when(Joi.ref('$dist'), { - is: true, - then: Joi.string(), - otherwise: Joi.string().default('a'.repeat(32)), - }), - roles: Joi.object({ - allow: Joi.array() - .items(Joi.string()) - .default(['reporting_user']), - }).default(), - index: Joi.string().default('.reporting'), - poll: Joi.object({ - jobCompletionNotifier: Joi.object({ - interval: Joi.number() - .integer() - .default(10000), - intervalErrorMultiplier: Joi.number() - .integer() - .default(5), - }).default(), - jobsRefresh: Joi.object({ - interval: Joi.number() - .integer() - .default(5000), - intervalErrorMultiplier: Joi.number() - .integer() - .default(5), - }).default(), - }).default(), - }).default(); -} diff --git a/x-pack/legacy/plugins/reporting/export_types/common/layouts/layout.ts b/x-pack/legacy/plugins/reporting/export_types/common/layouts/layout.ts index 2c43517dbcaa9..5cd2f3e636a93 100644 --- a/x-pack/legacy/plugins/reporting/export_types/common/layouts/layout.ts +++ b/x-pack/legacy/plugins/reporting/export_types/common/layouts/layout.ts @@ -54,7 +54,7 @@ export abstract class Layout { public abstract getPdfPageSize(pageSizeParams: PageSizeParams): string | Size; - public abstract getViewport(itemsCount: number): ViewZoomWidthHeight; + public abstract getViewport(itemsCount: number): ViewZoomWidthHeight | null; public abstract getBrowserZoom(): number; diff --git a/x-pack/legacy/plugins/reporting/export_types/common/lib/screenshots/observable.test.ts b/x-pack/legacy/plugins/reporting/export_types/common/lib/screenshots/observable.test.ts index 75ac3dca4ffa0..68d660257a56d 100644 --- a/x-pack/legacy/plugins/reporting/export_types/common/lib/screenshots/observable.test.ts +++ b/x-pack/legacy/plugins/reporting/export_types/common/lib/screenshots/observable.test.ts @@ -22,6 +22,7 @@ import { LevelLogger } from '../../../../server/lib'; import { createMockBrowserDriverFactory, createMockLayoutInstance } from '../../../../test_helpers'; import { ConditionalHeaders, HeadlessChromiumDriver } from '../../../../types'; import { CaptureConfig } from '../../../../server/types'; +import * as contexts from './constants'; import { screenshotsObservableFactory } from './observable'; import { ElementsPositionAndAttribute } from './types'; @@ -57,10 +58,30 @@ describe('Screenshot Observable Pipeline', () => { expect(result).toMatchInlineSnapshot(` Array [ Object { + "elementsPositionAndAttributes": Array [ + Object { + "attributes": Object { + "description": "Default ", + "title": "Default Mock Title", + }, + "position": Object { + "boundingClientRect": Object { + "height": 600, + "left": 0, + "top": 0, + "width": 800, + }, + "scroll": Object { + "x": 0, + "y": 0, + }, + }, + }, + ], "error": undefined, "screenshots": Array [ Object { - "base64EncodedData": "allyourBase64 of boundingClientRect,scroll", + "base64EncodedData": "allyourBase64", "description": "Default ", "title": "Default Mock Title", }, @@ -95,6 +116,26 @@ describe('Screenshot Observable Pipeline', () => { expect(result).toMatchInlineSnapshot(` Array [ Object { + "elementsPositionAndAttributes": Array [ + Object { + "attributes": Object { + "description": "Default ", + "title": "Default Mock Title", + }, + "position": Object { + "boundingClientRect": Object { + "height": 600, + "left": 0, + "top": 0, + "width": 800, + }, + "scroll": Object { + "x": 0, + "y": 0, + }, + }, + }, + ], "error": undefined, "screenshots": Array [ Object { @@ -106,6 +147,26 @@ describe('Screenshot Observable Pipeline', () => { "timeRange": "Default GetTimeRange Result", }, Object { + "elementsPositionAndAttributes": Array [ + Object { + "attributes": Object { + "description": "Default ", + "title": "Default Mock Title", + }, + "position": Object { + "boundingClientRect": Object { + "height": 600, + "left": 0, + "top": 0, + "width": 800, + }, + "scroll": Object { + "x": 0, + "y": 0, + }, + }, + }, + ], "error": undefined, "screenshots": Array [ Object { @@ -150,10 +211,27 @@ describe('Screenshot Observable Pipeline', () => { await expect(getScreenshot()).resolves.toMatchInlineSnapshot(` Array [ Object { + "elementsPositionAndAttributes": Array [ + Object { + "attributes": Object {}, + "position": Object { + "boundingClientRect": Object { + "height": 200, + "left": 0, + "top": 0, + "width": 200, + }, + "scroll": Object { + "x": 0, + "y": 0, + }, + }, + }, + ], "error": [Error: An error occurred when trying to read the page for visualization panel info. You may need to increase 'xpack.reporting.capture.timeouts.waitForElements'. Error: Mock error!], "screenshots": Array [ Object { - "base64EncodedData": "allyourBase64 of boundingClientRect,scroll", + "base64EncodedData": "allyourBase64", "description": undefined, "title": undefined, }, @@ -161,10 +239,27 @@ describe('Screenshot Observable Pipeline', () => { "timeRange": null, }, Object { + "elementsPositionAndAttributes": Array [ + Object { + "attributes": Object {}, + "position": Object { + "boundingClientRect": Object { + "height": 200, + "left": 0, + "top": 0, + "width": 200, + }, + "scroll": Object { + "x": 0, + "y": 0, + }, + }, + }, + ], "error": [Error: An error occurred when trying to read the page for visualization panel info. You may need to increase 'xpack.reporting.capture.timeouts.waitForElements'. Error: Mock error!], "screenshots": Array [ Object { - "base64EncodedData": "allyourBase64 of boundingClientRect,scroll", + "base64EncodedData": "allyourBase64", "description": undefined, "title": undefined, }, @@ -208,10 +303,27 @@ describe('Screenshot Observable Pipeline', () => { await expect(getScreenshot()).resolves.toMatchInlineSnapshot(` Array [ Object { + "elementsPositionAndAttributes": Array [ + Object { + "attributes": Object {}, + "position": Object { + "boundingClientRect": Object { + "height": 200, + "left": 0, + "top": 0, + "width": 200, + }, + "scroll": Object { + "x": 0, + "y": 0, + }, + }, + }, + ], "error": "Instant timeout has fired!", "screenshots": Array [ Object { - "base64EncodedData": "allyourBase64 of boundingClientRect,scroll", + "base64EncodedData": "allyourBase64", "description": undefined, "title": undefined, }, @@ -221,5 +333,69 @@ describe('Screenshot Observable Pipeline', () => { ] `); }); + + it(`uses defaults for element positions and size when Kibana page is not ready`, async () => { + // mocks + const mockBrowserEvaluate = jest.fn(); + mockBrowserEvaluate.mockImplementation(() => { + const lastCallIndex = mockBrowserEvaluate.mock.calls.length - 1; + const { context: mockCall } = mockBrowserEvaluate.mock.calls[lastCallIndex][1]; + + if (mockCall === contexts.CONTEXT_ELEMENTATTRIBUTES) { + return Promise.resolve(null); + } else { + return Promise.resolve(); + } + }); + mockBrowserDriverFactory = await createMockBrowserDriverFactory(logger, { + evaluate: mockBrowserEvaluate, + }); + mockLayout.getViewport = () => null; + + // test + const getScreenshots$ = screenshotsObservableFactory(mockConfig, mockBrowserDriverFactory); + const getScreenshot = async () => { + return await getScreenshots$({ + logger, + urls: ['/welcome/home/start/index.php3?page=./home.php3'], + conditionalHeaders: {} as ConditionalHeaders, + layout: mockLayout, + browserTimezone: 'UTC', + }).toPromise(); + }; + + await expect(getScreenshot()).resolves.toMatchInlineSnapshot(` + Array [ + Object { + "elementsPositionAndAttributes": Array [ + Object { + "attributes": Object {}, + "position": Object { + "boundingClientRect": Object { + "height": 1200, + "left": 0, + "top": 0, + "width": 1800, + }, + "scroll": Object { + "x": 0, + "y": 0, + }, + }, + }, + ], + "error": undefined, + "screenshots": Array [ + Object { + "base64EncodedData": "allyourBase64", + "description": undefined, + "title": undefined, + }, + ], + "timeRange": undefined, + }, + ] + `); + }); }); }); diff --git a/x-pack/legacy/plugins/reporting/export_types/common/lib/screenshots/observable.ts b/x-pack/legacy/plugins/reporting/export_types/common/lib/screenshots/observable.ts index 53a11c18abd79..519a3289395b9 100644 --- a/x-pack/legacy/plugins/reporting/export_types/common/lib/screenshots/observable.ts +++ b/x-pack/legacy/plugins/reporting/export_types/common/lib/screenshots/observable.ts @@ -18,6 +18,9 @@ import { ScreenSetupData, ScreenshotObservableOpts, ScreenshotResults } from './ import { waitForRenderComplete } from './wait_for_render'; import { waitForVisualizations } from './wait_for_visualizations'; +const DEFAULT_SCREENSHOT_CLIP_HEIGHT = 1200; +const DEFAULT_SCREENSHOT_CLIP_WIDTH = 1800; + export function screenshotsObservableFactory( captureConfig: CaptureConfig, browserDriverFactory: HeadlessChromiumDriverFactory @@ -42,7 +45,7 @@ export function screenshotsObservableFactory( mergeMap(() => openUrl(captureConfig, driver, url, conditionalHeaders, logger)), mergeMap(() => getNumberOfItems(captureConfig, driver, layout, logger)), mergeMap(async itemsCount => { - const viewport = layout.getViewport(itemsCount); + const viewport = layout.getViewport(itemsCount) || getDefaultViewPort(); await Promise.all([ driver.setViewport(viewport, logger), waitForVisualizations(captureConfig, driver, itemsCount, layout, logger), @@ -83,7 +86,12 @@ export function screenshotsObservableFactory( : getDefaultElementPosition(layout.getViewport(1)); const screenshots = await getScreenshots(driver, elements, logger); const { timeRange, error: setupError } = data; - return { timeRange, screenshots, error: setupError }; + return { + timeRange, + screenshots, + error: setupError, + elementsPositionAndAttributes: elements, + }; } ) ); @@ -97,17 +105,30 @@ export function screenshotsObservableFactory( }; } +/* + * If Kibana is showing a non-HTML error message, the viewport might not be + * provided by the browser. + */ +const getDefaultViewPort = () => ({ + height: DEFAULT_SCREENSHOT_CLIP_HEIGHT, + width: DEFAULT_SCREENSHOT_CLIP_WIDTH, + zoom: 1, +}); /* * If an error happens setting up the page, we don't know if there actually * are any visualizations showing. These defaults should help capture the page * enough for the user to see the error themselves */ -const getDefaultElementPosition = ({ height, width }: { height: number; width: number }) => [ - { +const getDefaultElementPosition = (dimensions: { height?: number; width?: number } | null) => { + const height = dimensions?.height || DEFAULT_SCREENSHOT_CLIP_HEIGHT; + const width = dimensions?.width || DEFAULT_SCREENSHOT_CLIP_WIDTH; + + const defaultObject = { position: { boundingClientRect: { top: 0, left: 0, height, width }, scroll: { x: 0, y: 0 }, }, attributes: {}, - }, -]; + }; + return [defaultObject]; +}; diff --git a/x-pack/legacy/plugins/reporting/export_types/common/lib/screenshots/types.ts b/x-pack/legacy/plugins/reporting/export_types/common/lib/screenshots/types.ts index 76613c2d631d6..e113a5d228cd7 100644 --- a/x-pack/legacy/plugins/reporting/export_types/common/lib/screenshots/types.ts +++ b/x-pack/legacy/plugins/reporting/export_types/common/lib/screenshots/types.ts @@ -45,4 +45,5 @@ export interface ScreenshotResults { timeRange: TimeRange | null; screenshots: Screenshot[]; error?: Error; + elementsPositionAndAttributes?: ElementsPositionAndAttribute[]; // NOTE: for testing } diff --git a/x-pack/legacy/plugins/reporting/export_types/csv/server/execute_job.test.js b/x-pack/legacy/plugins/reporting/export_types/csv/server/execute_job.test.js deleted file mode 100644 index 4870e1e35cdaf..0000000000000 --- a/x-pack/legacy/plugins/reporting/export_types/csv/server/execute_job.test.js +++ /dev/null @@ -1,993 +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 Puid from 'puid'; -import sinon from 'sinon'; -import nodeCrypto from '@elastic/node-crypto'; -import { CancellationToken } from '../../../common/cancellation_token'; -import { fieldFormats } from '../../../../../../../src/plugins/data/server'; -import { createMockReportingCore } from '../../../test_helpers'; -import { LevelLogger } from '../../../server/lib/level_logger'; -import { setFieldFormats } from '../../../server/services'; -import { executeJobFactory } from './execute_job'; -import { CSV_BOM_CHARS } from '../../../common/constants'; - -const delay = ms => new Promise(resolve => setTimeout(() => resolve(), ms)); - -const puid = new Puid(); -const getRandomScrollId = () => { - return puid.generate(); -}; - -describe('CSV Execute Job', function() { - const encryptionKey = 'testEncryptionKey'; - const headers = { - sid: 'test', - }; - const mockLogger = new LevelLogger({ - get: () => ({ - debug: jest.fn(), - warn: jest.fn(), - error: jest.fn(), - }), - }); - let defaultElasticsearchResponse; - let encryptedHeaders; - - let clusterStub; - let configGetStub; - let mockReportingConfig; - let mockReportingPlugin; - let callAsCurrentUserStub; - let cancellationToken; - - const mockElasticsearch = { - dataClient: { - asScoped: () => clusterStub, - }, - }; - const mockUiSettingsClient = { - get: sinon.stub(), - }; - - beforeAll(async function() { - const crypto = nodeCrypto({ encryptionKey }); - encryptedHeaders = await crypto.encrypt(headers); - }); - - beforeEach(async function() { - configGetStub = sinon.stub(); - configGetStub.withArgs('encryptionKey').returns(encryptionKey); - configGetStub.withArgs('csv', 'maxSizeBytes').returns(1024 * 1000); // 1mB - configGetStub.withArgs('csv', 'scroll').returns({}); - mockReportingConfig = { get: configGetStub, kbnConfig: { get: configGetStub } }; - - mockReportingPlugin = await createMockReportingCore(mockReportingConfig); - mockReportingPlugin.getUiSettingsServiceFactory = () => Promise.resolve(mockUiSettingsClient); - mockReportingPlugin.getElasticsearchService = () => Promise.resolve(mockElasticsearch); - - cancellationToken = new CancellationToken(); - - defaultElasticsearchResponse = { - hits: { - hits: [], - }, - _scroll_id: 'defaultScrollId', - }; - clusterStub = { - callAsCurrentUser: function() {}, - }; - - callAsCurrentUserStub = sinon - .stub(clusterStub, 'callAsCurrentUser') - .resolves(defaultElasticsearchResponse); - - mockUiSettingsClient.get.withArgs('csv:separator').returns(','); - mockUiSettingsClient.get.withArgs('csv:quoteValues').returns(true); - - setFieldFormats({ - fieldFormatServiceFactory: function() { - const uiConfigMock = {}; - uiConfigMock['format:defaultTypeMap'] = { - _default_: { id: 'string', params: {} }, - }; - - const fieldFormatsRegistry = new fieldFormats.FieldFormatsRegistry(); - - fieldFormatsRegistry.init(key => uiConfigMock[key], {}, [fieldFormats.StringFormat]); - - return fieldFormatsRegistry; - }, - }); - }); - - describe('basic Elasticsearch call behavior', function() { - it('should decrypt encrypted headers and pass to callAsCurrentUser', async function() { - const executeJob = await executeJobFactory(mockReportingPlugin, mockLogger); - await executeJob( - 'job456', - { headers: encryptedHeaders, fields: [], searchRequest: { index: null, body: null } }, - cancellationToken - ); - expect(callAsCurrentUserStub.called).toBe(true); - expect(callAsCurrentUserStub.firstCall.args[0]).toEqual('search'); - }); - - it('should pass the index and body to execute the initial search', async function() { - const index = 'index'; - const body = { - testBody: true, - }; - - const executeJob = await executeJobFactory(mockReportingPlugin, mockLogger); - const job = { - headers: encryptedHeaders, - fields: [], - searchRequest: { - index, - body, - }, - }; - - await executeJob('job777', job, cancellationToken); - - const searchCall = callAsCurrentUserStub.firstCall; - expect(searchCall.args[0]).toBe('search'); - expect(searchCall.args[1].index).toBe(index); - expect(searchCall.args[1].body).toBe(body); - }); - - it('should pass the scrollId from the initial search to the subsequent scroll', async function() { - const scrollId = getRandomScrollId(); - callAsCurrentUserStub.onFirstCall().resolves({ - hits: { - hits: [{}], - }, - _scroll_id: scrollId, - }); - callAsCurrentUserStub.onSecondCall().resolves(defaultElasticsearchResponse); - const executeJob = await executeJobFactory(mockReportingPlugin, mockLogger); - await executeJob( - 'job456', - { headers: encryptedHeaders, fields: [], searchRequest: { index: null, body: null } }, - cancellationToken - ); - - const scrollCall = callAsCurrentUserStub.secondCall; - - expect(scrollCall.args[0]).toBe('scroll'); - expect(scrollCall.args[1].scrollId).toBe(scrollId); - }); - - it('should not execute scroll if there are no hits from the search', async function() { - const executeJob = await executeJobFactory(mockReportingPlugin, mockLogger); - await executeJob( - 'job456', - { headers: encryptedHeaders, fields: [], searchRequest: { index: null, body: null } }, - cancellationToken - ); - - expect(callAsCurrentUserStub.callCount).toBe(2); - - const searchCall = callAsCurrentUserStub.firstCall; - expect(searchCall.args[0]).toBe('search'); - - const clearScrollCall = callAsCurrentUserStub.secondCall; - expect(clearScrollCall.args[0]).toBe('clearScroll'); - }); - - it('should stop executing scroll if there are no hits', async function() { - callAsCurrentUserStub.onFirstCall().resolves({ - hits: { - hits: [{}], - }, - _scroll_id: 'scrollId', - }); - callAsCurrentUserStub.onSecondCall().resolves({ - hits: { - hits: [], - }, - _scroll_id: 'scrollId', - }); - - const executeJob = await executeJobFactory(mockReportingPlugin, mockLogger); - await executeJob( - 'job456', - { headers: encryptedHeaders, fields: [], searchRequest: { index: null, body: null } }, - cancellationToken - ); - - expect(callAsCurrentUserStub.callCount).toBe(3); - - const searchCall = callAsCurrentUserStub.firstCall; - expect(searchCall.args[0]).toBe('search'); - - const scrollCall = callAsCurrentUserStub.secondCall; - expect(scrollCall.args[0]).toBe('scroll'); - - const clearScroll = callAsCurrentUserStub.thirdCall; - expect(clearScroll.args[0]).toBe('clearScroll'); - }); - - it('should call clearScroll with scrollId when there are no more hits', async function() { - const lastScrollId = getRandomScrollId(); - callAsCurrentUserStub.onFirstCall().resolves({ - hits: { - hits: [{}], - }, - _scroll_id: 'scrollId', - }); - - callAsCurrentUserStub.onSecondCall().resolves({ - hits: { - hits: [], - }, - _scroll_id: lastScrollId, - }); - - const executeJob = await executeJobFactory(mockReportingPlugin, mockLogger); - await executeJob( - 'job456', - { headers: encryptedHeaders, fields: [], searchRequest: { index: null, body: null } }, - cancellationToken - ); - - const lastCall = callAsCurrentUserStub.getCall(callAsCurrentUserStub.callCount - 1); - expect(lastCall.args[0]).toBe('clearScroll'); - expect(lastCall.args[1].scrollId).toEqual([lastScrollId]); - }); - - it('calls clearScroll when there is an error iterating the hits', async function() { - const lastScrollId = getRandomScrollId(); - callAsCurrentUserStub.onFirstCall().resolves({ - hits: { - hits: [ - { - _source: { - one: 'foo', - two: 'bar', - }, - }, - ], - }, - _scroll_id: lastScrollId, - }); - - const executeJob = await executeJobFactory(mockReportingPlugin, mockLogger); - const jobParams = { - headers: encryptedHeaders, - fields: ['one', 'two'], - conflictedTypesFields: undefined, - searchRequest: { index: null, body: null }, - }; - await expect( - executeJob('job123', jobParams, cancellationToken) - ).rejects.toMatchInlineSnapshot(`[TypeError: Cannot read property 'indexOf' of undefined]`); - - const lastCall = callAsCurrentUserStub.getCall(callAsCurrentUserStub.callCount - 1); - expect(lastCall.args[0]).toBe('clearScroll'); - expect(lastCall.args[1].scrollId).toEqual([lastScrollId]); - }); - }); - - describe('Cells with formula values', () => { - it('returns `csv_contains_formulas` when cells contain formulas', async function() { - configGetStub.withArgs('csv', 'checkForFormulas').returns(true); - callAsCurrentUserStub.onFirstCall().returns({ - hits: { - hits: [{ _source: { one: '=SUM(A1:A2)', two: 'bar' } }], - }, - _scroll_id: 'scrollId', - }); - - const executeJob = await executeJobFactory(mockReportingPlugin, mockLogger); - const jobParams = { - headers: encryptedHeaders, - fields: ['one', 'two'], - conflictedTypesFields: [], - searchRequest: { index: null, body: null }, - }; - const { csv_contains_formulas: csvContainsFormulas } = await executeJob( - 'job123', - jobParams, - cancellationToken - ); - - expect(csvContainsFormulas).toEqual(true); - }); - - it('returns warnings when headings contain formulas', async function() { - configGetStub.withArgs('csv', 'checkForFormulas').returns(true); - callAsCurrentUserStub.onFirstCall().returns({ - hits: { - hits: [{ _source: { '=SUM(A1:A2)': 'foo', two: 'bar' } }], - }, - _scroll_id: 'scrollId', - }); - - const executeJob = await executeJobFactory(mockReportingPlugin, mockLogger); - const jobParams = { - headers: encryptedHeaders, - fields: ['=SUM(A1:A2)', 'two'], - conflictedTypesFields: [], - searchRequest: { index: null, body: null }, - }; - const { csv_contains_formulas: csvContainsFormulas } = await executeJob( - 'job123', - jobParams, - cancellationToken - ); - - expect(csvContainsFormulas).toEqual(true); - }); - - it('returns no warnings when cells have no formulas', async function() { - configGetStub.withArgs('csv', 'checkForFormulas').returns(true); - callAsCurrentUserStub.onFirstCall().returns({ - hits: { - hits: [{ _source: { one: 'foo', two: 'bar' } }], - }, - _scroll_id: 'scrollId', - }); - - const executeJob = await executeJobFactory(mockReportingPlugin, mockLogger); - const jobParams = { - headers: encryptedHeaders, - fields: ['one', 'two'], - conflictedTypesFields: [], - searchRequest: { index: null, body: null }, - }; - const { csv_contains_formulas: csvContainsFormulas } = await executeJob( - 'job123', - jobParams, - cancellationToken - ); - - expect(csvContainsFormulas).toEqual(false); - }); - - it('returns no warnings when configured not to', async () => { - configGetStub.withArgs('csv', 'checkForFormulas').returns(false); - callAsCurrentUserStub.onFirstCall().returns({ - hits: { - hits: [{ _source: { one: '=SUM(A1:A2)', two: 'bar' } }], - }, - _scroll_id: 'scrollId', - }); - - const executeJob = await executeJobFactory(mockReportingPlugin, mockLogger); - const jobParams = { - headers: encryptedHeaders, - fields: ['one', 'two'], - conflictedTypesFields: [], - searchRequest: { index: null, body: null }, - }; - const { csv_contains_formulas: csvContainsFormulas } = await executeJob( - 'job123', - jobParams, - cancellationToken - ); - - expect(csvContainsFormulas).toEqual(false); - }); - }); - - describe('Byte order mark encoding', () => { - it('encodes CSVs with BOM', async () => { - configGetStub.withArgs('csv', 'useByteOrderMarkEncoding').returns(true); - callAsCurrentUserStub.onFirstCall().returns({ - hits: { - hits: [{ _source: { one: 'one', two: 'bar' } }], - }, - _scroll_id: 'scrollId', - }); - - const executeJob = await executeJobFactory(mockReportingPlugin, mockLogger); - const jobParams = { - headers: encryptedHeaders, - fields: ['one', 'two'], - conflictedTypesFields: [], - searchRequest: { index: null, body: null }, - }; - const { content } = await executeJob('job123', jobParams, cancellationToken); - - expect(content).toEqual(`${CSV_BOM_CHARS}one,two\none,bar\n`); - }); - - it('encodes CSVs without BOM', async () => { - configGetStub.withArgs('csv', 'useByteOrderMarkEncoding').returns(false); - callAsCurrentUserStub.onFirstCall().returns({ - hits: { - hits: [{ _source: { one: 'one', two: 'bar' } }], - }, - _scroll_id: 'scrollId', - }); - - const executeJob = await executeJobFactory(mockReportingPlugin, mockLogger); - const jobParams = { - headers: encryptedHeaders, - fields: ['one', 'two'], - conflictedTypesFields: [], - searchRequest: { index: null, body: null }, - }; - const { content } = await executeJob('job123', jobParams, cancellationToken); - - expect(content).toEqual('one,two\none,bar\n'); - }); - }); - - describe('Elasticsearch call errors', function() { - it('should reject Promise if search call errors out', async function() { - callAsCurrentUserStub.rejects(new Error()); - const executeJob = await executeJobFactory(mockReportingPlugin, mockLogger); - const jobParams = { - headers: encryptedHeaders, - fields: [], - searchRequest: { index: null, body: null }, - }; - await expect( - executeJob('job123', jobParams, cancellationToken) - ).rejects.toMatchInlineSnapshot(`[Error]`); - }); - - it('should reject Promise if scroll call errors out', async function() { - callAsCurrentUserStub.onFirstCall().resolves({ - hits: { - hits: [{}], - }, - _scroll_id: 'scrollId', - }); - callAsCurrentUserStub.onSecondCall().rejects(new Error()); - const executeJob = await executeJobFactory(mockReportingPlugin, mockLogger); - const jobParams = { - headers: encryptedHeaders, - fields: [], - searchRequest: { index: null, body: null }, - }; - await expect( - executeJob('job123', jobParams, cancellationToken) - ).rejects.toMatchInlineSnapshot(`[Error]`); - }); - }); - - describe('invalid responses', function() { - it('should reject Promise if search returns hits but no _scroll_id', async function() { - callAsCurrentUserStub.resolves({ - hits: { - hits: [{}], - }, - _scroll_id: undefined, - }); - - const executeJob = await executeJobFactory(mockReportingPlugin, mockLogger); - const jobParams = { - headers: encryptedHeaders, - fields: [], - searchRequest: { index: null, body: null }, - }; - await expect( - executeJob('job123', jobParams, cancellationToken) - ).rejects.toMatchInlineSnapshot( - `[Error: Expected _scroll_id in the following Elasticsearch response: {"hits":{"hits":[{}]}}]` - ); - }); - - it('should reject Promise if search returns no hits and no _scroll_id', async function() { - callAsCurrentUserStub.resolves({ - hits: { - hits: [], - }, - _scroll_id: undefined, - }); - - const executeJob = await executeJobFactory(mockReportingPlugin, mockLogger); - const jobParams = { - headers: encryptedHeaders, - fields: [], - searchRequest: { index: null, body: null }, - }; - await expect( - executeJob('job123', jobParams, cancellationToken) - ).rejects.toMatchInlineSnapshot( - `[Error: Expected _scroll_id in the following Elasticsearch response: {"hits":{"hits":[]}}]` - ); - }); - - it('should reject Promise if scroll returns hits but no _scroll_id', async function() { - callAsCurrentUserStub.onFirstCall().resolves({ - hits: { - hits: [{}], - }, - _scroll_id: 'scrollId', - }); - - callAsCurrentUserStub.onSecondCall().resolves({ - hits: { - hits: [{}], - }, - _scroll_id: undefined, - }); - - const executeJob = await executeJobFactory(mockReportingPlugin, mockLogger); - const jobParams = { - headers: encryptedHeaders, - fields: [], - searchRequest: { index: null, body: null }, - }; - await expect( - executeJob('job123', jobParams, cancellationToken) - ).rejects.toMatchInlineSnapshot( - `[Error: Expected _scroll_id in the following Elasticsearch response: {"hits":{"hits":[{}]}}]` - ); - }); - - it('should reject Promise if scroll returns no hits and no _scroll_id', async function() { - callAsCurrentUserStub.onFirstCall().resolves({ - hits: { - hits: [{}], - }, - _scroll_id: 'scrollId', - }); - - callAsCurrentUserStub.onSecondCall().resolves({ - hits: { - hits: [], - }, - _scroll_id: undefined, - }); - - const executeJob = await executeJobFactory(mockReportingPlugin, mockLogger); - const jobParams = { - headers: encryptedHeaders, - fields: [], - searchRequest: { index: null, body: null }, - }; - await expect( - executeJob('job123', jobParams, cancellationToken) - ).rejects.toMatchInlineSnapshot( - `[Error: Expected _scroll_id in the following Elasticsearch response: {"hits":{"hits":[]}}]` - ); - }); - }); - - describe('cancellation', function() { - const scrollId = getRandomScrollId(); - - beforeEach(function() { - // We have to "re-stub" the callAsCurrentUser stub here so that we can use the fakeFunction - // that delays the Promise resolution so we have a chance to call cancellationToken.cancel(). - // Otherwise, we get into an endless loop, and don't have a chance to call cancel - callAsCurrentUserStub.restore(); - callAsCurrentUserStub = sinon - .stub(clusterStub, 'callAsCurrentUser') - .callsFake(async function() { - await delay(1); - return { - hits: { - hits: [{}], - }, - _scroll_id: scrollId, - }; - }); - }); - - it('should stop calling Elasticsearch when cancellationToken.cancel is called', async function() { - const executeJob = await executeJobFactory(mockReportingPlugin, mockLogger); - executeJob( - 'job345', - { headers: encryptedHeaders, fields: [], searchRequest: { index: null, body: null } }, - cancellationToken - ); - - await delay(250); - const callCount = callAsCurrentUserStub.callCount; - cancellationToken.cancel(); - await delay(250); - expect(callAsCurrentUserStub.callCount).toBe(callCount + 1); // last call is to clear the scroll - }); - - it(`shouldn't call clearScroll if it never got a scrollId`, async function() { - const executeJob = await executeJobFactory(mockReportingPlugin, mockLogger); - executeJob( - 'job345', - { headers: encryptedHeaders, fields: [], searchRequest: { index: null, body: null } }, - cancellationToken - ); - cancellationToken.cancel(); - - for (let i = 0; i < callAsCurrentUserStub.callCount; ++i) { - expect(callAsCurrentUserStub.getCall(i).args[1]).to.not.be('clearScroll'); - } - }); - - it('should call clearScroll if it got a scrollId', async function() { - const executeJob = await executeJobFactory(mockReportingPlugin, mockLogger); - executeJob( - 'job345', - { headers: encryptedHeaders, fields: [], searchRequest: { index: null, body: null } }, - cancellationToken - ); - await delay(100); - cancellationToken.cancel(); - await delay(100); - - const lastCall = callAsCurrentUserStub.getCall(callAsCurrentUserStub.callCount - 1); - expect(lastCall.args[0]).toBe('clearScroll'); - expect(lastCall.args[1].scrollId).toEqual([scrollId]); - }); - }); - - describe('csv content', function() { - it('should write column headers to output, even if there are no results', async function() { - const executeJob = await executeJobFactory(mockReportingPlugin, mockLogger); - const jobParams = { - headers: encryptedHeaders, - fields: ['one', 'two'], - searchRequest: { index: null, body: null }, - }; - const { content } = await executeJob('job123', jobParams, cancellationToken); - expect(content).toBe(`one,two\n`); - }); - - it('should use custom uiSettings csv:separator for header', async function() { - mockUiSettingsClient.get.withArgs('csv:separator').returns(';'); - const executeJob = await executeJobFactory(mockReportingPlugin, mockLogger); - const jobParams = { - headers: encryptedHeaders, - fields: ['one', 'two'], - searchRequest: { index: null, body: null }, - }; - const { content } = await executeJob('job123', jobParams, cancellationToken); - expect(content).toBe(`one;two\n`); - }); - - it('should escape column headers if uiSettings csv:quoteValues is true', async function() { - mockUiSettingsClient.get.withArgs('csv:quoteValues').returns(true); - const executeJob = await executeJobFactory(mockReportingPlugin, mockLogger); - const jobParams = { - headers: encryptedHeaders, - fields: ['one and a half', 'two', 'three-and-four', 'five & six'], - searchRequest: { index: null, body: null }, - }; - const { content } = await executeJob('job123', jobParams, cancellationToken); - expect(content).toBe(`"one and a half",two,"three-and-four","five & six"\n`); - }); - - it(`shouldn't escape column headers if uiSettings csv:quoteValues is false`, async function() { - mockUiSettingsClient.get.withArgs('csv:quoteValues').returns(false); - const executeJob = await executeJobFactory(mockReportingPlugin, mockLogger); - const jobParams = { - headers: encryptedHeaders, - fields: ['one and a half', 'two', 'three-and-four', 'five & six'], - searchRequest: { index: null, body: null }, - }; - const { content } = await executeJob('job123', jobParams, cancellationToken); - expect(content).toBe(`one and a half,two,three-and-four,five & six\n`); - }); - - it('should write column headers to output, when there are results', async function() { - const executeJob = await executeJobFactory(mockReportingPlugin, mockLogger); - callAsCurrentUserStub.onFirstCall().resolves({ - hits: { - hits: [{ one: '1', two: '2' }], - }, - _scroll_id: 'scrollId', - }); - - const jobParams = { - headers: encryptedHeaders, - fields: ['one', 'two'], - searchRequest: { index: null, body: null }, - }; - const { content } = await executeJob('job123', jobParams, cancellationToken); - const lines = content.split('\n'); - const headerLine = lines[0]; - expect(headerLine).toBe('one,two'); - }); - - it('should use comma separated values of non-nested fields from _source', async function() { - const executeJob = await executeJobFactory(mockReportingPlugin, mockLogger); - callAsCurrentUserStub.onFirstCall().resolves({ - hits: { - hits: [{ _source: { one: 'foo', two: 'bar' } }], - }, - _scroll_id: 'scrollId', - }); - - const jobParams = { - headers: encryptedHeaders, - fields: ['one', 'two'], - conflictedTypesFields: [], - searchRequest: { index: null, body: null }, - }; - const { content } = await executeJob('job123', jobParams, cancellationToken); - const lines = content.split('\n'); - const valuesLine = lines[1]; - expect(valuesLine).toBe('foo,bar'); - }); - - it('should concatenate the hits from multiple responses', async function() { - const executeJob = await executeJobFactory(mockReportingPlugin, mockLogger); - callAsCurrentUserStub.onFirstCall().resolves({ - hits: { - hits: [{ _source: { one: 'foo', two: 'bar' } }], - }, - _scroll_id: 'scrollId', - }); - callAsCurrentUserStub.onSecondCall().resolves({ - hits: { - hits: [{ _source: { one: 'baz', two: 'qux' } }], - }, - _scroll_id: 'scrollId', - }); - - const jobParams = { - headers: encryptedHeaders, - fields: ['one', 'two'], - conflictedTypesFields: [], - searchRequest: { index: null, body: null }, - }; - const { content } = await executeJob('job123', jobParams, cancellationToken); - const lines = content.split('\n'); - - expect(lines[1]).toBe('foo,bar'); - expect(lines[2]).toBe('baz,qux'); - }); - - it('should use field formatters to format fields', async function() { - const executeJob = await executeJobFactory(mockReportingPlugin, mockLogger); - callAsCurrentUserStub.onFirstCall().resolves({ - hits: { - hits: [{ _source: { one: 'foo', two: 'bar' } }], - }, - _scroll_id: 'scrollId', - }); - - const jobParams = { - headers: encryptedHeaders, - fields: ['one', 'two'], - conflictedTypesFields: [], - searchRequest: { index: null, body: null }, - indexPatternSavedObject: { - id: 'logstash-*', - type: 'index-pattern', - attributes: { - title: 'logstash-*', - fields: '[{"name":"one","type":"string"}, {"name":"two","type":"string"}]', - fieldFormatMap: '{"one":{"id":"string","params":{"transform": "upper"}}}', - }, - }, - }; - const { content } = await executeJob('job123', jobParams, cancellationToken); - const lines = content.split('\n'); - - expect(lines[1]).toBe('FOO,bar'); - }); - }); - - describe('maxSizeBytes', function() { - // The following tests use explicitly specified lengths. UTF-8 uses between one and four 8-bit bytes for each - // code-point. However, any character that can be represented by ASCII requires one-byte, so a majority of the - // tests use these 'simple' characters to make the math easier - - describe('when only the headers exceed the maxSizeBytes', function() { - let content; - let maxSizeReached; - - beforeEach(async function() { - configGetStub.withArgs('csv', 'maxSizeBytes').returns(1); - - const executeJob = await executeJobFactory(mockReportingPlugin, mockLogger); - const jobParams = { - headers: encryptedHeaders, - fields: ['one', 'two'], - searchRequest: { index: null, body: null }, - }; - - ({ content, max_size_reached: maxSizeReached } = await executeJob( - 'job123', - jobParams, - cancellationToken - )); - }); - - it('should return max_size_reached', function() { - expect(maxSizeReached).toBe(true); - }); - - it('should return empty content', function() { - expect(content).toBe(''); - }); - }); - - describe('when headers are equal to maxSizeBytes', function() { - let content; - let maxSizeReached; - - beforeEach(async function() { - configGetStub.withArgs('csv', 'maxSizeBytes').returns(9); - - const executeJob = await executeJobFactory(mockReportingPlugin, mockLogger); - const jobParams = { - headers: encryptedHeaders, - fields: ['one', 'two'], - searchRequest: { index: null, body: null }, - }; - - ({ content, max_size_reached: maxSizeReached } = await executeJob( - 'job123', - jobParams, - cancellationToken - )); - }); - - it(`shouldn't return max_size_reached`, function() { - expect(maxSizeReached).toBe(false); - }); - - it(`should return content`, function() { - expect(content).toBe('one,two\n'); - }); - }); - - describe('when the data exceeds the maxSizeBytes', function() { - let content; - let maxSizeReached; - - beforeEach(async function() { - configGetStub.withArgs('csv', 'maxSizeBytes').returns(9); - - callAsCurrentUserStub.onFirstCall().returns({ - hits: { - hits: [{ _source: { one: 'foo', two: 'bar' } }], - }, - _scroll_id: 'scrollId', - }); - - const executeJob = await executeJobFactory(mockReportingPlugin, mockLogger); - const jobParams = { - headers: encryptedHeaders, - fields: ['one', 'two'], - conflictedTypesFields: [], - searchRequest: { index: null, body: null }, - }; - - ({ content, max_size_reached: maxSizeReached } = await executeJob( - 'job123', - jobParams, - cancellationToken - )); - }); - - it(`should return max_size_reached`, function() { - expect(maxSizeReached).toBe(true); - }); - - it(`should return the headers in the content`, function() { - expect(content).toBe('one,two\n'); - }); - }); - - describe('when headers and data equal the maxSizeBytes', function() { - let content; - let maxSizeReached; - - beforeEach(async function() { - mockReportingPlugin.getUiSettingsServiceFactory = () => mockUiSettingsClient; - configGetStub.withArgs('csv', 'maxSizeBytes').returns(18); - - callAsCurrentUserStub.onFirstCall().returns({ - hits: { - hits: [{ _source: { one: 'foo', two: 'bar' } }], - }, - _scroll_id: 'scrollId', - }); - - const executeJob = await executeJobFactory(mockReportingPlugin, mockLogger); - const jobParams = { - headers: encryptedHeaders, - fields: ['one', 'two'], - conflictedTypesFields: [], - searchRequest: { index: null, body: null }, - }; - - ({ content, max_size_reached: maxSizeReached } = await executeJob( - 'job123', - jobParams, - cancellationToken - )); - }); - - it(`shouldn't return max_size_reached`, async function() { - expect(maxSizeReached).toBe(false); - }); - - it('should return headers and data in content', function() { - expect(content).toBe('one,two\nfoo,bar\n'); - }); - }); - }); - - describe('scroll settings', function() { - it('passes scroll duration to initial search call', async function() { - const scrollDuration = 'test'; - configGetStub.withArgs('csv', 'scroll').returns({ duration: scrollDuration }); - - callAsCurrentUserStub.onFirstCall().returns({ - hits: { - hits: [{}], - }, - _scroll_id: 'scrollId', - }); - - const executeJob = await executeJobFactory(mockReportingPlugin, mockLogger); - const jobParams = { - headers: encryptedHeaders, - fields: ['one', 'two'], - conflictedTypesFields: [], - searchRequest: { index: null, body: null }, - }; - - await executeJob('job123', jobParams, cancellationToken); - - const searchCall = callAsCurrentUserStub.firstCall; - expect(searchCall.args[0]).toBe('search'); - expect(searchCall.args[1].scroll).toBe(scrollDuration); - }); - - it('passes scroll size to initial search call', async function() { - const scrollSize = 100; - configGetStub.withArgs('csv', 'scroll').returns({ size: scrollSize }); - - callAsCurrentUserStub.onFirstCall().resolves({ - hits: { - hits: [{}], - }, - _scroll_id: 'scrollId', - }); - - const executeJob = await executeJobFactory(mockReportingPlugin, mockLogger); - const jobParams = { - headers: encryptedHeaders, - fields: ['one', 'two'], - conflictedTypesFields: [], - searchRequest: { index: null, body: null }, - }; - - await executeJob('job123', jobParams, cancellationToken); - - const searchCall = callAsCurrentUserStub.firstCall; - expect(searchCall.args[0]).toBe('search'); - expect(searchCall.args[1].size).toBe(scrollSize); - }); - - it('passes scroll duration to subsequent scroll call', async function() { - const scrollDuration = 'test'; - configGetStub.withArgs('csv', 'scroll').returns({ duration: scrollDuration }); - - callAsCurrentUserStub.onFirstCall().resolves({ - hits: { - hits: [{}], - }, - _scroll_id: 'scrollId', - }); - - const executeJob = await executeJobFactory(mockReportingPlugin, mockLogger); - const jobParams = { - headers: encryptedHeaders, - fields: ['one', 'two'], - conflictedTypesFields: [], - searchRequest: { index: null, body: null }, - }; - - await executeJob('job123', jobParams, cancellationToken); - - const scrollCall = callAsCurrentUserStub.secondCall; - expect(scrollCall.args[0]).toBe('scroll'); - expect(scrollCall.args[1].scroll).toBe(scrollDuration); - }); - }); -}); diff --git a/x-pack/legacy/plugins/reporting/export_types/csv/server/execute_job.test.ts b/x-pack/legacy/plugins/reporting/export_types/csv/server/execute_job.test.ts new file mode 100644 index 0000000000000..ad35aaf003094 --- /dev/null +++ b/x-pack/legacy/plugins/reporting/export_types/csv/server/execute_job.test.ts @@ -0,0 +1,1104 @@ +/* + * 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. + */ + +// @ts-ignore +import Puid from 'puid'; +import sinon from 'sinon'; +import nodeCrypto from '@elastic/node-crypto'; +import { CancellationToken } from '../../../common/cancellation_token'; +import { fieldFormats } from '../../../../../../../src/plugins/data/server'; +import { createMockReportingCore } from '../../../test_helpers'; +import { LevelLogger } from '../../../server/lib/level_logger'; +import { setFieldFormats } from '../../../server/services'; +import { executeJobFactory } from './execute_job'; +import { JobDocPayloadDiscoverCsv } from '../types'; +import { CSV_BOM_CHARS } from '../../../common/constants'; + +const delay = (ms: number) => new Promise(resolve => setTimeout(() => resolve(), ms)); + +const puid = new Puid(); +const getRandomScrollId = () => { + return puid.generate(); +}; + +const getJobDocPayload = (baseObj: any) => baseObj as JobDocPayloadDiscoverCsv; + +describe('CSV Execute Job', function() { + const encryptionKey = 'testEncryptionKey'; + const headers = { + sid: 'test', + }; + const mockLogger = new LevelLogger({ + get: () => + ({ + debug: jest.fn(), + warn: jest.fn(), + error: jest.fn(), + } as any), + }); + let defaultElasticsearchResponse: any; + let encryptedHeaders: any; + + let clusterStub: any; + let configGetStub: any; + let mockReportingConfig: any; + let mockReportingPlugin: any; + let callAsCurrentUserStub: any; + let cancellationToken: any; + + const mockElasticsearch = { + dataClient: { + asScoped: () => clusterStub, + }, + }; + const mockUiSettingsClient = { + get: sinon.stub(), + }; + + beforeAll(async function() { + const crypto = nodeCrypto({ encryptionKey }); + encryptedHeaders = await crypto.encrypt(headers); + }); + + beforeEach(async function() { + configGetStub = sinon.stub(); + configGetStub.withArgs('encryptionKey').returns(encryptionKey); + configGetStub.withArgs('csv', 'maxSizeBytes').returns(1024 * 1000); // 1mB + configGetStub.withArgs('csv', 'scroll').returns({}); + mockReportingConfig = { get: configGetStub, kbnConfig: { get: configGetStub } }; + + mockReportingPlugin = await createMockReportingCore(mockReportingConfig); + mockReportingPlugin.getUiSettingsServiceFactory = () => Promise.resolve(mockUiSettingsClient); + mockReportingPlugin.getElasticsearchService = () => Promise.resolve(mockElasticsearch); + + cancellationToken = new CancellationToken(); + + defaultElasticsearchResponse = { + hits: { + hits: [], + }, + _scroll_id: 'defaultScrollId', + }; + clusterStub = { + callAsCurrentUser() {}, + }; + + callAsCurrentUserStub = sinon + .stub(clusterStub, 'callAsCurrentUser') + .resolves(defaultElasticsearchResponse); + + mockUiSettingsClient.get.withArgs('csv:separator').returns(','); + mockUiSettingsClient.get.withArgs('csv:quoteValues').returns(true); + + setFieldFormats({ + fieldFormatServiceFactory() { + const uiConfigMock = {}; + (uiConfigMock as any)['format:defaultTypeMap'] = { + _default_: { id: 'string', params: {} }, + }; + + const fieldFormatsRegistry = new fieldFormats.FieldFormatsRegistry(); + + fieldFormatsRegistry.init(key => (uiConfigMock as any)[key], {}, [ + fieldFormats.StringFormat, + ]); + + return Promise.resolve(fieldFormatsRegistry); + }, + }); + }); + + describe('basic Elasticsearch call behavior', function() { + it('should decrypt encrypted headers and pass to callAsCurrentUser', async function() { + const executeJob = await executeJobFactory(mockReportingPlugin, mockLogger); + await executeJob( + 'job456', + getJobDocPayload({ + headers: encryptedHeaders, + fields: [], + searchRequest: { index: null, body: null }, + }), + cancellationToken + ); + expect(callAsCurrentUserStub.called).toBe(true); + expect(callAsCurrentUserStub.firstCall.args[0]).toEqual('search'); + }); + + it('should pass the index and body to execute the initial search', async function() { + const index = 'index'; + const body = { + testBody: true, + }; + + const executeJob = await executeJobFactory(mockReportingPlugin, mockLogger); + const job = getJobDocPayload({ + headers: encryptedHeaders, + fields: [], + searchRequest: { + index, + body, + }, + }); + + await executeJob('job777', job, cancellationToken); + + const searchCall = callAsCurrentUserStub.firstCall; + expect(searchCall.args[0]).toBe('search'); + expect(searchCall.args[1].index).toBe(index); + expect(searchCall.args[1].body).toBe(body); + }); + + it('should pass the scrollId from the initial search to the subsequent scroll', async function() { + const scrollId = getRandomScrollId(); + callAsCurrentUserStub.onFirstCall().resolves({ + hits: { + hits: [{}], + }, + _scroll_id: scrollId, + }); + callAsCurrentUserStub.onSecondCall().resolves(defaultElasticsearchResponse); + const executeJob = await executeJobFactory(mockReportingPlugin, mockLogger); + await executeJob( + 'job456', + getJobDocPayload({ + headers: encryptedHeaders, + fields: [], + searchRequest: { index: null, body: null }, + }), + cancellationToken + ); + + const scrollCall = callAsCurrentUserStub.secondCall; + + expect(scrollCall.args[0]).toBe('scroll'); + expect(scrollCall.args[1].scrollId).toBe(scrollId); + }); + + it('should not execute scroll if there are no hits from the search', async function() { + const executeJob = await executeJobFactory(mockReportingPlugin, mockLogger); + await executeJob( + 'job456', + getJobDocPayload({ + headers: encryptedHeaders, + fields: [], + searchRequest: { index: null, body: null }, + }), + cancellationToken + ); + + expect(callAsCurrentUserStub.callCount).toBe(2); + + const searchCall = callAsCurrentUserStub.firstCall; + expect(searchCall.args[0]).toBe('search'); + + const clearScrollCall = callAsCurrentUserStub.secondCall; + expect(clearScrollCall.args[0]).toBe('clearScroll'); + }); + + it('should stop executing scroll if there are no hits', async function() { + callAsCurrentUserStub.onFirstCall().resolves({ + hits: { + hits: [{}], + }, + _scroll_id: 'scrollId', + }); + callAsCurrentUserStub.onSecondCall().resolves({ + hits: { + hits: [], + }, + _scroll_id: 'scrollId', + }); + + const executeJob = await executeJobFactory(mockReportingPlugin, mockLogger); + await executeJob( + 'job456', + getJobDocPayload({ + headers: encryptedHeaders, + fields: [], + searchRequest: { index: null, body: null }, + }), + cancellationToken + ); + + expect(callAsCurrentUserStub.callCount).toBe(3); + + const searchCall = callAsCurrentUserStub.firstCall; + expect(searchCall.args[0]).toBe('search'); + + const scrollCall = callAsCurrentUserStub.secondCall; + expect(scrollCall.args[0]).toBe('scroll'); + + const clearScroll = callAsCurrentUserStub.thirdCall; + expect(clearScroll.args[0]).toBe('clearScroll'); + }); + + it('should call clearScroll with scrollId when there are no more hits', async function() { + const lastScrollId = getRandomScrollId(); + callAsCurrentUserStub.onFirstCall().resolves({ + hits: { + hits: [{}], + }, + _scroll_id: 'scrollId', + }); + + callAsCurrentUserStub.onSecondCall().resolves({ + hits: { + hits: [], + }, + _scroll_id: lastScrollId, + }); + + const executeJob = await executeJobFactory(mockReportingPlugin, mockLogger); + await executeJob( + 'job456', + getJobDocPayload({ + headers: encryptedHeaders, + fields: [], + searchRequest: { index: null, body: null }, + }), + cancellationToken + ); + + const lastCall = callAsCurrentUserStub.getCall(callAsCurrentUserStub.callCount - 1); + expect(lastCall.args[0]).toBe('clearScroll'); + expect(lastCall.args[1].scrollId).toEqual([lastScrollId]); + }); + + it('calls clearScroll when there is an error iterating the hits', async function() { + const lastScrollId = getRandomScrollId(); + callAsCurrentUserStub.onFirstCall().resolves({ + hits: { + hits: [ + { + _source: { + one: 'foo', + two: 'bar', + }, + }, + ], + }, + _scroll_id: lastScrollId, + }); + + const executeJob = await executeJobFactory(mockReportingPlugin, mockLogger); + const jobParams = getJobDocPayload({ + headers: encryptedHeaders, + fields: ['one', 'two'], + conflictedTypesFields: undefined, + searchRequest: { index: null, body: null }, + }); + await expect( + executeJob('job123', jobParams, cancellationToken) + ).rejects.toMatchInlineSnapshot(`[TypeError: Cannot read property 'indexOf' of undefined]`); + + const lastCall = callAsCurrentUserStub.getCall(callAsCurrentUserStub.callCount - 1); + expect(lastCall.args[0]).toBe('clearScroll'); + expect(lastCall.args[1].scrollId).toEqual([lastScrollId]); + }); + }); + + describe('Warning when cells have formulas', () => { + it('returns `csv_contains_formulas` when cells contain formulas', async function() { + configGetStub.withArgs('csv', 'checkForFormulas').returns(true); + callAsCurrentUserStub.onFirstCall().returns({ + hits: { + hits: [{ _source: { one: '=SUM(A1:A2)', two: 'bar' } }], + }, + _scroll_id: 'scrollId', + }); + + const executeJob = await executeJobFactory(mockReportingPlugin, mockLogger); + const jobParams = getJobDocPayload({ + headers: encryptedHeaders, + fields: ['one', 'two'], + conflictedTypesFields: [], + searchRequest: { index: null, body: null }, + }); + const { csv_contains_formulas: csvContainsFormulas } = await executeJob( + 'job123', + jobParams, + cancellationToken + ); + + expect(csvContainsFormulas).toEqual(true); + }); + + it('returns warnings when headings contain formulas', async function() { + configGetStub.withArgs('csv', 'checkForFormulas').returns(true); + callAsCurrentUserStub.onFirstCall().returns({ + hits: { + hits: [{ _source: { '=SUM(A1:A2)': 'foo', two: 'bar' } }], + }, + _scroll_id: 'scrollId', + }); + + const executeJob = await executeJobFactory(mockReportingPlugin, mockLogger); + const jobParams = getJobDocPayload({ + headers: encryptedHeaders, + fields: ['=SUM(A1:A2)', 'two'], + conflictedTypesFields: [], + searchRequest: { index: null, body: null }, + }); + const { csv_contains_formulas: csvContainsFormulas } = await executeJob( + 'job123', + jobParams, + cancellationToken + ); + + expect(csvContainsFormulas).toEqual(true); + }); + + it('returns no warnings when cells have no formulas', async function() { + configGetStub.withArgs('csv', 'checkForFormulas').returns(true); + configGetStub.withArgs('csv', 'escapeFormulaValues').returns(false); + callAsCurrentUserStub.onFirstCall().returns({ + hits: { + hits: [{ _source: { one: 'foo', two: 'bar' } }], + }, + _scroll_id: 'scrollId', + }); + + const executeJob = await executeJobFactory(mockReportingPlugin, mockLogger); + const jobParams = getJobDocPayload({ + headers: encryptedHeaders, + fields: ['one', 'two'], + conflictedTypesFields: [], + searchRequest: { index: null, body: null }, + }); + const { csv_contains_formulas: csvContainsFormulas } = await executeJob( + 'job123', + jobParams, + cancellationToken + ); + + expect(csvContainsFormulas).toEqual(false); + }); + + it('returns no warnings when cells have formulas but are escaped', async function() { + configGetStub.withArgs('csv', 'checkForFormulas').returns(true); + configGetStub.withArgs('csv', 'escapeFormulaValues').returns(true); + callAsCurrentUserStub.onFirstCall().returns({ + hits: { + hits: [{ _source: { '=SUM(A1:A2)': 'foo', two: 'bar' } }], + }, + _scroll_id: 'scrollId', + }); + + const executeJob = await executeJobFactory(mockReportingPlugin, mockLogger); + const jobParams = getJobDocPayload({ + headers: encryptedHeaders, + fields: ['=SUM(A1:A2)', 'two'], + conflictedTypesFields: [], + searchRequest: { index: null, body: null }, + }); + + const { csv_contains_formulas: csvContainsFormulas } = await executeJob( + 'job123', + jobParams, + cancellationToken + ); + + expect(csvContainsFormulas).toEqual(false); + }); + + it('returns no warnings when configured not to', async () => { + configGetStub.withArgs('csv', 'checkForFormulas').returns(false); + callAsCurrentUserStub.onFirstCall().returns({ + hits: { + hits: [{ _source: { one: '=SUM(A1:A2)', two: 'bar' } }], + }, + _scroll_id: 'scrollId', + }); + + const executeJob = await executeJobFactory(mockReportingPlugin, mockLogger); + const jobParams = getJobDocPayload({ + headers: encryptedHeaders, + fields: ['one', 'two'], + conflictedTypesFields: [], + searchRequest: { index: null, body: null }, + }); + const { csv_contains_formulas: csvContainsFormulas } = await executeJob( + 'job123', + jobParams, + cancellationToken + ); + + expect(csvContainsFormulas).toEqual(false); + }); + }); + + describe('Byte order mark encoding', () => { + it('encodes CSVs with BOM', async () => { + configGetStub.withArgs('csv', 'useByteOrderMarkEncoding').returns(true); + callAsCurrentUserStub.onFirstCall().returns({ + hits: { + hits: [{ _source: { one: 'one', two: 'bar' } }], + }, + _scroll_id: 'scrollId', + }); + + const executeJob = await executeJobFactory(mockReportingPlugin, mockLogger); + const jobParams = getJobDocPayload({ + headers: encryptedHeaders, + fields: ['one', 'two'], + conflictedTypesFields: [], + searchRequest: { index: null, body: null }, + }); + const { content } = await executeJob('job123', jobParams, cancellationToken); + + expect(content).toEqual(`${CSV_BOM_CHARS}one,two\none,bar\n`); + }); + + it('encodes CSVs without BOM', async () => { + configGetStub.withArgs('csv', 'useByteOrderMarkEncoding').returns(false); + callAsCurrentUserStub.onFirstCall().returns({ + hits: { + hits: [{ _source: { one: 'one', two: 'bar' } }], + }, + _scroll_id: 'scrollId', + }); + + const executeJob = await executeJobFactory(mockReportingPlugin, mockLogger); + const jobParams = getJobDocPayload({ + headers: encryptedHeaders, + fields: ['one', 'two'], + conflictedTypesFields: [], + searchRequest: { index: null, body: null }, + }); + const { content } = await executeJob('job123', jobParams, cancellationToken); + + expect(content).toEqual('one,two\none,bar\n'); + }); + }); + + describe('Escaping cells with formulas', () => { + it('escapes values with formulas', async () => { + configGetStub.withArgs('csv', 'escapeFormulaValues').returns(true); + callAsCurrentUserStub.onFirstCall().returns({ + hits: { + hits: [{ _source: { one: `=cmd|' /C calc'!A0`, two: 'bar' } }], + }, + _scroll_id: 'scrollId', + }); + + const executeJob = await executeJobFactory(mockReportingPlugin, mockLogger); + const jobParams = getJobDocPayload({ + headers: encryptedHeaders, + fields: ['one', 'two'], + conflictedTypesFields: [], + searchRequest: { index: null, body: null }, + }); + const { content } = await executeJob('job123', jobParams, cancellationToken); + + expect(content).toEqual("one,two\n\"'=cmd|' /C calc'!A0\",bar\n"); + }); + + it('does not escapes values with formulas', async () => { + configGetStub.withArgs('csv', 'escapeFormulaValues').returns(false); + callAsCurrentUserStub.onFirstCall().returns({ + hits: { + hits: [{ _source: { one: `=cmd|' /C calc'!A0`, two: 'bar' } }], + }, + _scroll_id: 'scrollId', + }); + + const executeJob = await executeJobFactory(mockReportingPlugin, mockLogger); + const jobParams = getJobDocPayload({ + headers: encryptedHeaders, + fields: ['one', 'two'], + conflictedTypesFields: [], + searchRequest: { index: null, body: null }, + }); + const { content } = await executeJob('job123', jobParams, cancellationToken); + + expect(content).toEqual('one,two\n"=cmd|\' /C calc\'!A0",bar\n'); + }); + }); + + describe('Elasticsearch call errors', function() { + it('should reject Promise if search call errors out', async function() { + callAsCurrentUserStub.rejects(new Error()); + const executeJob = await executeJobFactory(mockReportingPlugin, mockLogger); + const jobParams = getJobDocPayload({ + headers: encryptedHeaders, + fields: [], + searchRequest: { index: null, body: null }, + }); + await expect( + executeJob('job123', jobParams, cancellationToken) + ).rejects.toMatchInlineSnapshot(`[Error]`); + }); + + it('should reject Promise if scroll call errors out', async function() { + callAsCurrentUserStub.onFirstCall().resolves({ + hits: { + hits: [{}], + }, + _scroll_id: 'scrollId', + }); + callAsCurrentUserStub.onSecondCall().rejects(new Error()); + const executeJob = await executeJobFactory(mockReportingPlugin, mockLogger); + const jobParams = getJobDocPayload({ + headers: encryptedHeaders, + fields: [], + searchRequest: { index: null, body: null }, + }); + await expect( + executeJob('job123', jobParams, cancellationToken) + ).rejects.toMatchInlineSnapshot(`[Error]`); + }); + }); + + describe('invalid responses', function() { + it('should reject Promise if search returns hits but no _scroll_id', async function() { + callAsCurrentUserStub.resolves({ + hits: { + hits: [{}], + }, + _scroll_id: undefined, + }); + + const executeJob = await executeJobFactory(mockReportingPlugin, mockLogger); + const jobParams = getJobDocPayload({ + headers: encryptedHeaders, + fields: [], + searchRequest: { index: null, body: null }, + }); + await expect( + executeJob('job123', jobParams, cancellationToken) + ).rejects.toMatchInlineSnapshot( + `[Error: Expected _scroll_id in the following Elasticsearch response: {"hits":{"hits":[{}]}}]` + ); + }); + + it('should reject Promise if search returns no hits and no _scroll_id', async function() { + callAsCurrentUserStub.resolves({ + hits: { + hits: [], + }, + _scroll_id: undefined, + }); + + const executeJob = await executeJobFactory(mockReportingPlugin, mockLogger); + const jobParams = getJobDocPayload({ + headers: encryptedHeaders, + fields: [], + searchRequest: { index: null, body: null }, + }); + await expect( + executeJob('job123', jobParams, cancellationToken) + ).rejects.toMatchInlineSnapshot( + `[Error: Expected _scroll_id in the following Elasticsearch response: {"hits":{"hits":[]}}]` + ); + }); + + it('should reject Promise if scroll returns hits but no _scroll_id', async function() { + callAsCurrentUserStub.onFirstCall().resolves({ + hits: { + hits: [{}], + }, + _scroll_id: 'scrollId', + }); + + callAsCurrentUserStub.onSecondCall().resolves({ + hits: { + hits: [{}], + }, + _scroll_id: undefined, + }); + + const executeJob = await executeJobFactory(mockReportingPlugin, mockLogger); + const jobParams = getJobDocPayload({ + headers: encryptedHeaders, + fields: [], + searchRequest: { index: null, body: null }, + }); + await expect( + executeJob('job123', jobParams, cancellationToken) + ).rejects.toMatchInlineSnapshot( + `[Error: Expected _scroll_id in the following Elasticsearch response: {"hits":{"hits":[{}]}}]` + ); + }); + + it('should reject Promise if scroll returns no hits and no _scroll_id', async function() { + callAsCurrentUserStub.onFirstCall().resolves({ + hits: { + hits: [{}], + }, + _scroll_id: 'scrollId', + }); + + callAsCurrentUserStub.onSecondCall().resolves({ + hits: { + hits: [], + }, + _scroll_id: undefined, + }); + + const executeJob = await executeJobFactory(mockReportingPlugin, mockLogger); + const jobParams = getJobDocPayload({ + headers: encryptedHeaders, + fields: [], + searchRequest: { index: null, body: null }, + }); + await expect( + executeJob('job123', jobParams, cancellationToken) + ).rejects.toMatchInlineSnapshot( + `[Error: Expected _scroll_id in the following Elasticsearch response: {"hits":{"hits":[]}}]` + ); + }); + }); + + describe('cancellation', function() { + const scrollId = getRandomScrollId(); + + beforeEach(function() { + // We have to "re-stub" the callAsCurrentUser stub here so that we can use the fakeFunction + // that delays the Promise resolution so we have a chance to call cancellationToken.cancel(). + // Otherwise, we get into an endless loop, and don't have a chance to call cancel + callAsCurrentUserStub.restore(); + callAsCurrentUserStub = sinon + .stub(clusterStub, 'callAsCurrentUser') + .callsFake(async function() { + await delay(1); + return { + hits: { + hits: [{}], + }, + _scroll_id: scrollId, + }; + }); + }); + + it('should stop calling Elasticsearch when cancellationToken.cancel is called', async function() { + const executeJob = await executeJobFactory(mockReportingPlugin, mockLogger); + executeJob( + 'job345', + getJobDocPayload({ + headers: encryptedHeaders, + fields: [], + searchRequest: { index: null, body: null }, + }), + cancellationToken + ); + + await delay(250); + const callCount = callAsCurrentUserStub.callCount; + cancellationToken.cancel(); + await delay(250); + expect(callAsCurrentUserStub.callCount).toBe(callCount + 1); // last call is to clear the scroll + }); + + it(`shouldn't call clearScroll if it never got a scrollId`, async function() { + const executeJob = await executeJobFactory(mockReportingPlugin, mockLogger); + executeJob( + 'job345', + getJobDocPayload({ + headers: encryptedHeaders, + fields: [], + searchRequest: { index: null, body: null }, + }), + cancellationToken + ); + cancellationToken.cancel(); + + for (let i = 0; i < callAsCurrentUserStub.callCount; ++i) { + expect(callAsCurrentUserStub.getCall(i).args[1]).not.toBe('clearScroll'); // dead code? + } + }); + + it('should call clearScroll if it got a scrollId', async function() { + const executeJob = await executeJobFactory(mockReportingPlugin, mockLogger); + executeJob( + 'job345', + getJobDocPayload({ + headers: encryptedHeaders, + fields: [], + searchRequest: { index: null, body: null }, + }), + cancellationToken + ); + await delay(100); + cancellationToken.cancel(); + await delay(100); + + const lastCall = callAsCurrentUserStub.getCall(callAsCurrentUserStub.callCount - 1); + expect(lastCall.args[0]).toBe('clearScroll'); + expect(lastCall.args[1].scrollId).toEqual([scrollId]); + }); + }); + + describe('csv content', function() { + it('should write column headers to output, even if there are no results', async function() { + const executeJob = await executeJobFactory(mockReportingPlugin, mockLogger); + const jobParams = getJobDocPayload({ + headers: encryptedHeaders, + fields: ['one', 'two'], + searchRequest: { index: null, body: null }, + }); + const { content } = await executeJob('job123', jobParams, cancellationToken); + expect(content).toBe(`one,two\n`); + }); + + it('should use custom uiSettings csv:separator for header', async function() { + mockUiSettingsClient.get.withArgs('csv:separator').returns(';'); + const executeJob = await executeJobFactory(mockReportingPlugin, mockLogger); + const jobParams = getJobDocPayload({ + headers: encryptedHeaders, + fields: ['one', 'two'], + searchRequest: { index: null, body: null }, + }); + const { content } = await executeJob('job123', jobParams, cancellationToken); + expect(content).toBe(`one;two\n`); + }); + + it('should escape column headers if uiSettings csv:quoteValues is true', async function() { + mockUiSettingsClient.get.withArgs('csv:quoteValues').returns(true); + const executeJob = await executeJobFactory(mockReportingPlugin, mockLogger); + const jobParams = getJobDocPayload({ + headers: encryptedHeaders, + fields: ['one and a half', 'two', 'three-and-four', 'five & six'], + searchRequest: { index: null, body: null }, + }); + const { content } = await executeJob('job123', jobParams, cancellationToken); + expect(content).toBe(`"one and a half",two,"three-and-four","five & six"\n`); + }); + + it(`shouldn't escape column headers if uiSettings csv:quoteValues is false`, async function() { + mockUiSettingsClient.get.withArgs('csv:quoteValues').returns(false); + const executeJob = await executeJobFactory(mockReportingPlugin, mockLogger); + const jobParams = getJobDocPayload({ + headers: encryptedHeaders, + fields: ['one and a half', 'two', 'three-and-four', 'five & six'], + searchRequest: { index: null, body: null }, + }); + const { content } = await executeJob('job123', jobParams, cancellationToken); + expect(content).toBe(`one and a half,two,three-and-four,five & six\n`); + }); + + it('should write column headers to output, when there are results', async function() { + const executeJob = await executeJobFactory(mockReportingPlugin, mockLogger); + callAsCurrentUserStub.onFirstCall().resolves({ + hits: { + hits: [{ one: '1', two: '2' }], + }, + _scroll_id: 'scrollId', + }); + + const jobParams = getJobDocPayload({ + headers: encryptedHeaders, + fields: ['one', 'two'], + searchRequest: { index: null, body: null }, + }); + const { content } = await executeJob('job123', jobParams, cancellationToken); + const lines = content.split('\n'); + const headerLine = lines[0]; + expect(headerLine).toBe('one,two'); + }); + + it('should use comma separated values of non-nested fields from _source', async function() { + const executeJob = await executeJobFactory(mockReportingPlugin, mockLogger); + callAsCurrentUserStub.onFirstCall().resolves({ + hits: { + hits: [{ _source: { one: 'foo', two: 'bar' } }], + }, + _scroll_id: 'scrollId', + }); + + const jobParams = getJobDocPayload({ + headers: encryptedHeaders, + fields: ['one', 'two'], + conflictedTypesFields: [], + searchRequest: { index: null, body: null }, + }); + const { content } = await executeJob('job123', jobParams, cancellationToken); + const lines = content.split('\n'); + const valuesLine = lines[1]; + expect(valuesLine).toBe('foo,bar'); + }); + + it('should concatenate the hits from multiple responses', async function() { + const executeJob = await executeJobFactory(mockReportingPlugin, mockLogger); + callAsCurrentUserStub.onFirstCall().resolves({ + hits: { + hits: [{ _source: { one: 'foo', two: 'bar' } }], + }, + _scroll_id: 'scrollId', + }); + callAsCurrentUserStub.onSecondCall().resolves({ + hits: { + hits: [{ _source: { one: 'baz', two: 'qux' } }], + }, + _scroll_id: 'scrollId', + }); + + const jobParams = getJobDocPayload({ + headers: encryptedHeaders, + fields: ['one', 'two'], + conflictedTypesFields: [], + searchRequest: { index: null, body: null }, + }); + const { content } = await executeJob('job123', jobParams, cancellationToken); + const lines = content.split('\n'); + + expect(lines[1]).toBe('foo,bar'); + expect(lines[2]).toBe('baz,qux'); + }); + + it('should use field formatters to format fields', async function() { + const executeJob = await executeJobFactory(mockReportingPlugin, mockLogger); + callAsCurrentUserStub.onFirstCall().resolves({ + hits: { + hits: [{ _source: { one: 'foo', two: 'bar' } }], + }, + _scroll_id: 'scrollId', + }); + + const jobParams = getJobDocPayload({ + headers: encryptedHeaders, + fields: ['one', 'two'], + conflictedTypesFields: [], + searchRequest: { index: null, body: null }, + indexPatternSavedObject: { + id: 'logstash-*', + type: 'index-pattern', + attributes: { + title: 'logstash-*', + fields: '[{"name":"one","type":"string"}, {"name":"two","type":"string"}]', + fieldFormatMap: '{"one":{"id":"string","params":{"transform": "upper"}}}', + }, + }, + }); + const { content } = await executeJob('job123', jobParams, cancellationToken); + const lines = content.split('\n'); + + expect(lines[1]).toBe('FOO,bar'); + }); + }); + + describe('maxSizeBytes', function() { + // The following tests use explicitly specified lengths. UTF-8 uses between one and four 8-bit bytes for each + // code-point. However, any character that can be represented by ASCII requires one-byte, so a majority of the + // tests use these 'simple' characters to make the math easier + + describe('when only the headers exceed the maxSizeBytes', function() { + let content: string; + let maxSizeReached: boolean; + + beforeEach(async function() { + configGetStub.withArgs('csv', 'maxSizeBytes').returns(1); + + const executeJob = await executeJobFactory(mockReportingPlugin, mockLogger); + const jobParams = getJobDocPayload({ + headers: encryptedHeaders, + fields: ['one', 'two'], + searchRequest: { index: null, body: null }, + }); + + ({ content, max_size_reached: maxSizeReached } = await executeJob( + 'job123', + jobParams, + cancellationToken + )); + }); + + it('should return max_size_reached', function() { + expect(maxSizeReached).toBe(true); + }); + + it('should return empty content', function() { + expect(content).toBe(''); + }); + }); + + describe('when headers are equal to maxSizeBytes', function() { + let content: string; + let maxSizeReached: boolean; + + beforeEach(async function() { + configGetStub.withArgs('csv', 'maxSizeBytes').returns(9); + + const executeJob = await executeJobFactory(mockReportingPlugin, mockLogger); + const jobParams = getJobDocPayload({ + headers: encryptedHeaders, + fields: ['one', 'two'], + searchRequest: { index: null, body: null }, + }); + + ({ content, max_size_reached: maxSizeReached } = await executeJob( + 'job123', + jobParams, + cancellationToken + )); + }); + + it(`shouldn't return max_size_reached`, function() { + expect(maxSizeReached).toBe(false); + }); + + it(`should return content`, function() { + expect(content).toBe('one,two\n'); + }); + }); + + describe('when the data exceeds the maxSizeBytes', function() { + let content: string; + let maxSizeReached: boolean; + + beforeEach(async function() { + configGetStub.withArgs('csv', 'maxSizeBytes').returns(9); + + callAsCurrentUserStub.onFirstCall().returns({ + hits: { + hits: [{ _source: { one: 'foo', two: 'bar' } }], + }, + _scroll_id: 'scrollId', + }); + + const executeJob = await executeJobFactory(mockReportingPlugin, mockLogger); + const jobParams = getJobDocPayload({ + headers: encryptedHeaders, + fields: ['one', 'two'], + conflictedTypesFields: [], + searchRequest: { index: null, body: null }, + }); + + ({ content, max_size_reached: maxSizeReached } = await executeJob( + 'job123', + jobParams, + cancellationToken + )); + }); + + it(`should return max_size_reached`, function() { + expect(maxSizeReached).toBe(true); + }); + + it(`should return the headers in the content`, function() { + expect(content).toBe('one,two\n'); + }); + }); + + describe('when headers and data equal the maxSizeBytes', function() { + let content: string; + let maxSizeReached: boolean; + + beforeEach(async function() { + mockReportingPlugin.getUiSettingsServiceFactory = () => mockUiSettingsClient; + configGetStub.withArgs('csv', 'maxSizeBytes').returns(18); + + callAsCurrentUserStub.onFirstCall().returns({ + hits: { + hits: [{ _source: { one: 'foo', two: 'bar' } }], + }, + _scroll_id: 'scrollId', + }); + + const executeJob = await executeJobFactory(mockReportingPlugin, mockLogger); + const jobParams = getJobDocPayload({ + headers: encryptedHeaders, + fields: ['one', 'two'], + conflictedTypesFields: [], + searchRequest: { index: null, body: null }, + }); + + ({ content, max_size_reached: maxSizeReached } = await executeJob( + 'job123', + jobParams, + cancellationToken + )); + }); + + it(`shouldn't return max_size_reached`, async function() { + expect(maxSizeReached).toBe(false); + }); + + it('should return headers and data in content', function() { + expect(content).toBe('one,two\nfoo,bar\n'); + }); + }); + }); + + describe('scroll settings', function() { + it('passes scroll duration to initial search call', async function() { + const scrollDuration = 'test'; + configGetStub.withArgs('csv', 'scroll').returns({ duration: scrollDuration }); + + callAsCurrentUserStub.onFirstCall().returns({ + hits: { + hits: [{}], + }, + _scroll_id: 'scrollId', + }); + + const executeJob = await executeJobFactory(mockReportingPlugin, mockLogger); + const jobParams = getJobDocPayload({ + headers: encryptedHeaders, + fields: ['one', 'two'], + conflictedTypesFields: [], + searchRequest: { index: null, body: null }, + }); + + await executeJob('job123', jobParams, cancellationToken); + + const searchCall = callAsCurrentUserStub.firstCall; + expect(searchCall.args[0]).toBe('search'); + expect(searchCall.args[1].scroll).toBe(scrollDuration); + }); + + it('passes scroll size to initial search call', async function() { + const scrollSize = 100; + configGetStub.withArgs('csv', 'scroll').returns({ size: scrollSize }); + + callAsCurrentUserStub.onFirstCall().resolves({ + hits: { + hits: [{}], + }, + _scroll_id: 'scrollId', + }); + + const executeJob = await executeJobFactory(mockReportingPlugin, mockLogger); + const jobParams = getJobDocPayload({ + headers: encryptedHeaders, + fields: ['one', 'two'], + conflictedTypesFields: [], + searchRequest: { index: null, body: null }, + }); + + await executeJob('job123', jobParams, cancellationToken); + + const searchCall = callAsCurrentUserStub.firstCall; + expect(searchCall.args[0]).toBe('search'); + expect(searchCall.args[1].size).toBe(scrollSize); + }); + + it('passes scroll duration to subsequent scroll call', async function() { + const scrollDuration = 'test'; + configGetStub.withArgs('csv', 'scroll').returns({ duration: scrollDuration }); + + callAsCurrentUserStub.onFirstCall().resolves({ + hits: { + hits: [{}], + }, + _scroll_id: 'scrollId', + }); + + const executeJob = await executeJobFactory(mockReportingPlugin, mockLogger); + const jobParams = getJobDocPayload({ + headers: encryptedHeaders, + fields: ['one', 'two'], + conflictedTypesFields: [], + searchRequest: { index: null, body: null }, + }); + + await executeJob('job123', jobParams, cancellationToken); + + const scrollCall = callAsCurrentUserStub.secondCall; + expect(scrollCall.args[0]).toBe('scroll'); + expect(scrollCall.args[1].scroll).toBe(scrollDuration); + }); + }); +}); diff --git a/x-pack/legacy/plugins/reporting/export_types/csv/server/execute_job.ts b/x-pack/legacy/plugins/reporting/export_types/csv/server/execute_job.ts index 376a398da274f..dbe305bc452db 100644 --- a/x-pack/legacy/plugins/reporting/export_types/csv/server/execute_job.ts +++ b/x-pack/legacy/plugins/reporting/export_types/csv/server/execute_job.ts @@ -123,7 +123,7 @@ export const executeJobFactory: ExecuteJobFactory + CSV_FORMULA_CHARS.some(formulaChar => startsWith(val, formulaChar)); diff --git a/x-pack/legacy/plugins/reporting/export_types/csv/server/lib/check_cells_for_formulas.ts b/x-pack/legacy/plugins/reporting/export_types/csv/server/lib/check_cells_for_formulas.ts index 09f7cd2061ffb..0ec39c527d656 100644 --- a/x-pack/legacy/plugins/reporting/export_types/csv/server/lib/check_cells_for_formulas.ts +++ b/x-pack/legacy/plugins/reporting/export_types/csv/server/lib/check_cells_for_formulas.ts @@ -5,8 +5,7 @@ */ import * as _ from 'lodash'; - -const formulaValues = ['=', '+', '-', '@']; +import { cellHasFormulas } from './cell_has_formula'; interface IFlattened { [header: string]: string; @@ -14,7 +13,7 @@ interface IFlattened { export const checkIfRowsHaveFormulas = (flattened: IFlattened, fields: string[]) => { const pruned = _.pick(flattened, fields); - const csvValues = [..._.keys(pruned), ...(_.values(pruned) as string[])]; + const cells = [..._.keys(pruned), ...(_.values(pruned) as string[])]; - return _.some(csvValues, cell => _.some(formulaValues, char => _.startsWith(cell, char))); + return _.some(cells, cell => cellHasFormulas(cell)); }; diff --git a/x-pack/legacy/plugins/reporting/export_types/csv/server/lib/escape_value.test.ts b/x-pack/legacy/plugins/reporting/export_types/csv/server/lib/escape_value.test.ts index 64b021a2aeea8..dd0f9d08b864b 100644 --- a/x-pack/legacy/plugins/reporting/export_types/csv/server/lib/escape_value.test.ts +++ b/x-pack/legacy/plugins/reporting/export_types/csv/server/lib/escape_value.test.ts @@ -11,7 +11,7 @@ describe('escapeValue', function() { describe('quoteValues is true', function() { let escapeValue: (val: string) => string; beforeEach(function() { - escapeValue = createEscapeValue(true); + escapeValue = createEscapeValue(true, false); }); it('should escape value with spaces', function() { @@ -46,7 +46,7 @@ describe('escapeValue', function() { describe('quoteValues is false', function() { let escapeValue: (val: string) => string; beforeEach(function() { - escapeValue = createEscapeValue(false); + escapeValue = createEscapeValue(false, false); }); it('should return the value unescaped', function() { @@ -54,4 +54,34 @@ describe('escapeValue', function() { expect(escapeValue(value)).to.be(value); }); }); + + describe('escapeValues', () => { + describe('when true', () => { + let escapeValue: (val: string) => string; + beforeEach(function() { + escapeValue = createEscapeValue(true, true); + }); + + ['@', '+', '-', '='].forEach(badChar => { + it(`should escape ${badChar} injection values`, function() { + expect(escapeValue(`${badChar}cmd|' /C calc'!A0`)).to.be( + `"'${badChar}cmd|' /C calc'!A0"` + ); + }); + }); + }); + + describe('when false', () => { + let escapeValue: (val: string) => string; + beforeEach(function() { + escapeValue = createEscapeValue(true, false); + }); + + ['@', '+', '-', '='].forEach(badChar => { + it(`should not escape ${badChar} injection values`, function() { + expect(escapeValue(`${badChar}cmd|' /C calc'!A0`)).to.be(`"${badChar}cmd|' /C calc'!A0"`); + }); + }); + }); + }); }); diff --git a/x-pack/legacy/plugins/reporting/export_types/csv/server/lib/escape_value.ts b/x-pack/legacy/plugins/reporting/export_types/csv/server/lib/escape_value.ts index 563de563350e9..60e75d74b2f98 100644 --- a/x-pack/legacy/plugins/reporting/export_types/csv/server/lib/escape_value.ts +++ b/x-pack/legacy/plugins/reporting/export_types/csv/server/lib/escape_value.ts @@ -5,15 +5,20 @@ */ import { RawValue } from './types'; +import { cellHasFormulas } from './cell_has_formula'; const nonAlphaNumRE = /[^a-zA-Z0-9]/; const allDoubleQuoteRE = /"/g; -export function createEscapeValue(quoteValues: boolean): (val: RawValue) => string { +export function createEscapeValue( + quoteValues: boolean, + escapeFormulas: boolean +): (val: RawValue) => string { return function escapeValue(val: RawValue) { if (val && typeof val === 'string') { - if (quoteValues && nonAlphaNumRE.test(val)) { - return `"${val.replace(allDoubleQuoteRE, '""')}"`; + const formulasEscaped = escapeFormulas && cellHasFormulas(val) ? "'" + val : val; + if (quoteValues && nonAlphaNumRE.test(formulasEscaped)) { + return `"${formulasEscaped.replace(allDoubleQuoteRE, '""')}"`; } } diff --git a/x-pack/legacy/plugins/reporting/export_types/csv/server/lib/generate_csv.ts b/x-pack/legacy/plugins/reporting/export_types/csv/server/lib/generate_csv.ts index 1986e68917ba8..c7996ebf832a1 100644 --- a/x-pack/legacy/plugins/reporting/export_types/csv/server/lib/generate_csv.ts +++ b/x-pack/legacy/plugins/reporting/export_types/csv/server/lib/generate_csv.ts @@ -4,6 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ +import { i18n } from '@kbn/i18n'; import { Logger } from '../../../../types'; import { GenerateCsvParams, SavedSearchGeneratorResult } from '../../types'; import { createFlattenHit } from './flatten_hit'; @@ -26,14 +27,17 @@ export function createGenerateCsv(logger: Logger) { cancellationToken, settings, }: GenerateCsvParams): Promise { - const escapeValue = createEscapeValue(settings.quoteValues); + const escapeValue = createEscapeValue(settings.quoteValues, settings.escapeFormulaValues); const builder = new MaxSizeStringBuilder(settings.maxSizeBytes); const header = `${fields.map(escapeValue).join(settings.separator)}\n`; + const warnings: string[] = []; + if (!builder.tryAppend(header)) { return { size: 0, content: '', maxSizeReached: true, + warnings: [], }; } @@ -82,11 +86,20 @@ export function createGenerateCsv(logger: Logger) { const size = builder.getSizeInBytes(); logger.debug(`finished generating, total size in bytes: ${size}`); + if (csvContainsFormulas && settings.escapeFormulaValues) { + warnings.push( + i18n.translate('xpack.reporting.exportTypes.csv.generateCsv.escapedFormulaValues', { + defaultMessage: 'CSV may contain formulas whose values have been escaped', + }) + ); + } + return { content: builder.getString(), - csvContainsFormulas, + csvContainsFormulas: csvContainsFormulas && !settings.escapeFormulaValues, maxSizeReached, size, + warnings, }; }; } diff --git a/x-pack/legacy/plugins/reporting/export_types/csv/types.d.ts b/x-pack/legacy/plugins/reporting/export_types/csv/types.d.ts index 529c195486bc6..40a42db352635 100644 --- a/x-pack/legacy/plugins/reporting/export_types/csv/types.d.ts +++ b/x-pack/legacy/plugins/reporting/export_types/csv/types.d.ts @@ -87,6 +87,7 @@ export interface SavedSearchGeneratorResult { size: number; maxSizeReached: boolean; csvContainsFormulas?: boolean; + warnings: string[]; } export interface CsvResultFromSearch { @@ -109,5 +110,6 @@ export interface GenerateCsvParams { maxSizeBytes: number; scroll: ScrollConfig; checkForFormulas?: boolean; + escapeFormulaValues: boolean; }; } diff --git a/x-pack/legacy/plugins/reporting/export_types/csv_from_savedobject/server/lib/generate_csv_search.ts b/x-pack/legacy/plugins/reporting/export_types/csv_from_savedobject/server/lib/generate_csv_search.ts index 9757c71c19cf4..2611b74c83de9 100644 --- a/x-pack/legacy/plugins/reporting/export_types/csv_from_savedobject/server/lib/generate_csv_search.ts +++ b/x-pack/legacy/plugins/reporting/export_types/csv_from_savedobject/server/lib/generate_csv_search.ts @@ -173,6 +173,7 @@ export async function generateCsvSearch( ...uiSettings, maxSizeBytes: config.get('csv', 'maxSizeBytes'), scroll: config.get('csv', 'scroll'), + escapeFormulaValues: config.get('csv', 'escapeFormulaValues'), timezone, }, }; diff --git a/x-pack/legacy/plugins/reporting/export_types/png/server/execute_job/index.test.js b/x-pack/legacy/plugins/reporting/export_types/png/server/execute_job/index.test.js deleted file mode 100644 index cb63e7dad2fdf..0000000000000 --- a/x-pack/legacy/plugins/reporting/export_types/png/server/execute_job/index.test.js +++ /dev/null @@ -1,121 +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 * as Rx from 'rxjs'; -import { createMockReportingCore } from '../../../../test_helpers'; -import { cryptoFactory } from '../../../../server/lib/crypto'; -import { executeJobFactory } from './index'; -import { generatePngObservableFactory } from '../lib/generate_png'; -import { LevelLogger } from '../../../../server/lib'; - -jest.mock('../lib/generate_png', () => ({ generatePngObservableFactory: jest.fn() })); - -let mockReporting; - -const cancellationToken = { - on: jest.fn(), -}; - -const mockLoggerFactory = { - get: jest.fn().mockImplementation(() => ({ - error: jest.fn(), - debug: jest.fn(), - warn: jest.fn(), - })), -}; -const getMockLogger = () => new LevelLogger(mockLoggerFactory); - -const mockEncryptionKey = 'abcabcsecuresecret'; -const encryptHeaders = async headers => { - const crypto = cryptoFactory(mockEncryptionKey); - return await crypto.encrypt(headers); -}; - -beforeEach(async () => { - const kbnConfig = { - 'server.basePath': '/sbp', - }; - const reportingConfig = { - encryptionKey: mockEncryptionKey, - 'kibanaServer.hostname': 'localhost', - 'kibanaServer.port': 5601, - 'kibanaServer.protocol': 'http', - }; - const mockReportingConfig = { - get: (...keys) => reportingConfig[keys.join('.')], - kbnConfig: { get: (...keys) => kbnConfig[keys.join('.')] }, - }; - - mockReporting = await createMockReportingCore(mockReportingConfig); - - const mockElasticsearch = { - dataClient: { - asScoped: () => ({ callAsCurrentUser: jest.fn() }), - }, - }; - const mockGetElasticsearch = jest.fn(); - mockGetElasticsearch.mockImplementation(() => Promise.resolve(mockElasticsearch)); - mockReporting.getElasticsearchService = mockGetElasticsearch; - - generatePngObservableFactory.mockReturnValue(jest.fn()); -}); - -afterEach(() => generatePngObservableFactory.mockReset()); - -test(`passes browserTimezone to generatePng`, async () => { - const encryptedHeaders = await encryptHeaders({}); - - const generatePngObservable = generatePngObservableFactory(); - generatePngObservable.mockReturnValue(Rx.of(Buffer.from(''))); - - const executeJob = await executeJobFactory(mockReporting, getMockLogger()); - const browserTimezone = 'UTC'; - await executeJob( - 'pngJobId', - { relativeUrl: '/app/kibana#/something', browserTimezone, headers: encryptedHeaders }, - cancellationToken - ); - - expect(generatePngObservable).toBeCalledWith( - expect.any(LevelLogger), - 'http://localhost:5601/sbp/app/kibana#/something', - browserTimezone, - expect.anything(), - undefined - ); -}); - -test(`returns content_type of application/png`, async () => { - const executeJob = await executeJobFactory(mockReporting, getMockLogger()); - const encryptedHeaders = await encryptHeaders({}); - - const generatePngObservable = generatePngObservableFactory(); - generatePngObservable.mockReturnValue(Rx.of(Buffer.from(''))); - - const { content_type: contentType } = await executeJob( - 'pngJobId', - { relativeUrl: '/app/kibana#/something', timeRange: {}, headers: encryptedHeaders }, - cancellationToken - ); - expect(contentType).toBe('image/png'); -}); - -test(`returns content of generatePng getBuffer base64 encoded`, async () => { - const testContent = 'test content'; - - const generatePngObservable = generatePngObservableFactory(); - generatePngObservable.mockReturnValue(Rx.of({ buffer: Buffer.from(testContent) })); - - const executeJob = await executeJobFactory(mockReporting, getMockLogger()); - const encryptedHeaders = await encryptHeaders({}); - const { content } = await executeJob( - 'pngJobId', - { relativeUrl: '/app/kibana#/something', timeRange: {}, headers: encryptedHeaders }, - cancellationToken - ); - - expect(content).toEqual(Buffer.from(testContent).toString('base64')); -}); diff --git a/x-pack/legacy/plugins/reporting/export_types/png/server/execute_job/index.test.ts b/x-pack/legacy/plugins/reporting/export_types/png/server/execute_job/index.test.ts new file mode 100644 index 0000000000000..c9cba64a732b6 --- /dev/null +++ b/x-pack/legacy/plugins/reporting/export_types/png/server/execute_job/index.test.ts @@ -0,0 +1,146 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import * as Rx from 'rxjs'; +import { createMockReportingCore, createMockBrowserDriverFactory } from '../../../../test_helpers'; +import { cryptoFactory } from '../../../../server/lib/crypto'; +import { executeJobFactory } from './index'; +import { generatePngObservableFactory } from '../lib/generate_png'; +import { CancellationToken } from '../../../../common/cancellation_token'; +import { LevelLogger } from '../../../../server/lib'; +import { ReportingCore, CaptureConfig } from '../../../../server/types'; +import { JobDocPayloadPNG } from '../../types'; + +jest.mock('../lib/generate_png', () => ({ generatePngObservableFactory: jest.fn() })); + +let mockReporting: ReportingCore; + +const cancellationToken = ({ + on: jest.fn(), +} as unknown) as CancellationToken; + +const mockLoggerFactory = { + get: jest.fn().mockImplementation(() => ({ + error: jest.fn(), + debug: jest.fn(), + warn: jest.fn(), + })), +}; +const getMockLogger = () => new LevelLogger(mockLoggerFactory); + +const captureConfig = {} as CaptureConfig; + +const mockEncryptionKey = 'abcabcsecuresecret'; +const encryptHeaders = async (headers: Record) => { + const crypto = cryptoFactory(mockEncryptionKey); + return await crypto.encrypt(headers); +}; + +const getJobDocPayload = (baseObj: any) => baseObj as JobDocPayloadPNG; + +beforeEach(async () => { + const kbnConfig = { + 'server.basePath': '/sbp', + }; + const reportingConfig = { + encryptionKey: mockEncryptionKey, + 'kibanaServer.hostname': 'localhost', + 'kibanaServer.port': 5601, + 'kibanaServer.protocol': 'http', + }; + const mockReportingConfig = { + get: (...keys: string[]) => (reportingConfig as any)[keys.join('.')], + kbnConfig: { get: (...keys: string[]) => (kbnConfig as any)[keys.join('.')] }, + }; + + mockReporting = await createMockReportingCore(mockReportingConfig); + + const mockElasticsearch = { + dataClient: { + asScoped: () => ({ callAsCurrentUser: jest.fn() }), + }, + }; + const mockGetElasticsearch = jest.fn(); + mockGetElasticsearch.mockImplementation(() => Promise.resolve(mockElasticsearch)); + mockReporting.getElasticsearchService = mockGetElasticsearch; + + (generatePngObservableFactory as jest.Mock).mockReturnValue(jest.fn()); +}); + +afterEach(() => (generatePngObservableFactory as jest.Mock).mockReset()); + +test(`passes browserTimezone to generatePng`, async () => { + const encryptedHeaders = await encryptHeaders({}); + const mockBrowserDriverFactory = await createMockBrowserDriverFactory(getMockLogger()); + + const generatePngObservable = generatePngObservableFactory( + captureConfig, + mockBrowserDriverFactory + ); + (generatePngObservable as jest.Mock).mockReturnValue(Rx.of(Buffer.from(''))); + + const executeJob = await executeJobFactory(mockReporting, getMockLogger()); + const browserTimezone = 'UTC'; + await executeJob( + 'pngJobId', + getJobDocPayload({ + relativeUrl: '/app/kibana#/something', + browserTimezone, + headers: encryptedHeaders, + }), + cancellationToken + ); + + expect(generatePngObservable).toBeCalledWith( + expect.any(LevelLogger), + 'http://localhost:5601/sbp/app/kibana#/something', + browserTimezone, + expect.anything(), + undefined + ); +}); + +test(`returns content_type of application/png`, async () => { + const executeJob = await executeJobFactory(mockReporting, getMockLogger()); + const encryptedHeaders = await encryptHeaders({}); + + const mockBrowserDriverFactory = await createMockBrowserDriverFactory(getMockLogger()); + + const generatePngObservable = generatePngObservableFactory( + captureConfig, + mockBrowserDriverFactory + ); + (generatePngObservable as jest.Mock).mockReturnValue(Rx.of(Buffer.from(''))); + + const { content_type: contentType } = await executeJob( + 'pngJobId', + getJobDocPayload({ relativeUrl: '/app/kibana#/something', headers: encryptedHeaders }), + cancellationToken + ); + expect(contentType).toBe('image/png'); +}); + +test(`returns content of generatePng getBuffer base64 encoded`, async () => { + const testContent = 'test content'; + + const mockBrowserDriverFactory = await createMockBrowserDriverFactory(getMockLogger()); + + const generatePngObservable = generatePngObservableFactory( + captureConfig, + mockBrowserDriverFactory + ); + (generatePngObservable as jest.Mock).mockReturnValue(Rx.of({ buffer: Buffer.from(testContent) })); + + const executeJob = await executeJobFactory(mockReporting, getMockLogger()); + const encryptedHeaders = await encryptHeaders({}); + const { content } = await executeJob( + 'pngJobId', + getJobDocPayload({ relativeUrl: '/app/kibana#/something', headers: encryptedHeaders }), + cancellationToken + ); + + expect(content).toEqual(Buffer.from(testContent).toString('base64')); +}); diff --git a/x-pack/legacy/plugins/reporting/export_types/printable_pdf/server/execute_job/index.test.js b/x-pack/legacy/plugins/reporting/export_types/printable_pdf/server/execute_job/index.test.js deleted file mode 100644 index c6f07f8ad2d34..0000000000000 --- a/x-pack/legacy/plugins/reporting/export_types/printable_pdf/server/execute_job/index.test.js +++ /dev/null @@ -1,98 +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 * as Rx from 'rxjs'; -import { createMockReportingCore } from '../../../../test_helpers'; -import { cryptoFactory } from '../../../../server/lib/crypto'; -import { executeJobFactory } from './index'; -import { generatePdfObservableFactory } from '../lib/generate_pdf'; -import { LevelLogger } from '../../../../server/lib'; - -jest.mock('../lib/generate_pdf', () => ({ generatePdfObservableFactory: jest.fn() })); - -let mockReporting; - -const cancellationToken = { - on: jest.fn(), -}; - -const mockLoggerFactory = { - get: jest.fn().mockImplementation(() => ({ - error: jest.fn(), - debug: jest.fn(), - warn: jest.fn(), - })), -}; -const getMockLogger = () => new LevelLogger(mockLoggerFactory); - -const mockEncryptionKey = 'testencryptionkey'; -const encryptHeaders = async headers => { - const crypto = cryptoFactory(mockEncryptionKey); - return await crypto.encrypt(headers); -}; - -beforeEach(async () => { - const kbnConfig = { - 'server.basePath': '/sbp', - }; - const reportingConfig = { - encryptionKey: mockEncryptionKey, - 'kibanaServer.hostname': 'localhost', - 'kibanaServer.port': 5601, - 'kibanaServer.protocol': 'http', - }; - const mockReportingConfig = { - get: (...keys) => reportingConfig[keys.join('.')], - kbnConfig: { get: (...keys) => kbnConfig[keys.join('.')] }, - }; - - mockReporting = await createMockReportingCore(mockReportingConfig); - - const mockElasticsearch = { - dataClient: { - asScoped: () => ({ callAsCurrentUser: jest.fn() }), - }, - }; - const mockGetElasticsearch = jest.fn(); - mockGetElasticsearch.mockImplementation(() => Promise.resolve(mockElasticsearch)); - mockReporting.getElasticsearchService = mockGetElasticsearch; - - generatePdfObservableFactory.mockReturnValue(jest.fn()); -}); - -afterEach(() => generatePdfObservableFactory.mockReset()); - -test(`returns content_type of application/pdf`, async () => { - const executeJob = await executeJobFactory(mockReporting, getMockLogger()); - const encryptedHeaders = await encryptHeaders({}); - - const generatePdfObservable = generatePdfObservableFactory(); - generatePdfObservable.mockReturnValue(Rx.of(Buffer.from(''))); - - const { content_type: contentType } = await executeJob( - 'pdfJobId', - { relativeUrls: [], timeRange: {}, headers: encryptedHeaders }, - cancellationToken - ); - expect(contentType).toBe('application/pdf'); -}); - -test(`returns content of generatePdf getBuffer base64 encoded`, async () => { - const testContent = 'test content'; - - const generatePdfObservable = generatePdfObservableFactory(); - generatePdfObservable.mockReturnValue(Rx.of({ buffer: Buffer.from(testContent) })); - - const executeJob = await executeJobFactory(mockReporting, getMockLogger()); - const encryptedHeaders = await encryptHeaders({}); - const { content } = await executeJob( - 'pdfJobId', - { relativeUrls: [], timeRange: {}, headers: encryptedHeaders }, - cancellationToken - ); - - expect(content).toEqual(Buffer.from(testContent).toString('base64')); -}); diff --git a/x-pack/legacy/plugins/reporting/export_types/printable_pdf/server/execute_job/index.test.ts b/x-pack/legacy/plugins/reporting/export_types/printable_pdf/server/execute_job/index.test.ts new file mode 100644 index 0000000000000..c3c0d38584bc1 --- /dev/null +++ b/x-pack/legacy/plugins/reporting/export_types/printable_pdf/server/execute_job/index.test.ts @@ -0,0 +1,114 @@ +/* + * 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 Rx from 'rxjs'; +import { createMockReportingCore, createMockBrowserDriverFactory } from '../../../../test_helpers'; +import { cryptoFactory } from '../../../../server/lib/crypto'; +import { LevelLogger } from '../../../../server/lib'; +import { CancellationToken } from '../../../../types'; +import { ReportingCore, CaptureConfig } from '../../../../server/types'; +import { generatePdfObservableFactory } from '../lib/generate_pdf'; +import { JobDocPayloadPDF } from '../../types'; +import { executeJobFactory } from './index'; + +jest.mock('../lib/generate_pdf', () => ({ generatePdfObservableFactory: jest.fn() })); + +let mockReporting: ReportingCore; + +const cancellationToken = ({ + on: jest.fn(), +} as unknown) as CancellationToken; + +const captureConfig = {} as CaptureConfig; + +const mockLoggerFactory = { + get: jest.fn().mockImplementation(() => ({ + error: jest.fn(), + debug: jest.fn(), + warn: jest.fn(), + })), +}; +const getMockLogger = () => new LevelLogger(mockLoggerFactory); + +const mockEncryptionKey = 'testencryptionkey'; +const encryptHeaders = async (headers: Record) => { + const crypto = cryptoFactory(mockEncryptionKey); + return await crypto.encrypt(headers); +}; + +const getJobDocPayload = (baseObj: any) => baseObj as JobDocPayloadPDF; + +beforeEach(async () => { + const kbnConfig = { + 'server.basePath': '/sbp', + }; + const reportingConfig = { + encryptionKey: mockEncryptionKey, + 'kibanaServer.hostname': 'localhost', + 'kibanaServer.port': 5601, + 'kibanaServer.protocol': 'http', + }; + const mockReportingConfig = { + get: (...keys: string[]) => (reportingConfig as any)[keys.join('.')], + kbnConfig: { get: (...keys: string[]) => (kbnConfig as any)[keys.join('.')] }, + }; + + mockReporting = await createMockReportingCore(mockReportingConfig); + + const mockElasticsearch = { + dataClient: { + asScoped: () => ({ callAsCurrentUser: jest.fn() }), + }, + }; + const mockGetElasticsearch = jest.fn(); + mockGetElasticsearch.mockImplementation(() => Promise.resolve(mockElasticsearch)); + mockReporting.getElasticsearchService = mockGetElasticsearch; + + (generatePdfObservableFactory as jest.Mock).mockReturnValue(jest.fn()); +}); + +afterEach(() => (generatePdfObservableFactory as jest.Mock).mockReset()); + +test(`returns content_type of application/pdf`, async () => { + const logger = getMockLogger(); + const executeJob = await executeJobFactory(mockReporting, logger); + const mockBrowserDriverFactory = await createMockBrowserDriverFactory(logger); + const encryptedHeaders = await encryptHeaders({}); + + const generatePdfObservable = generatePdfObservableFactory( + captureConfig, + mockBrowserDriverFactory + ); + (generatePdfObservable as jest.Mock).mockReturnValue(Rx.of(Buffer.from(''))); + + const { content_type: contentType } = await executeJob( + 'pdfJobId', + getJobDocPayload({ relativeUrls: [], headers: encryptedHeaders }), + cancellationToken + ); + expect(contentType).toBe('application/pdf'); +}); + +test(`returns content of generatePdf getBuffer base64 encoded`, async () => { + const testContent = 'test content'; + const mockBrowserDriverFactory = await createMockBrowserDriverFactory(getMockLogger()); + + const generatePdfObservable = generatePdfObservableFactory( + captureConfig, + mockBrowserDriverFactory + ); + (generatePdfObservable as jest.Mock).mockReturnValue(Rx.of({ buffer: Buffer.from(testContent) })); + + const executeJob = await executeJobFactory(mockReporting, getMockLogger()); + const encryptedHeaders = await encryptHeaders({}); + const { content } = await executeJob( + 'pdfJobId', + getJobDocPayload({ relativeUrls: [], headers: encryptedHeaders }), + cancellationToken + ); + + expect(content).toEqual(Buffer.from(testContent).toString('base64')); +}); diff --git a/x-pack/legacy/plugins/reporting/index.test.js b/x-pack/legacy/plugins/reporting/index.test.js deleted file mode 100644 index 0d9a717bd7d81..0000000000000 --- a/x-pack/legacy/plugins/reporting/index.test.js +++ /dev/null @@ -1,34 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { reporting } from './index'; -import { getConfigSchema } from '../../../test_utils'; - -// The snapshot records the number of cpus available -// to make the snapshot deterministic `os.cpus` needs to be mocked -// but the other members on `os` must remain untouched -jest.mock('os', () => { - const os = jest.requireActual('os'); - os.cpus = () => [{}, {}, {}, {}]; - return os; -}); - -// eslint-disable-next-line jest/valid-describe -const describeWithContext = describe.each([ - [{ dev: false, dist: false }], - [{ dev: true, dist: false }], - [{ dev: false, dist: true }], - [{ dev: true, dist: true }], -]); - -describeWithContext('config schema with context %j', context => { - it('produces correct config', async () => { - const schema = await getConfigSchema(reporting); - const value = await schema.validate({}, { context }); - value.capture.browser.chromium.disableSandbox = ''; - await expect(value).toMatchSnapshot(); - }); -}); diff --git a/x-pack/legacy/plugins/reporting/index.ts b/x-pack/legacy/plugins/reporting/index.ts index a5d27d0545da1..fb95e2c2edc24 100644 --- a/x-pack/legacy/plugins/reporting/index.ts +++ b/x-pack/legacy/plugins/reporting/index.ts @@ -8,7 +8,6 @@ import { i18n } from '@kbn/i18n'; import { Legacy } from 'kibana'; import { resolve } from 'path'; import { PLUGIN_ID, UI_SETTINGS_CUSTOM_PDF_LOGO } from './common/constants'; -import { config as reportingConfig } from './config'; import { legacyInit } from './server/legacy'; import { ReportingPluginSpecOptions } from './types'; @@ -17,10 +16,8 @@ const kbToBase64Length = (kb: number) => Math.floor((kb * 1024 * 8) / 6); export const reporting = (kibana: any) => { return new kibana.Plugin({ id: PLUGIN_ID, - configPrefix: 'xpack.reporting', publicDir: resolve(__dirname, 'public'), require: ['kibana', 'elasticsearch', 'xpack_main'], - config: reportingConfig, uiExports: { uiSettingDefaults: { @@ -47,14 +44,5 @@ export const reporting = (kibana: any) => { async init(server: Legacy.Server) { return legacyInit(server, this); }, - - deprecations({ unused }: any) { - return [ - unused('capture.concurrency'), - unused('capture.timeout'), - unused('capture.settleTime'), - unused('kibanaApp'), - ]; - }, } as ReportingPluginSpecOptions); }; diff --git a/x-pack/legacy/plugins/reporting/log_configuration.ts b/x-pack/legacy/plugins/reporting/log_configuration.ts deleted file mode 100644 index 7aaed2038bd52..0000000000000 --- a/x-pack/legacy/plugins/reporting/log_configuration.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 getosSync, { LinuxOs } from 'getos'; -import { promisify } from 'util'; -import { BROWSER_TYPE } from './common/constants'; -import { CaptureConfig } from './server/types'; -import { Logger } from './types'; - -const getos = promisify(getosSync); - -export async function logConfiguration(captureConfig: CaptureConfig, logger: Logger) { - const { - browser: { - type: browserType, - chromium: { disableSandbox }, - }, - } = captureConfig; - - logger.debug(`Browser type: ${browserType}`); - if (browserType === BROWSER_TYPE) { - logger.debug(`Chromium sandbox disabled: ${disableSandbox}`); - } - - const os = await getos(); - const { os: osName, dist, release } = os as LinuxOs; - if (dist) { - logger.debug(`Running on os "${osName}", distribution "${dist}", release "${release}"`); - } else { - logger.debug(`Running on os "${osName}"`); - } -} diff --git a/x-pack/legacy/plugins/reporting/server/browsers/chromium/driver/chromium_driver.ts b/x-pack/legacy/plugins/reporting/server/browsers/chromium/driver/chromium_driver.ts index 4b80e129c04da..dfaa87021c31c 100644 --- a/x-pack/legacy/plugins/reporting/server/browsers/chromium/driver/chromium_driver.ts +++ b/x-pack/legacy/plugins/reporting/server/browsers/chromium/driver/chromium_driver.ts @@ -190,19 +190,14 @@ export class HeadlessChromiumDriver { } public async screenshot(elementPosition: ElementPosition): Promise { - let clip; - if (elementPosition) { - const { boundingClientRect, scroll = { x: 0, y: 0 } } = elementPosition; - clip = { + const { boundingClientRect, scroll } = elementPosition; + const screenshot = await this.page.screenshot({ + clip: { x: boundingClientRect.left + scroll.x, y: boundingClientRect.top + scroll.y, height: boundingClientRect.height, width: boundingClientRect.width, - }; - } - - const screenshot = await this.page.screenshot({ - clip, + }, }); return screenshot.toString('base64'); diff --git a/x-pack/legacy/plugins/reporting/server/browsers/chromium/driver_factory/args.ts b/x-pack/legacy/plugins/reporting/server/browsers/chromium/driver_factory/args.ts index a2f7a1f3ad0da..928f3b8377809 100644 --- a/x-pack/legacy/plugins/reporting/server/browsers/chromium/driver_factory/args.ts +++ b/x-pack/legacy/plugins/reporting/server/browsers/chromium/driver_factory/args.ts @@ -10,7 +10,7 @@ type ViewportConfig = CaptureConfig['viewport']; type BrowserConfig = CaptureConfig['browser']['chromium']; interface LaunchArgs { - userDataDir: BrowserConfig['userDataDir']; + userDataDir: string; viewport: ViewportConfig; disableSandbox: BrowserConfig['disableSandbox']; proxy: BrowserConfig['proxy']; diff --git a/x-pack/legacy/plugins/reporting/server/browsers/default_chromium_sandbox_disabled.test.js b/x-pack/legacy/plugins/reporting/server/browsers/default_chromium_sandbox_disabled.test.js deleted file mode 100644 index a022506f9e2da..0000000000000 --- a/x-pack/legacy/plugins/reporting/server/browsers/default_chromium_sandbox_disabled.test.js +++ /dev/null @@ -1,32 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -jest.mock('getos', () => { - return jest.fn(); -}); - -import { getDefaultChromiumSandboxDisabled } from './default_chromium_sandbox_disabled'; -import getos from 'getos'; - -function defaultTest(os, expectedDefault) { - test(`${expectedDefault ? 'disabled' : 'enabled'} on ${JSON.stringify(os)}`, async () => { - getos.mockImplementation(cb => cb(null, os)); - const actualDefault = await getDefaultChromiumSandboxDisabled(); - expect(actualDefault).toBe(expectedDefault); - }); -} - -defaultTest({ os: 'win32' }, false); -defaultTest({ os: 'darwin' }, false); -defaultTest({ os: 'linux', dist: 'Centos', release: '7.0' }, true); -defaultTest({ os: 'linux', dist: 'Red Hat Linux', release: '7.0' }, true); -defaultTest({ os: 'linux', dist: 'Ubuntu Linux', release: '14.04' }, false); -defaultTest({ os: 'linux', dist: 'Ubuntu Linux', release: '16.04' }, false); -defaultTest({ os: 'linux', dist: 'SUSE Linux', release: '11' }, false); -defaultTest({ os: 'linux', dist: 'SUSE Linux', release: '12' }, false); -defaultTest({ os: 'linux', dist: 'SUSE Linux', release: '42.0' }, false); -defaultTest({ os: 'linux', dist: 'Debian', release: '8' }, true); -defaultTest({ os: 'linux', dist: 'Debian', release: '9' }, true); diff --git a/x-pack/legacy/plugins/reporting/server/browsers/index.ts b/x-pack/legacy/plugins/reporting/server/browsers/index.ts index 1e42e2736962e..7f902c84308f6 100644 --- a/x-pack/legacy/plugins/reporting/server/browsers/index.ts +++ b/x-pack/legacy/plugins/reporting/server/browsers/index.ts @@ -8,7 +8,6 @@ import * as chromiumDefinition from './chromium'; export { ensureAllBrowsersDownloaded } from './download'; export { createBrowserDriverFactory } from './create_browser_driver_factory'; -export { getDefaultChromiumSandboxDisabled } from './default_chromium_sandbox_disabled'; export { HeadlessChromiumDriver } from './chromium/driver'; export { HeadlessChromiumDriverFactory } from './chromium/driver_factory'; diff --git a/x-pack/legacy/plugins/reporting/server/config/config.js b/x-pack/legacy/plugins/reporting/server/config/config.js deleted file mode 100644 index 08e4db464b003..0000000000000 --- a/x-pack/legacy/plugins/reporting/server/config/config.js +++ /dev/null @@ -1,21 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { cpus } from 'os'; - -const defaultCPUCount = 2; - -function cpuCount() { - try { - return cpus().length; - } catch (e) { - return defaultCPUCount; - } -} - -export const config = { - concurrency: cpuCount(), -}; diff --git a/x-pack/legacy/plugins/reporting/server/config/index.ts b/x-pack/legacy/plugins/reporting/server/config/index.ts index b7b67b57932eb..c6b915be3a94a 100644 --- a/x-pack/legacy/plugins/reporting/server/config/index.ts +++ b/x-pack/legacy/plugins/reporting/server/config/index.ts @@ -6,10 +6,9 @@ import { Legacy } from 'kibana'; import { CoreSetup } from 'src/core/server'; -import { i18n } from '@kbn/i18n'; -import crypto from 'crypto'; import { get } from 'lodash'; -import { NetworkPolicy } from '../../types'; +import { ConfigType as ReportingConfigType } from '../../../../../plugins/reporting/server'; +export { ReportingConfigType }; // make config.get() aware of the value type it returns interface Config { @@ -56,131 +55,6 @@ export interface ReportingConfig extends Config { kbnConfig: Config; } -type BrowserType = 'chromium'; - -interface BrowserConfig { - inspect: boolean; - userDataDir: string; - viewport: { width: number; height: number }; - disableSandbox: boolean; - proxy: { - enabled: boolean; - server?: string; - bypass?: string[]; - }; -} - -interface CaptureConfig { - browser: { - type: BrowserType; - autoDownload: boolean; - chromium: BrowserConfig; - }; - maxAttempts: number; - networkPolicy: NetworkPolicy; - loadDelay: number; - timeouts: { - openUrl: number; - waitForElements: number; - renderComplete: number; - }; - viewport: any; - zoom: any; -} - -interface QueueConfig { - indexInterval: string; - pollEnabled: boolean; - pollInterval: number; - pollIntervalErrorMultiplier: number; - timeout: number; -} - -interface ScrollConfig { - duration: string; - size: number; -} - -export interface ReportingConfigType { - capture: CaptureConfig; - csv: { - scroll: ScrollConfig; - enablePanelActionDownload: boolean; - checkForFormulas: boolean; - maxSizeBytes: number; - useByteOrderMarkEncoding: boolean; - }; - encryptionKey: string; - kibanaServer: any; - index: string; - queue: QueueConfig; - roles: any; -} - -const addConfigDefaults = ( - server: Legacy.Server, - core: CoreSetup, - baseConfig: ReportingConfigType -) => { - // encryption key - let encryptionKey = baseConfig.encryptionKey; - if (encryptionKey === undefined) { - server.log( - ['reporting', 'config', 'warning'], - i18n.translate('xpack.reporting.selfCheckEncryptionKey.warning', { - defaultMessage: - `Generating a random key for {setting}. To prevent pending reports ` + - `from failing on restart, please set {setting} in kibana.yml`, - values: { - setting: 'xpack.reporting.encryptionKey', - }, - }) - ); - encryptionKey = crypto.randomBytes(16).toString('hex'); - } - - const { kibanaServer: reportingServer } = baseConfig; - const serverInfo = core.http.getServerInfo(); - - // kibanaServer.hostname, default to server.host, don't allow "0" - let kibanaServerHostname = reportingServer.hostname ? reportingServer.hostname : serverInfo.host; - if (kibanaServerHostname === '0') { - server.log( - ['reporting', 'config', 'warning'], - i18n.translate('xpack.reporting.selfCheckHostname.warning', { - defaultMessage: - `Found 'server.host: "0"' in settings. This is incompatible with Reporting. ` + - `To enable Reporting to work, '{setting}: 0.0.0.0' is being automatically to the configuration. ` + - `You can change to 'server.host: 0.0.0.0' or add '{setting}: 0.0.0.0' in kibana.yml to prevent this message.`, - values: { - setting: 'xpack.reporting.kibanaServer.hostname', - }, - }) - ); - kibanaServerHostname = '0.0.0.0'; - } - - // kibanaServer.port, default to server.port - const kibanaServerPort = reportingServer.port - ? reportingServer.port - : serverInfo.port; // prettier-ignore - - // kibanaServer.protocol, default to server.protocol - const kibanaServerProtocol = reportingServer.protocol - ? reportingServer.protocol - : serverInfo.protocol; - - return { - ...baseConfig, - encryptionKey, - kibanaServer: { - hostname: kibanaServerHostname, - port: kibanaServerPort, - protocol: kibanaServerProtocol, - }, - }; -}; - export const buildConfig = ( core: CoreSetup, server: Legacy.Server, @@ -204,10 +78,8 @@ export const buildConfig = ( }, }; - // spreading arguments as an array allows the return type to be known by the compiler - reportingConfig = addConfigDefaults(server, core, reportingConfig); return { - get: (...keys: string[]) => get(reportingConfig, keys.join('.'), null), + get: (...keys: string[]) => get(reportingConfig, keys.join('.'), null), // spreading arguments as an array allows the return type to be known by the compiler kbnConfig: { get: (...keys: string[]) => get(kbnConfig, keys.join('.'), null), }, diff --git a/x-pack/legacy/plugins/reporting/server/legacy.ts b/x-pack/legacy/plugins/reporting/server/legacy.ts index 679b42aca6de5..d044dc866ed0e 100644 --- a/x-pack/legacy/plugins/reporting/server/legacy.ts +++ b/x-pack/legacy/plugins/reporting/server/legacy.ts @@ -5,7 +5,9 @@ */ import { Legacy } from 'kibana'; +import { take } from 'rxjs/operators'; import { PluginInitializerContext } from 'src/core/server'; +import { PluginsSetup } from '../../../../plugins/reporting/server'; import { SecurityPluginSetup } from '../../../../plugins/security/server'; import { ReportingPluginSpecOptions } from '../types'; import { buildConfig } from './config'; @@ -17,7 +19,6 @@ const buildLegacyDependencies = ( reportingPlugin: ReportingPluginSpecOptions ): LegacySetup => ({ route: server.route.bind(server), - config: server.config, plugins: { xpack_main: server.plugins.xpack_main, reporting: reportingPlugin, @@ -32,14 +33,13 @@ export const legacyInit = async ( reportingLegacyPlugin: ReportingPluginSpecOptions ) => { const { core: coreSetup } = server.newPlatform.setup; - const legacyConfig = server.config(); - const reportingConfig = buildConfig(coreSetup, server, legacyConfig.get('xpack.reporting')); - + const { config$ } = (server.newPlatform.setup.plugins.reporting as PluginsSetup).__legacy; + const reportingConfig = await config$.pipe(take(1)).toPromise(); const __LEGACY = buildLegacyDependencies(server, reportingLegacyPlugin); const pluginInstance = plugin( server.newPlatform.coreContext as PluginInitializerContext, - reportingConfig + buildConfig(coreSetup, server, reportingConfig) ); await pluginInstance.setup(coreSetup, { elasticsearch: coreSetup.elasticsearch, diff --git a/x-pack/legacy/plugins/reporting/server/lib/esqueue/__tests__/worker.js b/x-pack/legacy/plugins/reporting/server/lib/esqueue/__tests__/worker.js index ad93a1882746d..ea80a652bb506 100644 --- a/x-pack/legacy/plugins/reporting/server/lib/esqueue/__tests__/worker.js +++ b/x-pack/legacy/plugins/reporting/server/lib/esqueue/__tests__/worker.js @@ -760,6 +760,27 @@ describe('Worker class', function() { }); }); + it('handle warnings in the output by reflecting a warning status', () => { + const workerFn = () => { + return Promise.resolve({ + ...payload, + warnings: [`Don't run with scissors!`], + }); + }; + worker = new Worker(mockQueue, 'test', workerFn, defaultWorkerOptions); + + return worker + ._performJob({ + test: true, + ...job, + }) + .then(() => { + sinon.assert.calledOnce(updateSpy); + const doc = updateSpy.firstCall.args[1].body.doc; + expect(doc).to.have.property('status', constants.JOB_STATUS_WARNINGS); + }); + }); + it('should emit completion event', function(done) { worker = new Worker(mockQueue, 'test', noop, defaultWorkerOptions); diff --git a/x-pack/legacy/plugins/reporting/server/lib/esqueue/constants/statuses.js b/x-pack/legacy/plugins/reporting/server/lib/esqueue/constants/statuses.js deleted file mode 100644 index 620a567e18fe7..0000000000000 --- a/x-pack/legacy/plugins/reporting/server/lib/esqueue/constants/statuses.js +++ /dev/null @@ -1,13 +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. - */ - -export const statuses = { - JOB_STATUS_PENDING: 'pending', - JOB_STATUS_PROCESSING: 'processing', - JOB_STATUS_COMPLETED: 'completed', - JOB_STATUS_FAILED: 'failed', - JOB_STATUS_CANCELLED: 'cancelled', -}; diff --git a/x-pack/legacy/plugins/reporting/server/lib/esqueue/constants/statuses.ts b/x-pack/legacy/plugins/reporting/server/lib/esqueue/constants/statuses.ts new file mode 100644 index 0000000000000..7c7f1431adf23 --- /dev/null +++ b/x-pack/legacy/plugins/reporting/server/lib/esqueue/constants/statuses.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. + */ + +export const statuses = { + JOB_STATUS_PENDING: 'pending', + JOB_STATUS_PROCESSING: 'processing', + JOB_STATUS_COMPLETED: 'completed', + JOB_STATUS_WARNINGS: 'completed_with_warnings', + JOB_STATUS_FAILED: 'failed', + JOB_STATUS_CANCELLED: 'cancelled', +}; diff --git a/x-pack/legacy/plugins/reporting/server/lib/esqueue/helpers/index_timestamp.js b/x-pack/legacy/plugins/reporting/server/lib/esqueue/helpers/index_timestamp.js index 6cdbe8f968f75..ceb4ef43b2d9d 100644 --- a/x-pack/legacy/plugins/reporting/server/lib/esqueue/helpers/index_timestamp.js +++ b/x-pack/legacy/plugins/reporting/server/lib/esqueue/helpers/index_timestamp.js @@ -8,6 +8,7 @@ import moment from 'moment'; export const intervals = ['year', 'month', 'week', 'day', 'hour', 'minute']; +// TODO: This helper function can be removed by using `schema.duration` objects in the reporting config schema export function indexTimestamp(intervalStr, separator = '-') { if (separator.match(/[a-z]/i)) throw new Error('Interval separator can not be a letter'); diff --git a/x-pack/legacy/plugins/reporting/server/lib/esqueue/worker.js b/x-pack/legacy/plugins/reporting/server/lib/esqueue/worker.js index 113059fa2fa47..ab0bb6740f078 100644 --- a/x-pack/legacy/plugins/reporting/server/lib/esqueue/worker.js +++ b/x-pack/legacy/plugins/reporting/server/lib/esqueue/worker.js @@ -285,8 +285,12 @@ export class Worker extends events.EventEmitter { const completedTime = moment().toISOString(); const docOutput = this._formatOutput(output); + const status = + output && output.warnings && output.warnings.length > 0 + ? constants.JOB_STATUS_WARNINGS + : constants.JOB_STATUS_COMPLETED; const doc = { - status: constants.JOB_STATUS_COMPLETED, + status, completed_at: completedTime, output: docOutput, }; diff --git a/x-pack/legacy/plugins/reporting/server/plugin.ts b/x-pack/legacy/plugins/reporting/server/plugin.ts index c9ed2e81c6792..e0fa99106a93e 100644 --- a/x-pack/legacy/plugins/reporting/server/plugin.ts +++ b/x-pack/legacy/plugins/reporting/server/plugin.ts @@ -5,15 +5,12 @@ */ import { CoreSetup, CoreStart, Plugin, PluginInitializerContext } from 'src/core/server'; -import { logConfiguration } from '../log_configuration'; import { createBrowserDriverFactory } from './browsers'; import { ReportingCore, ReportingConfig } from './core'; import { createQueueFactory, enqueueJobFactory, LevelLogger, runValidations } from './lib'; import { setFieldFormats } from './services'; import { ReportingSetup, ReportingSetupDeps, ReportingStart, ReportingStartDeps } from './types'; import { registerReportingUsageCollector } from './usage'; -// @ts-ignore no module definition -import { mirrorPluginStatus } from '../../../server/lib/mirror_plugin_status'; export class ReportingPlugin implements Plugin { @@ -61,8 +58,6 @@ export class ReportingPlugin setFieldFormats(plugins.data.fieldFormats); - logConfiguration(this.config.get('capture'), this.logger); - return {}; } diff --git a/x-pack/legacy/plugins/reporting/server/routes/jobs.test.js b/x-pack/legacy/plugins/reporting/server/routes/jobs.test.js deleted file mode 100644 index 9f0de844df369..0000000000000 --- a/x-pack/legacy/plugins/reporting/server/routes/jobs.test.js +++ /dev/null @@ -1,323 +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 Hapi from 'hapi'; -import { createMockReportingCore } from '../../test_helpers'; -import { ExportTypesRegistry } from '../lib/export_types_registry'; - -jest.mock('./lib/authorized_user_pre_routing', () => ({ - authorizedUserPreRoutingFactory: () => () => ({}), -})); -jest.mock('./lib/reporting_feature_pre_routing', () => ({ - reportingFeaturePreRoutingFactory: () => () => () => ({ - jobTypes: ['unencodedJobType', 'base64EncodedJobType'], - }), -})); - -import { registerJobInfoRoutes } from './jobs'; - -let mockServer; -let exportTypesRegistry; -let mockReportingPlugin; -let mockReportingConfig; -const mockLogger = { - error: jest.fn(), - debug: jest.fn(), -}; - -beforeEach(async () => { - mockServer = new Hapi.Server({ debug: false, port: 8080, routes: { log: { collect: true } } }); - exportTypesRegistry = new ExportTypesRegistry(); - exportTypesRegistry.register({ - id: 'unencoded', - jobType: 'unencodedJobType', - jobContentExtension: 'csv', - }); - exportTypesRegistry.register({ - id: 'base64Encoded', - jobType: 'base64EncodedJobType', - jobContentEncoding: 'base64', - jobContentExtension: 'pdf', - }); - - mockReportingConfig = { get: jest.fn(), kbnConfig: { get: jest.fn() } }; - mockReportingPlugin = await createMockReportingCore(mockReportingConfig); - mockReportingPlugin.getExportTypesRegistry = () => exportTypesRegistry; -}); - -const mockPlugins = { - elasticsearch: { - adminClient: { callAsInternalUser: jest.fn() }, - }, - security: null, -}; - -const getHits = (...sources) => { - return { - hits: { - hits: sources.map(source => ({ _source: source })), - }, - }; -}; - -const getErrorsFromRequest = request => - request.logs.filter(log => log.tags.includes('error')).map(log => log.error); - -test(`returns 404 if job not found`, async () => { - mockPlugins.elasticsearch.adminClient = { - callAsInternalUser: jest.fn().mockReturnValue(Promise.resolve(getHits())), - }; - - registerJobInfoRoutes(mockReportingPlugin, mockServer, mockPlugins, mockLogger); - - const request = { - method: 'GET', - url: '/api/reporting/jobs/download/1', - }; - - const response = await mockServer.inject(request); - const { statusCode } = response; - expect(statusCode).toBe(404); -}); - -test(`returns 401 if not valid job type`, async () => { - mockPlugins.elasticsearch.adminClient = { - callAsInternalUser: jest - .fn() - .mockReturnValue(Promise.resolve(getHits({ jobtype: 'invalidJobType' }))), - }; - - registerJobInfoRoutes(mockReportingPlugin, mockServer, mockPlugins, mockLogger); - - const request = { - method: 'GET', - url: '/api/reporting/jobs/download/1', - }; - - const { statusCode } = await mockServer.inject(request); - expect(statusCode).toBe(401); -}); - -describe(`when job is incomplete`, () => { - const getIncompleteResponse = async () => { - mockPlugins.elasticsearch.adminClient = { - callAsInternalUser: jest - .fn() - .mockReturnValue( - Promise.resolve(getHits({ jobtype: 'unencodedJobType', status: 'pending' })) - ), - }; - - registerJobInfoRoutes(mockReportingPlugin, mockServer, mockPlugins, mockLogger); - - const request = { - method: 'GET', - url: '/api/reporting/jobs/download/1', - }; - - return await mockServer.inject(request); - }; - - test(`sets statusCode to 503`, async () => { - const { statusCode } = await getIncompleteResponse(); - expect(statusCode).toBe(503); - }); - - test(`uses status as payload`, async () => { - const { payload } = await getIncompleteResponse(); - expect(payload).toBe('pending'); - }); - - test(`sets content-type header to application/json; charset=utf-8`, async () => { - const { headers } = await getIncompleteResponse(); - expect(headers['content-type']).toBe('application/json; charset=utf-8'); - }); - - test(`sets retry-after header to 30`, async () => { - const { headers } = await getIncompleteResponse(); - expect(headers['retry-after']).toBe(30); - }); -}); - -describe(`when job is failed`, () => { - const getFailedResponse = async () => { - const hits = getHits({ - jobtype: 'unencodedJobType', - status: 'failed', - output: { content: 'job failure message' }, - }); - mockPlugins.elasticsearch.adminClient = { - callAsInternalUser: jest.fn().mockReturnValue(Promise.resolve(hits)), - }; - - registerJobInfoRoutes(mockReportingPlugin, mockServer, mockPlugins, mockLogger); - - const request = { - method: 'GET', - url: '/api/reporting/jobs/download/1', - }; - - return await mockServer.inject(request); - }; - - test(`sets status code to 500`, async () => { - const { statusCode } = await getFailedResponse(); - expect(statusCode).toBe(500); - }); - - test(`sets content-type header to application/json; charset=utf-8`, async () => { - const { headers } = await getFailedResponse(); - expect(headers['content-type']).toBe('application/json; charset=utf-8'); - }); - - test(`sets the payload.reason to the job content`, async () => { - const { payload } = await getFailedResponse(); - expect(JSON.parse(payload).reason).toBe('job failure message'); - }); -}); - -describe(`when job is completed`, () => { - const getCompletedResponse = async ({ - jobType = 'unencodedJobType', - outputContent = 'job output content', - outputContentType = 'application/pdf', - title = '', - } = {}) => { - const hits = getHits({ - jobtype: jobType, - status: 'completed', - output: { content: outputContent, content_type: outputContentType }, - payload: { - title, - }, - }); - mockPlugins.elasticsearch.adminClient = { - callAsInternalUser: jest.fn().mockReturnValue(Promise.resolve(hits)), - }; - - registerJobInfoRoutes(mockReportingPlugin, mockServer, mockPlugins, mockLogger); - - const request = { - method: 'GET', - url: '/api/reporting/jobs/download/1', - }; - - return await mockServer.inject(request); - }; - - test(`sets statusCode to 200`, async () => { - const { statusCode, request } = await getCompletedResponse(); - const errorLogs = getErrorsFromRequest(request); - expect(errorLogs).toEqual([]); - expect(statusCode).toBe(200); - }); - - test(`doesn't encode output content for not-specified jobTypes`, async () => { - const { payload, request } = await getCompletedResponse({ - jobType: 'unencodedJobType', - outputContent: 'test', - }); - - const errorLogs = getErrorsFromRequest(request); - expect(errorLogs).toEqual([]); - - expect(payload).toBe('test'); - }); - - test(`base64 encodes output content for configured jobTypes`, async () => { - const { payload, request } = await getCompletedResponse({ - jobType: 'base64EncodedJobType', - outputContent: 'test', - }); - - const errorLogs = getErrorsFromRequest(request); - expect(errorLogs).toEqual([]); - - expect(payload).toBe(Buffer.from('test', 'base64').toString()); - }); - - test(`specifies text/csv; charset=utf-8 contentType header from the job output`, async () => { - const { headers, request } = await getCompletedResponse({ outputContentType: 'text/csv' }); - - const errorLogs = getErrorsFromRequest(request); - expect(errorLogs).toEqual([]); - - expect(headers['content-type']).toBe('text/csv; charset=utf-8'); - }); - - test(`specifies default filename in content-disposition header if no title`, async () => { - const { headers, request } = await getCompletedResponse({}); - const errorLogs = getErrorsFromRequest(request); - expect(errorLogs).toEqual([]); - expect(headers['content-disposition']).toBe('inline; filename="report.csv"'); - }); - - test(`specifies payload title in content-disposition header`, async () => { - const { headers, request } = await getCompletedResponse({ title: 'something' }); - const errorLogs = getErrorsFromRequest(request); - expect(errorLogs).toEqual([]); - expect(headers['content-disposition']).toBe('inline; filename="something.csv"'); - }); - - test(`specifies jobContentExtension in content-disposition header`, async () => { - const { headers, request } = await getCompletedResponse({ jobType: 'base64EncodedJobType' }); - const errorLogs = getErrorsFromRequest(request); - expect(errorLogs).toEqual([]); - expect(headers['content-disposition']).toBe('inline; filename="report.pdf"'); - }); - - test(`specifies application/pdf contentType header from the job output`, async () => { - const { headers, request } = await getCompletedResponse({ - outputContentType: 'application/pdf', - }); - const errorLogs = getErrorsFromRequest(request); - expect(errorLogs).toEqual([]); - expect(headers['content-type']).toBe('application/pdf'); - }); - - describe(`when non-whitelisted contentType specified in job output`, () => { - test(`sets statusCode to 500`, async () => { - const { statusCode, request } = await getCompletedResponse({ - outputContentType: 'application/html', - }); - const errorLogs = getErrorsFromRequest(request); - expect(errorLogs).toMatchInlineSnapshot(` - Array [ - [Error: Unsupported content-type of application/html specified by job output], - [Error: Unsupported content-type of application/html specified by job output], - ] - `); - expect(statusCode).toBe(500); - }); - - test(`doesn't include job output content in payload`, async () => { - const { payload, request } = await getCompletedResponse({ - outputContentType: 'application/html', - }); - expect(payload).toMatchInlineSnapshot( - `"{\\"statusCode\\":500,\\"error\\":\\"Internal Server Error\\",\\"message\\":\\"An internal server error occurred\\"}"` - ); - const errorLogs = getErrorsFromRequest(request); - expect(errorLogs).toMatchInlineSnapshot(` - Array [ - [Error: Unsupported content-type of application/html specified by job output], - [Error: Unsupported content-type of application/html specified by job output], - ] - `); - }); - - test(`logs error message about invalid content type`, async () => { - const { request } = await getCompletedResponse({ outputContentType: 'application/html' }); - const errorLogs = getErrorsFromRequest(request); - expect(errorLogs).toMatchInlineSnapshot(` - Array [ - [Error: Unsupported content-type of application/html specified by job output], - [Error: Unsupported content-type of application/html specified by job output], - ] - `); - }); - }); -}); diff --git a/x-pack/legacy/plugins/reporting/server/routes/jobs.test.ts b/x-pack/legacy/plugins/reporting/server/routes/jobs.test.ts new file mode 100644 index 0000000000000..5c58a7dfa0110 --- /dev/null +++ b/x-pack/legacy/plugins/reporting/server/routes/jobs.test.ts @@ -0,0 +1,331 @@ +/* + * 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 Hapi from 'hapi'; +import { createMockReportingCore } from '../../test_helpers'; +import { ExportTypeDefinition } from '../../types'; +import { ExportTypesRegistry } from '../lib/export_types_registry'; +import { LevelLogger } from '../lib/level_logger'; +import { ReportingConfig, ReportingCore, ReportingSetupDeps } from '../types'; + +jest.mock('./lib/authorized_user_pre_routing', () => ({ + authorizedUserPreRoutingFactory: () => () => ({}), +})); +jest.mock('./lib/reporting_feature_pre_routing', () => ({ + reportingFeaturePreRoutingFactory: () => () => () => ({ + jobTypes: ['unencodedJobType', 'base64EncodedJobType'], + }), +})); + +import { registerJobInfoRoutes } from './jobs'; + +let mockServer: any; +let exportTypesRegistry: ExportTypesRegistry; +let mockReportingPlugin: ReportingCore; +let mockReportingConfig: ReportingConfig; +const mockLogger = ({ + error: jest.fn(), + debug: jest.fn(), +} as unknown) as LevelLogger; + +beforeEach(async () => { + mockServer = new Hapi.Server({ debug: false, port: 8080, routes: { log: { collect: true } } }); + exportTypesRegistry = new ExportTypesRegistry(); + exportTypesRegistry.register({ + id: 'unencoded', + jobType: 'unencodedJobType', + jobContentExtension: 'csv', + } as ExportTypeDefinition); + exportTypesRegistry.register({ + id: 'base64Encoded', + jobType: 'base64EncodedJobType', + jobContentEncoding: 'base64', + jobContentExtension: 'pdf', + } as ExportTypeDefinition); + + mockReportingConfig = { get: jest.fn(), kbnConfig: { get: jest.fn() } }; + mockReportingPlugin = await createMockReportingCore(mockReportingConfig); + mockReportingPlugin.getExportTypesRegistry = () => exportTypesRegistry; +}); + +const mockPlugins = ({ + elasticsearch: { + adminClient: { callAsInternalUser: jest.fn() }, + }, + security: null, +} as unknown) as ReportingSetupDeps; + +const getHits = (...sources: any) => { + return { + hits: { + hits: sources.map((source: object) => ({ _source: source })), + }, + }; +}; + +const getErrorsFromRequest = (request: any) => + request.logs.filter((log: any) => log.tags.includes('error')).map((log: any) => log.error); + +test(`returns 404 if job not found`, async () => { + // @ts-ignore + mockPlugins.elasticsearch.adminClient = { + callAsInternalUser: jest.fn().mockReturnValue(Promise.resolve(getHits())), + }; + + registerJobInfoRoutes(mockReportingPlugin, mockServer, mockPlugins, mockLogger); + + const request = { + method: 'GET', + url: '/api/reporting/jobs/download/1', + }; + + const response = await mockServer.inject(request); + const { statusCode } = response; + expect(statusCode).toBe(404); +}); + +test(`returns 401 if not valid job type`, async () => { + // @ts-ignore + mockPlugins.elasticsearch.adminClient = { + callAsInternalUser: jest + .fn() + .mockReturnValue(Promise.resolve(getHits({ jobtype: 'invalidJobType' }))), + }; + + registerJobInfoRoutes(mockReportingPlugin, mockServer, mockPlugins, mockLogger); + + const request = { + method: 'GET', + url: '/api/reporting/jobs/download/1', + }; + + const { statusCode } = await mockServer.inject(request); + expect(statusCode).toBe(401); +}); + +describe(`when job is incomplete`, () => { + const getIncompleteResponse = async () => { + // @ts-ignore + mockPlugins.elasticsearch.adminClient = { + callAsInternalUser: jest + .fn() + .mockReturnValue( + Promise.resolve(getHits({ jobtype: 'unencodedJobType', status: 'pending' })) + ), + }; + + registerJobInfoRoutes(mockReportingPlugin, mockServer, mockPlugins, mockLogger); + + const request = { + method: 'GET', + url: '/api/reporting/jobs/download/1', + }; + + return await mockServer.inject(request); + }; + + test(`sets statusCode to 503`, async () => { + const { statusCode } = await getIncompleteResponse(); + expect(statusCode).toBe(503); + }); + + test(`uses status as payload`, async () => { + const { payload } = await getIncompleteResponse(); + expect(payload).toBe('pending'); + }); + + test(`sets content-type header to application/json; charset=utf-8`, async () => { + const { headers } = await getIncompleteResponse(); + expect(headers['content-type']).toBe('application/json; charset=utf-8'); + }); + + test(`sets retry-after header to 30`, async () => { + const { headers } = await getIncompleteResponse(); + expect(headers['retry-after']).toBe(30); + }); +}); + +describe(`when job is failed`, () => { + const getFailedResponse = async () => { + const hits = getHits({ + jobtype: 'unencodedJobType', + status: 'failed', + output: { content: 'job failure message' }, + }); + // @ts-ignore + mockPlugins.elasticsearch.adminClient = { + callAsInternalUser: jest.fn().mockReturnValue(Promise.resolve(hits)), + }; + + registerJobInfoRoutes(mockReportingPlugin, mockServer, mockPlugins, mockLogger); + + const request = { + method: 'GET', + url: '/api/reporting/jobs/download/1', + }; + + return await mockServer.inject(request); + }; + + test(`sets status code to 500`, async () => { + const { statusCode } = await getFailedResponse(); + expect(statusCode).toBe(500); + }); + + test(`sets content-type header to application/json; charset=utf-8`, async () => { + const { headers } = await getFailedResponse(); + expect(headers['content-type']).toBe('application/json; charset=utf-8'); + }); + + test(`sets the payload.reason to the job content`, async () => { + const { payload } = await getFailedResponse(); + expect(JSON.parse(payload).reason).toBe('job failure message'); + }); +}); + +describe(`when job is completed`, () => { + const getCompletedResponse = async ({ + jobType = 'unencodedJobType', + outputContent = 'job output content', + outputContentType = 'application/pdf', + title = '', + } = {}) => { + const hits = getHits({ + jobtype: jobType, + status: 'completed', + output: { content: outputContent, content_type: outputContentType }, + payload: { + title, + }, + }); + // @ts-ignore + mockPlugins.elasticsearch.adminClient = { + callAsInternalUser: jest.fn().mockReturnValue(Promise.resolve(hits)), + }; + + registerJobInfoRoutes(mockReportingPlugin, mockServer, mockPlugins, mockLogger); + + const request = { + method: 'GET', + url: '/api/reporting/jobs/download/1', + }; + + return await mockServer.inject(request); + }; + + test(`sets statusCode to 200`, async () => { + const { statusCode, request } = await getCompletedResponse(); + const errorLogs = getErrorsFromRequest(request); + expect(errorLogs).toEqual([]); + expect(statusCode).toBe(200); + }); + + test(`doesn't encode output content for not-specified jobTypes`, async () => { + const { payload, request } = await getCompletedResponse({ + jobType: 'unencodedJobType', + outputContent: 'test', + }); + + const errorLogs = getErrorsFromRequest(request); + expect(errorLogs).toEqual([]); + + expect(payload).toBe('test'); + }); + + test(`base64 encodes output content for configured jobTypes`, async () => { + const { payload, request } = await getCompletedResponse({ + jobType: 'base64EncodedJobType', + outputContent: 'test', + }); + + const errorLogs = getErrorsFromRequest(request); + expect(errorLogs).toEqual([]); + + expect(payload).toBe(Buffer.from('test', 'base64').toString()); + }); + + test(`specifies text/csv; charset=utf-8 contentType header from the job output`, async () => { + const { headers, request } = await getCompletedResponse({ outputContentType: 'text/csv' }); + + const errorLogs = getErrorsFromRequest(request); + expect(errorLogs).toEqual([]); + + expect(headers['content-type']).toBe('text/csv; charset=utf-8'); + }); + + test(`specifies default filename in content-disposition header if no title`, async () => { + const { headers, request } = await getCompletedResponse({}); + const errorLogs = getErrorsFromRequest(request); + expect(errorLogs).toEqual([]); + expect(headers['content-disposition']).toBe('inline; filename="report.csv"'); + }); + + test(`specifies payload title in content-disposition header`, async () => { + const { headers, request } = await getCompletedResponse({ title: 'something' }); + const errorLogs = getErrorsFromRequest(request); + expect(errorLogs).toEqual([]); + expect(headers['content-disposition']).toBe('inline; filename="something.csv"'); + }); + + test(`specifies jobContentExtension in content-disposition header`, async () => { + const { headers, request } = await getCompletedResponse({ jobType: 'base64EncodedJobType' }); + const errorLogs = getErrorsFromRequest(request); + expect(errorLogs).toEqual([]); + expect(headers['content-disposition']).toBe('inline; filename="report.pdf"'); + }); + + test(`specifies application/pdf contentType header from the job output`, async () => { + const { headers, request } = await getCompletedResponse({ + outputContentType: 'application/pdf', + }); + const errorLogs = getErrorsFromRequest(request); + expect(errorLogs).toEqual([]); + expect(headers['content-type']).toBe('application/pdf'); + }); + + describe(`when non-whitelisted contentType specified in job output`, () => { + test(`sets statusCode to 500`, async () => { + const { statusCode, request } = await getCompletedResponse({ + outputContentType: 'application/html', + }); + const errorLogs = getErrorsFromRequest(request); + expect(errorLogs).toMatchInlineSnapshot(` + Array [ + [Error: Unsupported content-type of application/html specified by job output], + [Error: Unsupported content-type of application/html specified by job output], + ] + `); + expect(statusCode).toBe(500); + }); + + test(`doesn't include job output content in payload`, async () => { + const { payload, request } = await getCompletedResponse({ + outputContentType: 'application/html', + }); + expect(payload).toMatchInlineSnapshot( + `"{\\"statusCode\\":500,\\"error\\":\\"Internal Server Error\\",\\"message\\":\\"An internal server error occurred\\"}"` + ); + const errorLogs = getErrorsFromRequest(request); + expect(errorLogs).toMatchInlineSnapshot(` + Array [ + [Error: Unsupported content-type of application/html specified by job output], + [Error: Unsupported content-type of application/html specified by job output], + ] + `); + }); + + test(`logs error message about invalid content type`, async () => { + const { request } = await getCompletedResponse({ outputContentType: 'application/html' }); + const errorLogs = getErrorsFromRequest(request); + expect(errorLogs).toMatchInlineSnapshot(` + Array [ + [Error: Unsupported content-type of application/html specified by job output], + [Error: Unsupported content-type of application/html specified by job output], + ] + `); + }); + }); +}); diff --git a/x-pack/legacy/plugins/reporting/server/routes/lib/get_document_payload.ts b/x-pack/legacy/plugins/reporting/server/routes/lib/get_document_payload.ts index aef37754681ec..c243d0b4266ea 100644 --- a/x-pack/legacy/plugins/reporting/server/routes/lib/get_document_payload.ts +++ b/x-pack/legacy/plugins/reporting/server/routes/lib/get_document_payload.ts @@ -9,6 +9,7 @@ import contentDisposition from 'content-disposition'; import * as _ from 'lodash'; import { CSV_JOB_TYPE } from '../../../common/constants'; import { ExportTypeDefinition, ExportTypesRegistry, JobDocOutput, JobSource } from '../../../types'; +import { statuses } from '../../lib/esqueue/constants/statuses'; interface ICustomHeaders { [x: string]: any; @@ -99,11 +100,11 @@ export function getDocumentPayloadFactory(exportTypesRegistry: ExportTypesRegist const { status, jobtype: jobType, payload: { title } = { title: '' } } = doc._source; const { output } = doc._source; - if (status === 'completed') { + if (status === statuses.JOB_STATUS_COMPLETED || status === statuses.JOB_STATUS_WARNINGS) { return getCompleted(output, jobType, title); } - if (status === 'failed') { + if (status === statuses.JOB_STATUS_FAILED) { return getFailure(output); } diff --git a/x-pack/legacy/plugins/reporting/server/types.d.ts b/x-pack/legacy/plugins/reporting/server/types.d.ts index bec00688432cc..fb77eae4e7eea 100644 --- a/x-pack/legacy/plugins/reporting/server/types.d.ts +++ b/x-pack/legacy/plugins/reporting/server/types.d.ts @@ -11,7 +11,7 @@ import { PluginStart as DataPluginStart } from '../../../../../src/plugins/data/ import { SecurityPluginSetup } from '../../../../plugins/security/server'; import { XPackMainPlugin } from '../../xpack_main/server/xpack_main'; import { ReportingPluginSpecOptions } from '../types'; -import { ReportingConfig, ReportingConfigType } from './core'; +import { ReportingConfigType } from './core'; export interface ReportingSetupDeps { elasticsearch: ElasticsearchServiceSetup; @@ -30,7 +30,6 @@ export type ReportingSetup = object; export type ReportingStart = object; export interface LegacySetup { - config: Legacy.Server['config']; plugins: { xpack_main: XPackMainPlugin & { status?: any; diff --git a/x-pack/legacy/plugins/reporting/server/usage/reporting_usage_collector.test.js b/x-pack/legacy/plugins/reporting/server/usage/reporting_usage_collector.test.js deleted file mode 100644 index 929109e66914d..0000000000000 --- a/x-pack/legacy/plugins/reporting/server/usage/reporting_usage_collector.test.js +++ /dev/null @@ -1,424 +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 sinon from 'sinon'; -import { createMockReportingCore } from '../../test_helpers'; -import { getExportTypesRegistry } from '../lib/export_types_registry'; -import { - registerReportingUsageCollector, - getReportingUsageCollector, -} from './reporting_usage_collector'; - -const exportTypesRegistry = getExportTypesRegistry(); - -function getMockUsageCollection() { - class MockUsageCollector { - constructor(_server, { fetch }) { - this.fetch = fetch; - } - } - return { - makeUsageCollector: options => { - return new MockUsageCollector(this, options); - }, - registerCollector: sinon.stub(), - }; -} - -function getPluginsMock( - { license, usageCollection = getMockUsageCollection() } = { license: 'platinum' } -) { - const mockXpackMain = { - info: { - isAvailable: sinon.stub().returns(true), - feature: () => ({ - getLicenseCheckResults: sinon.stub(), - }), - license: { - isOneOf: sinon.stub().returns(false), - getType: sinon.stub().returns(license), - }, - toJSON: () => ({ b: 1 }), - }, - }; - return { - usageCollection, - __LEGACY: { - plugins: { - xpack_main: mockXpackMain, - }, - }, - }; -} - -const getMockReportingConfig = () => ({ - get: () => {}, - kbnConfig: { get: () => '' }, -}); -const getResponseMock = (customization = {}) => customization; - -describe('license checks', () => { - let mockConfig; - beforeAll(async () => { - mockConfig = getMockReportingConfig(); - }); - - describe('with a basic license', () => { - let usageStats; - beforeAll(async () => { - const plugins = getPluginsMock({ license: 'basic' }); - const callClusterMock = jest.fn(() => Promise.resolve(getResponseMock())); - const { fetch } = getReportingUsageCollector( - mockConfig, - plugins.usageCollection, - plugins.__LEGACY.plugins.xpack_main.info, - exportTypesRegistry - ); - usageStats = await fetch(callClusterMock, exportTypesRegistry); - }); - - test('sets enables to true', async () => { - expect(usageStats.enabled).toBe(true); - }); - - test('sets csv available to true', async () => { - expect(usageStats.csv.available).toBe(true); - }); - - test('sets pdf availability to false', async () => { - expect(usageStats.printable_pdf.available).toBe(false); - }); - }); - - describe('with no license', () => { - let usageStats; - beforeAll(async () => { - const plugins = getPluginsMock({ license: 'none' }); - const callClusterMock = jest.fn(() => Promise.resolve(getResponseMock())); - const { fetch } = getReportingUsageCollector( - mockConfig, - plugins.usageCollection, - plugins.__LEGACY.plugins.xpack_main.info, - exportTypesRegistry - ); - usageStats = await fetch(callClusterMock, exportTypesRegistry); - }); - - test('sets enables to true', async () => { - expect(usageStats.enabled).toBe(true); - }); - - test('sets csv available to false', async () => { - expect(usageStats.csv.available).toBe(false); - }); - - test('sets pdf availability to false', async () => { - expect(usageStats.printable_pdf.available).toBe(false); - }); - }); - - describe('with platinum license', () => { - let usageStats; - beforeAll(async () => { - const plugins = getPluginsMock({ license: 'platinum' }); - const callClusterMock = jest.fn(() => Promise.resolve(getResponseMock())); - const { fetch } = getReportingUsageCollector( - mockConfig, - plugins.usageCollection, - plugins.__LEGACY.plugins.xpack_main.info, - exportTypesRegistry - ); - usageStats = await fetch(callClusterMock, exportTypesRegistry); - }); - - test('sets enables to true', async () => { - expect(usageStats.enabled).toBe(true); - }); - - test('sets csv available to true', async () => { - expect(usageStats.csv.available).toBe(true); - }); - - test('sets pdf availability to true', async () => { - expect(usageStats.printable_pdf.available).toBe(true); - }); - }); - - describe('with no usage data', () => { - let usageStats; - beforeAll(async () => { - const plugins = getPluginsMock({ license: 'basic' }); - const callClusterMock = jest.fn(() => Promise.resolve({})); - const { fetch } = getReportingUsageCollector( - mockConfig, - plugins.usageCollection, - plugins.__LEGACY.plugins.xpack_main.info, - exportTypesRegistry - ); - usageStats = await fetch(callClusterMock, exportTypesRegistry); - }); - - test('sets enables to true', async () => { - expect(usageStats.enabled).toBe(true); - }); - - test('sets csv available to true', async () => { - expect(usageStats.csv.available).toBe(true); - }); - }); -}); - -describe('data modeling', () => { - test('with normal looking usage data', async () => { - const mockConfig = getMockReportingConfig(); - const plugins = getPluginsMock(); - const { fetch } = getReportingUsageCollector( - mockConfig, - plugins.usageCollection, - plugins.__LEGACY.plugins.xpack_main.info, - exportTypesRegistry - ); - const callClusterMock = jest.fn(() => - Promise.resolve( - getResponseMock({ - aggregations: { - ranges: { - buckets: { - all: { - doc_count: 54, - layoutTypes: { - doc_count: 23, - pdf: { - doc_count_error_upper_bound: 0, - sum_other_doc_count: 0, - buckets: [ - { key: 'preserve_layout', doc_count: 13 }, - { key: 'print', doc_count: 10 }, - ], - }, - }, - objectTypes: { - doc_count: 23, - pdf: { - doc_count_error_upper_bound: 0, - sum_other_doc_count: 0, - buckets: [{ key: 'dashboard', doc_count: 23 }], - }, - }, - statusTypes: { - doc_count_error_upper_bound: 0, - sum_other_doc_count: 0, - buckets: [ - { key: 'pending', doc_count: 33 }, - { key: 'completed', doc_count: 20 }, - { key: 'processing', doc_count: 1 }, - ], - }, - jobTypes: { - doc_count_error_upper_bound: 0, - sum_other_doc_count: 0, - buckets: [ - { key: 'csv', doc_count: 27 }, - { key: 'printable_pdf', doc_count: 23 }, - { key: 'PNG', doc_count: 4 }, - ], - }, - }, - lastDay: { - doc_count: 11, - layoutTypes: { - doc_count: 2, - pdf: { - doc_count_error_upper_bound: 0, - sum_other_doc_count: 0, - buckets: [{ key: 'print', doc_count: 2 }], - }, - }, - objectTypes: { - doc_count: 2, - pdf: { - doc_count_error_upper_bound: 0, - sum_other_doc_count: 0, - buckets: [{ key: 'dashboard', doc_count: 2 }], - }, - }, - statusTypes: { - doc_count_error_upper_bound: 0, - sum_other_doc_count: 0, - buckets: [{ key: 'pending', doc_count: 11 }], - }, - jobTypes: { - doc_count_error_upper_bound: 0, - sum_other_doc_count: 0, - buckets: [ - { key: 'csv', doc_count: 5 }, - { key: 'PNG', doc_count: 4 }, - { key: 'printable_pdf', doc_count: 2 }, - ], - }, - }, - last7Days: { - doc_count: 27, - layoutTypes: { - doc_count: 13, - pdf: { - doc_count_error_upper_bound: 0, - sum_other_doc_count: 0, - buckets: [ - { key: 'print', doc_count: 10 }, - { key: 'preserve_layout', doc_count: 3 }, - ], - }, - }, - objectTypes: { - doc_count: 13, - pdf: { - doc_count_error_upper_bound: 0, - sum_other_doc_count: 0, - buckets: [{ key: 'dashboard', doc_count: 13 }], - }, - }, - statusTypes: { - doc_count_error_upper_bound: 0, - sum_other_doc_count: 0, - buckets: [{ key: 'pending', doc_count: 27 }], - }, - jobTypes: { - doc_count_error_upper_bound: 0, - sum_other_doc_count: 0, - buckets: [ - { key: 'printable_pdf', doc_count: 13 }, - { key: 'csv', doc_count: 10 }, - { key: 'PNG', doc_count: 4 }, - ], - }, - }, - }, - }, - }, - }) - ) - ); - - const usageStats = await fetch(callClusterMock); - expect(usageStats).toMatchInlineSnapshot(` - Object { - "PNG": Object { - "available": true, - "total": 4, - }, - "_all": 54, - "available": true, - "browser_type": undefined, - "csv": Object { - "available": true, - "total": 27, - }, - "enabled": true, - "last7Days": Object { - "PNG": Object { - "available": true, - "total": 4, - }, - "_all": 27, - "csv": Object { - "available": true, - "total": 10, - }, - "printable_pdf": Object { - "app": Object { - "dashboard": 13, - "visualization": 0, - }, - "available": true, - "layout": Object { - "preserve_layout": 3, - "print": 10, - }, - "total": 13, - }, - "status": Object { - "completed": 0, - "failed": 0, - "pending": 27, - }, - }, - "lastDay": Object { - "PNG": Object { - "available": true, - "total": 4, - }, - "_all": 11, - "csv": Object { - "available": true, - "total": 5, - }, - "printable_pdf": Object { - "app": Object { - "dashboard": 2, - "visualization": 0, - }, - "available": true, - "layout": Object { - "preserve_layout": 0, - "print": 2, - }, - "total": 2, - }, - "status": Object { - "completed": 0, - "failed": 0, - "pending": 11, - }, - }, - "printable_pdf": Object { - "app": Object { - "dashboard": 23, - "visualization": 0, - }, - "available": true, - "layout": Object { - "preserve_layout": 13, - "print": 10, - }, - "total": 23, - }, - "status": Object { - "completed": 20, - "failed": 0, - "pending": 33, - "processing": 1, - }, - } - `); - }); -}); - -describe('Ready for collection observable', () => { - test('converts observable to promise', async () => { - const mockConfig = getMockReportingConfig(); - const mockReporting = await createMockReportingCore(mockConfig); - - const usageCollection = getMockUsageCollection(); - const makeCollectorSpy = sinon.spy(); - usageCollection.makeUsageCollector = makeCollectorSpy; - - const plugins = getPluginsMock({ usageCollection }); - registerReportingUsageCollector(mockReporting, plugins); - - const [args] = makeCollectorSpy.firstCall.args; - expect(args).toMatchInlineSnapshot(` - Object { - "fetch": [Function], - "formatForBulkUpload": [Function], - "isReady": [Function], - "type": "reporting", - } - `); - - await expect(args.isReady()).resolves.toBe(true); - }); -}); diff --git a/x-pack/legacy/plugins/reporting/server/usage/reporting_usage_collector.test.ts b/x-pack/legacy/plugins/reporting/server/usage/reporting_usage_collector.test.ts new file mode 100644 index 0000000000000..dbc674ce36ec8 --- /dev/null +++ b/x-pack/legacy/plugins/reporting/server/usage/reporting_usage_collector.test.ts @@ -0,0 +1,442 @@ +/* + * 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 sinon from 'sinon'; +import { createMockReportingCore } from '../../test_helpers'; +import { getExportTypesRegistry } from '../lib/export_types_registry'; +import { + registerReportingUsageCollector, + getReportingUsageCollector, +} from './reporting_usage_collector'; +import { ReportingConfig } from '../types'; + +const exportTypesRegistry = getExportTypesRegistry(); + +function getMockUsageCollection() { + class MockUsageCollector { + // @ts-ignore fetch is not used + private fetch: any; + constructor(_server: any, { fetch }: any) { + this.fetch = fetch; + } + } + return { + makeUsageCollector: (options: any) => { + return new MockUsageCollector(null, options); + }, + registerCollector: sinon.stub(), + }; +} + +function getPluginsMock( + { license, usageCollection = getMockUsageCollection() } = { license: 'platinum' } +) { + const mockXpackMain = { + info: { + isAvailable: sinon.stub().returns(true), + feature: () => ({ + getLicenseCheckResults: sinon.stub(), + }), + license: { + isOneOf: sinon.stub().returns(false), + getType: sinon.stub().returns(license), + }, + toJSON: () => ({ b: 1 }), + }, + }; + return { + usageCollection, + __LEGACY: { + plugins: { + xpack_main: mockXpackMain, + }, + }, + } as any; +} + +const getMockReportingConfig = () => ({ + get: () => {}, + kbnConfig: { get: () => '' }, +}); +const getResponseMock = (customization = {}) => customization; + +describe('license checks', () => { + let mockConfig: ReportingConfig; + beforeAll(async () => { + mockConfig = getMockReportingConfig(); + }); + + describe('with a basic license', () => { + let usageStats: any; + beforeAll(async () => { + const plugins = getPluginsMock({ license: 'basic' }); + const callClusterMock = jest.fn(() => Promise.resolve(getResponseMock())); + const { fetch } = getReportingUsageCollector( + mockConfig, + plugins.usageCollection, + plugins.__LEGACY.plugins.xpack_main.info, + exportTypesRegistry, + function isReady() { + return Promise.resolve(true); + } + ); + usageStats = await fetch(callClusterMock as any); + }); + + test('sets enables to true', async () => { + expect(usageStats.enabled).toBe(true); + }); + + test('sets csv available to true', async () => { + expect(usageStats.csv.available).toBe(true); + }); + + test('sets pdf availability to false', async () => { + expect(usageStats.printable_pdf.available).toBe(false); + }); + }); + + describe('with no license', () => { + let usageStats: any; + beforeAll(async () => { + const plugins = getPluginsMock({ license: 'none' }); + const callClusterMock = jest.fn(() => Promise.resolve(getResponseMock())); + const { fetch } = getReportingUsageCollector( + mockConfig, + plugins.usageCollection, + plugins.__LEGACY.plugins.xpack_main.info, + exportTypesRegistry, + function isReady() { + return Promise.resolve(true); + } + ); + usageStats = await fetch(callClusterMock as any); + }); + + test('sets enables to true', async () => { + expect(usageStats.enabled).toBe(true); + }); + + test('sets csv available to false', async () => { + expect(usageStats.csv.available).toBe(false); + }); + + test('sets pdf availability to false', async () => { + expect(usageStats.printable_pdf.available).toBe(false); + }); + }); + + describe('with platinum license', () => { + let usageStats: any; + beforeAll(async () => { + const plugins = getPluginsMock({ license: 'platinum' }); + const callClusterMock = jest.fn(() => Promise.resolve(getResponseMock())); + const { fetch } = getReportingUsageCollector( + mockConfig, + plugins.usageCollection, + plugins.__LEGACY.plugins.xpack_main.info, + exportTypesRegistry, + function isReady() { + return Promise.resolve(true); + } + ); + usageStats = await fetch(callClusterMock as any); + }); + + test('sets enables to true', async () => { + expect(usageStats.enabled).toBe(true); + }); + + test('sets csv available to true', async () => { + expect(usageStats.csv.available).toBe(true); + }); + + test('sets pdf availability to true', async () => { + expect(usageStats.printable_pdf.available).toBe(true); + }); + }); + + describe('with no usage data', () => { + let usageStats: any; + beforeAll(async () => { + const plugins = getPluginsMock({ license: 'basic' }); + const callClusterMock = jest.fn(() => Promise.resolve({})); + const { fetch } = getReportingUsageCollector( + mockConfig, + plugins.usageCollection, + plugins.__LEGACY.plugins.xpack_main.info, + exportTypesRegistry, + function isReady() { + return Promise.resolve(true); + } + ); + usageStats = await fetch(callClusterMock as any); + }); + + test('sets enables to true', async () => { + expect(usageStats.enabled).toBe(true); + }); + + test('sets csv available to true', async () => { + expect(usageStats.csv.available).toBe(true); + }); + }); +}); + +describe('data modeling', () => { + test('with normal looking usage data', async () => { + const mockConfig = getMockReportingConfig(); + const plugins = getPluginsMock(); + const { fetch } = getReportingUsageCollector( + mockConfig, + plugins.usageCollection, + plugins.__LEGACY.plugins.xpack_main.info, + exportTypesRegistry, + function isReady() { + return Promise.resolve(true); + } + ); + const callClusterMock = jest.fn(() => + Promise.resolve( + getResponseMock({ + aggregations: { + ranges: { + buckets: { + all: { + doc_count: 54, + layoutTypes: { + doc_count: 23, + pdf: { + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, + buckets: [ + { key: 'preserve_layout', doc_count: 13 }, + { key: 'print', doc_count: 10 }, + ], + }, + }, + objectTypes: { + doc_count: 23, + pdf: { + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, + buckets: [{ key: 'dashboard', doc_count: 23 }], + }, + }, + statusTypes: { + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, + buckets: [ + { key: 'pending', doc_count: 33 }, + { key: 'completed', doc_count: 20 }, + { key: 'processing', doc_count: 1 }, + ], + }, + jobTypes: { + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, + buckets: [ + { key: 'csv', doc_count: 27 }, + { key: 'printable_pdf', doc_count: 23 }, + { key: 'PNG', doc_count: 4 }, + ], + }, + }, + lastDay: { + doc_count: 11, + layoutTypes: { + doc_count: 2, + pdf: { + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, + buckets: [{ key: 'print', doc_count: 2 }], + }, + }, + objectTypes: { + doc_count: 2, + pdf: { + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, + buckets: [{ key: 'dashboard', doc_count: 2 }], + }, + }, + statusTypes: { + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, + buckets: [{ key: 'pending', doc_count: 11 }], + }, + jobTypes: { + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, + buckets: [ + { key: 'csv', doc_count: 5 }, + { key: 'PNG', doc_count: 4 }, + { key: 'printable_pdf', doc_count: 2 }, + ], + }, + }, + last7Days: { + doc_count: 27, + layoutTypes: { + doc_count: 13, + pdf: { + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, + buckets: [ + { key: 'print', doc_count: 10 }, + { key: 'preserve_layout', doc_count: 3 }, + ], + }, + }, + objectTypes: { + doc_count: 13, + pdf: { + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, + buckets: [{ key: 'dashboard', doc_count: 13 }], + }, + }, + statusTypes: { + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, + buckets: [{ key: 'pending', doc_count: 27 }], + }, + jobTypes: { + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, + buckets: [ + { key: 'printable_pdf', doc_count: 13 }, + { key: 'csv', doc_count: 10 }, + { key: 'PNG', doc_count: 4 }, + ], + }, + }, + }, + }, + }, + }) + ) + ); + + const usageStats = await fetch(callClusterMock as any); + expect(usageStats).toMatchInlineSnapshot(` + Object { + "PNG": Object { + "available": true, + "total": 4, + }, + "_all": 54, + "available": true, + "browser_type": undefined, + "csv": Object { + "available": true, + "total": 27, + }, + "enabled": true, + "last7Days": Object { + "PNG": Object { + "available": true, + "total": 4, + }, + "_all": 27, + "csv": Object { + "available": true, + "total": 10, + }, + "printable_pdf": Object { + "app": Object { + "dashboard": 13, + "visualization": 0, + }, + "available": true, + "layout": Object { + "preserve_layout": 3, + "print": 10, + }, + "total": 13, + }, + "status": Object { + "completed": 0, + "failed": 0, + "pending": 27, + }, + }, + "lastDay": Object { + "PNG": Object { + "available": true, + "total": 4, + }, + "_all": 11, + "csv": Object { + "available": true, + "total": 5, + }, + "printable_pdf": Object { + "app": Object { + "dashboard": 2, + "visualization": 0, + }, + "available": true, + "layout": Object { + "preserve_layout": 0, + "print": 2, + }, + "total": 2, + }, + "status": Object { + "completed": 0, + "failed": 0, + "pending": 11, + }, + }, + "printable_pdf": Object { + "app": Object { + "dashboard": 23, + "visualization": 0, + }, + "available": true, + "layout": Object { + "preserve_layout": 13, + "print": 10, + }, + "total": 23, + }, + "status": Object { + "completed": 20, + "failed": 0, + "pending": 33, + "processing": 1, + }, + } + `); + }); +}); + +describe('Ready for collection observable', () => { + test('converts observable to promise', async () => { + const mockConfig = getMockReportingConfig(); + const mockReporting = await createMockReportingCore(mockConfig); + + const usageCollection = getMockUsageCollection(); + const makeCollectorSpy = sinon.spy(); + usageCollection.makeUsageCollector = makeCollectorSpy; + + const plugins = getPluginsMock({ usageCollection } as any); + registerReportingUsageCollector(mockReporting, plugins); + + const [args] = makeCollectorSpy.firstCall.args; + expect(args).toMatchInlineSnapshot(` + Object { + "fetch": [Function], + "formatForBulkUpload": [Function], + "isReady": [Function], + "type": "reporting", + } + `); + + await expect(args.isReady()).resolves.toBe(true); + }); +}); diff --git a/x-pack/legacy/plugins/reporting/test_helpers/create_mock_browserdriverfactory.ts b/x-pack/legacy/plugins/reporting/test_helpers/create_mock_browserdriverfactory.ts index 930aa7601b8cb..1be10f6a2056f 100644 --- a/x-pack/legacy/plugins/reporting/test_helpers/create_mock_browserdriverfactory.ts +++ b/x-pack/legacy/plugins/reporting/test_helpers/create_mock_browserdriverfactory.ts @@ -34,7 +34,7 @@ const getMockElementsPositionAndAttributes = ( ): ElementsPositionAndAttribute[] => [ { position: { - boundingClientRect: { top: 0, left: 0, width: 10, height: 11 }, + boundingClientRect: { top: 0, left: 0, width: 800, height: 600 }, scroll: { x: 0, y: 0 }, }, attributes: { title, description }, @@ -78,7 +78,7 @@ mockBrowserEvaluate.mockImplementation(() => { }); const mockScreenshot = jest.fn(); mockScreenshot.mockImplementation((item: ElementsPositionAndAttribute) => { - return Promise.resolve(`allyourBase64 of ${Object.keys(item)}`); + return Promise.resolve(`allyourBase64`); }); const getCreatePage = (driver: HeadlessChromiumDriver) => jest.fn().mockImplementation(() => Rx.of({ driver, exit$: Rx.never() })); @@ -92,33 +92,25 @@ const defaultOpts: CreateMockBrowserDriverFactoryOpts = { export const createMockBrowserDriverFactory = async ( logger: Logger, - opts: Partial + opts: Partial = {} ): Promise => { - const captureConfig = { + const captureConfig: CaptureConfig = { timeouts: { openUrl: 30000, waitForElements: 30000, renderComplete: 30000 }, browser: { type: 'chromium', chromium: { inspect: false, disableSandbox: false, - userDataDir: '/usr/data/dir', - viewport: { width: 12, height: 12 }, proxy: { enabled: false, server: undefined, bypass: undefined }, }, autoDownload: false, - inspect: true, - userDataDir: '/usr/data/dir', - viewport: { width: 12, height: 12 }, - disableSandbox: false, - proxy: { enabled: false, server: undefined, bypass: undefined }, - maxScreenshotDimension: undefined, }, networkPolicy: { enabled: true, rules: [] }, viewport: { width: 800, height: 600 }, loadDelay: 2000, - zoom: 1, + zoom: 2, maxAttempts: 1, - } as CaptureConfig; + }; const binaryPath = '/usr/local/share/common/secure/'; const mockBrowserDriverFactory = await createDriverFactory(binaryPath, logger, captureConfig); diff --git a/x-pack/legacy/plugins/reporting/test_helpers/create_mock_layoutinstance.ts b/x-pack/legacy/plugins/reporting/test_helpers/create_mock_layoutinstance.ts index be60b56dcc0c1..81090e7616501 100644 --- a/x-pack/legacy/plugins/reporting/test_helpers/create_mock_layoutinstance.ts +++ b/x-pack/legacy/plugins/reporting/test_helpers/create_mock_layoutinstance.ts @@ -12,7 +12,7 @@ import { CaptureConfig } from '../server/types'; export const createMockLayoutInstance = (captureConfig: CaptureConfig) => { const mockLayout = createLayout(captureConfig, { id: LayoutTypes.PRESERVE_LAYOUT, - dimensions: { height: 12, width: 12 }, + dimensions: { height: 100, width: 100 }, }) as LayoutInstance; mockLayout.selectors = { renderComplete: 'renderedSelector', diff --git a/x-pack/legacy/plugins/reporting/test_helpers/create_mock_reportingplugin.ts b/x-pack/legacy/plugins/reporting/test_helpers/create_mock_reportingplugin.ts index 34ff91d1972a0..ec00023b4d449 100644 --- a/x-pack/legacy/plugins/reporting/test_helpers/create_mock_reportingplugin.ts +++ b/x-pack/legacy/plugins/reporting/test_helpers/create_mock_reportingplugin.ts @@ -11,7 +11,6 @@ jest.mock('../server/browsers'); jest.mock('../server/lib/create_queue'); jest.mock('../server/lib/enqueue_job'); jest.mock('../server/lib/validate'); -jest.mock('../log_configuration'); import { EventEmitter } from 'events'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths diff --git a/x-pack/legacy/plugins/reporting/types.d.ts b/x-pack/legacy/plugins/reporting/types.d.ts index 7334a859005e0..2e7da6663ab03 100644 --- a/x-pack/legacy/plugins/reporting/types.d.ts +++ b/x-pack/legacy/plugins/reporting/types.d.ts @@ -8,6 +8,7 @@ import { EventEmitter } from 'events'; import { ResponseObject } from 'hapi'; import { Legacy } from 'kibana'; import { CallCluster } from '../../../../src/legacy/core_plugins/elasticsearch'; +import { JobStatus } from '../../../plugins/reporting'; // reporting new platform import { CancellationToken } from './common/cancellation_token'; import { ReportingCore } from './server/core'; import { LevelLogger } from './server/lib/level_logger'; @@ -150,7 +151,7 @@ export interface JobSource { jobtype: string; output: JobDocOutput; payload: JobDocPayload; - status: string; // completed, failed, etc + status: JobStatus; }; } @@ -186,7 +187,7 @@ export type ESQueueWorkerExecuteFn = ( jobId: string, job: JobDocPayloadType, cancellationToken?: CancellationToken -) => void; +) => Promise; /* * ImmediateExecuteFn receives the job doc payload because the payload was diff --git a/x-pack/legacy/plugins/rollup/README.md b/x-pack/legacy/plugins/rollup/README.md index 6d04973de591e..3647be38b6a09 100644 --- a/x-pack/legacy/plugins/rollup/README.md +++ b/x-pack/legacy/plugins/rollup/README.md @@ -14,7 +14,7 @@ The rest of this doc dives into the implementation details of each of the above ## Create and manage rollup jobs -The most straight forward part of this plugin! A new app called Rollup Jobs is registered in the Management section and follows a typical CRUD UI pattern. This app allows users to create, start, stop, clone, and delete rollup jobs. There is no way to edit an existing rollup job; instead, the UI offers a cloning ability. The client-side portion of this app lives [here](public/crud_app) and uses endpoints registered [here](server/routes/api/jobs.js). +The most straight forward part of this plugin! A new app called Rollup Jobs is registered in the Management section and follows a typical CRUD UI pattern. This app allows users to create, start, stop, clone, and delete rollup jobs. There is no way to edit an existing rollup job; instead, the UI offers a cloning ability. The client-side portion of this app lives [here](../../../plugins/rollup/public/crud_app) and uses endpoints registered [here](server/routes/api/jobs.js). Refer to the [Elasticsearch documentation](https://www.elastic.co/guide/en/elasticsearch/reference/current/rollup-getting-started.html) to understand rollup indices and how to create rollup jobs. @@ -22,22 +22,22 @@ Refer to the [Elasticsearch documentation](https://www.elastic.co/guide/en/elast Kibana uses index patterns to consume and visualize rollup indices. Typically, Kibana can inspect the indices captured by an index pattern, identify its aggregations and fields, and determine how to consume the data. Rollup indices don't contain this type of information, so we predefine how to consume a rollup index pattern with the type and typeMeta fields on the index pattern saved object. All rollup index patterns have `type` defined as "rollup" and `typeMeta` defined as an object of the index pattern's capabilities. -In the Index Pattern app, the "Create index pattern" button includes a context menu when a rollup index is detected. This menu offers items for creating a standard index pattern and a rollup index pattern. A [rollup config is registered to index pattern creation extension point](public/index_pattern_creation/rollup_index_pattern_creation_config.js). The context menu behavior in particular uses the `getIndexPatternCreationOption()` method. When the user chooses to create a rollup index pattern, this config changes the behavior of the index pattern creation wizard: +In the Index Pattern app, the "Create index pattern" button includes a context menu when a rollup index is detected. This menu offers items for creating a standard index pattern and a rollup index pattern. A [rollup config is registered to index pattern creation extension point](../../../plugins/rollup/public/index_pattern_creation/rollup_index_pattern_creation_config.js). The context menu behavior in particular uses the `getIndexPatternCreationOption()` method. When the user chooses to create a rollup index pattern, this config changes the behavior of the index pattern creation wizard: 1. Adds a `Rollup` badge to rollup indices using `getIndexTags()`. 2. Enforces index pattern rules using `checkIndicesForErrors()`. Rollup index patterns must match **one** rollup index, and optionally, any number of regular indices. A rollup index pattern configured with one or more regular indices is known as a "hybrid" index pattern. This allows the user to visualize historical (rollup) data and live (regular) data in the same visualization. 3. Routes to this plugin's [rollup `_fields_for_wildcard` endpoint](server/routes/api/index_patterns.js), instead of the standard one, using `getFetchForWildcardOptions()`, so that the internal rollup data field names are mapped to the original field names. 4. Writes additional information about aggregations, fields, histogram interval, and date histogram interval and timezone to the rollup index pattern saved object using `getIndexPatternMappings()`. This collection of information is referred to as its "capabilities". -Once a rollup index pattern is created, it is tagged with `Rollup` in the list of index patterns, and its details page displays capabilities information. This is done by registering [yet another config for the index pattern list](public/index_pattern_list/rollup_index_pattern_list_config.js) extension points. +Once a rollup index pattern is created, it is tagged with `Rollup` in the list of index patterns, and its details page displays capabilities information. This is done by registering [yet another config for the index pattern list](../../../plugins/rollup/public/index_pattern_list/rollup_index_pattern_list_config.js) extension points. ## Create visualizations from rollup index patterns This plugin enables the user to create visualizations from rollup data using the Visualize app, excluding TSVB, Vega, and Timelion. When Visualize sends search requests, this plugin routes the requests to the [Elasticsearch rollup search endpoint](https://www.elastic.co/guide/en/elasticsearch/reference/current/rollup-search.html), which searches the special document structure within rollup indices. The visualization options available to users are based on the capabilities of the rollup index pattern they're visualizing. -Routing to the Elasticsearch rollup search endpoint is done by creating an extension point in Courier, effectively allowing multiple "search strategies" to be registered. A [rollup search strategy](public/search/register.js) is registered by this plugin that queries [this plugin's rollup search endpoint](server/routes/api/search.js). +Routing to the Elasticsearch rollup search endpoint is done by creating an extension point in Courier, effectively allowing multiple "search strategies" to be registered. A [rollup search strategy](../../../plugins/rollup/public/search/register.js) is registered by this plugin that queries [this plugin's rollup search endpoint](server/routes/api/search.js). -Limiting visualization editor options is done by [registering configs](public/visualize/index.js) to various vis extension points. These configs use information stored on the rollup index pattern to limit: +Limiting visualization editor options is done by [registering configs](../../../plugins/rollup/public/visualize/index.js) to various vis extension points. These configs use information stored on the rollup index pattern to limit: * Available aggregation types * Available fields for a particular aggregation * Default and base interval for histogram aggregation @@ -47,6 +47,6 @@ Limiting visualization editor options is done by [registering configs](public/vi In Index Management, similar to system indices, rollup indices are hidden by default. A toggle is provided to show rollup indices and add a badge to the table rows. This is done by using Index Management's extension points. -The toggle and badge are registered on client-side [here](public/extend_index_management/index.js). +The toggle and badge are registered on client-side [here](../../../plugins/rollup/public/extend_index_management/index.js). Additional data needed to filter rollup indices in Index Management is provided with a [data enricher](rollup_data_enricher.js). diff --git a/x-pack/legacy/plugins/rollup/__jest__/client_integration/helpers/job_list.helpers.js b/x-pack/legacy/plugins/rollup/__jest__/client_integration/helpers/job_list.helpers.js deleted file mode 100644 index bcad8c29c87c0..0000000000000 --- a/x-pack/legacy/plugins/rollup/__jest__/client_integration/helpers/job_list.helpers.js +++ /dev/null @@ -1,24 +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 { registerTestBed } from '../../../../../../test_utils'; -import { registerRouter } from '../../../public/crud_app/services'; -import { createRollupJobsStore } from '../../../public/crud_app/store'; -import { JobList } from '../../../public/crud_app/sections/job_list'; - -import { wrapComponent } from './setup_context'; - -const testBedConfig = { - store: createRollupJobsStore, - memoryRouter: { - onRouter: router => { - // register our react memory router - registerRouter(router); - }, - }, -}; - -export const setup = registerTestBed(wrapComponent(JobList), testBedConfig); diff --git a/x-pack/legacy/plugins/rollup/__jest__/client_integration/job_create_review.test.js b/x-pack/legacy/plugins/rollup/__jest__/client_integration/job_create_review.test.js deleted file mode 100644 index 0fa9509368d3f..0000000000000 --- a/x-pack/legacy/plugins/rollup/__jest__/client_integration/job_create_review.test.js +++ /dev/null @@ -1,175 +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 { pageHelpers, mockHttpRequest } from './helpers'; -import { first } from 'lodash'; -import { setHttp } from '../../public/crud_app/services'; -import { JOBS } from './helpers/constants'; - -jest.mock('ui/new_platform'); - -jest.mock('lodash/function/debounce', () => fn => fn); - -const { setup } = pageHelpers.jobCreate; - -describe('Create Rollup Job, step 6: Review', () => { - let find; - let exists; - let actions; - let getEuiStepsHorizontalActive; - let goToStep; - let table; - let form; - let npStart; - - beforeAll(() => { - npStart = require('ui/new_platform').npStart; // eslint-disable-line - setHttp(npStart.core.http); - }); - - beforeEach(() => { - // Set "default" mock responses by not providing any arguments - mockHttpRequest(npStart.core.http); - ({ find, exists, actions, getEuiStepsHorizontalActive, goToStep, table, form } = setup()); - }); - - afterEach(() => { - npStart.core.http.get.mockClear(); - npStart.core.http.post.mockClear(); - npStart.core.http.put.mockClear(); - }); - - describe('layout', () => { - beforeEach(async () => { - await goToStep(6); - }); - - it('should have the horizontal step active on "Review"', () => { - expect(getEuiStepsHorizontalActive()).toContain('Review'); - }); - - it('should have the title set to "Review"', () => { - expect(exists('rollupJobCreateReviewTitle')).toBe(true); - }); - - it('should have the "next" and "save" button visible', () => { - expect(exists('rollupJobBackButton')).toBe(true); - expect(exists('rollupJobNextButton')).toBe(false); - expect(exists('rollupJobSaveButton')).toBe(true); - }); - - it('should go to the "Metrics" step when clicking the back button', async () => { - actions.clickPreviousStep(); - expect(getEuiStepsHorizontalActive()).toContain('Metrics'); - }); - }); - - describe('tabs', () => { - const getTabsText = () => find('stepReviewTab').map(tab => tab.text()); - const selectFirstField = step => { - find('rollupJobShowFieldChooserButton').simulate('click'); - - // Select the first term field - table - .getMetaData(`rollupJob${step}FieldChooser-table`) - .rows[0].reactWrapper.simulate('click'); - }; - - it('should have a "Summary" & "Request" tabs to review the Job', async () => { - await goToStep(6); - expect(getTabsText()).toEqual(['Summary', 'Request']); - }); - - it('should have a "Summary", "Terms" & "Request" tab if a term aggregation was added', async () => { - mockHttpRequest(npStart.core.http, { indxPatternVldtResp: { numericFields: ['my-field'] } }); - await goToStep(3); - selectFirstField('Terms'); - - actions.clickNextStep(); // go to step 4 - actions.clickNextStep(); // go to step 5 - actions.clickNextStep(); // go to review - - expect(getTabsText()).toEqual(['Summary', 'Terms', 'Request']); - }); - - it('should have a "Summary", "Histogram" & "Request" tab if a histogram field was added', async () => { - mockHttpRequest(npStart.core.http, { indxPatternVldtResp: { numericFields: ['a-field'] } }); - await goToStep(4); - selectFirstField('Histogram'); - form.setInputValue('rollupJobCreateHistogramInterval', 3); // set an interval - - actions.clickNextStep(); // go to step 5 - actions.clickNextStep(); // go to review - - expect(getTabsText()).toEqual(['Summary', 'Histogram', 'Request']); - }); - - it('should have a "Summary", "Metrics" & "Request" tab if a histogram field was added', async () => { - mockHttpRequest(npStart.core.http, { - indxPatternVldtResp: { - numericFields: ['a-field'], - dateFields: ['b-field'], - }, - }); - await goToStep(5); - selectFirstField('Metrics'); - form.selectCheckBox('rollupJobMetricsCheckbox-avg'); // select a metric - - actions.clickNextStep(); // go to review - - expect(getTabsText()).toEqual(['Summary', 'Metrics', 'Request']); - }); - }); - - describe('save()', () => { - const jobCreateApiPath = '/api/rollup/create'; - const jobStartApiPath = '/api/rollup/start'; - - describe('without starting job after creation', () => { - it('should call the "create" Api server endpoint', async () => { - mockHttpRequest(npStart.core.http, { - createdJob: first(JOBS.jobs), - }); - - await goToStep(6); - - expect(npStart.core.http.put).not.toHaveBeenCalledWith(jobCreateApiPath); // make sure it hasn't been called - expect(npStart.core.http.get).not.toHaveBeenCalledWith(jobStartApiPath); // make sure it hasn't been called - - actions.clickSave(); - // Given the following anti-jitter sleep x-pack/legacy/plugins/rollup/public/crud_app/store/actions/create_job.js - // we add a longer sleep here :( - await new Promise(res => setTimeout(res, 750)); - - expect(npStart.core.http.put).toHaveBeenCalledWith(jobCreateApiPath, expect.anything()); // It has been called! - expect(npStart.core.http.get).not.toHaveBeenCalledWith(jobStartApiPath); // It has still not been called! - }); - }); - - describe('with starting job after creation', () => { - it('should call the "create" and "start" Api server endpoints', async () => { - mockHttpRequest(npStart.core.http, { - createdJob: first(JOBS.jobs), - }); - - await goToStep(6); - - find('rollupJobToggleJobStartAfterCreation').simulate('change', { - target: { checked: true }, - }); - - expect(npStart.core.http.post).not.toHaveBeenCalledWith(jobStartApiPath); // make sure it hasn't been called - - actions.clickSave(); - // Given the following anti-jitter sleep x-pack/legacy/plugins/rollup/public/crud_app/store/actions/create_job.js - // we add a longer sleep here :( - await new Promise(res => setTimeout(res, 750)); - - expect(npStart.core.http.post).toHaveBeenCalledWith(jobStartApiPath, expect.anything()); // It has been called! - }); - }); - }); -}); diff --git a/x-pack/legacy/plugins/rollup/__jest__/client_integration/job_list.test.js b/x-pack/legacy/plugins/rollup/__jest__/client_integration/job_list.test.js deleted file mode 100644 index a9e474cf0b559..0000000000000 --- a/x-pack/legacy/plugins/rollup/__jest__/client_integration/job_list.test.js +++ /dev/null @@ -1,87 +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 { getRouter, setHttp } from '../../public/crud_app/services'; -import { mockHttpRequest, pageHelpers, nextTick } from './helpers'; -import { JOBS } from './helpers/constants'; - -jest.mock('ui/new_platform'); - -jest.mock('../../public/crud_app/services', () => { - const services = require.requireActual('../../public/crud_app/services'); - return { - ...services, - getRouterLinkProps: link => ({ href: link }), - }; -}); - -const { setup } = pageHelpers.jobList; - -describe('', () => { - describe('detail panel', () => { - let component; - let table; - let exists; - let npStart; - - beforeAll(() => { - npStart = require('ui/new_platform').npStart; // eslint-disable-line - setHttp(npStart.core.http); - }); - - beforeEach(async () => { - mockHttpRequest(npStart.core.http, { jobs: JOBS }); - - ({ component, exists, table } = setup()); - - await nextTick(); // We need to wait next tick for the mock server response to comes in - component.update(); - }); - - afterEach(() => { - npStart.core.http.get.mockClear(); - }); - - test('should open the detail panel when clicking on a job in the table', () => { - const { rows } = table.getMetaData('rollupJobsListTable'); - const button = rows[0].columns[1].reactWrapper.find('button'); - - expect(exists('rollupJobDetailFlyout')).toBe(false); // make sure it is not shown - - button.simulate('click'); - - expect(exists('rollupJobDetailFlyout')).toBe(true); - }); - - test('should add the Job id to the route query params when opening the detail panel', () => { - const { rows } = table.getMetaData('rollupJobsListTable'); - const button = rows[0].columns[1].reactWrapper.find('button'); - - expect(getRouter().history.location.search).toEqual(''); - - button.simulate('click'); - - const { - jobs: [ - { - config: { id: jobId }, - }, - ], - } = JOBS; - expect(getRouter().history.location.search).toEqual(`?job=${jobId}`); - }); - - test('should open the detail panel whenever a job id is added to the query params', () => { - expect(exists('rollupJobDetailFlyout')).toBe(false); // make sure it is not shown - - getRouter().history.replace({ search: `?job=bar` }); - - component.update(); - - expect(exists('rollupJobDetailFlyout')).toBe(true); - }); - }); -}); diff --git a/x-pack/legacy/plugins/rollup/__jest__/client_integration/job_list_clone.test.js b/x-pack/legacy/plugins/rollup/__jest__/client_integration/job_list_clone.test.js deleted file mode 100644 index 8a36af83def4c..0000000000000 --- a/x-pack/legacy/plugins/rollup/__jest__/client_integration/job_list_clone.test.js +++ /dev/null @@ -1,65 +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 { mockHttpRequest, pageHelpers, nextTick } from './helpers'; -import { JOB_TO_CLONE, JOB_CLONE_INDEX_PATTERN_CHECK } from './helpers/constants'; -import { getRouter } from '../../public/crud_app/services/routing'; -import { setHttp } from '../../public/crud_app/services'; -import { CRUD_APP_BASE_PATH } from '../../public/crud_app/constants'; - -jest.mock('ui/new_platform'); - -jest.mock('lodash/function/debounce', () => fn => fn); - -const { setup } = pageHelpers.jobList; - -describe('Smoke test cloning an existing rollup job from job list', () => { - let table; - let find; - let component; - let exists; - let npStart; - - beforeAll(() => { - npStart = require('ui/new_platform').npStart; // eslint-disable-line - setHttp(npStart.core.http); - }); - - beforeEach(async () => { - mockHttpRequest(npStart.core.http, { - jobs: JOB_TO_CLONE, - indxPatternVldtResp: JOB_CLONE_INDEX_PATTERN_CHECK, - }); - - ({ find, exists, table, component } = setup()); - - await nextTick(); // We need to wait next tick for the mock server response to comes in - component.update(); - }); - - afterEach(() => { - npStart.core.http.get.mockClear(); - }); - - it('should navigate to create view with default values set', async () => { - const router = getRouter(); - const { rows } = table.getMetaData('rollupJobsListTable'); - const button = rows[0].columns[1].reactWrapper.find('button'); - - expect(exists('rollupJobDetailFlyout')).toBe(false); // make sure it is not shown - - button.simulate('click'); - - expect(exists('rollupJobDetailFlyout')).toBe(true); - expect(exists('jobActionMenuButton')).toBe(true); - - find('jobActionMenuButton').simulate('click'); - - expect(router.history.location.pathname).not.toBe(`${CRUD_APP_BASE_PATH}/create`); - find('jobCloneActionContextMenu').simulate('click'); - expect(router.history.location.pathname).toBe(`${CRUD_APP_BASE_PATH}/create`); - }); -}); diff --git a/x-pack/legacy/plugins/rollup/common/index.ts b/x-pack/legacy/plugins/rollup/common/index.ts index 4229803462203..526af055a3ef6 100644 --- a/x-pack/legacy/plugins/rollup/common/index.ts +++ b/x-pack/legacy/plugins/rollup/common/index.ts @@ -16,24 +16,4 @@ export const PLUGIN = { }, }; -export const CONFIG_ROLLUPS = 'rollups:enableIndexPatterns'; - -export const API_BASE_PATH = '/api/rollup'; - -export { - UIM_APP_NAME, - UIM_APP_LOAD, - UIM_JOB_CREATE, - UIM_JOB_DELETE, - UIM_JOB_DELETE_MANY, - UIM_JOB_START, - UIM_JOB_START_MANY, - UIM_JOB_STOP, - UIM_JOB_STOP_MANY, - UIM_SHOW_DETAILS_CLICK, - UIM_DETAIL_PANEL_SUMMARY_TAB_CLICK, - UIM_DETAIL_PANEL_TERMS_TAB_CLICK, - UIM_DETAIL_PANEL_HISTOGRAM_TAB_CLICK, - UIM_DETAIL_PANEL_METRICS_TAB_CLICK, - UIM_DETAIL_PANEL_JSON_TAB_CLICK, -} from './ui_metric'; +export * from '../../../../plugins/rollup/common'; diff --git a/x-pack/legacy/plugins/rollup/index.ts b/x-pack/legacy/plugins/rollup/index.ts index 2c8363cc397f4..f33ae7cfee0a2 100644 --- a/x-pack/legacy/plugins/rollup/index.ts +++ b/x-pack/legacy/plugins/rollup/index.ts @@ -4,43 +4,19 @@ * you may not use this file except in compliance with the Elastic License. */ -import { resolve } from 'path'; -import { i18n } from '@kbn/i18n'; import { PluginInitializerContext } from 'src/core/server'; import { RollupSetup } from '../../../plugins/rollup/server'; -import { PLUGIN, CONFIG_ROLLUPS } from './common'; +import { PLUGIN } from './common'; import { plugin } from './server'; export function rollup(kibana: any) { return new kibana.Plugin({ id: PLUGIN.ID, configPrefix: 'xpack.rollup', - publicDir: resolve(__dirname, 'public'), require: ['kibana', 'elasticsearch', 'xpack_main'], - uiExports: { - styleSheetPaths: resolve(__dirname, 'public/index.scss'), - managementSections: ['plugins/rollup/legacy'], - uiSettingDefaults: { - [CONFIG_ROLLUPS]: { - name: i18n.translate('xpack.rollupJobs.rollupIndexPatternsTitle', { - defaultMessage: 'Enable rollup index patterns', - }), - value: true, - description: i18n.translate('xpack.rollupJobs.rollupIndexPatternsDescription', { - defaultMessage: `Enable the creation of index patterns which capture rollup indices, - which in turn enable visualizations based on rollup data. Refresh - the page to apply the changes.`, - }), - category: ['rollups'], - }, - }, - indexManagement: ['plugins/rollup/legacy'], - visualize: ['plugins/rollup/legacy'], - search: ['plugins/rollup/legacy'], - }, init(server: any) { const { core: coreSetup, plugins } = server.newPlatform.setup; - const { usageCollection, metrics, indexManagement } = plugins; + const { usageCollection, visTypeTimeseries, indexManagement } = plugins; const rollupSetup = (plugins.rollup as unknown) as RollupSetup; @@ -53,7 +29,7 @@ export function rollup(kibana: any) { rollupPluginInstance.setup(coreSetup, { usageCollection, - metrics, + visTypeTimeseries, indexManagement, __LEGACY: { plugins: { diff --git a/x-pack/legacy/plugins/rollup/kibana.json b/x-pack/legacy/plugins/rollup/kibana.json index 3df8bd7c187d5..78458c9218be3 100644 --- a/x-pack/legacy/plugins/rollup/kibana.json +++ b/x-pack/legacy/plugins/rollup/kibana.json @@ -4,7 +4,7 @@ "requiredPlugins": [ "home", "index_management", - "metrics", + "visTypeTimeseries", "indexPatternManagement" ], "optionalPlugins": [ diff --git a/x-pack/legacy/plugins/rollup/public/crud_app/sections/job_create/steps_config/index.js b/x-pack/legacy/plugins/rollup/public/crud_app/sections/job_create/steps_config/index.js deleted file mode 100644 index e1efcfdd24627..0000000000000 --- a/x-pack/legacy/plugins/rollup/public/crud_app/sections/job_create/steps_config/index.js +++ /dev/null @@ -1,188 +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 cloneDeep from 'lodash/lang/cloneDeep'; -import get from 'lodash/object/get'; -import pick from 'lodash/object/pick'; - -import { WEEK } from '../../../../../../../../../src/plugins/es_ui_shared/public'; - -import { validateId } from './validate_id'; -import { validateIndexPattern } from './validate_index_pattern'; -import { validateRollupIndex } from './validate_rollup_index'; -import { validateRollupCron } from './validate_rollup_cron'; -import { validateRollupPageSize } from './validate_rollup_page_size'; -import { validateRollupDelay } from './validate_rollup_delay'; -import { validateDateHistogramField } from './validate_date_histogram_field'; -import { validateDateHistogramInterval } from './validate_date_histogram_interval'; -import { validateHistogramInterval } from './validate_histogram_interval'; -import { validateMetrics } from './validate_metrics'; - -export const STEP_LOGISTICS = 'STEP_LOGISTICS'; -export const STEP_DATE_HISTOGRAM = 'STEP_DATE_HISTOGRAM'; -export const STEP_TERMS = 'STEP_TERMS'; -export const STEP_HISTOGRAM = 'STEP_HISTOGRAM'; -export const STEP_METRICS = 'STEP_METRICS'; -export const STEP_REVIEW = 'STEP_REVIEW'; - -export const stepIds = [ - STEP_LOGISTICS, - STEP_DATE_HISTOGRAM, - STEP_TERMS, - STEP_HISTOGRAM, - STEP_METRICS, - STEP_REVIEW, -]; - -/** - * Map a specific wizard step to two functions: - * 1. getDefaultFields: (overrides) => object - * 2. fieldValidations - * - * See x-pack/legacy/plugins/rollup/public/crud_app/services/jobs.js for more information on override's shape - */ -export const stepIdToStepConfigMap = { - [STEP_LOGISTICS]: { - getDefaultFields: (overrides = {}) => { - // We don't display the simple editor if there are overrides for the rollup's cron - const isAdvancedCronVisible = !!overrides.rollupCron; - - // The best page size boils down to how much memory the user has, e.g. how many buckets should - // be accumulated at one time. 1000 is probably a safe size without being too small. - const rollupPageSize = get(overrides, ['json', 'config', 'page_size'], 1000); - const clonedRollupId = overrides.id || undefined; - const id = overrides.id ? `${overrides.id}-copy` : ''; - - const defaults = { - indexPattern: '', - rollupIndex: '', - // Every week on Saturday, at 00:00:00 - rollupCron: '0 0 0 ? * 7', - simpleRollupCron: '0 0 0 ? * 7', - rollupPageSize, - // Though the API doesn't require a delay, in many real-world cases, servers will go down for - // a few hours as they're being restarted. A delay of 1d would allow them that period to reboot - // and the "expense" is pretty negligible in most cases: 1 day of extra non-rolled-up data. - rollupDelay: '1d', - cronFrequency: WEEK, - fieldToPreferredValueMap: {}, - }; - - return { - ...defaults, - ...pick(overrides, Object.keys(defaults)), - id, - isAdvancedCronVisible, - rollupPageSize, - clonedRollupId, - }; - }, - fieldsValidator: fields => { - const { - id, - indexPattern, - rollupIndex, - rollupCron, - rollupPageSize, - rollupDelay, - clonedRollupId, - } = fields; - return { - id: validateId(id, clonedRollupId), - indexPattern: validateIndexPattern(indexPattern, rollupIndex), - rollupIndex: validateRollupIndex(rollupIndex, indexPattern), - rollupCron: validateRollupCron(rollupCron), - rollupPageSize: validateRollupPageSize(rollupPageSize), - rollupDelay: validateRollupDelay(rollupDelay), - }; - }, - }, - [STEP_DATE_HISTOGRAM]: { - getDefaultFields: (overrides = {}) => { - const defaults = { - dateHistogramField: null, - dateHistogramInterval: null, - dateHistogramTimeZone: 'UTC', - }; - - return { - ...defaults, - ...pick(overrides, Object.keys(defaults)), - }; - }, - fieldsValidator: fields => { - const { dateHistogramField, dateHistogramInterval } = fields; - - return { - dateHistogramField: validateDateHistogramField(dateHistogramField), - dateHistogramInterval: validateDateHistogramInterval(dateHistogramInterval), - }; - }, - }, - [STEP_TERMS]: { - getDefaultFields: (overrides = {}) => { - return { - terms: [], - ...pick(overrides, ['terms']), - }; - }, - }, - [STEP_HISTOGRAM]: { - getDefaultFields: overrides => { - return { - histogram: [], - histogramInterval: undefined, - ...pick(overrides, ['histogram', 'histogramInterval']), - }; - }, - fieldsValidator: fields => { - const { histogram, histogramInterval } = fields; - - return { - histogramInterval: validateHistogramInterval(histogram, histogramInterval), - }; - }, - }, - [STEP_METRICS]: { - getDefaultFields: (overrides = {}) => { - return { - metrics: [], - ...pick(overrides, ['metrics']), - }; - }, - fieldsValidator: fields => { - const { metrics } = fields; - - return { - metrics: validateMetrics(metrics), - }; - }, - }, - [STEP_REVIEW]: { - getDefaultFields: () => ({}), - }, -}; - -export function getAffectedStepsFields(fields, stepsFields) { - const { indexPattern } = fields; - - const affectedStepsFields = cloneDeep(stepsFields); - - // A new index pattern means we have to clear all of the fields which depend upon it. - if (indexPattern) { - affectedStepsFields[STEP_DATE_HISTOGRAM].dateHistogramField = undefined; - affectedStepsFields[STEP_TERMS].terms = []; - affectedStepsFields[STEP_HISTOGRAM].histogram = []; - affectedStepsFields[STEP_METRICS].metrics = []; - } - - return affectedStepsFields; -} - -export function hasErrors(fieldErrors) { - const errorValues = Object.values(fieldErrors); - return errorValues.some(error => error !== undefined); -} diff --git a/x-pack/legacy/plugins/rollup/public/crud_app/sections/job_list/job_list.test.js b/x-pack/legacy/plugins/rollup/public/crud_app/sections/job_list/job_list.test.js deleted file mode 100644 index 725789fc584de..0000000000000 --- a/x-pack/legacy/plugins/rollup/public/crud_app/sections/job_list/job_list.test.js +++ /dev/null @@ -1,96 +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 from 'react'; -import { registerTestBed } from '../../../../../../../test_utils'; -import { rollupJobsStore } from '../../store'; -import { JobList } from './job_list'; - -import { KibanaContextProvider } from '../../../../../../../../src/plugins/kibana_react/public'; -import { coreMock } from '../../../../../../../../src/core/public/mocks'; -const startMock = coreMock.createStart(); - -jest.mock('ui/new_platform'); - -jest.mock('../../services', () => { - const services = require.requireActual('../../services'); - return { - ...services, - getRouterLinkProps: link => ({ href: link }), - }; -}); - -const defaultProps = { - history: { location: {} }, - loadJobs: () => {}, - refreshJobs: () => {}, - openDetailPanel: () => {}, - hasJobs: false, - isLoading: false, -}; - -const services = { - setBreadcrumbs: startMock.chrome.setBreadcrumbs, -}; -const Component = props => ( - - - -); - -const initTestBed = registerTestBed(Component, { defaultProps, store: rollupJobsStore }); - -describe('', () => { - it('should render empty prompt when loading is complete and there are no jobs', () => { - const { exists } = initTestBed(); - - expect(exists('jobListEmptyPrompt')).toBeTruthy(); - }); - - it('should display a loading message when loading the jobs', () => { - const { component, exists } = initTestBed({ isLoading: true }); - - expect(exists('jobListLoading')).toBeTruthy(); - expect(component.find('JobTable').length).toBeFalsy(); - }); - - it('should display the when there are jobs', () => { - const { component, exists } = initTestBed({ hasJobs: true }); - - expect(exists('jobListLoading')).toBeFalsy(); - expect(component.find('JobTable').length).toBeTruthy(); - }); - - describe('when there is an API error', () => { - const { exists, find } = initTestBed({ - jobLoadError: { - status: 400, - body: { statusCode: 400, error: 'Houston we got a problem.' }, - }, - }); - - it('should display a callout with the status and the message', () => { - expect(exists('jobListError')).toBeTruthy(); - expect( - find('jobListError') - .find('EuiText') - .text() - ).toEqual('400 Houston we got a problem.'); - }); - }); - - describe('when the user does not have the permission to access it', () => { - const { exists } = initTestBed({ jobLoadError: { status: 403 } }); - - it('should render a callout message', () => { - expect(exists('jobListNoPermission')).toBeTruthy(); - }); - - it('should display the page header', () => { - expect(exists('jobListPageHeader')).toBeTruthy(); - }); - }); -}); diff --git a/x-pack/legacy/plugins/rollup/public/crud_app/services/index.js b/x-pack/legacy/plugins/rollup/public/crud_app/services/index.js deleted file mode 100644 index 790770b9b6a9f..0000000000000 --- a/x-pack/legacy/plugins/rollup/public/crud_app/services/index.js +++ /dev/null @@ -1,50 +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. - */ - -export { createJob, deleteJobs, loadJobs, startJobs, stopJobs, validateIndexPattern } from './api'; - -export { showApiError, showApiWarning } from './api_errors'; - -export { listBreadcrumb, createBreadcrumb } from './breadcrumbs'; - -export { - setEsBaseAndXPackBase, - getLogisticalDetailsUrl, - getDateHistogramDetailsUrl, - getDateHistogramAggregationUrl, - getTermsDetailsUrl, - getHistogramDetailsUrl, - getMetricsDetailsUrl, - getCronUrl, -} from './documentation_links'; - -export { filterItems } from './filter_items'; - -export { flattenPanelTree } from './flatten_panel_tree'; - -export { formatFields } from './format_fields'; - -export { setHttp, getHttp } from './http_provider'; - -export { serializeJob, deserializeJob, deserializeJobs } from './jobs'; - -export { createNoticeableDelay } from './noticeable_delay'; - -export { extractQueryParams } from './query_params'; - -export { - setUserHasLeftApp, - getUserHasLeftApp, - registerRouter, - getRouter, - getRouterLinkProps, -} from './routing'; - -export { sortTable } from './sort_table'; - -export { retypeMetrics } from './retype_metrics'; - -export { trackUiMetric, METRIC_TYPE } from './track_ui_metric'; diff --git a/x-pack/legacy/plugins/rollup/public/crud_app/services/track_ui_metric.js b/x-pack/legacy/plugins/rollup/public/crud_app/services/track_ui_metric.js deleted file mode 100644 index 69a5995386bd7..0000000000000 --- a/x-pack/legacy/plugins/rollup/public/crud_app/services/track_ui_metric.js +++ /dev/null @@ -1,28 +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 { - createUiStatsReporter, - METRIC_TYPE, -} from '../../../../../../../src/legacy/core_plugins/ui_metric/public'; -import { UIM_APP_NAME } from '../../../common'; - -export const trackUiMetric = createUiStatsReporter(UIM_APP_NAME); -export { METRIC_TYPE }; - -/** - * Transparently return provided request Promise, while allowing us to track - * a successful completion of the request. - */ -export function trackUserRequest(request, actionType) { - // Only track successful actions. - return request.then(response => { - trackUiMetric(METRIC_TYPE.LOADED, actionType); - // We return the response immediately without waiting for the tracking request to resolve, - // to avoid adding additional latency. - return response; - }); -} diff --git a/x-pack/legacy/plugins/rollup/public/index.scss b/x-pack/legacy/plugins/rollup/public/index.scss deleted file mode 100644 index 0ad0eac50f7b9..0000000000000 --- a/x-pack/legacy/plugins/rollup/public/index.scss +++ /dev/null @@ -1,13 +0,0 @@ -// Import the EUI global scope so we can use EUI constants -@import 'src/legacy/ui/public/styles/_styling_constants'; - -// Index management plugin styles - -// Prefix all styles with "rollup" to avoid conflicts. -// Examples -// rollupChart -// rollupChart__legend -// rollupChart__legend--small -// rollupChart__legend-isLoading - -@import 'crud_app/_crud_app'; diff --git a/x-pack/legacy/plugins/rollup/public/kibana_services.ts b/x-pack/legacy/plugins/rollup/public/kibana_services.ts deleted file mode 100644 index 335eeb90282ca..0000000000000 --- a/x-pack/legacy/plugins/rollup/public/kibana_services.ts +++ /dev/null @@ -1,30 +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 { NotificationsStart, FatalErrorsSetup } from 'src/core/public'; - -let notifications: NotificationsStart | null = null; -let fatalErrors: FatalErrorsSetup | null = null; - -export function getNotifications() { - if (!notifications) { - throw new Error('Rollup notifications is not defined'); - } - return notifications; -} -export function setNotifications(newNotifications: NotificationsStart) { - notifications = newNotifications; -} - -export function getFatalErrors() { - if (!fatalErrors) { - throw new Error('Rollup fatalErrors is not defined'); - } - return fatalErrors; -} -export function setFatalErrors(newFatalErrors: FatalErrorsSetup) { - fatalErrors = newFatalErrors; -} diff --git a/x-pack/legacy/plugins/rollup/public/legacy.ts b/x-pack/legacy/plugins/rollup/public/legacy.ts deleted file mode 100644 index 83945110c2c76..0000000000000 --- a/x-pack/legacy/plugins/rollup/public/legacy.ts +++ /dev/null @@ -1,13 +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 { npSetup, npStart } from 'ui/new_platform'; -import { RollupPlugin } from './plugin'; - -const plugin = new RollupPlugin(); - -export const setup = plugin.setup(npSetup.core, npSetup.plugins); -export const start = plugin.start(npStart.core, npStart.plugins); diff --git a/x-pack/legacy/plugins/rollup/public/legacy_imports.ts b/x-pack/legacy/plugins/rollup/public/legacy_imports.ts deleted file mode 100644 index e82a41f60b1ca..0000000000000 --- a/x-pack/legacy/plugins/rollup/public/legacy_imports.ts +++ /dev/null @@ -1,7 +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. - */ - -export { PluginsStart } from 'ui/new_platform/new_platform'; diff --git a/x-pack/legacy/plugins/rollup/public/plugin.ts b/x-pack/legacy/plugins/rollup/public/plugin.ts deleted file mode 100644 index 5782e88c3448b..0000000000000 --- a/x-pack/legacy/plugins/rollup/public/plugin.ts +++ /dev/null @@ -1,109 +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 { i18n } from '@kbn/i18n'; -import { CoreSetup, CoreStart, Plugin } from 'kibana/public'; -import { PluginsStart } from './legacy_imports'; -import { rollupBadgeExtension, rollupToggleExtension } from './extend_index_management'; -// @ts-ignore -import { RollupIndexPatternCreationConfig } from './index_pattern_creation/rollup_index_pattern_creation_config'; -// @ts-ignore -import { RollupIndexPatternListConfig } from './index_pattern_list/rollup_index_pattern_list_config'; -// @ts-ignore -import { initAggTypeFilter } from './visualize/agg_type_filter'; -// @ts-ignore -import { initAggTypeFieldFilter } from './visualize/agg_type_field_filter'; -import { CONFIG_ROLLUPS } from '../common'; -import { - FeatureCatalogueCategory, - HomePublicPluginSetup, -} from '../../../../../src/plugins/home/public'; -// @ts-ignore -import { CRUD_APP_BASE_PATH } from './crud_app/constants'; -import { ManagementSetup } from '../../../../../src/plugins/management/public'; -import { IndexMgmtSetup } from '../../../../plugins/index_management/public'; -import { IndexPatternManagementSetup } from '../../../../../src/plugins/index_pattern_management/public'; -import { search } from '../../../../../src/plugins/data/public'; -// @ts-ignore -import { setEsBaseAndXPackBase, setHttp } from './crud_app/services'; -import { setNotifications, setFatalErrors } from './kibana_services'; -import { renderApp } from './application'; - -export interface RollupPluginSetupDependencies { - home?: HomePublicPluginSetup; - management: ManagementSetup; - indexManagement?: IndexMgmtSetup; - indexPatternManagement: IndexPatternManagementSetup; -} - -export class RollupPlugin implements Plugin { - setup( - core: CoreSetup, - { home, management, indexManagement, indexPatternManagement }: RollupPluginSetupDependencies - ) { - setFatalErrors(core.fatalErrors); - - if (indexManagement) { - indexManagement.extensionsService.addBadge(rollupBadgeExtension); - indexManagement.extensionsService.addToggle(rollupToggleExtension); - } - - const isRollupIndexPatternsEnabled = core.uiSettings.get(CONFIG_ROLLUPS); - - if (isRollupIndexPatternsEnabled) { - indexPatternManagement.creation.addCreationConfig(RollupIndexPatternCreationConfig); - indexPatternManagement.list.addListConfig(RollupIndexPatternListConfig); - } - - if (home) { - home.featureCatalogue.register({ - id: 'rollup_jobs', - title: 'Rollups', - description: i18n.translate('xpack.rollupJobs.featureCatalogueDescription', { - defaultMessage: - 'Summarize and store historical data in a smaller index for future analysis.', - }), - icon: 'indexRollupApp', - path: `#${CRUD_APP_BASE_PATH}/job_list`, - showOnHomePage: true, - category: FeatureCatalogueCategory.ADMIN, - }); - } - - const esSection = management.sections.getSection('elasticsearch'); - if (esSection) { - esSection.registerApp({ - id: 'rollup_jobs', - title: i18n.translate('xpack.rollupJobs.appTitle', { defaultMessage: 'Rollup Jobs' }), - order: 3, - mount(params) { - params.setBreadcrumbs([ - { - text: i18n.translate('xpack.rollupJobs.breadcrumbsTitle', { - defaultMessage: 'Rollup Jobs', - }), - }, - ]); - - return renderApp(core, params); - }, - }); - } - } - - start(core: CoreStart, plugins: PluginsStart) { - setHttp(core.http); - setNotifications(core.notifications); - setEsBaseAndXPackBase(core.docLinks.ELASTIC_WEBSITE_URL, core.docLinks.DOC_LINK_VERSION); - - const isRollupIndexPatternsEnabled = core.uiSettings.get(CONFIG_ROLLUPS); - - if (isRollupIndexPatternsEnabled) { - initAggTypeFilter(search.aggs.aggTypeFilters); - initAggTypeFieldFilter(plugins.data.search.__LEGACY.aggTypeFieldFilters); - } - } -} diff --git a/x-pack/legacy/plugins/rollup/public/search/rollup_search_strategy.ts b/x-pack/legacy/plugins/rollup/public/search/rollup_search_strategy.ts deleted file mode 100644 index 4709c0aa498f8..0000000000000 --- a/x-pack/legacy/plugins/rollup/public/search/rollup_search_strategy.ts +++ /dev/null @@ -1,104 +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 { HttpSetup } from 'src/core/public'; -import { - SearchError, - getSearchErrorType, - IIndexPattern, - SearchStrategyProvider, - SearchResponse, - SearchRequest, -} from '../../../../../../src/plugins/data/public'; - -function serializeFetchParams(searchRequests: SearchRequest[]) { - return JSON.stringify( - searchRequests.map(searchRequestWithFetchParams => { - const indexPattern = - searchRequestWithFetchParams.index.title || searchRequestWithFetchParams.index; - const { - body: { size, aggs, query: _query }, - } = searchRequestWithFetchParams; - - const query = { - size, - aggregations: aggs, - query: _query, - }; - - return { index: indexPattern, query }; - }) - ); -} - -// Rollup search always returns 0 hits, but visualizations expect search responses -// to return hits > 0, otherwise they do not render. We fake the number of hits here -// by counting the number of aggregation buckets/values returned by rollup search. -function shimHitsInFetchResponse(response: SearchResponse[]) { - return response.map(result => { - const buckets = result.aggregations - ? Object.keys(result.aggregations).reduce((allBuckets, agg) => { - return allBuckets.concat( - result.aggregations[agg].buckets || [result.aggregations[agg].value] || [] - ); - }, []) - : []; - return buckets && buckets.length - ? { - ...result, - hits: { - ...result.hits, - total: buckets.length, - }, - } - : result; - }); -} - -export const getRollupSearchStrategy = (fetch: HttpSetup['fetch']): SearchStrategyProvider => ({ - id: 'rollup', - - search: ({ searchRequests }) => { - // Serialize the fetch params into a format suitable for the body of an ES query. - const serializedFetchParams = serializeFetchParams(searchRequests); - - const controller = new AbortController(); - const promise = fetch('../api/rollup/search', { - signal: controller.signal, - method: 'POST', - body: serializedFetchParams, - }); - - return { - searching: promise.then(shimHitsInFetchResponse).catch(error => { - const { - body: { statusCode, error: title, message }, - res: { url }, - } = error; - - // Format fetch error as a SearchError. - const searchError = new SearchError({ - status: statusCode, - title, - message: `Rollup search error: ${message}`, - path: url, - type: getSearchErrorType({ message }) || '', - }); - - return Promise.reject(searchError); - }), - abort: () => controller.abort(), - }; - }, - - isViable: (indexPattern: IIndexPattern) => { - if (!indexPattern) { - return false; - } - - return indexPattern.type === 'rollup'; - }, -}); diff --git a/x-pack/legacy/plugins/rollup/server/plugin.ts b/x-pack/legacy/plugins/rollup/server/plugin.ts index 090cb8a47377a..05c22b030fff9 100644 --- a/x-pack/legacy/plugins/rollup/server/plugin.ts +++ b/x-pack/legacy/plugins/rollup/server/plugin.ts @@ -9,7 +9,7 @@ import { i18n } from '@kbn/i18n'; import { UsageCollectionSetup } from 'src/plugins/usage_collection/server'; import { VisTypeTimeseriesSetup } from 'src/plugins/vis_type_timeseries/server'; -import { IndexMgmtSetup } from '../../../../plugins/index_management/server'; +import { IndexManagementPluginSetup } from '../../../../plugins/index_management/server'; import { registerLicenseChecker } from '../../../server/lib/register_license_checker'; import { PLUGIN } from '../common'; import { ServerShim, RouteDependencies } from './types'; @@ -38,13 +38,13 @@ export class RollupsServerPlugin implements Plugin { { __LEGACY: serverShim, usageCollection, - metrics, + visTypeTimeseries, indexManagement, }: { __LEGACY: ServerShim; usageCollection?: UsageCollectionSetup; - metrics?: VisTypeTimeseriesSetup; - indexManagement?: IndexMgmtSetup; + visTypeTimeseries?: VisTypeTimeseriesSetup; + indexManagement?: IndexManagementPluginSetup; } ) { const elasticsearch = await elasticsearchService.adminClient; @@ -83,8 +83,8 @@ export class RollupsServerPlugin implements Plugin { indexManagement.indexDataEnricher.add(rollupDataEnricher); } - if (metrics) { - const { addSearchStrategy } = metrics; + if (visTypeTimeseries) { + const { addSearchStrategy } = visTypeTimeseries; registerRollupSearchStrategy(routeDependencies, addSearchStrategy); } } diff --git a/x-pack/legacy/plugins/siem/.gitattributes b/x-pack/legacy/plugins/siem/.gitattributes index f40e829b65453..a4071d39e63c0 100644 --- a/x-pack/legacy/plugins/siem/.gitattributes +++ b/x-pack/legacy/plugins/siem/.gitattributes @@ -1,6 +1,5 @@ # Auto-collapse generated files in GitHub # https://help.github.com/en/articles/customizing-how-changed-files-appear-on-github x-pack/legacy/plugins/siem/public/graphql/types.ts linguist-generated=true -x-pack/legacy/plugins/siem/server/graphql/types.ts linguist-generated=true x-pack/legacy/plugins/siem/public/graphql/introspection.json linguist-generated=true diff --git a/x-pack/legacy/plugins/siem/common/constants.ts b/x-pack/legacy/plugins/siem/common/constants.ts deleted file mode 100644 index 22f1b3beffa35..0000000000000 --- a/x-pack/legacy/plugins/siem/common/constants.ts +++ /dev/null @@ -1,113 +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. - */ - -export const APP_ID = 'siem'; -export const APP_NAME = 'SIEM'; -export const DEFAULT_BYTES_FORMAT = 'format:bytes:defaultPattern'; -export const DEFAULT_DATE_FORMAT = 'dateFormat'; -export const DEFAULT_DATE_FORMAT_TZ = 'dateFormat:tz'; -export const DEFAULT_DARK_MODE = 'theme:darkMode'; -export const DEFAULT_INDEX_KEY = 'siem:defaultIndex'; -export const DEFAULT_NUMBER_FORMAT = 'format:number:defaultPattern'; -export const DEFAULT_TIME_RANGE = 'timepicker:timeDefaults'; -export const DEFAULT_REFRESH_RATE_INTERVAL = 'timepicker:refreshIntervalDefaults'; -export const DEFAULT_SIEM_TIME_RANGE = 'siem:timeDefaults'; -export const DEFAULT_SIEM_REFRESH_INTERVAL = 'siem:refreshIntervalDefaults'; -export const DEFAULT_SIGNALS_INDEX = '.siem-signals'; -export const DEFAULT_MAX_SIGNALS = 100; -export const DEFAULT_SEARCH_AFTER_PAGE_SIZE = 100; -export const DEFAULT_ANOMALY_SCORE = 'siem:defaultAnomalyScore'; -export const DEFAULT_MAX_TABLE_QUERY_SIZE = 10000; -export const DEFAULT_SCALE_DATE_FORMAT = 'dateFormat:scaled'; -export const DEFAULT_FROM = 'now-24h'; -export const DEFAULT_TO = 'now'; -export const DEFAULT_INTERVAL_PAUSE = true; -export const DEFAULT_INTERVAL_TYPE = 'manual'; -export const DEFAULT_INTERVAL_VALUE = 300000; // ms -export const DEFAULT_TIMEPICKER_QUICK_RANGES = 'timepicker:quickRanges'; - -/** This Kibana Advanced Setting enables the `Security news` feed widget */ -export const ENABLE_NEWS_FEED_SETTING = 'siem:enableNewsFeed'; - -/** This Kibana Advanced Setting specifies the URL of the News feed widget */ -export const NEWS_FEED_URL_SETTING = 'siem:newsFeedUrl'; - -/** The default value for News feed widget */ -export const NEWS_FEED_URL_SETTING_DEFAULT = 'https://feeds.elastic.co/security-solution'; - -/** This Kibana Advanced Setting specifies the URLs of `IP Reputation Links`*/ -export const IP_REPUTATION_LINKS_SETTING = 'siem:ipReputationLinks'; - -/** The default value for `IP Reputation Links` */ -export const IP_REPUTATION_LINKS_SETTING_DEFAULT = `[ - { "name": "virustotal.com", "url_template": "https://www.virustotal.com/gui/search/{{ip}}" }, - { "name": "talosIntelligence.com", "url_template": "https://talosintelligence.com/reputation_center/lookup?search={{ip}}" } -]`; - -/** - * Id for the signals alerting type - */ -export const SIGNALS_ID = `${APP_ID}.signals`; - -/** - * Id for the notifications alerting type - */ -export const NOTIFICATIONS_ID = `${APP_ID}.notifications`; - -/** - * Special internal structure for tags for signals. This is used - * to filter out tags that have internal structures within them. - */ -export const INTERNAL_IDENTIFIER = '__internal'; -export const INTERNAL_RULE_ID_KEY = `${INTERNAL_IDENTIFIER}_rule_id`; -export const INTERNAL_RULE_ALERT_ID_KEY = `${INTERNAL_IDENTIFIER}_rule_alert_id`; -export const INTERNAL_IMMUTABLE_KEY = `${INTERNAL_IDENTIFIER}_immutable`; - -/** - * Detection engine routes - */ -export const DETECTION_ENGINE_URL = '/api/detection_engine'; -export const DETECTION_ENGINE_RULES_URL = `${DETECTION_ENGINE_URL}/rules`; -export const DETECTION_ENGINE_PREPACKAGED_URL = `${DETECTION_ENGINE_RULES_URL}/prepackaged`; -export const DETECTION_ENGINE_PRIVILEGES_URL = `${DETECTION_ENGINE_URL}/privileges`; -export const DETECTION_ENGINE_INDEX_URL = `${DETECTION_ENGINE_URL}/index`; -export const DETECTION_ENGINE_TAGS_URL = `${DETECTION_ENGINE_URL}/tags`; -export const DETECTION_ENGINE_RULES_STATUS_URL = `${DETECTION_ENGINE_RULES_URL}/_find_statuses`; -export const DETECTION_ENGINE_PREPACKAGED_RULES_STATUS_URL = `${DETECTION_ENGINE_RULES_URL}/prepackaged/_status`; - -export const TIMELINE_URL = '/api/timeline'; -export const TIMELINE_EXPORT_URL = `${TIMELINE_URL}/_export`; -export const TIMELINE_IMPORT_URL = `${TIMELINE_URL}/_import`; - -/** - * Default signals index key for kibana.dev.yml - */ -export const SIGNALS_INDEX_KEY = 'signalsIndex'; -export const DETECTION_ENGINE_SIGNALS_URL = `${DETECTION_ENGINE_URL}/signals`; -export const DETECTION_ENGINE_SIGNALS_STATUS_URL = `${DETECTION_ENGINE_SIGNALS_URL}/status`; -export const DETECTION_ENGINE_QUERY_SIGNALS_URL = `${DETECTION_ENGINE_SIGNALS_URL}/search`; - -/** - * Common naming convention for an unauthenticated user - */ -export const UNAUTHENTICATED_USER = 'Unauthenticated'; - -/* - Licensing requirements - */ -export const MINIMUM_ML_LICENSE = 'platinum'; - -/* - Rule notifications options -*/ -export const NOTIFICATION_SUPPORTED_ACTION_TYPES_IDS = [ - '.email', - '.slack', - '.pagerduty', - '.webhook', -]; -export const NOTIFICATION_THROTTLE_NO_ACTIONS = 'no_actions'; -export const NOTIFICATION_THROTTLE_RULE = 'rule'; diff --git a/x-pack/legacy/plugins/siem/common/detection_engine/types.ts b/x-pack/legacy/plugins/siem/common/detection_engine/types.ts deleted file mode 100644 index 39012d0b4b683..0000000000000 --- a/x-pack/legacy/plugins/siem/common/detection_engine/types.ts +++ /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 * as t from 'io-ts'; - -import { AlertAction } from '../../../../../plugins/alerting/common'; - -export type RuleAlertAction = Omit & { - action_type_id: string; -}; - -export const RuleTypeSchema = t.keyof({ - query: null, - saved_query: null, - machine_learning: null, -}); -export type RuleType = t.TypeOf; diff --git a/x-pack/legacy/plugins/siem/cypress.json b/x-pack/legacy/plugins/siem/cypress.json deleted file mode 100644 index a0333a1068146..0000000000000 --- a/x-pack/legacy/plugins/siem/cypress.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "baseUrl": "http://localhost:5601", - "defaultCommandTimeout": 120000, - "screenshotsFolder": "../../../../target/kibana-siem/cypress/screenshots", - "trashAssetsBeforeRuns": false, - "video": false, - "videosFolder": "../../../../target/kibana-siem/cypress/videos" -} diff --git a/x-pack/legacy/plugins/siem/cypress/README.md b/x-pack/legacy/plugins/siem/cypress/README.md deleted file mode 100644 index 41137ce6d8a9d..0000000000000 --- a/x-pack/legacy/plugins/siem/cypress/README.md +++ /dev/null @@ -1,283 +0,0 @@ -# Cypress Tests - -The `siem/cypress` directory contains end to end tests, (plus a few tests -that rely on mocked API calls), that execute via [Cypress](https://www.cypress.io/). - -Cypress tests may be run against: - -- A local Kibana instance, interactively or via the command line. Credentials -are specified via `kibana.dev.yml` or environment variables. -- A remote Elastic Cloud instance (override `baseUrl`), interactively or via -the command line. Again, credentials are specified via `kibana.dev.yml` or -environment variables. -- As part of CI (override `baseUrl` and pass credentials via the -`CYPRESS_ELASTICSEARCH_USERNAME` and `CYPRESS_ELASTICSEARCH_PASSWORD` -environment variables), via command line. - -At present, Cypress tests are only executed manually. They are **not** yet -integrated in the Kibana CI infrastructure, and therefore do **not** run -automatically when you submit a PR. - -## Smoke Tests - -Smoke Tests are located in `siem/cypress/integration/smoke_tests` - -## Structure - -### Tasks - -_Tasks_ are functions that my be re-used across tests. Inside the _tasks_ folder there are some other folders that represents -the page to which we will perform the actions. For each folder we are going to create a file for each one of the sections that - has the page. - -i.e. -- tasks - - hosts - - events.ts - -### Screens - -In _screens_ folder we are going to find all the elements we want to interact in our tests. Inside _screens_ fonder there -are some other folders that represents the page that contains the elements the tests are going to interact with. For each -folder we are going to create a file for each one of the sections that the page has. - -i.e. -- tasks - - hosts - - events.ts - -## Mock Data - -We prefer not to mock API responses in most of our smoke tests, but sometimes -it's necessary because a test must assert that a specific value is rendered, -and it's not possible to derive that value based on the data in the -environment where tests are running. - -Mocked responses API from the server are located in `siem/cypress/fixtures`. - -## Speeding up test execution time - -Loading the web page takes a big amount of time, in order to minimize that impact, the following points should be -taken into consideration until another solution is implemented: - -- Don't refresh the page for every test to clean the state of it. -- Instead, group the tests that are similar in different contexts. -- For every context login only once, clean the state between tests if needed without re-loading the page. -- All tests in a spec file must be order-independent. - - If you need to reload the page to make the tests order-independent, consider to create a new context. - -Remember that minimizing the number of times the web page is loaded, we minimize as well the execution time. - -## Authentication - -When running tests, there are two ways to specify the credentials used to -authenticate with Kibana: - -- Via `kibana.dev.yml` (recommended for developers) -- Via the `CYPRESS_ELASTICSEARCH_USERNAME` and `CYPRESS_ELASTICSEARCH_PASSWORD` -environment variables (recommended for CI), or when testing a remote Kibana -instance, e.g. in Elastic Cloud. - -Note: Tests that use the `login()` test helper function for authentication will -automatically use the `CYPRESS_ELASTICSEARCH_USERNAME` and `CYPRESS_ELASTICSEARCH_PASSWORD` -environment variables when they are defined, and fall back to the values in -`config/kibana.dev.yml` when they are unset. - -### Content Security Policy (CSP) Settings - -Your local or cloud Kibana server must have the `csp.strict: false` setting -configured in `kibana.dev.yml`, or `kibana.yml`, as shown in the example below: - -```yaml -csp.strict: false -``` - -The above setting is required to prevent the _Please upgrade -your browser_ / _This Kibana installation has strict security requirements -enabled that your current browser does not meet._ warning that's displayed for -unsupported user agents, like the one reported by Cypress when running tests. - -### Example `kibana.dev.yml` - -If you're a developer running tests interactively or on the command line, the -easiset way to specify the credentials used for authentication is to update - `kibana.dev.yml` per the following example: - -```yaml -csp.strict: false -elasticsearch: - username: 'elastic' - password: '' - hosts: ['https://:9200'] -``` - -## Running Tests Interactively - -Use the Cypress interactive test runner to develop and debug specific tests -by adding a `.only` to the test you're developing, or click on a specific -spec in the interactive test runner to run just the tests in that spec. - -To run and debug tests in interactively via the Cypress test runner: - -1. Disable CSP on the local or remote Kibana instance, as described in the -_Content Security Policy (CSP) Settings_ section above. - -2. To specify the credentials required for authentication, configure -`config/kibana.dev.yml`, as described in the _Server and Authentication -Requirements_ section above, or specify them via environment variables -as described later in this section. - -3. Start a local instance of the Kibana development server (only if testing against a -local host): - -```sh -yarn start --no-base-path -``` - -4. Launch the Cypress interactive test runner via one of the following options: - -- To run tests interactively against the default (local) host specified by -`baseUrl`, as configured in `plugins/siem/cypress.json`: - -```sh -cd x-pack/legacy/plugins/siem -yarn cypress:open -``` - -- To (optionally) run tests interactively against a different host, pass the -`CYPRESS_baseUrl` environment variable on the command line when launching the -test runner, as shown in the following example: - -```sh -cd x-pack/legacy/plugins/siem -CYPRESS_baseUrl=http://localhost:5601 yarn cypress:open -``` - -- To (optionally) override username and password via environment variables when -running tests interactively: - -```sh -cd x-pack/legacy/plugins/siem -CYPRESS_baseUrl=http://localhost:5601 CYPRESS_ELASTICSEARCH_USERNAME=elastic CYPRESS_ELASTICSEARCH_PASSWORD= yarn cypress:open -``` - -5. Click the `Run all specs` button in the Cypress test runner (after adding -a `.only` to an `it` or `describe` block). - -## Running (Headless) Tests on the Command Line - -To run (headless) tests on the command line: - -1. Disable CSP on the local or remote Kibana instance, as described in the -_Content Security Policy (CSP) Settings_ section above. - -2. To specify the credentials required for authentication, configure -`config/kibana.dev.yml`, as described in the _Server and Authentication -Requirements_ section above, or specify them via environment variables -as described later in this section. - -3. Start a local instance of the Kibana development server (only if testing against a -local host): - -```sh -yarn start --no-base-path -``` - -4. Launch the Cypress command line test runner via one of the following options: - -- To run tests on the command line against the default (local) host specified by -`baseUrl`, as configured in `plugins/siem/cypress.json`: - -```sh -cd x-pack/legacy/plugins/siem -yarn cypress:run -``` - -- To (optionally) run tests on the command line against a different host, pass -`CYPRESS_baseUrl` as an environment variable on the command line, as shown in -the following example: - -```sh -cd x-pack/legacy/plugins/siem -CYPRESS_baseUrl=http://localhost:5601 yarn cypress:run -``` - -- To (optionally) override username and password via environment variables when -running via the command line: - -```sh -cd x-pack/legacy/plugins/siem -CYPRESS_baseUrl=http://localhost:5601 CYPRESS_ELASTICSEARCH_USERNAME=elastic CYPRESS_ELASTICSEARCH_PASSWORD= yarn cypress:run -``` - -## Running (Headless) Tests on the Command Line as a Jenkins execution - -To run (headless) tests as a Jenkins execution. - -1. First bootstrap kibana changes from the Kibana root directory: - -```sh -yarn kbn bootstrap -``` - -2. Launch Cypress command line test runner: - -```sh -cd x-pack/legacy/plugins/siem -yarn cypress:run-as-ci -``` - -Note that with this type of execution you don't need to have running a kibana and elasticsearch instance. This is because - the command, as it would happen in the CI, will launch the instances. The elasticsearch instance will be fed with the data - placed in: `x-pack/test/siem_cypress/es_archives`. - -As in this case we want to mimic a CI execution we want to execute the tests with the same set of data, this is why -in this case does not make sense to override Cypress environment variables. - -## Reporting - -When Cypress tests are run on the command line via `yarn cypress:run`, -reporting artifacts are generated under the `target` directory in the root -of the Kibana, as detailed for each artifact type in the sections below. - -### HTML Reports - -An HTML report (e.g. for email notifications) is output to: - -``` -target/kibana-siem/cypress/results/output.html -``` - -### Screenshots - -Screenshots of failed tests are output to: - -``` -target/kibana-siem/cypress/screenshots -``` - -### `junit` Reports - -The Kibana CI process reports `junit` test results from the `target/junit` directory. - -Cypress `junit` reports are generated in `target/kibana-siem/cypress/results` -and copied to the `target/junit` directory. - -### Videos (optional) - -Videos are disabled by default, but can optionally be enabled by setting the -`CYPRESS_video=true` environment variable: - -``` -CYPRESS_video=true yarn cypress:run -``` - -Videos are (optionally) output to: - -``` -target/kibana-siem/cypress/videos -``` - -## Linting - -Optional linting rules for Cypress and linting setup can be found [here](https://github.com/cypress-io/eslint-plugin-cypress#usage) \ No newline at end of file diff --git a/x-pack/legacy/plugins/siem/cypress/integration/signal_detection_rules.spec.ts b/x-pack/legacy/plugins/siem/cypress/integration/signal_detection_rules.spec.ts deleted file mode 100644 index 2d2db9e70255b..0000000000000 --- a/x-pack/legacy/plugins/siem/cypress/integration/signal_detection_rules.spec.ts +++ /dev/null @@ -1,62 +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 { - FIFTH_RULE, - FIRST_RULE, - RULE_NAME, - SECOND_RULE, - SEVENTH_RULE, -} from '../screens/signal_detection_rules'; - -import { goToManageSignalDetectionRules } from '../tasks/detections'; -import { esArchiverLoad, esArchiverUnload } from '../tasks/es_archiver'; -import { loginAndWaitForPageWithoutDateRange } from '../tasks/login'; -import { - activateRule, - sortByActivatedRules, - waitForLoadElasticPrebuiltDetectionRulesTableToBeLoaded, - waitForRuleToBeActivated, -} from '../tasks/signal_detection_rules'; - -import { DETECTIONS } from '../urls/navigation'; - -describe('Signal detection rules', () => { - before(() => { - esArchiverLoad('prebuilt_rules_loaded'); - }); - - after(() => { - esArchiverUnload('prebuilt_rules_loaded'); - }); - - it.skip('Sorts by activated rules', () => { - loginAndWaitForPageWithoutDateRange(DETECTIONS); - goToManageSignalDetectionRules(); - waitForLoadElasticPrebuiltDetectionRulesTableToBeLoaded(); - cy.get(RULE_NAME) - .eq(FIFTH_RULE) - .invoke('text') - .then(fifthRuleName => { - activateRule(FIFTH_RULE); - waitForRuleToBeActivated(); - cy.get(RULE_NAME) - .eq(SEVENTH_RULE) - .invoke('text') - .then(seventhRuleName => { - activateRule(SEVENTH_RULE); - waitForRuleToBeActivated(); - sortByActivatedRules(); - - cy.get(RULE_NAME) - .eq(FIRST_RULE) - .should('have.text', fifthRuleName); - cy.get(RULE_NAME) - .eq(SECOND_RULE) - .should('have.text', seventhRuleName); - }); - }); - }); -}); diff --git a/x-pack/legacy/plugins/siem/cypress/tasks/es_archiver.ts b/x-pack/legacy/plugins/siem/cypress/tasks/es_archiver.ts deleted file mode 100644 index 6417a7d872251..0000000000000 --- a/x-pack/legacy/plugins/siem/cypress/tasks/es_archiver.ts +++ /dev/null @@ -1,45 +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. - */ - -export const esArchiverLoadEmptyKibana = () => { - cy.exec( - `node ../../../../scripts/es_archiver empty_kibana load empty--dir ../../../test/siem_cypress/es_archives --config ../../../../test/functional/config.js --es-url ${Cypress.env( - 'ELASTICSEARCH_URL' - )} --kibana-url ${Cypress.config().baseUrl}` - ); -}; - -export const esArchiverLoad = (folder: string) => { - cy.exec( - `node ../../../../scripts/es_archiver load ${folder} --dir ../../../test/siem_cypress/es_archives --config ../../../../test/functional/config.js --es-url ${Cypress.env( - 'ELASTICSEARCH_URL' - )} --kibana-url ${Cypress.config().baseUrl}` - ); -}; - -export const esArchiverUnload = (folder: string) => { - cy.exec( - `node ../../../../scripts/es_archiver unload ${folder} --dir ../../../test/siem_cypress/es_archives --config ../../../../test/functional/config.js --es-url ${Cypress.env( - 'ELASTICSEARCH_URL' - )} --kibana-url ${Cypress.config().baseUrl}` - ); -}; - -export const esArchiverUnloadEmptyKibana = () => { - cy.exec( - `node ../../../../scripts/es_archiver unload empty_kibana empty--dir ../../../test/siem_cypress/es_archives --config ../../../../test/functional/config.js --es-url ${Cypress.env( - 'ELASTICSEARCH_URL' - )} --kibana-url ${Cypress.config().baseUrl}` - ); -}; - -export const esArchiverResetKibana = () => { - cy.exec( - `node ../../../../scripts/es_archiver empty-kibana-index --config ../../../../test/functional/config.js --es-url ${Cypress.env( - 'ELASTICSEARCH_URL' - )} --kibana-url ${Cypress.config().baseUrl}` - ); -}; diff --git a/x-pack/legacy/plugins/siem/index.ts b/x-pack/legacy/plugins/siem/index.ts index 3773283555b32..6e03583dda69f 100644 --- a/x-pack/legacy/plugins/siem/index.ts +++ b/x-pack/legacy/plugins/siem/index.ts @@ -6,11 +6,10 @@ import { i18n } from '@kbn/i18n'; import { resolve } from 'path'; -import { Server } from 'hapi'; import { Root } from 'joi'; -import { plugin } from './server'; -import { savedObjectMappings } from './server/saved_objects'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { savedObjectMappings } from '../../../plugins/siem/server/saved_objects'; import { APP_ID, @@ -23,15 +22,13 @@ import { DEFAULT_INTERVAL_VALUE, DEFAULT_FROM, DEFAULT_TO, - DEFAULT_SIGNALS_INDEX, ENABLE_NEWS_FEED_SETTING, NEWS_FEED_URL_SETTING, NEWS_FEED_URL_SETTING_DEFAULT, - SIGNALS_INDEX_KEY, IP_REPUTATION_LINKS_SETTING, IP_REPUTATION_LINKS_SETTING_DEFAULT, -} from './common/constants'; -import { defaultIndexPattern } from './default_index_pattern'; + DEFAULT_INDEX_PATTERN, +} from '../../../plugins/siem/common/constants'; import { DEFAULT_APP_CATEGORIES } from '../../../../src/core/utils'; // eslint-disable-next-line @typescript-eslint/no-explicit-any @@ -102,7 +99,7 @@ export const siem = (kibana: any) => { name: i18n.translate('xpack.siem.uiSettings.defaultIndexLabel', { defaultMessage: 'Elasticsearch indices', }), - value: defaultIndexPattern, + value: DEFAULT_INDEX_PATTERN, description: i18n.translate('xpack.siem.uiSettings.defaultIndexDescription', { defaultMessage: '

Comma-delimited list of Elasticsearch indices from which the SIEM app collects events.

', @@ -162,31 +159,12 @@ export const siem = (kibana: any) => { }, mappings: savedObjectMappings, }, - init(server: Server) { - const { coreContext, env, setup, start } = server.newPlatform; - const initializerContext = { ...coreContext, env }; - const __legacy = { - config: server.config, - route: server.route.bind(server), - }; - - // @ts-ignore-next-line: NewPlatform shim is too loosely typed - const pluginInstance = plugin(initializerContext); - // @ts-ignore-next-line: NewPlatform shim is too loosely typed - pluginInstance.setup(setup.core, setup.plugins, __legacy); - // @ts-ignore-next-line: NewPlatform shim is too loosely typed - pluginInstance.start(start.core, start.plugins); - }, config(Joi: Root) { - // See x-pack/plugins/siem/server/config.ts if you're adding another - // value where the configuration has to be duplicated at the moment. - // When we move over to the new platform completely this will be - // removed and only server/config.ts should be used. return Joi.object() .keys({ enabled: Joi.boolean().default(true), - [SIGNALS_INDEX_KEY]: Joi.string().default(DEFAULT_SIGNALS_INDEX), }) + .unknown(true) .default(); }, }); diff --git a/x-pack/legacy/plugins/siem/package.json b/x-pack/legacy/plugins/siem/package.json index 472a473842f02..3a93beef963a0 100644 --- a/x-pack/legacy/plugins/siem/package.json +++ b/x-pack/legacy/plugins/siem/package.json @@ -1,16 +1,10 @@ { "author": "Elastic", - "name": "siem", + "name": "siem-legacy-ui", "version": "8.0.0", "private": true, "license": "Elastic-License", - "scripts": { - "extract-mitre-attacks": "node scripts/extract_tactics_techniques_mitre.js & node ../../../../scripts/eslint ./public/pages/detection_engine/mitre/mitre_tactics_techniques.ts --fix", - "build-graphql-types": "node scripts/generate_types_from_graphql.js", - "cypress:open": "../../../node_modules/.bin/cypress open", - "cypress:run": "../../../node_modules/.bin/cypress run --spec ./cypress/integration/**/*.spec.ts --reporter ../../../node_modules/cypress-multi-reporters --reporter-options configFile=./reporter_config.json; status=$?; ../../../node_modules/.bin/mochawesome-merge --reportDir ../../../../target/kibana-siem/cypress/results > ../../../../target/kibana-siem/cypress/results/output.json; ../../../../node_modules/.bin/marge ../../../../target/kibana-siem/cypress/results/output.json --reportDir ../../../../target/kibana-siem/cypress/results; mkdir -p ../../../../target/junit && cp ../../../../target/kibana-siem/cypress/results/*.xml ../../../../target/junit/ && exit $status;", - "cypress:run-as-ci": "node ../../../../scripts/functional_tests --config ../../../test/siem_cypress/config.ts" - }, + "scripts": {}, "devDependencies": { "@types/lodash": "^4.14.110", "@types/js-yaml": "^3.12.1", diff --git a/x-pack/legacy/plugins/siem/public/app/app.tsx b/x-pack/legacy/plugins/siem/public/app/app.tsx index 7413aeab549db..44c1c923cd6ee 100644 --- a/x-pack/legacy/plugins/siem/public/app/app.tsx +++ b/x-pack/legacy/plugins/siem/public/app/app.tsx @@ -20,7 +20,7 @@ import { pluck } from 'rxjs/operators'; import { KibanaContextProvider, useKibana, useUiSetting$ } from '../lib/kibana'; import { Storage } from '../../../../../../src/plugins/kibana_utils/public'; -import { DEFAULT_DARK_MODE } from '../../common/constants'; +import { DEFAULT_DARK_MODE } from '../../../../../plugins/siem/common/constants'; import { ErrorToastDispatcher } from '../components/error_toast_dispatcher'; import { compose } from '../lib/compose/kibana_compose'; import { AppFrontendLibs, AppApolloClient } from '../lib/lib'; diff --git a/x-pack/legacy/plugins/siem/public/components/alerts_viewer/alerts_table.tsx b/x-pack/legacy/plugins/siem/public/components/alerts_viewer/alerts_table.tsx index 05d8f97bb8849..dd608babef48f 100644 --- a/x-pack/legacy/plugins/siem/public/components/alerts_viewer/alerts_table.tsx +++ b/x-pack/legacy/plugins/siem/public/components/alerts_viewer/alerts_table.tsx @@ -17,7 +17,7 @@ export interface OwnProps { start: number; } -const ALERTS_TABLE_ID = 'timeline-alerts-table'; +const ALERTS_TABLE_ID = 'alerts-table'; const defaultAlertsFilters: Filter[] = [ { meta: { diff --git a/x-pack/legacy/plugins/siem/public/components/alerts_viewer/index.tsx b/x-pack/legacy/plugins/siem/public/components/alerts_viewer/index.tsx index 587002c24d526..778adc708d901 100644 --- a/x-pack/legacy/plugins/siem/public/components/alerts_viewer/index.tsx +++ b/x-pack/legacy/plugins/siem/public/components/alerts_viewer/index.tsx @@ -6,11 +6,11 @@ import React, { useEffect, useCallback, useMemo } from 'react'; import numeral from '@elastic/numeral'; +import { DEFAULT_NUMBER_FORMAT } from '../../../../../../plugins/siem/common/constants'; import { AlertsComponentsQueryProps } from './types'; import { AlertsTable } from './alerts_table'; import * as i18n from './translations'; import { useUiSetting$ } from '../../lib/kibana'; -import { DEFAULT_NUMBER_FORMAT } from '../../../common/constants'; import { MatrixHistogramContainer } from '../matrix_histogram'; import { histogramConfigs } from './histogram_configs'; import { MatrixHisrogramConfigs } from '../matrix_histogram/types'; diff --git a/x-pack/legacy/plugins/siem/public/components/charts/barchart.test.tsx b/x-pack/legacy/plugins/siem/public/components/charts/barchart.test.tsx index 272c41833f368..635d48cca10fc 100644 --- a/x-pack/legacy/plugins/siem/public/components/charts/barchart.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/charts/barchart.test.tsx @@ -4,15 +4,29 @@ * you may not use this file except in compliance with the Elastic License. */ -import { shallow, ShallowWrapper } from 'enzyme'; +import { Chart, BarSeries, Axis, ScaleType } from '@elastic/charts'; +import euiDarkVars from '@elastic/eui/dist/eui_theme_dark.json'; +import { mount, ReactWrapper, shallow, ShallowWrapper } from 'enzyme'; import React from 'react'; +import { ThemeProvider } from 'styled-components'; + +import { escapeDataProviderId } from '../drag_and_drop/helpers'; +import { TestProviders } from '../../mock'; import { BarChartBaseComponent, BarChartComponent } from './barchart'; import { ChartSeriesData } from './common'; -import { Chart, BarSeries, Axis, ScaleType } from '@elastic/charts'; jest.mock('../../lib/kibana'); +jest.mock('uuid', () => { + return { + v1: jest.fn(() => 'uuid.v1()'), + v4: jest.fn(() => 'uuid.v4()'), + }; +}); + +const theme = () => ({ eui: euiDarkVars, darkMode: true }); + const customHeight = '100px'; const customWidth = '120px'; const chartDataSets = [ @@ -116,6 +130,19 @@ const mockConfig = { customHeight: 324, }; +// Suppress warnings about "react-beautiful-dnd" +/* eslint-disable no-console */ +const originalError = console.error; +const originalWarn = console.warn; +beforeAll(() => { + console.warn = jest.fn(); + console.error = jest.fn(); +}); +afterAll(() => { + console.error = originalError; + console.warn = originalWarn; +}); + describe('BarChartBaseComponent', () => { let shallowWrapper: ShallowWrapper; const mockBarChartData: ChartSeriesData[] = [ @@ -280,6 +307,91 @@ describe.each(chartDataSets)('BarChart with valid data [%o]', data => { expect(shallowWrapper.find('BarChartBase')).toHaveLength(1); expect(shallowWrapper.find('ChartPlaceHolder')).toHaveLength(0); }); + + it('it does NOT render a draggable legend because stackByField is not provided', () => { + expect(shallowWrapper.find('[data-test-subj="draggable-legend"]').exists()).toBe(false); + }); +}); + +describe.each(chartDataSets)('BarChart with stackByField', () => { + let wrapper: ReactWrapper; + + const data = [ + { + key: 'python.exe', + value: [ + { + x: 1586754900000, + y: 9675, + g: 'python.exe', + }, + ], + }, + { + key: 'kernel', + value: [ + { + x: 1586754900000, + y: 8708, + g: 'kernel', + }, + { + x: 1586757600000, + y: 9282, + g: 'kernel', + }, + ], + }, + { + key: 'sshd', + value: [ + { + x: 1586754900000, + y: 5907, + g: 'sshd', + }, + ], + }, + ]; + + const expectedColors = ['#1EA593', '#2B70F7', '#CE0060']; + + const stackByField = 'process.name'; + + beforeAll(() => { + wrapper = mount( + + + + + + ); + }); + + it('it renders a draggable legend', () => { + expect(wrapper.find('[data-test-subj="draggable-legend"]').exists()).toBe(true); + }); + + expectedColors.forEach((color, i) => { + test(`it renders the expected legend color ${color} for legend item ${i}`, () => { + expect(wrapper.find(`div [color="${color}"]`).exists()).toBe(true); + }); + }); + + data.forEach(datum => { + test(`it renders the expected draggable legend text for datum ${datum.key}`, () => { + const dataProviderId = `draggableId.content.draggable-legend-item-uuid_v4()-${escapeDataProviderId( + stackByField + )}-${escapeDataProviderId(datum.key)}`; + + expect( + wrapper + .find(`div [data-rbd-draggable-id="${dataProviderId}"]`) + .first() + .text() + ).toEqual(datum.key); + }); + }); }); describe.each(chartHolderDataSets)('BarChart with invalid data [%o]', data => { diff --git a/x-pack/legacy/plugins/siem/public/components/charts/barchart.tsx b/x-pack/legacy/plugins/siem/public/components/charts/barchart.tsx index 2ae0e05850a37..64d15cd6731cb 100644 --- a/x-pack/legacy/plugins/siem/public/components/charts/barchart.tsx +++ b/x-pack/legacy/plugins/siem/public/components/charts/barchart.tsx @@ -4,13 +4,19 @@ * you may not use this file except in compliance with the Elastic License. */ -import React from 'react'; +import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; +import React, { useMemo } from 'react'; import { Chart, BarSeries, Axis, Position, ScaleType, Settings } from '@elastic/charts'; import { getOr, get, isNumber } from 'lodash/fp'; import deepmerge from 'deepmerge'; +import uuid from 'uuid'; +import styled from 'styled-components'; -import { useThrottledResizeObserver } from '../utils'; +import { escapeDataProviderId } from '../drag_and_drop/helpers'; import { useTimeZone } from '../../lib/kibana'; +import { defaultLegendColors } from '../matrix_histogram/utils'; +import { useThrottledResizeObserver } from '../utils'; + import { ChartPlaceHolder } from './chart_place_holder'; import { chartDefaultSettings, @@ -22,6 +28,12 @@ import { WrappedByAutoSizer, useTheme, } from './common'; +import { DraggableLegend } from './draggable_legend'; +import { LegendItem } from './draggable_legend_item'; + +const LegendFlexItem = styled(EuiFlexItem)` + overview: hidden; +`; const checkIfAllTheDataInTheSeriesAreValid = (series: ChartSeriesData): series is ChartSeriesData => series != null && @@ -38,12 +50,14 @@ const checkIfAnyValidSeriesExist = ( // Bar chart rotation: https://ela.st/chart-rotations export const BarChartBaseComponent = ({ data, + forceHiddenLegend = false, ...chartConfigs }: { data: ChartSeriesData[]; width: string | null | undefined; height: string | null | undefined; configs?: ChartSeriesConfigs | undefined; + forceHiddenLegend?: boolean; }) => { const theme = useTheme(); const timeZone = useTimeZone(); @@ -59,10 +73,10 @@ export const BarChartBaseComponent = ({ return chartConfigs.width && chartConfigs.height ? ( - + {data.map(series => { const barSeriesKey = series.key; - return checkIfAllTheDataInTheSeriesAreValid ? ( + return checkIfAllTheDataInTheSeriesAreValid(series) ? ( = ({ barChart, configs }) => { +const NO_LEGEND_DATA: LegendItem[] = []; + +export const BarChartComponent: React.FC = ({ + barChart, + configs, + stackByField, +}) => { const { ref: measureRef, width, height } = useThrottledResizeObserver(); + const legendItems: LegendItem[] = useMemo( + () => + barChart != null && stackByField != null + ? barChart.map((d, i) => ({ + color: d.color ?? i < defaultLegendColors.length ? defaultLegendColors[i] : undefined, + dataProviderId: escapeDataProviderId( + `draggable-legend-item-${uuid.v4()}-${stackByField}-${d.key}` + ), + field: stackByField, + value: d.key, + })) + : NO_LEGEND_DATA, + [barChart, stackByField] + ); + const customHeight = get('customHeight', configs); const customWidth = get('customWidth', configs); const chartHeight = getChartHeight(customHeight, height); const chartWidth = getChartWidth(customWidth, width); return checkIfAnyValidSeriesExist(barChart) ? ( - - - + + + + + + + + + + ) : ( ); diff --git a/x-pack/legacy/plugins/siem/public/components/charts/common.tsx b/x-pack/legacy/plugins/siem/public/components/charts/common.tsx index d8429cba1b4fb..c7b40c50ffde8 100644 --- a/x-pack/legacy/plugins/siem/public/components/charts/common.tsx +++ b/x-pack/legacy/plugins/siem/public/components/charts/common.tsx @@ -19,8 +19,8 @@ import { import React, { useMemo } from 'react'; import styled from 'styled-components'; +import { DEFAULT_DARK_MODE } from '../../../../../../plugins/siem/common/constants'; import { useUiSetting } from '../../lib/kibana'; -import { DEFAULT_DARK_MODE } from '../../../common/constants'; export const defaultChartHeight = '100%'; export const defaultChartWidth = '100%'; diff --git a/x-pack/legacy/plugins/siem/public/components/charts/draggable_legend.test.tsx b/x-pack/legacy/plugins/siem/public/components/charts/draggable_legend.test.tsx new file mode 100644 index 0000000000000..0da0c2bdc35f2 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/components/charts/draggable_legend.test.tsx @@ -0,0 +1,149 @@ +/* + * 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 euiDarkVars from '@elastic/eui/dist/eui_theme_dark.json'; +import { mount, ReactWrapper } from 'enzyme'; +import React from 'react'; +import { ThemeProvider } from 'styled-components'; + +import { TestProviders } from '../../mock'; + +import { MIN_LEGEND_HEIGHT, DraggableLegend } from './draggable_legend'; +import { LegendItem } from './draggable_legend_item'; + +const theme = () => ({ eui: euiDarkVars, darkMode: true }); + +const allOthersDataProviderId = + 'draggable-legend-item-527adabe-8e1c-4a1f-965c-2f3d65dda9e1-event_dataset-All others'; + +const legendItems: LegendItem[] = [ + { + color: '#1EA593', + dataProviderId: 'draggable-legend-item-3207fda7-d008-402a-86a0-8ad632081bad-event_dataset-flow', + field: 'event.dataset', + value: 'flow', + }, + { + color: '#2B70F7', + dataProviderId: + 'draggable-legend-item-83f6c824-811d-4ec8-b373-eba2b0de6398-event_dataset-suricata_eve', + field: 'event.dataset', + value: 'suricata.eve', + }, + { + color: '#CE0060', + dataProviderId: + 'draggable-legend-item-ec57bb8f-82cd-4e07-bd38-1d11b3f0ee5f-event_dataset-traefik_access', + field: 'event.dataset', + value: 'traefik.access', + }, + { + color: '#38007E', + dataProviderId: + 'draggable-legend-item-25d5fcd6-87ba-46b5-893e-c655d7d504e3-event_dataset-esensor', + field: 'event.dataset', + value: 'esensor', + }, + { + color: '#F37020', + dataProviderId: allOthersDataProviderId, + field: 'event.dataset', + value: 'All others', + }, +]; + +describe('DraggableLegend', () => { + const height = 400; + + // Suppress warnings about "react-beautiful-dnd" + /* eslint-disable no-console */ + const originalError = console.error; + const originalWarn = console.warn; + beforeAll(() => { + console.warn = jest.fn(); + console.error = jest.fn(); + }); + afterAll(() => { + console.error = originalError; + console.warn = originalWarn; + }); + + describe('rendering', () => { + let wrapper: ReactWrapper; + + beforeEach(() => { + wrapper = mount( + + + + + + ); + }); + + it(`renders a container with the specified non-zero 'height'`, () => { + expect(wrapper.find('[data-test-subj="draggable-legend"]').first()).toHaveStyleRule( + 'height', + `${height}px` + ); + }); + + it('scrolls when necessary', () => { + expect(wrapper.find('[data-test-subj="draggable-legend"]').first()).toHaveStyleRule( + 'overflow', + 'auto' + ); + }); + + it('renders the legend items', () => { + legendItems.forEach(item => + expect( + wrapper + .find( + item.dataProviderId !== allOthersDataProviderId + ? `[data-test-subj="legend-item-${item.dataProviderId}"]` + : '[data-test-subj="all-others-legend-item"]' + ) + .first() + .text() + ).toEqual(item.value) + ); + }); + + it('renders a spacer for every legend item', () => { + expect(wrapper.find('[data-test-subj="draggable-legend-spacer"]').hostNodes().length).toEqual( + legendItems.length + ); + }); + }); + + it('does NOT render the legend when an empty collection of legendItems is provided', () => { + const wrapper = mount( + + + + + + ); + + expect(wrapper.find('[data-test-subj="draggable-legend"]').exists()).toBe(false); + }); + + it(`renders a legend with the minimum height when 'height' is zero`, () => { + const wrapper = mount( + + + + + + ); + + expect(wrapper.find('[data-test-subj="draggable-legend"]').first()).toHaveStyleRule( + 'height', + `${MIN_LEGEND_HEIGHT}px` + ); + }); +}); diff --git a/x-pack/legacy/plugins/siem/public/components/charts/draggable_legend.tsx b/x-pack/legacy/plugins/siem/public/components/charts/draggable_legend.tsx new file mode 100644 index 0000000000000..ef3fbb8780d15 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/components/charts/draggable_legend.tsx @@ -0,0 +1,68 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { EuiFlexGroup, EuiFlexItem, EuiSpacer, EuiText } from '@elastic/eui'; +import { rgba } from 'polished'; +import React from 'react'; +import styled from 'styled-components'; + +import { DraggableLegendItem, LegendItem } from './draggable_legend_item'; + +export const MIN_LEGEND_HEIGHT = 175; + +const DraggableLegendContainer = styled.div<{ height: number }>` + height: ${({ height }) => `${height}px`}; + overflow: auto; + scrollbar-width: thin; + width: 165px; + + &::-webkit-scrollbar { + height: ${({ theme }) => theme.eui.euiScrollBar}; + width: ${({ theme }) => theme.eui.euiScrollBar}; + } + + &::-webkit-scrollbar-thumb { + background-clip: content-box; + background-color: ${({ theme }) => rgba(theme.eui.euiColorDarkShade, 0.5)}; + border: ${({ theme }) => theme.eui.euiScrollBarCorner} solid transparent; + } + + &::-webkit-scrollbar-corner, + &::-webkit-scrollbar-track { + background-color: transparent; + } +`; + +const DraggableLegendComponent: React.FC<{ + height: number; + legendItems: LegendItem[]; +}> = ({ height, legendItems }) => { + if (legendItems.length === 0) { + return null; + } + + return ( + + + + {legendItems.map(item => ( + + + + + ))} + + + + ); +}; + +DraggableLegendComponent.displayName = 'DraggableLegendComponent'; + +export const DraggableLegend = React.memo(DraggableLegendComponent); diff --git a/x-pack/legacy/plugins/siem/public/components/charts/draggable_legend_item.test.tsx b/x-pack/legacy/plugins/siem/public/components/charts/draggable_legend_item.test.tsx new file mode 100644 index 0000000000000..581952a8415f6 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/components/charts/draggable_legend_item.test.tsx @@ -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 euiDarkVars from '@elastic/eui/dist/eui_theme_dark.json'; +import { mount, ReactWrapper } from 'enzyme'; +import React from 'react'; +import { ThemeProvider } from 'styled-components'; + +import { TestProviders } from '../../mock'; + +import { DraggableLegendItem, LegendItem } from './draggable_legend_item'; + +const theme = () => ({ eui: euiDarkVars, darkMode: true }); + +describe('DraggableLegendItem', () => { + // Suppress warnings about "react-beautiful-dnd" + /* eslint-disable no-console */ + const originalError = console.error; + const originalWarn = console.warn; + beforeAll(() => { + console.warn = jest.fn(); + console.error = jest.fn(); + }); + afterAll(() => { + console.error = originalError; + console.warn = originalWarn; + }); + + describe('rendering a regular (non "All others") legend item', () => { + const legendItem: LegendItem = { + color: '#1EA593', + dataProviderId: + 'draggable-legend-item-3207fda7-d008-402a-86a0-8ad632081bad-event_dataset-flow', + field: 'event.dataset', + value: 'flow', + }; + + let wrapper: ReactWrapper; + + beforeEach(() => { + wrapper = mount( + + + + + + ); + }); + + it('renders a colored circle with the expected legend item color', () => { + expect( + wrapper + .find('[data-test-subj="legend-color"]') + .first() + .props().color + ).toEqual(legendItem.color); + }); + + it('renders draggable legend item text', () => { + expect( + wrapper + .find(`[data-test-subj="legend-item-${legendItem.dataProviderId}"]`) + .first() + .text() + ).toEqual(legendItem.value); + }); + + it('does NOT render a non-draggable "All others" legend item', () => { + expect(wrapper.find(`[data-test-subj="all-others-legend-item"]`).exists()).toBe(false); + }); + }); + + describe('rendering an "All others" legend item', () => { + const allOthersLegendItem: LegendItem = { + color: '#F37020', + dataProviderId: + 'draggable-legend-item-527adabe-8e1c-4a1f-965c-2f3d65dda9e1-event_dataset-All others', + field: 'event.dataset', + value: 'All others', + }; + + let wrapper: ReactWrapper; + + beforeEach(() => { + wrapper = mount( + + + + + + ); + }); + + it('renders a colored circle with the expected legend item color', () => { + expect( + wrapper + .find('[data-test-subj="legend-color"]') + .first() + .props().color + ).toEqual(allOthersLegendItem.color); + }); + + it('does NOT render a draggable legend item', () => { + expect( + wrapper + .find(`[data-test-subj="legend-item-${allOthersLegendItem.dataProviderId}"]`) + .exists() + ).toBe(false); + }); + + it('renders NON-draggable `All others` legend item text', () => { + expect( + wrapper + .find(`[data-test-subj="all-others-legend-item"]`) + .first() + .text() + ).toEqual(allOthersLegendItem.value); + }); + }); + + it('does NOT render a colored circle when the legend item has no color', () => { + const noColorLegendItem: LegendItem = { + // no `color` attribute for this `LegendItem`! + dataProviderId: + 'draggable-legend-item-3207fda7-d008-402a-86a0-8ad632081bad-event_dataset-flow', + field: 'event.dataset', + value: 'flow', + }; + + const wrapper = mount( + + + + + + ); + + expect(wrapper.find('[data-test-subj="legend-color"]').exists()).toBe(false); + }); +}); diff --git a/x-pack/legacy/plugins/siem/public/components/charts/draggable_legend_item.tsx b/x-pack/legacy/plugins/siem/public/components/charts/draggable_legend_item.tsx new file mode 100644 index 0000000000000..cdda1733932d5 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/components/charts/draggable_legend_item.tsx @@ -0,0 +1,62 @@ +/* + * 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 { EuiFlexGroup, EuiFlexItem, EuiHealth, EuiText } from '@elastic/eui'; +import React from 'react'; +import styled from 'styled-components'; + +import { DefaultDraggable } from '../draggables'; + +import * as i18n from './translation'; + +// The "All others" legend item is not draggable +const AllOthers = styled.span` + padding-left: 7px; +`; + +export interface LegendItem { + color?: string; + dataProviderId: string; + field: string; + value: string; +} + +const DraggableLegendItemComponent: React.FC<{ + legendItem: LegendItem; +}> = ({ legendItem }) => { + const { color, dataProviderId, field, value } = legendItem; + + return ( + + + {color != null && ( + + + + )} + + + {value !== i18n.ALL_OTHERS ? ( + + ) : ( + <> + {value} + + )} + + + + ); +}; + +DraggableLegendItemComponent.displayName = 'DraggableLegendItemComponent'; + +export const DraggableLegendItem = React.memo(DraggableLegendItemComponent); diff --git a/x-pack/legacy/plugins/siem/public/components/charts/translation.ts b/x-pack/legacy/plugins/siem/public/components/charts/translation.ts index 341cb7782f87c..891f59fc97bd1 100644 --- a/x-pack/legacy/plugins/siem/public/components/charts/translation.ts +++ b/x-pack/legacy/plugins/siem/public/components/charts/translation.ts @@ -13,3 +13,7 @@ export const ALL_VALUES_ZEROS_TITLE = i18n.translate('xpack.siem.chart.dataAllVa export const DATA_NOT_AVAILABLE_TITLE = i18n.translate('xpack.siem.chart.dataNotAvailableTitle', { defaultMessage: 'Chart Data Not Available', }); + +export const ALL_OTHERS = i18n.translate('xpack.siem.chart.allOthersGroupingLabel', { + defaultMessage: 'All others', +}); diff --git a/x-pack/legacy/plugins/siem/public/components/drag_and_drop/drag_drop_context_wrapper.tsx b/x-pack/legacy/plugins/siem/public/components/drag_and_drop/drag_drop_context_wrapper.tsx index 11db33fff6d72..248ae671550ef 100644 --- a/x-pack/legacy/plugins/siem/public/components/drag_and_drop/drag_drop_context_wrapper.tsx +++ b/x-pack/legacy/plugins/siem/public/components/drag_and_drop/drag_drop_context_wrapper.tsx @@ -27,6 +27,9 @@ import { draggableIsField, } from './helpers'; +// @ts-ignore +window['__react-beautiful-dnd-disable-dev-warnings'] = true; + interface Props { browserFields: BrowserFields; children: React.ReactNode; diff --git a/x-pack/legacy/plugins/siem/public/components/drag_and_drop/draggable_wrapper.test.tsx b/x-pack/legacy/plugins/siem/public/components/drag_and_drop/draggable_wrapper.test.tsx index 11891afabbf3d..cd9e1dc95ff01 100644 --- a/x-pack/legacy/plugins/siem/public/components/drag_and_drop/draggable_wrapper.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/drag_and_drop/draggable_wrapper.test.tsx @@ -7,12 +7,13 @@ import { shallow } from 'enzyme'; import React from 'react'; import { MockedProvider } from 'react-apollo/test-utils'; +import { DraggableStateSnapshot, DraggingStyle } from 'react-beautiful-dnd'; import { mockBrowserFields, mocksSource } from '../../containers/source/mock'; import { TestProviders } from '../../mock'; import { mockDataProviders } from '../timeline/data_providers/mock/mock_data_providers'; import { DragDropContextWrapper } from './drag_drop_context_wrapper'; -import { DraggableWrapper, ConditionalPortal } from './draggable_wrapper'; +import { ConditionalPortal, DraggableWrapper, getStyle } from './draggable_wrapper'; import { useMountAppended } from '../../utils/use_mount_appended'; describe('DraggableWrapper', () => { @@ -48,6 +49,36 @@ describe('DraggableWrapper', () => { expect(wrapper.text()).toEqual(message); }); + + test('it does NOT render hover actions when the mouse is NOT over the draggable wrapper', () => { + const wrapper = mount( + + + + message} /> + + + + ); + + expect(wrapper.find('[data-test-subj="copy-to-clipboard"]').exists()).toBe(false); + }); + + test('it renders hover actions when the mouse is over the draggable wrapper', () => { + const wrapper = mount( + + + + message} /> + + + + ); + + wrapper.simulate('mouseenter'); + wrapper.update(); + expect(wrapper.find('[data-test-subj="copy-to-clipboard"]').exists()).toBe(true); + }); }); describe('text truncation styling', () => { @@ -100,4 +131,36 @@ describe('ConditionalPortal', () => { expect(props.registerProvider.mock.calls.length).toEqual(1); }); + + describe('getStyle', () => { + const style: DraggingStyle = { + boxSizing: 'border-box', + height: 10, + left: 1, + pointerEvents: 'none', + position: 'fixed', + transition: 'none', + top: 123, + width: 50, + zIndex: 9999, + }; + + it('returns a style with no transitionDuration when the snapshot is not drop animating', () => { + const snapshot: DraggableStateSnapshot = { + isDragging: true, + isDropAnimating: false, // <-- NOT drop animating + }; + + expect(getStyle(style, snapshot)).not.toHaveProperty('transitionDuration'); + }); + + it('returns a style with a transitionDuration when the snapshot is drop animating', () => { + const snapshot: DraggableStateSnapshot = { + isDragging: true, + isDropAnimating: true, // <-- it is drop animating + }; + + expect(getStyle(style, snapshot)).toHaveProperty('transitionDuration', '0.00000001s'); + }); + }); }); diff --git a/x-pack/legacy/plugins/siem/public/components/drag_and_drop/draggable_wrapper.tsx b/x-pack/legacy/plugins/siem/public/components/drag_and_drop/draggable_wrapper.tsx index 3a6a4de7984db..c7da5b5c58951 100644 --- a/x-pack/legacy/plugins/siem/public/components/drag_and_drop/draggable_wrapper.tsx +++ b/x-pack/legacy/plugins/siem/public/components/drag_and_drop/draggable_wrapper.tsx @@ -4,12 +4,14 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { useCallback, useEffect, useState } from 'react'; +import React, { useCallback, useEffect, useMemo, useState } from 'react'; import { Draggable, DraggableProvided, DraggableStateSnapshot, + DraggingStyle, Droppable, + NotDraggingStyle, } from 'react-beautiful-dnd'; import { useDispatch } from 'react-redux'; import styled from 'styled-components'; @@ -18,6 +20,9 @@ import deepEqual from 'fast-deep-equal'; import { dragAndDropActions } from '../../store/drag_and_drop'; import { DataProvider } from '../timeline/data_providers/data_provider'; import { TruncatableText } from '../truncatable_text'; +import { WithHoverActions } from '../with_hover_actions'; + +import { DraggableWrapperHoverContent } from './draggable_wrapper_hover_content'; import { getDraggableId, getDroppableId } from './helpers'; import { ProviderContainer } from './provider_container'; @@ -67,23 +72,42 @@ type RenderFunctionProp = ( state: DraggableStateSnapshot ) => React.ReactNode; -interface OwnProps { +interface Props { dataProvider: DataProvider; inline?: boolean; render: RenderFunctionProp; truncate?: boolean; + onFilterAdded?: () => void; } -type Props = OwnProps; - /** * Wraps a draggable component to handle registration / unregistration of the * data provider associated with the item being dropped */ +export const getStyle = ( + style: DraggingStyle | NotDraggingStyle | undefined, + snapshot: DraggableStateSnapshot +) => { + if (!snapshot.isDropAnimating) { + return style; + } + + return { + ...style, + transitionDuration: '0.00000001s', // cannot be 0, but can be a very short duration + }; +}; + export const DraggableWrapper = React.memo( - ({ dataProvider, render, truncate }) => { + ({ dataProvider, onFilterAdded, render, truncate }) => { + const [showTopN, setShowTopN] = useState(false); + const toggleTopN = useCallback(() => { + setShowTopN(!showTopN); + }, [setShowTopN, showTopN]); + const [providerRegistered, setProviderRegistered] = useState(false); + const dispatch = useDispatch(); const registerProvider = useCallback(() => { @@ -105,65 +129,90 @@ export const DraggableWrapper = React.memo( [] ); - return ( - - - ( - -
- ( + + ), + [dataProvider, onFilterAdded, showTopN, toggleTopN] + ); + + const renderContent = useCallback( + () => ( + + + ( + +
- {render(dataProvider, provided, snapshot)} - -
-
- )} - > - {droppableProvided => ( -
- - {(provided, snapshot) => ( - - {truncate && !snapshot.isDragging ? ( - - {render(dataProvider, provided, snapshot)} - - ) : ( - - {render(dataProvider, provided, snapshot)} - - )} - - )} - - {droppableProvided.placeholder} -
- )} -
-
-
+ {render(dataProvider, provided, snapshot)} +
+
+
+ )} + > + {droppableProvided => ( +
+ + {(provided, snapshot) => ( + + {truncate && !snapshot.isDragging ? ( + + {render(dataProvider, provided, snapshot)} + + ) : ( + + {render(dataProvider, provided, snapshot)} + + )} + + )} + + {droppableProvided.placeholder} +
+ )} +
+
+
+ ), + [dataProvider, render, registerProvider, truncate] + ); + + return ( + ); }, (prevProps, nextProps) => diff --git a/x-pack/legacy/plugins/siem/public/components/drag_and_drop/draggable_wrapper_hover_content.test.tsx b/x-pack/legacy/plugins/siem/public/components/drag_and_drop/draggable_wrapper_hover_content.test.tsx new file mode 100644 index 0000000000000..f8b5eb7209ff4 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/components/drag_and_drop/draggable_wrapper_hover_content.test.tsx @@ -0,0 +1,559 @@ +/* + * 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 { mount, ReactWrapper } from 'enzyme'; +import React from 'react'; +import { MockedProvider } from 'react-apollo/test-utils'; + +import { mocksSource } from '../../containers/source/mock'; +import { wait } from '../../lib/helpers'; +import { useKibana } from '../../lib/kibana'; +import { TestProviders } from '../../mock'; +import { createKibanaCoreStartMock } from '../../mock/kibana_core'; +import { FilterManager } from '../../../../../../../src/plugins/data/public'; +import { TimelineContext } from '../timeline/timeline_context'; + +import { DraggableWrapperHoverContent } from './draggable_wrapper_hover_content'; + +jest.mock('../../lib/kibana'); + +jest.mock('uuid', () => { + return { + v1: jest.fn(() => 'uuid.v1()'), + v4: jest.fn(() => 'uuid.v4()'), + }; +}); + +const mockUiSettingsForFilterManager = createKibanaCoreStartMock().uiSettings; +const field = 'process.name'; +const value = 'nice'; + +describe('DraggableWrapperHoverContent', () => { + // Suppress warnings about "react-beautiful-dnd" + /* eslint-disable no-console */ + const originalError = console.error; + const originalWarn = console.warn; + beforeAll(() => { + console.warn = jest.fn(); + console.error = jest.fn(); + }); + afterAll(() => { + console.error = originalError; + console.warn = originalWarn; + }); + + /** + * The tests for "Filter for value" and "Filter out value" are similar enough + * to combine them into "table tests" using this array + */ + const forOrOut = ['for', 'out']; + + forOrOut.forEach(hoverAction => { + describe(`Filter ${hoverAction} value`, () => { + test(`it renders the 'Filter ${hoverAction} value' button when showTopN is false`, () => { + const wrapper = mount( + + + + ); + + expect( + wrapper + .find(`[data-test-subj="filter-${hoverAction}-value"]`) + .first() + .exists() + ).toBe(true); + }); + + test(`it does NOT render the 'Filter ${hoverAction} value' button when showTopN is true`, () => { + const wrapper = mount( + + + + ); + + expect( + wrapper + .find(`[data-test-subj="filter-${hoverAction}-value"]`) + .first() + .exists() + ).toBe(false); + }); + + describe('when run in the context of a timeline', () => { + let filterManager: FilterManager; + let wrapper: ReactWrapper; + let onFilterAdded: () => void; + + beforeEach(() => { + filterManager = new FilterManager(mockUiSettingsForFilterManager); + filterManager.addFilters = jest.fn(); + onFilterAdded = jest.fn(); + + wrapper = mount( + + + + + + ); + }); + + test('when clicked, it adds a filter to the timeline when running in the context of a timeline', () => { + wrapper + .find(`[data-test-subj="filter-${hoverAction}-value"]`) + .first() + .simulate('click'); + wrapper.update(); + + expect(filterManager.addFilters).toBeCalledWith({ + meta: { + alias: null, + disabled: false, + key: 'process.name', + negate: hoverAction === 'out' ? true : false, + params: { query: 'nice' }, + type: 'phrase', + value: 'nice', + }, + query: { match: { 'process.name': { query: 'nice', type: 'phrase' } } }, + }); + }); + + test('when clicked, invokes onFilterAdded when running in the context of a timeline', () => { + wrapper + .find(`[data-test-subj="filter-${hoverAction}-value"]`) + .first() + .simulate('click'); + wrapper.update(); + + expect(onFilterAdded).toBeCalled(); + }); + }); + + describe('when NOT run in the context of a timeline', () => { + let wrapper: ReactWrapper; + let onFilterAdded: () => void; + const kibana = useKibana(); + + beforeEach(() => { + kibana.services.data.query.filterManager.addFilters = jest.fn(); + onFilterAdded = jest.fn(); + + wrapper = mount( + + + + ); + }); + + test('when clicked, it adds a filter to the global filters when NOT running in the context of a timeline', () => { + wrapper + .find(`[data-test-subj="filter-${hoverAction}-value"]`) + .first() + .simulate('click'); + wrapper.update(); + + expect(kibana.services.data.query.filterManager.addFilters).toBeCalledWith({ + meta: { + alias: null, + disabled: false, + key: 'process.name', + negate: hoverAction === 'out' ? true : false, + params: { query: 'nice' }, + type: 'phrase', + value: 'nice', + }, + query: { match: { 'process.name': { query: 'nice', type: 'phrase' } } }, + }); + }); + + test('when clicked, invokes onFilterAdded when NOT running in the context of a timeline', () => { + wrapper + .find(`[data-test-subj="filter-${hoverAction}-value"]`) + .first() + .simulate('click'); + wrapper.update(); + + expect(onFilterAdded).toBeCalled(); + }); + }); + + describe('an empty string value when run in the context of a timeline', () => { + let filterManager: FilterManager; + let wrapper: ReactWrapper; + let onFilterAdded: () => void; + + beforeEach(() => { + filterManager = new FilterManager(mockUiSettingsForFilterManager); + filterManager.addFilters = jest.fn(); + onFilterAdded = jest.fn(); + + wrapper = mount( + + + + + + ); + }); + + const expectedFilterTypeDescription = + hoverAction === 'for' ? 'a "NOT exists"' : 'an "exists"'; + test(`when clicked, it adds ${expectedFilterTypeDescription} filter to the timeline when run in the context of a timeline`, () => { + const expected = + hoverAction === 'for' + ? { + exists: { field: 'process.name' }, + meta: { + alias: null, + disabled: false, + key: 'process.name', + negate: true, + type: 'exists', + value: 'exists', + }, + } + : { + exists: { field: 'process.name' }, + meta: { + alias: null, + disabled: false, + key: 'process.name', + negate: false, + type: 'exists', + value: 'exists', + }, + }; + + wrapper + .find(`[data-test-subj="filter-${hoverAction}-value"]`) + .first() + .simulate('click'); + wrapper.update(); + + expect(filterManager.addFilters).toBeCalledWith(expected); + }); + }); + + describe('an empty string value when NOT run in the context of a timeline', () => { + let wrapper: ReactWrapper; + let onFilterAdded: () => void; + const kibana = useKibana(); + + beforeEach(() => { + kibana.services.data.query.filterManager.addFilters = jest.fn(); + onFilterAdded = jest.fn(); + + wrapper = mount( + + + + ); + }); + + const expectedFilterTypeDescription = + hoverAction === 'for' ? 'a "NOT exists"' : 'an "exists"'; + test(`when clicked, it adds ${expectedFilterTypeDescription} filter to the global filters when NOT running in the context of a timeline`, () => { + const expected = + hoverAction === 'for' + ? { + exists: { field: 'process.name' }, + meta: { + alias: null, + disabled: false, + key: 'process.name', + negate: true, + type: 'exists', + value: 'exists', + }, + } + : { + exists: { field: 'process.name' }, + meta: { + alias: null, + disabled: false, + key: 'process.name', + negate: false, + type: 'exists', + value: 'exists', + }, + }; + + wrapper + .find(`[data-test-subj="filter-${hoverAction}-value"]`) + .first() + .simulate('click'); + wrapper.update(); + + expect(kibana.services.data.query.filterManager.addFilters).toBeCalledWith(expected); + }); + }); + }); + }); + + describe('Top N', () => { + test(`it renders the 'Show top field' button when showTopN is false and an aggregatable string field is provided`, async () => { + const aggregatableStringField = 'cloud.account.id'; + const wrapper = mount( + + + + + + ); + + await wait(); // https://github.com/apollographql/react-apollo/issues/1711 + wrapper.update(); + + expect( + wrapper + .find('[data-test-subj="show-top-field"]') + .first() + .exists() + ).toBe(true); + }); + + test(`it renders the 'Show top field' button when showTopN is false and a whitelisted signal field is provided`, async () => { + const whitelistedField = 'signal.rule.name'; + const wrapper = mount( + + + + + + ); + + await wait(); + wrapper.update(); + + expect( + wrapper + .find('[data-test-subj="show-top-field"]') + .first() + .exists() + ).toBe(true); + }); + + test(`it does NOT render the 'Show top field' button when showTopN is false and a field not known to BrowserFields is provided`, async () => { + const notKnownToBrowserFields = 'unknown.field'; + const wrapper = mount( + + + + + + ); + + await wait(); + wrapper.update(); + + expect( + wrapper + .find('[data-test-subj="show-top-field"]') + .first() + .exists() + ).toBe(false); + }); + + test(`invokes the toggleTopN function when the 'Show top field' button is clicked`, async () => { + const toggleTopN = jest.fn(); + const whitelistedField = 'signal.rule.name'; + const wrapper = mount( + + + + + + ); + + await wait(); + wrapper.update(); + + wrapper + .find('[data-test-subj="show-top-field"]') + .first() + .simulate('click'); + wrapper.update(); + + expect(toggleTopN).toBeCalled(); + }); + + test(`it does NOT render the Top N histogram when when showTopN is false`, async () => { + const whitelistedField = 'signal.rule.name'; + const wrapper = mount( + + + + + + ); + + await wait(); + wrapper.update(); + + expect( + wrapper + .find('[data-test-subj="eventsByDatasetOverviewPanel"]') + .first() + .exists() + ).toBe(false); + }); + + test(`it does NOT render the 'Show top field' button when showTopN is true`, async () => { + const whitelistedField = 'signal.rule.name'; + const wrapper = mount( + + + + + + ); + + await wait(); + wrapper.update(); + + expect( + wrapper + .find('[data-test-subj="show-top-field"]') + .first() + .exists() + ).toBe(false); + }); + + test(`it renders the Top N histogram when when showTopN is true`, async () => { + const whitelistedField = 'signal.rule.name'; + const wrapper = mount( + + + + + + ); + + await wait(); + wrapper.update(); + + expect( + wrapper + .find('[data-test-subj="eventsByDatasetOverview-uuid.v4()Panel"]') + .first() + .exists() + ).toBe(true); + }); + }); + + describe('Copy to Clipboard', () => { + test(`it renders the 'Copy to Clipboard' button when showTopN is false`, () => { + const wrapper = mount( + + + + ); + + expect( + wrapper + .find(`[data-test-subj="copy-to-clipboard"]`) + .first() + .exists() + ).toBe(true); + }); + + test(`it does NOT render the 'Copy to Clipboard' button when showTopN is true`, () => { + const wrapper = mount( + + + + ); + + expect( + wrapper + .find(`[data-test-subj="copy-to-clipboard"]`) + .first() + .exists() + ).toBe(false); + }); + }); +}); diff --git a/x-pack/legacy/plugins/siem/public/components/drag_and_drop/draggable_wrapper_hover_content.tsx b/x-pack/legacy/plugins/siem/public/components/drag_and_drop/draggable_wrapper_hover_content.tsx new file mode 100644 index 0000000000000..40725bea498f1 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/components/drag_and_drop/draggable_wrapper_hover_content.tsx @@ -0,0 +1,145 @@ +/* + * 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 { EuiButtonIcon, EuiToolTip } from '@elastic/eui'; +import React, { useCallback, useMemo } from 'react'; + +import { getAllFieldsByName, WithSource } from '../../containers/source'; +import { WithCopyToClipboard } from '../../lib/clipboard/with_copy_to_clipboard'; +import { useKibana } from '../../lib/kibana'; +import { createFilter } from '../page/add_filter_to_global_search_bar'; +import { useTimelineContext } from '../timeline/timeline_context'; +import { StatefulTopN } from '../top_n'; + +import { allowTopN } from './helpers'; +import * as i18n from './translations'; + +interface Props { + field: string; + onFilterAdded?: () => void; + showTopN: boolean; + toggleTopN: () => void; + value?: string[] | string | null; +} + +const DraggableWrapperHoverContentComponent: React.FC = ({ + field, + onFilterAdded, + showTopN, + toggleTopN, + value, +}) => { + const kibana = useKibana(); + const { filterManager: timelineFilterManager } = useTimelineContext(); + const filterManager = useMemo(() => kibana.services.data.query.filterManager, [ + kibana.services.data.query.filterManager, + ]); + + const filterForValue = useCallback(() => { + const filter = + value?.length === 0 ? createFilter(field, undefined) : createFilter(field, value); + const activeFilterManager = timelineFilterManager ?? filterManager; + + if (activeFilterManager != null) { + activeFilterManager.addFilters(filter); + + if (onFilterAdded != null) { + onFilterAdded(); + } + } + }, [field, value, timelineFilterManager, filterManager, onFilterAdded]); + + const filterOutValue = useCallback(() => { + const filter = + value?.length === 0 ? createFilter(field, null, false) : createFilter(field, value, true); + const activeFilterManager = timelineFilterManager ?? filterManager; + + if (activeFilterManager != null) { + activeFilterManager.addFilters(filter); + + if (onFilterAdded != null) { + onFilterAdded(); + } + } + }, [field, value, timelineFilterManager, filterManager, onFilterAdded]); + + return ( + <> + {!showTopN && value != null && ( + + + + )} + + {!showTopN && value != null && ( + + + + )} + + + {({ browserFields }) => ( + <> + {allowTopN({ + browserField: getAllFieldsByName(browserFields)[field], + fieldName: field, + }) && ( + <> + {!showTopN && ( + + + + )} + + {showTopN && ( + + )} + + )} + + )} + + + {!showTopN && ( + + + + )} + + ); +}; + +DraggableWrapperHoverContentComponent.displayName = 'DraggableWrapperHoverContentComponent'; + +export const DraggableWrapperHoverContent = React.memo(DraggableWrapperHoverContentComponent); diff --git a/x-pack/legacy/plugins/siem/public/components/drag_and_drop/helpers.test.ts b/x-pack/legacy/plugins/siem/public/components/drag_and_drop/helpers.test.ts index af4b9b280f3cd..753fa5b54eade 100644 --- a/x-pack/legacy/plugins/siem/public/components/drag_and_drop/helpers.test.ts +++ b/x-pack/legacy/plugins/siem/public/components/drag_and_drop/helpers.test.ts @@ -4,7 +4,10 @@ * you may not use this file except in compliance with the Elastic License. */ +import { omit } from 'lodash/fp'; + import { + allowTopN, destinationIsTimelineButton, destinationIsTimelineColumns, destinationIsTimelineProviders, @@ -717,4 +720,96 @@ describe('helpers', () => { expect(escaped).toEqual('hello.how.are.you?'); }); }); + + describe('#allowTopN', () => { + const aggregatableAllowedType = { + category: 'cloud', + description: + 'The cloud account or organization id used to identify different entities in a multi-tenant environment. Examples: AWS account id, Google Cloud ORG Id, or other unique identifier.', + example: '666777888999', + indexes: ['auditbeat', 'filebeat', 'packetbeat'], + name: 'cloud.account.id', + searchable: true, + type: 'string', + aggregatable: true, + format: '', + }; + + test('it returns true for an aggregatable field that is an allowed type', () => { + expect( + allowTopN({ + browserField: aggregatableAllowedType, + fieldName: aggregatableAllowedType.name, + }) + ).toBe(true); + }); + + test('it returns true for a whitelisted non-BrowserField', () => { + expect( + allowTopN({ + browserField: undefined, + fieldName: 'signal.rule.name', + }) + ).toBe(true); + }); + + test('it returns false for a NON-aggregatable field that is an allowed type', () => { + const nonAggregatableAllowedType = { + ...aggregatableAllowedType, + aggregatable: false, + }; + + expect( + allowTopN({ + browserField: nonAggregatableAllowedType, + fieldName: nonAggregatableAllowedType.name, + }) + ).toBe(false); + }); + + test('it returns false for a aggregatable field that is NOT an allowed type', () => { + const aggregatableNotAllowedType = { + ...aggregatableAllowedType, + type: 'not-an-allowed-type', + }; + + expect( + allowTopN({ + browserField: aggregatableNotAllowedType, + fieldName: aggregatableNotAllowedType.name, + }) + ).toBe(false); + }); + + test('it returns false if the BrowserField is missing the aggregatable property', () => { + const missingAggregatable = omit('aggregatable', aggregatableAllowedType); + + expect( + allowTopN({ + browserField: missingAggregatable, + fieldName: missingAggregatable.name, + }) + ).toBe(false); + }); + + test('it returns false if the BrowserField is missing the type property', () => { + const missingType = omit('type', aggregatableAllowedType); + + expect( + allowTopN({ + browserField: missingType, + fieldName: missingType.name, + }) + ).toBe(false); + }); + + test('it returns false for a non-whitelisted field when a BrowserField is not provided', () => { + expect( + allowTopN({ + browserField: undefined, + fieldName: 'non-whitelisted', + }) + ).toBe(false); + }); + }); }); diff --git a/x-pack/legacy/plugins/siem/public/components/drag_and_drop/helpers.ts b/x-pack/legacy/plugins/siem/public/components/drag_and_drop/helpers.ts index 82ddd2c9f29d7..cd3d7cc68d537 100644 --- a/x-pack/legacy/plugins/siem/public/components/drag_and_drop/helpers.ts +++ b/x-pack/legacy/plugins/siem/public/components/drag_and_drop/helpers.ts @@ -9,7 +9,7 @@ import { DropResult } from 'react-beautiful-dnd'; import { Dispatch } from 'redux'; import { ActionCreator } from 'typescript-fsa'; -import { BrowserFields, getAllFieldsByName } from '../../containers/source'; +import { BrowserField, BrowserFields, getAllFieldsByName } from '../../containers/source'; import { IdToDataProvider } from '../../store/drag_and_drop/model'; import { ColumnHeaderOptions } from '../../store/timeline/model'; import { DEFAULT_COLUMN_MIN_WIDTH } from '../timeline/body/constants'; @@ -227,3 +227,98 @@ export const IS_DRAGGING_CLASS_NAME = 'is-dragging'; /** This class is added to the document body while timeline field dragging */ export const IS_TIMELINE_FIELD_DRAGGING_CLASS_NAME = 'is-timeline-field-dragging'; + +export const allowTopN = ({ + browserField, + fieldName, +}: { + browserField: Partial | undefined; + fieldName: string; +}): boolean => { + const isAggregatable = browserField?.aggregatable ?? false; + const fieldType = browserField?.type ?? ''; + const isAllowedType = [ + 'boolean', + 'geo-point', + 'geo-shape', + 'ip', + 'keyword', + 'number', + 'numeric', + 'string', + ].includes(fieldType); + + // TODO: remove this explicit whitelist when the ECS documentation includes signals + const isWhitelistedNonBrowserField = [ + 'signal.ancestors.depth', + 'signal.ancestors.id', + 'signal.ancestors.rule', + 'signal.ancestors.type', + 'signal.original_event.action', + 'signal.original_event.category', + 'signal.original_event.code', + 'signal.original_event.created', + 'signal.original_event.dataset', + 'signal.original_event.duration', + 'signal.original_event.end', + 'signal.original_event.hash', + 'signal.original_event.id', + 'signal.original_event.kind', + 'signal.original_event.module', + 'signal.original_event.original', + 'signal.original_event.outcome', + 'signal.original_event.provider', + 'signal.original_event.risk_score', + 'signal.original_event.risk_score_norm', + 'signal.original_event.sequence', + 'signal.original_event.severity', + 'signal.original_event.start', + 'signal.original_event.timezone', + 'signal.original_event.type', + 'signal.original_time', + 'signal.parent.depth', + 'signal.parent.id', + 'signal.parent.index', + 'signal.parent.rule', + 'signal.parent.type', + 'signal.rule.created_by', + 'signal.rule.description', + 'signal.rule.enabled', + 'signal.rule.false_positives', + 'signal.rule.filters', + 'signal.rule.from', + 'signal.rule.id', + 'signal.rule.immutable', + 'signal.rule.index', + 'signal.rule.interval', + 'signal.rule.language', + 'signal.rule.max_signals', + 'signal.rule.name', + 'signal.rule.note', + 'signal.rule.output_index', + 'signal.rule.query', + 'signal.rule.references', + 'signal.rule.risk_score', + 'signal.rule.rule_id', + 'signal.rule.saved_id', + 'signal.rule.severity', + 'signal.rule.size', + 'signal.rule.tags', + 'signal.rule.threat', + 'signal.rule.threat.tactic.id', + 'signal.rule.threat.tactic.name', + 'signal.rule.threat.tactic.reference', + 'signal.rule.threat.technique.id', + 'signal.rule.threat.technique.name', + 'signal.rule.threat.technique.reference', + 'signal.rule.timeline_id', + 'signal.rule.timeline_title', + 'signal.rule.to', + 'signal.rule.type', + 'signal.rule.updated_by', + 'signal.rule.version', + 'signal.status', + ].includes(fieldName); + + return isWhitelistedNonBrowserField || (isAggregatable && isAllowedType); +}; diff --git a/x-pack/legacy/plugins/siem/public/components/drag_and_drop/translations.ts b/x-pack/legacy/plugins/siem/public/components/drag_and_drop/translations.ts new file mode 100644 index 0000000000000..61d036635a250 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/components/drag_and_drop/translations.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 { i18n } from '@kbn/i18n'; + +export const COPY_TO_CLIPBOARD = i18n.translate('xpack.siem.dragAndDrop.copyToClipboardTooltip', { + defaultMessage: 'Copy to Clipboard', +}); + +export const FIELD = i18n.translate('xpack.siem.dragAndDrop.fieldLabel', { + defaultMessage: 'Field', +}); + +export const FILTER_FOR_VALUE = i18n.translate('xpack.siem.dragAndDrop.filterForValueHoverAction', { + defaultMessage: 'Filter for value', +}); + +export const FILTER_OUT_VALUE = i18n.translate('xpack.siem.dragAndDrop.filterOutValueHoverAction', { + defaultMessage: 'Filter out value', +}); + +export const CLOSE = i18n.translate('xpack.siem.dragAndDrop.closeButtonLabel', { + defaultMessage: 'Close', +}); + +export const SHOW_TOP = (fieldName: string) => + i18n.translate('xpack.siem.overview.showTopTooltip', { + values: { fieldName }, + defaultMessage: `Show top {fieldName}`, + }); diff --git a/x-pack/legacy/plugins/siem/public/components/draggables/__snapshots__/index.test.tsx.snap b/x-pack/legacy/plugins/siem/public/components/draggables/__snapshots__/index.test.tsx.snap index 63ba13306ecd8..93608a181adff 100644 --- a/x-pack/legacy/plugins/siem/public/components/draggables/__snapshots__/index.test.tsx.snap +++ b/x-pack/legacy/plugins/siem/public/components/draggables/__snapshots__/index.test.tsx.snap @@ -10,6 +10,7 @@ exports[`draggables rendering it renders the default Badge 1`] = ` A child of this diff --git a/x-pack/legacy/plugins/siem/public/components/draggables/index.tsx b/x-pack/legacy/plugins/siem/public/components/draggables/index.tsx index 1fe6c936d2823..b3811d05eea04 100644 --- a/x-pack/legacy/plugins/siem/public/components/draggables/index.tsx +++ b/x-pack/legacy/plugins/siem/public/components/draggables/index.tsx @@ -8,7 +8,6 @@ import { EuiBadge, EuiToolTip, IconType } from '@elastic/eui'; import React from 'react'; import styled from 'styled-components'; -import { Omit } from '../../../common/utility_types'; import { DragEffects, DraggableWrapper } from '../drag_and_drop/draggable_wrapper'; import { escapeDataProviderId } from '../drag_and_drop/helpers'; import { getEmptyStringTag } from '../empty_value'; @@ -167,7 +166,7 @@ export const DraggableBadge = React.memo( tooltipContent={tooltipContent} queryValue={queryValue} > - + {children ? children : value !== '' ? value : getEmptyStringTag()} diff --git a/x-pack/legacy/plugins/siem/public/components/embeddables/embedded_map.tsx b/x-pack/legacy/plugins/siem/public/components/embeddables/embedded_map.tsx index cbb4006bbf933..a7272593c2b27 100644 --- a/x-pack/legacy/plugins/siem/public/components/embeddables/embedded_map.tsx +++ b/x-pack/legacy/plugins/siem/public/components/embeddables/embedded_map.tsx @@ -14,7 +14,7 @@ import { EmbeddablePanel, ErrorEmbeddable, } from '../../../../../../../src/plugins/embeddable/public'; -import { DEFAULT_INDEX_KEY } from '../../../common/constants'; +import { DEFAULT_INDEX_KEY } from '../../../../../../plugins/siem/common/constants'; import { getIndexPatternTitleIdMapping } from '../../hooks/api/helpers'; import { useIndexPatterns } from '../../hooks/use_index_patterns'; import { Loader } from '../loader'; diff --git a/x-pack/legacy/plugins/siem/public/components/embeddables/map_config.ts b/x-pack/legacy/plugins/siem/public/components/embeddables/map_config.ts index e8b267122f86f..8c96e0b75a136 100644 --- a/x-pack/legacy/plugins/siem/public/components/embeddables/map_config.ts +++ b/x-pack/legacy/plugins/siem/public/components/embeddables/map_config.ts @@ -13,6 +13,7 @@ import { LayerMappingDetails, } from './types'; import * as i18n from './translations'; +import { SOURCE_TYPES } from '../../../../../../plugins/maps/common/constants'; const euiVisColorPalette = euiPaletteColorBlind(); // Update field mappings to modify what fields will be returned to map tooltip @@ -101,7 +102,7 @@ export const lmc: LayerMappingCollection = { export const getLayerList = (indexPatternIds: IndexPatternMapping[]) => { return [ { - sourceDescriptor: { type: 'EMS_TMS', isAutoSelect: true }, + sourceDescriptor: { type: SOURCE_TYPES.EMS_TMS, isAutoSelect: true }, id: uuid.v4(), label: null, minZoom: 0, @@ -260,7 +261,7 @@ export const getLineLayer = ( layerDetails: LayerMapping ) => ({ sourceDescriptor: { - type: 'ES_PEW_PEW', + type: SOURCE_TYPES.ES_PEW_PEW, applyGlobalQuery: true, id: uuid.v4(), indexPatternId, diff --git a/x-pack/legacy/plugins/siem/public/components/event_details/columns.tsx b/x-pack/legacy/plugins/siem/public/components/event_details/columns.tsx index cd94a9fdcb5ac..131a3a63bae30 100644 --- a/x-pack/legacy/plugins/siem/public/components/event_details/columns.tsx +++ b/x-pack/legacy/plugins/siem/public/components/event_details/columns.tsx @@ -21,7 +21,6 @@ import styled from 'styled-components'; import { BrowserFields } from '../../containers/source'; import { ToStringArray } from '../../graphql/types'; -import { WithCopyToClipboard } from '../../lib/clipboard/with_copy_to_clipboard'; import { ColumnHeaderOptions } from '../../store/timeline/model'; import { DragEffects } from '../drag_and_drop/draggable_wrapper'; import { DroppableWrapper } from '../drag_and_drop/droppable_wrapper'; @@ -35,7 +34,6 @@ import { DEFAULT_COLUMN_MIN_WIDTH } from '../timeline/body/constants'; import { MESSAGE_FIELD_NAME } from '../timeline/body/renderers/constants'; import { FormattedFieldValue } from '../timeline/body/renderers/formatted_field'; import { OnUpdateColumns } from '../timeline/events'; -import { WithHoverActions } from '../with_hover_actions'; import { getIconFromType, getExampleText, getColumnsWithTimestamp } from './helpers'; import * as i18n from './translations'; import { EventFieldsData } from './types'; @@ -172,29 +170,18 @@ export const getColumns = ({ component="span" key={`event-details-value-flex-item-${contextId}-${eventId}-${data.field}-${i}-${value}`} > - - - - - - } - render={() => - data.field === MESSAGE_FIELD_NAME ? ( - - ) : ( - - ) - } - /> + {data.field === MESSAGE_FIELD_NAME ? ( + + ) : ( + + )} ))}
diff --git a/x-pack/legacy/plugins/siem/public/components/events_viewer/events_viewer.tsx b/x-pack/legacy/plugins/siem/public/components/events_viewer/events_viewer.tsx index ea2cb661763fa..d210c749dae9c 100644 --- a/x-pack/legacy/plugins/siem/public/components/events_viewer/events_viewer.tsx +++ b/x-pack/legacy/plugins/siem/public/components/events_viewer/events_viewer.tsx @@ -95,6 +95,7 @@ const EventsViewerComponent: React.FC = ({ }) => { const columnsHeader = isEmpty(columns) ? defaultHeaders : columns; const kibana = useKibana(); + const { filterManager } = useKibana().services.data.query; const combinedQueries = combineQueries({ config: esQuery.getEsQueryConfig(kibana.services.uiSettings), dataProviders, @@ -168,7 +169,11 @@ const EventsViewerComponent: React.FC = ({ {utilityBar?.(refetch, totalCountMinusDeleted)} - + { }); }); - test('it renders a hover actions panel for the category name', () => { - const wrapper = mount( - - - - ); - expect( - wrapper - .find('[data-test-subj="category-link"]') - .first() - .find('[data-test-subj="hover-actions-panel-container"]') - .first() - .exists() - ).toBe(true); - }); - test('it renders the selected category with bold text', () => { const selectedCategoryId = 'auditd'; diff --git a/x-pack/legacy/plugins/siem/public/components/fields_browser/category_columns.tsx b/x-pack/legacy/plugins/siem/public/components/fields_browser/category_columns.tsx index 0c7dd7e908ce3..7133e9b848c5c 100644 --- a/x-pack/legacy/plugins/siem/public/components/fields_browser/category_columns.tsx +++ b/x-pack/legacy/plugins/siem/public/components/fields_browser/category_columns.tsx @@ -6,15 +6,7 @@ /* eslint-disable react/display-name */ -import { - EuiIcon, - EuiFlexGroup, - EuiFlexItem, - EuiLink, - EuiPanel, - EuiText, - EuiToolTip, -} from '@elastic/eui'; +import { EuiIcon, EuiFlexGroup, EuiFlexItem, EuiLink, EuiText, EuiToolTip } from '@elastic/eui'; import React, { useContext } from 'react'; import styled from 'styled-components'; @@ -35,22 +27,6 @@ const CategoryName = styled.span<{ bold: boolean }>` CategoryName.displayName = 'CategoryName'; -const HoverActionsContainer = styled(EuiPanel)` - cursor: default; - left: 5px; - padding: 8px; - position: absolute; - top: -8px; -`; - -HoverActionsContainer.displayName = 'HoverActionsContainer'; - -const HoverActionsFlexGroup = styled(EuiFlexGroup)` - cursor: pointer; -`; - -HoverActionsFlexGroup.displayName = 'HoverActionsFlexGroup'; - const LinkContainer = styled.div` width: 100%; .euiLink { @@ -71,7 +47,7 @@ interface ToolTipProps { } const ToolTip = React.memo(({ categoryId, browserFields, onUpdateColumns }) => { - const isLoading = useContext(TimelineContext); + const { isLoading } = useContext(TimelineContext); return ( {!isLoading ? ( @@ -127,25 +103,11 @@ export const getCategoryColumns = ({ - - - - - - + } render={() => ( { expect(wrapper.find('[data-test-subj="copy-to-clipboard"]').exists()).toBe(true); }); - test('it renders a view category action menu item a user hovers over the name', () => { - const wrapper = mount( - - - - ); - - wrapper.simulate('mouseenter'); - wrapper.update(); - expect(wrapper.find('[data-test-subj="view-category"]').exists()).toBe(true); - }); - - test('it invokes onUpdateColumns when the view category action menu item is clicked', () => { - const onUpdateColumns = jest.fn(); - - const wrapper = mount( - - - - ); - - wrapper.simulate('mouseenter'); - wrapper.update(); - wrapper - .find('[data-test-subj="view-category"]') - .first() - .simulate('click'); - - expect(onUpdateColumns).toBeCalledWith([ - { - aggregatable: true, - category: 'base', - columnHeaderType: 'not-filtered', - description: - 'Date/time when the event originated. For log events this is the date/time when the event was generated, and not when it was read. Required field for all events.', - example: '2016-05-23T08:05:34.853Z', - id: '@timestamp', - type: 'date', - width: 190, - }, - ]); - }); - test('it highlights the text specified by the `highlight` prop', () => { const highlight = 'stamp'; diff --git a/x-pack/legacy/plugins/siem/public/components/fields_browser/field_name.tsx b/x-pack/legacy/plugins/siem/public/components/fields_browser/field_name.tsx index fe434a6ad63ce..fc9633b6f8748 100644 --- a/x-pack/legacy/plugins/siem/public/components/fields_browser/field_name.tsx +++ b/x-pack/legacy/plugins/siem/public/components/fields_browser/field_name.tsx @@ -4,26 +4,17 @@ * you may not use this file except in compliance with the Elastic License. */ -import { - EuiButtonIcon, - EuiHighlight, - EuiIcon, - EuiFlexGroup, - EuiFlexItem, - EuiPanel, - EuiText, - EuiToolTip, -} from '@elastic/eui'; -import React, { useContext } from 'react'; +import { EuiButtonIcon, EuiHighlight, EuiIcon, EuiText, EuiToolTip } from '@elastic/eui'; +import React, { useCallback, useContext, useState, useMemo } from 'react'; import styled from 'styled-components'; -import { WithCopyToClipboard } from '../../lib/clipboard/with_copy_to_clipboard'; import { ColumnHeaderOptions } from '../../store/timeline/model'; import { OnUpdateColumns } from '../timeline/events'; import { TimelineContext } from '../timeline/timeline_context'; import { WithHoverActions } from '../with_hover_actions'; import { LoadingSpinner } from './helpers'; import * as i18n from './translations'; +import { DraggableWrapperHoverContent } from '../drag_and_drop/draggable_wrapper_hover_content'; /** * The name of a (draggable) field @@ -82,22 +73,6 @@ export const FieldNameContainer = styled.span` FieldNameContainer.displayName = 'FieldNameContainer'; -const HoverActionsContainer = styled(EuiPanel)` - cursor: default; - left: 5px; - padding: 4px; - position: absolute; - top: -6px; -`; - -HoverActionsContainer.displayName = 'HoverActionsContainer'; - -const HoverActionsFlexGroup = styled(EuiFlexGroup)` - cursor: pointer; -`; - -HoverActionsFlexGroup.displayName = 'HoverActionsFlexGroup'; - const ViewCategoryIcon = styled(EuiIcon)` margin-left: 5px; `; @@ -112,7 +87,7 @@ interface ToolTipProps { const ViewCategory = React.memo( ({ categoryId, onUpdateColumns, categoryColumns }) => { - const isLoading = useContext(TimelineContext); + const { isLoading } = useContext(TimelineContext); return ( {!isLoading ? ( @@ -142,48 +117,33 @@ export const FieldName = React.memo<{ fieldId: string; highlight?: string; onUpdateColumns: OnUpdateColumns; -}>(({ categoryId, categoryColumns, fieldId, highlight = '', onUpdateColumns }) => ( - - - - - - - - - {categoryColumns.length > 0 && ( - - - - )} - - - } - render={() => ( - - +}>(({ fieldId, highlight = '' }) => { + const [showTopN, setShowTopN] = useState(false); + const toggleTopN = useCallback(() => { + setShowTopN(!showTopN); + }, [setShowTopN, showTopN]); + + const hoverContent = useMemo( + () => ( + + ), + [fieldId, showTopN, toggleTopN] + ); + + const render = useCallback( + () => ( + + {fieldId} - - - )} - /> -)); + + + ), + [fieldId, highlight] + ); + + return ; +}); FieldName.displayName = 'FieldName'; diff --git a/x-pack/legacy/plugins/siem/public/components/filter_popover/index.tsx b/x-pack/legacy/plugins/siem/public/components/filter_popover/index.tsx index 3c01ec18a879f..fca6396a53745 100644 --- a/x-pack/legacy/plugins/siem/public/components/filter_popover/index.tsx +++ b/x-pack/legacy/plugins/siem/public/components/filter_popover/index.tsx @@ -89,6 +89,7 @@ export const FilterPopoverComponent = ({ {options.map((option, index) => ( diff --git a/x-pack/legacy/plugins/siem/public/components/formatted_bytes/index.tsx b/x-pack/legacy/plugins/siem/public/components/formatted_bytes/index.tsx index 98a1acf471629..abde602c1bdac 100644 --- a/x-pack/legacy/plugins/siem/public/components/formatted_bytes/index.tsx +++ b/x-pack/legacy/plugins/siem/public/components/formatted_bytes/index.tsx @@ -7,7 +7,7 @@ import React from 'react'; import numeral from '@elastic/numeral'; -import { DEFAULT_BYTES_FORMAT } from '../../../common/constants'; +import { DEFAULT_BYTES_FORMAT } from '../../../../../../plugins/siem/common/constants'; import { useUiSetting$ } from '../../lib/kibana'; type Bytes = string | number; diff --git a/x-pack/legacy/plugins/siem/public/components/header_page/editable_title.tsx b/x-pack/legacy/plugins/siem/public/components/header_page/editable_title.tsx index 165be00384779..0c6f7258d09dc 100644 --- a/x-pack/legacy/plugins/siem/public/components/header_page/editable_title.tsx +++ b/x-pack/legacy/plugins/siem/public/components/header_page/editable_title.tsx @@ -60,12 +60,9 @@ const EditableTitleComponent: React.FC = ({ }, [changedTitle, title]); const handleOnChange = useCallback( - (e: ChangeEvent) => { - onTitleChange(e.target.value); - }, - [onTitleChange] + (e: ChangeEvent) => onTitleChange(e.target.value), + [] ); - return editMode ? ( @@ -107,7 +104,7 @@ const EditableTitleComponent: React.FC = ({ </EuiFlexItem> <EuiFlexItem grow={false}> - {isLoading && <MySpinner />} + {isLoading && <MySpinner data-test-subj="editable-title-loading" />} {!isLoading && ( <MyEuiButtonIcon isDisabled={disabled} diff --git a/x-pack/legacy/plugins/siem/public/components/header_page/title.tsx b/x-pack/legacy/plugins/siem/public/components/header_page/title.tsx index a1f3cfd857148..59039ddd6a23b 100644 --- a/x-pack/legacy/plugins/siem/public/components/header_page/title.tsx +++ b/x-pack/legacy/plugins/siem/public/components/header_page/title.tsx @@ -51,7 +51,9 @@ const TitleComponent: React.FC<Props> = ({ draggableArguments, title, badgeOptio tooltipPosition="bottom" /> ) : ( - <Badge color="hollow">{badgeOptions.text}</Badge> + <Badge color="hollow" title=""> + {badgeOptions.text} + </Badge> )} </> )} diff --git a/x-pack/legacy/plugins/siem/public/components/header_section/__snapshots__/index.test.tsx.snap b/x-pack/legacy/plugins/siem/public/components/header_section/__snapshots__/index.test.tsx.snap index d4c3763f51460..53b41e2240de2 100644 --- a/x-pack/legacy/plugins/siem/public/components/header_section/__snapshots__/index.test.tsx.snap +++ b/x-pack/legacy/plugins/siem/public/components/header_section/__snapshots__/index.test.tsx.snap @@ -11,13 +11,18 @@ exports[`HeaderSection it renders 1`] = ` responsive={false} > <EuiFlexItem> - <EuiTitle> + <EuiTitle + size="m" + > <h2 data-test-subj="header-section-title" > Test title </h2> </EuiTitle> + <Subtitle + data-test-subj="header-section-subtitle" + /> </EuiFlexItem> </EuiFlexGroup> </EuiFlexItem> diff --git a/x-pack/legacy/plugins/siem/public/components/header_section/index.test.tsx b/x-pack/legacy/plugins/siem/public/components/header_section/index.test.tsx index bc4692b6fe0c5..e61b39691203c 100644 --- a/x-pack/legacy/plugins/siem/public/components/header_section/index.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/header_section/index.test.tsx @@ -48,7 +48,7 @@ describe('HeaderSection', () => { ).toBe(true); }); - test('it DOES NOT render the subtitle when not provided', () => { + test('renders the subtitle when not provided (to prevent layout thrash)', () => { const wrapper = mount( <TestProviders> <HeaderSection title="Test title" /> @@ -60,7 +60,7 @@ describe('HeaderSection', () => { .find('[data-test-subj="header-section-subtitle"]') .first() .exists() - ).toBe(false); + ).toBe(true); }); test('it renders supplements when children provided', () => { diff --git a/x-pack/legacy/plugins/siem/public/components/header_section/index.tsx b/x-pack/legacy/plugins/siem/public/components/header_section/index.tsx index 3153e785a8a32..43245121dd393 100644 --- a/x-pack/legacy/plugins/siem/public/components/header_section/index.tsx +++ b/x-pack/legacy/plugins/siem/public/components/header_section/index.tsx @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { EuiFlexGroup, EuiFlexItem, EuiIconTip, EuiTitle } from '@elastic/eui'; +import { EuiFlexGroup, EuiFlexItem, EuiIconTip, EuiTitle, EuiTitleSize } from '@elastic/eui'; import React from 'react'; import styled, { css } from 'styled-components'; @@ -36,6 +36,7 @@ export interface HeaderSectionProps extends HeaderProps { split?: boolean; subtitle?: string | React.ReactNode; title: string | React.ReactNode; + titleSize?: EuiTitleSize; tooltip?: string; } @@ -46,6 +47,7 @@ const HeaderSectionComponent: React.FC<HeaderSectionProps> = ({ split, subtitle, title, + titleSize = 'm', tooltip, }) => ( <Header border={border}> @@ -53,7 +55,7 @@ const HeaderSectionComponent: React.FC<HeaderSectionProps> = ({ <EuiFlexItem> <EuiFlexGroup alignItems="center" responsive={false}> <EuiFlexItem> - <EuiTitle> + <EuiTitle size={titleSize}> <h2 data-test-subj="header-section-title"> {title} {tooltip && ( @@ -65,7 +67,7 @@ const HeaderSectionComponent: React.FC<HeaderSectionProps> = ({ </h2> </EuiTitle> - {subtitle && <Subtitle data-test-subj="header-section-subtitle" items={subtitle} />} + <Subtitle data-test-subj="header-section-subtitle" items={subtitle} /> </EuiFlexItem> {id && ( diff --git a/x-pack/legacy/plugins/siem/public/components/links/index.test.tsx b/x-pack/legacy/plugins/siem/public/components/links/index.test.tsx index d2d1d6569854d..214c0294f2cf4 100644 --- a/x-pack/legacy/plugins/siem/public/components/links/index.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/links/index.test.tsx @@ -24,6 +24,8 @@ import { ExternalLink, } from '.'; +jest.mock('../../pages/overview/events_by_dataset'); + jest.mock('../../lib/kibana', () => { return { useUiSetting$: jest.fn(), diff --git a/x-pack/legacy/plugins/siem/public/components/links/index.tsx b/x-pack/legacy/plugins/siem/public/components/links/index.tsx index 62a67af6e08b1..45225e31e9ac8 100644 --- a/x-pack/legacy/plugins/siem/public/components/links/index.tsx +++ b/x-pack/legacy/plugins/siem/public/components/links/index.tsx @@ -6,9 +6,10 @@ import { EuiLink, EuiToolTip, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; import React, { useMemo } from 'react'; - import { isNil } from 'lodash/fp'; import styled from 'styled-components'; + +import { IP_REPUTATION_LINKS_SETTING } from '../../../../../../plugins/siem/common/constants'; import { DefaultFieldRendererOverflow, DEFAULT_MORE_MAX_HEIGHT, @@ -22,7 +23,6 @@ import { } from '../link_to'; import { FlowTarget, FlowTargetSourceDest } from '../../graphql/types'; import { useUiSetting$ } from '../../lib/kibana'; -import { IP_REPUTATION_LINKS_SETTING } from '../../../common/constants'; import { isUrlInvalid } from '../../pages/detection_engine/rules/components/step_about_rule/helpers'; import { ExternalLinkIcon } from '../external_link_icon'; import { navTabs } from '../../pages/home/home_navigations'; diff --git a/x-pack/legacy/plugins/siem/public/components/matrix_histogram/__snapshots__/index.test.tsx.snap b/x-pack/legacy/plugins/siem/public/components/matrix_histogram/__snapshots__/index.test.tsx.snap index 0e518e48e2e88..5aa846d15b684 100644 --- a/x-pack/legacy/plugins/siem/public/components/matrix_histogram/__snapshots__/index.test.tsx.snap +++ b/x-pack/legacy/plugins/siem/public/components/matrix_histogram/__snapshots__/index.test.tsx.snap @@ -1,5 +1,5 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`Matrix Histogram Component not initial load it renders no MatrixLoader 1`] = `"<div class=\\"sc-AykKF jbBKkl\\"><div class=\\"euiPanel euiPanel--paddingMedium sc-AykKC sc-AykKH iNPult\\" data-test-subj=\\"mockIdPanel\\" height=\\"300\\"><div class=\\"headerSection\\"></div><div class=\\"barchart\\"></div></div></div><div class=\\"euiSpacer euiSpacer--l\\"></div>"`; +exports[`Matrix Histogram Component not initial load it renders no MatrixLoader 1`] = `"<div class=\\"sc-AykKF jbBKkl\\"><div class=\\"euiPanel euiPanel--paddingMedium sc-AykKC sc-AykKH iNPult\\" data-test-subj=\\"mockIdPanel\\" height=\\"300\\"><div class=\\"headerSection\\"></div><div class=\\"barchart\\"></div></div></div>"`; -exports[`Matrix Histogram Component on initial load it renders MatrixLoader 1`] = `"<div class=\\"sc-AykKF hneqJM\\"><div class=\\"euiPanel euiPanel--paddingMedium sc-AykKC sc-AykKH iNPult\\" data-test-subj=\\"mockIdPanel\\" height=\\"300\\"><div class=\\"headerSection\\"></div><div class=\\"matrixLoader\\"></div></div></div><div class=\\"euiSpacer euiSpacer--l\\"></div>"`; +exports[`Matrix Histogram Component on initial load it renders MatrixLoader 1`] = `"<div class=\\"sc-AykKF hneqJM\\"><div class=\\"euiPanel euiPanel--paddingMedium sc-AykKC sc-AykKH iNPult\\" data-test-subj=\\"mockIdPanel\\" height=\\"300\\"><div class=\\"headerSection\\"></div><div class=\\"matrixLoader\\"></div></div></div><div class=\\"euiSpacer euiSpacer--l\\" data-test-subj=\\"spacer\\"></div>"`; diff --git a/x-pack/legacy/plugins/siem/public/components/matrix_histogram/index.test.tsx b/x-pack/legacy/plugins/siem/public/components/matrix_histogram/index.test.tsx index db5b1f7f03ee3..3b8a43a0f395a 100644 --- a/x-pack/legacy/plugins/siem/public/components/matrix_histogram/index.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/matrix_histogram/index.test.tsx @@ -87,6 +87,17 @@ describe('Matrix Histogram Component', () => { }); }); + describe('spacer', () => { + test('it renders a spacer by default', () => { + expect(wrapper.find('[data-test-subj="spacer"]').exists()).toBe(true); + }); + + test('it does NOT render a spacer when showSpacer is false', () => { + wrapper = mount(<MatrixHistogram {...mockMatrixOverTimeHistogramProps} showSpacer={false} />); + expect(wrapper.find('[data-test-subj="spacer"]').exists()).toBe(false); + }); + }); + describe('not initial load', () => { beforeAll(() => { (useQuery as jest.Mock).mockReturnValue({ diff --git a/x-pack/legacy/plugins/siem/public/components/matrix_histogram/index.tsx b/x-pack/legacy/plugins/siem/public/components/matrix_histogram/index.tsx index 12a474009dc5b..3d4eebd68319c 100644 --- a/x-pack/legacy/plugins/siem/public/components/matrix_histogram/index.tsx +++ b/x-pack/legacy/plugins/siem/public/components/matrix_histogram/index.tsx @@ -37,6 +37,7 @@ import { import { SetQuery } from '../../pages/hosts/navigation/types'; import { QueryTemplateProps } from '../../containers/query_template'; import { setAbsoluteRangeDatePicker } from '../../store/inputs/actions'; +import { InputsModelId } from '../../store/inputs/constants'; import { HistogramType } from '../../graphql/types'; export interface OwnProps extends QueryTemplateProps { @@ -46,9 +47,12 @@ export interface OwnProps extends QueryTemplateProps { hideHistogramIfEmpty?: boolean; histogramType: HistogramType; id: string; + indexToAdd?: string[] | null; legendPosition?: Position; mapping?: MatrixHistogramMappingTypes; + showSpacer?: boolean; setQuery: SetQuery; + setAbsoluteRangeDatePickerTarget?: InputsModelId; showLegend?: boolean; stackByOptions: MatrixHistogramOption[]; subtitle?: string | GetSubTitle; @@ -62,6 +66,7 @@ const HeaderChildrenFlexItem = styled(EuiFlexItem)` margin-left: 24px; `; +// @ts-ignore - the EUI type definitions for Panel do no play nice with styled-components const HistogramPanel = styled(Panel)<{ height?: number }>` display: flex; flex-direction: column; @@ -79,16 +84,20 @@ export const MatrixHistogramComponent: React.FC<MatrixHistogramProps & histogramType, hideHistogramIfEmpty = false, id, + indexToAdd, isInspected, legendPosition, mapping, panelHeight = DEFAULT_PANEL_HEIGHT, + setAbsoluteRangeDatePickerTarget = 'global', setQuery, showLegend, + showSpacer = true, stackByOptions, startDate, subtitle, title, + titleSize, dispatchSetAbsoluteRangeDatePicker, yTickFormatter, }) => { @@ -100,7 +109,11 @@ export const MatrixHistogramComponent: React.FC<MatrixHistogramProps & legendPosition, to: endDate, onBrushEnd: (min: number, max: number) => { - dispatchSetAbsoluteRangeDatePicker({ id: 'global', from: min, to: max }); + dispatchSetAbsoluteRangeDatePicker({ + id: setAbsoluteRangeDatePickerTarget, + from: min, + to: max, + }); }, yTickFormatter, showLegend, @@ -122,7 +135,7 @@ export const MatrixHistogramComponent: React.FC<MatrixHistogramProps & const setSelectedChartOptionCallback = useCallback( (event: React.ChangeEvent<HTMLSelectElement>) => { setSelectedStackByOption( - stackByOptions?.find(co => co.value === event.target.value) ?? defaultStackByOption + stackByOptions.find(co => co.value === event.target.value) ?? defaultStackByOption ); }, [] @@ -134,6 +147,7 @@ export const MatrixHistogramComponent: React.FC<MatrixHistogramProps & errorMessage, filterQuery, histogramType, + indexToAdd, startDate, isInspected, stackByField: selectedStackByOption.value, @@ -144,10 +158,17 @@ export const MatrixHistogramComponent: React.FC<MatrixHistogramProps & () => (title != null && typeof title === 'function' ? title(selectedStackByOption) : title), [title, selectedStackByOption] ); - const subtitleWithCounts = useMemo( - () => (subtitle != null && typeof subtitle === 'function' ? subtitle(totalCount) : subtitle), - [subtitle, totalCount] - ); + const subtitleWithCounts = useMemo(() => { + if (isInitialLoading) { + return null; + } + + if (typeof subtitle === 'function') { + return totalCount >= 0 ? subtitle(totalCount) : null; + } + + return subtitle; + }, [isInitialLoading, subtitle, totalCount]); const hideHistogram = useMemo(() => (totalCount <= 0 && hideHistogramIfEmpty ? true : false), [ totalCount, hideHistogramIfEmpty, @@ -155,7 +176,9 @@ export const MatrixHistogramComponent: React.FC<MatrixHistogramProps & const barChartData = useMemo(() => getCustomChartData(data, mapping), [data, mapping]); useEffect(() => { - setQuery({ id, inspect, loading, refetch }); + if (!loading && !isInitialLoading) { + setQuery({ id, inspect, loading, refetch }); + } if (isInitialLoading && !!barChartData && data) { setIsInitialLoading(false); @@ -189,59 +212,39 @@ export const MatrixHistogramComponent: React.FC<MatrixHistogramProps & /> )} + <HeaderSection + id={id} + title={titleWithStackByField} + titleSize={titleSize} + subtitle={subtitleWithCounts} + > + <EuiFlexGroup alignItems="center" gutterSize="none"> + <EuiFlexItem grow={false}> + {stackByOptions.length > 1 && ( + <EuiSelect + onChange={setSelectedChartOptionCallback} + options={stackByOptions} + prepend={i18n.STACK_BY} + value={selectedStackByOption?.value} + /> + )} + </EuiFlexItem> + <HeaderChildrenFlexItem grow={false}>{headerChildren}</HeaderChildrenFlexItem> + </EuiFlexGroup> + </HeaderSection> + {isInitialLoading ? ( - <> - <HeaderSection - id={id} - title={titleWithStackByField} - subtitle={!isInitialLoading && (totalCount >= 0 ? subtitleWithCounts : null)} - > - <EuiFlexGroup alignItems="center" gutterSize="none"> - <EuiFlexItem grow={false}> - {stackByOptions?.length > 1 && ( - <EuiSelect - onChange={setSelectedChartOptionCallback} - options={stackByOptions} - prepend={i18n.STACK_BY} - value={selectedStackByOption?.value} - /> - )} - </EuiFlexItem> - <HeaderChildrenFlexItem grow={false}>{headerChildren}</HeaderChildrenFlexItem> - </EuiFlexGroup> - </HeaderSection> - <MatrixLoader /> - </> + <MatrixLoader /> ) : ( - <> - <HeaderSection - id={id} - title={titleWithStackByField} - subtitle={ - !isInitialLoading && - (totalCount != null && totalCount >= 0 ? subtitleWithCounts : null) - } - > - <EuiFlexGroup alignItems="center" gutterSize="none"> - <EuiFlexItem grow={false}> - {stackByOptions?.length > 1 && ( - <EuiSelect - onChange={setSelectedChartOptionCallback} - options={stackByOptions} - prepend={i18n.STACK_BY} - value={selectedStackByOption?.value} - /> - )} - </EuiFlexItem> - <HeaderChildrenFlexItem grow={false}>{headerChildren}</HeaderChildrenFlexItem> - </EuiFlexGroup> - </HeaderSection> - <BarChart barChart={barChartData} configs={barchartConfigs} /> - </> + <BarChart + barChart={barChartData} + configs={barchartConfigs} + stackByField={selectedStackByOption.value} + /> )} </HistogramPanel> </InspectButtonContainer> - <EuiSpacer size="l" /> + {showSpacer && <EuiSpacer data-test-subj="spacer" size="l" />} </> ); }; diff --git a/x-pack/legacy/plugins/siem/public/components/matrix_histogram/types.ts b/x-pack/legacy/plugins/siem/public/components/matrix_histogram/types.ts index a435c7be6c890..98437845a3ab7 100644 --- a/x-pack/legacy/plugins/siem/public/components/matrix_histogram/types.ts +++ b/x-pack/legacy/plugins/siem/public/components/matrix_histogram/types.ts @@ -4,9 +4,10 @@ * you may not use this file except in compliance with the Elastic License. */ +import { EuiTitleSize } from '@elastic/eui'; import { ScaleType, Position, TickFormatter } from '@elastic/charts'; import { ActionCreator } from 'redux'; -import { ESQuery } from '../../../common/typed_json'; +import { ESQuery } from '../../../../../../plugins/siem/common/typed_json'; import { SetQuery } from '../../pages/hosts/navigation/types'; import { InputsModelId } from '../../store/inputs/constants'; import { HistogramType } from '../../graphql/types'; @@ -34,6 +35,7 @@ export interface MatrixHisrogramConfigs { stackByOptions: MatrixHistogramOption[]; subtitle?: string | GetSubTitle; title: string | GetTitle; + titleSize?: EuiTitleSize; } interface MatrixHistogramBasicProps { @@ -57,14 +59,22 @@ interface MatrixHistogramBasicProps { stackByOptions: MatrixHistogramOption[]; subtitle?: string | GetSubTitle; title?: string | GetTitle; + titleSize?: EuiTitleSize; } export interface MatrixHistogramQueryProps { endDate: number; errorMessage: string; filterQuery?: ESQuery | string | undefined; + setAbsoluteRangeDatePicker?: ActionCreator<{ + id: InputsModelId; + from: number; + to: number; + }>; + setAbsoluteRangeDatePickerTarget?: InputsModelId; stackByField: string; startDate: number; + indexToAdd?: string[] | null; isInspected: boolean; histogramType: HistogramType; } @@ -73,6 +83,7 @@ export interface MatrixHistogramProps extends MatrixHistogramBasicProps { scaleType?: ScaleType; yTickFormatter?: (value: number) => string; showLegend?: boolean; + showSpacer?: boolean; legendPosition?: Position; } diff --git a/x-pack/legacy/plugins/siem/public/components/matrix_histogram/utils.ts b/x-pack/legacy/plugins/siem/public/components/matrix_histogram/utils.ts index ddac615cef50a..d31eb1da15ea1 100644 --- a/x-pack/legacy/plugins/siem/public/components/matrix_histogram/utils.ts +++ b/x-pack/legacy/plugins/siem/public/components/matrix_histogram/utils.ts @@ -69,6 +69,19 @@ export const getBarchartConfigs = ({ customHeight: chartHeight ?? DEFAULT_CHART_HEIGHT, }); +export const defaultLegendColors = [ + '#1EA593', + '#2B70F7', + '#CE0060', + '#38007E', + '#FCA5D3', + '#F37020', + '#E49E29', + '#B0916F', + '#7B000B', + '#34130C', +]; + export const formatToChartDataItem = ([key, value]: [ string, MatrixOverTimeHistogramData[] diff --git a/x-pack/legacy/plugins/siem/public/components/ml/anomaly/use_anomalies_table_data.ts b/x-pack/legacy/plugins/siem/public/components/ml/anomaly/use_anomalies_table_data.ts index c4ca7dc203619..cebfc172ee6ff 100644 --- a/x-pack/legacy/plugins/siem/public/components/ml/anomaly/use_anomalies_table_data.ts +++ b/x-pack/legacy/plugins/siem/public/components/ml/anomaly/use_anomalies_table_data.ts @@ -5,6 +5,8 @@ */ import { useState, useEffect } from 'react'; + +import { DEFAULT_ANOMALY_SCORE } from '../../../../../../../plugins/siem/common/constants'; import { anomaliesTableData } from '../api/anomalies_table_data'; import { InfluencerInput, Anomalies, CriteriaFields } from '../types'; import { hasMlUserPermissions } from '../permissions/has_ml_user_permissions'; @@ -14,7 +16,6 @@ import { useStateToaster, errorToToaster } from '../../toasters'; import * as i18n from './translations'; import { useTimeZone, useUiSetting$ } from '../../../lib/kibana'; -import { DEFAULT_ANOMALY_SCORE } from '../../../../common/constants'; interface Args { influencers?: InfluencerInput[]; diff --git a/x-pack/legacy/plugins/siem/public/components/ml/score/create_description_list.tsx b/x-pack/legacy/plugins/siem/public/components/ml/score/create_description_list.tsx index e0f3ea162ee78..24f203a3682d5 100644 --- a/x-pack/legacy/plugins/siem/public/components/ml/score/create_description_list.tsx +++ b/x-pack/legacy/plugins/siem/public/components/ml/score/create_description_list.tsx @@ -7,11 +7,12 @@ import { EuiText, EuiSpacer, EuiFlexGroup, EuiFlexItem, EuiLink } from '@elastic/eui'; import React from 'react'; import styled from 'styled-components'; + +import { DescriptionList } from '../../../../../../../plugins/siem/common/utility_types'; import { Anomaly, NarrowDateRange } from '../types'; import { getScoreString } from './score_health'; import { PreferenceFormattedDate } from '../../formatted_date'; import { createInfluencers } from './../influencers/create_influencers'; -import { DescriptionList } from '../../../../common/utility_types'; import * as i18n from './translations'; import { createExplorerLink } from '../links/create_explorer_link'; diff --git a/x-pack/legacy/plugins/siem/public/components/ml_popover/hooks/use_siem_jobs.tsx b/x-pack/legacy/plugins/siem/public/components/ml_popover/hooks/use_siem_jobs.tsx index 9a82859066f54..bc488ee00988b 100644 --- a/x-pack/legacy/plugins/siem/public/components/ml_popover/hooks/use_siem_jobs.tsx +++ b/x-pack/legacy/plugins/siem/public/components/ml_popover/hooks/use_siem_jobs.tsx @@ -6,12 +6,12 @@ import { useEffect, useState } from 'react'; +import { DEFAULT_INDEX_KEY } from '../../../../../../../plugins/siem/common/constants'; import { checkRecognizer, getJobsSummary, getModules } from '../api'; import { SiemJob } from '../types'; import { hasMlUserPermissions } from '../../ml/permissions/has_ml_user_permissions'; import { errorToToaster, useStateToaster } from '../../toasters'; import { useUiSetting$ } from '../../../lib/kibana'; -import { DEFAULT_INDEX_KEY } from '../../../../common/constants'; import * as i18n from './translations'; import { createSiemJobs } from './use_siem_jobs_helpers'; diff --git a/x-pack/legacy/plugins/siem/public/components/ml_popover/jobs_table/job_switch.tsx b/x-pack/legacy/plugins/siem/public/components/ml_popover/jobs_table/job_switch.tsx index a0e0c70d2f204..a0343608dc67a 100644 --- a/x-pack/legacy/plugins/siem/public/components/ml_popover/jobs_table/job_switch.tsx +++ b/x-pack/legacy/plugins/siem/public/components/ml_popover/jobs_table/job_switch.tsx @@ -7,12 +7,12 @@ import styled from 'styled-components'; import React, { useState, useCallback } from 'react'; import { EuiFlexGroup, EuiFlexItem, EuiLoadingSpinner, EuiSwitch } from '@elastic/eui'; -import { SiemJob } from '../types'; import { isJobLoading, isJobFailed, isJobStarted, -} from '../../../../common/detection_engine/ml_helpers'; +} from '../../../../../../../plugins/siem/common/detection_engine/ml_helpers'; +import { SiemJob } from '../types'; const StaticSwitch = styled(EuiSwitch)` .euiSwitch__thumb, diff --git a/x-pack/legacy/plugins/siem/public/components/navigation/breadcrumbs/index.ts b/x-pack/legacy/plugins/siem/public/components/navigation/breadcrumbs/index.ts index 155f63145ca95..5407eba8b5b29 100644 --- a/x-pack/legacy/plugins/siem/public/components/navigation/breadcrumbs/index.ts +++ b/x-pack/legacy/plugins/siem/public/components/navigation/breadcrumbs/index.ts @@ -7,7 +7,7 @@ import { getOr, omit } from 'lodash/fp'; import { ChromeBreadcrumb } from '../../../../../../../../src/core/public'; -import { APP_NAME } from '../../../../common/constants'; +import { APP_NAME } from '../../../../../../../plugins/siem/common/constants'; import { StartServices } from '../../../plugin'; import { getBreadcrumbs as getHostDetailsBreadcrumbs } from '../../../pages/hosts/details/utils'; import { getBreadcrumbs as getIPDetailsBreadcrumbs } from '../../../pages/network/ip_details'; diff --git a/x-pack/legacy/plugins/siem/public/components/news_feed/helpers.test.ts b/x-pack/legacy/plugins/siem/public/components/news_feed/helpers.test.ts index e7cd03d098da8..686ec4e86e785 100644 --- a/x-pack/legacy/plugins/siem/public/components/news_feed/helpers.test.ts +++ b/x-pack/legacy/plugins/siem/public/components/news_feed/helpers.test.ts @@ -4,8 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ +import { NEWS_FEED_URL_SETTING_DEFAULT } from '../../../../../../plugins/siem/common/constants'; import { KibanaServices } from '../../lib/kibana'; -import { NEWS_FEED_URL_SETTING_DEFAULT } from '../../../common/constants'; import { rawNewsApiResponse } from '../../mock/news'; import { rawNewsJSON } from '../../mock/raw_news'; diff --git a/x-pack/legacy/plugins/siem/public/components/notes/note_card/note_card_body.tsx b/x-pack/legacy/plugins/siem/public/components/notes/note_card/note_card_body.tsx index 11761c8fd39b0..4463f8d4ff602 100644 --- a/x-pack/legacy/plugins/siem/public/components/notes/note_card/note_card_body.tsx +++ b/x-pack/legacy/plugins/siem/public/components/notes/note_card/note_card_body.tsx @@ -5,7 +5,7 @@ */ import { EuiPanel, EuiToolTip } from '@elastic/eui'; -import React from 'react'; +import React, { useCallback, useMemo } from 'react'; import styled from 'styled-components'; import { WithCopyToClipboard } from '../../../lib/clipboard/with_copy_to_clipboard'; @@ -19,33 +19,23 @@ const BodyContainer = styled(EuiPanel)` BodyContainer.displayName = 'BodyContainer'; -const HoverActionsContainer = styled(EuiPanel)` - align-items: center; - display: flex; - flex-direction: row; - height: 25px; - justify-content: center; - left: 5px; - position: absolute; - top: -5px; - width: 30px; -`; - -HoverActionsContainer.displayName = 'HoverActionsContainer'; - -export const NoteCardBody = React.memo<{ rawNote: string }>(({ rawNote }) => ( - <BodyContainer data-test-subj="note-card-body" hasShadow={false} paddingSize="s"> - <WithHoverActions - hoverContent={ - <HoverActionsContainer data-test-subj="hover-actions-container"> - <EuiToolTip content={i18n.COPY_TO_CLIPBOARD}> - <WithCopyToClipboard text={rawNote} titleSummary={i18n.NOTE.toLowerCase()} /> - </EuiToolTip> - </HoverActionsContainer> - } - render={() => <Markdown raw={rawNote} />} - /> - </BodyContainer> -)); +export const NoteCardBody = React.memo<{ rawNote: string }>(({ rawNote }) => { + const hoverContent = useMemo( + () => ( + <EuiToolTip content={i18n.COPY_TO_CLIPBOARD}> + <WithCopyToClipboard text={rawNote} titleSummary={i18n.NOTE.toLowerCase()} /> + </EuiToolTip> + ), + [rawNote] + ); + + const render = useCallback(() => <Markdown raw={rawNote} />, [rawNote]); + + return ( + <BodyContainer data-test-subj="note-card-body" hasShadow={false} paddingSize="s"> + <WithHoverActions hoverContent={hoverContent} render={render} /> + </BodyContainer> + ); +}); NoteCardBody.displayName = 'NoteCardBody'; diff --git a/x-pack/legacy/plugins/siem/public/components/page/add_filter_to_global_search_bar/__snapshots__/index.test.tsx.snap b/x-pack/legacy/plugins/siem/public/components/page/add_filter_to_global_search_bar/__snapshots__/index.test.tsx.snap index 42ef4e5404faa..ef02311c0629e 100644 --- a/x-pack/legacy/plugins/siem/public/components/page/add_filter_to_global_search_bar/__snapshots__/index.test.tsx.snap +++ b/x-pack/legacy/plugins/siem/public/components/page/add_filter_to_global_search_bar/__snapshots__/index.test.tsx.snap @@ -3,22 +3,36 @@ exports[`AddFilterToGlobalSearchBar Component Rendering 1`] = ` <WithHoverActions hoverContent={ - <ForwardRef(Styled(EuiPanel)) + <div data-test-subj="hover-actions-container" - paddingSize="none" > <EuiToolTip content="Filter for value" delay="regular" position="top" > - <EuiIcon + <EuiButtonIcon + aria-label="Filter for value" + color="text" data-test-subj="add-to-filter" + iconType="magnifyWithPlus" onClick={[Function]} - type="filter" /> </EuiToolTip> - </ForwardRef(Styled(EuiPanel))> + <EuiToolTip + content="Filter out value" + delay="regular" + position="top" + > + <EuiButtonIcon + aria-label="Filter out value" + color="text" + data-test-subj="filter-out-value" + iconType="magnifyWithMinus" + onClick={[Function]} + /> + </EuiToolTip> + </div> } render={[Function]} /> diff --git a/x-pack/legacy/plugins/siem/public/components/page/add_filter_to_global_search_bar/helpers.test.tsx b/x-pack/legacy/plugins/siem/public/components/page/add_filter_to_global_search_bar/helpers.test.tsx index 7e5e53f575be8..5c920d923d9a4 100644 --- a/x-pack/legacy/plugins/siem/public/components/page/add_filter_to_global_search_bar/helpers.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/page/add_filter_to_global_search_bar/helpers.test.tsx @@ -24,6 +24,22 @@ describe('helpers', () => { }); }); + test('returns a negated filter when `negate` is true', () => { + const filter = createFilter('host.name', 'siem-xavier', true); + expect(filter).toEqual({ + meta: { + alias: null, + disabled: false, + key: 'host.name', + negate: true, // <-- filter is negated + params: { query: 'siem-xavier' }, + type: 'phrase', + value: 'siem-xavier', + }, + query: { match: { 'host.name': { query: 'siem-xavier', type: 'phrase' } } }, + }); + }); + test('return valid exists filter when valid key and null value are provided', () => { const filter = createFilter('host.name', null); expect(filter).toEqual({ diff --git a/x-pack/legacy/plugins/siem/public/components/page/add_filter_to_global_search_bar/helpers.ts b/x-pack/legacy/plugins/siem/public/components/page/add_filter_to_global_search_bar/helpers.ts index bafe033368c83..d88bc2bf3b7e6 100644 --- a/x-pack/legacy/plugins/siem/public/components/page/add_filter_to_global_search_bar/helpers.ts +++ b/x-pack/legacy/plugins/siem/public/components/page/add_filter_to_global_search_bar/helpers.ts @@ -6,13 +6,17 @@ import { Filter } from '../../../../../../../../src/plugins/data/public'; -export const createFilter = (key: string, value: string[] | string | null | undefined): Filter => { +export const createFilter = ( + key: string, + value: string[] | string | null | undefined, + negate: boolean = false +): Filter => { const queryValue = value != null ? (Array.isArray(value) ? value[0] : value) : null; return queryValue != null ? { meta: { alias: null, - negate: false, + negate, disabled: false, type: 'phrase', key, diff --git a/x-pack/legacy/plugins/siem/public/components/page/add_filter_to_global_search_bar/index.tsx b/x-pack/legacy/plugins/siem/public/components/page/add_filter_to_global_search_bar/index.tsx index 160cd020796db..127eb3bae0284 100644 --- a/x-pack/legacy/plugins/siem/public/components/page/add_filter_to_global_search_bar/index.tsx +++ b/x-pack/legacy/plugins/siem/public/components/page/add_filter_to_global_search_bar/index.tsx @@ -4,9 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ -import { EuiIcon, EuiPanel, EuiToolTip } from '@elastic/eui'; +import { EuiButtonIcon, EuiToolTip } from '@elastic/eui'; import React, { useCallback } from 'react'; -import styled from 'styled-components'; import { Filter } from '../../../../../../../../src/plugins/data/public'; import { WithHoverActions } from '../../with_hover_actions'; @@ -26,21 +25,52 @@ export const AddFilterToGlobalSearchBar = React.memo<OwnProps>( ({ children, filter, onFilterAdded }) => { const { filterManager } = useKibana().services.data.query; - const addToKql = useCallback(() => { + const filterForValue = useCallback(() => { filterManager.addFilters(filter); + if (onFilterAdded != null) { onFilterAdded(); } - }, [filter, filterManager, onFilterAdded]); + }, [filterManager, filter, onFilterAdded]); + + const filterOutValue = useCallback(() => { + filterManager.addFilters({ + ...filter, + meta: { + ...filter.meta, + negate: true, + }, + }); + + if (onFilterAdded != null) { + onFilterAdded(); + } + }, [filterManager, filter, onFilterAdded]); return ( <WithHoverActions hoverContent={ - <HoverActionsContainer data-test-subj="hover-actions-container" paddingSize="none"> + <div data-test-subj="hover-actions-container"> <EuiToolTip content={i18n.FILTER_FOR_VALUE}> - <EuiIcon data-test-subj="add-to-filter" type="filter" onClick={addToKql} /> + <EuiButtonIcon + aria-label={i18n.FILTER_FOR_VALUE} + color="text" + data-test-subj="add-to-filter" + iconType="magnifyWithPlus" + onClick={filterForValue} + /> + </EuiToolTip> + + <EuiToolTip content={i18n.FILTER_OUT_VALUE}> + <EuiButtonIcon + aria-label={i18n.FILTER_OUT_VALUE} + color="text" + data-test-subj="filter-out-value" + iconType="magnifyWithMinus" + onClick={filterOutValue} + /> </EuiToolTip> - </HoverActionsContainer> + </div> } render={() => children} /> @@ -49,16 +79,3 @@ export const AddFilterToGlobalSearchBar = React.memo<OwnProps>( ); AddFilterToGlobalSearchBar.displayName = 'AddFilterToGlobalSearchBar'; - -export const HoverActionsContainer = styled(EuiPanel)` - align-items: center; - display: flex; - flex-direction: row; - height: 34px; - justify-content: center; - left: 5px; - position: absolute; - top: -10px; - width: 34px; - cursor: pointer; -`; diff --git a/x-pack/legacy/plugins/siem/public/components/page/add_filter_to_global_search_bar/translations.ts b/x-pack/legacy/plugins/siem/public/components/page/add_filter_to_global_search_bar/translations.ts index 81772527e59db..f192c5c26fa49 100644 --- a/x-pack/legacy/plugins/siem/public/components/page/add_filter_to_global_search_bar/translations.ts +++ b/x-pack/legacy/plugins/siem/public/components/page/add_filter_to_global_search_bar/translations.ts @@ -12,3 +12,10 @@ export const FILTER_FOR_VALUE = i18n.translate( defaultMessage: 'Filter for value', } ); + +export const FILTER_OUT_VALUE = i18n.translate( + 'xpack.siem.add_filter_to_global_search_bar.filterOutValueHoverAction', + { + defaultMessage: 'Filter out value', + } +); diff --git a/x-pack/legacy/plugins/siem/public/components/page/hosts/host_overview/index.tsx b/x-pack/legacy/plugins/siem/public/components/page/hosts/host_overview/index.tsx index 4d0e6a737d303..a0ca5f855237c 100644 --- a/x-pack/legacy/plugins/siem/public/components/page/hosts/host_overview/index.tsx +++ b/x-pack/legacy/plugins/siem/public/components/page/hosts/host_overview/index.tsx @@ -10,8 +10,8 @@ import lightTheme from '@elastic/eui/dist/eui_theme_light.json'; import { getOr } from 'lodash/fp'; import React from 'react'; -import { DEFAULT_DARK_MODE } from '../../../../../common/constants'; -import { DescriptionList } from '../../../../../common/utility_types'; +import { DEFAULT_DARK_MODE } from '../../../../../../../../plugins/siem/common/constants'; +import { DescriptionList } from '../../../../../../../../plugins/siem/common/utility_types'; import { useUiSetting$ } from '../../../../lib/kibana'; import { getEmptyTagValue } from '../../../empty_value'; import { DefaultFieldRenderer, hostIdRenderer } from '../../../field_renderers/field_renderers'; diff --git a/x-pack/legacy/plugins/siem/public/components/page/hosts/hosts_table/columns.tsx b/x-pack/legacy/plugins/siem/public/components/page/hosts/hosts_table/columns.tsx index 8d490d2c152d9..6bd82f3192f9b 100644 --- a/x-pack/legacy/plugins/siem/public/components/page/hosts/hosts_table/columns.tsx +++ b/x-pack/legacy/plugins/siem/public/components/page/hosts/hosts_table/columns.tsx @@ -46,9 +46,7 @@ export const getHostsColumns = (): HostsTableColumns => [ <Provider dataProvider={dataProvider} /> </DragEffects> ) : ( - <AddFilterToGlobalSearchBar filter={createFilter('host.name', hostName[0])}> - <HostDetailsLink hostName={hostName[0]} /> - </AddFilterToGlobalSearchBar> + <HostDetailsLink hostName={hostName[0]} /> ) } /> diff --git a/x-pack/legacy/plugins/siem/public/components/page/index.tsx b/x-pack/legacy/plugins/siem/public/components/page/index.tsx index ef6a19f4b7448..3a36a2dce476b 100644 --- a/x-pack/legacy/plugins/siem/public/components/page/index.tsx +++ b/x-pack/legacy/plugins/siem/public/components/page/index.tsx @@ -17,7 +17,7 @@ export const AppGlobalStyle = createGlobalStyle` position: static; } /* end of dirty hack to fix draggables with tooltip on FF */ - + div.app-wrapper { background-color: rgba(0,0,0,0); } @@ -28,12 +28,13 @@ export const AppGlobalStyle = createGlobalStyle` .euiPopover__panel.euiPopover__panel-isOpen { z-index: 9900 !important; + min-width: 24px; } .euiToolTip { z-index: 9950 !important; } - /* + /* overrides the default styling of euiComboBoxOptionsList because it's implemented as a popover, so it's not selectable as a child of the styled component */ @@ -45,6 +46,17 @@ export const AppGlobalStyle = createGlobalStyle` .euiPanel-loading-hide-border { border: none; } + + /* hide open popovers when a modal is being displayed to prevent them from covering the modal */ + body.euiBody-hasOverlayMask .euiPopover__panel-isOpen { + visibility: hidden !important; + } + + /* ensure elastic charts tooltips appear above open euiPopovers */ + .echTooltip { + z-index: 9950; + } + `; export const DescriptionListStyled = styled(EuiDescriptionList)` diff --git a/x-pack/legacy/plugins/siem/public/components/page/manage_query.tsx b/x-pack/legacy/plugins/siem/public/components/page/manage_query.tsx index 138c38c02065b..3b723c66f5af5 100644 --- a/x-pack/legacy/plugins/siem/public/components/page/manage_query.tsx +++ b/x-pack/legacy/plugins/siem/public/components/page/manage_query.tsx @@ -42,6 +42,6 @@ export function manageQuery<T>(WrappedComponent: React.ComponentClass<T> | React return <WrappedComponent {...(otherProps as T)} />; } } - ManageQuery.displayName = `ManageQuery (${WrappedComponent.displayName || 'Unknown'})`; + ManageQuery.displayName = `ManageQuery (${WrappedComponent?.displayName ?? 'Unknown'})`; return ManageQuery; } diff --git a/x-pack/legacy/plugins/siem/public/components/page/network/ip_overview/index.tsx b/x-pack/legacy/plugins/siem/public/components/page/network/ip_overview/index.tsx index 56b59ca97156f..a652fef5508fc 100644 --- a/x-pack/legacy/plugins/siem/public/components/page/network/ip_overview/index.tsx +++ b/x-pack/legacy/plugins/siem/public/components/page/network/ip_overview/index.tsx @@ -9,8 +9,8 @@ import darkTheme from '@elastic/eui/dist/eui_theme_dark.json'; import lightTheme from '@elastic/eui/dist/eui_theme_light.json'; import React from 'react'; -import { DEFAULT_DARK_MODE } from '../../../../../common/constants'; -import { DescriptionList } from '../../../../../common/utility_types'; +import { DEFAULT_DARK_MODE } from '../../../../../../../../plugins/siem/common/constants'; +import { DescriptionList } from '../../../../../../../../plugins/siem/common/utility_types'; import { useUiSetting$ } from '../../../../lib/kibana'; import { FlowTarget, IpOverviewData, Overview } from '../../../../graphql/types'; import { networkModel } from '../../../../store'; diff --git a/x-pack/legacy/plugins/siem/public/components/page/overview/overview_host/index.tsx b/x-pack/legacy/plugins/siem/public/components/page/overview/overview_host/index.tsx index 52c142ceff480..b43efbbde51b3 100644 --- a/x-pack/legacy/plugins/siem/public/components/page/overview/overview_host/index.tsx +++ b/x-pack/legacy/plugins/siem/public/components/page/overview/overview_host/index.tsx @@ -10,8 +10,8 @@ import numeral from '@elastic/numeral'; import { FormattedMessage } from '@kbn/i18n/react'; import React, { useMemo } from 'react'; -import { DEFAULT_NUMBER_FORMAT } from '../../../../../common/constants'; -import { ESQuery } from '../../../../../common/typed_json'; +import { DEFAULT_NUMBER_FORMAT } from '../../../../../../../../plugins/siem/common/constants'; +import { ESQuery } from '../../../../../../../../plugins/siem/common/typed_json'; import { ID as OverviewHostQueryId, OverviewHostQuery, diff --git a/x-pack/legacy/plugins/siem/public/components/page/overview/overview_network/index.tsx b/x-pack/legacy/plugins/siem/public/components/page/overview/overview_network/index.tsx index d649a0dd9e923..af50fa88e5fe8 100644 --- a/x-pack/legacy/plugins/siem/public/components/page/overview/overview_network/index.tsx +++ b/x-pack/legacy/plugins/siem/public/components/page/overview/overview_network/index.tsx @@ -10,8 +10,8 @@ import numeral from '@elastic/numeral'; import { FormattedMessage } from '@kbn/i18n/react'; import React, { useMemo } from 'react'; -import { DEFAULT_NUMBER_FORMAT } from '../../../../../common/constants'; -import { ESQuery } from '../../../../../common/typed_json'; +import { DEFAULT_NUMBER_FORMAT } from '../../../../../../../../plugins/siem/common/constants'; +import { ESQuery } from '../../../../../../../../plugins/siem/common/typed_json'; import { HeaderSection } from '../../../header_section'; import { useUiSetting$ } from '../../../../lib/kibana'; import { manageQuery } from '../../../page/manage_query'; diff --git a/x-pack/legacy/plugins/siem/public/components/page/overview/stat_value.tsx b/x-pack/legacy/plugins/siem/public/components/page/overview/stat_value.tsx index 7615001eec9da..cada0a9aff939 100644 --- a/x-pack/legacy/plugins/siem/public/components/page/overview/stat_value.tsx +++ b/x-pack/legacy/plugins/siem/public/components/page/overview/stat_value.tsx @@ -9,7 +9,7 @@ import numeral from '@elastic/numeral'; import React, { useEffect, useState } from 'react'; import styled from 'styled-components'; -import { DEFAULT_NUMBER_FORMAT } from '../../../../common/constants'; +import { DEFAULT_NUMBER_FORMAT } from '../../../../../../../plugins/siem/common/constants'; import { useUiSetting$ } from '../../../lib/kibana'; const ProgressContainer = styled.div` diff --git a/x-pack/legacy/plugins/siem/public/components/paginated_table/index.test.tsx b/x-pack/legacy/plugins/siem/public/components/paginated_table/index.test.tsx index 947bdee6a5cd2..2f743c3387209 100644 --- a/x-pack/legacy/plugins/siem/public/components/paginated_table/index.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/paginated_table/index.test.tsx @@ -7,13 +7,13 @@ import { mount, shallow } from 'enzyme'; import React from 'react'; +import { DEFAULT_MAX_TABLE_QUERY_SIZE } from '../../../../../../plugins/siem/common/constants'; import { Direction } from '../../graphql/types'; import { BasicTableProps, PaginatedTable } from './index'; import { getHostsColumns, mockData, rowItems, sortedHosts } from './index.mock'; import { ThemeProvider } from 'styled-components'; import euiDarkVars from '@elastic/eui/dist/eui_theme_dark.json'; -import { DEFAULT_MAX_TABLE_QUERY_SIZE } from '../../../common/constants'; jest.mock('react', () => { const r = jest.requireActual('react'); diff --git a/x-pack/legacy/plugins/siem/public/components/paginated_table/index.tsx b/x-pack/legacy/plugins/siem/public/components/paginated_table/index.tsx index 5cd200cbb41b7..e481fe7245201 100644 --- a/x-pack/legacy/plugins/siem/public/components/paginated_table/index.tsx +++ b/x-pack/legacy/plugins/siem/public/components/paginated_table/index.tsx @@ -3,6 +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 { EuiBasicTable, EuiBasicTableProps, @@ -21,6 +22,7 @@ import { noop } from 'lodash/fp'; import React, { FC, memo, useState, useEffect, ComponentType } from 'react'; import styled from 'styled-components'; +import { DEFAULT_MAX_TABLE_QUERY_SIZE } from '../../../../../../plugins/siem/common/constants'; import { AuthTableColumns } from '../page/hosts/authentications_table'; import { HostsTableColumns } from '../page/hosts/hosts_table'; import { NetworkDnsColumns } from '../page/network/network_dns_table/columns'; @@ -39,7 +41,6 @@ import { UsersColumns } from '../page/network/users_table/columns'; import { HeaderSection } from '../header_section'; import { Loader } from '../loader'; import { useStateToaster } from '../toasters'; -import { DEFAULT_MAX_TABLE_QUERY_SIZE } from '../../../common/constants'; import * as i18n from './translations'; import { Panel } from '../panel'; @@ -246,21 +247,16 @@ const PaginatedTableComponent: FC<SiemTables> = ({ <EuiLoadingContent data-test-subj="initialLoadingPanelPaginatedTable" lines={10} /> ) : ( <> - { - // @ts-ignore avoid some type mismatches - } <BasicTable - // @ts-ignore `Columns` interface differs from EUI's `column` type and is used all over this plugin, so ignore the differences instead of refactoring a lot of code columns={columns} compressed items={pageOfItems} onChange={onChange} - // @ts-ignore TS complains sorting.field is type `never` sorting={ sorting ? { sort: { - field: sorting.field as any, // eslint-disable-line @typescript-eslint/no-explicit-any + field: sorting.field, direction: sorting.direction, }, } @@ -305,9 +301,7 @@ const PaginatedTableComponent: FC<SiemTables> = ({ export const PaginatedTable = memo(PaginatedTableComponent); type BasicTableType = ComponentType<EuiBasicTableProps<any>>; // eslint-disable-line @typescript-eslint/no-explicit-any -const BasicTable: typeof EuiBasicTable & { displayName: string } = styled( - EuiBasicTable as BasicTableType -)` +const BasicTable = styled(EuiBasicTable as BasicTableType)` tbody { th, td { diff --git a/x-pack/legacy/plugins/siem/public/components/query_bar/index.test.tsx b/x-pack/legacy/plugins/siem/public/components/query_bar/index.test.tsx index 870d0b40d8cd4..49afc8d5ef68b 100644 --- a/x-pack/legacy/plugins/siem/public/components/query_bar/index.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/query_bar/index.test.tsx @@ -7,9 +7,9 @@ import { mount } from 'enzyme'; import React from 'react'; +import { DEFAULT_FROM, DEFAULT_TO } from '../../../../../../plugins/siem/common/constants'; import { TestProviders, mockIndexPattern } from '../../mock'; import { createKibanaCoreStartMock } from '../../mock/kibana_core'; -import { DEFAULT_FROM, DEFAULT_TO } from '../../../common/constants'; import { FilterManager, SearchBar } from '../../../../../../../src/plugins/data/public'; import { QueryBar, QueryBarComponentProps } from '.'; import { createKibanaContextProviderMock } from '../../mock/kibana_react'; diff --git a/x-pack/legacy/plugins/siem/public/components/source_destination/source_destination_ip.tsx b/x-pack/legacy/plugins/siem/public/components/source_destination/source_destination_ip.tsx index b8192cce11e5a..62f01dfc020f5 100644 --- a/x-pack/legacy/plugins/siem/public/components/source_destination/source_destination_ip.tsx +++ b/x-pack/legacy/plugins/siem/public/components/source_destination/source_destination_ip.tsx @@ -171,7 +171,7 @@ export const SourceDestinationIp = React.memo<SourceDestinationIpProps>( return isIpFieldPopulated({ destinationIp, sourceIp, type }) || hasPorts({ destinationPort, sourcePort, type }) ? ( - <EuiBadge data-test-subj={`${type}-ip-badge`} color="hollow"> + <EuiBadge data-test-subj={`${type}-ip-badge`} color="hollow" title=""> <EuiFlexGroup alignItems="center" data-test-subj={`${type}-ip-group`} diff --git a/x-pack/legacy/plugins/siem/public/components/super_date_picker/index.test.tsx b/x-pack/legacy/plugins/siem/public/components/super_date_picker/index.test.tsx index c5838fa283e17..d64ddb9bb40b1 100644 --- a/x-pack/legacy/plugins/siem/public/components/super_date_picker/index.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/super_date_picker/index.test.tsx @@ -8,6 +8,7 @@ import { mount } from 'enzyme'; import React from 'react'; import { Provider as ReduxStoreProvider } from 'react-redux'; +import { DEFAULT_TIMEPICKER_QUICK_RANGES } from '../../../../../../plugins/siem/common/constants'; import { useUiSetting$ } from '../../lib/kibana'; import { apolloClientObservable, mockGlobalState } from '../../mock'; import { createUseUiSetting$Mock } from '../../mock/kibana_react'; @@ -15,7 +16,6 @@ import { createStore, State } from '../../store'; import { SuperDatePicker, makeMapStateToProps } from '.'; import { cloneDeep } from 'lodash/fp'; -import { DEFAULT_TIMEPICKER_QUICK_RANGES } from '../../../common/constants'; jest.mock('../../lib/kibana'); const mockUseUiSetting$ = useUiSetting$ as jest.Mock; diff --git a/x-pack/legacy/plugins/siem/public/components/super_date_picker/index.tsx b/x-pack/legacy/plugins/siem/public/components/super_date_picker/index.tsx index ad38a7d61bcba..cf350b3993a4b 100644 --- a/x-pack/legacy/plugins/siem/public/components/super_date_picker/index.tsx +++ b/x-pack/legacy/plugins/siem/public/components/super_date_picker/index.tsx @@ -17,7 +17,7 @@ import React, { useState, useCallback } from 'react'; import { connect, ConnectedProps } from 'react-redux'; import { Dispatch } from 'redux'; -import { DEFAULT_TIMEPICKER_QUICK_RANGES } from '../../../common/constants'; +import { DEFAULT_TIMEPICKER_QUICK_RANGES } from '../../../../../../plugins/siem/common/constants'; import { useUiSetting$ } from '../../lib/kibana'; import { inputsModel, State } from '../../store'; import { inputsActions, timelineActions } from '../../store/actions'; diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/__snapshots__/timeline.test.tsx.snap b/x-pack/legacy/plugins/siem/public/components/timeline/__snapshots__/timeline.test.tsx.snap index 02938cb2b86b9..a8ba787477797 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/__snapshots__/timeline.test.tsx.snap +++ b/x-pack/legacy/plugins/siem/public/components/timeline/__snapshots__/timeline.test.tsx.snap @@ -594,6 +594,42 @@ exports[`Timeline rendering renders correctly against snapshot 1`] = ` }, ] } + filterManager={ + FilterManager { + "fetch$": Subject { + "_isScalar": false, + "closed": false, + "hasError": false, + "isStopped": false, + "observers": Array [], + "thrownError": null, + }, + "filters": Array [], + "uiSettings": Object { + "get": [Function], + "get$": [MockFunction], + "getAll": [MockFunction], + "getSaved$": [MockFunction], + "getUpdate$": [MockFunction], + "getUpdateErrors$": [MockFunction], + "isCustom": [MockFunction], + "isDeclared": [MockFunction], + "isDefault": [MockFunction], + "isOverridden": [MockFunction], + "overrideLocalDefault": [MockFunction], + "remove": [MockFunction], + "set": [MockFunction], + }, + "updated$": Subject { + "_isScalar": false, + "closed": false, + "hasError": false, + "isStopped": false, + "observers": Array [], + "thrownError": null, + }, + } + } id="foo" indexPattern={ Object { diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/column_headers/actions/index.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/body/column_headers/actions/index.tsx index d9e04ad873537..7a2898d465b22 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/body/column_headers/actions/index.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/body/column_headers/actions/index.tsx @@ -44,7 +44,7 @@ export const CloseButton = React.memo<{ CloseButton.displayName = 'CloseButton'; export const Actions = React.memo<Props>(({ header, onColumnRemoved, sort }) => { - const isLoading = useTimelineContext(); + const { isLoading } = useTimelineContext(); return ( <> {sort.columnId === header.id && isLoading ? ( diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/column_headers/header/header_content.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/body/column_headers/header/header_content.tsx index 84781e6a24300..0a69cef618570 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/body/column_headers/header/header_content.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/body/column_headers/header/header_content.tsx @@ -32,7 +32,7 @@ const HeaderContentComponent: React.FC<HeaderContentProps> = ({ onClick, sort, }) => { - const isLoading = useTimelineContext(); + const { isLoading } = useTimelineContext(); return ( <EventsHeading data-test-subj="header" isLoading={isLoading}> diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/auditd/generic_row_renderer.test.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/auditd/generic_row_renderer.test.tsx index 5f59915eac418..417a078a08150 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/auditd/generic_row_renderer.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/auditd/generic_row_renderer.test.tsx @@ -19,6 +19,8 @@ import { createGenericFileRowRenderer, } from './generic_row_renderer'; +jest.mock('../../../../../pages/overview/events_by_dataset'); + describe('GenericRowRenderer', () => { const mount = useMountAppended(); diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/suricata/suricata_signature.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/suricata/suricata_signature.tsx index 9ccd1fb7a0519..24c52f3372d62 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/suricata/suricata_signature.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/suricata/suricata_signature.tsx @@ -44,7 +44,7 @@ export const Tokens = React.memo<{ tokens: string[] }>(({ tokens }) => ( <> {tokens.map(token => ( <TokensFlexItem key={token} grow={false}> - <EuiBadge iconType="tag" color="hollow"> + <EuiBadge iconType="tag" color="hollow" title=""> {token} </EuiBadge> </TokensFlexItem> @@ -81,7 +81,7 @@ export const DraggableSignatureId = React.memo<{ id: string; signatureId: number data-test-subj="signature-id-tooltip" content={SURICATA_SIGNATURE_ID_FIELD_NAME} > - <Badge iconType="number" color="hollow"> + <Badge iconType="number" color="hollow" title=""> {signatureId} </Badge> </EuiToolTip> diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/system/generic_details.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/system/generic_details.tsx index e1524c8e5aecb..2ad3eb4681454 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/system/generic_details.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/system/generic_details.tsx @@ -119,7 +119,7 @@ export const SystemGenericLine = React.memo<Props>( <EuiSpacer size="xs" /> <EuiFlexGroup justifyContent="center" gutterSize="none" wrap={true}> <TokensFlexItem grow={false} component="span"> - <Badge iconType="editorComment" color="hollow"> + <Badge iconType="editorComment" color="hollow" title=""> <OverflowField value={message} /> </Badge> </TokensFlexItem> diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/system/generic_file_details.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/system/generic_file_details.tsx index c47d9603cbea2..ef7c3b3ccf859 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/system/generic_file_details.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/system/generic_file_details.tsx @@ -193,7 +193,7 @@ export const SystemGenericFileLine = React.memo<Props>( <EuiSpacer size="xs" /> <EuiFlexGroup justifyContent="center" gutterSize="none" wrap={true}> <TokensFlexItem grow={false} component="span"> - <Badge iconType="editorComment" color="hollow"> + <Badge iconType="editorComment" color="hollow" title=""> <OverflowField value={message} /> </Badge> </TokensFlexItem> diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/system/generic_row_renderer.test.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/system/generic_row_renderer.test.tsx index abe77a63f4a27..2f5fa76855f2b 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/system/generic_row_renderer.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/system/generic_row_renderer.test.tsx @@ -48,6 +48,8 @@ import { } from './generic_row_renderer'; import * as i18n from './translations'; +jest.mock('../../../../../pages/overview/events_by_dataset'); + describe('GenericRowRenderer', () => { const mount = useMountAppended(); diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/zeek/zeek_signature.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/zeek/zeek_signature.tsx index f13a236e8ec36..39c21c4ffa33b 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/zeek/zeek_signature.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/zeek/zeek_signature.tsx @@ -92,7 +92,7 @@ export const DraggableZeekElement = React.memo<{ </DragEffects> ) : ( <EuiToolTip data-test-subj="badge-tooltip" content={field}> - <Badge iconType="tag" color="hollow"> + <Badge iconType="tag" color="hollow" title=""> {stringRenderer(value)} </Badge> </EuiToolTip> diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/data_providers/index.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/data_providers/index.tsx index 525cc8e301d11..f369b961807af 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/data_providers/index.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/data_providers/index.tsx @@ -109,7 +109,7 @@ export const DataProviders = React.memo<Props>( data-test-subj="dataProviders" > <TimelineContext.Consumer> - {isLoading => ( + {({ isLoading }) => ( <DroppableWrapper isDropDisabled={!show || isLoading} droppableId={getDroppableId(id)}> {dataProviders != null && dataProviders.length ? ( <Providers diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/data_providers/provider_badge.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/data_providers/provider_badge.tsx index a9eff1e868006..8e4665acc2c26 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/data_providers/provider_badge.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/data_providers/provider_badge.tsx @@ -7,10 +7,13 @@ import { EuiBadge } from '@elastic/eui'; import classNames from 'classnames'; import { isString } from 'lodash/fp'; -import React from 'react'; +import React, { useCallback, useMemo } from 'react'; import styled from 'styled-components'; import { getEmptyString } from '../../empty_value'; +import { WithCopyToClipboard } from '../../../lib/clipboard/with_copy_to_clipboard'; +import { WithHoverActions } from '../../with_hover_actions'; + import { EXISTS_OPERATOR, QueryOperator } from './data_provider'; import * as i18n from './translations'; @@ -57,57 +60,98 @@ interface ProviderBadgeProps { operator: QueryOperator; } +const closeButtonProps = { + // Removing tab focus on close button because the same option can be obtained through the context menu + // TODO: add a `DEL` keyboard press functionality + tabIndex: -1, +}; + export const ProviderBadge = React.memo<ProviderBadgeProps>( ({ deleteProvider, field, isEnabled, isExcluded, operator, providerId, togglePopover, val }) => { - const deleteFilter: React.MouseEventHandler<HTMLButtonElement> = ( - event: React.MouseEvent<HTMLButtonElement> - ) => { - // Make sure it doesn't also trigger the onclick for the whole badge - if (event.stopPropagation) { - event.stopPropagation(); - } - deleteProvider(); - }; - const classes = classNames('globalFilterItem', { - 'globalFilterItem-isDisabled': !isEnabled, - 'globalFilterItem-isExcluded': isExcluded, - }); - const formattedValue = isString(val) && val === '' ? getEmptyString() : val; - const prefix = isExcluded ? <span>{i18n.NOT} </span> : null; - const title = `${field}: "${formattedValue}"`; - - return ( - <ProviderBadgeStyled - id={`${providerId}-${field}-${val}`} - className={classes} - color="hollow" - title={title} - iconOnClick={deleteFilter} - iconOnClickAriaLabel={i18n.REMOVE_DATA_PROVIDER} - iconType="cross" - iconSide="right" - onClick={togglePopover} - onClickAriaLabel={`${i18n.SHOW_OPTIONS_DATA_PROVIDER} ${formattedValue}`} - closeButtonProps={{ - // Removing tab focus on close button because the same option can be obtained through the context menu - // TODO: add a `DEL` keyboard press functionality - tabIndex: -1, - }} - data-test-subj="providerBadge" - > - {prefix} - {operator !== EXISTS_OPERATOR ? ( - <> - <span className="field-value">{`${field}: `}</span> - <span className="field-value">{`"${formattedValue}"`}</span> - </> - ) : ( - <span className="field-value"> - {field} {i18n.EXISTS_LABEL} - </span> - )} - </ProviderBadgeStyled> + const deleteFilter: React.MouseEventHandler<HTMLButtonElement> = useCallback( + (event: React.MouseEvent<HTMLButtonElement>) => { + // Make sure it doesn't also trigger the onclick for the whole badge + if (event.stopPropagation) { + event.stopPropagation(); + } + deleteProvider(); + }, + [deleteProvider] + ); + + const classes = useMemo( + () => + classNames('globalFilterItem', { + 'globalFilterItem-isDisabled': !isEnabled, + 'globalFilterItem-isExcluded': isExcluded, + }), + [isEnabled, isExcluded] + ); + + const formattedValue = useMemo(() => (isString(val) && val === '' ? getEmptyString() : val), [ + val, + ]); + + const prefix = useMemo(() => (isExcluded ? <span>{i18n.NOT} </span> : null), [isExcluded]); + + const title = useMemo(() => `${field}: "${formattedValue}"`, [field, formattedValue]); + + const hoverContent = useMemo( + () => ( + <WithCopyToClipboard + data-test-subj="copy-to-clipboard" + text={`${field} : ${typeof val === 'string' ? `"${val}"` : `${val}`}`} + titleSummary={i18n.FIELD} + /> + ), + [field, val] ); + + const badge = useCallback( + () => ( + <ProviderBadgeStyled + id={`${providerId}-${field}-${val}`} + className={classes} + color="hollow" + title={title} + iconOnClick={deleteFilter} + iconOnClickAriaLabel={i18n.REMOVE_DATA_PROVIDER} + iconType="cross" + iconSide="right" + onClick={togglePopover} + onClickAriaLabel={`${i18n.SHOW_OPTIONS_DATA_PROVIDER} ${formattedValue}`} + closeButtonProps={closeButtonProps} + data-test-subj="providerBadge" + > + {prefix} + {operator !== EXISTS_OPERATOR ? ( + <> + <span className="field-value">{`${field}: `}</span> + <span className="field-value">{`"${formattedValue}"`}</span> + </> + ) : ( + <span className="field-value"> + {field} {i18n.EXISTS_LABEL} + </span> + )} + </ProviderBadgeStyled> + ), + [ + providerId, + field, + val, + classes, + title, + deleteFilter, + togglePopover, + formattedValue, + closeButtonProps, + prefix, + operator, + ] + ); + + return <WithHoverActions hoverContent={hoverContent} render={badge} />; } ); diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/data_providers/provider_item_badge.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/data_providers/provider_item_badge.tsx index 79f9c32a176f5..2cc19537d6a63 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/data_providers/provider_item_badge.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/data_providers/provider_item_badge.tsx @@ -71,7 +71,7 @@ export const ProviderItemBadge = React.memo<ProviderItemBadgeProps>( return ( <TimelineContext.Consumer> - {isLoading => ( + {({ isLoading }) => ( <ProviderItemActions andProviderId={andProviderId} browserFields={browserFields} diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/data_providers/providers.test.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/data_providers/providers.test.tsx index d8029babf65f3..a4de8ffa3b9c5 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/data_providers/providers.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/data_providers/providers.test.tsx @@ -7,8 +7,10 @@ import { shallow } from 'enzyme'; import React from 'react'; +import { createKibanaCoreStartMock } from '../../../mock/kibana_core'; import { TestProviders } from '../../../mock/test_providers'; import { DroppableWrapper } from '../../drag_and_drop/droppable_wrapper'; +import { FilterManager } from '../../../../../../../../src/plugins/data/public'; import { TimelineContext } from '../timeline_context'; import { mockDataProviders } from './mock/mock_data_providers'; @@ -16,9 +18,12 @@ import { getDraggableId, Providers } from './providers'; import { DELETE_CLASS_NAME, ENABLE_CLASS_NAME, EXCLUDE_CLASS_NAME } from './provider_item_actions'; import { useMountAppended } from '../../../utils/use_mount_appended'; +const mockUiSettingsForFilterManager = createKibanaCoreStartMock().uiSettings; + describe('Providers', () => { - const mockTimelineContext: boolean = true; + const isLoading: boolean = true; const mount = useMountAppended(); + const filterManager = new FilterManager(mockUiSettingsForFilterManager); describe('rendering', () => { test('renders correctly against snapshot', () => { @@ -96,7 +101,7 @@ describe('Providers', () => { const mockOnDataProviderRemoved = jest.fn(); const wrapper = mount( <TestProviders> - <TimelineContext.Provider value={mockTimelineContext}> + <TimelineContext.Provider value={{ filterManager, isLoading }}> <DroppableWrapper droppableId="unitTest"> <Providers browserFields={{}} @@ -159,7 +164,7 @@ describe('Providers', () => { const mockOnDataProviderRemoved = jest.fn(); const wrapper = mount( <TestProviders> - <TimelineContext.Provider value={mockTimelineContext}> + <TimelineContext.Provider value={{ filterManager, isLoading }}> <DroppableWrapper droppableId="unitTest"> <Providers browserFields={{}} @@ -241,7 +246,7 @@ describe('Providers', () => { const mockOnToggleDataProviderEnabled = jest.fn(); const wrapper = mount( <TestProviders> - <TimelineContext.Provider value={mockTimelineContext}> + <TimelineContext.Provider value={{ filterManager, isLoading }}> <DroppableWrapper droppableId="unitTest"> <Providers browserFields={{}} @@ -319,7 +324,7 @@ describe('Providers', () => { const wrapper = mount( <TestProviders> - <TimelineContext.Provider value={mockTimelineContext}> + <TimelineContext.Provider value={{ filterManager, isLoading }}> <DroppableWrapper droppableId="unitTest"> <Providers browserFields={{}} @@ -428,7 +433,7 @@ describe('Providers', () => { const wrapper = mount( <TestProviders> - <TimelineContext.Provider value={mockTimelineContext}> + <TimelineContext.Provider value={{ filterManager, isLoading }}> <DroppableWrapper droppableId="unitTest"> <Providers browserFields={{}} @@ -509,7 +514,7 @@ describe('Providers', () => { const wrapper = mount( <TestProviders> - <TimelineContext.Provider value={mockTimelineContext}> + <TimelineContext.Provider value={{ filterManager, isLoading }}> <DroppableWrapper droppableId="unitTest"> <Providers browserFields={{}} @@ -595,7 +600,7 @@ describe('Providers', () => { const wrapper = mount( <TestProviders> - <TimelineContext.Provider value={mockTimelineContext}> + <TimelineContext.Provider value={{ filterManager, isLoading }}> <DroppableWrapper droppableId="unitTest"> <Providers browserFields={{}} diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/data_providers/translations.ts b/x-pack/legacy/plugins/siem/public/components/timeline/data_providers/translations.ts index 006c4430d67c9..eec12177b8b72 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/data_providers/translations.ts +++ b/x-pack/legacy/plugins/siem/public/components/timeline/data_providers/translations.ts @@ -10,6 +10,10 @@ export const AND = i18n.translate('xpack.siem.dataProviders.and', { defaultMessage: 'AND', }); +export const COPY_TO_CLIPBOARD = i18n.translate('xpack.siem.dataProviders.copyToClipboardTooltip', { + defaultMessage: 'Copy to Clipboard', +}); + export const DELETE_DATA_PROVIDER = i18n.translate('xpack.siem.dataProviders.deleteDataProvider', { defaultMessage: 'Delete', }); @@ -25,6 +29,10 @@ export const DROP_ANYTHING = i18n.translate('xpack.siem.dataProviders.dropAnythi defaultMessage: 'Drop anything', }); +export const EDIT = i18n.translate('xpack.siem.dataProviders.edit', { + defaultMessage: 'Edit', +}); + export const EDIT_MENU_ITEM = i18n.translate('xpack.siem.dataProviders.editMenuItem', { defaultMessage: 'Edit filter', }); @@ -44,6 +52,10 @@ export const EXISTS_LABEL = i18n.translate('xpack.siem.dataProviders.existsLabel defaultMessage: 'exists', }); +export const FIELD = i18n.translate('xpack.siem.dataProviders.fieldLabel', { + defaultMessage: 'Field', +}); + export const FILTER_FOR_FIELD_PRESENT = i18n.translate( 'xpack.siem.dataProviders.filterForFieldPresentLabel', { diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/header/__snapshots__/index.test.tsx.snap b/x-pack/legacy/plugins/siem/public/components/timeline/header/__snapshots__/index.test.tsx.snap index 90d0dc1a8a66d..42a1d4cd7f0f0 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/header/__snapshots__/index.test.tsx.snap +++ b/x-pack/legacy/plugins/siem/public/components/timeline/header/__snapshots__/index.test.tsx.snap @@ -149,6 +149,42 @@ exports[`Header rendering renders correctly against snapshot 1`] = ` /> <Connect(StatefulSearchOrFilterComponent) browserFields={Object {}} + filterManager={ + FilterManager { + "fetch$": Subject { + "_isScalar": false, + "closed": false, + "hasError": false, + "isStopped": false, + "observers": Array [], + "thrownError": null, + }, + "filters": Array [], + "uiSettings": Object { + "get": [MockFunction], + "get$": [MockFunction], + "getAll": [MockFunction], + "getSaved$": [MockFunction], + "getUpdate$": [MockFunction], + "getUpdateErrors$": [MockFunction], + "isCustom": [MockFunction], + "isDeclared": [MockFunction], + "isDefault": [MockFunction], + "isOverridden": [MockFunction], + "overrideLocalDefault": [MockFunction], + "remove": [MockFunction], + "set": [MockFunction], + }, + "updated$": Subject { + "_isScalar": false, + "closed": false, + "hasError": false, + "isStopped": false, + "observers": Array [], + "thrownError": null, + }, + } + } indexPattern={ Object { "fields": Array [ diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/header/index.test.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/header/index.test.tsx index 317c68b63f691..c43c69da64ba4 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/header/index.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/header/index.test.tsx @@ -8,12 +8,16 @@ import { shallow } from 'enzyme'; import React from 'react'; import { mockIndexPattern } from '../../../mock'; +import { createKibanaCoreStartMock } from '../../../mock/kibana_core'; import { TestProviders } from '../../../mock/test_providers'; +import { FilterManager } from '../../../../../../../../src/plugins/data/public'; import { mockDataProviders } from '../data_providers/mock/mock_data_providers'; import { useMountAppended } from '../../../utils/use_mount_appended'; import { TimelineHeader } from '.'; +const mockUiSettingsForFilterManager = createKibanaCoreStartMock().uiSettings; + jest.mock('../../../lib/kibana'); describe('Header', () => { @@ -26,6 +30,7 @@ describe('Header', () => { <TimelineHeader browserFields={{}} dataProviders={mockDataProviders} + filterManager={new FilterManager(mockUiSettingsForFilterManager)} id="foo" indexPattern={indexPattern} onChangeDataProviderKqlQuery={jest.fn()} @@ -47,6 +52,7 @@ describe('Header', () => { <TimelineHeader browserFields={{}} dataProviders={mockDataProviders} + filterManager={new FilterManager(mockUiSettingsForFilterManager)} id="foo" indexPattern={indexPattern} onChangeDataProviderKqlQuery={jest.fn()} @@ -70,6 +76,7 @@ describe('Header', () => { <TimelineHeader browserFields={{}} dataProviders={mockDataProviders} + filterManager={new FilterManager(mockUiSettingsForFilterManager)} id="foo" indexPattern={indexPattern} onChangeDataProviderKqlQuery={jest.fn()} diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/header/index.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/header/index.tsx index 7cac03cec42b1..99964c955bafe 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/header/index.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/header/index.tsx @@ -6,7 +6,7 @@ import { EuiCallOut } from '@elastic/eui'; import React from 'react'; -import { IIndexPattern } from 'src/plugins/data/public'; +import { FilterManager, IIndexPattern } from 'src/plugins/data/public'; import deepEqual from 'fast-deep-equal'; import { DataProviders } from '../data_providers'; @@ -27,6 +27,7 @@ import * as i18n from './translations'; interface Props { browserFields: BrowserFields; dataProviders: DataProvider[]; + filterManager: FilterManager; id: string; indexPattern: IIndexPattern; onChangeDataProviderKqlQuery: OnChangeDataProviderKqlQuery; @@ -44,6 +45,7 @@ const TimelineHeaderComponent: React.FC<Props> = ({ id, indexPattern, dataProviders, + filterManager, onChangeDataProviderKqlQuery, onChangeDroppableAndProvider, onDataProviderEdited, @@ -77,6 +79,7 @@ const TimelineHeaderComponent: React.FC<Props> = ({ /> <StatefulSearchOrFilter browserFields={browserFields} + filterManager={filterManager} indexPattern={indexPattern} timelineId={id} /> @@ -90,6 +93,7 @@ export const TimelineHeader = React.memo( prevProps.id === nextProps.id && deepEqual(prevProps.indexPattern, nextProps.indexPattern) && deepEqual(prevProps.dataProviders, nextProps.dataProviders) && + prevProps.filterManager === nextProps.filterManager && prevProps.onChangeDataProviderKqlQuery === nextProps.onChangeDataProviderKqlQuery && prevProps.onChangeDroppableAndProvider === nextProps.onChangeDroppableAndProvider && prevProps.onDataProviderEdited === nextProps.onDataProviderEdited && diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/insert_timeline_popover/index.test.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/insert_timeline_popover/index.test.tsx index adac26a8ac92b..e63bce388ae80 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/insert_timeline_popover/index.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/insert_timeline_popover/index.test.tsx @@ -26,6 +26,7 @@ const mockLocationWithState = { state: { insertTimeline: { timelineId: 'timeline-id', + timelineSavedObjectId: '34578-3497-5893-47589-34759', timelineTitle: 'Timeline title', }, }, @@ -49,7 +50,7 @@ describe('Insert timeline popover ', () => { payload: { id: 'timeline-id', show: false }, type: 'x-pack/siem/local/timeline/SHOW_TIMELINE', }); - expect(onTimelineChange).toBeCalledWith('Timeline title', 'timeline-id'); + expect(onTimelineChange).toBeCalledWith('Timeline title', '34578-3497-5893-47589-34759'); }); it('should do nothing when router state', () => { jest.spyOn(routeData, 'useLocation').mockReturnValue(mockLocation); diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/insert_timeline_popover/index.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/insert_timeline_popover/index.tsx index cf1a4ebec9bb6..573e010868bab 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/insert_timeline_popover/index.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/insert_timeline_popover/index.tsx @@ -23,6 +23,7 @@ interface InsertTimelinePopoverProps { interface RouterState { insertTimeline: { timelineId: string; + timelineSavedObjectId: string; timelineTitle: string; }; } @@ -46,7 +47,7 @@ export const InsertTimelinePopoverComponent: React.FC<Props> = ({ ); onTimelineChange( routerState.insertTimeline.timelineTitle, - routerState.insertTimeline.timelineId + routerState.insertTimeline.timelineSavedObjectId ); setRouterState(null); } diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/properties/helpers.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/properties/helpers.tsx index 0a2ab5c9d186a..6f7e1f782d3f6 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/properties/helpers.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/properties/helpers.tsx @@ -21,6 +21,7 @@ import React, { useCallback } from 'react'; import uuid from 'uuid'; import styled from 'styled-components'; import { useHistory } from 'react-router-dom'; +import { useSelector } from 'react-redux'; import { Note } from '../../../lib/note'; import { Notes } from '../../notes'; @@ -29,6 +30,8 @@ import { NOTES_PANEL_WIDTH } from './notes_size'; import { ButtonContainer, DescriptionContainer, LabelText, NameField, StyledStar } from './styles'; import * as i18n from './translations'; import { SiemPageName } from '../../../pages/home/types'; +import { timelineSelectors } from '../../../store/timeline'; +import { State } from '../../../store'; export const historyToolTip = 'The chronological history of actions related to this timeline'; export const streamLiveToolTip = 'Update the Timeline as new data arrives'; @@ -121,6 +124,9 @@ interface NewCaseProps { export const NewCase = React.memo<NewCaseProps>(({ onClosePopover, timelineId, timelineTitle }) => { const history = useHistory(); + const { savedObjectId } = useSelector((state: State) => + timelineSelectors.selectTimeline(state, timelineId) + ); const handleClick = useCallback(() => { onClosePopover(); history.push({ @@ -128,6 +134,7 @@ export const NewCase = React.memo<NewCaseProps>(({ onClosePopover, timelineId, t state: { insertTimeline: { timelineId, + timelineSavedObjectId: savedObjectId, timelineTitle: timelineTitle.length > 0 ? timelineTitle : i18n.UNTITLED_TIMELINE, }, }, diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/query_bar/index.test.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/query_bar/index.test.tsx index b978ef3d478d8..a49f6cc930abd 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/query_bar/index.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/query_bar/index.test.tsx @@ -7,16 +7,20 @@ import { mount } from 'enzyme'; import React from 'react'; -import { DEFAULT_FROM, DEFAULT_TO } from '../../../../common/constants'; +import { DEFAULT_FROM, DEFAULT_TO } from '../../../../../../../plugins/siem/common/constants'; import { mockBrowserFields } from '../../../containers/source/mock'; import { convertKueryToElasticSearchQuery } from '../../../lib/keury'; import { mockIndexPattern, TestProviders } from '../../../mock'; +import { createKibanaCoreStartMock } from '../../../mock/kibana_core'; import { QueryBar } from '../../query_bar'; +import { FilterManager } from '../../../../../../../../src/plugins/data/public'; import { mockDataProviders } from '../data_providers/mock/mock_data_providers'; import { buildGlobalQuery } from '../helpers'; import { QueryBarTimeline, QueryBarTimelineComponentProps, getDataProviderFilter } from './index'; +const mockUiSettingsForFilterManager = createKibanaCoreStartMock().uiSettings; + jest.mock('../../../lib/kibana'); describe('Timeline QueryBar ', () => { @@ -58,6 +62,7 @@ describe('Timeline QueryBar ', () => { browserFields={mockBrowserFields} dataProviders={mockDataProviders} filters={[]} + filterManager={new FilterManager(mockUiSettingsForFilterManager)} filterQuery={{ expression: 'here: query', kind: 'kuery' }} filterQueryDraft={{ expression: 'here: query', kind: 'kuery' }} from={0} @@ -99,6 +104,7 @@ describe('Timeline QueryBar ', () => { browserFields={mockBrowserFields} dataProviders={mockDataProviders} filters={[]} + filterManager={new FilterManager(mockUiSettingsForFilterManager)} filterQuery={{ expression: 'here: query', kind: 'kuery' }} filterQueryDraft={{ expression: 'here: query', kind: 'kuery' }} from={0} @@ -145,6 +151,7 @@ describe('Timeline QueryBar ', () => { browserFields={mockBrowserFields} dataProviders={mockDataProviders} filters={[]} + filterManager={new FilterManager(mockUiSettingsForFilterManager)} filterQuery={{ expression: 'here: query', kind: 'kuery' }} filterQueryDraft={{ expression: 'here: query', kind: 'kuery' }} from={0} @@ -189,6 +196,7 @@ describe('Timeline QueryBar ', () => { browserFields={mockBrowserFields} dataProviders={mockDataProviders} filters={[]} + filterManager={new FilterManager(mockUiSettingsForFilterManager)} filterQuery={{ expression: 'here: query', kind: 'kuery' }} filterQueryDraft={{ expression: 'here: query', kind: 'kuery' }} from={0} @@ -235,6 +243,7 @@ describe('Timeline QueryBar ', () => { browserFields={mockBrowserFields} dataProviders={mockDataProviders} filters={[]} + filterManager={new FilterManager(mockUiSettingsForFilterManager)} filterQuery={{ expression: 'here: query', kind: 'kuery' }} filterQueryDraft={{ expression: 'here: query', kind: 'kuery' }} from={0} @@ -279,6 +288,7 @@ describe('Timeline QueryBar ', () => { browserFields={mockBrowserFields} dataProviders={mockDataProviders} filters={[]} + filterManager={new FilterManager(mockUiSettingsForFilterManager)} filterQuery={{ expression: 'here: query', kind: 'kuery' }} filterQueryDraft={{ expression: 'here: query', kind: 'kuery' }} from={0} diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/query_bar/index.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/query_bar/index.tsx index 7f662cdb2f1b4..f53f7bb56e2f4 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/query_bar/index.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/query_bar/index.tsx @@ -21,7 +21,6 @@ import { import { BrowserFields } from '../../../containers/source'; import { convertKueryToElasticSearchQuery } from '../../../lib/keury'; -import { useKibana } from '../../../lib/kibana'; import { KueryFilterQuery, KueryFilterQueryKind } from '../../../store'; import { KqlMode } from '../../../store/timeline/model'; import { useSavedQueryServices } from '../../../utils/saved_query_services'; @@ -35,6 +34,7 @@ export interface QueryBarTimelineComponentProps { browserFields: BrowserFields; dataProviders: DataProvider[]; filters: Filter[]; + filterManager: FilterManager; filterQuery: KueryFilterQuery; filterQueryDraft: KueryFilterQuery; from: number; @@ -61,6 +61,7 @@ export const QueryBarTimeline = memo<QueryBarTimelineComponentProps>( browserFields, dataProviders, filters, + filterManager, filterQuery, filterQueryDraft, from, @@ -94,9 +95,6 @@ export const QueryBarTimeline = memo<QueryBarTimelineComponentProps>( const [dataProvidersDsl, setDataProvidersDsl] = useState<string>( convertKueryToElasticSearchQuery(buildGlobalQuery(dataProviders, browserFields), indexPattern) ); - const kibana = useKibana(); - const [filterManager] = useState<FilterManager>(new FilterManager(kibana.services.uiSettings)); - const savedQueryServices = useSavedQueryServices(); useEffect(() => { diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/search_or_filter/index.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/search_or_filter/index.tsx index 87061bdbb5d02..a0a0ac4c2b85c 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/search_or_filter/index.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/search_or_filter/index.tsx @@ -10,7 +10,11 @@ import { connect, ConnectedProps } from 'react-redux'; import { Dispatch } from 'redux'; import deepEqual from 'fast-deep-equal'; -import { Filter, IIndexPattern } from '../../../../../../../../src/plugins/data/public'; +import { + Filter, + FilterManager, + IIndexPattern, +} from '../../../../../../../../src/plugins/data/public'; import { BrowserFields } from '../../../containers/source'; import { convertKueryToElasticSearchQuery } from '../../../lib/keury'; import { @@ -29,6 +33,7 @@ import { SearchOrFilter } from './search_or_filter'; interface OwnProps { browserFields: BrowserFields; + filterManager: FilterManager; indexPattern: IIndexPattern; timelineId: string; } @@ -42,6 +47,7 @@ const StatefulSearchOrFilterComponent = React.memo<Props>( dataProviders, eventType, filters, + filterManager, filterQuery, filterQueryDraft, from, @@ -122,6 +128,7 @@ const StatefulSearchOrFilterComponent = React.memo<Props>( dataProviders={dataProviders} eventType={eventType} filters={filters} + filterManager={filterManager} filterQuery={filterQuery} filterQueryDraft={filterQueryDraft} from={from} @@ -146,6 +153,7 @@ const StatefulSearchOrFilterComponent = React.memo<Props>( (prevProps, nextProps) => { return ( prevProps.eventType === nextProps.eventType && + prevProps.filterManager === nextProps.filterManager && prevProps.from === nextProps.from && prevProps.fromStr === nextProps.fromStr && prevProps.to === nextProps.to && diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/search_or_filter/search_or_filter.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/search_or_filter/search_or_filter.tsx index 7bdd92e745f21..02a575db259bb 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/search_or_filter/search_or_filter.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/search_or_filter/search_or_filter.tsx @@ -8,7 +8,11 @@ import { EuiFlexGroup, EuiFlexItem, EuiSuperSelect, EuiToolTip } from '@elastic/ import React from 'react'; import styled, { createGlobalStyle } from 'styled-components'; -import { Filter, IIndexPattern } from '../../../../../../../../src/plugins/data/public'; +import { + Filter, + FilterManager, + IIndexPattern, +} from '../../../../../../../../src/plugins/data/public'; import { BrowserFields } from '../../../containers/source'; import { KueryFilterQuery, KueryFilterQueryKind } from '../../../store'; import { KqlMode, EventType } from '../../../store/timeline/model'; @@ -44,6 +48,7 @@ interface Props { browserFields: BrowserFields; dataProviders: DataProvider[]; eventType: EventType; + filterManager: FilterManager; filterQuery: KueryFilterQuery; filterQueryDraft: KueryFilterQuery; from: number; @@ -95,6 +100,7 @@ export const SearchOrFilter = React.memo<Props>( indexPattern, isRefreshPaused, filters, + filterManager, filterQuery, filterQueryDraft, from, @@ -135,6 +141,7 @@ export const SearchOrFilter = React.memo<Props>( browserFields={browserFields} dataProviders={dataProviders} filters={filters} + filterManager={filterManager} filterQuery={filterQuery} filterQueryDraft={filterQueryDraft} from={from} diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/timeline.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/timeline.tsx index 098dd82791610..222cc0530bddb 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/timeline.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/timeline.tsx @@ -6,7 +6,7 @@ import { EuiFlyoutHeader, EuiFlyoutBody, EuiFlyoutFooter } from '@elastic/eui'; import { getOr, isEmpty } from 'lodash/fp'; -import React, { useMemo } from 'react'; +import React, { useState, useMemo } from 'react'; import styled from 'styled-components'; import { FlyoutHeaderWithCloseButton } from '../flyout/header_with_close_button'; @@ -34,7 +34,12 @@ import { TimelineHeader } from './header'; import { combineQueries } from './helpers'; import { TimelineRefetch } from './refetch_timeline'; import { ManageTimelineContext } from './timeline_context'; -import { esQuery, Filter, IIndexPattern } from '../../../../../../../src/plugins/data/public'; +import { + esQuery, + Filter, + FilterManager, + IIndexPattern, +} from '../../../../../../../src/plugins/data/public'; const TimelineContainer = styled.div` height: 100%; @@ -143,6 +148,7 @@ export const TimelineComponent: React.FC<Props> = ({ usersViewing, }) => { const kibana = useKibana(); + const [filterManager] = useState<FilterManager>(new FilterManager(kibana.services.uiSettings)); const combinedQueries = combineQueries({ config: esQuery.getEsQueryConfig(kibana.services.uiSettings), dataProviders, @@ -178,6 +184,7 @@ export const TimelineComponent: React.FC<Props> = ({ id={id} indexPattern={indexPattern} dataProviders={dataProviders} + filterManager={filterManager} onChangeDataProviderKqlQuery={onChangeDataProviderKqlQuery} onChangeDroppableAndProvider={onChangeDroppableAndProvider} onDataProviderEdited={onDataProviderEdited} @@ -211,7 +218,12 @@ export const TimelineComponent: React.FC<Props> = ({ getUpdatedAt, refetch, }) => ( - <ManageTimelineContext loading={loading || loadingIndexName}> + <ManageTimelineContext + filterManager={filterManager} + indexToAdd={indexToAdd} + loading={loading || loadingIndexName} + type={{ id }} + > <TimelineRefetch id={id} inputId="timeline" diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/timeline_context.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/timeline_context.tsx index f1100e17bd3cb..7c1eadd8e8bed 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/timeline_context.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/timeline_context.tsx @@ -5,15 +5,25 @@ */ import React, { createContext, memo, useContext, useEffect, useState } from 'react'; + +import { FilterManager } from '../../../../../../../src/plugins/data/public'; + import { TimelineAction } from './body/actions'; -const initTimelineContext = false; -export const TimelineContext = createContext<boolean>(initTimelineContext); +interface TimelineContextState { + filterManager: FilterManager | undefined; + isLoading: boolean; +} + +const initTimelineContext: TimelineContextState = { filterManager: undefined, isLoading: false }; +export const TimelineContext = createContext<TimelineContextState>(initTimelineContext); export const useTimelineContext = () => useContext(TimelineContext); export interface TimelineTypeContextProps { documentType?: string; footerText?: string; + id?: string; + indexToAdd?: string[] | null; loadingText?: string; queryFields?: string[]; selectAll?: boolean; @@ -24,6 +34,8 @@ export interface TimelineTypeContextProps { const initTimelineType: TimelineTypeContextProps = { documentType: undefined, footerText: undefined, + id: undefined, + indexToAdd: undefined, loadingText: undefined, queryFields: [], selectAll: false, @@ -36,6 +48,8 @@ export const useTimelineTypeContext = () => useContext(TimelineTypeContext); interface ManageTimelineContextProps { children: React.ReactNode; + filterManager: FilterManager; + indexToAdd?: string[] | null; loading: boolean; type?: TimelineTypeContextProps; } @@ -44,22 +58,27 @@ interface ManageTimelineContextProps { // to avoid so many Context, at least the separation of code is there now const ManageTimelineContextComponent: React.FC<ManageTimelineContextProps> = ({ children, + filterManager, + indexToAdd, loading, - type = initTimelineType, + type = { ...initTimelineType, indexToAdd }, }) => { - const [myLoading, setLoading] = useState(initTimelineContext); - const [myType, setType] = useState(initTimelineType); + const [myContextState, setMyContextState] = useState<TimelineContextState>({ + filterManager, + isLoading: false, + }); + const [myType, setType] = useState<TimelineTypeContextProps>(type); useEffect(() => { - setLoading(loading); - }, [loading]); + setMyContextState({ filterManager, isLoading: loading }); + }, [setMyContextState, filterManager, loading]); useEffect(() => { - setType(type); - }, [type]); + setType({ ...type, indexToAdd }); + }, [type, indexToAdd]); return ( - <TimelineContext.Provider value={myLoading}> + <TimelineContext.Provider value={myContextState}> <TimelineTypeContext.Provider value={myType}>{children}</TimelineTypeContext.Provider> </TimelineContext.Provider> ); diff --git a/x-pack/legacy/plugins/siem/public/components/top_n/helpers.test.tsx b/x-pack/legacy/plugins/siem/public/components/top_n/helpers.test.tsx new file mode 100644 index 0000000000000..da0f6f59b533f --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/components/top_n/helpers.test.tsx @@ -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 { allEvents, defaultOptions, getOptions, rawEvents, signalEvents } from './helpers'; + +describe('getOptions', () => { + test(`it returns the default options when 'activeTimelineEventType' is undefined`, () => { + expect(getOptions()).toEqual(defaultOptions); + }); + + test(`it returns 'allEvents' when 'activeTimelineEventType' is 'all'`, () => { + expect(getOptions('all')).toEqual(allEvents); + }); + + test(`it returns 'rawEvents' when 'activeTimelineEventType' is 'raw'`, () => { + expect(getOptions('raw')).toEqual(rawEvents); + }); + + test(`it returns 'signalEvents' when 'activeTimelineEventType' is 'signal'`, () => { + expect(getOptions('signal')).toEqual(signalEvents); + }); +}); diff --git a/x-pack/legacy/plugins/siem/public/components/top_n/helpers.ts b/x-pack/legacy/plugins/siem/public/components/top_n/helpers.ts new file mode 100644 index 0000000000000..8d9ae48d29b69 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/components/top_n/helpers.ts @@ -0,0 +1,66 @@ +/* + * 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 { EventType } from '../../store/timeline/model'; + +import * as i18n from './translations'; + +export interface TopNOption { + inputDisplay: string; + value: EventType; + 'data-test-subj': string; +} + +/** A (stable) array containing only the 'All events' option */ +export const allEvents: TopNOption[] = [ + { + value: 'all', + inputDisplay: i18n.ALL_EVENTS, + 'data-test-subj': 'option-all', + }, +]; + +/** A (stable) array containing only the 'Raw events' option */ +export const rawEvents: TopNOption[] = [ + { + value: 'raw', + inputDisplay: i18n.RAW_EVENTS, + 'data-test-subj': 'option-raw', + }, +]; + +/** A (stable) array containing only the 'Signal events' option */ +export const signalEvents: TopNOption[] = [ + { + value: 'signal', + inputDisplay: i18n.SIGNAL_EVENTS, + 'data-test-subj': 'option-signal', + }, +]; + +/** A (stable) array containing the default Top N options */ +export const defaultOptions = [...rawEvents, ...signalEvents]; + +/** + * Returns the options to be displayed in a Top N view select. When + * an `activeTimelineEventType` is provided, an array containing + * just one option (corresponding to `activeTimelineEventType`) + * will be returned, to ensure the data displayed in the Top N + * is always in sync with the `EventType` chosen by the user in + * the active timeline. + */ +export const getOptions = (activeTimelineEventType?: EventType): TopNOption[] => { + switch (activeTimelineEventType) { + case 'all': + return allEvents; + case 'raw': + return rawEvents; + case 'signal': + return signalEvents; + default: + return defaultOptions; + } +}; diff --git a/x-pack/legacy/plugins/siem/public/components/top_n/index.test.tsx b/x-pack/legacy/plugins/siem/public/components/top_n/index.test.tsx new file mode 100644 index 0000000000000..61772f1dc7a7a --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/components/top_n/index.test.tsx @@ -0,0 +1,379 @@ +/* + * 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 { mount, ReactWrapper } from 'enzyme'; +import React from 'react'; + +import { mockBrowserFields } from '../../containers/source/mock'; +import { apolloClientObservable, mockGlobalState, TestProviders } from '../../mock'; +import { createKibanaCoreStartMock } from '../../mock/kibana_core'; +import { FilterManager } from '../../../../../../../src/plugins/data/public'; +import { createStore, State } from '../../store'; +import { TimelineContext, TimelineTypeContext } from '../timeline/timeline_context'; + +import { Props } from './top_n'; +import { ACTIVE_TIMELINE_REDUX_ID, StatefulTopN } from '.'; + +jest.mock('../../lib/kibana'); + +const mockUiSettingsForFilterManager = createKibanaCoreStartMock().uiSettings; + +const field = 'process.name'; +const value = 'nice'; + +const state: State = { + ...mockGlobalState, + inputs: { + ...mockGlobalState.inputs, + global: { + ...mockGlobalState.inputs.global, + query: { + query: 'host.name : end*', + language: 'kuery', + }, + filters: [ + { + meta: { + alias: null, + negate: false, + disabled: false, + type: 'phrase', + key: 'host.os.name', + params: { + query: 'Linux', + }, + }, + query: { + match: { + 'host.os.name': { + query: 'Linux', + type: 'phrase', + }, + }, + }, + }, + ], + }, + timeline: { + ...mockGlobalState.inputs.timeline, + timerange: { + kind: 'relative', + fromStr: 'now-24h', + toStr: 'now', + from: 1586835969047, + to: 1586922369047, + }, + }, + }, + timeline: { + ...mockGlobalState.timeline, + timelineById: { + [ACTIVE_TIMELINE_REDUX_ID]: { + ...mockGlobalState.timeline.timelineById.test, + id: ACTIVE_TIMELINE_REDUX_ID, + dataProviders: [ + { + id: + 'draggable-badge-default-draggable-netflow-renderer-timeline-1-_qpBe3EBD7k-aQQL7v7--_qpBe3EBD7k-aQQL7v7--network_transport-tcp', + name: 'tcp', + enabled: true, + excluded: false, + kqlQuery: '', + queryMatch: { + field: 'network.transport', + value: 'tcp', + operator: ':', + }, + and: [], + }, + ], + eventType: 'all', + filters: [ + { + meta: { + alias: null, + disabled: false, + key: 'source.port', + negate: false, + params: { + query: '30045', + }, + type: 'phrase', + }, + query: { + match: { + 'source.port': { + query: '30045', + type: 'phrase', + }, + }, + }, + }, + ], + kqlMode: 'filter', + kqlQuery: { + filterQuery: { + kuery: { + kind: 'kuery', + expression: 'host.name : *', + }, + serializedQuery: + '{"bool":{"should":[{"exists":{"field":"host.name"}}],"minimum_should_match":1}}', + }, + filterQueryDraft: { + kind: 'kuery', + expression: 'host.name : *', + }, + }, + }, + }, + }, +}; +const store = createStore(state, apolloClientObservable); + +describe('StatefulTopN', () => { + // Suppress warnings about "react-beautiful-dnd" + /* eslint-disable no-console */ + const originalError = console.error; + const originalWarn = console.warn; + beforeAll(() => { + console.warn = jest.fn(); + console.error = jest.fn(); + }); + afterAll(() => { + console.error = originalError; + console.warn = originalWarn; + }); + + describe('rendering in a global NON-timeline context', () => { + let wrapper: ReactWrapper; + + beforeEach(() => { + wrapper = mount( + <TestProviders store={store}> + <StatefulTopN + browserFields={mockBrowserFields} + field={field} + toggleTopN={jest.fn()} + onFilterAdded={jest.fn()} + value={value} + /> + </TestProviders> + ); + }); + + test('it has undefined combinedQueries when rendering in a global context', () => { + const props = wrapper + .find('[data-test-subj="top-n"]') + .first() + .props() as Props; + + expect(props.combinedQueries).toBeUndefined(); + }); + + test(`defaults to the 'Raw events' view when rendering in a global context`, () => { + const props = wrapper + .find('[data-test-subj="top-n"]') + .first() + .props() as Props; + + expect(props.defaultView).toEqual('raw'); + }); + + test(`provides a 'deleteQuery' when rendering in a global context`, () => { + const props = wrapper + .find('[data-test-subj="top-n"]') + .first() + .props() as Props; + + expect(props.deleteQuery).toBeDefined(); + }); + + test(`provides filters from Redux state (inputs > global > filters) when rendering in a global context`, () => { + const props = wrapper + .find('[data-test-subj="top-n"]') + .first() + .props() as Props; + + expect(props.filters).toEqual([ + { + meta: { + alias: null, + negate: false, + disabled: false, + type: 'phrase', + key: 'host.os.name', + params: { query: 'Linux' }, + }, + query: { match: { 'host.os.name': { query: 'Linux', type: 'phrase' } } }, + }, + ]); + }); + + test(`provides 'from' via GlobalTime when rendering in a global context`, () => { + const props = wrapper + .find('[data-test-subj="top-n"]') + .first() + .props() as Props; + + expect(props.from).toEqual(0); + }); + + test('provides the global query from Redux state (inputs > global > query) when rendering in a global context', () => { + const props = wrapper + .find('[data-test-subj="top-n"]') + .first() + .props() as Props; + + expect(props.query).toEqual({ query: 'host.name : end*', language: 'kuery' }); + }); + + test(`provides a 'global' 'setAbsoluteRangeDatePickerTarget' when rendering in a global context`, () => { + const props = wrapper + .find('[data-test-subj="top-n"]') + .first() + .props() as Props; + + expect(props.setAbsoluteRangeDatePickerTarget).toEqual('global'); + }); + + test(`provides 'to' via GlobalTime when rendering in a global context`, () => { + const props = wrapper + .find('[data-test-subj="top-n"]') + .first() + .props() as Props; + + expect(props.to).toEqual(1); + }); + }); + + describe('rendering in a timeline context', () => { + let filterManager: FilterManager; + let wrapper: ReactWrapper; + + beforeEach(() => { + filterManager = new FilterManager(mockUiSettingsForFilterManager); + + wrapper = mount( + <TestProviders store={store}> + <TimelineContext.Provider value={{ filterManager, isLoading: false }}> + <TimelineTypeContext.Provider value={{ id: ACTIVE_TIMELINE_REDUX_ID }}> + <StatefulTopN + browserFields={mockBrowserFields} + field={field} + toggleTopN={jest.fn()} + onFilterAdded={jest.fn()} + value={value} + /> + </TimelineTypeContext.Provider> + </TimelineContext.Provider> + </TestProviders> + ); + }); + + test('it has a combinedQueries value from Redux state composed of the timeline [data providers + kql + filter-bar-filters] when rendering in a timeline context', () => { + const props = wrapper + .find('[data-test-subj="top-n"]') + .first() + .props() as Props; + + expect(props.combinedQueries).toEqual( + '{"bool":{"must":[],"filter":[{"bool":{"filter":[{"bool":{"filter":[{"bool":{"should":[{"match_phrase":{"network.transport":"tcp"}}],"minimum_should_match":1}},{"bool":{"should":[{"exists":{"field":"host.name"}}],"minimum_should_match":1}}]}},{"bool":{"filter":[{"bool":{"should":[{"range":{"@timestamp":{"gte":1586835969047}}}],"minimum_should_match":1}},{"bool":{"should":[{"range":{"@timestamp":{"lte":1586922369047}}}],"minimum_should_match":1}}]}}]}},{"match_phrase":{"source.port":{"query":"30045"}}}],"should":[],"must_not":[]}}' + ); + }); + + test('it provides only one view option that matches the `eventType` from redux when rendering in the context of the active timeline', () => { + const props = wrapper + .find('[data-test-subj="top-n"]') + .first() + .props() as Props; + + expect(props.defaultView).toEqual('all'); + }); + + test(`provides an undefined 'deleteQuery' when rendering in a timeline context`, () => { + const props = wrapper + .find('[data-test-subj="top-n"]') + .first() + .props() as Props; + + expect(props.deleteQuery).toBeUndefined(); + }); + + test(`provides empty filters when rendering in a timeline context`, () => { + const props = wrapper + .find('[data-test-subj="top-n"]') + .first() + .props() as Props; + + expect(props.filters).toEqual([]); + }); + + test(`provides 'from' via redux state (inputs > timeline > timerange) when rendering in a timeline context`, () => { + const props = wrapper + .find('[data-test-subj="top-n"]') + .first() + .props() as Props; + + expect(props.from).toEqual(1586835969047); + }); + + test('provides an empty query when rendering in a timeline context', () => { + const props = wrapper + .find('[data-test-subj="top-n"]') + .first() + .props() as Props; + + expect(props.query).toEqual({ query: '', language: 'kuery' }); + }); + + test(`provides a 'timeline' 'setAbsoluteRangeDatePickerTarget' when rendering in a timeline context`, () => { + const props = wrapper + .find('[data-test-subj="top-n"]') + .first() + .props() as Props; + + expect(props.setAbsoluteRangeDatePickerTarget).toEqual('timeline'); + }); + + test(`provides 'to' via redux state (inputs > timeline > timerange) when rendering in a timeline context`, () => { + const props = wrapper + .find('[data-test-subj="top-n"]') + .first() + .props() as Props; + + expect(props.to).toEqual(1586922369047); + }); + }); + + test(`defaults to the 'Signals events' option when rendering in a NON-active timeline context (e.g. the Signals table on the Detections page) when 'documentType' from 'useTimelineTypeContext()' is 'signals'`, () => { + const filterManager = new FilterManager(mockUiSettingsForFilterManager); + const wrapper = mount( + <TestProviders store={store}> + <TimelineContext.Provider value={{ filterManager, isLoading: false }}> + <TimelineTypeContext.Provider + value={{ documentType: 'signals', id: ACTIVE_TIMELINE_REDUX_ID }} + > + <StatefulTopN + browserFields={mockBrowserFields} + field={field} + toggleTopN={jest.fn()} + onFilterAdded={jest.fn()} + value={value} + /> + </TimelineTypeContext.Provider> + </TimelineContext.Provider> + </TestProviders> + ); + + const props = wrapper + .find('[data-test-subj="top-n"]') + .first() + .props() as Props; + + expect(props.defaultView).toEqual('signal'); + }); +}); diff --git a/x-pack/legacy/plugins/siem/public/components/top_n/index.tsx b/x-pack/legacy/plugins/siem/public/components/top_n/index.tsx new file mode 100644 index 0000000000000..983b234a04eaa --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/components/top_n/index.tsx @@ -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 React from 'react'; +import { connect, ConnectedProps } from 'react-redux'; + +import { GlobalTime } from '../../containers/global_time'; +import { BrowserFields, WithSource } from '../../containers/source'; +import { useKibana } from '../../lib/kibana'; +import { esQuery, Filter, Query } from '../../../../../../../src/plugins/data/public'; +import { inputsModel, inputsSelectors, State, timelineSelectors } from '../../store'; +import { setAbsoluteRangeDatePicker as dispatchSetAbsoluteRangeDatePicker } from '../../store/inputs/actions'; +import { timelineDefaults } from '../../store/timeline/defaults'; +import { TimelineModel } from '../../store/timeline/model'; +import { combineQueries } from '../timeline/helpers'; +import { useTimelineTypeContext } from '../timeline/timeline_context'; + +import { getOptions } from './helpers'; +import { TopN } from './top_n'; + +/** The currently active timeline always has this Redux ID */ +export const ACTIVE_TIMELINE_REDUX_ID = 'timeline-1'; + +const EMPTY_FILTERS: Filter[] = []; +const EMPTY_QUERY: Query = { query: '', language: 'kuery' }; + +const makeMapStateToProps = () => { + const getGlobalQuerySelector = inputsSelectors.globalQuerySelector(); + const getGlobalFiltersQuerySelector = inputsSelectors.globalFiltersQuerySelector(); + const getTimeline = timelineSelectors.getTimelineByIdSelector(); + const getInputsTimeline = inputsSelectors.getTimelineSelector(); + const getKqlQueryTimeline = timelineSelectors.getKqlFilterQuerySelector(); + + // The mapped Redux state provided to this component includes the global + // filters that appear at the top of most views in the app, and all the + // filters in the active timeline: + const mapStateToProps = (state: State) => { + const activeTimeline: TimelineModel = + getTimeline(state, ACTIVE_TIMELINE_REDUX_ID) ?? timelineDefaults; + const activeTimelineFilters = activeTimeline.filters ?? EMPTY_FILTERS; + const activeTimelineInput: inputsModel.InputsRange = getInputsTimeline(state); + + return { + activeTimelineEventType: activeTimeline.eventType, + activeTimelineFilters, + activeTimelineFrom: activeTimelineInput.timerange.from, + activeTimelineKqlQueryExpression: getKqlQueryTimeline(state, ACTIVE_TIMELINE_REDUX_ID), + activeTimelineTo: activeTimelineInput.timerange.to, + dataProviders: activeTimeline.dataProviders, + globalQuery: getGlobalQuerySelector(state), + globalFilters: getGlobalFiltersQuerySelector(state), + kqlMode: activeTimeline.kqlMode, + }; + }; + + return mapStateToProps; +}; + +const mapDispatchToProps = { setAbsoluteRangeDatePicker: dispatchSetAbsoluteRangeDatePicker }; + +const connector = connect(makeMapStateToProps, mapDispatchToProps); + +interface OwnProps { + browserFields: BrowserFields; + field: string; + toggleTopN: () => void; + onFilterAdded?: () => void; + value?: string[] | string | null; +} +type PropsFromRedux = ConnectedProps<typeof connector>; +type Props = OwnProps & PropsFromRedux; + +const StatefulTopNComponent: React.FC<Props> = ({ + activeTimelineEventType, + activeTimelineFilters, + activeTimelineFrom, + activeTimelineKqlQueryExpression, + activeTimelineTo, + browserFields, + dataProviders, + field, + globalFilters = EMPTY_FILTERS, + globalQuery = EMPTY_QUERY, + kqlMode, + onFilterAdded, + setAbsoluteRangeDatePicker, + toggleTopN, + value, +}) => { + const kibana = useKibana(); + + // Regarding data from useTimelineTypeContext: + // * `documentType` (e.g. 'signals') may only be populated in some views, + // e.g. the `Signals` view on the `Detections` page. + // * `id` (`timelineId`) may only be populated when we are rendered in the + // context of the active timeline. + // * `indexToAdd`, which enables the signals index to be appended to + // the `indexPattern` returned by `WithSource`, may only be populated when + // this component is rendered in the context of the active timeline. This + // behavior enables the 'All events' view by appending the signals index + // to the index pattern. + const { documentType, id: timelineId, indexToAdd } = useTimelineTypeContext(); + + const options = getOptions( + timelineId === ACTIVE_TIMELINE_REDUX_ID ? activeTimelineEventType : undefined + ); + + return ( + <GlobalTime> + {({ from, deleteQuery, setQuery, to }) => ( + <WithSource sourceId="default" indexToAdd={indexToAdd}> + {({ indexPattern }) => ( + <TopN + combinedQueries={ + timelineId === ACTIVE_TIMELINE_REDUX_ID + ? combineQueries({ + browserFields, + config: esQuery.getEsQueryConfig(kibana.services.uiSettings), + dataProviders, + end: activeTimelineTo, + filters: activeTimelineFilters, + indexPattern, + kqlMode, + kqlQuery: { + language: 'kuery', + query: activeTimelineKqlQueryExpression ?? '', + }, + start: activeTimelineFrom, + })?.filterQuery + : undefined + } + data-test-subj="top-n" + defaultView={ + documentType?.toLocaleLowerCase() === 'signals' ? 'signal' : options[0].value + } + deleteQuery={timelineId === ACTIVE_TIMELINE_REDUX_ID ? undefined : deleteQuery} + field={field} + filters={timelineId === ACTIVE_TIMELINE_REDUX_ID ? EMPTY_FILTERS : globalFilters} + from={timelineId === ACTIVE_TIMELINE_REDUX_ID ? activeTimelineFrom : from} + indexPattern={indexPattern} + indexToAdd={indexToAdd} + options={options} + query={timelineId === ACTIVE_TIMELINE_REDUX_ID ? EMPTY_QUERY : globalQuery} + setAbsoluteRangeDatePicker={setAbsoluteRangeDatePicker} + setAbsoluteRangeDatePickerTarget={ + timelineId === ACTIVE_TIMELINE_REDUX_ID ? 'timeline' : 'global' + } + setQuery={setQuery} + to={timelineId === ACTIVE_TIMELINE_REDUX_ID ? activeTimelineTo : to} + toggleTopN={toggleTopN} + onFilterAdded={onFilterAdded} + value={value} + /> + )} + </WithSource> + )} + </GlobalTime> + ); +}; + +StatefulTopNComponent.displayName = 'StatefulTopNComponent'; + +export const StatefulTopN = connector(React.memo(StatefulTopNComponent)); diff --git a/x-pack/legacy/plugins/siem/public/components/top_n/top_n.test.tsx b/x-pack/legacy/plugins/siem/public/components/top_n/top_n.test.tsx new file mode 100644 index 0000000000000..13b77ea0ccd4c --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/components/top_n/top_n.test.tsx @@ -0,0 +1,261 @@ +/* + * 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 { mount, ReactWrapper } from 'enzyme'; +import React from 'react'; + +import { TestProviders, mockIndexPattern } from '../../mock'; +import { setAbsoluteRangeDatePicker } from '../../store/inputs/actions'; + +import { allEvents, defaultOptions } from './helpers'; +import { TopN } from './top_n'; + +jest.mock('../../lib/kibana'); + +jest.mock('uuid', () => { + return { + v1: jest.fn(() => 'uuid.v1()'), + v4: jest.fn(() => 'uuid.v4()'), + }; +}); + +const field = 'process.name'; +const value = 'nice'; +const combinedQueries = { + bool: { + must: [], + filter: [ + { + bool: { + filter: [ + { + bool: { + filter: [ + { + bool: { + should: [{ match_phrase: { 'network.transport': 'tcp' } }], + minimum_should_match: 1, + }, + }, + { + bool: { should: [{ exists: { field: 'host.name' } }], minimum_should_match: 1 }, + }, + ], + }, + }, + { + bool: { + filter: [ + { + bool: { + should: [{ range: { '@timestamp': { gte: 1586824450493 } } }], + minimum_should_match: 1, + }, + }, + { + bool: { + should: [{ range: { '@timestamp': { lte: 1586910850493 } } }], + minimum_should_match: 1, + }, + }, + ], + }, + }, + ], + }, + }, + { match_phrase: { 'source.port': { query: '30045' } } }, + ], + should: [], + must_not: [], + }, +}; + +describe('TopN', () => { + // Suppress warnings about "react-beautiful-dnd" + /* eslint-disable no-console */ + const originalError = console.error; + const originalWarn = console.warn; + beforeAll(() => { + console.warn = jest.fn(); + console.error = jest.fn(); + }); + afterAll(() => { + console.error = originalError; + console.warn = originalWarn; + }); + + const query = { query: '', language: 'kuery' }; + + describe('common functionality', () => { + let toggleTopN: () => void; + let wrapper: ReactWrapper; + + beforeEach(() => { + toggleTopN = jest.fn(); + wrapper = mount( + <TestProviders> + <TopN + defaultView="raw" + field={field} + filters={[]} + from={1586824307695} + indexPattern={mockIndexPattern} + options={defaultOptions} + query={query} + setAbsoluteRangeDatePicker={setAbsoluteRangeDatePicker} + setAbsoluteRangeDatePickerTarget="global" + setQuery={jest.fn()} + to={1586910707695} + toggleTopN={toggleTopN} + value={value} + /> + </TestProviders> + ); + }); + + test('it invokes the toggleTopN function when the close button is clicked', () => { + wrapper + .find('[data-test-subj="close"]') + .first() + .simulate('click'); + wrapper.update(); + + expect(toggleTopN).toHaveBeenCalled(); + }); + + test('it enables the view select by default', () => { + expect( + wrapper + .find('[data-test-subj="view-select"]') + .first() + .props().disabled + ).toBe(false); + }); + }); + + describe('events view', () => { + let toggleTopN: () => void; + let wrapper: ReactWrapper; + + beforeEach(() => { + toggleTopN = jest.fn(); + wrapper = mount( + <TestProviders> + <TopN + defaultView="raw" + field={field} + filters={[]} + from={1586824307695} + indexPattern={mockIndexPattern} + options={defaultOptions} + query={query} + setAbsoluteRangeDatePicker={setAbsoluteRangeDatePicker} + setAbsoluteRangeDatePickerTarget="global" + setQuery={jest.fn()} + to={1586910707695} + toggleTopN={toggleTopN} + value={value} + /> + </TestProviders> + ); + }); + + test(`it renders EventsByDataset when defaultView is 'raw'`, () => { + expect( + wrapper.find('[data-test-subj="eventsByDatasetOverview-uuid.v4()Panel"]').exists() + ).toBe(true); + }); + + test(`it does NOT render SignalsByCategory when defaultView is 'raw'`, () => { + expect(wrapper.find('[data-test-subj="signals-histogram-panel"]').exists()).toBe(false); + }); + }); + + describe('signals view', () => { + let toggleTopN: () => void; + let wrapper: ReactWrapper; + + beforeEach(() => { + toggleTopN = jest.fn(); + wrapper = mount( + <TestProviders> + <TopN + defaultView="signal" + field={field} + filters={[]} + from={1586824307695} + indexPattern={mockIndexPattern} + options={defaultOptions} + query={query} + setAbsoluteRangeDatePicker={setAbsoluteRangeDatePicker} + setAbsoluteRangeDatePickerTarget="global" + setQuery={jest.fn()} + to={1586910707695} + toggleTopN={toggleTopN} + value={value} + /> + </TestProviders> + ); + }); + + test(`it renders SignalsByCategory when defaultView is 'signal'`, () => { + expect(wrapper.find('[data-test-subj="signals-histogram-panel"]').exists()).toBe(true); + }); + + test(`it does NOT render EventsByDataset when defaultView is 'signal'`, () => { + expect( + wrapper.find('[data-test-subj="eventsByDatasetOverview-uuid.v4()Panel"]').exists() + ).toBe(false); + }); + }); + + describe('All events, a view shown only when rendered in the context of the active timeline', () => { + let wrapper: ReactWrapper; + + beforeEach(() => { + wrapper = mount( + <TestProviders> + <TopN + combinedQueries={JSON.stringify(combinedQueries)} + defaultView="all" + field={field} + filters={[]} + from={1586824307695} + indexPattern={mockIndexPattern} + options={allEvents} + query={query} + setAbsoluteRangeDatePicker={setAbsoluteRangeDatePicker} + setAbsoluteRangeDatePickerTarget="global" + setQuery={jest.fn()} + to={1586910707695} + toggleTopN={jest.fn()} + value={value} + /> + </TestProviders> + ); + }); + + test(`it disables the view select when 'options' contains only one entry`, () => { + expect( + wrapper + .find('[data-test-subj="view-select"]') + .first() + .props().disabled + ).toBe(true); + }); + + test(`it renders EventsByDataset when defaultView is 'all'`, () => { + expect( + wrapper.find('[data-test-subj="eventsByDatasetOverview-uuid.v4()Panel"]').exists() + ).toBe(true); + }); + + test(`it does NOT render SignalsByCategory when defaultView is 'all'`, () => { + expect(wrapper.find('[data-test-subj="signals-histogram-panel"]').exists()).toBe(false); + }); + }); +}); diff --git a/x-pack/legacy/plugins/siem/public/components/top_n/top_n.tsx b/x-pack/legacy/plugins/siem/public/components/top_n/top_n.tsx new file mode 100644 index 0000000000000..136252617e2a2 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/components/top_n/top_n.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 { EuiButtonIcon, EuiSuperSelect } from '@elastic/eui'; +import React, { useCallback, useMemo, useState } from 'react'; +import styled from 'styled-components'; +import { ActionCreator } from 'typescript-fsa'; + +import { EventsByDataset } from '../../pages/overview/events_by_dataset'; +import { SignalsByCategory } from '../../pages/overview/signals_by_category'; +import { Filter, IIndexPattern, Query } from '../../../../../../../src/plugins/data/public'; +import { inputsModel } from '../../store'; +import { InputsModelId } from '../../store/inputs/constants'; +import { EventType } from '../../store/timeline/model'; + +import { TopNOption } from './helpers'; +import * as i18n from './translations'; + +const TopNContainer = styled.div` + width: 600px; +`; + +const CloseButton = styled(EuiButtonIcon)` + z-index: 999999; + position: absolute; + right: 4px; + top: 4px; +`; + +const ViewSelect = styled(EuiSuperSelect)` + z-index: 999999; + width: 155px; +`; + +const TopNContent = styled.div` + margin-top: 4px; + + .euiPanel { + border: none; + } +`; + +export interface Props { + combinedQueries?: string; + defaultView: EventType; + deleteQuery?: ({ id }: { id: string }) => void; + field: string; + filters: Filter[]; + from: number; + indexPattern: IIndexPattern; + indexToAdd?: string[] | null; + options: TopNOption[]; + query: Query; + setAbsoluteRangeDatePicker: ActionCreator<{ + id: InputsModelId; + from: number; + to: number; + }>; + setAbsoluteRangeDatePickerTarget: InputsModelId; + setQuery: (params: { + id: string; + inspect: inputsModel.InspectQuery | null; + loading: boolean; + refetch: inputsModel.Refetch; + }) => void; + to: number; + toggleTopN: () => void; + onFilterAdded?: () => void; + value?: string[] | string | null; +} + +const NO_FILTERS: Filter[] = []; +const DEFAULT_QUERY: Query = { query: '', language: 'kuery' }; + +const TopNComponent: React.FC<Props> = ({ + combinedQueries, + defaultView, + deleteQuery, + filters = NO_FILTERS, + field, + from, + indexPattern, + indexToAdd, + options, + query = DEFAULT_QUERY, + setAbsoluteRangeDatePicker, + setAbsoluteRangeDatePickerTarget, + setQuery, + to, + toggleTopN, +}) => { + const [view, setView] = useState<EventType>(defaultView); + const onViewSelected = useCallback((value: string) => setView(value as EventType), [setView]); + + const headerChildren = useMemo( + () => ( + <ViewSelect + data-test-subj="view-select" + disabled={options.length === 1} + onChange={onViewSelected} + options={options} + valueOfSelected={view} + /> + ), + [onViewSelected, options, view] + ); + + return ( + <TopNContainer> + <CloseButton + aria-label={i18n.CLOSE} + data-test-subj="close" + iconType="cross" + onClick={toggleTopN} + /> + + <TopNContent> + {view === 'raw' || view === 'all' ? ( + <EventsByDataset + combinedQueries={combinedQueries} + deleteQuery={deleteQuery} + filters={filters} + from={from} + headerChildren={headerChildren} + indexPattern={indexPattern} + indexToAdd={indexToAdd} + onlyField={field} + query={query} + setAbsoluteRangeDatePickerTarget={setAbsoluteRangeDatePickerTarget} + setQuery={setQuery} + showSpacer={false} + to={to} + /> + ) : ( + <SignalsByCategory + filters={filters} + from={from} + headerChildren={headerChildren} + indexPattern={indexPattern} + onlyField={field} + query={query} + setAbsoluteRangeDatePicker={setAbsoluteRangeDatePicker} + setAbsoluteRangeDatePickerTarget={setAbsoluteRangeDatePickerTarget} + setQuery={setQuery} + to={to} + /> + )} + </TopNContent> + </TopNContainer> + ); +}; + +TopNComponent.displayName = 'TopNComponent'; + +export const TopN = React.memo(TopNComponent); diff --git a/x-pack/legacy/plugins/siem/public/components/top_n/translations.ts b/x-pack/legacy/plugins/siem/public/components/top_n/translations.ts new file mode 100644 index 0000000000000..7db55fa94d42e --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/components/top_n/translations.ts @@ -0,0 +1,23 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { i18n } from '@kbn/i18n'; + +export const CLOSE = i18n.translate('xpack.siem.topN.closeButtonLabel', { + defaultMessage: 'Close', +}); + +export const ALL_EVENTS = i18n.translate('xpack.siem.topN.allEventsSelectLabel', { + defaultMessage: 'All events', +}); + +export const RAW_EVENTS = i18n.translate('xpack.siem.topN.rawEventsSelectLabel', { + defaultMessage: 'Raw events', +}); + +export const SIGNAL_EVENTS = i18n.translate('xpack.siem.topN.signalEventsSelectLabel', { + defaultMessage: 'Signal events', +}); diff --git a/x-pack/legacy/plugins/siem/public/components/with_hover_actions/index.tsx b/x-pack/legacy/plugins/siem/public/components/with_hover_actions/index.tsx index 07ea165fcbb5c..86a9acc486b6d 100644 --- a/x-pack/legacy/plugins/siem/public/components/with_hover_actions/index.tsx +++ b/x-pack/legacy/plugins/siem/public/components/with_hover_actions/index.tsx @@ -4,8 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { useState, useCallback } from 'react'; -import styled from 'styled-components'; +import { EuiPopover } from '@elastic/eui'; +import React, { useCallback, useMemo, useState } from 'react'; +import { IS_DRAGGING_CLASS_NAME } from '../drag_and_drop/helpers'; interface Props { /** @@ -26,34 +27,6 @@ interface Props { */ render: (showHoverContent: boolean) => JSX.Element; } - -const HoverActionsPanelContainer = styled.div` - color: ${({ theme }) => theme.eui.textColors.default}; - height: 100%; - position: relative; -`; - -HoverActionsPanelContainer.displayName = 'HoverActionsPanelContainer'; - -const HoverActionsPanel = React.memo<{ children: JSX.Element; show: boolean }>( - ({ children, show }) => ( - <HoverActionsPanelContainer data-test-subj="hover-actions-panel-container"> - {show ? children : null} - </HoverActionsPanelContainer> - ) -); - -HoverActionsPanel.displayName = 'HoverActionsPanel'; - -const WithHoverActionsContainer = styled.div` - display: flex; - flex-direction: row; - height: 100%; - padding-right: 5px; -`; - -WithHoverActionsContainer.displayName = 'WithHoverActionsContainer'; - /** * Decorates it's children with actions that are visible on hover. * This component does not enforce an opinion on the styling and @@ -68,20 +41,41 @@ export const WithHoverActions = React.memo<Props>( ({ alwaysShow = false, hoverContent, render }) => { const [showHoverContent, setShowHoverContent] = useState(false); const onMouseEnter = useCallback(() => { - setShowHoverContent(true); + // NOTE: the following read from the DOM is expensive, but not as + // expensive as the default behavior, which adds a div to the body, + // which-in turn performs a more expensive change to the layout + if (!document.body.classList.contains(IS_DRAGGING_CLASS_NAME)) { + setShowHoverContent(true); + } }, []); const onMouseLeave = useCallback(() => { setShowHoverContent(false); }, []); + const content = useMemo(() => <>{render(showHoverContent)}</>, [render, showHoverContent]); + + const isOpen = hoverContent != null && (showHoverContent || alwaysShow); + + const popover = useMemo(() => { + return ( + <EuiPopover + anchorPosition={'downCenter'} + button={content} + closePopover={onMouseLeave} + hasArrow={false} + isOpen={isOpen} + panelPaddingSize={!alwaysShow ? 's' : 'none'} + > + {isOpen ? hoverContent : null} + </EuiPopover> + ); + }, [content, onMouseLeave, isOpen, alwaysShow, hoverContent]); + return ( - <WithHoverActionsContainer onMouseEnter={onMouseEnter} onMouseLeave={onMouseLeave}> - <>{render(showHoverContent)}</> - <HoverActionsPanel show={showHoverContent || alwaysShow}> - {hoverContent != null ? hoverContent : <></>} - </HoverActionsPanel> - </WithHoverActionsContainer> + <div onMouseEnter={onMouseEnter} onMouseLeave={onMouseLeave}> + {popover} + </div> ); } ); diff --git a/x-pack/legacy/plugins/siem/public/containers/anomalies/anomalies_query_tab_body/index.tsx b/x-pack/legacy/plugins/siem/public/containers/anomalies/anomalies_query_tab_body/index.tsx index 85e19248f2eb5..83c38f2a76175 100644 --- a/x-pack/legacy/plugins/siem/public/containers/anomalies/anomalies_query_tab_body/index.tsx +++ b/x-pack/legacy/plugins/siem/public/containers/anomalies/anomalies_query_tab_body/index.tsx @@ -5,11 +5,12 @@ */ import React, { useEffect } from 'react'; + +import { DEFAULT_ANOMALY_SCORE } from '../../../../../../../plugins/siem/common/constants'; import { AnomaliesQueryTabBodyProps } from './types'; import { getAnomaliesFilterQuery } from './utils'; import { useSiemJobs } from '../../../components/ml_popover/hooks/use_siem_jobs'; import { useUiSetting$ } from '../../../lib/kibana'; -import { DEFAULT_ANOMALY_SCORE } from '../../../../common/constants'; import { MatrixHistogramContainer } from '../../../components/matrix_histogram'; import { histogramConfigs } from './histogram_configs'; const ID = 'anomaliesOverTimeQuery'; diff --git a/x-pack/legacy/plugins/siem/public/containers/anomalies/anomalies_query_tab_body/types.ts b/x-pack/legacy/plugins/siem/public/containers/anomalies/anomalies_query_tab_body/types.ts index f6cae81e3c6c4..d17eadc68d04b 100644 --- a/x-pack/legacy/plugins/siem/public/containers/anomalies/anomalies_query_tab_body/types.ts +++ b/x-pack/legacy/plugins/siem/public/containers/anomalies/anomalies_query_tab_body/types.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { ESTermQuery } from '../../../../common/typed_json'; +import { ESTermQuery } from '../../../../../../../plugins/siem/common/typed_json'; import { NarrowDateRange } from '../../../components/ml/types'; import { UpdateDateRange } from '../../../components/charts/common'; import { SetQuery } from '../../../pages/hosts/navigation/types'; diff --git a/x-pack/legacy/plugins/siem/public/containers/anomalies/anomalies_query_tab_body/utils.ts b/x-pack/legacy/plugins/siem/public/containers/anomalies/anomalies_query_tab_body/utils.ts index 9609619916ab1..f698e302d3423 100644 --- a/x-pack/legacy/plugins/siem/public/containers/anomalies/anomalies_query_tab_body/utils.ts +++ b/x-pack/legacy/plugins/siem/public/containers/anomalies/anomalies_query_tab_body/utils.ts @@ -5,8 +5,9 @@ */ import deepmerge from 'deepmerge'; + +import { ESTermQuery } from '../../../../../../../plugins/siem/common/typed_json'; import { createFilter } from '../../helpers'; -import { ESTermQuery } from '../../../../common/typed_json'; import { SiemJob } from '../../../components/ml_popover/types'; import { FlowTarget } from '../../../graphql/types'; diff --git a/x-pack/legacy/plugins/siem/public/containers/authentications/index.tsx b/x-pack/legacy/plugins/siem/public/containers/authentications/index.tsx index 6d4a88c45a768..13bb40dad04bd 100644 --- a/x-pack/legacy/plugins/siem/public/containers/authentications/index.tsx +++ b/x-pack/legacy/plugins/siem/public/containers/authentications/index.tsx @@ -10,7 +10,7 @@ import { Query } from 'react-apollo'; import { connect } from 'react-redux'; import { compose } from 'redux'; -import { DEFAULT_INDEX_KEY } from '../../../common/constants'; +import { DEFAULT_INDEX_KEY } from '../../../../../../plugins/siem/common/constants'; import { AuthenticationsEdges, GetAuthenticationsQuery, diff --git a/x-pack/legacy/plugins/siem/public/containers/case/__mocks__/api.ts b/x-pack/legacy/plugins/siem/public/containers/case/__mocks__/api.ts new file mode 100644 index 0000000000000..6d2cfb7147537 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/containers/case/__mocks__/api.ts @@ -0,0 +1,122 @@ +/* + * 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 { + ActionLicense, + AllCases, + BulkUpdateStatus, + Case, + CasesStatus, + CaseUserActions, + FetchCasesProps, + SortFieldCase, +} from '../types'; +import { + actionLicenses, + allCases, + basicCase, + basicCaseCommentPatch, + basicCasePost, + casesStatus, + caseUserActions, + pushedCase, + respReporters, + serviceConnector, + tags, +} from '../mock'; +import { + CaseExternalServiceRequest, + CasePatchRequest, + CasePostRequest, + CommentRequest, + ServiceConnectorCaseParams, + ServiceConnectorCaseResponse, + User, +} from '../../../../../../../plugins/case/common/api'; + +export const getCase = async ( + caseId: string, + includeComments: boolean = true, + signal: AbortSignal +): Promise<Case> => { + return Promise.resolve(basicCase); +}; + +export const getCasesStatus = async (signal: AbortSignal): Promise<CasesStatus> => + Promise.resolve(casesStatus); + +export const getTags = async (signal: AbortSignal): Promise<string[]> => Promise.resolve(tags); + +export const getReporters = async (signal: AbortSignal): Promise<User[]> => + Promise.resolve(respReporters); + +export const getCaseUserActions = async ( + caseId: string, + signal: AbortSignal +): Promise<CaseUserActions[]> => Promise.resolve(caseUserActions); + +export const getCases = async ({ + filterOptions = { + search: '', + reporters: [], + status: 'open', + tags: [], + }, + queryParams = { + page: 1, + perPage: 5, + sortField: SortFieldCase.createdAt, + sortOrder: 'desc', + }, + signal, +}: FetchCasesProps): Promise<AllCases> => Promise.resolve(allCases); + +export const postCase = async (newCase: CasePostRequest, signal: AbortSignal): Promise<Case> => + Promise.resolve(basicCasePost); + +export const patchCase = async ( + caseId: string, + updatedCase: Pick<CasePatchRequest, 'description' | 'status' | 'tags' | 'title'>, + version: string, + signal: AbortSignal +): Promise<Case[]> => Promise.resolve([basicCase]); + +export const patchCasesStatus = async ( + cases: BulkUpdateStatus[], + signal: AbortSignal +): Promise<Case[]> => Promise.resolve(allCases.cases); + +export const postComment = async ( + newComment: CommentRequest, + caseId: string, + signal: AbortSignal +): Promise<Case> => Promise.resolve(basicCase); + +export const patchComment = async ( + caseId: string, + commentId: string, + commentUpdate: string, + version: string, + signal: AbortSignal +): Promise<Case> => Promise.resolve(basicCaseCommentPatch); + +export const deleteCases = async (caseIds: string[], signal: AbortSignal): Promise<boolean> => + Promise.resolve(true); + +export const pushCase = async ( + caseId: string, + push: CaseExternalServiceRequest, + signal: AbortSignal +): Promise<Case> => Promise.resolve(pushedCase); + +export const pushToService = async ( + connectorId: string, + casePushParams: ServiceConnectorCaseParams, + signal: AbortSignal +): Promise<ServiceConnectorCaseResponse> => Promise.resolve(serviceConnector); + +export const getActionLicense = async (signal: AbortSignal): Promise<ActionLicense[]> => + Promise.resolve(actionLicenses); diff --git a/x-pack/legacy/plugins/siem/public/containers/case/api.test.tsx b/x-pack/legacy/plugins/siem/public/containers/case/api.test.tsx new file mode 100644 index 0000000000000..4f5655cc9f221 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/containers/case/api.test.tsx @@ -0,0 +1,463 @@ +/* + * 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 { KibanaServices } from '../../lib/kibana'; +import { + deleteCases, + getActionLicense, + getCase, + getCases, + getCasesStatus, + getCaseUserActions, + getReporters, + getTags, + patchCase, + patchCasesStatus, + patchComment, + postCase, + postComment, + pushCase, + pushToService, +} from './api'; +import { + actionLicenses, + allCases, + basicCase, + allCasesSnake, + basicCaseSnake, + actionTypeExecutorResult, + pushedCaseSnake, + casesStatus, + casesSnake, + cases, + caseUserActions, + pushedCase, + pushSnake, + reporters, + respReporters, + serviceConnector, + casePushParams, + tags, + caseUserActionsSnake, + casesStatusSnake, +} from './mock'; +import { CASES_URL } from './constants'; +import { DEFAULT_FILTER_OPTIONS, DEFAULT_QUERY_PARAMS } from './use_get_cases'; +import * as i18n from './translations'; + +const abortCtrl = new AbortController(); +const mockKibanaServices = KibanaServices.get as jest.Mock; +jest.mock('../../lib/kibana'); + +const fetchMock = jest.fn(); +mockKibanaServices.mockReturnValue({ http: { fetch: fetchMock } }); + +describe('Case Configuration API', () => { + describe('deleteCases', () => { + beforeEach(() => { + fetchMock.mockClear(); + fetchMock.mockResolvedValue(''); + }); + const data = ['1', '2']; + + test('check url, method, signal', async () => { + await deleteCases(data, abortCtrl.signal); + expect(fetchMock).toHaveBeenCalledWith(`${CASES_URL}`, { + method: 'DELETE', + query: { ids: JSON.stringify(data) }, + signal: abortCtrl.signal, + }); + }); + + test('happy path', async () => { + const resp = await deleteCases(data, abortCtrl.signal); + expect(resp).toEqual(''); + }); + }); + describe('getActionLicense', () => { + beforeEach(() => { + fetchMock.mockClear(); + fetchMock.mockResolvedValue(actionLicenses); + }); + test('check url, method, signal', async () => { + await getActionLicense(abortCtrl.signal); + expect(fetchMock).toHaveBeenCalledWith(`/api/action/types`, { + method: 'GET', + signal: abortCtrl.signal, + }); + }); + + test('happy path', async () => { + const resp = await getActionLicense(abortCtrl.signal); + expect(resp).toEqual(actionLicenses); + }); + }); + describe('getCase', () => { + beforeEach(() => { + fetchMock.mockClear(); + fetchMock.mockResolvedValue(basicCaseSnake); + }); + const data = basicCase.id; + + test('check url, method, signal', async () => { + await getCase(data, true, abortCtrl.signal); + expect(fetchMock).toHaveBeenCalledWith(`${CASES_URL}/${basicCase.id}`, { + method: 'GET', + query: { includeComments: true }, + signal: abortCtrl.signal, + }); + }); + + test('happy path', async () => { + const resp = await getCase(data, true, abortCtrl.signal); + expect(resp).toEqual(basicCase); + }); + }); + describe('getCases', () => { + beforeEach(() => { + fetchMock.mockClear(); + fetchMock.mockResolvedValue(allCasesSnake); + }); + test('check url, method, signal', async () => { + await getCases({ + filterOptions: DEFAULT_FILTER_OPTIONS, + queryParams: DEFAULT_QUERY_PARAMS, + signal: abortCtrl.signal, + }); + expect(fetchMock).toHaveBeenCalledWith(`${CASES_URL}/_find`, { + method: 'GET', + query: { + ...DEFAULT_QUERY_PARAMS, + reporters: [], + tags: [], + status: 'open', + }, + signal: abortCtrl.signal, + }); + }); + test('correctly applies filters', async () => { + await getCases({ + filterOptions: { + ...DEFAULT_FILTER_OPTIONS, + reporters: [...respReporters, { username: null, full_name: null, email: null }], + tags, + status: '', + search: 'hello', + }, + queryParams: DEFAULT_QUERY_PARAMS, + signal: abortCtrl.signal, + }); + expect(fetchMock).toHaveBeenCalledWith(`${CASES_URL}/_find`, { + method: 'GET', + query: { + ...DEFAULT_QUERY_PARAMS, + reporters, + tags, + search: 'hello', + }, + signal: abortCtrl.signal, + }); + }); + + test('happy path', async () => { + const resp = await getCases({ + filterOptions: DEFAULT_FILTER_OPTIONS, + queryParams: DEFAULT_QUERY_PARAMS, + signal: abortCtrl.signal, + }); + expect(resp).toEqual({ ...allCases }); + }); + }); + describe('getCasesStatus', () => { + beforeEach(() => { + fetchMock.mockClear(); + fetchMock.mockResolvedValue(casesStatusSnake); + }); + test('check url, method, signal', async () => { + await getCasesStatus(abortCtrl.signal); + expect(fetchMock).toHaveBeenCalledWith(`${CASES_URL}/status`, { + method: 'GET', + signal: abortCtrl.signal, + }); + }); + + test('happy path', async () => { + const resp = await getCasesStatus(abortCtrl.signal); + expect(resp).toEqual(casesStatus); + }); + }); + describe('getCaseUserActions', () => { + beforeEach(() => { + fetchMock.mockClear(); + fetchMock.mockResolvedValue(caseUserActionsSnake); + }); + + test('check url, method, signal', async () => { + await getCaseUserActions(basicCase.id, abortCtrl.signal); + expect(fetchMock).toHaveBeenCalledWith(`${CASES_URL}/${basicCase.id}/user_actions`, { + method: 'GET', + signal: abortCtrl.signal, + }); + }); + + test('happy path', async () => { + const resp = await getCaseUserActions(basicCase.id, abortCtrl.signal); + expect(resp).toEqual(caseUserActions); + }); + }); + describe('getReporters', () => { + beforeEach(() => { + fetchMock.mockClear(); + fetchMock.mockResolvedValue(respReporters); + }); + + test('check url, method, signal', async () => { + await getReporters(abortCtrl.signal); + expect(fetchMock).toHaveBeenCalledWith(`${CASES_URL}/reporters`, { + method: 'GET', + signal: abortCtrl.signal, + }); + }); + + test('happy path', async () => { + const resp = await getReporters(abortCtrl.signal); + expect(resp).toEqual(respReporters); + }); + }); + describe('getTags', () => { + beforeEach(() => { + fetchMock.mockClear(); + fetchMock.mockResolvedValue(tags); + }); + + test('check url, method, signal', async () => { + await getTags(abortCtrl.signal); + expect(fetchMock).toHaveBeenCalledWith(`${CASES_URL}/tags`, { + method: 'GET', + signal: abortCtrl.signal, + }); + }); + + test('happy path', async () => { + const resp = await getTags(abortCtrl.signal); + expect(resp).toEqual(tags); + }); + }); + describe('patchCase', () => { + beforeEach(() => { + fetchMock.mockClear(); + fetchMock.mockResolvedValue([basicCaseSnake]); + }); + const data = { description: 'updated description' }; + test('check url, method, signal', async () => { + await patchCase(basicCase.id, data, basicCase.version, abortCtrl.signal); + expect(fetchMock).toHaveBeenCalledWith(`${CASES_URL}`, { + method: 'PATCH', + body: JSON.stringify({ + cases: [{ ...data, id: basicCase.id, version: basicCase.version }], + }), + signal: abortCtrl.signal, + }); + }); + + test('happy path', async () => { + const resp = await patchCase( + basicCase.id, + { description: 'updated description' }, + basicCase.version, + abortCtrl.signal + ); + expect(resp).toEqual({ ...[basicCase] }); + }); + }); + describe('patchCasesStatus', () => { + beforeEach(() => { + fetchMock.mockClear(); + fetchMock.mockResolvedValue(casesSnake); + }); + const data = [ + { + status: 'closed', + id: basicCase.id, + version: basicCase.version, + }, + ]; + + test('check url, method, signal', async () => { + await patchCasesStatus(data, abortCtrl.signal); + expect(fetchMock).toHaveBeenCalledWith(`${CASES_URL}`, { + method: 'PATCH', + body: JSON.stringify({ cases: data }), + signal: abortCtrl.signal, + }); + }); + + test('happy path', async () => { + const resp = await patchCasesStatus(data, abortCtrl.signal); + expect(resp).toEqual({ ...cases }); + }); + }); + describe('patchComment', () => { + beforeEach(() => { + fetchMock.mockClear(); + fetchMock.mockResolvedValue(basicCaseSnake); + }); + + test('check url, method, signal', async () => { + await patchComment( + basicCase.id, + basicCase.comments[0].id, + 'updated comment', + basicCase.comments[0].version, + abortCtrl.signal + ); + expect(fetchMock).toHaveBeenCalledWith(`${CASES_URL}/${basicCase.id}/comments`, { + method: 'PATCH', + body: JSON.stringify({ + comment: 'updated comment', + id: basicCase.comments[0].id, + version: basicCase.comments[0].version, + }), + signal: abortCtrl.signal, + }); + }); + + test('happy path', async () => { + const resp = await patchComment( + basicCase.id, + basicCase.comments[0].id, + 'updated comment', + basicCase.comments[0].version, + abortCtrl.signal + ); + expect(resp).toEqual(basicCase); + }); + }); + describe('postCase', () => { + beforeEach(() => { + fetchMock.mockClear(); + fetchMock.mockResolvedValue(basicCaseSnake); + }); + const data = { + description: 'description', + tags: ['tag'], + title: 'title', + }; + + test('check url, method, signal', async () => { + await postCase(data, abortCtrl.signal); + expect(fetchMock).toHaveBeenCalledWith(`${CASES_URL}`, { + method: 'POST', + body: JSON.stringify(data), + signal: abortCtrl.signal, + }); + }); + + test('happy path', async () => { + const resp = await postCase(data, abortCtrl.signal); + expect(resp).toEqual(basicCase); + }); + }); + describe('postComment', () => { + beforeEach(() => { + fetchMock.mockClear(); + fetchMock.mockResolvedValue(basicCaseSnake); + }); + const data = { + comment: 'comment', + }; + + test('check url, method, signal', async () => { + await postComment(data, basicCase.id, abortCtrl.signal); + expect(fetchMock).toHaveBeenCalledWith(`${CASES_URL}/${basicCase.id}/comments`, { + method: 'POST', + body: JSON.stringify(data), + signal: abortCtrl.signal, + }); + }); + + test('happy path', async () => { + const resp = await postComment(data, basicCase.id, abortCtrl.signal); + expect(resp).toEqual(basicCase); + }); + }); + describe('pushCase', () => { + beforeEach(() => { + fetchMock.mockClear(); + fetchMock.mockResolvedValue(pushedCaseSnake); + }); + + test('check url, method, signal', async () => { + await pushCase(basicCase.id, pushSnake, abortCtrl.signal); + expect(fetchMock).toHaveBeenCalledWith(`${CASES_URL}/${basicCase.id}/_push`, { + method: 'POST', + body: JSON.stringify(pushSnake), + signal: abortCtrl.signal, + }); + }); + + test('happy path', async () => { + const resp = await pushCase(basicCase.id, pushSnake, abortCtrl.signal); + expect(resp).toEqual(pushedCase); + }); + }); + describe('pushToService', () => { + beforeEach(() => { + fetchMock.mockClear(); + fetchMock.mockResolvedValue(actionTypeExecutorResult); + }); + const connectorId = 'connectorId'; + test('check url, method, signal', async () => { + await pushToService(connectorId, casePushParams, abortCtrl.signal); + expect(fetchMock).toHaveBeenCalledWith(`/api/action/${connectorId}/_execute`, { + method: 'POST', + body: JSON.stringify({ params: casePushParams }), + signal: abortCtrl.signal, + }); + }); + + test('happy path', async () => { + const resp = await pushToService(connectorId, casePushParams, abortCtrl.signal); + expect(resp).toEqual(serviceConnector); + }); + + test('unhappy path - serviceMessage', async () => { + const theError = 'the error'; + fetchMock.mockResolvedValue({ + ...actionTypeExecutorResult, + status: 'error', + serviceMessage: theError, + message: 'not it', + }); + await expect( + pushToService(connectorId, casePushParams, abortCtrl.signal) + ).rejects.toMatchObject({ message: theError }); + }); + + test('unhappy path - message', async () => { + const theError = 'the error'; + fetchMock.mockResolvedValue({ + ...actionTypeExecutorResult, + status: 'error', + message: theError, + }); + await expect( + pushToService(connectorId, casePushParams, abortCtrl.signal) + ).rejects.toMatchObject({ message: theError }); + }); + + test('unhappy path - no message', async () => { + const theError = i18n.ERROR_PUSH_TO_SERVICE; + fetchMock.mockResolvedValue({ + ...actionTypeExecutorResult, + status: 'error', + }); + await expect( + pushToService(connectorId, casePushParams, abortCtrl.signal) + ).rejects.toMatchObject({ message: theError }); + }); + }); +}); diff --git a/x-pack/legacy/plugins/siem/public/containers/case/api.ts b/x-pack/legacy/plugins/siem/public/containers/case/api.ts index 69e1602b3d981..12b4c80a2dd89 100644 --- a/x-pack/legacy/plugins/siem/public/containers/case/api.ts +++ b/x-pack/legacy/plugins/siem/public/containers/case/api.ts @@ -204,13 +204,13 @@ export const patchComment = async ( return convertToCamelCase<CaseResponse, Case>(decodeCaseResponse(response)); }; -export const deleteCases = async (caseIds: string[], signal: AbortSignal): Promise<boolean> => { +export const deleteCases = async (caseIds: string[], signal: AbortSignal): Promise<string> => { const response = await KibanaServices.get().http.fetch<string>(CASES_URL, { method: 'DELETE', query: { ids: JSON.stringify(caseIds) }, signal, }); - return response === 'true' ? true : false; + return response; }; export const pushCase = async ( diff --git a/x-pack/legacy/plugins/siem/public/containers/case/configure/__mocks__/api.ts b/x-pack/legacy/plugins/siem/public/containers/case/configure/__mocks__/api.ts new file mode 100644 index 0000000000000..03f7d241e5dff --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/containers/case/configure/__mocks__/api.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 { + CasesConfigurePatch, + CasesConfigureRequest, + Connector, +} from '../../../../../../../../plugins/case/common/api'; + +import { ApiProps } from '../../types'; +import { CaseConfigure } from '../types'; +import { connectorsMock, caseConfigurationCamelCaseResponseMock } from '../mock'; + +export const fetchConnectors = async ({ signal }: ApiProps): Promise<Connector[]> => + Promise.resolve(connectorsMock); + +export const getCaseConfigure = async ({ signal }: ApiProps): Promise<CaseConfigure> => + Promise.resolve(caseConfigurationCamelCaseResponseMock); + +export const postCaseConfigure = async ( + caseConfiguration: CasesConfigureRequest, + signal: AbortSignal +): Promise<CaseConfigure> => Promise.resolve(caseConfigurationCamelCaseResponseMock); + +export const patchCaseConfigure = async ( + caseConfiguration: CasesConfigurePatch, + signal: AbortSignal +): Promise<CaseConfigure> => Promise.resolve(caseConfigurationCamelCaseResponseMock); diff --git a/x-pack/legacy/plugins/siem/public/containers/case/configure/api.test.ts b/x-pack/legacy/plugins/siem/public/containers/case/configure/api.test.ts new file mode 100644 index 0000000000000..ef0e51fb1c24d --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/containers/case/configure/api.test.ts @@ -0,0 +1,115 @@ +/* + * 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 { KibanaServices } from '../../../lib/kibana'; +import { fetchConnectors, getCaseConfigure, postCaseConfigure, patchCaseConfigure } from './api'; +import { + connectorsMock, + caseConfigurationMock, + caseConfigurationResposeMock, + caseConfigurationCamelCaseResponseMock, +} from './mock'; + +const abortCtrl = new AbortController(); +const mockKibanaServices = KibanaServices.get as jest.Mock; +jest.mock('../../../lib/kibana'); + +const fetchMock = jest.fn(); +mockKibanaServices.mockReturnValue({ http: { fetch: fetchMock } }); + +describe('Case Configuration API', () => { + describe('fetch connectors', () => { + beforeEach(() => { + fetchMock.mockClear(); + fetchMock.mockResolvedValue(connectorsMock); + }); + + test('check url, method, signal', async () => { + await fetchConnectors({ signal: abortCtrl.signal }); + expect(fetchMock).toHaveBeenCalledWith('/api/cases/configure/connectors/_find', { + method: 'GET', + signal: abortCtrl.signal, + }); + }); + + test('happy path', async () => { + const resp = await fetchConnectors({ signal: abortCtrl.signal }); + expect(resp).toEqual(connectorsMock); + }); + }); + + describe('fetch configuration', () => { + beforeEach(() => { + fetchMock.mockClear(); + fetchMock.mockResolvedValue(caseConfigurationResposeMock); + }); + + test('check url, method, signal', async () => { + await getCaseConfigure({ signal: abortCtrl.signal }); + expect(fetchMock).toHaveBeenCalledWith('/api/cases/configure', { + method: 'GET', + signal: abortCtrl.signal, + }); + }); + + test('happy path', async () => { + const resp = await getCaseConfigure({ signal: abortCtrl.signal }); + expect(resp).toEqual(caseConfigurationCamelCaseResponseMock); + }); + + test('return null on empty response', async () => { + fetchMock.mockResolvedValue({}); + const resp = await getCaseConfigure({ signal: abortCtrl.signal }); + expect(resp).toBe(null); + }); + }); + + describe('create configuration', () => { + beforeEach(() => { + fetchMock.mockClear(); + fetchMock.mockResolvedValue(caseConfigurationResposeMock); + }); + + test('check url, body, method, signal', async () => { + await postCaseConfigure(caseConfigurationMock, abortCtrl.signal); + expect(fetchMock).toHaveBeenCalledWith('/api/cases/configure', { + body: + '{"connector_id":"123","connector_name":"My Connector","closure_type":"close-by-user"}', + method: 'POST', + signal: abortCtrl.signal, + }); + }); + + test('happy path', async () => { + const resp = await postCaseConfigure(caseConfigurationMock, abortCtrl.signal); + expect(resp).toEqual(caseConfigurationCamelCaseResponseMock); + }); + }); + + describe('update configuration', () => { + beforeEach(() => { + fetchMock.mockClear(); + fetchMock.mockResolvedValue(caseConfigurationResposeMock); + }); + + test('check url, body, method, signal', async () => { + await patchCaseConfigure({ connector_id: '456', version: 'WzHJ12' }, abortCtrl.signal); + expect(fetchMock).toHaveBeenCalledWith('/api/cases/configure', { + body: '{"connector_id":"456","version":"WzHJ12"}', + method: 'PATCH', + signal: abortCtrl.signal, + }); + }); + + test('happy path', async () => { + const resp = await patchCaseConfigure( + { connector_id: '456', version: 'WzHJ12' }, + abortCtrl.signal + ); + expect(resp).toEqual(caseConfigurationCamelCaseResponseMock); + }); + }); +}); diff --git a/x-pack/legacy/plugins/siem/public/containers/case/configure/mock.ts b/x-pack/legacy/plugins/siem/public/containers/case/configure/mock.ts new file mode 100644 index 0000000000000..d2491b39fdf56 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/containers/case/configure/mock.ts @@ -0,0 +1,99 @@ +/* + * 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 { + Connector, + CasesConfigureResponse, + CasesConfigureRequest, +} from '../../../../../../../plugins/case/common/api'; +import { CaseConfigure } from './types'; + +export const connectorsMock: Connector[] = [ + { + id: '123', + actionTypeId: '.servicenow', + name: 'My Connector', + config: { + apiUrl: 'https://instance1.service-now.com', + casesConfiguration: { + mapping: [ + { + source: 'title', + target: 'short_description', + actionType: 'overwrite', + }, + { + source: 'description', + target: 'description', + actionType: 'append', + }, + { + source: 'comments', + target: 'comments', + actionType: 'append', + }, + ], + }, + }, + isPreconfigured: true, + }, + { + id: '456', + actionTypeId: '.servicenow', + name: 'My Connector 2', + config: { + apiUrl: 'https://instance2.service-now.com', + casesConfiguration: { + mapping: [ + { + source: 'title', + target: 'short_description', + actionType: 'overwrite', + }, + { + source: 'description', + target: 'description', + actionType: 'overwrite', + }, + { + source: 'comments', + target: 'comments', + actionType: 'append', + }, + ], + }, + }, + isPreconfigured: true, + }, +]; + +export const caseConfigurationResposeMock: CasesConfigureResponse = { + created_at: '2020-04-06T13:03:18.657Z', + created_by: { username: 'elastic', full_name: 'Elastic', email: 'elastic@elastic.co' }, + connector_id: '123', + connector_name: 'My Connector', + closure_type: 'close-by-user', + updated_at: '2020-04-06T14:03:18.657Z', + updated_by: { username: 'elastic', full_name: 'Elastic', email: 'elastic@elastic.co' }, + version: 'WzHJ12', +}; + +export const caseConfigurationMock: CasesConfigureRequest = { + connector_id: '123', + connector_name: 'My Connector', + closure_type: 'close-by-user', +}; + +export const caseConfigurationCamelCaseResponseMock: CaseConfigure = { + createdAt: '2020-04-06T13:03:18.657Z', + createdBy: { username: 'elastic', fullName: 'Elastic', email: 'elastic@elastic.co' }, + connectorId: '123', + connectorName: 'My Connector', + closureType: 'close-by-user', + updatedAt: '2020-04-06T14:03:18.657Z', + updatedBy: { username: 'elastic', fullName: 'Elastic', email: 'elastic@elastic.co' }, + version: 'WzHJ12', +}; diff --git a/x-pack/legacy/plugins/siem/public/containers/case/configure/use_configure.test.tsx b/x-pack/legacy/plugins/siem/public/containers/case/configure/use_configure.test.tsx new file mode 100644 index 0000000000000..3ee16e19eaf9f --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/containers/case/configure/use_configure.test.tsx @@ -0,0 +1,299 @@ +/* + * 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 { renderHook, act } from '@testing-library/react-hooks'; +import { useCaseConfigure, ReturnUseCaseConfigure, PersistCaseConfigure } from './use_configure'; +import { caseConfigurationCamelCaseResponseMock } from './mock'; +import * as api from './api'; + +jest.mock('./api'); + +const configuration: PersistCaseConfigure = { + connectorId: '456', + connectorName: 'My Connector 2', + closureType: 'close-by-pushing', +}; + +describe('useConfigure', () => { + beforeEach(() => { + jest.clearAllMocks(); + jest.restoreAllMocks(); + }); + + const args = { + setConnector: jest.fn(), + setClosureType: jest.fn(), + setCurrentConfiguration: jest.fn(), + }; + + test('init', async () => { + await act(async () => { + const { result, waitForNextUpdate } = renderHook<string, ReturnUseCaseConfigure>(() => + useCaseConfigure(args) + ); + await waitForNextUpdate(); + expect(result.current).toEqual({ + loading: true, + persistLoading: false, + refetchCaseConfigure: result.current.refetchCaseConfigure, + persistCaseConfigure: result.current.persistCaseConfigure, + }); + }); + }); + + test('fetch case configuration', async () => { + await act(async () => { + const { result, waitForNextUpdate } = renderHook<string, ReturnUseCaseConfigure>(() => + useCaseConfigure(args) + ); + await waitForNextUpdate(); + await waitForNextUpdate(); + expect(result.current).toEqual({ + loading: false, + persistLoading: false, + refetchCaseConfigure: result.current.refetchCaseConfigure, + persistCaseConfigure: result.current.persistCaseConfigure, + }); + }); + }); + + test('fetch case configuration - setConnector', async () => { + await act(async () => { + const { waitForNextUpdate } = renderHook<string, ReturnUseCaseConfigure>(() => + useCaseConfigure(args) + ); + await waitForNextUpdate(); + await waitForNextUpdate(); + expect(args.setConnector).toHaveBeenCalledWith('123', 'My Connector'); + }); + }); + + test('fetch case configuration - setClosureType', async () => { + await act(async () => { + const { waitForNextUpdate } = renderHook<string, ReturnUseCaseConfigure>(() => + useCaseConfigure(args) + ); + await waitForNextUpdate(); + await waitForNextUpdate(); + expect(args.setClosureType).toHaveBeenCalledWith('close-by-user'); + }); + }); + + test('fetch case configuration - setCurrentConfiguration', async () => { + await act(async () => { + const { waitForNextUpdate } = renderHook<string, ReturnUseCaseConfigure>(() => + useCaseConfigure(args) + ); + await waitForNextUpdate(); + await waitForNextUpdate(); + expect(args.setCurrentConfiguration).toHaveBeenCalledWith({ + connectorId: '123', + closureType: 'close-by-user', + }); + }); + }); + + test('fetch case configuration - only setConnector', async () => { + await act(async () => { + const { result, waitForNextUpdate } = renderHook<string, ReturnUseCaseConfigure>(() => + useCaseConfigure({ setConnector: jest.fn() }) + ); + await waitForNextUpdate(); + await waitForNextUpdate(); + expect(result.current).toEqual({ + loading: false, + persistLoading: false, + refetchCaseConfigure: result.current.refetchCaseConfigure, + persistCaseConfigure: result.current.persistCaseConfigure, + }); + }); + }); + + test('refetch case configuration', async () => { + const spyOnGetCaseConfigure = jest.spyOn(api, 'getCaseConfigure'); + + await act(async () => { + const { result, waitForNextUpdate } = renderHook<string, ReturnUseCaseConfigure>(() => + useCaseConfigure(args) + ); + await waitForNextUpdate(); + await waitForNextUpdate(); + result.current.refetchCaseConfigure(); + expect(spyOnGetCaseConfigure).toHaveBeenCalledTimes(2); + }); + }); + + test('set isLoading to true when fetching case configuration', async () => { + await act(async () => { + const { result, waitForNextUpdate } = renderHook<string, ReturnUseCaseConfigure>(() => + useCaseConfigure(args) + ); + await waitForNextUpdate(); + await waitForNextUpdate(); + result.current.refetchCaseConfigure(); + + expect(result.current.loading).toBe(true); + }); + }); + + test('persist case configuration', async () => { + await act(async () => { + const { result, waitForNextUpdate } = renderHook<string, ReturnUseCaseConfigure>(() => + useCaseConfigure(args) + ); + await waitForNextUpdate(); + await waitForNextUpdate(); + + result.current.persistCaseConfigure(configuration); + + expect(result.current).toEqual({ + loading: false, + persistLoading: true, + refetchCaseConfigure: result.current.refetchCaseConfigure, + persistCaseConfigure: result.current.persistCaseConfigure, + }); + }); + }); + + test('save case configuration - postCaseConfigure', async () => { + // When there is no version, a configuration is created. Otherwise is updated. + const spyOnGetCaseConfigure = jest.spyOn(api, 'getCaseConfigure'); + spyOnGetCaseConfigure.mockImplementation(() => + Promise.resolve({ + ...caseConfigurationCamelCaseResponseMock, + version: '', + }) + ); + + const spyOnPostCaseConfigure = jest.spyOn(api, 'postCaseConfigure'); + spyOnPostCaseConfigure.mockImplementation(() => + Promise.resolve({ + ...caseConfigurationCamelCaseResponseMock, + ...configuration, + }) + ); + + await act(async () => { + const { result, waitForNextUpdate } = renderHook<string, ReturnUseCaseConfigure>(() => + useCaseConfigure(args) + ); + await waitForNextUpdate(); + await waitForNextUpdate(); + + result.current.persistCaseConfigure(configuration); + + await waitForNextUpdate(); + + expect(args.setConnector).toHaveBeenNthCalledWith(2, '456'); + expect(args.setClosureType).toHaveBeenNthCalledWith(2, 'close-by-pushing'); + expect(args.setCurrentConfiguration).toHaveBeenNthCalledWith(2, { + connectorId: '456', + closureType: 'close-by-pushing', + }); + }); + }); + + test('save case configuration - patchCaseConfigure', async () => { + const spyOnPatchCaseConfigure = jest.spyOn(api, 'patchCaseConfigure'); + spyOnPatchCaseConfigure.mockImplementation(() => + Promise.resolve({ + ...caseConfigurationCamelCaseResponseMock, + ...configuration, + }) + ); + + await act(async () => { + const { result, waitForNextUpdate } = renderHook<string, ReturnUseCaseConfigure>(() => + useCaseConfigure(args) + ); + await waitForNextUpdate(); + await waitForNextUpdate(); + + result.current.persistCaseConfigure(configuration); + + await waitForNextUpdate(); + + expect(args.setConnector).toHaveBeenNthCalledWith(2, '456'); + expect(args.setClosureType).toHaveBeenNthCalledWith(2, 'close-by-pushing'); + expect(args.setCurrentConfiguration).toHaveBeenNthCalledWith(2, { + connectorId: '456', + closureType: 'close-by-pushing', + }); + }); + }); + + test('save case configuration - only setConnector', async () => { + await act(async () => { + const { result, waitForNextUpdate } = renderHook<string, ReturnUseCaseConfigure>(() => + useCaseConfigure({ setConnector: jest.fn() }) + ); + + await waitForNextUpdate(); + await waitForNextUpdate(); + + result.current.persistCaseConfigure(configuration); + + await waitForNextUpdate(); + + expect(result.current).toEqual({ + loading: false, + persistLoading: false, + refetchCaseConfigure: result.current.refetchCaseConfigure, + persistCaseConfigure: result.current.persistCaseConfigure, + }); + }); + }); + + test('unhappy path - fetch case configuration', async () => { + const spyOnGetCaseConfigure = jest.spyOn(api, 'getCaseConfigure'); + spyOnGetCaseConfigure.mockImplementation(() => { + throw new Error('Something went wrong'); + }); + + await act(async () => { + const { result, waitForNextUpdate } = renderHook<string, ReturnUseCaseConfigure>(() => + useCaseConfigure(args) + ); + + await waitForNextUpdate(); + await waitForNextUpdate(); + + expect(result.current).toEqual({ + loading: false, + persistLoading: false, + refetchCaseConfigure: result.current.refetchCaseConfigure, + persistCaseConfigure: result.current.persistCaseConfigure, + }); + }); + }); + + test('unhappy path - persist case configuration', async () => { + const spyOnPostCaseConfigure = jest.spyOn(api, 'postCaseConfigure'); + spyOnPostCaseConfigure.mockImplementation(() => { + throw new Error('Something went wrong'); + }); + + await act(async () => { + const { result, waitForNextUpdate } = renderHook<string, ReturnUseCaseConfigure>(() => + useCaseConfigure(args) + ); + + await waitForNextUpdate(); + await waitForNextUpdate(); + + result.current.persistCaseConfigure(configuration); + + await waitForNextUpdate(); + + expect(result.current).toEqual({ + loading: false, + persistLoading: false, + refetchCaseConfigure: result.current.refetchCaseConfigure, + persistCaseConfigure: result.current.persistCaseConfigure, + }); + }); + }); +}); diff --git a/x-pack/legacy/plugins/siem/public/containers/case/configure/use_configure.tsx b/x-pack/legacy/plugins/siem/public/containers/case/configure/use_configure.tsx index 19d80bba1e0f8..1c03a09a8c2ea 100644 --- a/x-pack/legacy/plugins/siem/public/containers/case/configure/use_configure.tsx +++ b/x-pack/legacy/plugins/siem/public/containers/case/configure/use_configure.tsx @@ -12,7 +12,7 @@ import * as i18n from './translations'; import { ClosureType } from './types'; import { CurrentConfiguration } from '../../../pages/case/components/configure_cases/reducer'; -interface PersistCaseConfigure { +export interface PersistCaseConfigure { connectorId: string; connectorName: string; closureType: ClosureType; @@ -55,7 +55,6 @@ export const useCaseConfigure = ({ setLoading(true); const res = await getCaseConfigure({ signal: abortCtrl.signal }); if (!didCancel) { - setLoading(false); if (res != null) { setConnector(res.connectorId, res.connectorName); if (setClosureType != null) { @@ -73,6 +72,7 @@ export const useCaseConfigure = ({ } } } + setLoading(false); } } catch (error) { if (!didCancel) { @@ -117,7 +117,6 @@ export const useCaseConfigure = ({ abortCtrl.signal ); if (!didCancel) { - setPersistLoading(false); setConnector(res.connectorId); if (setClosureType) { setClosureType(res.closureType); @@ -131,6 +130,7 @@ export const useCaseConfigure = ({ } displaySuccessToast(i18n.SUCCESS_CONFIGURE, dispatchToaster); + setPersistLoading(false); } } catch (error) { if (!didCancel) { diff --git a/x-pack/legacy/plugins/siem/public/containers/case/configure/use_connectors.test.tsx b/x-pack/legacy/plugins/siem/public/containers/case/configure/use_connectors.test.tsx new file mode 100644 index 0000000000000..0d6b6acfd9065 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/containers/case/configure/use_connectors.test.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 { renderHook, act } from '@testing-library/react-hooks'; +import { useConnectors, ReturnConnectors } from './use_connectors'; +import { connectorsMock } from './mock'; +import * as api from './api'; + +jest.mock('./api'); + +describe('useConnectors', () => { + beforeEach(() => { + jest.clearAllMocks(); + jest.restoreAllMocks(); + }); + + test('init', async () => { + await act(async () => { + const { result, waitForNextUpdate } = renderHook<string, ReturnConnectors>(() => + useConnectors() + ); + await waitForNextUpdate(); + expect(result.current).toEqual({ + loading: true, + connectors: [], + refetchConnectors: result.current.refetchConnectors, + }); + }); + }); + + test('fetch connectors', async () => { + await act(async () => { + const { result, waitForNextUpdate } = renderHook<string, ReturnConnectors>(() => + useConnectors() + ); + await waitForNextUpdate(); + await waitForNextUpdate(); + expect(result.current).toEqual({ + loading: false, + connectors: connectorsMock, + refetchConnectors: result.current.refetchConnectors, + }); + }); + }); + + test('refetch connectors', async () => { + const spyOnfetchConnectors = jest.spyOn(api, 'fetchConnectors'); + await act(async () => { + const { result, waitForNextUpdate } = renderHook<string, ReturnConnectors>(() => + useConnectors() + ); + await waitForNextUpdate(); + await waitForNextUpdate(); + result.current.refetchConnectors(); + expect(spyOnfetchConnectors).toHaveBeenCalledTimes(2); + }); + }); + + test('set isLoading to true when refetching connectors', async () => { + await act(async () => { + const { result, waitForNextUpdate } = renderHook<string, ReturnConnectors>(() => + useConnectors() + ); + await waitForNextUpdate(); + await waitForNextUpdate(); + result.current.refetchConnectors(); + + expect(result.current.loading).toBe(true); + }); + }); + + test('unhappy path', async () => { + const spyOnfetchConnectors = jest.spyOn(api, 'fetchConnectors'); + spyOnfetchConnectors.mockImplementation(() => { + throw new Error('Something went wrong'); + }); + + await act(async () => { + const { result, waitForNextUpdate } = renderHook<string, ReturnConnectors>(() => + useConnectors() + ); + await waitForNextUpdate(); + await waitForNextUpdate(); + + expect(result.current).toEqual({ + loading: false, + connectors: [], + refetchConnectors: result.current.refetchConnectors, + }); + }); + }); +}); diff --git a/x-pack/legacy/plugins/siem/public/containers/case/mock.ts b/x-pack/legacy/plugins/siem/public/containers/case/mock.ts new file mode 100644 index 0000000000000..0bda75e5bc9e0 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/containers/case/mock.ts @@ -0,0 +1,307 @@ +/* + * 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 { ActionLicense, AllCases, Case, CasesStatus, CaseUserActions, Comment } from './types'; + +import { + CommentResponse, + ServiceConnectorCaseResponse, + Status, + UserAction, + UserActionField, + CaseResponse, + CasesStatusResponse, + CaseUserActionsResponse, + CasesResponse, + CasesFindResponse, +} from '../../../../../../plugins/case/common/api/cases'; +import { UseGetCasesState, DEFAULT_FILTER_OPTIONS, DEFAULT_QUERY_PARAMS } from './use_get_cases'; + +export const basicCaseId = 'basic-case-id'; +const basicCommentId = 'basic-comment-id'; +const basicCreatedAt = '2020-02-19T23:06:33.798Z'; +const basicUpdatedAt = '2020-02-20T15:02:57.995Z'; +const laterTime = '2020-02-28T15:02:57.995Z'; +export const elasticUser = { + fullName: 'Leslie Knope', + username: 'lknope', + email: 'leslie.knope@elastic.co', +}; + +export const tags: string[] = ['coke', 'pepsi']; + +export const basicComment: Comment = { + comment: 'Solve this fast!', + id: basicCommentId, + createdAt: basicCreatedAt, + createdBy: elasticUser, + pushedAt: null, + pushedBy: null, + updatedAt: null, + updatedBy: null, + version: 'WzQ3LDFc', +}; + +export const basicCase: Case = { + closedAt: null, + closedBy: null, + id: basicCaseId, + comments: [basicComment], + createdAt: basicCreatedAt, + createdBy: elasticUser, + description: 'Security banana Issue', + externalService: null, + status: 'open', + tags, + title: 'Another horrible breach!!', + totalComment: 1, + updatedAt: basicUpdatedAt, + updatedBy: elasticUser, + version: 'WzQ3LDFd', +}; + +export const basicCasePost: Case = { + ...basicCase, + updatedAt: null, + updatedBy: null, +}; + +export const basicCommentPatch: Comment = { + ...basicComment, + updatedAt: basicUpdatedAt, + updatedBy: { + username: 'elastic', + }, +}; + +export const basicCaseCommentPatch = { + ...basicCase, + comments: [basicCommentPatch], +}; + +export const casesStatus: CasesStatus = { + countClosedCases: 130, + countOpenCases: 20, +}; + +const basicPush = { + connectorId: 'connector_id', + connectorName: 'connector name', + externalId: 'external_id', + externalTitle: 'external title', + externalUrl: 'basicPush.com', + pushedAt: basicUpdatedAt, + pushedBy: elasticUser, +}; + +export const pushedCase: Case = { + ...basicCase, + externalService: basicPush, +}; + +export const serviceConnector: ServiceConnectorCaseResponse = { + number: '123', + incidentId: '444', + pushedDate: basicUpdatedAt, + url: 'connector.com', + comments: [ + { + commentId: basicCommentId, + pushedDate: basicUpdatedAt, + }, + ], +}; + +const basicAction = { + actionAt: basicCreatedAt, + actionBy: elasticUser, + oldValue: null, + newValue: 'what a cool value', + caseId: basicCaseId, + commentId: null, +}; + +export const casePushParams = { + actionBy: elasticUser, + caseId: basicCaseId, + createdAt: basicCreatedAt, + createdBy: elasticUser, + incidentId: null, + title: 'what a cool value', + commentId: null, + updatedAt: basicCreatedAt, + updatedBy: elasticUser, + description: 'nice', +}; +export const actionTypeExecutorResult = { + actionId: 'string', + status: 'ok', + data: serviceConnector, +}; + +export const cases: Case[] = [ + basicCase, + { ...pushedCase, id: '1', totalComment: 0, comments: [] }, + { ...pushedCase, updatedAt: laterTime, id: '2', totalComment: 0, comments: [] }, + { ...basicCase, id: '3', totalComment: 0, comments: [] }, + { ...basicCase, id: '4', totalComment: 0, comments: [] }, +]; + +export const allCases: AllCases = { + cases, + page: 1, + perPage: 5, + total: 10, + ...casesStatus, +}; +export const actionLicenses: ActionLicense[] = [ + { + id: '.servicenow', + name: 'ServiceNow', + enabled: true, + enabledInConfig: true, + enabledInLicense: true, + }, +]; + +// Snake case for mock api responses +export const elasticUserSnake = { + full_name: 'Leslie Knope', + username: 'lknope', + email: 'leslie.knope@elastic.co', +}; +export const basicCommentSnake: CommentResponse = { + ...basicComment, + comment: 'Solve this fast!', + id: basicCommentId, + created_at: basicCreatedAt, + created_by: elasticUserSnake, + pushed_at: null, + pushed_by: null, + updated_at: null, + updated_by: null, +}; + +export const basicCaseSnake: CaseResponse = { + ...basicCase, + status: 'open' as Status, + closed_at: null, + closed_by: null, + comments: [basicCommentSnake], + created_at: basicCreatedAt, + created_by: elasticUserSnake, + external_service: null, + updated_at: basicUpdatedAt, + updated_by: elasticUserSnake, +}; + +export const casesStatusSnake: CasesStatusResponse = { + count_closed_cases: 130, + count_open_cases: 20, +}; + +export const pushSnake = { + connector_id: 'connector_id', + connector_name: 'connector name', + external_id: 'external_id', + external_title: 'external title', + external_url: 'basicPush.com', +}; +const basicPushSnake = { + ...pushSnake, + pushed_at: basicUpdatedAt, + pushed_by: elasticUserSnake, +}; +export const pushedCaseSnake = { + ...basicCaseSnake, + external_service: basicPushSnake, +}; + +export const reporters: string[] = ['alexis', 'kim', 'maria', 'steph']; +export const respReporters = [ + { username: 'alexis', full_name: null, email: null }, + { username: 'kim', full_name: null, email: null }, + { username: 'maria', full_name: null, email: null }, + { username: 'steph', full_name: null, email: null }, +]; +export const casesSnake: CasesResponse = [ + basicCaseSnake, + { ...pushedCaseSnake, id: '1', totalComment: 0, comments: [] }, + { ...pushedCaseSnake, updated_at: laterTime, id: '2', totalComment: 0, comments: [] }, + { ...basicCaseSnake, id: '3', totalComment: 0, comments: [] }, + { ...basicCaseSnake, id: '4', totalComment: 0, comments: [] }, +]; + +export const allCasesSnake: CasesFindResponse = { + cases: casesSnake, + page: 1, + per_page: 5, + total: 10, + ...casesStatusSnake, +}; + +const basicActionSnake = { + action_at: basicCreatedAt, + action_by: elasticUserSnake, + old_value: null, + new_value: 'what a cool value', + case_id: basicCaseId, + comment_id: null, +}; +export const getUserActionSnake = (af: UserActionField, a: UserAction) => ({ + ...basicActionSnake, + action_id: `${af[0]}-${a}`, + action_field: af, + action: a, + comment_id: af[0] === 'comment' ? basicCommentId : null, + new_value: + a === 'push-to-service' && af[0] === 'pushed' + ? JSON.stringify(basicPushSnake) + : basicAction.newValue, +}); + +export const caseUserActionsSnake: CaseUserActionsResponse = [ + getUserActionSnake(['description'], 'create'), + getUserActionSnake(['comment'], 'create'), + getUserActionSnake(['description'], 'update'), +]; + +// user actions + +export const getUserAction = (af: UserActionField, a: UserAction) => ({ + ...basicAction, + actionId: `${af[0]}-${a}`, + actionField: af, + action: a, + commentId: af[0] === 'comment' ? basicCommentId : null, + newValue: + a === 'push-to-service' && af[0] === 'pushed' + ? JSON.stringify(basicPushSnake) + : basicAction.newValue, +}); + +export const caseUserActions: CaseUserActions[] = [ + getUserAction(['description'], 'create'), + getUserAction(['comment'], 'create'), + getUserAction(['description'], 'update'), +]; + +// components tests +export const useGetCasesMockState: UseGetCasesState = { + data: allCases, + loading: [], + selectedCases: [], + isError: false, + queryParams: DEFAULT_QUERY_PARAMS, + filterOptions: DEFAULT_FILTER_OPTIONS, +}; + +export const basicCaseClosed: Case = { + ...basicCase, + closedAt: '2020-02-25T23:06:33.798Z', + closedBy: elasticUser, + status: 'closed', +}; diff --git a/x-pack/legacy/plugins/siem/public/containers/case/types.ts b/x-pack/legacy/plugins/siem/public/containers/case/types.ts index d2a58e9eeeff4..e552f22b55fa4 100644 --- a/x-pack/legacy/plugins/siem/public/containers/case/types.ts +++ b/x-pack/legacy/plugins/siem/public/containers/case/types.ts @@ -31,7 +31,7 @@ export interface CaseUserActions { export interface CaseExternalService { pushedAt: string; - pushedBy: string; + pushedBy: ElasticUser; connectorId: string; connectorName: string; externalId: string; diff --git a/x-pack/legacy/plugins/siem/public/containers/case/use_bulk_update_case.test.tsx b/x-pack/legacy/plugins/siem/public/containers/case/use_bulk_update_case.test.tsx new file mode 100644 index 0000000000000..329fda10424a8 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/containers/case/use_bulk_update_case.test.tsx @@ -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. + */ + +import { renderHook, act } from '@testing-library/react-hooks'; +import { useUpdateCases, UseUpdateCases } from './use_bulk_update_case'; +import { basicCase } from './mock'; +import * as api from './api'; + +jest.mock('./api'); + +describe('useUpdateCases', () => { + const abortCtrl = new AbortController(); + beforeEach(() => { + jest.clearAllMocks(); + jest.restoreAllMocks(); + }); + + it('init', async () => { + await act(async () => { + const { result, waitForNextUpdate } = renderHook<string, UseUpdateCases>(() => + useUpdateCases() + ); + await waitForNextUpdate(); + expect(result.current).toEqual({ + isLoading: false, + isError: false, + isUpdated: false, + updateBulkStatus: result.current.updateBulkStatus, + dispatchResetIsUpdated: result.current.dispatchResetIsUpdated, + }); + }); + }); + + it('calls patchCase with correct arguments', async () => { + const spyOnPatchCases = jest.spyOn(api, 'patchCasesStatus'); + + await act(async () => { + const { result, waitForNextUpdate } = renderHook<string, UseUpdateCases>(() => + useUpdateCases() + ); + await waitForNextUpdate(); + + result.current.updateBulkStatus([basicCase], 'closed'); + await waitForNextUpdate(); + expect(spyOnPatchCases).toBeCalledWith( + [ + { + status: 'closed', + id: basicCase.id, + version: basicCase.version, + }, + ], + abortCtrl.signal + ); + }); + }); + + it('patch cases', async () => { + await act(async () => { + const { result, waitForNextUpdate } = renderHook<string, UseUpdateCases>(() => + useUpdateCases() + ); + await waitForNextUpdate(); + result.current.updateBulkStatus([basicCase], 'closed'); + await waitForNextUpdate(); + expect(result.current).toEqual({ + isUpdated: true, + isLoading: false, + isError: false, + updateBulkStatus: result.current.updateBulkStatus, + dispatchResetIsUpdated: result.current.dispatchResetIsUpdated, + }); + }); + }); + + it('set isLoading to true when posting case', async () => { + await act(async () => { + const { result, waitForNextUpdate } = renderHook<string, UseUpdateCases>(() => + useUpdateCases() + ); + await waitForNextUpdate(); + result.current.updateBulkStatus([basicCase], 'closed'); + + expect(result.current.isLoading).toBe(true); + }); + }); + + it('dispatchResetIsUpdated resets is updated', async () => { + await act(async () => { + const { result, waitForNextUpdate } = renderHook<string, UseUpdateCases>(() => + useUpdateCases() + ); + + await waitForNextUpdate(); + result.current.updateBulkStatus([basicCase], 'closed'); + await waitForNextUpdate(); + expect(result.current.isUpdated).toBeTruthy(); + result.current.dispatchResetIsUpdated(); + expect(result.current.isUpdated).toBeFalsy(); + }); + }); + + it('unhappy path', async () => { + const spyOnPatchCases = jest.spyOn(api, 'patchCasesStatus'); + spyOnPatchCases.mockImplementation(() => { + throw new Error('Something went wrong'); + }); + + await act(async () => { + const { result, waitForNextUpdate } = renderHook<string, UseUpdateCases>(() => + useUpdateCases() + ); + await waitForNextUpdate(); + result.current.updateBulkStatus([basicCase], 'closed'); + + expect(result.current).toEqual({ + isUpdated: false, + isLoading: false, + isError: true, + updateBulkStatus: result.current.updateBulkStatus, + dispatchResetIsUpdated: result.current.dispatchResetIsUpdated, + }); + }); + }); +}); diff --git a/x-pack/legacy/plugins/siem/public/containers/case/use_bulk_update_case.tsx b/x-pack/legacy/plugins/siem/public/containers/case/use_bulk_update_case.tsx index 7d040c49f1971..d0cc4d99f8f9f 100644 --- a/x-pack/legacy/plugins/siem/public/containers/case/use_bulk_update_case.tsx +++ b/x-pack/legacy/plugins/siem/public/containers/case/use_bulk_update_case.tsx @@ -51,12 +51,12 @@ const dataFetchReducer = (state: UpdateState, action: Action): UpdateState => { return state; } }; -interface UseUpdateCase extends UpdateState { +export interface UseUpdateCases extends UpdateState { updateBulkStatus: (cases: Case[], status: string) => void; dispatchResetIsUpdated: () => void; } -export const useUpdateCases = (): UseUpdateCase => { +export const useUpdateCases = (): UseUpdateCases => { const [state, dispatch] = useReducer(dataFetchReducer, { isLoading: false, isError: false, diff --git a/x-pack/legacy/plugins/siem/public/containers/case/use_delete_cases.test.tsx b/x-pack/legacy/plugins/siem/public/containers/case/use_delete_cases.test.tsx new file mode 100644 index 0000000000000..45ba392f3b5b4 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/containers/case/use_delete_cases.test.tsx @@ -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 { renderHook, act } from '@testing-library/react-hooks'; +import { useDeleteCases, UseDeleteCase } from './use_delete_cases'; +import * as api from './api'; + +jest.mock('./api'); + +describe('useDeleteCases', () => { + const abortCtrl = new AbortController(); + const deleteObj = [{ id: '1' }, { id: '2' }, { id: '3' }]; + const deleteArr = ['1', '2', '3']; + it('init', async () => { + await act(async () => { + const { result, waitForNextUpdate } = renderHook<string, UseDeleteCase>(() => + useDeleteCases() + ); + await waitForNextUpdate(); + expect(result.current).toEqual({ + isDisplayConfirmDeleteModal: false, + isLoading: false, + isError: false, + isDeleted: false, + dispatchResetIsDeleted: result.current.dispatchResetIsDeleted, + handleOnDeleteConfirm: result.current.handleOnDeleteConfirm, + handleToggleModal: result.current.handleToggleModal, + }); + }); + }); + + it('calls deleteCases with correct arguments', async () => { + const spyOnDeleteCases = jest.spyOn(api, 'deleteCases'); + + await act(async () => { + const { result, waitForNextUpdate } = renderHook<string, UseDeleteCase>(() => + useDeleteCases() + ); + await waitForNextUpdate(); + + result.current.handleOnDeleteConfirm(deleteObj); + await waitForNextUpdate(); + expect(spyOnDeleteCases).toBeCalledWith(deleteArr, abortCtrl.signal); + }); + }); + + it('deletes cases', async () => { + await act(async () => { + const { result, waitForNextUpdate } = renderHook<string, UseDeleteCase>(() => + useDeleteCases() + ); + await waitForNextUpdate(); + result.current.handleToggleModal(); + result.current.handleOnDeleteConfirm(deleteObj); + await waitForNextUpdate(); + expect(result.current).toEqual({ + isDisplayConfirmDeleteModal: false, + isLoading: false, + isError: false, + isDeleted: true, + dispatchResetIsDeleted: result.current.dispatchResetIsDeleted, + handleOnDeleteConfirm: result.current.handleOnDeleteConfirm, + handleToggleModal: result.current.handleToggleModal, + }); + }); + }); + + it('resets is deleting', async () => { + await act(async () => { + const { result, waitForNextUpdate } = renderHook<string, UseDeleteCase>(() => + useDeleteCases() + ); + await waitForNextUpdate(); + result.current.handleToggleModal(); + result.current.handleOnDeleteConfirm(deleteObj); + await waitForNextUpdate(); + expect(result.current.isDeleted).toBeTruthy(); + result.current.handleToggleModal(); + result.current.dispatchResetIsDeleted(); + expect(result.current.isDeleted).toBeFalsy(); + }); + }); + + it('set isLoading to true when deleting cases', async () => { + await act(async () => { + const { result, waitForNextUpdate } = renderHook<string, UseDeleteCase>(() => + useDeleteCases() + ); + await waitForNextUpdate(); + result.current.handleToggleModal(); + result.current.handleOnDeleteConfirm(deleteObj); + expect(result.current.isLoading).toBe(true); + }); + }); + + it('unhappy path', async () => { + const spyOnDeleteCases = jest.spyOn(api, 'deleteCases'); + spyOnDeleteCases.mockImplementation(() => { + throw new Error('Something went wrong'); + }); + + await act(async () => { + const { result, waitForNextUpdate } = renderHook<string, UseDeleteCase>(() => + useDeleteCases() + ); + await waitForNextUpdate(); + result.current.handleToggleModal(); + result.current.handleOnDeleteConfirm(deleteObj); + + expect(result.current).toEqual({ + isDisplayConfirmDeleteModal: false, + isLoading: false, + isError: true, + isDeleted: false, + dispatchResetIsDeleted: result.current.dispatchResetIsDeleted, + handleOnDeleteConfirm: result.current.handleOnDeleteConfirm, + handleToggleModal: result.current.handleToggleModal, + }); + }); + }); +}); diff --git a/x-pack/legacy/plugins/siem/public/containers/case/use_delete_cases.tsx b/x-pack/legacy/plugins/siem/public/containers/case/use_delete_cases.tsx index 07e3786758aeb..3c49be551c064 100644 --- a/x-pack/legacy/plugins/siem/public/containers/case/use_delete_cases.tsx +++ b/x-pack/legacy/plugins/siem/public/containers/case/use_delete_cases.tsx @@ -59,9 +59,9 @@ const dataFetchReducer = (state: DeleteState, action: Action): DeleteState => { } }; -interface UseDeleteCase extends DeleteState { +export interface UseDeleteCase extends DeleteState { dispatchResetIsDeleted: () => void; - handleOnDeleteConfirm: (caseIds: DeleteCase[]) => void; + handleOnDeleteConfirm: (cases: DeleteCase[]) => void; handleToggleModal: () => void; } @@ -117,8 +117,8 @@ export const useDeleteCases = (): UseDeleteCase => { }, [state.isDisplayConfirmDeleteModal]); const handleOnDeleteConfirm = useCallback( - caseIds => { - dispatchDeleteCases(caseIds); + (cases: DeleteCase[]) => { + dispatchDeleteCases(cases); dispatchToggleDeleteModal(); }, [state.isDisplayConfirmDeleteModal] diff --git a/x-pack/legacy/plugins/siem/public/containers/case/use_get_action_license.test.tsx b/x-pack/legacy/plugins/siem/public/containers/case/use_get_action_license.test.tsx new file mode 100644 index 0000000000000..23c9ff5e49586 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/containers/case/use_get_action_license.test.tsx @@ -0,0 +1,89 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { renderHook, act } from '@testing-library/react-hooks'; +import { initialData, useGetActionLicense, ActionLicenseState } from './use_get_action_license'; +import { actionLicenses } from './mock'; +import * as api from './api'; + +jest.mock('./api'); + +describe('useGetActionLicense', () => { + const abortCtrl = new AbortController(); + beforeEach(() => { + jest.clearAllMocks(); + jest.restoreAllMocks(); + }); + + it('init', async () => { + await act(async () => { + const { result, waitForNextUpdate } = renderHook<string, ActionLicenseState>(() => + useGetActionLicense() + ); + await waitForNextUpdate(); + expect(result.current).toEqual(initialData); + }); + }); + + it('calls getActionLicense with correct arguments', async () => { + const spyOnGetActionLicense = jest.spyOn(api, 'getActionLicense'); + + await act(async () => { + const { waitForNextUpdate } = renderHook<string, ActionLicenseState>(() => + useGetActionLicense() + ); + await waitForNextUpdate(); + await waitForNextUpdate(); + expect(spyOnGetActionLicense).toBeCalledWith(abortCtrl.signal); + }); + }); + + it('gets action license', async () => { + await act(async () => { + const { result, waitForNextUpdate } = renderHook<string, ActionLicenseState>(() => + useGetActionLicense() + ); + await waitForNextUpdate(); + await waitForNextUpdate(); + expect(result.current).toEqual({ + isLoading: false, + isError: false, + actionLicense: actionLicenses[0], + }); + }); + }); + + it('set isLoading to true when posting case', async () => { + await act(async () => { + const { result, waitForNextUpdate } = renderHook<string, ActionLicenseState>(() => + useGetActionLicense() + ); + await waitForNextUpdate(); + expect(result.current.isLoading).toBe(true); + }); + }); + + it('unhappy path', async () => { + const spyOnGetActionLicense = jest.spyOn(api, 'getActionLicense'); + spyOnGetActionLicense.mockImplementation(() => { + throw new Error('Something went wrong'); + }); + + await act(async () => { + const { result, waitForNextUpdate } = renderHook<string, ActionLicenseState>(() => + useGetActionLicense() + ); + await waitForNextUpdate(); + await waitForNextUpdate(); + + expect(result.current).toEqual({ + actionLicense: null, + isLoading: false, + isError: true, + }); + }); + }); +}); diff --git a/x-pack/legacy/plugins/siem/public/containers/case/use_get_action_license.tsx b/x-pack/legacy/plugins/siem/public/containers/case/use_get_action_license.tsx index 12f92b2db039b..0d28a1b20c61f 100644 --- a/x-pack/legacy/plugins/siem/public/containers/case/use_get_action_license.tsx +++ b/x-pack/legacy/plugins/siem/public/containers/case/use_get_action_license.tsx @@ -11,13 +11,13 @@ import { getActionLicense } from './api'; import * as i18n from './translations'; import { ActionLicense } from './types'; -interface ActionLicenseState { +export interface ActionLicenseState { actionLicense: ActionLicense | null; isLoading: boolean; isError: boolean; } -const initialData: ActionLicenseState = { +export const initialData: ActionLicenseState = { actionLicense: null, isLoading: true, isError: false, diff --git a/x-pack/legacy/plugins/siem/public/containers/case/use_get_case.test.tsx b/x-pack/legacy/plugins/siem/public/containers/case/use_get_case.test.tsx new file mode 100644 index 0000000000000..10649da548d43 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/containers/case/use_get_case.test.tsx @@ -0,0 +1,112 @@ +/* + * 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 { renderHook, act } from '@testing-library/react-hooks'; +import { initialData, useGetCase, UseGetCase } from './use_get_case'; +import { basicCase } from './mock'; +import * as api from './api'; + +jest.mock('./api'); + +describe('useGetCase', () => { + const abortCtrl = new AbortController(); + beforeEach(() => { + jest.clearAllMocks(); + jest.restoreAllMocks(); + }); + + it('init', async () => { + await act(async () => { + const { result, waitForNextUpdate } = renderHook<string, UseGetCase>(() => + useGetCase(basicCase.id) + ); + await waitForNextUpdate(); + expect(result.current).toEqual({ + data: initialData, + isLoading: true, + isError: false, + fetchCase: result.current.fetchCase, + updateCase: result.current.updateCase, + }); + }); + }); + + it('calls getCase with correct arguments', async () => { + const spyOnGetCase = jest.spyOn(api, 'getCase'); + await act(async () => { + const { waitForNextUpdate } = renderHook<string, UseGetCase>(() => useGetCase(basicCase.id)); + await waitForNextUpdate(); + await waitForNextUpdate(); + expect(spyOnGetCase).toBeCalledWith(basicCase.id, true, abortCtrl.signal); + }); + }); + + it('fetch case', async () => { + await act(async () => { + const { result, waitForNextUpdate } = renderHook<string, UseGetCase>(() => + useGetCase(basicCase.id) + ); + await waitForNextUpdate(); + await waitForNextUpdate(); + expect(result.current).toEqual({ + data: basicCase, + isLoading: false, + isError: false, + fetchCase: result.current.fetchCase, + updateCase: result.current.updateCase, + }); + }); + }); + + it('refetch case', async () => { + const spyOnGetCase = jest.spyOn(api, 'getCase'); + await act(async () => { + const { result, waitForNextUpdate } = renderHook<string, UseGetCase>(() => + useGetCase(basicCase.id) + ); + await waitForNextUpdate(); + await waitForNextUpdate(); + result.current.fetchCase(); + expect(spyOnGetCase).toHaveBeenCalledTimes(2); + }); + }); + + it('set isLoading to true when refetching case', async () => { + await act(async () => { + const { result, waitForNextUpdate } = renderHook<string, UseGetCase>(() => + useGetCase(basicCase.id) + ); + await waitForNextUpdate(); + await waitForNextUpdate(); + result.current.fetchCase(); + + expect(result.current.isLoading).toBe(true); + }); + }); + + it('unhappy path', async () => { + const spyOnGetCase = jest.spyOn(api, 'getCase'); + spyOnGetCase.mockImplementation(() => { + throw new Error('Something went wrong'); + }); + + await act(async () => { + const { result, waitForNextUpdate } = renderHook<string, UseGetCase>(() => + useGetCase(basicCase.id) + ); + await waitForNextUpdate(); + await waitForNextUpdate(); + + expect(result.current).toEqual({ + data: initialData, + isLoading: false, + isError: true, + fetchCase: result.current.fetchCase, + updateCase: result.current.updateCase, + }); + }); + }); +}); diff --git a/x-pack/legacy/plugins/siem/public/containers/case/use_get_case.tsx b/x-pack/legacy/plugins/siem/public/containers/case/use_get_case.tsx index 835fb7153dc95..b2e3b6d0cacf6 100644 --- a/x-pack/legacy/plugins/siem/public/containers/case/use_get_case.tsx +++ b/x-pack/legacy/plugins/siem/public/containers/case/use_get_case.tsx @@ -53,7 +53,7 @@ const dataFetchReducer = (state: CaseState, action: Action): CaseState => { return state; } }; -const initialData: Case = { +export const initialData: Case = { id: '', closedAt: null, closedBy: null, @@ -73,7 +73,7 @@ const initialData: Case = { version: '', }; -interface UseGetCase extends CaseState { +export interface UseGetCase extends CaseState { fetchCase: () => void; updateCase: (newCase: Case) => void; } diff --git a/x-pack/legacy/plugins/siem/public/containers/case/use_get_case_user_actions.test.tsx b/x-pack/legacy/plugins/siem/public/containers/case/use_get_case_user_actions.test.tsx new file mode 100644 index 0000000000000..cdd40b84f8724 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/containers/case/use_get_case_user_actions.test.tsx @@ -0,0 +1,106 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { renderHook, act } from '@testing-library/react-hooks'; +import { + initialData, + useGetCaseUserActions, + UseGetCaseUserActions, +} from './use_get_case_user_actions'; +import { basicCaseId, caseUserActions, elasticUser } from './mock'; +import * as api from './api'; + +jest.mock('./api'); + +describe('useGetCaseUserActions', () => { + const abortCtrl = new AbortController(); + beforeEach(() => { + jest.clearAllMocks(); + jest.restoreAllMocks(); + }); + + it('init', async () => { + await act(async () => { + const { result, waitForNextUpdate } = renderHook<string, UseGetCaseUserActions>(() => + useGetCaseUserActions(basicCaseId) + ); + await waitForNextUpdate(); + expect(result.current).toEqual({ + ...initialData, + fetchCaseUserActions: result.current.fetchCaseUserActions, + }); + }); + }); + + it('calls getCaseUserActions with correct arguments', async () => { + const spyOnPostCase = jest.spyOn(api, 'getCaseUserActions'); + + await act(async () => { + const { result, waitForNextUpdate } = renderHook<string, UseGetCaseUserActions>(() => + useGetCaseUserActions(basicCaseId) + ); + await waitForNextUpdate(); + + result.current.fetchCaseUserActions(basicCaseId); + await waitForNextUpdate(); + expect(spyOnPostCase).toBeCalledWith(basicCaseId, abortCtrl.signal); + }); + }); + + it('retuns proper state on getCaseUserActions', async () => { + await act(async () => { + const { result, waitForNextUpdate } = renderHook<string, UseGetCaseUserActions>(() => + useGetCaseUserActions(basicCaseId) + ); + await waitForNextUpdate(); + result.current.fetchCaseUserActions(basicCaseId); + await waitForNextUpdate(); + expect(result.current).toEqual({ + ...initialData, + caseUserActions: caseUserActions.slice(1), + fetchCaseUserActions: result.current.fetchCaseUserActions, + hasDataToPush: true, + isError: false, + isLoading: false, + participants: [elasticUser], + }); + }); + }); + + it('set isLoading to true when posting case', async () => { + await act(async () => { + const { result, waitForNextUpdate } = renderHook<string, UseGetCaseUserActions>(() => + useGetCaseUserActions(basicCaseId) + ); + await waitForNextUpdate(); + result.current.fetchCaseUserActions(basicCaseId); + + expect(result.current.isLoading).toBe(true); + }); + }); + + it('unhappy path', async () => { + const spyOnPostCase = jest.spyOn(api, 'getCaseUserActions'); + spyOnPostCase.mockImplementation(() => { + throw new Error('Something went wrong'); + }); + + await act(async () => { + const { result, waitForNextUpdate } = renderHook<string, UseGetCaseUserActions>(() => + useGetCaseUserActions(basicCaseId) + ); + await waitForNextUpdate(); + result.current.fetchCaseUserActions(basicCaseId); + + expect(result.current).toEqual({ + ...initialData, + isLoading: false, + isError: true, + fetchCaseUserActions: result.current.fetchCaseUserActions, + }); + }); + }); +}); diff --git a/x-pack/legacy/plugins/siem/public/containers/case/use_get_case_user_actions.tsx b/x-pack/legacy/plugins/siem/public/containers/case/use_get_case_user_actions.tsx index 4c278bc038134..6d9874a655e97 100644 --- a/x-pack/legacy/plugins/siem/public/containers/case/use_get_case_user_actions.tsx +++ b/x-pack/legacy/plugins/siem/public/containers/case/use_get_case_user_actions.tsx @@ -22,7 +22,7 @@ interface CaseUserActionsState { lastIndexPushToService: number; } -const initialData: CaseUserActionsState = { +export const initialData: CaseUserActionsState = { caseUserActions: [], firstIndexPushToService: -1, lastIndexPushToService: -1, @@ -32,7 +32,7 @@ const initialData: CaseUserActionsState = { participants: [], }; -interface UseGetCaseUserActions extends CaseUserActionsState { +export interface UseGetCaseUserActions extends CaseUserActionsState { fetchCaseUserActions: (caseId: string) => void; } @@ -80,6 +80,7 @@ export const useGetCaseUserActions = (caseId: string): UseGetCaseUserActions => const participants = !isEmpty(response) ? uniqBy('actionBy.username', response).map(cau => cau.actionBy) : []; + const caseUserActions = !isEmpty(response) ? response.slice(1) : []; setCaseUserActionsState({ caseUserActions, diff --git a/x-pack/legacy/plugins/siem/public/containers/case/use_get_cases.test.tsx b/x-pack/legacy/plugins/siem/public/containers/case/use_get_cases.test.tsx new file mode 100644 index 0000000000000..4e274e074b036 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/containers/case/use_get_cases.test.tsx @@ -0,0 +1,202 @@ +/* + * 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 { renderHook, act } from '@testing-library/react-hooks'; +import { + DEFAULT_FILTER_OPTIONS, + DEFAULT_QUERY_PARAMS, + initialData, + useGetCases, + UseGetCases, +} from './use_get_cases'; +import { UpdateKey } from './use_update_case'; +import { allCases, basicCase } from './mock'; +import * as api from './api'; + +jest.mock('./api'); + +describe('useGetCases', () => { + const abortCtrl = new AbortController(); + beforeEach(() => { + jest.clearAllMocks(); + jest.restoreAllMocks(); + }); + + it('init', async () => { + await act(async () => { + const { result, waitForNextUpdate } = renderHook<string, UseGetCases>(() => useGetCases()); + await waitForNextUpdate(); + expect(result.current).toEqual({ + data: initialData, + dispatchUpdateCaseProperty: result.current.dispatchUpdateCaseProperty, + filterOptions: DEFAULT_FILTER_OPTIONS, + isError: false, + loading: [], + queryParams: DEFAULT_QUERY_PARAMS, + refetchCases: result.current.refetchCases, + selectedCases: [], + setFilters: result.current.setFilters, + setQueryParams: result.current.setQueryParams, + setSelectedCases: result.current.setSelectedCases, + }); + }); + }); + + it('calls getCases with correct arguments', async () => { + const spyOnGetCases = jest.spyOn(api, 'getCases'); + await act(async () => { + const { waitForNextUpdate } = renderHook<string, UseGetCases>(() => useGetCases()); + await waitForNextUpdate(); + await waitForNextUpdate(); + expect(spyOnGetCases).toBeCalledWith({ + filterOptions: DEFAULT_FILTER_OPTIONS, + queryParams: DEFAULT_QUERY_PARAMS, + signal: abortCtrl.signal, + }); + }); + }); + + it('fetch cases', async () => { + await act(async () => { + const { result, waitForNextUpdate } = renderHook<string, UseGetCases>(() => useGetCases()); + await waitForNextUpdate(); + await waitForNextUpdate(); + expect(result.current).toEqual({ + data: allCases, + dispatchUpdateCaseProperty: result.current.dispatchUpdateCaseProperty, + filterOptions: DEFAULT_FILTER_OPTIONS, + isError: false, + loading: [], + queryParams: DEFAULT_QUERY_PARAMS, + refetchCases: result.current.refetchCases, + selectedCases: [], + setFilters: result.current.setFilters, + setQueryParams: result.current.setQueryParams, + setSelectedCases: result.current.setSelectedCases, + }); + }); + }); + it('dispatch update case property', async () => { + const spyOnPatchCase = jest.spyOn(api, 'patchCase'); + await act(async () => { + const updateCase = { + updateKey: 'description' as UpdateKey, + updateValue: 'description update', + caseId: basicCase.id, + refetchCasesStatus: jest.fn(), + version: '99999', + }; + const { result, waitForNextUpdate } = renderHook<string, UseGetCases>(() => useGetCases()); + await waitForNextUpdate(); + await waitForNextUpdate(); + result.current.dispatchUpdateCaseProperty(updateCase); + expect(result.current.loading).toEqual(['caseUpdate']); + expect(spyOnPatchCase).toBeCalledWith( + basicCase.id, + { [updateCase.updateKey]: updateCase.updateValue }, + updateCase.version, + abortCtrl.signal + ); + }); + }); + + it('refetch cases', async () => { + const spyOnGetCases = jest.spyOn(api, 'getCases'); + await act(async () => { + const { result, waitForNextUpdate } = renderHook<string, UseGetCases>(() => useGetCases()); + await waitForNextUpdate(); + await waitForNextUpdate(); + result.current.refetchCases(); + expect(spyOnGetCases).toHaveBeenCalledTimes(2); + }); + }); + + it('set isLoading to true when refetching case', async () => { + await act(async () => { + const { result, waitForNextUpdate } = renderHook<string, UseGetCases>(() => useGetCases()); + await waitForNextUpdate(); + await waitForNextUpdate(); + result.current.refetchCases(); + + expect(result.current.loading).toEqual(['cases']); + }); + }); + + it('unhappy path', async () => { + const spyOnGetCases = jest.spyOn(api, 'getCases'); + spyOnGetCases.mockImplementation(() => { + throw new Error('Something went wrong'); + }); + + await act(async () => { + const { result, waitForNextUpdate } = renderHook<string, UseGetCases>(() => useGetCases()); + await waitForNextUpdate(); + await waitForNextUpdate(); + + expect(result.current).toEqual({ + data: initialData, + dispatchUpdateCaseProperty: result.current.dispatchUpdateCaseProperty, + filterOptions: DEFAULT_FILTER_OPTIONS, + isError: true, + loading: [], + queryParams: DEFAULT_QUERY_PARAMS, + refetchCases: result.current.refetchCases, + selectedCases: [], + setFilters: result.current.setFilters, + setQueryParams: result.current.setQueryParams, + setSelectedCases: result.current.setSelectedCases, + }); + }); + }); + it('set filters', async () => { + await act(async () => { + const spyOnGetCases = jest.spyOn(api, 'getCases'); + const newFilters = { + search: 'new', + tags: ['new'], + status: 'closed', + }; + const { result, waitForNextUpdate } = renderHook<string, UseGetCases>(() => useGetCases()); + await waitForNextUpdate(); + await waitForNextUpdate(); + result.current.setFilters(newFilters); + await waitForNextUpdate(); + expect(spyOnGetCases.mock.calls[1][0]).toEqual({ + filterOptions: { ...DEFAULT_FILTER_OPTIONS, ...newFilters }, + queryParams: DEFAULT_QUERY_PARAMS, + signal: abortCtrl.signal, + }); + }); + }); + it('set query params', async () => { + await act(async () => { + const spyOnGetCases = jest.spyOn(api, 'getCases'); + const newQueryParams = { + page: 2, + }; + const { result, waitForNextUpdate } = renderHook<string, UseGetCases>(() => useGetCases()); + await waitForNextUpdate(); + await waitForNextUpdate(); + result.current.setQueryParams(newQueryParams); + await waitForNextUpdate(); + expect(spyOnGetCases.mock.calls[1][0]).toEqual({ + filterOptions: DEFAULT_FILTER_OPTIONS, + queryParams: { ...DEFAULT_QUERY_PARAMS, ...newQueryParams }, + signal: abortCtrl.signal, + }); + }); + }); + it('set selected cases', async () => { + await act(async () => { + const selectedCases = [basicCase]; + const { result, waitForNextUpdate } = renderHook<string, UseGetCases>(() => useGetCases()); + await waitForNextUpdate(); + await waitForNextUpdate(); + result.current.setSelectedCases(selectedCases); + expect(result.current.selectedCases).toEqual(selectedCases); + }); + }); +}); diff --git a/x-pack/legacy/plugins/siem/public/containers/case/use_get_cases.tsx b/x-pack/legacy/plugins/siem/public/containers/case/use_get_cases.tsx index 1cbce5af6304b..465b50dbdc1bc 100644 --- a/x-pack/legacy/plugins/siem/public/containers/case/use_get_cases.tsx +++ b/x-pack/legacy/plugins/siem/public/containers/case/use_get_cases.tsx @@ -105,7 +105,7 @@ export const DEFAULT_QUERY_PARAMS: QueryParams = { sortOrder: 'desc', }; -const initialData: AllCases = { +export const initialData: AllCases = { cases: [], countClosedCases: null, countOpenCases: null, @@ -113,7 +113,7 @@ const initialData: AllCases = { perPage: 0, total: 0, }; -interface UseGetCases extends UseGetCasesState { +export interface UseGetCases extends UseGetCasesState { dispatchUpdateCaseProperty: ({ updateKey, updateValue, @@ -121,7 +121,7 @@ interface UseGetCases extends UseGetCasesState { version, refetchCasesStatus, }: UpdateCase) => void; - refetchCases: (filters: FilterOptions, queryParams: QueryParams) => void; + refetchCases: () => void; setFilters: (filters: Partial<FilterOptions>) => void; setQueryParams: (queryParams: Partial<QueryParams>) => void; setSelectedCases: (mySelectedCases: Case[]) => void; diff --git a/x-pack/legacy/plugins/siem/public/containers/case/use_get_cases_status.test.tsx b/x-pack/legacy/plugins/siem/public/containers/case/use_get_cases_status.test.tsx new file mode 100644 index 0000000000000..bfbcbd2525e3b --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/containers/case/use_get_cases_status.test.tsx @@ -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 { renderHook, act } from '@testing-library/react-hooks'; +import { useGetCasesStatus, UseGetCasesStatus } from './use_get_cases_status'; +import { casesStatus } from './mock'; +import * as api from './api'; + +jest.mock('./api'); + +describe('useGetCasesStatus', () => { + const abortCtrl = new AbortController(); + beforeEach(() => { + jest.clearAllMocks(); + jest.restoreAllMocks(); + }); + + it('init', async () => { + await act(async () => { + const { result, waitForNextUpdate } = renderHook<string, UseGetCasesStatus>(() => + useGetCasesStatus() + ); + await waitForNextUpdate(); + expect(result.current).toEqual({ + countClosedCases: null, + countOpenCases: null, + isLoading: true, + isError: false, + fetchCasesStatus: result.current.fetchCasesStatus, + }); + }); + }); + + it('calls getCasesStatus api', async () => { + const spyOnGetCasesStatus = jest.spyOn(api, 'getCasesStatus'); + await act(async () => { + const { waitForNextUpdate } = renderHook<string, UseGetCasesStatus>(() => + useGetCasesStatus() + ); + await waitForNextUpdate(); + await waitForNextUpdate(); + expect(spyOnGetCasesStatus).toBeCalledWith(abortCtrl.signal); + }); + }); + + it('fetch reporters', async () => { + await act(async () => { + const { result, waitForNextUpdate } = renderHook<string, UseGetCasesStatus>(() => + useGetCasesStatus() + ); + await waitForNextUpdate(); + await waitForNextUpdate(); + expect(result.current).toEqual({ + countClosedCases: casesStatus.countClosedCases, + countOpenCases: casesStatus.countOpenCases, + isLoading: false, + isError: false, + fetchCasesStatus: result.current.fetchCasesStatus, + }); + }); + }); + + it('unhappy path', async () => { + const spyOnGetCasesStatus = jest.spyOn(api, 'getCasesStatus'); + spyOnGetCasesStatus.mockImplementation(() => { + throw new Error('Something went wrong'); + }); + + await act(async () => { + const { result, waitForNextUpdate } = renderHook<string, UseGetCasesStatus>(() => + useGetCasesStatus() + ); + await waitForNextUpdate(); + await waitForNextUpdate(); + + expect(result.current).toEqual({ + countClosedCases: 0, + countOpenCases: 0, + isLoading: false, + isError: true, + fetchCasesStatus: result.current.fetchCasesStatus, + }); + }); + }); +}); diff --git a/x-pack/legacy/plugins/siem/public/containers/case/use_get_cases_status.tsx b/x-pack/legacy/plugins/siem/public/containers/case/use_get_cases_status.tsx index 7f56d27ef160e..0788464602357 100644 --- a/x-pack/legacy/plugins/siem/public/containers/case/use_get_cases_status.tsx +++ b/x-pack/legacy/plugins/siem/public/containers/case/use_get_cases_status.tsx @@ -23,7 +23,7 @@ const initialData: CasesStatusState = { isError: false, }; -interface UseGetCasesStatus extends CasesStatusState { +export interface UseGetCasesStatus extends CasesStatusState { fetchCasesStatus: () => void; } diff --git a/x-pack/legacy/plugins/siem/public/containers/case/use_get_reporters.test.tsx b/x-pack/legacy/plugins/siem/public/containers/case/use_get_reporters.test.tsx new file mode 100644 index 0000000000000..27b963eb6cb54 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/containers/case/use_get_reporters.test.tsx @@ -0,0 +1,99 @@ +/* + * 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 { renderHook, act } from '@testing-library/react-hooks'; +import { useGetReporters, UseGetReporters } from './use_get_reporters'; +import { reporters, respReporters } from './mock'; +import * as api from './api'; + +jest.mock('./api'); + +describe('useGetReporters', () => { + const abortCtrl = new AbortController(); + beforeEach(() => { + jest.clearAllMocks(); + jest.restoreAllMocks(); + }); + + it('init', async () => { + await act(async () => { + const { result, waitForNextUpdate } = renderHook<string, UseGetReporters>(() => + useGetReporters() + ); + await waitForNextUpdate(); + expect(result.current).toEqual({ + reporters: [], + respReporters: [], + isLoading: true, + isError: false, + fetchReporters: result.current.fetchReporters, + }); + }); + }); + + it('calls getReporters api', async () => { + const spyOnGetReporters = jest.spyOn(api, 'getReporters'); + await act(async () => { + const { waitForNextUpdate } = renderHook<string, UseGetReporters>(() => useGetReporters()); + await waitForNextUpdate(); + await waitForNextUpdate(); + expect(spyOnGetReporters).toBeCalledWith(abortCtrl.signal); + }); + }); + + it('fetch reporters', async () => { + await act(async () => { + const { result, waitForNextUpdate } = renderHook<string, UseGetReporters>(() => + useGetReporters() + ); + await waitForNextUpdate(); + await waitForNextUpdate(); + expect(result.current).toEqual({ + reporters, + respReporters, + isLoading: false, + isError: false, + fetchReporters: result.current.fetchReporters, + }); + }); + }); + + it('refetch reporters', async () => { + const spyOnGetReporters = jest.spyOn(api, 'getReporters'); + await act(async () => { + const { result, waitForNextUpdate } = renderHook<string, UseGetReporters>(() => + useGetReporters() + ); + await waitForNextUpdate(); + await waitForNextUpdate(); + result.current.fetchReporters(); + expect(spyOnGetReporters).toHaveBeenCalledTimes(2); + }); + }); + + it('unhappy path', async () => { + const spyOnGetReporters = jest.spyOn(api, 'getReporters'); + spyOnGetReporters.mockImplementation(() => { + throw new Error('Something went wrong'); + }); + + await act(async () => { + const { result, waitForNextUpdate } = renderHook<string, UseGetReporters>(() => + useGetReporters() + ); + await waitForNextUpdate(); + await waitForNextUpdate(); + + expect(result.current).toEqual({ + reporters: [], + respReporters: [], + isLoading: false, + isError: true, + fetchReporters: result.current.fetchReporters, + }); + }); + }); +}); diff --git a/x-pack/legacy/plugins/siem/public/containers/case/use_get_reporters.tsx b/x-pack/legacy/plugins/siem/public/containers/case/use_get_reporters.tsx index 2478172a3394b..2fc9b8294c8e0 100644 --- a/x-pack/legacy/plugins/siem/public/containers/case/use_get_reporters.tsx +++ b/x-pack/legacy/plugins/siem/public/containers/case/use_get_reporters.tsx @@ -26,7 +26,7 @@ const initialData: ReportersState = { isError: false, }; -interface UseGetReporters extends ReportersState { +export interface UseGetReporters extends ReportersState { fetchReporters: () => void; } diff --git a/x-pack/legacy/plugins/siem/public/containers/case/use_get_tags.test.tsx b/x-pack/legacy/plugins/siem/public/containers/case/use_get_tags.test.tsx new file mode 100644 index 0000000000000..2d70c4390e4dd --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/containers/case/use_get_tags.test.tsx @@ -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 { renderHook, act } from '@testing-library/react-hooks'; +import { useGetTags, UseGetTags } from './use_get_tags'; +import { tags } from './mock'; +import * as api from './api'; + +jest.mock('./api'); + +describe('useGetTags', () => { + const abortCtrl = new AbortController(); + beforeEach(() => { + jest.clearAllMocks(); + jest.restoreAllMocks(); + }); + + it('init', async () => { + await act(async () => { + const { result, waitForNextUpdate } = renderHook<string, UseGetTags>(() => useGetTags()); + await waitForNextUpdate(); + expect(result.current).toEqual({ + tags: [], + isLoading: true, + isError: false, + fetchTags: result.current.fetchTags, + }); + }); + }); + + it('calls getTags api', async () => { + const spyOnGetTags = jest.spyOn(api, 'getTags'); + await act(async () => { + const { waitForNextUpdate } = renderHook<string, UseGetTags>(() => useGetTags()); + await waitForNextUpdate(); + await waitForNextUpdate(); + expect(spyOnGetTags).toBeCalledWith(abortCtrl.signal); + }); + }); + + it('fetch tags', async () => { + await act(async () => { + const { result, waitForNextUpdate } = renderHook<string, UseGetTags>(() => useGetTags()); + await waitForNextUpdate(); + await waitForNextUpdate(); + expect(result.current).toEqual({ + tags, + isLoading: false, + isError: false, + fetchTags: result.current.fetchTags, + }); + }); + }); + + it('refetch tags', async () => { + const spyOnGetTags = jest.spyOn(api, 'getTags'); + await act(async () => { + const { result, waitForNextUpdate } = renderHook<string, UseGetTags>(() => useGetTags()); + await waitForNextUpdate(); + await waitForNextUpdate(); + result.current.fetchTags(); + expect(spyOnGetTags).toHaveBeenCalledTimes(2); + }); + }); + + it('unhappy path', async () => { + const spyOnGetTags = jest.spyOn(api, 'getTags'); + spyOnGetTags.mockImplementation(() => { + throw new Error('Something went wrong'); + }); + + await act(async () => { + const { result, waitForNextUpdate } = renderHook<string, UseGetTags>(() => useGetTags()); + await waitForNextUpdate(); + await waitForNextUpdate(); + + expect(result.current).toEqual({ + tags: [], + isLoading: false, + isError: true, + fetchTags: result.current.fetchTags, + }); + }); + }); +}); diff --git a/x-pack/legacy/plugins/siem/public/containers/case/use_get_tags.tsx b/x-pack/legacy/plugins/siem/public/containers/case/use_get_tags.tsx index b41d5aab5c07a..99bb65fa160f7 100644 --- a/x-pack/legacy/plugins/siem/public/containers/case/use_get_tags.tsx +++ b/x-pack/legacy/plugins/siem/public/containers/case/use_get_tags.tsx @@ -10,7 +10,7 @@ import { errorToToaster, useStateToaster } from '../../components/toasters'; import { getTags } from './api'; import * as i18n from './translations'; -interface TagsState { +export interface TagsState { tags: string[]; isLoading: boolean; isError: boolean; @@ -20,6 +20,10 @@ type Action = | { type: 'FETCH_SUCCESS'; payload: string[] } | { type: 'FETCH_FAILURE' }; +export interface UseGetTags extends TagsState { + fetchTags: () => void; +} + const dataFetchReducer = (state: TagsState, action: Action): TagsState => { switch (action.type) { case 'FETCH_INIT': @@ -47,15 +51,15 @@ const dataFetchReducer = (state: TagsState, action: Action): TagsState => { }; const initialData: string[] = []; -export const useGetTags = (): TagsState => { +export const useGetTags = (): UseGetTags => { const [state, dispatch] = useReducer(dataFetchReducer, { - isLoading: false, + isLoading: true, isError: false, tags: initialData, }); const [, dispatchToaster] = useStateToaster(); - useEffect(() => { + const callFetch = () => { let didCancel = false; const abortCtrl = new AbortController(); @@ -82,6 +86,9 @@ export const useGetTags = (): TagsState => { abortCtrl.abort(); didCancel = true; }; + }; + useEffect(() => { + callFetch(); }, []); - return state; + return { ...state, fetchTags: callFetch }; }; diff --git a/x-pack/legacy/plugins/siem/public/containers/case/use_post_case.test.tsx b/x-pack/legacy/plugins/siem/public/containers/case/use_post_case.test.tsx new file mode 100644 index 0000000000000..8b105fe041d27 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/containers/case/use_post_case.test.tsx @@ -0,0 +1,96 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { renderHook, act } from '@testing-library/react-hooks'; +import { usePostCase, UsePostCase } from './use_post_case'; +import { basicCasePost } from './mock'; +import * as api from './api'; + +jest.mock('./api'); + +describe('usePostCase', () => { + const abortCtrl = new AbortController(); + const samplePost = { + description: 'description', + tags: ['tags'], + title: 'title', + }; + beforeEach(() => { + jest.clearAllMocks(); + jest.restoreAllMocks(); + }); + + it('init', async () => { + await act(async () => { + const { result, waitForNextUpdate } = renderHook<string, UsePostCase>(() => usePostCase()); + await waitForNextUpdate(); + expect(result.current).toEqual({ + isLoading: false, + isError: false, + caseData: null, + postCase: result.current.postCase, + }); + }); + }); + + it('calls postCase with correct arguments', async () => { + const spyOnPostCase = jest.spyOn(api, 'postCase'); + + await act(async () => { + const { result, waitForNextUpdate } = renderHook<string, UsePostCase>(() => usePostCase()); + await waitForNextUpdate(); + + result.current.postCase(samplePost); + await waitForNextUpdate(); + expect(spyOnPostCase).toBeCalledWith(samplePost, abortCtrl.signal); + }); + }); + + it('post case', async () => { + await act(async () => { + const { result, waitForNextUpdate } = renderHook<string, UsePostCase>(() => usePostCase()); + await waitForNextUpdate(); + result.current.postCase(samplePost); + await waitForNextUpdate(); + expect(result.current).toEqual({ + caseData: basicCasePost, + isLoading: false, + isError: false, + postCase: result.current.postCase, + }); + }); + }); + + it('set isLoading to true when posting case', async () => { + await act(async () => { + const { result, waitForNextUpdate } = renderHook<string, UsePostCase>(() => usePostCase()); + await waitForNextUpdate(); + result.current.postCase(samplePost); + + expect(result.current.isLoading).toBe(true); + }); + }); + + it('unhappy path', async () => { + const spyOnPostCase = jest.spyOn(api, 'postCase'); + spyOnPostCase.mockImplementation(() => { + throw new Error('Something went wrong'); + }); + + await act(async () => { + const { result, waitForNextUpdate } = renderHook<string, UsePostCase>(() => usePostCase()); + await waitForNextUpdate(); + result.current.postCase(samplePost); + + expect(result.current).toEqual({ + caseData: null, + isLoading: false, + isError: true, + postCase: result.current.postCase, + }); + }); + }); +}); diff --git a/x-pack/legacy/plugins/siem/public/containers/case/use_post_case.tsx b/x-pack/legacy/plugins/siem/public/containers/case/use_post_case.tsx index 0e01364721dc5..aeb50fc098eee 100644 --- a/x-pack/legacy/plugins/siem/public/containers/case/use_post_case.tsx +++ b/x-pack/legacy/plugins/siem/public/containers/case/use_post_case.tsx @@ -48,7 +48,7 @@ const dataFetchReducer = (state: NewCaseState, action: Action): NewCaseState => } }; -interface UsePostCase extends NewCaseState { +export interface UsePostCase extends NewCaseState { postCase: (data: CasePostRequest) => void; } export const usePostCase = (): UsePostCase => { diff --git a/x-pack/legacy/plugins/siem/public/containers/case/use_post_comment.test.tsx b/x-pack/legacy/plugins/siem/public/containers/case/use_post_comment.test.tsx new file mode 100644 index 0000000000000..d7d9cf9c557c9 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/containers/case/use_post_comment.test.tsx @@ -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 { renderHook, act } from '@testing-library/react-hooks'; +import { usePostComment, UsePostComment } from './use_post_comment'; +import { basicCaseId } from './mock'; +import * as api from './api'; + +jest.mock('./api'); + +describe('usePostComment', () => { + const abortCtrl = new AbortController(); + const samplePost = { + comment: 'a comment', + }; + const updateCaseCallback = jest.fn(); + beforeEach(() => { + jest.clearAllMocks(); + jest.restoreAllMocks(); + }); + + it('init', async () => { + await act(async () => { + const { result, waitForNextUpdate } = renderHook<string, UsePostComment>(() => + usePostComment(basicCaseId) + ); + await waitForNextUpdate(); + expect(result.current).toEqual({ + isLoading: false, + isError: false, + postComment: result.current.postComment, + }); + }); + }); + + it('calls postComment with correct arguments', async () => { + const spyOnPostCase = jest.spyOn(api, 'postComment'); + + await act(async () => { + const { result, waitForNextUpdate } = renderHook<string, UsePostComment>(() => + usePostComment(basicCaseId) + ); + await waitForNextUpdate(); + + result.current.postComment(samplePost, updateCaseCallback); + await waitForNextUpdate(); + expect(spyOnPostCase).toBeCalledWith(samplePost, basicCaseId, abortCtrl.signal); + }); + }); + + it('post case', async () => { + await act(async () => { + const { result, waitForNextUpdate } = renderHook<string, UsePostComment>(() => + usePostComment(basicCaseId) + ); + await waitForNextUpdate(); + result.current.postComment(samplePost, updateCaseCallback); + await waitForNextUpdate(); + expect(result.current).toEqual({ + isLoading: false, + isError: false, + postComment: result.current.postComment, + }); + }); + }); + + it('set isLoading to true when posting case', async () => { + await act(async () => { + const { result, waitForNextUpdate } = renderHook<string, UsePostComment>(() => + usePostComment(basicCaseId) + ); + await waitForNextUpdate(); + result.current.postComment(samplePost, updateCaseCallback); + + expect(result.current.isLoading).toBe(true); + }); + }); + + it('unhappy path', async () => { + const spyOnPostCase = jest.spyOn(api, 'postComment'); + spyOnPostCase.mockImplementation(() => { + throw new Error('Something went wrong'); + }); + + await act(async () => { + const { result, waitForNextUpdate } = renderHook<string, UsePostComment>(() => + usePostComment(basicCaseId) + ); + await waitForNextUpdate(); + result.current.postComment(samplePost, updateCaseCallback); + + expect(result.current).toEqual({ + isLoading: false, + isError: true, + postComment: result.current.postComment, + }); + }); + }); +}); diff --git a/x-pack/legacy/plugins/siem/public/containers/case/use_post_comment.tsx b/x-pack/legacy/plugins/siem/public/containers/case/use_post_comment.tsx index 207b05814717f..c6d34b5449977 100644 --- a/x-pack/legacy/plugins/siem/public/containers/case/use_post_comment.tsx +++ b/x-pack/legacy/plugins/siem/public/containers/case/use_post_comment.tsx @@ -41,7 +41,7 @@ const dataFetchReducer = (state: NewCommentState, action: Action): NewCommentSta } }; -interface UsePostComment extends NewCommentState { +export interface UsePostComment extends NewCommentState { postComment: (data: CommentRequest, updateCase: (newCase: Case) => void) => void; } diff --git a/x-pack/legacy/plugins/siem/public/containers/case/use_post_push_to_service.test.tsx b/x-pack/legacy/plugins/siem/public/containers/case/use_post_push_to_service.test.tsx new file mode 100644 index 0000000000000..b07a346a8da46 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/containers/case/use_post_push_to_service.test.tsx @@ -0,0 +1,137 @@ +/* + * 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 { renderHook, act } from '@testing-library/react-hooks'; +import { + formatServiceRequestData, + usePostPushToService, + UsePostPushToService, +} from './use_post_push_to_service'; +import { basicCase, pushedCase, serviceConnector } from './mock'; +import * as api from './api'; + +jest.mock('./api'); + +describe('usePostPushToService', () => { + const abortCtrl = new AbortController(); + const updateCase = jest.fn(); + const samplePush = { + caseId: pushedCase.id, + connectorName: 'sample', + connectorId: '22', + updateCase, + }; + it('init', async () => { + await act(async () => { + const { result, waitForNextUpdate } = renderHook<string, UsePostPushToService>(() => + usePostPushToService() + ); + await waitForNextUpdate(); + expect(result.current).toEqual({ + serviceData: null, + pushedCaseData: null, + isLoading: false, + isError: false, + postPushToService: result.current.postPushToService, + }); + }); + }); + + it('calls pushCase with correct arguments', async () => { + const spyOnPushCase = jest.spyOn(api, 'pushCase'); + + await act(async () => { + const { result, waitForNextUpdate } = renderHook<string, UsePostPushToService>(() => + usePostPushToService() + ); + await waitForNextUpdate(); + result.current.postPushToService(samplePush); + await waitForNextUpdate(); + expect(spyOnPushCase).toBeCalledWith( + samplePush.caseId, + { + connector_id: samplePush.connectorId, + connector_name: samplePush.connectorName, + external_id: serviceConnector.incidentId, + external_title: serviceConnector.number, + external_url: serviceConnector.url, + }, + abortCtrl.signal + ); + }); + }); + + it('calls pushToService with correct arguments', async () => { + const spyOnPushToService = jest.spyOn(api, 'pushToService'); + + await act(async () => { + const { result, waitForNextUpdate } = renderHook<string, UsePostPushToService>(() => + usePostPushToService() + ); + await waitForNextUpdate(); + result.current.postPushToService(samplePush); + await waitForNextUpdate(); + expect(spyOnPushToService).toBeCalledWith( + samplePush.connectorId, + formatServiceRequestData(basicCase), + abortCtrl.signal + ); + }); + }); + + it('post push to service', async () => { + await act(async () => { + const { result, waitForNextUpdate } = renderHook<string, UsePostPushToService>(() => + usePostPushToService() + ); + await waitForNextUpdate(); + result.current.postPushToService(samplePush); + await waitForNextUpdate(); + expect(result.current).toEqual({ + serviceData: serviceConnector, + pushedCaseData: pushedCase, + isLoading: false, + isError: false, + postPushToService: result.current.postPushToService, + }); + }); + }); + + it('set isLoading to true when deleting cases', async () => { + await act(async () => { + const { result, waitForNextUpdate } = renderHook<string, UsePostPushToService>(() => + usePostPushToService() + ); + await waitForNextUpdate(); + result.current.postPushToService(samplePush); + expect(result.current.isLoading).toBe(true); + }); + }); + + it('unhappy path', async () => { + const spyOnPushToService = jest.spyOn(api, 'pushToService'); + spyOnPushToService.mockImplementation(() => { + throw new Error('Something went wrong'); + }); + + await act(async () => { + const { result, waitForNextUpdate } = renderHook<string, UsePostPushToService>(() => + usePostPushToService() + ); + await waitForNextUpdate(); + result.current.postPushToService(samplePush); + await waitForNextUpdate(); + + expect(result.current).toEqual({ + serviceData: null, + pushedCaseData: null, + isLoading: false, + isError: true, + postPushToService: result.current.postPushToService, + }); + }); + }); +}); diff --git a/x-pack/legacy/plugins/siem/public/containers/case/use_post_push_to_service.tsx b/x-pack/legacy/plugins/siem/public/containers/case/use_post_push_to_service.tsx index d9a32f26f7fe7..89e7e18cf0688 100644 --- a/x-pack/legacy/plugins/siem/public/containers/case/use_post_push_to_service.tsx +++ b/x-pack/legacy/plugins/siem/public/containers/case/use_post_push_to_service.tsx @@ -68,7 +68,7 @@ interface PushToServiceRequest { updateCase: (newCase: Case) => void; } -interface UsePostPushToService extends PushToServiceState { +export interface UsePostPushToService extends PushToServiceState { postPushToService: ({ caseId, connectorId, updateCase }: PushToServiceRequest) => void; } @@ -131,7 +131,7 @@ export const usePostPushToService = (): UsePostPushToService => { return { ...state, postPushToService }; }; -const formatServiceRequestData = (myCase: Case): ServiceConnectorCaseParams => { +export const formatServiceRequestData = (myCase: Case): ServiceConnectorCaseParams => { const { id: caseId, createdAt, diff --git a/x-pack/legacy/plugins/siem/public/containers/case/use_update_case.test.tsx b/x-pack/legacy/plugins/siem/public/containers/case/use_update_case.test.tsx new file mode 100644 index 0000000000000..86cfc3459c595 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/containers/case/use_update_case.test.tsx @@ -0,0 +1,119 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { renderHook, act } from '@testing-library/react-hooks'; +import { useUpdateCase, UseUpdateCase, UpdateKey } from './use_update_case'; +import { basicCase } from './mock'; +import * as api from './api'; + +jest.mock('./api'); + +describe('useUpdateCase', () => { + const abortCtrl = new AbortController(); + const fetchCaseUserActions = jest.fn(); + const updateCase = jest.fn(); + const updateKey: UpdateKey = 'description'; + const sampleUpdate = { + fetchCaseUserActions, + updateKey, + updateValue: 'updated description', + updateCase, + version: basicCase.version, + }; + beforeEach(() => { + jest.clearAllMocks(); + jest.restoreAllMocks(); + }); + + it('init', async () => { + await act(async () => { + const { result, waitForNextUpdate } = renderHook<string, UseUpdateCase>(() => + useUpdateCase({ caseId: basicCase.id }) + ); + await waitForNextUpdate(); + expect(result.current).toEqual({ + isLoading: false, + isError: false, + updateKey: null, + updateCaseProperty: result.current.updateCaseProperty, + }); + }); + }); + + it('calls patchCase with correct arguments', async () => { + const spyOnPatchCase = jest.spyOn(api, 'patchCase'); + + await act(async () => { + const { result, waitForNextUpdate } = renderHook<string, UseUpdateCase>(() => + useUpdateCase({ caseId: basicCase.id }) + ); + await waitForNextUpdate(); + + result.current.updateCaseProperty(sampleUpdate); + await waitForNextUpdate(); + expect(spyOnPatchCase).toBeCalledWith( + basicCase.id, + { description: 'updated description' }, + basicCase.version, + abortCtrl.signal + ); + }); + }); + + it('patch case', async () => { + await act(async () => { + const { result, waitForNextUpdate } = renderHook<string, UseUpdateCase>(() => + useUpdateCase({ caseId: basicCase.id }) + ); + await waitForNextUpdate(); + result.current.updateCaseProperty(sampleUpdate); + await waitForNextUpdate(); + expect(result.current).toEqual({ + updateKey: null, + isLoading: false, + isError: false, + updateCaseProperty: result.current.updateCaseProperty, + }); + expect(fetchCaseUserActions).toBeCalledWith(basicCase.id); + expect(updateCase).toBeCalledWith(basicCase); + }); + }); + + it('set isLoading to true when posting case', async () => { + await act(async () => { + const { result, waitForNextUpdate } = renderHook<string, UseUpdateCase>(() => + useUpdateCase({ caseId: basicCase.id }) + ); + await waitForNextUpdate(); + result.current.updateCaseProperty(sampleUpdate); + + expect(result.current.isLoading).toBe(true); + expect(result.current.updateKey).toBe(updateKey); + }); + }); + + it('unhappy path', async () => { + const spyOnPatchCase = jest.spyOn(api, 'patchCase'); + spyOnPatchCase.mockImplementation(() => { + throw new Error('Something went wrong'); + }); + + await act(async () => { + const { result, waitForNextUpdate } = renderHook<string, UseUpdateCase>(() => + useUpdateCase({ caseId: basicCase.id }) + ); + await waitForNextUpdate(); + result.current.updateCaseProperty(sampleUpdate); + + expect(result.current).toEqual({ + updateKey: null, + isLoading: false, + isError: true, + updateCaseProperty: result.current.updateCaseProperty, + }); + }); + }); +}); diff --git a/x-pack/legacy/plugins/siem/public/containers/case/use_update_case.tsx b/x-pack/legacy/plugins/siem/public/containers/case/use_update_case.tsx index 4973deef4d91a..7ebbbba076c12 100644 --- a/x-pack/legacy/plugins/siem/public/containers/case/use_update_case.tsx +++ b/x-pack/legacy/plugins/siem/public/containers/case/use_update_case.tsx @@ -12,7 +12,7 @@ import { patchCase } from './api'; import * as i18n from './translations'; import { Case } from './types'; -type UpdateKey = keyof Pick<CasePatchRequest, 'description' | 'status' | 'tags' | 'title'>; +export type UpdateKey = keyof Pick<CasePatchRequest, 'description' | 'status' | 'tags' | 'title'>; interface NewCaseState { isLoading: boolean; @@ -62,7 +62,7 @@ const dataFetchReducer = (state: NewCaseState, action: Action): NewCaseState => } }; -interface UseUpdateCase extends NewCaseState { +export interface UseUpdateCase extends NewCaseState { updateCaseProperty: (updates: UpdateByKey) => void; } export const useUpdateCase = ({ caseId }: { caseId: string }): UseUpdateCase => { diff --git a/x-pack/legacy/plugins/siem/public/containers/case/use_update_comment.test.tsx b/x-pack/legacy/plugins/siem/public/containers/case/use_update_comment.test.tsx new file mode 100644 index 0000000000000..5772ff4246866 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/containers/case/use_update_comment.test.tsx @@ -0,0 +1,116 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { renderHook, act } from '@testing-library/react-hooks'; +import { useUpdateComment, UseUpdateComment } from './use_update_comment'; +import { basicCase, basicCaseCommentPatch } from './mock'; +import * as api from './api'; + +jest.mock('./api'); + +describe('useUpdateComment', () => { + const abortCtrl = new AbortController(); + const fetchUserActions = jest.fn(); + const updateCase = jest.fn(); + const sampleUpdate = { + caseId: basicCase.id, + commentId: basicCase.comments[0].id, + commentUpdate: 'updated comment', + fetchUserActions, + updateCase, + version: basicCase.comments[0].version, + }; + beforeEach(() => { + jest.clearAllMocks(); + jest.restoreAllMocks(); + }); + + it('init', async () => { + await act(async () => { + const { result, waitForNextUpdate } = renderHook<string, UseUpdateComment>(() => + useUpdateComment() + ); + await waitForNextUpdate(); + expect(result.current).toEqual({ + isLoadingIds: [], + isError: false, + patchComment: result.current.patchComment, + }); + }); + }); + + it('calls patchComment with correct arguments', async () => { + const spyOnPatchComment = jest.spyOn(api, 'patchComment'); + + await act(async () => { + const { result, waitForNextUpdate } = renderHook<string, UseUpdateComment>(() => + useUpdateComment() + ); + await waitForNextUpdate(); + + result.current.patchComment(sampleUpdate); + await waitForNextUpdate(); + expect(spyOnPatchComment).toBeCalledWith( + basicCase.id, + basicCase.comments[0].id, + 'updated comment', + basicCase.comments[0].version, + abortCtrl.signal + ); + }); + }); + + it('patch comment', async () => { + await act(async () => { + const { result, waitForNextUpdate } = renderHook<string, UseUpdateComment>(() => + useUpdateComment() + ); + await waitForNextUpdate(); + result.current.patchComment(sampleUpdate); + await waitForNextUpdate(); + expect(result.current).toEqual({ + isLoadingIds: [], + isError: false, + patchComment: result.current.patchComment, + }); + expect(fetchUserActions).toBeCalled(); + expect(updateCase).toBeCalledWith(basicCaseCommentPatch); + }); + }); + + it('set isLoading to true when posting case', async () => { + await act(async () => { + const { result, waitForNextUpdate } = renderHook<string, UseUpdateComment>(() => + useUpdateComment() + ); + await waitForNextUpdate(); + result.current.patchComment(sampleUpdate); + + expect(result.current.isLoadingIds).toEqual([basicCase.comments[0].id]); + }); + }); + + it('unhappy path', async () => { + const spyOnPatchComment = jest.spyOn(api, 'patchComment'); + spyOnPatchComment.mockImplementation(() => { + throw new Error('Something went wrong'); + }); + + await act(async () => { + const { result, waitForNextUpdate } = renderHook<string, UseUpdateComment>(() => + useUpdateComment() + ); + await waitForNextUpdate(); + result.current.patchComment(sampleUpdate); + + expect(result.current).toEqual({ + isLoadingIds: [], + isError: true, + patchComment: result.current.patchComment, + }); + }); + }); +}); diff --git a/x-pack/legacy/plugins/siem/public/containers/case/use_update_comment.tsx b/x-pack/legacy/plugins/siem/public/containers/case/use_update_comment.tsx index faf9649a705c5..ffc5cffee7a55 100644 --- a/x-pack/legacy/plugins/siem/public/containers/case/use_update_comment.tsx +++ b/x-pack/legacy/plugins/siem/public/containers/case/use_update_comment.tsx @@ -60,7 +60,7 @@ interface UpdateComment { version: string; } -interface UseUpdateComment extends CommentUpdateState { +export interface UseUpdateComment extends CommentUpdateState { patchComment: ({ caseId, commentId, commentUpdate, fetchUserActions }: UpdateComment) => void; } diff --git a/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/api.test.ts b/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/api.test.ts index e8019659d49c6..9eb4acbdb6164 100644 --- a/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/api.test.ts +++ b/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/api.test.ts @@ -501,10 +501,8 @@ describe('Detections Rules API', () => { test('check parameter url, query', async () => { await getRuleStatusById({ id: 'mySuperRuleId', signal: abortCtrl.signal }); expect(fetchMock).toHaveBeenCalledWith('/api/detection_engine/rules/_find_statuses', { - query: { - ids: '["mySuperRuleId"]', - }, - method: 'GET', + body: '{"ids":["mySuperRuleId"]}', + method: 'POST', signal: abortCtrl.signal, }); }); diff --git a/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/api.ts b/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/api.ts index 2dd6955581eff..69f4c93a82e2c 100644 --- a/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/api.ts +++ b/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/api.ts @@ -4,6 +4,13 @@ * you may not use this file except in compliance with the Elastic License. */ +import { + DETECTION_ENGINE_RULES_URL, + DETECTION_ENGINE_PREPACKAGED_URL, + DETECTION_ENGINE_RULES_STATUS_URL, + DETECTION_ENGINE_PREPACKAGED_RULES_STATUS_URL, + DETECTION_ENGINE_TAGS_URL, +} from '../../../../../../../plugins/siem/common/constants'; import { AddRulesProps, DeleteRulesProps, @@ -23,13 +30,6 @@ import { BulkRuleResponse, } from './types'; import { KibanaServices } from '../../../lib/kibana'; -import { - DETECTION_ENGINE_RULES_URL, - DETECTION_ENGINE_PREPACKAGED_URL, - DETECTION_ENGINE_RULES_STATUS_URL, - DETECTION_ENGINE_PREPACKAGED_RULES_STATUS_URL, - DETECTION_ENGINE_TAGS_URL, -} from '../../../../common/constants'; import * as i18n from '../../../pages/detection_engine/rules/translations'; /** @@ -266,8 +266,8 @@ export const getRuleStatusById = async ({ signal: AbortSignal; }): Promise<RuleStatusResponse> => KibanaServices.get().http.fetch<RuleStatusResponse>(DETECTION_ENGINE_RULES_STATUS_URL, { - method: 'GET', - query: { ids: JSON.stringify([id]) }, + method: 'POST', + body: JSON.stringify({ ids: [id] }), signal, }); @@ -289,8 +289,8 @@ export const getRulesStatusByIds = async ({ const res = await KibanaServices.get().http.fetch<RuleStatusResponse>( DETECTION_ENGINE_RULES_STATUS_URL, { - method: 'GET', - query: { ids: JSON.stringify(ids) }, + method: 'POST', + body: JSON.stringify({ ids }), signal, } ); diff --git a/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/fetch_index_patterns.test.tsx b/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/fetch_index_patterns.test.tsx index cad78ac565903..83b8a3581a4be 100644 --- a/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/fetch_index_patterns.test.tsx +++ b/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/fetch_index_patterns.test.tsx @@ -6,7 +6,7 @@ import { renderHook, act } from '@testing-library/react-hooks'; -import { defaultIndexPattern } from '../../../../default_index_pattern'; +import { DEFAULT_INDEX_PATTERN } from '../../../../../../../plugins/siem/common/constants'; import { useApolloClient } from '../../../utils/apollo_context'; import { mocksSource } from '../../source/mock'; @@ -25,7 +25,7 @@ describe('useFetchIndexPatterns', () => { query: () => Promise.resolve(mocksSource[0].result), })); const { result, waitForNextUpdate } = renderHook<unknown, Return>(() => - useFetchIndexPatterns(defaultIndexPattern) + useFetchIndexPatterns(DEFAULT_INDEX_PATTERN) ); await waitForNextUpdate(); await waitForNextUpdate(); @@ -429,7 +429,7 @@ describe('useFetchIndexPatterns', () => { query: () => Promise.reject(new Error('Something went wrong')), })); const { result, waitForNextUpdate } = renderHook<unknown, Return>(() => - useFetchIndexPatterns(defaultIndexPattern) + useFetchIndexPatterns(DEFAULT_INDEX_PATTERN) ); await waitForNextUpdate(); diff --git a/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/types.ts b/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/types.ts index f89d21ef1aeb1..2f2de2e151664 100644 --- a/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/types.ts +++ b/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/types.ts @@ -6,7 +6,7 @@ import * as t from 'io-ts'; -import { RuleTypeSchema } from '../../../../common/detection_engine/types'; +import { RuleTypeSchema } from '../../../../../../../plugins/siem/common/detection_engine/types'; /** * Params is an "record", since it is a type of AlertActionParams which is action templates. diff --git a/x-pack/legacy/plugins/siem/public/containers/detection_engine/signals/api.ts b/x-pack/legacy/plugins/siem/public/containers/detection_engine/signals/api.ts index 25263c2d32735..ece2483adde3a 100644 --- a/x-pack/legacy/plugins/siem/public/containers/detection_engine/signals/api.ts +++ b/x-pack/legacy/plugins/siem/public/containers/detection_engine/signals/api.ts @@ -4,13 +4,13 @@ * you may not use this file except in compliance with the Elastic License. */ -import { KibanaServices } from '../../../lib/kibana'; import { DETECTION_ENGINE_QUERY_SIGNALS_URL, DETECTION_ENGINE_SIGNALS_STATUS_URL, DETECTION_ENGINE_INDEX_URL, DETECTION_ENGINE_PRIVILEGES_URL, -} from '../../../../common/constants'; +} from '../../../../../../../plugins/siem/common/constants'; +import { KibanaServices } from '../../../lib/kibana'; import { BasicSignals, Privilege, diff --git a/x-pack/legacy/plugins/siem/public/containers/events/last_event_time/index.ts b/x-pack/legacy/plugins/siem/public/containers/events/last_event_time/index.ts index 9cae503d30940..8628ba502f081 100644 --- a/x-pack/legacy/plugins/siem/public/containers/events/last_event_time/index.ts +++ b/x-pack/legacy/plugins/siem/public/containers/events/last_event_time/index.ts @@ -7,7 +7,7 @@ import { get } from 'lodash/fp'; import React, { useEffect, useState } from 'react'; -import { DEFAULT_INDEX_KEY } from '../../../../common/constants'; +import { DEFAULT_INDEX_KEY } from '../../../../../../../plugins/siem/common/constants'; import { GetLastEventTimeQuery, LastEventIndexKey, LastTimeDetails } from '../../../graphql/types'; import { inputsModel } from '../../../store'; import { QueryTemplateProps } from '../../query_template'; diff --git a/x-pack/legacy/plugins/siem/public/containers/events/last_event_time/mock.ts b/x-pack/legacy/plugins/siem/public/containers/events/last_event_time/mock.ts index ca8786077851f..5ef8e67dedddb 100644 --- a/x-pack/legacy/plugins/siem/public/containers/events/last_event_time/mock.ts +++ b/x-pack/legacy/plugins/siem/public/containers/events/last_event_time/mock.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { defaultIndexPattern } from '../../../../default_index_pattern'; +import { DEFAULT_INDEX_PATTERN } from '../../../../../../../plugins/siem/common/constants'; import { GetLastEventTimeQuery, LastEventIndexKey } from '../../../graphql/types'; import { LastEventTimeGqlQuery } from './last_event_time.gql_query'; @@ -43,7 +43,7 @@ export const mockLastEventTimeQuery: MockLastEventTimeQuery[] = [ sourceId: 'default', indexKey: LastEventIndexKey.hosts, details: {}, - defaultIndex: defaultIndexPattern, + defaultIndex: DEFAULT_INDEX_PATTERN, }, }, result: { diff --git a/x-pack/legacy/plugins/siem/public/containers/helpers.test.ts b/x-pack/legacy/plugins/siem/public/containers/helpers.test.ts index 5d378d79acc7a..67cfe259927ab 100644 --- a/x-pack/legacy/plugins/siem/public/containers/helpers.test.ts +++ b/x-pack/legacy/plugins/siem/public/containers/helpers.test.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { ESQuery } from '../../common/typed_json'; +import { ESQuery } from '../../../../../plugins/siem/common/typed_json'; import { createFilter } from './helpers'; diff --git a/x-pack/legacy/plugins/siem/public/containers/helpers.ts b/x-pack/legacy/plugins/siem/public/containers/helpers.ts index 5f66e3f4b88d4..7ff9577bfb05e 100644 --- a/x-pack/legacy/plugins/siem/public/containers/helpers.ts +++ b/x-pack/legacy/plugins/siem/public/containers/helpers.ts @@ -7,7 +7,7 @@ import { FetchPolicy } from 'apollo-client'; import { isString } from 'lodash/fp'; -import { ESQuery } from '../../common/typed_json'; +import { ESQuery } from '../../../../../plugins/siem/common/typed_json'; export const createFilter = (filterQuery: ESQuery | string | undefined) => isString(filterQuery) ? filterQuery : JSON.stringify(filterQuery); diff --git a/x-pack/legacy/plugins/siem/public/containers/hosts/first_last_seen/index.ts b/x-pack/legacy/plugins/siem/public/containers/hosts/first_last_seen/index.ts index e36da5bfbe4ee..5806125f2397b 100644 --- a/x-pack/legacy/plugins/siem/public/containers/hosts/first_last_seen/index.ts +++ b/x-pack/legacy/plugins/siem/public/containers/hosts/first_last_seen/index.ts @@ -8,8 +8,8 @@ import ApolloClient from 'apollo-client'; import { get } from 'lodash/fp'; import React, { useEffect, useState } from 'react'; +import { DEFAULT_INDEX_KEY } from '../../../../../../../plugins/siem/common/constants'; import { useUiSetting$ } from '../../../lib/kibana'; -import { DEFAULT_INDEX_KEY } from '../../../../common/constants'; import { GetHostFirstLastSeenQuery } from '../../../graphql/types'; import { inputsModel } from '../../../store'; import { QueryTemplateProps } from '../../query_template'; diff --git a/x-pack/legacy/plugins/siem/public/containers/hosts/first_last_seen/mock.ts b/x-pack/legacy/plugins/siem/public/containers/hosts/first_last_seen/mock.ts index 2c9d418763e8e..7376f38ae8d0f 100644 --- a/x-pack/legacy/plugins/siem/public/containers/hosts/first_last_seen/mock.ts +++ b/x-pack/legacy/plugins/siem/public/containers/hosts/first_last_seen/mock.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { defaultIndexPattern } from '../../../../default_index_pattern'; +import { DEFAULT_INDEX_PATTERN } from '../../../../../../../plugins/siem/common/constants'; import { GetHostFirstLastSeenQuery } from '../../../graphql/types'; import { HostFirstLastSeenGqlQuery } from './first_last_seen.gql_query'; @@ -34,7 +34,7 @@ export const mockFirstLastSeenHostQuery: MockedProvidedQuery[] = [ variables: { sourceId: 'default', hostName: 'kibana-siem', - defaultIndex: defaultIndexPattern, + defaultIndex: DEFAULT_INDEX_PATTERN, }, }, result: { diff --git a/x-pack/legacy/plugins/siem/public/containers/hosts/index.tsx b/x-pack/legacy/plugins/siem/public/containers/hosts/index.tsx index 733c2224d840a..edf3f6855f955 100644 --- a/x-pack/legacy/plugins/siem/public/containers/hosts/index.tsx +++ b/x-pack/legacy/plugins/siem/public/containers/hosts/index.tsx @@ -11,7 +11,7 @@ import { Query } from 'react-apollo'; import { connect } from 'react-redux'; import { compose } from 'redux'; -import { DEFAULT_INDEX_KEY } from '../../../common/constants'; +import { DEFAULT_INDEX_KEY } from '../../../../../../plugins/siem/common/constants'; import { Direction, GetHostsTableQuery, diff --git a/x-pack/legacy/plugins/siem/public/containers/hosts/overview/index.tsx b/x-pack/legacy/plugins/siem/public/containers/hosts/overview/index.tsx index 5057e872b5313..405c45348b54d 100644 --- a/x-pack/legacy/plugins/siem/public/containers/hosts/overview/index.tsx +++ b/x-pack/legacy/plugins/siem/public/containers/hosts/overview/index.tsx @@ -10,7 +10,7 @@ import { Query } from 'react-apollo'; import { connect } from 'react-redux'; import { compose } from 'redux'; -import { DEFAULT_INDEX_KEY } from '../../../../common/constants'; +import { DEFAULT_INDEX_KEY } from '../../../../../../../plugins/siem/common/constants'; import { inputsModel, inputsSelectors, State } from '../../../store'; import { getDefaultFetchPolicy } from '../../helpers'; import { QueryTemplate, QueryTemplateProps } from '../../query_template'; diff --git a/x-pack/legacy/plugins/siem/public/containers/ip_overview/index.tsx b/x-pack/legacy/plugins/siem/public/containers/ip_overview/index.tsx index ade94c430c6ef..954bfede07139 100644 --- a/x-pack/legacy/plugins/siem/public/containers/ip_overview/index.tsx +++ b/x-pack/legacy/plugins/siem/public/containers/ip_overview/index.tsx @@ -9,7 +9,7 @@ import React from 'react'; import { Query } from 'react-apollo'; import { connect, ConnectedProps } from 'react-redux'; -import { DEFAULT_INDEX_KEY } from '../../../common/constants'; +import { DEFAULT_INDEX_KEY } from '../../../../../../plugins/siem/common/constants'; import { GetIpOverviewQuery, IpOverviewData } from '../../graphql/types'; import { networkModel, inputsModel, inputsSelectors, State } from '../../store'; import { useUiSetting } from '../../lib/kibana'; diff --git a/x-pack/legacy/plugins/siem/public/containers/kpi_host_details/index.tsx b/x-pack/legacy/plugins/siem/public/containers/kpi_host_details/index.tsx index de9d54b1a185c..3933aefa60483 100644 --- a/x-pack/legacy/plugins/siem/public/containers/kpi_host_details/index.tsx +++ b/x-pack/legacy/plugins/siem/public/containers/kpi_host_details/index.tsx @@ -9,7 +9,7 @@ import React from 'react'; import { Query } from 'react-apollo'; import { connect, ConnectedProps } from 'react-redux'; -import { DEFAULT_INDEX_KEY } from '../../../common/constants'; +import { DEFAULT_INDEX_KEY } from '../../../../../../plugins/siem/common/constants'; import { KpiHostDetailsData, GetKpiHostDetailsQuery } from '../../graphql/types'; import { inputsModel, inputsSelectors, State } from '../../store'; import { useUiSetting } from '../../lib/kibana'; diff --git a/x-pack/legacy/plugins/siem/public/containers/kpi_hosts/index.tsx b/x-pack/legacy/plugins/siem/public/containers/kpi_hosts/index.tsx index 5be2423e8a162..7035d63193118 100644 --- a/x-pack/legacy/plugins/siem/public/containers/kpi_hosts/index.tsx +++ b/x-pack/legacy/plugins/siem/public/containers/kpi_hosts/index.tsx @@ -9,7 +9,7 @@ import React from 'react'; import { Query } from 'react-apollo'; import { connect, ConnectedProps } from 'react-redux'; -import { DEFAULT_INDEX_KEY } from '../../../common/constants'; +import { DEFAULT_INDEX_KEY } from '../../../../../../plugins/siem/common/constants'; import { GetKpiHostsQuery, KpiHostsData } from '../../graphql/types'; import { inputsModel, inputsSelectors, State } from '../../store'; import { useUiSetting } from '../../lib/kibana'; diff --git a/x-pack/legacy/plugins/siem/public/containers/kpi_network/index.tsx b/x-pack/legacy/plugins/siem/public/containers/kpi_network/index.tsx index 338cdc39b178c..002a819417df6 100644 --- a/x-pack/legacy/plugins/siem/public/containers/kpi_network/index.tsx +++ b/x-pack/legacy/plugins/siem/public/containers/kpi_network/index.tsx @@ -9,7 +9,7 @@ import React from 'react'; import { Query } from 'react-apollo'; import { connect, ConnectedProps } from 'react-redux'; -import { DEFAULT_INDEX_KEY } from '../../../common/constants'; +import { DEFAULT_INDEX_KEY } from '../../../../../../plugins/siem/common/constants'; import { GetKpiNetworkQuery, KpiNetworkData } from '../../graphql/types'; import { inputsModel, inputsSelectors, State } from '../../store'; import { useUiSetting } from '../../lib/kibana'; diff --git a/x-pack/legacy/plugins/siem/public/containers/matrix_histogram/index.ts b/x-pack/legacy/plugins/siem/public/containers/matrix_histogram/index.ts index 0b369b4180fb8..55d7e7cdc6e54 100644 --- a/x-pack/legacy/plugins/siem/public/containers/matrix_histogram/index.ts +++ b/x-pack/legacy/plugins/siem/public/containers/matrix_histogram/index.ts @@ -3,9 +3,12 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -import { useEffect, useState, useRef } from 'react'; + +import { isEmpty } from 'lodash/fp'; +import { useEffect, useMemo, useState, useRef } from 'react'; + +import { DEFAULT_INDEX_KEY } from '../../../../../../plugins/siem/common/constants'; import { MatrixHistogramQueryProps } from '../../components/matrix_histogram/types'; -import { DEFAULT_INDEX_KEY } from '../../../common/constants'; import { errorToToaster, useStateToaster } from '../../components/toasters'; import { useUiSetting$ } from '../../lib/kibana'; import { createFilter } from '../helpers'; @@ -19,11 +22,19 @@ export const useQuery = <Hit, Aggs, TCache = object>({ errorMessage, filterQuery, histogramType, + indexToAdd, isInspected, stackByField, startDate, }: MatrixHistogramQueryProps) => { - const [defaultIndex] = useUiSetting$<string[]>(DEFAULT_INDEX_KEY); + const [configIndex] = useUiSetting$<string[]>(DEFAULT_INDEX_KEY); + const defaultIndex = useMemo<string[]>(() => { + if (indexToAdd != null && !isEmpty(indexToAdd)) { + return [...configIndex, ...indexToAdd]; + } + return configIndex; + }, [configIndex, indexToAdd]); + const [, dispatchToaster] = useStateToaster(); const refetch = useRef<inputsModel.Refetch>(); const [loading, setLoading] = useState<boolean>(false); @@ -96,6 +107,7 @@ export const useQuery = <Hit, Aggs, TCache = object>({ errorMessage, filterQuery, histogramType, + indexToAdd, isInspected, stackByField, startDate, diff --git a/x-pack/legacy/plugins/siem/public/containers/network_dns/index.tsx b/x-pack/legacy/plugins/siem/public/containers/network_dns/index.tsx index 04c8783c30a0f..060b66fc3cbbe 100644 --- a/x-pack/legacy/plugins/siem/public/containers/network_dns/index.tsx +++ b/x-pack/legacy/plugins/siem/public/containers/network_dns/index.tsx @@ -12,7 +12,7 @@ import { compose } from 'redux'; import { DocumentNode } from 'graphql'; import { ScaleType } from '@elastic/charts'; -import { DEFAULT_INDEX_KEY } from '../../../common/constants'; +import { DEFAULT_INDEX_KEY } from '../../../../../../plugins/siem/common/constants'; import { GetNetworkDnsQuery, NetworkDnsEdges, diff --git a/x-pack/legacy/plugins/siem/public/containers/network_http/index.tsx b/x-pack/legacy/plugins/siem/public/containers/network_http/index.tsx index bf4e64f63d559..b13637fa88d07 100644 --- a/x-pack/legacy/plugins/siem/public/containers/network_http/index.tsx +++ b/x-pack/legacy/plugins/siem/public/containers/network_http/index.tsx @@ -10,7 +10,7 @@ import { Query } from 'react-apollo'; import { connect } from 'react-redux'; import { compose } from 'redux'; -import { DEFAULT_INDEX_KEY } from '../../../common/constants'; +import { DEFAULT_INDEX_KEY } from '../../../../../../plugins/siem/common/constants'; import { GetNetworkHttpQuery, NetworkHttpEdges, diff --git a/x-pack/legacy/plugins/siem/public/containers/network_top_countries/index.tsx b/x-pack/legacy/plugins/siem/public/containers/network_top_countries/index.tsx index bd1e1a002bbcd..17a14ce3a1120 100644 --- a/x-pack/legacy/plugins/siem/public/containers/network_top_countries/index.tsx +++ b/x-pack/legacy/plugins/siem/public/containers/network_top_countries/index.tsx @@ -10,7 +10,7 @@ import { Query } from 'react-apollo'; import { connect } from 'react-redux'; import { compose } from 'redux'; -import { DEFAULT_INDEX_KEY } from '../../../common/constants'; +import { DEFAULT_INDEX_KEY } from '../../../../../../plugins/siem/common/constants'; import { FlowTargetSourceDest, GetNetworkTopCountriesQuery, diff --git a/x-pack/legacy/plugins/siem/public/containers/network_top_n_flow/index.tsx b/x-pack/legacy/plugins/siem/public/containers/network_top_n_flow/index.tsx index f0f1f8257f29f..fdac282292a4b 100644 --- a/x-pack/legacy/plugins/siem/public/containers/network_top_n_flow/index.tsx +++ b/x-pack/legacy/plugins/siem/public/containers/network_top_n_flow/index.tsx @@ -10,7 +10,7 @@ import { Query } from 'react-apollo'; import { connect } from 'react-redux'; import { compose } from 'redux'; -import { DEFAULT_INDEX_KEY } from '../../../common/constants'; +import { DEFAULT_INDEX_KEY } from '../../../../../../plugins/siem/common/constants'; import { FlowTargetSourceDest, GetNetworkTopNFlowQuery, diff --git a/x-pack/legacy/plugins/siem/public/containers/overview/overview_host/index.tsx b/x-pack/legacy/plugins/siem/public/containers/overview/overview_host/index.tsx index 2dd9ccf24d802..e7b68bf557a21 100644 --- a/x-pack/legacy/plugins/siem/public/containers/overview/overview_host/index.tsx +++ b/x-pack/legacy/plugins/siem/public/containers/overview/overview_host/index.tsx @@ -9,7 +9,7 @@ import React from 'react'; import { Query } from 'react-apollo'; import { connect, ConnectedProps } from 'react-redux'; -import { DEFAULT_INDEX_KEY } from '../../../../common/constants'; +import { DEFAULT_INDEX_KEY } from '../../../../../../../plugins/siem/common/constants'; import { GetOverviewHostQuery, OverviewHostData } from '../../../graphql/types'; import { useUiSetting } from '../../../lib/kibana'; import { inputsModel, inputsSelectors } from '../../../store/inputs'; diff --git a/x-pack/legacy/plugins/siem/public/containers/overview/overview_network/index.tsx b/x-pack/legacy/plugins/siem/public/containers/overview/overview_network/index.tsx index d0acd41c224a5..c7f72ac6193f4 100644 --- a/x-pack/legacy/plugins/siem/public/containers/overview/overview_network/index.tsx +++ b/x-pack/legacy/plugins/siem/public/containers/overview/overview_network/index.tsx @@ -9,7 +9,7 @@ import React from 'react'; import { Query } from 'react-apollo'; import { connect, ConnectedProps } from 'react-redux'; -import { DEFAULT_INDEX_KEY } from '../../../../common/constants'; +import { DEFAULT_INDEX_KEY } from '../../../../../../../plugins/siem/common/constants'; import { GetOverviewNetworkQuery, OverviewNetworkData } from '../../../graphql/types'; import { useUiSetting } from '../../../lib/kibana'; import { State } from '../../../store'; diff --git a/x-pack/legacy/plugins/siem/public/containers/query_template.tsx b/x-pack/legacy/plugins/siem/public/containers/query_template.tsx index dfb452c24b86e..c33f5fd89a79b 100644 --- a/x-pack/legacy/plugins/siem/public/containers/query_template.tsx +++ b/x-pack/legacy/plugins/siem/public/containers/query_template.tsx @@ -8,7 +8,7 @@ import { ApolloQueryResult } from 'apollo-client'; import React from 'react'; import { FetchMoreOptions, FetchMoreQueryOptions, OperationVariables } from 'react-apollo'; -import { ESQuery } from '../../common/typed_json'; +import { ESQuery } from '../../../../../plugins/siem/common/typed_json'; export interface QueryTemplateProps { id?: string; diff --git a/x-pack/legacy/plugins/siem/public/containers/query_template_paginated.tsx b/x-pack/legacy/plugins/siem/public/containers/query_template_paginated.tsx index db618f216d83e..45041a6447611 100644 --- a/x-pack/legacy/plugins/siem/public/containers/query_template_paginated.tsx +++ b/x-pack/legacy/plugins/siem/public/containers/query_template_paginated.tsx @@ -10,7 +10,7 @@ import React from 'react'; import { FetchMoreOptions, FetchMoreQueryOptions, OperationVariables } from 'react-apollo'; import deepEqual from 'fast-deep-equal'; -import { ESQuery } from '../../common/typed_json'; +import { ESQuery } from '../../../../../plugins/siem/common/typed_json'; import { inputsModel } from '../store/model'; import { generateTablePaginationOptions } from '../components/paginated_table/helpers'; diff --git a/x-pack/legacy/plugins/siem/public/containers/source/index.tsx b/x-pack/legacy/plugins/siem/public/containers/source/index.tsx index e454421ca955d..3467e2b5f18d8 100644 --- a/x-pack/legacy/plugins/siem/public/containers/source/index.tsx +++ b/x-pack/legacy/plugins/siem/public/containers/source/index.tsx @@ -11,9 +11,9 @@ import React, { useEffect, useMemo, useState } from 'react'; import memoizeOne from 'memoize-one'; import { IIndexPattern } from 'src/plugins/data/public'; +import { DEFAULT_INDEX_KEY } from '../../../../../../plugins/siem/common/constants'; import { useUiSetting$ } from '../../lib/kibana'; -import { DEFAULT_INDEX_KEY } from '../../../common/constants'; import { IndexField, SourceQuery } from '../../graphql/types'; import { sourceQuery } from './index.gql_query'; diff --git a/x-pack/legacy/plugins/siem/public/containers/source/mock.ts b/x-pack/legacy/plugins/siem/public/containers/source/mock.ts index 738c1681f40af..805c69f7fcc12 100644 --- a/x-pack/legacy/plugins/siem/public/containers/source/mock.ts +++ b/x-pack/legacy/plugins/siem/public/containers/source/mock.ts @@ -4,9 +4,10 @@ * you may not use this file except in compliance with the Elastic License. */ +import { DEFAULT_INDEX_PATTERN } from '../../../../../../plugins/siem/common/constants'; + import { BrowserFields } from '.'; import { sourceQuery } from './index.gql_query'; -import { defaultIndexPattern } from '../../../default_index_pattern'; export const mocksSource = [ { @@ -14,7 +15,7 @@ export const mocksSource = [ query: sourceQuery, variables: { sourceId: 'default', - defaultIndex: defaultIndexPattern, + defaultIndex: DEFAULT_INDEX_PATTERN, }, }, result: { @@ -333,7 +334,7 @@ export const mocksSource = [ 'event.end contains the date when the event ended or when the activity was last observed.', example: null, format: '', - indexes: defaultIndexPattern, + indexes: DEFAULT_INDEX_PATTERN, name: 'event.end', searchable: true, type: 'date', @@ -661,7 +662,7 @@ export const mockBrowserFields: BrowserFields = { 'event.end contains the date when the event ended or when the activity was last observed.', example: null, format: '', - indexes: defaultIndexPattern, + indexes: DEFAULT_INDEX_PATTERN, name: 'event.end', searchable: true, type: 'date', diff --git a/x-pack/legacy/plugins/siem/public/containers/timeline/all/api.ts b/x-pack/legacy/plugins/siem/public/containers/timeline/all/api.ts index 4c8e2384de585..32ac62d594e1c 100644 --- a/x-pack/legacy/plugins/siem/public/containers/timeline/all/api.ts +++ b/x-pack/legacy/plugins/siem/public/containers/timeline/all/api.ts @@ -4,9 +4,12 @@ * you may not use this file except in compliance with the Elastic License. */ +import { + TIMELINE_IMPORT_URL, + TIMELINE_EXPORT_URL, +} from '../../../../../../../plugins/siem/common/constants'; import { ImportDataProps, ImportDataResponse } from '../../detection_engine/rules'; import { KibanaServices } from '../../../lib/kibana'; -import { TIMELINE_IMPORT_URL, TIMELINE_EXPORT_URL } from '../../../../common/constants'; import { ExportSelectedData } from '../../../components/generic_downloader'; export const importTimelines = async ({ diff --git a/x-pack/legacy/plugins/siem/public/containers/timeline/details/index.tsx b/x-pack/legacy/plugins/siem/public/containers/timeline/details/index.tsx index cf1b8954307e7..0debed9c5f9aa 100644 --- a/x-pack/legacy/plugins/siem/public/containers/timeline/details/index.tsx +++ b/x-pack/legacy/plugins/siem/public/containers/timeline/details/index.tsx @@ -9,7 +9,7 @@ import memoizeOne from 'memoize-one'; import React from 'react'; import { Query } from 'react-apollo'; -import { DEFAULT_INDEX_KEY } from '../../../../common/constants'; +import { DEFAULT_INDEX_KEY } from '../../../../../../../plugins/siem/common/constants'; import { DetailItem, GetTimelineDetailsQuery } from '../../../graphql/types'; import { useUiSetting } from '../../../lib/kibana'; diff --git a/x-pack/legacy/plugins/siem/public/containers/timeline/index.tsx b/x-pack/legacy/plugins/siem/public/containers/timeline/index.tsx index f726ec9779dc8..3c089ef6926dd 100644 --- a/x-pack/legacy/plugins/siem/public/containers/timeline/index.tsx +++ b/x-pack/legacy/plugins/siem/public/containers/timeline/index.tsx @@ -11,8 +11,8 @@ import { Query } from 'react-apollo'; import { compose, Dispatch } from 'redux'; import { connect, ConnectedProps } from 'react-redux'; +import { DEFAULT_INDEX_KEY } from '../../../../../../plugins/siem/common/constants'; import { IIndexPattern } from '../../../../../../../src/plugins/data/common/index_patterns'; -import { DEFAULT_INDEX_KEY } from '../../../common/constants'; import { GetTimelineQuery, PageInfo, diff --git a/x-pack/legacy/plugins/siem/public/containers/tls/index.tsx b/x-pack/legacy/plugins/siem/public/containers/tls/index.tsx index 3738355c8846e..20617b88bda94 100644 --- a/x-pack/legacy/plugins/siem/public/containers/tls/index.tsx +++ b/x-pack/legacy/plugins/siem/public/containers/tls/index.tsx @@ -10,7 +10,7 @@ import { Query } from 'react-apollo'; import { connect } from 'react-redux'; import { compose } from 'redux'; -import { DEFAULT_INDEX_KEY } from '../../../common/constants'; +import { DEFAULT_INDEX_KEY } from '../../../../../../plugins/siem/common/constants'; import { PageInfoPaginated, TlsEdges, diff --git a/x-pack/legacy/plugins/siem/public/containers/uncommon_processes/index.tsx b/x-pack/legacy/plugins/siem/public/containers/uncommon_processes/index.tsx index 0a2ce67d9be80..72e4e46bc6ae0 100644 --- a/x-pack/legacy/plugins/siem/public/containers/uncommon_processes/index.tsx +++ b/x-pack/legacy/plugins/siem/public/containers/uncommon_processes/index.tsx @@ -10,7 +10,7 @@ import { Query } from 'react-apollo'; import { connect, ConnectedProps } from 'react-redux'; import { compose } from 'redux'; -import { DEFAULT_INDEX_KEY } from '../../../common/constants'; +import { DEFAULT_INDEX_KEY } from '../../../../../../plugins/siem/common/constants'; import { GetUncommonProcessesQuery, PageInfoPaginated, diff --git a/x-pack/legacy/plugins/siem/public/containers/users/index.tsx b/x-pack/legacy/plugins/siem/public/containers/users/index.tsx index 5f71449c52460..658cb5785b54c 100644 --- a/x-pack/legacy/plugins/siem/public/containers/users/index.tsx +++ b/x-pack/legacy/plugins/siem/public/containers/users/index.tsx @@ -10,7 +10,7 @@ import { Query } from 'react-apollo'; import { connect, ConnectedProps } from 'react-redux'; import { compose } from 'redux'; -import { DEFAULT_INDEX_KEY } from '../../../common/constants'; +import { DEFAULT_INDEX_KEY } from '../../../../../../plugins/siem/common/constants'; import { GetUsersQuery, FlowTarget, PageInfoPaginated, UsersEdges } from '../../graphql/types'; import { inputsModel, networkModel, networkSelectors, State, inputsSelectors } from '../../store'; import { withKibana, WithKibanaProps } from '../../lib/kibana'; diff --git a/x-pack/legacy/plugins/siem/public/lib/kibana/hooks.ts b/x-pack/legacy/plugins/siem/public/lib/kibana/hooks.ts index 775a7d7c0acca..e1d0a445bf2fb 100644 --- a/x-pack/legacy/plugins/siem/public/lib/kibana/hooks.ts +++ b/x-pack/legacy/plugins/siem/public/lib/kibana/hooks.ts @@ -8,7 +8,10 @@ import moment from 'moment-timezone'; import { useCallback, useEffect, useState } from 'react'; import { i18n } from '@kbn/i18n'; -import { DEFAULT_DATE_FORMAT, DEFAULT_DATE_FORMAT_TZ } from '../../../common/constants'; +import { + DEFAULT_DATE_FORMAT, + DEFAULT_DATE_FORMAT_TZ, +} from '../../../../../../plugins/siem/common/constants'; import { useUiSetting, useKibana } from './kibana_react'; import { errorToToaster, useStateToaster } from '../../components/toasters'; import { AuthenticatedUser } from '../../../../../../plugins/security/common/model'; diff --git a/x-pack/legacy/plugins/siem/public/lib/theme/use_eui_theme.tsx b/x-pack/legacy/plugins/siem/public/lib/theme/use_eui_theme.tsx index 1696001203bc8..b72c34d3b59a7 100644 --- a/x-pack/legacy/plugins/siem/public/lib/theme/use_eui_theme.tsx +++ b/x-pack/legacy/plugins/siem/public/lib/theme/use_eui_theme.tsx @@ -7,7 +7,7 @@ import darkTheme from '@elastic/eui/dist/eui_theme_dark.json'; import lightTheme from '@elastic/eui/dist/eui_theme_light.json'; -import { DEFAULT_DARK_MODE } from '../../../common/constants'; +import { DEFAULT_DARK_MODE } from '../../../../../../plugins/siem/common/constants'; import { useUiSetting$ } from '../kibana'; export const useEuiTheme = () => { diff --git a/x-pack/legacy/plugins/siem/public/mock/global_state.ts b/x-pack/legacy/plugins/siem/public/mock/global_state.ts index 6678c3043a3da..266c3aadea8af 100644 --- a/x-pack/legacy/plugins/siem/public/mock/global_state.ts +++ b/x-pack/legacy/plugins/siem/public/mock/global_state.ts @@ -22,7 +22,7 @@ import { DEFAULT_TO, DEFAULT_INTERVAL_TYPE, DEFAULT_INTERVAL_VALUE, -} from '../../common/constants'; +} from '../../../../../plugins/siem/common/constants'; export const mockGlobalState: State = { app: { diff --git a/x-pack/legacy/plugins/siem/public/mock/kibana_react.ts b/x-pack/legacy/plugins/siem/public/mock/kibana_react.ts index 968ab6543f4fc..db7a931b3fb15 100644 --- a/x-pack/legacy/plugins/siem/public/mock/kibana_react.ts +++ b/x-pack/legacy/plugins/siem/public/mock/kibana_react.ts @@ -23,8 +23,8 @@ import { DEFAULT_INTERVAL_PAUSE, DEFAULT_INTERVAL_VALUE, DEFAULT_BYTES_FORMAT, -} from '../../common/constants'; -import { defaultIndexPattern } from '../../default_index_pattern'; + DEFAULT_INDEX_PATTERN, +} from '../../../../../plugins/siem/common/constants'; import { createKibanaCoreStartMock, createKibanaPluginsStartMock } from './kibana_core'; // eslint-disable-next-line @typescript-eslint/no-explicit-any @@ -39,7 +39,7 @@ export const mockUiSettings: Record<string, any> = { pause: DEFAULT_INTERVAL_PAUSE, value: DEFAULT_INTERVAL_VALUE, }, - [DEFAULT_INDEX_KEY]: defaultIndexPattern, + [DEFAULT_INDEX_KEY]: DEFAULT_INDEX_PATTERN, [DEFAULT_BYTES_FORMAT]: '0,0.[0]b', [DEFAULT_DATE_FORMAT_TZ]: 'UTC', [DEFAULT_DATE_FORMAT]: 'MMM D, YYYY @ HH:mm:ss.SSS', diff --git a/x-pack/legacy/plugins/siem/public/mock/test_providers.tsx b/x-pack/legacy/plugins/siem/public/mock/test_providers.tsx index c7692755c1330..952f7f51b63f2 100644 --- a/x-pack/legacy/plugins/siem/public/mock/test_providers.tsx +++ b/x-pack/legacy/plugins/siem/public/mock/test_providers.tsx @@ -20,6 +20,7 @@ import { ThemeProvider } from 'styled-components'; import { createStore, State } from '../store'; import { mockGlobalState } from './global_state'; import { createKibanaContextProviderMock } from './kibana_react'; +import { FieldHook, useForm } from '../shared_imports'; jest.mock('ui/new_platform'); @@ -91,3 +92,29 @@ const TestProviderWithoutDragAndDropComponent: React.FC<Props> = ({ ); export const TestProviderWithoutDragAndDrop = React.memo(TestProviderWithoutDragAndDropComponent); + +export const useFormFieldMock = (options?: Partial<FieldHook>): FieldHook => { + const { form } = useForm(); + + return { + path: 'path', + type: 'type', + value: [], + isPristine: false, + isValidating: false, + isValidated: false, + isChangingValue: false, + form, + errors: [], + isValid: true, + getErrorsMessages: jest.fn(), + onChange: jest.fn(), + setValue: jest.fn(), + setErrors: jest.fn(), + clearErrors: jest.fn(), + validate: jest.fn(), + reset: jest.fn(), + __serializeOutput: jest.fn(), + ...options, + }; +}; diff --git a/x-pack/legacy/plugins/siem/public/pages/case/case.tsx b/x-pack/legacy/plugins/siem/public/pages/case/case.tsx index 2ae35796387b8..2b613f6692df1 100644 --- a/x-pack/legacy/plugins/siem/public/pages/case/case.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/case/case.tsx @@ -11,11 +11,9 @@ import { useGetUserSavedObjectPermissions } from '../../lib/kibana'; import { SpyRoute } from '../../utils/route/spy_routes'; import { AllCases } from './components/all_cases'; -import { getSavedObjectReadOnly, CaseCallOut } from './components/callout'; +import { savedObjectReadOnly, CaseCallOut } from './components/callout'; import { CaseSavedObjectNoPermissions } from './saved_object_no_permissions'; -const infoReadSavedObject = getSavedObjectReadOnly(); - export const CasesPage = React.memo(() => { const userPermissions = useGetUserSavedObjectPermissions(); @@ -24,8 +22,8 @@ export const CasesPage = React.memo(() => { <WrapperPage> {userPermissions != null && !userPermissions?.crud && userPermissions?.read && ( <CaseCallOut - title={infoReadSavedObject.title} - message={infoReadSavedObject.description} + title={savedObjectReadOnly.title} + message={savedObjectReadOnly.description} /> )} <AllCases userCanCrud={userPermissions?.crud ?? false} /> diff --git a/x-pack/legacy/plugins/siem/public/pages/case/case_details.tsx b/x-pack/legacy/plugins/siem/public/pages/case/case_details.tsx index cbc7bbc62fbf9..4bb8afa7f8d42 100644 --- a/x-pack/legacy/plugins/siem/public/pages/case/case_details.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/case/case_details.tsx @@ -13,9 +13,7 @@ import { SpyRoute } from '../../utils/route/spy_routes'; import { getCaseUrl } from '../../components/link_to'; import { navTabs } from '../home/home_navigations'; import { CaseView } from './components/case_view'; -import { getSavedObjectReadOnly, CaseCallOut } from './components/callout'; - -const infoReadSavedObject = getSavedObjectReadOnly(); +import { savedObjectReadOnly, CaseCallOut } from './components/callout'; export const CaseDetailsPage = React.memo(() => { const userPermissions = useGetUserSavedObjectPermissions(); @@ -29,7 +27,7 @@ export const CaseDetailsPage = React.memo(() => { return caseId != null ? ( <> {userPermissions != null && !userPermissions?.crud && userPermissions?.read && ( - <CaseCallOut title={infoReadSavedObject.title} message={infoReadSavedObject.description} /> + <CaseCallOut title={savedObjectReadOnly.title} message={savedObjectReadOnly.description} /> )} <CaseView caseId={caseId} userCanCrud={userPermissions?.crud ?? false} /> <SpyRoute /> diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/__mock__/form.ts b/x-pack/legacy/plugins/siem/public/pages/case/components/__mock__/form.ts new file mode 100644 index 0000000000000..9d2ac29bc47d7 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/pages/case/components/__mock__/form.ts @@ -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. + */ +export const mockFormHook = { + isSubmitted: false, + isSubmitting: false, + isValid: true, + submit: jest.fn(), + subscribe: jest.fn(), + setFieldValue: jest.fn(), + setFieldErrors: jest.fn(), + getFields: jest.fn(), + getFormData: jest.fn(), + getFieldDefaultValue: jest.fn(), + /* Returns a list of all errors in the form */ + getErrors: jest.fn(), + reset: jest.fn(), + __options: {}, + __formData$: {}, + __addField: jest.fn(), + __removeField: jest.fn(), + __validateFields: jest.fn(), + __updateFormDataAt: jest.fn(), + __readFieldConfigFromSchema: jest.fn(), +}; +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export const getFormMock = (sampleData: any) => ({ + ...mockFormHook, + submit: () => + Promise.resolve({ + data: sampleData, + isValid: true, + }), + getFormData: () => sampleData, +}); diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/__mock__/router.ts b/x-pack/legacy/plugins/siem/public/pages/case/components/__mock__/router.ts new file mode 100644 index 0000000000000..a20ab00852a36 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/pages/case/components/__mock__/router.ts @@ -0,0 +1,39 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { Router } from 'react-router-dom'; +// eslint-disable-next-line @kbn/eslint/module_migration +import routeData from 'react-router'; +type Action = 'PUSH' | 'POP' | 'REPLACE'; +const pop: Action = 'POP'; +const location = { + pathname: '/network', + search: '', + state: '', + hash: '', +}; +export const mockHistory = { + length: 2, + location, + action: pop, + push: jest.fn(), + replace: jest.fn(), + go: jest.fn(), + goBack: jest.fn(), + goForward: jest.fn(), + block: jest.fn(), + createHref: jest.fn(), + listen: jest.fn(), +}; + +export const mockLocation = { + pathname: '/welcome', + hash: '', + search: '', + state: '', +}; + +export { Router, routeData }; diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/add_comment/index.test.tsx b/x-pack/legacy/plugins/siem/public/pages/case/components/add_comment/index.test.tsx new file mode 100644 index 0000000000000..74f6411f17fa0 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/pages/case/components/add_comment/index.test.tsx @@ -0,0 +1,144 @@ +/* + * 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 { AddComment } from './'; +import { TestProviders } from '../../../../mock'; +import { getFormMock } from '../__mock__/form'; +import { Router, routeData, mockHistory, mockLocation } from '../__mock__/router'; + +import { useInsertTimeline } from '../../../../components/timeline/insert_timeline_popover/use_insert_timeline'; +import { usePostComment } from '../../../../containers/case/use_post_comment'; +import { useForm } from '../../../../../../../../../src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/use_form'; +import { wait } from '../../../../lib/helpers'; +jest.mock( + '../../../../../../../../../src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/use_form' +); +jest.mock('../../../../components/timeline/insert_timeline_popover/use_insert_timeline'); +jest.mock('../../../../containers/case/use_post_comment'); + +export const useFormMock = useForm as jest.Mock; + +const useInsertTimelineMock = useInsertTimeline as jest.Mock; +const usePostCommentMock = usePostComment as jest.Mock; + +const onCommentSaving = jest.fn(); +const onCommentPosted = jest.fn(); +const postComment = jest.fn(); +const handleCursorChange = jest.fn(); +const handleOnTimelineChange = jest.fn(); + +const addCommentProps = { + caseId: '1234', + disabled: false, + insertQuote: null, + onCommentSaving, + onCommentPosted, + showLoading: false, +}; + +const defaultInsertTimeline = { + cursorPosition: { + start: 0, + end: 0, + }, + handleCursorChange, + handleOnTimelineChange, +}; + +const defaultPostCommment = { + isLoading: false, + isError: false, + postComment, +}; +const sampleData = { + comment: 'what a cool comment', +}; +describe('AddComment ', () => { + const formHookMock = getFormMock(sampleData); + + beforeEach(() => { + jest.resetAllMocks(); + useInsertTimelineMock.mockImplementation(() => defaultInsertTimeline); + usePostCommentMock.mockImplementation(() => defaultPostCommment); + useFormMock.mockImplementation(() => ({ form: formHookMock })); + jest.spyOn(routeData, 'useLocation').mockReturnValue(mockLocation); + }); + + it('should post comment on submit click', async () => { + const wrapper = mount( + <TestProviders> + <Router history={mockHistory}> + <AddComment {...addCommentProps} /> + </Router> + </TestProviders> + ); + expect(wrapper.find(`[data-test-subj="add-comment"]`).exists()).toBeTruthy(); + expect(wrapper.find(`[data-test-subj="loading-spinner"]`).exists()).toBeFalsy(); + + wrapper + .find(`[data-test-subj="submit-comment"]`) + .first() + .simulate('click'); + await wait(); + expect(onCommentSaving).toBeCalled(); + expect(postComment).toBeCalledWith(sampleData, onCommentPosted); + expect(formHookMock.reset).toBeCalled(); + }); + + it('should render spinner and disable submit when loading', () => { + usePostCommentMock.mockImplementation(() => ({ ...defaultPostCommment, isLoading: true })); + const wrapper = mount( + <TestProviders> + <Router history={mockHistory}> + <AddComment {...{ ...addCommentProps, showLoading: true }} /> + </Router> + </TestProviders> + ); + expect(wrapper.find(`[data-test-subj="loading-spinner"]`).exists()).toBeTruthy(); + expect( + wrapper + .find(`[data-test-subj="submit-comment"]`) + .first() + .prop('isDisabled') + ).toBeTruthy(); + }); + + it('should disable submit button when disabled prop passed', () => { + usePostCommentMock.mockImplementation(() => ({ ...defaultPostCommment, isLoading: true })); + const wrapper = mount( + <TestProviders> + <Router history={mockHistory}> + <AddComment {...{ ...addCommentProps, disabled: true }} /> + </Router> + </TestProviders> + ); + expect( + wrapper + .find(`[data-test-subj="submit-comment"]`) + .first() + .prop('isDisabled') + ).toBeTruthy(); + }); + + it('should insert a quote if one is available', () => { + const sampleQuote = 'what a cool quote'; + mount( + <TestProviders> + <Router history={mockHistory}> + <AddComment {...{ ...addCommentProps, insertQuote: sampleQuote }} /> + </Router> + </TestProviders> + ); + + expect(formHookMock.setFieldValue).toBeCalledWith( + 'comment', + `${sampleData.comment}\n\n${sampleQuote}` + ); + }); +}); diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/add_comment/index.tsx b/x-pack/legacy/plugins/siem/public/pages/case/components/add_comment/index.tsx index ecc57c50e28eb..eaba708948a99 100644 --- a/x-pack/legacy/plugins/siem/public/pages/case/components/add_comment/index.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/case/components/add_comment/index.tsx @@ -71,10 +71,9 @@ export const AddComment = React.memo<AddCommentProps>( form.reset(); } }, [form, onCommentPosted, onCommentSaving]); - return ( <span id="add-comment-permLink"> - {isLoading && showLoading && <MySpinner size="xl" />} + {isLoading && showLoading && <MySpinner data-test-subj="loading-spinner" size="xl" />} <Form form={form}> <UseField path="comment" @@ -82,11 +81,12 @@ export const AddComment = React.memo<AddCommentProps>( componentProps={{ idAria: 'caseComment', isDisabled: isLoading, - dataTestSubj: 'caseComment', + dataTestSubj: 'add-comment', placeholder: i18n.ADD_COMMENT_HELP_TEXT, onCursorPositionUpdate: handleCursorChange, bottomRightContent: ( <EuiButton + data-test-subj="submit-comment" iconType="plusInCircle" isDisabled={isLoading || disabled} isLoading={isLoading} diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/__mock__/index.tsx b/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/__mock__/index.tsx deleted file mode 100644 index d4ec32dfd070b..0000000000000 --- a/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/__mock__/index.tsx +++ /dev/null @@ -1,115 +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 { SortFieldCase } from '../../../../../containers/case/types'; -import { UseGetCasesState } from '../../../../../containers/case/use_get_cases'; - -export const useGetCasesMockState: UseGetCasesState = { - data: { - countClosedCases: 0, - countOpenCases: 5, - cases: [ - { - closedAt: null, - closedBy: null, - id: '3c4ddcc0-4e99-11ea-9290-35d05cb55c15', - createdAt: '2020-02-13T19:44:23.627Z', - createdBy: { username: 'elastic' }, - comments: [], - description: 'Security banana Issue', - externalService: null, - status: 'open', - tags: ['defacement'], - title: 'Another horrible breach', - totalComment: 0, - updatedAt: null, - updatedBy: null, - version: 'WzQ3LDFd', - }, - { - closedAt: null, - closedBy: null, - id: '362a5c10-4e99-11ea-9290-35d05cb55c15', - createdAt: '2020-02-13T19:44:13.328Z', - createdBy: { username: 'elastic' }, - comments: [], - description: 'Security banana Issue', - externalService: null, - status: 'open', - tags: ['phishing'], - title: 'Bad email', - totalComment: 0, - updatedAt: null, - updatedBy: null, - version: 'WzQ3LDFd', - }, - { - closedAt: null, - closedBy: null, - id: '34f8b9e0-4e99-11ea-9290-35d05cb55c15', - createdAt: '2020-02-13T19:44:11.328Z', - createdBy: { username: 'elastic' }, - comments: [], - description: 'Security banana Issue', - externalService: null, - status: 'open', - tags: ['phishing'], - title: 'Bad email', - totalComment: 0, - updatedAt: null, - updatedBy: null, - version: 'WzQ3LDFd', - }, - { - closedAt: '2020-02-13T19:44:13.328Z', - closedBy: { username: 'elastic' }, - id: '31890e90-4e99-11ea-9290-35d05cb55c15', - createdAt: '2020-02-13T19:44:05.563Z', - createdBy: { username: 'elastic' }, - comments: [], - description: 'Security banana Issue', - externalService: null, - status: 'closed', - tags: ['phishing'], - title: 'Uh oh', - totalComment: 0, - updatedAt: null, - updatedBy: null, - version: 'WzQ3LDFd', - }, - { - closedAt: null, - closedBy: null, - id: '2f5b3210-4e99-11ea-9290-35d05cb55c15', - createdAt: '2020-02-13T19:44:01.901Z', - createdBy: { username: 'elastic' }, - comments: [], - description: 'Security banana Issue', - externalService: null, - status: 'open', - tags: ['phishing'], - title: 'Uh oh', - totalComment: 0, - updatedAt: null, - updatedBy: null, - version: 'WzQ3LDFd', - }, - ], - page: 1, - perPage: 5, - total: 10, - }, - loading: [], - selectedCases: [], - isError: false, - queryParams: { - page: 1, - perPage: 5, - sortField: SortFieldCase.createdAt, - sortOrder: 'desc', - }, - filterOptions: { search: '', reporters: [], tags: [], status: 'open' }, -}; diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/columns.test.tsx b/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/columns.test.tsx new file mode 100644 index 0000000000000..31c795c05edd5 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/columns.test.tsx @@ -0,0 +1,48 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { mount } from 'enzyme'; + +import { ServiceNowColumn } from './columns'; + +import { useGetCasesMockState } from '../../../../containers/case/mock'; + +describe('ServiceNowColumn ', () => { + it('Not pushed render', () => { + const wrapper = mount( + <ServiceNowColumn {...{ theCase: useGetCasesMockState.data.cases[0] }} /> + ); + expect( + wrapper + .find(`[data-test-subj="case-table-column-external-notPushed"]`) + .last() + .exists() + ).toBeTruthy(); + }); + it('Up to date', () => { + const wrapper = mount( + <ServiceNowColumn {...{ theCase: useGetCasesMockState.data.cases[1] }} /> + ); + expect( + wrapper + .find(`[data-test-subj="case-table-column-external-upToDate"]`) + .last() + .exists() + ).toBeTruthy(); + }); + it('Needs update', () => { + const wrapper = mount( + <ServiceNowColumn {...{ theCase: useGetCasesMockState.data.cases[2] }} /> + ); + expect( + wrapper + .find(`[data-test-subj="case-table-column-external-requiresUpdate"]`) + .last() + .exists() + ).toBeTruthy(); + }); +}); diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/columns.tsx b/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/columns.tsx index 0e12f78e29bc2..e48e5cb0c5959 100644 --- a/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/columns.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/columns.tsx @@ -114,7 +114,9 @@ export const getCasesColumns = ( name: i18n.COMMENTS, sortable: true, render: (totalComment: Case['totalComment']) => - renderStringField(`${totalComment}`, `case-table-column-commentCount`), + totalComment != null + ? renderStringField(`${totalComment}`, `case-table-column-commentCount`) + : getEmptyTagValue(), }, filterStatus === 'open' ? { @@ -150,7 +152,7 @@ export const getCasesColumns = ( }, }, { - name: 'ServiceNow Incident', + name: i18n.SERVICENOW_INCIDENT, render: (theCase: Case) => { if (theCase.id != null) { return <ServiceNowColumn theCase={theCase} />; @@ -159,7 +161,7 @@ export const getCasesColumns = ( }, }, { - name: 'Actions', + name: i18n.ACTIONS, actions, }, ]; @@ -168,7 +170,7 @@ interface Props { theCase: Case; } -const ServiceNowColumn: React.FC<Props> = ({ theCase }) => { +export const ServiceNowColumn: React.FC<Props> = ({ theCase }) => { const handleRenderDataToPush = useCallback(() => { const lastCaseUpdate = theCase.updatedAt != null ? new Date(theCase.updatedAt) : null; const lastCasePush = @@ -190,7 +192,9 @@ const ServiceNowColumn: React.FC<Props> = ({ theCase }) => { > {theCase.externalService?.externalTitle} </EuiLink> - {hasDataToPush ? i18n.REQUIRES_UPDATE : i18n.UP_TO_DATE} + {hasDataToPush + ? renderStringField(i18n.REQUIRES_UPDATE, `case-table-column-external-requiresUpdate`) + : renderStringField(i18n.UP_TO_DATE, `case-table-column-external-upToDate`)} </p> ); }, [theCase]); diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/index.test.tsx b/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/index.test.tsx index a6da45a8c5bb1..58d0c1b0faaf3 100644 --- a/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/index.test.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/index.test.tsx @@ -9,11 +9,15 @@ import { mount } from 'enzyme'; import moment from 'moment-timezone'; import { AllCases } from './'; import { TestProviders } from '../../../../mock'; -import { useGetCasesMockState } from './__mock__'; +import { useGetCasesMockState } from '../../../../containers/case/mock'; +import * as i18n from './translations'; + +import { getEmptyTagValue } from '../../../../components/empty_value'; import { useDeleteCases } from '../../../../containers/case/use_delete_cases'; import { useGetCases } from '../../../../containers/case/use_get_cases'; import { useGetCasesStatus } from '../../../../containers/case/use_get_cases_status'; import { useUpdateCases } from '../../../../containers/case/use_bulk_update_case'; +import { getCasesColumns } from './columns'; jest.mock('../../../../containers/case/use_bulk_update_case'); jest.mock('../../../../containers/case/use_delete_cases'); jest.mock('../../../../containers/case/use_get_cases'); @@ -35,6 +39,7 @@ describe('AllCases', () => { const setSelectedCases = jest.fn(); const updateBulkStatus = jest.fn(); const fetchCasesStatus = jest.fn(); + const emptyTag = getEmptyTagValue().props.children; const defaultGetCases = { ...useGetCasesMockState, @@ -115,7 +120,7 @@ describe('AllCases', () => { .find(`[data-test-subj="case-table-column-createdBy"]`) .first() .text() - ).toEqual(useGetCasesMockState.data.cases[0].createdBy.username); + ).toEqual(useGetCasesMockState.data.cases[0].createdBy.fullName); expect( wrapper .find(`[data-test-subj="case-table-column-createdAt"]`) @@ -129,6 +134,39 @@ describe('AllCases', () => { .text() ).toEqual('Showing 10 cases'); }); + it('should render empty fields', () => { + useGetCasesMock.mockImplementation(() => ({ + ...defaultGetCases, + data: { + ...defaultGetCases.data, + cases: [ + { + ...defaultGetCases.data.cases[0], + id: null, + createdAt: null, + createdBy: null, + tags: null, + title: null, + totalComment: null, + }, + ], + }, + })); + const wrapper = mount( + <TestProviders> + <AllCases userCanCrud={true} /> + </TestProviders> + ); + const checkIt = (columnName: string, key: number) => { + const column = wrapper.find('[data-test-subj="cases-table"] tbody .euiTableRowCell').at(key); + if (columnName === i18n.ACTIONS) { + return; + } + expect(column.find('.euiTableRowCell--hideForDesktop').text()).toEqual(columnName); + expect(column.find('span').text()).toEqual(emptyTag); + }; + getCasesColumns([], 'open').map((i, key) => i.name != null && checkIt(`${i.name}`, key)); + }); it('should tableHeaderSortButton AllCases', () => { const wrapper = mount( <TestProviders> @@ -165,6 +203,30 @@ describe('AllCases', () => { version: firstCase.version, }); }); + it('opens case when row action icon clicked', () => { + useGetCasesMock.mockImplementation(() => ({ + ...defaultGetCases, + filterOptions: { ...defaultGetCases.filterOptions, status: 'closed' }, + })); + + const wrapper = mount( + <TestProviders> + <AllCases userCanCrud={true} /> + </TestProviders> + ); + wrapper + .find('[data-test-subj="action-open"]') + .first() + .simulate('click'); + const firstCase = useGetCasesMockState.data.cases[0]; + expect(dispatchUpdateCaseProperty).toBeCalledWith({ + caseId: firstCase.id, + updateKey: 'status', + updateValue: 'open', + refetchCasesStatus: fetchCasesStatus, + version: firstCase.version, + }); + }); it('Bulk delete', () => { useGetCasesMock.mockImplementation(() => ({ ...defaultGetCases, diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/index.tsx b/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/index.tsx index b0ff3dbada6c9..a6eca717a82a3 100644 --- a/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/index.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/index.tsx @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { useCallback, useEffect, useMemo, useState } from 'react'; +import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { EuiBasicTable, EuiButton, @@ -129,13 +129,25 @@ export const AllCases = React.memo<AllCasesProps>(({ userCanCrud }) => { id: '', }); const [deleteBulk, setDeleteBulk] = useState<DeleteCase[]>([]); - - const refreshCases = useCallback(() => { - refetchCases(filterOptions, queryParams); - fetchCasesStatus(); - setSelectedCases([]); - setDeleteBulk([]); - }, [filterOptions, queryParams]); + const filterRefetch = useRef<() => void>(); + const setFilterRefetch = useCallback( + (refetchFilter: () => void) => { + filterRefetch.current = refetchFilter; + }, + [filterRefetch.current] + ); + const refreshCases = useCallback( + (dataRefresh = true) => { + if (dataRefresh) refetchCases(); + fetchCasesStatus(); + setSelectedCases([]); + setDeleteBulk([]); + if (filterRefetch.current != null) { + filterRefetch.current(); + } + }, + [filterOptions, queryParams, filterRefetch.current] + ); useEffect(() => { if (isDeleted) { @@ -247,6 +259,7 @@ export const AllCases = React.memo<AllCasesProps>(({ userCanCrud }) => { }; } setQueryParams(newQueryParams); + refreshCases(false); }, [queryParams] ); @@ -259,6 +272,7 @@ export const AllCases = React.memo<AllCasesProps>(({ userCanCrud }) => { setQueryParams({ sortField: SortFieldCase.createdAt }); } setFilters(newFilterOptions); + refreshCases(false); }, [filterOptions, queryParams] ); @@ -347,6 +361,7 @@ export const AllCases = React.memo<AllCasesProps>(({ userCanCrud }) => { tags: filterOptions.tags, status: filterOptions.status, }} + setFilterRefetch={setFilterRefetch} /> {isCasesLoading && isDataEmpty ? ( <Div> diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/table_filters.test.tsx b/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/table_filters.test.tsx new file mode 100644 index 0000000000000..21dcc9732440d --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/table_filters.test.tsx @@ -0,0 +1,165 @@ +/* + * 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 { CasesTableFilters } from './table_filters'; +import { TestProviders } from '../../../../mock'; + +import { useGetTags } from '../../../../containers/case/use_get_tags'; +import { useGetReporters } from '../../../../containers/case/use_get_reporters'; +import { DEFAULT_FILTER_OPTIONS } from '../../../../containers/case/use_get_cases'; +jest.mock('../../../../components/timeline/insert_timeline_popover/use_insert_timeline'); +jest.mock('../../../../containers/case/use_get_reporters'); +jest.mock('../../../../containers/case/use_get_tags'); + +const onFilterChanged = jest.fn(); +const fetchReporters = jest.fn(); +const fetchTags = jest.fn(); +const setFilterRefetch = jest.fn(); + +const props = { + countClosedCases: 1234, + countOpenCases: 99, + onFilterChanged, + initial: DEFAULT_FILTER_OPTIONS, + setFilterRefetch, +}; +describe('CasesTableFilters ', () => { + beforeEach(() => { + jest.resetAllMocks(); + (useGetTags as jest.Mock).mockReturnValue({ tags: ['coke', 'pepsi'], fetchTags }); + (useGetReporters as jest.Mock).mockReturnValue({ + reporters: ['casetester'], + respReporters: [{ username: 'casetester' }], + isLoading: true, + isError: false, + fetchReporters, + }); + }); + it('should render the initial case count', () => { + const wrapper = mount( + <TestProviders> + <CasesTableFilters {...props} /> + </TestProviders> + ); + expect( + wrapper + .find(`[data-test-subj="open-case-count"]`) + .last() + .text() + ).toEqual('Open cases (99)'); + expect( + wrapper + .find(`[data-test-subj="closed-case-count"]`) + .last() + .text() + ).toEqual('Closed cases (1234)'); + }); + it('should call onFilterChange when selected tags change', () => { + const wrapper = mount( + <TestProviders> + <CasesTableFilters {...props} /> + </TestProviders> + ); + wrapper + .find(`[data-test-subj="options-filter-popover-button-Tags"]`) + .last() + .simulate('click'); + wrapper + .find(`[data-test-subj="options-filter-popover-item-0"]`) + .last() + .simulate('click'); + + expect(onFilterChanged).toBeCalledWith({ tags: ['coke'] }); + }); + it('should call onFilterChange when selected reporters change', () => { + const wrapper = mount( + <TestProviders> + <CasesTableFilters {...props} /> + </TestProviders> + ); + wrapper + .find(`[data-test-subj="options-filter-popover-button-Reporter"]`) + .last() + .simulate('click'); + + wrapper + .find(`[data-test-subj="options-filter-popover-item-0"]`) + .last() + .simulate('click'); + + expect(onFilterChanged).toBeCalledWith({ reporters: [{ username: 'casetester' }] }); + }); + it('should call onFilterChange when search changes', () => { + const wrapper = mount( + <TestProviders> + <CasesTableFilters {...props} /> + </TestProviders> + ); + + wrapper + .find(`[data-test-subj="search-cases"]`) + .last() + .simulate('keyup', { keyCode: 13, target: { value: 'My search' } }); + expect(onFilterChanged).toBeCalledWith({ search: 'My search' }); + }); + it('should call onFilterChange when status toggled', () => { + const wrapper = mount( + <TestProviders> + <CasesTableFilters {...props} /> + </TestProviders> + ); + wrapper + .find(`[data-test-subj="closed-case-count"]`) + .last() + .simulate('click'); + + expect(onFilterChanged).toBeCalledWith({ status: 'closed' }); + }); + it('should call on load setFilterRefetch', () => { + mount( + <TestProviders> + <CasesTableFilters {...props} /> + </TestProviders> + ); + expect(setFilterRefetch).toHaveBeenCalled(); + }); + it('should remove tag from selected tags when tag no longer exists', () => { + const ourProps = { + ...props, + initial: { + ...DEFAULT_FILTER_OPTIONS, + tags: ['pepsi', 'rc'], + }, + }; + mount( + <TestProviders> + <CasesTableFilters {...ourProps} /> + </TestProviders> + ); + expect(onFilterChanged).toHaveBeenCalledWith({ tags: ['pepsi'] }); + }); + it('should remove reporter from selected reporters when reporter no longer exists', () => { + const ourProps = { + ...props, + initial: { + ...DEFAULT_FILTER_OPTIONS, + reporters: [ + { username: 'casetester', full_name: null, email: null }, + { username: 'batman', full_name: null, email: null }, + ], + }, + }; + mount( + <TestProviders> + <CasesTableFilters {...ourProps} /> + </TestProviders> + ); + expect(onFilterChanged).toHaveBeenCalledWith({ reporters: [{ username: 'casetester' }] }); + }); +}); diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/table_filters.tsx b/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/table_filters.tsx index a344dd7891010..901fb133753e8 100644 --- a/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/table_filters.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/table_filters.tsx @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { useCallback, useState } from 'react'; +import React, { useCallback, useEffect, useState } from 'react'; import { isEqual } from 'lodash/fp'; import { EuiFieldSearch, @@ -25,6 +25,7 @@ interface CasesTableFiltersProps { countOpenCases: number | null; onFilterChanged: (filterOptions: Partial<FilterOptions>) => void; initial: FilterOptions; + setFilterRefetch: (val: () => void) => void; } /** @@ -41,20 +42,42 @@ const CasesTableFiltersComponent = ({ countOpenCases, onFilterChanged, initial = defaultInitial, + setFilterRefetch, }: CasesTableFiltersProps) => { - const [selectedReporters, setselectedReporters] = useState( + const [selectedReporters, setSelectedReporters] = useState( initial.reporters.map(r => r.full_name ?? r.username ?? '') ); const [search, setSearch] = useState(initial.search); const [selectedTags, setSelectedTags] = useState(initial.tags); const [showOpenCases, setShowOpenCases] = useState(initial.status === 'open'); - const { tags } = useGetTags(); - const { reporters, respReporters } = useGetReporters(); + const { tags, fetchTags } = useGetTags(); + const { reporters, respReporters, fetchReporters } = useGetReporters(); + const refetch = useCallback(() => { + fetchTags(); + fetchReporters(); + }, [fetchReporters, fetchTags]); + useEffect(() => { + if (setFilterRefetch != null) { + setFilterRefetch(refetch); + } + }, [refetch, setFilterRefetch]); + useEffect(() => { + if (selectedReporters.length) { + const newReporters = selectedReporters.filter(r => reporters.includes(r)); + handleSelectedReporters(newReporters); + } + }, [reporters]); + useEffect(() => { + if (selectedTags.length) { + const newTags = selectedTags.filter(t => tags.includes(t)); + handleSelectedTags(newTags); + } + }, [tags]); const handleSelectedReporters = useCallback( newReporters => { if (!isEqual(newReporters, selectedReporters)) { - setselectedReporters(newReporters); + setSelectedReporters(newReporters); const reportersObj = respReporters.filter( r => newReporters.includes(r.username) || newReporters.includes(r.full_name) ); @@ -97,6 +120,7 @@ const CasesTableFiltersComponent = ({ <EuiFlexItem grow={true}> <EuiFieldSearch aria-label={i18n.SEARCH_CASES} + data-test-subj="search-cases" fullWidth incremental={false} placeholder={i18n.SEARCH_PLACEHOLDER} @@ -107,6 +131,7 @@ const CasesTableFiltersComponent = ({ <EuiFlexItem grow={false}> <EuiFilterGroup> <EuiFilterButton + data-test-subj="open-case-count" withNext hasActiveFilters={showOpenCases} onClick={handleToggleFilter.bind(null, true)} @@ -115,6 +140,7 @@ const CasesTableFiltersComponent = ({ {countOpenCases != null ? ` (${countOpenCases})` : ''} </EuiFilterButton> <EuiFilterButton + data-test-subj="closed-case-count" hasActiveFilters={!showOpenCases} onClick={handleToggleFilter.bind(null, false)} > diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/translations.ts b/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/translations.ts index 1bee96bc23fff..d3dcfa50ecfa5 100644 --- a/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/translations.ts +++ b/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/translations.ts @@ -46,6 +46,10 @@ export const BULK_ACTIONS = i18n.translate('xpack.siem.case.caseTable.bulkAction defaultMessage: 'Bulk actions', }); +export const SERVICENOW_INCIDENT = i18n.translate('xpack.siem.case.caseTable.snIncident', { + defaultMessage: 'ServiceNow Incident', +}); + export const SEARCH_PLACEHOLDER = i18n.translate('xpack.siem.case.caseTable.searchPlaceholder', { defaultMessage: 'e.g. case name', }); diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/callout/helpers.tsx b/x-pack/legacy/plugins/siem/public/pages/case/components/callout/helpers.tsx index 929e8640dceb6..3237104274473 100644 --- a/x-pack/legacy/plugins/siem/public/pages/case/components/callout/helpers.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/case/components/callout/helpers.tsx @@ -6,7 +6,7 @@ import * as i18n from './translations'; -export const getSavedObjectReadOnly = () => ({ +export const savedObjectReadOnly = { title: i18n.READ_ONLY_SAVED_OBJECT_TITLE, description: i18n.READ_ONLY_SAVED_OBJECT_MSG, -}); +}; diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/callout/index.test.tsx b/x-pack/legacy/plugins/siem/public/pages/case/components/callout/index.test.tsx new file mode 100644 index 0000000000000..126ea13e96af6 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/pages/case/components/callout/index.test.tsx @@ -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 React from 'react'; +import { mount } from 'enzyme'; + +import { CaseCallOut } from './'; + +const defaultProps = { + title: 'hey title', +}; + +describe('CaseCallOut ', () => { + it('Renders single message callout', () => { + const props = { + ...defaultProps, + message: 'we have one message', + }; + const wrapper = mount(<CaseCallOut {...props} />); + expect( + wrapper + .find(`[data-test-subj="callout-message"]`) + .last() + .exists() + ).toBeTruthy(); + expect( + wrapper + .find(`[data-test-subj="callout-messages"]`) + .last() + .exists() + ).toBeFalsy(); + }); + it('Renders multi message callout', () => { + const props = { + ...defaultProps, + messages: [ + { ...defaultProps, description: <p>{'we have two messages'}</p> }, + { ...defaultProps, description: <p>{'for real'}</p> }, + ], + }; + const wrapper = mount(<CaseCallOut {...props} />); + expect( + wrapper + .find(`[data-test-subj="callout-message"]`) + .last() + .exists() + ).toBeFalsy(); + expect( + wrapper + .find(`[data-test-subj="callout-messages"]`) + .last() + .exists() + ).toBeTruthy(); + }); + it('Dismisses callout', () => { + const props = { + ...defaultProps, + message: 'we have one message', + }; + const wrapper = mount(<CaseCallOut {...props} />); + expect(wrapper.find(`[data-test-subj="case-call-out"]`).exists()).toBeTruthy(); + wrapper + .find(`[data-test-subj="callout-dismiss"]`) + .last() + .simulate('click'); + expect(wrapper.find(`[data-test-subj="case-call-out"]`).exists()).toBeFalsy(); + }); +}); diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/callout/index.tsx b/x-pack/legacy/plugins/siem/public/pages/case/components/callout/index.tsx index 30a95db2d82a5..0fc93af7f318d 100644 --- a/x-pack/legacy/plugins/siem/public/pages/case/components/callout/index.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/case/components/callout/index.tsx @@ -24,10 +24,12 @@ const CaseCallOutComponent = ({ title, message, messages }: CaseCallOutProps) => return showCallOut ? ( <> - <EuiCallOut title={title} color="primary" iconType="gear"> - {!isEmpty(messages) && <EuiDescriptionList listItems={messages} />} - {!isEmpty(message) && <p>{message}</p>} - <EuiButton color="primary" onClick={handleCallOut}> + <EuiCallOut title={title} color="primary" iconType="gear" data-test-subj="case-call-out"> + {!isEmpty(messages) && ( + <EuiDescriptionList data-test-subj="callout-messages" listItems={messages} /> + )} + {!isEmpty(message) && <p data-test-subj="callout-message">{message}</p>} + <EuiButton data-test-subj="callout-dismiss" color="primary" onClick={handleCallOut}> {i18n.DISMISS_CALLOUT} </EuiButton> </EuiCallOut> diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/case_status/index.tsx b/x-pack/legacy/plugins/siem/public/pages/case/components/case_status/index.tsx index 2b16dfa150d61..718eb95767f2e 100644 --- a/x-pack/legacy/plugins/siem/public/pages/case/components/case_status/index.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/case/components/case_status/index.tsx @@ -84,7 +84,7 @@ const CaseStatusComp: React.FC<CaseStatusProps> = ({ <EuiFlexItem grow={false}> <EuiFlexGroup gutterSize="l" alignItems="center"> <EuiFlexItem> - <EuiButtonEmpty iconType="refresh" onClick={onRefresh}> + <EuiButtonEmpty data-test-subj="case-refresh" iconType="refresh" onClick={onRefresh}> {i18n.CASE_REFRESH} </EuiButtonEmpty> </EuiFlexItem> diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/case_view/__mock__/index.tsx b/x-pack/legacy/plugins/siem/public/pages/case/components/case_view/__mock__/index.tsx deleted file mode 100644 index 0e57326707e97..0000000000000 --- a/x-pack/legacy/plugins/siem/public/pages/case/components/case_view/__mock__/index.tsx +++ /dev/null @@ -1,93 +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 { CaseProps } from '../index'; -import { Case } from '../../../../../containers/case/types'; - -const updateCase = jest.fn(); -const fetchCase = jest.fn(); - -export const caseProps: CaseProps = { - caseId: '3c4ddcc0-4e99-11ea-9290-35d05cb55c15', - userCanCrud: true, - caseData: { - closedAt: null, - closedBy: null, - id: '3c4ddcc0-4e99-11ea-9290-35d05cb55c15', - comments: [ - { - comment: 'Solve this fast!', - id: 'a357c6a0-5435-11ea-b427-fb51a1fcb7b8', - createdAt: '2020-02-20T23:06:33.798Z', - createdBy: { - fullName: 'Steph Milovic', - username: 'smilovic', - email: 'notmyrealemailfool@elastic.co', - }, - pushedAt: null, - pushedBy: null, - updatedAt: '2020-02-20T23:06:33.798Z', - updatedBy: { - username: 'elastic', - }, - version: 'WzQ3LDFd', - }, - ], - createdAt: '2020-02-13T19:44:23.627Z', - createdBy: { fullName: null, email: 'testemail@elastic.co', username: 'elastic' }, - description: 'Security banana Issue', - externalService: null, - status: 'open', - tags: ['defacement'], - title: 'Another horrible breach!!', - totalComment: 1, - updatedAt: '2020-02-19T15:02:57.995Z', - updatedBy: { - username: 'elastic', - }, - version: 'WzQ3LDFd', - }, - fetchCase, - updateCase, -}; - -export const caseClosedProps: CaseProps = { - ...caseProps, - caseData: { - ...caseProps.caseData, - closedAt: '2020-02-20T23:06:33.798Z', - closedBy: { - username: 'elastic', - }, - status: 'closed', - }, -}; - -export const data: Case = { - ...caseProps.caseData, -}; - -export const dataClosed: Case = { - ...caseClosedProps.caseData, -}; - -export const caseUserActions = [ - { - actionField: ['comment'], - action: 'create', - actionAt: '2020-03-20T17:10:09.814Z', - actionBy: { - fullName: 'Steph Milovic', - username: 'smilovic', - email: 'notmyrealemailfool@elastic.co', - }, - newValue: 'Solve this fast!', - oldValue: null, - actionId: '3c4ddcc0-4e99-11ea-9290-35d05cb55c15', - caseId: '9b833a50-6acd-11ea-8fad-af86b1071bd9', - commentId: 'a357c6a0-5435-11ea-b427-fb51a1fcb7b8', - }, -]; diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/case_view/actions.test.tsx b/x-pack/legacy/plugins/siem/public/pages/case/components/case_view/actions.test.tsx index 49f5f44cba271..8b6ee76dd783d 100644 --- a/x-pack/legacy/plugins/siem/public/pages/case/components/case_view/actions.test.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/case/components/case_view/actions.test.tsx @@ -9,7 +9,7 @@ import { mount } from 'enzyme'; import { useDeleteCases } from '../../../../containers/case/use_delete_cases'; import { TestProviders } from '../../../../mock'; -import { data } from './__mock__'; +import { basicCase } from '../../../../containers/case/mock'; import { CaseViewActions } from './actions'; jest.mock('../../../../containers/case/use_delete_cases'); const useDeleteCasesMock = useDeleteCases as jest.Mock; @@ -34,7 +34,7 @@ describe('CaseView actions', () => { it('clicking trash toggles modal', () => { const wrapper = mount( <TestProviders> - <CaseViewActions caseData={data} /> + <CaseViewActions caseData={basicCase} /> </TestProviders> ); @@ -54,12 +54,14 @@ describe('CaseView actions', () => { })); const wrapper = mount( <TestProviders> - <CaseViewActions caseData={data} /> + <CaseViewActions caseData={basicCase} /> </TestProviders> ); expect(wrapper.find('[data-test-subj="confirm-delete-case-modal"]').exists()).toBeTruthy(); wrapper.find('button[data-test-subj="confirmModalConfirmButton"]').simulate('click'); - expect(handleOnDeleteConfirm.mock.calls[0][0]).toEqual([{ id: data.id, title: data.title }]); + expect(handleOnDeleteConfirm.mock.calls[0][0]).toEqual([ + { id: basicCase.id, title: basicCase.title }, + ]); }); }); diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/case_view/actions.tsx b/x-pack/legacy/plugins/siem/public/pages/case/components/case_view/actions.tsx index 0b08b866df964..216180eb2cf0a 100644 --- a/x-pack/legacy/plugins/siem/public/pages/case/components/case_view/actions.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/case/components/case_view/actions.tsx @@ -40,7 +40,6 @@ const CaseViewActionsComponent: React.FC<CaseViewActions> = ({ caseData, disable ), [isDisplayConfirmDeleteModal, caseData] ); - // TO DO refactor each of these const's into their own components const propertyActions = useMemo( () => [ { diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/case_view/index.test.tsx b/x-pack/legacy/plugins/siem/public/pages/case/components/case_view/index.test.tsx index 3f5b3a3127177..7ce9d7b8533e4 100644 --- a/x-pack/legacy/plugins/siem/public/pages/case/components/case_view/index.test.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/case/components/case_view/index.test.tsx @@ -5,56 +5,51 @@ */ import React from 'react'; -import { Router } from 'react-router-dom'; import { mount } from 'enzyme'; -/* eslint-disable @kbn/eslint/module_migration */ -import routeData from 'react-router'; -/* eslint-enable @kbn/eslint/module_migration */ -import { CaseComponent } from './'; -import { caseProps, caseClosedProps, data, dataClosed, caseUserActions } from './__mock__'; + +import { Router, routeData, mockHistory, mockLocation } from '../__mock__/router'; +import { CaseComponent, CaseProps, CaseView } from './'; +import { basicCase, basicCaseClosed, caseUserActions } from '../../../../containers/case/mock'; import { TestProviders } from '../../../../mock'; import { useUpdateCase } from '../../../../containers/case/use_update_case'; +import { useGetCase } from '../../../../containers/case/use_get_case'; import { useGetCaseUserActions } from '../../../../containers/case/use_get_case_user_actions'; import { wait } from '../../../../lib/helpers'; import { usePushToService } from '../use_push_to_service'; jest.mock('../../../../containers/case/use_update_case'); jest.mock('../../../../containers/case/use_get_case_user_actions'); +jest.mock('../../../../containers/case/use_get_case'); jest.mock('../use_push_to_service'); const useUpdateCaseMock = useUpdateCase as jest.Mock; const useGetCaseUserActionsMock = useGetCaseUserActions as jest.Mock; const usePushToServiceMock = usePushToService as jest.Mock; -type Action = 'PUSH' | 'POP' | 'REPLACE'; -const pop: Action = 'POP'; -const location = { - pathname: '/network', - search: '', - state: '', - hash: '', -}; -const mockHistory = { - length: 2, - location, - action: pop, - push: jest.fn(), - replace: jest.fn(), - go: jest.fn(), - goBack: jest.fn(), - goForward: jest.fn(), - block: jest.fn(), - createHref: jest.fn(), - listen: jest.fn(), + +export const caseProps: CaseProps = { + caseId: basicCase.id, + userCanCrud: true, + caseData: basicCase, + fetchCase: jest.fn(), + updateCase: jest.fn(), }; -const mockLocation = { - pathname: '/welcome', - hash: '', - search: '', - state: '', +export const caseClosedProps: CaseProps = { + ...caseProps, + caseData: basicCaseClosed, }; describe('CaseView ', () => { const updateCaseProperty = jest.fn(); const fetchCaseUserActions = jest.fn(); + const fetchCase = jest.fn(); + const updateCase = jest.fn(); + const data = caseProps.caseData; + const defaultGetCase = { + isLoading: false, + isError: false, + data, + updateCase, + fetchCase, + }; /* eslint-disable no-console */ // Silence until enzyme fixed to use ReactTestUtils.act() const originalError = console.error; @@ -84,17 +79,23 @@ describe('CaseView ', () => { participants: [data.createdBy], }; - const defaultUsePushToServiceMock = { - pushButton: <>{'Hello Button'}</>, - pushCallouts: null, - }; - beforeEach(() => { jest.resetAllMocks(); useUpdateCaseMock.mockImplementation(() => defaultUpdateCaseState); jest.spyOn(routeData, 'useLocation').mockReturnValue(mockLocation); useGetCaseUserActionsMock.mockImplementation(() => defaultUseGetCaseUserActions); - usePushToServiceMock.mockImplementation(() => defaultUsePushToServiceMock); + usePushToServiceMock.mockImplementation(({ updateCase: updateCaseMockCall }) => ({ + pushButton: ( + <button + data-test-subj="mock-button" + onClick={() => updateCaseMockCall(caseProps.caseData)} + type="button" + > + {'Hello Button'} + </button> + ), + pushCallouts: null, + })); }); it('should render CaseComponent', async () => { @@ -120,7 +121,7 @@ describe('CaseView ', () => { ).toEqual(data.status); expect( wrapper - .find(`[data-test-subj="case-view-tag-list"] .euiBadge__text`) + .find(`[data-test-subj="case-view-tag-list"] [data-test-subj="case-tag"]`) .first() .text() ).toEqual(data.tags[0]); @@ -139,7 +140,7 @@ describe('CaseView ', () => { ).toEqual(data.createdAt); expect( wrapper - .find(`[data-test-subj="case-view-description"]`) + .find(`[data-test-subj="description-action"] [data-test-subj="user-action-markdown"]`) .first() .prop('raw') ).toEqual(data.description); @@ -148,7 +149,7 @@ describe('CaseView ', () => { it('should show closed indicators in header when case is closed', async () => { useUpdateCaseMock.mockImplementation(() => ({ ...defaultUpdateCaseState, - caseData: dataClosed, + caseData: basicCaseClosed, })); const wrapper = mount( <TestProviders> @@ -164,13 +165,13 @@ describe('CaseView ', () => { .find(`[data-test-subj="case-view-closedAt"]`) .first() .prop('value') - ).toEqual(dataClosed.closedAt); + ).toEqual(basicCaseClosed.closedAt); expect( wrapper .find(`[data-test-subj="case-view-status"]`) .first() .text() - ).toEqual(dataClosed.status); + ).toEqual(basicCaseClosed.status); }); it('should dispatch update state when button is toggled', async () => { @@ -188,7 +189,12 @@ describe('CaseView ', () => { expect(updateCaseProperty).toHaveBeenCalled(); }); - it('should render comments', async () => { + it('should display EditableTitle isLoading', () => { + useUpdateCaseMock.mockImplementation(() => ({ + ...defaultUpdateCaseState, + isLoading: true, + updateKey: 'title', + })); const wrapper = mount( <TestProviders> <Router history={mockHistory}> @@ -196,32 +202,230 @@ describe('CaseView ', () => { </Router> </TestProviders> ); - await wait(); expect( wrapper - .find( - `div[data-test-subj="user-action-${data.comments[0].id}-avatar"] [data-test-subj="user-action-avatar"]` - ) + .find('[data-test-subj="editable-title-loading"]') + .first() + .exists() + ).toBeTruthy(); + expect( + wrapper + .find('[data-test-subj="editable-title-edit-icon"]') .first() - .prop('name') - ).toEqual(data.comments[0].createdBy.fullName); + .exists() + ).toBeFalsy(); + }); + it('should display Toggle Status isLoading', () => { + useUpdateCaseMock.mockImplementation(() => ({ + ...defaultUpdateCaseState, + isLoading: true, + updateKey: 'status', + })); + const wrapper = mount( + <TestProviders> + <Router history={mockHistory}> + <CaseComponent {...caseProps} /> + </Router> + </TestProviders> + ); expect( wrapper - .find( - `div[data-test-subj="user-action-${data.comments[0].id}"] [data-test-subj="user-action-title"] strong` - ) + .find('[data-test-subj="toggle-case-status"]') .first() - .text() - ).toEqual(data.comments[0].createdBy.username); + .prop('isLoading') + ).toBeTruthy(); + }); + + it('should display description isLoading', () => { + useUpdateCaseMock.mockImplementation(() => ({ + ...defaultUpdateCaseState, + isLoading: true, + updateKey: 'description', + })); + const wrapper = mount( + <TestProviders> + <Router history={mockHistory}> + <CaseComponent {...caseProps} /> + </Router> + </TestProviders> + ); + expect( + wrapper + .find('[data-test-subj="description-action"] [data-test-subj="user-action-title-loading"]') + .first() + .exists() + ).toBeTruthy(); + expect( + wrapper + .find('[data-test-subj="description-action"] [data-test-subj="property-actions"]') + .first() + .exists() + ).toBeFalsy(); + }); + it('should display tags isLoading', () => { + useUpdateCaseMock.mockImplementation(() => ({ + ...defaultUpdateCaseState, + isLoading: true, + updateKey: 'tags', + })); + const wrapper = mount( + <TestProviders> + <Router history={mockHistory}> + <CaseComponent {...caseProps} /> + </Router> + </TestProviders> + ); expect( wrapper - .find( - `div[data-test-subj="user-action-${data.comments[0].id}"] [data-test-subj="markdown"]` - ) + .find('[data-test-subj="case-view-tag-list"] [data-test-subj="tag-list-loading"]') .first() - .prop('source') - ).toEqual(data.comments[0].comment); + .exists() + ).toBeTruthy(); + expect( + wrapper + .find('[data-test-subj="tag-list-edit"]') + .first() + .exists() + ).toBeFalsy(); + }); + + it('should update title', () => { + const wrapper = mount( + <TestProviders> + <Router history={mockHistory}> + <CaseComponent {...caseProps} /> + </Router> + </TestProviders> + ); + const newTitle = 'The new title'; + wrapper + .find(`[data-test-subj="editable-title-edit-icon"]`) + .first() + .simulate('click'); + wrapper.update(); + wrapper + .find(`[data-test-subj="editable-title-input-field"]`) + .last() + .simulate('change', { target: { value: newTitle } }); + + wrapper.update(); + wrapper + .find(`[data-test-subj="editable-title-submit-btn"]`) + .first() + .simulate('click'); + + wrapper.update(); + const updateObject = updateCaseProperty.mock.calls[0][0]; + expect(updateObject.updateKey).toEqual('title'); + expect(updateObject.updateValue).toEqual(newTitle); + }); + + it('should push updates on button click', async () => { + useGetCaseUserActionsMock.mockImplementation(() => ({ + ...defaultUseGetCaseUserActions, + hasDataToPush: true, + })); + const wrapper = mount( + <TestProviders> + <Router history={mockHistory}> + <CaseComponent {...{ ...caseProps, updateCase }} /> + </Router> + </TestProviders> + ); + expect( + wrapper + .find('[data-test-subj="has-data-to-push-button"]') + .first() + .exists() + ).toBeTruthy(); + wrapper + .find('[data-test-subj="mock-button"]') + .first() + .simulate('click'); + wrapper.update(); + await wait(); + expect(updateCase).toBeCalledWith(caseProps.caseData); + expect(fetchCaseUserActions).toBeCalledWith(caseProps.caseData.id); + }); + + it('should return null if error', () => { + (useGetCase as jest.Mock).mockImplementation(() => ({ + ...defaultGetCase, + isError: true, + })); + const wrapper = mount( + <TestProviders> + <Router history={mockHistory}> + <CaseView + {...{ + caseId: '1234', + userCanCrud: true, + }} + /> + </Router> + </TestProviders> + ); + expect(wrapper).toEqual({}); + }); + + it('should return spinner if loading', () => { + (useGetCase as jest.Mock).mockImplementation(() => ({ + ...defaultGetCase, + isLoading: true, + })); + const wrapper = mount( + <TestProviders> + <Router history={mockHistory}> + <CaseView + {...{ + caseId: '1234', + userCanCrud: true, + }} + /> + </Router> + </TestProviders> + ); + expect(wrapper.find('[data-test-subj="case-view-loading"]').exists()).toBeTruthy(); + }); + + it('should return case view when data is there', () => { + (useGetCase as jest.Mock).mockImplementation(() => defaultGetCase); + const wrapper = mount( + <TestProviders> + <Router history={mockHistory}> + <CaseView + {...{ + caseId: '1234', + userCanCrud: true, + }} + /> + </Router> + </TestProviders> + ); + expect(wrapper.find('[data-test-subj="case-view-title"]').exists()).toBeTruthy(); + }); + + it('should refresh data on refresh', () => { + (useGetCase as jest.Mock).mockImplementation(() => defaultGetCase); + const wrapper = mount( + <TestProviders> + <Router history={mockHistory}> + <CaseView + {...{ + caseId: '1234', + userCanCrud: true, + }} + /> + </Router> + </TestProviders> + ); + wrapper + .find('[data-test-subj="case-refresh"]') + .first() + .simulate('click'); + expect(fetchCaseUserActions).toBeCalledWith(caseProps.caseData.id); + expect(fetchCase).toBeCalled(); }); }); diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/case_view/index.tsx b/x-pack/legacy/plugins/siem/public/pages/case/components/case_view/index.tsx index 947da51365d66..3cf0405f40637 100644 --- a/x-pack/legacy/plugins/siem/public/pages/case/components/case_view/index.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/case/components/case_view/index.tsx @@ -271,7 +271,11 @@ export const CaseComponent = React.memo<CaseProps>( onChange={toggleStatusCase} /> </EuiFlexItem> - {hasDataToPush && <EuiFlexItem grow={false}>{pushButton}</EuiFlexItem>} + {hasDataToPush && ( + <EuiFlexItem data-test-subj="has-data-to-push-button" grow={false}> + {pushButton} + </EuiFlexItem> + )} </EuiFlexGroup> </> )} @@ -316,7 +320,7 @@ export const CaseView = React.memo(({ caseId, userCanCrud }: Props) => { return ( <MyEuiFlexGroup justifyContent="center" alignItems="center"> <EuiFlexItem grow={false}> - <EuiLoadingSpinner size="xl" /> + <EuiLoadingSpinner data-test-subj="case-view-loading" size="xl" /> </EuiFlexItem> </MyEuiFlexGroup> ); diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/case_view/translations.ts b/x-pack/legacy/plugins/siem/public/pages/case/components/case_view/translations.ts index 17132b9610754..70b8035db5c16 100644 --- a/x-pack/legacy/plugins/siem/public/pages/case/components/case_view/translations.ts +++ b/x-pack/legacy/plugins/siem/public/pages/case/components/case_view/translations.ts @@ -118,3 +118,6 @@ export const EMAIL_BODY = (caseUrl: string) => values: { caseUrl }, defaultMessage: 'Case reference: {caseUrl}', }); +export const UNKNOWN = i18n.translate('xpack.siem.case.caseView.unknown', { + defaultMessage: 'Unknown', +}); diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/create/index.test.tsx b/x-pack/legacy/plugins/siem/public/pages/case/components/create/index.test.tsx new file mode 100644 index 0000000000000..d480744fc932a --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/pages/case/components/create/index.test.tsx @@ -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 React from 'react'; +import { mount } from 'enzyme'; + +import { Create } from './'; +import { TestProviders } from '../../../../mock'; +import { getFormMock } from '../__mock__/form'; +import { Router, routeData, mockHistory, mockLocation } from '../__mock__/router'; + +import { useInsertTimeline } from '../../../../components/timeline/insert_timeline_popover/use_insert_timeline'; +import { usePostCase } from '../../../../containers/case/use_post_case'; +jest.mock('../../../../components/timeline/insert_timeline_popover/use_insert_timeline'); +jest.mock('../../../../containers/case/use_post_case'); +import { useForm } from '../../../../../../../../../src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks'; +import { wait } from '../../../../lib/helpers'; +import { SiemPageName } from '../../../home/types'; +jest.mock( + '../../../../../../../../../src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/use_form' +); + +export const useFormMock = useForm as jest.Mock; + +const useInsertTimelineMock = useInsertTimeline as jest.Mock; +const usePostCaseMock = usePostCase as jest.Mock; + +const postCase = jest.fn(); +const handleCursorChange = jest.fn(); +const handleOnTimelineChange = jest.fn(); + +const defaultInsertTimeline = { + cursorPosition: { + start: 0, + end: 0, + }, + handleCursorChange, + handleOnTimelineChange, +}; +const sampleData = { + description: 'what a great description', + tags: ['coke', 'pepsi'], + title: 'what a cool title', +}; +const defaultPostCase = { + isLoading: false, + isError: false, + caseData: null, + postCase, +}; +describe('Create case', () => { + const formHookMock = getFormMock(sampleData); + + beforeEach(() => { + jest.resetAllMocks(); + useInsertTimelineMock.mockImplementation(() => defaultInsertTimeline); + usePostCaseMock.mockImplementation(() => defaultPostCase); + useFormMock.mockImplementation(() => ({ form: formHookMock })); + jest.spyOn(routeData, 'useLocation').mockReturnValue(mockLocation); + }); + + it('should post case on submit click', async () => { + const wrapper = mount( + <TestProviders> + <Router history={mockHistory}> + <Create /> + </Router> + </TestProviders> + ); + wrapper + .find(`[data-test-subj="create-case-submit"]`) + .first() + .simulate('click'); + await wait(); + expect(postCase).toBeCalledWith(sampleData); + }); + + it('should redirect to all cases on cancel click', () => { + const wrapper = mount( + <TestProviders> + <Router history={mockHistory}> + <Create /> + </Router> + </TestProviders> + ); + wrapper + .find(`[data-test-subj="create-case-cancel"]`) + .first() + .simulate('click'); + expect(mockHistory.replace.mock.calls[0][0].pathname).toEqual(`/${SiemPageName.case}`); + }); + it('should redirect to new case when caseData is there', () => { + const sampleId = '777777'; + usePostCaseMock.mockImplementation(() => ({ ...defaultPostCase, caseData: { id: sampleId } })); + mount( + <TestProviders> + <Router history={mockHistory}> + <Create /> + </Router> + </TestProviders> + ); + expect(mockHistory.replace.mock.calls[0][0].pathname).toEqual( + `/${SiemPageName.case}/${sampleId}` + ); + }); + + it('should render spinner when loading', () => { + usePostCaseMock.mockImplementation(() => ({ ...defaultPostCase, isLoading: true })); + const wrapper = mount( + <TestProviders> + <Router history={mockHistory}> + <Create /> + </Router> + </TestProviders> + ); + expect(wrapper.find(`[data-test-subj="create-case-loading-spinner"]`).exists()).toBeTruthy(); + }); +}); diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/create/index.tsx b/x-pack/legacy/plugins/siem/public/pages/case/components/create/index.tsx index 740909db408ec..53b792bb9b5eb 100644 --- a/x-pack/legacy/plugins/siem/public/pages/case/components/create/index.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/case/components/create/index.tsx @@ -73,7 +73,7 @@ export const Create = React.memo(() => { const handleSetIsCancel = useCallback(() => { setIsCancel(true); - }, [isCancel]); + }, []); if (caseData != null && caseData.id) { return <Redirect to={`/${SiemPageName.case}/${caseData.id}`} />; @@ -85,7 +85,7 @@ export const Create = React.memo(() => { return ( <EuiPanel> - {isLoading && <MySpinner size="xl" />} + {isLoading && <MySpinner data-test-subj="create-case-loading-spinner" size="xl" />} <Form form={form}> <CommonUseField path="title" @@ -107,7 +107,7 @@ export const Create = React.memo(() => { euiFieldProps: { fullWidth: true, placeholder: '', - isDisabled: isLoading, + disabled: isLoading, }, }} /> @@ -151,6 +151,7 @@ export const Create = React.memo(() => { </EuiFlexItem> <EuiFlexItem grow={false}> <EuiButton + data-test-subj="create-case-submit" fill iconType="plusInCircle" isDisabled={isLoading} diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/tag_list/index.test.tsx b/x-pack/legacy/plugins/siem/public/pages/case/components/tag_list/index.test.tsx new file mode 100644 index 0000000000000..8ad2f8f8cb737 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/pages/case/components/tag_list/index.test.tsx @@ -0,0 +1,138 @@ +/* + * 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 { TagList } from './'; +import { getFormMock } from '../__mock__/form'; +import { TestProviders } from '../../../../mock'; +import { wait } from '../../../../lib/helpers'; +import { useForm } from '../../../../../../../../../src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks'; +import { act } from 'react-dom/test-utils'; + +jest.mock( + '../../../../../../../../../src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/use_form' +); +const onSubmit = jest.fn(); +const defaultProps = { + disabled: false, + isLoading: false, + onSubmit, + tags: [], +}; + +describe('TagList ', () => { + const sampleTags = ['coke', 'pepsi']; + const formHookMock = getFormMock({ tags: sampleTags }); + beforeEach(() => { + jest.resetAllMocks(); + (useForm as jest.Mock).mockImplementation(() => ({ form: formHookMock })); + }); + it('Renders no tags, and then edit', () => { + const wrapper = mount( + <TestProviders> + <TagList {...defaultProps} /> + </TestProviders> + ); + expect( + wrapper + .find(`[data-test-subj="no-tags"]`) + .last() + .exists() + ).toBeTruthy(); + wrapper + .find(`[data-test-subj="tag-list-edit-button"]`) + .last() + .simulate('click'); + expect( + wrapper + .find(`[data-test-subj="no-tags"]`) + .last() + .exists() + ).toBeFalsy(); + expect( + wrapper + .find(`[data-test-subj="edit-tags"]`) + .last() + .exists() + ).toBeTruthy(); + }); + it('Edit tag on submit', async () => { + const wrapper = mount( + <TestProviders> + <TagList {...defaultProps} /> + </TestProviders> + ); + wrapper + .find(`[data-test-subj="tag-list-edit-button"]`) + .last() + .simulate('click'); + await act(async () => { + wrapper + .find(`[data-test-subj="edit-tags-submit"]`) + .last() + .simulate('click'); + await wait(); + expect(onSubmit).toBeCalledWith(sampleTags); + }); + }); + it('Cancels on cancel', async () => { + const props = { + ...defaultProps, + tags: ['pepsi'], + }; + const wrapper = mount( + <TestProviders> + <TagList {...props} /> + </TestProviders> + ); + expect( + wrapper + .find(`[data-test-subj="case-tag"]`) + .last() + .exists() + ).toBeTruthy(); + wrapper + .find(`[data-test-subj="tag-list-edit-button"]`) + .last() + .simulate('click'); + await act(async () => { + expect( + wrapper + .find(`[data-test-subj="case-tag"]`) + .last() + .exists() + ).toBeFalsy(); + wrapper + .find(`[data-test-subj="edit-tags-cancel"]`) + .last() + .simulate('click'); + await wait(); + wrapper.update(); + expect( + wrapper + .find(`[data-test-subj="case-tag"]`) + .last() + .exists() + ).toBeTruthy(); + }); + }); + it('Renders disabled button', () => { + const props = { ...defaultProps, disabled: true }; + const wrapper = mount( + <TestProviders> + <TagList {...props} /> + </TestProviders> + ); + expect( + wrapper + .find(`[data-test-subj="tag-list-edit-button"]`) + .last() + .prop('disabled') + ).toBeTruthy(); + }); +}); diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/tag_list/index.tsx b/x-pack/legacy/plugins/siem/public/pages/case/components/tag_list/index.tsx index f7d890ca60b16..9bac000b93235 100644 --- a/x-pack/legacy/plugins/siem/public/pages/case/components/tag_list/index.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/case/components/tag_list/index.tsx @@ -61,10 +61,11 @@ export const TagList = React.memo( <EuiFlexItem grow={false}> <h4>{i18n.TAGS}</h4> </EuiFlexItem> - {isLoading && <EuiLoadingSpinner />} + {isLoading && <EuiLoadingSpinner data-test-subj="tag-list-loading" />} {!isLoading && ( - <EuiFlexItem grow={false}> + <EuiFlexItem data-test-subj="tag-list-edit" grow={false}> <EuiButtonIcon + data-test-subj="tag-list-edit-button" isDisabled={disabled} aria-label={i18n.EDIT_TAGS_ARIA} iconType={'pencil'} @@ -74,17 +75,19 @@ export const TagList = React.memo( )} </EuiFlexGroup> <EuiHorizontalRule margin="xs" /> - <MyFlexGroup gutterSize="xs"> - {tags.length === 0 && !isEditTags && <p>{i18n.NO_TAGS}</p>} + <MyFlexGroup gutterSize="xs" data-test-subj="grr"> + {tags.length === 0 && !isEditTags && <p data-test-subj="no-tags">{i18n.NO_TAGS}</p>} {tags.length > 0 && !isEditTags && tags.map((tag, key) => ( <EuiFlexItem grow={false} key={`${tag}${key}`}> - <EuiBadge color="hollow">{tag}</EuiBadge> + <EuiBadge data-test-subj="case-tag" color="hollow"> + {tag} + </EuiBadge> </EuiFlexItem> ))} {isEditTags && ( - <EuiFlexGroup direction="column"> + <EuiFlexGroup data-test-subj="edit-tags" direction="column"> <EuiFlexItem> <Form form={form}> <CommonUseField @@ -105,6 +108,7 @@ export const TagList = React.memo( <EuiFlexItem grow={false}> <EuiButton color="secondary" + data-test-subj="edit-tags-submit" fill iconType="save" onClick={onSubmitTags} @@ -115,6 +119,7 @@ export const TagList = React.memo( </EuiFlexItem> <EuiFlexItem grow={false}> <EuiButtonEmpty + data-test-subj="edit-tags-cancel" iconType="cross" onClick={setIsEditTags.bind(null, false)} size="s" diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/use_push_to_service/index.test.tsx b/x-pack/legacy/plugins/siem/public/pages/case/components/use_push_to_service/index.test.tsx new file mode 100644 index 0000000000000..77215e2318ded --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/pages/case/components/use_push_to_service/index.test.tsx @@ -0,0 +1,192 @@ +/* + * 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. + */ +/* eslint-disable react/display-name */ +import React from 'react'; +import { renderHook, act } from '@testing-library/react-hooks'; +import { usePushToService, ReturnUsePushToService, UsePushToService } from './'; +import { TestProviders } from '../../../../mock'; +import { usePostPushToService } from '../../../../containers/case/use_post_push_to_service'; +import { ClosureType } from '../../../../../../../../plugins/case/common/api/cases'; +import * as i18n from './translations'; +import { useGetActionLicense } from '../../../../containers/case/use_get_action_license'; +import { getKibanaConfigError, getLicenseError } from './helpers'; +import * as api from '../../../../containers/case/configure/api'; +jest.mock('../../../../containers/case/use_get_action_license'); +jest.mock('../../../../containers/case/use_post_push_to_service'); +jest.mock('../../../../containers/case/configure/api'); + +describe('usePushToService', () => { + const caseId = '12345'; + const updateCase = jest.fn(); + const postPushToService = jest.fn(); + const mockPostPush = { + isLoading: false, + postPushToService, + }; + const closureType: ClosureType = 'close-by-user'; + const mockConnector = { + connectorId: 'c00l', + connectorName: 'name', + }; + const mockCaseConfigure = { + ...mockConnector, + createdAt: 'string', + createdBy: {}, + closureType, + updatedAt: 'string', + updatedBy: {}, + version: 'string', + }; + const getConfigureMock = jest.spyOn(api, 'getCaseConfigure'); + const actionLicense = { + id: '.servicenow', + name: 'ServiceNow', + minimumLicenseRequired: 'platinum', + enabled: true, + enabledInConfig: true, + enabledInLicense: true, + }; + beforeEach(() => { + jest.resetAllMocks(); + (usePostPushToService as jest.Mock).mockImplementation(() => mockPostPush); + (useGetActionLicense as jest.Mock).mockImplementation(() => ({ + isLoading: false, + actionLicense, + })); + getConfigureMock.mockImplementation(() => Promise.resolve(mockCaseConfigure)); + }); + it('push case button posts the push with correct args', async () => { + await act(async () => { + const { result, waitForNextUpdate } = renderHook<UsePushToService, ReturnUsePushToService>( + () => + usePushToService({ + caseId, + caseStatus: 'open', + isNew: false, + updateCase, + userCanCrud: true, + }), + { + wrapper: ({ children }) => <TestProviders> {children}</TestProviders>, + } + ); + await waitForNextUpdate(); + await waitForNextUpdate(); + expect(getConfigureMock).toBeCalled(); + result.current.pushButton.props.children.props.onClick(); + expect(postPushToService).toBeCalledWith({ ...mockConnector, caseId, updateCase }); + expect(result.current.pushCallouts).toBeNull(); + }); + }); + it('Displays message when user does not have premium license', async () => { + (useGetActionLicense as jest.Mock).mockImplementation(() => ({ + isLoading: false, + actionLicense: { + ...actionLicense, + enabledInLicense: false, + }, + })); + await act(async () => { + const { result, waitForNextUpdate } = renderHook<UsePushToService, ReturnUsePushToService>( + () => + usePushToService({ + caseId, + caseStatus: 'open', + isNew: false, + updateCase, + userCanCrud: true, + }), + { + wrapper: ({ children }) => <TestProviders> {children}</TestProviders>, + } + ); + await waitForNextUpdate(); + await waitForNextUpdate(); + const errorsMsg = result.current.pushCallouts?.props.messages; + expect(errorsMsg).toHaveLength(1); + expect(errorsMsg[0].title).toEqual(getLicenseError().title); + }); + }); + it('Displays message when user does not have case enabled in config', async () => { + (useGetActionLicense as jest.Mock).mockImplementation(() => ({ + isLoading: false, + actionLicense: { + ...actionLicense, + enabledInConfig: false, + }, + })); + await act(async () => { + const { result, waitForNextUpdate } = renderHook<UsePushToService, ReturnUsePushToService>( + () => + usePushToService({ + caseId, + caseStatus: 'open', + isNew: false, + updateCase, + userCanCrud: true, + }), + { + wrapper: ({ children }) => <TestProviders> {children}</TestProviders>, + } + ); + await waitForNextUpdate(); + await waitForNextUpdate(); + const errorsMsg = result.current.pushCallouts?.props.messages; + expect(errorsMsg).toHaveLength(1); + expect(errorsMsg[0].title).toEqual(getKibanaConfigError().title); + }); + }); + it('Displays message when user does not have a connector configured', async () => { + getConfigureMock.mockImplementation(() => + Promise.resolve({ + ...mockCaseConfigure, + connectorId: 'none', + }) + ); + await act(async () => { + const { result, waitForNextUpdate } = renderHook<UsePushToService, ReturnUsePushToService>( + () => + usePushToService({ + caseId, + caseStatus: 'open', + isNew: false, + updateCase, + userCanCrud: true, + }), + { + wrapper: ({ children }) => <TestProviders> {children}</TestProviders>, + } + ); + await waitForNextUpdate(); + await waitForNextUpdate(); + const errorsMsg = result.current.pushCallouts?.props.messages; + expect(errorsMsg).toHaveLength(1); + expect(errorsMsg[0].title).toEqual(i18n.PUSH_DISABLE_BY_NO_CASE_CONFIG_TITLE); + }); + }); + it('Displays message when case is closed', async () => { + await act(async () => { + const { result, waitForNextUpdate } = renderHook<UsePushToService, ReturnUsePushToService>( + () => + usePushToService({ + caseId, + caseStatus: 'closed', + isNew: false, + updateCase, + userCanCrud: true, + }), + { + wrapper: ({ children }) => <TestProviders> {children}</TestProviders>, + } + ); + await waitForNextUpdate(); + await waitForNextUpdate(); + const errorsMsg = result.current.pushCallouts?.props.messages; + expect(errorsMsg).toHaveLength(1); + expect(errorsMsg[0].title).toEqual(i18n.PUSH_DISABLE_BECAUSE_CASE_CLOSED_TITLE); + }); + }); +}); diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/use_push_to_service/index.tsx b/x-pack/legacy/plugins/siem/public/pages/case/components/use_push_to_service/index.tsx index 4f370ec978906..5092cba6872e3 100644 --- a/x-pack/legacy/plugins/siem/public/pages/case/components/use_push_to_service/index.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/case/components/use_push_to_service/index.tsx @@ -19,7 +19,7 @@ import { CaseCallOut } from '../callout'; import { getLicenseError, getKibanaConfigError } from './helpers'; import * as i18n from './translations'; -interface UsePushToService { +export interface UsePushToService { caseId: string; caseStatus: string; isNew: boolean; @@ -32,7 +32,7 @@ interface Connector { connectorName: string; } -interface ReturnUsePushToService { +export interface ReturnUsePushToService { pushButton: JSX.Element; pushCallouts: JSX.Element | null; } @@ -122,6 +122,7 @@ export const usePushToService = ({ const pushToServiceButton = useMemo( () => ( <EuiButton + data-test-subj="push-to-service-now" fill iconType="importAction" onClick={handlePushToService} diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/user_action_tree/helpers.test.tsx b/x-pack/legacy/plugins/siem/public/pages/case/components/user_action_tree/helpers.test.tsx new file mode 100644 index 0000000000000..e34981286bc81 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/pages/case/components/user_action_tree/helpers.test.tsx @@ -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 React from 'react'; +import { getUserAction } from '../../../../containers/case/mock'; +import { getLabelTitle } from './helpers'; +import * as i18n from '../case_view/translations'; +import { mount } from 'enzyme'; + +describe('User action tree helpers', () => { + it('label title generated for update tags', () => { + const action = getUserAction(['title'], 'update'); + const result: string | JSX.Element = getLabelTitle({ + action, + field: 'tags', + firstIndexPushToService: 0, + index: 0, + }); + + const wrapper = mount(<>{result}</>); + expect( + wrapper + .find(`[data-test-subj="ua-tags-label"]`) + .first() + .text() + ).toEqual(` ${i18n.TAGS.toLowerCase()}`); + + expect( + wrapper + .find(`[data-test-subj="ua-tag"]`) + .first() + .text() + ).toEqual(action.newValue); + }); + it('label title generated for update title', () => { + const action = getUserAction(['title'], 'update'); + const result: string | JSX.Element = getLabelTitle({ + action, + field: 'title', + firstIndexPushToService: 0, + index: 0, + }); + + expect(result).toEqual( + `${i18n.CHANGED_FIELD.toLowerCase()} ${i18n.CASE_NAME.toLowerCase()} ${i18n.TO} "${ + action.newValue + }"` + ); + }); + it('label title generated for update description', () => { + const action = getUserAction(['description'], 'update'); + const result: string | JSX.Element = getLabelTitle({ + action, + field: 'description', + firstIndexPushToService: 0, + index: 0, + }); + + expect(result).toEqual(`${i18n.EDITED_FIELD} ${i18n.DESCRIPTION.toLowerCase()}`); + }); + it('label title generated for update status to open', () => { + const action = { ...getUserAction(['status'], 'update'), newValue: 'open' }; + const result: string | JSX.Element = getLabelTitle({ + action, + field: 'status', + firstIndexPushToService: 0, + index: 0, + }); + + expect(result).toEqual(`${i18n.REOPENED_CASE.toLowerCase()} ${i18n.CASE}`); + }); + it('label title generated for update status to closed', () => { + const action = { ...getUserAction(['status'], 'update'), newValue: 'closed' }; + const result: string | JSX.Element = getLabelTitle({ + action, + field: 'status', + firstIndexPushToService: 0, + index: 0, + }); + + expect(result).toEqual(`${i18n.CLOSED_CASE.toLowerCase()} ${i18n.CASE}`); + }); + it('label title generated for update comment', () => { + const action = getUserAction(['comment'], 'update'); + const result: string | JSX.Element = getLabelTitle({ + action, + field: 'comment', + firstIndexPushToService: 0, + index: 0, + }); + + expect(result).toEqual(`${i18n.EDITED_FIELD} ${i18n.COMMENT.toLowerCase()}`); + }); + it('label title generated for pushed incident', () => { + const action = getUserAction(['pushed'], 'push-to-service'); + const result: string | JSX.Element = getLabelTitle({ + action, + field: 'pushed', + firstIndexPushToService: 0, + index: 0, + }); + + const wrapper = mount(<>{result}</>); + expect( + wrapper + .find(`[data-test-subj="pushed-label"]`) + .first() + .text() + ).toEqual(i18n.PUSHED_NEW_INCIDENT); + expect( + wrapper + .find(`[data-test-subj="pushed-value"]`) + .first() + .prop('href') + ).toEqual(JSON.parse(action.newValue).external_url); + }); + it('label title generated for needs update incident', () => { + const action = getUserAction(['pushed'], 'push-to-service'); + const result: string | JSX.Element = getLabelTitle({ + action, + field: 'pushed', + firstIndexPushToService: 0, + index: 1, + }); + + const wrapper = mount(<>{result}</>); + expect( + wrapper + .find(`[data-test-subj="pushed-label"]`) + .first() + .text() + ).toEqual(i18n.UPDATE_INCIDENT); + expect( + wrapper + .find(`[data-test-subj="pushed-value"]`) + .first() + .prop('href') + ).toEqual(JSON.parse(action.newValue).external_url); + }); +}); diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/user_action_tree/helpers.tsx b/x-pack/legacy/plugins/siem/public/pages/case/components/user_action_tree/helpers.tsx index 008f4d7048f56..d6016e540bdc0 100644 --- a/x-pack/legacy/plugins/siem/public/pages/case/components/user_action_tree/helpers.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/case/components/user_action_tree/helpers.tsx @@ -41,14 +41,16 @@ export const getLabelTitle = ({ action, field, firstIndexPushToService, index }: const getTagsLabelTitle = (action: CaseUserActions) => ( <EuiFlexGroup alignItems="baseline" gutterSize="xs" component="span"> - <EuiFlexItem> + <EuiFlexItem data-test-subj="ua-tags-label"> {action.action === 'add' && i18n.ADDED_FIELD} {action.action === 'delete' && i18n.REMOVED_FIELD} {i18n.TAGS.toLowerCase()} </EuiFlexItem> {action.newValue != null && action.newValue.split(',').map(tag => ( <EuiFlexItem grow={false} key={tag}> - <EuiBadge color="default">{tag}</EuiBadge> + <EuiBadge data-test-subj={`ua-tag`} color="default"> + {tag} + </EuiBadge> </EuiFlexItem> ))} </EuiFlexGroup> @@ -61,12 +63,12 @@ const getPushedServiceLabelTitle = ( ) => { const pushedVal = JSON.parse(action.newValue ?? '') as CaseFullExternalService; return ( - <EuiFlexGroup alignItems="baseline" gutterSize="xs"> - <EuiFlexItem> + <EuiFlexGroup alignItems="baseline" gutterSize="xs" data-test-subj="pushed-service-label-title"> + <EuiFlexItem data-test-subj="pushed-label"> {firstIndexPushToService === index ? i18n.PUSHED_NEW_INCIDENT : i18n.UPDATE_INCIDENT} </EuiFlexItem> <EuiFlexItem grow={false}> - <EuiLink href={pushedVal?.external_url} target="_blank"> + <EuiLink data-test-subj="pushed-value" href={pushedVal?.external_url} target="_blank"> {pushedVal?.connector_name} {pushedVal?.external_title} </EuiLink> </EuiFlexItem> diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/user_action_tree/index.test.tsx b/x-pack/legacy/plugins/siem/public/pages/case/components/user_action_tree/index.test.tsx new file mode 100644 index 0000000000000..1c71260422d4b --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/pages/case/components/user_action_tree/index.test.tsx @@ -0,0 +1,331 @@ +/* + * 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 { Router, routeData, mockHistory, mockLocation } from '../__mock__/router'; +import { getFormMock } from '../__mock__/form'; +import { useUpdateComment } from '../../../../containers/case/use_update_comment'; +import { basicCase, getUserAction } from '../../../../containers/case/mock'; +import { UserActionTree } from './'; +import { TestProviders } from '../../../../mock'; +import { useFormMock } from '../create/index.test'; +import { wait } from '../../../../lib/helpers'; +import { act } from 'react-dom/test-utils'; +jest.mock( + '../../../../../../../../../src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/use_form' +); + +const fetchUserActions = jest.fn(); +const onUpdateField = jest.fn(); +const updateCase = jest.fn(); +const defaultProps = { + data: basicCase, + caseUserActions: [], + firstIndexPushToService: -1, + isLoadingDescription: false, + isLoadingUserActions: false, + lastIndexPushToService: -1, + userCanCrud: true, + fetchUserActions, + onUpdateField, + updateCase, +}; +const useUpdateCommentMock = useUpdateComment as jest.Mock; +jest.mock('../../../../containers/case/use_update_comment'); + +const patchComment = jest.fn(); +describe('UserActionTree ', () => { + const sampleData = { + content: 'what a great comment update', + }; + beforeEach(() => { + jest.clearAllMocks(); + jest.resetAllMocks(); + useUpdateCommentMock.mockImplementation(() => ({ + isLoadingIds: [], + patchComment, + })); + const formHookMock = getFormMock(sampleData); + useFormMock.mockImplementation(() => ({ form: formHookMock })); + jest.spyOn(routeData, 'useLocation').mockReturnValue(mockLocation); + }); + + it('Loading spinner when user actions loading and displays fullName/username', () => { + const wrapper = mount( + <TestProviders> + <Router history={mockHistory}> + <UserActionTree {...{ ...defaultProps, isLoadingUserActions: true }} /> + </Router> + </TestProviders> + ); + expect(wrapper.find(`[data-test-subj="user-actions-loading"]`).exists()).toBeTruthy(); + + expect( + wrapper + .find(`[data-test-subj="user-action-avatar"]`) + .first() + .prop('name') + ).toEqual(defaultProps.data.createdBy.fullName); + expect( + wrapper + .find(`[data-test-subj="user-action-title"] strong`) + .first() + .text() + ).toEqual(defaultProps.data.createdBy.username); + }); + it('Renders service now update line with top and bottom when push is required', () => { + const ourActions = [ + getUserAction(['comment'], 'push-to-service'), + getUserAction(['comment'], 'update'), + ]; + const props = { + ...defaultProps, + caseUserActions: ourActions, + lastIndexPushToService: 0, + }; + const wrapper = mount( + <TestProviders> + <Router history={mockHistory}> + <UserActionTree {...props} /> + </Router> + </TestProviders> + ); + expect(wrapper.find(`[data-test-subj="show-top-footer"]`).exists()).toBeTruthy(); + expect(wrapper.find(`[data-test-subj="show-bottom-footer"]`).exists()).toBeTruthy(); + }); + it('Renders service now update line with top only when push is up to date', () => { + const ourActions = [getUserAction(['comment'], 'push-to-service')]; + const props = { + ...defaultProps, + caseUserActions: ourActions, + lastIndexPushToService: 0, + }; + const wrapper = mount( + <TestProviders> + <Router history={mockHistory}> + <UserActionTree {...props} /> + </Router> + </TestProviders> + ); + expect(wrapper.find(`[data-test-subj="show-top-footer"]`).exists()).toBeTruthy(); + expect(wrapper.find(`[data-test-subj="show-bottom-footer"]`).exists()).toBeFalsy(); + }); + + it('Outlines comment when update move to link is clicked', () => { + const ourActions = [getUserAction(['comment'], 'create'), getUserAction(['comment'], 'update')]; + const props = { + ...defaultProps, + caseUserActions: ourActions, + }; + const wrapper = mount( + <TestProviders> + <Router history={mockHistory}> + <UserActionTree {...props} /> + </Router> + </TestProviders> + ); + expect( + wrapper + .find(`[data-test-subj="comment-create-action"]`) + .first() + .prop('idToOutline') + ).toEqual(''); + wrapper + .find(`[data-test-subj="comment-update-action"] [data-test-subj="move-to-link"]`) + .first() + .simulate('click'); + expect( + wrapper + .find(`[data-test-subj="comment-create-action"]`) + .first() + .prop('idToOutline') + ).toEqual(ourActions[0].commentId); + }); + + it('Switches to markdown when edit is clicked and back to panel when canceled', () => { + const ourActions = [getUserAction(['comment'], 'create')]; + const props = { + ...defaultProps, + caseUserActions: ourActions, + }; + const wrapper = mount( + <TestProviders> + <Router history={mockHistory}> + <UserActionTree {...props} /> + </Router> + </TestProviders> + ); + expect( + wrapper + .find( + `[data-test-subj="user-action-${props.data.comments[0].id}"] [data-test-subj="user-action-markdown-form"]` + ) + .exists() + ).toEqual(false); + wrapper + .find(`[data-test-subj="comment-create-action"] [data-test-subj="property-actions-ellipses"]`) + .first() + .simulate('click'); + wrapper + .find(`[data-test-subj="comment-create-action"] [data-test-subj="property-actions-pencil"]`) + .first() + .simulate('click'); + expect( + wrapper + .find( + `[data-test-subj="user-action-${props.data.comments[0].id}"] [data-test-subj="user-action-markdown-form"]` + ) + .exists() + ).toEqual(true); + wrapper + .find( + `[data-test-subj="user-action-${props.data.comments[0].id}"] [data-test-subj="user-action-cancel-markdown"]` + ) + .first() + .simulate('click'); + expect( + wrapper + .find( + `[data-test-subj="user-action-${props.data.comments[0].id}"] [data-test-subj="user-action-markdown-form"]` + ) + .exists() + ).toEqual(false); + }); + + it('calls update comment when comment markdown is saved', async () => { + const ourActions = [getUserAction(['comment'], 'create')]; + const props = { + ...defaultProps, + caseUserActions: ourActions, + }; + const wrapper = mount( + <TestProviders> + <Router history={mockHistory}> + <UserActionTree {...props} /> + </Router> + </TestProviders> + ); + wrapper + .find(`[data-test-subj="comment-create-action"] [data-test-subj="property-actions-ellipses"]`) + .first() + .simulate('click'); + wrapper + .find(`[data-test-subj="comment-create-action"] [data-test-subj="property-actions-pencil"]`) + .first() + .simulate('click'); + wrapper + .find( + `[data-test-subj="user-action-${props.data.comments[0].id}"] [data-test-subj="user-action-save-markdown"]` + ) + .first() + .simulate('click'); + await act(async () => { + await wait(); + wrapper.update(); + expect( + wrapper + .find( + `[data-test-subj="user-action-${props.data.comments[0].id}"] [data-test-subj="user-action-markdown-form"]` + ) + .exists() + ).toEqual(false); + expect(patchComment).toBeCalledWith({ + commentUpdate: sampleData.content, + caseId: props.data.id, + commentId: props.data.comments[0].id, + fetchUserActions, + updateCase, + version: props.data.comments[0].version, + }); + }); + }); + + it('calls update description when description markdown is saved', async () => { + const props = defaultProps; + const wrapper = mount( + <TestProviders> + <Router history={mockHistory}> + <UserActionTree {...props} /> + </Router> + </TestProviders> + ); + wrapper + .find(`[data-test-subj="description-action"] [data-test-subj="property-actions-ellipses"]`) + .first() + .simulate('click'); + wrapper + .find(`[data-test-subj="description-action"] [data-test-subj="property-actions-pencil"]`) + .first() + .simulate('click'); + wrapper + .find( + `[data-test-subj="user-action-description"] [data-test-subj="user-action-save-markdown"]` + ) + .first() + .simulate('click'); + await act(async () => { + await wait(); + expect( + wrapper + .find( + `[data-test-subj="user-action-${props.data.id}"] [data-test-subj="user-action-markdown-form"]` + ) + .exists() + ).toEqual(false); + expect(onUpdateField).toBeCalledWith('description', sampleData.content); + }); + }); + + it('quotes', async () => { + const commentData = { + comment: '', + }; + const formHookMock = getFormMock(commentData); + const setFieldValue = jest.fn(); + useFormMock.mockImplementation(() => ({ form: { ...formHookMock, setFieldValue } })); + const props = defaultProps; + const wrapper = mount( + <TestProviders> + <Router history={mockHistory}> + <UserActionTree {...props} /> + </Router> + </TestProviders> + ); + wrapper + .find(`[data-test-subj="description-action"] [data-test-subj="property-actions-ellipses"]`) + .first() + .simulate('click'); + wrapper + .find(`[data-test-subj="description-action"] [data-test-subj="property-actions-quote"]`) + .first() + .simulate('click'); + expect(setFieldValue).toBeCalledWith('comment', `> ${props.data.description} \n`); + }); + it('Outlines comment when url param is provided', () => { + const commentId = 'neat-comment-id'; + const ourActions = [getUserAction(['comment'], 'create')]; + const props = { + ...defaultProps, + caseUserActions: ourActions, + }; + jest.spyOn(routeData, 'useParams').mockReturnValue({ commentId }); + const wrapper = mount( + <TestProviders> + <Router history={mockHistory}> + <UserActionTree {...props} /> + </Router> + </TestProviders> + ); + expect( + wrapper + .find(`[data-test-subj="comment-create-action"]`) + .first() + .prop('idToOutline') + ).toEqual(commentId); + }); +}); diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/user_action_tree/index.tsx b/x-pack/legacy/plugins/siem/public/pages/case/components/user_action_tree/index.tsx index 0892d5dcb3ee7..d1e8eb3f6306b 100644 --- a/x-pack/legacy/plugins/siem/public/pages/case/components/user_action_tree/index.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/case/components/user_action_tree/index.tsx @@ -60,7 +60,6 @@ export const UserActionTree = React.memo( const currentUser = useCurrentUser(); const [manageMarkdownEditIds, setManangeMardownEditIds] = useState<string[]>([]); const [insertQuote, setInsertQuote] = useState<string | null>(null); - const handleManageMarkdownEditId = useCallback( (id: string) => { if (!manageMarkdownEditIds.includes(id)) { @@ -74,7 +73,6 @@ export const UserActionTree = React.memo( const handleSaveComment = useCallback( ({ id, version }: { id: string; version: string }, content: string) => { - handleManageMarkdownEditId(id); patchComment({ caseId: caseData.id, commentId: id, @@ -135,7 +133,6 @@ export const UserActionTree = React.memo( content={caseData.description} isEditable={manageMarkdownEditIds.includes(DESCRIPTION_ID)} onSaveContent={(content: string) => { - handleManageMarkdownEditId(DESCRIPTION_ID); onUpdateField(DESCRIPTION_ID, content); }} onChangeEditable={handleManageMarkdownEditId} @@ -166,11 +163,11 @@ export const UserActionTree = React.memo( } } }, [commentId, initLoading, isLoadingUserActions, isLoadingIds]); - return ( <> <UserActionItem createdAt={caseData.createdAt} + data-test-subj="description-action" disabled={!userCanCrud} id={DESCRIPTION_ID} isEditable={manageMarkdownEditIds.includes(DESCRIPTION_ID)} @@ -182,7 +179,7 @@ export const UserActionTree = React.memo( markdown={MarkdownDescription} onEdit={handleManageMarkdownEditId.bind(null, DESCRIPTION_ID)} onQuote={handleManageQuote.bind(null, caseData.description)} - username={caseData.createdBy.username ?? 'Unknown'} + username={caseData.createdBy.username ?? i18n.UNKNOWN} /> {caseUserActions.map((action, index) => { @@ -193,6 +190,7 @@ export const UserActionTree = React.memo( <UserActionItem key={action.actionId} createdAt={comment.createdAt} + data-test-subj={`comment-create-action`} disabled={!userCanCrud} id={comment.id} idToOutline={selectedOutlineCommentId} @@ -236,6 +234,7 @@ export const UserActionTree = React.memo( <UserActionItem key={action.actionId} createdAt={action.actionAt} + data-test-subj={`${action.actionField[0]}-${action.action}-action`} disabled={!userCanCrud} id={action.actionId} isEditable={false} @@ -263,11 +262,12 @@ export const UserActionTree = React.memo( {(isLoadingUserActions || isLoadingIds.includes(NEW_ID)) && ( <MyEuiFlexGroup justifyContent="center" alignItems="center"> <EuiFlexItem grow={false}> - <EuiLoadingSpinner size="l" /> + <EuiLoadingSpinner data-test-subj="user-actions-loading" size="l" /> </EuiFlexItem> </MyEuiFlexGroup> )} <UserActionItem + data-test-subj={`add-comment`} createdAt={new Date().toISOString()} disabled={!userCanCrud} id={NEW_ID} diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/user_action_tree/user_action_item.tsx b/x-pack/legacy/plugins/siem/public/pages/case/components/user_action_tree/user_action_item.tsx index bcb4edd6129a6..0acd0623f9413 100644 --- a/x-pack/legacy/plugins/siem/public/pages/case/components/user_action_tree/user_action_item.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/case/components/user_action_tree/user_action_item.tsx @@ -21,6 +21,7 @@ import * as i18n from './translations'; interface UserActionItemProps { createdAt: string; + 'data-test-subj'?: string; disabled: boolean; id: string; isEditable: boolean; @@ -112,6 +113,7 @@ const PushedInfoContainer = styled.div` export const UserActionItem = ({ createdAt, disabled, + 'data-test-subj': dataTestSubj, id, idToOutline, isEditable, @@ -130,7 +132,7 @@ export const UserActionItem = ({ username, updatedAt, }: UserActionItemProps) => ( - <UserActionItemContainer gutterSize={'none'} direction="column"> + <UserActionItemContainer data-test-subj={dataTestSubj} gutterSize={'none'} direction="column"> <EuiFlexItem> <EuiFlexGroup gutterSize={'none'}> <EuiFlexItem data-test-subj={`user-action-${id}-avatar`} grow={false}> @@ -145,24 +147,25 @@ export const UserActionItem = ({ {!isEditable && ( <MyEuiPanel className="userAction__panel" + data-test-subj={`user-action-panel`} paddingSize="none" showoutline={id === idToOutline ? 'true' : 'false'} > <UserActionTitle createdAt={createdAt} disabled={disabled} + fullName={fullName} id={id} isLoading={isLoading} labelEditAction={labelEditAction} labelQuoteAction={labelQuoteAction} labelTitle={labelTitle ?? <></>} linkId={linkId} - fullName={fullName} - username={username} - updatedAt={updatedAt} onEdit={onEdit} onQuote={onQuote} outlineComment={outlineComment} + updatedAt={updatedAt} + username={username} /> {markdown} </MyEuiPanel> @@ -171,7 +174,7 @@ export const UserActionItem = ({ </EuiFlexGroup> </EuiFlexItem> {showTopFooter && ( - <PushedContainer> + <PushedContainer data-test-subj="show-top-footer"> <PushedInfoContainer> <EuiText size="xs" color="subdued"> {i18n.ALREADY_PUSHED_TO_SERVICE} @@ -179,7 +182,7 @@ export const UserActionItem = ({ </PushedInfoContainer> <EuiHorizontalRule /> {showBottomFooter && ( - <PushedInfoContainer> + <PushedInfoContainer data-test-subj="show-bottom-footer"> <EuiText size="xs" color="subdued"> {i18n.REQUIRED_UPDATE_TO_SERVICE} </EuiText> diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/user_action_tree/user_action_markdown.tsx b/x-pack/legacy/plugins/siem/public/pages/case/components/user_action_tree/user_action_markdown.tsx index e8503bf43375c..827fe2df120ab 100644 --- a/x-pack/legacy/plugins/siem/public/pages/case/components/user_action_tree/user_action_markdown.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/case/components/user_action_tree/user_action_markdown.tsx @@ -62,12 +62,24 @@ export const UserActionMarkdown = ({ return ( <EuiFlexGroup gutterSize="s" alignItems="center"> <EuiFlexItem grow={false}> - <EuiButtonEmpty size="s" onClick={cancelAction} iconType="cross"> + <EuiButtonEmpty + data-test-subj="user-action-cancel-markdown" + size="s" + onClick={cancelAction} + iconType="cross" + > {i18n.CANCEL} </EuiButtonEmpty> </EuiFlexItem> <EuiFlexItem grow={false}> - <EuiButton color="secondary" fill iconType="save" onClick={saveAction} size="s"> + <EuiButton + data-test-subj="user-action-save-markdown" + color="secondary" + fill + iconType="save" + onClick={saveAction} + size="s" + > {i18n.SAVE} </EuiButton> </EuiFlexItem> @@ -77,7 +89,7 @@ export const UserActionMarkdown = ({ [handleCancelAction, handleSaveAction] ); return isEditable ? ( - <Form form={form}> + <Form form={form} data-test-subj="user-action-markdown-form"> <UseField path="content" component={MarkdownEditorForm} @@ -99,7 +111,7 @@ export const UserActionMarkdown = ({ </Form> ) : ( <ContentWrapper> - <Markdown raw={content} data-test-subj="case-view-description" /> + <Markdown raw={content} data-test-subj="user-action-markdown" /> </ContentWrapper> ); }; diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/user_action_tree/user_action_title.test.tsx b/x-pack/legacy/plugins/siem/public/pages/case/components/user_action_tree/user_action_title.test.tsx new file mode 100644 index 0000000000000..8a1e8a80f664d --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/pages/case/components/user_action_tree/user_action_title.test.tsx @@ -0,0 +1,57 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { mount } from 'enzyme'; +import copy from 'copy-to-clipboard'; +import { Router, routeData, mockHistory } from '../__mock__/router'; +import { caseUserActions as basicUserActions } from '../../../../containers/case/mock'; +import { UserActionTitle } from './user_action_title'; +import { TestProviders } from '../../../../mock'; + +const outlineComment = jest.fn(); +const onEdit = jest.fn(); +const onQuote = jest.fn(); + +jest.mock('copy-to-clipboard'); +const defaultProps = { + createdAt: basicUserActions[0].actionAt, + disabled: false, + fullName: basicUserActions[0].actionBy.fullName, + id: basicUserActions[0].actionId, + isLoading: false, + labelEditAction: 'labelEditAction', + labelQuoteAction: 'labelQuoteAction', + labelTitle: <>{'cool'}</>, + linkId: basicUserActions[0].commentId, + onEdit, + onQuote, + outlineComment, + updatedAt: basicUserActions[0].actionAt, + username: basicUserActions[0].actionBy.username, +}; + +describe('UserActionTitle ', () => { + beforeEach(() => { + jest.resetAllMocks(); + jest.spyOn(routeData, 'useParams').mockReturnValue({ commentId: '123' }); + }); + + it('Calls copy when copy link is clicked', async () => { + const wrapper = mount( + <TestProviders> + <Router history={mockHistory}> + <UserActionTitle {...defaultProps} /> + </Router> + </TestProviders> + ); + wrapper + .find(`[data-test-subj="copy-link"]`) + .first() + .simulate('click'); + expect(copy).toBeCalledTimes(1); + }); +}); diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/user_action_tree/user_action_title.tsx b/x-pack/legacy/plugins/siem/public/pages/case/components/user_action_tree/user_action_title.tsx index 9ccf921c87602..fc2a74466dedc 100644 --- a/x-pack/legacy/plugins/siem/public/pages/case/components/user_action_tree/user_action_title.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/case/components/user_action_tree/user_action_title.tsx @@ -43,7 +43,7 @@ interface UserActionTitleProps { linkId?: string | null; fullName?: string | null; updatedAt?: string | null; - username: string; + username?: string | null; onEdit?: (id: string) => void; onQuote?: (id: string) => void; outlineComment?: (id: string) => void; @@ -52,18 +52,18 @@ interface UserActionTitleProps { export const UserActionTitle = ({ createdAt, disabled, + fullName, id, isLoading, labelEditAction, labelQuoteAction, labelTitle, linkId, - fullName, - username, - updatedAt, onEdit, onQuote, outlineComment, + updatedAt, + username = i18n.UNKNOWN, }: UserActionTitleProps) => { const { detailName: caseId } = useParams(); const urlSearch = useGetUrlSearch(navTabs.case); @@ -94,10 +94,7 @@ export const UserActionTitle = ({ const handleAnchorLink = useCallback(() => { copy( - `${window.location.origin}${window.location.pathname}#${SiemPageName.case}/${caseId}/${id}${urlSearch}`, - { - debug: true, - } + `${window.location.origin}${window.location.pathname}#${SiemPageName.case}/${caseId}/${id}${urlSearch}` ); }, [caseId, id, urlSearch]); @@ -106,7 +103,6 @@ export const UserActionTitle = ({ outlineComment(linkId); } }, [linkId, outlineComment]); - return ( <EuiText size="s" className="userAction__title" data-test-subj={`user-action-title`}> <EuiFlexGroup @@ -155,6 +151,7 @@ export const UserActionTitle = ({ <EuiToolTip position="top" content={<p>{i18n.MOVE_TO_ORIGINAL_COMMENT}</p>}> <EuiButtonIcon aria-label={i18n.MOVE_TO_ORIGINAL_COMMENT} + data-test-subj={`move-to-link`} onClick={handleMoveToLink} iconType="arrowUp" /> @@ -165,6 +162,7 @@ export const UserActionTitle = ({ <EuiToolTip position="top" content={<p>{i18n.COPY_REFERENCE_LINK}</p>}> <EuiButtonIcon aria-label={i18n.COPY_REFERENCE_LINK} + data-test-subj={`copy-link`} onClick={handleAnchorLink} iconType="link" id={`${id}-permLink`} @@ -173,7 +171,7 @@ export const UserActionTitle = ({ </EuiFlexItem> {propertyActions.length > 0 && ( <EuiFlexItem grow={false}> - {isLoading && <MySpinner />} + {isLoading && <MySpinner data-test-subj="user-action-title-loading" />} {!isLoading && <PropertyActions propertyActions={propertyActions} />} </EuiFlexItem> )} diff --git a/x-pack/legacy/plugins/siem/public/pages/case/translations.ts b/x-pack/legacy/plugins/siem/public/pages/case/translations.ts index 0d1e6d1435ca3..097b8220156e2 100644 --- a/x-pack/legacy/plugins/siem/public/pages/case/translations.ts +++ b/x-pack/legacy/plugins/siem/public/pages/case/translations.ts @@ -131,6 +131,10 @@ export const TAGS = i18n.translate('xpack.siem.case.caseView.tags', { defaultMessage: 'Tags', }); +export const ACTIONS = i18n.translate('xpack.siem.case.allCases.actions', { + defaultMessage: 'Actions', +}); + export const NO_TAGS_AVAILABLE = i18n.translate('xpack.siem.case.allCases.noTagsAvailable', { defaultMessage: 'No tags available', }); diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/activity_monitor/index.test.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/activity_monitor/index.test.tsx new file mode 100644 index 0000000000000..c5a4057b64ea7 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/activity_monitor/index.test.tsx @@ -0,0 +1,18 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { shallow } from 'enzyme'; + +import { ActivityMonitor } from './index'; + +describe('activity_monitor', () => { + it('renders correctly', () => { + const wrapper = shallow(<ActivityMonitor />); + + expect(wrapper.find('[title="Activity monitor"]')).toBeTruthy(); + }); +}); diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/detection_engine_header_page/index.test.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/detection_engine_header_page/index.test.tsx new file mode 100644 index 0000000000000..a2685017f86d6 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/detection_engine_header_page/index.test.tsx @@ -0,0 +1,18 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { shallow } from 'enzyme'; + +import { DetectionEngineHeaderPage } from './index'; + +describe('detection_engine_header_page', () => { + it('renders correctly', () => { + const wrapper = shallow(<DetectionEngineHeaderPage title="Title" />); + + expect(wrapper.find('[title="Title"]')).toBeTruthy(); + }); +}); diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/no_api_integration_callout/index.test.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/no_api_integration_callout/index.test.tsx new file mode 100644 index 0000000000000..0e2589150e858 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/no_api_integration_callout/index.test.tsx @@ -0,0 +1,18 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { shallow } from 'enzyme'; + +import { NoApiIntegrationKeyCallOut } from './index'; + +describe('no_api_integration_callout', () => { + it('renders correctly', () => { + const wrapper = shallow(<NoApiIntegrationKeyCallOut />); + + expect(wrapper.find('EuiCallOut')).toBeTruthy(); + }); +}); diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/no_write_signals_callout/index.test.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/no_write_signals_callout/index.test.tsx new file mode 100644 index 0000000000000..2e6890e60fc61 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/no_write_signals_callout/index.test.tsx @@ -0,0 +1,18 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { shallow } from 'enzyme'; + +import { NoWriteSignalsCallOut } from './index'; + +describe('no_write_signals_callout', () => { + it('renders correctly', () => { + const wrapper = shallow(<NoWriteSignalsCallOut />); + + expect(wrapper.find('EuiCallOut')).toBeTruthy(); + }); +}); diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals/index.test.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals/index.test.tsx new file mode 100644 index 0000000000000..b66a9fc881045 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals/index.test.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 from 'react'; +import { shallow } from 'enzyme'; + +import { SignalsTableComponent } from './index'; + +describe('SignalsTableComponent', () => { + it('renders correctly', () => { + const wrapper = shallow( + <SignalsTableComponent + canUserCRUD + hasIndexWrite + from={0} + loading + signalsIndex="index" + to={1} + globalQuery={{ + query: 'query', + language: 'language', + }} + globalFilters={[]} + deletedEventIds={[]} + loadingEventIds={[]} + selectedEventIds={{}} + isSelectAllChecked={false} + clearSelected={jest.fn()} + setEventsLoading={jest.fn()} + clearEventsLoading={jest.fn()} + setEventsDeleted={jest.fn()} + clearEventsDeleted={jest.fn()} + updateTimelineIsLoading={jest.fn()} + updateTimeline={jest.fn()} + /> + ); + + expect(wrapper.find('[title="Signals"]')).toBeTruthy(); + }); +}); diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals/index.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals/index.tsx index 6cdb2f326901e..ce8ae2054b2c7 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals/index.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals/index.tsx @@ -61,7 +61,7 @@ interface OwnProps { type SignalsTableComponentProps = OwnProps & PropsFromRedux; -const SignalsTableComponent: React.FC<SignalsTableComponentProps> = ({ +export const SignalsTableComponent: React.FC<SignalsTableComponentProps> = ({ canUserCRUD, clearEventsDeleted, clearEventsLoading, diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals/signals_filter_group/index.test.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals/signals_filter_group/index.test.tsx new file mode 100644 index 0000000000000..dd30bb1b0a74d --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals/signals_filter_group/index.test.tsx @@ -0,0 +1,18 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { shallow } from 'enzyme'; + +import { SignalsTableFilterGroup } from './index'; + +describe('SignalsTableFilterGroup', () => { + it('renders correctly', () => { + const wrapper = shallow(<SignalsTableFilterGroup onFilterGroupChanged={jest.fn()} />); + + expect(wrapper.find('EuiFilterButton')).toBeTruthy(); + }); +}); diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals/signals_utility_bar/batch_actions.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals/signals_utility_bar/batch_actions.tsx deleted file mode 100644 index bb45ff68cb01d..0000000000000 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals/signals_utility_bar/batch_actions.tsx +++ /dev/null @@ -1,92 +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 { EuiContextMenuItem } from '@elastic/eui'; -import React from 'react'; -import * as i18n from './translations'; -import { TimelineNonEcsData } from '../../../../../graphql/types'; -import { SendSignalsToTimeline, UpdateSignalsStatus } from '../types'; -import { FILTER_CLOSED, FILTER_OPEN } from '../signals_filter_group'; - -interface GetBatchItems { - areEventsLoading: boolean; - allEventsSelected: boolean; - selectedEventIds: Readonly<Record<string, TimelineNonEcsData[]>>; - updateSignalsStatus: UpdateSignalsStatus; - sendSignalsToTimeline: SendSignalsToTimeline; - closePopover: () => void; - isFilteredToOpen: boolean; -} -/** - * Returns ViewInTimeline / UpdateSignalStatus actions to be display within an EuiContextMenuPanel - * - * @param areEventsLoading are any events loading - * @param allEventsSelected are all events on all pages selected - * @param selectedEventIds - * @param updateSignalsStatus function for updating signal status - * @param sendSignalsToTimeline function for sending signals to timeline - * @param closePopover - * @param isFilteredToOpen currently selected filter options - */ -export const getBatchItems = ({ - areEventsLoading, - allEventsSelected, - selectedEventIds, - updateSignalsStatus, - sendSignalsToTimeline, - closePopover, - isFilteredToOpen, -}: GetBatchItems) => { - const allDisabled = areEventsLoading || Object.keys(selectedEventIds).length === 0; - const sendToTimelineDisabled = allEventsSelected || uniqueRuleCount(selectedEventIds) > 1; - const filterString = isFilteredToOpen - ? i18n.BATCH_ACTION_CLOSE_SELECTED - : i18n.BATCH_ACTION_OPEN_SELECTED; - - return [ - <EuiContextMenuItem - key={i18n.BATCH_ACTION_VIEW_SELECTED_IN_TIMELINE} - icon="editorUnorderedList" - disabled={allDisabled || sendToTimelineDisabled} - onClick={async () => { - closePopover(); - sendSignalsToTimeline(); - }} - > - {i18n.BATCH_ACTION_VIEW_SELECTED_IN_TIMELINE} - </EuiContextMenuItem>, - - <EuiContextMenuItem - key={filterString} - icon={isFilteredToOpen ? 'securitySignalResolved' : 'securitySignalDetected'} - disabled={allDisabled} - onClick={async () => { - closePopover(); - await updateSignalsStatus({ - signalIds: Object.keys(selectedEventIds), - status: isFilteredToOpen ? FILTER_CLOSED : FILTER_OPEN, - }); - }} - > - {filterString} - </EuiContextMenuItem>, - ]; -}; - -/** - * Returns the number of unique rules for a given list of signals - * - * @param signals - */ -export const uniqueRuleCount = ( - signals: Readonly<Record<string, TimelineNonEcsData[]>> -): number => { - const ruleIds = Object.values(signals).flatMap( - data => data.find(d => d.field === 'signal.rule.id')?.value - ); - - return Array.from(new Set(ruleIds)).length; -}; diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals/signals_utility_bar/index.test.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals/signals_utility_bar/index.test.tsx new file mode 100644 index 0000000000000..6cab43b5285b5 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals/signals_utility_bar/index.test.tsx @@ -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 React from 'react'; +import { shallow } from 'enzyme'; + +import { SignalsUtilityBar } from './index'; + +jest.mock('../../../../../lib/kibana'); + +describe('SignalsUtilityBar', () => { + it('renders correctly', () => { + const wrapper = shallow( + <SignalsUtilityBar + canUserCRUD={true} + hasIndexWrite={true} + areEventsLoading={false} + clearSelection={jest.fn()} + totalCount={100} + selectedEventIds={{}} + isFilteredToOpen={false} + selectAll={jest.fn()} + showClearSelection={true} + updateSignalsStatus={jest.fn()} + /> + ); + + expect(wrapper.find('[dataTestSubj="openCloseSignal"]')).toBeTruthy(); + }); +}); diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals/signals_utility_bar/index.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals/signals_utility_bar/index.tsx index 2000a699ab18d..847fcc7860085 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals/signals_utility_bar/index.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals/signals_utility_bar/index.tsx @@ -7,6 +7,8 @@ import { isEmpty } from 'lodash/fp'; import React, { useCallback } from 'react'; import numeral from '@elastic/numeral'; + +import { DEFAULT_NUMBER_FORMAT } from '../../../../../../../../../plugins/siem/common/constants'; import { UtilityBar, UtilityBarAction, @@ -16,7 +18,6 @@ import { } from '../../../../../components/utility_bar'; import * as i18n from './translations'; import { useUiSetting$ } from '../../../../../lib/kibana'; -import { DEFAULT_NUMBER_FORMAT } from '../../../../../../common/constants'; import { TimelineNonEcsData } from '../../../../../graphql/types'; import { UpdateSignalsStatus } from '../types'; import { FILTER_CLOSED, FILTER_OPEN } from '../signals_filter_group'; diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals_histogram_panel/helpers.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals_histogram_panel/helpers.tsx index 27ee552146092..90bdd39e4a6fa 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals_histogram_panel/helpers.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals_histogram_panel/helpers.tsx @@ -4,6 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ +import { showAllOthersBucket } from '../../../../../../../../plugins/siem/common/constants'; import { HistogramData, SignalsAggregation, SignalsBucket, SignalsGroupBucket } from './types'; import { SignalSearchResponse } from '../../../../containers/detection_engine/signals/types'; import * as i18n from './translations'; @@ -34,48 +35,56 @@ export const getSignalsHistogramQuery = ( additionalFilters: Array<{ bool: { filter: unknown[]; should: unknown[]; must_not: unknown[]; must: unknown[] }; }> -) => ({ - aggs: { - signalsByGrouping: { - terms: { - field: stackByField, +) => { + const missing = showAllOthersBucket.includes(stackByField) + ? { missing: stackByField.endsWith('.ip') ? '0.0.0.0' : i18n.ALL_OTHERS, - order: { - _count: 'desc', + } + : {}; + + return { + aggs: { + signalsByGrouping: { + terms: { + field: stackByField, + ...missing, + order: { + _count: 'desc', + }, + size: 10, }, - size: 10, - }, - aggs: { - signals: { - date_histogram: { - field: '@timestamp', - fixed_interval: `${Math.floor((to - from) / 32)}ms`, - min_doc_count: 0, - extended_bounds: { - min: from, - max: to, + aggs: { + signals: { + date_histogram: { + field: '@timestamp', + fixed_interval: `${Math.floor((to - from) / 32)}ms`, + min_doc_count: 0, + extended_bounds: { + min: from, + max: to, + }, }, }, }, }, }, - }, - query: { - bool: { - filter: [ - ...additionalFilters, - { - range: { - '@timestamp': { - gte: from, - lte: to, + query: { + bool: { + filter: [ + ...additionalFilters, + { + range: { + '@timestamp': { + gte: from, + lte: to, + }, }, }, - }, - ], + ], + }, }, - }, -}); + }; +}; /** * Returns `true` when the signals histogram initial loading spinner should be shown diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals_histogram_panel/index.test.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals_histogram_panel/index.test.tsx new file mode 100644 index 0000000000000..6921c49d8a8b4 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals_histogram_panel/index.test.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 { shallow } from 'enzyme'; + +import { SignalsHistogramPanel } from './index'; + +jest.mock('../../../../lib/kibana'); +jest.mock('../../../../components/navigation/use_get_url_search'); + +describe('SignalsHistogramPanel', () => { + it('renders correctly', () => { + const wrapper = shallow( + <SignalsHistogramPanel + from={0} + signalIndexName="signalIndexName" + setQuery={jest.fn()} + to={1} + updateDateRange={jest.fn()} + /> + ); + + expect(wrapper.find('[id="detections-histogram"]')).toBeTruthy(); + }); +}); diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals_histogram_panel/index.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals_histogram_panel/index.tsx index e25442b31da4e..e70ba804ec018 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals_histogram_panel/index.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals_histogram_panel/index.tsx @@ -9,16 +9,20 @@ import numeral from '@elastic/numeral'; import React, { memo, useCallback, useMemo, useState, useEffect } from 'react'; import styled from 'styled-components'; import { isEmpty } from 'lodash/fp'; +import uuid from 'uuid'; +import { DEFAULT_NUMBER_FORMAT } from '../../../../../../../../plugins/siem/common/constants'; +import { LegendItem } from '../../../../components/charts/draggable_legend_item'; +import { escapeDataProviderId } from '../../../../components/drag_and_drop/helpers'; import { HeaderSection } from '../../../../components/header_section'; - import { Filter, esQuery, Query } from '../../../../../../../../../src/plugins/data/public'; -import { DEFAULT_NUMBER_FORMAT } from '../../../../../common/constants'; import { useQuerySignals } from '../../../../containers/detection_engine/signals/use_query'; import { getDetectionEngineUrl } from '../../../../components/link_to'; +import { defaultLegendColors } from '../../../../components/matrix_histogram/utils'; import { InspectButtonContainer } from '../../../../components/inspect'; import { useGetUrlSearch } from '../../../../components/navigation/use_get_url_search'; import { MatrixLoader } from '../../../../components/matrix_histogram/matrix_loader'; +import { MatrixHistogramOption } from '../../../../components/matrix_histogram/types'; import { useKibana, useUiSetting$ } from '../../../../lib/kibana'; import { navTabs } from '../../../home/home_navigations'; import { signalsHistogramOptions } from './config'; @@ -53,6 +57,9 @@ interface SignalsHistogramPanelProps { deleteQuery?: ({ id }: { id: string }) => void; filters?: Filter[]; from: number; + headerChildren?: React.ReactNode; + /** Override all defaults, and only display this field */ + onlyField?: string; query?: Query; legendPosition?: Position; panelHeight?: number; @@ -66,12 +73,21 @@ interface SignalsHistogramPanelProps { updateDateRange: (min: number, max: number) => void; } +const getHistogramOption = (fieldName: string): MatrixHistogramOption => ({ + text: fieldName, + value: fieldName, +}); + +const NO_LEGEND_DATA: LegendItem[] = []; + export const SignalsHistogramPanel = memo<SignalsHistogramPanelProps>( ({ chartHeight, defaultStackByOption = signalsHistogramOptions[0], deleteQuery, filters, + headerChildren, + onlyField, query, from, legendPosition = 'right', @@ -85,11 +101,13 @@ export const SignalsHistogramPanel = memo<SignalsHistogramPanelProps>( title = i18n.HISTOGRAM_HEADER, updateDateRange, }) => { + // create a unique, but stable (across re-renders) query id + const uniqueQueryId = useMemo(() => `${DETECTIONS_HISTOGRAM_ID}-${uuid.v4()}`, []); const [isInitialLoading, setIsInitialLoading] = useState(true); const [defaultNumberFormat] = useUiSetting$<string>(DEFAULT_NUMBER_FORMAT); const [totalSignalsObj, setTotalSignalsObj] = useState<SignalsTotal>(defaultTotalSignalsObj); const [selectedStackByOption, setSelectedStackByOption] = useState<SignalsHistogramOption>( - defaultStackByOption + onlyField == null ? defaultStackByOption : getHistogramOption(onlyField) ); const { loading: isLoadingSignals, @@ -123,6 +141,21 @@ export const SignalsHistogramPanel = memo<SignalsHistogramPanelProps>( const formattedSignalsData = useMemo(() => formatSignalsData(signalsData), [signalsData]); + const legendItems: LegendItem[] = useMemo( + () => + signalsData?.aggregations?.signalsByGrouping?.buckets != null + ? signalsData.aggregations.signalsByGrouping.buckets.map((bucket, i) => ({ + color: i < defaultLegendColors.length ? defaultLegendColors[i] : undefined, + dataProviderId: escapeDataProviderId( + `draggable-legend-item-${uuid.v4()}-${selectedStackByOption.value}-${bucket.key}` + ), + field: selectedStackByOption.value, + value: bucket.key, + })) + : NO_LEGEND_DATA, + [signalsData, selectedStackByOption.value] + ); + useEffect(() => { let canceled = false; @@ -138,7 +171,7 @@ export const SignalsHistogramPanel = memo<SignalsHistogramPanelProps>( useEffect(() => { return () => { if (deleteQuery) { - deleteQuery({ id: DETECTIONS_HISTOGRAM_ID }); + deleteQuery({ id: uniqueQueryId }); } }; }, []); @@ -146,7 +179,7 @@ export const SignalsHistogramPanel = memo<SignalsHistogramPanelProps>( useEffect(() => { if (refetch != null && setQuery != null) { setQuery({ - id: DETECTIONS_HISTOGRAM_ID, + id: uniqueQueryId, inspect: { dsl: [request], response: [response], @@ -197,46 +230,49 @@ export const SignalsHistogramPanel = memo<SignalsHistogramPanelProps>( } }, [showLinkToSignals, urlSearch]); + const titleText = useMemo(() => (onlyField == null ? title : i18n.TOP(onlyField)), [ + onlyField, + title, + ]); + return ( - <InspectButtonContainer show={!isInitialLoading}> + <InspectButtonContainer data-test-subj="signals-histogram-panel" show={!isInitialLoading}> <StyledEuiPanel height={panelHeight}> + <HeaderSection + id={uniqueQueryId} + title={titleText} + titleSize={onlyField == null ? 'm' : 's'} + subtitle={!isInitialLoading && showTotalSignalsCount && totalSignals} + > + <EuiFlexGroup alignItems="center" gutterSize="none"> + <EuiFlexItem grow={false}> + {stackByOptions && ( + <EuiSelect + onChange={setSelectedOptionCallback} + options={stackByOptions} + prepend={i18n.STACK_BY_LABEL} + value={selectedStackByOption.value} + /> + )} + {headerChildren != null && headerChildren} + </EuiFlexItem> + {linkButton} + </EuiFlexGroup> + </HeaderSection> + {isInitialLoading ? ( - <> - <HeaderSection id={DETECTIONS_HISTOGRAM_ID} title={title} /> - <MatrixLoader /> - </> + <MatrixLoader /> ) : ( - <> - <HeaderSection - id={DETECTIONS_HISTOGRAM_ID} - title={title} - subtitle={showTotalSignalsCount && totalSignals} - > - <EuiFlexGroup alignItems="center" gutterSize="none"> - <EuiFlexItem grow={false}> - {stackByOptions && ( - <EuiSelect - onChange={setSelectedOptionCallback} - options={stackByOptions} - prepend={i18n.STACK_BY_LABEL} - value={selectedStackByOption.value} - /> - )} - </EuiFlexItem> - {linkButton} - </EuiFlexGroup> - </HeaderSection> - - <SignalsHistogram - chartHeight={chartHeight} - data={formattedSignalsData} - from={from} - legendPosition={legendPosition} - loading={isLoadingSignals} - to={to} - updateDateRange={updateDateRange} - /> - </> + <SignalsHistogram + chartHeight={chartHeight} + data={formattedSignalsData} + from={from} + legendItems={legendItems} + legendPosition={legendPosition} + loading={isLoadingSignals} + to={to} + updateDateRange={updateDateRange} + /> )} </StyledEuiPanel> </InspectButtonContainer> diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals_histogram_panel/signals_histogram.test.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals_histogram_panel/signals_histogram.test.tsx new file mode 100644 index 0000000000000..6a116efb8f2f8 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals_histogram_panel/signals_histogram.test.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 { shallow } from 'enzyme'; + +import { SignalsHistogram } from './signals_histogram'; + +jest.mock('../../../../lib/kibana'); + +describe('SignalsHistogram', () => { + it('renders correctly', () => { + const wrapper = shallow( + <SignalsHistogram + legendItems={[]} + loading={false} + data={[]} + from={0} + to={1} + updateDateRange={jest.fn()} + /> + ); + + expect(wrapper.find('Chart')).toBeTruthy(); + }); +}); diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals_histogram_panel/signals_histogram.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals_histogram_panel/signals_histogram.tsx index 40e5b8abde072..4bb7e9f6e122f 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals_histogram_panel/signals_histogram.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals_histogram_panel/signals_histogram.tsx @@ -12,11 +12,14 @@ import { Settings, ChartSizeArray, } from '@elastic/charts'; +import { EuiFlexGroup, EuiFlexItem, EuiProgress } from '@elastic/eui'; import React, { useMemo } from 'react'; -import { EuiProgress } from '@elastic/eui'; import { useTheme } from '../../../../components/charts/common'; import { histogramDateTimeFormatter } from '../../../../components/utils'; +import { DraggableLegend } from '../../../../components/charts/draggable_legend'; +import { LegendItem } from '../../../../components/charts/draggable_legend_item'; + import { HistogramData } from './types'; const DEFAULT_CHART_HEIGHT = 174; @@ -24,18 +27,19 @@ const DEFAULT_CHART_HEIGHT = 174; interface HistogramSignalsProps { chartHeight?: number; from: number; + legendItems: LegendItem[]; legendPosition?: Position; loading: boolean; to: number; data: HistogramData[]; updateDateRange: (min: number, max: number) => void; } - export const SignalsHistogram = React.memo<HistogramSignalsProps>( ({ chartHeight = DEFAULT_CHART_HEIGHT, data, from, + legendItems, legendPosition = 'right', loading, to, @@ -62,29 +66,38 @@ export const SignalsHistogram = React.memo<HistogramSignalsProps>( /> )} - <Chart size={chartSize}> - <Settings - legendPosition={legendPosition} - onBrushEnd={updateDateRange} - showLegend - showLegendExtra - theme={theme} - /> + <EuiFlexGroup gutterSize="none"> + <EuiFlexItem grow={true}> + <Chart size={chartSize}> + <Settings + legendPosition={legendPosition} + onBrushEnd={updateDateRange} + showLegend={legendItems.length === 0} + showLegendExtra + theme={theme} + /> - <Axis id={xAxisId} position="bottom" tickFormat={tickFormat} /> + <Axis id={xAxisId} position="bottom" tickFormat={tickFormat} /> - <Axis id={yAxisId} position="left" /> + <Axis id={yAxisId} position="left" /> - <HistogramBarSeries - id={id} - xScaleType="time" - yScaleType="linear" - xAccessor="x" - yAccessors={yAccessors} - splitSeriesAccessors={splitSeriesAccessors} - data={data} - /> - </Chart> + <HistogramBarSeries + id={id} + xScaleType="time" + yScaleType="linear" + xAccessor="x" + yAccessors={yAccessors} + splitSeriesAccessors={splitSeriesAccessors} + data={data} + /> + </Chart> + </EuiFlexItem> + <EuiFlexItem grow={false}> + {legendItems.length > 0 && ( + <DraggableLegend legendItems={legendItems} height={chartHeight} /> + )} + </EuiFlexItem> + </EuiFlexGroup> </> ); } diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals_histogram_panel/translations.ts b/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals_histogram_panel/translations.ts index 8c88fa4a5dae6..e7b76a48c7592 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals_histogram_panel/translations.ts +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals_histogram_panel/translations.ts @@ -83,6 +83,12 @@ export const STACK_BY_USERS = i18n.translate( } ); +export const TOP = (fieldName: string) => + i18n.translate('xpack.siem.detectionEngine.signals.histogram.topNLabel', { + values: { fieldName }, + defaultMessage: `Top {fieldName}`, + }); + export const HISTOGRAM_HEADER = i18n.translate( 'xpack.siem.detectionEngine.signals.histogram.headerTitle', { diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/user_info/index.test.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/user_info/index.test.tsx new file mode 100644 index 0000000000000..b3d710de5e94e --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/user_info/index.test.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 { renderHook } from '@testing-library/react-hooks'; +import { useUserInfo } from './index'; + +import { usePrivilegeUser } from '../../../../containers/detection_engine/signals/use_privilege_user'; +import { useSignalIndex } from '../../../../containers/detection_engine/signals/use_signal_index'; +import { useKibana } from '../../../../lib/kibana'; +jest.mock('../../../../containers/detection_engine/signals/use_privilege_user'); +jest.mock('../../../../containers/detection_engine/signals/use_signal_index'); +jest.mock('../../../../lib/kibana'); + +describe('useUserInfo', () => { + beforeAll(() => { + (usePrivilegeUser as jest.Mock).mockReturnValue({}); + (useSignalIndex as jest.Mock).mockReturnValue({}); + (useKibana as jest.Mock).mockReturnValue({ + services: { + application: { + capabilities: { + siem: { + crud: true, + }, + }, + }, + }, + }); + }); + it('returns default state', () => { + const { result } = renderHook(() => useUserInfo()); + + expect(result).toEqual({ + current: { + canUserCRUD: null, + hasEncryptionKey: null, + hasIndexManage: null, + hasIndexWrite: null, + isAuthenticated: null, + isSignalIndexExists: null, + loading: true, + signalIndexName: null, + }, + error: undefined, + }); + }); +}); diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/detection_engine.test.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/detection_engine.test.tsx new file mode 100644 index 0000000000000..779e9a4557f2a --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/detection_engine.test.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 from 'react'; +import { shallow } from 'enzyme'; +import { useParams } from 'react-router-dom'; + +import '../../mock/match_media'; +import { setAbsoluteRangeDatePicker } from '../../store/inputs/actions'; +import { DetectionEnginePageComponent } from './detection_engine'; +import { useUserInfo } from './components/user_info'; + +jest.mock('./components/user_info'); +jest.mock('../../lib/kibana'); +jest.mock('react-router-dom', () => { + const originalModule = jest.requireActual('react-router-dom'); + + return { + ...originalModule, + useParams: jest.fn(), + }; +}); + +describe('DetectionEnginePageComponent', () => { + beforeAll(() => { + (useParams as jest.Mock).mockReturnValue({}); + (useUserInfo as jest.Mock).mockReturnValue({}); + }); + it('renders correctly', () => { + const wrapper = shallow( + <DetectionEnginePageComponent + query={{ query: 'query', language: 'language' }} + filters={[]} + setAbsoluteRangeDatePicker={setAbsoluteRangeDatePicker} + /> + ); + + expect(wrapper.find('WithSource')).toHaveLength(1); + }); +}); diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/detection_engine.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/detection_engine.tsx index 1bd7ab2c4f1ae..a26d7f5672106 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/detection_engine.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/detection_engine.tsx @@ -59,7 +59,7 @@ const detectionsTabs: Record<string, NavTab> = { }, }; -const DetectionEnginePageComponent: React.FC<PropsFromRedux> = ({ +export const DetectionEnginePageComponent: React.FC<PropsFromRedux> = ({ filters, query, setAbsoluteRangeDatePicker, diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/detection_engine_empty_page.test.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/detection_engine_empty_page.test.tsx new file mode 100644 index 0000000000000..f64526fd2f7c4 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/detection_engine_empty_page.test.tsx @@ -0,0 +1,19 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { shallow } from 'enzyme'; + +import { DetectionEngineEmptyPage } from './detection_engine_empty_page'; +jest.mock('../../lib/kibana'); + +describe('DetectionEngineEmptyPage', () => { + it('renders correctly', () => { + const wrapper = shallow(<DetectionEngineEmptyPage />); + + expect(wrapper.find('EmptyPage')).toHaveLength(1); + }); +}); diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/detection_engine_no_signal_index.test.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/detection_engine_no_signal_index.test.tsx new file mode 100644 index 0000000000000..e9f05f7aafe3c --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/detection_engine_no_signal_index.test.tsx @@ -0,0 +1,19 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { shallow } from 'enzyme'; + +import { DetectionEngineNoIndex } from './detection_engine_no_signal_index'; +jest.mock('../../lib/kibana'); + +describe('DetectionEngineNoIndex', () => { + it('renders correctly', () => { + const wrapper = shallow(<DetectionEngineNoIndex />); + + expect(wrapper.find('EmptyPage')).toHaveLength(1); + }); +}); diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/detection_engine_user_unauthenticated.test.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/detection_engine_user_unauthenticated.test.tsx new file mode 100644 index 0000000000000..e71f4de2b010b --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/detection_engine_user_unauthenticated.test.tsx @@ -0,0 +1,19 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { shallow } from 'enzyme'; + +import { DetectionEngineUserUnauthenticated } from './detection_engine_user_unauthenticated'; +jest.mock('../../lib/kibana'); + +describe('DetectionEngineUserUnauthenticated', () => { + it('renders correctly', () => { + const wrapper = shallow(<DetectionEngineUserUnauthenticated />); + + expect(wrapper.find('EmptyPage')).toHaveLength(1); + }); +}); diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/helpers.ts b/x-pack/legacy/plugins/siem/public/pages/detection_engine/helpers.ts deleted file mode 100644 index 1399df0fcf6d1..0000000000000 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/helpers.ts +++ /dev/null @@ -1,18 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -export const sampleChartOptions = [ - { text: 'Risk scores', value: 'risk_scores' }, - { text: 'Severities', value: 'severities' }, - { text: 'Top destination IPs', value: 'destination_ips' }, - { text: 'Top event actions', value: 'event_actions' }, - { text: 'Top event categories', value: 'event_categories' }, - { text: 'Top host names', value: 'host_names' }, - { text: 'Top rule types', value: 'rule_types' }, - { text: 'Top rules', value: 'rules' }, - { text: 'Top source IPs', value: 'source_ips' }, - { text: 'Top users', value: 'users' }, -]; diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/index.test.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/index.test.tsx new file mode 100644 index 0000000000000..6c4980f1d1500 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/index.test.tsx @@ -0,0 +1,19 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { shallow } from 'enzyme'; + +import '../../mock/match_media'; +import { DetectionEngineContainer } from './index'; + +describe('DetectionEngineContainer', () => { + it('renders correctly', () => { + const wrapper = shallow(<DetectionEngineContainer url="url" />); + + expect(wrapper.find('ManageUserInfo')).toHaveLength(1); + }); +}); diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/all/columns.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/all/columns.tsx index 80e644f800334..8bea504f84206 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/all/columns.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/all/columns.tsx @@ -19,6 +19,7 @@ import { FormattedRelative } from '@kbn/i18n/react'; import * as H from 'history'; import React, { Dispatch } from 'react'; +import { isMlRule } from '../../../../../../../../plugins/siem/common/detection_engine/ml_helpers'; import { Rule, RuleStatus } from '../../../../containers/detection_engine/rules'; import { getEmptyTagValue } from '../../../../components/empty_value'; import { FormattedDate } from '../../../../components/formatted_date'; @@ -38,7 +39,6 @@ import { import { Action } from './reducer'; import { LocalizedDateTooltip } from '../../../../components/localized_date_tooltip'; import * as detectionI18n from '../../translations'; -import { isMlRule } from '../../../../../common/detection_engine/ml_helpers'; export const getActions = ( dispatch: React.Dispatch<Action>, diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/all/index.test.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/all/index.test.tsx new file mode 100644 index 0000000000000..59b3b02ff3587 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/all/index.test.tsx @@ -0,0 +1,230 @@ +/* + * 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 { act } from 'react-dom/test-utils'; + +import { createKibanaContextProviderMock } from '../../../../mock/kibana_react'; +import { TestProviders } from '../../../../mock'; +import { wait } from '../../../../lib/helpers'; +import { AllRules } from './index'; + +jest.mock('./reducer', () => { + return { + allRulesReducer: jest.fn().mockReturnValue(() => ({ + exportRuleIds: [], + filterOptions: { + filter: 'some filter', + sortField: 'some sort field', + sortOrder: 'desc', + }, + loadingRuleIds: [], + loadingRulesAction: null, + pagination: { + page: 1, + perPage: 20, + total: 1, + }, + rules: [ + { + actions: [], + created_at: '2020-02-14T19:49:28.178Z', + created_by: 'elastic', + description: 'jibber jabber', + enabled: false, + false_positives: [], + filters: [], + from: 'now-660s', + id: 'rule-id-1', + immutable: true, + index: ['endgame-*'], + interval: '10m', + language: 'kuery', + max_signals: 100, + name: 'Credential Dumping - Detected - Elastic Endpoint', + output_index: '.siem-signals-default', + query: 'host.name:*', + references: [], + risk_score: 73, + rule_id: '571afc56-5ed9-465d-a2a9-045f099f6e7e', + severity: 'high', + tags: ['Elastic', 'Endpoint'], + threat: [], + throttle: null, + to: 'now', + type: 'query', + updated_at: '2020-02-14T19:49:28.320Z', + updated_by: 'elastic', + version: 1, + }, + ], + selectedRuleIds: [], + })), + }; +}); + +jest.mock('../../../../containers/detection_engine/rules', () => { + return { + useRules: jest.fn().mockReturnValue([ + false, + { + page: 1, + perPage: 20, + total: 1, + data: [ + { + actions: [], + created_at: '2020-02-14T19:49:28.178Z', + created_by: 'elastic', + description: 'jibber jabber', + enabled: false, + false_positives: [], + filters: [], + from: 'now-660s', + id: 'rule-id-1', + immutable: true, + index: ['endgame-*'], + interval: '10m', + language: 'kuery', + max_signals: 100, + name: 'Credential Dumping - Detected - Elastic Endpoint', + output_index: '.siem-signals-default', + query: 'host.name:*', + references: [], + risk_score: 73, + rule_id: '571afc56-5ed9-465d-a2a9-045f099f6e7e', + severity: 'high', + tags: ['Elastic', 'Endpoint'], + threat: [], + throttle: null, + to: 'now', + type: 'query', + updated_at: '2020-02-14T19:49:28.320Z', + updated_by: 'elastic', + version: 1, + }, + ], + }, + ]), + useRulesStatuses: jest.fn().mockReturnValue({ + loading: false, + rulesStatuses: [ + { + current_status: { + alert_id: 'alertId', + bulk_create_time_durations: ['2235.01'], + gap: null, + last_failure_at: null, + last_failure_message: null, + last_look_back_date: new Date().toISOString(), + last_success_at: new Date().toISOString(), + last_success_message: 'it is a success', + search_after_time_durations: ['616.97'], + status: 'succeeded', + status_date: new Date().toISOString(), + }, + failures: [], + id: '12345678987654321', + activate: true, + name: 'Test rule', + }, + ], + }), + }; +}); + +jest.mock('react-router-dom', () => { + const originalModule = jest.requireActual('react-router-dom'); + + return { + ...originalModule, + useHistory: jest.fn(), + }; +}); + +describe('AllRules', () => { + it('renders correctly', () => { + const wrapper = shallow( + <AllRules + createPrePackagedRules={jest.fn()} + hasNoPermissions={false} + loading={false} + loadingCreatePrePackagedRules={false} + refetchPrePackagedRulesStatus={jest.fn()} + rulesCustomInstalled={0} + rulesInstalled={0} + rulesNotInstalled={0} + rulesNotUpdated={0} + setRefreshRulesData={jest.fn()} + /> + ); + + expect(wrapper.find('[title="All rules"]')).toHaveLength(1); + }); + + it('renders rules tab', async () => { + const KibanaContext = createKibanaContextProviderMock(); + const wrapper = mount( + <TestProviders> + <KibanaContext services={{ storage: { get: jest.fn() } }}> + <AllRules + createPrePackagedRules={jest.fn()} + hasNoPermissions={false} + loading={false} + loadingCreatePrePackagedRules={false} + refetchPrePackagedRulesStatus={jest.fn()} + rulesCustomInstalled={1} + rulesInstalled={0} + rulesNotInstalled={0} + rulesNotUpdated={0} + setRefreshRulesData={jest.fn()} + /> + </KibanaContext> + </TestProviders> + ); + + await act(async () => { + await wait(); + + expect(wrapper.exists('[data-test-subj="monitoring-table"]')).toBeFalsy(); + expect(wrapper.exists('[data-test-subj="rules-table"]')).toBeTruthy(); + }); + }); + + it('renders monitoring tab when monitoring tab clicked', async () => { + const KibanaContext = createKibanaContextProviderMock(); + + const wrapper = mount( + <TestProviders> + <KibanaContext services={{ storage: { get: jest.fn() } }}> + <AllRules + createPrePackagedRules={jest.fn()} + hasNoPermissions={false} + loading={false} + loadingCreatePrePackagedRules={false} + refetchPrePackagedRulesStatus={jest.fn()} + rulesCustomInstalled={1} + rulesInstalled={0} + rulesNotInstalled={0} + rulesNotUpdated={0} + setRefreshRulesData={jest.fn()} + /> + </KibanaContext> + </TestProviders> + ); + const monitoringTab = wrapper.find('[data-test-subj="allRulesTableTab-monitoring"] button'); + monitoringTab.simulate('click'); + + await act(async () => { + wrapper.update(); + await wait(); + + expect(wrapper.exists('[data-test-subj="monitoring-table"]')).toBeTruthy(); + expect(wrapper.exists('[data-test-subj="rules-table"]')).toBeFalsy(); + }); + }); +}); diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/all/index.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/all/index.tsx index e96ed856208bd..18ca4d42bd018 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/all/index.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/all/index.tsx @@ -4,7 +4,14 @@ * you may not use this file except in compliance with the Elastic License. */ -import { EuiBasicTable, EuiContextMenuPanel, EuiLoadingContent, EuiSpacer } from '@elastic/eui'; +import { + EuiBasicTable, + EuiContextMenuPanel, + EuiLoadingContent, + EuiSpacer, + EuiTab, + EuiTabs, +} from '@elastic/eui'; import React, { useCallback, useEffect, useMemo, useReducer, useRef, useState } from 'react'; import { useHistory } from 'react-router-dom'; import uuid from 'uuid'; @@ -75,6 +82,24 @@ interface AllRulesProps { setRefreshRulesData: (refreshRule: (refreshPrePackagedRule?: boolean) => void) => void; } +export enum AllRulesTabs { + rules = 'rules', + monitoring = 'monitoring', +} + +const allRulesTabs = [ + { + id: AllRulesTabs.rules, + name: i18n.RULES_TAB, + disabled: false, + }, + { + id: AllRulesTabs.monitoring, + name: i18n.MONITORING_TAB, + disabled: false, + }, +]; + /** * Table Component for displaying all Rules for a given cluster. Provides the ability to filter * by name, sort by enabled, and perform the following actions: @@ -114,6 +139,7 @@ export const AllRules = React.memo<AllRulesProps>( const history = useHistory(); const [, dispatchToaster] = useStateToaster(); const mlCapabilities = useMlCapabilities(); + const [allRulesTab, setAllRulesTab] = useState(AllRulesTabs.rules); // TODO: Refactor license check + hasMlAdminPermissions to common check const hasMlPermissions = @@ -271,6 +297,25 @@ export const AllRules = React.memo<AllRulesProps>( return false; }, [loadingRuleIds, loadingRulesAction]); + const tabs = useMemo( + () => ( + <EuiTabs> + {allRulesTabs.map(tab => ( + <EuiTab + data-test-subj={`allRulesTableTab-${tab.id}`} + onClick={() => setAllRulesTab(tab.id)} + isSelected={tab.id === allRulesTab} + disabled={tab.disabled} + key={tab.id} + > + {tab.name} + </EuiTab> + ))} + </EuiTabs> + ), + [allRulesTabs, allRulesTab, setAllRulesTab] + ); + return ( <> <GenericDownloader @@ -291,6 +336,8 @@ export const AllRules = React.memo<AllRulesProps>( exportSelectedData={exportRules} /> <EuiSpacer /> + {tabs} + <EuiSpacer /> <Panel loading={loading || isLoadingRules || isLoadingRulesStatuses}> <> @@ -321,7 +368,7 @@ export const AllRules = React.memo<AllRulesProps>( )} {showRulesTable({ rulesCustomInstalled, rulesInstalled }) && !initLoading && ( <> - <UtilityBar border> + <UtilityBar> <UtilityBarSection> <UtilityBarGroup> <UtilityBarText dataTestSubj="showingRules"> @@ -352,6 +399,7 @@ export const AllRules = React.memo<AllRulesProps>( </UtilityBarSection> </UtilityBar> <AllRulesTables + selectedTab={allRulesTab} euiBasicTableSelectionProps={euiBasicTableSelectionProps} hasNoPermissions={hasNoPermissions} monitoringColumns={monitoringColumns} diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/all/rules_table_filters/rules_table_filters.test.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/all/rules_table_filters/rules_table_filters.test.tsx new file mode 100644 index 0000000000000..92f69d79110d2 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/all/rules_table_filters/rules_table_filters.test.tsx @@ -0,0 +1,20 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { shallow } from 'enzyme'; + +import { RulesTableFilters } from './rules_table_filters'; + +describe('RulesTableFilters', () => { + it('renders correctly', () => { + const wrapper = shallow( + <RulesTableFilters onFilterChanged={jest.fn()} rulesCustomInstalled={0} rulesInstalled={0} /> + ); + + expect(wrapper.find('[data-test-subj="show-elastic-rules-filter-button"]')).toHaveLength(1); + }); +}); diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/all/rules_table_filters/tags_filter_popover.test.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/all/rules_table_filters/tags_filter_popover.test.tsx new file mode 100644 index 0000000000000..e31b8394e07d6 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/all/rules_table_filters/tags_filter_popover.test.tsx @@ -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 React from 'react'; +import { shallow } from 'enzyme'; + +import { TagsFilterPopover } from './tags_filter_popover'; + +describe('TagsFilterPopover', () => { + it('renders correctly', () => { + const wrapper = shallow( + <TagsFilterPopover + tags={[]} + selectedTags={[]} + onSelectedTagsChanged={jest.fn()} + isLoading={false} + /> + ); + + expect(wrapper.find('EuiPopover')).toHaveLength(1); + }); +}); diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/accordion_title/index.test.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/accordion_title/index.test.tsx new file mode 100644 index 0000000000000..9202da3336565 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/accordion_title/index.test.tsx @@ -0,0 +1,18 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { shallow } from 'enzyme'; + +import { AccordionTitle } from './index'; + +describe('AccordionTitle', () => { + it('renders correctly', () => { + const wrapper = shallow(<AccordionTitle name="name" title="title" type="valid" />); + + expect(wrapper.find('h6').text()).toContain('title'); + }); +}); diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/add_item_form/index.test.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/add_item_form/index.test.tsx new file mode 100644 index 0000000000000..eafa89a33f596 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/add_item_form/index.test.tsx @@ -0,0 +1,32 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { shallow } from 'enzyme'; + +import { AddItem } from './index'; +import { useFormFieldMock } from '../../../../../../public/mock/test_providers'; + +describe('AddItem', () => { + it('renders correctly', () => { + const Component = () => { + const field = useFormFieldMock(); + + return ( + <AddItem + addText="text" + field={field} + dataTestSubj="dataTestSubj" + idAria="idAria" + isDisabled={false} + /> + ); + }; + const wrapper = shallow(<Component />); + + expect(wrapper.dive().find('[iconType="plusInCircle"]')).toHaveLength(1); + }); +}); diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/all_rules_tables/index.test.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/all_rules_tables/index.test.tsx new file mode 100644 index 0000000000000..8afb8db0c8d5b --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/all_rules_tables/index.test.tsx @@ -0,0 +1,120 @@ +/* + * 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, { useRef } from 'react'; +import { shallow } from 'enzyme'; + +import { AllRulesTables } from './index'; +import { AllRulesTabs } from '../../all'; + +describe('AllRulesTables', () => { + it('renders correctly', () => { + const Component = () => { + const ref = useRef(); + + return ( + <AllRulesTables + selectedTab={AllRulesTabs.rules} + euiBasicTableSelectionProps={{}} + hasNoPermissions={false} + monitoringColumns={[]} + rules={[]} + rulesColumns={[]} + rulesStatuses={[]} + tableOnChangeCallback={jest.fn()} + tableRef={ref} + pagination={{ + pageIndex: 0, + pageSize: 0, + totalItemCount: 0, + pageSizeOptions: [0], + }} + sorting={{ + sort: { + field: 'enabled', + direction: 'asc', + }, + }} + /> + ); + }; + const wrapper = shallow(<Component />); + + expect(wrapper.dive().find('[data-test-subj="rules-table"]')).toHaveLength(1); + }); + + it('renders rules tab when "selectedTab" is "rules"', () => { + const Component = () => { + const ref = useRef(); + + return ( + <AllRulesTables + selectedTab={AllRulesTabs.rules} + euiBasicTableSelectionProps={{}} + hasNoPermissions={false} + monitoringColumns={[]} + rules={[]} + rulesColumns={[]} + rulesStatuses={[]} + tableOnChangeCallback={jest.fn()} + tableRef={ref} + pagination={{ + pageIndex: 0, + pageSize: 0, + totalItemCount: 0, + pageSizeOptions: [0], + }} + sorting={{ + sort: { + field: 'enabled', + direction: 'asc', + }, + }} + /> + ); + }; + const wrapper = shallow(<Component />); + + expect(wrapper.dive().find('[data-test-subj="rules-table"]')).toHaveLength(1); + expect(wrapper.dive().find('[data-test-subj="monitoring-table"]')).toHaveLength(0); + }); + + it('renders monitoring tab when "selectedTab" is "monitoring"', () => { + const Component = () => { + const ref = useRef(); + + return ( + <AllRulesTables + selectedTab={AllRulesTabs.monitoring} + euiBasicTableSelectionProps={{}} + hasNoPermissions={false} + monitoringColumns={[]} + rules={[]} + rulesColumns={[]} + rulesStatuses={[]} + tableOnChangeCallback={jest.fn()} + tableRef={ref} + pagination={{ + pageIndex: 0, + pageSize: 0, + totalItemCount: 0, + pageSizeOptions: [0], + }} + sorting={{ + sort: { + field: 'enabled', + direction: 'asc', + }, + }} + /> + ); + }; + const wrapper = shallow(<Component />); + + expect(wrapper.dive().find('[data-test-subj="rules-table"]')).toHaveLength(0); + expect(wrapper.dive().find('[data-test-subj="monitoring-table"]')).toHaveLength(1); + }); +}); diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/all_rules_tables/index.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/all_rules_tables/index.tsx index 31aaa426e4f3b..8ea5606d0082c 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/all_rules_tables/index.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/all_rules_tables/index.tsx @@ -7,13 +7,11 @@ import { EuiBasicTable, EuiBasicTableColumn, - EuiTab, - EuiTabs, EuiEmptyPrompt, Direction, EuiTableSelectionType, } from '@elastic/eui'; -import React, { useMemo, memo, useState } from 'react'; +import React, { useMemo, memo } from 'react'; import styled from 'styled-components'; import { EuiBasicTableOnChange } from '../../types'; @@ -23,6 +21,7 @@ import { RuleStatusRowItemType, } from '../../../../../pages/detection_engine/rules/all/columns'; import { Rule, Rules } from '../../../../../containers/detection_engine/rules'; +import { AllRulesTabs } from '../../all'; // EuiBasicTable give me a hardtime with adding the ref attributes so I went the easy way // after few hours of fight with typescript !!!! I lost :( @@ -57,27 +56,10 @@ interface AllRulesTablesProps { }; tableOnChangeCallback: ({ page, sort }: EuiBasicTableOnChange) => void; tableRef?: React.MutableRefObject<EuiBasicTable | undefined>; + selectedTab: AllRulesTabs; } -enum AllRulesTabs { - rules = 'rules', - monitoring = 'monitoring', -} - -const allRulesTabs = [ - { - id: AllRulesTabs.rules, - name: i18n.RULES_TAB, - disabled: false, - }, - { - id: AllRulesTabs.monitoring, - name: i18n.MONITORING_TAB, - disabled: false, - }, -]; - -const AllRulesTablesComponent: React.FC<AllRulesTablesProps> = ({ +export const AllRulesTablesComponent: React.FC<AllRulesTablesProps> = ({ euiBasicTableSelectionProps, hasNoPermissions, monitoringColumns, @@ -88,34 +70,17 @@ const AllRulesTablesComponent: React.FC<AllRulesTablesProps> = ({ sorting, tableOnChangeCallback, tableRef, + selectedTab, }) => { - const [allRulesTab, setAllRulesTab] = useState(AllRulesTabs.rules); const emptyPrompt = useMemo(() => { return ( <EuiEmptyPrompt title={<h3>{i18n.NO_RULES}</h3>} titleSize="xs" body={i18n.NO_RULES_BODY} /> ); }, []); - const tabs = useMemo( - () => ( - <EuiTabs> - {allRulesTabs.map(tab => ( - <EuiTab - onClick={() => setAllRulesTab(tab.id)} - isSelected={tab.id === allRulesTab} - disabled={tab.disabled} - key={tab.id} - > - {tab.name} - </EuiTab> - ))} - </EuiTabs> - ), - [allRulesTabs, allRulesTab, setAllRulesTab] - ); + return ( <> - {tabs} - {allRulesTab === AllRulesTabs.rules && ( + {selectedTab === AllRulesTabs.rules && ( <MyEuiBasicTable data-test-subj="rules-table" columns={rulesColumns} @@ -130,7 +95,7 @@ const AllRulesTablesComponent: React.FC<AllRulesTablesProps> = ({ selection={hasNoPermissions ? undefined : euiBasicTableSelectionProps} /> )} - {allRulesTab === AllRulesTabs.monitoring && ( + {selectedTab === AllRulesTabs.monitoring && ( <MyEuiBasicTable data-test-subj="monitoring-table" columns={monitoringColumns} diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/anomaly_threshold_slider/index.test.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/anomaly_threshold_slider/index.test.tsx new file mode 100644 index 0000000000000..c0e957d94261f --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/anomaly_threshold_slider/index.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 from 'react'; +import { shallow } from 'enzyme'; + +import { AnomalyThresholdSlider } from './index'; +import { useFormFieldMock } from '../../../../../mock'; + +describe('AnomalyThresholdSlider', () => { + it('renders correctly', () => { + const Component = () => { + const field = useFormFieldMock(); + + return <AnomalyThresholdSlider describedByIds={[]} field={field} />; + }; + const wrapper = shallow(<Component />); + + expect(wrapper.dive().find('EuiRange')).toHaveLength(1); + }); +}); diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/anomaly_threshold_slider/index.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/anomaly_threshold_slider/index.tsx index 19d1c698cbd9b..01fddf98b97d8 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/anomaly_threshold_slider/index.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/anomaly_threshold_slider/index.tsx @@ -16,10 +16,10 @@ interface AnomalyThresholdSliderProps { type Event = React.ChangeEvent<HTMLInputElement>; type EventArg = Event | React.MouseEvent<HTMLButtonElement>; -export const AnomalyThresholdSlider: React.FC<AnomalyThresholdSliderProps> = ({ +export const AnomalyThresholdSlider = ({ describedByIds = [], field, -}) => { +}: AnomalyThresholdSliderProps) => { const threshold = field.value as number; const onThresholdChange = useCallback( (event: EventArg) => { diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/description_step/helpers.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/description_step/helpers.tsx index 79da7999b081a..5b7a85e23834d 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/description_step/helpers.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/description_step/helpers.tsx @@ -19,9 +19,9 @@ import { isEmpty } from 'lodash/fp'; import React from 'react'; import styled from 'styled-components'; +import { RuleType } from '../../../../../../../../../plugins/siem/common/detection_engine/types'; import { esFilters } from '../../../../../../../../../../src/plugins/data/public'; -import { RuleType } from '../../../../../../common/detection_engine/types'; import { tacticsOptions, techniquesOptions } from '../../../mitre/mitre_tactics_techniques'; import * as i18n from './translations'; diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/description_step/index.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/description_step/index.tsx index 05e47225c8f4b..49977713a585a 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/description_step/index.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/description_step/index.tsx @@ -9,13 +9,13 @@ import { isEmpty, chunk, get, pick, isNumber } from 'lodash/fp'; import React, { memo, useState } from 'react'; import styled from 'styled-components'; +import { RuleType } from '../../../../../../../../../plugins/siem/common/detection_engine/types'; import { IIndexPattern, Filter, esFilters, FilterManager, } from '../../../../../../../../../../src/plugins/data/public'; -import { RuleType } from '../../../../../../common/detection_engine/types'; import { DEFAULT_TIMELINE_TITLE } from '../../../../../components/timeline/translations'; import { useKibana } from '../../../../../lib/kibana'; import { IMitreEnterpriseAttack } from '../../types'; diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/description_step/ml_job_description.test.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/description_step/ml_job_description.test.tsx new file mode 100644 index 0000000000000..59231c31d15bb --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/description_step/ml_job_description.test.tsx @@ -0,0 +1,54 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { shallow } from 'enzyme'; + +import { MlJobDescription, AuditIcon, JobStatusBadge } from './ml_job_description'; +jest.mock('../../../../../lib/kibana'); + +const job = { + moduleId: 'moduleId', + defaultIndexPattern: 'defaultIndexPattern', + isCompatible: true, + isInstalled: true, + isElasticJob: true, + datafeedId: 'datafeedId', + datafeedIndices: [], + datafeedState: 'datafeedState', + description: 'description', + groups: [], + hasDatafeed: true, + id: 'id', + isSingleMetricViewerJob: false, + jobState: 'jobState', + memory_status: 'memory_status', + processed_record_count: 0, +}; + +describe('MlJobDescription', () => { + it('renders correctly', () => { + const wrapper = shallow(<MlJobDescription job={job} />); + + expect(wrapper.find('[data-test-subj="machineLearningJobId"]')).toHaveLength(1); + }); +}); + +describe('AuditIcon', () => { + it('renders correctly', () => { + const wrapper = shallow(<AuditIcon message={undefined} />); + + expect(wrapper.find('EuiToolTip')).toHaveLength(0); + }); +}); + +describe('JobStatusBadge', () => { + it('renders correctly', () => { + const wrapper = shallow(<JobStatusBadge job={job} />); + + expect(wrapper.find('EuiBadge')).toHaveLength(1); + }); +}); diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/description_step/ml_job_description.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/description_step/ml_job_description.tsx index 5a9593f1a6de2..5e8681a90d428 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/description_step/ml_job_description.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/description_step/ml_job_description.tsx @@ -8,11 +8,11 @@ import React from 'react'; import styled from 'styled-components'; import { EuiBadge, EuiIcon, EuiLink, EuiToolTip } from '@elastic/eui'; +import { isJobStarted } from '../../../../../../../../../plugins/siem/common/detection_engine/ml_helpers'; import { useKibana } from '../../../../../lib/kibana'; import { SiemJob } from '../../../../../components/ml_popover/types'; import { ListItems } from './types'; import { ML_JOB_STARTED, ML_JOB_STOPPED } from './translations'; -import { isJobStarted } from '../../../../../../common/detection_engine/ml_helpers'; enum MessageLevels { info = 'info', @@ -20,7 +20,7 @@ enum MessageLevels { error = 'error', } -const AuditIcon: React.FC<{ +const AuditIconComponent: React.FC<{ message: SiemJob['auditMessage']; }> = ({ message }) => { if (!message) { @@ -45,7 +45,9 @@ const AuditIcon: React.FC<{ ); }; -export const JobStatusBadge: React.FC<{ job: SiemJob }> = ({ job }) => { +export const AuditIcon = React.memo(AuditIconComponent); + +const JobStatusBadgeComponent: React.FC<{ job: SiemJob }> = ({ job }) => { const isStarted = isJobStarted(job.jobState, job.datafeedState); const color = isStarted ? 'secondary' : 'danger'; const text = isStarted ? ML_JOB_STARTED : ML_JOB_STOPPED; @@ -57,6 +59,8 @@ export const JobStatusBadge: React.FC<{ job: SiemJob }> = ({ job }) => { ); }; +export const JobStatusBadge = React.memo(JobStatusBadgeComponent); + const JobLink = styled(EuiLink)` margin-right: ${({ theme }) => theme.eui.euiSizeS}; `; @@ -65,7 +69,7 @@ const Wrapper = styled.div` overflow: hidden; `; -export const MlJobDescription: React.FC<{ job: SiemJob }> = ({ job }) => { +const MlJobDescriptionComponent: React.FC<{ job: SiemJob }> = ({ job }) => { const jobUrl = useKibana().services.application.getUrlForApp( `ml#/jobs?mlManagement=(jobId:${encodeURI(job.id)})` ); @@ -83,6 +87,8 @@ export const MlJobDescription: React.FC<{ job: SiemJob }> = ({ job }) => { ); }; +export const MlJobDescription = React.memo(MlJobDescriptionComponent); + export const buildMlJobDescription = ( jobId: string, label: string, diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/mitre/helpers.test.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/mitre/helpers.test.tsx new file mode 100644 index 0000000000000..dc201eb21c911 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/mitre/helpers.test.tsx @@ -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 { isMitreAttackInvalid } from './helpers'; + +describe('isMitreAttackInvalid', () => { + it('returns true if tacticName is empty', () => { + expect(isMitreAttackInvalid('', undefined)).toBe(true); + }); +}); diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/mitre/index.test.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/mitre/index.test.tsx new file mode 100644 index 0000000000000..3e8d542682456 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/mitre/index.test.tsx @@ -0,0 +1,31 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { shallow } from 'enzyme'; + +import { AddMitreThreat } from './index'; +import { useFormFieldMock } from '../../../../../mock'; + +describe('AddMitreThreat', () => { + it('renders correctly', () => { + const Component = () => { + const field = useFormFieldMock(); + + return ( + <AddMitreThreat + dataTestSubj="dataTestSubj" + idAria="idAria" + isDisabled={false} + field={field} + /> + ); + }; + const wrapper = shallow(<Component />); + + expect(wrapper.dive().find('[data-test-subj="addMitre"]')).toHaveLength(1); + }); +}); diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/ml_job_select/index.test.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/ml_job_select/index.test.tsx new file mode 100644 index 0000000000000..dea27d8d04536 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/ml_job_select/index.test.tsx @@ -0,0 +1,31 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { shallow } from 'enzyme'; + +import { MlJobSelect } from './index'; +import { useSiemJobs } from '../../../../../components/ml_popover/hooks/use_siem_jobs'; +import { useFormFieldMock } from '../../../../../mock'; +jest.mock('../../../../../components/ml_popover/hooks/use_siem_jobs'); +jest.mock('../../../../../lib/kibana'); + +describe('MlJobSelect', () => { + beforeAll(() => { + (useSiemJobs as jest.Mock).mockReturnValue([false, []]); + }); + + it('renders correctly', () => { + const Component = () => { + const field = useFormFieldMock(); + + return <MlJobSelect describedByIds={[]} field={field} />; + }; + const wrapper = shallow(<Component />); + + expect(wrapper.dive().find('[data-test-subj="mlJobSelect"]')).toHaveLength(1); + }); +}); diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/ml_job_select/index.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/ml_job_select/index.tsx index 794edf0ab5de7..82350150488d0 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/ml_job_select/index.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/ml_job_select/index.tsx @@ -17,6 +17,7 @@ import { } from '@elastic/eui'; import styled from 'styled-components'; +import { isJobStarted } from '../../../../../../../../../plugins/siem/common/detection_engine/ml_helpers'; import { FieldHook, getFieldValidityAndErrorMessage } from '../../../../../shared_imports'; import { useSiemJobs } from '../../../../../components/ml_popover/hooks/use_siem_jobs'; import { useKibana } from '../../../../../lib/kibana'; @@ -24,7 +25,6 @@ import { ML_JOB_SELECT_PLACEHOLDER_TEXT, ENABLE_ML_JOB_WARNING, } from '../step_define_rule/translations'; -import { isJobStarted } from '../../../../../../common/detection_engine/ml_helpers'; const HelpTextWarningContainer = styled.div` margin-top: 10px; diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/optional_field_label/index.test.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/optional_field_label/index.test.tsx new file mode 100644 index 0000000000000..789f12f290a34 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/optional_field_label/index.test.tsx @@ -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 { shallow } from 'enzyme'; + +import { OptionalFieldLabel } from './index'; + +describe('OptionalFieldLabel', () => { + it('renders correctly', () => { + const wrapper = shallow(OptionalFieldLabel); + + expect(wrapper.find('EuiTextColor')).toHaveLength(1); + }); +}); diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/pick_timeline/index.test.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/pick_timeline/index.test.tsx new file mode 100644 index 0000000000000..fefc9697176c4 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/pick_timeline/index.test.tsx @@ -0,0 +1,31 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { shallow } from 'enzyme'; + +import { PickTimeline } from './index'; +import { useFormFieldMock } from '../../../../../mock'; + +describe('PickTimeline', () => { + it('renders correctly', () => { + const Component = () => { + const field = useFormFieldMock(); + + return ( + <PickTimeline + dataTestSubj="pick-timeline" + idAria="idAria" + isDisabled={false} + field={field} + /> + ); + }; + const wrapper = shallow(<Component />); + + expect(wrapper.dive().find('[data-test-subj="pick-timeline"]')).toHaveLength(1); + }); +}); diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/pre_packaged_rules/load_empty_prompt.test.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/pre_packaged_rules/load_empty_prompt.test.tsx new file mode 100644 index 0000000000000..8ace42fc5c3f9 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/pre_packaged_rules/load_empty_prompt.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 from 'react'; +import { shallow } from 'enzyme'; + +import { PrePackagedRulesPrompt } from './load_empty_prompt'; + +describe('PrePackagedRulesPrompt', () => { + it('renders correctly', () => { + const wrapper = shallow( + <PrePackagedRulesPrompt + createPrePackagedRules={jest.fn()} + loading={false} + userHasNoPermissions={false} + /> + ); + + expect(wrapper.find('EmptyPrompt')).toHaveLength(1); + }); +}); diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/pre_packaged_rules/load_empty_prompt.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/pre_packaged_rules/load_empty_prompt.tsx index 1cff4751e8188..5d136265ef1f2 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/pre_packaged_rules/load_empty_prompt.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/pre_packaged_rules/load_empty_prompt.tsx @@ -15,6 +15,8 @@ const EmptyPrompt = styled(EuiEmptyPrompt)` align-self: center; /* Corrects horizontal centering in IE11 */ `; +EmptyPrompt.displayName = 'EmptyPrompt'; + interface PrePackagedRulesPromptProps { createPrePackagedRules: () => void; loading: boolean; diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/pre_packaged_rules/update_callout.test.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/pre_packaged_rules/update_callout.test.tsx new file mode 100644 index 0000000000000..807da79fb7a1a --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/pre_packaged_rules/update_callout.test.tsx @@ -0,0 +1,36 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { shallow } from 'enzyme'; + +import { UpdatePrePackagedRulesCallOut } from './update_callout'; +import { useKibana } from '../../../../../lib/kibana'; +jest.mock('../../../../../lib/kibana'); + +describe('UpdatePrePackagedRulesCallOut', () => { + beforeAll(() => { + (useKibana as jest.Mock).mockReturnValue({ + services: { + docLinks: { + ELASTIC_WEBSITE_URL: '', + DOC_LINK_VERSION: '', + }, + }, + }); + }); + it('renders correctly', () => { + const wrapper = shallow( + <UpdatePrePackagedRulesCallOut + loading={false} + numberOfUpdatedRules={0} + updateRules={jest.fn()} + /> + ); + + expect(wrapper.find('EuiCallOut')).toHaveLength(1); + }); +}); diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/query_bar/index.test.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/query_bar/index.test.tsx new file mode 100644 index 0000000000000..cdd06ad58bb4b --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/query_bar/index.test.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 { shallow } from 'enzyme'; + +import { QueryBarDefineRule } from './index'; +import { useFormFieldMock } from '../../../../../mock'; + +jest.mock('../../../../../lib/kibana'); + +describe('QueryBarDefineRule', () => { + it('renders correctly', () => { + const Component = () => { + const field = useFormFieldMock(); + + return ( + <QueryBarDefineRule + browserFields={{}} + isLoading={false} + indexPattern={{ fields: [], title: 'title' }} + onCloseTimelineSearch={jest.fn()} + openTimelineSearch={true} + dataTestSubj="query-bar-define-rule" + idAria="idAria" + field={field} + /> + ); + }; + const wrapper = shallow(<Component />); + + expect(wrapper.dive().find('[data-test-subj="query-bar-define-rule"]')).toHaveLength(1); + }); +}); diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/read_only_callout/index.test.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/read_only_callout/index.test.tsx new file mode 100644 index 0000000000000..e761cb3323b2c --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/read_only_callout/index.test.tsx @@ -0,0 +1,18 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { shallow } from 'enzyme'; + +import { ReadOnlyCallOut } from './index'; + +describe('ReadOnlyCallOut', () => { + it('renders correctly', () => { + const wrapper = shallow(<ReadOnlyCallOut />); + + expect(wrapper.find('EuiCallOut')).toHaveLength(1); + }); +}); diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/rule_actions_field/index.test.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/rule_actions_field/index.test.tsx new file mode 100644 index 0000000000000..4cfad36b2933f --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/rule_actions_field/index.test.tsx @@ -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 React from 'react'; +import { shallow } from 'enzyme'; + +import { RuleActionsField } from './index'; +import { useKibana } from '../../../../../lib/kibana'; +import { useFormFieldMock } from '../../../../../mock'; +jest.mock('../../../../../lib/kibana'); + +describe('RuleActionsField', () => { + it('should not render ActionForm is no actions are supported', () => { + (useKibana as jest.Mock).mockReturnValue({ + services: { + triggers_actions_ui: { + actionTypeRegistry: {}, + }, + }, + }); + const Component = () => { + const field = useFormFieldMock(); + + return <RuleActionsField euiFieldProps={{ options: [] }} field={field} />; + }; + const wrapper = shallow(<Component />); + + expect(wrapper.dive().find('ActionForm')).toHaveLength(0); + }); +}); diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/rule_actions_field/index.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/rule_actions_field/index.tsx index a746d381c494c..b4d813c48b43f 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/rule_actions_field/index.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/rule_actions_field/index.tsx @@ -7,6 +7,7 @@ import React, { useCallback, useEffect, useState } from 'react'; import deepMerge from 'deepmerge'; +import { NOTIFICATION_SUPPORTED_ACTION_TYPES_IDS } from '../../../../../../../../../plugins/siem/common/constants'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths import { loadActionTypes } from '../../../../../../../../../plugins/triggers_actions_ui/public/application/lib/action_connector_api'; import { SelectField } from '../../../../../shared_imports'; @@ -16,7 +17,6 @@ import { } from '../../../../../../../../../plugins/triggers_actions_ui/public'; import { AlertAction } from '../../../../../../../../../plugins/alerting/common'; import { useKibana } from '../../../../../lib/kibana'; -import { NOTIFICATION_SUPPORTED_ACTION_TYPES_IDS } from '../../../../../../common/constants'; type ThrottleSelectField = typeof SelectField; diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/rule_status/helpers.test.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/rule_status/helpers.test.tsx new file mode 100644 index 0000000000000..aba30e4b7f3ca --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/rule_status/helpers.test.tsx @@ -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 { getStatusColor } from './helpers'; + +describe('rule_status helpers', () => { + it('getStatusColor returns subdued if null was provided', () => { + expect(getStatusColor(null)).toBe('subdued'); + }); +}); diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/rule_status/index.test.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/rule_status/index.test.tsx new file mode 100644 index 0000000000000..6e230de11c4f3 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/rule_status/index.test.tsx @@ -0,0 +1,18 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { shallow } from 'enzyme'; + +import { RuleStatus } from './index'; + +describe('RuleStatus', () => { + it('renders loader correctly', () => { + const wrapper = shallow(<RuleStatus ruleId="ruleId" ruleEnabled={true} />); + + expect(wrapper.dive().find('[data-test-subj="rule-status-loader"]')).toHaveLength(1); + }); +}); diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/schedule_item_form/index.test.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/schedule_item_form/index.test.tsx new file mode 100644 index 0000000000000..3829af02ca4f1 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/schedule_item_form/index.test.tsx @@ -0,0 +1,31 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { shallow } from 'enzyme'; + +import { ScheduleItem } from './index'; +import { useFormFieldMock } from '../../../../../mock'; + +describe('ScheduleItem', () => { + it('renders correctly', () => { + const Component = () => { + const field = useFormFieldMock(); + + return ( + <ScheduleItem + dataTestSubj="schedule-item" + idAria="idAria" + isDisabled={false} + field={field} + /> + ); + }; + const wrapper = shallow(<Component />); + + expect(wrapper.dive().find('[data-test-subj="schedule-item"]')).toHaveLength(1); + }); +}); diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/select_rule_type/index.test.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/select_rule_type/index.test.tsx new file mode 100644 index 0000000000000..3d832d61abb28 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/select_rule_type/index.test.tsx @@ -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 React from 'react'; +import { shallow } from 'enzyme'; + +import { SelectRuleType } from './index'; +import { useFormFieldMock } from '../../../../../mock'; +jest.mock('../../../../../lib/kibana'); + +describe('SelectRuleType', () => { + it('renders correctly', () => { + const Component = () => { + const field = useFormFieldMock(); + + return <SelectRuleType field={field} />; + }; + const wrapper = shallow(<Component />); + + expect(wrapper.dive().find('[data-test-subj="selectRuleType"]')).toHaveLength(1); + }); +}); diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/select_rule_type/index.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/select_rule_type/index.tsx index 9d3b37f1788fa..2b1e5a367a965 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/select_rule_type/index.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/select_rule_type/index.tsx @@ -16,12 +16,19 @@ import { EuiText, } from '@elastic/eui'; -import { isMlRule } from '../../../../../../common/detection_engine/ml_helpers'; -import { RuleType } from '../../../../../../common/detection_engine/types'; +import { isMlRule } from '../../../../../../../../../plugins/siem/common/detection_engine/ml_helpers'; +import { RuleType } from '../../../../../../../../../plugins/siem/common/detection_engine/types'; import { FieldHook } from '../../../../../shared_imports'; +import { useKibana } from '../../../../../lib/kibana'; import * as i18n from './translations'; -const MlCardDescription = ({ hasValidLicense = false }: { hasValidLicense?: boolean }) => ( +const MlCardDescription = ({ + subscriptionUrl, + hasValidLicense = false, +}: { + subscriptionUrl: string; + hasValidLicense?: boolean; +}) => ( <EuiText size="s"> {hasValidLicense ? ( i18n.ML_TYPE_DESCRIPTION @@ -31,7 +38,7 @@ const MlCardDescription = ({ hasValidLicense = false }: { hasValidLicense?: bool defaultMessage="Access to ML requires a {subscriptionsLink}." values={{ subscriptionsLink: ( - <EuiLink href="https://www.elastic.co/subscriptions" target="_blank"> + <EuiLink href={subscriptionUrl} target="_blank"> <FormattedMessage id="xpack.siem.components.stepDefineRule.ruleTypeField.subscriptionsLink" defaultMessage="Platinum subscription" @@ -69,6 +76,9 @@ export const SelectRuleType: React.FC<SelectRuleTypeProps> = ({ const setMl = useCallback(() => setType('machine_learning'), [setType]); const setQuery = useCallback(() => setType('query'), [setType]); const mlCardDisabled = isReadOnly || !hasValidLicense || !isMlAdmin; + const licensingUrl = useKibana().services.application.getUrlForApp('kibana', { + path: '#/management/elasticsearch/license_management', + }); return ( <EuiFormRow @@ -95,7 +105,9 @@ export const SelectRuleType: React.FC<SelectRuleTypeProps> = ({ <EuiCard data-test-subj="machineLearningRuleType" title={i18n.ML_TYPE_TITLE} - description={<MlCardDescription hasValidLicense={hasValidLicense} />} + description={ + <MlCardDescription subscriptionUrl={licensingUrl} hasValidLicense={hasValidLicense} /> + } icon={<EuiIcon size="l" type="machineLearningApp" />} isDisabled={mlCardDisabled} selectable={{ diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/severity_badge/index.test.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/severity_badge/index.test.tsx new file mode 100644 index 0000000000000..a9dddfedc2bab --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/severity_badge/index.test.tsx @@ -0,0 +1,18 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { shallow } from 'enzyme'; + +import { SeverityBadge } from './index'; + +describe('SeverityBadge', () => { + it('renders correctly', () => { + const wrapper = shallow(<SeverityBadge value="low" />); + + expect(wrapper.find('EuiHealth')).toHaveLength(1); + }); +}); diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/status_icon/index.test.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/status_icon/index.test.tsx new file mode 100644 index 0000000000000..89b8a56e79054 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/status_icon/index.test.tsx @@ -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 React from 'react'; +import { shallow } from 'enzyme'; + +import { TestProviders } from '../../../../../mock'; +import { RuleStatusIcon } from './index'; +jest.mock('../../../../../lib/kibana'); + +describe('RuleStatusIcon', () => { + it('renders correctly', () => { + const wrapper = shallow(<RuleStatusIcon name="name" type="active" />, { + wrappingComponent: TestProviders, + }); + + expect(wrapper.find('EuiAvatar')).toHaveLength(1); + }); +}); diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_content_wrapper/index.test.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_content_wrapper/index.test.tsx new file mode 100644 index 0000000000000..af0547ea03261 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_content_wrapper/index.test.tsx @@ -0,0 +1,18 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { shallow } from 'enzyme'; + +import { StepContentWrapper } from './index'; + +describe('StepContentWrapper', () => { + it('renders correctly', () => { + const wrapper = shallow(<StepContentWrapper />); + + expect(wrapper.find('div')).toHaveLength(1); + }); +}); diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_content_wrapper/index.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_content_wrapper/index.tsx index b04a321dab05b..a7343a87a7ef8 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_content_wrapper/index.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_content_wrapper/index.tsx @@ -16,3 +16,5 @@ StyledDiv.defaultProps = { }; export const StepContentWrapper = React.memo(StyledDiv); + +StepContentWrapper.displayName = 'StepContentWrapper'; diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_define_rule/index.test.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_define_rule/index.test.tsx new file mode 100644 index 0000000000000..ebef6348d477e --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_define_rule/index.test.tsx @@ -0,0 +1,20 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { shallow } from 'enzyme'; + +import { StepDefineRule } from './index'; + +jest.mock('../../../../../lib/kibana'); + +describe('StepDefineRule', () => { + it('renders correctly', () => { + const wrapper = shallow(<StepDefineRule isReadOnlyView={false} isLoading={false} />); + + expect(wrapper.find('Form[data-test-subj="stepDefineRule"]')).toHaveLength(1); + }); +}); diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_define_rule/index.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_define_rule/index.tsx index 05043e5b96a30..be9e919b806b5 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_define_rule/index.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_define_rule/index.tsx @@ -9,10 +9,10 @@ import React, { FC, memo, useCallback, useState, useEffect } from 'react'; import styled from 'styled-components'; import deepEqual from 'fast-deep-equal'; +import { DEFAULT_INDEX_KEY } from '../../../../../../../../../plugins/siem/common/constants'; +import { isMlRule } from '../../../../../../../../../plugins/siem/common/detection_engine/ml_helpers'; import { IIndexPattern } from '../../../../../../../../../../src/plugins/data/public'; import { useFetchIndexPatterns } from '../../../../../containers/detection_engine/rules'; -import { DEFAULT_INDEX_KEY } from '../../../../../../common/constants'; -import { isMlRule } from '../../../../../../common/detection_engine/ml_helpers'; import { DEFAULT_TIMELINE_TITLE } from '../../../../../components/timeline/translations'; import { useMlCapabilities } from '../../../../../components/ml_popover/hooks/use_ml_capabilities'; import { useUiSetting$ } from '../../../../../lib/kibana'; diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_define_rule/schema.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_define_rule/schema.tsx index 4a132f94a9871..629c6758a1414 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_define_rule/schema.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_define_rule/schema.tsx @@ -9,8 +9,8 @@ import { EuiText } from '@elastic/eui'; import { isEmpty } from 'lodash/fp'; import React from 'react'; +import { isMlRule } from '../../../../../../../../../plugins/siem/common/detection_engine/ml_helpers'; import { esKuery } from '../../../../../../../../../../src/plugins/data/public'; -import { isMlRule } from '../../../../../../common/detection_engine/ml_helpers'; import { FieldValueQueryBar } from '../query_bar'; import { ERROR_CODE, diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_panel/index.test.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_panel/index.test.tsx new file mode 100644 index 0000000000000..ce01d6995a2d8 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_panel/index.test.tsx @@ -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 React from 'react'; +import { shallow } from 'enzyme'; + +import { StepPanel } from './index'; + +describe('StepPanel', () => { + it('renders correctly', () => { + const wrapper = shallow( + <StepPanel loading={false} title="Title"> + <div /> + </StepPanel> + ); + + expect(wrapper.find('MyPanel')).toHaveLength(1); + }); +}); diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_panel/index.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_panel/index.tsx index 88cecadb8b137..1923ed09252dd 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_panel/index.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_panel/index.tsx @@ -20,6 +20,8 @@ const MyPanel = styled(EuiPanel)` position: relative; `; +MyPanel.displayName = 'MyPanel'; + const StepPanelComponent: React.FC<StepPanelProps> = ({ children, loading, title }) => ( <MyPanel> {loading && <EuiProgress size="xs" color="accent" position="absolute" />} diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_rule_actions/index.test.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_rule_actions/index.test.tsx new file mode 100644 index 0000000000000..69d118ba9f28e --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_rule_actions/index.test.tsx @@ -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 React from 'react'; +import { shallow } from 'enzyme'; + +import { StepRuleActions } from './index'; + +jest.mock('../../../../../lib/kibana'); + +describe('StepRuleActions', () => { + it('renders correctly', () => { + const wrapper = shallow( + <StepRuleActions actionMessageParams={[]} isReadOnlyView={false} isLoading={false} /> + ); + + expect(wrapper.find('[data-test-subj="stepRuleActions"]')).toHaveLength(1); + }); +}); diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_rule_actions/schema.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_rule_actions/schema.tsx index bc3b0dfe720bc..1b27d0e0fcc0e 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_rule_actions/schema.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_rule_actions/schema.tsx @@ -4,6 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ +/* istanbul ignore file */ + import { i18n } from '@kbn/i18n'; import { FormSchema } from '../../../../../shared_imports'; diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_schedule_rule/index.test.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_schedule_rule/index.test.tsx new file mode 100644 index 0000000000000..98de933590d60 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_schedule_rule/index.test.tsx @@ -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 React from 'react'; +import { shallow, mount } from 'enzyme'; + +import { TestProviders } from '../../../../../mock'; +import { StepScheduleRule } from './index'; + +describe('StepScheduleRule', () => { + it('renders correctly', () => { + const wrapper = mount(<StepScheduleRule isReadOnlyView={false} isLoading={false} />, { + wrappingComponent: TestProviders, + }); + + expect(wrapper.find('Form[data-test-subj="stepScheduleRule"]')).toHaveLength(1); + }); + + it('renders correctly if isReadOnlyView', () => { + const wrapper = shallow(<StepScheduleRule isReadOnlyView={true} isLoading={false} />); + + expect(wrapper.find('StepContentWrapper')).toHaveLength(1); + }); +}); diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_schedule_rule/schema.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_schedule_rule/schema.tsx index 8fbfdf5f25a51..e79aec2be6e15 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_schedule_rule/schema.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_schedule_rule/schema.tsx @@ -4,6 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ +/* istanbul ignore file */ + import { i18n } from '@kbn/i18n'; import { OptionalFieldLabel } from '../optional_field_label'; diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/throttle_select_field/index.test.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/throttle_select_field/index.test.tsx new file mode 100644 index 0000000000000..0ab19b671494e --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/throttle_select_field/index.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 from 'react'; +import { shallow } from 'enzyme'; + +import { ThrottleSelectField } from './index'; +import { useFormFieldMock } from '../../../../../mock'; + +describe('ThrottleSelectField', () => { + it('renders correctly', () => { + const Component = () => { + const field = useFormFieldMock(); + + return <ThrottleSelectField field={field} euiFieldProps={{ options: [] }} />; + }; + const wrapper = shallow(<Component />); + + expect(wrapper.dive().find('SelectField')).toHaveLength(1); + }); +}); diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/throttle_select_field/index.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/throttle_select_field/index.tsx index 0cf15c41a0f91..3b297a623e34d 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/throttle_select_field/index.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/throttle_select_field/index.tsx @@ -9,7 +9,7 @@ import React, { useCallback } from 'react'; import { NOTIFICATION_THROTTLE_RULE, NOTIFICATION_THROTTLE_NO_ACTIONS, -} from '../../../../../../common/constants'; +} from '../../../../../../../../../plugins/siem/common/constants'; import { SelectField } from '../../../../../shared_imports'; export const THROTTLE_OPTIONS = [ diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/create/helpers.ts b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/create/helpers.ts index 7ad116c313361..a65e8178f75c4 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/create/helpers.ts +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/create/helpers.ts @@ -8,10 +8,10 @@ import { has, isEmpty } from 'lodash/fp'; import moment from 'moment'; import deepmerge from 'deepmerge'; -import { NOTIFICATION_THROTTLE_NO_ACTIONS } from '../../../../../common/constants'; -import { transformAlertToRuleAction } from '../../../../../common/detection_engine/transform_actions'; -import { RuleType } from '../../../../../common/detection_engine/types'; -import { isMlRule } from '../../../../../common/detection_engine/ml_helpers'; +import { NOTIFICATION_THROTTLE_NO_ACTIONS } from '../../../../../../../../plugins/siem/common/constants'; +import { transformAlertToRuleAction } from '../../../../../../../../plugins/siem/common/detection_engine/transform_actions'; +import { RuleType } from '../../../../../../../../plugins/siem/common/detection_engine/types'; +import { isMlRule } from '../../../../../../../../plugins/siem/common/detection_engine/ml_helpers'; import { NewRule } from '../../../../containers/detection_engine/rules'; import { diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/create/index.test.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/create/index.test.tsx new file mode 100644 index 0000000000000..db32be652d0f7 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/create/index.test.tsx @@ -0,0 +1,23 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { shallow } from 'enzyme'; + +import { TestProviders } from '../../../../mock'; +import { CreateRulePage } from './index'; +import { useUserInfo } from '../../components/user_info'; + +jest.mock('../../components/user_info'); + +describe('CreateRulePage', () => { + it('renders correctly', () => { + (useUserInfo as jest.Mock).mockReturnValue({}); + const wrapper = shallow(<CreateRulePage />, { wrappingComponent: TestProviders }); + + expect(wrapper.find('[title="Create new rule"]')).toHaveLength(1); + }); +}); diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/details/failure_history.test.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/details/failure_history.test.tsx new file mode 100644 index 0000000000000..a83ff4c54b076 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/details/failure_history.test.tsx @@ -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 React from 'react'; +import { shallow } from 'enzyme'; + +import { TestProviders } from '../../../../mock'; +import { FailureHistory } from './failure_history'; +import { useRuleStatus } from '../../../../containers/detection_engine/rules'; +jest.mock('../../../../containers/detection_engine/rules'); + +describe('FailureHistory', () => { + beforeAll(() => { + (useRuleStatus as jest.Mock).mockReturnValue([false, null]); + }); + + it('renders correctly', () => { + const wrapper = shallow(<FailureHistory id="id" />, { + wrappingComponent: TestProviders, + }); + + expect(wrapper.find('EuiBasicTable')).toHaveLength(1); + }); +}); diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/details/index.test.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/details/index.test.tsx new file mode 100644 index 0000000000000..19c6f39a9bc7e --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/details/index.test.tsx @@ -0,0 +1,47 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { shallow } from 'enzyme'; + +import '../../../../mock/match_media'; +import { TestProviders } from '../../../../mock'; +import { RuleDetailsPageComponent } from './index'; +import { setAbsoluteRangeDatePicker } from '../../../../store/inputs/actions'; +import { useUserInfo } from '../../components/user_info'; +import { useParams } from 'react-router-dom'; + +jest.mock('../../components/user_info'); +jest.mock('react-router-dom', () => { + const originalModule = jest.requireActual('react-router-dom'); + + return { + ...originalModule, + useParams: jest.fn(), + }; +}); + +describe('RuleDetailsPageComponent', () => { + beforeAll(() => { + (useUserInfo as jest.Mock).mockReturnValue({}); + (useParams as jest.Mock).mockReturnValue({}); + }); + + it('renders correctly', () => { + const wrapper = shallow( + <RuleDetailsPageComponent + query={{ query: '', language: 'language' }} + filters={[]} + setAbsoluteRangeDatePicker={setAbsoluteRangeDatePicker} + />, + { + wrappingComponent: TestProviders, + } + ); + + expect(wrapper.find('WithSource')).toHaveLength(1); + }); +}); diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/details/index.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/details/index.tsx index 2b648a3b3f825..14e5f2b90882e 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/details/index.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/details/index.tsx @@ -88,7 +88,7 @@ const ruleDetailTabs = [ }, ]; -const RuleDetailsPageComponent: FC<PropsFromRedux> = ({ +export const RuleDetailsPageComponent: FC<PropsFromRedux> = ({ filters, query, setAbsoluteRangeDatePicker, diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/details/status_failed_callout.test.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/details/status_failed_callout.test.tsx new file mode 100644 index 0000000000000..3394b0fc8c5c0 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/details/status_failed_callout.test.tsx @@ -0,0 +1,18 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { shallow } from 'enzyme'; + +import { RuleStatusFailedCallOut } from './status_failed_callout'; + +describe('RuleStatusFailedCallOut', () => { + it('renders correctly', () => { + const wrapper = shallow(<RuleStatusFailedCallOut date="date" message="message" />); + + expect(wrapper.find('EuiCallOut')).toHaveLength(1); + }); +}); diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/edit/index.test.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/edit/index.test.tsx new file mode 100644 index 0000000000000..d22bc12abf9fa --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/edit/index.test.tsx @@ -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 React from 'react'; +import { shallow } from 'enzyme'; + +import { TestProviders } from '../../../../mock'; +import { EditRulePage } from './index'; +import { useUserInfo } from '../../components/user_info'; +import { useParams } from 'react-router-dom'; + +jest.mock('../../components/user_info'); +jest.mock('react-router-dom', () => { + const originalModule = jest.requireActual('react-router-dom'); + + return { + ...originalModule, + useParams: jest.fn(), + }; +}); + +describe('EditRulePage', () => { + it('renders correctly', () => { + (useUserInfo as jest.Mock).mockReturnValue({}); + (useParams as jest.Mock).mockReturnValue({}); + const wrapper = shallow(<EditRulePage />, { wrappingComponent: TestProviders }); + + expect(wrapper.find('[title="Edit rule settings"]')).toHaveLength(1); + }); +}); diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/helpers.test.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/helpers.test.tsx index 443dbd2c93a35..1c01a19573cd6 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/helpers.test.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/helpers.test.tsx @@ -302,11 +302,25 @@ describe('rule helpers', () => { test('returns expected ActionsStepRule rule object', () => { const mockedRule = { ...mockRule('test-id'), - actions: [], + actions: [ + { + id: 'id', + group: 'group', + params: {}, + action_type_id: 'action_type_id', + }, + ], }; const result: ActionsStepRule = getActionsStepsData(mockedRule); const expected = { - actions: [], + actions: [ + { + id: 'id', + group: 'group', + params: {}, + actionTypeId: 'action_type_id', + }, + ], enabled: mockedRule.enabled, isNew: false, throttle: 'no_actions', diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/helpers.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/helpers.tsx index 58a1b0fd3133e..7bea41c2ab4d5 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/helpers.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/helpers.tsx @@ -10,9 +10,12 @@ import moment from 'moment'; import memoizeOne from 'memoize-one'; import { useLocation } from 'react-router-dom'; -import { RuleAlertAction, RuleType } from '../../../../common/detection_engine/types'; -import { isMlRule } from '../../../../common/detection_engine/ml_helpers'; -import { transformRuleToAlertAction } from '../../../../common/detection_engine/transform_actions'; +import { + RuleAlertAction, + RuleType, +} from '../../../../../../../plugins/siem/common/detection_engine/types'; +import { isMlRule } from '../../../../../../../plugins/siem/common/detection_engine/ml_helpers'; +import { transformRuleToAlertAction } from '../../../../../../../plugins/siem/common/detection_engine/transform_actions'; import { Filter } from '../../../../../../../../src/plugins/data/public'; import { Rule } from '../../../containers/detection_engine/rules'; import { FormData, FormHook, FormSchema } from '../../../shared_imports'; diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/index.test.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/index.test.tsx new file mode 100644 index 0000000000000..3fa81ca3ced08 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/index.test.tsx @@ -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 React from 'react'; +import { shallow } from 'enzyme'; + +import { RulesPage } from './index'; +import { useUserInfo } from '../components/user_info'; +import { usePrePackagedRules } from '../../../containers/detection_engine/rules'; + +jest.mock('../components/user_info'); +jest.mock('../../../containers/detection_engine/rules'); + +describe('RulesPage', () => { + beforeAll(() => { + (useUserInfo as jest.Mock).mockReturnValue({}); + (usePrePackagedRules as jest.Mock).mockReturnValue({}); + }); + it('renders correctly', () => { + const wrapper = shallow(<RulesPage />); + + expect(wrapper.find('AllRules')).toHaveLength(1); + }); +}); diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/types.ts b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/types.ts index 1c366e6640b29..380ef52190349 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/types.ts +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/types.ts @@ -4,8 +4,11 @@ * you may not use this file except in compliance with the Elastic License. */ +import { + RuleAlertAction, + RuleType, +} from '../../../../../../../plugins/siem/common/detection_engine/types'; import { AlertAction } from '../../../../../../../plugins/alerting/common'; -import { RuleAlertAction, RuleType } from '../../../../common/detection_engine/types'; import { Filter } from '../../../../../../../../src/plugins/data/common'; import { FieldValueQueryBar } from './components/query_bar'; import { FormData, FormHook } from '../../../shared_imports'; diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/utils.test.ts b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/utils.test.ts new file mode 100644 index 0000000000000..34a521ed32b12 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/utils.test.ts @@ -0,0 +1,24 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { getBreadcrumbs } from './utils'; + +describe('getBreadcrumbs', () => { + it('returns default value for incorrect params', () => { + expect( + getBreadcrumbs( + { + pageName: 'pageName', + detailName: 'detailName', + tabName: undefined, + search: '', + pathName: 'pathName', + }, + [] + ) + ).toEqual([{ href: '#/link-to/detections', text: 'Detections' }]); + }); +}); diff --git a/x-pack/legacy/plugins/siem/public/pages/hosts/navigation/types.ts b/x-pack/legacy/plugins/siem/public/pages/hosts/navigation/types.ts index cb5fc62b96582..207b86fee02b9 100644 --- a/x-pack/legacy/plugins/siem/public/pages/hosts/navigation/types.ts +++ b/x-pack/legacy/plugins/siem/public/pages/hosts/navigation/types.ts @@ -4,9 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ +import { ESTermQuery } from '../../../../../../../plugins/siem/common/typed_json'; import { Filter, IIndexPattern } from '../../../../../../../../src/plugins/data/public'; import { NarrowDateRange } from '../../../components/ml/types'; -import { ESTermQuery } from '../../../../common/typed_json'; import { InspectQuery, Refetch } from '../../../store/inputs/model'; import { HostsTableType, HostsType } from '../../../store/hosts/model'; diff --git a/x-pack/legacy/plugins/siem/public/pages/network/ip_details/types.ts b/x-pack/legacy/plugins/siem/public/pages/network/ip_details/types.ts index ef989fb64eabe..efd9c644ec6b6 100644 --- a/x-pack/legacy/plugins/siem/public/pages/network/ip_details/types.ts +++ b/x-pack/legacy/plugins/siem/public/pages/network/ip_details/types.ts @@ -6,8 +6,8 @@ import { IIndexPattern } from 'src/plugins/data/public'; +import { ESTermQuery } from '../../../../../../../plugins/siem/common/typed_json'; import { NetworkType } from '../../../store/network/model'; -import { ESTermQuery } from '../../../../common/typed_json'; import { InspectQuery, Refetch } from '../../../store/inputs/model'; import { FlowTarget, FlowTargetSourceDest } from '../../../graphql/types'; import { GlobalTimeArgs } from '../../../containers/global_time'; diff --git a/x-pack/legacy/plugins/siem/public/pages/network/navigation/types.ts b/x-pack/legacy/plugins/siem/public/pages/network/navigation/types.ts index 222a99992917d..90c18b6ff69f4 100644 --- a/x-pack/legacy/plugins/siem/public/pages/network/navigation/types.ts +++ b/x-pack/legacy/plugins/siem/public/pages/network/navigation/types.ts @@ -4,12 +4,12 @@ * you may not use this file except in compliance with the Elastic License. */ +import { ESTermQuery } from '../../../../../../../plugins/siem/common/typed_json'; import { IIndexPattern } from '../../../../../../../../src/plugins/data/common/'; import { NavTab } from '../../../components/navigation/types'; import { FlowTargetSourceDest } from '../../../graphql/types'; import { networkModel } from '../../../store'; -import { ESTermQuery } from '../../../../common/typed_json'; import { GlobalTimeArgs } from '../../../containers/global_time'; import { SetAbsoluteRangeDatePicker } from '../types'; diff --git a/x-pack/legacy/plugins/siem/public/pages/overview/alerts_by_category/index.test.tsx b/x-pack/legacy/plugins/siem/public/pages/overview/alerts_by_category/index.test.tsx index d838b936a2d65..bd9743bdccb4b 100644 --- a/x-pack/legacy/plugins/siem/public/pages/overview/alerts_by_category/index.test.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/overview/alerts_by_category/index.test.tsx @@ -66,8 +66,8 @@ describe('Alerts by category', () => { ); }); - test('it does NOT render the subtitle', () => { - expect(wrapper.find('[data-test-subj="header-panel-subtitle"]').exists()).toBe(false); + test('it renders the subtitle (to prevent layout thrashing)', () => { + expect(wrapper.find('[data-test-subj="header-panel-subtitle"]').exists()).toBe(true); }); test('it renders the expected filter fields', () => { diff --git a/x-pack/legacy/plugins/siem/public/pages/overview/alerts_by_category/index.tsx b/x-pack/legacy/plugins/siem/public/pages/overview/alerts_by_category/index.tsx index 744102fbac4b3..8e09572cb2796 100644 --- a/x-pack/legacy/plugins/siem/public/pages/overview/alerts_by_category/index.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/overview/alerts_by_category/index.tsx @@ -7,9 +7,9 @@ import { EuiButton } from '@elastic/eui'; import numeral from '@elastic/numeral'; import React, { useEffect, useMemo } from 'react'; - import { Position } from '@elastic/charts'; -import { DEFAULT_NUMBER_FORMAT } from '../../../../common/constants'; + +import { DEFAULT_NUMBER_FORMAT } from '../../../../../../../plugins/siem/common/constants'; import { SHOWING, UNIT } from '../../../components/alerts_viewer/translations'; import { getDetectionEngineAlertUrl } from '../../../components/link_to/redirect_to_detection_engine'; import { MatrixHistogramContainer } from '../../../components/matrix_histogram'; diff --git a/x-pack/legacy/plugins/siem/public/pages/overview/events_by_dataset/__mocks__/index.tsx b/x-pack/legacy/plugins/siem/public/pages/overview/events_by_dataset/__mocks__/index.tsx new file mode 100644 index 0000000000000..dad1e0034b4e2 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/pages/overview/events_by_dataset/__mocks__/index.tsx @@ -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 EventsByDataset = () => 'mock EventsByDataset'; diff --git a/x-pack/legacy/plugins/siem/public/pages/overview/events_by_dataset/index.tsx b/x-pack/legacy/plugins/siem/public/pages/overview/events_by_dataset/index.tsx index cc1f9b1cc5681..14cc29adb505a 100644 --- a/x-pack/legacy/plugins/siem/public/pages/overview/events_by_dataset/index.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/overview/events_by_dataset/index.tsx @@ -4,18 +4,26 @@ * you may not use this file except in compliance with the Elastic License. */ +import { Position } from '@elastic/charts'; import { EuiButton } from '@elastic/eui'; import numeral from '@elastic/numeral'; import React, { useEffect, useMemo } from 'react'; +import uuid from 'uuid'; -import { Position } from '@elastic/charts'; +import { DEFAULT_NUMBER_FORMAT } from '../../../../../../../plugins/siem/common/constants'; import { SHOWING, UNIT } from '../../../components/events_viewer/translations'; -import { convertToBuildEsQuery } from '../../../lib/keury'; import { getTabsOnHostsUrl } from '../../../components/link_to/redirect_to_hosts'; -import { histogramConfigs } from '../../../pages/hosts/navigation/events_query_tab_body'; import { MatrixHistogramContainer } from '../../../components/matrix_histogram'; +import { + MatrixHisrogramConfigs, + MatrixHistogramOption, +} from '../../../components/matrix_histogram/types'; +import { useGetUrlSearch } from '../../../components/navigation/use_get_url_search'; +import { navTabs } from '../../home/home_navigations'; import { eventsStackByOptions } from '../../hosts/navigation'; +import { convertToBuildEsQuery } from '../../../lib/keury'; import { useKibana, useUiSetting$ } from '../../../lib/kibana'; +import { histogramConfigs } from '../../../pages/hosts/navigation/events_query_tab_body'; import { Filter, esQuery, @@ -24,12 +32,9 @@ import { } from '../../../../../../../../src/plugins/data/public'; import { inputsModel } from '../../../store'; import { HostsTableType, HostsType } from '../../../store/hosts/model'; -import { DEFAULT_NUMBER_FORMAT } from '../../../../common/constants'; +import { InputsModelId } from '../../../store/inputs/constants'; import * as i18n from '../translations'; -import { MatrixHisrogramConfigs } from '../../../components/matrix_histogram/types'; -import { useGetUrlSearch } from '../../../components/navigation/use_get_url_search'; -import { navTabs } from '../../home/home_navigations'; const NO_FILTERS: Filter[] = []; const DEFAULT_QUERY: Query = { query: '', language: 'kuery' }; @@ -38,36 +43,56 @@ const DEFAULT_STACK_BY = 'event.dataset'; const ID = 'eventsByDatasetOverview'; interface Props { + combinedQueries?: string; deleteQuery?: ({ id }: { id: string }) => void; filters?: Filter[]; from: number; + headerChildren?: React.ReactNode; indexPattern: IIndexPattern; + indexToAdd?: string[] | null; + onlyField?: string; query?: Query; + setAbsoluteRangeDatePickerTarget?: InputsModelId; setQuery: (params: { id: string; inspect: inputsModel.InspectQuery | null; loading: boolean; refetch: inputsModel.Refetch; }) => void; + showSpacer?: boolean; to: number; } +const getHistogramOption = (fieldName: string): MatrixHistogramOption => ({ + text: fieldName, + value: fieldName, +}); + const EventsByDatasetComponent: React.FC<Props> = ({ + combinedQueries, deleteQuery, filters = NO_FILTERS, from, + headerChildren, indexPattern, + indexToAdd, + onlyField, query = DEFAULT_QUERY, + setAbsoluteRangeDatePickerTarget, setQuery, + showSpacer = true, to, }) => { + // create a unique, but stable (across re-renders) query id + const uniqueQueryId = useMemo(() => `${ID}-${uuid.v4()}`, []); + useEffect(() => { return () => { if (deleteQuery) { - deleteQuery({ id: ID }); + deleteQuery({ id: uniqueQueryId }); } }; - }, [deleteQuery]); + }, [deleteQuery, uniqueQueryId]); const kibana = useKibana(); const [defaultNumberFormat] = useUiSetting$<string>(DEFAULT_NUMBER_FORMAT); @@ -84,38 +109,62 @@ const EventsByDatasetComponent: React.FC<Props> = ({ const filterQuery = useMemo( () => - convertToBuildEsQuery({ - config: esQuery.getEsQueryConfig(kibana.services.uiSettings), - indexPattern, - queries: [query], - filters, - }), - [kibana, indexPattern, query, filters] + combinedQueries == null + ? convertToBuildEsQuery({ + config: esQuery.getEsQueryConfig(kibana.services.uiSettings), + indexPattern, + queries: [query], + filters, + }) + : combinedQueries, + [combinedQueries, kibana, indexPattern, query, filters] ); const eventsByDatasetHistogramConfigs: MatrixHisrogramConfigs = useMemo( () => ({ ...histogramConfigs, + stackByOptions: + onlyField != null ? [getHistogramOption(onlyField)] : histogramConfigs.stackByOptions, defaultStackByOption: - eventsStackByOptions.find(o => o.text === DEFAULT_STACK_BY) ?? eventsStackByOptions[0], + onlyField != null + ? getHistogramOption(onlyField) + : eventsStackByOptions.find(o => o.text === DEFAULT_STACK_BY) ?? eventsStackByOptions[0], legendPosition: Position.Right, subtitle: (totalCount: number) => `${SHOWING}: ${numeral(totalCount).format(defaultNumberFormat)} ${UNIT(totalCount)}`, + titleSize: onlyField == null ? 'm' : 's', }), - [] + [onlyField, defaultNumberFormat] ); + const headerContent = useMemo(() => { + if (onlyField == null || headerChildren != null) { + return ( + <> + {headerChildren} + {onlyField == null && eventsCountViewEventsButton} + </> + ); + } else { + return null; + } + }, [onlyField, headerChildren, eventsCountViewEventsButton]); + return ( <MatrixHistogramContainer endDate={to} filterQuery={filterQuery} - headerChildren={eventsCountViewEventsButton} - id={ID} + headerChildren={headerContent} + id={uniqueQueryId} + indexToAdd={indexToAdd} + setAbsoluteRangeDatePickerTarget={setAbsoluteRangeDatePickerTarget} setQuery={setQuery} + showSpacer={showSpacer} sourceId="default" startDate={from} type={HostsType.page} {...eventsByDatasetHistogramConfigs} + title={onlyField != null ? i18n.TOP(onlyField) : eventsByDatasetHistogramConfigs.title} /> ); }; diff --git a/x-pack/legacy/plugins/siem/public/pages/overview/overview.tsx b/x-pack/legacy/plugins/siem/public/pages/overview/overview.tsx index 2db49e60193fc..82f4444728902 100644 --- a/x-pack/legacy/plugins/siem/public/pages/overview/overview.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/overview/overview.tsx @@ -63,7 +63,7 @@ const OverviewComponent: React.FC<PropsFromRedux> = ({ from={from} indexPattern={indexPattern} query={query} - setAbsoluteRangeDatePicker={setAbsoluteRangeDatePicker!} + setAbsoluteRangeDatePicker={setAbsoluteRangeDatePicker} setQuery={setQuery} to={to} /> diff --git a/x-pack/legacy/plugins/siem/public/pages/overview/sidebar/sidebar.tsx b/x-pack/legacy/plugins/siem/public/pages/overview/sidebar/sidebar.tsx index 52e36b472a0ec..4d4d96803cd65 100644 --- a/x-pack/legacy/plugins/siem/public/pages/overview/sidebar/sidebar.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/overview/sidebar/sidebar.tsx @@ -8,9 +8,12 @@ import { EuiFlexGroup, EuiFlexItem, EuiSpacer } from '@elastic/eui'; import React, { useMemo } from 'react'; import styled from 'styled-components'; +import { + ENABLE_NEWS_FEED_SETTING, + NEWS_FEED_URL_SETTING, +} from '../../../../../../../plugins/siem/common/constants'; import { Filters as RecentCasesFilters } from '../../../components/recent_cases/filters'; import { Filters as RecentTimelinesFilters } from '../../../components/recent_timelines/filters'; -import { ENABLE_NEWS_FEED_SETTING, NEWS_FEED_URL_SETTING } from '../../../../common/constants'; import { StatefulRecentCases } from '../../../components/recent_cases'; import { StatefulRecentTimelines } from '../../../components/recent_timelines'; import { StatefulNewsFeed } from '../../../components/news_feed'; diff --git a/x-pack/legacy/plugins/siem/public/pages/overview/signals_by_category/index.tsx b/x-pack/legacy/plugins/siem/public/pages/overview/signals_by_category/index.tsx index 5f78c4c10eb37..feba80539a11b 100644 --- a/x-pack/legacy/plugins/siem/public/pages/overview/signals_by_category/index.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/overview/signals_by_category/index.tsx @@ -12,6 +12,7 @@ import { useSignalIndex } from '../../../containers/detection_engine/signals/use import { SetAbsoluteRangeDatePicker } from '../../network/types'; import { Filter, IIndexPattern, Query } from '../../../../../../../../src/plugins/data/public'; import { inputsModel } from '../../../store'; +import { InputsModelId } from '../../../store/inputs/constants'; import * as i18n from '../translations'; const DEFAULT_QUERY: Query = { query: '', language: 'kuery' }; @@ -22,9 +23,13 @@ interface Props { deleteQuery?: ({ id }: { id: string }) => void; filters?: Filter[]; from: number; + headerChildren?: React.ReactNode; indexPattern: IIndexPattern; + /** Override all defaults, and only display this field */ + onlyField?: string; query?: Query; setAbsoluteRangeDatePicker: SetAbsoluteRangeDatePicker; + setAbsoluteRangeDatePickerTarget?: InputsModelId; setQuery: (params: { id: string; inspect: inputsModel.InspectQuery | null; @@ -38,15 +43,18 @@ const SignalsByCategoryComponent: React.FC<Props> = ({ deleteQuery, filters = NO_FILTERS, from, + headerChildren, + onlyField, query = DEFAULT_QUERY, setAbsoluteRangeDatePicker, + setAbsoluteRangeDatePickerTarget = 'global', setQuery, to, }) => { const { signalIndexName } = useSignalIndex(); const updateDateRangeCallback = useCallback( (min: number, max: number) => { - setAbsoluteRangeDatePicker!({ id: 'global', from: min, to: max }); + setAbsoluteRangeDatePicker({ id: setAbsoluteRangeDatePickerTarget, from: min, to: max }); }, [setAbsoluteRangeDatePicker] ); @@ -60,12 +68,14 @@ const SignalsByCategoryComponent: React.FC<Props> = ({ defaultStackByOption={defaultStackByOption} filters={filters} from={from} + headerChildren={headerChildren} + onlyField={onlyField} query={query} signalIndexName={signalIndexName} setQuery={setQuery} showTotalSignalsCount={true} - showLinkToSignals={true} - stackByOptions={signalsHistogramOptions} + showLinkToSignals={onlyField == null ? true : false} + stackByOptions={onlyField == null ? signalsHistogramOptions : undefined} legendPosition={'right'} to={to} title={i18n.SIGNAL_COUNT} diff --git a/x-pack/legacy/plugins/siem/public/pages/overview/translations.ts b/x-pack/legacy/plugins/siem/public/pages/overview/translations.ts index 601a629d86e57..b7bee15e4c5bf 100644 --- a/x-pack/legacy/plugins/siem/public/pages/overview/translations.ts +++ b/x-pack/legacy/plugins/siem/public/pages/overview/translations.ts @@ -38,6 +38,12 @@ export const SIGNAL_COUNT = i18n.translate('xpack.siem.overview.signalCountTitle defaultMessage: 'Signal count', }); +export const TOP = (fieldName: string) => + i18n.translate('xpack.siem.overview.topNLabel', { + values: { fieldName }, + defaultMessage: `Top {fieldName}`, + }); + export const VIEW_ALERTS = i18n.translate('xpack.siem.overview.viewAlertsButtonLabel', { defaultMessage: 'View alerts', }); diff --git a/x-pack/legacy/plugins/siem/public/pages/timelines/timelines_page.test.tsx b/x-pack/legacy/plugins/siem/public/pages/timelines/timelines_page.test.tsx index 62399891c9606..ae95a1316a600 100644 --- a/x-pack/legacy/plugins/siem/public/pages/timelines/timelines_page.test.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/timelines/timelines_page.test.tsx @@ -10,6 +10,8 @@ import { shallow, ShallowWrapper } from 'enzyme'; import React from 'react'; import ApolloClient from 'apollo-client'; +jest.mock('../../pages/overview/events_by_dataset'); + jest.mock('../../lib/kibana', () => { return { useKibana: jest.fn(), diff --git a/x-pack/legacy/plugins/siem/public/register_feature.ts b/x-pack/legacy/plugins/siem/public/register_feature.ts index ca7a22408b6ff..b5e8f78ebc560 100644 --- a/x-pack/legacy/plugins/siem/public/register_feature.ts +++ b/x-pack/legacy/plugins/siem/public/register_feature.ts @@ -6,7 +6,7 @@ import { npSetup } from 'ui/new_platform'; import { FeatureCatalogueCategory } from '../../../../../src/plugins/home/public'; -import { APP_ID } from '../common/constants'; +import { APP_ID } from '../../../../plugins/siem/common/constants'; // TODO(rylnd): move this into Plugin.setup once we're on NP npSetup.plugins.home.featureCatalogue.register({ diff --git a/x-pack/legacy/plugins/siem/public/store/inputs/model.ts b/x-pack/legacy/plugins/siem/public/store/inputs/model.ts index 04facf3b98c3b..e851caf523eb4 100644 --- a/x-pack/legacy/plugins/siem/public/store/inputs/model.ts +++ b/x-pack/legacy/plugins/siem/public/store/inputs/model.ts @@ -5,7 +5,6 @@ */ import { Dispatch } from 'redux'; -import { Omit } from '../../../common/utility_types'; import { InputsModelId } from './constants'; import { CONSTANTS } from '../../components/url_state/constants'; import { Query, Filter, SavedQuery } from '../../../../../../../src/plugins/data/public'; diff --git a/x-pack/legacy/plugins/siem/public/utils/default_date_settings.test.ts b/x-pack/legacy/plugins/siem/public/utils/default_date_settings.test.ts index 9dc179ba7a6e2..bb66067512d1f 100644 --- a/x-pack/legacy/plugins/siem/public/utils/default_date_settings.test.ts +++ b/x-pack/legacy/plugins/siem/public/utils/default_date_settings.test.ts @@ -21,7 +21,7 @@ import { DEFAULT_INTERVAL_PAUSE, DEFAULT_INTERVAL_VALUE, DEFAULT_INTERVAL_TYPE, -} from '../../common/constants'; +} from '../../../../../plugins/siem/common/constants'; import { KibanaServices } from '../lib/kibana'; import { Policy } from '../store/inputs/model'; @@ -30,7 +30,7 @@ import { Policy } from '../store/inputs/model'; // we have to repeat ourselves once const DEFAULT_FROM_DATE = '1983-05-31T13:03:54.234Z'; const DEFAULT_TO_DATE = '1990-05-31T13:03:54.234Z'; -jest.mock('../../common/constants', () => ({ +jest.mock('../../../../../plugins/siem/common/constants', () => ({ DEFAULT_FROM: '1983-05-31T13:03:54.234Z', DEFAULT_TO: '1990-05-31T13:03:54.234Z', DEFAULT_INTERVAL_PAUSE: true, diff --git a/x-pack/legacy/plugins/siem/public/utils/default_date_settings.ts b/x-pack/legacy/plugins/siem/public/utils/default_date_settings.ts index c4869a4851ae5..89f7d34d85131 100644 --- a/x-pack/legacy/plugins/siem/public/utils/default_date_settings.ts +++ b/x-pack/legacy/plugins/siem/public/utils/default_date_settings.ts @@ -15,7 +15,7 @@ import { DEFAULT_TO, DEFAULT_INTERVAL_TYPE, DEFAULT_INTERVAL_VALUE, -} from '../../common/constants'; +} from '../../../../../plugins/siem/common/constants'; import { KibanaServices } from '../lib/kibana'; import { Policy } from '../store/inputs/model'; diff --git a/x-pack/legacy/plugins/siem/reporter_config.json b/x-pack/legacy/plugins/siem/reporter_config.json deleted file mode 100644 index dda68d501f975..0000000000000 --- a/x-pack/legacy/plugins/siem/reporter_config.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "reporterEnabled": "mochawesome, mocha-junit-reporter", - "reporterOptions": { - "html": false, - "json": true, - "mochaFile": "../../../../target/kibana-siem/cypress/results/TEST-siem-cypress-[hash].xml", - "overwrite": false, - "reportDir": "../../../../target/kibana-siem/cypress/results" - } -} diff --git a/x-pack/legacy/plugins/siem/scripts/check_circular_deps.js b/x-pack/legacy/plugins/siem/scripts/check_circular_deps.js deleted file mode 100644 index 046cc010621d7..0000000000000 --- a/x-pack/legacy/plugins/siem/scripts/check_circular_deps.js +++ /dev/null @@ -1,8 +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. - */ - -require('../../../../../src/setup_node_env'); -require('../dev_tools/circular_deps/run_check_circular_deps_cli'); diff --git a/x-pack/legacy/plugins/siem/scripts/optimize_tsconfig/README.md b/x-pack/legacy/plugins/siem/scripts/optimize_tsconfig/README.md deleted file mode 100644 index d3615d2870ef9..0000000000000 --- a/x-pack/legacy/plugins/siem/scripts/optimize_tsconfig/README.md +++ /dev/null @@ -1,16 +0,0 @@ -Hard forked from here: -x-pack/legacy/plugins/apm/scripts/optimize-tsconfig.js - - -#### Optimizing TypeScript - -Kibana and X-Pack are very large TypeScript projects, and it comes at a cost. Editor responsiveness is not great, and the CLI type check for X-Pack takes about a minute. To get faster feedback, we create a smaller SIEM TypeScript project that only type checks the SIEM project and the files it uses. This optimization consists of creating a `tsconfig.json` in SIEM that includes the Kibana/X-Pack typings, and editing the Kibana/X-Pack configurations to not include any files, or removing the configurations altogether. The script configures git to ignore any changes in these files, and has an undo script as well. - -To run the optimization: - -`$ node x-pack/legacy/plugins/siem/scripts/optimize_tsconfig` - -To undo the optimization: - -`$ node x-pack/legacy/plugins/siem/scripts/unoptimize_tsconfig` - diff --git a/x-pack/legacy/plugins/siem/scripts/optimize_tsconfig/tsconfig.json b/x-pack/legacy/plugins/siem/scripts/optimize_tsconfig/tsconfig.json deleted file mode 100644 index c4705c8b8c16a..0000000000000 --- a/x-pack/legacy/plugins/siem/scripts/optimize_tsconfig/tsconfig.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "include": [ - "typings/**/*", - "plugins/siem/**/*", - "legacy/plugins/siem/**/*", - "plugins/apm/typings/numeral.d.ts", - "legacy/plugins/canvas/types/webpack.d.ts", - "plugins/triggers_actions_ui/**/*" - ], - "exclude": [ - "test/**/*", - "**/__fixtures__/**/*", - "legacy/plugins/siem/cypress/**/*", - "**/typespec_tests.ts" - ] -} diff --git a/x-pack/legacy/plugins/siem/server/client/client.ts b/x-pack/legacy/plugins/siem/server/client/client.ts deleted file mode 100644 index 245b81d0be97a..0000000000000 --- a/x-pack/legacy/plugins/siem/server/client/client.ts +++ /dev/null @@ -1,21 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { Legacy } from 'kibana'; - -import { APP_ID, SIGNALS_INDEX_KEY } from '../../common/constants'; - -export class SiemClient { - public readonly signalsIndex: string; - - constructor(private spaceId: string, private config: Legacy.Server['config']) { - const configuredSignalsIndex = this.config().get<string>( - `xpack.${APP_ID}.${SIGNALS_INDEX_KEY}` - ); - - this.signalsIndex = `${configuredSignalsIndex}-${this.spaceId}`; - } -} diff --git a/x-pack/legacy/plugins/siem/server/client/factory.ts b/x-pack/legacy/plugins/siem/server/client/factory.ts deleted file mode 100644 index d31920bdf2c77..0000000000000 --- a/x-pack/legacy/plugins/siem/server/client/factory.ts +++ /dev/null @@ -1,36 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { Legacy } from 'kibana'; - -import { KibanaRequest } from '../../../../../../src/core/server'; -import { SiemClient } from './client'; - -interface SetupDependencies { - getSpaceId?: (request: KibanaRequest) => string | undefined; - config: Legacy.Server['config']; -} - -export class SiemClientFactory { - private getSpaceId?: SetupDependencies['getSpaceId']; - private config?: SetupDependencies['config']; - - public setup({ getSpaceId, config }: SetupDependencies) { - this.getSpaceId = getSpaceId; - this.config = config; - } - - public create(request: KibanaRequest): SiemClient { - if (this.config == null) { - throw new Error( - 'Cannot create SiemClient as config is not present. Did you forget to call setup()?' - ); - } - - const spaceId = this.getSpaceId?.(request) ?? 'default'; - return new SiemClient(spaceId, this.config); - } -} diff --git a/x-pack/legacy/plugins/siem/server/index.ts b/x-pack/legacy/plugins/siem/server/index.ts deleted file mode 100644 index 8513f871cb6c1..0000000000000 --- a/x-pack/legacy/plugins/siem/server/index.ts +++ /dev/null @@ -1,12 +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 { PluginInitializerContext } from '../../../../../src/core/server'; -import { Plugin } from './plugin'; - -export const plugin = (context: PluginInitializerContext) => { - return new Plugin(context); -}; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/README.md b/x-pack/legacy/plugins/siem/server/lib/detection_engine/README.md deleted file mode 100644 index 1e8e3d5e3dd75..0000000000000 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/README.md +++ /dev/null @@ -1,167 +0,0 @@ -README.md for developers working on the backend detection engine on how to get started -using the CURL scripts in the scripts folder. - -The scripts rely on CURL and jq: - -- [CURL](https://curl.haxx.se) -- [jq](https://stedolan.github.io/jq/) - -Install curl and jq - -```sh -brew update -brew install curl -brew install jq -``` - -Open `$HOME/.zshrc` or `${HOME}.bashrc` depending on your SHELL output from `echo $SHELL` -and add these environment variables: - -```sh -export ELASTICSEARCH_USERNAME=${user} -export ELASTICSEARCH_PASSWORD=${password} -export ELASTICSEARCH_URL=https://${ip}:9200 -export KIBANA_URL=http://localhost:5601 -export TASK_MANAGER_INDEX=.kibana-task-manager-${your user id} -export KIBANA_INDEX=.kibana-${your user id} -``` - -source `$HOME/.zshrc` or `${HOME}.bashrc` to ensure variables are set: - -```sh -source ~/.zshrc -``` - -Open your `kibana.dev.yml` file and add these lines: - -```sh -xpack.siem.signalsIndex: .siem-signals-${your user id} -``` - -Restart Kibana and ensure that you are using `--no-base-path` as changing the base path is a feature but will -get in the way of the CURL scripts written as is. You should see alerting and actions starting up like so afterwards - -```sh -server log [22:05:22.277] [info][status][plugin:alerting@8.0.0] Status changed from uninitialized to green - Ready -server log [22:05:22.270] [info][status][plugin:actions@8.0.0] Status changed from uninitialized to green - Ready -``` - -Go to the scripts folder `cd kibana/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts` and run: - -```sh -./hard_reset.sh -./post_rule.sh -``` - -which will: - -- Delete any existing actions you have -- Delete any existing alerts you have -- Delete any existing alert tasks you have -- Delete any existing signal mapping, policies, and template, you might have previously had. -- Add the latest signal index and its mappings using your settings from `kibana.dev.yml` environment variable of `xpack.siem.signalsIndex`. -- Posts the sample rule from `./rules/queries/query_with_rule_id.json` -- The sample rule checks for root or admin every 5 minutes and reports that as a signal if it is a positive hit - -Now you can run - -```sh -./find_rules.sh -``` - -You should see the new rules created like so: - -```sh -{ - "page": 1, - "perPage": 20, - "total": 1, - "data": [ - { - "created_by": "elastic", - "description": "Detecting root and admin users", - "enabled": true, - "false_positives": [], - "from": "now-6m", - "id": "a556065c-0656-4ba1-ad64-a77ca9d2013b", - "immutable": false, - "index": [ - "auditbeat-*", - "filebeat-*", - "packetbeat-*", - "winlogbeat-*" - ], - "interval": "5m", - "rule_id": "rule-1", - "language": "kuery", - "output_index": ".siem-signals-some-name", - "max_signals": 100, - "risk_score": 1, - "name": "Detect Root/Admin Users", - "query": "user.name: root or user.name: admin", - "references": [ - "http://www.example.com", - "https://ww.example.com" - ], - "severity": "high", - "updated_by": "elastic", - "tags": [], - "to": "now", - "type": "query" - } - ] -} -``` - -Every 5 minutes if you get positive hits you will see messages on info like so: - -```sh -server log [09:54:59.013] [info][plugins][siem] Total signals found from signal rule "id: a556065c-0656-4ba1-ad64-a77ca9d2013b", "ruleId: rule-1": 10000 -``` - -Rules are [space aware](https://www.elastic.co/guide/en/kibana/master/xpack-spaces.html) and default -to the "default" (empty) URL space if you do not export the variable of `SPACE_URL`. Example, if you want to -post rules to `test-space` you set `SPACE_URL` to be: - -```sh -export SPACE_URL=/s/test-space -``` - -The `${SPACE_URL}` is in front of all the APIs to correctly create, modify, delete, and update -them from within the defined space. If this variable is not defined the default which is the url of an -empty string will be used. - -Add the `.siem-signals-${your user id}` to your advanced SIEM settings to see any signals -created which should update once every 5 minutes at this point. - -Also add the `.siem-signals-${your user id}` as a kibana index for Maps to be able to see the -signals - -Optionally you can add these debug statements to your `kibana.dev.yml` to see more information when running the detection -engine - -```sh -logging.verbose: true -logging.events: - { - log: ['siem', 'info', 'warning', 'error', 'fatal'], - request: ['info', 'warning', 'error', 'fatal'], - error: '*', - ops: __no-ops__, - } -``` - -See these two README.md's pages for more references on the alerting and actions API: -https://github.com/elastic/kibana/blob/master/x-pack/plugins/alerting/README.md -https://github.com/elastic/kibana/tree/master/x-pack/plugins/actions - -### Signals API - -To update the status of a signal or group of signals, the following scripts provide an example of how to -go about doing so. -`cd x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts` -`./signals/put_signal_doc.sh` will post a sample signal doc into the signals index to play with -`./signals/set_status_with_id.sh closed` will update the status of the sample signal to closed -`./signals/set_status_with_id.sh open` will update the status of the sample signal to open -`./signals/set_status_with_query.sh closed` will update the status of the signals in the result of the query to closed. -`./signals/set_status_with_query.sh open` will update the status of the signals in the result of the query to open. diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/notifications/rules_notification_alert_type.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/notifications/rules_notification_alert_type.test.ts deleted file mode 100644 index 50ac10347e062..0000000000000 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/notifications/rules_notification_alert_type.test.ts +++ /dev/null @@ -1,142 +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 { savedObjectsClientMock } from 'src/core/server/mocks'; -import { loggerMock } from 'src/core/server/logging/logger.mock'; -import { getResult } from '../routes/__mocks__/request_responses'; -import { rulesNotificationAlertType } from './rules_notification_alert_type'; -import { buildSignalsSearchQuery } from './build_signals_query'; -import { AlertInstance } from '../../../../../../../plugins/alerting/server'; -import { NotificationExecutorOptions } from './types'; -jest.mock('./build_signals_query'); - -describe('rules_notification_alert_type', () => { - let payload: NotificationExecutorOptions; - let alert: ReturnType<typeof rulesNotificationAlertType>; - let alertInstanceMock: Record<string, jest.Mock>; - let alertInstanceFactoryMock: () => AlertInstance; - let savedObjectsClient: ReturnType<typeof savedObjectsClientMock.create>; - let logger: ReturnType<typeof loggerMock.create>; - let callClusterMock: jest.Mock; - - beforeEach(() => { - alertInstanceMock = { - scheduleActions: jest.fn(), - replaceState: jest.fn(), - }; - alertInstanceMock.replaceState.mockReturnValue(alertInstanceMock); - alertInstanceFactoryMock = jest.fn().mockReturnValue(alertInstanceMock); - callClusterMock = jest.fn(); - savedObjectsClient = savedObjectsClientMock.create(); - logger = loggerMock.create(); - - payload = { - alertId: '1111', - services: { - savedObjectsClient, - alertInstanceFactory: alertInstanceFactoryMock, - callCluster: callClusterMock, - }, - params: { ruleAlertId: '2222' }, - state: {}, - spaceId: '', - name: 'name', - tags: [], - startedAt: new Date('2019-12-14T16:40:33.400Z'), - previousStartedAt: new Date('2019-12-13T16:40:33.400Z'), - createdBy: 'elastic', - updatedBy: 'elastic', - }; - - alert = rulesNotificationAlertType({ - logger, - }); - }); - - describe('executor', () => { - it('throws an error if rule alert was not found', async () => { - savedObjectsClient.get.mockResolvedValue({ - id: 'id', - attributes: {}, - type: 'type', - references: [], - }); - await alert.executor(payload); - expect(logger.error).toHaveBeenCalledWith( - `Saved object for alert ${payload.params.ruleAlertId} was not found` - ); - }); - - it('should call buildSignalsSearchQuery with proper params', async () => { - const ruleAlert = getResult(); - savedObjectsClient.get.mockResolvedValue({ - id: 'id', - type: 'type', - references: [], - attributes: ruleAlert, - }); - callClusterMock.mockResolvedValue({ - count: 0, - }); - - await alert.executor(payload); - - expect(buildSignalsSearchQuery).toHaveBeenCalledWith( - expect.objectContaining({ - from: '1576255233400', - index: '.siem-signals', - ruleId: 'rule-1', - to: '1576341633400', - }) - ); - }); - - it('should not call alertInstanceFactory if signalsCount was 0', async () => { - const ruleAlert = getResult(); - savedObjectsClient.get.mockResolvedValue({ - id: 'id', - type: 'type', - references: [], - attributes: ruleAlert, - }); - callClusterMock.mockResolvedValue({ - count: 0, - }); - - await alert.executor(payload); - - expect(alertInstanceFactoryMock).not.toHaveBeenCalled(); - }); - - it('should call scheduleActions if signalsCount was greater than 0', async () => { - const ruleAlert = getResult(); - savedObjectsClient.get.mockResolvedValue({ - id: 'id', - type: 'type', - references: [], - attributes: ruleAlert, - }); - callClusterMock.mockResolvedValue({ - count: 10, - }); - - await alert.executor(payload); - - expect(alertInstanceFactoryMock).toHaveBeenCalled(); - expect(alertInstanceMock.replaceState).toHaveBeenCalledWith( - expect.objectContaining({ signals_count: 10 }) - ); - expect(alertInstanceMock.scheduleActions).toHaveBeenCalledWith( - 'default', - expect.objectContaining({ - rule: expect.objectContaining({ - name: ruleAlert.name, - }), - }) - ); - }); - }); -}); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/notifications/types.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/notifications/types.ts deleted file mode 100644 index 32a8737adc7c9..0000000000000 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/notifications/types.ts +++ /dev/null @@ -1,106 +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 { - AlertsClient, - PartialAlert, - AlertType, - State, - AlertExecutorOptions, -} from '../../../../../../../plugins/alerting/server'; -import { Alert } from '../../../../../../../plugins/alerting/common'; -import { NOTIFICATIONS_ID } from '../../../../common/constants'; -import { RuleAlertAction } from '../../../../common/detection_engine/types'; - -export interface RuleNotificationAlertType extends Alert { - params: { - ruleAlertId: string; - }; -} - -export interface FindNotificationParams { - alertsClient: AlertsClient; - perPage?: number; - page?: number; - sortField?: string; - filter?: string; - fields?: string[]; - sortOrder?: 'asc' | 'desc'; -} - -export interface FindNotificationsRequestParams { - per_page: number; - page: number; - search?: string; - sort_field?: string; - filter?: string; - fields?: string[]; - sort_order?: 'asc' | 'desc'; -} - -export interface Clients { - alertsClient: AlertsClient; -} - -export type UpdateNotificationParams = Omit< - NotificationAlertParams, - 'interval' | 'actions' | 'tags' -> & { - actions: RuleAlertAction[]; - interval: string | null | undefined; - ruleAlertId: string; -} & Clients; - -export type DeleteNotificationParams = Clients & { - id?: string; - ruleAlertId?: string; -}; - -export interface NotificationAlertParams { - actions: RuleAlertAction[]; - enabled: boolean; - ruleAlertId: string; - interval: string; - name: string; -} - -export type CreateNotificationParams = NotificationAlertParams & Clients; - -export interface ReadNotificationParams { - alertsClient: AlertsClient; - id?: string | null; - ruleAlertId?: string | null; -} - -export const isAlertTypes = ( - partialAlert: PartialAlert[] -): partialAlert is RuleNotificationAlertType[] => { - return partialAlert.every(rule => isAlertType(rule)); -}; - -export const isAlertType = ( - partialAlert: PartialAlert -): partialAlert is RuleNotificationAlertType => { - return partialAlert.alertTypeId === NOTIFICATIONS_ID; -}; - -export type NotificationExecutorOptions = Omit<AlertExecutorOptions, 'params'> & { - params: { - ruleAlertId: string; - }; -}; - -// This returns true because by default a NotificationAlertTypeDefinition is an AlertType -// since we are only increasing the strictness of params. -export const isNotificationAlertExecutor = ( - obj: NotificationAlertTypeDefinition -): obj is AlertType => { - return true; -}; - -export type NotificationAlertTypeDefinition = Omit<AlertType, 'executor'> & { - executor: ({ services, params, state }: NotificationExecutorOptions) => Promise<State | void>; -}; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/__mocks__/index.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/__mocks__/index.ts deleted file mode 100644 index 1ccd43c06aacc..0000000000000 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/__mocks__/index.ts +++ /dev/null @@ -1,17 +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 { requestContextMock } from './request_context'; -import { serverMock } from './server'; -import { requestMock } from './request'; -import { responseMock } from './response_factory'; - -export { requestMock, requestContextMock, responseMock, serverMock }; - -export const createMockConfig = () => () => ({ - get: jest.fn(), - has: jest.fn(), -}); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/__mocks__/request_responses.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/__mocks__/request_responses.ts deleted file mode 100644 index e400360a5a5b2..0000000000000 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/__mocks__/request_responses.ts +++ /dev/null @@ -1,709 +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 { SavedObjectsFindResponse } from 'kibana/server'; -import { ActionResult } from '../../../../../../../../plugins/actions/server'; -import { - SignalsStatusRestParams, - SignalsQueryRestParams, - SignalSearchResponse, -} from '../../signals/types'; -import { - DETECTION_ENGINE_RULES_URL, - DETECTION_ENGINE_SIGNALS_STATUS_URL, - DETECTION_ENGINE_PRIVILEGES_URL, - DETECTION_ENGINE_QUERY_SIGNALS_URL, - INTERNAL_RULE_ID_KEY, - INTERNAL_IMMUTABLE_KEY, - DETECTION_ENGINE_PREPACKAGED_URL, -} from '../../../../../common/constants'; -import { ShardsResponse } from '../../../types'; -import { - RuleAlertType, - IRuleSavedAttributesSavedObjectAttributes, - HapiReadableStream, -} from '../../rules/types'; -import { RuleAlertParamsRest, PrepackagedRules } from '../../types'; -import { requestMock } from './request'; -import { RuleNotificationAlertType } from '../../notifications/types'; - -export const mockPrepackagedRule = (): PrepackagedRules => ({ - rule_id: 'rule-1', - description: 'Detecting root and admin users', - index: ['auditbeat-*', 'filebeat-*', 'packetbeat-*', 'winlogbeat-*'], - interval: '5m', - name: 'Detect Root/Admin Users', - output_index: '.siem-signals', - risk_score: 50, - type: 'query', - from: 'now-6m', - to: 'now', - severity: 'high', - query: 'user.name: root or user.name: admin', - language: 'kuery', - threat: [ - { - framework: 'fake', - tactic: { id: 'fakeId', name: 'fakeName', reference: 'fakeRef' }, - technique: [{ id: 'techniqueId', name: 'techniqueName', reference: 'techniqueRef' }], - }, - ], - throttle: null, - enabled: true, - filters: [], - immutable: false, - references: [], - meta: {}, - tags: [], - version: 1, - false_positives: [], - max_signals: 100, - note: '', - timeline_id: 'timeline-id', - timeline_title: 'timeline-title', -}); - -export const typicalPayload = (): Partial<RuleAlertParamsRest> => ({ - rule_id: 'rule-1', - description: 'Detecting root and admin users', - index: ['auditbeat-*', 'filebeat-*', 'packetbeat-*', 'winlogbeat-*'], - interval: '5m', - name: 'Detect Root/Admin Users', - output_index: '.siem-signals', - risk_score: 50, - type: 'query', - from: 'now-6m', - to: 'now', - severity: 'high', - query: 'user.name: root or user.name: admin', - language: 'kuery', - threat: [ - { - framework: 'fake', - tactic: { id: 'fakeId', name: 'fakeName', reference: 'fakeRef' }, - technique: [{ id: 'techniqueId', name: 'techniqueName', reference: 'techniqueRef' }], - }, - ], -}); - -export const typicalSetStatusSignalByIdsPayload = (): Partial<SignalsStatusRestParams> => ({ - signal_ids: ['somefakeid1', 'somefakeid2'], - status: 'closed', -}); - -export const typicalSetStatusSignalByQueryPayload = (): Partial<SignalsStatusRestParams> => ({ - query: { bool: { filter: { range: { '@timestamp': { gte: 'now-2M', lte: 'now/M' } } } } }, - status: 'closed', -}); - -export const typicalSignalsQuery = (): Partial<SignalsQueryRestParams> => ({ - query: { match_all: {} }, -}); - -export const typicalSignalsQueryAggs = (): Partial<SignalsQueryRestParams> => ({ - aggs: { statuses: { terms: { field: 'signal.status', size: 10 } } }, -}); - -export const setStatusSignalMissingIdsAndQueryPayload = (): Partial<SignalsStatusRestParams> => ({ - status: 'closed', -}); - -export const getUpdateRequest = () => - requestMock.create({ - method: 'put', - path: DETECTION_ENGINE_RULES_URL, - body: typicalPayload(), - }); - -export const getPatchRequest = () => - requestMock.create({ - method: 'patch', - path: DETECTION_ENGINE_RULES_URL, - body: typicalPayload(), - }); - -export const getReadRequest = () => - requestMock.create({ - method: 'get', - path: DETECTION_ENGINE_RULES_URL, - query: { rule_id: 'rule-1' }, - }); - -export const getFindRequest = () => - requestMock.create({ - method: 'get', - path: `${DETECTION_ENGINE_RULES_URL}/_find`, - }); - -export const getReadBulkRequest = () => - requestMock.create({ - method: 'post', - path: `${DETECTION_ENGINE_RULES_URL}/_bulk_create`, - body: [typicalPayload()], - }); - -export const getUpdateBulkRequest = () => - requestMock.create({ - method: 'put', - path: `${DETECTION_ENGINE_RULES_URL}/_bulk_update`, - body: [typicalPayload()], - }); - -export const getPatchBulkRequest = () => - requestMock.create({ - method: 'patch', - path: `${DETECTION_ENGINE_RULES_URL}/_bulk_update`, - body: [typicalPayload()], - }); - -export const getDeleteBulkRequest = () => - requestMock.create({ - method: 'delete', - path: `${DETECTION_ENGINE_RULES_URL}/_bulk_delete`, - body: [{ rule_id: 'rule-1' }], - }); - -export const getDeleteBulkRequestById = () => - requestMock.create({ - method: 'delete', - path: `${DETECTION_ENGINE_RULES_URL}/_bulk_delete`, - body: [{ id: 'rule-04128c15-0d1b-4716-a4c5-46997ac7f3bd' }], - }); - -export const getDeleteAsPostBulkRequestById = () => - requestMock.create({ - method: 'post', - path: `${DETECTION_ENGINE_RULES_URL}/_bulk_delete`, - body: [{ id: 'rule-04128c15-0d1b-4716-a4c5-46997ac7f3bd' }], - }); - -export const getDeleteAsPostBulkRequest = () => - requestMock.create({ - method: 'post', - path: `${DETECTION_ENGINE_RULES_URL}/_bulk_delete`, - body: [{ rule_id: 'rule-1' }], - }); - -export const getPrivilegeRequest = () => - requestMock.create({ - method: 'get', - path: DETECTION_ENGINE_PRIVILEGES_URL, - }); - -export const addPrepackagedRulesRequest = () => - requestMock.create({ - method: 'put', - path: DETECTION_ENGINE_PREPACKAGED_URL, - }); - -export const getPrepackagedRulesStatusRequest = () => - requestMock.create({ - method: 'get', - path: `${DETECTION_ENGINE_PREPACKAGED_URL}/_status`, - }); - -export interface FindHit<T = RuleAlertType> { - page: number; - perPage: number; - total: number; - data: T[]; -} - -export const getEmptyFindResult = (): FindHit => ({ - page: 1, - perPage: 1, - total: 0, - data: [], -}); - -export const getFindResultWithSingleHit = (): FindHit => ({ - page: 1, - perPage: 1, - total: 1, - data: [getResult()], -}); - -export const nonRuleFindResult = (): FindHit => ({ - page: 1, - perPage: 1, - total: 1, - data: [nonRuleAlert()], -}); - -export const getFindResultWithMultiHits = ({ - data, - page = 1, - perPage = 1, - total, -}: { - data: RuleAlertType[]; - page?: number; - perPage?: number; - total?: number; -}) => { - return { - page, - perPage, - total: total != null ? total : data.length, - data, - }; -}; - -export const ruleStatusRequest = () => - requestMock.create({ - method: 'get', - path: `${DETECTION_ENGINE_RULES_URL}/_find_statuses`, - query: { ids: ['someId'] }, - }); - -export const getImportRulesRequest = (hapiStream?: HapiReadableStream) => - requestMock.create({ - method: 'post', - path: `${DETECTION_ENGINE_RULES_URL}/_import`, - body: { file: hapiStream }, - }); - -export const getImportRulesRequestOverwriteTrue = (hapiStream?: HapiReadableStream) => - requestMock.create({ - method: 'post', - path: `${DETECTION_ENGINE_RULES_URL}/_import`, - body: { file: hapiStream }, - query: { overwrite: true }, - }); - -export const getDeleteRequest = () => - requestMock.create({ - method: 'delete', - path: DETECTION_ENGINE_RULES_URL, - query: { rule_id: 'rule-1' }, - }); - -export const getDeleteRequestById = () => - requestMock.create({ - method: 'delete', - path: DETECTION_ENGINE_RULES_URL, - query: { id: '04128c15-0d1b-4716-a4c5-46997ac7f3bd' }, - }); - -export const getCreateRequest = () => - requestMock.create({ - method: 'post', - path: DETECTION_ENGINE_RULES_URL, - body: typicalPayload(), - }); - -export const typicalMlRulePayload = () => { - const { query, language, index, ...mlParams } = typicalPayload(); - - return { - ...mlParams, - type: 'machine_learning', - anomaly_threshold: 58, - machine_learning_job_id: 'typical-ml-job-id', - }; -}; - -export const createMlRuleRequest = () => { - return requestMock.create({ - method: 'post', - path: DETECTION_ENGINE_RULES_URL, - body: typicalMlRulePayload(), - }); -}; - -export const createBulkMlRuleRequest = () => { - return requestMock.create({ - method: 'post', - path: DETECTION_ENGINE_RULES_URL, - body: [typicalMlRulePayload()], - }); -}; - -export const createRuleWithActionsRequest = () => { - const payload = typicalPayload(); - - return requestMock.create({ - method: 'post', - path: DETECTION_ENGINE_RULES_URL, - body: { - ...payload, - throttle: '5m', - actions: [ - { - group: 'default', - id: '99403909-ca9b-49ba-9d7a-7e5320e68d05', - params: { message: 'Rule generated {{state.signals_count}} signals' }, - action_type_id: '.slack', - }, - ], - }, - }); -}; - -export const getSetSignalStatusByIdsRequest = () => - requestMock.create({ - method: 'post', - path: DETECTION_ENGINE_SIGNALS_STATUS_URL, - body: typicalSetStatusSignalByIdsPayload(), - }); - -export const getSetSignalStatusByQueryRequest = () => - requestMock.create({ - method: 'post', - path: DETECTION_ENGINE_SIGNALS_STATUS_URL, - body: typicalSetStatusSignalByQueryPayload(), - }); - -export const getSignalsQueryRequest = () => - requestMock.create({ - method: 'post', - path: DETECTION_ENGINE_QUERY_SIGNALS_URL, - body: typicalSignalsQuery(), - }); - -export const getSignalsAggsQueryRequest = () => - requestMock.create({ - method: 'post', - path: DETECTION_ENGINE_QUERY_SIGNALS_URL, - body: typicalSignalsQueryAggs(), - }); - -export const getSignalsAggsAndQueryRequest = () => - requestMock.create({ - method: 'post', - path: DETECTION_ENGINE_QUERY_SIGNALS_URL, - body: { ...typicalSignalsQuery(), ...typicalSignalsQueryAggs() }, - }); - -export const createActionResult = (): ActionResult => ({ - id: 'result-1', - actionTypeId: 'action-id-1', - name: '', - config: {}, - isPreconfigured: false, -}); - -export const nonRuleAlert = () => ({ - ...getResult(), - id: '04128c15-0d1b-4716-a4c5-46997ac7f3bc', - name: 'Non-Rule Alert', - alertTypeId: 'something', -}); - -export const getResult = (): RuleAlertType => ({ - id: '04128c15-0d1b-4716-a4c5-46997ac7f3bd', - name: 'Detect Root/Admin Users', - tags: [`${INTERNAL_RULE_ID_KEY}:rule-1`, `${INTERNAL_IMMUTABLE_KEY}:false`], - alertTypeId: 'siem.signals', - consumer: 'siem', - params: { - anomalyThreshold: undefined, - description: 'Detecting root and admin users', - ruleId: 'rule-1', - index: ['auditbeat-*', 'filebeat-*', 'packetbeat-*', 'winlogbeat-*'], - falsePositives: [], - from: 'now-6m', - immutable: false, - query: 'user.name: root or user.name: admin', - language: 'kuery', - machineLearningJobId: undefined, - outputIndex: '.siem-signals', - timelineId: 'some-timeline-id', - timelineTitle: 'some-timeline-title', - meta: { someMeta: 'someField' }, - filters: [ - { - query: { - match_phrase: { - 'host.name': 'some-host', - }, - }, - }, - ], - riskScore: 50, - maxSignals: 100, - severity: 'high', - to: 'now', - type: 'query', - threat: [ - { - framework: 'MITRE ATT&CK', - tactic: { - id: 'TA0040', - name: 'impact', - reference: 'https://attack.mitre.org/tactics/TA0040/', - }, - technique: [ - { - id: 'T1499', - name: 'endpoint denial of service', - reference: 'https://attack.mitre.org/techniques/T1499/', - }, - ], - }, - ], - references: ['http://www.example.com', 'https://ww.example.com'], - note: '# Investigative notes', - version: 1, - lists: [ - { - field: 'source.ip', - values_operator: 'included', - values_type: 'exists', - }, - { - field: 'host.name', - values_operator: 'excluded', - values_type: 'match', - values: [ - { - name: 'rock01', - }, - ], - and: [ - { - field: 'host.id', - values_operator: 'included', - values_type: 'match_all', - values: [ - { - name: '123', - }, - { - name: '678', - }, - ], - }, - ], - }, - ], - }, - createdAt: new Date('2019-12-13T16:40:33.400Z'), - updatedAt: new Date('2019-12-13T16:40:33.400Z'), - schedule: { interval: '5m' }, - enabled: true, - actions: [], - throttle: null, - createdBy: 'elastic', - updatedBy: 'elastic', - apiKey: null, - apiKeyOwner: 'elastic', - muteAll: false, - mutedInstanceIds: [], - scheduledTaskId: '2dabe330-0702-11ea-8b50-773b89126888', -}); - -export const getMlResult = (): RuleAlertType => { - const result = getResult(); - - return { - ...result, - params: { - ...result.params, - query: undefined, - language: undefined, - filters: undefined, - index: undefined, - type: 'machine_learning', - anomalyThreshold: 44, - machineLearningJobId: 'some_job_id', - }, - }; -}; - -export const updateActionResult = (): ActionResult => ({ - id: 'result-1', - actionTypeId: 'action-id-1', - name: '', - config: {}, - isPreconfigured: false, -}); - -export const getMockPrivilegesResult = () => ({ - username: 'test-space', - has_all_requested: false, - cluster: { - monitor_ml: true, - manage_ccr: false, - manage_index_templates: true, - monitor_watcher: true, - monitor_transform: true, - read_ilm: true, - manage_api_key: false, - manage_security: false, - manage_own_api_key: false, - manage_saml: false, - all: false, - manage_ilm: true, - manage_ingest_pipelines: true, - read_ccr: false, - manage_rollup: true, - monitor: true, - manage_watcher: true, - manage: true, - manage_transform: true, - manage_token: false, - manage_ml: true, - manage_pipeline: true, - monitor_rollup: true, - transport_client: true, - create_snapshot: true, - }, - index: { - '.siem-signals-test-space': { - all: false, - manage_ilm: true, - read: false, - create_index: true, - read_cross_cluster: false, - index: false, - monitor: true, - delete: false, - manage: true, - delete_index: true, - create_doc: false, - view_index_metadata: true, - create: false, - manage_follow_index: true, - manage_leader_index: true, - write: false, - }, - }, - application: {}, -}); - -export const getFindResultStatusEmpty = (): SavedObjectsFindResponse<IRuleSavedAttributesSavedObjectAttributes> => ({ - page: 1, - per_page: 1, - total: 0, - saved_objects: [], -}); - -export const getFindResultStatus = (): SavedObjectsFindResponse<IRuleSavedAttributesSavedObjectAttributes> => ({ - page: 1, - per_page: 6, - total: 2, - saved_objects: [ - { - type: 'my-type', - id: 'e0b86950-4e9f-11ea-bdbd-07b56aa159b3', - attributes: { - alertId: '1ea5a820-4da1-4e82-92a1-2b43a7bece08', - statusDate: '2020-02-18T15:26:49.783Z', - status: 'succeeded', - lastFailureAt: null, - lastSuccessAt: '2020-02-18T15:26:49.783Z', - lastFailureMessage: null, - lastSuccessMessage: 'succeeded', - lastLookBackDate: new Date('2020-02-18T15:14:58.806Z').toISOString(), - gap: '500.32', - searchAfterTimeDurations: ['200.00'], - bulkCreateTimeDurations: ['800.43'], - }, - references: [], - updated_at: '2020-02-18T15:26:51.333Z', - version: 'WzQ2LDFd', - }, - { - type: 'my-type', - id: '91246bd0-5261-11ea-9650-33b954270f67', - attributes: { - alertId: '1ea5a820-4da1-4e82-92a1-2b43a7bece08', - statusDate: '2020-02-18T15:15:58.806Z', - status: 'failed', - lastFailureAt: '2020-02-18T15:15:58.806Z', - lastSuccessAt: '2020-02-13T20:31:59.855Z', - lastFailureMessage: - 'Signal rule name: "Query with a rule id Number 1", id: "1ea5a820-4da1-4e82-92a1-2b43a7bece08", rule_id: "query-rule-id-1" has a time gap of 5 days (412682928ms), and could be missing signals within that time. Consider increasing your look behind time or adding more Kibana instances.', - lastSuccessMessage: 'succeeded', - lastLookBackDate: new Date('2020-02-18T15:14:58.806Z').toISOString(), - gap: '500.32', - searchAfterTimeDurations: ['200.00'], - bulkCreateTimeDurations: ['800.43'], - }, - references: [], - updated_at: '2020-02-18T15:15:58.860Z', - version: 'WzMyLDFd', - }, - ], -}); - -export const getEmptySignalsResponse = (): SignalSearchResponse => ({ - took: 1, - timed_out: false, - _shards: { total: 1, successful: 1, skipped: 0, failed: 0 }, - hits: { total: { value: 0, relation: 'eq' }, max_score: 0, hits: [] }, - aggregations: { - signalsByGrouping: { doc_count_error_upper_bound: 0, sum_other_doc_count: 0, buckets: [] }, - }, -}); - -export const getSuccessfulSignalUpdateResponse = () => ({ - took: 18, - timed_out: false, - total: 1, - updated: 1, - deleted: 0, - batches: 1, - version_conflicts: 0, - noops: 0, - retries: { bulk: 0, search: 0 }, - throttled_millis: 0, - requests_per_second: -1, - throttled_until_millis: 0, - failures: [], -}); - -export const getIndexName = () => 'index-name'; -export const getEmptyIndex = (): { _shards: Partial<ShardsResponse> } => ({ - _shards: { total: 0 }, -}); -export const getNonEmptyIndex = (): { _shards: Partial<ShardsResponse> } => ({ - _shards: { total: 1 }, -}); - -export const getNotificationResult = (): RuleNotificationAlertType => ({ - id: '200dbf2f-b269-4bf9-aa85-11ba32ba73ba', - name: 'Notification for Rule Test', - tags: ['__internal_rule_alert_id:85b64e8a-2e40-4096-86af-5ac172c10825'], - alertTypeId: 'siem.notifications', - consumer: 'siem', - params: { - ruleAlertId: '85b64e8a-2e40-4096-86af-5ac172c10825', - }, - schedule: { - interval: '5m', - }, - enabled: true, - actions: [ - { - actionTypeId: '.slack', - params: { - message: - 'Rule generated {{state.signals_count}} signals\n\n{{context.rule.name}}\n{{{context.results_link}}}', - }, - group: 'default', - id: '99403909-ca9b-49ba-9d7a-7e5320e68d05', - }, - ], - throttle: null, - apiKey: null, - apiKeyOwner: 'elastic', - createdBy: 'elastic', - updatedBy: 'elastic', - createdAt: new Date('2020-03-21T11:15:13.530Z'), - muteAll: false, - mutedInstanceIds: [], - scheduledTaskId: '62b3a130-6b70-11ea-9ce9-6b9818c4cbd7', - updatedAt: new Date('2020-03-21T12:37:08.730Z'), -}); - -export const getFindNotificationsResultWithSingleHit = (): FindHit<RuleNotificationAlertType> => ({ - page: 1, - perPage: 1, - total: 1, - data: [getNotificationResult()], -}); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/__mocks__/utils.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/__mocks__/utils.ts deleted file mode 100644 index c929b0718207d..0000000000000 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/__mocks__/utils.ts +++ /dev/null @@ -1,191 +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 { Readable } from 'stream'; - -import { OutputRuleAlertRest } from '../../types'; -import { HapiReadableStream } from '../../rules/types'; - -/** - * This is a typical simple rule for testing that is easy for most basic testing - * @param ruleId - */ -export const getSimpleRule = (ruleId = 'rule-1'): Partial<OutputRuleAlertRest> => ({ - name: 'Simple Rule Query', - description: 'Simple Rule Query', - risk_score: 1, - rule_id: ruleId, - severity: 'high', - type: 'query', - query: 'user.name: root or user.name: admin', -}); - -/** - * This is a typical ML rule for testing - * @param ruleId - */ -export const getSimpleMlRule = (ruleId = 'rule-1'): Partial<OutputRuleAlertRest> => ({ - name: 'Simple Rule Query', - description: 'Simple Rule Query', - risk_score: 1, - rule_id: ruleId, - severity: 'high', - type: 'machine_learning', - anomaly_threshold: 44, - machine_learning_job_id: 'some_job_id', -}); - -/** - * This is a typical simple rule for testing that is easy for most basic testing - * @param ruleId - */ -export const getSimpleRuleWithId = (id = 'rule-1'): Partial<OutputRuleAlertRest> => ({ - name: 'Simple Rule Query', - description: 'Simple Rule Query', - risk_score: 1, - id, - severity: 'high', - type: 'query', - query: 'user.name: root or user.name: admin', -}); - -/** - * Given an array of rules, builds an NDJSON string of rules - * as we might import/export - * @param rules Array of rule objects with which to generate rule JSON - */ -export const rulesToNdJsonString = (rules: Array<Partial<OutputRuleAlertRest>>) => { - return rules.map(rule => JSON.stringify(rule)).join('\r\n'); -}; - -/** - * Given an array of rule IDs, builds an NDJSON string of rules - * as we might import/export - * @param ruleIds Array of ruleIds with which to generate rule JSON - */ -export const ruleIdsToNdJsonString = (ruleIds: string[]) => { - const rules = ruleIds.map(ruleId => getSimpleRule(ruleId)); - return rulesToNdJsonString(rules); -}; - -/** - * Given a string, builds a hapi stream as our - * route handler would receive it. - * @param string contents of the stream - * @param filename String to declare file extension - */ -export const buildHapiStream = (string: string, filename = 'file.ndjson'): HapiReadableStream => { - const HapiStream = class extends Readable { - public readonly hapi: { filename: string }; - constructor(fileName: string) { - super(); - this.hapi = { filename: fileName }; - } - }; - - const stream = new HapiStream(filename); - stream.push(string); - stream.push(null); - - return stream; -}; - -export const getOutputRuleAlertForRest = (): Omit< - OutputRuleAlertRest, - 'machine_learning_job_id' | 'anomaly_threshold' -> => ({ - actions: [], - created_by: 'elastic', - created_at: '2019-12-13T16:40:33.400Z', - updated_at: '2019-12-13T16:40:33.400Z', - description: 'Detecting root and admin users', - enabled: true, - false_positives: [], - from: 'now-6m', - id: '04128c15-0d1b-4716-a4c5-46997ac7f3bd', - immutable: false, - index: ['auditbeat-*', 'filebeat-*', 'packetbeat-*', 'winlogbeat-*'], - interval: '5m', - risk_score: 50, - rule_id: 'rule-1', - language: 'kuery', - max_signals: 100, - name: 'Detect Root/Admin Users', - output_index: '.siem-signals', - query: 'user.name: root or user.name: admin', - references: ['http://www.example.com', 'https://ww.example.com'], - severity: 'high', - updated_by: 'elastic', - tags: [], - throttle: 'no_actions', - threat: [ - { - framework: 'MITRE ATT&CK', - tactic: { - id: 'TA0040', - name: 'impact', - reference: 'https://attack.mitre.org/tactics/TA0040/', - }, - technique: [ - { - id: 'T1499', - name: 'endpoint denial of service', - reference: 'https://attack.mitre.org/techniques/T1499/', - }, - ], - }, - ], - lists: [ - { - field: 'source.ip', - values_operator: 'included', - values_type: 'exists', - }, - { - field: 'host.name', - values_operator: 'excluded', - values_type: 'match', - values: [ - { - name: 'rock01', - }, - ], - and: [ - { - field: 'host.id', - values_operator: 'included', - values_type: 'match_all', - values: [ - { - name: '123', - }, - { - name: '678', - }, - ], - }, - ], - }, - ], - filters: [ - { - query: { - match_phrase: { - 'host.name': 'some-host', - }, - }, - }, - ], - meta: { - someMeta: 'someField', - }, - timeline_id: 'some-timeline-id', - timeline_title: 'some-timeline-title', - to: 'now', - type: 'query', - note: '# Investigative notes', - version: 1, -}); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/utils.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/utils.test.ts deleted file mode 100644 index 31a0f37fe81c9..0000000000000 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/utils.test.ts +++ /dev/null @@ -1,671 +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 { Readable } from 'stream'; -import { - transformAlertToRule, - getIdError, - transformFindAlerts, - transform, - transformTags, - getIdBulkError, - transformOrBulkError, - transformDataToNdjson, - transformAlertsToRules, - transformOrImportError, - getDuplicates, - getTupleDuplicateErrorsAndUniqueRules, -} from './utils'; -import { getResult } from '../__mocks__/request_responses'; -import { INTERNAL_IDENTIFIER } from '../../../../../common/constants'; -import { ImportRuleAlertRest, RuleAlertParamsRest, RuleTypeParams } from '../../types'; -import { BulkError, ImportSuccessError } from '../utils'; -import { sampleRule } from '../../signals/__mocks__/es_results'; -import { getSimpleRule, getOutputRuleAlertForRest } from '../__mocks__/utils'; -import { createRulesStreamFromNdJson } from '../../rules/create_rules_stream_from_ndjson'; -import { createPromiseFromStreams } from '../../../../../../../../../src/legacy/utils/streams'; -import { PartialAlert } from '../../../../../../../../plugins/alerting/server'; -import { SanitizedAlert } from '../../../../../../../../plugins/alerting/server/types'; -import { RuleAlertType } from '../../rules/types'; -import { setFeatureFlagsForTestsOnly, unSetFeatureFlagsForTestsOnly } from '../../feature_flags'; - -type PromiseFromStreams = ImportRuleAlertRest | Error; - -describe('utils', () => { - beforeAll(() => { - setFeatureFlagsForTestsOnly(); - }); - - afterAll(() => { - unSetFeatureFlagsForTestsOnly(); - }); - - describe('transformAlertToRule', () => { - test('should work with a full data set', () => { - const fullRule = getResult(); - const rule = transformAlertToRule(fullRule); - expect(rule).toEqual(getOutputRuleAlertForRest()); - }); - - test('should work with a partial data set missing data', () => { - const fullRule = getResult(); - const { from, language, ...omitParams } = fullRule.params; - fullRule.params = omitParams as RuleTypeParams; - const rule = transformAlertToRule(fullRule); - const { - from: from2, - language: language2, - ...expectedWithoutFromWithoutLanguage - } = getOutputRuleAlertForRest(); - expect(rule).toEqual(expectedWithoutFromWithoutLanguage); - }); - - test('should omit query if query is null', () => { - const fullRule = getResult(); - fullRule.params.query = null; - const rule = transformAlertToRule(fullRule); - const { query, ...expectedWithoutQuery } = getOutputRuleAlertForRest(); - expect(rule).toEqual(expectedWithoutQuery); - }); - - test('should omit query if query is undefined', () => { - const fullRule = getResult(); - fullRule.params.query = undefined; - const rule = transformAlertToRule(fullRule); - const { query, ...expectedWithoutQuery } = getOutputRuleAlertForRest(); - expect(rule).toEqual(expectedWithoutQuery); - }); - - test('should omit a mix of undefined, null, and missing fields', () => { - const fullRule = getResult(); - fullRule.params.query = undefined; - fullRule.params.language = null; - const { from, ...omitParams } = fullRule.params; - fullRule.params = omitParams as RuleTypeParams; - const { enabled, ...omitEnabled } = fullRule; - const rule = transformAlertToRule(omitEnabled as RuleAlertType); - const { - from: from2, - enabled: enabled2, - language, - query, - ...expectedWithoutFromEnabledLanguageQuery - } = getOutputRuleAlertForRest(); - expect(rule).toEqual(expectedWithoutFromEnabledLanguageQuery); - }); - - test('should return enabled is equal to false', () => { - const fullRule = getResult(); - fullRule.enabled = false; - const ruleWithEnabledFalse = transformAlertToRule(fullRule); - const expected = getOutputRuleAlertForRest(); - expected.enabled = false; - expect(ruleWithEnabledFalse).toEqual(expected); - }); - - test('should return immutable is equal to false', () => { - const fullRule = getResult(); - fullRule.params.immutable = false; - const ruleWithEnabledFalse = transformAlertToRule(fullRule); - const expected = getOutputRuleAlertForRest(); - expect(ruleWithEnabledFalse).toEqual(expected); - }); - - test('should work with tags but filter out any internal tags', () => { - const fullRule = getResult(); - fullRule.tags = ['tag 1', 'tag 2', `${INTERNAL_IDENTIFIER}_some_other_value`]; - const rule = transformAlertToRule(fullRule); - const expected = getOutputRuleAlertForRest(); - expected.tags = ['tag 1', 'tag 2']; - expect(rule).toEqual(expected); - }); - - it('transforms ML Rule fields', () => { - const mlRule = getResult(); - mlRule.params.anomalyThreshold = 55; - mlRule.params.machineLearningJobId = 'some_job_id'; - mlRule.params.type = 'machine_learning'; - - const rule = transformAlertToRule(mlRule); - expect(rule).toEqual( - expect.objectContaining({ - anomaly_threshold: 55, - machine_learning_job_id: 'some_job_id', - type: 'machine_learning', - }) - ); - }); - }); - - describe('getIdError', () => { - test('it should have a status code', () => { - const error = getIdError({ id: '123', ruleId: undefined }); - expect(error).toEqual({ - message: 'id: "123" not found', - statusCode: 404, - }); - }); - - test('outputs message about id not being found if only id is defined and ruleId is undefined', () => { - const error = getIdError({ id: '123', ruleId: undefined }); - expect(error).toEqual({ - message: 'id: "123" not found', - statusCode: 404, - }); - }); - - test('outputs message about id not being found if only id is defined and ruleId is null', () => { - const error = getIdError({ id: '123', ruleId: null }); - expect(error).toEqual({ - message: 'id: "123" not found', - statusCode: 404, - }); - }); - - test('outputs message about ruleId not being found if only ruleId is defined and id is undefined', () => { - const error = getIdError({ id: undefined, ruleId: 'rule-id-123' }); - expect(error).toEqual({ - message: 'rule_id: "rule-id-123" not found', - statusCode: 404, - }); - }); - - test('outputs message about ruleId not being found if only ruleId is defined and id is null', () => { - const error = getIdError({ id: null, ruleId: 'rule-id-123' }); - expect(error).toEqual({ - message: 'rule_id: "rule-id-123" not found', - statusCode: 404, - }); - }); - - test('outputs message about both being not defined when both are undefined', () => { - const error = getIdError({ id: undefined, ruleId: undefined }); - expect(error).toEqual({ - message: 'id or rule_id should have been defined', - statusCode: 404, - }); - }); - - test('outputs message about both being not defined when both are null', () => { - const error = getIdError({ id: null, ruleId: null }); - expect(error).toEqual({ - message: 'id or rule_id should have been defined', - statusCode: 404, - }); - }); - - test('outputs message about both being not defined when id is null and ruleId is undefined', () => { - const error = getIdError({ id: null, ruleId: undefined }); - expect(error).toEqual({ - message: 'id or rule_id should have been defined', - statusCode: 404, - }); - }); - - test('outputs message about both being not defined when id is undefined and ruleId is null', () => { - const error = getIdError({ id: undefined, ruleId: null }); - expect(error).toEqual({ - message: 'id or rule_id should have been defined', - statusCode: 404, - }); - }); - }); - - describe('transformFindAlerts', () => { - test('outputs empty data set when data set is empty correct', () => { - const output = transformFindAlerts({ data: [], page: 1, perPage: 0, total: 0 }, []); - expect(output).toEqual({ data: [], page: 1, perPage: 0, total: 0 }); - }); - - test('outputs 200 if the data is of type siem alert', () => { - const output = transformFindAlerts( - { - page: 1, - perPage: 0, - total: 0, - data: [getResult()], - }, - [] - ); - const expected = getOutputRuleAlertForRest(); - expect(output).toEqual({ - page: 1, - perPage: 0, - total: 0, - data: [expected], - }); - }); - - test('returns 500 if the data is not of type siem alert', () => { - const unsafeCast = ([{ name: 'something else' }] as unknown) as SanitizedAlert[]; - const output = transformFindAlerts( - { - data: unsafeCast, - page: 1, - perPage: 1, - total: 1, - }, - [] - ); - expect(output).toBeNull(); - }); - }); - - describe('transform', () => { - test('outputs 200 if the data is of type siem alert', () => { - const output = transform(getResult()); - const expected = getOutputRuleAlertForRest(); - expect(output).toEqual(expected); - }); - - test('returns 500 if the data is not of type siem alert', () => { - const unsafeCast = ({ data: [{ random: 1 }] } as unknown) as PartialAlert; - const output = transform(unsafeCast); - expect(output).toBeNull(); - }); - }); - - describe('transformTags', () => { - test('it returns tags that have no internal structures', () => { - expect(transformTags(['tag 1', 'tag 2'])).toEqual(['tag 1', 'tag 2']); - }); - - test('it returns empty tags given empty tags', () => { - expect(transformTags([])).toEqual([]); - }); - - test('it returns tags with internal tags stripped out', () => { - expect(transformTags(['tag 1', `${INTERNAL_IDENTIFIER}_some_value`, 'tag 2'])).toEqual([ - 'tag 1', - 'tag 2', - ]); - }); - }); - - describe('getIdBulkError', () => { - test('outputs message about id and rule_id not being found if both are not null', () => { - const error = getIdBulkError({ id: '123', ruleId: '456' }); - const expected: BulkError = { - id: '123', - rule_id: '456', - error: { message: 'id: "123" and rule_id: "456" not found', status_code: 404 }, - }; - expect(error).toEqual(expected); - }); - - test('outputs message about id not being found if only id is defined and ruleId is undefined', () => { - const error = getIdBulkError({ id: '123', ruleId: undefined }); - const expected: BulkError = { - id: '123', - error: { message: 'id: "123" not found', status_code: 404 }, - }; - expect(error).toEqual(expected); - }); - - test('outputs message about id not being found if only id is defined and ruleId is null', () => { - const error = getIdBulkError({ id: '123', ruleId: null }); - const expected: BulkError = { - id: '123', - error: { message: 'id: "123" not found', status_code: 404 }, - }; - expect(error).toEqual(expected); - }); - - test('outputs message about ruleId not being found if only ruleId is defined and id is undefined', () => { - const error = getIdBulkError({ id: undefined, ruleId: 'rule-id-123' }); - const expected: BulkError = { - rule_id: 'rule-id-123', - error: { message: 'rule_id: "rule-id-123" not found', status_code: 404 }, - }; - expect(error).toEqual(expected); - }); - - test('outputs message about ruleId not being found if only ruleId is defined and id is null', () => { - const error = getIdBulkError({ id: null, ruleId: 'rule-id-123' }); - const expected: BulkError = { - rule_id: 'rule-id-123', - error: { message: 'rule_id: "rule-id-123" not found', status_code: 404 }, - }; - expect(error).toEqual(expected); - }); - - test('outputs message about both being not defined when both are undefined', () => { - const error = getIdBulkError({ id: undefined, ruleId: undefined }); - const expected: BulkError = { - rule_id: '(unknown id)', - error: { message: 'id or rule_id should have been defined', status_code: 404 }, - }; - expect(error).toEqual(expected); - }); - - test('outputs message about both being not defined when both are null', () => { - const error = getIdBulkError({ id: null, ruleId: null }); - const expected: BulkError = { - rule_id: '(unknown id)', - error: { message: 'id or rule_id should have been defined', status_code: 404 }, - }; - expect(error).toEqual(expected); - }); - - test('outputs message about both being not defined when id is null and ruleId is undefined', () => { - const error = getIdBulkError({ id: null, ruleId: undefined }); - const expected: BulkError = { - rule_id: '(unknown id)', - error: { message: 'id or rule_id should have been defined', status_code: 404 }, - }; - expect(error).toEqual(expected); - }); - - test('outputs message about both being not defined when id is undefined and ruleId is null', () => { - const error = getIdBulkError({ id: undefined, ruleId: null }); - const expected: BulkError = { - rule_id: '(unknown id)', - error: { message: 'id or rule_id should have been defined', status_code: 404 }, - }; - expect(error).toEqual(expected); - }); - }); - - describe('transformOrBulkError', () => { - test('outputs 200 if the data is of type siem alert', () => { - const output = transformOrBulkError('rule-1', getResult(), { - id: '04128c15-0d1b-4716-a4c5-46997ac7f3bd', - actions: [], - ruleThrottle: 'no_actions', - alertThrottle: null, - }); - const expected = getOutputRuleAlertForRest(); - expect(output).toEqual(expected); - }); - - test('returns 500 if the data is not of type siem alert', () => { - const unsafeCast = ({ name: 'something else' } as unknown) as PartialAlert; - const output = transformOrBulkError('rule-1', unsafeCast, { - id: '04128c15-0d1b-4716-a4c5-46997ac7f3bd', - actions: [], - ruleThrottle: 'no_actions', - alertThrottle: null, - }); - const expected: BulkError = { - rule_id: 'rule-1', - error: { message: 'Internal error transforming', status_code: 500 }, - }; - expect(output).toEqual(expected); - }); - }); - - describe('transformDataToNdjson', () => { - test('if rules are empty it returns an empty string', () => { - const ruleNdjson = transformDataToNdjson([]); - expect(ruleNdjson).toEqual(''); - }); - - test('single rule will transform with new line ending character for ndjson', () => { - const rule = sampleRule(); - const ruleNdjson = transformDataToNdjson([rule]); - expect(ruleNdjson.endsWith('\n')).toBe(true); - }); - - test('multiple rules will transform with two new line ending characters for ndjson', () => { - const result1 = sampleRule(); - const result2 = sampleRule(); - result2.id = 'some other id'; - result2.rule_id = 'some other id'; - result2.name = 'Some other rule'; - - const ruleNdjson = transformDataToNdjson([result1, result2]); - // this is how we count characters in JavaScript :-) - const count = ruleNdjson.split('\n').length - 1; - expect(count).toBe(2); - }); - - test('you can parse two rules back out without errors', () => { - const result1 = sampleRule(); - const result2 = sampleRule(); - result2.id = 'some other id'; - result2.rule_id = 'some other id'; - result2.name = 'Some other rule'; - - const ruleNdjson = transformDataToNdjson([result1, result2]); - const ruleStrings = ruleNdjson.split('\n'); - const reParsed1 = JSON.parse(ruleStrings[0]); - const reParsed2 = JSON.parse(ruleStrings[1]); - expect(reParsed1).toEqual(result1); - expect(reParsed2).toEqual(result2); - }); - }); - - describe('transformAlertsToRules', () => { - test('given an empty array returns an empty array', () => { - expect(transformAlertsToRules([])).toEqual([]); - }); - - test('given single alert will return the alert transformed', () => { - const result1 = getResult(); - const transformed = transformAlertsToRules([result1]); - const expected = getOutputRuleAlertForRest(); - expect(transformed).toEqual([expected]); - }); - - test('given two alerts will return the two alerts transformed', () => { - const result1 = getResult(); - const result2 = getResult(); - result2.id = 'some other id'; - result2.params.ruleId = 'some other id'; - - const transformed = transformAlertsToRules([result1, result2]); - const expected1 = getOutputRuleAlertForRest(); - const expected2 = getOutputRuleAlertForRest(); - expected2.id = 'some other id'; - expected2.rule_id = 'some other id'; - expect(transformed).toEqual([expected1, expected2]); - }); - }); - - describe('transformOrImportError', () => { - test('returns 1 given success if the alert is an alert type and the existing success count is 0', () => { - const output = transformOrImportError('rule-1', getResult(), { - success: true, - success_count: 0, - errors: [], - }); - const expected: ImportSuccessError = { - success: true, - errors: [], - success_count: 1, - }; - expect(output).toEqual(expected); - }); - - test('returns 2 given successes if the alert is an alert type and the existing success count is 1', () => { - const output = transformOrImportError('rule-1', getResult(), { - success: true, - success_count: 1, - errors: [], - }); - const expected: ImportSuccessError = { - success: true, - errors: [], - success_count: 2, - }; - expect(output).toEqual(expected); - }); - - test('returns 1 error and success of false if the data is not of type siem alert', () => { - const unsafeCast = ({ name: 'something else' } as unknown) as PartialAlert; - const output = transformOrImportError('rule-1', unsafeCast, { - success: true, - success_count: 1, - errors: [], - }); - const expected: ImportSuccessError = { - success: false, - errors: [ - { - rule_id: 'rule-1', - error: { - message: 'Internal error transforming', - status_code: 500, - }, - }, - ], - success_count: 1, - }; - expect(output).toEqual(expected); - }); - }); - - describe('getDuplicates', () => { - test("returns array of ruleIds showing the duplicate keys of 'value2' and 'value3'", () => { - const output = getDuplicates( - [ - { rule_id: 'value1' }, - { rule_id: 'value2' }, - { rule_id: 'value2' }, - { rule_id: 'value3' }, - { rule_id: 'value3' }, - {}, - {}, - ] as RuleAlertParamsRest[], - 'rule_id' - ); - const expected = ['value2', 'value3']; - expect(output).toEqual(expected); - }); - test('returns null when given a map of no duplicates', () => { - const output = getDuplicates( - [ - { rule_id: 'value1' }, - { rule_id: 'value2' }, - { rule_id: 'value3' }, - {}, - {}, - ] as RuleAlertParamsRest[], - 'rule_id' - ); - const expected: string[] = []; - expect(output).toEqual(expected); - }); - }); - - describe('getTupleDuplicateErrorsAndUniqueRules', () => { - test('returns tuple of empty duplicate errors array and rule array with instance of Syntax Error when imported rule contains parse error', async () => { - const multipartPayload = - '{"name"::"Simple Rule Query","description":"Simple Rule Query","risk_score":1,"rule_id":"rule-1","severity":"high","type":"query","query":"user.name: root or user.name: admin"}\n'; - const ndJsonStream = new Readable({ - read() { - this.push(multipartPayload); - this.push(null); - }, - }); - const rulesObjectsStream = createRulesStreamFromNdJson(1000); - const parsedObjects = await createPromiseFromStreams<PromiseFromStreams[]>([ - ndJsonStream, - ...rulesObjectsStream, - ]); - const [errors, output] = getTupleDuplicateErrorsAndUniqueRules(parsedObjects, false); - const isInstanceOfError = output[0] instanceof Error; - - expect(isInstanceOfError).toEqual(true); - expect(errors.length).toEqual(0); - }); - - test('returns tuple of duplicate conflict error and single rule when rules with matching rule-ids passed in and `overwrite` is false', async () => { - const rule = getSimpleRule('rule-1'); - const rule2 = getSimpleRule('rule-1'); - const ndJsonStream = new Readable({ - read() { - this.push(`${JSON.stringify(rule)}\n`); - this.push(`${JSON.stringify(rule2)}\n`); - this.push(null); - }, - }); - const rulesObjectsStream = createRulesStreamFromNdJson(1000); - const parsedObjects = await createPromiseFromStreams<PromiseFromStreams[]>([ - ndJsonStream, - ...rulesObjectsStream, - ]); - const [errors, output] = getTupleDuplicateErrorsAndUniqueRules(parsedObjects, false); - - expect(output.length).toEqual(1); - expect(errors).toEqual([ - { - error: { - message: 'More than one rule with rule-id: "rule-1" found', - status_code: 400, - }, - rule_id: 'rule-1', - }, - ]); - }); - - test('returns tuple of duplicate conflict error and single rule when rules with matching ids passed in and `overwrite` is false', async () => { - const rule = getSimpleRule('rule-1'); - delete rule.rule_id; - const rule2 = getSimpleRule('rule-1'); - delete rule2.rule_id; - const ndJsonStream = new Readable({ - read() { - this.push(`${JSON.stringify(rule)}\n`); - this.push(`${JSON.stringify(rule2)}\n`); - this.push(null); - }, - }); - const rulesObjectsStream = createRulesStreamFromNdJson(1000); - const parsedObjects = await createPromiseFromStreams<PromiseFromStreams[]>([ - ndJsonStream, - ...rulesObjectsStream, - ]); - const [errors, output] = getTupleDuplicateErrorsAndUniqueRules(parsedObjects, false); - const isInstanceOfError = output[0] instanceof Error; - - expect(isInstanceOfError).toEqual(true); - expect(errors).toEqual([]); - }); - - test('returns tuple of empty duplicate errors array and single rule when rules with matching rule-ids passed in and `overwrite` is true', async () => { - const rule = getSimpleRule('rule-1'); - const rule2 = getSimpleRule('rule-1'); - const ndJsonStream = new Readable({ - read() { - this.push(`${JSON.stringify(rule)}\n`); - this.push(`${JSON.stringify(rule2)}\n`); - this.push(null); - }, - }); - const rulesObjectsStream = createRulesStreamFromNdJson(1000); - const parsedObjects = await createPromiseFromStreams<PromiseFromStreams[]>([ - ndJsonStream, - ...rulesObjectsStream, - ]); - const [errors, output] = getTupleDuplicateErrorsAndUniqueRules(parsedObjects, true); - - expect(output.length).toEqual(1); - expect(errors.length).toEqual(0); - }); - - test('returns tuple of empty duplicate errors array and single rule when rules without a rule-id is passed in', async () => { - const simpleRule = getSimpleRule(); - delete simpleRule.rule_id; - const multipartPayload = `${JSON.stringify(simpleRule)}\n`; - const ndJsonStream = new Readable({ - read() { - this.push(multipartPayload); - this.push(null); - }, - }); - const rulesObjectsStream = createRulesStreamFromNdJson(1000); - const parsedObjects = await createPromiseFromStreams<PromiseFromStreams[]>([ - ndJsonStream, - ...rulesObjectsStream, - ]); - const [errors, output] = getTupleDuplicateErrorsAndUniqueRules(parsedObjects, false); - const isInstanceOfError = output[0] instanceof Error; - - expect(isInstanceOfError).toEqual(true); - expect(errors.length).toEqual(0); - }); - }); -}); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/utils.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/utils.ts deleted file mode 100644 index 4d13fa1b6ae50..0000000000000 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/utils.ts +++ /dev/null @@ -1,303 +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 { pickBy, countBy } from 'lodash/fp'; -import { SavedObject, SavedObjectsFindResponse } from 'kibana/server'; -import uuid from 'uuid'; - -import { PartialAlert, FindResult } from '../../../../../../../../plugins/alerting/server'; -import { INTERNAL_IDENTIFIER } from '../../../../../common/constants'; -import { - RuleAlertType, - isAlertType, - isAlertTypes, - IRuleSavedAttributesSavedObjectAttributes, - isRuleStatusFindType, - isRuleStatusFindTypes, - isRuleStatusSavedObjectType, -} from '../../rules/types'; -import { OutputRuleAlertRest, ImportRuleAlertRest, RuleAlertParamsRest } from '../../types'; -import { - createBulkErrorObject, - BulkError, - createSuccessObject, - ImportSuccessError, - createImportErrorObject, - OutputError, -} from '../utils'; -import { hasListsFeature } from '../../feature_flags'; -// import { transformAlertToRuleAction } from '../../../../../common/detection_engine/transform_actions'; -import { RuleActions } from '../../rule_actions/types'; - -type PromiseFromStreams = ImportRuleAlertRest | Error; - -export const getIdError = ({ - id, - ruleId, -}: { - id: string | undefined | null; - ruleId: string | undefined | null; -}): OutputError => { - if (id != null) { - return { - message: `id: "${id}" not found`, - statusCode: 404, - }; - } else if (ruleId != null) { - return { - message: `rule_id: "${ruleId}" not found`, - statusCode: 404, - }; - } else { - return { - message: 'id or rule_id should have been defined', - statusCode: 404, - }; - } -}; - -export const getIdBulkError = ({ - id, - ruleId, -}: { - id: string | undefined | null; - ruleId: string | undefined | null; -}): BulkError => { - if (id != null && ruleId != null) { - return createBulkErrorObject({ - id, - ruleId, - statusCode: 404, - message: `id: "${id}" and rule_id: "${ruleId}" not found`, - }); - } else if (id != null) { - return createBulkErrorObject({ - id, - statusCode: 404, - message: `id: "${id}" not found`, - }); - } else if (ruleId != null) { - return createBulkErrorObject({ - ruleId, - statusCode: 404, - message: `rule_id: "${ruleId}" not found`, - }); - } else { - return createBulkErrorObject({ - statusCode: 404, - message: `id or rule_id should have been defined`, - }); - } -}; - -export const transformTags = (tags: string[]): string[] => { - return tags.filter(tag => !tag.startsWith(INTERNAL_IDENTIFIER)); -}; - -// Transforms the data but will remove any null or undefined it encounters and not include -// those on the export -export const transformAlertToRule = ( - alert: RuleAlertType, - ruleActions?: RuleActions | null, - ruleStatus?: SavedObject<IRuleSavedAttributesSavedObjectAttributes> -): Partial<OutputRuleAlertRest> => { - return pickBy<OutputRuleAlertRest>((value: unknown) => value != null, { - actions: ruleActions?.actions ?? [], - created_at: alert.createdAt.toISOString(), - updated_at: alert.updatedAt.toISOString(), - created_by: alert.createdBy, - description: alert.params.description, - enabled: alert.enabled, - anomaly_threshold: alert.params.anomalyThreshold, - false_positives: alert.params.falsePositives, - filters: alert.params.filters, - from: alert.params.from, - id: alert.id, - immutable: alert.params.immutable, - index: alert.params.index, - interval: alert.schedule.interval, - rule_id: alert.params.ruleId, - language: alert.params.language, - output_index: alert.params.outputIndex, - max_signals: alert.params.maxSignals, - machine_learning_job_id: alert.params.machineLearningJobId, - risk_score: alert.params.riskScore, - name: alert.name, - query: alert.params.query, - references: alert.params.references, - saved_id: alert.params.savedId, - timeline_id: alert.params.timelineId, - timeline_title: alert.params.timelineTitle, - meta: alert.params.meta, - severity: alert.params.severity, - updated_by: alert.updatedBy, - tags: transformTags(alert.tags), - to: alert.params.to, - type: alert.params.type, - threat: alert.params.threat, - throttle: ruleActions?.ruleThrottle || 'no_actions', - note: alert.params.note, - version: alert.params.version, - status: ruleStatus?.attributes.status, - status_date: ruleStatus?.attributes.statusDate, - last_failure_at: ruleStatus?.attributes.lastFailureAt, - last_success_at: ruleStatus?.attributes.lastSuccessAt, - last_failure_message: ruleStatus?.attributes.lastFailureMessage, - last_success_message: ruleStatus?.attributes.lastSuccessMessage, - // TODO: (LIST-FEATURE) Remove hasListsFeature() check once we have lists available for a release - lists: hasListsFeature() ? alert.params.lists : null, - }); -}; - -export const transformDataToNdjson = (data: unknown[]): string => { - if (data.length !== 0) { - const dataString = data.map(rule => JSON.stringify(rule)).join('\n'); - return `${dataString}\n`; - } else { - return ''; - } -}; - -export const transformAlertsToRules = ( - alerts: RuleAlertType[] -): Array<Partial<OutputRuleAlertRest>> => { - return alerts.map(alert => transformAlertToRule(alert)); -}; - -export const transformFindAlerts = ( - findResults: FindResult, - ruleActions: Array<RuleActions | null>, - ruleStatuses?: Array<SavedObjectsFindResponse<IRuleSavedAttributesSavedObjectAttributes>> -): { - page: number; - perPage: number; - total: number; - data: Array<Partial<OutputRuleAlertRest>>; -} | null => { - if (!ruleStatuses && isAlertTypes(findResults.data)) { - return { - page: findResults.page, - perPage: findResults.perPage, - total: findResults.total, - data: findResults.data.map((alert, idx) => transformAlertToRule(alert, ruleActions[idx])), - }; - } else if (isAlertTypes(findResults.data) && isRuleStatusFindTypes(ruleStatuses)) { - return { - page: findResults.page, - perPage: findResults.perPage, - total: findResults.total, - data: findResults.data.map((alert, idx) => - transformAlertToRule(alert, ruleActions[idx], ruleStatuses[idx].saved_objects[0]) - ), - }; - } else { - return null; - } -}; - -export const transform = ( - alert: PartialAlert, - ruleActions?: RuleActions | null, - ruleStatus?: SavedObject<IRuleSavedAttributesSavedObjectAttributes> -): Partial<OutputRuleAlertRest> | null => { - if (isAlertType(alert)) { - return transformAlertToRule( - alert, - ruleActions, - isRuleStatusSavedObjectType(ruleStatus) ? ruleStatus : undefined - ); - } - - return null; -}; - -export const transformOrBulkError = ( - ruleId: string, - alert: PartialAlert, - ruleActions: RuleActions, - ruleStatus?: unknown -): Partial<OutputRuleAlertRest> | BulkError => { - if (isAlertType(alert)) { - if (isRuleStatusFindType(ruleStatus) && ruleStatus?.saved_objects.length > 0) { - return transformAlertToRule(alert, ruleActions, ruleStatus?.saved_objects[0] ?? ruleStatus); - } else { - return transformAlertToRule(alert, ruleActions); - } - } else { - return createBulkErrorObject({ - ruleId, - statusCode: 500, - message: 'Internal error transforming', - }); - } -}; - -export const transformOrImportError = ( - ruleId: string, - alert: PartialAlert, - existingImportSuccessError: ImportSuccessError -): ImportSuccessError => { - if (isAlertType(alert)) { - return createSuccessObject(existingImportSuccessError); - } else { - return createImportErrorObject({ - ruleId, - statusCode: 500, - message: 'Internal error transforming', - existingImportSuccessError, - }); - } -}; - -export const getDuplicates = (ruleDefinitions: RuleAlertParamsRest[], by: 'rule_id'): string[] => { - const mappedDuplicates = countBy( - by, - ruleDefinitions.filter(r => r[by] != null) - ); - const hasDuplicates = Object.values(mappedDuplicates).some(i => i > 1); - if (hasDuplicates) { - return Object.keys(mappedDuplicates).filter(key => mappedDuplicates[key] > 1); - } - return []; -}; - -export const getTupleDuplicateErrorsAndUniqueRules = ( - rules: PromiseFromStreams[], - isOverwrite: boolean -): [BulkError[], PromiseFromStreams[]] => { - const { errors, rulesAcc } = rules.reduce( - (acc, parsedRule) => { - if (parsedRule instanceof Error) { - acc.rulesAcc.set(uuid.v4(), parsedRule); - } else { - const { rule_id: ruleId } = parsedRule; - if (ruleId != null) { - if (acc.rulesAcc.has(ruleId) && !isOverwrite) { - acc.errors.set( - uuid.v4(), - createBulkErrorObject({ - ruleId, - statusCode: 400, - message: `More than one rule with rule-id: "${ruleId}" found`, - }) - ); - } - acc.rulesAcc.set(ruleId, parsedRule); - } else { - acc.rulesAcc.set(uuid.v4(), parsedRule); - } - } - - return acc; - }, // using map (preserves ordering) - { - errors: new Map<string, BulkError>(), - rulesAcc: new Map<string, PromiseFromStreams>(), - } - ); - - return [Array.from(errors.values()), Array.from(rulesAcc.values())]; -}; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/response/__mocks__/utils.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/response/__mocks__/utils.ts deleted file mode 100644 index d5ea950d163f5..0000000000000 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/response/__mocks__/utils.ts +++ /dev/null @@ -1,144 +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 * as t from 'io-ts'; -import { fold } from 'fp-ts/lib/Either'; -import { RulesSchema } from '../rules_schema'; -import { RulesBulkSchema } from '../rules_bulk_schema'; -import { ErrorSchema } from '../error_schema'; -import { FindRulesSchema } from '../find_rules_schema'; -import { formatErrors } from '../utils'; -import { pipe } from 'fp-ts/lib/pipeable'; - -interface Message<T> { - errors: t.Errors; - schema: T | {}; -} - -const onLeft = <T>(errors: t.Errors): Message<T> => { - return { schema: {}, errors }; -}; - -const onRight = <T>(schema: T): Message<T> => { - return { - schema, - errors: [], - }; -}; - -export const foldLeftRight = fold(onLeft, onRight); - -export const ANCHOR_DATE = '2020-02-20T03:57:54.037Z'; - -export const getBaseResponsePayload = (anchorDate: string = ANCHOR_DATE): RulesSchema => ({ - id: '7a7065d7-6e8b-4aae-8d20-c93613dec9f9', - created_at: new Date(anchorDate).toISOString(), - updated_at: new Date(anchorDate).toISOString(), - created_by: 'elastic', - description: 'some description', - enabled: true, - false_positives: ['false positive 1', 'false positive 2'], - from: 'now-6m', - immutable: false, - name: 'Query with a rule id', - query: 'user.name: root or user.name: admin', - references: ['test 1', 'test 2'], - severity: 'high', - updated_by: 'elastic_kibana', - tags: [], - to: 'now', - type: 'query', - threat: [], - version: 1, - status: 'succeeded', - status_date: '2020-02-22T16:47:50.047Z', - last_success_at: '2020-02-22T16:47:50.047Z', - last_success_message: 'succeeded', - output_index: '.siem-signals-hassanabad-frank-default', - max_signals: 100, - risk_score: 55, - language: 'kuery', - rule_id: 'query-rule-id', - interval: '5m', - lists: [ - { - field: 'source.ip', - values_operator: 'included', - values_type: 'exists', - }, - { - field: 'host.name', - values_operator: 'excluded', - values_type: 'match', - values: [ - { - name: 'rock01', - }, - ], - and: [ - { - field: 'host.id', - values_operator: 'included', - values_type: 'match_all', - values: [ - { - name: '123', - }, - { - name: '678', - }, - ], - }, - ], - }, - ], -}); - -export const getRulesBulkPayload = (): RulesBulkSchema => [getBaseResponsePayload()]; - -export const getMlRuleResponsePayload = (anchorDate: string = ANCHOR_DATE): RulesSchema => { - const basePayload = getBaseResponsePayload(anchorDate); - const { filters, index, query, language, ...rest } = basePayload; - - return { - ...rest, - type: 'machine_learning', - anomaly_threshold: 59, - machine_learning_job_id: 'some_machine_learning_job_id', - }; -}; - -export const getErrorPayload = ( - id: string = '819eded6-e9c8-445b-a647-519aea39e063' -): ErrorSchema => ({ - id, - error: { - status_code: 404, - message: 'id: "819eded6-e9c8-445b-a647-519aea39e063" not found', - }, -}); - -export const getFindResponseSingle = (): FindRulesSchema => ({ - page: 1, - perPage: 1, - total: 1, - data: [getBaseResponsePayload()], -}); - -/** - * Convenience utility to keep the error message handling within tests to be - * very concise. - * @param validation The validation to get the errors from - */ -export const getPaths = <A>(validation: t.Validation<A>): string[] => { - return pipe( - validation, - fold( - errors => formatErrors(errors), - () => ['no errors'] - ) - ); -}; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/types/lists_default_array.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/types/lists_default_array.ts deleted file mode 100644 index 85a38e296494a..0000000000000 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/types/lists_default_array.ts +++ /dev/null @@ -1,27 +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 * as t from 'io-ts'; -import { Either } from 'fp-ts/lib/Either'; - -import { list_and as listAnd } from '../response/schemas'; - -export type ListsDefaultArrayC = t.Type<List[], List[], unknown>; -type List = t.TypeOf<typeof listAnd>; - -/** - * Types the ListsDefaultArray as: - * - If null or undefined, then a default array will be set for the list - */ -export const ListsDefaultArray: ListsDefaultArrayC = new t.Type<List[], List[], unknown>( - 'listsWithDefaultArray', - t.array(listAnd).is, - (input): Either<t.Errors, List[]> => - input == null ? t.success([]) : t.array(listAnd).decode(input), - t.identity -); - -export type ListsDefaultArraySchema = t.TypeOf<typeof ListsDefaultArray>; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/utils.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/utils.test.ts deleted file mode 100644 index 9efe4e491968b..0000000000000 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/utils.test.ts +++ /dev/null @@ -1,396 +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 Boom from 'boom'; - -import { SavedObjectsFindResponse } from 'kibana/server'; -import { IRuleSavedAttributesSavedObjectAttributes, IRuleStatusAttributes } from '../rules/types'; -import { BadRequestError } from '../errors/bad_request_error'; -import { - transformError, - transformBulkError, - BulkError, - createSuccessObject, - ImportSuccessError, - createImportErrorObject, - transformImportError, - convertToSnakeCase, - SiemResponseFactory, - validateLicenseForRuleType, -} from './utils'; -import { responseMock } from './__mocks__'; -import { setFeatureFlagsForTestsOnly, unSetFeatureFlagsForTestsOnly } from '../feature_flags'; -import { licensingMock } from '../../../../../../../plugins/licensing/server/mocks'; - -describe('utils', () => { - beforeAll(() => { - setFeatureFlagsForTestsOnly(); - }); - - afterAll(() => { - unSetFeatureFlagsForTestsOnly(); - }); - - describe('transformError', () => { - test('returns transformed output error from boom object with a 500 and payload of internal server error', () => { - const boom = new Boom('some boom message'); - const transformed = transformError(boom); - expect(transformed).toEqual({ - message: 'An internal server error occurred', - statusCode: 500, - }); - }); - - test('returns transformed output if it is some non boom object that has a statusCode', () => { - const error: Error & { statusCode?: number } = { - statusCode: 403, - name: 'some name', - message: 'some message', - }; - const transformed = transformError(error); - expect(transformed).toEqual({ - message: 'some message', - statusCode: 403, - }); - }); - - test('returns a transformed message with the message set and statusCode', () => { - const error: Error & { statusCode?: number } = { - statusCode: 403, - name: 'some name', - message: 'some message', - }; - const transformed = transformError(error); - expect(transformed).toEqual({ - message: 'some message', - statusCode: 403, - }); - }); - - test('transforms best it can if it is some non boom object but it does not have a status Code.', () => { - const error: Error = { - name: 'some name', - message: 'some message', - }; - const transformed = transformError(error); - expect(transformed).toEqual({ - message: 'some message', - statusCode: 500, - }); - }); - - test('it detects a BadRequestError and returns a status code of 400 from that particular error type', () => { - const error: BadRequestError = new BadRequestError('I have a type error'); - const transformed = transformError(error); - expect(transformed).toEqual({ - message: 'I have a type error', - statusCode: 400, - }); - }); - - test('it detects a BadRequestError and returns a Boom status of 400', () => { - const error: BadRequestError = new BadRequestError('I have a type error'); - const transformed = transformError(error); - expect(transformed).toEqual({ - message: 'I have a type error', - statusCode: 400, - }); - }); - }); - - describe('transformBulkError', () => { - test('returns transformed object if it is a boom object', () => { - const boom = new Boom('some boom message', { statusCode: 400 }); - const transformed = transformBulkError('rule-1', boom); - const expected: BulkError = { - rule_id: 'rule-1', - error: { message: 'some boom message', status_code: 400 }, - }; - expect(transformed).toEqual(expected); - }); - - test('returns a normal error if it is some non boom object that has a statusCode', () => { - const error: Error & { statusCode?: number } = { - statusCode: 403, - name: 'some name', - message: 'some message', - }; - const transformed = transformBulkError('rule-1', error); - const expected: BulkError = { - rule_id: 'rule-1', - error: { message: 'some message', status_code: 403 }, - }; - expect(transformed).toEqual(expected); - }); - - test('returns a 500 if the status code is not set', () => { - const error: Error & { statusCode?: number } = { - name: 'some name', - message: 'some message', - }; - const transformed = transformBulkError('rule-1', error); - const expected: BulkError = { - rule_id: 'rule-1', - error: { message: 'some message', status_code: 500 }, - }; - expect(transformed).toEqual(expected); - }); - - test('it detects a BadRequestError and returns a Boom status of 400', () => { - const error: BadRequestError = new BadRequestError('I have a type error'); - const transformed = transformBulkError('rule-1', error); - const expected: BulkError = { - rule_id: 'rule-1', - error: { message: 'I have a type error', status_code: 400 }, - }; - expect(transformed).toEqual(expected); - }); - }); - - describe('createSuccessObject', () => { - test('it should increment the existing success object by 1', () => { - const success = createSuccessObject({ - success_count: 0, - success: true, - errors: [], - }); - const expected: ImportSuccessError = { - success_count: 1, - success: true, - errors: [], - }; - expect(success).toEqual(expected); - }); - - test('it should increment the existing success object by 1 and not touch the boolean or errors', () => { - const success = createSuccessObject({ - success_count: 0, - success: false, - errors: [ - { rule_id: 'rule-1', error: { status_code: 500, message: 'some sad sad sad error' } }, - ], - }); - const expected: ImportSuccessError = { - success_count: 1, - success: false, - errors: [ - { rule_id: 'rule-1', error: { status_code: 500, message: 'some sad sad sad error' } }, - ], - }; - expect(success).toEqual(expected); - }); - }); - - describe('createImportErrorObject', () => { - test('it creates an error message and does not increment the success count', () => { - const error = createImportErrorObject({ - ruleId: 'some-rule-id', - statusCode: 400, - message: 'some-message', - existingImportSuccessError: { - success_count: 1, - success: true, - errors: [], - }, - }); - const expected: ImportSuccessError = { - success_count: 1, - success: false, - errors: [{ rule_id: 'some-rule-id', error: { status_code: 400, message: 'some-message' } }], - }; - expect(error).toEqual(expected); - }); - - test('appends a second error message and does not increment the success count', () => { - const error = createImportErrorObject({ - ruleId: 'some-rule-id', - statusCode: 400, - message: 'some-message', - existingImportSuccessError: { - success_count: 1, - success: false, - errors: [ - { rule_id: 'rule-1', error: { status_code: 500, message: 'some sad sad sad error' } }, - ], - }, - }); - const expected: ImportSuccessError = { - success_count: 1, - success: false, - errors: [ - { rule_id: 'rule-1', error: { status_code: 500, message: 'some sad sad sad error' } }, - { rule_id: 'some-rule-id', error: { status_code: 400, message: 'some-message' } }, - ], - }; - expect(error).toEqual(expected); - }); - }); - - describe('transformImportError', () => { - test('returns transformed object if it is a boom object', () => { - const boom = new Boom('some boom message', { statusCode: 400 }); - const transformed = transformImportError('rule-1', boom, { - success_count: 1, - success: false, - errors: [{ rule_id: 'some-rule-id', error: { status_code: 400, message: 'some-message' } }], - }); - const expected: ImportSuccessError = { - success_count: 1, - success: false, - errors: [ - { rule_id: 'some-rule-id', error: { status_code: 400, message: 'some-message' } }, - { rule_id: 'rule-1', error: { status_code: 400, message: 'some boom message' } }, - ], - }; - expect(transformed).toEqual(expected); - }); - - test('returns a normal error if it is some non boom object that has a statusCode', () => { - const error: Error & { statusCode?: number } = { - statusCode: 403, - name: 'some name', - message: 'some message', - }; - const transformed = transformImportError('rule-1', error, { - success_count: 1, - success: false, - errors: [{ rule_id: 'some-rule-id', error: { status_code: 400, message: 'some-message' } }], - }); - const expected: ImportSuccessError = { - success_count: 1, - success: false, - errors: [ - { rule_id: 'some-rule-id', error: { status_code: 400, message: 'some-message' } }, - { rule_id: 'rule-1', error: { status_code: 403, message: 'some message' } }, - ], - }; - expect(transformed).toEqual(expected); - }); - - test('returns a 500 if the status code is not set', () => { - const error: Error & { statusCode?: number } = { - name: 'some name', - message: 'some message', - }; - const transformed = transformImportError('rule-1', error, { - success_count: 1, - success: false, - errors: [{ rule_id: 'some-rule-id', error: { status_code: 400, message: 'some-message' } }], - }); - const expected: ImportSuccessError = { - success_count: 1, - success: false, - errors: [ - { rule_id: 'some-rule-id', error: { status_code: 400, message: 'some-message' } }, - { rule_id: 'rule-1', error: { status_code: 500, message: 'some message' } }, - ], - }; - expect(transformed).toEqual(expected); - }); - - test('it detects a BadRequestError and returns a Boom status of 400', () => { - const error: BadRequestError = new BadRequestError('I have a type error'); - const transformed = transformImportError('rule-1', error, { - success_count: 1, - success: false, - errors: [{ rule_id: 'some-rule-id', error: { status_code: 400, message: 'some-message' } }], - }); - const expected: ImportSuccessError = { - success_count: 1, - success: false, - errors: [ - { rule_id: 'some-rule-id', error: { status_code: 400, message: 'some-message' } }, - { rule_id: 'rule-1', error: { status_code: 400, message: 'I have a type error' } }, - ], - }; - expect(transformed).toEqual(expected); - }); - }); - - describe('convertToSnakeCase', () => { - it('converts camelCase to snakeCase', () => { - const values = { myTestCamelCaseKey: 'something' }; - expect(convertToSnakeCase(values)).toEqual({ my_test_camel_case_key: 'something' }); - }); - it('returns empty object when object is empty', () => { - const values = {}; - expect(convertToSnakeCase(values)).toEqual({}); - }); - it('returns null when passed in undefined', () => { - // Array accessors can result in undefined but - // this is not represented in typescript for some reason, - // https://github.com/Microsoft/TypeScript/issues/11122 - const values: SavedObjectsFindResponse<IRuleSavedAttributesSavedObjectAttributes> = { - page: 0, - per_page: 5, - total: 0, - saved_objects: [], - }; - expect( - convertToSnakeCase<IRuleStatusAttributes>(values.saved_objects[0]?.attributes) // this is undefined, but it says it's not - ).toEqual(null); - }); - }); - - describe('SiemResponseFactory', () => { - it('builds a custom response', () => { - const response = responseMock.create(); - const responseFactory = new SiemResponseFactory(response); - - responseFactory.error({ statusCode: 400 }); - expect(response.custom).toHaveBeenCalled(); - }); - - it('generates a status_code key on the response', () => { - const response = responseMock.create(); - const responseFactory = new SiemResponseFactory(response); - - responseFactory.error({ statusCode: 400 }); - const [[{ statusCode, body }]] = response.custom.mock.calls; - - expect(statusCode).toEqual(400); - expect(body).toBeInstanceOf(Buffer); - expect(JSON.parse(body!.toString())).toEqual( - expect.objectContaining({ - message: 'Bad Request', - status_code: 400, - }) - ); - }); - }); - - describe('validateLicenseForRuleType', () => { - let licenseMock: ReturnType<typeof licensingMock.createLicenseMock>; - - beforeEach(() => { - licenseMock = licensingMock.createLicenseMock(); - }); - - it('throws a BadRequestError if operating on an ML Rule with an insufficient license', () => { - licenseMock.hasAtLeast.mockReturnValue(false); - - expect(() => - validateLicenseForRuleType({ license: licenseMock, ruleType: 'machine_learning' }) - ).toThrowError(BadRequestError); - }); - - it('does not throw if operating on an ML Rule with a sufficient license', () => { - licenseMock.hasAtLeast.mockReturnValue(true); - - expect(() => - validateLicenseForRuleType({ license: licenseMock, ruleType: 'machine_learning' }) - ).not.toThrowError(BadRequestError); - }); - - it('does not throw if operating on a query rule', () => { - licenseMock.hasAtLeast.mockReturnValue(false); - - expect(() => - validateLicenseForRuleType({ license: licenseMock, ruleType: 'query' }) - ).not.toThrowError(BadRequestError); - }); - }); -}); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/utils.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/utils.ts deleted file mode 100644 index e4015ad8bafa4..0000000000000 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/utils.ts +++ /dev/null @@ -1,321 +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 Boom from 'boom'; -import Joi from 'joi'; -import { has, snakeCase } from 'lodash/fp'; -import { i18n } from '@kbn/i18n'; - -import { - RouteValidationFunction, - KibanaResponseFactory, - CustomHttpResponseOptions, -} from '../../../../../../../../src/core/server'; -import { ILicense } from '../../../../../../../plugins/licensing/server'; -import { MINIMUM_ML_LICENSE } from '../../../../common/constants'; -import { RuleType } from '../../../../common/detection_engine/types'; -import { isMlRule } from '../../../../common/detection_engine/ml_helpers'; -import { BadRequestError } from '../errors/bad_request_error'; - -export interface OutputError { - message: string; - statusCode: number; -} - -export const transformError = (err: Error & { statusCode?: number }): OutputError => { - if (Boom.isBoom(err)) { - return { - message: err.output.payload.message, - statusCode: err.output.statusCode, - }; - } else { - if (err.statusCode != null) { - return { - message: err.message, - statusCode: err.statusCode, - }; - } else if (err instanceof BadRequestError) { - // allows us to throw request validation errors in the absence of Boom - return { - message: err.message, - statusCode: 400, - }; - } else { - // natively return the err and allow the regular framework - // to deal with the error when it is a non Boom - return { - message: err.message ?? '(unknown error message)', - statusCode: 500, - }; - } - } -}; - -export interface BulkError { - id?: string; - rule_id?: string; - error: { - status_code: number; - message: string; - }; -} - -export const createBulkErrorObject = ({ - ruleId, - id, - statusCode, - message, -}: { - ruleId?: string; - id?: string; - statusCode: number; - message: string; -}): BulkError => { - if (id != null && ruleId != null) { - return { - id, - rule_id: ruleId, - error: { - status_code: statusCode, - message, - }, - }; - } else if (id != null) { - return { - id, - error: { - status_code: statusCode, - message, - }, - }; - } else if (ruleId != null) { - return { - rule_id: ruleId, - error: { - status_code: statusCode, - message, - }, - }; - } else { - return { - rule_id: '(unknown id)', - error: { - status_code: statusCode, - message, - }, - }; - } -}; - -export interface ImportRegular { - rule_id: string; - status_code: number; - message?: string; -} - -export type ImportRuleResponse = ImportRegular | BulkError; - -export const isBulkError = ( - importRuleResponse: ImportRuleResponse -): importRuleResponse is BulkError => { - return has('error', importRuleResponse); -}; - -export const isImportRegular = ( - importRuleResponse: ImportRuleResponse -): importRuleResponse is ImportRegular => { - return !has('error', importRuleResponse) && has('status_code', importRuleResponse); -}; - -export interface ImportSuccessError { - success: boolean; - success_count: number; - errors: BulkError[]; -} - -export const createSuccessObject = ( - existingImportSuccessError: ImportSuccessError -): ImportSuccessError => { - return { - success_count: existingImportSuccessError.success_count + 1, - success: existingImportSuccessError.success, - errors: existingImportSuccessError.errors, - }; -}; - -export const createImportErrorObject = ({ - ruleId, - statusCode, - message, - existingImportSuccessError, -}: { - ruleId: string; - statusCode: number; - message: string; - existingImportSuccessError: ImportSuccessError; -}): ImportSuccessError => { - return { - success: false, - errors: [ - ...existingImportSuccessError.errors, - createBulkErrorObject({ - ruleId, - statusCode, - message, - }), - ], - success_count: existingImportSuccessError.success_count, - }; -}; - -export const transformImportError = ( - ruleId: string, - err: Error & { statusCode?: number }, - existingImportSuccessError: ImportSuccessError -): ImportSuccessError => { - if (Boom.isBoom(err)) { - return createImportErrorObject({ - ruleId, - statusCode: err.output.statusCode, - message: err.message, - existingImportSuccessError, - }); - } else if (err instanceof BadRequestError) { - return createImportErrorObject({ - ruleId, - statusCode: 400, - message: err.message, - existingImportSuccessError, - }); - } else { - return createImportErrorObject({ - ruleId, - statusCode: err.statusCode ?? 500, - message: err.message, - existingImportSuccessError, - }); - } -}; - -export const transformBulkError = ( - ruleId: string, - err: Error & { statusCode?: number } -): BulkError => { - if (Boom.isBoom(err)) { - return createBulkErrorObject({ - ruleId, - statusCode: err.output.statusCode, - message: err.message, - }); - } else if (err instanceof BadRequestError) { - return createBulkErrorObject({ - ruleId, - statusCode: 400, - message: err.message, - }); - } else { - return createBulkErrorObject({ - ruleId, - statusCode: err.statusCode ?? 500, - message: err.message, - }); - } -}; - -export const buildRouteValidation = <T>(schema: Joi.Schema): RouteValidationFunction<T> => ( - payload: T, - { ok, badRequest } -) => { - const { value, error } = schema.validate(payload); - if (error) { - return badRequest(error.message); - } - return ok(value); -}; - -const statusToErrorMessage = (statusCode: number) => { - switch (statusCode) { - case 400: - return 'Bad Request'; - case 401: - return 'Unauthorized'; - case 403: - return 'Forbidden'; - case 404: - return 'Not Found'; - case 409: - return 'Conflict'; - case 500: - return 'Internal Error'; - default: - return '(unknown error)'; - } -}; - -export class SiemResponseFactory { - constructor(private response: KibanaResponseFactory) {} - - error<T>({ statusCode, body, headers }: CustomHttpResponseOptions<T>) { - const contentType: CustomHttpResponseOptions<T>['headers'] = { - 'content-type': 'application/json', - }; - const defaultedHeaders: CustomHttpResponseOptions<T>['headers'] = { - ...contentType, - ...(headers ?? {}), - }; - - return this.response.custom({ - headers: defaultedHeaders, - statusCode, - body: Buffer.from( - JSON.stringify({ - message: body ?? statusToErrorMessage(statusCode), - status_code: statusCode, - }) - ), - }); - } -} - -export const buildSiemResponse = (response: KibanaResponseFactory) => - new SiemResponseFactory(response); - -export const convertToSnakeCase = <T extends Record<string, unknown>>( - obj: T -): Partial<T> | null => { - if (!obj) { - return null; - } - return Object.keys(obj).reduce((acc, item) => { - const newKey = snakeCase(item); - return { ...acc, [newKey]: obj[item] }; - }, {}); -}; - -/** - * Checks the current Kibana License against the rule under operation. - * - * @param license ILicense representing the user license - * @param ruleType the type of the current rule - * - * @throws BadRequestError if rule and license are incompatible - */ -export const validateLicenseForRuleType = ({ - license, - ruleType, -}: { - license: ILicense; - ruleType: RuleType; -}): void => { - if (isMlRule(ruleType) && !license.hasAtLeast(MINIMUM_ML_LICENSE)) { - const message = i18n.translate('xpack.siem.licensing.unsupportedMachineLearningMessage', { - defaultMessage: - 'Your license does not support machine learning. Please upgrade your license.', - }); - - throw new BadRequestError(message); - } -}; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/create_rules_stream_from_ndjson.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/create_rules_stream_from_ndjson.ts deleted file mode 100644 index 3e22999528101..0000000000000 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/create_rules_stream_from_ndjson.ts +++ /dev/null @@ -1,87 +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 { Transform } from 'stream'; -import { has, isString } from 'lodash/fp'; -import { ImportRuleAlertRest } from '../types'; -import { - createSplitStream, - createMapStream, - createFilterStream, - createConcatStream, -} from '../../../../../../../../src/legacy/utils/streams'; -import { importRulesSchema } from '../routes/schemas/import_rules_schema'; -import { BadRequestError } from '../errors/bad_request_error'; - -export interface RulesObjectsExportResultDetails { - /** number of successfully exported objects */ - exportedCount: number; -} - -export const parseNdjsonStrings = (): Transform => { - return createMapStream((ndJsonStr: string) => { - if (isString(ndJsonStr) && ndJsonStr.trim() !== '') { - try { - return JSON.parse(ndJsonStr); - } catch (err) { - return err; - } - } - }); -}; - -export const filterExportedCounts = (): Transform => { - return createFilterStream<ImportRuleAlertRest | RulesObjectsExportResultDetails>( - obj => obj != null && !has('exported_count', obj) - ); -}; - -export const validateRules = (): Transform => { - return createMapStream((obj: ImportRuleAlertRest) => { - if (!(obj instanceof Error)) { - const validated = importRulesSchema.validate(obj); - if (validated.error != null) { - return new BadRequestError(validated.error.message); - } else { - return validated.value; - } - } else { - return obj; - } - }); -}; - -// Adaptation from: saved_objects/import/create_limit_stream.ts -export const createLimitStream = (limit: number): Transform => { - let counter = 0; - return new Transform({ - objectMode: true, - async transform(obj, _, done) { - if (counter >= limit) { - return done(new Error(`Can't import more than ${limit} rules`)); - } - counter++; - done(undefined, obj); - }, - }); -}; - -// TODO: Capture both the line number and the rule_id if you have that information for the error message -// eventually and then pass it down so we can give error messages on the line number - -/** - * Inspiration and the pattern of code followed is from: - * saved_objects/lib/create_saved_objects_stream_from_ndjson.ts - */ -export const createRulesStreamFromNdJson = (ruleLimit: number) => { - return [ - createSplitStream('\n'), - parseNdjsonStrings(), - filterExportedCounts(), - validateRules(), - createLimitStream(ruleLimit), - createConcatStream([]), - ]; -}; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/linux_anomalous_network_activity.json b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/linux_anomalous_network_activity.json deleted file mode 100644 index 1123c1161c4ce..0000000000000 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/linux_anomalous_network_activity.json +++ /dev/null @@ -1,24 +0,0 @@ -{ - "anomaly_threshold": 50, - "description": "Identifies Linux processes that do not usually use the network but have unexpected network activity, which can indicate command-and-control, lateral movement, persistence, or data exfiltration activity. A process with unusual network activity can denote process exploitation or injection, where the process is used to run persistence mechanisms that allow a malicious actor remote access or control of the host, data exfiltration, and execution of unauthorized network applications.", - "false_positives": [ - "A newly installed program or one that rarely uses the network could trigger this signal." - ], - "from": "now-16m", - "interval": "15m", - "machine_learning_job_id": "linux_anomalous_network_activity_ecs", - "name": "Unusual Linux Network Activity", - "references": [ - "https://www.elastic.co/guide/en/siem/guide/current/prebuilt-ml-jobs.html" - ], - "risk_score": 21, - "rule_id": "52afbdc5-db15-485e-bc24-f5707f820c4b", - "severity": "low", - "tags": [ - "Elastic", - "Linux", - "ML" - ], - "type": "machine_learning", - "version": 1 -} \ No newline at end of file diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/linux_anomalous_process_all_hosts.json b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/linux_anomalous_process_all_hosts.json deleted file mode 100644 index 6bac2f25fd7de..0000000000000 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/linux_anomalous_process_all_hosts.json +++ /dev/null @@ -1,24 +0,0 @@ -{ - "anomaly_threshold": 50, - "description": "Searches for rare processes running on multiple Linux hosts in an entire fleet or network. This reduces the detection of false positives since automated maintenance processes usually only run occasionally on a single machine but are common to all or many hosts in a fleet.", - "false_positives": [ - "A newly installed program or one that runs rarely as part of a monthly or quarterly workflow could trigger this signal." - ], - "from": "now-16m", - "interval": "15m", - "machine_learning_job_id": "linux_anomalous_process_all_hosts_ecs", - "name": "Anomalous Process For a Linux Population", - "references": [ - "https://www.elastic.co/guide/en/siem/guide/current/prebuilt-ml-jobs.html" - ], - "risk_score": 21, - "rule_id": "647fc812-7996-4795-8869-9c4ea595fe88", - "severity": "low", - "tags": [ - "Elastic", - "Linux", - "ML" - ], - "type": "machine_learning", - "version": 1 -} \ No newline at end of file diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/linux_anomalous_user_name.json b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/linux_anomalous_user_name.json deleted file mode 100644 index 8b7e6c89482f7..0000000000000 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/linux_anomalous_user_name.json +++ /dev/null @@ -1,24 +0,0 @@ -{ - "anomaly_threshold": 50, - "description": "A machine learning job detected activity for a username that is not normally active, which can indicate unauthorized changes, activity by unauthorized users, lateral movement, or compromised credentials. In many organizations, new usernames are not often created apart from specific types of system activities, such as creating new accounts for new employees. These user accounts quickly become active and routine. Events from rarely used usernames can point to suspicious activity. Additionally, automated Linux fleets tend to see activity from rarely used usernames only when personnel log in to make authorized or unauthorized changes, or threat actors have acquired credentials and log in for malicious purposes. Unusual usernames can also indicate pivoting, where compromised credentials are used to try and move laterally from one host to another.", - "false_positives": [ - "Uncommon user activity can be due to an engineer logging onto a server instance in order to perform manual troubleshooting or reconfiguration." - ], - "from": "now-16m", - "interval": "15m", - "machine_learning_job_id": "linux_anomalous_user_name_ecs", - "name": "Unusual Linux Username", - "references": [ - "https://www.elastic.co/guide/en/siem/guide/current/prebuilt-ml-jobs.html" - ], - "risk_score": 21, - "rule_id": "b347b919-665f-4aac-b9e8-68369bf2340c", - "severity": "low", - "tags": [ - "Elastic", - "Linux", - "ML" - ], - "type": "machine_learning", - "version": 1 -} \ No newline at end of file diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/rare_process_by_host_linux.json b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/rare_process_by_host_linux.json deleted file mode 100644 index 048f93e170656..0000000000000 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/rare_process_by_host_linux.json +++ /dev/null @@ -1,24 +0,0 @@ -{ - "anomaly_threshold": 50, - "description": "Identifies rare processes that do not usually run on individual hosts, which can indicate execution of unauthorized services, malware, or persistence mechanisms. Processes are considered rare when they only run occasionally as compared with other processes running on the host.", - "false_positives": [ - "A newly installed program or one that runs rarely as part of a monthly or quarterly workflow could trigger this signal." - ], - "from": "now-16m", - "interval": "15m", - "machine_learning_job_id": "rare_process_by_host_linux_ecs", - "name": "Unusual Process For a Linux Host", - "references": [ - "https://www.elastic.co/guide/en/siem/guide/current/prebuilt-ml-jobs.html" - ], - "risk_score": 21, - "rule_id": "46f804f5-b289-43d6-a881-9387cf594f75", - "severity": "low", - "tags": [ - "Elastic", - "Linux", - "ML" - ], - "type": "machine_learning", - "version": 1 -} \ No newline at end of file diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/rare_process_by_host_windows.json b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/rare_process_by_host_windows.json deleted file mode 100644 index 7bc46cdc04dd2..0000000000000 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/rare_process_by_host_windows.json +++ /dev/null @@ -1,24 +0,0 @@ -{ - "anomaly_threshold": 50, - "description": "Identifies rare processes that do not usually run on individual hosts, which can indicate execution of unauthorized services, malware, or persistence mechanisms. Processes are considered rare when they only run occasionally as compared with other processes running on the host.", - "false_positives": [ - "A newly installed program or one that runs rarely as part of a monthly or quarterly workflow could trigger this signal." - ], - "from": "now-16m", - "interval": "15m", - "machine_learning_job_id": "rare_process_by_host_windows_ecs", - "name": "Unusual Process For a Windows Host", - "references": [ - "https://www.elastic.co/guide/en/siem/guide/current/prebuilt-ml-jobs.html" - ], - "risk_score": 21, - "rule_id": "6d448b96-c922-4adb-b51c-b767f1ea5b76", - "severity": "low", - "tags": [ - "Elastic", - "ML", - "Windows" - ], - "type": "machine_learning", - "version": 1 -} \ No newline at end of file diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/windows_anomalous_network_activity.json b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/windows_anomalous_network_activity.json deleted file mode 100644 index 72671760c9c8d..0000000000000 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/windows_anomalous_network_activity.json +++ /dev/null @@ -1,24 +0,0 @@ -{ - "anomaly_threshold": 50, - "description": "Identifies Windows processes that do not usually use the network but have unexpected network activity, which can indicate command-and-control, lateral movement, persistence, or data exfiltration activity. A process with unusual network activity can denote process exploitation or injection, where the process is used to run persistence mechanisms that allow a malicious actor remote access or control of the host, data exfiltration, and execution of unauthorized network applications.", - "false_positives": [ - "A newly installed program or one that rarely uses the network could trigger this signal." - ], - "from": "now-16m", - "interval": "15m", - "machine_learning_job_id": "windows_anomalous_network_activity_ecs", - "name": "Unusual Windows Network Activity", - "references": [ - "https://www.elastic.co/guide/en/siem/guide/current/prebuilt-ml-jobs.html" - ], - "risk_score": 21, - "rule_id": "ba342eb2-583c-439f-b04d-1fdd7c1417cc", - "severity": "low", - "tags": [ - "Elastic", - "ML", - "Windows" - ], - "type": "machine_learning", - "version": 1 -} \ No newline at end of file diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/windows_anomalous_process_all_hosts.json b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/windows_anomalous_process_all_hosts.json deleted file mode 100644 index 93469b5a06223..0000000000000 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/windows_anomalous_process_all_hosts.json +++ /dev/null @@ -1,24 +0,0 @@ -{ - "anomaly_threshold": 50, - "description": "Searches for rare processes running on multiple hosts in an entire fleet or network. This reduces the detection of false positives since automated maintenance processes usually only run occasionally on a single machine but are common to all or many hosts in a fleet.", - "false_positives": [ - "A newly installed program or one that runs rarely as part of a monthly or quarterly workflow could trigger this signal." - ], - "from": "now-16m", - "interval": "15m", - "machine_learning_job_id": "windows_anomalous_process_all_hosts_ecs", - "name": "Anomalous Process For a Windows Population", - "references": [ - "https://www.elastic.co/guide/en/siem/guide/current/prebuilt-ml-jobs.html" - ], - "risk_score": 21, - "rule_id": "6e40d56f-5c0e-4ac6-aece-bee96645b172", - "severity": "low", - "tags": [ - "Elastic", - "ML", - "Windows" - ], - "type": "machine_learning", - "version": 1 -} \ No newline at end of file diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/windows_anomalous_user_name.json b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/windows_anomalous_user_name.json deleted file mode 100644 index 217404b6eb474..0000000000000 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/windows_anomalous_user_name.json +++ /dev/null @@ -1,24 +0,0 @@ -{ - "anomaly_threshold": 50, - "description": "A machine learning job detected activity for a username that is not normally active, which can indicate unauthorized changes, activity by unauthorized users, lateral movement, or compromised credentials. In many organizations, new usernames are not often created apart from specific types of system activities, such as creating new accounts for new employees. These user accounts quickly become active and routine. Events from rarely used usernames can point to suspicious activity. Additionally, automated Linux fleets tend to see activity from rarely used usernames only when personnel log in to make authorized or unauthorized changes, or threat actors have acquired credentials and log in for malicious purposes. Unusual usernames can also indicate pivoting, where compromised credentials are used to try and move laterally from one host to another.", - "false_positives": [ - "Uncommon user activity can be due to an administrator or help desk technician logging onto a workstation or server in order to perform manual troubleshooting or reconfiguration." - ], - "from": "now-16m", - "interval": "15m", - "machine_learning_job_id": "windows_anomalous_user_name_ecs", - "name": "Unusual Windows Username", - "references": [ - "https://www.elastic.co/guide/en/siem/guide/current/prebuilt-ml-jobs.html" - ], - "risk_score": 21, - "rule_id": "1781d055-5c66-4adf-9c59-fc0fa58336a5", - "severity": "low", - "tags": [ - "Elastic", - "ML", - "Windows" - ], - "type": "machine_learning", - "version": 1 -} \ No newline at end of file diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/windows_rare_user_type10_remote_login.json b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/windows_rare_user_type10_remote_login.json deleted file mode 100644 index 09ff2a0cedf41..0000000000000 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/windows_rare_user_type10_remote_login.json +++ /dev/null @@ -1,24 +0,0 @@ -{ - "anomaly_threshold": 50, - "description": "A machine learning job detected an unusual remote desktop protocol (RDP) username, which can indicate account takeover or credentialed persistence using compromised accounts. RDP attacks, such as BlueKeep, also tend to use unusual usernames.", - "false_positives": [ - "Uncommon username activity can be due to an engineer logging onto a server instance in order to perform manual troubleshooting or reconfiguration." - ], - "from": "now-16m", - "interval": "15m", - "machine_learning_job_id": "windows_rare_user_type10_remote_login", - "name": "Unusual Windows Remote User", - "references": [ - "https://www.elastic.co/guide/en/siem/guide/current/prebuilt-ml-jobs.html" - ], - "risk_score": 21, - "rule_id": "1781d055-5c66-4adf-9e93-fc0fa69550c9", - "severity": "low", - "tags": [ - "Elastic", - "ML", - "Windows" - ], - "type": "machine_learning", - "version": 1 -} \ No newline at end of file diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/types.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/types.ts deleted file mode 100644 index b1bed5d716155..0000000000000 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/types.ts +++ /dev/null @@ -1,194 +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 { get } from 'lodash/fp'; -import { Readable } from 'stream'; - -import { - SavedObject, - SavedObjectAttributes, - SavedObjectsFindResponse, - SavedObjectsClientContract, -} from 'kibana/server'; -import { AlertsClient, PartialAlert } from '../../../../../../../plugins/alerting/server'; -import { Alert } from '../../../../../../../plugins/alerting/common'; -import { SIGNALS_ID } from '../../../../common/constants'; -import { ActionsClient } from '../../../../../../../plugins/actions/server'; -import { RuleAlertParams, RuleTypeParams, RuleAlertParamsRest } from '../types'; - -export type PatchRuleAlertParamsRest = Partial<RuleAlertParamsRest> & { - id: string | undefined; - rule_id: RuleAlertParams['ruleId'] | undefined; -}; - -export type UpdateRuleAlertParamsRest = RuleAlertParamsRest & { - id: string | undefined; - rule_id: RuleAlertParams['ruleId'] | undefined; -}; - -export interface FindParamsRest { - per_page: number; - page: number; - sort_field: string; - sort_order: 'asc' | 'desc'; - fields: string[]; - filter: string; -} - -export interface RuleAlertType extends Alert { - params: RuleTypeParams; -} - -// eslint-disable-next-line @typescript-eslint/no-explicit-any -export interface IRuleStatusAttributes extends Record<string, any> { - alertId: string; // created alert id. - statusDate: string; - lastFailureAt: string | null | undefined; - lastFailureMessage: string | null | undefined; - lastSuccessAt: string | null | undefined; - lastSuccessMessage: string | null | undefined; - status: RuleStatusString | null | undefined; - lastLookBackDate: string | null | undefined; - gap: string | null | undefined; - bulkCreateTimeDurations: string[] | null | undefined; - searchAfterTimeDurations: string[] | null | undefined; -} - -export interface RuleStatusResponse { - [key: string]: { - current_status: IRuleStatusAttributes | null | undefined; - failures: IRuleStatusAttributes[] | null | undefined; - }; -} - -export interface IRuleSavedAttributesSavedObjectAttributes - extends IRuleStatusAttributes, - SavedObjectAttributes {} - -export interface IRuleStatusSavedObject { - type: string; - id: string; - attributes: Array<SavedObject<IRuleStatusAttributes & SavedObjectAttributes>>; - references: unknown[]; - updated_at: string; - version: string; -} - -export interface IRuleStatusFindType { - page: number; - per_page: number; - total: number; - saved_objects: IRuleStatusSavedObject[]; -} - -export type RuleStatusString = 'succeeded' | 'failed' | 'going to run'; - -export interface HapiReadableStream extends Readable { - hapi: { - filename: string; - }; -} -export interface ImportRulesRequestParams { - query: { overwrite: boolean }; - body: { file: HapiReadableStream }; -} - -export interface ExportRulesRequestParams { - body: { objects: Array<{ rule_id: string }> | null | undefined }; - query: { - file_name: string; - exclude_export_details: boolean; - }; -} - -export interface RuleRequestParams { - id: string | undefined; - rule_id: string | undefined; -} - -export type ReadRuleRequestParams = RuleRequestParams; -export type DeleteRuleRequestParams = RuleRequestParams; -export type DeleteRulesRequestParams = RuleRequestParams[]; - -export interface FindRuleParams { - alertsClient: AlertsClient; - perPage?: number; - page?: number; - sortField?: string; - filter?: string; - fields?: string[]; - sortOrder?: 'asc' | 'desc'; -} - -export interface FindRulesRequestParams { - per_page: number; - page: number; - search?: string; - sort_field?: string; - filter?: string; - fields?: string[]; - sort_order?: 'asc' | 'desc'; -} - -export interface FindRulesStatusesRequestParams { - ids: string[]; -} - -export interface Clients { - alertsClient: AlertsClient; - actionsClient: ActionsClient; -} - -export type PatchRuleParams = Partial<Omit<RuleAlertParams, 'throttle'>> & { - id: string | undefined | null; - savedObjectsClient: SavedObjectsClientContract; -} & Clients; - -export type UpdateRuleParams = Omit<RuleAlertParams, 'immutable' | 'throttle'> & { - id: string | undefined | null; - savedObjectsClient: SavedObjectsClientContract; -} & Clients; - -export type DeleteRuleParams = Clients & { - id: string | undefined; - ruleId: string | undefined | null; -}; - -export type CreateRuleParams = Omit<RuleAlertParams, 'ruleId' | 'throttle'> & { - ruleId: string; -} & Clients; - -export interface ReadRuleParams { - alertsClient: AlertsClient; - id?: string | undefined | null; - ruleId?: string | undefined | null; -} - -export const isAlertTypes = (partialAlert: PartialAlert[]): partialAlert is RuleAlertType[] => { - return partialAlert.every(rule => isAlertType(rule)); -}; - -export const isAlertType = (partialAlert: PartialAlert): partialAlert is RuleAlertType => { - return partialAlert.alertTypeId === SIGNALS_ID; -}; - -export const isRuleStatusSavedObjectType = ( - obj: unknown -): obj is SavedObject<IRuleSavedAttributesSavedObjectAttributes> => { - return get('attributes', obj) != null; -}; - -export const isRuleStatusFindType = ( - obj: unknown -): obj is SavedObjectsFindResponse<IRuleSavedAttributesSavedObjectAttributes> => { - return get('saved_objects', obj) != null; -}; - -export const isRuleStatusFindTypes = ( - obj: unknown[] | undefined -): obj is Array<SavedObjectsFindResponse<IRuleSavedAttributesSavedObjectAttributes>> => { - return obj ? obj.every(ruleStatus => isRuleStatusFindType(ruleStatus)) : false; -}; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/find_rules_statuses_by_ids.sh b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/find_rules_statuses_by_ids.sh deleted file mode 100755 index 543c019067e8e..0000000000000 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/find_rules_statuses_by_ids.sh +++ /dev/null @@ -1,17 +0,0 @@ -#!/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: ./find_rules_statuses_by_ids.sh '["12345","6789abc"]' -curl -g -k \ - -u ${ELASTICSEARCH_USERNAME}:${ELASTICSEARCH_PASSWORD} \ - -X GET "${KIBANA_URL}${SPACE_URL}/api/detection_engine/rules/_find_statuses?ids=$1" \ - | jq . diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/patches/update_list.json b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/patches/update_list.json deleted file mode 100644 index 4db8724db4e13..0000000000000 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/patches/update_list.json +++ /dev/null @@ -1,32 +0,0 @@ -{ - "rule_id": "query-with-list", - "lists": [ - { - "field": "source.ip", - "values_operator": "excluded", - "values_type": "exists" - }, - { - "field": "host.name", - "values_operator": "included", - "values_type": "match", - "values": [ - { - "name": "rock01" - } - ], - "and": [ - { - "field": "host.id", - "values_operator": "included", - "values_type": "match_all", - "values": [ - { - "name": "123456" - } - ] - } - ] - } - ] -} diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/queries/query_with_list.json b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/queries/query_with_list.json deleted file mode 100644 index 997d03369a699..0000000000000 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/queries/query_with_list.json +++ /dev/null @@ -1,41 +0,0 @@ -{ - "name": "Query with a list", - "description": "Query with a list", - "rule_id": "query-with-list", - "risk_score": 1, - "severity": "high", - "type": "query", - "query": "user.name: root or user.name: admin", - "lists": [ - { - "field": "source.ip", - "values_operator": "included", - "values_type": "exists" - }, - { - "field": "host.name", - "values_operator": "excluded", - "values_type": "match", - "values": [ - { - "name": "rock01" - } - ], - "and": [ - { - "field": "host.id", - "values_operator": "included", - "values_type": "match_all", - "values": [ - { - "name": "123" - }, - { - "name": "678" - } - ] - } - ] - } - ] -} diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/updates/update_list.json b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/updates/update_list.json deleted file mode 100644 index 66b198974f574..0000000000000 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/updates/update_list.json +++ /dev/null @@ -1,38 +0,0 @@ -{ - "name": "Query with a list", - "description": "Query with a list", - "rule_id": "query-with-list", - "risk_score": 1, - "severity": "high", - "type": "query", - "query": "user.name: root or user.name: admin", - "lists": [ - { - "field": "source.ip", - "values_operator": "excluded", - "values_type": "exists" - }, - { - "field": "host.name", - "values_operator": "included", - "values_type": "match", - "values": [ - { - "name": "rock01" - } - ], - "and": [ - { - "field": "host.id", - "values_operator": "included", - "values_type": "match_all", - "values": [ - { - "name": "123456" - } - ] - } - ] - } - ] -} diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/get_input_output_index.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/get_input_output_index.test.ts deleted file mode 100644 index 18286dc7754e0..0000000000000 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/get_input_output_index.test.ts +++ /dev/null @@ -1,103 +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 { savedObjectsClientMock } from 'src/core/server/mocks'; -import { DEFAULT_INDEX_KEY } from '../../../../common/constants'; -import { getInputIndex } from './get_input_output_index'; -import { defaultIndexPattern } from '../../../../default_index_pattern'; -import { AlertServices } from '../../../../../../../plugins/alerting/server'; - -describe('get_input_output_index', () => { - let savedObjectsClient = savedObjectsClientMock.create(); - savedObjectsClient.get = jest.fn().mockImplementation(() => ({ - attributes: {}, - })); - let servicesMock: AlertServices = { - savedObjectsClient, - callCluster: jest.fn(), - alertInstanceFactory: jest.fn(), - }; - - beforeAll(() => { - jest.resetAllMocks(); - }); - - afterAll(() => { - jest.resetAllMocks(); - }); - - beforeEach(() => { - savedObjectsClient = savedObjectsClientMock.create(); - savedObjectsClient.get = jest.fn().mockImplementation(() => ({ - attributes: {}, - })); - servicesMock = { - savedObjectsClient, - callCluster: jest.fn(), - alertInstanceFactory: jest.fn(), - }; - }); - - describe('getInputOutputIndex', () => { - test('Returns inputIndex if inputIndex is passed in', async () => { - savedObjectsClient.get = jest.fn().mockImplementation(() => ({ - attributes: {}, - })); - const inputIndex = await getInputIndex(servicesMock, '8.0.0', ['test-input-index-1']); - expect(inputIndex).toEqual(['test-input-index-1']); - }); - - test('Returns a saved object inputIndex if passed in inputIndex is undefined', async () => { - savedObjectsClient.get = jest.fn().mockImplementation(() => ({ - attributes: { - [DEFAULT_INDEX_KEY]: ['configured-index-1', 'configured-index-2'], - }, - })); - const inputIndex = await getInputIndex(servicesMock, '8.0.0', undefined); - expect(inputIndex).toEqual(['configured-index-1', 'configured-index-2']); - }); - - test('Returns a saved object inputIndex if passed in inputIndex is null', async () => { - savedObjectsClient.get = jest.fn().mockImplementation(() => ({ - attributes: { - [DEFAULT_INDEX_KEY]: ['configured-index-1', 'configured-index-2'], - }, - })); - const inputIndex = await getInputIndex(servicesMock, '8.0.0', null); - expect(inputIndex).toEqual(['configured-index-1', 'configured-index-2']); - }); - - test('Returns a saved object inputIndex default from constants if inputIndex passed in is null and the key is also null', async () => { - savedObjectsClient.get = jest.fn().mockImplementation(() => ({ - attributes: { - [DEFAULT_INDEX_KEY]: null, - }, - })); - const inputIndex = await getInputIndex(servicesMock, '8.0.0', null); - expect(inputIndex).toEqual(defaultIndexPattern); - }); - - test('Returns a saved object inputIndex default from constants if inputIndex passed in is undefined and the key is also null', async () => { - savedObjectsClient.get = jest.fn().mockImplementation(() => ({ - attributes: { - [DEFAULT_INDEX_KEY]: null, - }, - })); - const inputIndex = await getInputIndex(servicesMock, '8.0.0', undefined); - expect(inputIndex).toEqual(defaultIndexPattern); - }); - - test('Returns a saved object inputIndex default from constants if both passed in inputIndex and configuration attributes are missing and the index is undefined', async () => { - const inputIndex = await getInputIndex(servicesMock, '8.0.0', undefined); - expect(inputIndex).toEqual(defaultIndexPattern); - }); - - test('Returns a saved object inputIndex default from constants if both passed in inputIndex and configuration attributes are missing and the index is null', async () => { - const inputIndex = await getInputIndex(servicesMock, '8.0.0', null); - expect(inputIndex).toEqual(defaultIndexPattern); - }); - }); -}); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/types.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/types.ts deleted file mode 100644 index 040e32aa0d360..0000000000000 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/types.ts +++ /dev/null @@ -1,171 +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 { RuleAlertAction } from '../../../../common/detection_engine/types'; -import { RuleAlertParams, OutputRuleAlertRest } from '../types'; -import { SearchResponse } from '../../types'; -import { - AlertType, - State, - AlertExecutorOptions, -} from '../../../../../../../plugins/alerting/server'; - -export interface SignalsParams { - signalIds: string[] | undefined | null; - query: object | undefined | null; - status: 'open' | 'closed'; -} - -export interface SignalsStatusParams { - signalIds: string[] | undefined | null; - query: object | undefined | null; - status: 'open' | 'closed'; -} - -export interface SignalQueryParams { - query: object | undefined | null; - aggs: object | undefined | null; - _source: string[] | undefined | null; - size: number | undefined | null; - track_total_hits: boolean | undefined | null; -} - -export type SignalsStatusRestParams = Omit<SignalsStatusParams, 'signalIds'> & { - signal_ids: SignalsStatusParams['signalIds']; -}; - -export type SignalsQueryRestParams = SignalQueryParams; - -export type SearchTypes = - | string - | string[] - | number - | number[] - | boolean - | boolean[] - | object - | object[] - | undefined; - -export interface SignalSource { - [key: string]: SearchTypes; - '@timestamp': string; - signal?: { - parent: Ancestor; - ancestors: Ancestor[]; - }; -} - -export interface BulkResponse { - took: number; - errors: boolean; - items: [ - { - create: { - _index: string; - _type?: string; - _id: string; - _version: number; - result?: string; - _shards?: { - total: number; - successful: number; - failed: number; - }; - _seq_no?: number; - _primary_term?: number; - status: number; - error?: { - type: string; - reason: string; - index_uuid?: string; - shard: string; - index: string; - }; - }; - } - ]; -} - -export interface MGetResponse { - docs: GetResponse[]; -} -export interface GetResponse { - _index: string; - _type: string; - _id: string; - _version: number; - _seq_no: number; - _primary_term: number; - found: boolean; - _source: SearchTypes; -} - -export type SignalSearchResponse = SearchResponse<SignalSource>; -export type SignalSourceHit = SignalSearchResponse['hits']['hits'][number]; - -export type RuleExecutorOptions = Omit<AlertExecutorOptions, 'params'> & { - params: RuleAlertParams & { - scrollSize: number; - scrollLock: string; - }; -}; - -// This returns true because by default a RuleAlertTypeDefinition is an AlertType -// since we are only increasing the strictness of params. -export const isAlertExecutor = (obj: SignalRuleAlertTypeDefinition): obj is AlertType => { - return true; -}; - -export type SignalRuleAlertTypeDefinition = Omit<AlertType, 'executor'> & { - executor: ({ services, params, state }: RuleExecutorOptions) => Promise<State | void>; -}; - -export interface Ancestor { - rule: string; - id: string; - type: string; - index: string; - depth: number; -} - -export interface Signal { - rule: Partial<OutputRuleAlertRest>; - parent: Ancestor; - ancestors: Ancestor[]; - original_time: string; - original_event?: SearchTypes; - status: 'open' | 'closed'; -} - -export interface SignalHit { - '@timestamp': string; - event: object; - signal: Partial<Signal>; -} - -export interface AlertAttributes { - actions: RuleAlertAction[]; - enabled: boolean; - name: string; - tags: string[]; - createdBy: string; - createdAt: string; - updatedBy: string; - schedule: { - interval: string; - }; - throttle: string; -} - -export interface RuleAlertAttributes extends AlertAttributes { - params: Omit< - RuleAlertParams, - 'ruleId' | 'name' | 'enabled' | 'interval' | 'tags' | 'actions' | 'throttle' - > & { - ruleId: string; - }; -} diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/utils.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/utils.test.ts deleted file mode 100644 index 873e06fcbb44e..0000000000000 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/utils.test.ts +++ /dev/null @@ -1,354 +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 moment from 'moment'; -import sinon from 'sinon'; - -import { - generateId, - parseInterval, - parseScheduleDates, - getDriftTolerance, - getGapBetweenRuns, -} from './utils'; - -describe('utils', () => { - const anchor = '2020-01-01T06:06:06.666Z'; - const unix = moment(anchor).valueOf(); - let nowDate = moment('2020-01-01T00:00:00.000Z'); - let clock: sinon.SinonFakeTimers; - - beforeEach(() => { - nowDate = moment('2020-01-01T00:00:00.000Z'); - clock = sinon.useFakeTimers(unix); - }); - - afterEach(() => { - clock.restore(); - }); - - describe('generateId', () => { - test('it generates expected output', () => { - const id = generateId('index-123', 'doc-123', 'version-123', 'rule-123'); - expect(id).toEqual('10622e7d06c9e38a532e71fc90e3426c1100001fb617aec8cb974075da52db06'); - }); - - test('expected output is a hex', () => { - const id = generateId('index-123', 'doc-123', 'version-123', 'rule-123'); - expect(id).toMatch(/[a-f0-9]+/); - }); - }); - - describe('parseInterval', () => { - test('it returns a duration when given one that is valid', () => { - const duration = parseInterval('5m'); - expect(duration).not.toBeNull(); - expect(duration?.asMilliseconds()).toEqual(moment.duration(5, 'minutes').asMilliseconds()); - }); - - test('it returns null given an invalid duration', () => { - const duration = parseInterval('junk'); - expect(duration).toBeNull(); - }); - }); - - describe('parseScheduleDates', () => { - test('it returns a moment when given an ISO string', () => { - const result = parseScheduleDates('2020-01-01T00:00:00.000Z'); - expect(result).not.toBeNull(); - expect(result).toEqual(moment('2020-01-01T00:00:00.000Z')); - }); - - test('it returns a moment when given `now`', () => { - const result = parseScheduleDates('now'); - - expect(result).not.toBeNull(); - expect(moment.isMoment(result)).toBeTruthy(); - }); - - test('it returns a moment when given `now-x`', () => { - const result = parseScheduleDates('now-6m'); - - expect(result).not.toBeNull(); - expect(moment.isMoment(result)).toBeTruthy(); - }); - - test('it returns null when given a string that is not an ISO string, `now` or `now-x`', () => { - const result = parseScheduleDates('invalid'); - - expect(result).toBeNull(); - }); - }); - - describe('getDriftTolerance', () => { - test('it returns a drift tolerance in milliseconds of 1 minute when "from" overlaps "to" by 1 minute and the interval is 5 minutes', () => { - const drift = getDriftTolerance({ - from: 'now-6m', - to: 'now', - interval: moment.duration(5, 'minutes'), - }); - expect(drift).not.toBeNull(); - expect(drift?.asMilliseconds()).toEqual(moment.duration(1, 'minute').asMilliseconds()); - }); - - test('it returns a drift tolerance of 0 when "from" equals the interval', () => { - const drift = getDriftTolerance({ - from: 'now-5m', - to: 'now', - interval: moment.duration(5, 'minutes'), - }); - expect(drift?.asMilliseconds()).toEqual(0); - }); - - test('it returns a drift tolerance of 5 minutes when "from" is 10 minutes but the interval is 5 minutes', () => { - const drift = getDriftTolerance({ - from: 'now-10m', - to: 'now', - interval: moment.duration(5, 'minutes'), - }); - expect(drift).not.toBeNull(); - expect(drift?.asMilliseconds()).toEqual(moment.duration(5, 'minutes').asMilliseconds()); - }); - - test('it returns a drift tolerance of 10 minutes when "from" is 10 minutes ago and the interval is 0', () => { - const drift = getDriftTolerance({ - from: 'now-10m', - to: 'now', - interval: moment.duration(0, 'milliseconds'), - }); - expect(drift).not.toBeNull(); - expect(drift?.asMilliseconds()).toEqual(moment.duration(10, 'minutes').asMilliseconds()); - }); - - test('returns a drift tolerance of 1 minute when "from" is invalid and defaults to "now-6m" and interval is 5 minutes', () => { - const drift = getDriftTolerance({ - from: 'invalid', - to: 'now', - interval: moment.duration(5, 'minutes'), - }); - expect(drift).not.toBeNull(); - expect(drift?.asMilliseconds()).toEqual(moment.duration(1, 'minute').asMilliseconds()); - }); - - test('returns a drift tolerance of 1 minute when "from" does not include `now` and defaults to "now-6m" and interval is 5 minutes', () => { - const drift = getDriftTolerance({ - from: '10m', - to: 'now', - interval: moment.duration(5, 'minutes'), - }); - expect(drift).not.toBeNull(); - expect(drift?.asMilliseconds()).toEqual(moment.duration(1, 'minute').asMilliseconds()); - }); - - test('returns a drift tolerance of 4 minutes when "to" is "now-x", from is a valid input and interval is 5 minute', () => { - const drift = getDriftTolerance({ - from: 'now-10m', - to: 'now-1m', - interval: moment.duration(5, 'minutes'), - }); - expect(drift).not.toBeNull(); - expect(drift?.asMilliseconds()).toEqual(moment.duration(4, 'minutes').asMilliseconds()); - }); - - test('it returns expected drift tolerance when "from" is an ISO string', () => { - const drift = getDriftTolerance({ - from: moment() - .subtract(10, 'minutes') - .toISOString(), - to: 'now', - interval: moment.duration(5, 'minutes'), - }); - expect(drift).not.toBeNull(); - expect(drift?.asMilliseconds()).toEqual(moment.duration(5, 'minutes').asMilliseconds()); - }); - - test('it returns expected drift tolerance when "to" is an ISO string', () => { - const drift = getDriftTolerance({ - from: 'now-6m', - to: moment().toISOString(), - interval: moment.duration(5, 'minutes'), - }); - expect(drift).not.toBeNull(); - expect(drift?.asMilliseconds()).toEqual(moment.duration(1, 'minute').asMilliseconds()); - }); - }); - - describe('getGapBetweenRuns', () => { - test('it returns a gap of 0 when "from" and interval match each other and the previous started was from the previous interval time', () => { - const gap = getGapBetweenRuns({ - previousStartedAt: nowDate - .clone() - .subtract(5, 'minutes') - .toDate(), - interval: '5m', - from: 'now-5m', - to: 'now', - now: nowDate.clone(), - }); - expect(gap).not.toBeNull(); - expect(gap?.asMilliseconds()).toEqual(0); - }); - - test('it returns a negative gap of 1 minute when "from" overlaps to by 1 minute and the previousStartedAt was 5 minutes ago', () => { - const gap = getGapBetweenRuns({ - previousStartedAt: nowDate - .clone() - .subtract(5, 'minutes') - .toDate(), - interval: '5m', - from: 'now-6m', - to: 'now', - now: nowDate.clone(), - }); - expect(gap).not.toBeNull(); - expect(gap?.asMilliseconds()).toEqual(moment.duration(-1, 'minute').asMilliseconds()); - }); - - test('it returns a negative gap of 5 minutes when "from" overlaps to by 1 minute and the previousStartedAt was 5 minutes ago', () => { - const gap = getGapBetweenRuns({ - previousStartedAt: nowDate - .clone() - .subtract(5, 'minutes') - .toDate(), - interval: '5m', - from: 'now-10m', - to: 'now', - now: nowDate.clone(), - }); - expect(gap).not.toBeNull(); - expect(gap?.asMilliseconds()).toEqual(moment.duration(-5, 'minute').asMilliseconds()); - }); - - test('it returns a negative gap of 1 minute when "from" overlaps to by 1 minute and the previousStartedAt was 10 minutes ago and so was the interval', () => { - const gap = getGapBetweenRuns({ - previousStartedAt: nowDate - .clone() - .subtract(10, 'minutes') - .toDate(), - interval: '10m', - from: 'now-11m', - to: 'now', - now: nowDate.clone(), - }); - expect(gap).not.toBeNull(); - expect(gap?.asMilliseconds()).toEqual(moment.duration(-1, 'minute').asMilliseconds()); - }); - - test('it returns a gap of only -30 seconds when the from overlaps with now by 1 minute, the interval is 5 minutes but the previous started is 30 seconds more', () => { - const gap = getGapBetweenRuns({ - previousStartedAt: nowDate - .clone() - .subtract(5, 'minutes') - .subtract(30, 'seconds') - .toDate(), - interval: '5m', - from: 'now-6m', - to: 'now', - now: nowDate.clone(), - }); - expect(gap).not.toBeNull(); - expect(gap?.asMilliseconds()).toEqual(moment.duration(-30, 'seconds').asMilliseconds()); - }); - - test('it returns an exact 0 gap when the from overlaps with now by 1 minute, the interval is 5 minutes but the previous started is one minute late', () => { - const gap = getGapBetweenRuns({ - previousStartedAt: nowDate - .clone() - .subtract(6, 'minutes') - .toDate(), - interval: '5m', - from: 'now-6m', - to: 'now', - now: nowDate.clone(), - }); - expect(gap).not.toBeNull(); - expect(gap?.asMilliseconds()).toEqual(moment.duration(0, 'minute').asMilliseconds()); - }); - - test('it returns a gap of 30 seconds when the from overlaps with now by 1 minute, the interval is 5 minutes but the previous started is one minute and 30 seconds late', () => { - const gap = getGapBetweenRuns({ - previousStartedAt: nowDate - .clone() - .subtract(6, 'minutes') - .subtract(30, 'seconds') - .toDate(), - interval: '5m', - from: 'now-6m', - to: 'now', - now: nowDate.clone(), - }); - expect(gap).not.toBeNull(); - expect(gap?.asMilliseconds()).toEqual(moment.duration(30, 'seconds').asMilliseconds()); - }); - - test('it returns a gap of 1 minute when the from overlaps with now by 1 minute, the interval is 5 minutes but the previous started is two minutes late', () => { - const gap = getGapBetweenRuns({ - previousStartedAt: nowDate - .clone() - .subtract(7, 'minutes') - .toDate(), - interval: '5m', - from: 'now-6m', - to: 'now', - now: nowDate.clone(), - }); - expect(gap?.asMilliseconds()).not.toBeNull(); - expect(gap?.asMilliseconds()).toEqual(moment.duration(1, 'minute').asMilliseconds()); - }); - - test('it returns null if given a previousStartedAt of null', () => { - const gap = getGapBetweenRuns({ - previousStartedAt: null, - interval: '5m', - from: 'now-5m', - to: 'now', - now: nowDate.clone(), - }); - expect(gap).toBeNull(); - }); - - test('it returns null if the interval is an invalid string such as "invalid"', () => { - const gap = getGapBetweenRuns({ - previousStartedAt: nowDate.clone().toDate(), - interval: 'invalid', // if not set to "x" where x is an interval such as 6m - from: 'now-5m', - to: 'now', - now: nowDate.clone(), - }); - expect(gap).toBeNull(); - }); - - test('it returns the expected result when "from" is an invalid string such as "invalid"', () => { - const gap = getGapBetweenRuns({ - previousStartedAt: nowDate - .clone() - .subtract(7, 'minutes') - .toDate(), - interval: '5m', - from: 'invalid', - to: 'now', - now: nowDate.clone(), - }); - expect(gap?.asMilliseconds()).not.toBeNull(); - expect(gap?.asMilliseconds()).toEqual(moment.duration(1, 'minute').asMilliseconds()); - }); - - test('it returns the expected result when "to" is an invalid string such as "invalid"', () => { - const gap = getGapBetweenRuns({ - previousStartedAt: nowDate - .clone() - .subtract(7, 'minutes') - .toDate(), - interval: '5m', - from: 'now-6m', - to: 'invalid', - now: nowDate.clone(), - }); - expect(gap?.asMilliseconds()).not.toBeNull(); - expect(gap?.asMilliseconds()).toEqual(moment.duration(1, 'minute').asMilliseconds()); - }); - }); -}); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/utils.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/utils.ts deleted file mode 100644 index 49af310db559f..0000000000000 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/utils.ts +++ /dev/null @@ -1,93 +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 { createHash } from 'crypto'; -import moment from 'moment'; -import dateMath from '@elastic/datemath'; -import { parseDuration } from '../../../../../../../plugins/alerting/server'; - -export const generateId = ( - docIndex: string, - docId: string, - version: string, - ruleId: string -): string => - createHash('sha256') - .update(docIndex.concat(docId, version, ruleId)) - .digest('hex'); - -export const parseInterval = (intervalString: string): moment.Duration | null => { - try { - return moment.duration(parseDuration(intervalString)); - } catch (err) { - return null; - } -}; - -export const parseScheduleDates = (time: string): moment.Moment | null => { - const isValidDateString = !isNaN(Date.parse(time)); - const isValidInput = isValidDateString || time.trim().startsWith('now'); - const formattedDate = isValidDateString - ? moment(time) - : isValidInput - ? dateMath.parse(time) - : null; - - return formattedDate ?? null; -}; - -export const getDriftTolerance = ({ - from, - to, - interval, - now = moment(), -}: { - from: string; - to: string; - interval: moment.Duration; - now?: moment.Moment; -}): moment.Duration | null => { - const toDate = parseScheduleDates(to) ?? now; - const fromDate = parseScheduleDates(from) ?? dateMath.parse('now-6m'); - const timeSegment = toDate.diff(fromDate); - const duration = moment.duration(timeSegment); - - if (duration !== null) { - return duration.subtract(interval); - } else { - return null; - } -}; - -export const getGapBetweenRuns = ({ - previousStartedAt, - interval, - from, - to, - now = moment(), -}: { - previousStartedAt: Date | undefined | null; - interval: string; - from: string; - to: string; - now?: moment.Moment; -}): moment.Duration | null => { - if (previousStartedAt == null) { - return null; - } - const intervalDuration = parseInterval(interval); - if (intervalDuration == null) { - return null; - } - const driftTolerance = getDriftTolerance({ from, to, interval: intervalDuration }); - if (driftTolerance == null) { - return null; - } - const diff = moment.duration(now.diff(previousStartedAt)); - const drift = diff.subtract(intervalDuration); - return drift.subtract(driftTolerance); -}; - -export const makeFloatString = (num: number): string => Number(num).toFixed(2); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/types.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/types.ts deleted file mode 100644 index 035f1b10ff8b2..0000000000000 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/types.ts +++ /dev/null @@ -1,153 +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 { CallAPIOptions } from '../../../../../../../src/core/server'; -import { Filter } from '../../../../../../../src/plugins/data/server'; -import { IRuleStatusAttributes } from './rules/types'; -import { ListsDefaultArraySchema } from './routes/schemas/types/lists_default_array'; -import { RuleAlertAction, RuleType } from '../../../common/detection_engine/types'; - -export type PartialFilter = Partial<Filter>; - -export interface IMitreAttack { - id: string; - name: string; - reference: string; -} - -export interface ThreatParams { - framework: string; - tactic: IMitreAttack; - technique: IMitreAttack[]; -} - -// Notice below we are using lists: ListsDefaultArraySchema[]; which is coming directly from the response output section. -// TODO: Eventually this whole RuleAlertParams will be replaced with io-ts. For now we can slowly strangle it out and reduce duplicate types -// We don't have the input types defined through io-ts just yet but as we being introducing types from there we will more and more remove -// types and share them between input and output schema but have an input Rule Schema and an output Rule Schema. - -export interface Meta { - [key: string]: {} | string | undefined | null; - kibana_siem_app_url?: string | undefined; -} - -export interface RuleAlertParams { - actions: RuleAlertAction[]; - anomalyThreshold: number | undefined; - description: string; - note: string | undefined | null; - enabled: boolean; - falsePositives: string[]; - filters: PartialFilter[] | undefined | null; - from: string; - immutable: boolean; - index: string[] | undefined | null; - interval: string; - ruleId: string | undefined | null; - language: string | undefined | null; - maxSignals: number; - machineLearningJobId: string | undefined; - riskScore: number; - outputIndex: string; - name: string; - query: string | undefined | null; - references: string[]; - savedId?: string | undefined | null; - meta: Meta | undefined | null; - severity: string; - tags: string[]; - to: string; - timelineId: string | undefined | null; - timelineTitle: string | undefined | null; - threat: ThreatParams[] | undefined | null; - type: RuleType; - version: number; - throttle: string | undefined | null; - lists: ListsDefaultArraySchema | null | undefined; -} - -export type RuleTypeParams = Omit< - RuleAlertParams, - 'name' | 'enabled' | 'interval' | 'tags' | 'actions' | 'throttle' ->; - -export type RuleAlertParamsRest = Omit< - RuleAlertParams, - | 'anomalyThreshold' - | 'ruleId' - | 'falsePositives' - | 'immutable' - | 'maxSignals' - | 'machineLearningJobId' - | 'savedId' - | 'riskScore' - | 'timelineId' - | 'timelineTitle' - | 'outputIndex' -> & - Omit< - IRuleStatusAttributes, - | 'status' - | 'alertId' - | 'statusDate' - | 'lastFailureAt' - | 'lastSuccessAt' - | 'lastSuccessMessage' - | 'lastFailureMessage' - > & { - anomaly_threshold: RuleAlertParams['anomalyThreshold']; - rule_id: RuleAlertParams['ruleId']; - false_positives: RuleAlertParams['falsePositives']; - saved_id?: RuleAlertParams['savedId']; - timeline_id: RuleAlertParams['timelineId']; - timeline_title: RuleAlertParams['timelineTitle']; - max_signals: RuleAlertParams['maxSignals']; - machine_learning_job_id: RuleAlertParams['machineLearningJobId']; - risk_score: RuleAlertParams['riskScore']; - output_index: RuleAlertParams['outputIndex']; - created_at: string; - updated_at: string; - status?: IRuleStatusAttributes['status'] | undefined; - status_date?: IRuleStatusAttributes['statusDate'] | undefined; - last_failure_at?: IRuleStatusAttributes['lastFailureAt'] | undefined; - last_success_at?: IRuleStatusAttributes['lastSuccessAt'] | undefined; - last_failure_message?: IRuleStatusAttributes['lastFailureMessage'] | undefined; - last_success_message?: IRuleStatusAttributes['lastSuccessMessage'] | undefined; - }; - -export type OutputRuleAlertRest = RuleAlertParamsRest & { - id: string; - created_by: string | undefined | null; - updated_by: string | undefined | null; - immutable: boolean; -}; - -export type ImportRuleAlertRest = Omit<OutputRuleAlertRest, 'rule_id' | 'id'> & { - id: string | undefined | null; - rule_id: string; - immutable: boolean; -}; - -export type PrepackagedRules = Omit< - RuleAlertParamsRest, - | 'status' - | 'status_date' - | 'last_failure_at' - | 'last_success_at' - | 'last_failure_message' - | 'last_success_message' - | 'updated_at' - | 'created_at' -> & { rule_id: string; immutable: boolean }; - -// eslint-disable-next-line @typescript-eslint/no-explicit-any -export type CallWithRequest<T extends Record<string, any>, V> = ( - endpoint: string, - params: T, - options?: CallAPIOptions -) => Promise<V>; - -export type RefreshTypes = false | 'wait_for'; diff --git a/x-pack/legacy/plugins/siem/server/lib/events/mock.ts b/x-pack/legacy/plugins/siem/server/lib/events/mock.ts deleted file mode 100644 index 3eb841cbad411..0000000000000 --- a/x-pack/legacy/plugins/siem/server/lib/events/mock.ts +++ /dev/null @@ -1,3411 +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 { cloneDeep } from 'lodash/fp'; -import { defaultIndexPattern } from '../../../default_index_pattern'; -import { RequestDetailsOptions } from './types'; - -export const mockResponseSearchTimelineDetails = { - took: 5, - timed_out: false, - _shards: { - total: 1, - successful: 1, - skipped: 0, - failed: 0, - }, - hits: { - total: { - value: 1, - relation: 'eq', - }, - max_score: 1, - hits: [ - { - _index: 'auditbeat-8.0.0-2019.03.29-000003', - _type: '_doc', - _id: 'TUfUymkBCQofM5eXGBYL', - _score: 1, - _source: { - '@timestamp': '2019-03-29T19:01:23.420Z', - service: { - type: 'auditd', - }, - user: { - audit: { - id: 'unset', - }, - group: { - id: '0', - name: 'root', - }, - effective: { - group: { - id: '0', - name: 'root', - }, - id: '0', - name: 'root', - }, - filesystem: { - group: { - name: 'root', - id: '0', - }, - name: 'root', - id: '0', - }, - saved: { - group: { - id: '0', - name: 'root', - }, - id: '0', - name: 'root', - }, - id: '0', - name: 'root', - }, - process: { - executable: '/root/go/src/github.com/elastic/beats/x-pack/auditbeat/auditbeat', - working_directory: '/root/go/src/github.com/elastic/beats/x-pack/auditbeat', - pid: 15990, - ppid: 1, - title: - '/root/go/src/github.com/elastic/beats/x-pack/auditbeat/auditbeat -e -c /root/go/src/github.com/elastic/beats/x-pack/auditbeat/au', - name: 'auditbeat', - }, - host: { - architecture: 'x86_64', - os: { - name: 'Ubuntu', - kernel: '4.15.0-45-generic', - codename: 'bionic', - platform: 'ubuntu', - version: '18.04.2 LTS (Bionic Beaver)', - family: 'debian', - }, - id: '7c21f5ed03b04d0299569d221fe18bbc', - containerized: false, - name: 'zeek-london', - ip: ['46.101.3.136', '10.16.0.5', 'fe80::4066:42ff:fe19:b3b9'], - mac: ['42:66:42:19:b3:b9'], - hostname: 'zeek-london', - }, - cloud: { - provider: 'digitalocean', - instance: { - id: '136398786', - }, - region: 'lon1', - }, - file: { - device: '00:00', - inode: '3926', - mode: '0644', - uid: '0', - gid: '0', - owner: 'root', - group: 'root', - path: '/etc/passwd', - }, - auditd: { - session: 'unset', - data: { - tty: '(none)', - a3: '0', - a2: '80000', - syscall: 'openat', - a1: '7fe0f63df220', - a0: 'ffffff9c', - arch: 'x86_64', - exit: '12', - }, - summary: { - actor: { - primary: 'unset', - secondary: 'root', - }, - object: { - primary: '/etc/passwd', - type: 'file', - }, - how: '/root/go/src/github.com/elastic/beats/x-pack/auditbeat/auditbeat', - }, - paths: [ - { - rdev: '00:00', - cap_fe: '0', - nametype: 'NORMAL', - ogid: '0', - ouid: '0', - inode: '3926', - item: '0', - mode: '0100644', - name: '/etc/passwd', - cap_fi: '0000000000000000', - cap_fp: '0000000000000000', - cap_fver: '0', - dev: 'fc:01', - }, - ], - message_type: 'syscall', - sequence: 8817905, - result: 'success', - }, - event: { - category: 'audit-rule', - action: 'opened-file', - original: [ - 'type=SYSCALL msg=audit(1553886083.420:8817905): arch=c000003e syscall=257 success=yes exit=12 a0=ffffff9c a1=7fe0f63df220 a2=80000 a3=0 items=1 ppid=1 pid=15990 auid=4294967295 uid=0 gid=0 euid=0 suid=0 fsuid=0 egid=0 sgid=0 fsgid=0 tty=(none) ses=4294967295 comm="auditbeat" exe="/root/go/src/github.com/elastic/beats/x-pack/auditbeat/auditbeat" key=(null)', - 'type=CWD msg=audit(1553886083.420:8817905): cwd="/root/go/src/github.com/elastic/beats/x-pack/auditbeat"', - 'type=PATH msg=audit(1553886083.420:8817905): item=0 name="/etc/passwd" inode=3926 dev=fc:01 mode=0100644 ouid=0 ogid=0 rdev=00:00 nametype=NORMAL cap_fp=0000000000000000 cap_fi=0000000000000000 cap_fe=0 cap_fver=0', - 'type=PROCTITLE msg=audit(1553886083.420:8817905): proctitle=2F726F6F742F676F2F7372632F6769746875622E636F6D2F656C61737469632F62656174732F782D7061636B2F6175646974626561742F617564697462656174002D65002D63002F726F6F742F676F2F7372632F6769746875622E636F6D2F656C61737469632F62656174732F782D7061636B2F6175646974626561742F6175', - ], - module: 'auditd', - }, - ecs: { - version: '1.0.0', - }, - agent: { - ephemeral_id: '6d541d59-52d0-4e70-b4d2-2660c0a99ff7', - hostname: 'zeek-london', - id: 'cc1f4183-36c6-45c4-b21b-7ce70c3572db', - version: '8.0.0', - type: 'auditbeat', - }, - }, - }, - ], - }, -}; -export const mockOptions: RequestDetailsOptions = { - indexName: 'auditbeat-8.0.0-2019.03.29-000003', - eventId: 'TUfUymkBCQofM5eXGBYL', - defaultIndex: defaultIndexPattern, -}; - -export const mockRequest = { - body: { - operationName: 'GetNetworkTopNFlowQuery', - variables: { - indexName: 'auditbeat-8.0.0-2019.03.29-000003', - eventId: 'TUfUymkBCQofM5eXGBYL', - }, - query: `query GetTimelineDetailsQuery($eventId: String!, $indexName: String!) { - source(id: "default") { - TimelineDetails(eventId: $eventId, indexName: $indexName) { - data { - category - description - example - field - type - values - originalValue - } - } - } - }`, - }, -}; - -export const mockResponseMap = { - 'auditbeat-8.0.0-2019.03.29-000003': { - mappings: { - _meta: { - beat: 'auditbeat', - version: '8.0.0', - }, - dynamic_templates: [ - { - 'container.labels': { - path_match: 'container.labels.*', - match_mapping_type: 'string', - mapping: { - type: 'keyword', - }, - }, - }, - { - fields: { - path_match: 'fields.*', - match_mapping_type: 'string', - mapping: { - type: 'keyword', - }, - }, - }, - { - 'docker.container.labels': { - path_match: 'docker.container.labels.*', - match_mapping_type: 'string', - mapping: { - type: 'keyword', - }, - }, - }, - { - strings_as_keyword: { - match_mapping_type: 'string', - mapping: { - ignore_above: 1024, - type: 'keyword', - }, - }, - }, - ], - date_detection: false, - properties: { - '@timestamp': { - type: 'date', - }, - agent: { - properties: { - ephemeral_id: { - type: 'keyword', - ignore_above: 1024, - }, - hostname: { - type: 'keyword', - ignore_above: 1024, - }, - id: { - type: 'keyword', - ignore_above: 1024, - }, - name: { - type: 'keyword', - ignore_above: 1024, - }, - type: { - type: 'keyword', - ignore_above: 1024, - }, - version: { - type: 'keyword', - ignore_above: 1024, - }, - }, - }, - auditd: { - properties: { - data: { - properties: { - a0: { - type: 'keyword', - ignore_above: 1024, - }, - a1: { - type: 'keyword', - ignore_above: 1024, - }, - a2: { - type: 'keyword', - ignore_above: 1024, - }, - a3: { - type: 'keyword', - ignore_above: 1024, - }, - 'a[0-3]': { - type: 'keyword', - ignore_above: 1024, - }, - acct: { - type: 'keyword', - ignore_above: 1024, - }, - acl: { - type: 'keyword', - ignore_above: 1024, - }, - action: { - type: 'keyword', - ignore_above: 1024, - }, - added: { - type: 'keyword', - ignore_above: 1024, - }, - addr: { - type: 'keyword', - ignore_above: 1024, - }, - apparmor: { - type: 'keyword', - ignore_above: 1024, - }, - arch: { - type: 'keyword', - ignore_above: 1024, - }, - argc: { - type: 'keyword', - ignore_above: 1024, - }, - audit_backlog_limit: { - type: 'keyword', - ignore_above: 1024, - }, - audit_backlog_wait_time: { - type: 'keyword', - ignore_above: 1024, - }, - audit_enabled: { - type: 'keyword', - ignore_above: 1024, - }, - audit_failure: { - type: 'keyword', - ignore_above: 1024, - }, - banners: { - type: 'keyword', - ignore_above: 1024, - }, - bool: { - type: 'keyword', - ignore_above: 1024, - }, - bus: { - type: 'keyword', - ignore_above: 1024, - }, - cap_fe: { - type: 'keyword', - ignore_above: 1024, - }, - cap_fi: { - type: 'keyword', - ignore_above: 1024, - }, - cap_fp: { - type: 'keyword', - ignore_above: 1024, - }, - cap_fver: { - type: 'keyword', - ignore_above: 1024, - }, - cap_pe: { - type: 'keyword', - ignore_above: 1024, - }, - cap_pi: { - type: 'keyword', - ignore_above: 1024, - }, - cap_pp: { - type: 'keyword', - ignore_above: 1024, - }, - capability: { - type: 'keyword', - ignore_above: 1024, - }, - cgroup: { - type: 'keyword', - ignore_above: 1024, - }, - changed: { - type: 'keyword', - ignore_above: 1024, - }, - cipher: { - type: 'keyword', - ignore_above: 1024, - }, - class: { - type: 'keyword', - ignore_above: 1024, - }, - cmd: { - type: 'keyword', - ignore_above: 1024, - }, - code: { - type: 'keyword', - ignore_above: 1024, - }, - compat: { - type: 'keyword', - ignore_above: 1024, - }, - daddr: { - type: 'keyword', - ignore_above: 1024, - }, - data: { - type: 'keyword', - ignore_above: 1024, - }, - 'default-context': { - type: 'keyword', - ignore_above: 1024, - }, - dev: { - type: 'keyword', - ignore_above: 1024, - }, - device: { - type: 'keyword', - ignore_above: 1024, - }, - dir: { - type: 'keyword', - ignore_above: 1024, - }, - direction: { - type: 'keyword', - ignore_above: 1024, - }, - dmac: { - type: 'keyword', - ignore_above: 1024, - }, - dport: { - type: 'keyword', - ignore_above: 1024, - }, - enforcing: { - type: 'keyword', - ignore_above: 1024, - }, - entries: { - type: 'keyword', - ignore_above: 1024, - }, - exit: { - type: 'keyword', - ignore_above: 1024, - }, - fam: { - type: 'keyword', - ignore_above: 1024, - }, - family: { - type: 'keyword', - ignore_above: 1024, - }, - fd: { - type: 'keyword', - ignore_above: 1024, - }, - fe: { - type: 'keyword', - ignore_above: 1024, - }, - feature: { - type: 'keyword', - ignore_above: 1024, - }, - fi: { - type: 'keyword', - ignore_above: 1024, - }, - file: { - type: 'keyword', - ignore_above: 1024, - }, - flags: { - type: 'keyword', - ignore_above: 1024, - }, - format: { - type: 'keyword', - ignore_above: 1024, - }, - fp: { - type: 'keyword', - ignore_above: 1024, - }, - fver: { - type: 'keyword', - ignore_above: 1024, - }, - grantors: { - type: 'keyword', - ignore_above: 1024, - }, - grp: { - type: 'keyword', - ignore_above: 1024, - }, - hook: { - type: 'keyword', - ignore_above: 1024, - }, - hostname: { - type: 'keyword', - ignore_above: 1024, - }, - icmp_type: { - type: 'keyword', - ignore_above: 1024, - }, - id: { - type: 'keyword', - ignore_above: 1024, - }, - igid: { - type: 'keyword', - ignore_above: 1024, - }, - 'img-ctx': { - type: 'keyword', - ignore_above: 1024, - }, - inif: { - type: 'keyword', - ignore_above: 1024, - }, - ino: { - type: 'keyword', - ignore_above: 1024, - }, - inode: { - type: 'keyword', - ignore_above: 1024, - }, - inode_gid: { - type: 'keyword', - ignore_above: 1024, - }, - inode_uid: { - type: 'keyword', - ignore_above: 1024, - }, - invalid_context: { - type: 'keyword', - ignore_above: 1024, - }, - ioctlcmd: { - type: 'keyword', - ignore_above: 1024, - }, - ip: { - type: 'keyword', - ignore_above: 1024, - }, - ipid: { - type: 'keyword', - ignore_above: 1024, - }, - 'ipx-net': { - type: 'keyword', - ignore_above: 1024, - }, - item: { - type: 'keyword', - ignore_above: 1024, - }, - items: { - type: 'keyword', - ignore_above: 1024, - }, - iuid: { - type: 'keyword', - ignore_above: 1024, - }, - kernel: { - type: 'keyword', - ignore_above: 1024, - }, - kind: { - type: 'keyword', - ignore_above: 1024, - }, - ksize: { - type: 'keyword', - ignore_above: 1024, - }, - laddr: { - type: 'keyword', - ignore_above: 1024, - }, - len: { - type: 'keyword', - ignore_above: 1024, - }, - list: { - type: 'keyword', - ignore_above: 1024, - }, - lport: { - type: 'keyword', - ignore_above: 1024, - }, - mac: { - type: 'keyword', - ignore_above: 1024, - }, - macproto: { - type: 'keyword', - ignore_above: 1024, - }, - maj: { - type: 'keyword', - ignore_above: 1024, - }, - major: { - type: 'keyword', - ignore_above: 1024, - }, - minor: { - type: 'keyword', - ignore_above: 1024, - }, - mode: { - type: 'keyword', - ignore_above: 1024, - }, - model: { - type: 'keyword', - ignore_above: 1024, - }, - msg: { - type: 'keyword', - ignore_above: 1024, - }, - name: { - type: 'keyword', - ignore_above: 1024, - }, - nametype: { - type: 'keyword', - ignore_above: 1024, - }, - nargs: { - type: 'keyword', - ignore_above: 1024, - }, - net: { - type: 'keyword', - ignore_above: 1024, - }, - new: { - type: 'keyword', - ignore_above: 1024, - }, - 'new-chardev': { - type: 'keyword', - ignore_above: 1024, - }, - 'new-disk': { - type: 'keyword', - ignore_above: 1024, - }, - 'new-enabled': { - type: 'keyword', - ignore_above: 1024, - }, - 'new-fs': { - type: 'keyword', - ignore_above: 1024, - }, - 'new-level': { - type: 'keyword', - ignore_above: 1024, - }, - 'new-log_passwd': { - type: 'keyword', - ignore_above: 1024, - }, - 'new-mem': { - type: 'keyword', - ignore_above: 1024, - }, - 'new-net': { - type: 'keyword', - ignore_above: 1024, - }, - 'new-range': { - type: 'keyword', - ignore_above: 1024, - }, - 'new-rng': { - type: 'keyword', - ignore_above: 1024, - }, - 'new-role': { - type: 'keyword', - ignore_above: 1024, - }, - 'new-seuser': { - type: 'keyword', - ignore_above: 1024, - }, - 'new-vcpu': { - type: 'keyword', - ignore_above: 1024, - }, - new_gid: { - type: 'keyword', - ignore_above: 1024, - }, - new_lock: { - type: 'keyword', - ignore_above: 1024, - }, - new_pe: { - type: 'keyword', - ignore_above: 1024, - }, - new_pi: { - type: 'keyword', - ignore_above: 1024, - }, - new_pp: { - type: 'keyword', - ignore_above: 1024, - }, - 'nlnk-fam': { - type: 'keyword', - ignore_above: 1024, - }, - 'nlnk-grp': { - type: 'keyword', - ignore_above: 1024, - }, - 'nlnk-pid': { - type: 'keyword', - ignore_above: 1024, - }, - oauid: { - type: 'keyword', - ignore_above: 1024, - }, - obj: { - type: 'keyword', - ignore_above: 1024, - }, - obj_gid: { - type: 'keyword', - ignore_above: 1024, - }, - obj_uid: { - type: 'keyword', - ignore_above: 1024, - }, - ocomm: { - type: 'keyword', - ignore_above: 1024, - }, - oflag: { - type: 'keyword', - ignore_above: 1024, - }, - old: { - type: 'keyword', - ignore_above: 1024, - }, - 'old-auid': { - type: 'keyword', - ignore_above: 1024, - }, - 'old-chardev': { - type: 'keyword', - ignore_above: 1024, - }, - 'old-disk': { - type: 'keyword', - ignore_above: 1024, - }, - 'old-enabled': { - type: 'keyword', - ignore_above: 1024, - }, - 'old-fs': { - type: 'keyword', - ignore_above: 1024, - }, - 'old-level': { - type: 'keyword', - ignore_above: 1024, - }, - 'old-log_passwd': { - type: 'keyword', - ignore_above: 1024, - }, - 'old-mem': { - type: 'keyword', - ignore_above: 1024, - }, - 'old-net': { - type: 'keyword', - ignore_above: 1024, - }, - 'old-range': { - type: 'keyword', - ignore_above: 1024, - }, - 'old-rng': { - type: 'keyword', - ignore_above: 1024, - }, - 'old-role': { - type: 'keyword', - ignore_above: 1024, - }, - 'old-ses': { - type: 'keyword', - ignore_above: 1024, - }, - 'old-seuser': { - type: 'keyword', - ignore_above: 1024, - }, - 'old-vcpu': { - type: 'keyword', - ignore_above: 1024, - }, - old_enforcing: { - type: 'keyword', - ignore_above: 1024, - }, - old_lock: { - type: 'keyword', - ignore_above: 1024, - }, - old_pe: { - type: 'keyword', - ignore_above: 1024, - }, - old_pi: { - type: 'keyword', - ignore_above: 1024, - }, - old_pp: { - type: 'keyword', - ignore_above: 1024, - }, - old_prom: { - type: 'keyword', - ignore_above: 1024, - }, - old_val: { - type: 'keyword', - ignore_above: 1024, - }, - op: { - type: 'keyword', - ignore_above: 1024, - }, - opid: { - type: 'keyword', - ignore_above: 1024, - }, - oses: { - type: 'keyword', - ignore_above: 1024, - }, - outif: { - type: 'keyword', - ignore_above: 1024, - }, - parent: { - type: 'keyword', - ignore_above: 1024, - }, - path: { - type: 'keyword', - ignore_above: 1024, - }, - per: { - type: 'keyword', - ignore_above: 1024, - }, - perm: { - type: 'keyword', - ignore_above: 1024, - }, - perm_mask: { - type: 'keyword', - ignore_above: 1024, - }, - permissive: { - type: 'keyword', - ignore_above: 1024, - }, - pfs: { - type: 'keyword', - ignore_above: 1024, - }, - printer: { - type: 'keyword', - ignore_above: 1024, - }, - prom: { - type: 'keyword', - ignore_above: 1024, - }, - proto: { - type: 'keyword', - ignore_above: 1024, - }, - qbytes: { - type: 'keyword', - ignore_above: 1024, - }, - range: { - type: 'keyword', - ignore_above: 1024, - }, - rdev: { - type: 'keyword', - ignore_above: 1024, - }, - reason: { - type: 'keyword', - ignore_above: 1024, - }, - removed: { - type: 'keyword', - ignore_above: 1024, - }, - res: { - type: 'keyword', - ignore_above: 1024, - }, - resrc: { - type: 'keyword', - ignore_above: 1024, - }, - rport: { - type: 'keyword', - ignore_above: 1024, - }, - sauid: { - type: 'keyword', - ignore_above: 1024, - }, - scontext: { - type: 'keyword', - ignore_above: 1024, - }, - 'selected-context': { - type: 'keyword', - ignore_above: 1024, - }, - seperm: { - type: 'keyword', - ignore_above: 1024, - }, - seperms: { - type: 'keyword', - ignore_above: 1024, - }, - seqno: { - type: 'keyword', - ignore_above: 1024, - }, - seresult: { - type: 'keyword', - ignore_above: 1024, - }, - ses: { - type: 'keyword', - ignore_above: 1024, - }, - seuser: { - type: 'keyword', - ignore_above: 1024, - }, - sig: { - type: 'keyword', - ignore_above: 1024, - }, - sigev_signo: { - type: 'keyword', - ignore_above: 1024, - }, - smac: { - type: 'keyword', - ignore_above: 1024, - }, - socket: { - properties: { - addr: { - type: 'keyword', - ignore_above: 1024, - }, - family: { - type: 'keyword', - ignore_above: 1024, - }, - path: { - type: 'keyword', - ignore_above: 1024, - }, - port: { - type: 'keyword', - ignore_above: 1024, - }, - saddr: { - type: 'keyword', - ignore_above: 1024, - }, - }, - }, - spid: { - type: 'keyword', - ignore_above: 1024, - }, - sport: { - type: 'keyword', - ignore_above: 1024, - }, - state: { - type: 'keyword', - ignore_above: 1024, - }, - subj: { - type: 'keyword', - ignore_above: 1024, - }, - success: { - type: 'keyword', - ignore_above: 1024, - }, - syscall: { - type: 'keyword', - ignore_above: 1024, - }, - table: { - type: 'keyword', - ignore_above: 1024, - }, - tclass: { - type: 'keyword', - ignore_above: 1024, - }, - tcontext: { - type: 'keyword', - ignore_above: 1024, - }, - terminal: { - type: 'keyword', - ignore_above: 1024, - }, - tty: { - type: 'keyword', - ignore_above: 1024, - }, - unit: { - type: 'keyword', - ignore_above: 1024, - }, - uri: { - type: 'keyword', - ignore_above: 1024, - }, - uuid: { - type: 'keyword', - ignore_above: 1024, - }, - val: { - type: 'keyword', - ignore_above: 1024, - }, - ver: { - type: 'keyword', - ignore_above: 1024, - }, - virt: { - type: 'keyword', - ignore_above: 1024, - }, - vm: { - type: 'keyword', - ignore_above: 1024, - }, - 'vm-ctx': { - type: 'keyword', - ignore_above: 1024, - }, - 'vm-pid': { - type: 'keyword', - ignore_above: 1024, - }, - watch: { - type: 'keyword', - ignore_above: 1024, - }, - }, - }, - message_type: { - type: 'keyword', - ignore_above: 1024, - }, - paths: { - properties: { - cap_fe: { - type: 'keyword', - ignore_above: 1024, - }, - cap_fi: { - type: 'keyword', - ignore_above: 1024, - }, - cap_fp: { - type: 'keyword', - ignore_above: 1024, - }, - cap_fver: { - type: 'keyword', - ignore_above: 1024, - }, - dev: { - type: 'keyword', - ignore_above: 1024, - }, - inode: { - type: 'keyword', - ignore_above: 1024, - }, - item: { - type: 'keyword', - ignore_above: 1024, - }, - mode: { - type: 'keyword', - ignore_above: 1024, - }, - name: { - type: 'keyword', - ignore_above: 1024, - }, - nametype: { - type: 'keyword', - ignore_above: 1024, - }, - obj_domain: { - type: 'keyword', - ignore_above: 1024, - }, - obj_level: { - type: 'keyword', - ignore_above: 1024, - }, - obj_role: { - type: 'keyword', - ignore_above: 1024, - }, - obj_user: { - type: 'keyword', - ignore_above: 1024, - }, - objtype: { - type: 'keyword', - ignore_above: 1024, - }, - ogid: { - type: 'keyword', - ignore_above: 1024, - }, - ouid: { - type: 'keyword', - ignore_above: 1024, - }, - rdev: { - type: 'keyword', - ignore_above: 1024, - }, - }, - }, - result: { - type: 'keyword', - ignore_above: 1024, - }, - sequence: { - type: 'long', - }, - session: { - type: 'keyword', - ignore_above: 1024, - }, - summary: { - properties: { - actor: { - properties: { - primary: { - type: 'keyword', - ignore_above: 1024, - }, - secondary: { - type: 'keyword', - ignore_above: 1024, - }, - }, - }, - how: { - type: 'keyword', - ignore_above: 1024, - }, - object: { - properties: { - primary: { - type: 'keyword', - ignore_above: 1024, - }, - secondary: { - type: 'keyword', - ignore_above: 1024, - }, - type: { - type: 'keyword', - ignore_above: 1024, - }, - }, - }, - }, - }, - }, - }, - client: { - properties: { - address: { - type: 'keyword', - ignore_above: 1024, - }, - bytes: { - type: 'long', - }, - domain: { - type: 'keyword', - ignore_above: 1024, - }, - geo: { - properties: { - city_name: { - type: 'keyword', - ignore_above: 1024, - }, - continent_name: { - type: 'keyword', - ignore_above: 1024, - }, - country_iso_code: { - type: 'keyword', - ignore_above: 1024, - }, - country_name: { - type: 'keyword', - ignore_above: 1024, - }, - location: { - type: 'geo_point', - }, - name: { - type: 'keyword', - ignore_above: 1024, - }, - region_iso_code: { - type: 'keyword', - ignore_above: 1024, - }, - region_name: { - type: 'keyword', - ignore_above: 1024, - }, - }, - }, - ip: { - type: 'ip', - }, - mac: { - type: 'keyword', - ignore_above: 1024, - }, - packets: { - type: 'long', - }, - port: { - type: 'long', - }, - }, - }, - cloud: { - properties: { - account: { - properties: { - id: { - type: 'keyword', - ignore_above: 1024, - }, - }, - }, - availability_zone: { - type: 'keyword', - ignore_above: 1024, - }, - instance: { - properties: { - id: { - type: 'keyword', - ignore_above: 1024, - }, - name: { - type: 'keyword', - ignore_above: 1024, - }, - }, - }, - machine: { - properties: { - type: { - type: 'keyword', - ignore_above: 1024, - }, - }, - }, - project: { - properties: { - id: { - type: 'keyword', - ignore_above: 1024, - }, - }, - }, - provider: { - type: 'keyword', - ignore_above: 1024, - }, - region: { - type: 'keyword', - ignore_above: 1024, - }, - }, - }, - container: { - properties: { - id: { - type: 'keyword', - ignore_above: 1024, - }, - image: { - properties: { - name: { - type: 'keyword', - ignore_above: 1024, - }, - tag: { - type: 'keyword', - ignore_above: 1024, - }, - }, - }, - labels: { - type: 'object', - }, - name: { - type: 'keyword', - ignore_above: 1024, - }, - runtime: { - type: 'keyword', - ignore_above: 1024, - }, - }, - }, - destination: { - properties: { - address: { - type: 'keyword', - ignore_above: 1024, - }, - bytes: { - type: 'long', - }, - domain: { - type: 'keyword', - ignore_above: 1024, - }, - geo: { - properties: { - city_name: { - type: 'keyword', - ignore_above: 1024, - }, - continent_name: { - type: 'keyword', - ignore_above: 1024, - }, - country_iso_code: { - type: 'keyword', - ignore_above: 1024, - }, - country_name: { - type: 'keyword', - ignore_above: 1024, - }, - location: { - type: 'geo_point', - }, - name: { - type: 'keyword', - ignore_above: 1024, - }, - region_iso_code: { - type: 'keyword', - ignore_above: 1024, - }, - region_name: { - type: 'keyword', - ignore_above: 1024, - }, - }, - }, - ip: { - type: 'ip', - }, - mac: { - type: 'keyword', - ignore_above: 1024, - }, - packets: { - type: 'long', - }, - path: { - type: 'keyword', - ignore_above: 1024, - }, - port: { - type: 'long', - }, - }, - }, - docker: { - properties: { - container: { - properties: { - labels: { - type: 'object', - }, - }, - }, - }, - }, - ecs: { - properties: { - version: { - type: 'keyword', - ignore_above: 1024, - }, - }, - }, - error: { - properties: { - code: { - type: 'keyword', - ignore_above: 1024, - }, - id: { - type: 'keyword', - ignore_above: 1024, - }, - message: { - type: 'text', - norms: false, - }, - type: { - type: 'keyword', - ignore_above: 1024, - }, - }, - }, - event: { - properties: { - action: { - type: 'keyword', - ignore_above: 1024, - }, - category: { - type: 'keyword', - ignore_above: 1024, - }, - created: { - type: 'date', - }, - dataset: { - type: 'keyword', - ignore_above: 1024, - }, - duration: { - type: 'long', - }, - end: { - type: 'date', - }, - hash: { - type: 'keyword', - ignore_above: 1024, - }, - id: { - type: 'keyword', - ignore_above: 1024, - }, - kind: { - type: 'keyword', - ignore_above: 1024, - }, - module: { - type: 'keyword', - ignore_above: 1024, - }, - origin: { - type: 'keyword', - ignore_above: 1024, - }, - original: { - type: 'keyword', - index: false, - doc_values: false, - ignore_above: 1024, - }, - outcome: { - type: 'keyword', - ignore_above: 1024, - }, - risk_score: { - type: 'float', - }, - risk_score_norm: { - type: 'float', - }, - severity: { - type: 'long', - }, - start: { - type: 'date', - }, - timezone: { - type: 'keyword', - ignore_above: 1024, - }, - type: { - type: 'keyword', - ignore_above: 1024, - }, - }, - }, - fields: { - type: 'object', - }, - file: { - properties: { - ctime: { - type: 'date', - }, - device: { - type: 'keyword', - ignore_above: 1024, - }, - extension: { - type: 'keyword', - ignore_above: 1024, - }, - gid: { - type: 'keyword', - ignore_above: 1024, - }, - group: { - type: 'keyword', - ignore_above: 1024, - }, - inode: { - type: 'keyword', - ignore_above: 1024, - }, - mode: { - type: 'keyword', - ignore_above: 1024, - }, - mtime: { - type: 'date', - }, - origin: { - type: 'keyword', - fields: { - raw: { - type: 'keyword', - ignore_above: 1024, - }, - }, - ignore_above: 1024, - }, - owner: { - type: 'keyword', - ignore_above: 1024, - }, - path: { - type: 'keyword', - ignore_above: 1024, - }, - selinux: { - properties: { - domain: { - type: 'keyword', - ignore_above: 1024, - }, - level: { - type: 'keyword', - ignore_above: 1024, - }, - role: { - type: 'keyword', - ignore_above: 1024, - }, - user: { - type: 'keyword', - ignore_above: 1024, - }, - }, - }, - setgid: { - type: 'boolean', - }, - setuid: { - type: 'boolean', - }, - size: { - type: 'long', - }, - target_path: { - type: 'keyword', - ignore_above: 1024, - }, - type: { - type: 'keyword', - ignore_above: 1024, - }, - uid: { - type: 'keyword', - ignore_above: 1024, - }, - }, - }, - geoip: { - properties: { - city_name: { - type: 'keyword', - ignore_above: 1024, - }, - continent_name: { - type: 'keyword', - ignore_above: 1024, - }, - country_iso_code: { - type: 'keyword', - ignore_above: 1024, - }, - location: { - type: 'geo_point', - }, - region_name: { - type: 'keyword', - ignore_above: 1024, - }, - }, - }, - group: { - properties: { - id: { - type: 'keyword', - ignore_above: 1024, - }, - name: { - type: 'keyword', - ignore_above: 1024, - }, - }, - }, - hash: { - properties: { - blake2b_256: { - type: 'keyword', - ignore_above: 1024, - }, - blake2b_384: { - type: 'keyword', - ignore_above: 1024, - }, - blake2b_512: { - type: 'keyword', - ignore_above: 1024, - }, - md5: { - type: 'keyword', - ignore_above: 1024, - }, - sha1: { - type: 'keyword', - ignore_above: 1024, - }, - sha224: { - type: 'keyword', - ignore_above: 1024, - }, - sha256: { - type: 'keyword', - ignore_above: 1024, - }, - sha384: { - type: 'keyword', - ignore_above: 1024, - }, - sha3_224: { - type: 'keyword', - ignore_above: 1024, - }, - sha3_256: { - type: 'keyword', - ignore_above: 1024, - }, - sha3_384: { - type: 'keyword', - ignore_above: 1024, - }, - sha3_512: { - type: 'keyword', - ignore_above: 1024, - }, - sha512: { - type: 'keyword', - ignore_above: 1024, - }, - sha512_224: { - type: 'keyword', - ignore_above: 1024, - }, - sha512_256: { - type: 'keyword', - ignore_above: 1024, - }, - xxh64: { - type: 'keyword', - ignore_above: 1024, - }, - }, - }, - host: { - properties: { - architecture: { - type: 'keyword', - ignore_above: 1024, - }, - containerized: { - type: 'boolean', - }, - geo: { - properties: { - city_name: { - type: 'keyword', - ignore_above: 1024, - }, - continent_name: { - type: 'keyword', - ignore_above: 1024, - }, - country_iso_code: { - type: 'keyword', - ignore_above: 1024, - }, - country_name: { - type: 'keyword', - ignore_above: 1024, - }, - location: { - type: 'geo_point', - }, - name: { - type: 'keyword', - ignore_above: 1024, - }, - region_iso_code: { - type: 'keyword', - ignore_above: 1024, - }, - region_name: { - type: 'keyword', - ignore_above: 1024, - }, - }, - }, - hostname: { - type: 'keyword', - ignore_above: 1024, - }, - id: { - type: 'keyword', - ignore_above: 1024, - }, - ip: { - type: 'ip', - }, - mac: { - type: 'keyword', - ignore_above: 1024, - }, - name: { - type: 'keyword', - ignore_above: 1024, - }, - os: { - properties: { - codename: { - type: 'keyword', - ignore_above: 1024, - }, - family: { - type: 'keyword', - ignore_above: 1024, - }, - full: { - type: 'keyword', - ignore_above: 1024, - }, - kernel: { - type: 'keyword', - ignore_above: 1024, - }, - name: { - type: 'keyword', - ignore_above: 1024, - }, - platform: { - type: 'keyword', - ignore_above: 1024, - }, - version: { - type: 'keyword', - ignore_above: 1024, - }, - }, - }, - type: { - type: 'keyword', - ignore_above: 1024, - }, - }, - }, - http: { - properties: { - request: { - properties: { - body: { - properties: { - bytes: { - type: 'long', - }, - content: { - type: 'keyword', - ignore_above: 1024, - }, - }, - }, - bytes: { - type: 'long', - }, - method: { - type: 'keyword', - ignore_above: 1024, - }, - referrer: { - type: 'keyword', - ignore_above: 1024, - }, - }, - }, - response: { - properties: { - body: { - properties: { - bytes: { - type: 'long', - }, - content: { - type: 'keyword', - ignore_above: 1024, - }, - }, - }, - bytes: { - type: 'long', - }, - status_code: { - type: 'long', - }, - }, - }, - version: { - type: 'keyword', - ignore_above: 1024, - }, - }, - }, - kubernetes: { - properties: { - annotations: { - type: 'object', - }, - container: { - properties: { - image: { - type: 'keyword', - ignore_above: 1024, - }, - name: { - type: 'keyword', - ignore_above: 1024, - }, - }, - }, - labels: { - type: 'object', - }, - namespace: { - type: 'keyword', - ignore_above: 1024, - }, - node: { - properties: { - name: { - type: 'keyword', - ignore_above: 1024, - }, - }, - }, - pod: { - properties: { - name: { - type: 'keyword', - ignore_above: 1024, - }, - uid: { - type: 'keyword', - ignore_above: 1024, - }, - }, - }, - }, - }, - labels: { - type: 'object', - }, - log: { - properties: { - level: { - type: 'keyword', - ignore_above: 1024, - }, - original: { - type: 'keyword', - index: false, - doc_values: false, - ignore_above: 1024, - }, - }, - }, - message: { - type: 'text', - norms: false, - }, - network: { - properties: { - application: { - type: 'keyword', - ignore_above: 1024, - }, - bytes: { - type: 'long', - }, - community_id: { - type: 'keyword', - ignore_above: 1024, - }, - direction: { - type: 'keyword', - ignore_above: 1024, - }, - forwarded_ip: { - type: 'ip', - }, - iana_number: { - type: 'keyword', - ignore_above: 1024, - }, - name: { - type: 'keyword', - ignore_above: 1024, - }, - packets: { - type: 'long', - }, - protocol: { - type: 'keyword', - ignore_above: 1024, - }, - transport: { - type: 'keyword', - ignore_above: 1024, - }, - type: { - type: 'keyword', - ignore_above: 1024, - }, - }, - }, - observer: { - properties: { - geo: { - properties: { - city_name: { - type: 'keyword', - ignore_above: 1024, - }, - continent_name: { - type: 'keyword', - ignore_above: 1024, - }, - country_iso_code: { - type: 'keyword', - ignore_above: 1024, - }, - country_name: { - type: 'keyword', - ignore_above: 1024, - }, - location: { - type: 'geo_point', - }, - name: { - type: 'keyword', - ignore_above: 1024, - }, - region_iso_code: { - type: 'keyword', - ignore_above: 1024, - }, - region_name: { - type: 'keyword', - ignore_above: 1024, - }, - }, - }, - hostname: { - type: 'keyword', - ignore_above: 1024, - }, - ip: { - type: 'ip', - }, - mac: { - type: 'keyword', - ignore_above: 1024, - }, - os: { - properties: { - family: { - type: 'keyword', - ignore_above: 1024, - }, - full: { - type: 'keyword', - ignore_above: 1024, - }, - kernel: { - type: 'keyword', - ignore_above: 1024, - }, - name: { - type: 'keyword', - ignore_above: 1024, - }, - platform: { - type: 'keyword', - ignore_above: 1024, - }, - version: { - type: 'keyword', - ignore_above: 1024, - }, - }, - }, - serial_number: { - type: 'keyword', - ignore_above: 1024, - }, - type: { - type: 'keyword', - ignore_above: 1024, - }, - vendor: { - type: 'keyword', - ignore_above: 1024, - }, - version: { - type: 'keyword', - ignore_above: 1024, - }, - }, - }, - organization: { - properties: { - id: { - type: 'keyword', - ignore_above: 1024, - }, - name: { - type: 'keyword', - ignore_above: 1024, - }, - }, - }, - os: { - properties: { - family: { - type: 'keyword', - ignore_above: 1024, - }, - full: { - type: 'keyword', - ignore_above: 1024, - }, - kernel: { - type: 'keyword', - ignore_above: 1024, - }, - name: { - type: 'keyword', - ignore_above: 1024, - }, - platform: { - type: 'keyword', - ignore_above: 1024, - }, - version: { - type: 'keyword', - ignore_above: 1024, - }, - }, - }, - process: { - properties: { - args: { - type: 'keyword', - ignore_above: 1024, - }, - entity_id: { - type: 'keyword', - ignore_above: 1024, - }, - executable: { - type: 'keyword', - ignore_above: 1024, - }, - name: { - type: 'keyword', - ignore_above: 1024, - }, - pid: { - type: 'long', - }, - ppid: { - type: 'long', - }, - start: { - type: 'date', - }, - thread: { - properties: { - id: { - type: 'long', - }, - }, - }, - title: { - type: 'keyword', - ignore_above: 1024, - }, - working_directory: { - type: 'keyword', - ignore_above: 1024, - }, - }, - }, - related: { - properties: { - ip: { - type: 'ip', - }, - }, - }, - server: { - properties: { - address: { - type: 'keyword', - ignore_above: 1024, - }, - bytes: { - type: 'long', - }, - domain: { - type: 'keyword', - ignore_above: 1024, - }, - geo: { - properties: { - city_name: { - type: 'keyword', - ignore_above: 1024, - }, - continent_name: { - type: 'keyword', - ignore_above: 1024, - }, - country_iso_code: { - type: 'keyword', - ignore_above: 1024, - }, - country_name: { - type: 'keyword', - ignore_above: 1024, - }, - location: { - type: 'geo_point', - }, - name: { - type: 'keyword', - ignore_above: 1024, - }, - region_iso_code: { - type: 'keyword', - ignore_above: 1024, - }, - region_name: { - type: 'keyword', - ignore_above: 1024, - }, - }, - }, - ip: { - type: 'ip', - }, - mac: { - type: 'keyword', - ignore_above: 1024, - }, - packets: { - type: 'long', - }, - port: { - type: 'long', - }, - }, - }, - service: { - properties: { - ephemeral_id: { - type: 'keyword', - ignore_above: 1024, - }, - id: { - type: 'keyword', - ignore_above: 1024, - }, - name: { - type: 'keyword', - ignore_above: 1024, - }, - state: { - type: 'keyword', - ignore_above: 1024, - }, - type: { - type: 'keyword', - ignore_above: 1024, - }, - version: { - type: 'keyword', - ignore_above: 1024, - }, - }, - }, - socket: { - properties: { - entity_id: { - type: 'keyword', - ignore_above: 1024, - }, - }, - }, - source: { - properties: { - address: { - type: 'keyword', - ignore_above: 1024, - }, - bytes: { - type: 'long', - }, - domain: { - type: 'keyword', - ignore_above: 1024, - }, - geo: { - properties: { - city_name: { - type: 'keyword', - ignore_above: 1024, - }, - continent_name: { - type: 'keyword', - ignore_above: 1024, - }, - country_iso_code: { - type: 'keyword', - ignore_above: 1024, - }, - country_name: { - type: 'keyword', - ignore_above: 1024, - }, - location: { - type: 'geo_point', - }, - name: { - type: 'keyword', - ignore_above: 1024, - }, - region_iso_code: { - type: 'keyword', - ignore_above: 1024, - }, - region_name: { - type: 'keyword', - ignore_above: 1024, - }, - }, - }, - ip: { - type: 'ip', - }, - mac: { - type: 'keyword', - ignore_above: 1024, - }, - packets: { - type: 'long', - }, - path: { - type: 'keyword', - ignore_above: 1024, - }, - port: { - type: 'long', - }, - }, - }, - system: { - properties: { - audit: { - properties: { - host: { - properties: { - architecture: { - type: 'keyword', - ignore_above: 1024, - }, - boottime: { - type: 'date', - }, - containerized: { - type: 'boolean', - }, - hostname: { - type: 'keyword', - ignore_above: 1024, - }, - id: { - type: 'keyword', - ignore_above: 1024, - }, - ip: { - type: 'ip', - }, - mac: { - type: 'keyword', - ignore_above: 1024, - }, - os: { - properties: { - family: { - type: 'keyword', - ignore_above: 1024, - }, - kernel: { - type: 'keyword', - ignore_above: 1024, - }, - name: { - type: 'keyword', - ignore_above: 1024, - }, - platform: { - type: 'keyword', - ignore_above: 1024, - }, - version: { - type: 'keyword', - ignore_above: 1024, - }, - }, - }, - timezone: { - properties: { - name: { - type: 'keyword', - ignore_above: 1024, - }, - offset: { - properties: { - sec: { - type: 'long', - }, - }, - }, - }, - }, - uptime: { - type: 'long', - }, - }, - }, - package: { - properties: { - arch: { - type: 'keyword', - ignore_above: 1024, - }, - entity_id: { - type: 'keyword', - ignore_above: 1024, - }, - installtime: { - type: 'date', - }, - license: { - type: 'keyword', - ignore_above: 1024, - }, - name: { - type: 'keyword', - ignore_above: 1024, - }, - release: { - type: 'keyword', - ignore_above: 1024, - }, - size: { - type: 'long', - }, - summary: { - type: 'keyword', - ignore_above: 1024, - }, - url: { - type: 'keyword', - ignore_above: 1024, - }, - version: { - type: 'keyword', - ignore_above: 1024, - }, - }, - }, - user: { - properties: { - dir: { - type: 'keyword', - ignore_above: 1024, - }, - gid: { - type: 'keyword', - ignore_above: 1024, - }, - group: { - properties: { - gid: { - type: 'keyword', - ignore_above: 1024, - }, - name: { - type: 'keyword', - ignore_above: 1024, - }, - }, - }, - name: { - type: 'keyword', - ignore_above: 1024, - }, - password: { - properties: { - last_changed: { - type: 'date', - }, - type: { - type: 'keyword', - ignore_above: 1024, - }, - }, - }, - shell: { - type: 'keyword', - ignore_above: 1024, - }, - uid: { - type: 'keyword', - ignore_above: 1024, - }, - user_information: { - type: 'keyword', - ignore_above: 1024, - }, - }, - }, - }, - }, - }, - }, - tags: { - type: 'keyword', - ignore_above: 1024, - }, - url: { - properties: { - domain: { - type: 'keyword', - ignore_above: 1024, - }, - fragment: { - type: 'keyword', - ignore_above: 1024, - }, - full: { - type: 'keyword', - ignore_above: 1024, - }, - original: { - type: 'keyword', - ignore_above: 1024, - }, - password: { - type: 'keyword', - ignore_above: 1024, - }, - path: { - type: 'keyword', - ignore_above: 1024, - }, - port: { - type: 'long', - }, - query: { - type: 'keyword', - ignore_above: 1024, - }, - scheme: { - type: 'keyword', - ignore_above: 1024, - }, - username: { - type: 'keyword', - ignore_above: 1024, - }, - }, - }, - user: { - properties: { - audit: { - properties: { - id: { - type: 'keyword', - ignore_above: 1024, - }, - name: { - type: 'keyword', - ignore_above: 1024, - }, - }, - }, - effective: { - properties: { - group: { - properties: { - id: { - type: 'keyword', - ignore_above: 1024, - }, - name: { - type: 'keyword', - ignore_above: 1024, - }, - }, - }, - id: { - type: 'keyword', - ignore_above: 1024, - }, - name: { - type: 'keyword', - ignore_above: 1024, - }, - }, - }, - email: { - type: 'keyword', - ignore_above: 1024, - }, - entity_id: { - type: 'keyword', - ignore_above: 1024, - }, - filesystem: { - properties: { - group: { - properties: { - id: { - type: 'keyword', - ignore_above: 1024, - }, - name: { - type: 'keyword', - ignore_above: 1024, - }, - }, - }, - id: { - type: 'keyword', - ignore_above: 1024, - }, - name: { - type: 'keyword', - ignore_above: 1024, - }, - }, - }, - full_name: { - type: 'keyword', - ignore_above: 1024, - }, - group: { - properties: { - id: { - type: 'keyword', - ignore_above: 1024, - }, - name: { - type: 'keyword', - ignore_above: 1024, - }, - }, - }, - hash: { - type: 'keyword', - ignore_above: 1024, - }, - id: { - type: 'keyword', - ignore_above: 1024, - }, - name: { - type: 'keyword', - ignore_above: 1024, - }, - name_map: { - type: 'object', - }, - ogid: { - properties: { - id: { - type: 'keyword', - ignore_above: 1024, - }, - name: { - type: 'keyword', - ignore_above: 1024, - }, - }, - }, - ouid: { - properties: { - id: { - type: 'keyword', - ignore_above: 1024, - }, - name: { - type: 'keyword', - ignore_above: 1024, - }, - }, - }, - saved: { - properties: { - group: { - properties: { - id: { - type: 'keyword', - ignore_above: 1024, - }, - name: { - type: 'keyword', - ignore_above: 1024, - }, - }, - }, - id: { - type: 'keyword', - ignore_above: 1024, - }, - name: { - type: 'keyword', - ignore_above: 1024, - }, - }, - }, - selinux: { - properties: { - category: { - type: 'keyword', - ignore_above: 1024, - }, - domain: { - type: 'keyword', - ignore_above: 1024, - }, - level: { - type: 'keyword', - ignore_above: 1024, - }, - role: { - type: 'keyword', - ignore_above: 1024, - }, - user: { - type: 'keyword', - ignore_above: 1024, - }, - }, - }, - terminal: { - type: 'keyword', - ignore_above: 1024, - }, - }, - }, - user_agent: { - properties: { - device: { - properties: { - name: { - type: 'keyword', - ignore_above: 1024, - }, - }, - }, - name: { - type: 'keyword', - ignore_above: 1024, - }, - original: { - type: 'keyword', - ignore_above: 1024, - }, - os: { - properties: { - family: { - type: 'keyword', - ignore_above: 1024, - }, - full: { - type: 'keyword', - ignore_above: 1024, - }, - kernel: { - type: 'keyword', - ignore_above: 1024, - }, - name: { - type: 'keyword', - ignore_above: 1024, - }, - platform: { - type: 'keyword', - ignore_above: 1024, - }, - version: { - type: 'keyword', - ignore_above: 1024, - }, - }, - }, - version: { - type: 'keyword', - ignore_above: 1024, - }, - }, - }, - }, - }, - }, -}; - -export const mockDetailsQueryDsl = { - mockDetailsQueryDsl: 'mockDetailsQueryDsl', -}; - -export const mockQueryDsl = { - mockQueryDsl: 'mockQueryDsl', -}; - -const mockTimelineDetailsInspectResponse = cloneDeep(mockResponseSearchTimelineDetails); -delete mockTimelineDetailsInspectResponse.hits.hits[0]._source; - -export const mockTimelineDetailsResult = { - inspect: { - dsl: [JSON.stringify(mockDetailsQueryDsl, null, 2)], - response: [JSON.stringify(mockTimelineDetailsInspectResponse, null, 2)], - }, - data: [ - { - category: 'base', - field: '@timestamp', - values: '2019-03-29T19:01:23.420Z', - originalValue: '2019-03-29T19:01:23.420Z', - }, - { - category: 'service', - field: 'service.type', - values: 'auditd', - originalValue: 'auditd', - }, - { - category: 'user', - field: 'user.audit.id', - values: 'unset', - originalValue: 'unset', - }, - { - category: 'user', - field: 'user.group.id', - values: '0', - originalValue: '0', - }, - { - category: 'user', - field: 'user.group.name', - values: 'root', - originalValue: 'root', - }, - { - category: 'user', - field: 'user.effective.group.id', - values: '0', - originalValue: '0', - }, - { - category: 'user', - field: 'user.effective.group.name', - values: 'root', - originalValue: 'root', - }, - { - category: 'user', - field: 'user.effective.id', - values: '0', - originalValue: '0', - }, - { - category: 'user', - field: 'user.effective.name', - values: 'root', - originalValue: 'root', - }, - { - category: 'user', - field: 'user.filesystem.group.name', - values: 'root', - originalValue: 'root', - }, - { - category: 'user', - field: 'user.filesystem.group.id', - values: '0', - originalValue: '0', - }, - { - category: 'user', - field: 'user.filesystem.name', - values: 'root', - originalValue: 'root', - }, - { - category: 'user', - field: 'user.filesystem.id', - values: '0', - originalValue: '0', - }, - { - category: 'user', - field: 'user.saved.group.id', - values: '0', - originalValue: '0', - }, - { - category: 'user', - field: 'user.saved.group.name', - values: 'root', - originalValue: 'root', - }, - { - category: 'user', - field: 'user.saved.id', - values: '0', - originalValue: '0', - }, - { - category: 'user', - field: 'user.saved.name', - values: 'root', - originalValue: 'root', - }, - { - category: 'user', - field: 'user.id', - values: '0', - originalValue: '0', - }, - { - category: 'user', - field: 'user.name', - values: 'root', - originalValue: 'root', - }, - { - category: 'process', - field: 'process.executable', - values: '/root/go/src/github.com/elastic/beats/x-pack/auditbeat/auditbeat', - originalValue: '/root/go/src/github.com/elastic/beats/x-pack/auditbeat/auditbeat', - }, - { - category: 'process', - field: 'process.working_directory', - values: '/root/go/src/github.com/elastic/beats/x-pack/auditbeat', - originalValue: '/root/go/src/github.com/elastic/beats/x-pack/auditbeat', - }, - { - category: 'process', - field: 'process.pid', - values: 15990, - originalValue: 15990, - }, - { - category: 'process', - field: 'process.ppid', - values: 1, - originalValue: 1, - }, - { - category: 'process', - field: 'process.title', - values: - '/root/go/src/github.com/elastic/beats/x-pack/auditbeat/auditbeat -e -c /root/go/src/github.com/elastic/beats/x-pack/auditbeat/au', - originalValue: - '/root/go/src/github.com/elastic/beats/x-pack/auditbeat/auditbeat -e -c /root/go/src/github.com/elastic/beats/x-pack/auditbeat/au', - }, - { - category: 'process', - field: 'process.name', - values: 'auditbeat', - originalValue: 'auditbeat', - }, - { - category: 'host', - field: 'host.architecture', - values: 'x86_64', - originalValue: 'x86_64', - }, - { - category: 'host', - field: 'host.os.name', - values: 'Ubuntu', - originalValue: 'Ubuntu', - }, - { - category: 'host', - field: 'host.os.kernel', - values: '4.15.0-45-generic', - originalValue: '4.15.0-45-generic', - }, - { - category: 'host', - field: 'host.os.codename', - values: 'bionic', - originalValue: 'bionic', - }, - { - category: 'host', - field: 'host.os.platform', - values: 'ubuntu', - originalValue: 'ubuntu', - }, - { - category: 'host', - field: 'host.os.version', - values: '18.04.2 LTS (Bionic Beaver)', - originalValue: '18.04.2 LTS (Bionic Beaver)', - }, - { - category: 'host', - field: 'host.os.family', - values: 'debian', - originalValue: 'debian', - }, - { - category: 'host', - field: 'host.id', - values: '7c21f5ed03b04d0299569d221fe18bbc', - originalValue: '7c21f5ed03b04d0299569d221fe18bbc', - }, - { - category: 'host', - field: 'host.name', - values: 'zeek-london', - originalValue: 'zeek-london', - }, - { - category: 'host', - field: 'host.ip', - values: ['46.101.3.136', '10.16.0.5', 'fe80::4066:42ff:fe19:b3b9'], - originalValue: ['46.101.3.136', '10.16.0.5', 'fe80::4066:42ff:fe19:b3b9'], - }, - { - category: 'host', - field: 'host.mac', - values: ['42:66:42:19:b3:b9'], - originalValue: ['42:66:42:19:b3:b9'], - }, - { - category: 'host', - field: 'host.hostname', - values: 'zeek-london', - originalValue: 'zeek-london', - }, - { - category: 'cloud', - field: 'cloud.provider', - values: 'digitalocean', - originalValue: 'digitalocean', - }, - { - category: 'cloud', - field: 'cloud.instance.id', - values: '136398786', - originalValue: '136398786', - }, - { - category: 'cloud', - field: 'cloud.region', - values: 'lon1', - originalValue: 'lon1', - }, - { - category: 'file', - field: 'file.device', - values: '00:00', - originalValue: '00:00', - }, - { - category: 'file', - field: 'file.inode', - values: '3926', - originalValue: '3926', - }, - { - category: 'file', - field: 'file.mode', - values: '0644', - originalValue: '0644', - }, - { - category: 'file', - field: 'file.uid', - values: '0', - originalValue: '0', - }, - { - category: 'file', - field: 'file.gid', - values: '0', - originalValue: '0', - }, - { - category: 'file', - field: 'file.owner', - values: 'root', - originalValue: 'root', - }, - { - category: 'file', - field: 'file.group', - values: 'root', - originalValue: 'root', - }, - { - category: 'file', - field: 'file.path', - values: '/etc/passwd', - originalValue: '/etc/passwd', - }, - { - category: 'auditd', - field: 'auditd.session', - values: 'unset', - originalValue: 'unset', - }, - { - category: 'auditd', - field: 'auditd.data.tty', - values: '(none)', - originalValue: '(none)', - }, - { - category: 'auditd', - field: 'auditd.data.a3', - values: '0', - originalValue: '0', - }, - { - category: 'auditd', - field: 'auditd.data.a2', - values: '80000', - originalValue: '80000', - }, - { - category: 'auditd', - field: 'auditd.data.syscall', - values: 'openat', - originalValue: 'openat', - }, - { - category: 'auditd', - field: 'auditd.data.a1', - values: '7fe0f63df220', - originalValue: '7fe0f63df220', - }, - { - category: 'auditd', - field: 'auditd.data.a0', - values: 'ffffff9c', - originalValue: 'ffffff9c', - }, - { - category: 'auditd', - field: 'auditd.data.arch', - values: 'x86_64', - originalValue: 'x86_64', - }, - { - category: 'auditd', - field: 'auditd.data.exit', - values: '12', - originalValue: '12', - }, - { - category: 'auditd', - field: 'auditd.summary.actor.primary', - values: 'unset', - originalValue: 'unset', - }, - { - category: 'auditd', - field: 'auditd.summary.actor.secondary', - values: 'root', - originalValue: 'root', - }, - { - category: 'auditd', - field: 'auditd.summary.object.primary', - values: '/etc/passwd', - originalValue: '/etc/passwd', - }, - { - category: 'auditd', - field: 'auditd.summary.object.type', - values: 'file', - originalValue: 'file', - }, - { - category: 'auditd', - field: 'auditd.summary.how', - values: '/root/go/src/github.com/elastic/beats/x-pack/auditbeat/auditbeat', - originalValue: '/root/go/src/github.com/elastic/beats/x-pack/auditbeat/auditbeat', - }, - { - category: 'auditd', - field: 'auditd.paths', - values: [ - { - rdev: '00:00', - cap_fe: '0', - nametype: 'NORMAL', - ogid: '0', - ouid: '0', - inode: '3926', - item: '0', - mode: '0100644', - name: '/etc/passwd', - cap_fi: '0000000000000000', - cap_fp: '0000000000000000', - cap_fver: '0', - dev: 'fc:01', - }, - ], - originalValue: [ - { - rdev: '00:00', - cap_fe: '0', - nametype: 'NORMAL', - ogid: '0', - ouid: '0', - inode: '3926', - item: '0', - mode: '0100644', - name: '/etc/passwd', - cap_fi: '0000000000000000', - cap_fp: '0000000000000000', - cap_fver: '0', - dev: 'fc:01', - }, - ], - }, - { - category: 'auditd', - field: 'auditd.message_type', - values: 'syscall', - originalValue: 'syscall', - }, - { - category: 'auditd', - field: 'auditd.sequence', - values: 8817905, - originalValue: 8817905, - }, - { - category: 'auditd', - field: 'auditd.result', - values: 'success', - originalValue: 'success', - }, - { - category: 'event', - field: 'event.category', - values: 'audit-rule', - originalValue: 'audit-rule', - }, - { - category: 'event', - field: 'event.action', - values: 'opened-file', - originalValue: 'opened-file', - }, - { - category: 'event', - field: 'event.original', - values: [ - 'type=SYSCALL msg=audit(1553886083.420:8817905): arch=c000003e syscall=257 success=yes exit=12 a0=ffffff9c a1=7fe0f63df220 a2=80000 a3=0 items=1 ppid=1 pid=15990 auid=4294967295 uid=0 gid=0 euid=0 suid=0 fsuid=0 egid=0 sgid=0 fsgid=0 tty=(none) ses=4294967295 comm="auditbeat" exe="/root/go/src/github.com/elastic/beats/x-pack/auditbeat/auditbeat" key=(null)', - 'type=CWD msg=audit(1553886083.420:8817905): cwd="/root/go/src/github.com/elastic/beats/x-pack/auditbeat"', - 'type=PATH msg=audit(1553886083.420:8817905): item=0 name="/etc/passwd" inode=3926 dev=fc:01 mode=0100644 ouid=0 ogid=0 rdev=00:00 nametype=NORMAL cap_fp=0000000000000000 cap_fi=0000000000000000 cap_fe=0 cap_fver=0', - 'type=PROCTITLE msg=audit(1553886083.420:8817905): proctitle=2F726F6F742F676F2F7372632F6769746875622E636F6D2F656C61737469632F62656174732F782D7061636B2F6175646974626561742F617564697462656174002D65002D63002F726F6F742F676F2F7372632F6769746875622E636F6D2F656C61737469632F62656174732F782D7061636B2F6175646974626561742F6175', - ], - originalValue: [ - 'type=SYSCALL msg=audit(1553886083.420:8817905): arch=c000003e syscall=257 success=yes exit=12 a0=ffffff9c a1=7fe0f63df220 a2=80000 a3=0 items=1 ppid=1 pid=15990 auid=4294967295 uid=0 gid=0 euid=0 suid=0 fsuid=0 egid=0 sgid=0 fsgid=0 tty=(none) ses=4294967295 comm="auditbeat" exe="/root/go/src/github.com/elastic/beats/x-pack/auditbeat/auditbeat" key=(null)', - 'type=CWD msg=audit(1553886083.420:8817905): cwd="/root/go/src/github.com/elastic/beats/x-pack/auditbeat"', - 'type=PATH msg=audit(1553886083.420:8817905): item=0 name="/etc/passwd" inode=3926 dev=fc:01 mode=0100644 ouid=0 ogid=0 rdev=00:00 nametype=NORMAL cap_fp=0000000000000000 cap_fi=0000000000000000 cap_fe=0 cap_fver=0', - 'type=PROCTITLE msg=audit(1553886083.420:8817905): proctitle=2F726F6F742F676F2F7372632F6769746875622E636F6D2F656C61737469632F62656174732F782D7061636B2F6175646974626561742F617564697462656174002D65002D63002F726F6F742F676F2F7372632F6769746875622E636F6D2F656C61737469632F62656174732F782D7061636B2F6175646974626561742F6175', - ], - }, - { - category: 'event', - field: 'event.module', - values: 'auditd', - originalValue: 'auditd', - }, - { - category: 'ecs', - field: 'ecs.version', - values: '1.0.0', - originalValue: '1.0.0', - }, - { - category: 'agent', - field: 'agent.ephemeral_id', - values: '6d541d59-52d0-4e70-b4d2-2660c0a99ff7', - originalValue: '6d541d59-52d0-4e70-b4d2-2660c0a99ff7', - }, - { - category: 'agent', - field: 'agent.hostname', - values: 'zeek-london', - originalValue: 'zeek-london', - }, - { - category: 'agent', - field: 'agent.id', - values: 'cc1f4183-36c6-45c4-b21b-7ce70c3572db', - originalValue: 'cc1f4183-36c6-45c4-b21b-7ce70c3572db', - }, - { - category: 'agent', - field: 'agent.version', - values: '8.0.0', - originalValue: '8.0.0', - }, - { - category: 'agent', - field: 'agent.type', - values: 'auditbeat', - originalValue: 'auditbeat', - }, - { - category: '_index', - field: '_index', - values: 'auditbeat-8.0.0-2019.03.29-000003', - originalValue: 'auditbeat-8.0.0-2019.03.29-000003', - }, - { - category: '_type', - field: '_type', - values: '_doc', - originalValue: '_doc', - }, - { - category: '_id', - field: '_id', - values: 'TUfUymkBCQofM5eXGBYL', - originalValue: 'TUfUymkBCQofM5eXGBYL', - }, - { - category: '_score', - field: '_score', - values: 1, - originalValue: 1, - }, - ], -}; diff --git a/x-pack/legacy/plugins/siem/server/lib/framework/types.ts b/x-pack/legacy/plugins/siem/server/lib/framework/types.ts deleted file mode 100644 index 7d049d1dcd195..0000000000000 --- a/x-pack/legacy/plugins/siem/server/lib/framework/types.ts +++ /dev/null @@ -1,135 +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 { IndicesGetMappingParams } from 'elasticsearch'; -import { GraphQLSchema } from 'graphql'; - -import { RequestHandlerContext, KibanaRequest } from '../../../../../../../src/core/server'; -import { AuthenticatedUser } from '../../../../../../plugins/security/common/model'; -import { ESQuery } from '../../../common/typed_json'; -import { - PaginationInput, - PaginationInputPaginated, - SortField, - SourceConfiguration, - TimerangeInput, - Maybe, - HistogramType, -} from '../../graphql/types'; - -export * from '../../utils/typed_resolvers'; - -export const internalFrameworkRequest = Symbol('internalFrameworkRequest'); - -export interface FrameworkAdapter { - registerGraphQLEndpoint(routePath: string, schema: GraphQLSchema): void; - callWithRequest<Hit = {}, Aggregation = undefined>( - req: FrameworkRequest, - method: 'search', - options?: object - ): Promise<DatabaseSearchResponse<Hit, Aggregation>>; - callWithRequest<Hit = {}, Aggregation = undefined>( - req: FrameworkRequest, - method: 'msearch', - options?: object - ): Promise<DatabaseMultiResponse<Hit, Aggregation>>; - callWithRequest( - req: FrameworkRequest, - method: 'indices.getMapping', - options?: IndicesGetMappingParams // eslint-disable-line - ): Promise<MappingResponse>; - getIndexPatternsService(req: FrameworkRequest): FrameworkIndexPatternsService; -} - -export interface FrameworkRequest extends Pick<KibanaRequest, 'body'> { - [internalFrameworkRequest]: KibanaRequest; - context: RequestHandlerContext; - user: AuthenticatedUser | null; -} - -export interface DatabaseResponse { - took: number; - timeout: boolean; -} - -export interface DatabaseSearchResponse<Hit = {}, Aggregations = undefined> - extends DatabaseResponse { - _shards: { - total: number; - successful: number; - skipped: number; - failed: number; - }; - aggregations?: Aggregations; - hits: { - total: number; - hits: Hit[]; - }; -} - -export interface DatabaseMultiResponse<Hit, Aggregation> extends DatabaseResponse { - responses: Array<DatabaseSearchResponse<Hit, Aggregation>>; -} - -export interface MappingProperties { - type: string; - path: string; - ignore_above: number; - properties: Readonly<Record<string, Partial<MappingProperties>>>; -} - -export interface MappingResponse { - [indexName: string]: { - mappings: { - _meta: { - beat: string; - version: string; - }; - dynamic_templates: object[]; - date_detection: boolean; - properties: Readonly<Record<string, Partial<MappingProperties>>>; - }; - }; -} - -interface FrameworkIndexFieldDescriptor { - aggregatable: boolean; - esTypes: string[]; - name: string; - readFromDocValues: boolean; - searchable: boolean; - type: string; -} - -export interface FrameworkIndexPatternsService { - getFieldsForWildcard(options: { - pattern: string | string[]; - }): Promise<FrameworkIndexFieldDescriptor[]>; -} - -export interface RequestBasicOptions { - sourceConfiguration: SourceConfiguration; - timerange: TimerangeInput; - filterQuery: ESQuery | undefined; - defaultIndex: string[]; -} - -export interface MatrixHistogramRequestOptions extends RequestBasicOptions { - stackByField: Maybe<string>; - histogramType: HistogramType; -} - -export interface RequestOptions extends RequestBasicOptions { - pagination: PaginationInput; - fields: readonly string[]; - sortField?: SortField; -} - -export interface RequestOptionsPaginated extends RequestBasicOptions { - pagination: PaginationInputPaginated; - fields: readonly string[]; - sortField?: SortField; -} diff --git a/x-pack/legacy/plugins/siem/server/lib/hosts/mock.ts b/x-pack/legacy/plugins/siem/server/lib/hosts/mock.ts deleted file mode 100644 index 6b72c4a5a2843..0000000000000 --- a/x-pack/legacy/plugins/siem/server/lib/hosts/mock.ts +++ /dev/null @@ -1,567 +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 { Direction, HostsFields } from '../../graphql/types'; -import { defaultIndexPattern } from '../../../default_index_pattern'; - -import { - HostOverviewRequestOptions, - HostLastFirstSeenRequestOptions, - HostsRequestOptions, -} from '.'; - -export const mockGetHostsOptions: HostsRequestOptions = { - defaultIndex: defaultIndexPattern, - sourceConfiguration: { - fields: { - container: 'docker.container.name', - host: 'beat.hostname', - message: ['message', '@message'], - pod: 'kubernetes.pod.name', - tiebreaker: '_doc', - timestamp: '@timestamp', - }, - }, - timerange: { interval: '12h', to: 1554824274610, from: 1554737874610 }, - sort: { field: HostsFields.lastSeen, direction: Direction.asc }, - pagination: { - activePage: 0, - cursorStart: 0, - fakePossibleCount: 10, - querySize: 2, - }, - filterQuery: {}, - fields: [ - 'totalCount', - '_id', - 'host.id', - 'host.name', - 'host.os.name', - 'host.os.version', - 'edges.cursor.value', - 'pageInfo.activePage', - 'pageInfo.fakeTotalCount', - 'pageInfo.showMorePagesIndicator', - ], -}; - -export const mockGetHostsRequest = { - body: { - operationName: 'GetHostsTableQuery', - variables: { - sourceId: 'default', - timerange: { interval: '12h', from: 1554737729201, to: 1554824129202 }, - pagination: { - activePage: 0, - cursorStart: 0, - fakePossibleCount: 10, - querySize: 2, - }, - sort: { field: HostsFields.lastSeen, direction: Direction.asc }, - filterQuery: '', - }, - query: - 'query GetHostsTableQuery($sourceId: ID!, $timerange: TimerangeInput!, $pagination: PaginationInput!, $sort: HostsSortField!, $filterQuery: String) {\n source(id: $sourceId) {\n id\n Hosts(timerange: $timerange, pagination: $pagination, sort: $sort, filterQuery: $filterQuery) {\n totalCount\n edges {\n node {\n _id\n host {\n id\n name\n os {\n name\n version\n __typename\n }\n __typename\n }\n __typename\n }\n cursor {\n value\n __typename\n }\n __typename\n }\n pageInfo {\n endCursor {\n value\n __typename\n }\n hasNextPage\n __typename\n }\n __typename\n }\n __typename\n }\n}\n', - }, -}; - -export const mockGetHostsResponse = { - took: 1695, - timed_out: false, - _shards: { - total: 59, - successful: 59, - skipped: 0, - failed: 0, - }, - hits: { - total: { - value: 4018586, - relation: 'eq', - }, - max_score: null, - hits: [], - }, - aggregations: { - host_data: { - doc_count_error_upper_bound: -1, - sum_other_doc_count: 3082125, - buckets: [ - { - key: 'beats-ci-immutable-centos-7-1554823376629262884', - doc_count: 991, - lastSeen: { - value: 1554823916544, - value_as_string: '2019-04-09T15:31:56.544Z', - }, - host_os_version: { - doc_count_error_upper_bound: 0, - sum_other_doc_count: 0, - buckets: [ - { - key: '7 (Core)', - doc_count: 991, - timestamp: { - value: 1554823916544, - value_as_string: '2019-04-09T15:31:56.544Z', - }, - }, - ], - }, - firstSeen: { - value: 1554823396740, - value_as_string: '2019-04-09T15:23:16.740Z', - }, - host_os_name: { - doc_count_error_upper_bound: 0, - sum_other_doc_count: 0, - buckets: [ - { - key: 'CentOS Linux', - doc_count: 991, - timestamp: { - value: 1554823916544, - value_as_string: '2019-04-09T15:31:56.544Z', - }, - }, - ], - }, - host_name: { - doc_count_error_upper_bound: 0, - sum_other_doc_count: 0, - buckets: [ - { - key: 'beats-ci-immutable-centos-7-1554823376629262884', - doc_count: 991, - timestamp: { - value: 1554823916544, - value_as_string: '2019-04-09T15:31:56.544Z', - }, - }, - ], - }, - host_id: { - doc_count_error_upper_bound: 0, - sum_other_doc_count: 0, - buckets: [ - { - key: 'f85edea1973c34f862c376cac4ebc777', - doc_count: 991, - timestamp: { - value: 1554823916544, - value_as_string: '2019-04-09T15:31:56.544Z', - }, - }, - ], - }, - }, - { - key: 'beats-ci-immutable-centos-7-1554823376629299914', - doc_count: 571, - lastSeen: { - value: 1554823916302, - value_as_string: '2019-04-09T15:31:56.302Z', - }, - host_os_version: { - doc_count_error_upper_bound: 0, - sum_other_doc_count: 0, - buckets: [ - { - key: '7 (Core)', - doc_count: 571, - timestamp: { - value: 1554823916302, - value_as_string: '2019-04-09T15:31:56.302Z', - }, - }, - ], - }, - firstSeen: { - value: 1554823398628, - value_as_string: '2019-04-09T15:23:18.628Z', - }, - host_os_name: { - doc_count_error_upper_bound: 0, - sum_other_doc_count: 0, - buckets: [ - { - key: 'CentOS Linux', - doc_count: 571, - timestamp: { - value: 1554823916302, - value_as_string: '2019-04-09T15:31:56.302Z', - }, - }, - ], - }, - host_name: { - doc_count_error_upper_bound: 0, - sum_other_doc_count: 0, - buckets: [ - { - key: 'beats-ci-immutable-centos-7-1554823376629299914', - doc_count: 571, - timestamp: { - value: 1554823916302, - value_as_string: '2019-04-09T15:31:56.302Z', - }, - }, - ], - }, - host_id: { - doc_count_error_upper_bound: 0, - sum_other_doc_count: 0, - buckets: [ - { - key: 'f85edea1973c34f862c376cac4ebc777', - doc_count: 571, - timestamp: { - value: 1554823916302, - value_as_string: '2019-04-09T15:31:56.302Z', - }, - }, - ], - }, - }, - ], - }, - host_count: { - value: 1627, - }, - }, -}; - -export const mockGetHostsQueryDsl = { mockGetHostsQueryDsl: 'mockGetHostsQueryDsl' }; - -export const mockGetHostsResult = { - inspect: { - dsl: [JSON.stringify(mockGetHostsQueryDsl, null, 2)], - response: [JSON.stringify(mockGetHostsResponse, null, 2)], - }, - edges: [ - { - node: { - _id: 'beats-ci-immutable-centos-7-1554823376629262884', - host: { - id: 'f85edea1973c34f862c376cac4ebc777', - name: 'beats-ci-immutable-centos-7-1554823376629262884', - os: { - name: 'CentOS Linux', - version: '7 (Core)', - }, - }, - }, - cursor: { - value: 'beats-ci-immutable-centos-7-1554823376629262884', - tiebreaker: null, - }, - }, - { - node: { - _id: 'beats-ci-immutable-centos-7-1554823376629299914', - host: { - id: 'f85edea1973c34f862c376cac4ebc777', - name: 'beats-ci-immutable-centos-7-1554823376629299914', - os: { - name: 'CentOS Linux', - version: '7 (Core)', - }, - }, - }, - cursor: { - value: 'beats-ci-immutable-centos-7-1554823376629299914', - tiebreaker: null, - }, - }, - ], - totalCount: 1627, - pageInfo: { - activePage: 0, - fakeTotalCount: 10, - showMorePagesIndicator: true, - }, -}; - -export const mockGetHostOverviewOptions: HostOverviewRequestOptions = { - sourceConfiguration: { - fields: { - container: 'docker.container.name', - host: 'beat.hostname', - message: ['message', '@message'], - pod: 'kubernetes.pod.name', - tiebreaker: '_doc', - timestamp: '@timestamp', - }, - }, - timerange: { interval: '12h', to: 1554824274610, from: 1554737874610 }, - defaultIndex: defaultIndexPattern, - fields: [ - '_id', - 'host.architecture', - 'host.id', - 'host.ip', - 'host.mac', - 'host.name', - 'host.os.family', - 'host.os.name', - 'host.os.platform', - 'host.os.version', - 'host.os.__typename', - 'host.type', - 'host.__typename', - 'cloud.instance.id', - 'cloud.instance.__typename', - 'cloud.machine.type', - 'cloud.machine.__typename', - 'cloud.provider', - 'cloud.region', - 'cloud.__typename', - '__typename', - ], - hostName: 'siem-es', -}; - -export const mockGetHostOverviewRequest = { - body: { - operationName: 'GetHostOverviewQuery', - variables: { sourceId: 'default', hostName: 'siem-es' }, - query: - 'query GetHostOverviewQuery($sourceId: ID!, $hostName: String!, $timerange: TimerangeInput!) {\n source(id: $sourceId) {\n id\n HostOverview(hostName: $hostName, timerange: $timerange) {\n _id\n host {\n architecture\n id\n ip\n mac\n name\n os {\n family\n name\n platform\n version\n __typename\n }\n type\n __typename\n }\n cloud {\n instance {\n id\n __typename\n }\n machine {\n type\n __typename\n }\n provider\n region\n __typename\n }\n __typename\n }\n __typename\n }\n}\n', - }, -}; - -export const mockGetHostOverviewResponse = { - took: 2205, - timed_out: false, - _shards: { total: 59, successful: 59, skipped: 0, failed: 0 }, - hits: { total: { value: 611894, relation: 'eq' }, max_score: null, hits: [] }, - aggregations: { - host_mac: { doc_count_error_upper_bound: 0, sum_other_doc_count: 0, buckets: [] }, - host_ip: { doc_count_error_upper_bound: 0, sum_other_doc_count: 0, buckets: [] }, - cloud_region: { - doc_count_error_upper_bound: 0, - sum_other_doc_count: 0, - buckets: [ - { - key: 'us-east-1', - doc_count: 4308, - timestamp: { value: 1556903543093, value_as_string: '2019-05-03T17:12:23.093Z' }, - }, - ], - }, - cloud_provider: { - doc_count_error_upper_bound: 0, - sum_other_doc_count: 0, - buckets: [ - { - key: 'gce', - doc_count: 432808, - timestamp: { value: 1556903543093, value_as_string: '2019-05-03T17:12:23.093Z' }, - }, - ], - }, - cloud_instance_id: { - doc_count_error_upper_bound: 0, - sum_other_doc_count: 0, - buckets: [ - { - key: '5412578377715150143', - doc_count: 432808, - timestamp: { value: 1556903543093, value_as_string: '2019-05-03T17:12:23.093Z' }, - }, - ], - }, - cloud_machine_type: { - doc_count_error_upper_bound: 0, - sum_other_doc_count: 0, - buckets: [ - { - key: 'n1-standard-1', - doc_count: 432808, - timestamp: { value: 1556903543093, value_as_string: '2019-05-03T17:12:23.093Z' }, - }, - ], - }, - host_os_version: { - doc_count_error_upper_bound: 0, - sum_other_doc_count: 0, - buckets: [ - { - key: '9 (stretch)', - doc_count: 611894, - timestamp: { value: 1554826117972, value_as_string: '2019-04-09T16:08:37.972Z' }, - }, - ], - }, - host_architecture: { - doc_count_error_upper_bound: 0, - sum_other_doc_count: 0, - buckets: [ - { - key: 'x86_64', - doc_count: 611894, - timestamp: { value: 1554826117972, value_as_string: '2019-04-09T16:08:37.972Z' }, - }, - ], - }, - host_os_platform: { - doc_count_error_upper_bound: 0, - sum_other_doc_count: 0, - buckets: [ - { - key: 'debian', - doc_count: 611894, - timestamp: { value: 1554826117972, value_as_string: '2019-04-09T16:08:37.972Z' }, - }, - ], - }, - host_os_name: { - doc_count_error_upper_bound: 0, - sum_other_doc_count: 0, - buckets: [ - { - key: 'Debian GNU/Linux', - doc_count: 611894, - timestamp: { value: 1554826117972, value_as_string: '2019-04-09T16:08:37.972Z' }, - }, - ], - }, - host_os_family: { - doc_count_error_upper_bound: 0, - sum_other_doc_count: 0, - buckets: [ - { - key: 'debian', - doc_count: 611894, - timestamp: { value: 1554826117972, value_as_string: '2019-04-09T16:08:37.972Z' }, - }, - ], - }, - host_name: { - doc_count_error_upper_bound: 0, - sum_other_doc_count: 0, - buckets: [ - { - key: 'siem-es', - doc_count: 611894, - timestamp: { value: 1554826117972, value_as_string: '2019-04-09T16:08:37.972Z' }, - }, - ], - }, - host_id: { - doc_count_error_upper_bound: 0, - sum_other_doc_count: 0, - buckets: [ - { - key: 'b6d5264e4b9c8880ad1053841067a4a6', - doc_count: 611894, - timestamp: { value: 1554826117972, value_as_string: '2019-04-09T16:08:37.972Z' }, - }, - ], - }, - }, -}; - -export const mockGetHostOverviewRequestDsl = { - mockGetHostOverviewRequestDsl: 'mockGetHostOverviewRequestDsl', -}; - -export const mockGetHostOverviewResult = { - inspect: { - dsl: [JSON.stringify(mockGetHostOverviewRequestDsl, null, 2)], - response: [JSON.stringify(mockGetHostOverviewResponse, null, 2)], - }, - _id: 'siem-es', - host: { - architecture: 'x86_64', - id: 'b6d5264e4b9c8880ad1053841067a4a6', - ip: [], - mac: [], - name: 'siem-es', - os: { - family: 'debian', - name: 'Debian GNU/Linux', - platform: 'debian', - version: '9 (stretch)', - }, - }, - cloud: { - instance: { - id: ['5412578377715150143'], - }, - machine: { - type: ['n1-standard-1'], - }, - provider: ['gce'], - region: ['us-east-1'], - }, -}; - -export const mockGetHostLastFirstSeenOptions: HostLastFirstSeenRequestOptions = { - defaultIndex: defaultIndexPattern, - sourceConfiguration: { - fields: { - container: 'docker.container.name', - host: 'beat.hostname', - message: ['message', '@message'], - pod: 'kubernetes.pod.name', - tiebreaker: '_doc', - timestamp: '@timestamp', - }, - }, - hostName: 'siem-es', -}; - -export const mockGetHostLastFirstSeenRequest = { - body: { - operationName: 'GetHostLastFirstSeenQuery', - variables: { sourceId: 'default', hostName: 'siem-es' }, - query: - 'query GetHostLastFirstSeenQuery($sourceId: ID!, $hostName: String!) {\n source(id: $sourceId) {\n id\n HostLastFirstSeen(hostName: $hostName) {\n firstSeen\n lastSeen\n __typename\n }\n __typename\n }\n}\n', - }, -}; - -export const mockGetHostLastFirstSeenResponse = { - took: 60, - timed_out: false, - _shards: { - total: 59, - successful: 59, - skipped: 0, - failed: 0, - }, - hits: { - total: { - value: 612092, - relation: 'eq', - }, - max_score: null, - hits: [], - }, - aggregations: { - lastSeen: { - value: 1554826692178, - value_as_string: '2019-04-09T16:18:12.178Z', - }, - firstSeen: { - value: 1550806892826, - value_as_string: '2019-02-22T03:41:32.826Z', - }, - }, -}; - -export const mockGetHostLastFirstSeenDsl = { - mockGetHostLastFirstSeenDsl: 'mockGetHostLastFirstSeenDsl', -}; - -export const mockGetHostLastFirstSeenResult = { - inspect: { - dsl: [JSON.stringify(mockGetHostLastFirstSeenDsl, null, 2)], - response: [JSON.stringify(mockGetHostLastFirstSeenResponse, null, 2)], - }, - firstSeen: '2019-02-22T03:41:32.826Z', - lastSeen: '2019-04-09T16:18:12.178Z', -}; diff --git a/x-pack/legacy/plugins/siem/server/lib/kpi_hosts/mock.ts b/x-pack/legacy/plugins/siem/server/lib/kpi_hosts/mock.ts deleted file mode 100644 index ed9fbf0ba0646..0000000000000 --- a/x-pack/legacy/plugins/siem/server/lib/kpi_hosts/mock.ts +++ /dev/null @@ -1,606 +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 { defaultIndexPattern } from '../../../default_index_pattern'; -import { RequestBasicOptions } from '../framework/types'; - -const FROM = new Date('2019-05-03T13:24:00.660Z').valueOf(); -const TO = new Date('2019-05-04T13:24:00.660Z').valueOf(); - -export const mockKpiHostsOptions: RequestBasicOptions = { - defaultIndex: defaultIndexPattern, - sourceConfiguration: { - fields: { - container: 'docker.container.name', - host: 'beat.hostname', - message: ['message', '@message'], - pod: 'kubernetes.pod.name', - tiebreaker: '_doc', - timestamp: '@timestamp', - }, - }, - timerange: { interval: '12h', to: TO, from: FROM }, - filterQuery: undefined, -}; - -export const mockKpiHostDetailsOptions: RequestBasicOptions = { - defaultIndex: defaultIndexPattern, - sourceConfiguration: { - fields: { - container: 'docker.container.name', - host: 'beat.hostname', - message: ['message', '@message'], - pod: 'kubernetes.pod.name', - tiebreaker: '_doc', - timestamp: '@timestamp', - }, - }, - timerange: { interval: '12h', to: TO, from: FROM }, - filterQuery: { term: { 'host.name': 'beats-ci-immutable-ubuntu-1604-1560970771368235343' } }, -}; - -export const mockKpiHostsRequest = { - body: { - operationName: 'GetKpiHostsQuery', - variables: { - sourceId: 'default', - timerange: { interval: '12h', from: FROM, to: TO }, - filterQuery: '', - }, - query: - 'fragment KpiHostChartFields on KpiHostHistogramData {\n x\n y\n __typename\n}\n\nquery GetKpiHostsQuery($sourceId: ID!, $timerange: TimerangeInput!, $filterQuery: String, $defaultIndex: [String!]!) {\n source(id: $sourceId) {\n id\n KpiHosts(timerange: $timerange, filterQuery: $filterQuery, defaultIndex: $defaultIndex) {\n hosts\n hostsHistogram {\n ...KpiHostChartFields\n __typename\n }\n authSuccess\n authSuccessHistogram {\n ...KpiHostChartFields\n __typename\n }\n authFailure\n authFailureHistogram {\n ...KpiHostChartFields\n __typename\n }\n uniqueSourceIps\n uniqueSourceIpsHistogram {\n ...KpiHostChartFields\n __typename\n }\n uniqueDestinationIps\n uniqueDestinationIpsHistogram {\n ...KpiHostChartFields\n __typename\n }\n __typename\n }\n __typename\n }\n}\n', - }, -}; - -export const mockKpiHostDetailsRequest = { - body: { - operationName: 'GetKpiHostDetailsQuery', - variables: { - sourceId: 'default', - timerange: { interval: '12h', from: FROM, to: TO }, - filterQuery: { term: { 'host.name': 'beats-ci-immutable-ubuntu-1604-1560970771368235343' } }, - }, - query: - 'fragment KpiHostDetailsChartFields on KpiHostHistogramData {\n x\n y\n __typename\n}\n\nquery GetKpiHostDetailsQuery($sourceId: ID!, $timerange: TimerangeInput!, $filterQuery: String, $defaultIndex: [String!]!, $hostName: String!) {\n source(id: $sourceId) {\n id\n KpiHostDetails(timerange: $timerange, filterQuery: $filterQuery, defaultIndex: $defaultIndex, hostName: $hostName) {\n authSuccess\n authSuccessHistogram {\n ...KpiHostDetailsChartFields\n __typename\n }\n authFailure\n authFailureHistogram {\n ...KpiHostDetailsChartFields\n __typename\n }\n uniqueSourceIps\n uniqueSourceIpsHistogram {\n ...KpiHostDetailsChartFields\n __typename\n }\n uniqueDestinationIps\n uniqueDestinationIpsHistogram {\n ...KpiHostDetailsChartFields\n __typename\n }\n __typename\n }\n __typename\n }\n}\n', - }, -}; - -const mockUniqueIpsResponse = { - took: 1234, - timed_out: false, - _shards: { - total: 71, - successful: 71, - skipped: 65, - failed: 0, - }, - hits: { - max_score: null, - hits: [], - }, - aggregations: { - unique_destination_ips: { - value: 1954, - }, - unique_destination_ips_histogram: { - buckets: [ - { - key_as_string: '2019-05-03T13:00:00.000Z', - key: 1556888400000, - doc_count: 3158515, - count: { - value: 1809, - }, - }, - { - key_as_string: '2019-05-04T01:00:00.000Z', - key: 1556931600000, - doc_count: 703032, - count: { - value: 407, - }, - }, - { - key_as_string: '2019-05-04T13:00:00.000Z', - key: 1556974800000, - doc_count: 1780, - count: { - value: 64, - }, - }, - ], - interval: '12h', - }, - unique_source_ips: { - value: 1407, - }, - unique_source_ips_histogram: { - buckets: [ - { - key_as_string: '2019-05-03T13:00:00.000Z', - key: 1556888400000, - doc_count: 3158515, - count: { - value: 1182, - }, - }, - { - key_as_string: '2019-05-04T01:00:00.000Z', - key: 1556931600000, - doc_count: 703032, - count: { - value: 364, - }, - }, - { - key_as_string: '2019-05-04T13:00:00.000Z', - key: 1556974800000, - doc_count: 1780, - count: { - value: 63, - }, - }, - ], - interval: '12h', - }, - }, - status: 200, -}; - -const mockAuthResponse = { - took: 320, - timed_out: false, - _shards: { - total: 71, - successful: 71, - skipped: 65, - failed: 0, - }, - hits: { - max_score: null, - hits: [], - }, - aggregations: { - authentication_success: { - doc_count: 61, - }, - authentication_failure: { - doc_count: 15722, - }, - authentication_failure_histogram: { - buckets: [ - { - key_as_string: '2019-05-03T13:00:00.000Z', - key: 1556888400000, - doc_count: 11739, - count: { - doc_count: 11731, - }, - }, - { - key_as_string: '2019-05-04T01:00:00.000Z', - key: 1556931600000, - doc_count: 4031, - count: { - doc_count: 3979, - }, - }, - { - key_as_string: '2019-05-04T13:00:00.000Z', - key: 1556974800000, - doc_count: 13, - count: { - doc_count: 12, - }, - }, - ], - interval: '12h', - }, - authentication_success_histogram: { - buckets: [ - { - key_as_string: '2019-05-03T13:00:00.000Z', - key: 1556888400000, - doc_count: 11739, - count: { - doc_count: 8, - }, - }, - { - key_as_string: '2019-05-04T01:00:00.000Z', - key: 1556931600000, - doc_count: 4031, - count: { - doc_count: 52, - }, - }, - { - key_as_string: '2019-05-04T13:00:00.000Z', - key: 1556974800000, - doc_count: 13, - count: { - doc_count: 1, - }, - }, - ], - interval: '12h', - }, - }, - status: 200, -}; - -const mockHostsReponse = { - took: 1234, - timed_out: false, - _shards: { - total: 71, - successful: 71, - skipped: 65, - failed: 0, - }, - hits: { - max_score: null, - hits: [], - }, - aggregations: { - hosts: { - value: 986, - }, - hosts_histogram: { - buckets: [ - { - key_as_string: '2019-05-03T13:00:00.000Z', - key: 1556888400000, - doc_count: 3158515, - count: { - value: 919, - }, - }, - { - key_as_string: '2019-05-04T01:00:00.000Z', - key: 1556931600000, - doc_count: 703032, - count: { - value: 82, - }, - }, - { - key_as_string: '2019-05-04T13:00:00.000Z', - key: 1556974800000, - doc_count: 1780, - count: { - value: 4, - }, - }, - ], - interval: '12h', - }, - }, - status: 200, -}; - -export const mockKpiHostsResponse = { - took: 4405, - responses: [mockHostsReponse, mockAuthResponse, mockUniqueIpsResponse], -}; - -export const mockKpiHostsResponseNodata = { responses: [null, null, null] }; - -const mockMsearchHeader = { - index: defaultIndexPattern, - allowNoIndices: true, - ignoreUnavailable: true, -}; - -const mockHostNameFilter = { - term: { 'host.name': 'beats-ci-immutable-ubuntu-1604-1560970771368235343' }, -}; -const mockTimerangeFilter = { range: { '@timestamp': { gte: FROM, lte: TO } } }; - -export const mockHostsQuery = [ - mockMsearchHeader, - { - aggregations: { - hosts: { cardinality: { field: 'host.name' } }, - hosts_histogram: { - auto_date_histogram: { field: '@timestamp', buckets: '6' }, - aggs: { count: { cardinality: { field: 'host.name' } } }, - }, - }, - query: { - bool: { filter: [{ range: { '@timestamp': mockTimerangeFilter } }] }, - }, - size: 0, - track_total_hits: false, - }, -]; - -const mockUniqueIpsAggs = { - unique_source_ips: { cardinality: { field: 'source.ip' } }, - unique_source_ips_histogram: { - auto_date_histogram: { field: '@timestamp', buckets: '6' }, - aggs: { count: { cardinality: { field: 'source.ip' } } }, - }, - unique_destination_ips: { cardinality: { field: 'destination.ip' } }, - unique_destination_ips_histogram: { - auto_date_histogram: { field: '@timestamp', buckets: '6' }, - aggs: { count: { cardinality: { field: 'destination.ip' } } }, - }, -}; - -export const mockKpiHostsUniqueIpsQuery = [ - mockMsearchHeader, - { - aggregations: mockUniqueIpsAggs, - query: { - bool: { filter: [mockTimerangeFilter] }, - }, - size: 0, - track_total_hits: false, - }, -]; - -export const mockKpiHostDetailsUniqueIpsQuery = [ - mockMsearchHeader, - { - aggregations: mockUniqueIpsAggs, - query: { - bool: { filter: [mockHostNameFilter, mockTimerangeFilter] }, - }, - size: 0, - track_total_hits: false, - }, -]; - -const mockAuthAggs = { - authentication_success: { filter: { term: { 'event.outcome': 'success' } } }, - authentication_success_histogram: { - auto_date_histogram: { field: '@timestamp', buckets: '6' }, - aggs: { count: { filter: { term: { 'event.outcome': 'success' } } } }, - }, - authentication_failure: { filter: { term: { 'event.outcome': 'failure' } } }, - authentication_failure_histogram: { - auto_date_histogram: { field: '@timestamp', buckets: '6' }, - aggs: { count: { filter: { term: { 'event.outcome': 'failure' } } } }, - }, -}; - -const mockAuthFilter = { - bool: { - filter: [ - { - term: { - 'event.category': 'authentication', - }, - }, - ], - }, -}; - -export const mockKpiHostsAuthQuery = [ - mockMsearchHeader, - { - aggs: mockAuthAggs, - query: { - bool: { - filter: [mockAuthFilter, mockTimerangeFilter], - }, - }, - size: 0, - track_total_hits: false, - }, -]; - -export const mockKpiHostDetailsAuthQuery = [ - mockMsearchHeader, - { - aggs: mockAuthAggs, - query: { - bool: { - filter: [mockHostNameFilter, mockAuthFilter, mockTimerangeFilter], - }, - }, - size: 0, - track_total_hits: false, - }, -]; - -export const mockKpiHostsMsearchOptions = { - body: [...mockHostsQuery, ...mockKpiHostsAuthQuery, ...mockKpiHostsUniqueIpsQuery], -}; - -export const mockKpiHostDetailsMsearchOptions = { - body: [...mockKpiHostDetailsAuthQuery, ...mockKpiHostDetailsUniqueIpsQuery], -}; - -export const mockKpiHostsQueryDsl = [ - JSON.stringify({ ...mockHostsQuery[0], body: mockHostsQuery[1] }, null, 2), - JSON.stringify({ ...mockKpiHostsAuthQuery[0], body: mockKpiHostsAuthQuery[1] }, null, 2), - JSON.stringify( - { ...mockKpiHostsUniqueIpsQuery[0], body: mockKpiHostsUniqueIpsQuery[1] }, - null, - 2 - ), -]; - -export const mockKpiHostsResult = { - inspect: { - dsl: mockKpiHostsQueryDsl, - response: [ - JSON.stringify(mockKpiHostsResponse.responses[0], null, 2), - JSON.stringify(mockKpiHostsResponse.responses[1], null, 2), - JSON.stringify(mockKpiHostsResponse.responses[2], null, 2), - ], - }, - hosts: 986, - hostsHistogram: [ - { - x: new Date('2019-05-03T13:00:00.000Z').valueOf(), - y: 919, - }, - { - x: new Date('2019-05-04T01:00:00.000Z').valueOf(), - y: 82, - }, - { - x: new Date('2019-05-04T13:00:00.000Z').valueOf(), - y: 4, - }, - ], - authSuccess: 61, - authSuccessHistogram: [ - { - x: new Date('2019-05-03T13:00:00.000Z').valueOf(), - y: 8, - }, - { - x: new Date('2019-05-04T01:00:00.000Z').valueOf(), - y: 52, - }, - { - x: new Date('2019-05-04T13:00:00.000Z').valueOf(), - y: 1, - }, - ], - authFailure: 15722, - authFailureHistogram: [ - { - x: new Date('2019-05-03T13:00:00.000Z').valueOf(), - y: 11731, - }, - { - x: new Date('2019-05-04T01:00:00.000Z').valueOf(), - y: 3979, - }, - { - x: new Date('2019-05-04T13:00:00.000Z').valueOf(), - y: 12, - }, - ], - uniqueSourceIps: 1407, - uniqueSourceIpsHistogram: [ - { - x: new Date('2019-05-03T13:00:00.000Z').valueOf(), - y: 1182, - }, - { - x: new Date('2019-05-04T01:00:00.000Z').valueOf(), - y: 364, - }, - { - x: new Date('2019-05-04T13:00:00.000Z').valueOf(), - y: 63, - }, - ], - uniqueDestinationIps: 1954, - uniqueDestinationIpsHistogram: [ - { - x: new Date('2019-05-03T13:00:00.000Z').valueOf(), - y: 1809, - }, - { - x: new Date('2019-05-04T01:00:00.000Z').valueOf(), - y: 407, - }, - { - x: new Date('2019-05-04T13:00:00.000Z').valueOf(), - y: 64, - }, - ], -}; - -export const mockKpiHostDetailsResponse = { - took: 4405, - responses: [mockAuthResponse, mockUniqueIpsResponse], -}; - -export const mockKpiHostDetailsResponseNoData = { - took: 4405, - responses: [null, null], -}; - -export const mockKpiHostDetailsDsl = [ - JSON.stringify( - { ...mockKpiHostDetailsAuthQuery[0], body: mockKpiHostDetailsAuthQuery[1] }, - null, - 2 - ), - JSON.stringify( - { ...mockKpiHostDetailsUniqueIpsQuery[0], body: mockKpiHostDetailsUniqueIpsQuery[1] }, - null, - 2 - ), -]; - -export const mockKpiHostDetailsResult = { - inspect: { - dsl: mockKpiHostDetailsDsl, - response: [ - JSON.stringify(mockKpiHostDetailsResponse.responses[0], null, 2), - JSON.stringify(mockKpiHostDetailsResponse.responses[1], null, 2), - ], - }, - authSuccess: 61, - authSuccessHistogram: [ - { - x: new Date('2019-05-03T13:00:00.000Z').valueOf(), - y: 8, - }, - { - x: new Date('2019-05-04T01:00:00.000Z').valueOf(), - y: 52, - }, - { - x: new Date('2019-05-04T13:00:00.000Z').valueOf(), - y: 1, - }, - ], - authFailure: 15722, - authFailureHistogram: [ - { - x: new Date('2019-05-03T13:00:00.000Z').valueOf(), - y: 11731, - }, - { - x: new Date('2019-05-04T01:00:00.000Z').valueOf(), - y: 3979, - }, - { - x: new Date('2019-05-04T13:00:00.000Z').valueOf(), - y: 12, - }, - ], - uniqueSourceIps: 1407, - uniqueSourceIpsHistogram: [ - { - x: new Date('2019-05-03T13:00:00.000Z').valueOf(), - y: 1182, - }, - { - x: new Date('2019-05-04T01:00:00.000Z').valueOf(), - y: 364, - }, - { - x: new Date('2019-05-04T13:00:00.000Z').valueOf(), - y: 63, - }, - ], - uniqueDestinationIps: 1954, - uniqueDestinationIpsHistogram: [ - { - x: new Date('2019-05-03T13:00:00.000Z').valueOf(), - y: 1809, - }, - { - x: new Date('2019-05-04T01:00:00.000Z').valueOf(), - y: 407, - }, - { - x: new Date('2019-05-04T13:00:00.000Z').valueOf(), - y: 64, - }, - ], -}; diff --git a/x-pack/legacy/plugins/siem/server/lib/kpi_network/mock.ts b/x-pack/legacy/plugins/siem/server/lib/kpi_network/mock.ts deleted file mode 100644 index 7d86769de09f1..0000000000000 --- a/x-pack/legacy/plugins/siem/server/lib/kpi_network/mock.ts +++ /dev/null @@ -1,331 +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 { defaultIndexPattern } from '../../../default_index_pattern'; -import { RequestBasicOptions } from '../framework/types'; - -export const mockOptions: RequestBasicOptions = { - defaultIndex: defaultIndexPattern, - sourceConfiguration: { - fields: { - container: 'docker.container.name', - host: 'beat.hostname', - message: ['message', '@message'], - pod: 'kubernetes.pod.name', - tiebreaker: '_doc', - timestamp: '@timestamp', - }, - }, - timerange: { interval: '12h', to: 1549852006071, from: 1549765606071 }, - filterQuery: {}, -}; - -export const mockRequest = { - body: { - operationName: 'GetKpiNetworkQuery', - variables: { - sourceId: 'default', - timerange: { interval: '12h', from: 1557445721842, to: 1557532121842 }, - filterQuery: '', - }, - query: - 'fragment KpiNetworkChartFields on KpiNetworkHistogramData {\n x\n y\n __typename\n}\n\nquery GetKpiNetworkQuery($sourceId: ID!, $timerange: TimerangeInput!, $filterQuery: String, $defaultIndex: [String!]!) {\n source(id: $sourceId) {\n id\n KpiNetwork(timerange: $timerange, filterQuery: $filterQuery, defaultIndex: $defaultIndex) {\n networkEvents\n uniqueFlowId\n uniqueSourcePrivateIps\n uniqueSourcePrivateIpsHistogram {\n ...KpiNetworkChartFields\n __typename\n }\n uniqueDestinationPrivateIps\n uniqueDestinationPrivateIpsHistogram {\n ...KpiNetworkChartFields\n __typename\n }\n dnsQueries\n tlsHandshakes\n __typename\n }\n __typename\n }\n}\n', - }, -}; - -export const mockResponse = { - responses: [ - { - took: 384, - timed_out: false, - _shards: { - total: 10, - successful: 10, - skipped: 0, - failed: 0, - }, - hits: { - total: { - value: 733106, - relation: 'eq', - }, - max_score: null, - hits: [], - }, - status: 200, - }, - { - took: 64, - timed_out: false, - _shards: { - total: 10, - successful: 10, - skipped: 0, - failed: 0, - }, - hits: { - total: { - value: 10942, - relation: 'eq', - }, - max_score: null, - hits: [], - }, - status: 200, - }, - { - took: 224, - timed_out: false, - _shards: { - total: 10, - successful: 10, - skipped: 0, - failed: 0, - }, - hits: { - total: { - value: 480755, - relation: 'eq', - }, - max_score: null, - hits: [], - }, - aggregations: { - source: { - histogram: { - buckets: [ - { - key_as_string: '2019-05-09T23:00:00.000Z', - key: 1557442800000, - doc_count: 42109, - count: { - value: 14, - }, - }, - { - key_as_string: '2019-05-10T11:00:00.000Z', - key: 1557486000000, - doc_count: 437160, - count: { - value: 385, - }, - }, - { - key_as_string: '2019-05-10T23:00:00.000Z', - key: 1557529200000, - doc_count: 1486, - count: { - value: 7, - }, - }, - ], - interval: '12h', - }, - unique_private_ips: { - value: 387, - }, - }, - destination: { - histogram: { - buckets: [ - { - key_as_string: '2019-05-09T23:00:00.000Z', - key: 1557442800000, - doc_count: 36253, - count: { - value: 11, - }, - }, - { - key_as_string: '2019-05-10T11:00:00.000Z', - key: 1557486000000, - doc_count: 421719, - count: { - value: 877, - }, - }, - { - key_as_string: '2019-05-10T23:00:00.000Z', - key: 1557529200000, - doc_count: 1311, - count: { - value: 7, - }, - }, - ], - interval: '12h', - }, - unique_private_ips: { - value: 878, - }, - }, - }, - status: 200, - }, - { - took: 384, - timed_out: false, - _shards: { - total: 10, - successful: 10, - skipped: 0, - failed: 0, - }, - hits: { - total: { - value: 733106, - relation: 'eq', - }, - max_score: null, - hits: [], - }, - aggregations: { - unique_flow_id: { - value: 195415, - }, - }, - status: 200, - }, - { - took: 57, - timed_out: false, - _shards: { - total: 10, - successful: 10, - skipped: 0, - failed: 0, - }, - hits: { - total: { - value: 54482, - relation: 'eq', - }, - max_score: null, - hits: [], - }, - status: 200, - }, - ], -}; -const mockMsearchHeader = { - index: 'defaultIndex', - allowNoIndices: true, - ignoreUnavailable: true, -}; -const mockMsearchBody = { - query: {}, - aggregations: {}, - size: 0, - track_total_hits: false, -}; -export const mockNetworkEventsQueryDsl = [mockMsearchHeader, mockMsearchBody]; -export const mockUniqueFlowIdsQueryDsl = [ - mockMsearchHeader, - { mockUniqueFlowIdsQueryDsl: 'mockUniqueFlowIdsQueryDsl' }, -]; -export const mockUniquePrvateIpsQueryDsl = [ - mockMsearchHeader, - { mockUniquePrvateIpsQueryDsl: 'mockUniquePrvateIpsQueryDsl' }, -]; -export const mockDnsQueryDsl = [mockMsearchHeader, { mockDnsQueryDsl: 'mockDnsQueryDsl' }]; -export const mockTlsHandshakesQueryDsl = [ - mockMsearchHeader, - { mockTlsHandshakesQueryDsl: 'mockTlsHandshakesQueryDsl' }, -]; - -export const mockMsearchOptions = { - body: [ - ...mockNetworkEventsQueryDsl, - ...mockDnsQueryDsl, - ...mockUniquePrvateIpsQueryDsl, - ...mockUniqueFlowIdsQueryDsl, - ...mockTlsHandshakesQueryDsl, - ], -}; - -const mockDsl = [ - JSON.stringify({ ...mockNetworkEventsQueryDsl[0], body: mockNetworkEventsQueryDsl[1] }, null, 2), - JSON.stringify({ ...mockDnsQueryDsl[0], body: mockDnsQueryDsl[1] }, null, 2), - JSON.stringify( - { ...mockUniquePrvateIpsQueryDsl[0], body: mockUniquePrvateIpsQueryDsl[1] }, - null, - 2 - ), - JSON.stringify({ ...mockUniqueFlowIdsQueryDsl[0], body: mockUniqueFlowIdsQueryDsl[1] }, null, 2), - JSON.stringify({ ...mockTlsHandshakesQueryDsl[0], body: mockTlsHandshakesQueryDsl[1] }, null, 2), -]; - -export const mockResult = { - inspect: { - dsl: mockDsl, - response: [ - JSON.stringify(mockResponse.responses[0], null, 2), - JSON.stringify(mockResponse.responses[1], null, 2), - JSON.stringify(mockResponse.responses[2], null, 2), - JSON.stringify(mockResponse.responses[3], null, 2), - JSON.stringify(mockResponse.responses[4], null, 2), - ], - }, - dnsQueries: 10942, - networkEvents: 733106, - tlsHandshakes: 54482, - uniqueDestinationPrivateIps: 878, - uniqueDestinationPrivateIpsHistogram: [ - { - x: new Date('2019-05-09T23:00:00.000Z').valueOf(), - y: 11, - }, - { - x: new Date('2019-05-10T11:00:00.000Z').valueOf(), - y: 877, - }, - { - x: new Date('2019-05-10T23:00:00.000Z').valueOf(), - y: 7, - }, - ], - uniqueFlowId: 195415, - uniqueSourcePrivateIps: 387, - uniqueSourcePrivateIpsHistogram: [ - { - x: new Date('2019-05-09T23:00:00.000Z').valueOf(), - y: 14, - }, - { - x: new Date('2019-05-10T11:00:00.000Z').valueOf(), - y: 385, - }, - { - x: new Date('2019-05-10T23:00:00.000Z').valueOf(), - y: 7, - }, - ], -}; - -export const mockResponseNoData = { - responses: [null, null, null, null, null], -}; - -export const mockResultNoData = { - inspect: { - dsl: mockDsl, - response: [ - JSON.stringify(mockResponseNoData.responses[0], null, 2), - JSON.stringify(mockResponseNoData.responses[1], null, 2), - JSON.stringify(mockResponseNoData.responses[2], null, 2), - JSON.stringify(mockResponseNoData.responses[3], null, 2), - JSON.stringify(mockResponseNoData.responses[4], null, 2), - ], - }, - networkEvents: null, - uniqueFlowId: null, - uniqueSourcePrivateIps: null, - uniqueSourcePrivateIpsHistogram: null, - uniqueDestinationPrivateIps: null, - uniqueDestinationPrivateIpsHistogram: null, - dnsQueries: null, - tlsHandshakes: null, -}; diff --git a/x-pack/legacy/plugins/siem/server/lib/machine_learning/index.ts b/x-pack/legacy/plugins/siem/server/lib/machine_learning/index.ts deleted file mode 100644 index aa83df15f68d4..0000000000000 --- a/x-pack/legacy/plugins/siem/server/lib/machine_learning/index.ts +++ /dev/null @@ -1,90 +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 { SearchResponse } from 'elasticsearch'; - -import { AlertServices } from '../../../../../../plugins/alerting/server'; -import { AnomalyRecordDoc as Anomaly } from '../../../../../../plugins/ml/common/types/anomalies'; - -export { Anomaly }; -export type AnomalyResults = SearchResponse<Anomaly>; - -export interface AnomaliesSearchParams { - jobIds: string[]; - threshold: number; - earliestMs: number; - latestMs: number; - maxRecords?: number; -} - -export const getAnomalies = async ( - params: AnomaliesSearchParams, - callCluster: AlertServices['callCluster'] -): Promise<AnomalyResults> => { - const boolCriteria = buildCriteria(params); - - return callCluster('search', { - index: '.ml-anomalies-*', - size: params.maxRecords || 100, - body: { - query: { - bool: { - filter: [ - { - query_string: { - query: 'result_type:record', - analyze_wildcard: false, - }, - }, - { - bool: { - must: boolCriteria, - }, - }, - ], - }, - }, - sort: [{ record_score: { order: 'desc' } }], - }, - }); -}; - -const buildCriteria = (params: AnomaliesSearchParams): object[] => { - const { earliestMs, jobIds, latestMs, threshold } = params; - const jobIdsFilterable = jobIds.length > 0 && !(jobIds.length === 1 && jobIds[0] === '*'); - - const boolCriteria: object[] = [ - { - range: { - timestamp: { - gte: earliestMs, - lte: latestMs, - format: 'epoch_millis', - }, - }, - }, - { - range: { - record_score: { - gte: threshold, - }, - }, - }, - ]; - - if (jobIdsFilterable) { - const jobIdFilter = jobIds.map(jobId => `job_id:${jobId}`).join(' OR '); - - boolCriteria.push({ - query_string: { - analyze_wildcard: false, - query: jobIdFilter, - }, - }); - } - - return boolCriteria; -}; diff --git a/x-pack/legacy/plugins/siem/server/lib/matrix_histogram/mock.ts b/x-pack/legacy/plugins/siem/server/lib/matrix_histogram/mock.ts deleted file mode 100644 index 3e51e926bea87..0000000000000 --- a/x-pack/legacy/plugins/siem/server/lib/matrix_histogram/mock.ts +++ /dev/null @@ -1,118 +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 { defaultIndexPattern } from '../../../default_index_pattern'; -import { HistogramType } from '../../graphql/types'; - -export const mockAlertsHistogramDataResponse = { - took: 513, - timed_out: false, - _shards: { - total: 62, - successful: 61, - skipped: 0, - failed: 1, - failures: [ - { - shard: 0, - index: 'auditbeat-7.2.0', - node: 'jBC5kcOeT1exvECDMrk5Ug', - reason: { - type: 'illegal_argument_exception', - reason: - 'Fielddata is disabled on text fields by default. Set fielddata=true on [event.module] in order to load fielddata in memory by uninverting the inverted index. Note that this can however use significant memory. Alternatively use a keyword field instead.', - }, - }, - ], - }, - hits: { - total: { - value: 1599508, - relation: 'eq', - }, - max_score: null, - hits: [], - }, - aggregations: { - alertsGroup: { - doc_count_error_upper_bound: 0, - sum_other_doc_count: 802087, - buckets: [ - { - key: 'All others', - doc_count: 451519, - alerts: { - buckets: [ - { - key_as_string: '2019-12-15T09:30:00.000Z', - key: 1576402200000, - doc_count: 3008, - }, - { - key_as_string: '2019-12-15T10:00:00.000Z', - key: 1576404000000, - doc_count: 8671, - }, - ], - }, - }, - { - key: 'suricata', - doc_count: 345902, - alerts: { - buckets: [ - { - key_as_string: '2019-12-15T09:30:00.000Z', - key: 1576402200000, - doc_count: 1785, - }, - { - key_as_string: '2019-12-15T10:00:00.000Z', - key: 1576404000000, - doc_count: 5342, - }, - ], - }, - }, - ], - }, - }, -}; -export const mockAlertsHistogramDataFormattedResponse = [ - { - x: 1576402200000, - y: 3008, - g: 'All others', - }, - { - x: 1576404000000, - y: 8671, - g: 'All others', - }, - { - x: 1576402200000, - y: 1785, - g: 'suricata', - }, - { - x: 1576404000000, - y: 5342, - g: 'suricata', - }, -]; -export const mockAlertsHistogramQueryDsl = 'mockAlertsHistogramQueryDsl'; -export const mockRequest = 'mockRequest'; -export const mockOptions = { - sourceConfiguration: { field: {} }, - timerange: { - to: 9999, - from: 1234, - }, - defaultIndex: defaultIndexPattern, - filterQuery: '', - stackByField: 'event.module', - histogramType: HistogramType.alerts, -}; diff --git a/x-pack/legacy/plugins/siem/server/lib/network/mock.ts b/x-pack/legacy/plugins/siem/server/lib/network/mock.ts deleted file mode 100644 index 7ea692f27ef04..0000000000000 --- a/x-pack/legacy/plugins/siem/server/lib/network/mock.ts +++ /dev/null @@ -1,1675 +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 { defaultIndexPattern } from '../../../default_index_pattern'; -import { Direction, FlowTargetSourceDest, NetworkTopTablesFields } from '../../graphql/types'; - -import { NetworkTopNFlowRequestOptions } from '.'; - -export const mockOptions: NetworkTopNFlowRequestOptions = { - defaultIndex: defaultIndexPattern, - sourceConfiguration: { - fields: { - container: 'docker.container.name', - host: 'beat.hostname', - message: ['message', '@message'], - pod: 'kubernetes.pod.name', - tiebreaker: '_doc', - timestamp: '@timestamp', - }, - }, - timerange: { interval: '12h', to: 1549852006071, from: 1549765606071 }, - pagination: { - activePage: 0, - cursorStart: 0, - fakePossibleCount: 50, - querySize: 10, - }, - filterQuery: {}, - fields: [ - 'totalCount', - 'source.ip', - 'source.domain', - 'source.__typename', - 'destination.ip', - 'destination.domain', - 'destination.__typename', - 'event.duration', - 'event.__typename', - 'network.bytes_in', - 'network.bytes_out', - 'network.__typename', - '__typename', - 'edges.cursor.value', - 'edges.cursor.__typename', - 'edges.__typename', - 'pageInfo.activePage', - 'pageInfo.__typename', - 'pageInfo.fakeTotalCount', - 'pageInfo.__typename', - 'pageInfo.showMorePagesIndicator', - 'pageInfo.__typename', - '__typename', - ], - networkTopNFlowSort: { field: NetworkTopTablesFields.bytes_out, direction: Direction.desc }, - flowTarget: FlowTargetSourceDest.source, -}; - -export const mockRequest = { - body: { - operationName: 'GetNetworkTopNFlowQuery', - variables: { - filterQuery: '', - flowTarget: FlowTargetSourceDest.source, - pagination: { - activePage: 0, - cursorStart: 0, - fakePossibleCount: 50, - querySize: 10, - }, - sourceId: 'default', - timerange: { interval: '12h', from: 1549765830772, to: 1549852230772 }, - }, - query: ` - query GetNetworkTopNFlowQuery( - $sourceId: ID! - $ip: String - $filterQuery: String - $pagination: PaginationInputPaginated! - $sort: NetworkTopTablesSortField! - $flowTarget: FlowTargetSourceDest! - $timerange: TimerangeInput! - $defaultIndex: [String!]! - $inspect: Boolean! - ) { - source(id: $sourceId) { - id - NetworkTopNFlow( - filterQuery: $filterQuery - flowTarget: $flowTarget - ip: $ip - pagination: $pagination - sort: $sort - timerange: $timerange - defaultIndex: $defaultIndex - ) { - totalCount - edges { - node { - source { - autonomous_system { - name - number - } - domain - ip - location { - geo { - continent_name - country_name - country_iso_code - city_name - region_iso_code - region_name - } - flowTarget - } - flows - destination_ips - } - destination { - autonomous_system { - name - number - } - domain - ip - location { - geo { - continent_name - country_name - country_iso_code - city_name - region_iso_code - region_name - } - flowTarget - } - flows - source_ips - } - network { - bytes_in - bytes_out - } - } - cursor { - value - } - } - pageInfo { - activePage - fakeTotalCount - showMorePagesIndicator - } - inspect @include(if: $inspect) { - dsl - response - } - } - } - } -`, - }, -}; - -export const mockResponse = { - took: 122, - timed_out: false, - _shards: { - total: 11, - successful: 11, - skipped: 0, - failed: 0, - }, - hits: { - max_score: null, - hits: [], - }, - aggregations: { - top_n_flow_count: { - value: 545, - }, - [FlowTargetSourceDest.source]: { - buckets: [ - { - key: '1.1.1.1', - flows: { value: 1234567 }, - destination_ips: { value: 345345 }, - bytes_in: { - value: 11276023407, - }, - bytes_out: { - value: 1025631, - }, - location: { - doc_count: 14, - top_geo: { - hits: { - total: { - value: 14, - relation: 'eq', - }, - max_score: 1, - hits: [ - { - _index: 'filebeat-8.0.0-2019.06.19-000005', - _type: '_doc', - _id: 'dd4fa2d4bd-692279846149410', - _score: 1, - _source: { - source: { - geo: { - continent_name: 'North America', - region_iso_code: 'US-PA', - city_name: 'Philadelphia', - country_iso_code: 'US', - region_name: 'Pennsylvania', - location: { - lon: -75.1534, - lat: 39.9359, - }, - }, - }, - }, - }, - ], - }, - }, - }, - autonomous_system: { - doc_count: 14, - top_as: { - hits: { - total: { - value: 14, - relation: 'eq', - }, - max_score: 1, - hits: [ - { - _index: 'filebeat-8.0.0-2019.06.19-000005', - _type: '_doc', - _id: 'dd4fa2d4bd-692279846149410', - _score: 1, - _source: { - source: { - as: { - number: 3356, - organization: { - name: 'Level 3 Parent, LLC', - }, - }, - }, - }, - }, - ], - }, - }, - }, - domain: { - buckets: [ - { - key: 'test.1.net', - }, - ], - }, - }, - { - key: '2.2.2.2', - flows: { value: 1234567 }, - destination_ips: { value: 345345 }, - bytes_in: { - value: 5469323342, - }, - bytes_out: { - value: 2811441, - }, - location: { - doc_count: 14, - top_geo: { - hits: { - total: { - value: 14, - relation: 'eq', - }, - max_score: 1, - hits: [ - { - _index: 'filebeat-8.0.0-2019.06.19-000005', - _type: '_doc', - _id: 'dd4fa2d4bd-692279846149410', - _score: 1, - _source: { - source: { - geo: { - continent_name: 'North America', - region_iso_code: 'US-PA', - city_name: 'Philadelphia', - country_iso_code: 'US', - region_name: 'Pennsylvania', - location: { - lon: -75.1534, - lat: 39.9359, - }, - }, - }, - }, - }, - ], - }, - }, - }, - autonomous_system: { - doc_count: 14, - top_as: { - hits: { - total: { - value: 14, - relation: 'eq', - }, - max_score: 1, - hits: [ - { - _index: 'filebeat-8.0.0-2019.06.19-000005', - _type: '_doc', - _id: 'dd4fa2d4bd-692279846149410', - _score: 1, - _source: { - source: { - as: { - number: 3356, - organization: { - name: 'Level 3 Parent, LLC', - }, - }, - }, - }, - }, - ], - }, - }, - }, - domain: { - buckets: [ - { - key: 'test.2.net', - }, - ], - }, - }, - { - key: '3.3.3.3', - flows: { value: 1234567 }, - destination_ips: { value: 345345 }, - bytes_in: { - value: 3807671322, - }, - bytes_out: { - value: 4494034, - }, - location: { - doc_count: 14, - top_geo: { - hits: { - total: { - value: 14, - relation: 'eq', - }, - max_score: 1, - hits: [ - { - _index: 'filebeat-8.0.0-2019.06.19-000005', - _type: '_doc', - _id: 'dd4fa2d4bd-692279846149410', - _score: 1, - _source: { - source: { - geo: { - continent_name: 'North America', - region_iso_code: 'US-PA', - city_name: 'Philadelphia', - country_iso_code: 'US', - region_name: 'Pennsylvania', - location: { - lon: -75.1534, - lat: 39.9359, - }, - }, - }, - }, - }, - ], - }, - }, - }, - autonomous_system: { - doc_count: 14, - top_as: { - hits: { - total: { - value: 14, - relation: 'eq', - }, - max_score: 1, - hits: [ - { - _index: 'filebeat-8.0.0-2019.06.19-000005', - _type: '_doc', - _id: 'dd4fa2d4bd-692279846149410', - _score: 1, - _source: { - source: { - as: { - number: 3356, - organization: { - name: 'Level 3 Parent, LLC', - }, - }, - }, - }, - }, - ], - }, - }, - }, - domain: { - buckets: [ - { - key: 'test.3.com', - }, - { - key: 'test.3-duplicate.com', - }, - ], - }, - }, - { - key: '4.4.4.4', - flows: { value: 1234567 }, - destination_ips: { value: 345345 }, - bytes_in: { - value: 166517626, - }, - bytes_out: { - value: 3194782, - }, - location: { - doc_count: 14, - top_geo: { - hits: { - total: { - value: 14, - relation: 'eq', - }, - max_score: 1, - hits: [ - { - _index: 'filebeat-8.0.0-2019.06.19-000005', - _type: '_doc', - _id: 'dd4fa2d4bd-692279846149410', - _score: 1, - _source: { - source: { - geo: { - continent_name: 'North America', - region_iso_code: 'US-PA', - city_name: 'Philadelphia', - country_iso_code: 'US', - region_name: 'Pennsylvania', - location: { - lon: -75.1534, - lat: 39.9359, - }, - }, - }, - }, - }, - ], - }, - }, - }, - autonomous_system: { - doc_count: 14, - top_as: { - hits: { - total: { - value: 14, - relation: 'eq', - }, - max_score: 1, - hits: [ - { - _index: 'filebeat-8.0.0-2019.06.19-000005', - _type: '_doc', - _id: 'dd4fa2d4bd-692279846149410', - _score: 1, - _source: { - source: { - as: { - number: 3356, - organization: { - name: 'Level 3 Parent, LLC', - }, - }, - }, - }, - }, - ], - }, - }, - }, - domain: { - buckets: [ - { - key: 'test.4.com', - }, - ], - }, - }, - { - key: '5.5.5.5', - flows: { value: 1234567 }, - destination_ips: { value: 345345 }, - bytes_in: { - value: 104785026, - }, - bytes_out: { - value: 1838597, - }, - location: { - doc_count: 14, - top_geo: { - hits: { - total: { - value: 14, - relation: 'eq', - }, - max_score: 1, - hits: [ - { - _index: 'filebeat-8.0.0-2019.06.19-000005', - _type: '_doc', - _id: 'dd4fa2d4bd-692279846149410', - _score: 1, - _source: { - source: { - geo: { - continent_name: 'North America', - region_iso_code: 'US-PA', - city_name: 'Philadelphia', - country_iso_code: 'US', - region_name: 'Pennsylvania', - location: { - lon: -75.1534, - lat: 39.9359, - }, - }, - }, - }, - }, - ], - }, - }, - }, - autonomous_system: { - doc_count: 14, - top_as: { - hits: { - total: { - value: 14, - relation: 'eq', - }, - max_score: 1, - hits: [ - { - _index: 'filebeat-8.0.0-2019.06.19-000005', - _type: '_doc', - _id: 'dd4fa2d4bd-692279846149410', - _score: 1, - _source: { - source: { - as: { - number: 3356, - organization: { - name: 'Level 3 Parent, LLC', - }, - }, - }, - }, - }, - ], - }, - }, - }, - domain: { - buckets: [ - { - key: 'test.5.com', - }, - ], - }, - }, - { - key: '6.6.6.6', - flows: { value: 1234567 }, - destination_ips: { value: 345345 }, - bytes_in: { - value: 28804250, - }, - bytes_out: { - value: 482982, - }, - location: { - doc_count: 14, - top_geo: { - hits: { - total: { - value: 14, - relation: 'eq', - }, - max_score: 1, - hits: [ - { - _index: 'filebeat-8.0.0-2019.06.19-000005', - _type: '_doc', - _id: 'dd4fa2d4bd-692279846149410', - _score: 1, - _source: { - source: { - geo: { - continent_name: 'North America', - region_iso_code: 'US-PA', - city_name: 'Philadelphia', - country_iso_code: 'US', - region_name: 'Pennsylvania', - location: { - lon: -75.1534, - lat: 39.9359, - }, - }, - }, - }, - }, - ], - }, - }, - }, - autonomous_system: { - doc_count: 14, - top_as: { - hits: { - total: { - value: 14, - relation: 'eq', - }, - max_score: 1, - hits: [ - { - _index: 'filebeat-8.0.0-2019.06.19-000005', - _type: '_doc', - _id: 'dd4fa2d4bd-692279846149410', - _score: 1, - _source: { - source: { - as: { - number: 3356, - organization: { - name: 'Level 3 Parent, LLC', - }, - }, - }, - }, - }, - ], - }, - }, - }, - domain: { - doc_count_error_upper_bound: 0, - sum_other_doc_count: 31, - buckets: [ - { - key: 'test.6.com', - }, - ], - }, - }, - { - key: '7.7.7.7', - flows: { value: 1234567 }, - destination_ips: { value: 345345 }, - bytes_in: { - value: 23032363, - }, - bytes_out: { - value: 400623, - }, - location: { - doc_count: 14, - top_geo: { - hits: { - total: { - value: 14, - relation: 'eq', - }, - max_score: 1, - hits: [ - { - _index: 'filebeat-8.0.0-2019.06.19-000005', - _type: '_doc', - _id: 'dd4fa2d4bd-692279846149410', - _score: 1, - _source: { - source: { - geo: { - continent_name: 'North America', - region_iso_code: 'US-PA', - city_name: 'Philadelphia', - country_iso_code: 'US', - region_name: 'Pennsylvania', - location: { - lon: -75.1534, - lat: 39.9359, - }, - }, - }, - }, - }, - ], - }, - }, - }, - autonomous_system: { - doc_count: 14, - top_as: { - hits: { - total: { - value: 14, - relation: 'eq', - }, - max_score: 1, - hits: [ - { - _index: 'filebeat-8.0.0-2019.06.19-000005', - _type: '_doc', - _id: 'dd4fa2d4bd-692279846149410', - _score: 1, - _source: { - source: { - as: { - number: 3356, - organization: { - name: 'Level 3 Parent, LLC', - }, - }, - }, - }, - }, - ], - }, - }, - }, - domain: { - doc_count_error_upper_bound: 0, - sum_other_doc_count: 0, - buckets: [ - { - key: 'test.7.com', - }, - ], - }, - }, - { - key: '8.8.8.8', - flows: { value: 1234567 }, - destination_ips: { value: 345345 }, - bytes_in: { - value: 21424889, - }, - bytes_out: { - value: 344357, - }, - location: { - doc_count: 14, - top_geo: { - hits: { - total: { - value: 14, - relation: 'eq', - }, - max_score: 1, - hits: [ - { - _index: 'filebeat-8.0.0-2019.06.19-000005', - _type: '_doc', - _id: 'dd4fa2d4bd-692279846149410', - _score: 1, - _source: { - source: { - geo: { - continent_name: 'North America', - region_iso_code: 'US-PA', - city_name: 'Philadelphia', - country_iso_code: 'US', - region_name: 'Pennsylvania', - location: { - lon: -75.1534, - lat: 39.9359, - }, - }, - }, - }, - }, - ], - }, - }, - }, - autonomous_system: { - doc_count: 14, - top_as: { - hits: { - total: { - value: 14, - relation: 'eq', - }, - max_score: 1, - hits: [ - { - _index: 'filebeat-8.0.0-2019.06.19-000005', - _type: '_doc', - _id: 'dd4fa2d4bd-692279846149410', - _score: 1, - _source: { - source: { - as: { - number: 3356, - organization: { - name: 'Level 3 Parent, LLC', - }, - }, - }, - }, - }, - ], - }, - }, - }, - domain: { - buckets: [ - { - key: 'test.8.com', - }, - ], - }, - }, - { - key: '9.9.9.9', - flows: { value: 1234567 }, - destination_ips: { value: 345345 }, - bytes_in: { - value: 19205000, - }, - bytes_out: { - value: 355663, - }, - location: { - doc_count: 14, - top_geo: { - hits: { - total: { - value: 14, - relation: 'eq', - }, - max_score: 1, - hits: [ - { - _index: 'filebeat-8.0.0-2019.06.19-000005', - _type: '_doc', - _id: 'dd4fa2d4bd-692279846149410', - _score: 1, - _source: { - source: { - geo: { - continent_name: 'North America', - region_iso_code: 'US-PA', - city_name: 'Philadelphia', - country_iso_code: 'US', - region_name: 'Pennsylvania', - location: { - lon: -75.1534, - lat: 39.9359, - }, - }, - }, - }, - }, - ], - }, - }, - }, - autonomous_system: { - doc_count: 14, - top_as: { - hits: { - total: { - value: 14, - relation: 'eq', - }, - max_score: 1, - hits: [ - { - _index: 'filebeat-8.0.0-2019.06.19-000005', - _type: '_doc', - _id: 'dd4fa2d4bd-692279846149410', - _score: 1, - _source: { - source: { - as: { - number: 3356, - organization: { - name: 'Level 3 Parent, LLC', - }, - }, - }, - }, - }, - ], - }, - }, - }, - domain: { - buckets: [ - { - key: 'test.9.com', - }, - ], - }, - }, - { - key: '10.10.10.10', - flows: { value: 1234567 }, - destination_ips: { value: 345345 }, - bytes_in: { - value: 11407633, - }, - bytes_out: { - value: 199360, - }, - location: { - doc_count: 14, - top_geo: { - hits: { - total: { - value: 14, - relation: 'eq', - }, - max_score: 1, - hits: [ - { - _index: 'filebeat-8.0.0-2019.06.19-000005', - _type: '_doc', - _id: 'dd4fa2d4bd-692279846149410', - _score: 1, - _source: { - source: { - geo: { - continent_name: 'North America', - region_iso_code: 'US-PA', - city_name: 'Philadelphia', - country_iso_code: 'US', - region_name: 'Pennsylvania', - location: { - lon: -75.1534, - lat: 39.9359, - }, - }, - }, - }, - }, - ], - }, - }, - }, - autonomous_system: { - doc_count: 14, - top_as: { - hits: { - total: { - value: 14, - relation: 'eq', - }, - max_score: 1, - hits: [ - { - _index: 'filebeat-8.0.0-2019.06.19-000005', - _type: '_doc', - _id: 'dd4fa2d4bd-692279846149410', - _score: 1, - _source: { - source: { - as: { - number: 3356, - organization: { - name: 'Level 3 Parent, LLC', - }, - }, - }, - }, - }, - ], - }, - }, - }, - domain: { - buckets: [ - { - key: 'test.10.com', - }, - ], - }, - }, - { - key: '11.11.11.11', - flows: { value: 1234567 }, - destination_ips: { value: 345345 }, - bytes_in: { - value: 11393327, - }, - bytes_out: { - value: 195914, - }, - location: { - doc_count: 14, - top_geo: { - hits: { - total: { - value: 14, - relation: 'eq', - }, - max_score: 1, - hits: [ - { - _index: 'filebeat-8.0.0-2019.06.19-000005', - _type: '_doc', - _id: 'dd4fa2d4bd-692279846149410', - _score: 1, - _source: { - source: { - geo: { - continent_name: 'North America', - region_iso_code: 'US-PA', - city_name: 'Philadelphia', - country_iso_code: 'US', - region_name: 'Pennsylvania', - location: { - lon: -75.1534, - lat: 39.9359, - }, - }, - }, - }, - }, - ], - }, - }, - }, - autonomous_system: { - doc_count: 14, - top_as: { - hits: { - total: { - value: 14, - relation: 'eq', - }, - max_score: 1, - hits: [ - { - _index: 'filebeat-8.0.0-2019.06.19-000005', - _type: '_doc', - _id: 'dd4fa2d4bd-692279846149410', - _score: 1, - _source: { - source: { - as: { - number: 3356, - organization: { - name: 'Level 3 Parent, LLC', - }, - }, - }, - }, - }, - ], - }, - }, - }, - domain: { - buckets: [ - { - key: 'test.11.com', - }, - ], - }, - }, - ], - }, - }, -}; - -export const mockTopNFlowQueryDsl = { - mockTopNFlowQueryDsl: 'mockTopNFlowQueryDsl', -}; - -export const mockResult = { - inspect: { - dsl: [JSON.stringify(mockTopNFlowQueryDsl, null, 2)], - response: [JSON.stringify(mockResponse, null, 2)], - }, - edges: [ - { - cursor: { - tiebreaker: null, - value: '1.1.1.1', - }, - node: { - _id: '1.1.1.1', - network: { - bytes_in: 11276023407, - bytes_out: 1025631, - }, - source: { - domain: ['test.1.net'], - ip: '1.1.1.1', - autonomous_system: { - name: 'Level 3 Parent, LLC', - number: 3356, - }, - location: { - flowTarget: 'source', - geo: { - city_name: 'Philadelphia', - continent_name: 'North America', - country_iso_code: 'US', - location: { - lat: 39.9359, - lon: -75.1534, - }, - region_iso_code: 'US-PA', - region_name: 'Pennsylvania', - }, - }, - flows: 1234567, - destination_ips: 345345, - }, - }, - }, - { - cursor: { - tiebreaker: null, - value: '2.2.2.2', - }, - node: { - _id: '2.2.2.2', - network: { - bytes_in: 5469323342, - bytes_out: 2811441, - }, - source: { - domain: ['test.2.net'], - ip: '2.2.2.2', - location: { - flowTarget: 'source', - geo: { - city_name: 'Philadelphia', - continent_name: 'North America', - country_iso_code: 'US', - location: { - lat: 39.9359, - lon: -75.1534, - }, - region_iso_code: 'US-PA', - region_name: 'Pennsylvania', - }, - }, - autonomous_system: { - name: 'Level 3 Parent, LLC', - number: 3356, - }, - flows: 1234567, - destination_ips: 345345, - }, - }, - }, - { - cursor: { - tiebreaker: null, - value: '3.3.3.3', - }, - node: { - _id: '3.3.3.3', - network: { - bytes_in: 3807671322, - bytes_out: 4494034, - }, - source: { - domain: ['test.3.com', 'test.3-duplicate.com'], - ip: '3.3.3.3', - location: { - flowTarget: 'source', - geo: { - city_name: 'Philadelphia', - continent_name: 'North America', - country_iso_code: 'US', - location: { - lat: 39.9359, - lon: -75.1534, - }, - region_iso_code: 'US-PA', - region_name: 'Pennsylvania', - }, - }, - autonomous_system: { - name: 'Level 3 Parent, LLC', - number: 3356, - }, - flows: 1234567, - destination_ips: 345345, - }, - }, - }, - { - cursor: { - tiebreaker: null, - value: '4.4.4.4', - }, - node: { - _id: '4.4.4.4', - network: { - bytes_in: 166517626, - bytes_out: 3194782, - }, - source: { - domain: ['test.4.com'], - ip: '4.4.4.4', - location: { - flowTarget: 'source', - geo: { - city_name: 'Philadelphia', - continent_name: 'North America', - country_iso_code: 'US', - location: { - lat: 39.9359, - lon: -75.1534, - }, - region_iso_code: 'US-PA', - region_name: 'Pennsylvania', - }, - }, - autonomous_system: { - name: 'Level 3 Parent, LLC', - number: 3356, - }, - flows: 1234567, - destination_ips: 345345, - }, - }, - }, - { - cursor: { - tiebreaker: null, - value: '5.5.5.5', - }, - node: { - _id: '5.5.5.5', - network: { - bytes_in: 104785026, - bytes_out: 1838597, - }, - source: { - domain: ['test.5.com'], - ip: '5.5.5.5', - location: { - flowTarget: 'source', - geo: { - city_name: 'Philadelphia', - continent_name: 'North America', - country_iso_code: 'US', - location: { - lat: 39.9359, - lon: -75.1534, - }, - region_iso_code: 'US-PA', - region_name: 'Pennsylvania', - }, - }, - autonomous_system: { - name: 'Level 3 Parent, LLC', - number: 3356, - }, - flows: 1234567, - destination_ips: 345345, - }, - }, - }, - { - cursor: { - tiebreaker: null, - value: '6.6.6.6', - }, - node: { - _id: '6.6.6.6', - network: { - bytes_in: 28804250, - bytes_out: 482982, - }, - source: { - domain: ['test.6.com'], - ip: '6.6.6.6', - location: { - flowTarget: 'source', - geo: { - city_name: 'Philadelphia', - continent_name: 'North America', - country_iso_code: 'US', - location: { - lat: 39.9359, - lon: -75.1534, - }, - region_iso_code: 'US-PA', - region_name: 'Pennsylvania', - }, - }, - autonomous_system: { - name: 'Level 3 Parent, LLC', - number: 3356, - }, - flows: 1234567, - destination_ips: 345345, - }, - }, - }, - { - cursor: { - tiebreaker: null, - value: '7.7.7.7', - }, - node: { - _id: '7.7.7.7', - network: { - bytes_in: 23032363, - bytes_out: 400623, - }, - source: { - domain: ['test.7.com'], - ip: '7.7.7.7', - location: { - flowTarget: 'source', - geo: { - city_name: 'Philadelphia', - continent_name: 'North America', - country_iso_code: 'US', - location: { - lat: 39.9359, - lon: -75.1534, - }, - region_iso_code: 'US-PA', - region_name: 'Pennsylvania', - }, - }, - autonomous_system: { - name: 'Level 3 Parent, LLC', - number: 3356, - }, - flows: 1234567, - destination_ips: 345345, - }, - }, - }, - { - cursor: { - tiebreaker: null, - value: '8.8.8.8', - }, - node: { - _id: '8.8.8.8', - network: { - bytes_in: 21424889, - bytes_out: 344357, - }, - source: { - domain: ['test.8.com'], - ip: '8.8.8.8', - location: { - flowTarget: 'source', - geo: { - city_name: 'Philadelphia', - continent_name: 'North America', - country_iso_code: 'US', - location: { - lat: 39.9359, - lon: -75.1534, - }, - region_iso_code: 'US-PA', - region_name: 'Pennsylvania', - }, - }, - autonomous_system: { - name: 'Level 3 Parent, LLC', - number: 3356, - }, - flows: 1234567, - destination_ips: 345345, - }, - }, - }, - { - cursor: { - tiebreaker: null, - value: '9.9.9.9', - }, - node: { - _id: '9.9.9.9', - network: { - bytes_in: 19205000, - bytes_out: 355663, - }, - source: { - domain: ['test.9.com'], - ip: '9.9.9.9', - location: { - flowTarget: 'source', - geo: { - city_name: 'Philadelphia', - continent_name: 'North America', - country_iso_code: 'US', - location: { - lat: 39.9359, - lon: -75.1534, - }, - region_iso_code: 'US-PA', - region_name: 'Pennsylvania', - }, - }, - autonomous_system: { - name: 'Level 3 Parent, LLC', - number: 3356, - }, - flows: 1234567, - destination_ips: 345345, - }, - }, - }, - { - cursor: { - tiebreaker: null, - value: '10.10.10.10', - }, - node: { - _id: '10.10.10.10', - network: { - bytes_in: 11407633, - bytes_out: 199360, - }, - source: { - domain: ['test.10.com'], - ip: '10.10.10.10', - location: { - flowTarget: 'source', - geo: { - city_name: 'Philadelphia', - continent_name: 'North America', - country_iso_code: 'US', - location: { - lat: 39.9359, - lon: -75.1534, - }, - region_iso_code: 'US-PA', - region_name: 'Pennsylvania', - }, - }, - autonomous_system: { - name: 'Level 3 Parent, LLC', - number: 3356, - }, - flows: 1234567, - destination_ips: 345345, - }, - }, - }, - ], - pageInfo: { - activePage: 0, - fakeTotalCount: 50, - showMorePagesIndicator: true, - }, - totalCount: 545, -}; - -export const mockOptionsIp: NetworkTopNFlowRequestOptions = { - ...mockOptions, - ip: '1.1.1.1', -}; - -export const mockRequestIp = { - ...mockRequest, - body: { - ...mockRequest.body, - variables: { - ...mockRequest.body.variables, - ip: '1.1.1.1', - }, - }, -}; - -export const mockResponseIp = { - took: 122, - timed_out: false, - _shards: { - total: 1, - successful: 1, - skipped: 0, - failed: 0, - }, - hits: { - max_score: null, - hits: [], - }, - aggregations: { - top_n_flow_count: { - value: 1, - }, - [FlowTargetSourceDest.source]: { - buckets: [ - { - key: '1.1.1.1', - flows: { value: 1234567 }, - destination_ips: { value: 345345 }, - bytes_in: { - value: 11276023407, - }, - bytes_out: { - value: 1025631, - }, - location: { - doc_count: 14, - top_geo: { - hits: { - total: { - value: 14, - relation: 'eq', - }, - max_score: 1, - hits: [ - { - _index: 'filebeat-8.0.0-2019.06.19-000005', - _type: '_doc', - _id: 'dd4fa2d4bd-692279846149410', - _score: 1, - _source: { - source: { - geo: { - continent_name: 'North America', - region_iso_code: 'US-PA', - city_name: 'Philadelphia', - country_iso_code: 'US', - region_name: 'Pennsylvania', - location: { - lon: -75.1534, - lat: 39.9359, - }, - }, - }, - }, - }, - ], - }, - }, - }, - autonomous_system: { - doc_count: 14, - top_as: { - hits: { - total: { - value: 14, - relation: 'eq', - }, - max_score: 1, - hits: [ - { - _index: 'filebeat-8.0.0-2019.06.19-000005', - _type: '_doc', - _id: 'dd4fa2d4bd-692279846149410', - _score: 1, - _source: { - source: { - as: { - number: 3356, - organization: { - name: 'Level 3 Parent, LLC', - }, - }, - }, - }, - }, - ], - }, - }, - }, - domain: { - buckets: [ - { - key: 'test.1.net', - }, - ], - }, - }, - ], - }, - }, -}; - -export const mockResultIp = { - inspect: { - dsl: [JSON.stringify(mockTopNFlowQueryDsl, null, 2)], - response: [JSON.stringify(mockResponseIp, null, 2)], - }, - edges: [ - { - cursor: { - tiebreaker: null, - value: '1.1.1.1', - }, - node: { - _id: '1.1.1.1', - network: { - bytes_in: 11276023407, - bytes_out: 1025631, - }, - source: { - domain: ['test.1.net'], - ip: '1.1.1.1', - autonomous_system: { - name: 'Level 3 Parent, LLC', - number: 3356, - }, - location: { - flowTarget: 'source', - geo: { - city_name: 'Philadelphia', - continent_name: 'North America', - country_iso_code: 'US', - location: { - lat: 39.9359, - lon: -75.1534, - }, - region_iso_code: 'US-PA', - region_name: 'Pennsylvania', - }, - }, - flows: 1234567, - destination_ips: 345345, - }, - }, - }, - ], - pageInfo: { - activePage: 0, - fakeTotalCount: 1, - showMorePagesIndicator: false, - }, - totalCount: 1, -}; diff --git a/x-pack/legacy/plugins/siem/server/lib/note/saved_object.ts b/x-pack/legacy/plugins/siem/server/lib/note/saved_object.ts deleted file mode 100644 index 23162f38bffba..0000000000000 --- a/x-pack/legacy/plugins/siem/server/lib/note/saved_object.ts +++ /dev/null @@ -1,239 +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 { failure } from 'io-ts/lib/PathReporter'; -import { getOr } from 'lodash/fp'; -import uuid from 'uuid'; - -import { pipe } from 'fp-ts/lib/pipeable'; -import { map, fold } from 'fp-ts/lib/Either'; -import { identity } from 'fp-ts/lib/function'; - -import { SavedObjectsFindOptions } from '../../../../../../../src/core/server'; -import { AuthenticatedUser } from '../../../../../../plugins/security/common/model'; -import { UNAUTHENTICATED_USER } from '../../../common/constants'; -import { - PageInfoNote, - ResponseNote, - ResponseNotes, - SortNote, - NoteResult, -} from '../../graphql/types'; -import { FrameworkRequest } from '../framework'; -import { SavedNote, NoteSavedObjectRuntimeType, NoteSavedObject } from './types'; -import { noteSavedObjectType } from './saved_object_mappings'; -import { timelineSavedObjectType } from '../../saved_objects'; -import { pickSavedTimeline } from '../timeline/pick_saved_timeline'; -import { convertSavedObjectToSavedTimeline } from '../timeline/convert_saved_object_to_savedtimeline'; - -export class Note { - public async deleteNote(request: FrameworkRequest, noteIds: string[]) { - const savedObjectsClient = request.context.core.savedObjects.client; - - await Promise.all( - noteIds.map(noteId => savedObjectsClient.delete(noteSavedObjectType, noteId)) - ); - } - - public async deleteNoteByTimelineId(request: FrameworkRequest, timelineId: string) { - const options: SavedObjectsFindOptions = { - type: noteSavedObjectType, - search: timelineId, - searchFields: ['timelineId'], - }; - const notesToBeDeleted = await this.getAllSavedNote(request, options); - const savedObjectsClient = request.context.core.savedObjects.client; - - await Promise.all( - notesToBeDeleted.notes.map(note => - savedObjectsClient.delete(noteSavedObjectType, note.noteId) - ) - ); - } - - public async getNote(request: FrameworkRequest, noteId: string): Promise<NoteSavedObject> { - return this.getSavedNote(request, noteId); - } - - public async getNotesByEventId( - request: FrameworkRequest, - eventId: string - ): Promise<NoteSavedObject[]> { - const options: SavedObjectsFindOptions = { - type: noteSavedObjectType, - search: eventId, - searchFields: ['eventId'], - }; - const notesByEventId = await this.getAllSavedNote(request, options); - return notesByEventId.notes; - } - - public async getNotesByTimelineId( - request: FrameworkRequest, - timelineId: string - ): Promise<NoteSavedObject[]> { - const options: SavedObjectsFindOptions = { - type: noteSavedObjectType, - search: timelineId, - searchFields: ['timelineId'], - }; - const notesByTimelineId = await this.getAllSavedNote(request, options); - return notesByTimelineId.notes; - } - - public async getAllNotes( - request: FrameworkRequest, - pageInfo: PageInfoNote | null, - search: string | null, - sort: SortNote | null - ): Promise<ResponseNotes> { - const options: SavedObjectsFindOptions = { - type: noteSavedObjectType, - perPage: pageInfo != null ? pageInfo.pageSize : undefined, - page: pageInfo != null ? pageInfo.pageIndex : undefined, - search: search != null ? search : undefined, - searchFields: ['note'], - sortField: sort != null ? sort.sortField : undefined, - sortOrder: sort != null ? sort.sortOrder : undefined, - }; - return this.getAllSavedNote(request, options); - } - - public async persistNote( - request: FrameworkRequest, - noteId: string | null, - version: string | null, - note: SavedNote - ): Promise<ResponseNote> { - try { - const savedObjectsClient = request.context.core.savedObjects.client; - - if (noteId == null) { - const timelineVersionSavedObject = - note.timelineId == null - ? await (async () => { - const timelineResult = convertSavedObjectToSavedTimeline( - await savedObjectsClient.create( - timelineSavedObjectType, - pickSavedTimeline(null, {}, request.user) - ) - ); - note.timelineId = timelineResult.savedObjectId; - return timelineResult.version; - })() - : null; - - // Create new note - return { - code: 200, - message: 'success', - note: convertSavedObjectToSavedNote( - await savedObjectsClient.create( - noteSavedObjectType, - pickSavedNote(noteId, note, request.user) - ), - timelineVersionSavedObject != null ? timelineVersionSavedObject : undefined - ), - }; - } - - // Update new note - - const existingNote = await this.getSavedNote(request, noteId); - return { - code: 200, - message: 'success', - note: convertSavedObjectToSavedNote( - await savedObjectsClient.update( - noteSavedObjectType, - noteId, - pickSavedNote(noteId, note, request.user), - { - version: existingNote.version || undefined, - } - ) - ), - }; - } catch (err) { - if (getOr(null, 'output.statusCode', err) === 403) { - const noteToReturn: NoteResult = { - ...note, - noteId: uuid.v1(), - version: '', - timelineId: '', - timelineVersion: '', - }; - return { - code: 403, - message: err.message, - note: noteToReturn, - }; - } - throw err; - } - } - - private async getSavedNote(request: FrameworkRequest, NoteId: string) { - const savedObjectsClient = request.context.core.savedObjects.client; - const savedObject = await savedObjectsClient.get(noteSavedObjectType, NoteId); - - return convertSavedObjectToSavedNote(savedObject); - } - - private async getAllSavedNote(request: FrameworkRequest, options: SavedObjectsFindOptions) { - const savedObjectsClient = request.context.core.savedObjects.client; - const savedObjects = await savedObjectsClient.find(options); - - return { - totalCount: savedObjects.total, - notes: savedObjects.saved_objects.map(savedObject => - convertSavedObjectToSavedNote(savedObject) - ), - }; - } -} - -export const convertSavedObjectToSavedNote = ( - savedObject: unknown, - timelineVersion?: string | undefined | null -): NoteSavedObject => - pipe( - NoteSavedObjectRuntimeType.decode(savedObject), - map(savedNote => ({ - noteId: savedNote.id, - version: savedNote.version, - timelineVersion, - ...savedNote.attributes, - })), - fold(errors => { - throw new Error(failure(errors).join('\n')); - }, identity) - ); - -// we have to use any here because the SavedObjectAttributes interface is like below -// export interface SavedObjectAttributes { -// [key: string]: SavedObjectAttributes | string | number | boolean | null; -// } -// then this interface does not allow types without index signature -// this is limiting us with our type for now so the easy way was to use any - -const pickSavedNote = ( - noteId: string | null, - savedNote: SavedNote, - userInfo: AuthenticatedUser | null - // eslint-disable-next-line @typescript-eslint/no-explicit-any -): any => { - if (noteId == null) { - savedNote.created = new Date().valueOf(); - savedNote.createdBy = userInfo?.username ?? UNAUTHENTICATED_USER; - savedNote.updated = new Date().valueOf(); - savedNote.updatedBy = userInfo?.username ?? UNAUTHENTICATED_USER; - } else if (noteId != null) { - savedNote.updated = new Date().valueOf(); - savedNote.updatedBy = userInfo?.username ?? UNAUTHENTICATED_USER; - } - return savedNote; -}; diff --git a/x-pack/legacy/plugins/siem/server/lib/overview/mock.ts b/x-pack/legacy/plugins/siem/server/lib/overview/mock.ts deleted file mode 100644 index 410b4d90b1e78..0000000000000 --- a/x-pack/legacy/plugins/siem/server/lib/overview/mock.ts +++ /dev/null @@ -1,168 +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 { defaultIndexPattern } from '../../../default_index_pattern'; -import { RequestBasicOptions } from '../framework/types'; - -export const mockOptionsNetwork: RequestBasicOptions = { - defaultIndex: defaultIndexPattern, - sourceConfiguration: { - fields: { - container: 'docker.container.name', - host: 'beat.hostname', - message: ['message', '@message'], - pod: 'kubernetes.pod.name', - tiebreaker: '_doc', - timestamp: '@timestamp', - }, - }, - timerange: { interval: '12h', to: 1549852006071, from: 1549765606071 }, - filterQuery: {}, -}; - -export const mockRequestNetwork = { - body: { - operationName: 'GetOverviewNetworkQuery', - variables: { - sourceId: 'default', - timerange: { interval: '12h', from: 1549765830772, to: 1549852230772 }, - filterQuery: '', - }, - query: - 'query GetOverviewNetworkQuery(\n $sourceId: ID!\n $timerange: TimerangeInput!\n $filterQuery: String\n ) {\n source(id: $sourceId) {\n id\n OverviewNetwork(timerange: $timerange, filterQuery: $filterQuery) {\n packetbeatFlow\n packetbeatDNS\n filebeatSuricata\n filebeatZeek\n auditbeatSocket\n }\n }\n }', - }, -}; - -export const mockResponseNetwork = { - took: 89, - timed_out: false, - _shards: { total: 18, successful: 18, skipped: 0, failed: 0 }, - hits: { total: { value: 950867, relation: 'eq' }, max_score: null, hits: [] }, - aggregations: { - unique_flow_count: { doc_count: 50243 }, - unique_dns_count: { doc_count: 15000 }, - unique_suricata_count: { doc_count: 2375 }, - unique_zeek_count: { doc_count: 456 }, - unique_socket_count: { doc_count: 13 }, - unique_filebeat_count: { - doc_count: 456756, - unique_cisco_count: { doc_count: 14 }, - unique_netflow_count: { doc_count: 992 }, - unique_panw_count: { doc_count: 225 }, - }, - unique_packetbeat_count: { doc_count: 7897896, unique_tls_count: { doc_count: 2009 } }, - }, -}; - -export const mockBuildOverviewHostQuery = { buildOverviewHostQuery: 'buildOverviewHostQuery' }; -export const mockBuildOverviewNetworkQuery = { - buildOverviewNetworkQuery: 'buildOverviewNetworkQuery', -}; - -export const mockResultNetwork = { - inspect: { - dsl: [JSON.stringify(mockBuildOverviewNetworkQuery, null, 2)], - response: [JSON.stringify(mockResponseNetwork, null, 2)], - }, - packetbeatFlow: 50243, - packetbeatDNS: 15000, - filebeatSuricata: 2375, - filebeatZeek: 456, - auditbeatSocket: 13, - filebeatCisco: 14, - filebeatNetflow: 992, - filebeatPanw: 225, - packetbeatTLS: 2009, -}; - -export const mockOptionsHost: RequestBasicOptions = { - defaultIndex: defaultIndexPattern, - sourceConfiguration: { - fields: { - container: 'docker.container.name', - host: 'beat.hostname', - message: ['message', '@message'], - pod: 'kubernetes.pod.name', - tiebreaker: '_doc', - timestamp: '@timestamp', - }, - }, - timerange: { interval: '12h', to: 1549852006071, from: 1549765606071 }, - filterQuery: {}, -}; - -export const mockRequestHost = { - body: { - operationName: 'GetOverviewHostQuery', - variables: { - sourceId: 'default', - timerange: { interval: '12h', from: 1549765830772, to: 1549852230772 }, - filterQuery: '', - }, - query: - 'query GetOverviewHostQuery(\n $sourceId: ID!\n $timerange: TimerangeInput!\n $filterQuery: String\n ) {\n source(id: $sourceId) {\n id\n OverviewHost(timerange: $timerange, filterQuery: $filterQuery) {\n auditbeatAuditd\n auditbeatFIM\n auditbeatLogin\n auditbeatPackage\n auditbeatProcess\n auditbeatUser\n }\n }\n }', - }, -}; - -export const mockResponseHost = { - took: 89, - timed_out: false, - _shards: { total: 18, successful: 18, skipped: 0, failed: 0 }, - hits: { total: { value: 950867, relation: 'eq' }, max_score: null, hits: [] }, - aggregations: { - auditd_count: { doc_count: 73847 }, - endgame_module: { - doc_count: 6258, - dns_event_count: { doc_count: 891 }, - file_event_count: { doc_count: 892 }, - image_load_event_count: { doc_count: 893 }, - network_event_count: { doc_count: 894 }, - process_event_count: { doc_count: 895 }, - registry_event: { doc_count: 896 }, - security_event_count: { doc_count: 897 }, - }, - fim_count: { doc_count: 107307 }, - system_module: { - doc_count: 20000000, - login_count: { doc_count: 60015 }, - package_count: { doc_count: 2003 }, - process_count: { doc_count: 1200 }, - user_count: { doc_count: 1979 }, - filebeat_count: { doc_count: 225 }, - }, - winlog_module: { - security_event_count: { - doc_count: 523, - }, - mwsysmon_operational_event_count: { - doc_count: 214, - }, - }, - }, -}; - -export const mockResultHost = { - inspect: { - dsl: [JSON.stringify(mockBuildOverviewHostQuery, null, 2)], - response: [JSON.stringify(mockResponseHost, null, 2)], - }, - auditbeatAuditd: 73847, - auditbeatFIM: 107307, - auditbeatLogin: 60015, - auditbeatPackage: 2003, - auditbeatProcess: 1200, - auditbeatUser: 1979, - endgameDns: 891, - endgameFile: 892, - endgameImageLoad: 893, - endgameNetwork: 894, - endgameProcess: 895, - endgameRegistry: 896, - endgameSecurity: 897, - filebeatSystemModule: 225, - winlogbeatSecurity: 523, - winlogbeatMWSysmonOperational: 214, -}; diff --git a/x-pack/legacy/plugins/siem/server/lib/pinned_event/saved_object.ts b/x-pack/legacy/plugins/siem/server/lib/pinned_event/saved_object.ts deleted file mode 100644 index a95c1da197f57..0000000000000 --- a/x-pack/legacy/plugins/siem/server/lib/pinned_event/saved_object.ts +++ /dev/null @@ -1,225 +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 { failure } from 'io-ts/lib/PathReporter'; -import { getOr } from 'lodash/fp'; -import { pipe } from 'fp-ts/lib/pipeable'; -import { map, fold } from 'fp-ts/lib/Either'; -import { identity } from 'fp-ts/lib/function'; - -import { SavedObjectsFindOptions } from '../../../../../../../src/core/server'; -import { AuthenticatedUser } from '../../../../../../plugins/security/common/model'; -import { UNAUTHENTICATED_USER } from '../../../common/constants'; -import { FrameworkRequest } from '../framework'; -import { - PinnedEventSavedObject, - PinnedEventSavedObjectRuntimeType, - SavedPinnedEvent, -} from './types'; -import { PageInfoNote, SortNote, PinnedEvent as PinnedEventResponse } from '../../graphql/types'; -import { pinnedEventSavedObjectType, timelineSavedObjectType } from '../../saved_objects'; -import { pickSavedTimeline } from '../timeline/pick_saved_timeline'; -import { convertSavedObjectToSavedTimeline } from '../timeline/convert_saved_object_to_savedtimeline'; - -export class PinnedEvent { - public async deletePinnedEventOnTimeline(request: FrameworkRequest, pinnedEventIds: string[]) { - const savedObjectsClient = request.context.core.savedObjects.client; - - await Promise.all( - pinnedEventIds.map(pinnedEventId => - savedObjectsClient.delete(pinnedEventSavedObjectType, pinnedEventId) - ) - ); - } - - public async deleteAllPinnedEventsOnTimeline(request: FrameworkRequest, timelineId: string) { - const savedObjectsClient = request.context.core.savedObjects.client; - const options: SavedObjectsFindOptions = { - type: pinnedEventSavedObjectType, - search: timelineId, - searchFields: ['timelineId'], - }; - const pinnedEventToBeDeleted = await this.getAllSavedPinnedEvents(request, options); - await Promise.all( - pinnedEventToBeDeleted.map(pinnedEvent => - savedObjectsClient.delete(pinnedEventSavedObjectType, pinnedEvent.pinnedEventId) - ) - ); - } - - public async getPinnedEvent( - request: FrameworkRequest, - pinnedEventId: string - ): Promise<PinnedEventSavedObject> { - return this.getSavedPinnedEvent(request, pinnedEventId); - } - - public async getAllPinnedEventsByTimelineId( - request: FrameworkRequest, - timelineId: string - ): Promise<PinnedEventSavedObject[]> { - const options: SavedObjectsFindOptions = { - type: pinnedEventSavedObjectType, - search: timelineId, - searchFields: ['timelineId'], - }; - return this.getAllSavedPinnedEvents(request, options); - } - - public async getAllPinnedEvents( - request: FrameworkRequest, - pageInfo: PageInfoNote | null, - search: string | null, - sort: SortNote | null - ): Promise<PinnedEventSavedObject[]> { - const options: SavedObjectsFindOptions = { - type: pinnedEventSavedObjectType, - perPage: pageInfo != null ? pageInfo.pageSize : undefined, - page: pageInfo != null ? pageInfo.pageIndex : undefined, - search: search != null ? search : undefined, - searchFields: ['timelineId', 'eventId'], - sortField: sort != null ? sort.sortField : undefined, - sortOrder: sort != null ? sort.sortOrder : undefined, - }; - return this.getAllSavedPinnedEvents(request, options); - } - - public async persistPinnedEventOnTimeline( - request: FrameworkRequest, - pinnedEventId: string | null, // pinned event saved object id - eventId: string, - timelineId: string | null - ): Promise<PinnedEventResponse | null> { - const savedObjectsClient = request.context.core.savedObjects.client; - - try { - if (pinnedEventId == null) { - const timelineVersionSavedObject = - timelineId == null - ? await (async () => { - const timelineResult = convertSavedObjectToSavedTimeline( - await savedObjectsClient.create( - timelineSavedObjectType, - pickSavedTimeline(null, {}, request.user || null) - ) - ); - timelineId = timelineResult.savedObjectId; // eslint-disable-line no-param-reassign - return timelineResult.version; - })() - : null; - - if (timelineId != null) { - const allPinnedEventId = await this.getAllPinnedEventsByTimelineId(request, timelineId); - const isPinnedAlreadyExisting = allPinnedEventId.filter( - pinnedEvent => pinnedEvent.eventId === eventId - ); - - if (isPinnedAlreadyExisting.length === 0) { - const savedPinnedEvent: SavedPinnedEvent = { - eventId, - timelineId, - }; - // create Pinned Event on Timeline - return convertSavedObjectToSavedPinnedEvent( - await savedObjectsClient.create( - pinnedEventSavedObjectType, - pickSavedPinnedEvent(pinnedEventId, savedPinnedEvent, request.user || null) - ), - timelineVersionSavedObject != null ? timelineVersionSavedObject : undefined - ); - } - return isPinnedAlreadyExisting[0]; - } - throw new Error('You can NOT pinned event without a timelineID'); - } - // Delete Pinned Event on Timeline - await this.deletePinnedEventOnTimeline(request, [pinnedEventId]); - return null; - } catch (err) { - if (getOr(null, 'output.statusCode', err) === 404) { - /* - * Why we are doing that, because if it is not found for sure that it will be unpinned - * There is no need to bring back this error since we can assume that it is unpinned - */ - return null; - } - if (getOr(null, 'output.statusCode', err) === 403) { - return pinnedEventId != null - ? { - code: 403, - message: err.message, - pinnedEventId: eventId, - timelineId: '', - timelineVersion: '', - } - : null; - } - throw err; - } - } - - private async getSavedPinnedEvent(request: FrameworkRequest, pinnedEventId: string) { - const savedObjectsClient = request.context.core.savedObjects.client; - const savedObject = await savedObjectsClient.get(pinnedEventSavedObjectType, pinnedEventId); - - return convertSavedObjectToSavedPinnedEvent(savedObject); - } - - private async getAllSavedPinnedEvents( - request: FrameworkRequest, - options: SavedObjectsFindOptions - ) { - const savedObjectsClient = request.context.core.savedObjects.client; - const savedObjects = await savedObjectsClient.find(options); - - return savedObjects.saved_objects.map(savedObject => - convertSavedObjectToSavedPinnedEvent(savedObject) - ); - } -} - -export const convertSavedObjectToSavedPinnedEvent = ( - savedObject: unknown, - timelineVersion?: string | undefined | null -): PinnedEventSavedObject => - pipe( - PinnedEventSavedObjectRuntimeType.decode(savedObject), - map(savedPinnedEvent => ({ - pinnedEventId: savedPinnedEvent.id, - version: savedPinnedEvent.version, - timelineVersion, - ...savedPinnedEvent.attributes, - })), - fold(errors => { - throw new Error(failure(errors).join('\n')); - }, identity) - ); - -// we have to use any here because the SavedObjectAttributes interface is like below -// export interface SavedObjectAttributes { -// [key: string]: SavedObjectAttributes | string | number | boolean | null; -// } -// then this interface does not allow types without index signature -// this is limiting us with our type for now so the easy way was to use any - -export const pickSavedPinnedEvent = ( - pinnedEventId: string | null, - savedPinnedEvent: SavedPinnedEvent, - userInfo: AuthenticatedUser | null - // eslint-disable-next-line @typescript-eslint/no-explicit-any -): any => { - const dateNow = new Date().valueOf(); - if (pinnedEventId == null) { - savedPinnedEvent.created = dateNow; - savedPinnedEvent.createdBy = userInfo?.username ?? UNAUTHENTICATED_USER; - savedPinnedEvent.updated = dateNow; - savedPinnedEvent.updatedBy = userInfo?.username ?? UNAUTHENTICATED_USER; - } else if (pinnedEventId != null) { - savedPinnedEvent.updated = dateNow; - savedPinnedEvent.updatedBy = userInfo?.username ?? UNAUTHENTICATED_USER; - } - return savedPinnedEvent; -}; diff --git a/x-pack/legacy/plugins/siem/server/lib/timeline/create_timelines_stream_from_ndjson.ts b/x-pack/legacy/plugins/siem/server/lib/timeline/create_timelines_stream_from_ndjson.ts deleted file mode 100644 index 5373570a4f8cc..0000000000000 --- a/x-pack/legacy/plugins/siem/server/lib/timeline/create_timelines_stream_from_ndjson.ts +++ /dev/null @@ -1,46 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { Transform } from 'stream'; -import { - createConcatStream, - createSplitStream, - createMapStream, -} from '../../../../../../../src/legacy/utils'; -import { - parseNdjsonStrings, - filterExportedCounts, - createLimitStream, -} from '../detection_engine/rules/create_rules_stream_from_ndjson'; -import { importTimelinesSchema } from './routes/schemas/import_timelines_schema'; -import { BadRequestError } from '../detection_engine/errors/bad_request_error'; -import { ImportTimelineResponse } from './routes/utils/import_timelines'; - -export const validateTimelines = (): Transform => { - return createMapStream((obj: ImportTimelineResponse) => { - if (!(obj instanceof Error)) { - const validated = importTimelinesSchema.validate(obj); - if (validated.error != null) { - return new BadRequestError(validated.error.message); - } else { - return validated.value; - } - } else { - return obj; - } - }); -}; - -export const createTimelinesStreamFromNdJson = (ruleLimit: number) => { - return [ - createSplitStream('\n'), - parseNdjsonStrings(), - filterExportedCounts(), - validateTimelines(), - createLimitStream(ruleLimit), - createConcatStream([]), - ]; -}; diff --git a/x-pack/legacy/plugins/siem/server/lib/timeline/routes/__mocks__/request_responses.ts b/x-pack/legacy/plugins/siem/server/lib/timeline/routes/__mocks__/request_responses.ts deleted file mode 100644 index 0e73e4bdd6c97..0000000000000 --- a/x-pack/legacy/plugins/siem/server/lib/timeline/routes/__mocks__/request_responses.ts +++ /dev/null @@ -1,270 +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 { TIMELINE_EXPORT_URL, TIMELINE_IMPORT_URL } from '../../../../../common/constants'; -import { requestMock } from '../../../detection_engine/routes/__mocks__'; - -export const getExportTimelinesRequest = () => - requestMock.create({ - method: 'get', - path: TIMELINE_EXPORT_URL, - body: { - ids: ['f0e58720-57b6-11ea-b88d-3f1a31716be8', '890b8ae0-57df-11ea-a7c9-3976b7f1cb37'], - }, - }); - -export const getImportTimelinesRequest = (filename?: string) => - requestMock.create({ - method: 'post', - path: TIMELINE_IMPORT_URL, - query: { overwrite: false }, - body: { - file: { hapi: { filename: filename ?? 'filename.ndjson' } }, - }, - }); - -export const getImportTimelinesRequestEnableOverwrite = (filename?: string) => - requestMock.create({ - method: 'post', - path: TIMELINE_IMPORT_URL, - query: { overwrite: true }, - body: { - file: { hapi: { filename: filename ?? 'filename.ndjson' } }, - }, - }); - -export const mockTimelinesSavedObjects = () => ({ - saved_objects: [ - { - id: 'f0e58720-57b6-11ea-b88d-3f1a31716be8', - type: 'fakeType', - attributes: {}, - references: [], - }, - { - id: '890b8ae0-57df-11ea-a7c9-3976b7f1cb37', - type: 'fakeType', - attributes: {}, - references: [], - }, - ], -}); - -export const mockTimelines = () => ({ - saved_objects: [ - { - savedObjectId: 'f0e58720-57b6-11ea-b88d-3f1a31716be8', - version: 'Wzk0OSwxXQ==', - columns: [ - { - indexes: null, - name: null, - columnHeaderType: 'not-filtered', - id: '@timestamp', - searchable: null, - }, - { - indexes: null, - name: null, - columnHeaderType: 'not-filtered', - id: 'message', - searchable: null, - }, - { - indexes: null, - name: null, - columnHeaderType: 'not-filtered', - id: 'event.category', - searchable: null, - }, - { - indexes: null, - name: null, - columnHeaderType: 'not-filtered', - id: 'event.action', - searchable: null, - }, - { - indexes: null, - name: null, - columnHeaderType: 'not-filtered', - id: 'host.name', - searchable: null, - }, - { - indexes: null, - name: null, - columnHeaderType: 'not-filtered', - id: 'source.ip', - searchable: null, - }, - { - indexes: null, - name: null, - columnHeaderType: 'not-filtered', - id: 'destination.ip', - searchable: null, - }, - { - indexes: null, - name: null, - columnHeaderType: 'not-filtered', - id: 'user.name', - searchable: null, - }, - ], - dataProviders: [], - description: 'with a global note', - eventType: 'raw', - filters: [], - kqlMode: 'filter', - kqlQuery: { - filterQuery: { - kuery: { kind: 'kuery', expression: 'zeek.files.sha1 : * ' }, - serializedQuery: - '{"bool":{"should":[{"exists":{"field":"zeek.files.sha1"}}],"minimum_should_match":1}}', - }, - }, - title: 'test no.2', - dateRange: { start: 1582538951145, end: 1582625351145 }, - savedQueryId: null, - sort: { columnId: '@timestamp', sortDirection: 'desc' }, - created: 1582625382448, - createdBy: 'elastic', - updated: 1583741197521, - updatedBy: 'elastic', - }, - { - savedObjectId: '890b8ae0-57df-11ea-a7c9-3976b7f1cb37', - version: 'Wzk0NywxXQ==', - columns: [ - { - indexes: null, - name: null, - columnHeaderType: 'not-filtered', - id: '@timestamp', - searchable: null, - }, - { - indexes: null, - name: null, - columnHeaderType: 'not-filtered', - id: 'message', - searchable: null, - }, - { - indexes: null, - name: null, - columnHeaderType: 'not-filtered', - id: 'event.category', - searchable: null, - }, - { - indexes: null, - name: null, - columnHeaderType: 'not-filtered', - id: 'event.action', - searchable: null, - }, - { - indexes: null, - name: null, - columnHeaderType: 'not-filtered', - id: 'host.name', - searchable: null, - }, - { - indexes: null, - name: null, - columnHeaderType: 'not-filtered', - id: 'source.ip', - searchable: null, - }, - { - indexes: null, - name: null, - columnHeaderType: 'not-filtered', - id: 'destination.ip', - searchable: null, - }, - { - indexes: null, - name: null, - columnHeaderType: 'not-filtered', - id: 'user.name', - searchable: null, - }, - ], - dataProviders: [], - description: 'with an event note', - eventType: 'raw', - filters: [], - kqlMode: 'filter', - kqlQuery: { - filterQuery: { - serializedQuery: - '{"bool":{"should":[{"exists":{"field":"zeek.files.sha1"}}],"minimum_should_match":1}}', - kuery: { expression: 'zeek.files.sha1 : * ', kind: 'kuery' }, - }, - }, - title: 'test no.3', - dateRange: { start: 1582538951145, end: 1582625351145 }, - savedQueryId: null, - sort: { columnId: '@timestamp', sortDirection: 'desc' }, - created: 1582642817439, - createdBy: 'elastic', - updated: 1583741175216, - updatedBy: 'elastic', - }, - ], -}); - -export const mockNotesSavedObjects = () => ({ - saved_objects: [ - { - id: 'eb3f3930-61dc-11ea-8a49-e77254c5b742', - type: 'fakeType', - attributes: {}, - references: [], - }, - { - id: '706e7510-5d52-11ea-8f07-0392944939c1', - type: 'fakeType', - attributes: {}, - references: [], - }, - ], -}); - -export const mockNotes = () => ({ - saved_objects: [ - { - noteId: 'eb3f3930-61dc-11ea-8a49-e77254c5b742', - version: 'Wzk1MCwxXQ==', - note: 'Global note', - timelineId: 'f0e58720-57b6-11ea-b88d-3f1a31716be8', - created: 1583741205473, - createdBy: 'elastic', - updated: 1583741205473, - updatedBy: 'elastic', - }, - { - noteId: '706e7510-5d52-11ea-8f07-0392944939c1', - version: 'WzEwMiwxXQ==', - eventId: '6HW_eHABMQha2n6bHvQ0', - note: 'this is a note!!', - timelineId: '890b8ae0-57df-11ea-a7c9-3976b7f1cb37', - created: 1583241924223, - createdBy: 'elastic', - updated: 1583241924223, - updatedBy: 'elastic', - }, - ], -}); - -export const mockPinnedEvents = () => ({ - saved_objects: [], -}); diff --git a/x-pack/legacy/plugins/siem/server/lib/timeline/routes/export_timelines_route.test.ts b/x-pack/legacy/plugins/siem/server/lib/timeline/routes/export_timelines_route.test.ts deleted file mode 100644 index fe434b5399212..0000000000000 --- a/x-pack/legacy/plugins/siem/server/lib/timeline/routes/export_timelines_route.test.ts +++ /dev/null @@ -1,97 +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 { - mockTimelines, - mockNotes, - mockTimelinesSavedObjects, - mockPinnedEvents, - getExportTimelinesRequest, -} from './__mocks__/request_responses'; -import { exportTimelinesRoute } from './export_timelines_route'; -import { - serverMock, - requestContextMock, - requestMock, -} from '../../detection_engine/routes/__mocks__'; -import { TIMELINE_EXPORT_URL } from '../../../../common/constants'; -import { convertSavedObjectToSavedNote } from '../../note/saved_object'; -import { convertSavedObjectToSavedPinnedEvent } from '../../pinned_event/saved_object'; -import { convertSavedObjectToSavedTimeline } from '../convert_saved_object_to_savedtimeline'; -jest.mock('../convert_saved_object_to_savedtimeline', () => { - return { - convertSavedObjectToSavedTimeline: jest.fn(), - }; -}); - -jest.mock('../../note/saved_object', () => { - return { - convertSavedObjectToSavedNote: jest.fn(), - }; -}); - -jest.mock('../../pinned_event/saved_object', () => { - return { - convertSavedObjectToSavedPinnedEvent: jest.fn(), - }; -}); -describe('export timelines', () => { - let server: ReturnType<typeof serverMock.create>; - let { clients, context } = requestContextMock.createTools(); - const config = jest.fn().mockImplementation(() => { - return { - get: () => { - return 100; - }, - has: jest.fn(), - }; - }); - - beforeEach(() => { - server = serverMock.create(); - ({ clients, context } = requestContextMock.createTools()); - - clients.savedObjectsClient.bulkGet.mockResolvedValue(mockTimelinesSavedObjects()); - - ((convertSavedObjectToSavedTimeline as unknown) as jest.Mock).mockReturnValue(mockTimelines()); - ((convertSavedObjectToSavedNote as unknown) as jest.Mock).mockReturnValue(mockNotes()); - ((convertSavedObjectToSavedPinnedEvent as unknown) as jest.Mock).mockReturnValue( - mockPinnedEvents() - ); - exportTimelinesRoute(server.router, config); - }); - - describe('status codes', () => { - test('returns 200 when finding selected timelines', async () => { - const response = await server.inject(getExportTimelinesRequest(), context); - expect(response.status).toEqual(200); - }); - - test('catch error when status search throws error', async () => { - clients.savedObjectsClient.bulkGet.mockReset(); - clients.savedObjectsClient.bulkGet.mockRejectedValue(new Error('Test error')); - const response = await server.inject(getExportTimelinesRequest(), context); - expect(response.status).toEqual(500); - expect(response.body).toEqual({ - message: 'Test error', - status_code: 500, - }); - }); - }); - - describe('request validation', () => { - test('disallows singular id query param', async () => { - const request = requestMock.create({ - method: 'get', - path: TIMELINE_EXPORT_URL, - body: { id: 'someId' }, - }); - const result = server.validate(request); - - expect(result.badRequest).toHaveBeenCalledWith('"id" is not allowed'); - }); - }); -}); diff --git a/x-pack/legacy/plugins/siem/server/lib/timeline/routes/export_timelines_route.ts b/x-pack/legacy/plugins/siem/server/lib/timeline/routes/export_timelines_route.ts deleted file mode 100644 index b8e7be13fff34..0000000000000 --- a/x-pack/legacy/plugins/siem/server/lib/timeline/routes/export_timelines_route.ts +++ /dev/null @@ -1,75 +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 { set as _set } from 'lodash/fp'; -import { IRouter } from '../../../../../../../../src/core/server'; -import { LegacyServices } from '../../../types'; -import { ExportTimelineRequestParams } from '../types'; - -import { - transformError, - buildRouteValidation, - buildSiemResponse, -} from '../../detection_engine/routes/utils'; -import { TIMELINE_EXPORT_URL } from '../../../../common/constants'; - -import { - exportTimelinesSchema, - exportTimelinesQuerySchema, -} from './schemas/export_timelines_schema'; - -import { getExportTimelineByObjectIds } from './utils/export_timelines'; - -export const exportTimelinesRoute = (router: IRouter, config: LegacyServices['config']) => { - router.post( - { - path: TIMELINE_EXPORT_URL, - validate: { - query: buildRouteValidation<ExportTimelineRequestParams['query']>( - exportTimelinesQuerySchema - ), - body: buildRouteValidation<ExportTimelineRequestParams['body']>(exportTimelinesSchema), - }, - options: { - tags: ['access:siem'], - }, - }, - async (context, request, response) => { - try { - const siemResponse = buildSiemResponse(response); - const savedObjectsClient = context.core.savedObjects.client; - const exportSizeLimit = config().get<number>('savedObjects.maxImportExportSize'); - if (request.body?.ids != null && request.body.ids.length > exportSizeLimit) { - return siemResponse.error({ - statusCode: 400, - body: `Can't export more than ${exportSizeLimit} timelines`, - }); - } - - const responseBody = await getExportTimelineByObjectIds({ - client: savedObjectsClient, - request, - }); - - return response.ok({ - headers: { - 'Content-Disposition': `attachment; filename="${request.query.file_name}"`, - 'Content-Type': 'application/ndjson', - }, - body: responseBody, - }); - } catch (err) { - const error = transformError(err); - const siemResponse = buildSiemResponse(response); - - return siemResponse.error({ - body: error.message, - statusCode: error.statusCode, - }); - } - } - ); -}; diff --git a/x-pack/legacy/plugins/siem/server/lib/timeline/routes/schemas/export_timelines_schema.ts b/x-pack/legacy/plugins/siem/server/lib/timeline/routes/schemas/export_timelines_schema.ts deleted file mode 100644 index 04edbbd7046c9..0000000000000 --- a/x-pack/legacy/plugins/siem/server/lib/timeline/routes/schemas/export_timelines_schema.ts +++ /dev/null @@ -1,20 +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 Joi from 'joi'; - -/* eslint-disable @typescript-eslint/camelcase */ -import { ids, exclude_export_details, file_name } from './schemas'; -/* eslint-disable @typescript-eslint/camelcase */ - -export const exportTimelinesSchema = Joi.object({ - ids, -}).min(1); - -export const exportTimelinesQuerySchema = Joi.object({ - file_name: file_name.default('export.ndjson'), - exclude_export_details: exclude_export_details.default(false), -}); diff --git a/x-pack/legacy/plugins/siem/server/lib/timeline/routes/schemas/import_timelines_schema.ts b/x-pack/legacy/plugins/siem/server/lib/timeline/routes/schemas/import_timelines_schema.ts deleted file mode 100644 index 61ffa9681c53a..0000000000000 --- a/x-pack/legacy/plugins/siem/server/lib/timeline/routes/schemas/import_timelines_schema.ts +++ /dev/null @@ -1,57 +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 Joi from 'joi'; -import { - columns, - created, - createdBy, - dataProviders, - dateRange, - description, - eventNotes, - eventType, - favorite, - filters, - globalNotes, - kqlMode, - kqlQuery, - savedObjectId, - savedQueryId, - sort, - title, - updated, - updatedBy, - version, - pinnedEventIds, -} from './schemas'; - -export const importTimelinesPayloadSchema = Joi.object({ - file: Joi.object().required(), -}); - -export const importTimelinesSchema = Joi.object({ - columns, - created, - createdBy, - dataProviders, - dateRange, - description, - eventNotes, - eventType, - filters, - favorite, - globalNotes, - kqlMode, - kqlQuery, - savedObjectId, - savedQueryId, - sort, - title, - updated, - updatedBy, - version, - pinnedEventIds, -}); diff --git a/x-pack/legacy/plugins/siem/server/lib/timeline/routes/schemas/schemas.ts b/x-pack/legacy/plugins/siem/server/lib/timeline/routes/schemas/schemas.ts deleted file mode 100644 index fc87a775a9e68..0000000000000 --- a/x-pack/legacy/plugins/siem/server/lib/timeline/routes/schemas/schemas.ts +++ /dev/null @@ -1,158 +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 Joi from 'joi'; - -const allowEmptyString = Joi.string().allow([null, '']); -const columnHeaderType = allowEmptyString; -export const created = Joi.number().allow(null); -export const createdBy = allowEmptyString; - -export const description = allowEmptyString; -export const end = Joi.number(); -export const eventId = allowEmptyString; -export const eventType = allowEmptyString; - -export const filters = Joi.array() - .items( - Joi.object({ - meta: Joi.object({ - alias: allowEmptyString, - controlledBy: allowEmptyString, - disabled: Joi.boolean().allow(null), - field: allowEmptyString, - formattedValue: allowEmptyString, - index: allowEmptyString, - key: allowEmptyString, - negate: Joi.boolean().allow(null), - params: allowEmptyString, - type: allowEmptyString, - value: allowEmptyString, - }), - exists: allowEmptyString, - match_all: allowEmptyString, - missing: allowEmptyString, - query: allowEmptyString, - range: allowEmptyString, - script: allowEmptyString, - }) - ) - .allow(null); - -const name = allowEmptyString; - -export const noteId = allowEmptyString; -export const note = allowEmptyString; - -export const start = Joi.number(); -export const savedQueryId = allowEmptyString; -export const savedObjectId = allowEmptyString; - -export const timelineId = allowEmptyString; -export const title = allowEmptyString; - -export const updated = Joi.number().allow(null); -export const updatedBy = allowEmptyString; -export const version = allowEmptyString; - -export const columns = Joi.array().items( - Joi.object({ - aggregatable: Joi.boolean().allow(null), - category: allowEmptyString, - columnHeaderType, - description, - example: allowEmptyString, - indexes: allowEmptyString, - id: allowEmptyString, - name, - placeholder: allowEmptyString, - searchable: Joi.boolean().allow(null), - type: allowEmptyString, - }).required() -); -export const dataProviders = Joi.array() - .items( - Joi.object({ - id: allowEmptyString, - name: allowEmptyString, - enabled: Joi.boolean().allow(null), - excluded: Joi.boolean().allow(null), - kqlQuery: allowEmptyString, - queryMatch: Joi.object({ - field: allowEmptyString, - displayField: allowEmptyString, - value: allowEmptyString, - displayValue: allowEmptyString, - operator: allowEmptyString, - }), - and: Joi.array() - .items( - Joi.object({ - id: allowEmptyString, - name, - enabled: Joi.boolean().allow(null), - excluded: Joi.boolean().allow(null), - kqlQuery: allowEmptyString, - queryMatch: Joi.object({ - field: allowEmptyString, - displayField: allowEmptyString, - value: allowEmptyString, - displayValue: allowEmptyString, - operator: allowEmptyString, - }).allow(null), - }) - ) - .allow(null), - }) - ) - .allow(null); -export const dateRange = Joi.object({ - start, - end, -}); -export const favorite = Joi.array().items( - Joi.object({ - keySearch: allowEmptyString, - fullName: allowEmptyString, - userName: allowEmptyString, - favoriteDate: Joi.number(), - }).allow(null) -); -const noteItem = Joi.object({ - noteId, - version, - eventId, - note, - timelineId, - created, - createdBy, - updated, - updatedBy, -}); -export const eventNotes = Joi.array().items(noteItem); -export const globalNotes = Joi.array().items(noteItem); -export const kqlMode = allowEmptyString; -export const kqlQuery = Joi.object({ - filterQuery: Joi.object({ - kuery: Joi.object({ - kind: allowEmptyString, - expression: allowEmptyString, - }).allow(null), - serializedQuery: allowEmptyString, - }).allow(null), -}); -export const pinnedEventIds = Joi.array() - .items(allowEmptyString) - .allow(null); -export const sort = Joi.object({ - columnId: allowEmptyString, - sortDirection: allowEmptyString, -}); -/* eslint-disable @typescript-eslint/camelcase */ - -export const ids = Joi.array().items(allowEmptyString); - -export const exclude_export_details = Joi.boolean(); -export const file_name = allowEmptyString; diff --git a/x-pack/legacy/plugins/siem/server/lib/timeline/saved_object.ts b/x-pack/legacy/plugins/siem/server/lib/timeline/saved_object.ts deleted file mode 100644 index bc6975331ad9b..0000000000000 --- a/x-pack/legacy/plugins/siem/server/lib/timeline/saved_object.ts +++ /dev/null @@ -1,293 +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 { getOr } from 'lodash/fp'; - -import { SavedObjectsFindOptions } from '../../../../../../../src/core/server'; -import { UNAUTHENTICATED_USER } from '../../../common/constants'; -import { - ResponseTimeline, - PageInfoTimeline, - SortTimeline, - ResponseFavoriteTimeline, - TimelineResult, -} from '../../graphql/types'; -import { FrameworkRequest } from '../framework'; -import { Note } from '../note/saved_object'; -import { NoteSavedObject } from '../note/types'; -import { PinnedEventSavedObject } from '../pinned_event/types'; -import { PinnedEvent } from '../pinned_event/saved_object'; -import { convertSavedObjectToSavedTimeline } from './convert_saved_object_to_savedtimeline'; -import { pickSavedTimeline } from './pick_saved_timeline'; -import { timelineSavedObjectType } from './saved_object_mappings'; -import { SavedTimeline, TimelineSavedObject } from './types'; - -interface ResponseTimelines { - timeline: TimelineSavedObject[]; - totalCount: number; -} - -export class Timeline { - private readonly note = new Note(); - private readonly pinnedEvent = new PinnedEvent(); - - public async getTimeline( - request: FrameworkRequest, - timelineId: string - ): Promise<TimelineSavedObject> { - return this.getSavedTimeline(request, timelineId); - } - - public async getAllTimeline( - request: FrameworkRequest, - onlyUserFavorite: boolean | null, - pageInfo: PageInfoTimeline | null, - search: string | null, - sort: SortTimeline | null - ): Promise<ResponseTimelines> { - const options: SavedObjectsFindOptions = { - type: timelineSavedObjectType, - perPage: pageInfo != null ? pageInfo.pageSize : undefined, - page: pageInfo != null ? pageInfo.pageIndex : undefined, - search: search != null ? search : undefined, - searchFields: onlyUserFavorite - ? ['title', 'description', 'favorite.keySearch'] - : ['title', 'description'], - sortField: sort != null ? sort.sortField : undefined, - sortOrder: sort != null ? sort.sortOrder : undefined, - }; - - return this.getAllSavedTimeline(request, options); - } - - public async persistFavorite( - request: FrameworkRequest, - timelineId: string | null - ): Promise<ResponseFavoriteTimeline> { - const userName = request.user?.username ?? UNAUTHENTICATED_USER; - const fullName = request.user?.full_name ?? ''; - try { - let timeline: SavedTimeline = {}; - if (timelineId != null) { - const { - eventIdToNoteIds, - notes, - noteIds, - pinnedEventIds, - pinnedEventsSaveObject, - savedObjectId, - version, - ...savedTimeline - } = await this.getBasicSavedTimeline(request, timelineId); - timelineId = savedObjectId; // eslint-disable-line no-param-reassign - timeline = savedTimeline; - } - - const userFavoriteTimeline = { - keySearch: userName != null ? convertStringToBase64(userName) : null, - favoriteDate: new Date().valueOf(), - fullName, - userName, - }; - if (timeline.favorite != null) { - const alreadyExistsTimelineFavoriteByUser = timeline.favorite.findIndex( - user => user.userName === userName - ); - - timeline.favorite = - alreadyExistsTimelineFavoriteByUser > -1 - ? [ - ...timeline.favorite.slice(0, alreadyExistsTimelineFavoriteByUser), - ...timeline.favorite.slice(alreadyExistsTimelineFavoriteByUser + 1), - ] - : [...timeline.favorite, userFavoriteTimeline]; - } else if (timeline.favorite == null) { - timeline.favorite = [userFavoriteTimeline]; - } - - const persistResponse = await this.persistTimeline(request, timelineId, null, timeline); - return { - savedObjectId: persistResponse.timeline.savedObjectId, - version: persistResponse.timeline.version, - favorite: - persistResponse.timeline.favorite != null - ? persistResponse.timeline.favorite.filter(fav => fav.userName === userName) - : [], - }; - } catch (err) { - if (getOr(null, 'output.statusCode', err) === 403) { - return { - savedObjectId: '', - version: '', - favorite: [], - code: 403, - message: err.message, - }; - } - throw err; - } - } - - public async persistTimeline( - request: FrameworkRequest, - timelineId: string | null, - version: string | null, - timeline: SavedTimeline - ): Promise<ResponseTimeline> { - const savedObjectsClient = request.context.core.savedObjects.client; - try { - if (timelineId == null) { - // Create new timeline - const newTimeline = convertSavedObjectToSavedTimeline( - await savedObjectsClient.create( - timelineSavedObjectType, - pickSavedTimeline(timelineId, timeline, request.user) - ) - ); - return { - code: 200, - message: 'success', - timeline: newTimeline, - }; - } - // Update Timeline - await savedObjectsClient.update( - timelineSavedObjectType, - timelineId, - pickSavedTimeline(timelineId, timeline, request.user), - { - version: version || undefined, - } - ); - - return { - code: 200, - message: 'success', - timeline: await this.getSavedTimeline(request, timelineId), - }; - } catch (err) { - if (timelineId != null && savedObjectsClient.errors.isConflictError(err)) { - return { - code: 409, - message: err.message, - timeline: await this.getSavedTimeline(request, timelineId), - }; - } else if (getOr(null, 'output.statusCode', err) === 403) { - const timelineToReturn: TimelineResult = { - ...timeline, - savedObjectId: '', - version: '', - }; - return { - code: 403, - message: err.message, - timeline: timelineToReturn, - }; - } - throw err; - } - } - - public async deleteTimeline(request: FrameworkRequest, timelineIds: string[]) { - const savedObjectsClient = request.context.core.savedObjects.client; - - await Promise.all( - timelineIds.map(timelineId => - Promise.all([ - savedObjectsClient.delete(timelineSavedObjectType, timelineId), - this.note.deleteNoteByTimelineId(request, timelineId), - this.pinnedEvent.deleteAllPinnedEventsOnTimeline(request, timelineId), - ]) - ) - ); - } - - private async getBasicSavedTimeline(request: FrameworkRequest, timelineId: string) { - const savedObjectsClient = request.context.core.savedObjects.client; - const savedObject = await savedObjectsClient.get(timelineSavedObjectType, timelineId); - - return convertSavedObjectToSavedTimeline(savedObject); - } - - private async getSavedTimeline(request: FrameworkRequest, timelineId: string) { - const userName = request.user?.username ?? UNAUTHENTICATED_USER; - - const savedObjectsClient = request.context.core.savedObjects.client; - const savedObject = await savedObjectsClient.get(timelineSavedObjectType, timelineId); - const timelineSaveObject = convertSavedObjectToSavedTimeline(savedObject); - const timelineWithNotesAndPinnedEvents = await Promise.all([ - this.note.getNotesByTimelineId(request, timelineSaveObject.savedObjectId), - this.pinnedEvent.getAllPinnedEventsByTimelineId(request, timelineSaveObject.savedObjectId), - Promise.resolve(timelineSaveObject), - ]); - - const [notes, pinnedEvents, timeline] = timelineWithNotesAndPinnedEvents; - - return timelineWithReduxProperties(notes, pinnedEvents, timeline, userName); - } - - private async getAllSavedTimeline(request: FrameworkRequest, options: SavedObjectsFindOptions) { - const userName = request.user?.username ?? UNAUTHENTICATED_USER; - const savedObjectsClient = request.context.core.savedObjects.client; - if (options.searchFields != null && options.searchFields.includes('favorite.keySearch')) { - options.search = `${options.search != null ? options.search : ''} ${ - userName != null ? convertStringToBase64(userName) : null - }`; - } - - const savedObjects = await savedObjectsClient.find(options); - - const timelinesWithNotesAndPinnedEvents = await Promise.all( - savedObjects.saved_objects.map(async savedObject => { - const timelineSaveObject = convertSavedObjectToSavedTimeline(savedObject); - return Promise.all([ - this.note.getNotesByTimelineId(request, timelineSaveObject.savedObjectId), - this.pinnedEvent.getAllPinnedEventsByTimelineId( - request, - timelineSaveObject.savedObjectId - ), - Promise.resolve(timelineSaveObject), - ]); - }) - ); - - return { - totalCount: savedObjects.total, - timeline: timelinesWithNotesAndPinnedEvents.map(([notes, pinnedEvents, timeline]) => - timelineWithReduxProperties(notes, pinnedEvents, timeline, userName) - ), - }; - } -} - -export const convertStringToBase64 = (text: string): string => Buffer.from(text).toString('base64'); - -// we have to use any here because the SavedObjectAttributes interface is like below -// export interface SavedObjectAttributes { -// [key: string]: SavedObjectAttributes | string | number | boolean | null; -// } -// then this interface does not allow types without index signature -// this is limiting us with our type for now so the easy way was to use any - -export const timelineWithReduxProperties = ( - notes: NoteSavedObject[], - pinnedEvents: PinnedEventSavedObject[], - timeline: TimelineSavedObject, - userName: string -): TimelineSavedObject => ({ - ...timeline, - favorite: - timeline.favorite != null && userName != null - ? timeline.favorite.filter(fav => fav.userName === userName) - : [], - eventIdToNoteIds: notes.filter(note => note.eventId != null), - noteIds: notes - .filter(note => note.eventId == null && note.noteId != null) - .map(note => note.noteId), - notes, - pinnedEventIds: pinnedEvents.map(pinnedEvent => pinnedEvent.eventId), - pinnedEventsSaveObject: pinnedEvents, -}); diff --git a/x-pack/legacy/plugins/siem/server/lib/timeline/types.ts b/x-pack/legacy/plugins/siem/server/lib/timeline/types.ts deleted file mode 100644 index 35bf86c17db7e..0000000000000 --- a/x-pack/legacy/plugins/siem/server/lib/timeline/types.ts +++ /dev/null @@ -1,256 +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. - */ - -/* eslint-disable @typescript-eslint/no-empty-interface */ - -import * as runtimeTypes from 'io-ts'; - -import { unionWithNullType } from '../framework'; -import { NoteSavedObjectToReturnRuntimeType, NoteSavedObject } from '../note/types'; -import { - PinnedEventToReturnSavedObjectRuntimeType, - PinnedEventSavedObject, -} from '../pinned_event/types'; -import { SavedObjectsClient, KibanaRequest } from '../../../../../../../src/core/server'; - -/* - * ColumnHeader Types - */ -const SavedColumnHeaderRuntimeType = runtimeTypes.partial({ - aggregatable: unionWithNullType(runtimeTypes.boolean), - category: unionWithNullType(runtimeTypes.string), - columnHeaderType: unionWithNullType(runtimeTypes.string), - description: unionWithNullType(runtimeTypes.string), - example: unionWithNullType(runtimeTypes.string), - indexes: unionWithNullType(runtimeTypes.array(runtimeTypes.string)), - id: unionWithNullType(runtimeTypes.string), - name: unionWithNullType(runtimeTypes.string), - placeholder: unionWithNullType(runtimeTypes.string), - searchable: unionWithNullType(runtimeTypes.boolean), - type: unionWithNullType(runtimeTypes.string), -}); - -/* - * DataProvider Types - */ -const SavedDataProviderQueryMatchBasicRuntimeType = runtimeTypes.partial({ - field: unionWithNullType(runtimeTypes.string), - displayField: unionWithNullType(runtimeTypes.string), - value: unionWithNullType(runtimeTypes.string), - displayValue: unionWithNullType(runtimeTypes.string), - operator: unionWithNullType(runtimeTypes.string), -}); - -const SavedDataProviderQueryMatchRuntimeType = runtimeTypes.partial({ - id: unionWithNullType(runtimeTypes.string), - name: unionWithNullType(runtimeTypes.string), - enabled: unionWithNullType(runtimeTypes.boolean), - excluded: unionWithNullType(runtimeTypes.boolean), - kqlQuery: unionWithNullType(runtimeTypes.string), - queryMatch: unionWithNullType(SavedDataProviderQueryMatchBasicRuntimeType), -}); - -const SavedDataProviderRuntimeType = runtimeTypes.partial({ - id: unionWithNullType(runtimeTypes.string), - name: unionWithNullType(runtimeTypes.string), - enabled: unionWithNullType(runtimeTypes.boolean), - excluded: unionWithNullType(runtimeTypes.boolean), - kqlQuery: unionWithNullType(runtimeTypes.string), - queryMatch: unionWithNullType(SavedDataProviderQueryMatchBasicRuntimeType), - and: unionWithNullType(runtimeTypes.array(SavedDataProviderQueryMatchRuntimeType)), -}); - -/* - * Filters Types - */ -const SavedFilterMetaRuntimeType = runtimeTypes.partial({ - alias: unionWithNullType(runtimeTypes.string), - controlledBy: unionWithNullType(runtimeTypes.string), - disabled: unionWithNullType(runtimeTypes.boolean), - field: unionWithNullType(runtimeTypes.string), - formattedValue: unionWithNullType(runtimeTypes.string), - index: unionWithNullType(runtimeTypes.string), - key: unionWithNullType(runtimeTypes.string), - negate: unionWithNullType(runtimeTypes.boolean), - params: unionWithNullType(runtimeTypes.string), - type: unionWithNullType(runtimeTypes.string), - value: unionWithNullType(runtimeTypes.string), -}); - -const SavedFilterRuntimeType = runtimeTypes.partial({ - exists: unionWithNullType(runtimeTypes.string), - meta: unionWithNullType(SavedFilterMetaRuntimeType), - match_all: unionWithNullType(runtimeTypes.string), - missing: unionWithNullType(runtimeTypes.string), - query: unionWithNullType(runtimeTypes.string), - range: unionWithNullType(runtimeTypes.string), - script: unionWithNullType(runtimeTypes.string), -}); - -/* - * kqlQuery -> filterQuery Types - */ -const SavedKueryFilterQueryRuntimeType = runtimeTypes.partial({ - kind: unionWithNullType(runtimeTypes.string), - expression: unionWithNullType(runtimeTypes.string), -}); - -const SavedSerializedFilterQueryQueryRuntimeType = runtimeTypes.partial({ - kuery: unionWithNullType(SavedKueryFilterQueryRuntimeType), - serializedQuery: unionWithNullType(runtimeTypes.string), -}); - -const SavedFilterQueryQueryRuntimeType = runtimeTypes.partial({ - filterQuery: unionWithNullType(SavedSerializedFilterQueryQueryRuntimeType), -}); - -/* - * DatePicker Range Types - */ -const SavedDateRangePickerRuntimeType = runtimeTypes.partial({ - start: unionWithNullType(runtimeTypes.number), - end: unionWithNullType(runtimeTypes.number), -}); - -/* - * Favorite Types - */ -const SavedFavoriteRuntimeType = runtimeTypes.partial({ - keySearch: unionWithNullType(runtimeTypes.string), - favoriteDate: unionWithNullType(runtimeTypes.number), - fullName: unionWithNullType(runtimeTypes.string), - userName: unionWithNullType(runtimeTypes.string), -}); - -/* - * Sort Types - */ -const SavedSortRuntimeType = runtimeTypes.partial({ - columnId: unionWithNullType(runtimeTypes.string), - sortDirection: unionWithNullType(runtimeTypes.string), -}); - -/* - * Timeline Types - */ -export const SavedTimelineRuntimeType = runtimeTypes.partial({ - columns: unionWithNullType(runtimeTypes.array(SavedColumnHeaderRuntimeType)), - dataProviders: unionWithNullType(runtimeTypes.array(SavedDataProviderRuntimeType)), - description: unionWithNullType(runtimeTypes.string), - eventType: unionWithNullType(runtimeTypes.string), - favorite: unionWithNullType(runtimeTypes.array(SavedFavoriteRuntimeType)), - filters: unionWithNullType(runtimeTypes.array(SavedFilterRuntimeType)), - kqlMode: unionWithNullType(runtimeTypes.string), - kqlQuery: unionWithNullType(SavedFilterQueryQueryRuntimeType), - title: unionWithNullType(runtimeTypes.string), - dateRange: unionWithNullType(SavedDateRangePickerRuntimeType), - savedQueryId: unionWithNullType(runtimeTypes.string), - sort: unionWithNullType(SavedSortRuntimeType), - created: unionWithNullType(runtimeTypes.number), - createdBy: unionWithNullType(runtimeTypes.string), - updated: unionWithNullType(runtimeTypes.number), - updatedBy: unionWithNullType(runtimeTypes.string), -}); - -export interface SavedTimeline extends runtimeTypes.TypeOf<typeof SavedTimelineRuntimeType> {} - -export interface SavedTimelineNote extends runtimeTypes.TypeOf<typeof SavedTimelineRuntimeType> {} - -/** - * Timeline Saved object type with metadata - */ - -export const TimelineSavedObjectRuntimeType = runtimeTypes.intersection([ - runtimeTypes.type({ - id: runtimeTypes.string, - attributes: SavedTimelineRuntimeType, - version: runtimeTypes.string, - }), - runtimeTypes.partial({ - savedObjectId: runtimeTypes.string, - }), -]); - -export const TimelineSavedToReturnObjectRuntimeType = runtimeTypes.intersection([ - SavedTimelineRuntimeType, - runtimeTypes.type({ - savedObjectId: runtimeTypes.string, - version: runtimeTypes.string, - }), - runtimeTypes.partial({ - eventIdToNoteIds: runtimeTypes.array(NoteSavedObjectToReturnRuntimeType), - noteIds: runtimeTypes.array(runtimeTypes.string), - notes: runtimeTypes.array(NoteSavedObjectToReturnRuntimeType), - pinnedEventIds: runtimeTypes.array(runtimeTypes.string), - pinnedEventsSaveObject: runtimeTypes.array(PinnedEventToReturnSavedObjectRuntimeType), - }), -]); - -export interface TimelineSavedObject - extends runtimeTypes.TypeOf<typeof TimelineSavedToReturnObjectRuntimeType> {} - -/** - * All Timeline Saved object type with metadata - */ - -export const AllTimelineSavedObjectRuntimeType = runtimeTypes.type({ - total: runtimeTypes.number, - data: TimelineSavedToReturnObjectRuntimeType, -}); - -export interface AllTimelineSavedObject - extends runtimeTypes.TypeOf<typeof AllTimelineSavedObjectRuntimeType> {} - -export interface ExportTimelineRequestParams { - body: { ids: string[] }; - query: { - file_name: string; - exclude_export_details: boolean; - }; -} - -export type ExportTimelineRequest = KibanaRequest< - unknown, - ExportTimelineRequestParams['query'], - ExportTimelineRequestParams['body'], - 'post' ->; - -export type ExportTimelineSavedObjectsClient = Pick< - SavedObjectsClient, - | 'get' - | 'errors' - | 'create' - | 'bulkCreate' - | 'delete' - | 'find' - | 'bulkGet' - | 'update' - | 'bulkUpdate' ->; - -export type ExportedGlobalNotes = Array<Exclude<NoteSavedObject, 'eventId'>>; -export type ExportedEventNotes = NoteSavedObject[]; - -export interface ExportedNotes { - eventNotes: ExportedEventNotes; - globalNotes: ExportedGlobalNotes; -} - -export type ExportedTimelines = TimelineSavedObject & - ExportedNotes & { - pinnedEventIds: string[]; - }; - -export interface BulkGetInput { - type: string; - id: string; -} - -export type NotesAndPinnedEventsByTimelineId = Record< - string, - { notes: NoteSavedObject[]; pinnedEvents: PinnedEventSavedObject[] } ->; diff --git a/x-pack/legacy/plugins/siem/server/lib/tls/types.ts b/x-pack/legacy/plugins/siem/server/lib/tls/types.ts deleted file mode 100644 index 1fbb31ba3e0f3..0000000000000 --- a/x-pack/legacy/plugins/siem/server/lib/tls/types.ts +++ /dev/null @@ -1,36 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { FrameworkRequest, RequestBasicOptions } from '../framework'; -import { TlsData } from '../../../public/graphql/types'; - -export interface TlsAdapter { - getTls(request: FrameworkRequest, options: RequestBasicOptions): Promise<TlsData>; -} - -export interface TlsBuckets { - key: string; - timestamp?: { - value: number; - value_as_string: string; - }; - - subjects: { - buckets: Readonly<Array<{ key: string; doc_count: number }>>; - }; - - ja3: { - buckets: Readonly<Array<{ key: string; doc_count: number }>>; - }; - - issuers: { - buckets: Readonly<Array<{ key: string; doc_count: number }>>; - }; - - not_after: { - buckets: Readonly<Array<{ key: number; key_as_string: string; doc_count: number }>>; - }; -} diff --git a/x-pack/legacy/plugins/siem/server/lib/types.ts b/x-pack/legacy/plugins/siem/server/lib/types.ts deleted file mode 100644 index 323ced734d24b..0000000000000 --- a/x-pack/legacy/plugins/siem/server/lib/types.ts +++ /dev/null @@ -1,184 +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 { AuthenticatedUser } from '../../../../../plugins/security/public'; -import { RequestHandlerContext } from '../../../../../../src/core/server'; -export { ConfigType as Configuration } from '../../../../../plugins/siem/server'; - -import { Authentications } from './authentications'; -import { Events } from './events'; -import { FrameworkAdapter, FrameworkRequest } from './framework'; -import { Hosts } from './hosts'; -import { IndexFields } from './index_fields'; -import { IpDetails } from './ip_details'; -import { KpiHosts } from './kpi_hosts'; -import { KpiNetwork } from './kpi_network'; -import { Network } from './network'; -import { Overview } from './overview'; -import { SourceStatus } from './source_status'; -import { Sources } from './sources'; -import { UncommonProcesses } from './uncommon_processes'; -import { Note } from './note/saved_object'; -import { PinnedEvent } from './pinned_event/saved_object'; -import { Timeline } from './timeline/saved_object'; -import { TLS } from './tls'; -import { MatrixHistogram } from './matrix_histogram'; - -export * from './hosts'; - -export interface AppDomainLibs { - authentications: Authentications; - events: Events; - fields: IndexFields; - hosts: Hosts; - ipDetails: IpDetails; - matrixHistogram: MatrixHistogram; - network: Network; - kpiNetwork: KpiNetwork; - overview: Overview; - uncommonProcesses: UncommonProcesses; - kpiHosts: KpiHosts; - tls: TLS; -} - -export interface AppBackendLibs extends AppDomainLibs { - framework: FrameworkAdapter; - sources: Sources; - sourceStatus: SourceStatus; - timeline: Timeline; - note: Note; - pinnedEvent: PinnedEvent; -} - -export interface SiemContext { - req: FrameworkRequest; - context: RequestHandlerContext; - user: AuthenticatedUser | null; -} - -export interface TotalValue { - value: number; - relation: string; -} - -export interface SearchResponse<T> { - took: number; - timed_out: boolean; - _scroll_id?: string; - _shards: ShardsResponse; - hits: { - total: TotalValue | number; - max_score: number; - hits: Array<{ - _index: string; - _type: string; - _id: string; - _score: number; - _source: T; - _version?: number; - _explanation?: Explanation; - fields?: string[]; - // eslint-disable-next-line @typescript-eslint/no-explicit-any - highlight?: any; - // eslint-disable-next-line @typescript-eslint/no-explicit-any - inner_hits?: any; - matched_queries?: string[]; - sort?: string[]; - }>; - }; - // eslint-disable-next-line @typescript-eslint/no-explicit-any - aggregations?: any; -} - -export interface ShardsResponse { - total: number; - successful: number; - failed: number; - skipped: number; -} - -export interface Explanation { - value: number; - description: string; - details: Explanation[]; -} - -export type SearchHit = SearchResponse<object>['hits']['hits'][0]; - -export interface TermAggregation { - [agg: string]: { - buckets: Array<{ - key: string; - doc_count: number; - }>; - }; -} - -export interface TotalHit { - value: number; - relation: string; -} - -export interface Hit { - _index: string; - _type: string; - _id: string; - _score: number | null; -} - -export interface Hits<T, U> { - hits: { - total: T; - max_score: number | null; - hits: U[]; - }; -} -export type SortRequestDirection = 'asc' | 'desc'; - -interface SortRequestField { - [field: string]: SortRequestDirection; -} - -export type SortRequest = SortRequestField[]; - -export interface MSearchHeader { - index: string[] | string; - allowNoIndices?: boolean; - ignoreUnavailable?: boolean; -} - -export interface AggregationRequest { - [aggField: string]: { - terms?: { - field: string; - size?: number; - order?: { - [aggSortField: string]: SortRequestDirection; - }; - }; - max?: { - field: string; - }; - aggs?: { - [aggSortField: string]: { - [aggType: string]: { - field: string; - }; - }; - }; - top_hits?: { - size?: number; - sort?: Array<{ - [aggSortField: string]: { - order: SortRequestDirection; - }; - }>; - _source: { - includes: string[]; - }; - }; - }; -} diff --git a/x-pack/legacy/plugins/siem/server/plugin.ts b/x-pack/legacy/plugins/siem/server/plugin.ts deleted file mode 100644 index 13b58fa1d57eb..0000000000000 --- a/x-pack/legacy/plugins/siem/server/plugin.ts +++ /dev/null @@ -1,200 +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 { i18n } from '@kbn/i18n'; - -import { - PluginStartContract as AlertingStart, - PluginSetupContract as AlertingSetup, -} from '../../../../plugins/alerting/server'; -import { - CoreSetup, - CoreStart, - PluginInitializerContext, - Logger, -} from '../../../../../src/core/server'; -import { SecurityPluginSetup as SecuritySetup } from '../../../../plugins/security/server'; -import { PluginSetupContract as FeaturesSetup } from '../../../../plugins/features/server'; -import { MlPluginSetup as MlSetup } from '../../../../plugins/ml/server'; -import { EncryptedSavedObjectsPluginSetup as EncryptedSavedObjectsSetup } from '../../../../plugins/encrypted_saved_objects/server'; -import { SpacesPluginSetup as SpacesSetup } from '../../../../plugins/spaces/server'; -import { PluginStartContract as ActionsStart } from '../../../../plugins/actions/server'; -import { LicensingPluginSetup } from '../../../../plugins/licensing/server'; -import { LegacyServices } from './types'; -import { initServer } from './init_server'; -import { compose } from './lib/compose/kibana'; -import { initRoutes } from './routes'; -import { isAlertExecutor } from './lib/detection_engine/signals/types'; -import { signalRulesAlertType } from './lib/detection_engine/signals/signal_rule_alert_type'; -import { rulesNotificationAlertType } from './lib/detection_engine/notifications/rules_notification_alert_type'; -import { isNotificationAlertExecutor } from './lib/detection_engine/notifications/types'; -import { - noteSavedObjectType, - pinnedEventSavedObjectType, - timelineSavedObjectType, - ruleStatusSavedObjectType, - ruleActionsSavedObjectType, -} from './saved_objects'; -import { SiemClientFactory } from './client'; -import { hasListsFeature, listsEnvFeatureFlagName } from './lib/detection_engine/feature_flags'; - -export { CoreSetup, CoreStart }; - -export interface SetupPlugins { - alerting: AlertingSetup; - encryptedSavedObjects: EncryptedSavedObjectsSetup; - features: FeaturesSetup; - licensing: LicensingPluginSetup; - security?: SecuritySetup; - spaces?: SpacesSetup; - ml?: MlSetup; -} - -export interface StartPlugins { - actions: ActionsStart; - alerting: AlertingStart; -} - -export class Plugin { - readonly name = 'siem'; - private readonly logger: Logger; - private context: PluginInitializerContext; - private siemClientFactory: SiemClientFactory; - - constructor(context: PluginInitializerContext) { - this.context = context; - this.logger = context.logger.get('plugins', this.name); - this.siemClientFactory = new SiemClientFactory(); - - this.logger.debug('Shim plugin initialized'); - } - - public setup(core: CoreSetup, plugins: SetupPlugins, __legacy: LegacyServices) { - this.logger.debug('Shim plugin setup'); - if (hasListsFeature()) { - // TODO: Remove this once we have the lists feature supported - this.logger.error( - `You have activated the lists feature flag which is NOT currently supported for SIEM! You should turn this feature flag off immediately by un-setting the environment variable: ${listsEnvFeatureFlagName} and restarting Kibana` - ); - } - - const router = core.http.createRouter(); - core.http.registerRouteHandlerContext(this.name, (context, request, response) => ({ - getSiemClient: () => this.siemClientFactory.create(request), - })); - - this.siemClientFactory.setup({ - getSpaceId: plugins.spaces?.spacesService?.getSpaceId, - config: __legacy.config, - }); - - initRoutes( - router, - __legacy.config, - plugins.encryptedSavedObjects?.usingEphemeralEncryptionKey ?? false, - plugins.security - ); - - plugins.features.registerFeature({ - id: this.name, - name: i18n.translate('xpack.siem.featureRegistry.linkSiemTitle', { - defaultMessage: 'SIEM', - }), - order: 1100, - icon: 'securityAnalyticsApp', - navLinkId: 'siem', - app: ['siem', 'kibana'], - catalogue: ['siem'], - privileges: { - all: { - app: ['siem', 'kibana'], - catalogue: ['siem'], - api: ['siem', 'actions-read', 'actions-all', 'alerting-read', 'alerting-all'], - savedObject: { - all: [ - 'alert', - 'action', - 'action_task_params', - noteSavedObjectType, - pinnedEventSavedObjectType, - timelineSavedObjectType, - ruleStatusSavedObjectType, - ruleActionsSavedObjectType, - 'cases', - 'cases-comments', - 'cases-configure', - 'cases-user-actions', - ], - read: ['config'], - }, - ui: [ - 'show', - 'crud', - 'alerting:show', - 'actions:show', - 'alerting:save', - 'actions:save', - 'alerting:delete', - 'actions:delete', - ], - }, - read: { - app: ['siem', 'kibana'], - catalogue: ['siem'], - api: ['siem', 'actions-read', 'actions-all', 'alerting-read', 'alerting-all'], - savedObject: { - all: ['alert', 'action', 'action_task_params'], - read: [ - 'config', - noteSavedObjectType, - pinnedEventSavedObjectType, - timelineSavedObjectType, - ruleStatusSavedObjectType, - ruleActionsSavedObjectType, - 'cases', - 'cases-comments', - 'cases-configure', - 'cases-user-actions', - ], - }, - ui: [ - 'show', - 'alerting:show', - 'actions:show', - 'alerting:save', - 'actions:save', - 'alerting:delete', - 'actions:delete', - ], - }, - }, - }); - - if (plugins.alerting != null) { - const signalRuleType = signalRulesAlertType({ - logger: this.logger, - version: this.context.env.packageInfo.version, - ml: plugins.ml, - }); - const ruleNotificationType = rulesNotificationAlertType({ - logger: this.logger, - }); - - if (isAlertExecutor(signalRuleType)) { - plugins.alerting.registerType(signalRuleType); - } - - if (isNotificationAlertExecutor(ruleNotificationType)) { - plugins.alerting.registerType(ruleNotificationType); - } - } - - const libs = compose(core, plugins, this.context.env.mode.prod); - initServer(libs); - } - - public start(core: CoreStart, plugins: StartPlugins) {} -} diff --git a/x-pack/legacy/plugins/siem/server/routes/index.ts b/x-pack/legacy/plugins/siem/server/routes/index.ts deleted file mode 100644 index 8c9f92890c26a..0000000000000 --- a/x-pack/legacy/plugins/siem/server/routes/index.ts +++ /dev/null @@ -1,83 +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 { IRouter } from '../../../../../../src/core/server'; -import { LegacyServices } from '../types'; - -import { createRulesRoute } from '../lib/detection_engine/routes/rules/create_rules_route'; -import { createIndexRoute } from '../lib/detection_engine/routes/index/create_index_route'; -import { readIndexRoute } from '../lib/detection_engine/routes/index/read_index_route'; -import { readRulesRoute } from '../lib/detection_engine/routes/rules/read_rules_route'; -import { findRulesRoute } from '../lib/detection_engine/routes/rules/find_rules_route'; -import { deleteRulesRoute } from '../lib/detection_engine/routes/rules/delete_rules_route'; -import { updateRulesRoute } from '../lib/detection_engine/routes/rules/update_rules_route'; -import { patchRulesRoute } from '../lib/detection_engine/routes/rules/patch_rules_route'; -import { setSignalsStatusRoute } from '../lib/detection_engine/routes/signals/open_close_signals_route'; -import { querySignalsRoute } from '../lib/detection_engine/routes/signals/query_signals_route'; -import { deleteIndexRoute } from '../lib/detection_engine/routes/index/delete_index_route'; -import { readTagsRoute } from '../lib/detection_engine/routes/tags/read_tags_route'; -import { readPrivilegesRoute } from '../lib/detection_engine/routes/privileges/read_privileges_route'; -import { addPrepackedRulesRoute } from '../lib/detection_engine/routes/rules/add_prepackaged_rules_route'; -import { createRulesBulkRoute } from '../lib/detection_engine/routes/rules/create_rules_bulk_route'; -import { updateRulesBulkRoute } from '../lib/detection_engine/routes/rules/update_rules_bulk_route'; -import { patchRulesBulkRoute } from '../lib/detection_engine/routes/rules/patch_rules_bulk_route'; -import { deleteRulesBulkRoute } from '../lib/detection_engine/routes/rules/delete_rules_bulk_route'; -import { importRulesRoute } from '../lib/detection_engine/routes/rules/import_rules_route'; -import { exportRulesRoute } from '../lib/detection_engine/routes/rules/export_rules_route'; -import { findRulesStatusesRoute } from '../lib/detection_engine/routes/rules/find_rules_status_route'; -import { getPrepackagedRulesStatusRoute } from '../lib/detection_engine/routes/rules/get_prepackaged_rules_status_route'; -import { importTimelinesRoute } from '../lib/timeline/routes/import_timelines_route'; -import { exportTimelinesRoute } from '../lib/timeline/routes/export_timelines_route'; -import { SetupPlugins } from '../plugin'; - -export const initRoutes = ( - router: IRouter, - config: LegacyServices['config'], - usingEphemeralEncryptionKey: boolean, - security: SetupPlugins['security'] -) => { - // Detection Engine Rule routes that have the REST endpoints of /api/detection_engine/rules - // All REST rule creation, deletion, updating, etc...... - createRulesRoute(router); - readRulesRoute(router); - updateRulesRoute(router); - patchRulesRoute(router); - deleteRulesRoute(router); - findRulesRoute(router); - - addPrepackedRulesRoute(router); - getPrepackagedRulesStatusRoute(router); - createRulesBulkRoute(router); - updateRulesBulkRoute(router); - patchRulesBulkRoute(router); - deleteRulesBulkRoute(router); - - importRulesRoute(router, config); - exportRulesRoute(router, config); - - importTimelinesRoute(router, config, security); - exportTimelinesRoute(router, config); - - findRulesStatusesRoute(router); - - // Detection Engine Signals routes that have the REST endpoints of /api/detection_engine/signals - // POST /api/detection_engine/signals/status - // Example usage can be found in siem/server/lib/detection_engine/scripts/signals - setSignalsStatusRoute(router); - querySignalsRoute(router); - - // Detection Engine index routes that have the REST endpoints of /api/detection_engine/index - // All REST index creation, policy management for spaces - createIndexRoute(router); - readIndexRoute(router); - deleteIndexRoute(router); - - // Detection Engine tags routes that have the REST endpoints of /api/detection_engine/tags - readTagsRoute(router); - - // Privileges API to get the generic user privileges - readPrivilegesRoute(router, security, usingEphemeralEncryptionKey); -}; diff --git a/x-pack/legacy/plugins/siem/server/types.ts b/x-pack/legacy/plugins/siem/server/types.ts deleted file mode 100644 index a52322f5f830c..0000000000000 --- a/x-pack/legacy/plugins/siem/server/types.ts +++ /dev/null @@ -1,24 +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 { Legacy } from 'kibana'; -import { SiemClient } from './client'; - -export interface LegacyServices { - config: Legacy.Server['config']; -} - -export { SiemClient }; - -export interface SiemRequestContext { - getSiemClient: () => SiemClient; -} - -declare module 'src/core/server' { - interface RequestHandlerContext { - siem?: SiemRequestContext; - } -} diff --git a/x-pack/legacy/plugins/siem/tsconfig.json b/x-pack/legacy/plugins/siem/tsconfig.json deleted file mode 100644 index b027bb4567b97..0000000000000 --- a/x-pack/legacy/plugins/siem/tsconfig.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "extends": "../../../tsconfig.json" -} \ No newline at end of file diff --git a/x-pack/legacy/plugins/tilemap/index.js b/x-pack/legacy/plugins/tilemap/index.js deleted file mode 100644 index d4105519ee0a7..0000000000000 --- a/x-pack/legacy/plugins/tilemap/index.js +++ /dev/null @@ -1,31 +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 { mirrorPluginStatus } from '../../server/lib/mirror_plugin_status'; -import { inspectSettings } from './server/lib/inspect_settings'; -import { resolve } from 'path'; - -export const tilemap = kibana => { - return new kibana.Plugin({ - id: 'tilemap', - configPrefix: 'xpack.tilemap', - require: ['xpack_main', 'vis_type_vislib'], - publicDir: resolve(__dirname, 'public'), - uiExports: { - hacks: ['plugins/tilemap/vis_type_enhancers/update_tilemap_settings'], - }, - init: function(server) { - const thisPlugin = this; - const xpackMainPlugin = server.plugins.xpack_main; - mirrorPluginStatus(xpackMainPlugin, thisPlugin); - xpackMainPlugin.status.once('green', () => { - xpackMainPlugin.info - .feature(thisPlugin.id) - .registerLicenseCheckResultsGenerator(inspectSettings); - }); - }, - }); -}; diff --git a/x-pack/legacy/plugins/tilemap/public/vis_type_enhancers/update_tilemap_settings.js b/x-pack/legacy/plugins/tilemap/public/vis_type_enhancers/update_tilemap_settings.js deleted file mode 100644 index 45764016f0311..0000000000000 --- a/x-pack/legacy/plugins/tilemap/public/vis_type_enhancers/update_tilemap_settings.js +++ /dev/null @@ -1,23 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import uiRoutes from 'ui/routes'; -import { xpackInfo } from 'plugins/xpack_main/services/xpack_info'; -import 'ui/vis/map/service_settings'; - -uiRoutes.addSetupWork(function($injector, serviceSettings) { - const tileMapPluginInfo = xpackInfo.get('features.tilemap'); - - if (!tileMapPluginInfo) { - return; - } - - if (!tileMapPluginInfo.license.active || !tileMapPluginInfo.license.valid) { - return; - } - serviceSettings.addQueryParams({ license: tileMapPluginInfo.license.uid }); - serviceSettings.disableZoomMessage(); -}); diff --git a/x-pack/legacy/plugins/tilemap/server/lib/__tests__/inspect_settings.js b/x-pack/legacy/plugins/tilemap/server/lib/__tests__/inspect_settings.js deleted file mode 100644 index ce7987ee396b8..0000000000000 --- a/x-pack/legacy/plugins/tilemap/server/lib/__tests__/inspect_settings.js +++ /dev/null @@ -1,36 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import expect from '@kbn/expect'; -import { inspectSettings } from '../../../server/lib/inspect_settings'; - -describe('inspectSettings', function() { - it('should propagate x-pack info', function() { - const mockSettings = { - isAvailable: () => true, - license: { - getUid: () => 'foobar', - isActive: () => true, - isOneOf: () => true, - }, - }; - - const licenseInfo = inspectSettings(mockSettings); - expect(licenseInfo.license.uid).to.equal('foobar'); - expect(licenseInfo.license.active).to.equal(true); - expect(licenseInfo.license.valid).to.equal(true); - }); - - it('should break when unavailable info', function() { - const mockSettings = { - isAvailable: () => false, - }; - - const licenseInfo = inspectSettings(mockSettings); - expect(licenseInfo).to.have.property('message'); - expect(typeof licenseInfo.message === 'string').to.be.ok(); - }); -}); diff --git a/x-pack/legacy/plugins/tilemap/server/lib/inspect_settings.js b/x-pack/legacy/plugins/tilemap/server/lib/inspect_settings.js deleted file mode 100644 index cd6fb8bd8c110..0000000000000 --- a/x-pack/legacy/plugins/tilemap/server/lib/inspect_settings.js +++ /dev/null @@ -1,32 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -export function inspectSettings(xpackInfo) { - if (!xpackInfo || !xpackInfo.isAvailable()) { - return { - message: - 'You cannot use the Tilemap Plugin because license information is not available at this time.', - }; - } - - /** - *Propagate these settings to the client - */ - return { - license: { - uid: xpackInfo.license.getUid(), - active: xpackInfo.license.isActive(), - valid: xpackInfo.license.isOneOf([ - 'trial', - 'standard', - 'basic', - 'gold', - 'platinum', - 'enterprise', - ]), - }, - }; -} diff --git a/x-pack/legacy/plugins/uptime/README.md b/x-pack/legacy/plugins/uptime/README.md index 2ed0e2fc77cbc..92162341ff426 100644 --- a/x-pack/legacy/plugins/uptime/README.md +++ b/x-pack/legacy/plugins/uptime/README.md @@ -3,7 +3,7 @@ ## Purpose The purpose of this plugin is to provide users of Heartbeat more visibility of what's happening -in their infrastructure. It's primarily built using React and Apollo's GraphQL tools. +in their infrastructure. ## Layout @@ -11,13 +11,15 @@ There are three sections to the app, `common`, `public`, and `server`. ### common -Contains GraphQL types, constants and a few other files. +Contains runtime types types, constants and a few other files. + +Notably, we use `io-ts`/`fp-ts` functions and types to help provide +additional runtime safety for our API requests/responses. ### public -Components come in two main types, queries and functional. Queries are extended from Apollo's queries -type which abstracts a lot of the GraphQL connectivity away. Functional are dumb components that -don't store any state. +We use Redux and associated tools for managing our app state. Components come in the usual `connect`ed and +presentational varieties. The `lib` directory controls bootstrapping code and adapter types. @@ -27,12 +29,13 @@ The principal structure of the app is stored in `uptime_app.tsx`. ### server -There is a `graphql` directory which contains the resolvers, schema files, and constants. - The `lib` directory contains `adapters`, which are connections to external resources like Kibana Server, Elasticsearch, etc. In addition, it contains domains, which are libraries that provide functionality via adapters. +The `requests` directory contains functions responsible for querying Elasticsearch and parsing its +responses. + There's also a `rest_api` folder that defines the structure of the RESTful API endpoints. ## Testing diff --git a/x-pack/legacy/plugins/uptime/common/constants/chart_format_limits.ts b/x-pack/legacy/plugins/uptime/common/constants/chart_format_limits.ts index f291450ab2a7a..3bd204a003c9d 100644 --- a/x-pack/legacy/plugins/uptime/common/constants/chart_format_limits.ts +++ b/x-pack/legacy/plugins/uptime/common/constants/chart_format_limits.ts @@ -11,7 +11,7 @@ const WEEK = DAY * 7; const MONTH = WEEK * 4; /** - * These contsants are used by the charting code to determine + * These constants are used by the charting code to determine * what label should be applied to chart axes so as to help users * understand the timeseries data they're being shown. */ diff --git a/x-pack/legacy/plugins/uptime/common/constants/context_defaults.ts b/x-pack/legacy/plugins/uptime/common/constants/context_defaults.ts index 540e60a28b066..0c493326add72 100644 --- a/x-pack/legacy/plugins/uptime/common/constants/context_defaults.ts +++ b/x-pack/legacy/plugins/uptime/common/constants/context_defaults.ts @@ -4,14 +4,14 @@ * you may not use this file except in compliance with the Elastic License. */ -import { SortOrder, CursorDirection } from '../graphql/types'; +import { CursorDirection, SortOrder } from '../runtime_types'; /** * The Uptime UI utilizes a settings context, the defaults for which are stored here. */ export const CONTEXT_DEFAULTS = { /** - * The application cannot assume a basepath. + * The application cannot assume a basePath. */ BASE_PATH: '', diff --git a/x-pack/legacy/plugins/uptime/common/constants/rest_api.ts b/x-pack/legacy/plugins/uptime/common/constants/rest_api.ts index dffa131870db1..169d175f02d3b 100644 --- a/x-pack/legacy/plugins/uptime/common/constants/rest_api.ts +++ b/x-pack/legacy/plugins/uptime/common/constants/rest_api.ts @@ -5,8 +5,10 @@ */ export enum API_URLS { + CERTS = '/api/uptime/certs', INDEX_PATTERN = `/api/uptime/index_pattern`, INDEX_STATUS = '/api/uptime/index_status', + MONITOR_LIST = `/api/uptime/monitor/list`, MONITOR_LOCATIONS = `/api/uptime/monitor/locations`, MONITOR_DURATION = `/api/uptime/monitor/duration`, MONITOR_DETAILS = `/api/uptime/monitor/details`, diff --git a/x-pack/legacy/plugins/uptime/common/domain_types/index.ts b/x-pack/legacy/plugins/uptime/common/domain_types/index.ts deleted file mode 100644 index 1cf72ca2dac0b..0000000000000 --- a/x-pack/legacy/plugins/uptime/common/domain_types/index.ts +++ /dev/null @@ -1,7 +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. - */ - -export * from './monitors'; diff --git a/x-pack/legacy/plugins/uptime/common/domain_types/monitors.ts b/x-pack/legacy/plugins/uptime/common/domain_types/monitors.ts deleted file mode 100644 index 7f5699eb7e8a4..0000000000000 --- a/x-pack/legacy/plugins/uptime/common/domain_types/monitors.ts +++ /dev/null @@ -1,10 +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. - */ - -export interface UMGqlRange { - dateRangeStart: string; - dateRangeEnd: string; -} diff --git a/x-pack/legacy/plugins/uptime/common/graphql/types.ts b/x-pack/legacy/plugins/uptime/common/graphql/types.ts index c8beb91d807d5..e69de29bb2d1d 100644 --- a/x-pack/legacy/plugins/uptime/common/graphql/types.ts +++ b/x-pack/legacy/plugins/uptime/common/graphql/types.ts @@ -1,572 +0,0 @@ -/* tslint:disable */ - -// ==================================================== -// START: Typescript template -// ==================================================== - -// ==================================================== -// Scalars -// ==================================================== - -export type UnsignedInteger = any; - -// ==================================================== -// Types -// ==================================================== - -export interface Query { - /** Get a list of all recorded pings for all monitors */ - allPings: PingResults; - - /** Fetches the current state of Uptime monitors for the given parameters. */ - getMonitorStates?: MonitorSummaryResult | null; -} - -export interface PingResults { - /** Total number of matching pings */ - total: UnsignedInteger; - /** Unique list of all locations the query matched */ - locations: string[]; - /** List of pings */ - pings: Ping[]; -} -/** A request sent from a monitor to a host */ -export interface Ping { - /** unique ID for this ping */ - id: string; - /** The timestamp of the ping's creation */ - timestamp: string; - /** The agent that recorded the ping */ - beat?: Beat | null; - - container?: Container | null; - - docker?: Docker | null; - - ecs?: Ecs | null; - - error?: Error | null; - - host?: Host | null; - - http?: Http | null; - - icmp?: Icmp | null; - - kubernetes?: Kubernetes | null; - - meta?: Meta | null; - - monitor?: Monitor | null; - - observer?: Observer | null; - - resolve?: Resolve | null; - - socks5?: Socks5 | null; - - summary?: Summary | null; - - tags?: string | null; - - tcp?: Tcp | null; - - tls?: PingTls | null; - - url?: Url | null; -} -/** An agent for recording a beat */ -export interface Beat { - hostname?: string | null; - - name?: string | null; - - timezone?: string | null; - - type?: string | null; -} - -export interface Container { - id?: string | null; - - image?: ContainerImage | null; - - name?: string | null; - - runtime?: string | null; -} - -export interface ContainerImage { - name?: string | null; - - tag?: string | null; -} - -export interface Docker { - id?: string | null; - - image?: string | null; - - name?: string | null; -} - -export interface Ecs { - version?: string | null; -} - -export interface Error { - code?: number | null; - - message?: string | null; - - type?: string | null; -} - -export interface Host { - architecture?: string | null; - - id?: string | null; - - hostname?: string | null; - - ip?: string | null; - - mac?: string | null; - - name?: string | null; - - os?: Os | null; -} - -export interface Os { - family?: string | null; - - kernel?: string | null; - - platform?: string | null; - - version?: string | null; - - name?: string | null; - - build?: string | null; -} - -export interface Http { - response?: HttpResponse | null; - - rtt?: HttpRtt | null; - - url?: string | null; -} - -export interface HttpResponse { - status_code?: UnsignedInteger | null; - - body?: HttpBody | null; -} - -export interface HttpBody { - /** Size of HTTP response body in bytes */ - bytes?: UnsignedInteger | null; - /** Hash of the HTTP response body */ - hash?: string | null; - /** Response body of the HTTP Response. May be truncated based on client settings. */ - content?: string | null; - /** Byte length of the content string, taking into account multibyte chars. */ - content_bytes?: UnsignedInteger | null; -} - -export interface HttpRtt { - content?: Duration | null; - - response_header?: Duration | null; - - total?: Duration | null; - - validate?: Duration | null; - - validate_body?: Duration | null; - - write_request?: Duration | null; -} -/** The monitor's status for a ping */ -export interface Duration { - us?: UnsignedInteger | null; -} - -export interface Icmp { - requests?: number | null; - - rtt?: number | null; -} - -export interface Kubernetes { - container?: KubernetesContainer | null; - - namespace?: string | null; - - node?: KubernetesNode | null; - - pod?: KubernetesPod | null; -} - -export interface KubernetesContainer { - image?: string | null; - - name?: string | null; -} - -export interface KubernetesNode { - name?: string | null; -} - -export interface KubernetesPod { - name?: string | null; - - uid?: string | null; -} - -export interface Meta { - cloud?: MetaCloud | null; -} - -export interface MetaCloud { - availability_zone?: string | null; - - instance_id?: string | null; - - instance_name?: string | null; - - machine_type?: string | null; - - project_id?: string | null; - - provider?: string | null; - - region?: string | null; -} - -export interface Monitor { - duration?: Duration | null; - - host?: string | null; - /** The id of the monitor */ - id?: string | null; - /** The IP pinged by the monitor */ - ip?: string | null; - /** The name of the protocol being monitored */ - name?: string | null; - /** The protocol scheme of the monitored host */ - scheme?: string | null; - /** The status of the monitored host */ - status?: string | null; - /** The type of host being monitored */ - type?: string | null; - - check_group?: string | null; -} -/** Metadata added by a proccessor, which is specified in its configuration. */ -export interface Observer { - /** Geolocation data for the agent. */ - geo?: Geo | null; -} -/** Geolocation data added via processors to enrich events. */ -export interface Geo { - /** Name of the city in which the agent is running. */ - city_name?: string | null; - /** The name of the continent on which the agent is running. */ - continent_name?: string | null; - /** ISO designation for the agent's country. */ - country_iso_code?: string | null; - /** The name of the agent's country. */ - country_name?: string | null; - /** The lat/long of the agent. */ - location?: string | null; - /** A name for the host's location, e.g. 'us-east-1' or 'LAX'. */ - name?: string | null; - /** ISO designation of the agent's region. */ - region_iso_code?: string | null; - /** Name of the region hosting the agent. */ - region_name?: string | null; -} - -export interface Resolve { - host?: string | null; - - ip?: string | null; - - rtt?: Duration | null; -} - -export interface Socks5 { - rtt?: Rtt | null; -} - -export interface Rtt { - connect?: Duration | null; - - handshake?: Duration | null; - - validate?: Duration | null; -} - -export interface Summary { - up?: number | null; - - down?: number | null; - - geo?: CheckGeo | null; -} - -export interface CheckGeo { - name?: string | null; - - location?: Location | null; -} - -export interface Location { - lat?: number | null; - - lon?: number | null; -} - -export interface Tcp { - port?: number | null; - - rtt?: Rtt | null; -} -/** Contains monitor transmission encryption information. */ -export interface PingTls { - /** The date and time after which the certificate is invalid. */ - certificate_not_valid_after?: string | null; - - certificate_not_valid_before?: string | null; - - certificates?: string | null; - - rtt?: Rtt | null; -} - -export interface Url { - full?: string | null; - - scheme?: string | null; - - domain?: string | null; - - port?: number | null; - - path?: string | null; - - query?: string | null; -} - -export interface DocCount { - count: UnsignedInteger; -} - -export interface Snapshot { - counts: SnapshotCount; -} - -export interface SnapshotCount { - up: number; - - down: number; - - total: number; -} - -/** The primary object returned for monitor states. */ -export interface MonitorSummaryResult { - /** Used to go to the next page of results */ - prevPagePagination?: string | null; - /** Used to go to the previous page of results */ - nextPagePagination?: string | null; - /** The objects representing the state of a series of heartbeat monitors. */ - summaries?: MonitorSummary[] | null; - /** The number of summaries. */ - totalSummaryCount: number; -} -/** Represents the current state and associated data for an Uptime monitor. */ -export interface MonitorSummary { - /** The ID assigned by the config or generated by the user. */ - monitor_id: string; - /** The state of the monitor and its associated details. */ - state: State; - - histogram?: SummaryHistogram | null; -} -/** Unifies the subsequent data for an uptime monitor. */ -export interface State { - /** The agent processing the monitor. */ - agent?: Agent | null; - /** There is a check object for each instance of the monitoring agent. */ - checks?: Check[] | null; - - geo?: StateGeo | null; - - observer?: StateObserver | null; - - monitor?: MonitorState | null; - - summary: Summary; - - timestamp: UnsignedInteger; - /** Transport encryption information. */ - tls?: (StateTls | null)[] | null; - - url?: StateUrl | null; -} - -export interface Agent { - id: string; -} - -export interface Check { - agent?: Agent | null; - - container?: StateContainer | null; - - kubernetes?: StateKubernetes | null; - - monitor: CheckMonitor; - - observer?: CheckObserver | null; - - timestamp: string; -} - -export interface StateContainer { - id?: string | null; -} - -export interface StateKubernetes { - pod?: StatePod | null; -} - -export interface StatePod { - uid?: string | null; -} - -export interface CheckMonitor { - ip?: string | null; - - name?: string | null; - - status: string; -} - -export interface CheckObserver { - geo?: CheckGeo | null; -} - -export interface StateGeo { - name?: (string | null)[] | null; - - location?: Location | null; -} - -export interface StateObserver { - geo?: StateGeo | null; -} - -export interface MonitorState { - status?: string | null; - - name?: string | null; - - id?: string | null; - - type?: string | null; -} -/** Contains monitor transmission encryption information. */ -export interface StateTls { - /** The date and time after which the certificate is invalid. */ - certificate_not_valid_after?: string | null; - - certificate_not_valid_before?: string | null; - - certificates?: string | null; - - rtt?: Rtt | null; -} - -export interface StateUrl { - domain?: string | null; - - full?: string | null; - - path?: string | null; - - port?: number | null; - - scheme?: string | null; -} -/** Monitor status data over time. */ -export interface SummaryHistogram { - /** The number of documents used to assemble the histogram. */ - count: number; - /** The individual histogram data points. */ - points: SummaryHistogramPoint[]; -} -/** Represents a monitor's statuses for a period of time. */ -export interface SummaryHistogramPoint { - /** The time at which these data were collected. */ - timestamp: UnsignedInteger; - /** The number of _up_ documents. */ - up: number; - /** The number of _down_ documents. */ - down: number; -} - -export interface AllPingsQueryArgs { - /** Optional: the direction to sort by. Accepts 'asc' and 'desc'. Defaults to 'desc'. */ - sort?: string | null; - /** Optional: the number of results to return. */ - size?: number | null; - /** Optional: the monitor ID filter. */ - monitorId?: string | null; - /** Optional: the check status to filter by. */ - status?: string | null; - /** The lower limit of the date range. */ - dateRangeStart: string; - /** The upper limit of the date range. */ - dateRangeEnd: string; - /** Optional: agent location to filter by. */ - location?: string | null; - page?: number; -} - -export interface GetMonitorStatesQueryArgs { - dateRangeStart: string; - - dateRangeEnd: string; - - pagination?: string | null; - - filters?: string | null; - - statusFilter?: string | null; - - pageSize: number; -} - -// ==================================================== -// Enums -// ==================================================== - -export enum CursorDirection { - AFTER = 'AFTER', - BEFORE = 'BEFORE', -} - -export enum SortOrder { - ASC = 'ASC', - DESC = 'DESC', -} - -// ==================================================== -// END: Typescript template -// ==================================================== diff --git a/x-pack/legacy/plugins/uptime/common/runtime_types/certs.ts b/x-pack/legacy/plugins/uptime/common/runtime_types/certs.ts new file mode 100644 index 0000000000000..e8be67abf3a44 --- /dev/null +++ b/x-pack/legacy/plugins/uptime/common/runtime_types/certs.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 * as t from 'io-ts'; + +export const GetCertsParamsType = t.intersection([ + t.type({ + from: t.string, + to: t.string, + index: t.number, + size: t.number, + }), + t.partial({ + search: t.string, + }), +]); + +export type GetCertsParams = t.TypeOf<typeof GetCertsParamsType>; + +export const CertType = t.intersection([ + t.type({ + monitors: t.array( + t.partial({ + name: t.string, + id: t.string, + }) + ), + sha256: t.string, + }), + t.partial({ + certificate_not_valid_after: t.string, + certificate_not_valid_before: t.string, + common_name: t.string, + issuer: t.string, + sha1: t.string, + }), +]); + +export type Cert = t.TypeOf<typeof CertType>; diff --git a/x-pack/legacy/plugins/uptime/common/runtime_types/common.ts b/x-pack/legacy/plugins/uptime/common/runtime_types/common.ts index 37101b5b46fd2..e07c46fa01cfe 100644 --- a/x-pack/legacy/plugins/uptime/common/runtime_types/common.ts +++ b/x-pack/legacy/plugins/uptime/common/runtime_types/common.ts @@ -27,7 +27,12 @@ export const StatesIndexStatusType = t.type({ docCount: t.number, }); +export const DateRangeType = t.type({ + from: t.string, + to: t.string, +}); + export type Summary = t.TypeOf<typeof SummaryType>; -export type CheckGeo = t.TypeOf<typeof CheckGeoType>; export type Location = t.TypeOf<typeof LocationType>; export type StatesIndexStatus = t.TypeOf<typeof StatesIndexStatusType>; +export type DateRange = t.TypeOf<typeof DateRangeType>; diff --git a/x-pack/legacy/plugins/uptime/common/runtime_types/dynamic_settings.ts b/x-pack/legacy/plugins/uptime/common/runtime_types/dynamic_settings.ts index 8dedd4672eeae..985b51891da99 100644 --- a/x-pack/legacy/plugins/uptime/common/runtime_types/dynamic_settings.ts +++ b/x-pack/legacy/plugins/uptime/common/runtime_types/dynamic_settings.ts @@ -6,10 +6,20 @@ import * as t from 'io-ts'; -export const DynamicSettingsType = t.type({ - heartbeatIndices: t.string, +export const CertificatesStatesThresholdType = t.interface({ + warningState: t.number, + errorState: t.number, }); +export const DynamicSettingsType = t.intersection([ + t.type({ + heartbeatIndices: t.string, + }), + t.partial({ + certificatesThresholds: CertificatesStatesThresholdType, + }), +]); + export const DynamicSettingsSaveType = t.intersection([ t.type({ success: t.boolean, @@ -21,7 +31,12 @@ export const DynamicSettingsSaveType = t.intersection([ export type DynamicSettings = t.TypeOf<typeof DynamicSettingsType>; export type DynamicSettingsSaveResponse = t.TypeOf<typeof DynamicSettingsSaveType>; +export type CertificatesStatesThreshold = t.TypeOf<typeof CertificatesStatesThresholdType>; export const defaultDynamicSettings: DynamicSettings = { heartbeatIndices: 'heartbeat-8*', + certificatesThresholds: { + errorState: 7, + warningState: 30, + }, }; diff --git a/x-pack/legacy/plugins/uptime/common/runtime_types/index.ts b/x-pack/legacy/plugins/uptime/common/runtime_types/index.ts index 5e3fb2326bdb9..78aab3806ae04 100644 --- a/x-pack/legacy/plugins/uptime/common/runtime_types/index.ts +++ b/x-pack/legacy/plugins/uptime/common/runtime_types/index.ts @@ -5,8 +5,10 @@ */ export * from './alerts'; +export * from './certs'; export * from './common'; export * from './monitor'; export * from './overview_filters'; +export * from './ping'; export * from './snapshot'; export * from './dynamic_settings'; diff --git a/x-pack/legacy/plugins/uptime/common/runtime_types/monitor/index.ts b/x-pack/legacy/plugins/uptime/common/runtime_types/monitor/index.ts index 80b48d09dc5b8..46b19d8442a94 100644 --- a/x-pack/legacy/plugins/uptime/common/runtime_types/monitor/index.ts +++ b/x-pack/legacy/plugins/uptime/common/runtime_types/monitor/index.ts @@ -6,3 +6,4 @@ export * from './details'; export * from './locations'; +export * from './state'; diff --git a/x-pack/legacy/plugins/uptime/common/runtime_types/monitor/state.ts b/x-pack/legacy/plugins/uptime/common/runtime_types/monitor/state.ts new file mode 100644 index 0000000000000..90aa692f89a42 --- /dev/null +++ b/x-pack/legacy/plugins/uptime/common/runtime_types/monitor/state.ts @@ -0,0 +1,145 @@ +/* + * 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 t from 'io-ts'; + +export const CheckMonitorType = t.intersection([ + t.partial({ + name: t.string, + ip: t.union([t.array(t.string), t.string]), + }), + t.type({ + status: t.string, + }), +]); + +export const CheckType = t.intersection([ + t.partial({ + agent: t.partial({ + id: t.string, + }), + container: t.type({ + id: t.string, + }), + kubernetes: t.type({ + pod: t.type({ + uid: t.string, + }), + }), + observer: t.type({ + geo: t.partial({ + name: t.string, + location: t.partial({ + lat: t.number, + lon: t.number, + }), + }), + }), + }), + t.type({ + monitor: CheckMonitorType, + timestamp: t.number, + }), +]); + +export type Check = t.TypeOf<typeof CheckType>; + +export const StateType = t.intersection([ + t.partial({ + checks: t.array(CheckType), + observer: t.partial({ + geo: t.partial({ + name: t.array(t.string), + }), + }), + summary: t.partial({ + up: t.number, + down: t.number, + geo: t.partial({ + name: t.string, + location: t.partial({ + lat: t.number, + lon: t.number, + }), + }), + }), + }), + t.type({ + timestamp: t.string, + url: t.partial({ + domain: t.string, + full: t.string, + path: t.string, + port: t.number, + scheme: t.string, + }), + }), +]); + +export const HistogramPointType = t.type({ + timestamp: t.number, + up: t.number, + down: t.number, +}); + +export type HistogramPoint = t.TypeOf<typeof HistogramPointType>; + +export const HistogramType = t.type({ + count: t.number, + points: t.array(HistogramPointType), +}); + +export type Histogram = t.TypeOf<typeof HistogramType>; + +export const MonitorSummaryType = t.intersection([ + t.type({ + monitor_id: t.string, + state: StateType, + }), + t.partial({ + histogram: HistogramType, + }), +]); + +export type MonitorSummary = t.TypeOf<typeof MonitorSummaryType>; + +export const MonitorSummaryResultType = t.intersection([ + t.partial({ + summaries: t.array(MonitorSummaryType), + }), + t.type({ + prevPagePagination: t.union([t.string, t.null]), + nextPagePagination: t.union([t.string, t.null]), + totalSummaryCount: t.number, + }), +]); + +export type MonitorSummaryResult = t.TypeOf<typeof MonitorSummaryResultType>; + +export const FetchMonitorStatesQueryArgsType = t.intersection([ + t.partial({ + pagination: t.string, + filters: t.string, + statusFilter: t.string, + }), + t.type({ + dateRangeStart: t.string, + dateRangeEnd: t.string, + pageSize: t.number, + }), +]); + +export type FetchMonitorStatesQueryArgs = t.TypeOf<typeof FetchMonitorStatesQueryArgsType>; + +export enum CursorDirection { + AFTER = 'AFTER', + BEFORE = 'BEFORE', +} + +export enum SortOrder { + ASC = 'ASC', + DESC = 'DESC', +} diff --git a/x-pack/legacy/plugins/uptime/common/types/ping/histogram.ts b/x-pack/legacy/plugins/uptime/common/runtime_types/ping/histogram.ts similarity index 77% rename from x-pack/legacy/plugins/uptime/common/types/ping/histogram.ts rename to x-pack/legacy/plugins/uptime/common/runtime_types/ping/histogram.ts index 3ae32e15ca55c..2c3b52051be0f 100644 --- a/x-pack/legacy/plugins/uptime/common/types/ping/histogram.ts +++ b/x-pack/legacy/plugins/uptime/common/runtime_types/ping/histogram.ts @@ -28,3 +28,15 @@ export interface HistogramResult { histogram: HistogramDataPoint[]; interval: string; } + +export interface HistogramQueryResult { + key: number; + key_as_string: string; + doc_count: number; + down: { + doc_count: number; + }; + up: { + doc_count: number; + }; +} diff --git a/x-pack/legacy/plugins/uptime/common/runtime_types/ping/index.ts b/x-pack/legacy/plugins/uptime/common/runtime_types/ping/index.ts new file mode 100644 index 0000000000000..a2fc7c1b243ba --- /dev/null +++ b/x-pack/legacy/plugins/uptime/common/runtime_types/ping/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 * from './histogram'; +export * from './ping'; diff --git a/x-pack/legacy/plugins/uptime/common/runtime_types/ping/ping.ts b/x-pack/legacy/plugins/uptime/common/runtime_types/ping/ping.ts new file mode 100644 index 0000000000000..ee14b298f3810 --- /dev/null +++ b/x-pack/legacy/plugins/uptime/common/runtime_types/ping/ping.ts @@ -0,0 +1,181 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import * as t from 'io-ts'; +import { DateRangeType } from '../common'; + +export const HttpResponseBodyType = t.partial({ + bytes: t.number, + content: t.string, + content_bytes: t.number, + hash: t.string, +}); + +export type HttpResponseBody = t.TypeOf<typeof HttpResponseBodyType>; + +export const TlsType = t.partial({ + certificate_not_valid_after: t.string, + certificate_not_valid_before: t.string, +}); + +export type Tls = t.TypeOf<typeof TlsType>; + +export const MonitorType = t.intersection([ + t.type({ + duration: t.type({ + us: t.number, + }), + id: t.string, + status: t.string, + type: t.string, + }), + t.partial({ + check_group: t.string, + ip: t.string, + name: t.string, + timespan: t.partial({ + gte: t.string, + lte: t.string, + }), + }), +]); + +export type Monitor = t.TypeOf<typeof MonitorType>; + +export const PingType = t.intersection([ + t.type({ + timestamp: t.string, + monitor: MonitorType, + docId: t.string, + }), + t.partial({ + agent: t.intersection([ + t.type({ + ephemeral_id: t.string, + hostname: t.string, + id: t.string, + type: t.string, + version: t.string, + }), + t.partial({ + name: t.string, + }), + ]), + container: t.partial({ + id: t.string, + image: t.partial({ + name: t.string, + tag: t.string, + }), + name: t.string, + runtime: t.string, + }), + ecs: t.partial({ + version: t.string, + }), + error: t.intersection([ + t.partial({ + code: t.string, + id: t.string, + stack_trace: t.string, + type: t.string, + }), + t.type({ + // this is _always_ on the error field + message: t.string, + }), + ]), + http: t.partial({ + request: t.partial({ + body: t.partial({ + bytes: t.number, + content: t.partial({ + text: t.string, + }), + }), + bytes: t.number, + method: t.string, + referrer: t.string, + }), + response: t.partial({ + body: HttpResponseBodyType, + bytes: t.number, + redirects: t.string, + status_code: t.number, + }), + version: t.string, + }), + icmp: t.partial({ + requests: t.number, + rtt: t.partial({ + us: t.number, + }), + }), + kubernetes: t.partial({ + pod: t.partial({ + name: t.string, + uid: t.string, + }), + }), + observer: t.partial({ + geo: t.partial({ + name: t.string, + }), + }), + resolve: t.partial({ + ip: t.string, + rtt: t.partial({ + us: t.number, + }), + }), + summary: t.partial({ + down: t.number, + up: t.number, + }), + tags: t.array(t.string), + tcp: t.partial({ + rtt: t.partial({ + connect: t.partial({ + us: t.number, + }), + }), + }), + tls: TlsType, + // should this be partial? + url: t.partial({ + domain: t.string, + full: t.string, + port: t.number, + scheme: t.string, + }), + }), +]); + +export type Ping = t.TypeOf<typeof PingType>; + +export const PingsResponseType = t.type({ + total: t.number, + locations: t.array(t.string), + pings: t.array(PingType), +}); + +export type PingsResponse = t.TypeOf<typeof PingsResponseType>; + +export const GetPingsParamsType = t.intersection([ + t.type({ + dateRange: DateRangeType, + }), + t.partial({ + index: t.number, + size: t.number, + location: t.string, + monitorId: t.string, + sort: t.string, + status: t.string, + }), +]); + +export type GetPingsParams = t.TypeOf<typeof GetPingsParamsType>; diff --git a/x-pack/legacy/plugins/uptime/common/types/index.ts b/x-pack/legacy/plugins/uptime/common/types/index.ts index 2c39f2a3b7314..a32eabd49a3e5 100644 --- a/x-pack/legacy/plugins/uptime/common/types/index.ts +++ b/x-pack/legacy/plugins/uptime/common/types/index.ts @@ -34,12 +34,8 @@ export interface LocationDurationLine { export interface MonitorDurationResult { /** The average values for the monitor duration. */ locationDurationLines: LocationDurationLine[]; - /** The counts of up/down checks for the monitor. */ - status: StatusData[]; - /** The maximum status doc count in this chart. */ - statusMaxCount: number; - /** The maximum duration value in this chart. */ - durationMaxValue: number; } -export * from './ping/histogram'; +export interface MonitorIdParam { + monitorId: string; +} diff --git a/x-pack/legacy/plugins/uptime/public/apps/plugin.ts b/x-pack/legacy/plugins/uptime/public/apps/plugin.ts index eec49418910f8..e73598c44c9f0 100644 --- a/x-pack/legacy/plugins/uptime/public/apps/plugin.ts +++ b/x-pack/legacy/plugins/uptime/public/apps/plugin.ts @@ -4,23 +4,13 @@ * you may not use this file except in compliance with the Elastic License. */ -import { - LegacyCoreStart, - LegacyCoreSetup, - PluginInitializerContext, - AppMountParameters, -} from 'src/core/public'; -import { PluginsStart, PluginsSetup } from 'ui/new_platform/new_platform'; +import { LegacyCoreSetup, PluginInitializerContext, AppMountParameters } from 'src/core/public'; +import { PluginsSetup } from 'ui/new_platform/new_platform'; import { FeatureCatalogueCategory } from '../../../../../../src/plugins/home/public'; import { UMFrontendLibs } from '../lib/lib'; import { PLUGIN } from '../../common/constants'; import { getKibanaFrameworkAdapter } from '../lib/adapters/framework/new_platform_adapter'; -export interface StartObject { - core: LegacyCoreStart; - plugins: PluginsStart; -} - export interface SetupObject { core: LegacyCoreSetup; plugins: PluginsSetup; diff --git a/x-pack/legacy/plugins/uptime/public/components/functional/monitor_list/monitor_list_drawer/__tests__/__snapshots__/location_link.test.tsx.snap b/x-pack/legacy/plugins/uptime/public/components/common/__tests__/__snapshots__/location_link.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/functional/monitor_list/monitor_list_drawer/__tests__/__snapshots__/location_link.test.tsx.snap rename to x-pack/legacy/plugins/uptime/public/components/common/__tests__/__snapshots__/location_link.test.tsx.snap diff --git a/x-pack/legacy/plugins/uptime/public/components/functional/__tests__/__snapshots__/uptime_date_picker.test.tsx.snap b/x-pack/legacy/plugins/uptime/public/components/common/__tests__/__snapshots__/uptime_date_picker.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/functional/__tests__/__snapshots__/uptime_date_picker.test.tsx.snap rename to x-pack/legacy/plugins/uptime/public/components/common/__tests__/__snapshots__/uptime_date_picker.test.tsx.snap diff --git a/x-pack/legacy/plugins/uptime/public/components/functional/monitor_list/monitor_list_drawer/__tests__/location_link.test.tsx b/x-pack/legacy/plugins/uptime/public/components/common/__tests__/location_link.test.tsx similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/functional/monitor_list/monitor_list_drawer/__tests__/location_link.test.tsx rename to x-pack/legacy/plugins/uptime/public/components/common/__tests__/location_link.test.tsx diff --git a/x-pack/legacy/plugins/uptime/public/components/functional/__tests__/uptime_date_picker.test.tsx b/x-pack/legacy/plugins/uptime/public/components/common/__tests__/uptime_date_picker.test.tsx similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/functional/__tests__/uptime_date_picker.test.tsx rename to x-pack/legacy/plugins/uptime/public/components/common/__tests__/uptime_date_picker.test.tsx diff --git a/x-pack/legacy/plugins/uptime/public/components/functional/charts/__tests__/__snapshots__/chart_empty_state.test.tsx.snap b/x-pack/legacy/plugins/uptime/public/components/common/charts/__tests__/__snapshots__/chart_empty_state.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/functional/charts/__tests__/__snapshots__/chart_empty_state.test.tsx.snap rename to x-pack/legacy/plugins/uptime/public/components/common/charts/__tests__/__snapshots__/chart_empty_state.test.tsx.snap diff --git a/x-pack/legacy/plugins/uptime/public/components/functional/charts/__tests__/__snapshots__/chart_wrapper.test.tsx.snap b/x-pack/legacy/plugins/uptime/public/components/common/charts/__tests__/__snapshots__/chart_wrapper.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/functional/charts/__tests__/__snapshots__/chart_wrapper.test.tsx.snap rename to x-pack/legacy/plugins/uptime/public/components/common/charts/__tests__/__snapshots__/chart_wrapper.test.tsx.snap diff --git a/x-pack/legacy/plugins/uptime/public/components/functional/charts/__tests__/__snapshots__/donut_chart.test.tsx.snap b/x-pack/legacy/plugins/uptime/public/components/common/charts/__tests__/__snapshots__/donut_chart.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/functional/charts/__tests__/__snapshots__/donut_chart.test.tsx.snap rename to x-pack/legacy/plugins/uptime/public/components/common/charts/__tests__/__snapshots__/donut_chart.test.tsx.snap diff --git a/x-pack/legacy/plugins/uptime/public/components/functional/charts/__tests__/__snapshots__/donut_chart_legend.test.tsx.snap b/x-pack/legacy/plugins/uptime/public/components/common/charts/__tests__/__snapshots__/donut_chart_legend.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/functional/charts/__tests__/__snapshots__/donut_chart_legend.test.tsx.snap rename to x-pack/legacy/plugins/uptime/public/components/common/charts/__tests__/__snapshots__/donut_chart_legend.test.tsx.snap diff --git a/x-pack/legacy/plugins/uptime/public/components/functional/charts/__tests__/__snapshots__/donut_chart_legend_row.test.tsx.snap b/x-pack/legacy/plugins/uptime/public/components/common/charts/__tests__/__snapshots__/donut_chart_legend_row.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/functional/charts/__tests__/__snapshots__/donut_chart_legend_row.test.tsx.snap rename to x-pack/legacy/plugins/uptime/public/components/common/charts/__tests__/__snapshots__/donut_chart_legend_row.test.tsx.snap diff --git a/x-pack/legacy/plugins/uptime/public/components/functional/charts/__tests__/__snapshots__/duration_charts.test.tsx.snap b/x-pack/legacy/plugins/uptime/public/components/common/charts/__tests__/__snapshots__/duration_charts.test.tsx.snap similarity index 99% rename from x-pack/legacy/plugins/uptime/public/components/functional/charts/__tests__/__snapshots__/duration_charts.test.tsx.snap rename to x-pack/legacy/plugins/uptime/public/components/common/charts/__tests__/__snapshots__/duration_charts.test.tsx.snap index 6c38f3e338cfd..96918ab68f716 100644 --- a/x-pack/legacy/plugins/uptime/public/components/functional/charts/__tests__/__snapshots__/duration_charts.test.tsx.snap +++ b/x-pack/legacy/plugins/uptime/public/components/common/charts/__tests__/__snapshots__/duration_charts.test.tsx.snap @@ -53,7 +53,6 @@ exports[`MonitorCharts component renders the component without errors 1`] = ` > <DurationChartComponent anomalies={null} - hasMLJob={false} loading={false} locationDurationLines={ Array [ diff --git a/x-pack/legacy/plugins/uptime/public/components/functional/charts/__tests__/__snapshots__/monitor_bar_series.test.tsx.snap b/x-pack/legacy/plugins/uptime/public/components/common/charts/__tests__/__snapshots__/monitor_bar_series.test.tsx.snap similarity index 98% rename from x-pack/legacy/plugins/uptime/public/components/functional/charts/__tests__/__snapshots__/monitor_bar_series.test.tsx.snap rename to x-pack/legacy/plugins/uptime/public/components/common/charts/__tests__/__snapshots__/monitor_bar_series.test.tsx.snap index 8ca73879cab8c..b389139d71dbf 100644 --- a/x-pack/legacy/plugins/uptime/public/components/functional/charts/__tests__/__snapshots__/monitor_bar_series.test.tsx.snap +++ b/x-pack/legacy/plugins/uptime/public/components/common/charts/__tests__/__snapshots__/monitor_bar_series.test.tsx.snap @@ -82,7 +82,6 @@ exports[`MonitorBarSeries component shallow renders a series when there are down } > <MonitorBarSeries - dangerColor="A danger color" histogramSeries={ Array [ Object { diff --git a/x-pack/legacy/plugins/uptime/public/components/functional/charts/__tests__/__snapshots__/ping_histogram.test.tsx.snap b/x-pack/legacy/plugins/uptime/public/components/common/charts/__tests__/__snapshots__/ping_histogram.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/functional/charts/__tests__/__snapshots__/ping_histogram.test.tsx.snap rename to x-pack/legacy/plugins/uptime/public/components/common/charts/__tests__/__snapshots__/ping_histogram.test.tsx.snap diff --git a/x-pack/legacy/plugins/uptime/public/components/functional/charts/__tests__/chart_empty_state.test.tsx b/x-pack/legacy/plugins/uptime/public/components/common/charts/__tests__/chart_empty_state.test.tsx similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/functional/charts/__tests__/chart_empty_state.test.tsx rename to x-pack/legacy/plugins/uptime/public/components/common/charts/__tests__/chart_empty_state.test.tsx diff --git a/x-pack/legacy/plugins/uptime/public/components/functional/charts/__tests__/chart_wrapper.test.tsx b/x-pack/legacy/plugins/uptime/public/components/common/charts/__tests__/chart_wrapper.test.tsx similarity index 97% rename from x-pack/legacy/plugins/uptime/public/components/functional/charts/__tests__/chart_wrapper.test.tsx rename to x-pack/legacy/plugins/uptime/public/components/common/charts/__tests__/chart_wrapper.test.tsx index 443874ecdb5da..9cf54bc836714 100644 --- a/x-pack/legacy/plugins/uptime/public/components/functional/charts/__tests__/chart_wrapper.test.tsx +++ b/x-pack/legacy/plugins/uptime/public/components/common/charts/__tests__/chart_wrapper.test.tsx @@ -10,7 +10,7 @@ import { mount } from 'enzyme'; import { nextTick } from 'test_utils/enzyme_helpers'; import { shallowWithIntl } from 'test_utils/enzyme_helpers'; import { ChartWrapper } from '../chart_wrapper'; -import { SnapshotHeading } from '../../snapshot_heading'; +import { SnapshotHeading } from '../../../overview/snapshot/snapshot_heading'; import { DonutChart } from '../donut_chart'; const SNAPSHOT_CHART_WIDTH = 144; const SNAPSHOT_CHART_HEIGHT = 144; diff --git a/x-pack/legacy/plugins/uptime/public/components/functional/charts/__tests__/donut_chart.test.tsx b/x-pack/legacy/plugins/uptime/public/components/common/charts/__tests__/donut_chart.test.tsx similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/functional/charts/__tests__/donut_chart.test.tsx rename to x-pack/legacy/plugins/uptime/public/components/common/charts/__tests__/donut_chart.test.tsx diff --git a/x-pack/legacy/plugins/uptime/public/components/functional/charts/__tests__/donut_chart_legend.test.tsx b/x-pack/legacy/plugins/uptime/public/components/common/charts/__tests__/donut_chart_legend.test.tsx similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/functional/charts/__tests__/donut_chart_legend.test.tsx rename to x-pack/legacy/plugins/uptime/public/components/common/charts/__tests__/donut_chart_legend.test.tsx diff --git a/x-pack/legacy/plugins/uptime/public/components/functional/charts/__tests__/donut_chart_legend_row.test.tsx b/x-pack/legacy/plugins/uptime/public/components/common/charts/__tests__/donut_chart_legend_row.test.tsx similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/functional/charts/__tests__/donut_chart_legend_row.test.tsx rename to x-pack/legacy/plugins/uptime/public/components/common/charts/__tests__/donut_chart_legend_row.test.tsx diff --git a/x-pack/legacy/plugins/uptime/public/components/common/charts/__tests__/duration_charts.test.tsx b/x-pack/legacy/plugins/uptime/public/components/common/charts/__tests__/duration_charts.test.tsx new file mode 100644 index 0000000000000..589fbc6bceb45 --- /dev/null +++ b/x-pack/legacy/plugins/uptime/public/components/common/charts/__tests__/duration_charts.test.tsx @@ -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 React from 'react'; +import DateMath from '@elastic/datemath'; +import { DurationChartComponent } from '../duration_chart'; +import { MonitorDurationResult } from '../../../../../common/types'; +import { shallowWithRouter } from '../../../../lib'; + +describe('MonitorCharts component', () => { + let dateMathSpy: any; + const MOCK_DATE_VALUE = 20; + + beforeEach(() => { + dateMathSpy = jest.spyOn(DateMath, 'parse'); + dateMathSpy.mockReturnValue(MOCK_DATE_VALUE); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + const chartResponse: { monitorChartsData: MonitorDurationResult } = { + monitorChartsData: { + locationDurationLines: [ + { + name: 'somewhere', + line: [ + { x: 1548697620000, y: 743928.2027027027 }, + { x: 1548697920000, y: 766840.0133333333 }, + { x: 1548698220000, y: 786970.8266666667 }, + { x: 1548698520000, y: 781064.7808219178 }, + { x: 1548698820000, y: 741563.04 }, + { x: 1548699120000, y: 759354.6756756756 }, + { x: 1548699420000, y: 737533.3866666667 }, + { x: 1548699720000, y: 728669.0266666666 }, + { x: 1548700020000, y: 719951.64 }, + { x: 1548700320000, y: 769181.7866666666 }, + { x: 1548700620000, y: 740805.2666666667 }, + ], + }, + ], + }, + }; + + it('renders the component without errors', () => { + const component = shallowWithRouter( + <DurationChartComponent + loading={false} + anomalies={null} + locationDurationLines={chartResponse.monitorChartsData.locationDurationLines} + /> + ); + expect(component).toMatchSnapshot(); + }); +}); diff --git a/x-pack/legacy/plugins/uptime/public/components/functional/charts/__tests__/get_tick_format.test.ts b/x-pack/legacy/plugins/uptime/public/components/common/charts/__tests__/get_tick_format.test.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/functional/charts/__tests__/get_tick_format.test.ts rename to x-pack/legacy/plugins/uptime/public/components/common/charts/__tests__/get_tick_format.test.ts diff --git a/x-pack/legacy/plugins/uptime/public/components/functional/charts/__tests__/monitor_bar_series.test.tsx b/x-pack/legacy/plugins/uptime/public/components/common/charts/__tests__/monitor_bar_series.test.tsx similarity index 90% rename from x-pack/legacy/plugins/uptime/public/components/functional/charts/__tests__/monitor_bar_series.test.tsx rename to x-pack/legacy/plugins/uptime/public/components/common/charts/__tests__/monitor_bar_series.test.tsx index 5d4e112aa5f28..4522f8d633fa6 100644 --- a/x-pack/legacy/plugins/uptime/public/components/functional/charts/__tests__/monitor_bar_series.test.tsx +++ b/x-pack/legacy/plugins/uptime/public/components/common/charts/__tests__/monitor_bar_series.test.tsx @@ -7,14 +7,13 @@ import React from 'react'; import { MonitorBarSeries, MonitorBarSeriesProps } from '../monitor_bar_series'; import { renderWithRouter, shallowWithRouter } from '../../../../lib'; -import { SummaryHistogramPoint } from '../../../../../common/graphql/types'; +import { HistogramPoint } from '../../../../../common/runtime_types'; describe('MonitorBarSeries component', () => { let props: MonitorBarSeriesProps; - let histogramSeries: SummaryHistogramPoint[]; + let histogramSeries: HistogramPoint[]; beforeEach(() => { props = { - dangerColor: 'A danger color', histogramSeries: [ { timestamp: 124, @@ -193,16 +192,12 @@ describe('MonitorBarSeries component', () => { }); it('shallow renders nothing if the data series is null', () => { - const component = shallowWithRouter( - <MonitorBarSeries dangerColor="danger" histogramSeries={null} /> - ); + const component = shallowWithRouter(<MonitorBarSeries histogramSeries={null} />); expect(component).toEqual({}); }); it('renders if the data series is present', () => { - const component = renderWithRouter( - <MonitorBarSeries dangerColor="danger" histogramSeries={histogramSeries} /> - ); + const component = renderWithRouter(<MonitorBarSeries histogramSeries={histogramSeries} />); expect(component).toMatchSnapshot(); }); }); diff --git a/x-pack/legacy/plugins/uptime/public/components/functional/charts/__tests__/ping_histogram.test.tsx b/x-pack/legacy/plugins/uptime/public/components/common/charts/__tests__/ping_histogram.test.tsx similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/functional/charts/__tests__/ping_histogram.test.tsx rename to x-pack/legacy/plugins/uptime/public/components/common/charts/__tests__/ping_histogram.test.tsx diff --git a/x-pack/legacy/plugins/uptime/public/components/functional/charts/annotation_tooltip.tsx b/x-pack/legacy/plugins/uptime/public/components/common/charts/annotation_tooltip.tsx similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/functional/charts/annotation_tooltip.tsx rename to x-pack/legacy/plugins/uptime/public/components/common/charts/annotation_tooltip.tsx diff --git a/x-pack/legacy/plugins/uptime/public/components/functional/charts/chart_empty_state.tsx b/x-pack/legacy/plugins/uptime/public/components/common/charts/chart_empty_state.tsx similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/functional/charts/chart_empty_state.tsx rename to x-pack/legacy/plugins/uptime/public/components/common/charts/chart_empty_state.tsx diff --git a/x-pack/legacy/plugins/uptime/public/components/functional/charts/chart_wrapper/chart_wrapper.tsx b/x-pack/legacy/plugins/uptime/public/components/common/charts/chart_wrapper/chart_wrapper.tsx similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/functional/charts/chart_wrapper/chart_wrapper.tsx rename to x-pack/legacy/plugins/uptime/public/components/common/charts/chart_wrapper/chart_wrapper.tsx diff --git a/x-pack/legacy/plugins/uptime/public/components/functional/charts/chart_wrapper/index.ts b/x-pack/legacy/plugins/uptime/public/components/common/charts/chart_wrapper/index.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/functional/charts/chart_wrapper/index.ts rename to x-pack/legacy/plugins/uptime/public/components/common/charts/chart_wrapper/index.ts diff --git a/x-pack/legacy/plugins/uptime/public/components/functional/charts/donut_chart.tsx b/x-pack/legacy/plugins/uptime/public/components/common/charts/donut_chart.tsx similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/functional/charts/donut_chart.tsx rename to x-pack/legacy/plugins/uptime/public/components/common/charts/donut_chart.tsx diff --git a/x-pack/legacy/plugins/uptime/public/components/functional/charts/donut_chart_legend.tsx b/x-pack/legacy/plugins/uptime/public/components/common/charts/donut_chart_legend.tsx similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/functional/charts/donut_chart_legend.tsx rename to x-pack/legacy/plugins/uptime/public/components/common/charts/donut_chart_legend.tsx diff --git a/x-pack/legacy/plugins/uptime/public/components/functional/charts/donut_chart_legend_row.tsx b/x-pack/legacy/plugins/uptime/public/components/common/charts/donut_chart_legend_row.tsx similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/functional/charts/donut_chart_legend_row.tsx rename to x-pack/legacy/plugins/uptime/public/components/common/charts/donut_chart_legend_row.tsx diff --git a/x-pack/legacy/plugins/uptime/public/components/common/charts/duration_chart.tsx b/x-pack/legacy/plugins/uptime/public/components/common/charts/duration_chart.tsx new file mode 100644 index 0000000000000..c82b2a1cf9fe2 --- /dev/null +++ b/x-pack/legacy/plugins/uptime/public/components/common/charts/duration_chart.tsx @@ -0,0 +1,122 @@ +/* + * 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, { useState } from 'react'; +import { i18n } from '@kbn/i18n'; +import moment from 'moment'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { Axis, Chart, Position, timeFormatter, Settings, SeriesIdentifier } from '@elastic/charts'; +import { getChartDateLabel } from '../../../lib/helper'; +import { LocationDurationLine } from '../../../../common/types'; +import { DurationLineSeriesList } from './duration_line_series_list'; +import { ChartWrapper } from './chart_wrapper'; +import { useUrlParams } from '../../../hooks'; +import { getTickFormat } from './get_tick_format'; +import { ChartEmptyState } from './chart_empty_state'; +import { DurationAnomaliesBar } from './duration_line_bar_list'; +import { AnomalyRecords } from '../../../state/actions'; + +interface DurationChartProps { + /** + * Timeseries data that is used to express an average line series + * on the duration chart. One entry per location + */ + locationDurationLines: LocationDurationLine[]; + + /** + * To represent the loading spinner on chart + */ + loading: boolean; + + anomalies: AnomalyRecords | null; +} + +/** + * This chart is intended to visualize monitor duration performance over time to + * the users in a helpful way. Its x-axis is based on a timeseries, the y-axis is in + * milliseconds. + * @param props The props required for this component to render properly + */ +export const DurationChartComponent = ({ + locationDurationLines, + anomalies, + loading, +}: DurationChartProps) => { + const hasLines = locationDurationLines.length > 0; + const [getUrlParams, updateUrlParams] = useUrlParams(); + const { absoluteDateRangeStart: min, absoluteDateRangeEnd: max } = getUrlParams(); + + const [hiddenLegends, setHiddenLegends] = useState<string[]>([]); + + const onBrushEnd = (minX: number, maxX: number) => { + updateUrlParams({ + dateRangeStart: moment(minX).toISOString(), + dateRangeEnd: moment(maxX).toISOString(), + }); + }; + + const legendToggleVisibility = (legendItem: SeriesIdentifier | null) => { + if (legendItem) { + setHiddenLegends(prevState => { + if (prevState.includes(legendItem.specId)) { + return [...prevState.filter(item => item !== legendItem.specId)]; + } else { + return [...prevState, legendItem.specId]; + } + }); + } + }; + + return ( + <ChartWrapper height="400px" loading={loading}> + {hasLines ? ( + <Chart> + <Settings + xDomain={{ min, max }} + showLegend + showLegendExtra + legendPosition={Position.Bottom} + onBrushEnd={onBrushEnd} + onLegendItemClick={legendToggleVisibility} + /> + <Axis + id="bottom" + position={Position.Bottom} + showOverlappingTicks={true} + tickFormat={timeFormatter(getChartDateLabel(min, max))} + title={i18n.translate('xpack.uptime.monitorCharts.durationChart.bottomAxis.title', { + defaultMessage: 'Timestamp', + })} + /> + <Axis + domain={{ min: 0 }} + id="left" + position={Position.Left} + tickFormat={d => getTickFormat(d)} + title={i18n.translate('xpack.uptime.monitorCharts.durationChart.leftAxis.title', { + defaultMessage: 'Duration ms', + })} + /> + <DurationLineSeriesList lines={locationDurationLines} /> + <DurationAnomaliesBar anomalies={anomalies} hiddenLegends={hiddenLegends} /> + </Chart> + ) : ( + <ChartEmptyState + body={ + <FormattedMessage + id="xpack.uptime.durationChart.emptyPrompt.description" + defaultMessage="This monitor has never been {emphasizedText} during the selected time range." + values={{ emphasizedText: <strong>up</strong> }} + /> + } + title={i18n.translate('xpack.uptime.durationChart.emptyPrompt.title', { + defaultMessage: 'No duration data available', + })} + /> + )} + </ChartWrapper> + ); +}; diff --git a/x-pack/legacy/plugins/uptime/public/components/functional/charts/duration_line_bar_list.tsx b/x-pack/legacy/plugins/uptime/public/components/common/charts/duration_line_bar_list.tsx similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/functional/charts/duration_line_bar_list.tsx rename to x-pack/legacy/plugins/uptime/public/components/common/charts/duration_line_bar_list.tsx diff --git a/x-pack/legacy/plugins/uptime/public/components/functional/charts/duration_line_series_list.tsx b/x-pack/legacy/plugins/uptime/public/components/common/charts/duration_line_series_list.tsx similarity index 97% rename from x-pack/legacy/plugins/uptime/public/components/functional/charts/duration_line_series_list.tsx rename to x-pack/legacy/plugins/uptime/public/components/common/charts/duration_line_series_list.tsx index 912bc5bb0501b..4223e918393b6 100644 --- a/x-pack/legacy/plugins/uptime/public/components/functional/charts/duration_line_series_list.tsx +++ b/x-pack/legacy/plugins/uptime/public/components/common/charts/duration_line_series_list.tsx @@ -21,7 +21,7 @@ export const DurationLineSeriesList = ({ lines }: Props) => ( // this id is used for the line chart representing the average duration length data={line.map(({ x, y }) => [x, microsToMillis(y || null)])} id={`loc-avg-${name}`} - key={`locline-${name}`} + key={`loc-line-${name}`} name={name} xAccessor={0} xScaleType="time" diff --git a/x-pack/legacy/plugins/uptime/public/components/functional/charts/get_tick_format.ts b/x-pack/legacy/plugins/uptime/public/components/common/charts/get_tick_format.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/functional/charts/get_tick_format.ts rename to x-pack/legacy/plugins/uptime/public/components/common/charts/get_tick_format.ts diff --git a/x-pack/legacy/plugins/uptime/public/components/functional/charts/index.ts b/x-pack/legacy/plugins/uptime/public/components/common/charts/index.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/functional/charts/index.ts rename to x-pack/legacy/plugins/uptime/public/components/common/charts/index.ts diff --git a/x-pack/legacy/plugins/uptime/public/components/functional/charts/monitor_bar_series.tsx b/x-pack/legacy/plugins/uptime/public/components/common/charts/monitor_bar_series.tsx similarity index 87% rename from x-pack/legacy/plugins/uptime/public/components/functional/charts/monitor_bar_series.tsx rename to x-pack/legacy/plugins/uptime/public/components/common/charts/monitor_bar_series.tsx index 6a1e255d308d7..5e11685e36140 100644 --- a/x-pack/legacy/plugins/uptime/public/components/functional/charts/monitor_bar_series.tsx +++ b/x-pack/legacy/plugins/uptime/public/components/common/charts/monitor_bar_series.tsx @@ -14,23 +14,20 @@ import { timeFormatter, } from '@elastic/charts'; import { i18n } from '@kbn/i18n'; -import React from 'react'; +import React, { useContext } from 'react'; import moment from 'moment'; import { FormattedMessage } from '@kbn/i18n/react'; import { EuiText, EuiToolTip } from '@elastic/eui'; -import { SummaryHistogramPoint } from '../../../../common/graphql/types'; +import { HistogramPoint } from '../../../../common/runtime_types'; import { getChartDateLabel, seriesHasDownValues } from '../../../lib/helper'; import { useUrlParams } from '../../../hooks'; +import { UptimeThemeContext } from '../../../contexts'; export interface MonitorBarSeriesProps { - /** - * The color to use for the display of down states. - */ - dangerColor: string; /** * The timeseries data to display. */ - histogramSeries: SummaryHistogramPoint[] | null; + histogramSeries: HistogramPoint[] | null; } /** @@ -38,7 +35,10 @@ export interface MonitorBarSeriesProps { * so we will only render the series component if there are down counts for the selected monitor. * @param props - the values for the monitor this chart visualizes */ -export const MonitorBarSeries = ({ dangerColor, histogramSeries }: MonitorBarSeriesProps) => { +export const MonitorBarSeries = ({ histogramSeries }: MonitorBarSeriesProps) => { + const { + colors: { danger }, + } = useContext(UptimeThemeContext); const [getUrlParams, updateUrlParams] = useUrlParams(); const { absoluteDateRangeStart, absoluteDateRangeEnd } = getUrlParams(); @@ -68,7 +68,7 @@ export const MonitorBarSeries = ({ dangerColor, histogramSeries }: MonitorBarSer /> <BarSeries id={id} - color={dangerColor} + color={danger} data={(histogramSeries || []).map(({ timestamp, down }) => [timestamp, down])} name={i18n.translate('xpack.uptime.monitorList.downLineSeries.downLabel', { defaultMessage: 'Down checks', diff --git a/x-pack/legacy/plugins/uptime/public/components/common/charts/ping_histogram.tsx b/x-pack/legacy/plugins/uptime/public/components/common/charts/ping_histogram.tsx new file mode 100644 index 0000000000000..66e86d6731236 --- /dev/null +++ b/x-pack/legacy/plugins/uptime/public/components/common/charts/ping_histogram.tsx @@ -0,0 +1,173 @@ +/* + * 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 { Axis, BarSeries, Chart, Position, Settings, timeFormatter } from '@elastic/charts'; +import { EuiTitle } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import React, { useContext } from 'react'; +import { FormattedMessage } from '@kbn/i18n/react'; +import moment from 'moment'; +import { getChartDateLabel } from '../../../lib/helper'; +import { ChartWrapper } from './chart_wrapper'; +import { UptimeThemeContext } from '../../../contexts'; +import { HistogramResult } from '../../../../common/runtime_types'; +import { useUrlParams } from '../../../hooks'; +import { ChartEmptyState } from './chart_empty_state'; + +export interface PingHistogramComponentProps { + /** + * The date/time for the start of the timespan. + */ + absoluteStartDate: number; + /** + * The date/time for the end of the timespan. + */ + absoluteEndDate: number; + + /** + * Height is needed, since by default charts takes height of 100% + */ + height?: string; + + data: HistogramResult | null; + + loading?: boolean; +} + +interface BarPoint { + x?: number; + y?: number; + type: string; +} + +export const PingHistogramComponent: React.FC<PingHistogramComponentProps> = ({ + absoluteStartDate, + absoluteEndDate, + data, + loading = false, + height, +}) => { + const { + colors: { danger, gray }, + } = useContext(UptimeThemeContext); + + const [, updateUrlParams] = useUrlParams(); + + let content: JSX.Element | undefined; + if (!data?.histogram?.length) { + content = ( + <ChartEmptyState + title={i18n.translate('xpack.uptime.snapshot.noDataTitle', { + defaultMessage: 'No ping data available', + })} + body={i18n.translate('xpack.uptime.snapshot.noDataDescription', { + defaultMessage: 'There are no pings in the selected time range.', + })} + /> + ); + } else { + const { histogram } = data; + + const downSpecId = i18n.translate('xpack.uptime.snapshotHistogram.series.downLabel', { + defaultMessage: 'Down', + }); + + const upMonitorsId = i18n.translate('xpack.uptime.snapshotHistogram.series.upLabel', { + defaultMessage: 'Up', + }); + + const onBrushEnd = (min: number, max: number) => { + updateUrlParams({ + dateRangeStart: moment(min).toISOString(), + dateRangeEnd: moment(max).toISOString(), + }); + }; + + const barData: BarPoint[] = []; + + histogram.forEach(({ x, upCount, downCount }) => { + barData.push( + { x, y: downCount ?? 0, type: downSpecId }, + { x, y: upCount ?? 0, type: upMonitorsId } + ); + }); + + content = ( + <ChartWrapper + height={height} + loading={loading} + aria-label={i18n.translate('xpack.uptime.snapshotHistogram.description', { + defaultMessage: + 'Bar Chart showing uptime status over time from {startTime} to {endTime}.', + values: { + startTime: moment(new Date(absoluteStartDate).valueOf()).fromNow(), + endTime: moment(new Date(absoluteEndDate).valueOf()).fromNow(), + }, + })} + > + <Chart> + <Settings + xDomain={{ + min: absoluteStartDate, + max: absoluteEndDate, + }} + showLegend={false} + onBrushEnd={onBrushEnd} + /> + <Axis + id={i18n.translate('xpack.uptime.snapshotHistogram.xAxisId', { + defaultMessage: 'Ping X Axis', + })} + position={Position.Bottom} + showOverlappingTicks={false} + tickFormat={timeFormatter(getChartDateLabel(absoluteStartDate, absoluteEndDate))} + /> + <Axis + id={i18n.translate('xpack.uptime.snapshotHistogram.yAxisId', { + defaultMessage: 'Ping Y Axis', + })} + position="left" + title={i18n.translate('xpack.uptime.snapshotHistogram.yAxis.title', { + defaultMessage: 'Pings', + description: + 'The label on the y-axis of a chart that displays the number of times Heartbeat has pinged a set of services/websites.', + })} + /> + + <BarSeries + color={[danger, gray]} + data={barData} + id={downSpecId} + name={i18n.translate('xpack.uptime.snapshotHistogram.series.pings', { + defaultMessage: 'Monitor Pings', + })} + stackAccessors={['x']} + splitSeriesAccessors={['type']} + timeZone="local" + xAccessor="x" + xScaleType="time" + yAccessors={['y']} + yScaleType="linear" + /> + </Chart> + </ChartWrapper> + ); + } + + return ( + <> + <EuiTitle size="xs"> + <h2> + <FormattedMessage + id="xpack.uptime.snapshot.pingsOverTimeTitle" + defaultMessage="Pings over time" + /> + </h2> + </EuiTitle> + {content} + </> + ); +}; diff --git a/x-pack/legacy/plugins/uptime/public/components/higher_order/__tests__/__snapshots__/responsive_wrapper.test.tsx.snap b/x-pack/legacy/plugins/uptime/public/components/common/higher_order/__tests__/__snapshots__/responsive_wrapper.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/higher_order/__tests__/__snapshots__/responsive_wrapper.test.tsx.snap rename to x-pack/legacy/plugins/uptime/public/components/common/higher_order/__tests__/__snapshots__/responsive_wrapper.test.tsx.snap diff --git a/x-pack/legacy/plugins/uptime/public/components/higher_order/__tests__/responsive_wrapper.test.tsx b/x-pack/legacy/plugins/uptime/public/components/common/higher_order/__tests__/responsive_wrapper.test.tsx similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/higher_order/__tests__/responsive_wrapper.test.tsx rename to x-pack/legacy/plugins/uptime/public/components/common/higher_order/__tests__/responsive_wrapper.test.tsx diff --git a/x-pack/legacy/plugins/uptime/public/components/common/higher_order/index.ts b/x-pack/legacy/plugins/uptime/public/components/common/higher_order/index.ts new file mode 100644 index 0000000000000..0682f6d0478f3 --- /dev/null +++ b/x-pack/legacy/plugins/uptime/public/components/common/higher_order/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 { ResponsiveWrapperProps, withResponsiveWrapper } from './responsive_wrapper'; diff --git a/x-pack/legacy/plugins/uptime/public/components/higher_order/responsive_wrapper.tsx b/x-pack/legacy/plugins/uptime/public/components/common/higher_order/responsive_wrapper.tsx similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/higher_order/responsive_wrapper.tsx rename to x-pack/legacy/plugins/uptime/public/components/common/higher_order/responsive_wrapper.tsx diff --git a/x-pack/legacy/plugins/uptime/public/components/functional/monitor_list/monitor_list_drawer/location_link.tsx b/x-pack/legacy/plugins/uptime/public/components/common/location_link.tsx similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/functional/monitor_list/monitor_list_drawer/location_link.tsx rename to x-pack/legacy/plugins/uptime/public/components/common/location_link.tsx diff --git a/x-pack/legacy/plugins/uptime/public/components/functional/uptime_date_picker.tsx b/x-pack/legacy/plugins/uptime/public/components/common/uptime_date_picker.tsx similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/functional/uptime_date_picker.tsx rename to x-pack/legacy/plugins/uptime/public/components/common/uptime_date_picker.tsx index 7d2123af8ff9c..4254004dba4e0 100644 --- a/x-pack/legacy/plugins/uptime/public/components/functional/uptime_date_picker.tsx +++ b/x-pack/legacy/plugins/uptime/public/components/common/uptime_date_picker.tsx @@ -4,8 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ -import { EuiSuperDatePicker } from '@elastic/eui'; import React, { useContext } from 'react'; +import { EuiSuperDatePicker } from '@elastic/eui'; import { useUrlParams } from '../../hooks'; import { CLIENT_DEFAULTS } from '../../../common/constants'; import { UptimeRefreshContext, UptimeSettingsContext } from '../../contexts'; diff --git a/x-pack/legacy/plugins/uptime/public/components/connected/alerts/alert_monitor_status.tsx b/x-pack/legacy/plugins/uptime/public/components/connected/alerts/alert_monitor_status.tsx deleted file mode 100644 index 1529ab6db8875..0000000000000 --- a/x-pack/legacy/plugins/uptime/public/components/connected/alerts/alert_monitor_status.tsx +++ /dev/null @@ -1,43 +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 from 'react'; -import { useSelector } from 'react-redux'; -import { DataPublicPluginSetup } from 'src/plugins/data/public'; -import { selectMonitorStatusAlert } from '../../../state/selectors'; -import { AlertMonitorStatusComponent } from '../../functional/alerts/alert_monitor_status'; - -interface Props { - autocomplete: DataPublicPluginSetup['autocomplete']; - enabled: boolean; - numTimes: number; - setAlertParams: (key: string, value: any) => void; - timerange: { - from: string; - to: string; - }; -} - -export const AlertMonitorStatus = ({ - autocomplete, - enabled, - numTimes, - setAlertParams, - timerange, -}: Props) => { - const { filters, locations } = useSelector(selectMonitorStatusAlert); - return ( - <AlertMonitorStatusComponent - autocomplete={autocomplete} - enabled={enabled} - filters={filters} - locations={locations} - numTimes={numTimes} - setAlertParams={setAlertParams} - timerange={timerange} - /> - ); -}; diff --git a/x-pack/legacy/plugins/uptime/public/components/connected/charts/monitor_duration.tsx b/x-pack/legacy/plugins/uptime/public/components/connected/charts/monitor_duration.tsx deleted file mode 100644 index 7d1cb08cb8b1c..0000000000000 --- a/x-pack/legacy/plugins/uptime/public/components/connected/charts/monitor_duration.tsx +++ /dev/null @@ -1,86 +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, { useContext, useEffect } from 'react'; -import { useDispatch, useSelector } from 'react-redux'; -import { useUrlParams } from '../../../hooks'; -import { - getAnomalyRecordsAction, - getMLCapabilitiesAction, - getMonitorDurationAction, -} from '../../../state/actions'; -import { DurationChartComponent } from '../../functional/charts'; -import { - anomaliesSelector, - hasMLFeatureAvailable, - hasMLJobSelector, - selectDurationLines, -} from '../../../state/selectors'; -import { UptimeRefreshContext } from '../../../contexts'; -import { getMLJobId } from '../../../state/api/ml_anomaly'; -import { JobStat } from '../../../../../../../plugins/ml/common/types/data_recognizer'; - -interface Props { - monitorId: string; -} - -export const DurationChart: React.FC<Props> = ({ monitorId }: Props) => { - const [getUrlParams] = useUrlParams(); - const { - dateRangeStart, - dateRangeEnd, - absoluteDateRangeStart, - absoluteDateRangeEnd, - } = getUrlParams(); - - const { durationLines, loading } = useSelector(selectDurationLines); - - const isMLAvailable = useSelector(hasMLFeatureAvailable); - - const { data: mlJobs, loading: jobsLoading } = useSelector(hasMLJobSelector); - - const hasMLJob = - !!mlJobs?.jobsExist && - !!mlJobs.jobs.find((job: JobStat) => job.id === getMLJobId(monitorId as string)); - - const anomalies = useSelector(anomaliesSelector); - - const dispatch = useDispatch(); - - const { lastRefresh } = useContext(UptimeRefreshContext); - - useEffect(() => { - if (isMLAvailable) { - const anomalyParams = { - listOfMonitorIds: [monitorId], - dateStart: absoluteDateRangeStart, - dateEnd: absoluteDateRangeEnd, - }; - - dispatch(getAnomalyRecordsAction.get(anomalyParams)); - } - - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [dateRangeStart, dateRangeEnd, dispatch, lastRefresh, monitorId, isMLAvailable]); - - useEffect(() => { - const params = { monitorId, dateStart: dateRangeStart, dateEnd: dateRangeEnd }; - dispatch(getMonitorDurationAction(params)); - }, [dateRangeStart, dateRangeEnd, dispatch, lastRefresh, monitorId]); - - useEffect(() => { - dispatch(getMLCapabilitiesAction.get()); - }, [dispatch]); - - return ( - <DurationChartComponent - anomalies={anomalies} - hasMLJob={hasMLJob} - loading={loading || jobsLoading} - locationDurationLines={durationLines?.locationDurationLines ?? []} - /> - ); -}; diff --git a/x-pack/legacy/plugins/uptime/public/components/connected/charts/ping_histogram.tsx b/x-pack/legacy/plugins/uptime/public/components/connected/charts/ping_histogram.tsx deleted file mode 100644 index 50f91be4ff09f..0000000000000 --- a/x-pack/legacy/plugins/uptime/public/components/connected/charts/ping_histogram.tsx +++ /dev/null @@ -1,83 +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, { useEffect } from 'react'; -import { connect } from 'react-redux'; -import { AppState } from '../../../state'; -import { - PingHistogramComponent, - PingHistogramComponentProps, -} from '../../functional/charts/ping_histogram'; -import { getPingHistogram } from '../../../state/actions'; -import { selectPingHistogram } from '../../../state/selectors'; -import { withResponsiveWrapper, ResponsiveWrapperProps } from '../../higher_order'; -import { GetPingHistogramParams, HistogramResult } from '../../../../common/types'; -import { useUrlParams } from '../../../hooks'; - -type Props = ResponsiveWrapperProps & - Pick<PingHistogramComponentProps, 'height' | 'data' | 'loading'> & - DispatchProps & { lastRefresh: number; monitorId?: string; esKuery?: string }; - -const PingHistogramContainer: React.FC<Props> = ({ - data, - loadData, - monitorId, - lastRefresh, - height, - loading, - esKuery, -}) => { - const [getUrlParams] = useUrlParams(); - const { - absoluteDateRangeStart, - absoluteDateRangeEnd, - dateRangeStart: dateStart, - dateRangeEnd: dateEnd, - statusFilter, - } = getUrlParams(); - - useEffect(() => { - loadData({ monitorId, dateStart, dateEnd, statusFilter, filters: esKuery }); - }, [loadData, dateStart, dateEnd, monitorId, statusFilter, lastRefresh, esKuery]); - return ( - <PingHistogramComponent - data={data} - absoluteStartDate={absoluteDateRangeStart} - absoluteEndDate={absoluteDateRangeEnd} - height={height} - loading={loading} - /> - ); -}; - -interface StateProps { - data: HistogramResult | null; - loading: boolean; - lastRefresh: number; - esKuery: string; -} - -interface DispatchProps { - loadData: typeof getPingHistogram; -} - -const mapStateToProps = (state: AppState): StateProps => ({ ...selectPingHistogram(state) }); - -const mapDispatchToProps = (dispatch: any): DispatchProps => ({ - loadData: (params: GetPingHistogramParams) => { - return dispatch(getPingHistogram(params)); - }, -}); - -export const PingHistogram = connect< - StateProps, - DispatchProps, - Pick<PingHistogramComponentProps, 'height'>, - AppState ->( - mapStateToProps, - mapDispatchToProps -)(withResponsiveWrapper(PingHistogramContainer)); diff --git a/x-pack/legacy/plugins/uptime/public/components/connected/charts/snapshot_container.tsx b/x-pack/legacy/plugins/uptime/public/components/connected/charts/snapshot_container.tsx deleted file mode 100644 index ac8ff13d1edce..0000000000000 --- a/x-pack/legacy/plugins/uptime/public/components/connected/charts/snapshot_container.tsx +++ /dev/null @@ -1,94 +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, { useEffect } from 'react'; -import { connect } from 'react-redux'; -import { useUrlParams } from '../../../hooks'; -import { AppState } from '../../../state'; -import { getSnapshotCountAction } from '../../../state/actions'; -import { SnapshotComponent } from '../../functional/snapshot'; -import { Snapshot as SnapshotType } from '../../../../common/runtime_types'; -import { SnapShotQueryParams } from '../../../state/api'; - -/** - * Props expected from parent components. - */ -interface OwnProps { - /** - * Height is needed, since by default charts takes height of 100% - */ - height?: string; -} - -/** - * Props given by the Redux store based on action input. - */ -interface StoreProps { - count: SnapshotType; - lastRefresh: number; - loading: boolean; - esKuery: string; -} - -/** - * Contains functions that will dispatch actions used - * for this component's life cycle - */ -interface DispatchProps { - loadSnapshotCount: typeof getSnapshotCountAction; -} - -/** - * Props used to render the Snapshot component. - */ -type Props = OwnProps & StoreProps & DispatchProps; - -export const Container: React.FC<Props> = ({ - count, - height, - lastRefresh, - loading, - esKuery, - loadSnapshotCount, -}: Props) => { - const [getUrlParams] = useUrlParams(); - const { dateRangeStart, dateRangeEnd, statusFilter } = getUrlParams(); - - useEffect(() => { - loadSnapshotCount({ dateRangeStart, dateRangeEnd, filters: esKuery, statusFilter }); - }, [dateRangeStart, dateRangeEnd, esKuery, lastRefresh, loadSnapshotCount, statusFilter]); - return <SnapshotComponent count={count} height={height} loading={loading} />; -}; - -/** - * Provides state to connected component. - * @param state the root app state - */ -const mapStateToProps = ({ - snapshot: { count, loading }, - ui: { lastRefresh, esKuery }, -}: AppState): StoreProps => ({ - count, - lastRefresh, - loading, - esKuery, -}); - -/** - * Used for fetching snapshot counts. - * @param dispatch redux-provided action dispatcher - */ -const mapDispatchToProps = (dispatch: any) => ({ - loadSnapshotCount: (params: SnapShotQueryParams): DispatchProps => { - return dispatch(getSnapshotCountAction(params)); - }, -}); - -export const Snapshot = connect<StoreProps, DispatchProps, OwnProps>( - // @ts-ignore connect is expecting null | undefined for some reason - mapStateToProps, - mapDispatchToProps -)(Container); diff --git a/x-pack/legacy/plugins/uptime/public/components/connected/empty_state/empty_state.tsx b/x-pack/legacy/plugins/uptime/public/components/connected/empty_state/empty_state.tsx deleted file mode 100644 index b383a696095a3..0000000000000 --- a/x-pack/legacy/plugins/uptime/public/components/connected/empty_state/empty_state.tsx +++ /dev/null @@ -1,30 +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, { useEffect } from 'react'; -import { useDispatch, useSelector } from 'react-redux'; -import { indexStatusAction } from '../../../state/actions'; -import { indexStatusSelector } from '../../../state/selectors'; -import { EmptyStateComponent } from '../../functional/empty_state/empty_state'; - -export const EmptyState: React.FC = ({ children }) => { - const { data, loading, error } = useSelector(indexStatusSelector); - - const dispatch = useDispatch(); - - useEffect(() => { - dispatch(indexStatusAction.get()); - }, [dispatch]); - - return ( - <EmptyStateComponent - statesIndexStatus={data} - loading={loading} - errors={error ? [error] : undefined} - children={children as React.ReactElement} - /> - ); -}; diff --git a/x-pack/legacy/plugins/uptime/public/components/connected/index.ts b/x-pack/legacy/plugins/uptime/public/components/connected/index.ts deleted file mode 100644 index 7e442cbe850ba..0000000000000 --- a/x-pack/legacy/plugins/uptime/public/components/connected/index.ts +++ /dev/null @@ -1,17 +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. - */ - -export { AlertMonitorStatus, ToggleAlertFlyoutButton, UptimeAlertsFlyoutWrapper } from './alerts'; -export { PingHistogram } from './charts/ping_histogram'; -export { Snapshot } from './charts/snapshot_container'; -export { KueryBar } from './kuerybar/kuery_bar_container'; -export { FilterGroup } from './filter_group/filter_group_container'; -export { MonitorStatusDetails } from './monitor/status_details_container'; -export { MonitorStatusBar } from './monitor/status_bar_container'; -export { MonitorListDrawer } from './monitor/list_drawer_container'; -export { MonitorListActionsPopover } from './monitor/drawer_popover_container'; -export { DurationChart } from './charts/monitor_duration'; -export { EmptyState } from './empty_state/empty_state'; diff --git a/x-pack/legacy/plugins/uptime/public/components/connected/monitor/drawer_popover_container.tsx b/x-pack/legacy/plugins/uptime/public/components/connected/monitor/drawer_popover_container.tsx deleted file mode 100644 index be29e12f716a9..0000000000000 --- a/x-pack/legacy/plugins/uptime/public/components/connected/monitor/drawer_popover_container.tsx +++ /dev/null @@ -1,26 +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 { connect } from 'react-redux'; -import { AppState } from '../../../state'; -import { isIntegrationsPopupOpen } from '../../../state/selectors'; -import { PopoverState, toggleIntegrationsPopover } from '../../../state/actions'; -import { MonitorListActionsPopoverComponent } from '../../functional/monitor_list/monitor_list_drawer'; - -const mapStateToProps = (state: AppState) => ({ - popoverState: isIntegrationsPopupOpen(state), -}); - -const mapDispatchToProps = (dispatch: any) => ({ - togglePopoverIsVisible: (popoverState: PopoverState) => { - return dispatch(toggleIntegrationsPopover(popoverState)); - }, -}); - -export const MonitorListActionsPopover = connect( - mapStateToProps, - mapDispatchToProps -)(MonitorListActionsPopoverComponent); diff --git a/x-pack/legacy/plugins/uptime/public/components/connected/monitor/list_drawer_container.tsx b/x-pack/legacy/plugins/uptime/public/components/connected/monitor/list_drawer_container.tsx deleted file mode 100644 index ceeaa7026059f..0000000000000 --- a/x-pack/legacy/plugins/uptime/public/components/connected/monitor/list_drawer_container.tsx +++ /dev/null @@ -1,49 +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, { useEffect } from 'react'; -import { connect } from 'react-redux'; -import { AppState } from '../../../state'; -import { monitorDetailsSelector } from '../../../state/selectors'; -import { MonitorDetailsActionPayload } from '../../../state/actions/types'; -import { getMonitorDetailsAction } from '../../../state/actions/monitor'; -import { MonitorListDrawerComponent } from '../../functional/monitor_list/monitor_list_drawer/monitor_list_drawer'; -import { useUrlParams } from '../../../hooks'; -import { MonitorSummary } from '../../../../common/graphql/types'; -import { MonitorDetails } from '../../../../common/runtime_types/monitor'; - -interface ContainerProps { - summary: MonitorSummary; - monitorDetails: MonitorDetails; - loadMonitorDetails: typeof getMonitorDetailsAction; -} - -const Container: React.FC<ContainerProps> = ({ summary, loadMonitorDetails, monitorDetails }) => { - const monitorId = summary?.monitor_id; - - const [getUrlParams] = useUrlParams(); - const { dateRangeStart: dateStart, dateRangeEnd: dateEnd } = getUrlParams(); - - useEffect(() => { - loadMonitorDetails({ - dateStart, - dateEnd, - monitorId, - }); - }, [dateStart, dateEnd, monitorId, loadMonitorDetails]); - return <MonitorListDrawerComponent monitorDetails={monitorDetails} summary={summary} />; -}; - -const mapStateToProps = (state: AppState, { summary }: any) => ({ - monitorDetails: monitorDetailsSelector(state, summary), -}); - -const mapDispatchToProps = (dispatch: any) => ({ - loadMonitorDetails: (actionPayload: MonitorDetailsActionPayload) => - dispatch(getMonitorDetailsAction(actionPayload)), -}); - -export const MonitorListDrawer = connect(mapStateToProps, mapDispatchToProps)(Container); diff --git a/x-pack/legacy/plugins/uptime/public/components/connected/monitor/status_bar_container.tsx b/x-pack/legacy/plugins/uptime/public/components/connected/monitor/status_bar_container.tsx deleted file mode 100644 index dd6f7a89cf9a3..0000000000000 --- a/x-pack/legacy/plugins/uptime/public/components/connected/monitor/status_bar_container.tsx +++ /dev/null @@ -1,72 +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, { useContext, useEffect } from 'react'; -import { connect } from 'react-redux'; -import { Dispatch } from 'redux'; -import { AppState } from '../../../state'; -import { monitorLocationsSelector, monitorStatusSelector } from '../../../state/selectors'; -import { MonitorStatusBarComponent } from '../../functional/monitor_status_details/monitor_status_bar'; -import { getMonitorStatusAction } from '../../../state/actions'; -import { useUrlParams } from '../../../hooks'; -import { Ping } from '../../../../common/graphql/types'; -import { MonitorLocations } from '../../../../common/runtime_types/monitor'; -import { UptimeRefreshContext } from '../../../contexts'; - -interface StateProps { - monitorStatus: Ping; - monitorLocations: MonitorLocations; -} - -interface DispatchProps { - loadMonitorStatus: typeof getMonitorStatusAction; -} - -interface OwnProps { - monitorId: string; -} - -type Props = OwnProps & StateProps & DispatchProps; - -const Container: React.FC<Props> = ({ - loadMonitorStatus, - monitorId, - monitorStatus, - monitorLocations, -}: Props) => { - const { lastRefresh } = useContext(UptimeRefreshContext); - - const [getUrlParams] = useUrlParams(); - const { dateRangeStart: dateStart, dateRangeEnd: dateEnd } = getUrlParams(); - - useEffect(() => { - loadMonitorStatus({ dateStart, dateEnd, monitorId }); - }, [monitorId, dateStart, dateEnd, loadMonitorStatus, lastRefresh]); - - return ( - <MonitorStatusBarComponent - monitorId={monitorId} - monitorStatus={monitorStatus} - monitorLocations={monitorLocations} - /> - ); -}; - -const mapStateToProps = (state: AppState, ownProps: OwnProps) => ({ - monitorStatus: monitorStatusSelector(state), - monitorLocations: monitorLocationsSelector(state, ownProps.monitorId), -}); - -const mapDispatchToProps = (dispatch: Dispatch<any>): DispatchProps => ({ - loadMonitorStatus: params => dispatch(getMonitorStatusAction(params)), -}); - -// @ts-ignore TODO: Investigate typescript issues here -export const MonitorStatusBar = connect<StateProps, DispatchProps, MonitorStatusBarProps>( - // @ts-ignore TODO: Investigate typescript issues here - mapStateToProps, - mapDispatchToProps -)(Container); diff --git a/x-pack/legacy/plugins/uptime/public/components/connected/monitor/status_details_container.tsx b/x-pack/legacy/plugins/uptime/public/components/connected/monitor/status_details_container.tsx deleted file mode 100644 index 3ced251dfab8c..0000000000000 --- a/x-pack/legacy/plugins/uptime/public/components/connected/monitor/status_details_container.tsx +++ /dev/null @@ -1,64 +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, { useContext, useEffect } from 'react'; -import { connect } from 'react-redux'; -import { Dispatch } from 'redux'; -import { useUrlParams } from '../../../hooks'; -import { AppState } from '../../../state'; -import { monitorLocationsSelector } from '../../../state/selectors'; -import { getMonitorLocationsAction, MonitorLocationsPayload } from '../../../state/actions/monitor'; -import { MonitorStatusDetailsComponent } from '../../functional/monitor_status_details'; -import { MonitorLocations } from '../../../../common/runtime_types'; -import { UptimeRefreshContext } from '../../../contexts'; - -interface OwnProps { - monitorId: string; -} - -interface StoreProps { - monitorLocations: MonitorLocations; -} - -interface DispatchProps { - loadMonitorLocations: typeof getMonitorLocationsAction; -} - -type Props = OwnProps & StoreProps & DispatchProps; - -export const Container: React.FC<Props> = ({ - loadMonitorLocations, - monitorLocations, - monitorId, -}: Props) => { - const { lastRefresh } = useContext(UptimeRefreshContext); - - const [getUrlParams] = useUrlParams(); - const { dateRangeStart: dateStart, dateRangeEnd: dateEnd } = getUrlParams(); - - useEffect(() => { - loadMonitorLocations({ dateStart, dateEnd, monitorId }); - }, [loadMonitorLocations, monitorId, dateStart, dateEnd, lastRefresh]); - - return ( - <MonitorStatusDetailsComponent monitorId={monitorId} monitorLocations={monitorLocations} /> - ); -}; -const mapStateToProps = (state: AppState, { monitorId }: OwnProps) => ({ - monitorLocations: monitorLocationsSelector(state, monitorId), -}); - -const mapDispatchToProps = (dispatch: Dispatch<any>) => ({ - loadMonitorLocations: (params: MonitorLocationsPayload) => { - dispatch(getMonitorLocationsAction(params)); - }, -}); - -export const MonitorStatusDetails = connect<StoreProps, DispatchProps, OwnProps>( - // @ts-ignore TODO: Investigate typescript issues here - mapStateToProps, - mapDispatchToProps -)(Container); diff --git a/x-pack/legacy/plugins/uptime/public/components/connected/pages/overview_container.tsx b/x-pack/legacy/plugins/uptime/public/components/connected/pages/overview_container.tsx deleted file mode 100644 index 79aaa071507e1..0000000000000 --- a/x-pack/legacy/plugins/uptime/public/components/connected/pages/overview_container.tsx +++ /dev/null @@ -1,23 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { connect } from 'react-redux'; -import { OverviewPageComponent } from '../../../pages/overview'; -import { selectIndexPattern } from '../../../state/selectors'; -import { AppState } from '../../../state'; -import { setEsKueryString } from '../../../state/actions'; - -interface DispatchProps { - setEsKueryFilters: typeof setEsKueryString; -} - -const mapDispatchToProps = (dispatch: any): DispatchProps => ({ - setEsKueryFilters: (esFilters: string) => dispatch(setEsKueryString(esFilters)), -}); - -const mapStateToProps = (state: AppState) => ({ ...selectIndexPattern(state) }); - -export const OverviewPage = connect(mapStateToProps, mapDispatchToProps)(OverviewPageComponent); diff --git a/x-pack/legacy/plugins/uptime/public/components/functional/__tests__/overview_page_parsing_error_callout.test.tsx b/x-pack/legacy/plugins/uptime/public/components/functional/__tests__/overview_page_parsing_error_callout.test.tsx deleted file mode 100644 index fbe55dfedc2fc..0000000000000 --- a/x-pack/legacy/plugins/uptime/public/components/functional/__tests__/overview_page_parsing_error_callout.test.tsx +++ /dev/null @@ -1,26 +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 from 'react'; -import { shallowWithIntl } from 'test_utils/enzyme_helpers'; -import { OverviewPageParsingErrorCallout } from '../overview_page_parsing_error_callout'; - -describe('OverviewPageParsingErrorCallout', () => { - it('renders without errors when a valid error is provided', () => { - expect( - shallowWithIntl( - <OverviewPageParsingErrorCallout - error={{ message: 'Unable to convert to Elasticsearch query, invalid syntax.' }} - /> - ) - ).toMatchSnapshot(); - }); - - it('renders without errors when an error with no message is provided', () => { - const error: any = {}; - expect(shallowWithIntl(<OverviewPageParsingErrorCallout error={error} />)).toMatchSnapshot(); - }); -}); diff --git a/x-pack/legacy/plugins/uptime/public/components/functional/alerts/alert_monitor_status.tsx b/x-pack/legacy/plugins/uptime/public/components/functional/alerts/alert_monitor_status.tsx deleted file mode 100644 index b86e85f35b17d..0000000000000 --- a/x-pack/legacy/plugins/uptime/public/components/functional/alerts/alert_monitor_status.tsx +++ /dev/null @@ -1,445 +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, { useState, useEffect } from 'react'; -import { - EuiExpression, - EuiFieldNumber, - EuiFlexGroup, - EuiFlexItem, - EuiPopover, - EuiSelectable, - EuiSpacer, - EuiSwitch, - EuiTitle, -} from '@elastic/eui'; -import { i18n } from '@kbn/i18n'; -import { FormattedMessage } from '@kbn/i18n/react'; -import { DataPublicPluginSetup } from 'src/plugins/data/public'; -import { KueryBar } from '../../connected/kuerybar/kuery_bar_container'; - -interface AlertFieldNumberProps { - 'aria-label': string; - 'data-test-subj': string; - disabled: boolean; - fieldValue: number; - setFieldValue: React.Dispatch<React.SetStateAction<number>>; -} - -export const handleAlertFieldNumberChange = ( - e: React.ChangeEvent<HTMLInputElement>, - isInvalid: boolean, - setIsInvalid: React.Dispatch<React.SetStateAction<boolean>>, - setFieldValue: React.Dispatch<React.SetStateAction<number>> -) => { - const num = parseInt(e.target.value, 10); - if (isNaN(num) || num < 1) { - setIsInvalid(true); - } else { - if (isInvalid) setIsInvalid(false); - setFieldValue(num); - } -}; - -export const AlertFieldNumber = ({ - 'aria-label': ariaLabel, - 'data-test-subj': dataTestSubj, - disabled, - fieldValue, - setFieldValue, -}: AlertFieldNumberProps) => { - const [isInvalid, setIsInvalid] = useState<boolean>(false); - - return ( - <EuiFieldNumber - aria-label={ariaLabel} - compressed - data-test-subj={dataTestSubj} - min={1} - onChange={e => handleAlertFieldNumberChange(e, isInvalid, setIsInvalid, setFieldValue)} - disabled={disabled} - value={fieldValue} - isInvalid={isInvalid} - /> - ); -}; - -interface AlertExpressionPopoverProps { - 'aria-label': string; - content: React.ReactElement; - description: string; - 'data-test-subj': string; - id: string; - value: string; -} - -const AlertExpressionPopover: React.FC<AlertExpressionPopoverProps> = ({ - 'aria-label': ariaLabel, - content, - 'data-test-subj': dataTestSubj, - description, - id, - value, -}) => { - const [isOpen, setIsOpen] = useState<boolean>(false); - return ( - <EuiPopover - id={id} - anchorPosition="downLeft" - button={ - <EuiExpression - aria-label={ariaLabel} - color={isOpen ? 'primary' : 'secondary'} - data-test-subj={dataTestSubj} - description={description} - isActive={isOpen} - onClick={() => setIsOpen(!isOpen)} - value={value} - /> - } - isOpen={isOpen} - closePopover={() => setIsOpen(false)} - > - {content} - </EuiPopover> - ); -}; - -export const selectedLocationsToString = (selectedLocations: any[]) => - // create a nicely-formatted description string for all `on` locations - selectedLocations - .filter(({ checked }) => checked === 'on') - .map(({ label }) => label) - .sort() - .reduce((acc, cur) => { - if (acc === '') { - return cur; - } - return acc + `, ${cur}`; - }, ''); - -interface AlertMonitorStatusProps { - autocomplete: DataPublicPluginSetup['autocomplete']; - enabled: boolean; - filters: string; - locations: string[]; - numTimes: number; - setAlertParams: (key: string, value: any) => void; - timerange: { - from: string; - to: string; - }; -} - -export const AlertMonitorStatusComponent: React.FC<AlertMonitorStatusProps> = props => { - const { filters, locations } = props; - const [numTimes, setNumTimes] = useState<number>(5); - const [numMins, setNumMins] = useState<number>(15); - const [allLabels, setAllLabels] = useState<boolean>(true); - - // locations is an array of `Option[]`, but that type doesn't seem to be exported by EUI - const [selectedLocations, setSelectedLocations] = useState<any[]>( - locations.map(location => ({ - 'aria-label': i18n.translate('xpack.uptime.alerts.locationSelectionItem.ariaLabel', { - defaultMessage: 'Location selection item for "{location}"', - values: { - location, - }, - }), - disabled: allLabels, - label: location, - })) - ); - const [timerangeUnitOptions, setTimerangeUnitOptions] = useState<any[]>([ - { - 'aria-label': i18n.translate( - 'xpack.uptime.alerts.timerangeUnitSelectable.secondsOption.ariaLabel', - { - defaultMessage: '"Seconds" time range select item', - } - ), - 'data-test-subj': 'xpack.uptime.alerts.monitorStatus.timerangeUnitSelectable.secondsOption', - key: 's', - label: i18n.translate('xpack.uptime.alerts.monitorStatus.timerangeOption.seconds', { - defaultMessage: 'seconds', - }), - }, - { - 'aria-label': i18n.translate( - 'xpack.uptime.alerts.timerangeUnitSelectable.minutesOption.ariaLabel', - { - defaultMessage: '"Minutes" time range select item', - } - ), - 'data-test-subj': 'xpack.uptime.alerts.monitorStatus.timerangeUnitSelectable.minutesOption', - checked: 'on', - key: 'm', - label: i18n.translate('xpack.uptime.alerts.monitorStatus.timerangeOption.minutes', { - defaultMessage: 'minutes', - }), - }, - { - 'aria-label': i18n.translate( - 'xpack.uptime.alerts.timerangeUnitSelectable.hoursOption.ariaLabel', - { - defaultMessage: '"Hours" time range select item', - } - ), - 'data-test-subj': 'xpack.uptime.alerts.monitorStatus.timerangeUnitSelectable.hoursOption', - key: 'h', - label: i18n.translate('xpack.uptime.alerts.monitorStatus.timerangeOption.hours', { - defaultMessage: 'hours', - }), - }, - { - 'aria-label': i18n.translate( - 'xpack.uptime.alerts.timerangeUnitSelectable.daysOption.ariaLabel', - { - defaultMessage: '"Days" time range select item', - } - ), - 'data-test-subj': 'xpack.uptime.alerts.monitorStatus.timerangeUnitSelectable.daysOption', - key: 'd', - label: i18n.translate('xpack.uptime.alerts.monitorStatus.timerangeOption.days', { - defaultMessage: 'days', - }), - }, - ]); - - const { setAlertParams } = props; - - useEffect(() => { - setAlertParams('numTimes', numTimes); - }, [numTimes, setAlertParams]); - - useEffect(() => { - const timerangeUnit = timerangeUnitOptions.find(({ checked }) => checked === 'on')?.key ?? 'm'; - setAlertParams('timerange', { from: `now-${numMins}${timerangeUnit}`, to: 'now' }); - }, [numMins, timerangeUnitOptions, setAlertParams]); - - useEffect(() => { - if (allLabels) { - setAlertParams('locations', []); - } else { - setAlertParams( - 'locations', - selectedLocations.filter(l => l.checked === 'on').map(l => l.label) - ); - } - }, [selectedLocations, setAlertParams, allLabels]); - - useEffect(() => { - setAlertParams('filters', filters); - }, [filters, setAlertParams]); - - return ( - <> - <EuiSpacer size="m" /> - <KueryBar - aria-label={i18n.translate('xpack.uptime.alerts.monitorStatus.filterBar.ariaLabel', { - defaultMessage: 'Input that allows filtering criteria for the monitor status alert', - })} - autocomplete={props.autocomplete} - data-test-subj="xpack.uptime.alerts.monitorStatus.filterBar" - /> - <EuiSpacer size="s" /> - <AlertExpressionPopover - aria-label={i18n.translate( - 'xpack.uptime.alerts.monitorStatus.numTimesExpression.ariaLabel', - { - defaultMessage: 'Open the popover for down count input', - } - )} - content={ - <AlertFieldNumber - aria-label={i18n.translate( - 'xpack.uptime.alerts.monitorStatus.numTimesField.ariaLabel', - { - defaultMessage: 'Enter number of down counts required to trigger the alert', - } - )} - data-test-subj="xpack.uptime.alerts.monitorStatus.numTimesField" - disabled={false} - fieldValue={numTimes} - setFieldValue={setNumTimes} - /> - } - data-test-subj="xpack.uptime.alerts.monitorStatus.numTimesExpression" - description={ - filters - ? i18n.translate( - 'xpack.uptime.alerts.monitorStatus.numTimesExpression.matchingMonitors.description', - { - defaultMessage: 'matching monitors are down >', - } - ) - : i18n.translate( - 'xpack.uptime.alerts.monitorStatus.numTimesExpression.anyMonitors.description', - { - defaultMessage: 'any monitor is down >', - } - ) - } - id="ping-count" - value={`${numTimes} times`} - /> - <EuiSpacer size="xs" /> - <EuiFlexGroup gutterSize="s"> - <EuiFlexItem grow={false}> - <AlertExpressionPopover - aria-label={i18n.translate( - 'xpack.uptime.alerts.monitorStatus.timerangeValueExpression.ariaLabel', - { - defaultMessage: 'Open the popover for time range value field', - } - )} - content={ - <AlertFieldNumber - aria-label={i18n.translate( - 'xpack.uptime.alerts.monitorStatus.timerangeValueField.ariaLabel', - { - defaultMessage: `Enter the number of time units for the alert's range`, - } - )} - data-test-subj="xpack.uptime.alerts.monitorStatus.timerangeValueField" - disabled={false} - fieldValue={numMins} - setFieldValue={setNumMins} - /> - } - data-test-subj="xpack.uptime.alerts.monitorStatus.timerangeValueExpression" - description="within" - id="timerange" - value={`last ${numMins}`} - /> - </EuiFlexItem> - <EuiFlexItem> - <AlertExpressionPopover - aria-label={i18n.translate( - 'xpack.uptime.alerts.monitorStatus.timerangeUnitExpression.ariaLabel', - { - defaultMessage: 'Open the popover for time range unit select field', - } - )} - content={ - <> - <EuiTitle size="xxs"> - <h5> - <FormattedMessage - id="xpack.uptime.alerts.monitorStatus.timerangeSelectionHeader" - defaultMessage="Select time range unit" - /> - </h5> - </EuiTitle> - <EuiSelectable - aria-label={i18n.translate( - 'xpack.uptime.alerts.monitorStatus.timerangeUnitSelectable', - { - defaultMessage: 'Selectable field for the time range units alerts should use', - } - )} - data-test-subj="xpack.uptime.alerts.monitorStatus.timerangeUnitSelectable" - options={timerangeUnitOptions} - onChange={newOptions => { - if (newOptions.reduce((acc, { checked }) => acc || checked === 'on', false)) { - setTimerangeUnitOptions(newOptions); - } - }} - singleSelection={true} - listProps={{ - showIcons: true, - }} - > - {list => list} - </EuiSelectable> - </> - } - data-test-subj="xpack.uptime.alerts.monitorStatus.timerangeUnitExpression" - description="" - id="timerange-unit" - value={ - timerangeUnitOptions.find(({ checked }) => checked === 'on')?.label.toLowerCase() ?? - '' - } - /> - </EuiFlexItem> - </EuiFlexGroup> - <EuiSpacer size="xs" /> - {selectedLocations.length === 0 && ( - <EuiExpression - color="secondary" - data-test-subj="xpack.uptime.alerts.monitorStatus.locationsEmpty" - description="in" - isActive={false} - value="all locations" - /> - )} - {selectedLocations.length > 0 && ( - <AlertExpressionPopover - aria-label={i18n.translate( - 'xpack.uptime.alerts.monitorStatus.locationsSelectionExpression.ariaLabel', - { - defaultMessage: 'Open the popover to select locations the alert should trigger', - } - )} - content={ - <EuiFlexGroup direction="column"> - <EuiFlexItem> - <EuiSwitch - aria-label={i18n.translate( - 'xpack.uptime.alerts.monitorStatus.locationSelectionSwitch.ariaLabel', - { - defaultMessage: 'Select the locations the alert should trigger', - } - )} - data-test-subj="xpack.uptime.alerts.monitorStatus.locationsSelectionSwitch" - label="Check all locations" - checked={allLabels} - onChange={() => { - setAllLabels(!allLabels); - setSelectedLocations( - selectedLocations.map((l: any) => ({ - 'aria-label': i18n.translate( - 'xpack.uptime.alerts.monitorStatus.locationSelection', - { - defaultMessage: 'Select the location {location}', - values: { - location: l, - }, - } - ), - ...l, - 'data-test-subj': `xpack.uptime.alerts.monitorStatus.locationSelection.${l.label}LocationOption`, - disabled: !allLabels, - })) - ); - }} - /> - </EuiFlexItem> - <EuiFlexItem> - <EuiSelectable - data-test-subj="xpack.uptime.alerts.monitorStatus.locationsSelectionSelectable" - options={selectedLocations} - onChange={e => setSelectedLocations(e)} - > - {location => location} - </EuiSelectable> - </EuiFlexItem> - </EuiFlexGroup> - } - data-test-subj="xpack.uptime.alerts.monitorStatus.locationsSelectionExpression" - description="from" - id="locations" - value={ - selectedLocations.length === 0 || allLabels - ? 'any location' - : selectedLocationsToString(selectedLocations) - } - /> - )} - </> - ); -}; diff --git a/x-pack/legacy/plugins/uptime/public/components/functional/alerts/index.ts b/x-pack/legacy/plugins/uptime/public/components/functional/alerts/index.ts deleted file mode 100644 index 275333b60c5ee..0000000000000 --- a/x-pack/legacy/plugins/uptime/public/components/functional/alerts/index.ts +++ /dev/null @@ -1,10 +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. - */ - -export { AlertMonitorStatusComponent } from './alert_monitor_status'; -export { ToggleAlertFlyoutButtonComponent } from './toggle_alert_flyout_button'; -export { UptimeAlertsContextProvider } from './uptime_alerts_context_provider'; -export { UptimeAlertsFlyoutWrapperComponent } from './uptime_alerts_flyout_wrapper'; diff --git a/x-pack/legacy/plugins/uptime/public/components/functional/charts/__tests__/duration_charts.test.tsx b/x-pack/legacy/plugins/uptime/public/components/functional/charts/__tests__/duration_charts.test.tsx deleted file mode 100644 index aac84c69d8d7b..0000000000000 --- a/x-pack/legacy/plugins/uptime/public/components/functional/charts/__tests__/duration_charts.test.tsx +++ /dev/null @@ -1,75 +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 from 'react'; -import DateMath from '@elastic/datemath'; -import { DurationChartComponent } from '../duration_chart'; -import { MonitorDurationResult } from '../../../../../common/types'; -import { shallowWithRouter } from '../../../../lib'; - -describe('MonitorCharts component', () => { - let dateMathSpy: any; - const MOCK_DATE_VALUE = 20; - - beforeEach(() => { - dateMathSpy = jest.spyOn(DateMath, 'parse'); - dateMathSpy.mockReturnValue(MOCK_DATE_VALUE); - }); - - afterEach(() => { - jest.clearAllMocks(); - }); - - const chartResponse: { monitorChartsData: MonitorDurationResult } = { - monitorChartsData: { - locationDurationLines: [ - { - name: 'somewhere', - line: [ - { x: 1548697620000, y: 743928.2027027027 }, - { x: 1548697920000, y: 766840.0133333333 }, - { x: 1548698220000, y: 786970.8266666667 }, - { x: 1548698520000, y: 781064.7808219178 }, - { x: 1548698820000, y: 741563.04 }, - { x: 1548699120000, y: 759354.6756756756 }, - { x: 1548699420000, y: 737533.3866666667 }, - { x: 1548699720000, y: 728669.0266666666 }, - { x: 1548700020000, y: 719951.64 }, - { x: 1548700320000, y: 769181.7866666666 }, - { x: 1548700620000, y: 740805.2666666667 }, - ], - }, - ], - status: [ - { x: 1548697620000, up: 74, down: null, total: 74 }, - { x: 1548697920000, up: 75, down: null, total: 75 }, - { x: 1548698220000, up: 75, down: null, total: 75 }, - { x: 1548698520000, up: 73, down: null, total: 73 }, - { x: 1548698820000, up: 75, down: null, total: 75 }, - { x: 1548699120000, up: 74, down: null, total: 74 }, - { x: 1548699420000, up: 75, down: null, total: 75 }, - { x: 1548699720000, up: 75, down: null, total: 75 }, - { x: 1548700020000, up: 75, down: null, total: 75 }, - { x: 1548700320000, up: 75, down: null, total: 75 }, - { x: 1548700620000, up: 75, down: null, total: 75 }, - ], - statusMaxCount: 75, - durationMaxValue: 6669234, - }, - }; - - it('renders the component without errors', () => { - const component = shallowWithRouter( - <DurationChartComponent - loading={false} - hasMLJob={false} - anomalies={null} - locationDurationLines={chartResponse.monitorChartsData.locationDurationLines} - /> - ); - expect(component).toMatchSnapshot(); - }); -}); diff --git a/x-pack/legacy/plugins/uptime/public/components/functional/charts/duration_chart.tsx b/x-pack/legacy/plugins/uptime/public/components/functional/charts/duration_chart.tsx deleted file mode 100644 index d149e7a6deb5a..0000000000000 --- a/x-pack/legacy/plugins/uptime/public/components/functional/charts/duration_chart.tsx +++ /dev/null @@ -1,156 +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, { useState } from 'react'; -import { i18n } from '@kbn/i18n'; -import moment from 'moment'; -import { FormattedMessage } from '@kbn/i18n/react'; -import { EuiFlexGroup, EuiFlexItem, EuiPanel, EuiTitle } from '@elastic/eui'; -import { Axis, Chart, Position, timeFormatter, Settings } from '@elastic/charts'; -import { SeriesIdentifier } from '@elastic/charts/dist/chart_types/xy_chart/utils/series'; -import { getChartDateLabel } from '../../../lib/helper'; -import { LocationDurationLine } from '../../../../common/types'; -import { DurationLineSeriesList } from './duration_line_series_list'; -import { ChartWrapper } from './chart_wrapper'; -import { useUrlParams } from '../../../hooks'; -import { getTickFormat } from './get_tick_format'; -import { ChartEmptyState } from './chart_empty_state'; -import { DurationAnomaliesBar } from './duration_line_bar_list'; -import { MLIntegrationComponent } from '../../monitor_details/ml/ml_integeration'; -import { AnomalyRecords } from '../../../state/actions'; - -interface DurationChartProps { - /** - * Timeseries data that is used to express an average line series - * on the duration chart. One entry per location - */ - locationDurationLines: LocationDurationLine[]; - - /** - * To represent the loading spinner on chart - */ - loading: boolean; - - hasMLJob: boolean; - - anomalies: AnomalyRecords | null; -} - -/** - * This chart is intended to visualize monitor duration performance over time to - * the users in a helpful way. Its x-axis is based on a timeseries, the y-axis is in - * milliseconds. - * @param props The props required for this component to render properly - */ -export const DurationChartComponent = ({ - locationDurationLines, - anomalies, - loading, - hasMLJob, -}: DurationChartProps) => { - const hasLines = locationDurationLines.length > 0; - const [getUrlParams, updateUrlParams] = useUrlParams(); - const { absoluteDateRangeStart: min, absoluteDateRangeEnd: max } = getUrlParams(); - - const [hiddenLegends, setHiddenLegends] = useState<string[]>([]); - - const onBrushEnd = (minX: number, maxX: number) => { - updateUrlParams({ - dateRangeStart: moment(minX).toISOString(), - dateRangeEnd: moment(maxX).toISOString(), - }); - }; - - const legendToggleVisibility = (legendItem: SeriesIdentifier | null) => { - if (legendItem) { - setHiddenLegends(prevState => { - if (prevState.includes(legendItem.specId)) { - return [...prevState.filter(item => item !== legendItem.specId)]; - } else { - return [...prevState, legendItem.specId]; - } - }); - } - }; - - return ( - <> - <EuiPanel paddingSize="m"> - <EuiFlexGroup> - <EuiFlexItem> - <EuiTitle size="xs"> - <h4> - {hasMLJob ? ( - <FormattedMessage - id="xpack.uptime.monitorCharts.monitorDuration.titleLabelWithAnomaly" - defaultMessage="Monitor duration (Anomalies: {noOfAnomalies})" - values={{ noOfAnomalies: anomalies?.anomalies?.length ?? 0 }} - /> - ) : ( - <FormattedMessage - id="xpack.uptime.monitorCharts.monitorDuration.titleLabel" - defaultMessage="Monitor duration" - /> - )} - </h4> - </EuiTitle> - </EuiFlexItem> - <EuiFlexItem grow={false}> - <MLIntegrationComponent /> - </EuiFlexItem> - </EuiFlexGroup> - - <ChartWrapper height="400px" loading={loading}> - {hasLines ? ( - <Chart> - <Settings - xDomain={{ min, max }} - showLegend - showLegendExtra - legendPosition={Position.Bottom} - onBrushEnd={onBrushEnd} - onLegendItemClick={legendToggleVisibility} - /> - <Axis - id="bottom" - position={Position.Bottom} - showOverlappingTicks={true} - tickFormat={timeFormatter(getChartDateLabel(min, max))} - title={i18n.translate('xpack.uptime.monitorCharts.durationChart.bottomAxis.title', { - defaultMessage: 'Timestamp', - })} - /> - <Axis - domain={{ min: 0 }} - id="left" - position={Position.Left} - tickFormat={d => getTickFormat(d)} - title={i18n.translate('xpack.uptime.monitorCharts.durationChart.leftAxis.title', { - defaultMessage: 'Duration ms', - })} - /> - <DurationLineSeriesList lines={locationDurationLines} /> - <DurationAnomaliesBar anomalies={anomalies} hiddenLegends={hiddenLegends} /> - </Chart> - ) : ( - <ChartEmptyState - body={ - <FormattedMessage - id="xpack.uptime.durationChart.emptyPrompt.description" - defaultMessage="This monitor has never been {emphasizedText} during the selected time range." - values={{ emphasizedText: <strong>up</strong> }} - /> - } - title={i18n.translate('xpack.uptime.durationChart.emptyPrompt.title', { - defaultMessage: 'No duration data available', - })} - /> - )} - </ChartWrapper> - </EuiPanel> - </> - ); -}; diff --git a/x-pack/legacy/plugins/uptime/public/components/functional/charts/ping_histogram.tsx b/x-pack/legacy/plugins/uptime/public/components/functional/charts/ping_histogram.tsx deleted file mode 100644 index f988afc7fc84d..0000000000000 --- a/x-pack/legacy/plugins/uptime/public/components/functional/charts/ping_histogram.tsx +++ /dev/null @@ -1,173 +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 { Axis, BarSeries, Chart, Position, Settings, timeFormatter } from '@elastic/charts'; -import { EuiTitle } from '@elastic/eui'; -import { i18n } from '@kbn/i18n'; -import React, { useContext } from 'react'; -import { FormattedMessage } from '@kbn/i18n/react'; -import moment from 'moment'; -import { getChartDateLabel } from '../../../lib/helper'; -import { ChartWrapper } from './chart_wrapper'; -import { UptimeThemeContext } from '../../../contexts'; -import { HistogramResult } from '../../../../common/types'; -import { useUrlParams } from '../../../hooks'; -import { ChartEmptyState } from './chart_empty_state'; - -export interface PingHistogramComponentProps { - /** - * The date/time for the start of the timespan. - */ - absoluteStartDate: number; - /** - * The date/time for the end of the timespan. - */ - absoluteEndDate: number; - - /** - * Height is needed, since by default charts takes height of 100% - */ - height?: string; - - data: HistogramResult | null; - - loading?: boolean; -} - -interface BarPoint { - x?: number; - y?: number; - type: string; -} - -export const PingHistogramComponent: React.FC<PingHistogramComponentProps> = ({ - absoluteStartDate, - absoluteEndDate, - data, - loading = false, - height, -}) => { - const { - colors: { danger, gray }, - } = useContext(UptimeThemeContext); - - const [, updateUrlParams] = useUrlParams(); - - let content: JSX.Element | undefined; - if (!data?.histogram?.length) { - content = ( - <ChartEmptyState - title={i18n.translate('xpack.uptime.snapshot.noDataTitle', { - defaultMessage: 'No ping data available', - })} - body={i18n.translate('xpack.uptime.snapshot.noDataDescription', { - defaultMessage: 'There are no pings in the selected time range.', - })} - /> - ); - } else { - const { histogram } = data; - - const downSpecId = i18n.translate('xpack.uptime.snapshotHistogram.series.downLabel', { - defaultMessage: 'Down', - }); - - const upMonitorsId = i18n.translate('xpack.uptime.snapshotHistogram.series.upLabel', { - defaultMessage: 'Up', - }); - - const onBrushEnd = (min: number, max: number) => { - updateUrlParams({ - dateRangeStart: moment(min).toISOString(), - dateRangeEnd: moment(max).toISOString(), - }); - }; - - const barData: BarPoint[] = []; - - histogram.forEach(({ x, upCount, downCount }) => { - barData.push( - { x, y: downCount ?? 0, type: downSpecId }, - { x, y: upCount ?? 0, type: upMonitorsId } - ); - }); - - content = ( - <ChartWrapper - height={height} - loading={loading} - aria-label={i18n.translate('xpack.uptime.snapshotHistogram.description', { - defaultMessage: - 'Bar Chart showing uptime status over time from {startTime} to {endTime}.', - values: { - startTime: moment(new Date(absoluteStartDate).valueOf()).fromNow(), - endTime: moment(new Date(absoluteEndDate).valueOf()).fromNow(), - }, - })} - > - <Chart> - <Settings - xDomain={{ - min: absoluteStartDate, - max: absoluteEndDate, - }} - showLegend={false} - onBrushEnd={onBrushEnd} - /> - <Axis - id={i18n.translate('xpack.uptime.snapshotHistogram.xAxisId', { - defaultMessage: 'Ping X Axis', - })} - position={Position.Bottom} - showOverlappingTicks={false} - tickFormat={timeFormatter(getChartDateLabel(absoluteStartDate, absoluteEndDate))} - /> - <Axis - id={i18n.translate('xpack.uptime.snapshotHistogram.yAxisId', { - defaultMessage: 'Ping Y Axis', - })} - position="left" - title={i18n.translate('xpack.uptime.snapshotHistogram.yAxis.title', { - defaultMessage: 'Pings', - description: - 'The label on the y-axis of a chart that displays the number of times Heartbeat has pinged a set of services/websites.', - })} - /> - - <BarSeries - color={[danger, gray]} - data={barData} - id={downSpecId} - name={i18n.translate('xpack.uptime.snapshotHistogram.series.pings', { - defaultMessage: 'Monitor Pings', - })} - stackAccessors={['x']} - splitSeriesAccessors={['type']} - timeZone="local" - xAccessor="x" - xScaleType="time" - yAccessors={['y']} - yScaleType="linear" - /> - </Chart> - </ChartWrapper> - ); - } - - return ( - <> - <EuiTitle size="xs"> - <h2> - <FormattedMessage - id="xpack.uptime.snapshot.pingsOverTimeTitle" - defaultMessage="Pings over time" - /> - </h2> - </EuiTitle> - {content} - </> - ); -}; diff --git a/x-pack/legacy/plugins/uptime/public/components/functional/empty_state/__tests__/__snapshots__/data_missing.test.tsx.snap b/x-pack/legacy/plugins/uptime/public/components/functional/empty_state/__tests__/__snapshots__/data_missing.test.tsx.snap deleted file mode 100644 index 2182bfb4e656c..0000000000000 --- a/x-pack/legacy/plugins/uptime/public/components/functional/empty_state/__tests__/__snapshots__/data_missing.test.tsx.snap +++ /dev/null @@ -1,52 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`DataMissing component renders basePath and headingMessage 1`] = ` -<EuiFlexGroup - data-test-subj="data-missing" - justifyContent="center" -> - <EuiFlexItem - grow={false} - > - <EuiSpacer - size="xs" - /> - <EuiPanel> - <EuiEmptyPrompt - body={ - <p> - <FormattedMessage - defaultMessage="{configureHeartbeatLink} to start logging uptime data." - id="xpack.uptime.emptyState.configureHeartbeatToGetStartedMessage" - values={ - Object { - "configureHeartbeatLink": <ForwardRef - href="/app/kibana#/home/tutorial/uptimeMonitors" - target="_blank" - > - <FormattedMessage - defaultMessage="Configure Heartbeat" - id="xpack.uptime.emptyState.configureHeartbeatLinkText" - values={Object {}} - /> - </ForwardRef>, - } - } - /> - </p> - } - iconType="uptimeApp" - title={ - <EuiTitle - size="l" - > - <h3> - bar - </h3> - </EuiTitle> - } - /> - </EuiPanel> - </EuiFlexItem> -</EuiFlexGroup> -`; diff --git a/x-pack/legacy/plugins/uptime/public/components/functional/empty_state/__tests__/__snapshots__/empty_state.test.tsx.snap b/x-pack/legacy/plugins/uptime/public/components/functional/empty_state/__tests__/__snapshots__/empty_state.test.tsx.snap deleted file mode 100644 index 2d45bbd18a60c..0000000000000 --- a/x-pack/legacy/plugins/uptime/public/components/functional/empty_state/__tests__/__snapshots__/empty_state.test.tsx.snap +++ /dev/null @@ -1,1141 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`EmptyState component does not render empty state with appropriate base path and no docs 1`] = ` -<EmptyStateComponent - intl={ - Object { - "defaultFormats": Object {}, - "defaultLocale": "en", - "formatDate": [Function], - "formatHTMLMessage": [Function], - "formatMessage": [Function], - "formatNumber": [Function], - "formatPlural": [Function], - "formatRelative": [Function], - "formatTime": [Function], - "formats": Object { - "date": Object { - "full": Object { - "day": "numeric", - "month": "long", - "weekday": "long", - "year": "numeric", - }, - "long": Object { - "day": "numeric", - "month": "long", - "year": "numeric", - }, - "medium": Object { - "day": "numeric", - "month": "short", - "year": "numeric", - }, - "short": Object { - "day": "numeric", - "month": "numeric", - "year": "2-digit", - }, - }, - "number": Object { - "currency": Object { - "style": "currency", - }, - "percent": Object { - "style": "percent", - }, - }, - "relative": Object { - "days": Object { - "units": "day", - }, - "hours": Object { - "units": "hour", - }, - "minutes": Object { - "units": "minute", - }, - "months": Object { - "units": "month", - }, - "seconds": Object { - "units": "second", - }, - "years": Object { - "units": "year", - }, - }, - "time": Object { - "full": Object { - "hour": "numeric", - "minute": "numeric", - "second": "numeric", - "timeZoneName": "short", - }, - "long": Object { - "hour": "numeric", - "minute": "numeric", - "second": "numeric", - "timeZoneName": "short", - }, - "medium": Object { - "hour": "numeric", - "minute": "numeric", - "second": "numeric", - }, - "short": Object { - "hour": "numeric", - "minute": "numeric", - }, - }, - }, - "formatters": Object { - "getDateTimeFormat": [Function], - "getMessageFormat": [Function], - "getNumberFormat": [Function], - "getPluralFormat": [Function], - "getRelativeFormat": [Function], - }, - "locale": "en", - "messages": Object {}, - "now": [Function], - "onError": [Function], - "textComponent": Symbol(react.fragment), - "timeZone": null, - } - } - loading={false} - statesIndexStatus={ - Object { - "docCount": 0, - "indexExists": true, - } - } -> - <DataMissing - headingMessage="No uptime data found" - > - <EuiFlexGroup - data-test-subj="data-missing" - justifyContent="center" - > - <div - className="euiFlexGroup euiFlexGroup--gutterLarge euiFlexGroup--justifyContentCenter euiFlexGroup--directionRow euiFlexGroup--responsive" - data-test-subj="data-missing" - > - <EuiFlexItem - grow={false} - > - <div - className="euiFlexItem euiFlexItem--flexGrowZero" - > - <EuiSpacer - size="xs" - > - <div - className="euiSpacer euiSpacer--xs" - /> - </EuiSpacer> - <EuiPanel> - <div - className="euiPanel euiPanel--paddingMedium" - > - <EuiEmptyPrompt - body={ - <p> - <FormattedMessage - defaultMessage="{configureHeartbeatLink} to start logging uptime data." - id="xpack.uptime.emptyState.configureHeartbeatToGetStartedMessage" - values={ - Object { - "configureHeartbeatLink": <ForwardRef - href="/app/kibana#/home/tutorial/uptimeMonitors" - target="_blank" - > - <FormattedMessage - defaultMessage="Configure Heartbeat" - id="xpack.uptime.emptyState.configureHeartbeatLinkText" - values={Object {}} - /> - </ForwardRef>, - } - } - /> - </p> - } - iconType="uptimeApp" - title={ - <EuiTitle - size="l" - > - <h3> - No uptime data found - </h3> - </EuiTitle> - } - > - <div - className="euiEmptyPrompt" - > - <EuiIcon - color="subdued" - size="xxl" - type="uptimeApp" - > - <div - color="subdued" - data-euiicon-type="uptimeApp" - size="xxl" - /> - </EuiIcon> - <EuiSpacer - size="s" - > - <div - className="euiSpacer euiSpacer--s" - /> - </EuiSpacer> - <EuiTextColor - color="subdued" - > - <span - className="euiTextColor euiTextColor--subdued" - > - <EuiTitle> - <EuiTitle - className="euiTitle euiTitle--medium" - size="l" - > - <h3 - className="euiTitle euiTitle--large euiTitle euiTitle--medium" - > - No uptime data found - </h3> - </EuiTitle> - </EuiTitle> - <EuiSpacer - size="m" - > - <div - className="euiSpacer euiSpacer--m" - /> - </EuiSpacer> - <EuiText> - <div - className="euiText euiText--medium" - > - <p> - <FormattedMessage - defaultMessage="{configureHeartbeatLink} to start logging uptime data." - id="xpack.uptime.emptyState.configureHeartbeatToGetStartedMessage" - values={ - Object { - "configureHeartbeatLink": <ForwardRef - href="/app/kibana#/home/tutorial/uptimeMonitors" - target="_blank" - > - <FormattedMessage - defaultMessage="Configure Heartbeat" - id="xpack.uptime.emptyState.configureHeartbeatLinkText" - values={Object {}} - /> - </ForwardRef>, - } - } - > - <EuiLink - href="/app/kibana#/home/tutorial/uptimeMonitors" - target="_blank" - > - <a - className="euiLink euiLink--primary" - href="/app/kibana#/home/tutorial/uptimeMonitors" - rel="noopener noreferrer" - target="_blank" - > - <FormattedMessage - defaultMessage="Configure Heartbeat" - id="xpack.uptime.emptyState.configureHeartbeatLinkText" - values={Object {}} - > - Configure Heartbeat - </FormattedMessage> - </a> - </EuiLink> - to start logging uptime data. - </FormattedMessage> - </p> - </div> - </EuiText> - </span> - </EuiTextColor> - </div> - </EuiEmptyPrompt> - </div> - </EuiPanel> - </div> - </EuiFlexItem> - </div> - </EuiFlexGroup> - </DataMissing> -</EmptyStateComponent> -`; - -exports[`EmptyState component doesn't render child components when count is falsy 1`] = ` -<EmptyStateComponent - intl={ - Object { - "defaultFormats": Object {}, - "defaultLocale": "en", - "formatDate": [Function], - "formatHTMLMessage": [Function], - "formatMessage": [Function], - "formatNumber": [Function], - "formatPlural": [Function], - "formatRelative": [Function], - "formatTime": [Function], - "formats": Object { - "date": Object { - "full": Object { - "day": "numeric", - "month": "long", - "weekday": "long", - "year": "numeric", - }, - "long": Object { - "day": "numeric", - "month": "long", - "year": "numeric", - }, - "medium": Object { - "day": "numeric", - "month": "short", - "year": "numeric", - }, - "short": Object { - "day": "numeric", - "month": "numeric", - "year": "2-digit", - }, - }, - "number": Object { - "currency": Object { - "style": "currency", - }, - "percent": Object { - "style": "percent", - }, - }, - "relative": Object { - "days": Object { - "units": "day", - }, - "hours": Object { - "units": "hour", - }, - "minutes": Object { - "units": "minute", - }, - "months": Object { - "units": "month", - }, - "seconds": Object { - "units": "second", - }, - "years": Object { - "units": "year", - }, - }, - "time": Object { - "full": Object { - "hour": "numeric", - "minute": "numeric", - "second": "numeric", - "timeZoneName": "short", - }, - "long": Object { - "hour": "numeric", - "minute": "numeric", - "second": "numeric", - "timeZoneName": "short", - }, - "medium": Object { - "hour": "numeric", - "minute": "numeric", - "second": "numeric", - }, - "short": Object { - "hour": "numeric", - "minute": "numeric", - }, - }, - }, - "formatters": Object { - "getDateTimeFormat": [Function], - "getMessageFormat": [Function], - "getNumberFormat": [Function], - "getPluralFormat": [Function], - "getRelativeFormat": [Function], - }, - "locale": "en", - "messages": Object {}, - "now": [Function], - "onError": [Function], - "textComponent": Symbol(react.fragment), - "timeZone": null, - } - } - loading={false} - statesIndexStatus={null} -> - <EmptyStateLoading> - <EuiEmptyPrompt - body={ - <React.Fragment> - <EuiLoadingSpinner - size="xl" - /> - <EuiSpacer /> - <EuiTitle - size="l" - > - <h2> - Loading… - </h2> - </EuiTitle> - </React.Fragment> - } - > - <div - className="euiEmptyPrompt" - > - <EuiTextColor - color="subdued" - > - <span - className="euiTextColor euiTextColor--subdued" - > - <EuiText> - <div - className="euiText euiText--medium" - > - <EuiLoadingSpinner - size="xl" - > - <span - className="euiLoadingSpinner euiLoadingSpinner--xLarge" - /> - </EuiLoadingSpinner> - <EuiSpacer> - <div - className="euiSpacer euiSpacer--l" - /> - </EuiSpacer> - <EuiTitle - size="l" - > - <h2 - className="euiTitle euiTitle--large" - > - Loading… - </h2> - </EuiTitle> - </div> - </EuiText> - </span> - </EuiTextColor> - </div> - </EuiEmptyPrompt> - </EmptyStateLoading> -</EmptyStateComponent> -`; - -exports[`EmptyState component notifies when index does not exist 1`] = ` -<EmptyStateComponent - intl={ - Object { - "defaultFormats": Object {}, - "defaultLocale": "en", - "formatDate": [Function], - "formatHTMLMessage": [Function], - "formatMessage": [Function], - "formatNumber": [Function], - "formatPlural": [Function], - "formatRelative": [Function], - "formatTime": [Function], - "formats": Object { - "date": Object { - "full": Object { - "day": "numeric", - "month": "long", - "weekday": "long", - "year": "numeric", - }, - "long": Object { - "day": "numeric", - "month": "long", - "year": "numeric", - }, - "medium": Object { - "day": "numeric", - "month": "short", - "year": "numeric", - }, - "short": Object { - "day": "numeric", - "month": "numeric", - "year": "2-digit", - }, - }, - "number": Object { - "currency": Object { - "style": "currency", - }, - "percent": Object { - "style": "percent", - }, - }, - "relative": Object { - "days": Object { - "units": "day", - }, - "hours": Object { - "units": "hour", - }, - "minutes": Object { - "units": "minute", - }, - "months": Object { - "units": "month", - }, - "seconds": Object { - "units": "second", - }, - "years": Object { - "units": "year", - }, - }, - "time": Object { - "full": Object { - "hour": "numeric", - "minute": "numeric", - "second": "numeric", - "timeZoneName": "short", - }, - "long": Object { - "hour": "numeric", - "minute": "numeric", - "second": "numeric", - "timeZoneName": "short", - }, - "medium": Object { - "hour": "numeric", - "minute": "numeric", - "second": "numeric", - }, - "short": Object { - "hour": "numeric", - "minute": "numeric", - }, - }, - }, - "formatters": Object { - "getDateTimeFormat": [Function], - "getMessageFormat": [Function], - "getNumberFormat": [Function], - "getPluralFormat": [Function], - "getRelativeFormat": [Function], - }, - "locale": "en", - "messages": Object {}, - "now": [Function], - "onError": [Function], - "textComponent": Symbol(react.fragment), - "timeZone": null, - } - } - loading={false} - statesIndexStatus={ - Object { - "docCount": 1, - "indexExists": false, - } - } -> - <DataMissing - headingMessage="Uptime index not found" - > - <EuiFlexGroup - data-test-subj="data-missing" - justifyContent="center" - > - <div - className="euiFlexGroup euiFlexGroup--gutterLarge euiFlexGroup--justifyContentCenter euiFlexGroup--directionRow euiFlexGroup--responsive" - data-test-subj="data-missing" - > - <EuiFlexItem - grow={false} - > - <div - className="euiFlexItem euiFlexItem--flexGrowZero" - > - <EuiSpacer - size="xs" - > - <div - className="euiSpacer euiSpacer--xs" - /> - </EuiSpacer> - <EuiPanel> - <div - className="euiPanel euiPanel--paddingMedium" - > - <EuiEmptyPrompt - body={ - <p> - <FormattedMessage - defaultMessage="{configureHeartbeatLink} to start logging uptime data." - id="xpack.uptime.emptyState.configureHeartbeatToGetStartedMessage" - values={ - Object { - "configureHeartbeatLink": <ForwardRef - href="/app/kibana#/home/tutorial/uptimeMonitors" - target="_blank" - > - <FormattedMessage - defaultMessage="Configure Heartbeat" - id="xpack.uptime.emptyState.configureHeartbeatLinkText" - values={Object {}} - /> - </ForwardRef>, - } - } - /> - </p> - } - iconType="uptimeApp" - title={ - <EuiTitle - size="l" - > - <h3> - Uptime index not found - </h3> - </EuiTitle> - } - > - <div - className="euiEmptyPrompt" - > - <EuiIcon - color="subdued" - size="xxl" - type="uptimeApp" - > - <div - color="subdued" - data-euiicon-type="uptimeApp" - size="xxl" - /> - </EuiIcon> - <EuiSpacer - size="s" - > - <div - className="euiSpacer euiSpacer--s" - /> - </EuiSpacer> - <EuiTextColor - color="subdued" - > - <span - className="euiTextColor euiTextColor--subdued" - > - <EuiTitle> - <EuiTitle - className="euiTitle euiTitle--medium" - size="l" - > - <h3 - className="euiTitle euiTitle--large euiTitle euiTitle--medium" - > - Uptime index not found - </h3> - </EuiTitle> - </EuiTitle> - <EuiSpacer - size="m" - > - <div - className="euiSpacer euiSpacer--m" - /> - </EuiSpacer> - <EuiText> - <div - className="euiText euiText--medium" - > - <p> - <FormattedMessage - defaultMessage="{configureHeartbeatLink} to start logging uptime data." - id="xpack.uptime.emptyState.configureHeartbeatToGetStartedMessage" - values={ - Object { - "configureHeartbeatLink": <ForwardRef - href="/app/kibana#/home/tutorial/uptimeMonitors" - target="_blank" - > - <FormattedMessage - defaultMessage="Configure Heartbeat" - id="xpack.uptime.emptyState.configureHeartbeatLinkText" - values={Object {}} - /> - </ForwardRef>, - } - } - > - <EuiLink - href="/app/kibana#/home/tutorial/uptimeMonitors" - target="_blank" - > - <a - className="euiLink euiLink--primary" - href="/app/kibana#/home/tutorial/uptimeMonitors" - rel="noopener noreferrer" - target="_blank" - > - <FormattedMessage - defaultMessage="Configure Heartbeat" - id="xpack.uptime.emptyState.configureHeartbeatLinkText" - values={Object {}} - > - Configure Heartbeat - </FormattedMessage> - </a> - </EuiLink> - to start logging uptime data. - </FormattedMessage> - </p> - </div> - </EuiText> - </span> - </EuiTextColor> - </div> - </EuiEmptyPrompt> - </div> - </EuiPanel> - </div> - </EuiFlexItem> - </div> - </EuiFlexGroup> - </DataMissing> -</EmptyStateComponent> -`; - -exports[`EmptyState component renders child components when count is truthy 1`] = ` -<Fragment> - <div> - Foo - </div> - <div> - Bar - </div> - <div> - Baz - </div> -</Fragment> -`; - -exports[`EmptyState component renders error message when an error occurs 1`] = ` -<EmptyStateComponent - errors={ - Array [ - [error: There was an error fetching your data.], - ] - } - intl={ - Object { - "defaultFormats": Object {}, - "defaultLocale": "en", - "formatDate": [Function], - "formatHTMLMessage": [Function], - "formatMessage": [Function], - "formatNumber": [Function], - "formatPlural": [Function], - "formatRelative": [Function], - "formatTime": [Function], - "formats": Object { - "date": Object { - "full": Object { - "day": "numeric", - "month": "long", - "weekday": "long", - "year": "numeric", - }, - "long": Object { - "day": "numeric", - "month": "long", - "year": "numeric", - }, - "medium": Object { - "day": "numeric", - "month": "short", - "year": "numeric", - }, - "short": Object { - "day": "numeric", - "month": "numeric", - "year": "2-digit", - }, - }, - "number": Object { - "currency": Object { - "style": "currency", - }, - "percent": Object { - "style": "percent", - }, - }, - "relative": Object { - "days": Object { - "units": "day", - }, - "hours": Object { - "units": "hour", - }, - "minutes": Object { - "units": "minute", - }, - "months": Object { - "units": "month", - }, - "seconds": Object { - "units": "second", - }, - "years": Object { - "units": "year", - }, - }, - "time": Object { - "full": Object { - "hour": "numeric", - "minute": "numeric", - "second": "numeric", - "timeZoneName": "short", - }, - "long": Object { - "hour": "numeric", - "minute": "numeric", - "second": "numeric", - "timeZoneName": "short", - }, - "medium": Object { - "hour": "numeric", - "minute": "numeric", - "second": "numeric", - }, - "short": Object { - "hour": "numeric", - "minute": "numeric", - }, - }, - }, - "formatters": Object { - "getDateTimeFormat": [Function], - "getMessageFormat": [Function], - "getNumberFormat": [Function], - "getPluralFormat": [Function], - "getRelativeFormat": [Function], - }, - "locale": "en", - "messages": Object {}, - "now": [Function], - "onError": [Function], - "textComponent": Symbol(react.fragment), - "timeZone": null, - } - } - loading={false} - statesIndexStatus={null} -> - <EmptyStateError - errors={ - Array [ - [error: There was an error fetching your data.], - ] - } - > - <EuiFlexGroup - justifyContent="center" - > - <div - className="euiFlexGroup euiFlexGroup--gutterLarge euiFlexGroup--justifyContentCenter euiFlexGroup--directionRow euiFlexGroup--responsive" - > - <EuiFlexItem - grow={false} - > - <div - className="euiFlexItem euiFlexItem--flexGrowZero" - > - <EuiPanel> - <div - className="euiPanel euiPanel--paddingMedium" - > - <EuiEmptyPrompt - body={ - <React.Fragment> - <p> - There was an error fetching your data. - </p> - </React.Fragment> - } - iconColor="subdued" - iconType="securityApp" - title={ - <EuiTitle - size="m" - > - <h3> - Error - </h3> - </EuiTitle> - } - > - <div - className="euiEmptyPrompt" - > - <EuiIcon - color="subdued" - size="xxl" - type="securityApp" - > - <div - color="subdued" - data-euiicon-type="securityApp" - size="xxl" - /> - </EuiIcon> - <EuiSpacer - size="s" - > - <div - className="euiSpacer euiSpacer--s" - /> - </EuiSpacer> - <EuiTextColor - color="subdued" - > - <span - className="euiTextColor euiTextColor--subdued" - > - <EuiTitle> - <EuiTitle - className="euiTitle euiTitle--medium" - size="m" - > - <h3 - className="euiTitle euiTitle--medium euiTitle euiTitle--medium" - > - Error - </h3> - </EuiTitle> - </EuiTitle> - <EuiSpacer - size="m" - > - <div - className="euiSpacer euiSpacer--m" - /> - </EuiSpacer> - <EuiText> - <div - className="euiText euiText--medium" - > - <p - key="There was an error fetching your data." - > - There was an error fetching your data. - </p> - </div> - </EuiText> - </span> - </EuiTextColor> - </div> - </EuiEmptyPrompt> - </div> - </EuiPanel> - </div> - </EuiFlexItem> - </div> - </EuiFlexGroup> - </EmptyStateError> -</EmptyStateComponent> -`; - -exports[`EmptyState component renders loading state if no errors or doc count 1`] = ` -<EmptyStateComponent - intl={ - Object { - "defaultFormats": Object {}, - "defaultLocale": "en", - "formatDate": [Function], - "formatHTMLMessage": [Function], - "formatMessage": [Function], - "formatNumber": [Function], - "formatPlural": [Function], - "formatRelative": [Function], - "formatTime": [Function], - "formats": Object { - "date": Object { - "full": Object { - "day": "numeric", - "month": "long", - "weekday": "long", - "year": "numeric", - }, - "long": Object { - "day": "numeric", - "month": "long", - "year": "numeric", - }, - "medium": Object { - "day": "numeric", - "month": "short", - "year": "numeric", - }, - "short": Object { - "day": "numeric", - "month": "numeric", - "year": "2-digit", - }, - }, - "number": Object { - "currency": Object { - "style": "currency", - }, - "percent": Object { - "style": "percent", - }, - }, - "relative": Object { - "days": Object { - "units": "day", - }, - "hours": Object { - "units": "hour", - }, - "minutes": Object { - "units": "minute", - }, - "months": Object { - "units": "month", - }, - "seconds": Object { - "units": "second", - }, - "years": Object { - "units": "year", - }, - }, - "time": Object { - "full": Object { - "hour": "numeric", - "minute": "numeric", - "second": "numeric", - "timeZoneName": "short", - }, - "long": Object { - "hour": "numeric", - "minute": "numeric", - "second": "numeric", - "timeZoneName": "short", - }, - "medium": Object { - "hour": "numeric", - "minute": "numeric", - "second": "numeric", - }, - "short": Object { - "hour": "numeric", - "minute": "numeric", - }, - }, - }, - "formatters": Object { - "getDateTimeFormat": [Function], - "getMessageFormat": [Function], - "getNumberFormat": [Function], - "getPluralFormat": [Function], - "getRelativeFormat": [Function], - }, - "locale": "en", - "messages": Object {}, - "now": [Function], - "onError": [Function], - "textComponent": Symbol(react.fragment), - "timeZone": null, - } - } - loading={true} - statesIndexStatus={null} -> - <EmptyStateLoading> - <EuiEmptyPrompt - body={ - <React.Fragment> - <EuiLoadingSpinner - size="xl" - /> - <EuiSpacer /> - <EuiTitle - size="l" - > - <h2> - Loading… - </h2> - </EuiTitle> - </React.Fragment> - } - > - <div - className="euiEmptyPrompt" - > - <EuiTextColor - color="subdued" - > - <span - className="euiTextColor euiTextColor--subdued" - > - <EuiText> - <div - className="euiText euiText--medium" - > - <EuiLoadingSpinner - size="xl" - > - <span - className="euiLoadingSpinner euiLoadingSpinner--xLarge" - /> - </EuiLoadingSpinner> - <EuiSpacer> - <div - className="euiSpacer euiSpacer--l" - /> - </EuiSpacer> - <EuiTitle - size="l" - > - <h2 - className="euiTitle euiTitle--large" - > - Loading… - </h2> - </EuiTitle> - </div> - </EuiText> - </span> - </EuiTextColor> - </div> - </EuiEmptyPrompt> - </EmptyStateLoading> -</EmptyStateComponent> -`; diff --git a/x-pack/legacy/plugins/uptime/public/components/functional/empty_state/__tests__/data_missing.test.tsx b/x-pack/legacy/plugins/uptime/public/components/functional/empty_state/__tests__/data_missing.test.tsx deleted file mode 100644 index 8605d2966aaae..0000000000000 --- a/x-pack/legacy/plugins/uptime/public/components/functional/empty_state/__tests__/data_missing.test.tsx +++ /dev/null @@ -1,16 +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 { shallowWithIntl } from 'test_utils/enzyme_helpers'; -import React from 'react'; -import { DataMissing } from '../data_missing'; - -describe('DataMissing component', () => { - it('renders basePath and headingMessage', () => { - const component = shallowWithIntl(<DataMissing headingMessage="bar" />); - expect(component).toMatchSnapshot(); - }); -}); diff --git a/x-pack/legacy/plugins/uptime/public/components/functional/empty_state/data_missing.tsx b/x-pack/legacy/plugins/uptime/public/components/functional/empty_state/data_missing.tsx deleted file mode 100644 index 337c08774e8e8..0000000000000 --- a/x-pack/legacy/plugins/uptime/public/components/functional/empty_state/data_missing.tsx +++ /dev/null @@ -1,64 +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 { - EuiFlexGroup, - EuiEmptyPrompt, - EuiFlexItem, - EuiSpacer, - EuiPanel, - EuiTitle, - EuiLink, -} from '@elastic/eui'; -import { FormattedMessage } from '@kbn/i18n/react'; -import React, { useContext } from 'react'; -import { UptimeSettingsContext } from '../../../contexts'; - -interface DataMissingProps { - headingMessage: string; -} - -export const DataMissing = ({ headingMessage }: DataMissingProps) => { - const { basePath } = useContext(UptimeSettingsContext); - return ( - <EuiFlexGroup justifyContent="center" data-test-subj="data-missing"> - <EuiFlexItem grow={false}> - <EuiSpacer size="xs" /> - <EuiPanel> - <EuiEmptyPrompt - iconType="uptimeApp" - title={ - <EuiTitle size="l"> - <h3>{headingMessage}</h3> - </EuiTitle> - } - body={ - <p> - <FormattedMessage - id="xpack.uptime.emptyState.configureHeartbeatToGetStartedMessage" - defaultMessage="{configureHeartbeatLink} to start logging uptime data." - values={{ - configureHeartbeatLink: ( - <EuiLink - target="_blank" - href={`${basePath}/app/kibana#/home/tutorial/uptimeMonitors`} - > - <FormattedMessage - id="xpack.uptime.emptyState.configureHeartbeatLinkText" - defaultMessage="Configure Heartbeat" - /> - </EuiLink> - ), - }} - /> - </p> - } - /> - </EuiPanel> - </EuiFlexItem> - </EuiFlexGroup> - ); -}; diff --git a/x-pack/legacy/plugins/uptime/public/components/functional/empty_state/empty_index.tsx b/x-pack/legacy/plugins/uptime/public/components/functional/empty_state/empty_index.tsx deleted file mode 100644 index 0141198ec15e0..0000000000000 --- a/x-pack/legacy/plugins/uptime/public/components/functional/empty_state/empty_index.tsx +++ /dev/null @@ -1,67 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { - EuiEmptyPrompt, - EuiFlexGroup, - EuiFlexItem, - EuiLink, - EuiPanel, - EuiSpacer, - EuiTitle, -} from '@elastic/eui'; -import { FormattedMessage } from '@kbn/i18n/react'; -import React, { Fragment } from 'react'; - -interface EmptyIndexProps { - basePath: string; -} - -export const EmptyIndex = ({ basePath }: EmptyIndexProps) => ( - <EuiFlexGroup justifyContent="center"> - <EuiFlexItem grow={false}> - <EuiSpacer size="xs" /> - <EuiPanel> - <EuiEmptyPrompt - iconType="uptimeApp" - title={ - <EuiTitle size="l"> - <h3> - <FormattedMessage - id="xpack.uptime.emptyState.noDataTitle" - defaultMessage="No uptime data available" - /> - </h3> - </EuiTitle> - } - body={ - <Fragment> - <p> - <FormattedMessage - id="xpack.uptime.emptyState.configureHeartbeatToGetStartedMessage" - defaultMessage="{configureHeartbeatLink} to start logging uptime data." - values={{ - configureHeartbeatLink: ( - <EuiLink - target="_blank" - href={`${basePath}/app/kibana#/home/tutorial/uptimeMonitors`} - > - <FormattedMessage - id="xpack.uptime.emptyState.configureHeartbeatLinkText" - defaultMessage="Configure Heartbeat" - /> - </EuiLink> - ), - }} - /> - </p> - </Fragment> - } - /> - </EuiPanel> - </EuiFlexItem> - </EuiFlexGroup> -); diff --git a/x-pack/legacy/plugins/uptime/public/components/functional/empty_state/empty_state.tsx b/x-pack/legacy/plugins/uptime/public/components/functional/empty_state/empty_state.tsx deleted file mode 100644 index ae6a1b892bc99..0000000000000 --- a/x-pack/legacy/plugins/uptime/public/components/functional/empty_state/empty_state.tsx +++ /dev/null @@ -1,60 +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 { i18n } from '@kbn/i18n'; -import { EmptyStateError } from './empty_state_error'; -import { EmptyStateLoading } from './empty_state_loading'; -import { DataMissing } from './data_missing'; -import { StatesIndexStatus } from '../../../../common/runtime_types'; -import { IHttpFetchError } from '../../../../../../../../target/types/core/public/http'; - -interface EmptyStateProps { - children: JSX.Element[] | JSX.Element; - statesIndexStatus: StatesIndexStatus | null; - loading: boolean; - errors?: IHttpFetchError[]; -} - -export const EmptyStateComponent = ({ - children, - statesIndexStatus, - loading, - errors, -}: EmptyStateProps) => { - if (errors?.length) { - return <EmptyStateError errors={errors} />; - } - if (!loading && statesIndexStatus) { - const { indexExists, docCount } = statesIndexStatus; - if (!indexExists) { - return ( - <DataMissing - headingMessage={i18n.translate('xpack.uptime.emptyState.noIndexTitle', { - defaultMessage: 'Uptime index not found', - })} - /> - ); - } else if (indexExists && docCount === 0) { - return ( - <DataMissing - headingMessage={i18n.translate('xpack.uptime.emptyState.noDataMessage', { - defaultMessage: 'No uptime data found', - })} - /> - ); - } - /** - * We choose to render the children any time the count > 0, even if - * the component is loading. If we render the loading state for this component, - * it will blow away the state of child components and trigger an ugly - * jittery UX any time the components refresh. This way we'll keep the stale - * state displayed during the fetching process. - */ - return <Fragment>{children}</Fragment>; - } - return <EmptyStateLoading />; -}; diff --git a/x-pack/legacy/plugins/uptime/public/components/functional/filter_group/index.ts b/x-pack/legacy/plugins/uptime/public/components/functional/filter_group/index.ts deleted file mode 100644 index 2aae026144d8f..0000000000000 --- a/x-pack/legacy/plugins/uptime/public/components/functional/filter_group/index.ts +++ /dev/null @@ -1,7 +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. - */ - -export { FilterGroupComponent } from './filter_group'; diff --git a/x-pack/legacy/plugins/uptime/public/components/functional/index.ts b/x-pack/legacy/plugins/uptime/public/components/functional/index.ts deleted file mode 100644 index 8d0352e01d40e..0000000000000 --- a/x-pack/legacy/plugins/uptime/public/components/functional/index.ts +++ /dev/null @@ -1,20 +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. - */ - -export { - ToggleAlertFlyoutButtonComponent, - UptimeAlertsContextProvider, - UptimeAlertsFlyoutWrapperComponent, -} from './alerts'; -export * from './alerts'; -export { DonutChart } from './charts/donut_chart'; -export { KueryBarComponent } from './kuery_bar/kuery_bar'; -export { MonitorCharts } from './monitor_charts'; -export { MonitorList } from './monitor_list'; -export { OverviewPageParsingErrorCallout } from './overview_page_parsing_error_callout'; -export { PingList } from './ping_list'; -export { PingHistogramComponent } from './charts'; -export { StatusPanel } from './status_panel'; diff --git a/x-pack/legacy/plugins/uptime/public/components/functional/kuery_bar/typeahead/index.d.ts b/x-pack/legacy/plugins/uptime/public/components/functional/kuery_bar/typeahead/index.d.ts deleted file mode 100644 index c9f43b3a620bd..0000000000000 --- a/x-pack/legacy/plugins/uptime/public/components/functional/kuery_bar/typeahead/index.d.ts +++ /dev/null @@ -1,43 +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, { Component } from 'react'; - -interface TypeaheadProps { - onChange: (inputValue: string, selectionStart: number) => void; - onSubmit: (inputValue: string) => void; - suggestions: unknown[]; - queryExample: string; - initialValue?: string; - isLoading?: boolean; - disabled?: boolean; -} - -export class Typeahead extends React.Component<TypeaheadProps> { - incrementIndex(currentIndex: any): void; - - decrementIndex(currentIndex: any): void; - - onKeyUp(event: any): void; - - onKeyDown(event: any): void; - - selectSuggestion(suggestion: any): void; - - onClickOutside(): void; - - onChangeInputValue(event: any): void; - - onClickInput(event: any): void; - - onClickSuggestion(suggestion: any): void; - - onMouseEnterSuggestion(index: any): void; - - onSubmit(): void; - - render(): any; -} diff --git a/x-pack/legacy/plugins/uptime/public/components/functional/monitor_charts.tsx b/x-pack/legacy/plugins/uptime/public/components/functional/monitor_charts.tsx deleted file mode 100644 index c5edd0fd85977..0000000000000 --- a/x-pack/legacy/plugins/uptime/public/components/functional/monitor_charts.tsx +++ /dev/null @@ -1,26 +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 from 'react'; -import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; -import { PingHistogram, DurationChart } from '../connected'; - -interface MonitorChartsProps { - monitorId: string; -} - -export const MonitorCharts = ({ monitorId }: MonitorChartsProps) => { - return ( - <EuiFlexGroup> - <EuiFlexItem> - <DurationChart monitorId={monitorId} /> - </EuiFlexItem> - <EuiFlexItem> - <PingHistogram height="400px" isResponsive={false} monitorId={monitorId} /> - </EuiFlexItem> - </EuiFlexGroup> - ); -}; diff --git a/x-pack/legacy/plugins/uptime/public/components/functional/monitor_list/__tests__/__snapshots__/monitor_list.test.tsx.snap b/x-pack/legacy/plugins/uptime/public/components/functional/monitor_list/__tests__/__snapshots__/monitor_list.test.tsx.snap deleted file mode 100644 index 2b8bc0bb06ddf..0000000000000 --- a/x-pack/legacy/plugins/uptime/public/components/functional/monitor_list/__tests__/__snapshots__/monitor_list.test.tsx.snap +++ /dev/null @@ -1,856 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`MonitorList component renders a no items message when no data is provided 1`] = ` -<Fragment> - <EuiPanel> - <EuiTitle - size="xs" - > - <h5> - <FormattedMessage - defaultMessage="Monitor status" - id="xpack.uptime.monitorList.monitoringStatusTitle" - values={Object {}} - /> - </h5> - </EuiTitle> - <EuiSpacer - size="s" - /> - <EuiBasicTable - aria-label="Monitor Status table with columns for Status, Name, URL, IP, Downtime History and Integrations. The table is currently displaying 0 items." - columns={ - Array [ - Object { - "align": "left", - "field": "state.monitor.status", - "mobileOptions": Object { - "fullWidth": true, - }, - "name": "Status", - "render": [Function], - }, - Object { - "align": "left", - "field": "state.monitor.name", - "mobileOptions": Object { - "fullWidth": true, - }, - "name": "Name", - "render": [Function], - "sortable": true, - }, - Object { - "align": "left", - "field": "state.url.full", - "name": "Url", - "render": [Function], - }, - Object { - "align": "center", - "field": "histogram.points", - "mobileOptions": Object { - "show": false, - }, - "name": "Downtime history", - "render": [Function], - }, - Object { - "align": "right", - "field": "monitor_id", - "isExpander": true, - "name": "", - "render": [Function], - "sortable": true, - "width": "24px", - }, - ] - } - hasActions={true} - isExpandable={true} - itemId="monitor_id" - itemIdToExpandedRowMap={Object {}} - items={Array []} - loading={false} - noItemsMessage="No uptime monitors found" - responsive={true} - tableLayout="fixed" - /> - <EuiSpacer - size="m" - /> - <EuiFlexGroup - justifyContent="spaceBetween" - responsive={false} - > - <EuiFlexItem - grow={false} - > - <MonitorListPageSizeSelect - setSize={[MockFunction]} - size={25} - /> - </EuiFlexItem> - <EuiFlexItem - grow={false} - > - <EuiFlexGroup - responsive={false} - > - <EuiFlexItem - grow={false} - > - <OverviewPageLink - dataTestSubj="xpack.uptime.monitorList.prevButton" - direction="prev" - pagination="" - /> - </EuiFlexItem> - <EuiFlexItem - grow={false} - > - <OverviewPageLink - dataTestSubj="xpack.uptime.monitorList.nextButton" - direction="next" - pagination="" - /> - </EuiFlexItem> - </EuiFlexGroup> - </EuiFlexItem> - </EuiFlexGroup> - </EuiPanel> -</Fragment> -`; - -exports[`MonitorList component renders the monitor list 1`] = ` -.c1 { - padding-left: 17px; -} - -.c3 { - padding-top: 12px; -} - -.c2 { - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; -} - -@media (max-width:574px) { - .c0 { - min-width: 230px; - } -} - -<div - class="euiPanel euiPanel--paddingMedium" -> - <h5 - class="euiTitle euiTitle--xsmall" - > - Monitor status - </h5> - <div - class="euiSpacer euiSpacer--s" - /> - <div - aria-label="Monitor Status table with columns for Status, Name, URL, IP, Downtime History and Integrations. The table is currently displaying 2 items." - class="euiBasicTable" - > - <div> - <div - class="euiTableHeaderMobile" - > - <div - class="euiFlexGroup euiFlexGroup--gutterLarge euiFlexGroup--alignItemsBaseline euiFlexGroup--justifyContentSpaceBetween euiFlexGroup--directionRow" - > - <div - class="euiFlexItem euiFlexItem--flexGrowZero" - /> - <div - class="euiFlexItem euiFlexItem--flexGrowZero" - /> - </div> - </div> - <table - class="euiTable euiTable--responsive" - > - <caption - class="euiScreenReaderOnly euiTableCaption" - /> - <thead> - <tr> - <th - class="euiTableHeaderCell euiTableHeaderCell--hideForMobile" - data-test-subj="tableHeaderCell_state.monitor.status_0" - role="columnheader" - scope="col" - > - <div - class="euiTableCellContent" - > - <span - class="euiTableCellContent__text" - > - Status - </span> - </div> - </th> - <th - class="euiTableHeaderCell euiTableHeaderCell--hideForMobile" - data-test-subj="tableHeaderCell_state.monitor.name_1" - role="columnheader" - scope="col" - > - <div - class="euiTableCellContent" - > - <span - class="euiTableCellContent__text" - > - Name - </span> - </div> - </th> - <th - class="euiTableHeaderCell" - data-test-subj="tableHeaderCell_state.url.full_2" - role="columnheader" - scope="col" - > - <div - class="euiTableCellContent" - > - <span - class="euiTableCellContent__text" - > - Url - </span> - </div> - </th> - <th - class="euiTableHeaderCell euiTableHeaderCell--hideForMobile" - data-test-subj="tableHeaderCell_histogram.points_3" - role="columnheader" - scope="col" - > - <div - class="euiTableCellContent euiTableCellContent--alignCenter" - > - <span - class="euiTableCellContent__text" - > - Downtime history - </span> - </div> - </th> - <td - class="euiTableHeaderCell" - data-test-subj="tableHeaderCell_monitor_id_4" - role="columnheader" - scope="col" - style="width:24px" - > - <div - class="euiTableCellContent euiTableCellContent--alignRight" - > - <span - class="euiTableCellContent__text" - /> - </div> - </td> - </tr> - </thead> - <tbody> - <tr - class="euiTableRow euiTableRow-hasActions euiTableRow-isExpandable" - > - <td - class="euiTableRowCell euiTableRowCell--isMobileFullWidth" - > - <div - class="euiTableRowCell__mobileHeader euiTableRowCell--hideForDesktop" - > - Status - </div> - <div - class="euiTableCellContent euiTableCellContent--overflowingContent" - > - <div - class="euiFlexGroup euiFlexGroup--alignItemsCenter euiFlexGroup--directionRow c0" - > - <div - class="euiFlexItem euiFlexItem--flexGrow1" - style="flex-basis:40px" - > - <div - class="euiHealth" - style="display:block" - > - <div - class="euiFlexGroup euiFlexGroup--gutterExtraSmall euiFlexGroup--alignItemsCenter euiFlexGroup--directionRow" - > - <div - class="euiFlexItem euiFlexItem--flexGrowZero" - > - <div - color="" - data-euiicon-type="dot" - /> - </div> - <div - class="euiFlexItem euiFlexItem--flexGrowZero" - /> - </div> - </div> - <span - class="c1" - > - <span - class="euiToolTipAnchor" - > - <div - class="euiText euiText--extraSmall" - > - <div - class="euiTextColor euiTextColor--subdued" - > - 1897 Yr ago - </div> - </div> - </span> - </span> - </div> - <div - class="euiFlexItem euiFlexItem--flexGrow2" - > - <div - class="euiText euiText--small" - > - in 0/1 Location - </div> - </div> - </div> - </div> - </td> - <td - class="euiTableRowCell euiTableRowCell--isMobileFullWidth" - > - <div - class="euiTableRowCell__mobileHeader euiTableRowCell--hideForDesktop" - > - Name - </div> - <div - class="euiTableCellContent euiTableCellContent--overflowingContent" - > - <button - class="euiLink euiLink--primary" - type="button" - > - <a - data-test-subj="monitor-page-link-foo" - href="/monitor/Zm9v" - > - Unnamed - foo - </a> - </button> - </div> - </td> - <td - class="euiTableRowCell" - > - <div - class="euiTableRowCell__mobileHeader euiTableRowCell--hideForDesktop" - > - Url - </div> - <div - class="euiTableCellContent euiTableCellContent--overflowingContent" - > - <button - class="euiLink euiLink--text c2" - type="button" - > - - <div - color="subbdued" - data-euiicon-type="popout" - /> - </button> - </div> - </td> - <td - class="euiTableRowCell euiTableRowCell--hideForMobile" - > - <div - class="euiTableCellContent euiTableCellContent--alignCenter euiTableCellContent--overflowingContent" - > - <span - class="euiToolTipAnchor" - > - <div - class="euiText euiText--medium" - > - <div - class="euiTextColor euiTextColor--secondary" - > - -- - </div> - </div> - </span> - </div> - </td> - <td - class="euiTableRowCell euiTableRowCell--isExpander" - style="width:24px" - > - <div - class="euiTableCellContent euiTableCellContent--alignRight euiTableCellContent--overflowingContent" - > - <button - aria-label="Expand row for monitor with ID foo" - class="euiButtonIcon euiButtonIcon--primary" - type="button" - > - <div - aria-hidden="true" - class="euiButtonIcon__icon" - data-euiicon-type="arrowDown" - /> - </button> - </div> - </td> - </tr> - <tr - class="euiTableRow euiTableRow-hasActions euiTableRow-isExpandable" - > - <td - class="euiTableRowCell euiTableRowCell--isMobileFullWidth" - > - <div - class="euiTableRowCell__mobileHeader euiTableRowCell--hideForDesktop" - > - Status - </div> - <div - class="euiTableCellContent euiTableCellContent--overflowingContent" - > - <div - class="euiFlexGroup euiFlexGroup--alignItemsCenter euiFlexGroup--directionRow c0" - > - <div - class="euiFlexItem euiFlexItem--flexGrow1" - style="flex-basis:40px" - > - <div - class="euiHealth" - style="display:block" - > - <div - class="euiFlexGroup euiFlexGroup--gutterExtraSmall euiFlexGroup--alignItemsCenter euiFlexGroup--directionRow" - > - <div - class="euiFlexItem euiFlexItem--flexGrowZero" - > - <div - color="" - data-euiicon-type="dot" - /> - </div> - <div - class="euiFlexItem euiFlexItem--flexGrowZero" - /> - </div> - </div> - <span - class="c1" - > - <span - class="euiToolTipAnchor" - > - <div - class="euiText euiText--extraSmall" - > - <div - class="euiTextColor euiTextColor--subdued" - > - 1895 Yr ago - </div> - </div> - </span> - </span> - </div> - <div - class="euiFlexItem euiFlexItem--flexGrow2" - > - <div - class="euiText euiText--small" - > - in 1/1 Location - </div> - </div> - </div> - </div> - </td> - <td - class="euiTableRowCell euiTableRowCell--isMobileFullWidth" - > - <div - class="euiTableRowCell__mobileHeader euiTableRowCell--hideForDesktop" - > - Name - </div> - <div - class="euiTableCellContent euiTableCellContent--overflowingContent" - > - <button - class="euiLink euiLink--primary" - type="button" - > - <a - data-test-subj="monitor-page-link-bar" - href="/monitor/YmFy" - > - Unnamed - bar - </a> - </button> - </div> - </td> - <td - class="euiTableRowCell" - > - <div - class="euiTableRowCell__mobileHeader euiTableRowCell--hideForDesktop" - > - Url - </div> - <div - class="euiTableCellContent euiTableCellContent--overflowingContent" - > - <button - class="euiLink euiLink--text c2" - type="button" - > - - <div - color="subbdued" - data-euiicon-type="popout" - /> - </button> - </div> - </td> - <td - class="euiTableRowCell euiTableRowCell--hideForMobile" - > - <div - class="euiTableCellContent euiTableCellContent--alignCenter euiTableCellContent--overflowingContent" - > - <span - class="euiToolTipAnchor" - > - <div - class="euiText euiText--medium" - > - <div - class="euiTextColor euiTextColor--secondary" - > - -- - </div> - </div> - </span> - </div> - </td> - <td - class="euiTableRowCell euiTableRowCell--isExpander" - style="width:24px" - > - <div - class="euiTableCellContent euiTableCellContent--alignRight euiTableCellContent--overflowingContent" - > - <button - aria-label="Expand row for monitor with ID bar" - class="euiButtonIcon euiButtonIcon--primary" - type="button" - > - <div - aria-hidden="true" - class="euiButtonIcon__icon" - data-euiicon-type="arrowDown" - /> - </button> - </div> - </td> - </tr> - </tbody> - </table> - </div> - </div> - <div - class="euiSpacer euiSpacer--m" - /> - <div - class="euiFlexGroup euiFlexGroup--gutterLarge euiFlexGroup--justifyContentSpaceBetween euiFlexGroup--directionRow" - > - <div - class="euiFlexItem euiFlexItem--flexGrowZero" - > - <div - class="euiPopover euiPopover--anchorUpLeft" - > - <div - class="euiPopover__anchor" - > - <button - class="euiButtonEmpty euiButtonEmpty--text euiButtonEmpty--iconRight" - data-test-subj="xpack.uptime.monitorList.pageSizeSelect.popoverOpen" - type="button" - > - <span - class="euiButtonEmpty__content" - > - <div - aria-hidden="true" - class="euiButtonEmpty__icon" - data-euiicon-type="arrowDown" - /> - <span - class="euiButtonEmpty__text" - > - Rows per page: 25 - </span> - </span> - </button> - </div> - </div> - </div> - <div - class="euiFlexItem euiFlexItem--flexGrowZero" - > - <div - class="euiFlexGroup euiFlexGroup--gutterLarge euiFlexGroup--directionRow" - > - <div - class="euiFlexItem euiFlexItem--flexGrowZero" - > - <button - aria-label="A disabled pagination button indicating that there cannot be any further navigation in the monitors list." - class="euiButtonIcon euiButtonIcon--text c3" - data-test-subj="xpack.uptime.monitorList.prevButton" - disabled="" - type="button" - > - <div - aria-hidden="true" - class="euiButtonIcon__icon" - data-euiicon-type="arrowLeft" - /> - </button> - </div> - <div - class="euiFlexItem euiFlexItem--flexGrowZero" - > - <button - aria-label="A disabled pagination button indicating that there cannot be any further navigation in the monitors list." - class="euiButtonIcon euiButtonIcon--text c3" - data-test-subj="xpack.uptime.monitorList.nextButton" - disabled="" - type="button" - > - <div - aria-hidden="true" - class="euiButtonIcon__icon" - data-euiicon-type="arrowRight" - /> - </button> - </div> - </div> - </div> - </div> -</div> -`; - -exports[`MonitorList component shallow renders the monitor list 1`] = ` -<Fragment> - <EuiPanel> - <EuiTitle - size="xs" - > - <h5> - <FormattedMessage - defaultMessage="Monitor status" - id="xpack.uptime.monitorList.monitoringStatusTitle" - values={Object {}} - /> - </h5> - </EuiTitle> - <EuiSpacer - size="s" - /> - <EuiBasicTable - aria-label="Monitor Status table with columns for Status, Name, URL, IP, Downtime History and Integrations. The table is currently displaying 2 items." - columns={ - Array [ - Object { - "align": "left", - "field": "state.monitor.status", - "mobileOptions": Object { - "fullWidth": true, - }, - "name": "Status", - "render": [Function], - }, - Object { - "align": "left", - "field": "state.monitor.name", - "mobileOptions": Object { - "fullWidth": true, - }, - "name": "Name", - "render": [Function], - "sortable": true, - }, - Object { - "align": "left", - "field": "state.url.full", - "name": "Url", - "render": [Function], - }, - Object { - "align": "center", - "field": "histogram.points", - "mobileOptions": Object { - "show": false, - }, - "name": "Downtime history", - "render": [Function], - }, - Object { - "align": "right", - "field": "monitor_id", - "isExpander": true, - "name": "", - "render": [Function], - "sortable": true, - "width": "24px", - }, - ] - } - hasActions={true} - isExpandable={true} - itemId="monitor_id" - itemIdToExpandedRowMap={Object {}} - items={ - Array [ - Object { - "monitor_id": "foo", - "state": Object { - "checks": Array [ - Object { - "monitor": Object { - "ip": "127.0.0.1", - "status": "up", - }, - "timestamp": "124", - }, - Object { - "monitor": Object { - "ip": "127.0.0.2", - "status": "down", - }, - "timestamp": "125", - }, - Object { - "monitor": Object { - "ip": "127.0.0.3", - "status": "down", - }, - "timestamp": "126", - }, - ], - "summary": Object { - "down": 2, - "up": 1, - }, - "timestamp": "123", - }, - }, - Object { - "monitor_id": "bar", - "state": Object { - "checks": Array [ - Object { - "monitor": Object { - "ip": "127.0.0.1", - "status": "up", - }, - "timestamp": "125", - }, - Object { - "monitor": Object { - "ip": "127.0.0.2", - "status": "up", - }, - "timestamp": "126", - }, - ], - "summary": Object { - "down": 0, - "up": 2, - }, - "timestamp": "125", - }, - }, - ] - } - loading={false} - noItemsMessage="No uptime monitors found" - responsive={true} - tableLayout="fixed" - /> - <EuiSpacer - size="m" - /> - <EuiFlexGroup - justifyContent="spaceBetween" - responsive={false} - > - <EuiFlexItem - grow={false} - > - <MonitorListPageSizeSelect - setSize={[MockFunction]} - size={25} - /> - </EuiFlexItem> - <EuiFlexItem - grow={false} - > - <EuiFlexGroup - responsive={false} - > - <EuiFlexItem - grow={false} - > - <OverviewPageLink - dataTestSubj="xpack.uptime.monitorList.prevButton" - direction="prev" - pagination="" - /> - </EuiFlexItem> - <EuiFlexItem - grow={false} - > - <OverviewPageLink - dataTestSubj="xpack.uptime.monitorList.nextButton" - direction="next" - pagination="" - /> - </EuiFlexItem> - </EuiFlexGroup> - </EuiFlexItem> - </EuiFlexGroup> - </EuiPanel> -</Fragment> -`; diff --git a/x-pack/legacy/plugins/uptime/public/components/functional/monitor_list/__tests__/__snapshots__/monitor_list_pagination.test.tsx.snap b/x-pack/legacy/plugins/uptime/public/components/functional/monitor_list/__tests__/__snapshots__/monitor_list_pagination.test.tsx.snap deleted file mode 100644 index db5bfa72deb36..0000000000000 --- a/x-pack/legacy/plugins/uptime/public/components/functional/monitor_list/__tests__/__snapshots__/monitor_list_pagination.test.tsx.snap +++ /dev/null @@ -1,307 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`MonitorListPagination component renders a no items message when no data is provided 1`] = ` -<Fragment> - <EuiPanel> - <EuiTitle - size="xs" - > - <h5> - <FormattedMessage - defaultMessage="Monitor status" - id="xpack.uptime.monitorList.monitoringStatusTitle" - values={Object {}} - /> - </h5> - </EuiTitle> - <EuiSpacer - size="s" - /> - <EuiBasicTable - aria-label="Monitor Status table with columns for Status, Name, URL, IP, Downtime History and Integrations. The table is currently displaying 0 items." - columns={ - Array [ - Object { - "align": "left", - "field": "state.monitor.status", - "mobileOptions": Object { - "fullWidth": true, - }, - "name": "Status", - "render": [Function], - }, - Object { - "align": "left", - "field": "state.monitor.name", - "mobileOptions": Object { - "fullWidth": true, - }, - "name": "Name", - "render": [Function], - "sortable": true, - }, - Object { - "align": "left", - "field": "state.url.full", - "name": "Url", - "render": [Function], - }, - Object { - "align": "center", - "field": "histogram.points", - "mobileOptions": Object { - "show": false, - }, - "name": "Downtime history", - "render": [Function], - }, - Object { - "align": "right", - "field": "monitor_id", - "isExpander": true, - "name": "", - "render": [Function], - "sortable": true, - "width": "24px", - }, - ] - } - hasActions={true} - isExpandable={true} - itemId="monitor_id" - itemIdToExpandedRowMap={Object {}} - items={Array []} - loading={false} - noItemsMessage="No uptime monitors found" - responsive={true} - tableLayout="fixed" - /> - <EuiSpacer - size="m" - /> - <EuiFlexGroup - justifyContent="spaceBetween" - responsive={false} - > - <EuiFlexItem - grow={false} - > - <MonitorListPageSizeSelect - setSize={[MockFunction]} - size={25} - /> - </EuiFlexItem> - <EuiFlexItem - grow={false} - > - <EuiFlexGroup - responsive={false} - > - <EuiFlexItem - grow={false} - > - <OverviewPageLink - dataTestSubj="xpack.uptime.monitorList.prevButton" - direction="prev" - pagination="" - /> - </EuiFlexItem> - <EuiFlexItem - grow={false} - > - <OverviewPageLink - dataTestSubj="xpack.uptime.monitorList.nextButton" - direction="next" - pagination="" - /> - </EuiFlexItem> - </EuiFlexGroup> - </EuiFlexItem> - </EuiFlexGroup> - </EuiPanel> -</Fragment> -`; - -exports[`MonitorListPagination component renders the monitor list 1`] = ` -<Fragment> - <EuiPanel> - <EuiTitle - size="xs" - > - <h5> - <FormattedMessage - defaultMessage="Monitor status" - id="xpack.uptime.monitorList.monitoringStatusTitle" - values={Object {}} - /> - </h5> - </EuiTitle> - <EuiSpacer - size="s" - /> - <EuiBasicTable - aria-label="Monitor Status table with columns for Status, Name, URL, IP, Downtime History and Integrations. The table is currently displaying 2 items." - columns={ - Array [ - Object { - "align": "left", - "field": "state.monitor.status", - "mobileOptions": Object { - "fullWidth": true, - }, - "name": "Status", - "render": [Function], - }, - Object { - "align": "left", - "field": "state.monitor.name", - "mobileOptions": Object { - "fullWidth": true, - }, - "name": "Name", - "render": [Function], - "sortable": true, - }, - Object { - "align": "left", - "field": "state.url.full", - "name": "Url", - "render": [Function], - }, - Object { - "align": "center", - "field": "histogram.points", - "mobileOptions": Object { - "show": false, - }, - "name": "Downtime history", - "render": [Function], - }, - Object { - "align": "right", - "field": "monitor_id", - "isExpander": true, - "name": "", - "render": [Function], - "sortable": true, - "width": "24px", - }, - ] - } - hasActions={true} - isExpandable={true} - itemId="monitor_id" - itemIdToExpandedRowMap={Object {}} - items={ - Array [ - Object { - "monitor_id": "foo", - "state": Object { - "checks": Array [ - Object { - "monitor": Object { - "ip": "127.0.0.1", - "status": "up", - }, - "timestamp": "124", - }, - Object { - "monitor": Object { - "ip": "127.0.0.2", - "status": "down", - }, - "timestamp": "125", - }, - Object { - "monitor": Object { - "ip": "127.0.0.3", - "status": "down", - }, - "timestamp": "126", - }, - ], - "summary": Object { - "down": 2, - "up": 1, - }, - "timestamp": "123", - }, - }, - Object { - "monitor_id": "bar", - "state": Object { - "checks": Array [ - Object { - "monitor": Object { - "ip": "127.0.0.1", - "status": "up", - }, - "timestamp": "125", - }, - Object { - "monitor": Object { - "ip": "127.0.0.2", - "status": "up", - }, - "timestamp": "126", - }, - ], - "summary": Object { - "down": 0, - "up": 2, - }, - "timestamp": "125", - }, - }, - ] - } - loading={false} - noItemsMessage="No uptime monitors found" - responsive={true} - tableLayout="fixed" - /> - <EuiSpacer - size="m" - /> - <EuiFlexGroup - justifyContent="spaceBetween" - responsive={false} - > - <EuiFlexItem - grow={false} - > - <MonitorListPageSizeSelect - setSize={[MockFunction]} - size={25} - /> - </EuiFlexItem> - <EuiFlexItem - grow={false} - > - <EuiFlexGroup - responsive={false} - > - <EuiFlexItem - grow={false} - > - <OverviewPageLink - dataTestSubj="xpack.uptime.monitorList.prevButton" - direction="prev" - pagination="{\\"cursorKey\\":{\\"monitor_id\\":123},\\"cursorDirection\\":\\"BEFORE\\",\\"sortOrder\\":\\"ASC\\"}" - /> - </EuiFlexItem> - <EuiFlexItem - grow={false} - > - <OverviewPageLink - dataTestSubj="xpack.uptime.monitorList.nextButton" - direction="next" - pagination="{\\"cursorKey\\":{\\"monitor_id\\":456},\\"cursorDirection\\":\\"AFTER\\",\\"sortOrder\\":\\"ASC\\"}" - /> - </EuiFlexItem> - </EuiFlexGroup> - </EuiFlexItem> - </EuiFlexGroup> - </EuiPanel> -</Fragment> -`; diff --git a/x-pack/legacy/plugins/uptime/public/components/functional/monitor_list/__tests__/monitor_list.test.tsx b/x-pack/legacy/plugins/uptime/public/components/functional/monitor_list/__tests__/monitor_list.test.tsx deleted file mode 100644 index d2030155d0092..0000000000000 --- a/x-pack/legacy/plugins/uptime/public/components/functional/monitor_list/__tests__/monitor_list.test.tsx +++ /dev/null @@ -1,129 +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 { shallowWithIntl } from 'test_utils/enzyme_helpers'; -import React from 'react'; -import { MonitorSummaryResult } from '../../../../../common/graphql/types'; -import { MonitorListComponent } from '../monitor_list'; -import { renderWithRouter } from '../../../../lib'; - -describe('MonitorList component', () => { - let result: MonitorSummaryResult; - - beforeEach(() => { - result = { - summaries: [ - { - monitor_id: 'foo', - state: { - checks: [ - { - monitor: { - ip: '127.0.0.1', - status: 'up', - }, - timestamp: '124', - }, - { - monitor: { - ip: '127.0.0.2', - status: 'down', - }, - timestamp: '125', - }, - { - monitor: { - ip: '127.0.0.3', - status: 'down', - }, - timestamp: '126', - }, - ], - summary: { - up: 1, - down: 2, - }, - timestamp: '123', - }, - }, - { - monitor_id: 'bar', - state: { - checks: [ - { - monitor: { - ip: '127.0.0.1', - status: 'up', - }, - timestamp: '125', - }, - { - monitor: { - ip: '127.0.0.2', - status: 'up', - }, - timestamp: '126', - }, - ], - summary: { - up: 2, - down: 0, - }, - timestamp: '125', - }, - }, - ], - totalSummaryCount: 2, - }; - }); - - it('shallow renders the monitor list', () => { - const component = shallowWithIntl( - <MonitorListComponent - dangerColor="danger" - data={{ monitorStates: result }} - hasActiveFilters={false} - loading={false} - pageSize={25} - setPageSize={jest.fn()} - successColor="primary" - /> - ); - - expect(component).toMatchSnapshot(); - }); - - it('renders a no items message when no data is provided', () => { - const component = shallowWithIntl( - <MonitorListComponent - dangerColor="danger" - data={{}} - hasActiveFilters={false} - loading={false} - pageSize={25} - setPageSize={jest.fn()} - successColor="primary" - /> - ); - expect(component).toMatchSnapshot(); - }); - - it('renders the monitor list', () => { - const component = renderWithRouter( - <MonitorListComponent - dangerColor="danger" - data={{ monitorStates: result }} - hasActiveFilters={false} - loading={false} - pageSize={25} - setPageSize={jest.fn()} - successColor="primary" - /> - ); - - expect(component).toMatchSnapshot(); - }); -}); diff --git a/x-pack/legacy/plugins/uptime/public/components/functional/monitor_list/__tests__/monitor_list_pagination.test.tsx b/x-pack/legacy/plugins/uptime/public/components/functional/monitor_list/__tests__/monitor_list_pagination.test.tsx deleted file mode 100644 index b08b8b3fabc3e..0000000000000 --- a/x-pack/legacy/plugins/uptime/public/components/functional/monitor_list/__tests__/monitor_list_pagination.test.tsx +++ /dev/null @@ -1,126 +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 { shallowWithIntl } from 'test_utils/enzyme_helpers'; -import React from 'react'; -import { - CursorDirection, - MonitorSummaryResult, - SortOrder, -} from '../../../../../common/graphql/types'; -import { MonitorListComponent } from '../monitor_list'; - -describe('MonitorListPagination component', () => { - let result: MonitorSummaryResult; - - beforeEach(() => { - result = { - prevPagePagination: JSON.stringify({ - cursorKey: { monitor_id: 123 }, - cursorDirection: CursorDirection.BEFORE, - sortOrder: SortOrder.ASC, - }), - nextPagePagination: JSON.stringify({ - cursorKey: { monitor_id: 456 }, - cursorDirection: CursorDirection.AFTER, - sortOrder: SortOrder.ASC, - }), - summaries: [ - { - monitor_id: 'foo', - state: { - checks: [ - { - monitor: { - ip: '127.0.0.1', - status: 'up', - }, - timestamp: '124', - }, - { - monitor: { - ip: '127.0.0.2', - status: 'down', - }, - timestamp: '125', - }, - { - monitor: { - ip: '127.0.0.3', - status: 'down', - }, - timestamp: '126', - }, - ], - summary: { - up: 1, - down: 2, - }, - timestamp: '123', - }, - }, - { - monitor_id: 'bar', - state: { - checks: [ - { - monitor: { - ip: '127.0.0.1', - status: 'up', - }, - timestamp: '125', - }, - { - monitor: { - ip: '127.0.0.2', - status: 'up', - }, - timestamp: '126', - }, - ], - summary: { - up: 2, - down: 0, - }, - timestamp: '125', - }, - }, - ], - totalSummaryCount: 2, - }; - }); - - it('renders the monitor list', () => { - const component = shallowWithIntl( - <MonitorListComponent - dangerColor="danger" - data={{ monitorStates: result }} - loading={false} - pageSize={25} - setPageSize={jest.fn()} - successColor="primary" - hasActiveFilters={false} - /> - ); - - expect(component).toMatchSnapshot(); - }); - - it('renders a no items message when no data is provided', () => { - const component = shallowWithIntl( - <MonitorListComponent - dangerColor="danger" - data={{}} - loading={false} - successColor="primary" - pageSize={25} - setPageSize={jest.fn()} - hasActiveFilters={false} - /> - ); - expect(component).toMatchSnapshot(); - }); -}); diff --git a/x-pack/legacy/plugins/uptime/public/components/functional/monitor_list/index.ts b/x-pack/legacy/plugins/uptime/public/components/functional/monitor_list/index.ts deleted file mode 100644 index a83330a7a3a0b..0000000000000 --- a/x-pack/legacy/plugins/uptime/public/components/functional/monitor_list/index.ts +++ /dev/null @@ -1,9 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -export { MonitorList } from './monitor_list'; -export { Criteria, Pagination } from './types'; -export { LocationLink } from './monitor_list_drawer'; diff --git a/x-pack/legacy/plugins/uptime/public/components/functional/monitor_list/monitor_list.tsx b/x-pack/legacy/plugins/uptime/public/components/functional/monitor_list/monitor_list.tsx deleted file mode 100644 index a9fb1ce2f4be1..0000000000000 --- a/x-pack/legacy/plugins/uptime/public/components/functional/monitor_list/monitor_list.tsx +++ /dev/null @@ -1,222 +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 { - EuiButtonIcon, - EuiBasicTable, - EuiFlexGroup, - EuiFlexItem, - EuiIcon, - EuiLink, - EuiPanel, - EuiSpacer, - EuiTitle, -} from '@elastic/eui'; -import { FormattedMessage } from '@kbn/i18n/react'; -import React, { useState } from 'react'; -import styled from 'styled-components'; -import { withUptimeGraphQL, UptimeGraphQLQueryProps } from '../../higher_order'; -import { monitorStatesQuery } from '../../../queries/monitor_states_query'; -import { - MonitorSummary, - MonitorSummaryResult, - SummaryHistogramPoint, -} from '../../../../common/graphql/types'; -import { MonitorListStatusColumn } from './monitor_list_status_column'; -import { formatUptimeGraphQLErrorList } from '../../../lib/helper/format_error_list'; -import { ExpandedRowMap } from './types'; -import { MonitorBarSeries } from '../charts'; -import { MonitorPageLink } from './monitor_page_link'; -import { OverviewPageLink } from './overview_page_link'; -import * as labels from './translations'; -import { MonitorListDrawer } from '../../connected'; -import { MonitorListPageSizeSelect } from './monitor_list_page_size_select'; - -interface MonitorListQueryResult { - monitorStates?: MonitorSummaryResult; -} - -interface MonitorListProps { - dangerColor: string; - hasActiveFilters: boolean; - successColor: string; - linkParameters?: string; - pageSize: number; - setPageSize: (size: number) => void; -} - -type Props = UptimeGraphQLQueryProps<MonitorListQueryResult> & MonitorListProps; - -const TruncatedEuiLink = styled(EuiLink)` - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; -`; - -export const MonitorListComponent = (props: Props) => { - const { dangerColor, data, errors, hasActiveFilters, linkParameters, loading } = props; - const [drawerIds, updateDrawerIds] = useState<string[]>([]); - - const items = data?.monitorStates?.summaries ?? []; - - const nextPagePagination = data?.monitorStates?.nextPagePagination ?? ''; - const prevPagePagination = data?.monitorStates?.prevPagePagination ?? ''; - - const getExpandedRowMap = () => { - return drawerIds.reduce((map: ExpandedRowMap, id: string) => { - return { - ...map, - [id]: ( - <MonitorListDrawer - summary={items.find(({ monitor_id: monitorId }) => monitorId === id)} - /> - ), - }; - }, {}); - }; - - const columns = [ - { - align: 'left' as const, - field: 'state.monitor.status', - name: labels.STATUS_COLUMN_LABEL, - mobileOptions: { - fullWidth: true, - }, - render: (status: string, { state: { timestamp, checks } }: MonitorSummary) => { - return ( - <MonitorListStatusColumn status={status} timestamp={timestamp} checks={checks ?? []} /> - ); - }, - }, - { - align: 'left' as const, - field: 'state.monitor.name', - name: labels.NAME_COLUMN_LABEL, - mobileOptions: { - fullWidth: true, - }, - render: (name: string, summary: MonitorSummary) => ( - <MonitorPageLink monitorId={summary.monitor_id} linkParameters={linkParameters}> - {name ? name : `Unnamed - ${summary.monitor_id}`} - </MonitorPageLink> - ), - sortable: true, - }, - { - align: 'left' as const, - field: 'state.url.full', - name: labels.URL, - render: (url: string, summary: MonitorSummary) => ( - <TruncatedEuiLink href={url} target="_blank" color="text"> - {url} <EuiIcon size="s" type="popout" color="subbdued" /> - </TruncatedEuiLink> - ), - }, - { - align: 'center' as const, - field: 'histogram.points', - name: labels.HISTORY_COLUMN_LABEL, - mobileOptions: { - show: false, - }, - render: (histogramSeries: SummaryHistogramPoint[] | null) => ( - <MonitorBarSeries dangerColor={dangerColor} histogramSeries={histogramSeries} /> - ), - }, - { - align: 'right' as const, - field: 'monitor_id', - name: '', - sortable: true, - isExpander: true, - width: '24px', - render: (id: string) => { - return ( - <EuiButtonIcon - aria-label={labels.getExpandDrawerLabel(id)} - iconType={drawerIds.includes(id) ? 'arrowUp' : 'arrowDown'} - onClick={() => { - if (drawerIds.includes(id)) { - updateDrawerIds(drawerIds.filter(p => p !== id)); - } else { - updateDrawerIds([...drawerIds, id]); - } - }} - /> - ); - }, - }, - ]; - - return ( - <> - <EuiPanel> - <EuiTitle size="xs"> - <h5> - <FormattedMessage - id="xpack.uptime.monitorList.monitoringStatusTitle" - defaultMessage="Monitor status" - /> - </h5> - </EuiTitle> - <EuiSpacer size="s" /> - <EuiBasicTable - aria-label={labels.getDescriptionLabel(items.length)} - error={errors ? formatUptimeGraphQLErrorList(errors) : errors} - // Only set loading to true when there are no items present to prevent the bug outlined in - // in https://github.com/elastic/eui/issues/2393 . Once that is fixed we can simply set the value here to - // loading={loading} - loading={loading && (!items || items.length < 1)} - isExpandable={true} - hasActions={true} - itemId="monitor_id" - itemIdToExpandedRowMap={getExpandedRowMap()} - items={items} - // TODO: not needed without sorting and pagination - // onChange={onChange} - noItemsMessage={ - hasActiveFilters ? labels.NO_MONITOR_ITEM_SELECTED : labels.NO_DATA_MESSAGE - } - // TODO: reintegrate pagination in future release - // pagination={pagination} - // TODO: reintegrate sorting in future release - // sorting={sorting} - columns={columns} - /> - <EuiSpacer size="m" /> - <EuiFlexGroup justifyContent="spaceBetween" responsive={false}> - <EuiFlexItem grow={false}> - <MonitorListPageSizeSelect size={props.pageSize} setSize={props.setPageSize} /> - </EuiFlexItem> - <EuiFlexItem grow={false}> - <EuiFlexGroup responsive={false}> - <EuiFlexItem grow={false}> - <OverviewPageLink - dataTestSubj="xpack.uptime.monitorList.prevButton" - direction="prev" - pagination={prevPagePagination} - /> - </EuiFlexItem> - <EuiFlexItem grow={false}> - <OverviewPageLink - dataTestSubj="xpack.uptime.monitorList.nextButton" - direction="next" - pagination={nextPagePagination} - /> - </EuiFlexItem> - </EuiFlexGroup> - </EuiFlexItem> - </EuiFlexGroup> - </EuiPanel> - </> - ); -}; - -export const MonitorList = withUptimeGraphQL<MonitorListQueryResult, MonitorListProps>( - MonitorListComponent, - monitorStatesQuery -); diff --git a/x-pack/legacy/plugins/uptime/public/components/functional/monitor_list/monitor_list_drawer/__tests__/monitor_status_list.test.tsx b/x-pack/legacy/plugins/uptime/public/components/functional/monitor_list/monitor_list_drawer/__tests__/monitor_status_list.test.tsx deleted file mode 100644 index 8c07d0b1a7d22..0000000000000 --- a/x-pack/legacy/plugins/uptime/public/components/functional/monitor_list/monitor_list_drawer/__tests__/monitor_status_list.test.tsx +++ /dev/null @@ -1,141 +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 { shallowWithIntl } from 'test_utils/enzyme_helpers'; -import React from 'react'; -import moment from 'moment'; -import { MonitorStatusList } from '../monitor_status_list'; -import { Check } from '../../../../../../common/graphql/types'; - -describe('MonitorStatusList component', () => { - let checks: Check[]; - - beforeAll(() => { - moment.prototype.toLocaleString = jest.fn(() => '2019-06-21 15:29:26'); - moment.prototype.from = jest.fn(() => 'a few moments ago'); - }); - - beforeEach(() => { - checks = [ - { - agent: { id: '8f9a37fb-573a-4fdc-9895-440a5b39c250' }, - monitor: { - ip: '151.101.130.217', - name: 'elastic', - status: 'up', - }, - observer: { - geo: { name: null, location: null }, - }, - timestamp: '1570538236414', - }, - { - agent: { id: '8f9a37fb-573a-4fdc-9895-440a5b39c250' }, - monitor: { - ip: '151.101.194.217', - name: 'elastic', - status: 'up', - }, - observer: { - geo: { name: null, location: null }, - }, - timestamp: '1570538236414', - }, - { - agent: { id: '8f9a37fb-573a-4fdc-9895-440a5b39c250' }, - monitor: { - ip: '151.101.2.217', - name: 'elastic', - status: 'up', - }, - observer: { - geo: { name: null, location: null }, - }, - timestamp: '1570538236414', - }, - { - agent: { id: '8f9a37fb-573a-4fdc-9895-440a5b39c250' }, - container: null, - kubernetes: null, - monitor: { - ip: '151.101.66.217', - name: 'elastic', - status: 'up', - }, - observer: { - geo: { name: null, location: null }, - }, - timestamp: '1570538236414', - }, - { - agent: { id: '8f9a37fb-573a-4fdc-9895-440a5b39c250' }, - container: null, - kubernetes: null, - monitor: { - ip: '2a04:4e42:200::729', - name: 'elastic', - status: 'down', - }, - observer: { - geo: { name: null, location: null }, - }, - timestamp: '1570538236414', - }, - { - agent: { id: '8f9a37fb-573a-4fdc-9895-440a5b39c250' }, - container: null, - kubernetes: null, - monitor: { - ip: '2a04:4e42:400::729', - name: 'elastic', - status: 'down', - }, - observer: { - geo: { name: null, location: null }, - }, - timestamp: '1570538236414', - }, - { - agent: { id: '8f9a37fb-573a-4fdc-9895-440a5b39c250' }, - container: null, - kubernetes: null, - monitor: { - ip: '2a04:4e42:600::729', - name: 'elastic', - status: 'down', - }, - observer: { - geo: { name: null, location: null }, - }, - timestamp: '1570538236414', - }, - { - agent: { id: '8f9a37fb-573a-4fdc-9895-440a5b39c250' }, - container: null, - kubernetes: null, - monitor: { - ip: '2a04:4e42::729', - name: 'elastic', - status: 'down', - }, - observer: { - geo: { name: null, location: null }, - }, - timestamp: '1570538236414', - }, - ]; - }); - - it('renders checks', () => { - const component = shallowWithIntl(<MonitorStatusList checks={checks} />); - expect(component).toMatchSnapshot(); - }); - - it('renders null in place of child status with missing ip', () => { - const component = shallowWithIntl(<MonitorStatusList checks={checks} />); - expect(component).toMatchSnapshot(); - }); -}); diff --git a/x-pack/legacy/plugins/uptime/public/components/functional/monitor_list/monitor_list_drawer/index.ts b/x-pack/legacy/plugins/uptime/public/components/functional/monitor_list/monitor_list_drawer/index.ts deleted file mode 100644 index 2933a71c2240b..0000000000000 --- a/x-pack/legacy/plugins/uptime/public/components/functional/monitor_list/monitor_list_drawer/index.ts +++ /dev/null @@ -1,8 +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. - */ - -export { LocationLink } from './location_link'; -export { MonitorListActionsPopoverComponent } from './monitor_list_actions_popover'; diff --git a/x-pack/legacy/plugins/uptime/public/components/functional/monitor_list/monitor_list_drawer/monitor_list_actions_popover.tsx b/x-pack/legacy/plugins/uptime/public/components/functional/monitor_list/monitor_list_drawer/monitor_list_actions_popover.tsx deleted file mode 100644 index 6b946baa8d403..0000000000000 --- a/x-pack/legacy/plugins/uptime/public/components/functional/monitor_list/monitor_list_drawer/monitor_list_actions_popover.tsx +++ /dev/null @@ -1,58 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { get } from 'lodash'; -import { i18n } from '@kbn/i18n'; -import React from 'react'; -import { EuiPopover, EuiButton } from '@elastic/eui'; -import { IntegrationGroup } from './integration_group'; -import { MonitorSummary } from '../../../../../common/graphql/types'; -import { toggleIntegrationsPopover, PopoverState } from '../../../../state/actions'; - -interface MonitorListActionsPopoverProps { - summary: MonitorSummary; - popoverState: PopoverState | null; - togglePopoverIsVisible: typeof toggleIntegrationsPopover; -} - -export const MonitorListActionsPopoverComponent = ({ - summary, - popoverState, - togglePopoverIsVisible, -}: MonitorListActionsPopoverProps) => { - const popoverId = `${summary.monitor_id}_popover`; - - const monitorUrl: string | undefined = get(summary, 'state.url.full', undefined); - const isPopoverOpen: boolean = - !!popoverState && popoverState.open && popoverState.id === popoverId; - return ( - <EuiPopover - button={ - <EuiButton - aria-label={i18n.translate( - 'xpack.uptime.monitorList.observabilityIntegrationsColumn.popoverIconButton.ariaLabel', - { - defaultMessage: 'Opens integrations popover for monitor with url {monitorUrl}', - description: - 'A message explaining that this button opens a popover with links to other apps for a given monitor', - values: { monitorUrl }, - } - )} - onClick={() => togglePopoverIsVisible({ id: popoverId, open: true })} - iconType="arrowDown" - iconSide="right" - > - Integrations - </EuiButton> - } - closePopover={() => togglePopoverIsVisible({ id: popoverId, open: false })} - id={popoverId} - isOpen={isPopoverOpen} - > - <IntegrationGroup summary={summary} /> - </EuiPopover> - ); -}; diff --git a/x-pack/legacy/plugins/uptime/public/components/functional/monitor_list/types.ts b/x-pack/legacy/plugins/uptime/public/components/functional/monitor_list/types.ts deleted file mode 100644 index a25603d3603d9..0000000000000 --- a/x-pack/legacy/plugins/uptime/public/components/functional/monitor_list/types.ts +++ /dev/null @@ -1,42 +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. - */ - -export interface CondensedCheck { - childStatuses: CondensedCheckStatus[]; - location: string | null; - status: string; - timestamp: string; -} - -export interface CondensedCheckStatus { - ip?: string | null; - status: string; - timestamp: string; -} - -export interface Criteria { - page?: { - index: number; - size: number; - }; - sort?: { - field: string; - direction: 'asc' | 'desc'; - }; -} - -export interface ExpandedRowMap { - [key: string]: JSX.Element; -} - -export interface Pagination { - hidePerPageOptions?: boolean; - initialPageSize: number; - pageIndex: number; - pageSize: number; - pageSizeOptions: number[]; - totalItemCount: number; -} diff --git a/x-pack/legacy/plugins/uptime/public/components/functional/monitor_status_details/index.ts b/x-pack/legacy/plugins/uptime/public/components/functional/monitor_status_details/index.ts deleted file mode 100644 index 385788cc825a0..0000000000000 --- a/x-pack/legacy/plugins/uptime/public/components/functional/monitor_status_details/index.ts +++ /dev/null @@ -1,9 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -export { MonitorStatusBarComponent } from './monitor_status_bar'; -export { MonitorStatusDetailsComponent } from './monitor_status_details'; -export { StatusByLocations } from './monitor_status_bar/status_by_location'; diff --git a/x-pack/legacy/plugins/uptime/public/components/functional/monitor_status_details/monitor_status_bar/index.ts b/x-pack/legacy/plugins/uptime/public/components/functional/monitor_status_details/monitor_status_bar/index.ts deleted file mode 100644 index 0cb11587eee48..0000000000000 --- a/x-pack/legacy/plugins/uptime/public/components/functional/monitor_status_details/monitor_status_bar/index.ts +++ /dev/null @@ -1,8 +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. - */ - -export { MonitorSSLCertificate } from './monitor_ssl_certificate'; -export { MonitorStatusBarComponent } from './monitor_status_bar'; diff --git a/x-pack/legacy/plugins/uptime/public/components/functional/monitor_status_details/monitor_status_bar/monitor_ssl_certificate.tsx b/x-pack/legacy/plugins/uptime/public/components/functional/monitor_status_details/monitor_status_bar/monitor_ssl_certificate.tsx deleted file mode 100644 index c57348c4ab4cd..0000000000000 --- a/x-pack/legacy/plugins/uptime/public/components/functional/monitor_status_details/monitor_status_bar/monitor_ssl_certificate.tsx +++ /dev/null @@ -1,59 +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 from 'react'; -import moment from 'moment'; -import { EuiSpacer, EuiText, EuiBadge } from '@elastic/eui'; -import { FormattedMessage } from '@kbn/i18n/react'; -import { i18n } from '@kbn/i18n'; - -import { PingTls } from '../../../../../common/graphql/types'; - -interface Props { - /** - * TLS information coming from monitor in ES heartbeat index - */ - tls: PingTls | null | undefined; -} - -export const MonitorSSLCertificate = ({ tls }: Props) => { - const certValidityDate = new Date(tls?.certificate_not_valid_after ?? ''); - - const isValidDate = !isNaN(certValidityDate.valueOf()); - - const dateIn30Days = moment().add('30', 'days'); - - const isExpiringInMonth = isValidDate && dateIn30Days > moment(certValidityDate); - - return isValidDate ? ( - <> - <EuiSpacer size="s" /> - <EuiText - grow={false} - size="s" - aria-label={i18n.translate( - 'xpack.uptime.monitorStatusBar.sslCertificateExpiry.label.ariaLabel', - { - defaultMessage: 'SSL certificate expires {validityDate}', - values: { validityDate: moment(certValidityDate).fromNow() }, - } - )} - > - <FormattedMessage - id="xpack.uptime.monitorStatusBar.sslCertificateExpiry.badgeContent" - defaultMessage="SSL certificate expires {emphasizedText}" - values={{ - emphasizedText: ( - <EuiBadge color={isExpiringInMonth ? 'warning' : 'default'}> - {moment(certValidityDate).fromNow()} - </EuiBadge> - ), - }} - /> - </EuiText> - </> - ) : null; -}; diff --git a/x-pack/legacy/plugins/uptime/public/components/functional/monitor_status_details/monitor_status_bar/monitor_status_bar.tsx b/x-pack/legacy/plugins/uptime/public/components/functional/monitor_status_details/monitor_status_bar/monitor_status_bar.tsx deleted file mode 100644 index 22e4377944be1..0000000000000 --- a/x-pack/legacy/plugins/uptime/public/components/functional/monitor_status_details/monitor_status_bar/monitor_status_bar.tsx +++ /dev/null @@ -1,61 +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 from 'react'; -import { - EuiLink, - EuiTitle, - EuiTextColor, - EuiSpacer, - EuiText, - EuiFlexGroup, - EuiFlexItem, -} from '@elastic/eui'; -import { MonitorSSLCertificate } from './monitor_ssl_certificate'; -import * as labels from './translations'; -import { StatusByLocations } from './status_by_location'; -import { Ping } from '../../../../../common/graphql/types'; -import { MonitorLocations } from '../../../../../common/runtime_types'; - -interface MonitorStatusBarProps { - monitorId: string; - monitorStatus: Ping; - monitorLocations: MonitorLocations; -} - -export const MonitorStatusBarComponent: React.FC<MonitorStatusBarProps> = ({ - monitorId, - monitorStatus, - monitorLocations, -}) => { - const full = monitorStatus?.url?.full ?? ''; - - return ( - <EuiFlexGroup direction="column" gutterSize="none" responsive={false}> - <EuiFlexItem grow={false}> - <StatusByLocations locations={monitorLocations?.locations ?? []} /> - </EuiFlexItem> - <EuiFlexItem grow={false}> - <EuiText> - <EuiLink aria-label={labels.monitorUrlLinkAriaLabel} href={full} target="_blank"> - {full} - </EuiLink> - </EuiText> - </EuiFlexItem> - <EuiFlexItem> - <EuiTitle size="xs"> - <EuiTextColor color="subdued"> - <h1 data-test-subj="monitor-page-title">{monitorId}</h1> - </EuiTextColor> - </EuiTitle> - </EuiFlexItem> - <EuiSpacer /> - <EuiFlexItem grow={false}> - <MonitorSSLCertificate tls={monitorStatus?.tls} /> - </EuiFlexItem> - </EuiFlexGroup> - ); -}; diff --git a/x-pack/legacy/plugins/uptime/public/components/functional/monitor_status_details/monitor_status_bar/translations.ts b/x-pack/legacy/plugins/uptime/public/components/functional/monitor_status_details/monitor_status_bar/translations.ts deleted file mode 100644 index 1c2844f4f6ccf..0000000000000 --- a/x-pack/legacy/plugins/uptime/public/components/functional/monitor_status_details/monitor_status_bar/translations.ts +++ /dev/null @@ -1,49 +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 { i18n } from '@kbn/i18n'; - -export const healthStatusMessageAriaLabel = i18n.translate( - 'xpack.uptime.monitorStatusBar.healthStatusMessageAriaLabel', - { - defaultMessage: 'Monitor status', - } -); - -export const upLabel = i18n.translate('xpack.uptime.monitorStatusBar.healthStatusMessage.upLabel', { - defaultMessage: 'Up', -}); - -export const downLabel = i18n.translate( - 'xpack.uptime.monitorStatusBar.healthStatusMessage.downLabel', - { - defaultMessage: 'Down', - } -); - -export const monitorUrlLinkAriaLabel = i18n.translate( - 'xpack.uptime.monitorStatusBar.monitorUrlLinkAriaLabel', - { - defaultMessage: 'Monitor URL link', - } -); - -export const durationTextAriaLabel = i18n.translate( - 'xpack.uptime.monitorStatusBar.durationTextAriaLabel', - { - defaultMessage: 'Monitor duration in milliseconds', - } -); - -export const timestampFromNowTextAriaLabel = i18n.translate( - 'xpack.uptime.monitorStatusBar.timestampFromNowTextAriaLabel', - { - defaultMessage: 'Time since last check', - } -); - -export const loadingMessage = i18n.translate('xpack.uptime.monitorStatusBar.loadingMessage', { - defaultMessage: 'Loading…', -}); diff --git a/x-pack/legacy/plugins/uptime/public/components/functional/monitor_status_details/monitor_status_details.tsx b/x-pack/legacy/plugins/uptime/public/components/functional/monitor_status_details/monitor_status_details.tsx deleted file mode 100644 index 7dea73da7bba0..0000000000000 --- a/x-pack/legacy/plugins/uptime/public/components/functional/monitor_status_details/monitor_status_details.tsx +++ /dev/null @@ -1,64 +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, { useContext, useEffect, useState } from 'react'; -import { EuiFlexGroup, EuiFlexItem, EuiPanel } from '@elastic/eui'; -import styled from 'styled-components'; -import { LocationMap } from '../location_map'; -import { UptimeRefreshContext } from '../../../contexts'; -import { MonitorLocations } from '../../../../common/runtime_types'; -import { MonitorStatusBar } from '../../connected'; - -interface MonitorStatusDetailsProps { - monitorId: string; - monitorLocations: MonitorLocations; -} - -const WrapFlexItem = styled(EuiFlexItem)` - @media (max-width: 1150px) { - width: 100%; - } -`; - -export const MonitorStatusDetailsComponent = ({ - monitorId, - monitorLocations, -}: MonitorStatusDetailsProps) => { - const { refreshApp } = useContext(UptimeRefreshContext); - - const [isTabActive] = useState(document.visibilityState); - const onTabActive = () => { - if (document.visibilityState === 'visible' && isTabActive === 'hidden') { - refreshApp(); - } - }; - - // Refreshing application state after Tab becomes active to render latest map state - // If application renders in when tab is not in focus it gives some unexpected behaviors - // Where map is not visible on change - useEffect(() => { - document.addEventListener('visibilitychange', onTabActive); - return () => { - document.removeEventListener('visibilitychange', onTabActive); - }; - - // we want this effect to execute exactly once after the component mounts - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); - - return ( - <EuiPanel> - <EuiFlexGroup gutterSize="l" wrap responsive={true}> - <EuiFlexItem grow={true}> - <MonitorStatusBar monitorId={monitorId} /> - </EuiFlexItem> - <WrapFlexItem grow={false}> - <LocationMap monitorLocations={monitorLocations} /> - </WrapFlexItem> - </EuiFlexGroup> - </EuiPanel> - ); -}; diff --git a/x-pack/legacy/plugins/uptime/public/components/functional/monitor_status_details/translations.ts b/x-pack/legacy/plugins/uptime/public/components/functional/monitor_status_details/translations.ts deleted file mode 100644 index 1c2844f4f6ccf..0000000000000 --- a/x-pack/legacy/plugins/uptime/public/components/functional/monitor_status_details/translations.ts +++ /dev/null @@ -1,49 +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 { i18n } from '@kbn/i18n'; - -export const healthStatusMessageAriaLabel = i18n.translate( - 'xpack.uptime.monitorStatusBar.healthStatusMessageAriaLabel', - { - defaultMessage: 'Monitor status', - } -); - -export const upLabel = i18n.translate('xpack.uptime.monitorStatusBar.healthStatusMessage.upLabel', { - defaultMessage: 'Up', -}); - -export const downLabel = i18n.translate( - 'xpack.uptime.monitorStatusBar.healthStatusMessage.downLabel', - { - defaultMessage: 'Down', - } -); - -export const monitorUrlLinkAriaLabel = i18n.translate( - 'xpack.uptime.monitorStatusBar.monitorUrlLinkAriaLabel', - { - defaultMessage: 'Monitor URL link', - } -); - -export const durationTextAriaLabel = i18n.translate( - 'xpack.uptime.monitorStatusBar.durationTextAriaLabel', - { - defaultMessage: 'Monitor duration in milliseconds', - } -); - -export const timestampFromNowTextAriaLabel = i18n.translate( - 'xpack.uptime.monitorStatusBar.timestampFromNowTextAriaLabel', - { - defaultMessage: 'Time since last check', - } -); - -export const loadingMessage = i18n.translate('xpack.uptime.monitorStatusBar.loadingMessage', { - defaultMessage: 'Loading…', -}); diff --git a/x-pack/legacy/plugins/uptime/public/components/functional/overview_page_parsing_error_callout.tsx b/x-pack/legacy/plugins/uptime/public/components/functional/overview_page_parsing_error_callout.tsx deleted file mode 100644 index b71a4f2f8646a..0000000000000 --- a/x-pack/legacy/plugins/uptime/public/components/functional/overview_page_parsing_error_callout.tsx +++ /dev/null @@ -1,49 +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 { EuiCallOut, EuiCodeBlock } from '@elastic/eui'; -import { i18n } from '@kbn/i18n'; -import { FormattedMessage } from '@kbn/i18n/react'; -import React from 'react'; - -interface HasMessage { - message: string; -} - -interface OverviewPageParsingErrorCalloutProps { - error: HasMessage; -} - -export const OverviewPageParsingErrorCallout = ({ - error, -}: OverviewPageParsingErrorCalloutProps) => ( - <EuiCallOut - title={i18n.translate('xpack.uptime.overviewPageParsingErrorCallout.title', { - defaultMessage: 'Parsing error', - })} - color="danger" - iconType="alert" - style={{ width: '100%' }} - > - <p> - <FormattedMessage - id="xpack.uptime.overviewPageParsingErrorCallout.content" - defaultMessage="There was an error parsing the filter query. {content}" - values={{ - content: ( - <EuiCodeBlock> - {error.message - ? error.message - : i18n.translate('xpack.uptime.overviewPageParsingErrorCallout.noMessage', { - defaultMessage: 'There was no error message', - })} - </EuiCodeBlock> - ), - }} - /> - </p> - </EuiCallOut> -); diff --git a/x-pack/legacy/plugins/uptime/public/components/functional/ping_list/__tests__/__snapshots__/ping_list.test.tsx.snap b/x-pack/legacy/plugins/uptime/public/components/functional/ping_list/__tests__/__snapshots__/ping_list.test.tsx.snap deleted file mode 100644 index 2e59ec5e57337..0000000000000 --- a/x-pack/legacy/plugins/uptime/public/components/functional/ping_list/__tests__/__snapshots__/ping_list.test.tsx.snap +++ /dev/null @@ -1,384 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`PingList component renders sorted list without errors 1`] = ` -<Fragment> - <EuiPanel> - <EuiTitle - size="xs" - > - <h4> - <FormattedMessage - defaultMessage="History" - id="xpack.uptime.pingList.checkHistoryTitle" - values={Object {}} - /> - </h4> - </EuiTitle> - <EuiSpacer - size="s" - /> - <EuiFlexGroup - justifyContent="spaceBetween" - > - <EuiFlexItem - grow={false} - > - <EuiFlexGroup> - <EuiFlexItem - style={ - Object { - "minWidth": 200, - } - } - > - <EuiFlexGroup> - <EuiFlexItem> - <EuiFormRow - aria-label="Status" - describedByIds={Array []} - display="row" - fullWidth={false} - hasChildLabel={true} - hasEmptyLabelSpace={false} - label="Status" - labelType="label" - > - <EuiSelect - aria-label="Status" - onChange={[Function]} - options={ - Array [ - Object { - "text": "All", - "value": "", - }, - Object { - "text": "Up", - "value": "up", - }, - Object { - "text": "Down", - "value": "down", - }, - ] - } - value="down" - /> - </EuiFormRow> - </EuiFlexItem> - <EuiFlexItem> - <EuiFormRow - aria-label="Location" - describedByIds={Array []} - display="row" - fullWidth={false} - hasChildLabel={true} - hasEmptyLabelSpace={false} - label="Location" - labelType="label" - > - <EuiSelect - aria-label="Location" - onChange={[Function]} - options={ - Array [ - Object { - "text": "All", - "value": "", - }, - Object { - "text": "nyc", - "value": "nyc", - }, - ] - } - value="" - /> - </EuiFormRow> - </EuiFlexItem> - </EuiFlexGroup> - </EuiFlexItem> - </EuiFlexGroup> - </EuiFlexItem> - </EuiFlexGroup> - <EuiSpacer - size="s" - /> - <EuiBasicTable - columns={ - Array [ - Object { - "field": "monitor.status", - "name": "Status", - "render": [Function], - }, - Object { - "align": "left", - "field": "observer.geo.name", - "name": "Location", - "render": [Function], - }, - Object { - "align": "right", - "dataType": "number", - "field": "monitor.ip", - "name": "IP", - }, - Object { - "align": "right", - "field": "monitor.duration.us", - "name": "Duration", - "render": [Function], - }, - Object { - "align": "right", - "field": "error.type", - "name": "Error type", - "render": [Function], - }, - Object { - "align": "right", - "field": "http.response.status_code", - "name": <ForwardRef(styled.span)> - Response code - </ForwardRef(styled.span)>, - "render": [Function], - }, - Object { - "align": "right", - "isExpander": true, - "render": [Function], - "width": "24px", - }, - ] - } - hasActions={true} - isExpandable={true} - itemId="id" - itemIdToExpandedRowMap={Object {}} - items={ - Array [ - Object { - "error": Object { - "message": "dial tcp 127.0.0.1:9200: connect: connection refused", - "type": "io", - }, - "http": null, - "id": "id1", - "monitor": Object { - "duration": Object { - "us": 1430, - }, - "id": "auto-tcp-0X81440A68E839814C", - "ip": "127.0.0.1", - "name": "", - "scheme": null, - "status": "down", - "type": "tcp", - }, - "timestamp": "2019-01-28T17:47:08.078Z", - }, - Object { - "error": Object { - "message": "dial tcp 127.0.0.1:9200: connect: connection refused", - "type": "io", - }, - "http": null, - "id": "id2", - "monitor": Object { - "duration": Object { - "us": 1370, - }, - "id": "auto-tcp-0X81440A68E839814C", - "ip": "127.0.0.1", - "name": "", - "scheme": null, - "status": "down", - "type": "tcp", - }, - "timestamp": "2019-01-28T17:47:09.075Z", - }, - Object { - "error": null, - "http": null, - "id": "id3", - "monitor": Object { - "duration": Object { - "us": 1452, - }, - "id": "auto-tcp-0X81440A68E839814C", - "ip": "127.0.0.1", - "name": "", - "scheme": null, - "status": "up", - "type": "tcp", - }, - "timestamp": "2019-01-28T17:47:06.077Z", - }, - Object { - "error": Object { - "message": "dial tcp 127.0.0.1:9200: connect: connection refused", - "type": "io", - }, - "http": null, - "id": "id4", - "monitor": Object { - "duration": Object { - "us": 1094, - }, - "id": "auto-tcp-0X81440A68E839814C", - "ip": "127.0.0.1", - "name": "", - "scheme": null, - "status": "down", - "type": "tcp", - }, - "timestamp": "2019-01-28T17:47:07.075Z", - }, - Object { - "error": Object { - "message": "Get http://localhost:12349/: dial tcp 127.0.0.1:12349: connect: connection refused", - "type": "io", - }, - "http": null, - "id": "id5", - "monitor": Object { - "duration": Object { - "us": 1597, - }, - "id": "auto-http-0X3675F89EF0612091", - "ip": "127.0.0.1", - "name": "", - "scheme": null, - "status": "down", - "type": "http", - }, - "timestamp": "2019-01-28T17:47:07.074Z", - }, - Object { - "error": Object { - "message": "dial tcp 127.0.0.1:9200: connect: connection refused", - "type": "io", - }, - "http": null, - "id": "id6", - "monitor": Object { - "duration": Object { - "us": 1699, - }, - "id": "auto-tcp-0X81440A68E839814C", - "ip": "127.0.0.1", - "name": "", - "scheme": null, - "status": "down", - "type": "tcp", - }, - "timestamp": "2019-01-28T17:47:18.080Z", - }, - Object { - "error": Object { - "message": "dial tcp 127.0.0.1:9200: connect: connection refused", - "type": "io", - }, - "http": null, - "id": "id7", - "monitor": Object { - "duration": Object { - "us": 5384, - }, - "id": "auto-tcp-0X81440A68E839814C", - "ip": "127.0.0.1", - "name": "", - "scheme": null, - "status": "down", - "type": "tcp", - }, - "timestamp": "2019-01-28T17:47:19.076Z", - }, - Object { - "error": Object { - "message": "Get http://localhost:12349/: dial tcp 127.0.0.1:12349: connect: connection refused", - "type": "io", - }, - "http": null, - "id": "id8", - "monitor": Object { - "duration": Object { - "us": 5397, - }, - "id": "auto-http-0X3675F89EF0612091", - "ip": "127.0.0.1", - "name": "", - "scheme": null, - "status": "down", - "type": "http", - }, - "timestamp": "2019-01-28T17:47:19.076Z", - }, - Object { - "error": null, - "http": Object { - "response": Object { - "status_code": 200, - }, - }, - "id": "id9", - "monitor": Object { - "duration": Object { - "us": 127511, - }, - "id": "auto-http-0X131221E73F825974", - "ip": "172.217.7.4", - "name": "", - "scheme": null, - "status": "up", - "type": "http", - }, - "timestamp": "2019-01-28T17:47:19.077Z", - }, - Object { - "error": null, - "http": Object { - "response": Object { - "status_code": 200, - }, - }, - "id": "id10", - "monitor": Object { - "duration": Object { - "us": 287543, - }, - "id": "auto-http-0X9CB71300ABD5A2A8", - "ip": "192.30.253.112", - "name": "", - "scheme": null, - "status": "up", - "type": "http", - }, - "timestamp": "2019-01-28T17:47:19.077Z", - }, - ] - } - loading={false} - noItemsMessage="No items found" - onChange={[Function]} - pagination={ - Object { - "initialPageSize": 25, - "pageIndex": 0, - "pageSize": 10, - "pageSizeOptions": Array [ - 10, - 25, - 50, - 100, - ], - "totalItemCount": 9231, - } - } - responsive={true} - tableLayout="fixed" - /> - </EuiPanel> -</Fragment> -`; diff --git a/x-pack/legacy/plugins/uptime/public/components/functional/ping_list/__tests__/ping_list.test.tsx b/x-pack/legacy/plugins/uptime/public/components/functional/ping_list/__tests__/ping_list.test.tsx deleted file mode 100644 index 68d285bd0baf1..0000000000000 --- a/x-pack/legacy/plugins/uptime/public/components/functional/ping_list/__tests__/ping_list.test.tsx +++ /dev/null @@ -1,251 +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 from 'react'; -import { shallowWithIntl } from 'test_utils/enzyme_helpers'; -import { PingResults, Ping } from '../../../../../common/graphql/types'; -import { PingListComponent, AllLocationOption, toggleDetails } from '../ping_list'; -import { ExpandedRowMap } from '../../monitor_list/types'; - -describe('PingList component', () => { - let pingList: { allPings: PingResults }; - - beforeEach(() => { - pingList = { - allPings: { - total: 9231, - pings: [ - { - id: 'id1', - timestamp: '2019-01-28T17:47:08.078Z', - http: null, - error: { - message: 'dial tcp 127.0.0.1:9200: connect: connection refused', - type: 'io', - }, - monitor: { - duration: { us: 1430 }, - id: 'auto-tcp-0X81440A68E839814C', - ip: '127.0.0.1', - name: '', - scheme: null, - status: 'down', - type: 'tcp', - }, - }, - { - id: 'id2', - timestamp: '2019-01-28T17:47:09.075Z', - http: null, - error: { - message: 'dial tcp 127.0.0.1:9200: connect: connection refused', - type: 'io', - }, - monitor: { - duration: { us: 1370 }, - id: 'auto-tcp-0X81440A68E839814C', - ip: '127.0.0.1', - name: '', - scheme: null, - status: 'down', - type: 'tcp', - }, - }, - { - id: 'id3', - timestamp: '2019-01-28T17:47:06.077Z', - http: null, - error: null, - monitor: { - duration: { us: 1452 }, - id: 'auto-tcp-0X81440A68E839814C', - ip: '127.0.0.1', - name: '', - scheme: null, - status: 'up', - type: 'tcp', - }, - }, - { - id: 'id4', - timestamp: '2019-01-28T17:47:07.075Z', - http: null, - error: { - message: 'dial tcp 127.0.0.1:9200: connect: connection refused', - type: 'io', - }, - monitor: { - duration: { us: 1094 }, - id: 'auto-tcp-0X81440A68E839814C', - ip: '127.0.0.1', - name: '', - scheme: null, - status: 'down', - type: 'tcp', - }, - }, - { - id: 'id5', - timestamp: '2019-01-28T17:47:07.074Z', - http: null, - error: { - message: - 'Get http://localhost:12349/: dial tcp 127.0.0.1:12349: connect: connection refused', - type: 'io', - }, - monitor: { - duration: { us: 1597 }, - id: 'auto-http-0X3675F89EF0612091', - ip: '127.0.0.1', - name: '', - scheme: null, - status: 'down', - type: 'http', - }, - }, - { - id: 'id6', - timestamp: '2019-01-28T17:47:18.080Z', - http: null, - error: { - message: 'dial tcp 127.0.0.1:9200: connect: connection refused', - type: 'io', - }, - monitor: { - duration: { us: 1699 }, - id: 'auto-tcp-0X81440A68E839814C', - ip: '127.0.0.1', - name: '', - scheme: null, - status: 'down', - type: 'tcp', - }, - }, - { - id: 'id7', - timestamp: '2019-01-28T17:47:19.076Z', - http: null, - error: { - message: 'dial tcp 127.0.0.1:9200: connect: connection refused', - type: 'io', - }, - monitor: { - duration: { us: 5384 }, - id: 'auto-tcp-0X81440A68E839814C', - ip: '127.0.0.1', - name: '', - scheme: null, - status: 'down', - type: 'tcp', - }, - }, - { - id: 'id8', - timestamp: '2019-01-28T17:47:19.076Z', - http: null, - error: { - message: - 'Get http://localhost:12349/: dial tcp 127.0.0.1:12349: connect: connection refused', - type: 'io', - }, - monitor: { - duration: { us: 5397 }, - id: 'auto-http-0X3675F89EF0612091', - ip: '127.0.0.1', - name: '', - scheme: null, - status: 'down', - type: 'http', - }, - }, - { - id: 'id9', - timestamp: '2019-01-28T17:47:19.077Z', - http: { response: { status_code: 200 } }, - error: null, - monitor: { - duration: { us: 127511 }, - id: 'auto-http-0X131221E73F825974', - ip: '172.217.7.4', - name: '', - scheme: null, - status: 'up', - type: 'http', - }, - }, - { - id: 'id10', - timestamp: '2019-01-28T17:47:19.077Z', - http: { response: { status_code: 200 } }, - error: null, - monitor: { - duration: { us: 287543 }, - id: 'auto-http-0X9CB71300ABD5A2A8', - ip: '192.30.253.112', - name: '', - scheme: null, - status: 'up', - type: 'http', - }, - }, - ], - locations: ['nyc'], - }, - }; - }); - - it('renders sorted list without errors', () => { - const { allPings } = pingList; - const component = shallowWithIntl( - <PingListComponent - loading={false} - data={{ allPings }} - onPageCountChange={jest.fn()} - onPageIndexChange={jest.fn()} - onSelectedLocationChange={(_loc: any[]) => {}} - onSelectedStatusChange={jest.fn()} - pageIndex={0} - pageSize={10} - selectedOption="down" - selectedLocation={AllLocationOption.value} - /> - ); - expect(component).toMatchSnapshot(); - }); - - describe('toggleDetails', () => { - let itemIdToExpandedRowMap: ExpandedRowMap; - let pings: Ping[]; - - const setItemIdToExpandedRowMap = (update: ExpandedRowMap) => (itemIdToExpandedRowMap = update); - - beforeEach(() => { - itemIdToExpandedRowMap = {}; - pings = pingList.allPings.pings; - }); - - it('should expand an item if empty', () => { - const ping = pings[0]; - toggleDetails(ping, itemIdToExpandedRowMap, setItemIdToExpandedRowMap); - expect(itemIdToExpandedRowMap).toHaveProperty(ping.id); - }); - - it('should un-expand an item if clicked again', () => { - const ping = pings[0]; - toggleDetails(ping, itemIdToExpandedRowMap, setItemIdToExpandedRowMap); - toggleDetails(ping, itemIdToExpandedRowMap, setItemIdToExpandedRowMap); - expect(itemIdToExpandedRowMap).toEqual({}); - }); - - it('should expand the new row and close the old when when a new row is clicked', () => { - const pingA = pings[0]; - const pingB = pings[1]; - toggleDetails(pingA, itemIdToExpandedRowMap, setItemIdToExpandedRowMap); - toggleDetails(pingB, itemIdToExpandedRowMap, setItemIdToExpandedRowMap); - expect(itemIdToExpandedRowMap).toHaveProperty(pingB.id); - }); - }); -}); diff --git a/x-pack/legacy/plugins/uptime/public/components/functional/ping_list/index.tsx b/x-pack/legacy/plugins/uptime/public/components/functional/ping_list/index.tsx deleted file mode 100644 index e57b229dfd973..0000000000000 --- a/x-pack/legacy/plugins/uptime/public/components/functional/ping_list/index.tsx +++ /dev/null @@ -1,7 +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. - */ - -export * from './ping_list'; diff --git a/x-pack/legacy/plugins/uptime/public/components/functional/ping_list/ping_list.tsx b/x-pack/legacy/plugins/uptime/public/components/functional/ping_list/ping_list.tsx deleted file mode 100644 index 19768c7104e91..0000000000000 --- a/x-pack/legacy/plugins/uptime/public/components/functional/ping_list/ping_list.tsx +++ /dev/null @@ -1,339 +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 { - EuiBadge, - EuiBasicTable, - EuiFlexGroup, - EuiFlexItem, - EuiHealth, - EuiPanel, - EuiSelect, - EuiSpacer, - EuiText, - EuiTitle, - EuiFormRow, - EuiButtonIcon, -} from '@elastic/eui'; -import { i18n } from '@kbn/i18n'; -import { FormattedMessage } from '@kbn/i18n/react'; -import { get } from 'lodash'; -import moment from 'moment'; -import React, { Fragment, useState } from 'react'; -import styled from 'styled-components'; -import { CriteriaWithPagination } from '@elastic/eui/src/components/basic_table/basic_table'; -import { Ping, PingResults } from '../../../../common/graphql/types'; -import { convertMicrosecondsToMilliseconds as microsToMillis } from '../../../lib/helper'; -import { UptimeGraphQLQueryProps, withUptimeGraphQL } from '../../higher_order'; -import { pingsQuery } from '../../../queries'; -import { LocationName } from './location_name'; -import { Pagination } from './../monitor_list'; -import { PingListExpandedRowComponent } from './expanded_row'; - -interface PingListQueryResult { - allPings?: PingResults; -} - -interface PingListProps { - onSelectedStatusChange: (status: string | undefined) => void; - onSelectedLocationChange: (location: any) => void; - onPageCountChange: (itemCount: number) => void; - onPageIndexChange: (index: number) => void; - pageSize: number; - pageIndex: number; - selectedOption: string; - selectedLocation: string | undefined; -} - -type Props = UptimeGraphQLQueryProps<PingListQueryResult> & PingListProps; -interface ExpandedRowMap { - [key: string]: JSX.Element; -} - -export const AllLocationOption = { text: 'All', value: '' }; - -export const toggleDetails = ( - ping: Ping, - itemIdToExpandedRowMap: ExpandedRowMap, - setItemIdToExpandedRowMap: (update: ExpandedRowMap) => any -) => { - // If the user has clicked on the expanded map, close all expanded rows. - if (itemIdToExpandedRowMap[ping.id]) { - setItemIdToExpandedRowMap({}); - return; - } - - // Otherwise expand this row - const newItemIdToExpandedRowMap: ExpandedRowMap = {}; - newItemIdToExpandedRowMap[ping.id] = <PingListExpandedRowComponent ping={ping} />; - setItemIdToExpandedRowMap(newItemIdToExpandedRowMap); -}; - -const SpanWithMargin = styled.span` - margin-right: 16px; -`; - -export const PingListComponent = ({ - data, - loading, - onPageCountChange, - onPageIndexChange, - onSelectedLocationChange, - onSelectedStatusChange, - pageIndex, - pageSize, - selectedOption, - selectedLocation, -}: Props) => { - const [itemIdToExpandedRowMap, setItemIdToExpandedRowMap] = useState<ExpandedRowMap>({}); - - const statusOptions = [ - { - text: i18n.translate('xpack.uptime.pingList.statusOptions.allStatusOptionLabel', { - defaultMessage: 'All', - }), - value: '', - }, - { - text: i18n.translate('xpack.uptime.pingList.statusOptions.upStatusOptionLabel', { - defaultMessage: 'Up', - }), - value: 'up', - }, - { - text: i18n.translate('xpack.uptime.pingList.statusOptions.downStatusOptionLabel', { - defaultMessage: 'Down', - }), - value: 'down', - }, - ]; - const locations = get<string[]>(data, 'allPings.locations'); - const locationOptions = !locations - ? [AllLocationOption] - : [AllLocationOption].concat( - locations.map(name => { - return { text: name, value: name }; - }) - ); - - const pings: Ping[] = data?.allPings?.pings ?? []; - - const hasStatus: boolean = pings.reduce( - (hasHttpStatus: boolean, currentPing: Ping) => - hasHttpStatus || !!currentPing.http?.response?.status_code, - false - ); - - const columns: any[] = [ - { - field: 'monitor.status', - name: i18n.translate('xpack.uptime.pingList.statusColumnLabel', { - defaultMessage: 'Status', - }), - render: (pingStatus: string, item: Ping) => ( - <div> - <EuiHealth color={pingStatus === 'up' ? 'success' : 'danger'}> - {pingStatus === 'up' - ? i18n.translate('xpack.uptime.pingList.statusColumnHealthUpLabel', { - defaultMessage: 'Up', - }) - : i18n.translate('xpack.uptime.pingList.statusColumnHealthDownLabel', { - defaultMessage: 'Down', - })} - </EuiHealth> - <EuiText size="xs" color="subdued"> - {i18n.translate('xpack.uptime.pingList.recencyMessage', { - values: { fromNow: moment(item.timestamp).fromNow() }, - defaultMessage: 'Checked {fromNow}', - description: - 'A string used to inform our users how long ago Heartbeat pinged the selected host.', - })} - </EuiText> - </div> - ), - }, - { - align: 'left', - field: 'observer.geo.name', - name: i18n.translate('xpack.uptime.pingList.locationNameColumnLabel', { - defaultMessage: 'Location', - }), - render: (location: string) => <LocationName location={location} />, - }, - { - align: 'right', - dataType: 'number', - field: 'monitor.ip', - name: i18n.translate('xpack.uptime.pingList.ipAddressColumnLabel', { - defaultMessage: 'IP', - }), - }, - { - align: 'right', - field: 'monitor.duration.us', - name: i18n.translate('xpack.uptime.pingList.durationMsColumnLabel', { - defaultMessage: 'Duration', - }), - render: (duration: number) => - i18n.translate('xpack.uptime.pingList.durationMsColumnFormatting', { - values: { millis: microsToMillis(duration) }, - defaultMessage: '{millis} ms', - }), - }, - { - align: hasStatus ? 'right' : 'center', - field: 'error.type', - name: i18n.translate('xpack.uptime.pingList.errorTypeColumnLabel', { - defaultMessage: 'Error type', - }), - render: (error: string) => error ?? '-', - }, - // Only add this column is there is any status present in list - ...(hasStatus - ? [ - { - field: 'http.response.status_code', - align: 'right', - name: ( - <SpanWithMargin> - {i18n.translate('xpack.uptime.pingList.responseCodeColumnLabel', { - defaultMessage: 'Response code', - })} - </SpanWithMargin> - ), - render: (statusCode: string) => ( - <SpanWithMargin> - <EuiBadge>{statusCode}</EuiBadge> - </SpanWithMargin> - ), - }, - ] - : []), - { - align: 'right', - width: '24px', - isExpander: true, - render: (item: Ping) => { - return ( - <EuiButtonIcon - onClick={() => toggleDetails(item, itemIdToExpandedRowMap, setItemIdToExpandedRowMap)} - disabled={!item.error && !(item.http?.response?.body?.bytes > 0)} - aria-label={ - itemIdToExpandedRowMap[item.id] - ? i18n.translate('xpack.uptime.pingList.collapseRow', { - defaultMessage: 'Collapse', - }) - : i18n.translate('xpack.uptime.pingList.expandRow', { defaultMessage: 'Expand' }) - } - iconType={itemIdToExpandedRowMap[item.id] ? 'arrowUp' : 'arrowDown'} - /> - ); - }, - }, - ]; - - const pagination: Pagination = { - initialPageSize: 25, - pageIndex, - pageSize, - pageSizeOptions: [10, 25, 50, 100], - totalItemCount: data?.allPings?.total ?? pageSize, - }; - - return ( - <Fragment> - <EuiPanel> - <EuiTitle size="xs"> - <h4> - <FormattedMessage - id="xpack.uptime.pingList.checkHistoryTitle" - defaultMessage="History" - /> - </h4> - </EuiTitle> - <EuiSpacer size="s" /> - <EuiFlexGroup justifyContent="spaceBetween"> - <EuiFlexItem grow={false}> - <EuiFlexGroup> - <EuiFlexItem style={{ minWidth: 200 }}> - <EuiFlexGroup> - <EuiFlexItem> - <EuiFormRow - label="Status" - aria-label={i18n.translate('xpack.uptime.pingList.statusLabel', { - defaultMessage: 'Status', - })} - > - <EuiSelect - options={statusOptions} - aria-label={i18n.translate('xpack.uptime.pingList.statusLabel', { - defaultMessage: 'Status', - })} - value={selectedOption} - onChange={selected => { - if (typeof selected.target.value === 'string') { - onSelectedStatusChange( - selected.target && selected.target.value !== '' - ? selected.target.value - : undefined - ); - } - }} - /> - </EuiFormRow> - </EuiFlexItem> - <EuiFlexItem> - <EuiFormRow - label="Location" - aria-label={i18n.translate('xpack.uptime.pingList.locationLabel', { - defaultMessage: 'Location', - })} - > - <EuiSelect - options={locationOptions} - value={selectedLocation} - aria-label={i18n.translate('xpack.uptime.pingList.locationLabel', { - defaultMessage: 'Location', - })} - onChange={selected => { - onSelectedLocationChange( - selected.target && selected.target.value !== '' - ? selected.target.value - : null - ); - }} - /> - </EuiFormRow> - </EuiFlexItem> - </EuiFlexGroup> - </EuiFlexItem> - </EuiFlexGroup> - </EuiFlexItem> - </EuiFlexGroup> - <EuiSpacer size="s" /> - <EuiBasicTable - loading={loading} - columns={columns} - isExpandable={true} - hasActions={true} - items={pings} - itemId="id" - itemIdToExpandedRowMap={itemIdToExpandedRowMap} - pagination={pagination} - onChange={(criteria: CriteriaWithPagination<Ping>) => { - onPageCountChange(criteria.page!.size); - onPageIndexChange(criteria.page!.index); - }} - /> - </EuiPanel> - </Fragment> - ); -}; - -export const PingList = withUptimeGraphQL<PingListQueryResult, PingListProps>( - PingListComponent, - pingsQuery -); diff --git a/x-pack/legacy/plugins/uptime/public/components/functional/search_schema.ts b/x-pack/legacy/plugins/uptime/public/components/functional/search_schema.ts deleted file mode 100644 index bd451a9835288..0000000000000 --- a/x-pack/legacy/plugins/uptime/public/components/functional/search_schema.ts +++ /dev/null @@ -1,17 +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. - */ - -export const filterBarSearchSchema = { - strict: true, - fields: { - 'monitor.id': { type: 'string' }, - 'monitor.status': { type: 'string' }, - 'monitor.ip': { type: 'string' }, - 'monitor.host': { type: 'string' }, - 'monitor.scheme': { type: 'string' }, - 'url.port': { type: 'number' }, - }, -}; diff --git a/x-pack/legacy/plugins/uptime/public/components/higher_order/index.ts b/x-pack/legacy/plugins/uptime/public/components/higher_order/index.ts deleted file mode 100644 index e0e14456cfc68..0000000000000 --- a/x-pack/legacy/plugins/uptime/public/components/higher_order/index.ts +++ /dev/null @@ -1,8 +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. - */ - -export { UptimeGraphQLQueryProps, withUptimeGraphQL } from './uptime_graphql_query'; -export { ResponsiveWrapperProps, withResponsiveWrapper } from './responsive_wrapper'; diff --git a/x-pack/legacy/plugins/uptime/public/components/higher_order/uptime_graphql_query.tsx b/x-pack/legacy/plugins/uptime/public/components/higher_order/uptime_graphql_query.tsx deleted file mode 100644 index 6839050cec7a8..0000000000000 --- a/x-pack/legacy/plugins/uptime/public/components/higher_order/uptime_graphql_query.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 { OperationVariables } from 'apollo-client'; -import { GraphQLError } from 'graphql'; -import React, { Fragment, useContext, useEffect, useState } from 'react'; -import { withApollo, WithApolloClient } from 'react-apollo'; -import { formatUptimeGraphQLErrorList } from '../../lib/helper/format_error_list'; -import { UptimeRefreshContext } from '../../contexts'; - -export interface UptimeGraphQLQueryProps<T> { - loading: boolean; - data?: T; - errors?: GraphQLError[]; -} - -interface UptimeGraphQLProps { - implementsCustomErrorState?: boolean; - variables: OperationVariables; -} - -/** - * This HOC abstracts the task of querying our GraphQL endpoint, - * which eliminates the need for a lot of boilerplate code in the other components. - * - * @type T - the expected result's type - * @type P - any props the wrapped component will require - * @param WrappedComponent - the consuming component - * @param query - the graphQL query - */ -export function withUptimeGraphQL<T, P = {}>(WrappedComponent: any, query: any) { - type Props = UptimeGraphQLProps & WithApolloClient<T> & P; - - return withApollo((props: Props) => { - const { lastRefresh } = useContext(UptimeRefreshContext); - const [loading, setLoading] = useState<boolean>(true); - const [data, setData] = useState<T | undefined>(undefined); - const [errors, setErrors] = useState<GraphQLError[] | undefined>(undefined); - let updateState = ( - loadingVal: boolean, - dataVal: T | undefined, - errorsVal: GraphQLError[] | undefined - ) => { - setLoading(loadingVal); - setData(dataVal); - setErrors(errorsVal); - }; - const { client, implementsCustomErrorState, variables } = props; - const fetch = () => { - setLoading(true); - client - .query<T>({ fetchPolicy: 'network-only', query, variables }) - .then( - (result: any) => { - updateState(result.loading, result.data, result.errors); - }, - (result: any) => { - updateState(false, undefined, result.graphQLErrors); - } - ); - }; - useEffect(() => { - fetch(); - - /** - * If the `then` handler in `fetch`'s promise is fired after - * this component has unmounted, it will try to set state on an - * unmounted component, which indicates a memory leak and will trigger - * React warnings. - * - * We counteract this side effect by providing a cleanup function that will - * reassign the update function to do nothing with the returned values. - */ - return () => { - // this component is planned to be deprecated, for the time being - // we will want to preserve this for the reason above. - // eslint-disable-next-line react-hooks/exhaustive-deps - updateState = () => {}; - }; - }, [variables, lastRefresh]); - if (!implementsCustomErrorState && errors && errors.length > 0) { - return <Fragment>{formatUptimeGraphQLErrorList(errors)}</Fragment>; - } - return <WrappedComponent {...props} loading={loading} data={data} errors={errors} />; - }); -} diff --git a/x-pack/legacy/plugins/uptime/public/components/functional/__tests__/__snapshots__/monitor_charts.test.tsx.snap b/x-pack/legacy/plugins/uptime/public/components/monitor/__tests__/__snapshots__/monitor_charts.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/functional/__tests__/__snapshots__/monitor_charts.test.tsx.snap rename to x-pack/legacy/plugins/uptime/public/components/monitor/__tests__/__snapshots__/monitor_charts.test.tsx.snap diff --git a/x-pack/legacy/plugins/uptime/public/components/functional/__tests__/monitor_charts.test.tsx b/x-pack/legacy/plugins/uptime/public/components/monitor/__tests__/monitor_charts.test.tsx similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/functional/__tests__/monitor_charts.test.tsx rename to x-pack/legacy/plugins/uptime/public/components/monitor/__tests__/monitor_charts.test.tsx diff --git a/x-pack/legacy/plugins/uptime/public/components/monitor/index.ts b/x-pack/legacy/plugins/uptime/public/components/monitor/index.ts new file mode 100644 index 0000000000000..cb7b27afded02 --- /dev/null +++ b/x-pack/legacy/plugins/uptime/public/components/monitor/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. + */ + +export * from './ml'; +export * from './ping_list'; +export * from './location_map'; +export * from './monitor_status_details'; +export * from './ping_histogram'; +export * from './monitor_charts'; diff --git a/x-pack/legacy/plugins/uptime/public/components/functional/location_map/__tests__/__snapshots__/location_map.test.tsx.snap b/x-pack/legacy/plugins/uptime/public/components/monitor/location_map/__tests__/__snapshots__/location_map.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/functional/location_map/__tests__/__snapshots__/location_map.test.tsx.snap rename to x-pack/legacy/plugins/uptime/public/components/monitor/location_map/__tests__/__snapshots__/location_map.test.tsx.snap diff --git a/x-pack/legacy/plugins/uptime/public/components/functional/location_map/__tests__/__snapshots__/location_missing.test.tsx.snap b/x-pack/legacy/plugins/uptime/public/components/monitor/location_map/__tests__/__snapshots__/location_missing.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/functional/location_map/__tests__/__snapshots__/location_missing.test.tsx.snap rename to x-pack/legacy/plugins/uptime/public/components/monitor/location_map/__tests__/__snapshots__/location_missing.test.tsx.snap diff --git a/x-pack/legacy/plugins/uptime/public/components/functional/location_map/__tests__/__snapshots__/location_status_tags.test.tsx.snap b/x-pack/legacy/plugins/uptime/public/components/monitor/location_map/__tests__/__snapshots__/location_status_tags.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/functional/location_map/__tests__/__snapshots__/location_status_tags.test.tsx.snap rename to x-pack/legacy/plugins/uptime/public/components/monitor/location_map/__tests__/__snapshots__/location_status_tags.test.tsx.snap diff --git a/x-pack/legacy/plugins/uptime/public/components/functional/location_map/__tests__/location_map.test.tsx b/x-pack/legacy/plugins/uptime/public/components/monitor/location_map/__tests__/location_map.test.tsx similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/functional/location_map/__tests__/location_map.test.tsx rename to x-pack/legacy/plugins/uptime/public/components/monitor/location_map/__tests__/location_map.test.tsx diff --git a/x-pack/legacy/plugins/uptime/public/components/functional/location_map/__tests__/location_missing.test.tsx b/x-pack/legacy/plugins/uptime/public/components/monitor/location_map/__tests__/location_missing.test.tsx similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/functional/location_map/__tests__/location_missing.test.tsx rename to x-pack/legacy/plugins/uptime/public/components/monitor/location_map/__tests__/location_missing.test.tsx diff --git a/x-pack/legacy/plugins/uptime/public/components/functional/location_map/__tests__/location_status_tags.test.tsx b/x-pack/legacy/plugins/uptime/public/components/monitor/location_map/__tests__/location_status_tags.test.tsx similarity index 97% rename from x-pack/legacy/plugins/uptime/public/components/functional/location_map/__tests__/location_status_tags.test.tsx rename to x-pack/legacy/plugins/uptime/public/components/monitor/location_map/__tests__/location_status_tags.test.tsx index 2359938dbbc35..7dde38af99fc3 100644 --- a/x-pack/legacy/plugins/uptime/public/components/functional/location_map/__tests__/location_status_tags.test.tsx +++ b/x-pack/legacy/plugins/uptime/public/components/monitor/location_map/__tests__/location_status_tags.test.tsx @@ -8,9 +8,8 @@ import React from 'react'; import moment from 'moment'; import { renderWithIntl, shallowWithIntl } from 'test_utils/enzyme_helpers'; import { MonitorLocation } from '../../../../../common/runtime_types/monitor'; -import { LocationStatusTags } from '../'; +import { LocationStatusTags } from '../index'; -// Failing: https://github.com/elastic/kibana/issues/54818 describe('LocationStatusTags component', () => { let monitorLocations: MonitorLocation[]; diff --git a/x-pack/legacy/plugins/uptime/public/components/functional/location_map/embeddables/__tests__/__mocks__/mock.ts b/x-pack/legacy/plugins/uptime/public/components/monitor/location_map/embeddables/__tests__/__mocks__/mock.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/functional/location_map/embeddables/__tests__/__mocks__/mock.ts rename to x-pack/legacy/plugins/uptime/public/components/monitor/location_map/embeddables/__tests__/__mocks__/mock.ts diff --git a/x-pack/legacy/plugins/uptime/public/components/functional/location_map/embeddables/__tests__/map_config.test.ts b/x-pack/legacy/plugins/uptime/public/components/monitor/location_map/embeddables/__tests__/map_config.test.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/functional/location_map/embeddables/__tests__/map_config.test.ts rename to x-pack/legacy/plugins/uptime/public/components/monitor/location_map/embeddables/__tests__/map_config.test.ts diff --git a/x-pack/legacy/plugins/uptime/public/components/functional/location_map/embeddables/embedded_map.tsx b/x-pack/legacy/plugins/uptime/public/components/monitor/location_map/embeddables/embedded_map.tsx similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/functional/location_map/embeddables/embedded_map.tsx rename to x-pack/legacy/plugins/uptime/public/components/monitor/location_map/embeddables/embedded_map.tsx diff --git a/x-pack/legacy/plugins/uptime/public/components/functional/location_map/embeddables/low_poly_layer.json b/x-pack/legacy/plugins/uptime/public/components/monitor/location_map/embeddables/low_poly_layer.json similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/functional/location_map/embeddables/low_poly_layer.json rename to x-pack/legacy/plugins/uptime/public/components/monitor/location_map/embeddables/low_poly_layer.json diff --git a/x-pack/legacy/plugins/uptime/public/components/functional/location_map/embeddables/map_config.ts b/x-pack/legacy/plugins/uptime/public/components/monitor/location_map/embeddables/map_config.ts similarity index 98% rename from x-pack/legacy/plugins/uptime/public/components/functional/location_map/embeddables/map_config.ts rename to x-pack/legacy/plugins/uptime/public/components/monitor/location_map/embeddables/map_config.ts index a43edae438252..ddb52e119fa87 100644 --- a/x-pack/legacy/plugins/uptime/public/components/functional/location_map/embeddables/map_config.ts +++ b/x-pack/legacy/plugins/uptime/public/components/monitor/location_map/embeddables/map_config.ts @@ -5,7 +5,7 @@ */ import lowPolyLayerFeatures from './low_poly_layer.json'; -import { LocationPoint } from './embedded_map.js'; +import { LocationPoint } from './embedded_map'; import { UptimeAppColors } from '../../../../uptime_app'; /** diff --git a/x-pack/legacy/plugins/uptime/public/components/functional/location_map/embeddables/translations.ts b/x-pack/legacy/plugins/uptime/public/components/monitor/location_map/embeddables/translations.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/functional/location_map/embeddables/translations.ts rename to x-pack/legacy/plugins/uptime/public/components/monitor/location_map/embeddables/translations.ts diff --git a/x-pack/legacy/plugins/uptime/public/components/functional/location_map/index.tsx b/x-pack/legacy/plugins/uptime/public/components/monitor/location_map/index.tsx similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/functional/location_map/index.tsx rename to x-pack/legacy/plugins/uptime/public/components/monitor/location_map/index.tsx diff --git a/x-pack/legacy/plugins/uptime/public/components/functional/location_map/location_map.tsx b/x-pack/legacy/plugins/uptime/public/components/monitor/location_map/location_map.tsx similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/functional/location_map/location_map.tsx rename to x-pack/legacy/plugins/uptime/public/components/monitor/location_map/location_map.tsx diff --git a/x-pack/legacy/plugins/uptime/public/components/functional/location_map/location_missing.tsx b/x-pack/legacy/plugins/uptime/public/components/monitor/location_map/location_missing.tsx similarity index 97% rename from x-pack/legacy/plugins/uptime/public/components/functional/location_map/location_missing.tsx rename to x-pack/legacy/plugins/uptime/public/components/monitor/location_map/location_missing.tsx index a20889f6cc653..6ce31e4cc8243 100644 --- a/x-pack/legacy/plugins/uptime/public/components/functional/location_map/location_missing.tsx +++ b/x-pack/legacy/plugins/uptime/public/components/monitor/location_map/location_missing.tsx @@ -16,7 +16,7 @@ import { } from '@elastic/eui'; import styled from 'styled-components'; import { FormattedMessage } from '@kbn/i18n/react'; -import { LocationLink } from '../monitor_list/monitor_list_drawer'; +import { LocationLink } from '../../common/location_link'; const EuiPopoverRight = styled(EuiFlexItem)` margin-left: auto; diff --git a/x-pack/legacy/plugins/uptime/public/components/functional/location_map/location_status_tags.tsx b/x-pack/legacy/plugins/uptime/public/components/monitor/location_map/location_status_tags.tsx similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/functional/location_map/location_status_tags.tsx rename to x-pack/legacy/plugins/uptime/public/components/monitor/location_map/location_status_tags.tsx diff --git a/x-pack/legacy/plugins/uptime/public/components/monitor_details/ml/__tests__/__snapshots__/confirm_delete.test.tsx.snap b/x-pack/legacy/plugins/uptime/public/components/monitor/ml/__tests__/__snapshots__/confirm_delete.test.tsx.snap similarity index 93% rename from x-pack/legacy/plugins/uptime/public/components/monitor_details/ml/__tests__/__snapshots__/confirm_delete.test.tsx.snap rename to x-pack/legacy/plugins/uptime/public/components/monitor/ml/__tests__/__snapshots__/confirm_delete.test.tsx.snap index 24ef7eda0d129..d83e45fea1aec 100644 --- a/x-pack/legacy/plugins/uptime/public/components/monitor_details/ml/__tests__/__snapshots__/confirm_delete.test.tsx.snap +++ b/x-pack/legacy/plugins/uptime/public/components/monitor/ml/__tests__/__snapshots__/confirm_delete.test.tsx.snap @@ -6,6 +6,7 @@ exports[`ML Confirm Job Delete shallow renders without errors 1`] = ` buttonColor="danger" cancelButtonText="Cancel" confirmButtonText="Delete" + data-test-subj="uptimeMLJobDeleteConfirmModel" defaultFocusedButton="confirm" onCancel={[MockFunction]} onConfirm={[MockFunction]} @@ -35,6 +36,7 @@ exports[`ML Confirm Job Delete shallow renders without errors while loading 1`] buttonColor="danger" cancelButtonText="Cancel" confirmButtonText="Delete" + data-test-subj="uptimeMLJobDeleteConfirmModel" defaultFocusedButton="confirm" onCancel={[MockFunction]} onConfirm={[MockFunction]} diff --git a/x-pack/legacy/plugins/uptime/public/components/monitor_details/ml/__tests__/__snapshots__/license_info.test.tsx.snap b/x-pack/legacy/plugins/uptime/public/components/monitor/ml/__tests__/__snapshots__/license_info.test.tsx.snap similarity index 95% rename from x-pack/legacy/plugins/uptime/public/components/monitor_details/ml/__tests__/__snapshots__/license_info.test.tsx.snap rename to x-pack/legacy/plugins/uptime/public/components/monitor/ml/__tests__/__snapshots__/license_info.test.tsx.snap index 2457488c4facc..fb40a42e47f75 100644 --- a/x-pack/legacy/plugins/uptime/public/components/monitor_details/ml/__tests__/__snapshots__/license_info.test.tsx.snap +++ b/x-pack/legacy/plugins/uptime/public/components/monitor/ml/__tests__/__snapshots__/license_info.test.tsx.snap @@ -4,6 +4,7 @@ exports[`ShowLicenseInfo renders without errors 1`] = ` Array [ <div class="euiCallOut euiCallOut--primary license-info-trial" + data-test-subj="uptimeMLLicenseInfo" > <div class="euiCallOutHeader" @@ -54,6 +55,7 @@ exports[`ShowLicenseInfo shallow renders without errors 1`] = ` <EuiCallOut className="license-info-trial" color="primary" + data-test-subj="uptimeMLLicenseInfo" iconType="help" title="Start free 14-day trial" > diff --git a/x-pack/legacy/plugins/uptime/public/components/monitor_details/ml/__tests__/__snapshots__/ml_flyout.test.tsx.snap b/x-pack/legacy/plugins/uptime/public/components/monitor/ml/__tests__/__snapshots__/ml_flyout.test.tsx.snap similarity index 97% rename from x-pack/legacy/plugins/uptime/public/components/monitor_details/ml/__tests__/__snapshots__/ml_flyout.test.tsx.snap rename to x-pack/legacy/plugins/uptime/public/components/monitor/ml/__tests__/__snapshots__/ml_flyout.test.tsx.snap index ead27425c26f3..a83a1d99d7bb0 100644 --- a/x-pack/legacy/plugins/uptime/public/components/monitor_details/ml/__tests__/__snapshots__/ml_flyout.test.tsx.snap +++ b/x-pack/legacy/plugins/uptime/public/components/monitor/ml/__tests__/__snapshots__/ml_flyout.test.tsx.snap @@ -3,6 +3,7 @@ exports[`ML Flyout component renders without errors 1`] = ` <EuiFlyout closeButtonAriaLabel="Closes this dialog" + data-test-subj="uptimeMLFlyout" hideCloseButton={false} maxWidth={false} onClose={[Function]} @@ -69,6 +70,7 @@ exports[`ML Flyout component renders without errors 1`] = ` grow={false} > <EuiButton + data-test-subj="uptimeMLCreateJobBtn" disabled={true} fill={true} isLoading={false} @@ -99,6 +101,7 @@ exports[`ML Flyout component shows license info if no ml available 1`] = ` > <div class="euiFlyout euiFlyout--small" + data-test-subj="uptimeMLFlyout" role="dialog" tabindex="0" > @@ -137,6 +140,7 @@ exports[`ML Flyout component shows license info if no ml available 1`] = ` > <div class="euiCallOut euiCallOut--primary license-info-trial" + data-test-subj="uptimeMLLicenseInfo" > <div class="euiCallOutHeader" @@ -240,6 +244,7 @@ exports[`ML Flyout component shows license info if no ml available 1`] = ` > <button class="euiButton euiButton--primary euiButton--fill" + data-test-subj="uptimeMLCreateJobBtn" disabled="" type="button" > diff --git a/x-pack/legacy/plugins/uptime/public/components/monitor_details/ml/__tests__/__snapshots__/ml_integerations.test.tsx.snap b/x-pack/legacy/plugins/uptime/public/components/monitor/ml/__tests__/__snapshots__/ml_integerations.test.tsx.snap similarity index 97% rename from x-pack/legacy/plugins/uptime/public/components/monitor_details/ml/__tests__/__snapshots__/ml_integerations.test.tsx.snap rename to x-pack/legacy/plugins/uptime/public/components/monitor/ml/__tests__/__snapshots__/ml_integerations.test.tsx.snap index ac4630f4e69a8..fa9b59e13c34e 100644 --- a/x-pack/legacy/plugins/uptime/public/components/monitor_details/ml/__tests__/__snapshots__/ml_integerations.test.tsx.snap +++ b/x-pack/legacy/plugins/uptime/public/components/monitor/ml/__tests__/__snapshots__/ml_integerations.test.tsx.snap @@ -9,6 +9,7 @@ exports[`ML Integrations renders without errors 1`] = ` > <button class="euiButtonEmpty euiButtonEmpty--primary" + data-test-subj="uptimeEnableAnomalyBtn" type="button" > <span diff --git a/x-pack/legacy/plugins/uptime/public/components/monitor_details/ml/__tests__/__snapshots__/ml_job_link.test.tsx.snap b/x-pack/legacy/plugins/uptime/public/components/monitor/ml/__tests__/__snapshots__/ml_job_link.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/monitor_details/ml/__tests__/__snapshots__/ml_job_link.test.tsx.snap rename to x-pack/legacy/plugins/uptime/public/components/monitor/ml/__tests__/__snapshots__/ml_job_link.test.tsx.snap diff --git a/x-pack/legacy/plugins/uptime/public/components/monitor_details/ml/__tests__/__snapshots__/ml_manage_job.test.tsx.snap b/x-pack/legacy/plugins/uptime/public/components/monitor/ml/__tests__/__snapshots__/ml_manage_job.test.tsx.snap similarity index 97% rename from x-pack/legacy/plugins/uptime/public/components/monitor_details/ml/__tests__/__snapshots__/ml_manage_job.test.tsx.snap rename to x-pack/legacy/plugins/uptime/public/components/monitor/ml/__tests__/__snapshots__/ml_manage_job.test.tsx.snap index 6eb2930c4875a..91bd9fa3d86b5 100644 --- a/x-pack/legacy/plugins/uptime/public/components/monitor_details/ml/__tests__/__snapshots__/ml_manage_job.test.tsx.snap +++ b/x-pack/legacy/plugins/uptime/public/components/monitor/ml/__tests__/__snapshots__/ml_manage_job.test.tsx.snap @@ -9,6 +9,7 @@ exports[`Manage ML Job renders without errors 1`] = ` > <button class="euiButtonEmpty euiButtonEmpty--primary euiButtonEmpty--iconRight" + data-test-subj="uptimeManageMLJobBtn" type="button" > <span diff --git a/x-pack/legacy/plugins/uptime/public/components/monitor_details/ml/__tests__/confirm_delete.test.tsx b/x-pack/legacy/plugins/uptime/public/components/monitor/ml/__tests__/confirm_delete.test.tsx similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/monitor_details/ml/__tests__/confirm_delete.test.tsx rename to x-pack/legacy/plugins/uptime/public/components/monitor/ml/__tests__/confirm_delete.test.tsx diff --git a/x-pack/legacy/plugins/uptime/public/components/monitor_details/ml/__tests__/license_info.test.tsx b/x-pack/legacy/plugins/uptime/public/components/monitor/ml/__tests__/license_info.test.tsx similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/monitor_details/ml/__tests__/license_info.test.tsx rename to x-pack/legacy/plugins/uptime/public/components/monitor/ml/__tests__/license_info.test.tsx diff --git a/x-pack/legacy/plugins/uptime/public/components/monitor_details/ml/__tests__/ml_flyout.test.tsx b/x-pack/legacy/plugins/uptime/public/components/monitor/ml/__tests__/ml_flyout.test.tsx similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/monitor_details/ml/__tests__/ml_flyout.test.tsx rename to x-pack/legacy/plugins/uptime/public/components/monitor/ml/__tests__/ml_flyout.test.tsx diff --git a/x-pack/legacy/plugins/uptime/public/components/monitor_details/ml/__tests__/ml_integerations.test.tsx b/x-pack/legacy/plugins/uptime/public/components/monitor/ml/__tests__/ml_integerations.test.tsx similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/monitor_details/ml/__tests__/ml_integerations.test.tsx rename to x-pack/legacy/plugins/uptime/public/components/monitor/ml/__tests__/ml_integerations.test.tsx diff --git a/x-pack/legacy/plugins/uptime/public/components/monitor_details/ml/__tests__/ml_job_link.test.tsx b/x-pack/legacy/plugins/uptime/public/components/monitor/ml/__tests__/ml_job_link.test.tsx similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/monitor_details/ml/__tests__/ml_job_link.test.tsx rename to x-pack/legacy/plugins/uptime/public/components/monitor/ml/__tests__/ml_job_link.test.tsx diff --git a/x-pack/legacy/plugins/uptime/public/components/monitor_details/ml/__tests__/ml_manage_job.test.tsx b/x-pack/legacy/plugins/uptime/public/components/monitor/ml/__tests__/ml_manage_job.test.tsx similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/monitor_details/ml/__tests__/ml_manage_job.test.tsx rename to x-pack/legacy/plugins/uptime/public/components/monitor/ml/__tests__/ml_manage_job.test.tsx diff --git a/x-pack/legacy/plugins/uptime/public/components/monitor_details/ml/confirm_delete.tsx b/x-pack/legacy/plugins/uptime/public/components/monitor/ml/confirm_delete.tsx similarity index 97% rename from x-pack/legacy/plugins/uptime/public/components/monitor_details/ml/confirm_delete.tsx rename to x-pack/legacy/plugins/uptime/public/components/monitor/ml/confirm_delete.tsx index 6754676765fb6..628f943ef2e08 100644 --- a/x-pack/legacy/plugins/uptime/public/components/monitor_details/ml/confirm_delete.tsx +++ b/x-pack/legacy/plugins/uptime/public/components/monitor/ml/confirm_delete.tsx @@ -25,6 +25,7 @@ export const ConfirmJobDeletion: React.FC<Props> = ({ loading, onConfirm, onCanc confirmButtonText="Delete" buttonColor="danger" defaultFocusedButton="confirm" + data-test-subj="uptimeMLJobDeleteConfirmModel" > {!loading ? ( <p> diff --git a/x-pack/legacy/plugins/uptime/public/components/monitor/ml/index.ts b/x-pack/legacy/plugins/uptime/public/components/monitor/ml/index.ts new file mode 100644 index 0000000000000..c644c94d13878 --- /dev/null +++ b/x-pack/legacy/plugins/uptime/public/components/monitor/ml/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 { ManageMLJobComponent } from './manage_ml_job'; diff --git a/x-pack/legacy/plugins/uptime/public/components/monitor_details/ml/license_info.tsx b/x-pack/legacy/plugins/uptime/public/components/monitor/ml/license_info.tsx similarity index 95% rename from x-pack/legacy/plugins/uptime/public/components/monitor_details/ml/license_info.tsx rename to x-pack/legacy/plugins/uptime/public/components/monitor/ml/license_info.tsx index 92badb4043ed6..fae81177a728c 100644 --- a/x-pack/legacy/plugins/uptime/public/components/monitor_details/ml/license_info.tsx +++ b/x-pack/legacy/plugins/uptime/public/components/monitor/ml/license_info.tsx @@ -13,6 +13,7 @@ export const ShowLicenseInfo = () => { return ( <> <EuiCallOut + data-test-subj="uptimeMLLicenseInfo" className="license-info-trial" title={labels.START_TRAIL} color="primary" diff --git a/x-pack/legacy/plugins/uptime/public/components/monitor_details/ml/manage_ml_job.tsx b/x-pack/legacy/plugins/uptime/public/components/monitor/ml/manage_ml_job.tsx similarity index 82% rename from x-pack/legacy/plugins/uptime/public/components/monitor_details/ml/manage_ml_job.tsx rename to x-pack/legacy/plugins/uptime/public/components/monitor/ml/manage_ml_job.tsx index 29f003437f7cb..46ac24e9455e5 100644 --- a/x-pack/legacy/plugins/uptime/public/components/monitor_details/ml/manage_ml_job.tsx +++ b/x-pack/legacy/plugins/uptime/public/components/monitor/ml/manage_ml_job.tsx @@ -7,13 +7,13 @@ import React, { useContext, useState } from 'react'; import { EuiButtonEmpty, EuiContextMenu, EuiIcon, EuiPopover } from '@elastic/eui'; -import { useParams } from 'react-router-dom'; import { useSelector } from 'react-redux'; import { canDeleteMLJobSelector } from '../../../state/selectors'; import { UptimeSettingsContext } from '../../../contexts'; import * as labels from './translations'; import { getMLJobLinkHref } from './ml_job_link'; -import { useUrlParams } from '../../../hooks'; +import { useGetUrlParams } from '../../../hooks'; +import { useMonitorId } from '../../../hooks'; interface Props { hasMLJob: boolean; @@ -28,14 +28,13 @@ export const ManageMLJobComponent = ({ hasMLJob, onEnableJob, onJobDelete }: Pro const canDeleteMLJob = useSelector(canDeleteMLJobSelector); - const [getUrlParams] = useUrlParams(); - const { dateRangeStart, dateRangeEnd } = getUrlParams(); + const { dateRangeStart, dateRangeEnd } = useGetUrlParams(); - let { monitorId } = useParams(); - monitorId = atob(monitorId || ''); + const monitorId = useMonitorId(); const button = ( <EuiButtonEmpty + data-test-subj={hasMLJob ? 'uptimeManageMLJobBtn' : 'uptimeEnableAnomalyBtn'} iconType={hasMLJob ? 'arrowDown' : 'machineLearningApp'} iconSide={hasMLJob ? 'right' : 'left'} onClick={hasMLJob ? () => setIsPopOverOpen(true) : onEnableJob} @@ -62,6 +61,7 @@ export const ManageMLJobComponent = ({ hasMLJob, onEnableJob, onJobDelete }: Pro }, { name: labels.DISABLE_ANOMALY_DETECTION, + 'data-test-subj': 'uptimeDeleteMLJobBtn', icon: <EuiIcon type="trash" size="m" />, onClick: () => { setIsPopOverOpen(false); @@ -74,7 +74,11 @@ export const ManageMLJobComponent = ({ hasMLJob, onEnableJob, onJobDelete }: Pro return ( <EuiPopover button={button} isOpen={isPopOverOpen} closePopover={() => setIsPopOverOpen(false)}> - <EuiContextMenu initialPanelId={0} panels={panels} /> + <EuiContextMenu + initialPanelId={0} + panels={panels} + data-test-subj="uptimeManageMLContextMenu" + /> </EuiPopover> ); }; diff --git a/x-pack/legacy/plugins/uptime/public/components/monitor_details/ml/ml_flyout.tsx b/x-pack/legacy/plugins/uptime/public/components/monitor/ml/ml_flyout.tsx similarity index 95% rename from x-pack/legacy/plugins/uptime/public/components/monitor_details/ml/ml_flyout.tsx rename to x-pack/legacy/plugins/uptime/public/components/monitor/ml/ml_flyout.tsx index fdecfbf20810c..8c3f814e841f7 100644 --- a/x-pack/legacy/plugins/uptime/public/components/monitor_details/ml/ml_flyout.tsx +++ b/x-pack/legacy/plugins/uptime/public/components/monitor/ml/ml_flyout.tsx @@ -39,7 +39,7 @@ export function MLFlyoutView({ isCreatingJob, onClickCreate, onClose, canCreateM const hasPlatinumLicense = license?.getFeature('ml')?.isAvailable; return ( - <EuiFlyout onClose={onClose} size="s"> + <EuiFlyout onClose={onClose} size="s" data-test-subj="uptimeMLFlyout"> <EuiFlyoutHeader> <EuiTitle> <h2>{labels.ENABLE_ANOMALY_DETECTION}</h2> @@ -76,6 +76,7 @@ export function MLFlyoutView({ isCreatingJob, onClickCreate, onClose, canCreateM </EuiFlexItem> <EuiFlexItem grow={false}> <EuiButton + data-test-subj="uptimeMLCreateJobBtn" onClick={() => onClickCreate()} fill isLoading={isCreatingJob} diff --git a/x-pack/legacy/plugins/uptime/public/components/monitor_details/ml/ml_flyout_container.tsx b/x-pack/legacy/plugins/uptime/public/components/monitor/ml/ml_flyout_container.tsx similarity index 91% rename from x-pack/legacy/plugins/uptime/public/components/monitor_details/ml/ml_flyout_container.tsx rename to x-pack/legacy/plugins/uptime/public/components/monitor/ml/ml_flyout_container.tsx index 9eed24e2810d8..c3e8579ca4837 100644 --- a/x-pack/legacy/plugins/uptime/public/components/monitor_details/ml/ml_flyout_container.tsx +++ b/x-pack/legacy/plugins/uptime/public/components/monitor/ml/ml_flyout_container.tsx @@ -6,7 +6,6 @@ import React, { useContext, useEffect, useState } from 'react'; import { useDispatch, useSelector } from 'react-redux'; -import { useParams } from 'react-router-dom'; import { canCreateMLJobSelector, hasMLJobSelector, @@ -24,8 +23,9 @@ import { import { MLFlyoutView } from './ml_flyout'; import { ML_JOB_ID } from '../../../../common/constants'; import { UptimeRefreshContext, UptimeSettingsContext } from '../../../contexts'; -import { useUrlParams } from '../../../hooks'; +import { useGetUrlParams } from '../../../hooks'; import { getDynamicSettings } from '../../../state/actions/dynamic_settings'; +import { useMonitorId } from '../../../hooks'; interface Props { onClose: () => void; @@ -41,7 +41,9 @@ const showMLJobNotification = ( ) => { if (success) { notifications.toasts.success({ - title: <p>{labels.JOB_CREATED_SUCCESS_TITLE}</p>, + title: ( + <p data-test-subj="uptimeMLJobSuccessfullyCreated">{labels.JOB_CREATED_SUCCESS_TITLE}</p> + ), body: ( <p> {labels.JOB_CREATED_SUCCESS_MESSAGE} @@ -54,7 +56,7 @@ const showMLJobNotification = ( }); } else { notifications.toasts.danger({ - title: <p>{labels.JOB_CREATION_FAILED}</p>, + title: <p data-test-subj="uptimeMLJobCreationFailed">{labels.JOB_CREATION_FAILED}</p>, body: message ?? <p>{labels.JOB_CREATION_FAILED_MESSAGE}</p>, toastLifeTimeMs: 10000, }); @@ -77,8 +79,7 @@ export const MachineLearningFlyout: React.FC<Props> = ({ onClose }) => { const { refreshApp } = useContext(UptimeRefreshContext); - let { monitorId } = useParams(); - monitorId = atob(monitorId || ''); + const monitorId = useMonitorId(); const canCreateMLJob = useSelector(canCreateMLJobSelector) && heartbeatIndices !== ''; @@ -93,8 +94,7 @@ export const MachineLearningFlyout: React.FC<Props> = ({ onClose }) => { const [isCreatingJob, setIsCreatingJob] = useState(false); - const [getUrlParams] = useUrlParams(); - const { dateRangeStart, dateRangeEnd } = getUrlParams(); + const { dateRangeStart, dateRangeEnd } = useGetUrlParams(); useEffect(() => { if (isCreatingJob && !isMLJobCreating) { diff --git a/x-pack/legacy/plugins/uptime/public/components/monitor_details/ml/ml_integeration.tsx b/x-pack/legacy/plugins/uptime/public/components/monitor/ml/ml_integeration.tsx similarity index 95% rename from x-pack/legacy/plugins/uptime/public/components/monitor_details/ml/ml_integeration.tsx rename to x-pack/legacy/plugins/uptime/public/components/monitor/ml/ml_integeration.tsx index a27796167091e..4963a901f0ecc 100644 --- a/x-pack/legacy/plugins/uptime/public/components/monitor_details/ml/ml_integeration.tsx +++ b/x-pack/legacy/plugins/uptime/public/components/monitor/ml/ml_integeration.tsx @@ -5,8 +5,6 @@ */ import React, { useContext, useEffect, useState } from 'react'; - -import { useParams } from 'react-router-dom'; import { useDispatch, useSelector } from 'react-redux'; import { MachineLearningFlyout } from './ml_flyout_container'; import { @@ -23,6 +21,7 @@ import * as labels from './translations'; import { useKibana } from '../../../../../../../../src/plugins/kibana_react/public'; import { ManageMLJobComponent } from './manage_ml_job'; import { JobStat } from '../../../../../../../plugins/ml/common/types/data_recognizer'; +import { useMonitorId } from '../../../hooks'; export const MLIntegrationComponent = () => { const [isMlFlyoutOpen, setIsMlFlyoutOpen] = useState(false); @@ -32,8 +31,7 @@ export const MLIntegrationComponent = () => { const { notifications } = useKibana(); - let { monitorId } = useParams(); - monitorId = atob(monitorId || ''); + const monitorId = useMonitorId(); const dispatch = useDispatch(); @@ -59,7 +57,7 @@ export const MLIntegrationComponent = () => { if (isConfirmDeleteJobOpen && jobDeletionSuccess?.[getMLJobId(monitorId as string)]?.deleted) { setIsConfirmDeleteJobOpen(false); notifications.toasts.success({ - title: <p>{labels.JOB_DELETION}</p>, + title: <p data-test-subj="uptimeMLJobSuccessfullyDeleted">{labels.JOB_DELETION}</p>, body: <p>{labels.JOB_DELETION_SUCCESS}</p>, toastLifeTimeMs: 3000, }); diff --git a/x-pack/legacy/plugins/uptime/public/components/monitor_details/ml/ml_job_link.tsx b/x-pack/legacy/plugins/uptime/public/components/monitor/ml/ml_job_link.tsx similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/monitor_details/ml/ml_job_link.tsx rename to x-pack/legacy/plugins/uptime/public/components/monitor/ml/ml_job_link.tsx diff --git a/x-pack/legacy/plugins/uptime/public/components/monitor_details/ml/translations.tsx b/x-pack/legacy/plugins/uptime/public/components/monitor/ml/translations.tsx similarity index 95% rename from x-pack/legacy/plugins/uptime/public/components/monitor_details/ml/translations.tsx rename to x-pack/legacy/plugins/uptime/public/components/monitor/ml/translations.tsx index 32374674771e8..bcc3fca770652 100644 --- a/x-pack/legacy/plugins/uptime/public/components/monitor_details/ml/translations.tsx +++ b/x-pack/legacy/plugins/uptime/public/components/monitor/ml/translations.tsx @@ -96,13 +96,6 @@ export const MANAGE_ANOMALY_DETECTION = i18n.translate( } ); -export const VIEW_EXISTING_JOB = i18n.translate( - 'xpack.uptime.ml.enableAnomalyDetectionPanel.callout.jobExistsDescription.viewJobLinkText', - { - defaultMessage: 'View existing job', - } -); - export const ML_MANAGEMENT_PAGE = i18n.translate( 'xpack.uptime.ml.enableAnomalyDetectionPanel.manageMLJobDescription.mlJobsPageLinkText', { diff --git a/x-pack/legacy/plugins/uptime/public/components/monitor/monitor_charts.tsx b/x-pack/legacy/plugins/uptime/public/components/monitor/monitor_charts.tsx new file mode 100644 index 0000000000000..f9cc1aa52b902 --- /dev/null +++ b/x-pack/legacy/plugins/uptime/public/components/monitor/monitor_charts.tsx @@ -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 React from 'react'; +import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; +import { PingHistogram } from './ping_histogram/ping_histogram_container'; +import { MonitorDuration } from './monitor_duration/monitor_duration_container'; + +interface MonitorChartsProps { + monitorId: string; +} + +export const MonitorCharts = ({ monitorId }: MonitorChartsProps) => { + return ( + <EuiFlexGroup> + <EuiFlexItem> + <MonitorDuration monitorId={monitorId} /> + </EuiFlexItem> + <EuiFlexItem> + <PingHistogram height="400px" isResponsive={false} /> + </EuiFlexItem> + </EuiFlexGroup> + ); +}; diff --git a/x-pack/legacy/plugins/uptime/public/components/monitor/monitor_duration/index.ts b/x-pack/legacy/plugins/uptime/public/components/monitor/monitor_duration/index.ts new file mode 100644 index 0000000000000..aa3230a3f9bc0 --- /dev/null +++ b/x-pack/legacy/plugins/uptime/public/components/monitor/monitor_duration/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 { MonitorDuration } from './monitor_duration_container'; +export { MonitorDurationComponent } from './monitor_duration'; diff --git a/x-pack/legacy/plugins/uptime/public/components/monitor/monitor_duration/monitor_duration.tsx b/x-pack/legacy/plugins/uptime/public/components/monitor/monitor_duration/monitor_duration.tsx new file mode 100644 index 0000000000000..af1c8dbdc49e3 --- /dev/null +++ b/x-pack/legacy/plugins/uptime/public/components/monitor/monitor_duration/monitor_duration.tsx @@ -0,0 +1,66 @@ +/* + * 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 { FormattedMessage } from '@kbn/i18n/react'; +import { EuiFlexGroup, EuiFlexItem, EuiPanel, EuiTitle } from '@elastic/eui'; +import { LocationDurationLine } from '../../../../common/types'; +import { MLIntegrationComponent } from '../ml/ml_integeration'; +import { AnomalyRecords } from '../../../state/actions'; +import { DurationChartComponent } from '../../common/charts'; + +interface DurationChartProps { + loading: boolean; + hasMLJob: boolean; + anomalies: AnomalyRecords | null; + locationDurationLines: LocationDurationLine[]; +} + +/** + * This chart is intended to visualize monitor duration performance over time to + * the users in a helpful way. Its x-axis is based on a timeseries, the y-axis is in + * milliseconds. + * @param props The props required for this component to render properly + */ +export const MonitorDurationComponent = ({ + locationDurationLines, + anomalies, + loading, + hasMLJob, +}: DurationChartProps) => { + return ( + <EuiPanel paddingSize="m"> + <EuiFlexGroup> + <EuiFlexItem> + <EuiTitle size="xs"> + <h4> + {hasMLJob ? ( + <FormattedMessage + id="xpack.uptime.monitorCharts.monitorDuration.titleLabelWithAnomaly" + defaultMessage="Monitor duration (Anomalies: {noOfAnomalies})" + values={{ noOfAnomalies: anomalies?.anomalies?.length ?? 0 }} + /> + ) : ( + <FormattedMessage + id="xpack.uptime.monitorCharts.monitorDuration.titleLabel" + defaultMessage="Monitor duration" + /> + )} + </h4> + </EuiTitle> + </EuiFlexItem> + <EuiFlexItem grow={false}> + <MLIntegrationComponent /> + </EuiFlexItem> + </EuiFlexGroup> + <DurationChartComponent + locationDurationLines={locationDurationLines} + loading={loading} + anomalies={anomalies} + /> + </EuiPanel> + ); +}; diff --git a/x-pack/legacy/plugins/uptime/public/components/monitor/monitor_duration/monitor_duration_container.tsx b/x-pack/legacy/plugins/uptime/public/components/monitor/monitor_duration/monitor_duration_container.tsx new file mode 100644 index 0000000000000..7e39b977f1271 --- /dev/null +++ b/x-pack/legacy/plugins/uptime/public/components/monitor/monitor_duration/monitor_duration_container.tsx @@ -0,0 +1,82 @@ +/* + * 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 { useDispatch, useSelector } from 'react-redux'; +import { useGetUrlParams } from '../../../hooks'; +import { + getAnomalyRecordsAction, + getMLCapabilitiesAction, + getMonitorDurationAction, +} from '../../../state/actions'; +import { + anomaliesSelector, + hasMLFeatureAvailable, + hasMLJobSelector, + selectDurationLines, +} from '../../../state/selectors'; +import { UptimeRefreshContext } from '../../../contexts'; +import { getMLJobId } from '../../../state/api/ml_anomaly'; +import { JobStat } from '../../../../../../../plugins/ml/common/types/data_recognizer'; +import { MonitorDurationComponent } from './monitor_duration'; +import { MonitorIdParam } from '../../../../common/types'; + +export const MonitorDuration: React.FC<MonitorIdParam> = ({ monitorId }) => { + const { + dateRangeStart, + dateRangeEnd, + absoluteDateRangeStart, + absoluteDateRangeEnd, + } = useGetUrlParams(); + + const { durationLines, loading } = useSelector(selectDurationLines); + + const isMLAvailable = useSelector(hasMLFeatureAvailable); + + const { data: mlJobs, loading: jobsLoading } = useSelector(hasMLJobSelector); + + const hasMLJob = + !!mlJobs?.jobsExist && + !!mlJobs.jobs.find((job: JobStat) => job.id === getMLJobId(monitorId as string)); + + const anomalies = useSelector(anomaliesSelector); + + const dispatch = useDispatch(); + + const { lastRefresh } = useContext(UptimeRefreshContext); + + useEffect(() => { + if (isMLAvailable) { + const anomalyParams = { + listOfMonitorIds: [monitorId], + dateStart: absoluteDateRangeStart, + dateEnd: absoluteDateRangeEnd, + }; + + dispatch(getAnomalyRecordsAction.get(anomalyParams)); + } + + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [dateRangeStart, dateRangeEnd, dispatch, lastRefresh, monitorId, isMLAvailable]); + + useEffect(() => { + const params = { monitorId, dateStart: dateRangeStart, dateEnd: dateRangeEnd }; + dispatch(getMonitorDurationAction(params)); + }, [dateRangeStart, dateRangeEnd, dispatch, lastRefresh, monitorId]); + + useEffect(() => { + dispatch(getMLCapabilitiesAction.get()); + }, [dispatch]); + + return ( + <MonitorDurationComponent + anomalies={anomalies} + hasMLJob={hasMLJob} + loading={loading || jobsLoading} + locationDurationLines={durationLines?.locationDurationLines ?? []} + /> + ); +}; diff --git a/x-pack/legacy/plugins/uptime/public/components/functional/monitor_status_details/__test__/__snapshots__/monitor_ssl_certificate.test.tsx.snap b/x-pack/legacy/plugins/uptime/public/components/monitor/monitor_status_details/__test__/__snapshots__/monitor_ssl_certificate.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/functional/monitor_status_details/__test__/__snapshots__/monitor_ssl_certificate.test.tsx.snap rename to x-pack/legacy/plugins/uptime/public/components/monitor/monitor_status_details/__test__/__snapshots__/monitor_ssl_certificate.test.tsx.snap diff --git a/x-pack/legacy/plugins/uptime/public/components/functional/monitor_status_details/__test__/__snapshots__/monitor_status.bar.test.tsx.snap b/x-pack/legacy/plugins/uptime/public/components/monitor/monitor_status_details/__test__/__snapshots__/monitor_status.bar.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/functional/monitor_status_details/__test__/__snapshots__/monitor_status.bar.test.tsx.snap rename to x-pack/legacy/plugins/uptime/public/components/monitor/monitor_status_details/__test__/__snapshots__/monitor_status.bar.test.tsx.snap diff --git a/x-pack/legacy/plugins/uptime/public/components/functional/monitor_status_details/__test__/__snapshots__/status_by_location.test.tsx.snap b/x-pack/legacy/plugins/uptime/public/components/monitor/monitor_status_details/__test__/__snapshots__/status_by_location.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/functional/monitor_status_details/__test__/__snapshots__/status_by_location.test.tsx.snap rename to x-pack/legacy/plugins/uptime/public/components/monitor/monitor_status_details/__test__/__snapshots__/status_by_location.test.tsx.snap diff --git a/x-pack/legacy/plugins/uptime/public/components/functional/monitor_status_details/__test__/monitor_ssl_certificate.test.tsx b/x-pack/legacy/plugins/uptime/public/components/monitor/monitor_status_details/__test__/monitor_ssl_certificate.test.tsx similarity index 96% rename from x-pack/legacy/plugins/uptime/public/components/functional/monitor_status_details/__test__/monitor_ssl_certificate.test.tsx rename to x-pack/legacy/plugins/uptime/public/components/monitor/monitor_status_details/__test__/monitor_ssl_certificate.test.tsx index 2eae14301fd4d..57ed09cc30ef1 100644 --- a/x-pack/legacy/plugins/uptime/public/components/functional/monitor_status_details/__test__/monitor_ssl_certificate.test.tsx +++ b/x-pack/legacy/plugins/uptime/public/components/monitor/monitor_status_details/__test__/monitor_ssl_certificate.test.tsx @@ -9,11 +9,11 @@ import moment from 'moment'; import { mountWithIntl } from 'test_utils/enzyme_helpers'; import { EuiBadge } from '@elastic/eui'; import { renderWithIntl } from 'test_utils/enzyme_helpers'; -import { PingTls } from '../../../../../common/graphql/types'; +import { Tls } from '../../../../../common/runtime_types'; import { MonitorSSLCertificate } from '../monitor_status_bar'; describe('MonitorStatusBar component', () => { - let monitorTls: PingTls; + let monitorTls: Tls; beforeEach(() => { const dateInTwoMonths = moment() diff --git a/x-pack/legacy/plugins/uptime/public/components/functional/monitor_status_details/__test__/monitor_status.bar.test.tsx b/x-pack/legacy/plugins/uptime/public/components/monitor/monitor_status_details/__test__/monitor_status.bar.test.tsx similarity index 92% rename from x-pack/legacy/plugins/uptime/public/components/functional/monitor_status_details/__test__/monitor_status.bar.test.tsx rename to x-pack/legacy/plugins/uptime/public/components/monitor/monitor_status_details/__test__/monitor_status.bar.test.tsx index 0a53eeb89d793..5fd32c808da42 100644 --- a/x-pack/legacy/plugins/uptime/public/components/functional/monitor_status_details/__test__/monitor_status.bar.test.tsx +++ b/x-pack/legacy/plugins/uptime/public/components/monitor/monitor_status_details/__test__/monitor_status.bar.test.tsx @@ -8,7 +8,7 @@ import moment from 'moment'; import React from 'react'; import { renderWithIntl } from 'test_utils/enzyme_helpers'; import { MonitorStatusBarComponent } from '../monitor_status_bar'; -import { Ping } from '../../../../../common/graphql/types'; +import { Ping } from '../../../../../common/runtime_types'; describe('MonitorStatusBar component', () => { let monitorStatus: Ping; @@ -16,7 +16,7 @@ describe('MonitorStatusBar component', () => { beforeEach(() => { monitorStatus = { - id: 'id1', + docId: 'few213kl', timestamp: moment(new Date()) .subtract(15, 'm') .toString(), @@ -24,7 +24,9 @@ describe('MonitorStatusBar component', () => { duration: { us: 1234567, }, + id: 'id1', status: 'up', + type: 'http', }, url: { full: 'https://www.example.com/', diff --git a/x-pack/legacy/plugins/uptime/public/components/functional/monitor_status_details/__test__/status_by_location.test.tsx b/x-pack/legacy/plugins/uptime/public/components/monitor/monitor_status_details/__test__/status_by_location.test.tsx similarity index 98% rename from x-pack/legacy/plugins/uptime/public/components/functional/monitor_status_details/__test__/status_by_location.test.tsx rename to x-pack/legacy/plugins/uptime/public/components/monitor/monitor_status_details/__test__/status_by_location.test.tsx index 34f9260e0525b..b2619825311d7 100644 --- a/x-pack/legacy/plugins/uptime/public/components/functional/monitor_status_details/__test__/status_by_location.test.tsx +++ b/x-pack/legacy/plugins/uptime/public/components/monitor/monitor_status_details/__test__/status_by_location.test.tsx @@ -7,7 +7,7 @@ import React from 'react'; import { renderWithIntl, shallowWithIntl } from 'test_utils/enzyme_helpers'; import { MonitorLocation } from '../../../../../common/runtime_types'; -import { StatusByLocations } from '../'; +import { StatusByLocations } from '../index'; describe('StatusByLocation component', () => { let monitorLocations: MonitorLocation[]; diff --git a/x-pack/legacy/plugins/uptime/public/components/monitor/monitor_status_details/index.ts b/x-pack/legacy/plugins/uptime/public/components/monitor/monitor_status_details/index.ts new file mode 100644 index 0000000000000..e95f14472e9e8 --- /dev/null +++ b/x-pack/legacy/plugins/uptime/public/components/monitor/monitor_status_details/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. + */ + +export { MonitorStatusBarComponent } from './monitor_status_bar'; +export { MonitorStatusDetailsComponent } from './status_details'; +export { StatusByLocations } from './monitor_status_bar/status_by_location'; + +export { MonitorStatusDetails } from './status_details_container'; +export { MonitorStatusBar } from './monitor_status_bar/status_bar_container'; diff --git a/x-pack/legacy/plugins/uptime/public/components/monitor/monitor_status_details/monitor_status_bar/index.ts b/x-pack/legacy/plugins/uptime/public/components/monitor/monitor_status_details/monitor_status_bar/index.ts new file mode 100644 index 0000000000000..3c861412a39e9 --- /dev/null +++ b/x-pack/legacy/plugins/uptime/public/components/monitor/monitor_status_details/monitor_status_bar/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 { MonitorSSLCertificate } from './ssl_certificate'; +export { MonitorStatusBarComponent } from './status_bar'; +export { MonitorStatusBar } from './status_bar_container'; diff --git a/x-pack/legacy/plugins/uptime/public/components/monitor/monitor_status_details/monitor_status_bar/ssl_certificate.tsx b/x-pack/legacy/plugins/uptime/public/components/monitor/monitor_status_details/monitor_status_bar/ssl_certificate.tsx new file mode 100644 index 0000000000000..d92534aecd175 --- /dev/null +++ b/x-pack/legacy/plugins/uptime/public/components/monitor/monitor_status_details/monitor_status_bar/ssl_certificate.tsx @@ -0,0 +1,58 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import moment from 'moment'; +import { EuiSpacer, EuiText, EuiBadge } from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { i18n } from '@kbn/i18n'; +import { Tls } from '../../../../../common/runtime_types'; + +interface Props { + /** + * TLS information coming from monitor in ES heartbeat index + */ + tls: Tls | null | undefined; +} + +export const MonitorSSLCertificate = ({ tls }: Props) => { + const certValidityDate = new Date(tls?.certificate_not_valid_after ?? ''); + + const isValidDate = !isNaN(certValidityDate.valueOf()); + + const dateIn30Days = moment().add('30', 'days'); + + const isExpiringInMonth = isValidDate && dateIn30Days > moment(certValidityDate); + + return isValidDate ? ( + <> + <EuiSpacer size="s" /> + <EuiText + grow={false} + size="s" + aria-label={i18n.translate( + 'xpack.uptime.monitorStatusBar.sslCertificateExpiry.label.ariaLabel', + { + defaultMessage: 'SSL certificate expires {validityDate}', + values: { validityDate: moment(certValidityDate).fromNow() }, + } + )} + > + <FormattedMessage + id="xpack.uptime.monitorStatusBar.sslCertificateExpiry.badgeContent" + defaultMessage="SSL certificate expires {emphasizedText}" + values={{ + emphasizedText: ( + <EuiBadge color={isExpiringInMonth ? 'warning' : 'default'}> + {moment(certValidityDate).fromNow()} + </EuiBadge> + ), + }} + /> + </EuiText> + </> + ) : null; +}; diff --git a/x-pack/legacy/plugins/uptime/public/components/monitor/monitor_status_details/monitor_status_bar/status_bar.tsx b/x-pack/legacy/plugins/uptime/public/components/monitor/monitor_status_details/monitor_status_bar/status_bar.tsx new file mode 100644 index 0000000000000..36159dc29eccd --- /dev/null +++ b/x-pack/legacy/plugins/uptime/public/components/monitor/monitor_status_details/monitor_status_bar/status_bar.tsx @@ -0,0 +1,61 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { + EuiLink, + EuiTitle, + EuiTextColor, + EuiSpacer, + EuiText, + EuiFlexGroup, + EuiFlexItem, +} from '@elastic/eui'; +import { MonitorSSLCertificate } from './ssl_certificate'; +import * as labels from './translations'; +import { StatusByLocations } from './status_by_location'; +import { Ping } from '../../../../../common/runtime_types'; +import { MonitorLocations } from '../../../../../common/runtime_types'; + +interface MonitorStatusBarProps { + monitorId: string; + monitorStatus: Ping | null; + monitorLocations: MonitorLocations; +} + +export const MonitorStatusBarComponent: React.FC<MonitorStatusBarProps> = ({ + monitorId, + monitorStatus, + monitorLocations, +}) => { + const full = monitorStatus?.url?.full ?? ''; + + return ( + <EuiFlexGroup direction="column" gutterSize="none" responsive={false}> + <EuiFlexItem grow={false}> + <StatusByLocations locations={monitorLocations?.locations ?? []} /> + </EuiFlexItem> + <EuiFlexItem grow={false}> + <EuiText> + <EuiLink aria-label={labels.monitorUrlLinkAriaLabel} href={full} target="_blank"> + {full} + </EuiLink> + </EuiText> + </EuiFlexItem> + <EuiFlexItem> + <EuiTitle size="xs"> + <EuiTextColor color="subdued"> + <h1 data-test-subj="monitor-page-title">{monitorId}</h1> + </EuiTextColor> + </EuiTitle> + </EuiFlexItem> + <EuiSpacer /> + <EuiFlexItem grow={false}> + <MonitorSSLCertificate tls={monitorStatus?.tls} /> + </EuiFlexItem> + </EuiFlexGroup> + ); +}; diff --git a/x-pack/legacy/plugins/uptime/public/components/monitor/monitor_status_details/monitor_status_bar/status_bar_container.tsx b/x-pack/legacy/plugins/uptime/public/components/monitor/monitor_status_details/monitor_status_bar/status_bar_container.tsx new file mode 100644 index 0000000000000..9562295437515 --- /dev/null +++ b/x-pack/legacy/plugins/uptime/public/components/monitor/monitor_status_details/monitor_status_bar/status_bar_container.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, { useContext, useEffect } from 'react'; +import { useDispatch, useSelector } from 'react-redux'; +import { monitorLocationsSelector, monitorStatusSelector } from '../../../../state/selectors'; +import { MonitorStatusBarComponent } from './index'; +import { getMonitorStatusAction } from '../../../../state/actions'; +import { useGetUrlParams } from '../../../../hooks'; +import { UptimeRefreshContext } from '../../../../contexts'; +import { MonitorIdParam } from '../../../../../common/types'; +import { AppState } from '../../../../state'; + +export const MonitorStatusBar: React.FC<MonitorIdParam> = ({ monitorId }) => { + const { lastRefresh } = useContext(UptimeRefreshContext); + + const { dateRangeStart: dateStart, dateRangeEnd: dateEnd } = useGetUrlParams(); + + const dispatch = useDispatch(); + + const monitorStatus = useSelector(monitorStatusSelector); + const monitorLocations = useSelector((state: AppState) => + monitorLocationsSelector(state, monitorId) + ); + + useEffect(() => { + dispatch(getMonitorStatusAction({ dateStart, dateEnd, monitorId })); + }, [monitorId, dateStart, dateEnd, lastRefresh, dispatch]); + + return ( + <MonitorStatusBarComponent + monitorId={monitorId} + monitorStatus={monitorStatus} + monitorLocations={monitorLocations!} + /> + ); +}; diff --git a/x-pack/legacy/plugins/uptime/public/components/functional/monitor_status_details/monitor_status_bar/status_by_location.tsx b/x-pack/legacy/plugins/uptime/public/components/monitor/monitor_status_details/monitor_status_bar/status_by_location.tsx similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/functional/monitor_status_details/monitor_status_bar/status_by_location.tsx rename to x-pack/legacy/plugins/uptime/public/components/monitor/monitor_status_details/monitor_status_bar/status_by_location.tsx diff --git a/x-pack/legacy/plugins/uptime/public/components/monitor/monitor_status_details/monitor_status_bar/translations.ts b/x-pack/legacy/plugins/uptime/public/components/monitor/monitor_status_details/monitor_status_bar/translations.ts new file mode 100644 index 0000000000000..f60a1ceeaafb8 --- /dev/null +++ b/x-pack/legacy/plugins/uptime/public/components/monitor/monitor_status_details/monitor_status_bar/translations.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 { i18n } from '@kbn/i18n'; + +export const healthStatusMessageAriaLabel = i18n.translate( + 'xpack.uptime.monitorStatusBar.healthStatusMessageAriaLabel', + { + defaultMessage: 'Monitor status', + } +); + +export const upLabel = i18n.translate('xpack.uptime.monitorStatusBar.healthStatusMessage.upLabel', { + defaultMessage: 'Up', +}); + +export const downLabel = i18n.translate( + 'xpack.uptime.monitorStatusBar.healthStatusMessage.downLabel', + { + defaultMessage: 'Down', + } +); + +export const monitorUrlLinkAriaLabel = i18n.translate( + 'xpack.uptime.monitorStatusBar.monitorUrlLinkAriaLabel', + { + defaultMessage: 'Monitor URL link', + } +); + +export const durationTextAriaLabel = i18n.translate( + 'xpack.uptime.monitorStatusBar.durationTextAriaLabel', + { + defaultMessage: 'Monitor duration in milliseconds', + } +); + +export const timestampFromNowTextAriaLabel = i18n.translate( + 'xpack.uptime.monitorStatusBar.timestampFromNowTextAriaLabel', + { + defaultMessage: 'Time since last check', + } +); + +export const loadingMessage = i18n.translate('xpack.uptime.monitorStatusBar.loadingMessage', { + defaultMessage: 'Loading…', +}); diff --git a/x-pack/legacy/plugins/uptime/public/components/monitor/monitor_status_details/status_details.tsx b/x-pack/legacy/plugins/uptime/public/components/monitor/monitor_status_details/status_details.tsx new file mode 100644 index 0000000000000..ebd16b05ecb4a --- /dev/null +++ b/x-pack/legacy/plugins/uptime/public/components/monitor/monitor_status_details/status_details.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, { useContext, useEffect, useState } from 'react'; +import { EuiFlexGroup, EuiFlexItem, EuiPanel } from '@elastic/eui'; +import styled from 'styled-components'; +import { LocationMap } from '../location_map'; +import { UptimeRefreshContext } from '../../../contexts'; +import { MonitorLocations } from '../../../../common/runtime_types'; +import { MonitorStatusBar } from './monitor_status_bar'; + +interface MonitorStatusDetailsProps { + monitorId: string; + monitorLocations: MonitorLocations; +} + +const WrapFlexItem = styled(EuiFlexItem)` + @media (max-width: 1150px) { + width: 100%; + } +`; + +export const MonitorStatusDetailsComponent = ({ + monitorId, + monitorLocations, +}: MonitorStatusDetailsProps) => { + const { refreshApp } = useContext(UptimeRefreshContext); + + const [isTabActive] = useState(document.visibilityState); + const onTabActive = () => { + if (document.visibilityState === 'visible' && isTabActive === 'hidden') { + refreshApp(); + } + }; + + // Refreshing application state after Tab becomes active to render latest map state + // If application renders in when tab is not in focus it gives some unexpected behaviors + // Where map is not visible on change + useEffect(() => { + document.addEventListener('visibilitychange', onTabActive); + return () => { + document.removeEventListener('visibilitychange', onTabActive); + }; + + // we want this effect to execute exactly once after the component mounts + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + return ( + <EuiPanel> + <EuiFlexGroup gutterSize="l" wrap responsive={true}> + <EuiFlexItem grow={true}> + <MonitorStatusBar monitorId={monitorId} /> + </EuiFlexItem> + <WrapFlexItem grow={false}> + <LocationMap monitorLocations={monitorLocations} /> + </WrapFlexItem> + </EuiFlexGroup> + </EuiPanel> + ); +}; diff --git a/x-pack/legacy/plugins/uptime/public/components/monitor/monitor_status_details/status_details_container.tsx b/x-pack/legacy/plugins/uptime/public/components/monitor/monitor_status_details/status_details_container.tsx new file mode 100644 index 0000000000000..251f3562f9d1a --- /dev/null +++ b/x-pack/legacy/plugins/uptime/public/components/monitor/monitor_status_details/status_details_container.tsx @@ -0,0 +1,34 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { useContext, useEffect } from 'react'; +import { useDispatch, useSelector } from 'react-redux'; +import { useGetUrlParams } from '../../../hooks'; +import { monitorLocationsSelector } from '../../../state/selectors'; +import { getMonitorLocationsAction } from '../../../state/actions/monitor'; +import { MonitorStatusDetailsComponent } from './index'; +import { UptimeRefreshContext } from '../../../contexts'; +import { AppState } from '../../../state'; +import { MonitorIdParam } from '../../../../common/types'; + +export const MonitorStatusDetails: React.FC<MonitorIdParam> = ({ monitorId }) => { + const { lastRefresh } = useContext(UptimeRefreshContext); + + const { dateRangeStart: dateStart, dateRangeEnd: dateEnd } = useGetUrlParams(); + + const dispatch = useDispatch(); + const monitorLocations = useSelector((state: AppState) => + monitorLocationsSelector(state, monitorId) + ); + + useEffect(() => { + dispatch(getMonitorLocationsAction({ dateStart, dateEnd, monitorId })); + }, [monitorId, dateStart, dateEnd, lastRefresh, dispatch]); + + return ( + <MonitorStatusDetailsComponent monitorId={monitorId} monitorLocations={monitorLocations!} /> + ); +}; diff --git a/x-pack/legacy/plugins/uptime/public/components/monitor/monitor_status_details/translations.ts b/x-pack/legacy/plugins/uptime/public/components/monitor/monitor_status_details/translations.ts new file mode 100644 index 0000000000000..f60a1ceeaafb8 --- /dev/null +++ b/x-pack/legacy/plugins/uptime/public/components/monitor/monitor_status_details/translations.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 { i18n } from '@kbn/i18n'; + +export const healthStatusMessageAriaLabel = i18n.translate( + 'xpack.uptime.monitorStatusBar.healthStatusMessageAriaLabel', + { + defaultMessage: 'Monitor status', + } +); + +export const upLabel = i18n.translate('xpack.uptime.monitorStatusBar.healthStatusMessage.upLabel', { + defaultMessage: 'Up', +}); + +export const downLabel = i18n.translate( + 'xpack.uptime.monitorStatusBar.healthStatusMessage.downLabel', + { + defaultMessage: 'Down', + } +); + +export const monitorUrlLinkAriaLabel = i18n.translate( + 'xpack.uptime.monitorStatusBar.monitorUrlLinkAriaLabel', + { + defaultMessage: 'Monitor URL link', + } +); + +export const durationTextAriaLabel = i18n.translate( + 'xpack.uptime.monitorStatusBar.durationTextAriaLabel', + { + defaultMessage: 'Monitor duration in milliseconds', + } +); + +export const timestampFromNowTextAriaLabel = i18n.translate( + 'xpack.uptime.monitorStatusBar.timestampFromNowTextAriaLabel', + { + defaultMessage: 'Time since last check', + } +); + +export const loadingMessage = i18n.translate('xpack.uptime.monitorStatusBar.loadingMessage', { + defaultMessage: 'Loading…', +}); diff --git a/x-pack/legacy/plugins/uptime/public/components/monitor/ping_histogram/index.ts b/x-pack/legacy/plugins/uptime/public/components/monitor/ping_histogram/index.ts new file mode 100644 index 0000000000000..c980b41167d0c --- /dev/null +++ b/x-pack/legacy/plugins/uptime/public/components/monitor/ping_histogram/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 { PingHistogram } from './ping_histogram_container'; diff --git a/x-pack/legacy/plugins/uptime/public/components/monitor/ping_histogram/ping_histogram_container.tsx b/x-pack/legacy/plugins/uptime/public/components/monitor/ping_histogram/ping_histogram_container.tsx new file mode 100644 index 0000000000000..c0e17966f5b9f --- /dev/null +++ b/x-pack/legacy/plugins/uptime/public/components/monitor/ping_histogram/ping_histogram_container.tsx @@ -0,0 +1,48 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { useEffect } from 'react'; +import { useDispatch, useSelector } from 'react-redux'; +import { PingHistogramComponent } from '../../common/charts'; +import { getPingHistogram } from '../../../state/actions'; +import { selectPingHistogram } from '../../../state/selectors'; +import { useGetUrlParams } from '../../../hooks'; +import { useMonitorId } from '../../../hooks'; +import { ResponsiveWrapperProps, withResponsiveWrapper } from '../../common/higher_order'; + +interface Props { + height: string; +} + +const Container: React.FC<Props & ResponsiveWrapperProps> = ({ height }) => { + const { + statusFilter, + absoluteDateRangeStart, + absoluteDateRangeEnd, + dateRangeStart: dateStart, + dateRangeEnd: dateEnd, + } = useGetUrlParams(); + + const dispatch = useDispatch(); + const monitorId = useMonitorId(); + + const { loading, data, esKuery, lastRefresh } = useSelector(selectPingHistogram); + + useEffect(() => { + dispatch(getPingHistogram({ monitorId, dateStart, dateEnd, statusFilter, filters: esKuery })); + }, [dateStart, dateEnd, monitorId, statusFilter, lastRefresh, esKuery, dispatch]); + return ( + <PingHistogramComponent + data={data} + absoluteStartDate={absoluteDateRangeStart} + absoluteEndDate={absoluteDateRangeEnd} + height={height} + loading={loading} + /> + ); +}; + +export const PingHistogram = withResponsiveWrapper(Container); diff --git a/x-pack/legacy/plugins/uptime/public/components/functional/ping_list/__tests__/__snapshots__/doc_link_body.test.tsx.snap b/x-pack/legacy/plugins/uptime/public/components/monitor/ping_list/__tests__/__snapshots__/doc_link_body.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/functional/ping_list/__tests__/__snapshots__/doc_link_body.test.tsx.snap rename to x-pack/legacy/plugins/uptime/public/components/monitor/ping_list/__tests__/__snapshots__/doc_link_body.test.tsx.snap diff --git a/x-pack/legacy/plugins/uptime/public/components/functional/ping_list/__tests__/__snapshots__/expanded_row.test.tsx.snap b/x-pack/legacy/plugins/uptime/public/components/monitor/ping_list/__tests__/__snapshots__/expanded_row.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/functional/ping_list/__tests__/__snapshots__/expanded_row.test.tsx.snap rename to x-pack/legacy/plugins/uptime/public/components/monitor/ping_list/__tests__/__snapshots__/expanded_row.test.tsx.snap diff --git a/x-pack/legacy/plugins/uptime/public/components/monitor/ping_list/__tests__/__snapshots__/ping_list.test.tsx.snap b/x-pack/legacy/plugins/uptime/public/components/monitor/ping_list/__tests__/__snapshots__/ping_list.test.tsx.snap new file mode 100644 index 0000000000000..154ab6399452d --- /dev/null +++ b/x-pack/legacy/plugins/uptime/public/components/monitor/ping_list/__tests__/__snapshots__/ping_list.test.tsx.snap @@ -0,0 +1,349 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`PingList component renders sorted list without errors 1`] = ` +<EuiPanel> + <EuiTitle + size="xs" + > + <h4> + <FormattedMessage + defaultMessage="History" + id="xpack.uptime.pingList.checkHistoryTitle" + values={Object {}} + /> + </h4> + </EuiTitle> + <EuiSpacer + size="s" + /> + <EuiFlexGroup + justifyContent="spaceBetween" + > + <EuiFlexItem + grow={false} + > + <EuiFormRow + aria-label="Status" + describedByIds={Array []} + display="row" + fullWidth={false} + hasChildLabel={true} + hasEmptyLabelSpace={false} + label="Status" + labelType="label" + > + <EuiSelect + aria-label="Status" + data-test-subj="xpack.uptime.pingList.statusSelect" + onChange={[Function]} + options={ + Array [ + Object { + "data-test-subj": "xpack.uptime.pingList.statusOptions.all", + "text": "All", + "value": "", + }, + Object { + "data-test-subj": "xpack.uptime.pingList.statusOptions.up", + "text": "Up", + "value": "up", + }, + Object { + "data-test-subj": "xpack.uptime.pingList.statusOptions.down", + "text": "Down", + "value": "down", + }, + ] + } + value="" + /> + </EuiFormRow> + </EuiFlexItem> + <EuiFlexItem> + <EuiFormRow + aria-label="Location" + describedByIds={Array []} + display="row" + fullWidth={false} + hasChildLabel={true} + hasEmptyLabelSpace={false} + label="Location" + labelType="label" + > + <EuiSelect + aria-label="Location" + data-test-subj="xpack.uptime.pingList.locationSelect" + onChange={[Function]} + options={ + Array [ + Object { + "data-test-subj": "xpack.uptime.pingList.locationOptions.all", + "text": "All", + "value": "", + }, + ] + } + value="" + /> + </EuiFormRow> + </EuiFlexItem> + </EuiFlexGroup> + <EuiSpacer + size="s" + /> + <EuiBasicTable + columns={ + Array [ + Object { + "field": "monitor.status", + "name": "Status", + "render": [Function], + }, + Object { + "align": "left", + "field": "observer.geo.name", + "name": "Location", + "render": [Function], + }, + Object { + "align": "right", + "dataType": "number", + "field": "monitor.ip", + "name": "IP", + }, + Object { + "align": "right", + "field": "monitor.duration.us", + "name": "Duration", + "render": [Function], + }, + Object { + "align": "right", + "field": "error.type", + "name": "Error type", + "render": [Function], + }, + Object { + "align": "right", + "field": "http.response.status_code", + "name": <ForwardRef(styled.span)> + Response code + </ForwardRef(styled.span)>, + "render": [Function], + }, + Object { + "align": "right", + "isExpander": true, + "render": [Function], + "width": "24px", + }, + ] + } + hasActions={true} + isExpandable={true} + itemId="docId" + itemIdToExpandedRowMap={Object {}} + items={ + Array [ + Object { + "docId": "fewjio21", + "error": Object { + "message": "dial tcp 127.0.0.1:9200: connect: connection refused", + "type": "io", + }, + "monitor": Object { + "duration": Object { + "us": 1430, + }, + "id": "auto-tcp-0X81440A68E839814F", + "ip": "127.0.0.1", + "name": "", + "status": "down", + "type": "tcp", + }, + "timestamp": "2019-01-28T17:47:08.078Z", + }, + Object { + "docId": "fewjoo21", + "error": Object { + "message": "dial tcp 127.0.0.1:9200: connect: connection refused", + "type": "io", + }, + "monitor": Object { + "duration": Object { + "us": 1370, + }, + "id": "auto-tcp-0X81440A68E839814D", + "ip": "127.0.0.1", + "name": "", + "status": "down", + "type": "tcp", + }, + "timestamp": "2019-01-28T17:47:09.075Z", + }, + Object { + "docId": "fejjio21", + "monitor": Object { + "duration": Object { + "us": 1452, + }, + "id": "auto-tcp-0X81440A68E839814D", + "ip": "127.0.0.1", + "name": "", + "status": "up", + "type": "tcp", + }, + "timestamp": "2019-01-28T17:47:06.077Z", + }, + Object { + "docId": "fewzio21", + "error": Object { + "message": "dial tcp 127.0.0.1:9200: connect: connection refused", + "type": "io", + }, + "monitor": Object { + "duration": Object { + "us": 1094, + }, + "id": "auto-tcp-0X81440A68E839814E", + "ip": "127.0.0.1", + "name": "", + "status": "down", + "type": "tcp", + }, + "timestamp": "2019-01-28T17:47:07.075Z", + }, + Object { + "docId": "fewpi321", + "error": Object { + "message": "Get http://localhost:12349/: dial tcp 127.0.0.1:12349: connect: connection refused", + "type": "io", + }, + "monitor": Object { + "duration": Object { + "us": 1597, + }, + "id": "auto-http-0X3675F89EF061209G", + "ip": "127.0.0.1", + "name": "", + "status": "down", + "type": "http", + }, + "timestamp": "2019-01-28T17:47:07.074Z", + }, + Object { + "docId": "0ewjio21", + "error": Object { + "message": "dial tcp 127.0.0.1:9200: connect: connection refused", + "type": "io", + }, + "monitor": Object { + "duration": Object { + "us": 1699, + }, + "id": "auto-tcp-0X81440A68E839814H", + "ip": "127.0.0.1", + "name": "", + "status": "down", + "type": "tcp", + }, + "timestamp": "2019-01-28T17:47:18.080Z", + }, + Object { + "docId": "3ewjio21", + "error": Object { + "message": "dial tcp 127.0.0.1:9200: connect: connection refused", + "type": "io", + }, + "monitor": Object { + "duration": Object { + "us": 5384, + }, + "id": "auto-tcp-0X81440A68E839814I", + "ip": "127.0.0.1", + "name": "", + "status": "down", + "type": "tcp", + }, + "timestamp": "2019-01-28T17:47:19.076Z", + }, + Object { + "docId": "fewjip21", + "error": Object { + "message": "Get http://localhost:12349/: dial tcp 127.0.0.1:12349: connect: connection refused", + "type": "io", + }, + "monitor": Object { + "duration": Object { + "us": 5397, + }, + "id": "auto-http-0X3675F89EF061209J", + "ip": "127.0.0.1", + "name": "", + "status": "down", + "type": "http", + }, + "timestamp": "2019-01-28T17:47:19.076Z", + }, + Object { + "docId": "fewjio21", + "http": Object { + "response": Object { + "status_code": 200, + }, + }, + "monitor": Object { + "duration": Object { + "us": 127511, + }, + "id": "auto-tcp-0X81440A68E839814C", + "ip": "172.217.7.4", + "name": "", + "status": "up", + "type": "http", + }, + "timestamp": "2019-01-28T17:47:19.077Z", + }, + Object { + "docId": "fewjik81", + "http": Object { + "response": Object { + "status_code": 200, + }, + }, + "monitor": Object { + "duration": Object { + "us": 287543, + }, + "id": "auto-http-0X131221E73F825974", + "ip": "192.30.253.112", + "name": "", + "status": "up", + "type": "http", + }, + "timestamp": "2019-01-28T17:47:19.077Z", + }, + ] + } + loading={false} + noItemsMessage="No items found" + onChange={[Function]} + pagination={ + Object { + "initialPageSize": 10, + "pageIndex": 0, + "pageSize": 10, + "pageSizeOptions": Array [ + 10, + 25, + 50, + 100, + ], + "totalItemCount": 10, + } + } + responsive={true} + tableLayout="fixed" + /> +</EuiPanel> +`; diff --git a/x-pack/legacy/plugins/uptime/public/components/functional/ping_list/__tests__/doc_link_body.test.tsx b/x-pack/legacy/plugins/uptime/public/components/monitor/ping_list/__tests__/doc_link_body.test.tsx similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/functional/ping_list/__tests__/doc_link_body.test.tsx rename to x-pack/legacy/plugins/uptime/public/components/monitor/ping_list/__tests__/doc_link_body.test.tsx diff --git a/x-pack/legacy/plugins/uptime/public/components/functional/ping_list/__tests__/expanded_row.test.tsx b/x-pack/legacy/plugins/uptime/public/components/monitor/ping_list/__tests__/expanded_row.test.tsx similarity index 92% rename from x-pack/legacy/plugins/uptime/public/components/functional/ping_list/__tests__/expanded_row.test.tsx rename to x-pack/legacy/plugins/uptime/public/components/monitor/ping_list/__tests__/expanded_row.test.tsx index 9dbe48ec5553a..2c1434cfd64bd 100644 --- a/x-pack/legacy/plugins/uptime/public/components/functional/ping_list/__tests__/expanded_row.test.tsx +++ b/x-pack/legacy/plugins/uptime/public/components/monitor/ping_list/__tests__/expanded_row.test.tsx @@ -7,15 +7,23 @@ import { mountWithIntl, renderWithIntl, shallowWithIntl } from 'test_utils/enzyme_helpers'; import React from 'react'; import { PingListExpandedRowComponent } from '../expanded_row'; -import { Ping } from '../../../../../common/graphql/types'; +import { Ping } from '../../../../../common/runtime_types'; import { DocLinkForBody } from '../doc_link_body'; describe('PingListExpandedRow', () => { let ping: Ping; beforeEach(() => { ping = { - id: '123', + docId: 'fdeio12', timestamp: '19290310', + monitor: { + duration: { + us: 12345, + }, + id: '123', + status: 'down', + type: 'http', + }, http: { response: { body: { @@ -34,7 +42,7 @@ describe('PingListExpandedRow', () => { it('renders error information when an error field is present', () => { ping.error = { - code: 403, + code: '403', message: 'Forbidden', }; expect(shallowWithIntl(<PingListExpandedRowComponent ping={ping} />)).toMatchSnapshot(); diff --git a/x-pack/legacy/plugins/uptime/public/components/monitor/ping_list/__tests__/ping_list.test.tsx b/x-pack/legacy/plugins/uptime/public/components/monitor/ping_list/__tests__/ping_list.test.tsx new file mode 100644 index 0000000000000..cb8413ba08a81 --- /dev/null +++ b/x-pack/legacy/plugins/uptime/public/components/monitor/ping_list/__tests__/ping_list.test.tsx @@ -0,0 +1,277 @@ +/* + * 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 { shallowWithIntl } from 'test_utils/enzyme_helpers'; +import { PingListComponent, toggleDetails } from '../ping_list'; +import { Ping, PingsResponse } from '../../../../../common/runtime_types'; +import { ExpandedRowMap } from '../../../overview/monitor_list/types'; + +describe('PingList component', () => { + let response: PingsResponse; + + beforeEach(() => { + response = { + total: 9231, + locations: ['nyc'], + pings: [ + { + docId: 'fewjio21', + timestamp: '2019-01-28T17:47:08.078Z', + error: { + message: 'dial tcp 127.0.0.1:9200: connect: connection refused', + type: 'io', + }, + monitor: { + duration: { us: 1430 }, + id: 'auto-tcp-0X81440A68E839814F', + ip: '127.0.0.1', + name: '', + status: 'down', + type: 'tcp', + }, + }, + { + docId: 'fewjoo21', + timestamp: '2019-01-28T17:47:09.075Z', + error: { + message: 'dial tcp 127.0.0.1:9200: connect: connection refused', + type: 'io', + }, + monitor: { + duration: { us: 1370 }, + id: 'auto-tcp-0X81440A68E839814D', + ip: '127.0.0.1', + name: '', + status: 'down', + type: 'tcp', + }, + }, + { + docId: 'fejjio21', + timestamp: '2019-01-28T17:47:06.077Z', + monitor: { + duration: { us: 1452 }, + id: 'auto-tcp-0X81440A68E839814D', + ip: '127.0.0.1', + name: '', + status: 'up', + type: 'tcp', + }, + }, + { + docId: 'fewzio21', + timestamp: '2019-01-28T17:47:07.075Z', + error: { + message: 'dial tcp 127.0.0.1:9200: connect: connection refused', + type: 'io', + }, + monitor: { + duration: { us: 1094 }, + id: 'auto-tcp-0X81440A68E839814E', + ip: '127.0.0.1', + name: '', + status: 'down', + type: 'tcp', + }, + }, + { + docId: 'fewpi321', + timestamp: '2019-01-28T17:47:07.074Z', + error: { + message: + 'Get http://localhost:12349/: dial tcp 127.0.0.1:12349: connect: connection refused', + type: 'io', + }, + monitor: { + duration: { us: 1597 }, + id: 'auto-http-0X3675F89EF061209G', + ip: '127.0.0.1', + name: '', + status: 'down', + type: 'http', + }, + }, + { + docId: '0ewjio21', + timestamp: '2019-01-28T17:47:18.080Z', + error: { + message: 'dial tcp 127.0.0.1:9200: connect: connection refused', + type: 'io', + }, + monitor: { + duration: { us: 1699 }, + id: 'auto-tcp-0X81440A68E839814H', + ip: '127.0.0.1', + name: '', + status: 'down', + type: 'tcp', + }, + }, + { + docId: '3ewjio21', + timestamp: '2019-01-28T17:47:19.076Z', + error: { + message: 'dial tcp 127.0.0.1:9200: connect: connection refused', + type: 'io', + }, + monitor: { + duration: { us: 5384 }, + id: 'auto-tcp-0X81440A68E839814I', + ip: '127.0.0.1', + name: '', + status: 'down', + type: 'tcp', + }, + }, + { + docId: 'fewjip21', + timestamp: '2019-01-28T17:47:19.076Z', + error: { + message: + 'Get http://localhost:12349/: dial tcp 127.0.0.1:12349: connect: connection refused', + type: 'io', + }, + monitor: { + duration: { us: 5397 }, + id: 'auto-http-0X3675F89EF061209J', + ip: '127.0.0.1', + name: '', + status: 'down', + type: 'http', + }, + }, + { + docId: 'fewjio21', + timestamp: '2019-01-28T17:47:19.077Z', + http: { response: { status_code: 200 } }, + monitor: { + duration: { us: 127511 }, + id: 'auto-tcp-0X81440A68E839814C', + ip: '172.217.7.4', + name: '', + status: 'up', + type: 'http', + }, + }, + { + docId: 'fewjik81', + timestamp: '2019-01-28T17:47:19.077Z', + http: { response: { status_code: 200 } }, + monitor: { + duration: { us: 287543 }, + id: 'auto-http-0X131221E73F825974', + ip: '192.30.253.112', + name: '', + status: 'up', + type: 'http', + }, + }, + ], + }; + }); + + it('renders sorted list without errors', () => { + const component = shallowWithIntl( + <PingListComponent + dateRange={{ + from: 'now-15m', + to: 'now', + }} + getPings={jest.fn()} + lastRefresh={123} + loading={false} + locations={[]} + monitorId="foo" + pings={response.pings} + total={10} + /> + ); + expect(component).toMatchSnapshot(); + }); + + describe('toggleDetails', () => { + let itemIdToExpandedRowMap: ExpandedRowMap; + let pings: Ping[]; + + const setItemIdToExpandedRowMap = (update: ExpandedRowMap) => (itemIdToExpandedRowMap = update); + + beforeEach(() => { + itemIdToExpandedRowMap = {}; + pings = response.pings; + }); + + it('should expand an item if empty', () => { + const ping = pings[0]; + toggleDetails(ping, itemIdToExpandedRowMap, setItemIdToExpandedRowMap); + expect(itemIdToExpandedRowMap).toMatchInlineSnapshot(` + Object { + "fewjio21": <PingListExpandedRowComponent + ping={ + Object { + "docId": "fewjio21", + "error": Object { + "message": "dial tcp 127.0.0.1:9200: connect: connection refused", + "type": "io", + }, + "monitor": Object { + "duration": Object { + "us": 1430, + }, + "id": "auto-tcp-0X81440A68E839814F", + "ip": "127.0.0.1", + "name": "", + "status": "down", + "type": "tcp", + }, + "timestamp": "2019-01-28T17:47:08.078Z", + } + } + />, + } + `); + }); + + it('should un-expand an item if clicked again', () => { + const ping = pings[0]; + toggleDetails(ping, itemIdToExpandedRowMap, setItemIdToExpandedRowMap); + toggleDetails(ping, itemIdToExpandedRowMap, setItemIdToExpandedRowMap); + expect(itemIdToExpandedRowMap).toEqual({}); + }); + + it('should expand the new row and close the old when when a new row is clicked', () => { + const pingA = pings[0]; + const pingB = pings[1]; + toggleDetails(pingA, itemIdToExpandedRowMap, setItemIdToExpandedRowMap); + toggleDetails(pingB, itemIdToExpandedRowMap, setItemIdToExpandedRowMap); + expect(pingA.docId).not.toEqual(pingB.docId); + expect(itemIdToExpandedRowMap[pingB.docId]).toMatchInlineSnapshot(` + <PingListExpandedRowComponent + ping={ + Object { + "docId": "fewjoo21", + "error": Object { + "message": "dial tcp 127.0.0.1:9200: connect: connection refused", + "type": "io", + }, + "monitor": Object { + "duration": Object { + "us": 1370, + }, + "id": "auto-tcp-0X81440A68E839814D", + "ip": "127.0.0.1", + "name": "", + "status": "down", + "type": "tcp", + }, + "timestamp": "2019-01-28T17:47:09.075Z", + } + } + /> + `); + }); + }); +}); diff --git a/x-pack/legacy/plugins/uptime/public/components/functional/ping_list/doc_link_body.tsx b/x-pack/legacy/plugins/uptime/public/components/monitor/ping_list/doc_link_body.tsx similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/functional/ping_list/doc_link_body.tsx rename to x-pack/legacy/plugins/uptime/public/components/monitor/ping_list/doc_link_body.tsx diff --git a/x-pack/legacy/plugins/uptime/public/components/functional/ping_list/expanded_row.tsx b/x-pack/legacy/plugins/uptime/public/components/monitor/ping_list/expanded_row.tsx similarity index 94% rename from x-pack/legacy/plugins/uptime/public/components/functional/ping_list/expanded_row.tsx rename to x-pack/legacy/plugins/uptime/public/components/monitor/ping_list/expanded_row.tsx index c684235122e34..28b96fcb1bf7b 100644 --- a/x-pack/legacy/plugins/uptime/public/components/functional/ping_list/expanded_row.tsx +++ b/x-pack/legacy/plugins/uptime/public/components/monitor/ping_list/expanded_row.tsx @@ -16,14 +16,14 @@ import { } from '@elastic/eui'; import React from 'react'; import { i18n } from '@kbn/i18n'; -import { Ping, HttpBody } from '../../../../common/graphql/types'; +import { Ping, HttpResponseBody } from '../../../../common/runtime_types'; import { DocLinkForBody } from './doc_link_body'; interface Props { ping: Ping; } -const BodyDescription = ({ body }: { body: HttpBody }) => { +const BodyDescription = ({ body }: { body: HttpResponseBody }) => { const contentBytes = body.content_bytes || 0; const bodyBytes = body.bytes || 0; diff --git a/x-pack/legacy/plugins/uptime/public/components/monitor/ping_list/index.tsx b/x-pack/legacy/plugins/uptime/public/components/monitor/ping_list/index.tsx new file mode 100644 index 0000000000000..7fc19bbc9622b --- /dev/null +++ b/x-pack/legacy/plugins/uptime/public/components/monitor/ping_list/index.tsx @@ -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 { PingListComponent } from './ping_list'; +export { PingList } from './ping_list_container'; diff --git a/x-pack/legacy/plugins/uptime/public/components/functional/ping_list/location_name.tsx b/x-pack/legacy/plugins/uptime/public/components/monitor/ping_list/location_name.tsx similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/functional/ping_list/location_name.tsx rename to x-pack/legacy/plugins/uptime/public/components/monitor/ping_list/location_name.tsx diff --git a/x-pack/legacy/plugins/uptime/public/components/monitor/ping_list/ping_list.tsx b/x-pack/legacy/plugins/uptime/public/components/monitor/ping_list/ping_list.tsx new file mode 100644 index 0000000000000..5dfc1c0647430 --- /dev/null +++ b/x-pack/legacy/plugins/uptime/public/components/monitor/ping_list/ping_list.tsx @@ -0,0 +1,336 @@ +/* + * 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 { + EuiBadge, + EuiBasicTable, + EuiFlexGroup, + EuiFlexItem, + EuiHealth, + EuiPanel, + EuiSelect, + EuiSpacer, + EuiText, + EuiTitle, + EuiFormRow, + EuiButtonIcon, +} from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; +import moment from 'moment'; +import React, { useState, useEffect } from 'react'; +import styled from 'styled-components'; +import { Ping, GetPingsParams, DateRange } from '../../../../common/runtime_types'; +import { convertMicrosecondsToMilliseconds as microsToMillis } from '../../../lib/helper'; +import { LocationName } from './location_name'; +import { Pagination } from '../../overview/monitor_list'; +import { PingListExpandedRowComponent } from './expanded_row'; +import { PingListProps } from './ping_list_container'; + +export const AllLocationOption = { + 'data-test-subj': 'xpack.uptime.pingList.locationOptions.all', + text: 'All', + value: '', +}; + +export const toggleDetails = ( + ping: Ping, + expandedRows: Record<string, JSX.Element>, + setExpandedRows: (update: Record<string, JSX.Element>) => any +) => { + // If already expanded, collapse + if (expandedRows[ping.docId]) { + delete expandedRows[ping.docId]; + setExpandedRows({ ...expandedRows }); + return; + } + + // Otherwise expand this row + setExpandedRows({ + ...expandedRows, + [ping.docId]: <PingListExpandedRowComponent ping={ping} />, + }); +}; + +const SpanWithMargin = styled.span` + margin-right: 16px; +`; + +interface Props extends PingListProps { + dateRange: DateRange; + error?: Error; + getPings: (props: GetPingsParams) => void; + lastRefresh: number; + loading: boolean; + locations: string[]; + pings: Ping[]; + total: number; +} + +const DEFAULT_PAGE_SIZE = 10; + +const statusOptions = [ + { + 'data-test-subj': 'xpack.uptime.pingList.statusOptions.all', + text: i18n.translate('xpack.uptime.pingList.statusOptions.allStatusOptionLabel', { + defaultMessage: 'All', + }), + value: '', + }, + { + 'data-test-subj': 'xpack.uptime.pingList.statusOptions.up', + text: i18n.translate('xpack.uptime.pingList.statusOptions.upStatusOptionLabel', { + defaultMessage: 'Up', + }), + value: 'up', + }, + { + 'data-test-subj': 'xpack.uptime.pingList.statusOptions.down', + text: i18n.translate('xpack.uptime.pingList.statusOptions.downStatusOptionLabel', { + defaultMessage: 'Down', + }), + value: 'down', + }, +]; + +export const PingListComponent = (props: Props) => { + const [selectedLocation, setSelectedLocation] = useState<string>(''); + const [status, setStatus] = useState<string>(''); + const [pageSize, setPageSize] = useState(DEFAULT_PAGE_SIZE); + const [pageIndex, setPageIndex] = useState(0); + const { + dateRange: { from, to }, + error, + getPings, + lastRefresh, + loading, + locations, + monitorId, + pings, + total, + } = props; + + useEffect(() => { + getPings({ + dateRange: { + from, + to, + }, + location: selectedLocation, + monitorId, + index: pageIndex, + size: pageSize, + status: status !== 'all' ? status : '', + }); + }, [from, to, getPings, monitorId, lastRefresh, selectedLocation, pageIndex, pageSize, status]); + + const [expandedRows, setExpandedRows] = useState<Record<string, JSX.Element>>({}); + + const locationOptions = !locations + ? [AllLocationOption] + : [AllLocationOption].concat( + locations.map(name => ({ + text: name, + 'data-test-subj': `xpack.uptime.pingList.locationOptions.${name}`, + value: name, + })) + ); + + const hasStatus = pings.reduce( + (hasHttpStatus: boolean, currentPing) => + hasHttpStatus || !!currentPing.http?.response?.status_code, + false + ); + + const columns: any[] = [ + { + field: 'monitor.status', + name: i18n.translate('xpack.uptime.pingList.statusColumnLabel', { + defaultMessage: 'Status', + }), + render: (pingStatus: string, item: Ping) => ( + <div data-test-subj={`xpack.uptime.pingList.ping-${item.docId}`}> + <EuiHealth color={pingStatus === 'up' ? 'success' : 'danger'}> + {pingStatus === 'up' + ? i18n.translate('xpack.uptime.pingList.statusColumnHealthUpLabel', { + defaultMessage: 'Up', + }) + : i18n.translate('xpack.uptime.pingList.statusColumnHealthDownLabel', { + defaultMessage: 'Down', + })} + </EuiHealth> + <EuiText size="xs" color="subdued"> + {i18n.translate('xpack.uptime.pingList.recencyMessage', { + values: { fromNow: moment(item.timestamp).fromNow() }, + defaultMessage: 'Checked {fromNow}', + description: + 'A string used to inform our users how long ago Heartbeat pinged the selected host.', + })} + </EuiText> + </div> + ), + }, + { + align: 'left', + field: 'observer.geo.name', + name: i18n.translate('xpack.uptime.pingList.locationNameColumnLabel', { + defaultMessage: 'Location', + }), + render: (location: string) => <LocationName location={location} />, + }, + { + align: 'right', + dataType: 'number', + field: 'monitor.ip', + name: i18n.translate('xpack.uptime.pingList.ipAddressColumnLabel', { + defaultMessage: 'IP', + }), + }, + { + align: 'right', + field: 'monitor.duration.us', + name: i18n.translate('xpack.uptime.pingList.durationMsColumnLabel', { + defaultMessage: 'Duration', + }), + render: (duration: number) => + i18n.translate('xpack.uptime.pingList.durationMsColumnFormatting', { + values: { millis: microsToMillis(duration) }, + defaultMessage: '{millis} ms', + }), + }, + { + align: hasStatus ? 'right' : 'center', + field: 'error.type', + name: i18n.translate('xpack.uptime.pingList.errorTypeColumnLabel', { + defaultMessage: 'Error type', + }), + render: (errorType: string) => errorType ?? '-', + }, + // Only add this column is there is any status present in list + ...(hasStatus + ? [ + { + field: 'http.response.status_code', + align: 'right', + name: ( + <SpanWithMargin> + {i18n.translate('xpack.uptime.pingList.responseCodeColumnLabel', { + defaultMessage: 'Response code', + })} + </SpanWithMargin> + ), + render: (statusCode: string) => ( + <SpanWithMargin> + <EuiBadge>{statusCode}</EuiBadge> + </SpanWithMargin> + ), + }, + ] + : []), + { + align: 'right', + width: '24px', + isExpander: true, + render: (item: Ping) => { + return ( + <EuiButtonIcon + onClick={() => toggleDetails(item, expandedRows, setExpandedRows)} + disabled={!item.error && !(item.http?.response?.body?.bytes ?? 0 > 0)} + aria-label={ + expandedRows[item.docId] + ? i18n.translate('xpack.uptime.pingList.collapseRow', { + defaultMessage: 'Collapse', + }) + : i18n.translate('xpack.uptime.pingList.expandRow', { defaultMessage: 'Expand' }) + } + iconType={expandedRows[item.docId] ? 'arrowUp' : 'arrowDown'} + /> + ); + }, + }, + ]; + + const pagination: Pagination = { + initialPageSize: DEFAULT_PAGE_SIZE, + pageIndex, + pageSize, + pageSizeOptions: [10, 25, 50, 100], + /** + * we're not currently supporting pagination in this component + * so the first page is the only page + */ + totalItemCount: total, + }; + + return ( + <EuiPanel> + <EuiTitle size="xs"> + <h4> + <FormattedMessage id="xpack.uptime.pingList.checkHistoryTitle" defaultMessage="History" /> + </h4> + </EuiTitle> + <EuiSpacer size="s" /> + <EuiFlexGroup justifyContent="spaceBetween"> + <EuiFlexItem grow={false}> + <EuiFormRow + label="Status" + aria-label={i18n.translate('xpack.uptime.pingList.statusLabel', { + defaultMessage: 'Status', + })} + > + <EuiSelect + options={statusOptions} + aria-label={i18n.translate('xpack.uptime.pingList.statusLabel', { + defaultMessage: 'Status', + })} + data-test-subj="xpack.uptime.pingList.statusSelect" + value={status} + onChange={selected => { + setStatus(selected.target.value); + }} + /> + </EuiFormRow> + </EuiFlexItem> + <EuiFlexItem> + <EuiFormRow + label="Location" + aria-label={i18n.translate('xpack.uptime.pingList.locationLabel', { + defaultMessage: 'Location', + })} + > + <EuiSelect + options={locationOptions} + value={selectedLocation} + aria-label={i18n.translate('xpack.uptime.pingList.locationLabel', { + defaultMessage: 'Location', + })} + data-test-subj="xpack.uptime.pingList.locationSelect" + onChange={selected => { + setSelectedLocation(selected.target.value); + }} + /> + </EuiFormRow> + </EuiFlexItem> + </EuiFlexGroup> + <EuiSpacer size="s" /> + <EuiBasicTable + loading={loading} + columns={columns} + error={error?.message} + isExpandable={true} + hasActions={true} + items={pings} + itemId="docId" + itemIdToExpandedRowMap={expandedRows} + pagination={pagination} + onChange={(criteria: any) => { + setPageSize(criteria.page!.size); + setPageIndex(criteria.page!.index); + }} + /> + </EuiPanel> + ); +}; diff --git a/x-pack/legacy/plugins/uptime/public/components/monitor/ping_list/ping_list_container.tsx b/x-pack/legacy/plugins/uptime/public/components/monitor/ping_list/ping_list_container.tsx new file mode 100644 index 0000000000000..3c3caab365e3a --- /dev/null +++ b/x-pack/legacy/plugins/uptime/public/components/monitor/ping_list/ping_list_container.tsx @@ -0,0 +1,52 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { useSelector, useDispatch } from 'react-redux'; +import React, { useContext, useCallback } from 'react'; +import { selectPingList } from '../../../state/selectors'; +import { getPings } from '../../../state/actions'; +import { GetPingsParams } from '../../../../common/runtime_types'; +import { UptimeSettingsContext } from '../../../contexts'; +import { PingListComponent } from './index'; + +export interface PingListProps { + monitorId: string; +} + +export const PingList = (props: PingListProps) => { + const { + lastRefresh, + pings: { + error, + loading, + pingList: { locations, pings, total }, + }, + } = useSelector(selectPingList); + + const { dateRangeStart: drs, dateRangeEnd: dre } = useContext(UptimeSettingsContext); + + const dispatch = useDispatch(); + const getPingsCallback = useCallback((params: GetPingsParams) => dispatch(getPings(params)), [ + dispatch, + ]); + + return ( + <PingListComponent + dateRange={{ + from: drs, + to: dre, + }} + error={error} + getPings={getPingsCallback} + lastRefresh={lastRefresh} + loading={loading} + locations={locations} + pings={pings} + total={total} + {...props} + /> + ); +}; diff --git a/x-pack/legacy/plugins/uptime/public/components/functional/__tests__/__snapshots__/overview_page_parsing_error_callout.test.tsx.snap b/x-pack/legacy/plugins/uptime/public/components/overview/__tests__/__snapshots__/parsing_error_callout.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/functional/__tests__/__snapshots__/overview_page_parsing_error_callout.test.tsx.snap rename to x-pack/legacy/plugins/uptime/public/components/overview/__tests__/__snapshots__/parsing_error_callout.test.tsx.snap diff --git a/x-pack/legacy/plugins/uptime/public/components/functional/__tests__/__snapshots__/snapshot.test.tsx.snap b/x-pack/legacy/plugins/uptime/public/components/overview/__tests__/__snapshots__/snapshot.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/functional/__tests__/__snapshots__/snapshot.test.tsx.snap rename to x-pack/legacy/plugins/uptime/public/components/overview/__tests__/__snapshots__/snapshot.test.tsx.snap diff --git a/x-pack/legacy/plugins/uptime/public/components/functional/__tests__/__snapshots__/snapshot_heading.test.tsx.snap b/x-pack/legacy/plugins/uptime/public/components/overview/__tests__/__snapshots__/snapshot_heading.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/functional/__tests__/__snapshots__/snapshot_heading.test.tsx.snap rename to x-pack/legacy/plugins/uptime/public/components/overview/__tests__/__snapshots__/snapshot_heading.test.tsx.snap diff --git a/x-pack/legacy/plugins/uptime/public/components/overview/__tests__/parsing_error_callout.test.tsx b/x-pack/legacy/plugins/uptime/public/components/overview/__tests__/parsing_error_callout.test.tsx new file mode 100644 index 0000000000000..01204c33b79d5 --- /dev/null +++ b/x-pack/legacy/plugins/uptime/public/components/overview/__tests__/parsing_error_callout.test.tsx @@ -0,0 +1,26 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { shallowWithIntl } from 'test_utils/enzyme_helpers'; +import { ParsingErrorCallout } from '../parsing_error_callout'; + +describe('OverviewPageParsingErrorCallout', () => { + it('renders without errors when a valid error is provided', () => { + expect( + shallowWithIntl( + <ParsingErrorCallout + error={{ message: 'Unable to convert to Elasticsearch query, invalid syntax.' }} + /> + ) + ).toMatchSnapshot(); + }); + + it('renders without errors when an error with no message is provided', () => { + const error: any = {}; + expect(shallowWithIntl(<ParsingErrorCallout error={error} />)).toMatchSnapshot(); + }); +}); diff --git a/x-pack/legacy/plugins/uptime/public/components/functional/__tests__/snapshot.test.tsx b/x-pack/legacy/plugins/uptime/public/components/overview/__tests__/snapshot.test.tsx similarity index 92% rename from x-pack/legacy/plugins/uptime/public/components/functional/__tests__/snapshot.test.tsx rename to x-pack/legacy/plugins/uptime/public/components/overview/__tests__/snapshot.test.tsx index 214b0394369f7..cfcab673dcb35 100644 --- a/x-pack/legacy/plugins/uptime/public/components/functional/__tests__/snapshot.test.tsx +++ b/x-pack/legacy/plugins/uptime/public/components/overview/__tests__/snapshot.test.tsx @@ -7,7 +7,7 @@ import React from 'react'; import { shallowWithIntl } from 'test_utils/enzyme_helpers'; import { Snapshot } from '../../../../common/runtime_types'; -import { SnapshotComponent } from '../snapshot'; +import { SnapshotComponent } from '../snapshot/snapshot'; describe('Snapshot component', () => { const snapshot: Snapshot = { diff --git a/x-pack/legacy/plugins/uptime/public/components/functional/__tests__/snapshot_heading.test.tsx b/x-pack/legacy/plugins/uptime/public/components/overview/__tests__/snapshot_heading.test.tsx similarity index 93% rename from x-pack/legacy/plugins/uptime/public/components/functional/__tests__/snapshot_heading.test.tsx rename to x-pack/legacy/plugins/uptime/public/components/overview/__tests__/snapshot_heading.test.tsx index 70d082b26d653..805c116ef538a 100644 --- a/x-pack/legacy/plugins/uptime/public/components/functional/__tests__/snapshot_heading.test.tsx +++ b/x-pack/legacy/plugins/uptime/public/components/overview/__tests__/snapshot_heading.test.tsx @@ -6,7 +6,7 @@ import { shallowWithIntl } from 'test_utils/enzyme_helpers'; import React from 'react'; -import { SnapshotHeading } from '../snapshot_heading'; +import { SnapshotHeading } from '../snapshot/snapshot_heading'; describe('SnapshotHeading', () => { it('renders custom heading for no down monitors', () => { diff --git a/x-pack/legacy/plugins/uptime/public/components/functional/alerts/__tests__/alert_monitor_status.test.tsx b/x-pack/legacy/plugins/uptime/public/components/overview/alerts/__tests__/alert_monitor_status.test.tsx similarity index 98% rename from x-pack/legacy/plugins/uptime/public/components/functional/alerts/__tests__/alert_monitor_status.test.tsx rename to x-pack/legacy/plugins/uptime/public/components/overview/alerts/__tests__/alert_monitor_status.test.tsx index af8d17d1fc242..8f33b6f652b9d 100644 --- a/x-pack/legacy/plugins/uptime/public/components/functional/alerts/__tests__/alert_monitor_status.test.tsx +++ b/x-pack/legacy/plugins/uptime/public/components/overview/alerts/__tests__/alert_monitor_status.test.tsx @@ -14,8 +14,8 @@ import { mountWithIntl } from 'test_utils/enzyme_helpers'; describe('alert monitor status component', () => { describe('handleAlertFieldNumberChange', () => { - let mockSetIsInvalid: jest.Mock<any, any>; - let mockSetFieldValue: jest.Mock<any, any>; + let mockSetIsInvalid: jest.Mock; + let mockSetFieldValue: jest.Mock; beforeEach(() => { mockSetIsInvalid = jest.fn(); diff --git a/x-pack/legacy/plugins/uptime/public/components/overview/alerts/alert_monitor_status.tsx b/x-pack/legacy/plugins/uptime/public/components/overview/alerts/alert_monitor_status.tsx new file mode 100644 index 0000000000000..83892bf23dced --- /dev/null +++ b/x-pack/legacy/plugins/uptime/public/components/overview/alerts/alert_monitor_status.tsx @@ -0,0 +1,445 @@ +/* + * 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, { useState, useEffect } from 'react'; +import { + EuiExpression, + EuiFieldNumber, + EuiFlexGroup, + EuiFlexItem, + EuiPopover, + EuiSelectable, + EuiSpacer, + EuiSwitch, + EuiTitle, +} from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { DataPublicPluginSetup } from 'src/plugins/data/public'; +import { KueryBar } from '..'; + +interface AlertFieldNumberProps { + 'aria-label': string; + 'data-test-subj': string; + disabled: boolean; + fieldValue: number; + setFieldValue: React.Dispatch<React.SetStateAction<number>>; +} + +export const handleAlertFieldNumberChange = ( + e: React.ChangeEvent<HTMLInputElement>, + isInvalid: boolean, + setIsInvalid: React.Dispatch<React.SetStateAction<boolean>>, + setFieldValue: React.Dispatch<React.SetStateAction<number>> +) => { + const num = parseInt(e.target.value, 10); + if (isNaN(num) || num < 1) { + setIsInvalid(true); + } else { + if (isInvalid) setIsInvalid(false); + setFieldValue(num); + } +}; + +export const AlertFieldNumber = ({ + 'aria-label': ariaLabel, + 'data-test-subj': dataTestSubj, + disabled, + fieldValue, + setFieldValue, +}: AlertFieldNumberProps) => { + const [isInvalid, setIsInvalid] = useState<boolean>(false); + + return ( + <EuiFieldNumber + aria-label={ariaLabel} + compressed + data-test-subj={dataTestSubj} + min={1} + onChange={e => handleAlertFieldNumberChange(e, isInvalid, setIsInvalid, setFieldValue)} + disabled={disabled} + value={fieldValue} + isInvalid={isInvalid} + /> + ); +}; + +interface AlertExpressionPopoverProps { + 'aria-label': string; + content: React.ReactElement; + description: string; + 'data-test-subj': string; + id: string; + value: string; +} + +const AlertExpressionPopover: React.FC<AlertExpressionPopoverProps> = ({ + 'aria-label': ariaLabel, + content, + 'data-test-subj': dataTestSubj, + description, + id, + value, +}) => { + const [isOpen, setIsOpen] = useState<boolean>(false); + return ( + <EuiPopover + id={id} + anchorPosition="downLeft" + button={ + <EuiExpression + aria-label={ariaLabel} + color={isOpen ? 'primary' : 'secondary'} + data-test-subj={dataTestSubj} + description={description} + isActive={isOpen} + onClick={() => setIsOpen(!isOpen)} + value={value} + /> + } + isOpen={isOpen} + closePopover={() => setIsOpen(false)} + > + {content} + </EuiPopover> + ); +}; + +export const selectedLocationsToString = (selectedLocations: any[]) => + // create a nicely-formatted description string for all `on` locations + selectedLocations + .filter(({ checked }) => checked === 'on') + .map(({ label }) => label) + .sort() + .reduce((acc, cur) => { + if (acc === '') { + return cur; + } + return acc + `, ${cur}`; + }, ''); + +interface AlertMonitorStatusProps { + autocomplete: DataPublicPluginSetup['autocomplete']; + enabled: boolean; + filters: string; + locations: string[]; + numTimes: number; + setAlertParams: (key: string, value: any) => void; + timerange: { + from: string; + to: string; + }; +} + +export const AlertMonitorStatusComponent: React.FC<AlertMonitorStatusProps> = props => { + const { filters, locations } = props; + const [numTimes, setNumTimes] = useState<number>(5); + const [numMins, setNumMins] = useState<number>(15); + const [allLabels, setAllLabels] = useState<boolean>(true); + + // locations is an array of `Option[]`, but that type doesn't seem to be exported by EUI + const [selectedLocations, setSelectedLocations] = useState<any[]>( + locations.map(location => ({ + 'aria-label': i18n.translate('xpack.uptime.alerts.locationSelectionItem.ariaLabel', { + defaultMessage: 'Location selection item for "{location}"', + values: { + location, + }, + }), + disabled: allLabels, + label: location, + })) + ); + const [timerangeUnitOptions, setTimerangeUnitOptions] = useState<any[]>([ + { + 'aria-label': i18n.translate( + 'xpack.uptime.alerts.timerangeUnitSelectable.secondsOption.ariaLabel', + { + defaultMessage: '"Seconds" time range select item', + } + ), + 'data-test-subj': 'xpack.uptime.alerts.monitorStatus.timerangeUnitSelectable.secondsOption', + key: 's', + label: i18n.translate('xpack.uptime.alerts.monitorStatus.timerangeOption.seconds', { + defaultMessage: 'seconds', + }), + }, + { + 'aria-label': i18n.translate( + 'xpack.uptime.alerts.timerangeUnitSelectable.minutesOption.ariaLabel', + { + defaultMessage: '"Minutes" time range select item', + } + ), + 'data-test-subj': 'xpack.uptime.alerts.monitorStatus.timerangeUnitSelectable.minutesOption', + checked: 'on', + key: 'm', + label: i18n.translate('xpack.uptime.alerts.monitorStatus.timerangeOption.minutes', { + defaultMessage: 'minutes', + }), + }, + { + 'aria-label': i18n.translate( + 'xpack.uptime.alerts.timerangeUnitSelectable.hoursOption.ariaLabel', + { + defaultMessage: '"Hours" time range select item', + } + ), + 'data-test-subj': 'xpack.uptime.alerts.monitorStatus.timerangeUnitSelectable.hoursOption', + key: 'h', + label: i18n.translate('xpack.uptime.alerts.monitorStatus.timerangeOption.hours', { + defaultMessage: 'hours', + }), + }, + { + 'aria-label': i18n.translate( + 'xpack.uptime.alerts.timerangeUnitSelectable.daysOption.ariaLabel', + { + defaultMessage: '"Days" time range select item', + } + ), + 'data-test-subj': 'xpack.uptime.alerts.monitorStatus.timerangeUnitSelectable.daysOption', + key: 'd', + label: i18n.translate('xpack.uptime.alerts.monitorStatus.timerangeOption.days', { + defaultMessage: 'days', + }), + }, + ]); + + const { setAlertParams } = props; + + useEffect(() => { + setAlertParams('numTimes', numTimes); + }, [numTimes, setAlertParams]); + + useEffect(() => { + const timerangeUnit = timerangeUnitOptions.find(({ checked }) => checked === 'on')?.key ?? 'm'; + setAlertParams('timerange', { from: `now-${numMins}${timerangeUnit}`, to: 'now' }); + }, [numMins, timerangeUnitOptions, setAlertParams]); + + useEffect(() => { + if (allLabels) { + setAlertParams('locations', []); + } else { + setAlertParams( + 'locations', + selectedLocations.filter(l => l.checked === 'on').map(l => l.label) + ); + } + }, [selectedLocations, setAlertParams, allLabels]); + + useEffect(() => { + setAlertParams('filters', filters); + }, [filters, setAlertParams]); + + return ( + <> + <EuiSpacer size="m" /> + <KueryBar + aria-label={i18n.translate('xpack.uptime.alerts.monitorStatus.filterBar.ariaLabel', { + defaultMessage: 'Input that allows filtering criteria for the monitor status alert', + })} + autocomplete={props.autocomplete} + data-test-subj="xpack.uptime.alerts.monitorStatus.filterBar" + /> + <EuiSpacer size="s" /> + <AlertExpressionPopover + aria-label={i18n.translate( + 'xpack.uptime.alerts.monitorStatus.numTimesExpression.ariaLabel', + { + defaultMessage: 'Open the popover for down count input', + } + )} + content={ + <AlertFieldNumber + aria-label={i18n.translate( + 'xpack.uptime.alerts.monitorStatus.numTimesField.ariaLabel', + { + defaultMessage: 'Enter number of down counts required to trigger the alert', + } + )} + data-test-subj="xpack.uptime.alerts.monitorStatus.numTimesField" + disabled={false} + fieldValue={numTimes} + setFieldValue={setNumTimes} + /> + } + data-test-subj="xpack.uptime.alerts.monitorStatus.numTimesExpression" + description={ + filters + ? i18n.translate( + 'xpack.uptime.alerts.monitorStatus.numTimesExpression.matchingMonitors.description', + { + defaultMessage: 'matching monitors are down >', + } + ) + : i18n.translate( + 'xpack.uptime.alerts.monitorStatus.numTimesExpression.anyMonitors.description', + { + defaultMessage: 'any monitor is down >', + } + ) + } + id="ping-count" + value={`${numTimes} times`} + /> + <EuiSpacer size="xs" /> + <EuiFlexGroup gutterSize="s"> + <EuiFlexItem grow={false}> + <AlertExpressionPopover + aria-label={i18n.translate( + 'xpack.uptime.alerts.monitorStatus.timerangeValueExpression.ariaLabel', + { + defaultMessage: 'Open the popover for time range value field', + } + )} + content={ + <AlertFieldNumber + aria-label={i18n.translate( + 'xpack.uptime.alerts.monitorStatus.timerangeValueField.ariaLabel', + { + defaultMessage: `Enter the number of time units for the alert's range`, + } + )} + data-test-subj="xpack.uptime.alerts.monitorStatus.timerangeValueField" + disabled={false} + fieldValue={numMins} + setFieldValue={setNumMins} + /> + } + data-test-subj="xpack.uptime.alerts.monitorStatus.timerangeValueExpression" + description="within" + id="timerange" + value={`last ${numMins}`} + /> + </EuiFlexItem> + <EuiFlexItem> + <AlertExpressionPopover + aria-label={i18n.translate( + 'xpack.uptime.alerts.monitorStatus.timerangeUnitExpression.ariaLabel', + { + defaultMessage: 'Open the popover for time range unit select field', + } + )} + content={ + <> + <EuiTitle size="xxs"> + <h5> + <FormattedMessage + id="xpack.uptime.alerts.monitorStatus.timerangeSelectionHeader" + defaultMessage="Select time range unit" + /> + </h5> + </EuiTitle> + <EuiSelectable + aria-label={i18n.translate( + 'xpack.uptime.alerts.monitorStatus.timerangeUnitSelectable', + { + defaultMessage: 'Selectable field for the time range units alerts should use', + } + )} + data-test-subj="xpack.uptime.alerts.monitorStatus.timerangeUnitSelectable" + options={timerangeUnitOptions} + onChange={newOptions => { + if (newOptions.reduce((acc, { checked }) => acc || checked === 'on', false)) { + setTimerangeUnitOptions(newOptions); + } + }} + singleSelection={true} + listProps={{ + showIcons: true, + }} + > + {list => list} + </EuiSelectable> + </> + } + data-test-subj="xpack.uptime.alerts.monitorStatus.timerangeUnitExpression" + description="" + id="timerange-unit" + value={ + timerangeUnitOptions.find(({ checked }) => checked === 'on')?.label.toLowerCase() ?? + '' + } + /> + </EuiFlexItem> + </EuiFlexGroup> + <EuiSpacer size="xs" /> + {selectedLocations.length === 0 && ( + <EuiExpression + color="secondary" + data-test-subj="xpack.uptime.alerts.monitorStatus.locationsEmpty" + description="in" + isActive={false} + value="all locations" + /> + )} + {selectedLocations.length > 0 && ( + <AlertExpressionPopover + aria-label={i18n.translate( + 'xpack.uptime.alerts.monitorStatus.locationsSelectionExpression.ariaLabel', + { + defaultMessage: 'Open the popover to select locations the alert should trigger', + } + )} + content={ + <EuiFlexGroup direction="column"> + <EuiFlexItem> + <EuiSwitch + aria-label={i18n.translate( + 'xpack.uptime.alerts.monitorStatus.locationSelectionSwitch.ariaLabel', + { + defaultMessage: 'Select the locations the alert should trigger', + } + )} + data-test-subj="xpack.uptime.alerts.monitorStatus.locationsSelectionSwitch" + label="Check all locations" + checked={allLabels} + onChange={() => { + setAllLabels(!allLabels); + setSelectedLocations( + selectedLocations.map((l: any) => ({ + 'aria-label': i18n.translate( + 'xpack.uptime.alerts.monitorStatus.locationSelection', + { + defaultMessage: 'Select the location {location}', + values: { + location: l, + }, + } + ), + ...l, + 'data-test-subj': `xpack.uptime.alerts.monitorStatus.locationSelection.${l.label}LocationOption`, + disabled: !allLabels, + })) + ); + }} + /> + </EuiFlexItem> + <EuiFlexItem> + <EuiSelectable + data-test-subj="xpack.uptime.alerts.monitorStatus.locationsSelectionSelectable" + options={selectedLocations} + onChange={e => setSelectedLocations(e)} + > + {location => location} + </EuiSelectable> + </EuiFlexItem> + </EuiFlexGroup> + } + data-test-subj="xpack.uptime.alerts.monitorStatus.locationsSelectionExpression" + description="from" + id="locations" + value={ + selectedLocations.length === 0 || allLabels + ? 'any location' + : selectedLocationsToString(selectedLocations) + } + /> + )} + </> + ); +}; diff --git a/x-pack/legacy/plugins/uptime/public/components/overview/alerts/alerts_containers/alert_monitor_status.tsx b/x-pack/legacy/plugins/uptime/public/components/overview/alerts/alerts_containers/alert_monitor_status.tsx new file mode 100644 index 0000000000000..9dd27db0be607 --- /dev/null +++ b/x-pack/legacy/plugins/uptime/public/components/overview/alerts/alerts_containers/alert_monitor_status.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 from 'react'; +import { useSelector } from 'react-redux'; +import { DataPublicPluginSetup } from 'src/plugins/data/public'; +import { selectMonitorStatusAlert } from '../../../../state/selectors'; +import { AlertMonitorStatusComponent } from '../index'; + +interface Props { + autocomplete: DataPublicPluginSetup['autocomplete']; + enabled: boolean; + numTimes: number; + setAlertParams: (key: string, value: any) => void; + timerange: { + from: string; + to: string; + }; +} + +export const AlertMonitorStatus = ({ + autocomplete, + enabled, + numTimes, + setAlertParams, + timerange, +}: Props) => { + const { filters, locations } = useSelector(selectMonitorStatusAlert); + return ( + <AlertMonitorStatusComponent + autocomplete={autocomplete} + enabled={enabled} + filters={filters} + locations={locations} + numTimes={numTimes} + setAlertParams={setAlertParams} + timerange={timerange} + /> + ); +}; diff --git a/x-pack/legacy/plugins/uptime/public/components/connected/alerts/index.ts b/x-pack/legacy/plugins/uptime/public/components/overview/alerts/alerts_containers/index.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/connected/alerts/index.ts rename to x-pack/legacy/plugins/uptime/public/components/overview/alerts/alerts_containers/index.ts diff --git a/x-pack/legacy/plugins/uptime/public/components/connected/alerts/toggle_alert_flyout_button.tsx b/x-pack/legacy/plugins/uptime/public/components/overview/alerts/alerts_containers/toggle_alert_flyout_button.tsx similarity index 80% rename from x-pack/legacy/plugins/uptime/public/components/connected/alerts/toggle_alert_flyout_button.tsx rename to x-pack/legacy/plugins/uptime/public/components/overview/alerts/alerts_containers/toggle_alert_flyout_button.tsx index 43b0be45365a1..45ba72d76fba6 100644 --- a/x-pack/legacy/plugins/uptime/public/components/connected/alerts/toggle_alert_flyout_button.tsx +++ b/x-pack/legacy/plugins/uptime/public/components/overview/alerts/alerts_containers/toggle_alert_flyout_button.tsx @@ -6,8 +6,8 @@ import React from 'react'; import { useDispatch } from 'react-redux'; -import { ToggleAlertFlyoutButtonComponent } from '../../functional'; -import { setAlertFlyoutVisible } from '../../../state/actions'; +import { setAlertFlyoutVisible } from '../../../../state/actions'; +import { ToggleAlertFlyoutButtonComponent } from '../index'; export const ToggleAlertFlyoutButton = () => { const dispatch = useDispatch(); diff --git a/x-pack/legacy/plugins/uptime/public/components/connected/alerts/uptime_alerts_flyout_wrapper.tsx b/x-pack/legacy/plugins/uptime/public/components/overview/alerts/alerts_containers/uptime_alerts_flyout_wrapper.tsx similarity index 83% rename from x-pack/legacy/plugins/uptime/public/components/connected/alerts/uptime_alerts_flyout_wrapper.tsx rename to x-pack/legacy/plugins/uptime/public/components/overview/alerts/alerts_containers/uptime_alerts_flyout_wrapper.tsx index a49468ad3dd06..7bfd44a762455 100644 --- a/x-pack/legacy/plugins/uptime/public/components/connected/alerts/uptime_alerts_flyout_wrapper.tsx +++ b/x-pack/legacy/plugins/uptime/public/components/overview/alerts/alerts_containers/uptime_alerts_flyout_wrapper.tsx @@ -6,9 +6,9 @@ import React from 'react'; import { useDispatch, useSelector } from 'react-redux'; -import { UptimeAlertsFlyoutWrapperComponent } from '../../functional'; -import { setAlertFlyoutVisible } from '../../../state/actions'; -import { selectAlertFlyoutVisibility } from '../../../state/selectors'; +import { setAlertFlyoutVisible } from '../../../../state/actions'; +import { selectAlertFlyoutVisibility } from '../../../../state/selectors'; +import { UptimeAlertsFlyoutWrapperComponent } from '../index'; interface Props { alertTypeId?: string; diff --git a/x-pack/legacy/plugins/uptime/public/components/overview/alerts/index.ts b/x-pack/legacy/plugins/uptime/public/components/overview/alerts/index.ts new file mode 100644 index 0000000000000..5ca0f4c3fe8a7 --- /dev/null +++ b/x-pack/legacy/plugins/uptime/public/components/overview/alerts/index.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. + */ + +export { AlertMonitorStatusComponent } from './alert_monitor_status'; +export { ToggleAlertFlyoutButtonComponent } from './toggle_alert_flyout_button'; +export { UptimeAlertsContextProvider } from './uptime_alerts_context_provider'; +export { UptimeAlertsFlyoutWrapperComponent } from './uptime_alerts_flyout_wrapper'; +export * from './alerts_containers'; diff --git a/x-pack/legacy/plugins/uptime/public/components/functional/alerts/toggle_alert_flyout_button.tsx b/x-pack/legacy/plugins/uptime/public/components/overview/alerts/toggle_alert_flyout_button.tsx similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/functional/alerts/toggle_alert_flyout_button.tsx rename to x-pack/legacy/plugins/uptime/public/components/overview/alerts/toggle_alert_flyout_button.tsx diff --git a/x-pack/legacy/plugins/uptime/public/components/functional/alerts/uptime_alerts_context_provider.tsx b/x-pack/legacy/plugins/uptime/public/components/overview/alerts/uptime_alerts_context_provider.tsx similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/functional/alerts/uptime_alerts_context_provider.tsx rename to x-pack/legacy/plugins/uptime/public/components/overview/alerts/uptime_alerts_context_provider.tsx diff --git a/x-pack/legacy/plugins/uptime/public/components/functional/alerts/uptime_alerts_flyout_wrapper.tsx b/x-pack/legacy/plugins/uptime/public/components/overview/alerts/uptime_alerts_flyout_wrapper.tsx similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/functional/alerts/uptime_alerts_flyout_wrapper.tsx rename to x-pack/legacy/plugins/uptime/public/components/overview/alerts/uptime_alerts_flyout_wrapper.tsx diff --git a/x-pack/legacy/plugins/uptime/public/components/overview/empty_state/__tests__/__snapshots__/data_or_index_missing.test.tsx.snap b/x-pack/legacy/plugins/uptime/public/components/overview/empty_state/__tests__/__snapshots__/data_or_index_missing.test.tsx.snap new file mode 100644 index 0000000000000..25ac5a1f0974e --- /dev/null +++ b/x-pack/legacy/plugins/uptime/public/components/overview/empty_state/__tests__/__snapshots__/data_or_index_missing.test.tsx.snap @@ -0,0 +1,92 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`DataOrIndexMissing component renders headingMessage 1`] = ` +<EuiFlexGroup + data-test-subj="data-missing" + justifyContent="center" +> + <EuiFlexItem + grow={false} + style={ + Object { + "flexBasis": 700, + } + } + > + <EuiSpacer + size="m" + /> + <EuiPanel> + <EuiEmptyPrompt + actions={ + <ForwardRef> + <EuiFlexItem> + <EuiButton + color="primary" + fill={true} + href="/app/kibana#/home/tutorial/uptimeMonitors" + > + <FormattedMessage + defaultMessage="View setup instructions" + id="xpack.uptime.emptyState.viewSetupInstructions" + values={Object {}} + /> + </EuiButton> + </EuiFlexItem> + <EuiFlexItem> + <EuiButton + color="primary" + href="/app/uptime#/settings" + > + <FormattedMessage + defaultMessage="Update index pattern settings" + id="xpack.uptime.emptyState.updateIndexPattern" + values={Object {}} + /> + </EuiButton> + </EuiFlexItem> + </ForwardRef> + } + body={ + <React.Fragment> + <p> + <FormattedMessage + defaultMessage="If you have not setup heartbeat yet, you can setup heartbeat to start monitoring your services." + id="xpack.uptime.emptyState.configureHeartbeatToGetStartedMessage" + values={Object {}} + /> + </p> + <p> + <FormattedMessage + defaultMessage="If you have setup heartbeat and confirmed data is being sent to Elasticsearch, update your index pattern settings and insure they are aligned with your Heartbeat config." + id="xpack.uptime.emptyState.configureHeartbeatIndexSettings" + values={Object {}} + /> + </p> + </React.Fragment> + } + iconType="logoUptime" + title={ + <EuiTitle + size="l" + > + <h3> + <FormattedMessage + defaultMessage="Uptime index {indexName} not found" + id="xpack.uptime.emptyState.noIndexTitle" + values={ + Object { + "indexName": <em> + heartbeat-* + </em>, + } + } + /> + </h3> + </EuiTitle> + } + /> + </EuiPanel> + </EuiFlexItem> +</EuiFlexGroup> +`; diff --git a/x-pack/legacy/plugins/uptime/public/components/overview/empty_state/__tests__/__snapshots__/empty_state.test.tsx.snap b/x-pack/legacy/plugins/uptime/public/components/overview/empty_state/__tests__/__snapshots__/empty_state.test.tsx.snap new file mode 100644 index 0000000000000..d0e7af24e1c1b --- /dev/null +++ b/x-pack/legacy/plugins/uptime/public/components/overview/empty_state/__tests__/__snapshots__/empty_state.test.tsx.snap @@ -0,0 +1,1627 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`EmptyState component does not render empty state with appropriate base path and no docs 1`] = ` +<Router + history={ + Object { + "action": "POP", + "block": [Function], + "canGo": [Function], + "createHref": [Function], + "entries": Array [ + Object { + "hash": "", + "key": "TestKeyForTesting", + "pathname": "/", + "search": "", + "state": undefined, + }, + ], + "go": [Function], + "goBack": [Function], + "goForward": [Function], + "index": 0, + "length": 1, + "listen": [Function], + "location": Object { + "hash": "", + "key": "TestKeyForTesting", + "pathname": "/", + "search": "", + "state": undefined, + }, + "push": [Function], + "replace": [Function], + } + } + intl={ + Object { + "defaultFormats": Object {}, + "defaultLocale": "en", + "formatDate": [Function], + "formatHTMLMessage": [Function], + "formatMessage": [Function], + "formatNumber": [Function], + "formatPlural": [Function], + "formatRelative": [Function], + "formatTime": [Function], + "formats": Object { + "date": Object { + "full": Object { + "day": "numeric", + "month": "long", + "weekday": "long", + "year": "numeric", + }, + "long": Object { + "day": "numeric", + "month": "long", + "year": "numeric", + }, + "medium": Object { + "day": "numeric", + "month": "short", + "year": "numeric", + }, + "short": Object { + "day": "numeric", + "month": "numeric", + "year": "2-digit", + }, + }, + "number": Object { + "currency": Object { + "style": "currency", + }, + "percent": Object { + "style": "percent", + }, + }, + "relative": Object { + "days": Object { + "units": "day", + }, + "hours": Object { + "units": "hour", + }, + "minutes": Object { + "units": "minute", + }, + "months": Object { + "units": "month", + }, + "seconds": Object { + "units": "second", + }, + "years": Object { + "units": "year", + }, + }, + "time": Object { + "full": Object { + "hour": "numeric", + "minute": "numeric", + "second": "numeric", + "timeZoneName": "short", + }, + "long": Object { + "hour": "numeric", + "minute": "numeric", + "second": "numeric", + "timeZoneName": "short", + }, + "medium": Object { + "hour": "numeric", + "minute": "numeric", + "second": "numeric", + }, + "short": Object { + "hour": "numeric", + "minute": "numeric", + }, + }, + }, + "formatters": Object { + "getDateTimeFormat": [Function], + "getMessageFormat": [Function], + "getNumberFormat": [Function], + "getPluralFormat": [Function], + "getRelativeFormat": [Function], + }, + "locale": "en", + "messages": Object {}, + "now": [Function], + "onError": [Function], + "textComponent": Symbol(react.fragment), + "timeZone": null, + } + } +> + <EmptyStateComponent + loading={false} + statesIndexStatus={ + Object { + "docCount": 0, + "indexExists": true, + } + } + > + <DataOrIndexMissing + headingMessage={ + <FormattedMessage + defaultMessage="No uptime data found in index {indexName}" + id="xpack.uptime.emptyState.noDataMessage" + values={ + Object { + "indexName": <em />, + } + } + /> + } + > + <EuiFlexGroup + data-test-subj="data-missing" + justifyContent="center" + > + <div + className="euiFlexGroup euiFlexGroup--gutterLarge euiFlexGroup--justifyContentCenter euiFlexGroup--directionRow euiFlexGroup--responsive" + data-test-subj="data-missing" + > + <EuiFlexItem + grow={false} + style={ + Object { + "flexBasis": 700, + } + } + > + <div + className="euiFlexItem euiFlexItem--flexGrowZero" + style={ + Object { + "flexBasis": 700, + } + } + > + <EuiSpacer + size="m" + > + <div + className="euiSpacer euiSpacer--m" + /> + </EuiSpacer> + <EuiPanel> + <div + className="euiPanel euiPanel--paddingMedium" + > + <EuiEmptyPrompt + actions={ + <ForwardRef> + <EuiFlexItem> + <EuiButton + color="primary" + fill={true} + href="/app/kibana#/home/tutorial/uptimeMonitors" + > + <FormattedMessage + defaultMessage="View setup instructions" + id="xpack.uptime.emptyState.viewSetupInstructions" + values={Object {}} + /> + </EuiButton> + </EuiFlexItem> + <EuiFlexItem> + <EuiButton + color="primary" + href="/app/uptime#/settings" + > + <FormattedMessage + defaultMessage="Update index pattern settings" + id="xpack.uptime.emptyState.updateIndexPattern" + values={Object {}} + /> + </EuiButton> + </EuiFlexItem> + </ForwardRef> + } + body={ + <React.Fragment> + <p> + <FormattedMessage + defaultMessage="If you have not setup heartbeat yet, you can setup heartbeat to start monitoring your services." + id="xpack.uptime.emptyState.configureHeartbeatToGetStartedMessage" + values={Object {}} + /> + </p> + <p> + <FormattedMessage + defaultMessage="If you have setup heartbeat and confirmed data is being sent to Elasticsearch, update your index pattern settings and insure they are aligned with your Heartbeat config." + id="xpack.uptime.emptyState.configureHeartbeatIndexSettings" + values={Object {}} + /> + </p> + </React.Fragment> + } + iconType="logoUptime" + title={ + <EuiTitle + size="l" + > + <h3> + <FormattedMessage + defaultMessage="No uptime data found in index {indexName}" + id="xpack.uptime.emptyState.noDataMessage" + values={ + Object { + "indexName": <em />, + } + } + /> + </h3> + </EuiTitle> + } + > + <div + className="euiEmptyPrompt" + > + <EuiIcon + color="subdued" + size="xxl" + type="logoUptime" + > + <div + color="subdued" + data-euiicon-type="logoUptime" + size="xxl" + /> + </EuiIcon> + <EuiSpacer + size="s" + > + <div + className="euiSpacer euiSpacer--s" + /> + </EuiSpacer> + <EuiTextColor + color="subdued" + > + <span + className="euiTextColor euiTextColor--subdued" + > + <EuiTitle> + <EuiTitle + className="euiTitle euiTitle--medium" + size="l" + > + <h3 + className="euiTitle euiTitle--large euiTitle euiTitle--medium" + > + <FormattedMessage + defaultMessage="No uptime data found in index {indexName}" + id="xpack.uptime.emptyState.noDataMessage" + values={ + Object { + "indexName": <em />, + } + } + > + No uptime data found in index + <em /> + </FormattedMessage> + </h3> + </EuiTitle> + </EuiTitle> + <EuiSpacer + size="m" + > + <div + className="euiSpacer euiSpacer--m" + /> + </EuiSpacer> + <EuiText> + <div + className="euiText euiText--medium" + > + <p> + <FormattedMessage + defaultMessage="If you have not setup heartbeat yet, you can setup heartbeat to start monitoring your services." + id="xpack.uptime.emptyState.configureHeartbeatToGetStartedMessage" + values={Object {}} + > + If you have not setup heartbeat yet, you can setup heartbeat to start monitoring your services. + </FormattedMessage> + </p> + <p> + <FormattedMessage + defaultMessage="If you have setup heartbeat and confirmed data is being sent to Elasticsearch, update your index pattern settings and insure they are aligned with your Heartbeat config." + id="xpack.uptime.emptyState.configureHeartbeatIndexSettings" + values={Object {}} + > + If you have setup heartbeat and confirmed data is being sent to Elasticsearch, update your index pattern settings and insure they are aligned with your Heartbeat config. + </FormattedMessage> + </p> + </div> + </EuiText> + </span> + </EuiTextColor> + <EuiSpacer + size="l" + > + <div + className="euiSpacer euiSpacer--l" + /> + </EuiSpacer> + <EuiSpacer + size="s" + > + <div + className="euiSpacer euiSpacer--s" + /> + </EuiSpacer> + <EuiFlexGroup> + <div + className="euiFlexGroup euiFlexGroup--gutterLarge euiFlexGroup--directionRow euiFlexGroup--responsive" + > + <EuiFlexItem> + <div + className="euiFlexItem" + > + <EuiButton + color="primary" + fill={true} + href="/app/kibana#/home/tutorial/uptimeMonitors" + > + <a + className="euiButton euiButton--primary euiButton--fill" + href="/app/kibana#/home/tutorial/uptimeMonitors" + rel="noreferrer" + > + <span + className="euiButton__content" + > + <span + className="euiButton__text" + > + <FormattedMessage + defaultMessage="View setup instructions" + id="xpack.uptime.emptyState.viewSetupInstructions" + values={Object {}} + > + View setup instructions + </FormattedMessage> + </span> + </span> + </a> + </EuiButton> + </div> + </EuiFlexItem> + <EuiFlexItem> + <div + className="euiFlexItem" + > + <EuiButton + color="primary" + href="/app/uptime#/settings" + > + <a + className="euiButton euiButton--primary" + href="/app/uptime#/settings" + rel="noreferrer" + > + <span + className="euiButton__content" + > + <span + className="euiButton__text" + > + <FormattedMessage + defaultMessage="Update index pattern settings" + id="xpack.uptime.emptyState.updateIndexPattern" + values={Object {}} + > + Update index pattern settings + </FormattedMessage> + </span> + </span> + </a> + </EuiButton> + </div> + </EuiFlexItem> + </div> + </EuiFlexGroup> + </div> + </EuiEmptyPrompt> + </div> + </EuiPanel> + </div> + </EuiFlexItem> + </div> + </EuiFlexGroup> + </DataOrIndexMissing> + </EmptyStateComponent> +</Router> +`; + +exports[`EmptyState component doesn't render child components when count is falsy 1`] = ` +<Router + history={ + Object { + "action": "POP", + "block": [Function], + "canGo": [Function], + "createHref": [Function], + "entries": Array [ + Object { + "hash": "", + "key": "TestKeyForTesting", + "pathname": "/", + "search": "", + "state": undefined, + }, + ], + "go": [Function], + "goBack": [Function], + "goForward": [Function], + "index": 0, + "length": 1, + "listen": [Function], + "location": Object { + "hash": "", + "key": "TestKeyForTesting", + "pathname": "/", + "search": "", + "state": undefined, + }, + "push": [Function], + "replace": [Function], + } + } + intl={ + Object { + "defaultFormats": Object {}, + "defaultLocale": "en", + "formatDate": [Function], + "formatHTMLMessage": [Function], + "formatMessage": [Function], + "formatNumber": [Function], + "formatPlural": [Function], + "formatRelative": [Function], + "formatTime": [Function], + "formats": Object { + "date": Object { + "full": Object { + "day": "numeric", + "month": "long", + "weekday": "long", + "year": "numeric", + }, + "long": Object { + "day": "numeric", + "month": "long", + "year": "numeric", + }, + "medium": Object { + "day": "numeric", + "month": "short", + "year": "numeric", + }, + "short": Object { + "day": "numeric", + "month": "numeric", + "year": "2-digit", + }, + }, + "number": Object { + "currency": Object { + "style": "currency", + }, + "percent": Object { + "style": "percent", + }, + }, + "relative": Object { + "days": Object { + "units": "day", + }, + "hours": Object { + "units": "hour", + }, + "minutes": Object { + "units": "minute", + }, + "months": Object { + "units": "month", + }, + "seconds": Object { + "units": "second", + }, + "years": Object { + "units": "year", + }, + }, + "time": Object { + "full": Object { + "hour": "numeric", + "minute": "numeric", + "second": "numeric", + "timeZoneName": "short", + }, + "long": Object { + "hour": "numeric", + "minute": "numeric", + "second": "numeric", + "timeZoneName": "short", + }, + "medium": Object { + "hour": "numeric", + "minute": "numeric", + "second": "numeric", + }, + "short": Object { + "hour": "numeric", + "minute": "numeric", + }, + }, + }, + "formatters": Object { + "getDateTimeFormat": [Function], + "getMessageFormat": [Function], + "getNumberFormat": [Function], + "getPluralFormat": [Function], + "getRelativeFormat": [Function], + }, + "locale": "en", + "messages": Object {}, + "now": [Function], + "onError": [Function], + "textComponent": Symbol(react.fragment), + "timeZone": null, + } + } +> + <EmptyStateComponent + loading={false} + statesIndexStatus={null} + > + <EmptyStateLoading> + <EuiEmptyPrompt + body={ + <React.Fragment> + <EuiLoadingSpinner + size="xl" + /> + <EuiSpacer /> + <EuiTitle + size="l" + > + <h2> + Loading… + </h2> + </EuiTitle> + </React.Fragment> + } + > + <div + className="euiEmptyPrompt" + > + <EuiTextColor + color="subdued" + > + <span + className="euiTextColor euiTextColor--subdued" + > + <EuiText> + <div + className="euiText euiText--medium" + > + <EuiLoadingSpinner + size="xl" + > + <span + className="euiLoadingSpinner euiLoadingSpinner--xLarge" + /> + </EuiLoadingSpinner> + <EuiSpacer> + <div + className="euiSpacer euiSpacer--l" + /> + </EuiSpacer> + <EuiTitle + size="l" + > + <h2 + className="euiTitle euiTitle--large" + > + Loading… + </h2> + </EuiTitle> + </div> + </EuiText> + </span> + </EuiTextColor> + </div> + </EuiEmptyPrompt> + </EmptyStateLoading> + </EmptyStateComponent> +</Router> +`; + +exports[`EmptyState component notifies when index does not exist 1`] = ` +<Router + history={ + Object { + "action": "POP", + "block": [Function], + "canGo": [Function], + "createHref": [Function], + "entries": Array [ + Object { + "hash": "", + "key": "TestKeyForTesting", + "pathname": "/", + "search": "", + "state": undefined, + }, + ], + "go": [Function], + "goBack": [Function], + "goForward": [Function], + "index": 0, + "length": 1, + "listen": [Function], + "location": Object { + "hash": "", + "key": "TestKeyForTesting", + "pathname": "/", + "search": "", + "state": undefined, + }, + "push": [Function], + "replace": [Function], + } + } + intl={ + Object { + "defaultFormats": Object {}, + "defaultLocale": "en", + "formatDate": [Function], + "formatHTMLMessage": [Function], + "formatMessage": [Function], + "formatNumber": [Function], + "formatPlural": [Function], + "formatRelative": [Function], + "formatTime": [Function], + "formats": Object { + "date": Object { + "full": Object { + "day": "numeric", + "month": "long", + "weekday": "long", + "year": "numeric", + }, + "long": Object { + "day": "numeric", + "month": "long", + "year": "numeric", + }, + "medium": Object { + "day": "numeric", + "month": "short", + "year": "numeric", + }, + "short": Object { + "day": "numeric", + "month": "numeric", + "year": "2-digit", + }, + }, + "number": Object { + "currency": Object { + "style": "currency", + }, + "percent": Object { + "style": "percent", + }, + }, + "relative": Object { + "days": Object { + "units": "day", + }, + "hours": Object { + "units": "hour", + }, + "minutes": Object { + "units": "minute", + }, + "months": Object { + "units": "month", + }, + "seconds": Object { + "units": "second", + }, + "years": Object { + "units": "year", + }, + }, + "time": Object { + "full": Object { + "hour": "numeric", + "minute": "numeric", + "second": "numeric", + "timeZoneName": "short", + }, + "long": Object { + "hour": "numeric", + "minute": "numeric", + "second": "numeric", + "timeZoneName": "short", + }, + "medium": Object { + "hour": "numeric", + "minute": "numeric", + "second": "numeric", + }, + "short": Object { + "hour": "numeric", + "minute": "numeric", + }, + }, + }, + "formatters": Object { + "getDateTimeFormat": [Function], + "getMessageFormat": [Function], + "getNumberFormat": [Function], + "getPluralFormat": [Function], + "getRelativeFormat": [Function], + }, + "locale": "en", + "messages": Object {}, + "now": [Function], + "onError": [Function], + "textComponent": Symbol(react.fragment), + "timeZone": null, + } + } +> + <EmptyStateComponent + loading={false} + statesIndexStatus={ + Object { + "docCount": 1, + "indexExists": false, + } + } + > + <DataOrIndexMissing + headingMessage={ + <FormattedMessage + defaultMessage="No indices found matching pattern {indexName}" + id="xpack.uptime.emptyState.noIndexTitle" + values={ + Object { + "indexName": <em />, + } + } + /> + } + > + <EuiFlexGroup + data-test-subj="data-missing" + justifyContent="center" + > + <div + className="euiFlexGroup euiFlexGroup--gutterLarge euiFlexGroup--justifyContentCenter euiFlexGroup--directionRow euiFlexGroup--responsive" + data-test-subj="data-missing" + > + <EuiFlexItem + grow={false} + style={ + Object { + "flexBasis": 700, + } + } + > + <div + className="euiFlexItem euiFlexItem--flexGrowZero" + style={ + Object { + "flexBasis": 700, + } + } + > + <EuiSpacer + size="m" + > + <div + className="euiSpacer euiSpacer--m" + /> + </EuiSpacer> + <EuiPanel> + <div + className="euiPanel euiPanel--paddingMedium" + > + <EuiEmptyPrompt + actions={ + <ForwardRef> + <EuiFlexItem> + <EuiButton + color="primary" + fill={true} + href="/app/kibana#/home/tutorial/uptimeMonitors" + > + <FormattedMessage + defaultMessage="View setup instructions" + id="xpack.uptime.emptyState.viewSetupInstructions" + values={Object {}} + /> + </EuiButton> + </EuiFlexItem> + <EuiFlexItem> + <EuiButton + color="primary" + href="/app/uptime#/settings" + > + <FormattedMessage + defaultMessage="Update index pattern settings" + id="xpack.uptime.emptyState.updateIndexPattern" + values={Object {}} + /> + </EuiButton> + </EuiFlexItem> + </ForwardRef> + } + body={ + <React.Fragment> + <p> + <FormattedMessage + defaultMessage="If you have not setup heartbeat yet, you can setup heartbeat to start monitoring your services." + id="xpack.uptime.emptyState.configureHeartbeatToGetStartedMessage" + values={Object {}} + /> + </p> + <p> + <FormattedMessage + defaultMessage="If you have setup heartbeat and confirmed data is being sent to Elasticsearch, update your index pattern settings and insure they are aligned with your Heartbeat config." + id="xpack.uptime.emptyState.configureHeartbeatIndexSettings" + values={Object {}} + /> + </p> + </React.Fragment> + } + iconType="logoUptime" + title={ + <EuiTitle + size="l" + > + <h3> + <FormattedMessage + defaultMessage="No indices found matching pattern {indexName}" + id="xpack.uptime.emptyState.noIndexTitle" + values={ + Object { + "indexName": <em />, + } + } + /> + </h3> + </EuiTitle> + } + > + <div + className="euiEmptyPrompt" + > + <EuiIcon + color="subdued" + size="xxl" + type="logoUptime" + > + <div + color="subdued" + data-euiicon-type="logoUptime" + size="xxl" + /> + </EuiIcon> + <EuiSpacer + size="s" + > + <div + className="euiSpacer euiSpacer--s" + /> + </EuiSpacer> + <EuiTextColor + color="subdued" + > + <span + className="euiTextColor euiTextColor--subdued" + > + <EuiTitle> + <EuiTitle + className="euiTitle euiTitle--medium" + size="l" + > + <h3 + className="euiTitle euiTitle--large euiTitle euiTitle--medium" + > + <FormattedMessage + defaultMessage="No indices found matching pattern {indexName}" + id="xpack.uptime.emptyState.noIndexTitle" + values={ + Object { + "indexName": <em />, + } + } + > + No indices found matching pattern + <em /> + </FormattedMessage> + </h3> + </EuiTitle> + </EuiTitle> + <EuiSpacer + size="m" + > + <div + className="euiSpacer euiSpacer--m" + /> + </EuiSpacer> + <EuiText> + <div + className="euiText euiText--medium" + > + <p> + <FormattedMessage + defaultMessage="If you have not setup heartbeat yet, you can setup heartbeat to start monitoring your services." + id="xpack.uptime.emptyState.configureHeartbeatToGetStartedMessage" + values={Object {}} + > + If you have not setup heartbeat yet, you can setup heartbeat to start monitoring your services. + </FormattedMessage> + </p> + <p> + <FormattedMessage + defaultMessage="If you have setup heartbeat and confirmed data is being sent to Elasticsearch, update your index pattern settings and insure they are aligned with your Heartbeat config." + id="xpack.uptime.emptyState.configureHeartbeatIndexSettings" + values={Object {}} + > + If you have setup heartbeat and confirmed data is being sent to Elasticsearch, update your index pattern settings and insure they are aligned with your Heartbeat config. + </FormattedMessage> + </p> + </div> + </EuiText> + </span> + </EuiTextColor> + <EuiSpacer + size="l" + > + <div + className="euiSpacer euiSpacer--l" + /> + </EuiSpacer> + <EuiSpacer + size="s" + > + <div + className="euiSpacer euiSpacer--s" + /> + </EuiSpacer> + <EuiFlexGroup> + <div + className="euiFlexGroup euiFlexGroup--gutterLarge euiFlexGroup--directionRow euiFlexGroup--responsive" + > + <EuiFlexItem> + <div + className="euiFlexItem" + > + <EuiButton + color="primary" + fill={true} + href="/app/kibana#/home/tutorial/uptimeMonitors" + > + <a + className="euiButton euiButton--primary euiButton--fill" + href="/app/kibana#/home/tutorial/uptimeMonitors" + rel="noreferrer" + > + <span + className="euiButton__content" + > + <span + className="euiButton__text" + > + <FormattedMessage + defaultMessage="View setup instructions" + id="xpack.uptime.emptyState.viewSetupInstructions" + values={Object {}} + > + View setup instructions + </FormattedMessage> + </span> + </span> + </a> + </EuiButton> + </div> + </EuiFlexItem> + <EuiFlexItem> + <div + className="euiFlexItem" + > + <EuiButton + color="primary" + href="/app/uptime#/settings" + > + <a + className="euiButton euiButton--primary" + href="/app/uptime#/settings" + rel="noreferrer" + > + <span + className="euiButton__content" + > + <span + className="euiButton__text" + > + <FormattedMessage + defaultMessage="Update index pattern settings" + id="xpack.uptime.emptyState.updateIndexPattern" + values={Object {}} + > + Update index pattern settings + </FormattedMessage> + </span> + </span> + </a> + </EuiButton> + </div> + </EuiFlexItem> + </div> + </EuiFlexGroup> + </div> + </EuiEmptyPrompt> + </div> + </EuiPanel> + </div> + </EuiFlexItem> + </div> + </EuiFlexGroup> + </DataOrIndexMissing> + </EmptyStateComponent> +</Router> +`; + +exports[`EmptyState component renders child components when count is truthy 1`] = ` +<ContextProvider + value={ + Object { + "history": Object { + "action": "POP", + "block": [Function], + "canGo": [Function], + "createHref": [Function], + "entries": Array [ + Object { + "hash": "", + "key": "TestKeyForTesting", + "pathname": "/", + "search": "", + "state": undefined, + }, + ], + "go": [Function], + "goBack": [Function], + "goForward": [Function], + "index": 0, + "length": 1, + "listen": [Function], + "location": Object { + "hash": "", + "key": "TestKeyForTesting", + "pathname": "/", + "search": "", + "state": undefined, + }, + "push": [Function], + "replace": [Function], + }, + "location": Object { + "hash": "", + "key": "TestKeyForTesting", + "pathname": "/", + "search": "", + "state": undefined, + }, + "match": Object { + "isExact": true, + "params": Object {}, + "path": "/", + "url": "/", + }, + "staticContext": undefined, + } + } +> + <EmptyStateComponent + loading={false} + statesIndexStatus={ + Object { + "docCount": 1, + "indexExists": true, + } + } + > + <div> + Foo + </div> + <div> + Bar + </div> + <div> + Baz + </div> + </EmptyStateComponent> +</ContextProvider> +`; + +exports[`EmptyState component renders error message when an error occurs 1`] = ` +<Router + history={ + Object { + "action": "POP", + "block": [Function], + "canGo": [Function], + "createHref": [Function], + "entries": Array [ + Object { + "hash": "", + "key": "TestKeyForTesting", + "pathname": "/", + "search": "", + "state": undefined, + }, + ], + "go": [Function], + "goBack": [Function], + "goForward": [Function], + "index": 0, + "length": 1, + "listen": [Function], + "location": Object { + "hash": "", + "key": "TestKeyForTesting", + "pathname": "/", + "search": "", + "state": undefined, + }, + "push": [Function], + "replace": [Function], + } + } + intl={ + Object { + "defaultFormats": Object {}, + "defaultLocale": "en", + "formatDate": [Function], + "formatHTMLMessage": [Function], + "formatMessage": [Function], + "formatNumber": [Function], + "formatPlural": [Function], + "formatRelative": [Function], + "formatTime": [Function], + "formats": Object { + "date": Object { + "full": Object { + "day": "numeric", + "month": "long", + "weekday": "long", + "year": "numeric", + }, + "long": Object { + "day": "numeric", + "month": "long", + "year": "numeric", + }, + "medium": Object { + "day": "numeric", + "month": "short", + "year": "numeric", + }, + "short": Object { + "day": "numeric", + "month": "numeric", + "year": "2-digit", + }, + }, + "number": Object { + "currency": Object { + "style": "currency", + }, + "percent": Object { + "style": "percent", + }, + }, + "relative": Object { + "days": Object { + "units": "day", + }, + "hours": Object { + "units": "hour", + }, + "minutes": Object { + "units": "minute", + }, + "months": Object { + "units": "month", + }, + "seconds": Object { + "units": "second", + }, + "years": Object { + "units": "year", + }, + }, + "time": Object { + "full": Object { + "hour": "numeric", + "minute": "numeric", + "second": "numeric", + "timeZoneName": "short", + }, + "long": Object { + "hour": "numeric", + "minute": "numeric", + "second": "numeric", + "timeZoneName": "short", + }, + "medium": Object { + "hour": "numeric", + "minute": "numeric", + "second": "numeric", + }, + "short": Object { + "hour": "numeric", + "minute": "numeric", + }, + }, + }, + "formatters": Object { + "getDateTimeFormat": [Function], + "getMessageFormat": [Function], + "getNumberFormat": [Function], + "getPluralFormat": [Function], + "getRelativeFormat": [Function], + }, + "locale": "en", + "messages": Object {}, + "now": [Function], + "onError": [Function], + "textComponent": Symbol(react.fragment), + "timeZone": null, + } + } +> + <EmptyStateComponent + errors={ + Array [ + [error: There was an error fetching your data.], + ] + } + loading={false} + statesIndexStatus={null} + > + <EmptyStateError + errors={ + Array [ + [error: There was an error fetching your data.], + ] + } + > + <EuiFlexGroup + justifyContent="center" + > + <div + className="euiFlexGroup euiFlexGroup--gutterLarge euiFlexGroup--justifyContentCenter euiFlexGroup--directionRow euiFlexGroup--responsive" + > + <EuiFlexItem + grow={false} + > + <div + className="euiFlexItem euiFlexItem--flexGrowZero" + > + <EuiPanel> + <div + className="euiPanel euiPanel--paddingMedium" + > + <EuiEmptyPrompt + body={ + <React.Fragment> + <p> + There was an error fetching your data. + </p> + </React.Fragment> + } + iconColor="subdued" + iconType="securityApp" + title={ + <EuiTitle + size="m" + > + <h3> + Error + </h3> + </EuiTitle> + } + > + <div + className="euiEmptyPrompt" + > + <EuiIcon + color="subdued" + size="xxl" + type="securityApp" + > + <div + color="subdued" + data-euiicon-type="securityApp" + size="xxl" + /> + </EuiIcon> + <EuiSpacer + size="s" + > + <div + className="euiSpacer euiSpacer--s" + /> + </EuiSpacer> + <EuiTextColor + color="subdued" + > + <span + className="euiTextColor euiTextColor--subdued" + > + <EuiTitle> + <EuiTitle + className="euiTitle euiTitle--medium" + size="m" + > + <h3 + className="euiTitle euiTitle--medium euiTitle euiTitle--medium" + > + Error + </h3> + </EuiTitle> + </EuiTitle> + <EuiSpacer + size="m" + > + <div + className="euiSpacer euiSpacer--m" + /> + </EuiSpacer> + <EuiText> + <div + className="euiText euiText--medium" + > + <p + key="There was an error fetching your data." + > + There was an error fetching your data. + </p> + </div> + </EuiText> + </span> + </EuiTextColor> + </div> + </EuiEmptyPrompt> + </div> + </EuiPanel> + </div> + </EuiFlexItem> + </div> + </EuiFlexGroup> + </EmptyStateError> + </EmptyStateComponent> +</Router> +`; + +exports[`EmptyState component renders loading state if no errors or doc count 1`] = ` +<Router + history={ + Object { + "action": "POP", + "block": [Function], + "canGo": [Function], + "createHref": [Function], + "entries": Array [ + Object { + "hash": "", + "key": "TestKeyForTesting", + "pathname": "/", + "search": "", + "state": undefined, + }, + ], + "go": [Function], + "goBack": [Function], + "goForward": [Function], + "index": 0, + "length": 1, + "listen": [Function], + "location": Object { + "hash": "", + "key": "TestKeyForTesting", + "pathname": "/", + "search": "", + "state": undefined, + }, + "push": [Function], + "replace": [Function], + } + } + intl={ + Object { + "defaultFormats": Object {}, + "defaultLocale": "en", + "formatDate": [Function], + "formatHTMLMessage": [Function], + "formatMessage": [Function], + "formatNumber": [Function], + "formatPlural": [Function], + "formatRelative": [Function], + "formatTime": [Function], + "formats": Object { + "date": Object { + "full": Object { + "day": "numeric", + "month": "long", + "weekday": "long", + "year": "numeric", + }, + "long": Object { + "day": "numeric", + "month": "long", + "year": "numeric", + }, + "medium": Object { + "day": "numeric", + "month": "short", + "year": "numeric", + }, + "short": Object { + "day": "numeric", + "month": "numeric", + "year": "2-digit", + }, + }, + "number": Object { + "currency": Object { + "style": "currency", + }, + "percent": Object { + "style": "percent", + }, + }, + "relative": Object { + "days": Object { + "units": "day", + }, + "hours": Object { + "units": "hour", + }, + "minutes": Object { + "units": "minute", + }, + "months": Object { + "units": "month", + }, + "seconds": Object { + "units": "second", + }, + "years": Object { + "units": "year", + }, + }, + "time": Object { + "full": Object { + "hour": "numeric", + "minute": "numeric", + "second": "numeric", + "timeZoneName": "short", + }, + "long": Object { + "hour": "numeric", + "minute": "numeric", + "second": "numeric", + "timeZoneName": "short", + }, + "medium": Object { + "hour": "numeric", + "minute": "numeric", + "second": "numeric", + }, + "short": Object { + "hour": "numeric", + "minute": "numeric", + }, + }, + }, + "formatters": Object { + "getDateTimeFormat": [Function], + "getMessageFormat": [Function], + "getNumberFormat": [Function], + "getPluralFormat": [Function], + "getRelativeFormat": [Function], + }, + "locale": "en", + "messages": Object {}, + "now": [Function], + "onError": [Function], + "textComponent": Symbol(react.fragment), + "timeZone": null, + } + } +> + <EmptyStateComponent + loading={true} + statesIndexStatus={null} + > + <EmptyStateLoading> + <EuiEmptyPrompt + body={ + <React.Fragment> + <EuiLoadingSpinner + size="xl" + /> + <EuiSpacer /> + <EuiTitle + size="l" + > + <h2> + Loading… + </h2> + </EuiTitle> + </React.Fragment> + } + > + <div + className="euiEmptyPrompt" + > + <EuiTextColor + color="subdued" + > + <span + className="euiTextColor euiTextColor--subdued" + > + <EuiText> + <div + className="euiText euiText--medium" + > + <EuiLoadingSpinner + size="xl" + > + <span + className="euiLoadingSpinner euiLoadingSpinner--xLarge" + /> + </EuiLoadingSpinner> + <EuiSpacer> + <div + className="euiSpacer euiSpacer--l" + /> + </EuiSpacer> + <EuiTitle + size="l" + > + <h2 + className="euiTitle euiTitle--large" + > + Loading… + </h2> + </EuiTitle> + </div> + </EuiText> + </span> + </EuiTextColor> + </div> + </EuiEmptyPrompt> + </EmptyStateLoading> + </EmptyStateComponent> +</Router> +`; diff --git a/x-pack/legacy/plugins/uptime/public/components/overview/empty_state/__tests__/data_or_index_missing.test.tsx b/x-pack/legacy/plugins/uptime/public/components/overview/empty_state/__tests__/data_or_index_missing.test.tsx new file mode 100644 index 0000000000000..333802962fd3e --- /dev/null +++ b/x-pack/legacy/plugins/uptime/public/components/overview/empty_state/__tests__/data_or_index_missing.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 from 'react'; +import { shallowWithIntl } from 'test_utils/enzyme_helpers'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { DataOrIndexMissing } from '../data_or_index_missing'; + +describe('DataOrIndexMissing component', () => { + it('renders headingMessage', () => { + const headingMessage = ( + <FormattedMessage + id="xpack.uptime.emptyState.noIndexTitle" + defaultMessage="Uptime index {indexName} not found" + values={{ indexName: <em>heartbeat-*</em> }} + /> + ); + const component = shallowWithIntl(<DataOrIndexMissing headingMessage={headingMessage} />); + expect(component).toMatchSnapshot(); + }); +}); diff --git a/x-pack/legacy/plugins/uptime/public/components/functional/empty_state/__tests__/empty_state.test.tsx b/x-pack/legacy/plugins/uptime/public/components/overview/empty_state/__tests__/empty_state.test.tsx similarity index 89% rename from x-pack/legacy/plugins/uptime/public/components/functional/empty_state/__tests__/empty_state.test.tsx rename to x-pack/legacy/plugins/uptime/public/components/overview/empty_state/__tests__/empty_state.test.tsx index a74ad543c3318..acfe2ada5b68d 100644 --- a/x-pack/legacy/plugins/uptime/public/components/functional/empty_state/__tests__/empty_state.test.tsx +++ b/x-pack/legacy/plugins/uptime/public/components/overview/empty_state/__tests__/empty_state.test.tsx @@ -5,11 +5,11 @@ */ import React from 'react'; -import { mountWithIntl, shallowWithIntl } from 'test_utils/enzyme_helpers'; import { EmptyStateComponent } from '../empty_state'; import { StatesIndexStatus } from '../../../../../common/runtime_types'; import { IHttpFetchError } from '../../../../../../../../../target/types/core/public/http'; import { HttpFetchError } from '../../../../../../../../../src/core/public/http/http_fetch_error'; +import { mountWithRouter, shallowWithRouter } from '../../../../lib'; describe('EmptyState component', () => { let statesIndexStatus: StatesIndexStatus; @@ -22,7 +22,7 @@ describe('EmptyState component', () => { }); it('renders child components when count is truthy', () => { - const component = shallowWithIntl( + const component = shallowWithRouter( <EmptyStateComponent statesIndexStatus={statesIndexStatus} loading={false}> <div>Foo</div> <div>Bar</div> @@ -33,7 +33,7 @@ describe('EmptyState component', () => { }); it(`doesn't render child components when count is falsy`, () => { - const component = mountWithIntl( + const component = mountWithRouter( <EmptyStateComponent statesIndexStatus={null} loading={false}> <div>Shouldn't be rendered</div> </EmptyStateComponent> @@ -45,7 +45,7 @@ describe('EmptyState component', () => { const errors: IHttpFetchError[] = [ new HttpFetchError('There was an error fetching your data.', 'error', {} as any), ]; - const component = mountWithIntl( + const component = mountWithRouter( <EmptyStateComponent statesIndexStatus={null} errors={errors} loading={false}> <div>Shouldn't appear...</div> </EmptyStateComponent> @@ -54,7 +54,7 @@ describe('EmptyState component', () => { }); it('renders loading state if no errors or doc count', () => { - const component = mountWithIntl( + const component = mountWithRouter( <EmptyStateComponent loading={true} statesIndexStatus={null}> <div>Should appear even while loading...</div> </EmptyStateComponent> @@ -67,7 +67,7 @@ describe('EmptyState component', () => { docCount: 0, indexExists: true, }; - const component = mountWithIntl( + const component = mountWithRouter( <EmptyStateComponent statesIndexStatus={statesIndexStatus} loading={false}> <div>If this is in the snapshot the test should fail</div> </EmptyStateComponent> @@ -77,7 +77,7 @@ describe('EmptyState component', () => { it('notifies when index does not exist', () => { statesIndexStatus.indexExists = false; - const component = mountWithIntl( + const component = mountWithRouter( <EmptyStateComponent statesIndexStatus={statesIndexStatus} loading={false}> <div>This text should not render</div> </EmptyStateComponent> diff --git a/x-pack/legacy/plugins/uptime/public/components/overview/empty_state/data_or_index_missing.tsx b/x-pack/legacy/plugins/uptime/public/components/overview/empty_state/data_or_index_missing.tsx new file mode 100644 index 0000000000000..88c0920138f68 --- /dev/null +++ b/x-pack/legacy/plugins/uptime/public/components/overview/empty_state/data_or_index_missing.tsx @@ -0,0 +1,86 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { + EuiFlexGroup, + EuiEmptyPrompt, + EuiFlexItem, + EuiSpacer, + EuiPanel, + EuiTitle, + EuiButton, +} from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n/react'; +import React, { useContext } from 'react'; +import { UptimeSettingsContext } from '../../../contexts'; +import { DynamicSettings } from '../../../../common/runtime_types'; + +interface DataMissingProps { + headingMessage: JSX.Element; + settings?: DynamicSettings; +} + +export const DataOrIndexMissing = ({ headingMessage, settings }: DataMissingProps) => { + const { basePath } = useContext(UptimeSettingsContext); + return ( + <EuiFlexGroup justifyContent="center" data-test-subj="data-missing"> + <EuiFlexItem grow={false} style={{ flexBasis: 700 }}> + <EuiSpacer size="m" /> + <EuiPanel> + <EuiEmptyPrompt + iconType="logoUptime" + title={ + <EuiTitle size="l"> + <h3>{headingMessage}</h3> + </EuiTitle> + } + body={ + <> + <p> + <FormattedMessage + id="xpack.uptime.emptyState.configureHeartbeatToGetStartedMessage" + defaultMessage="If you have not setup heartbeat yet, you can setup heartbeat to start monitoring your services." + /> + </p> + <p> + <FormattedMessage + id="xpack.uptime.emptyState.configureHeartbeatIndexSettings" + defaultMessage="If you have setup heartbeat and confirmed data is being sent to Elasticsearch, + update your index pattern settings and insure they are aligned with your Heartbeat config." + /> + </p> + </> + } + actions={ + <EuiFlexGroup> + <EuiFlexItem> + <EuiButton + fill + color="primary" + href={`${basePath}/app/kibana#/home/tutorial/uptimeMonitors`} + > + <FormattedMessage + id="xpack.uptime.emptyState.viewSetupInstructions" + defaultMessage="View setup instructions" + /> + </EuiButton> + </EuiFlexItem> + <EuiFlexItem> + <EuiButton color="primary" href={`${basePath}/app/uptime#/settings`}> + <FormattedMessage + id="xpack.uptime.emptyState.updateIndexPattern" + defaultMessage="Update index pattern settings" + /> + </EuiButton> + </EuiFlexItem> + </EuiFlexGroup> + } + /> + </EuiPanel> + </EuiFlexItem> + </EuiFlexGroup> + ); +}; diff --git a/x-pack/legacy/plugins/uptime/public/components/overview/empty_state/empty_state.tsx b/x-pack/legacy/plugins/uptime/public/components/overview/empty_state/empty_state.tsx new file mode 100644 index 0000000000000..651103a34bf21 --- /dev/null +++ b/x-pack/legacy/plugins/uptime/public/components/overview/empty_state/empty_state.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, { Fragment } from 'react'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { EmptyStateError } from './empty_state_error'; +import { EmptyStateLoading } from './empty_state_loading'; +import { DataOrIndexMissing } from './data_or_index_missing'; +import { DynamicSettings, StatesIndexStatus } from '../../../../common/runtime_types'; +import { IHttpFetchError } from '../../../../../../../../target/types/core/public/http'; + +interface EmptyStateProps { + children: JSX.Element[] | JSX.Element; + statesIndexStatus: StatesIndexStatus | null; + loading: boolean; + errors?: IHttpFetchError[]; + settings?: DynamicSettings; +} + +export const EmptyStateComponent = ({ + children, + statesIndexStatus, + loading, + errors, + settings, +}: EmptyStateProps) => { + if (errors?.length) { + return <EmptyStateError errors={errors} />; + } + if (!loading && statesIndexStatus) { + const { indexExists, docCount } = statesIndexStatus; + if (!indexExists) { + return ( + <DataOrIndexMissing + settings={settings} + headingMessage={ + <FormattedMessage + id="xpack.uptime.emptyState.noIndexTitle" + defaultMessage="No indices found matching pattern {indexName}" + values={{ indexName: <em>{settings?.heartbeatIndices}</em> }} + /> + } + /> + ); + } else if (indexExists && docCount === 0) { + return ( + <DataOrIndexMissing + settings={settings} + headingMessage={ + <FormattedMessage + id="xpack.uptime.emptyState.noDataMessage" + defaultMessage="No uptime data found in index {indexName}" + values={{ indexName: <em>{settings?.heartbeatIndices}</em> }} + /> + } + /> + ); + } + /** + * We choose to render the children any time the count > 0, even if + * the component is loading. If we render the loading state for this component, + * it will blow away the state of child components and trigger an ugly + * jittery UX any time the components refresh. This way we'll keep the stale + * state displayed during the fetching process. + */ + return <Fragment>{children}</Fragment>; + } + return <EmptyStateLoading />; +}; diff --git a/x-pack/legacy/plugins/uptime/public/components/overview/empty_state/empty_state_container.tsx b/x-pack/legacy/plugins/uptime/public/components/overview/empty_state/empty_state_container.tsx new file mode 100644 index 0000000000000..9a62cb9cdaeee --- /dev/null +++ b/x-pack/legacy/plugins/uptime/public/components/overview/empty_state/empty_state_container.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 { useDispatch, useSelector } from 'react-redux'; +import { indexStatusAction } from '../../../state/actions'; +import { indexStatusSelector, selectDynamicSettings } from '../../../state/selectors'; +import { EmptyStateComponent } from './index'; +import { UptimeRefreshContext } from '../../../contexts'; +import { getDynamicSettings } from '../../../state/actions/dynamic_settings'; + +export const EmptyState: React.FC = ({ children }) => { + const { data, loading, error } = useSelector(indexStatusSelector); + const { lastRefresh } = useContext(UptimeRefreshContext); + + const { settings } = useSelector(selectDynamicSettings); + + const heartbeatIndices = settings?.heartbeatIndices || ''; + + const dispatch = useDispatch(); + + useEffect(() => { + if (!data || data?.docCount === 0 || data?.indexExists === false) { + dispatch(indexStatusAction.get()); + } + // Don't add data , it will create endless loop + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [dispatch, lastRefresh]); + + useEffect(() => { + dispatch(indexStatusAction.get()); + }, [dispatch, heartbeatIndices]); + + useEffect(() => { + dispatch(getDynamicSettings()); + }, [dispatch]); + + return ( + <EmptyStateComponent + statesIndexStatus={data} + loading={loading} + errors={error ? [error] : undefined} + children={children as React.ReactElement} + settings={settings} + /> + ); +}; diff --git a/x-pack/legacy/plugins/uptime/public/components/functional/empty_state/empty_state_error.tsx b/x-pack/legacy/plugins/uptime/public/components/overview/empty_state/empty_state_error.tsx similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/functional/empty_state/empty_state_error.tsx rename to x-pack/legacy/plugins/uptime/public/components/overview/empty_state/empty_state_error.tsx diff --git a/x-pack/legacy/plugins/uptime/public/components/functional/empty_state/empty_state_loading.tsx b/x-pack/legacy/plugins/uptime/public/components/overview/empty_state/empty_state_loading.tsx similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/functional/empty_state/empty_state_loading.tsx rename to x-pack/legacy/plugins/uptime/public/components/overview/empty_state/empty_state_loading.tsx diff --git a/x-pack/legacy/plugins/uptime/public/components/overview/empty_state/index.ts b/x-pack/legacy/plugins/uptime/public/components/overview/empty_state/index.ts new file mode 100644 index 0000000000000..9f2a668f4c3a5 --- /dev/null +++ b/x-pack/legacy/plugins/uptime/public/components/overview/empty_state/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 { EmptyStateComponent } from './empty_state'; +export { EmptyState } from './empty_state_container'; diff --git a/x-pack/legacy/plugins/uptime/public/components/functional/filter_group/__tests__/__snapshots__/filter_popover.test.tsx.snap b/x-pack/legacy/plugins/uptime/public/components/overview/filter_group/__tests__/__snapshots__/filter_popover.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/functional/filter_group/__tests__/__snapshots__/filter_popover.test.tsx.snap rename to x-pack/legacy/plugins/uptime/public/components/overview/filter_group/__tests__/__snapshots__/filter_popover.test.tsx.snap diff --git a/x-pack/legacy/plugins/uptime/public/components/functional/filter_group/__tests__/__snapshots__/filter_status_button.test.tsx.snap b/x-pack/legacy/plugins/uptime/public/components/overview/filter_group/__tests__/__snapshots__/filter_status_button.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/functional/filter_group/__tests__/__snapshots__/filter_status_button.test.tsx.snap rename to x-pack/legacy/plugins/uptime/public/components/overview/filter_group/__tests__/__snapshots__/filter_status_button.test.tsx.snap diff --git a/x-pack/legacy/plugins/uptime/public/components/functional/filter_group/__tests__/__snapshots__/parse_filter_map.test.ts.snap b/x-pack/legacy/plugins/uptime/public/components/overview/filter_group/__tests__/__snapshots__/parse_filter_map.test.ts.snap similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/functional/filter_group/__tests__/__snapshots__/parse_filter_map.test.ts.snap rename to x-pack/legacy/plugins/uptime/public/components/overview/filter_group/__tests__/__snapshots__/parse_filter_map.test.ts.snap diff --git a/x-pack/legacy/plugins/uptime/public/components/functional/filter_group/__tests__/filter_popover.test.tsx b/x-pack/legacy/plugins/uptime/public/components/overview/filter_group/__tests__/filter_popover.test.tsx similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/functional/filter_group/__tests__/filter_popover.test.tsx rename to x-pack/legacy/plugins/uptime/public/components/overview/filter_group/__tests__/filter_popover.test.tsx diff --git a/x-pack/legacy/plugins/uptime/public/components/functional/filter_group/__tests__/filter_status_button.test.tsx b/x-pack/legacy/plugins/uptime/public/components/overview/filter_group/__tests__/filter_status_button.test.tsx similarity index 93% rename from x-pack/legacy/plugins/uptime/public/components/functional/filter_group/__tests__/filter_status_button.test.tsx rename to x-pack/legacy/plugins/uptime/public/components/overview/filter_group/__tests__/filter_status_button.test.tsx index 1813229a97d1b..2ad4d971cf3b0 100644 --- a/x-pack/legacy/plugins/uptime/public/components/functional/filter_group/__tests__/filter_status_button.test.tsx +++ b/x-pack/legacy/plugins/uptime/public/components/overview/filter_group/__tests__/filter_status_button.test.tsx @@ -6,7 +6,7 @@ import React from 'react'; import { FilterStatusButton, FilterStatusButtonProps } from '../filter_status_button'; -import { shallowWithRouter } from '../../../../lib/'; +import { shallowWithRouter } from '../../../../lib'; describe('FilterStatusButton', () => { let props: FilterStatusButtonProps; diff --git a/x-pack/legacy/plugins/uptime/public/components/functional/filter_group/__tests__/parse_filter_map.test.ts b/x-pack/legacy/plugins/uptime/public/components/overview/filter_group/__tests__/parse_filter_map.test.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/functional/filter_group/__tests__/parse_filter_map.test.ts rename to x-pack/legacy/plugins/uptime/public/components/overview/filter_group/__tests__/parse_filter_map.test.ts diff --git a/x-pack/legacy/plugins/uptime/public/components/functional/filter_group/__tests__/toggle_selected_item.test.ts b/x-pack/legacy/plugins/uptime/public/components/overview/filter_group/__tests__/toggle_selected_item.test.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/functional/filter_group/__tests__/toggle_selected_item.test.ts rename to x-pack/legacy/plugins/uptime/public/components/overview/filter_group/__tests__/toggle_selected_item.test.ts diff --git a/x-pack/legacy/plugins/uptime/public/components/functional/filter_group/filter_group.tsx b/x-pack/legacy/plugins/uptime/public/components/overview/filter_group/filter_group.tsx similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/functional/filter_group/filter_group.tsx rename to x-pack/legacy/plugins/uptime/public/components/overview/filter_group/filter_group.tsx diff --git a/x-pack/legacy/plugins/uptime/public/components/connected/filter_group/filter_group_container.tsx b/x-pack/legacy/plugins/uptime/public/components/overview/filter_group/filter_group_container.tsx similarity index 94% rename from x-pack/legacy/plugins/uptime/public/components/connected/filter_group/filter_group_container.tsx rename to x-pack/legacy/plugins/uptime/public/components/overview/filter_group/filter_group_container.tsx index 569c6bb883cbd..3612604fdf116 100644 --- a/x-pack/legacy/plugins/uptime/public/components/connected/filter_group/filter_group_container.tsx +++ b/x-pack/legacy/plugins/uptime/public/components/overview/filter_group/filter_group_container.tsx @@ -7,10 +7,10 @@ import React, { useContext, useEffect } from 'react'; import { connect } from 'react-redux'; import { useUrlParams } from '../../../hooks'; -import { parseFiltersMap } from '../../functional/filter_group/parse_filter_map'; +import { parseFiltersMap } from './parse_filter_map'; import { AppState } from '../../../state'; import { fetchOverviewFilters, GetOverviewFiltersPayload } from '../../../state/actions'; -import { FilterGroupComponent } from '../../functional/filter_group'; +import { FilterGroupComponent } from './index'; import { OverviewFilters } from '../../../../common/runtime_types/overview_filters'; import { UptimeRefreshContext } from '../../../contexts'; diff --git a/x-pack/legacy/plugins/uptime/public/components/functional/filter_group/filter_popover.tsx b/x-pack/legacy/plugins/uptime/public/components/overview/filter_group/filter_popover.tsx similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/functional/filter_group/filter_popover.tsx rename to x-pack/legacy/plugins/uptime/public/components/overview/filter_group/filter_popover.tsx diff --git a/x-pack/legacy/plugins/uptime/public/components/functional/filter_group/filter_status_button.tsx b/x-pack/legacy/plugins/uptime/public/components/overview/filter_group/filter_status_button.tsx similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/functional/filter_group/filter_status_button.tsx rename to x-pack/legacy/plugins/uptime/public/components/overview/filter_group/filter_status_button.tsx diff --git a/x-pack/legacy/plugins/uptime/public/components/overview/filter_group/index.ts b/x-pack/legacy/plugins/uptime/public/components/overview/filter_group/index.ts new file mode 100644 index 0000000000000..933fddf1cde27 --- /dev/null +++ b/x-pack/legacy/plugins/uptime/public/components/overview/filter_group/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 { FilterGroupComponent } from './filter_group'; +export { FilterGroup } from './filter_group_container'; diff --git a/x-pack/legacy/plugins/uptime/public/components/functional/filter_group/parse_filter_map.ts b/x-pack/legacy/plugins/uptime/public/components/overview/filter_group/parse_filter_map.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/functional/filter_group/parse_filter_map.ts rename to x-pack/legacy/plugins/uptime/public/components/overview/filter_group/parse_filter_map.ts diff --git a/x-pack/legacy/plugins/uptime/public/components/functional/filter_group/toggle_selected_item.ts b/x-pack/legacy/plugins/uptime/public/components/overview/filter_group/toggle_selected_item.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/functional/filter_group/toggle_selected_item.ts rename to x-pack/legacy/plugins/uptime/public/components/overview/filter_group/toggle_selected_item.ts diff --git a/x-pack/legacy/plugins/uptime/public/components/functional/filter_group/uptime_filter_button.tsx b/x-pack/legacy/plugins/uptime/public/components/overview/filter_group/uptime_filter_button.tsx similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/functional/filter_group/uptime_filter_button.tsx rename to x-pack/legacy/plugins/uptime/public/components/overview/filter_group/uptime_filter_button.tsx diff --git a/x-pack/legacy/plugins/uptime/public/components/overview/index.ts b/x-pack/legacy/plugins/uptime/public/components/overview/index.ts new file mode 100644 index 0000000000000..ac293e9233c8c --- /dev/null +++ b/x-pack/legacy/plugins/uptime/public/components/overview/index.ts @@ -0,0 +1,14 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export * from './monitor_list'; +export * from './empty_state'; +export * from './filter_group'; +export * from './alerts'; +export * from './snapshot'; +export * from './kuery_bar'; + +export { ParsingErrorCallout } from './parsing_error_callout'; diff --git a/x-pack/legacy/plugins/uptime/public/components/overview/kuery_bar/index.ts b/x-pack/legacy/plugins/uptime/public/components/overview/kuery_bar/index.ts new file mode 100644 index 0000000000000..60801a0ab705a --- /dev/null +++ b/x-pack/legacy/plugins/uptime/public/components/overview/kuery_bar/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 { KueryBarComponent } from './kuery_bar'; +export { KueryBar } from './kuery_bar_container'; diff --git a/x-pack/legacy/plugins/uptime/public/components/functional/kuery_bar/kuery_bar.tsx b/x-pack/legacy/plugins/uptime/public/components/overview/kuery_bar/kuery_bar.tsx similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/functional/kuery_bar/kuery_bar.tsx rename to x-pack/legacy/plugins/uptime/public/components/overview/kuery_bar/kuery_bar.tsx diff --git a/x-pack/legacy/plugins/uptime/public/components/connected/kuerybar/kuery_bar_container.tsx b/x-pack/legacy/plugins/uptime/public/components/overview/kuery_bar/kuery_bar_container.tsx similarity index 90% rename from x-pack/legacy/plugins/uptime/public/components/connected/kuerybar/kuery_bar_container.tsx rename to x-pack/legacy/plugins/uptime/public/components/overview/kuery_bar/kuery_bar_container.tsx index 132ae57b5154f..5e1e184b2d6e6 100644 --- a/x-pack/legacy/plugins/uptime/public/components/connected/kuerybar/kuery_bar_container.tsx +++ b/x-pack/legacy/plugins/uptime/public/components/overview/kuery_bar/kuery_bar_container.tsx @@ -8,7 +8,7 @@ import { connect } from 'react-redux'; import { AppState } from '../../../state'; import { selectIndexPattern } from '../../../state/selectors'; import { getIndexPattern } from '../../../state/actions'; -import { KueryBarComponent } from '../../functional/kuery_bar/kuery_bar'; +import { KueryBarComponent } from './kuery_bar'; const mapStateToProps = (state: AppState) => ({ ...selectIndexPattern(state) }); diff --git a/x-pack/legacy/plugins/uptime/public/components/functional/kuery_bar/typeahead/click_outside.js b/x-pack/legacy/plugins/uptime/public/components/overview/kuery_bar/typeahead/click_outside.js similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/functional/kuery_bar/typeahead/click_outside.js rename to x-pack/legacy/plugins/uptime/public/components/overview/kuery_bar/typeahead/click_outside.js diff --git a/x-pack/legacy/plugins/uptime/public/components/overview/kuery_bar/typeahead/index.d.ts b/x-pack/legacy/plugins/uptime/public/components/overview/kuery_bar/typeahead/index.d.ts new file mode 100644 index 0000000000000..defde6203a8c5 --- /dev/null +++ b/x-pack/legacy/plugins/uptime/public/components/overview/kuery_bar/typeahead/index.d.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 React from 'react'; + +interface TypeaheadProps { + onChange: (inputValue: string, selectionStart: number) => void; + onSubmit: (inputValue: string) => void; + suggestions: unknown[]; + queryExample: string; + initialValue?: string; + isLoading?: boolean; + disabled?: boolean; +} + +export class Typeahead extends React.Component<TypeaheadProps> { + incrementIndex(currentIndex: any): void; + + decrementIndex(currentIndex: any): void; + + onKeyUp(event: any): void; + + onKeyDown(event: any): void; + + selectSuggestion(suggestion: any): void; + + onClickOutside(): void; + + onChangeInputValue(event: any): void; + + onClickInput(event: any): void; + + onClickSuggestion(suggestion: any): void; + + onMouseEnterSuggestion(index: any): void; + + onSubmit(): void; + + render(): any; +} diff --git a/x-pack/legacy/plugins/uptime/public/components/functional/kuery_bar/typeahead/index.js b/x-pack/legacy/plugins/uptime/public/components/overview/kuery_bar/typeahead/index.js similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/functional/kuery_bar/typeahead/index.js rename to x-pack/legacy/plugins/uptime/public/components/overview/kuery_bar/typeahead/index.js diff --git a/x-pack/legacy/plugins/uptime/public/components/functional/kuery_bar/typeahead/suggestion.js b/x-pack/legacy/plugins/uptime/public/components/overview/kuery_bar/typeahead/suggestion.js similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/functional/kuery_bar/typeahead/suggestion.js rename to x-pack/legacy/plugins/uptime/public/components/overview/kuery_bar/typeahead/suggestion.js diff --git a/x-pack/legacy/plugins/uptime/public/components/functional/kuery_bar/typeahead/suggestions.js b/x-pack/legacy/plugins/uptime/public/components/overview/kuery_bar/typeahead/suggestions.js similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/functional/kuery_bar/typeahead/suggestions.js rename to x-pack/legacy/plugins/uptime/public/components/overview/kuery_bar/typeahead/suggestions.js diff --git a/x-pack/legacy/plugins/uptime/public/components/overview/monitor_list/__tests__/__snapshots__/monitor_list.test.tsx.snap b/x-pack/legacy/plugins/uptime/public/components/overview/monitor_list/__tests__/__snapshots__/monitor_list.test.tsx.snap new file mode 100644 index 0000000000000..ed5602323d254 --- /dev/null +++ b/x-pack/legacy/plugins/uptime/public/components/overview/monitor_list/__tests__/__snapshots__/monitor_list.test.tsx.snap @@ -0,0 +1,1213 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`MonitorList component MonitorListPagination component renders a no items message when no data is provided 1`] = ` +<ContextProvider + value={ + Object { + "history": Object { + "action": "POP", + "block": [Function], + "canGo": [Function], + "createHref": [Function], + "entries": Array [ + Object { + "hash": "", + "key": "TestKeyForTesting", + "pathname": "/", + "search": "", + "state": undefined, + }, + ], + "go": [Function], + "goBack": [Function], + "goForward": [Function], + "index": 0, + "length": 1, + "listen": [Function], + "location": Object { + "hash": "", + "key": "TestKeyForTesting", + "pathname": "/", + "search": "", + "state": undefined, + }, + "push": [Function], + "replace": [Function], + }, + "location": Object { + "hash": "", + "key": "TestKeyForTesting", + "pathname": "/", + "search": "", + "state": undefined, + }, + "match": Object { + "isExact": true, + "params": Object {}, + "path": "/", + "url": "/", + }, + "staticContext": undefined, + } + } +> + <MonitorListComponent + getMonitorList={[MockFunction]} + lastRefresh={123} + monitorList={ + Object { + "list": Object { + "nextPagePagination": null, + "prevPagePagination": null, + "summaries": Array [], + "totalSummaryCount": 0, + }, + "loading": false, + } + } + /> +</ContextProvider> +`; + +exports[`MonitorList component MonitorListPagination component renders the pagination 1`] = ` +<ContextProvider + value={ + Object { + "history": Object { + "action": "POP", + "block": [Function], + "canGo": [Function], + "createHref": [Function], + "entries": Array [ + Object { + "hash": "", + "key": "TestKeyForTesting", + "pathname": "/", + "search": "", + "state": undefined, + }, + ], + "go": [Function], + "goBack": [Function], + "goForward": [Function], + "index": 0, + "length": 1, + "listen": [Function], + "location": Object { + "hash": "", + "key": "TestKeyForTesting", + "pathname": "/", + "search": "", + "state": undefined, + }, + "push": [Function], + "replace": [Function], + }, + "location": Object { + "hash": "", + "key": "TestKeyForTesting", + "pathname": "/", + "search": "", + "state": undefined, + }, + "match": Object { + "isExact": true, + "params": Object {}, + "path": "/", + "url": "/", + }, + "staticContext": undefined, + } + } +> + <MonitorListComponent + getMonitorList={[MockFunction]} + lastRefresh={123} + monitorList={ + Object { + "list": Object { + "nextPagePagination": "{\\"cursorKey\\":{\\"monitor_id\\":456},\\"cursorDirection\\":\\"AFTER\\",\\"sortOrder\\":\\"ASC\\"}", + "prevPagePagination": "{\\"cursorKey\\":{\\"monitor_id\\":123},\\"cursorDirection\\":\\"BEFORE\\",\\"sortOrder\\":\\"ASC\\"}", + "summaries": Array [ + Object { + "monitor_id": "foo", + "state": Object { + "checks": Array [ + Object { + "monitor": Object { + "ip": "127.0.0.1", + "status": "up", + }, + "timestamp": 124, + }, + Object { + "monitor": Object { + "ip": "127.0.0.2", + "status": "down", + }, + "timestamp": 125, + }, + Object { + "monitor": Object { + "ip": "127.0.0.3", + "status": "down", + }, + "timestamp": 126, + }, + ], + "summary": Object { + "down": 2, + "up": 1, + }, + "timestamp": "123", + "url": Object {}, + }, + }, + Object { + "monitor_id": "bar", + "state": Object { + "checks": Array [ + Object { + "monitor": Object { + "ip": "127.0.0.1", + "status": "up", + }, + "timestamp": 125, + }, + Object { + "monitor": Object { + "ip": "127.0.0.2", + "status": "up", + }, + "timestamp": 126, + }, + ], + "summary": Object { + "down": 0, + "up": 2, + }, + "timestamp": "125", + "url": Object {}, + }, + }, + ], + "totalSummaryCount": 2, + }, + "loading": false, + } + } + /> +</ContextProvider> +`; + +exports[`MonitorList component renders a no items message when no data is provided 1`] = ` +<ContextProvider + value={ + Object { + "history": Object { + "action": "POP", + "block": [Function], + "canGo": [Function], + "createHref": [Function], + "entries": Array [ + Object { + "hash": "", + "key": "TestKeyForTesting", + "pathname": "/", + "search": "", + "state": undefined, + }, + ], + "go": [Function], + "goBack": [Function], + "goForward": [Function], + "index": 0, + "length": 1, + "listen": [Function], + "location": Object { + "hash": "", + "key": "TestKeyForTesting", + "pathname": "/", + "search": "", + "state": undefined, + }, + "push": [Function], + "replace": [Function], + }, + "location": Object { + "hash": "", + "key": "TestKeyForTesting", + "pathname": "/", + "search": "", + "state": undefined, + }, + "match": Object { + "isExact": true, + "params": Object {}, + "path": "/", + "url": "/", + }, + "staticContext": undefined, + } + } +> + <MonitorListComponent + getMonitorList={[MockFunction]} + lastRefresh={123} + monitorList={ + Object { + "list": Object { + "nextPagePagination": null, + "prevPagePagination": null, + "summaries": Array [], + "totalSummaryCount": 0, + }, + "loading": true, + } + } + /> +</ContextProvider> +`; + +exports[`MonitorList component renders error list 1`] = ` +<ContextProvider + value={ + Object { + "history": Object { + "action": "POP", + "block": [Function], + "canGo": [Function], + "createHref": [Function], + "entries": Array [ + Object { + "hash": "", + "key": "TestKeyForTesting", + "pathname": "/", + "search": "", + "state": undefined, + }, + ], + "go": [Function], + "goBack": [Function], + "goForward": [Function], + "index": 0, + "length": 1, + "listen": [Function], + "location": Object { + "hash": "", + "key": "TestKeyForTesting", + "pathname": "/", + "search": "", + "state": undefined, + }, + "push": [Function], + "replace": [Function], + }, + "location": Object { + "hash": "", + "key": "TestKeyForTesting", + "pathname": "/", + "search": "", + "state": undefined, + }, + "match": Object { + "isExact": true, + "params": Object {}, + "path": "/", + "url": "/", + }, + "staticContext": undefined, + } + } +> + <MonitorListComponent + getMonitorList={[MockFunction]} + lastRefresh={123} + monitorList={ + Object { + "error": [Error: foo message], + "list": Object { + "nextPagePagination": null, + "prevPagePagination": null, + "summaries": Array [ + Object { + "monitor_id": "foo", + "state": Object { + "checks": Array [ + Object { + "monitor": Object { + "ip": "127.0.0.1", + "status": "up", + }, + "timestamp": 124, + }, + Object { + "monitor": Object { + "ip": "127.0.0.2", + "status": "down", + }, + "timestamp": 125, + }, + Object { + "monitor": Object { + "ip": "127.0.0.3", + "status": "down", + }, + "timestamp": 126, + }, + ], + "summary": Object { + "down": 2, + "up": 1, + }, + "timestamp": "123", + "url": Object {}, + }, + }, + Object { + "monitor_id": "bar", + "state": Object { + "checks": Array [ + Object { + "monitor": Object { + "ip": "127.0.0.1", + "status": "up", + }, + "timestamp": 125, + }, + Object { + "monitor": Object { + "ip": "127.0.0.2", + "status": "up", + }, + "timestamp": 126, + }, + ], + "summary": Object { + "down": 0, + "up": 2, + }, + "timestamp": "125", + "url": Object {}, + }, + }, + ], + "totalSummaryCount": 2, + }, + "loading": false, + } + } + /> +</ContextProvider> +`; + +exports[`MonitorList component renders loading state 1`] = ` +<ContextProvider + value={ + Object { + "history": Object { + "action": "POP", + "block": [Function], + "canGo": [Function], + "createHref": [Function], + "entries": Array [ + Object { + "hash": "", + "key": "TestKeyForTesting", + "pathname": "/", + "search": "", + "state": undefined, + }, + ], + "go": [Function], + "goBack": [Function], + "goForward": [Function], + "index": 0, + "length": 1, + "listen": [Function], + "location": Object { + "hash": "", + "key": "TestKeyForTesting", + "pathname": "/", + "search": "", + "state": undefined, + }, + "push": [Function], + "replace": [Function], + }, + "location": Object { + "hash": "", + "key": "TestKeyForTesting", + "pathname": "/", + "search": "", + "state": undefined, + }, + "match": Object { + "isExact": true, + "params": Object {}, + "path": "/", + "url": "/", + }, + "staticContext": undefined, + } + } +> + <MonitorListComponent + getMonitorList={[MockFunction]} + lastRefresh={123} + monitorList={ + Object { + "list": Object { + "nextPagePagination": null, + "prevPagePagination": null, + "summaries": Array [ + Object { + "monitor_id": "foo", + "state": Object { + "checks": Array [ + Object { + "monitor": Object { + "ip": "127.0.0.1", + "status": "up", + }, + "timestamp": 124, + }, + Object { + "monitor": Object { + "ip": "127.0.0.2", + "status": "down", + }, + "timestamp": 125, + }, + Object { + "monitor": Object { + "ip": "127.0.0.3", + "status": "down", + }, + "timestamp": 126, + }, + ], + "summary": Object { + "down": 2, + "up": 1, + }, + "timestamp": "123", + "url": Object {}, + }, + }, + Object { + "monitor_id": "bar", + "state": Object { + "checks": Array [ + Object { + "monitor": Object { + "ip": "127.0.0.1", + "status": "up", + }, + "timestamp": 125, + }, + Object { + "monitor": Object { + "ip": "127.0.0.2", + "status": "up", + }, + "timestamp": 126, + }, + ], + "summary": Object { + "down": 0, + "up": 2, + }, + "timestamp": "125", + "url": Object {}, + }, + }, + ], + "totalSummaryCount": 2, + }, + "loading": true, + } + } + /> +</ContextProvider> +`; + +exports[`MonitorList component renders the monitor list 1`] = ` +.c1 { + padding-left: 17px; +} + +.c3 { + padding-top: 12px; +} + +.c2 { + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +@media (max-width:574px) { + .c0 { + min-width: 230px; + } +} + +<div + class="euiPanel euiPanel--paddingMedium" +> + <h5 + class="euiTitle euiTitle--xsmall" + > + Monitor status + </h5> + <div + class="euiSpacer euiSpacer--s" + /> + <div + aria-label="Monitor Status table with columns for Status, Name, URL, IP, Downtime History and Integrations. The table is currently displaying 2 items." + class="euiBasicTable" + > + <div> + <div + class="euiTableHeaderMobile" + > + <div + class="euiFlexGroup euiFlexGroup--gutterLarge euiFlexGroup--alignItemsBaseline euiFlexGroup--justifyContentSpaceBetween euiFlexGroup--directionRow" + > + <div + class="euiFlexItem euiFlexItem--flexGrowZero" + /> + <div + class="euiFlexItem euiFlexItem--flexGrowZero" + /> + </div> + </div> + <table + class="euiTable euiTable--responsive" + > + <caption + class="euiScreenReaderOnly euiTableCaption" + /> + <thead> + <tr> + <th + class="euiTableHeaderCell euiTableHeaderCell--hideForMobile" + data-test-subj="tableHeaderCell_state.monitor.status_0" + role="columnheader" + scope="col" + > + <div + class="euiTableCellContent" + > + <span + class="euiTableCellContent__text" + > + Status + </span> + </div> + </th> + <th + class="euiTableHeaderCell euiTableHeaderCell--hideForMobile" + data-test-subj="tableHeaderCell_state.monitor.name_1" + role="columnheader" + scope="col" + > + <div + class="euiTableCellContent" + > + <span + class="euiTableCellContent__text" + > + Name + </span> + </div> + </th> + <th + class="euiTableHeaderCell" + data-test-subj="tableHeaderCell_state.url.full_2" + role="columnheader" + scope="col" + > + <div + class="euiTableCellContent" + > + <span + class="euiTableCellContent__text" + > + Url + </span> + </div> + </th> + <th + class="euiTableHeaderCell euiTableHeaderCell--hideForMobile" + data-test-subj="tableHeaderCell_histogram.points_3" + role="columnheader" + scope="col" + > + <div + class="euiTableCellContent euiTableCellContent--alignCenter" + > + <span + class="euiTableCellContent__text" + > + Downtime history + </span> + </div> + </th> + <td + class="euiTableHeaderCell" + data-test-subj="tableHeaderCell_monitor_id_4" + role="columnheader" + scope="col" + style="width:24px" + > + <div + class="euiTableCellContent euiTableCellContent--alignRight" + > + <span + class="euiTableCellContent__text" + /> + </div> + </td> + </tr> + </thead> + <tbody> + <tr + class="euiTableRow euiTableRow-hasActions euiTableRow-isExpandable" + > + <td + class="euiTableRowCell euiTableRowCell--isMobileFullWidth" + > + <div + class="euiTableRowCell__mobileHeader euiTableRowCell--hideForDesktop" + > + Status + </div> + <div + class="euiTableCellContent euiTableCellContent--overflowingContent" + > + <div + class="euiFlexGroup euiFlexGroup--alignItemsCenter euiFlexGroup--directionRow c0" + > + <div + class="euiFlexItem euiFlexItem--flexGrow1" + style="flex-basis:40px" + > + <div + class="euiHealth" + style="display:block" + > + <div + class="euiFlexGroup euiFlexGroup--gutterExtraSmall euiFlexGroup--alignItemsCenter euiFlexGroup--directionRow" + > + <div + class="euiFlexItem euiFlexItem--flexGrowZero" + > + <div + color="" + data-euiicon-type="dot" + /> + </div> + <div + class="euiFlexItem euiFlexItem--flexGrowZero" + /> + </div> + </div> + <span + class="c1" + > + <span + class="euiToolTipAnchor" + > + <div + class="euiText euiText--extraSmall" + > + <div + class="euiTextColor euiTextColor--subdued" + > + 1897 Yr ago + </div> + </div> + </span> + </span> + </div> + <div + class="euiFlexItem euiFlexItem--flexGrow2" + > + <div + class="euiText euiText--small" + > + in 0/1 Location + </div> + </div> + </div> + </div> + </td> + <td + class="euiTableRowCell euiTableRowCell--isMobileFullWidth" + > + <div + class="euiTableRowCell__mobileHeader euiTableRowCell--hideForDesktop" + > + Name + </div> + <div + class="euiTableCellContent euiTableCellContent--overflowingContent" + > + <button + class="euiLink euiLink--primary" + type="button" + > + <a + data-test-subj="monitor-page-link-foo" + href="/monitor/Zm9v" + > + Unnamed - foo + </a> + </button> + </div> + </td> + <td + class="euiTableRowCell" + > + <div + class="euiTableRowCell__mobileHeader euiTableRowCell--hideForDesktop" + > + Url + </div> + <div + class="euiTableCellContent euiTableCellContent--overflowingContent" + > + <button + class="euiLink euiLink--text c2" + type="button" + > + + <div + color="subbdued" + data-euiicon-type="popout" + /> + </button> + </div> + </td> + <td + class="euiTableRowCell euiTableRowCell--hideForMobile" + > + <div + class="euiTableCellContent euiTableCellContent--alignCenter euiTableCellContent--overflowingContent" + > + <span + class="euiToolTipAnchor" + > + <div + class="euiText euiText--medium" + > + <div + class="euiTextColor euiTextColor--secondary" + > + -- + </div> + </div> + </span> + </div> + </td> + <td + class="euiTableRowCell euiTableRowCell--isExpander" + style="width:24px" + > + <div + class="euiTableCellContent euiTableCellContent--alignRight euiTableCellContent--overflowingContent" + > + <button + aria-label="Expand row for monitor with ID foo" + class="euiButtonIcon euiButtonIcon--primary" + type="button" + > + <div + aria-hidden="true" + class="euiButtonIcon__icon" + data-euiicon-type="arrowDown" + /> + </button> + </div> + </td> + </tr> + <tr + class="euiTableRow euiTableRow-hasActions euiTableRow-isExpandable" + > + <td + class="euiTableRowCell euiTableRowCell--isMobileFullWidth" + > + <div + class="euiTableRowCell__mobileHeader euiTableRowCell--hideForDesktop" + > + Status + </div> + <div + class="euiTableCellContent euiTableCellContent--overflowingContent" + > + <div + class="euiFlexGroup euiFlexGroup--alignItemsCenter euiFlexGroup--directionRow c0" + > + <div + class="euiFlexItem euiFlexItem--flexGrow1" + style="flex-basis:40px" + > + <div + class="euiHealth" + style="display:block" + > + <div + class="euiFlexGroup euiFlexGroup--gutterExtraSmall euiFlexGroup--alignItemsCenter euiFlexGroup--directionRow" + > + <div + class="euiFlexItem euiFlexItem--flexGrowZero" + > + <div + color="" + data-euiicon-type="dot" + /> + </div> + <div + class="euiFlexItem euiFlexItem--flexGrowZero" + /> + </div> + </div> + <span + class="c1" + > + <span + class="euiToolTipAnchor" + > + <div + class="euiText euiText--extraSmall" + > + <div + class="euiTextColor euiTextColor--subdued" + > + 1895 Yr ago + </div> + </div> + </span> + </span> + </div> + <div + class="euiFlexItem euiFlexItem--flexGrow2" + > + <div + class="euiText euiText--small" + > + in 1/1 Location + </div> + </div> + </div> + </div> + </td> + <td + class="euiTableRowCell euiTableRowCell--isMobileFullWidth" + > + <div + class="euiTableRowCell__mobileHeader euiTableRowCell--hideForDesktop" + > + Name + </div> + <div + class="euiTableCellContent euiTableCellContent--overflowingContent" + > + <button + class="euiLink euiLink--primary" + type="button" + > + <a + data-test-subj="monitor-page-link-bar" + href="/monitor/YmFy" + > + Unnamed - bar + </a> + </button> + </div> + </td> + <td + class="euiTableRowCell" + > + <div + class="euiTableRowCell__mobileHeader euiTableRowCell--hideForDesktop" + > + Url + </div> + <div + class="euiTableCellContent euiTableCellContent--overflowingContent" + > + <button + class="euiLink euiLink--text c2" + type="button" + > + + <div + color="subbdued" + data-euiicon-type="popout" + /> + </button> + </div> + </td> + <td + class="euiTableRowCell euiTableRowCell--hideForMobile" + > + <div + class="euiTableCellContent euiTableCellContent--alignCenter euiTableCellContent--overflowingContent" + > + <span + class="euiToolTipAnchor" + > + <div + class="euiText euiText--medium" + > + <div + class="euiTextColor euiTextColor--secondary" + > + -- + </div> + </div> + </span> + </div> + </td> + <td + class="euiTableRowCell euiTableRowCell--isExpander" + style="width:24px" + > + <div + class="euiTableCellContent euiTableCellContent--alignRight euiTableCellContent--overflowingContent" + > + <button + aria-label="Expand row for monitor with ID bar" + class="euiButtonIcon euiButtonIcon--primary" + type="button" + > + <div + aria-hidden="true" + class="euiButtonIcon__icon" + data-euiicon-type="arrowDown" + /> + </button> + </div> + </td> + </tr> + </tbody> + </table> + </div> + </div> + <div + class="euiSpacer euiSpacer--m" + /> + <div + class="euiFlexGroup euiFlexGroup--gutterLarge euiFlexGroup--justifyContentSpaceBetween euiFlexGroup--directionRow" + > + <div + class="euiFlexItem euiFlexItem--flexGrowZero" + > + <div + class="euiPopover euiPopover--anchorUpLeft" + > + <div + class="euiPopover__anchor" + > + <button + class="euiButtonEmpty euiButtonEmpty--text euiButtonEmpty--iconRight" + data-test-subj="xpack.uptime.monitorList.pageSizeSelect.popoverOpen" + type="button" + > + <span + class="euiButtonEmpty__content" + > + <div + aria-hidden="true" + class="euiButtonEmpty__icon" + data-euiicon-type="arrowDown" + /> + <span + class="euiButtonEmpty__text" + > + Rows per page: 25 + </span> + </span> + </button> + </div> + </div> + </div> + <div + class="euiFlexItem euiFlexItem--flexGrowZero" + > + <div + class="euiFlexGroup euiFlexGroup--gutterLarge euiFlexGroup--directionRow" + > + <div + class="euiFlexItem euiFlexItem--flexGrowZero" + > + <button + aria-label="A disabled pagination button indicating that there cannot be any further navigation in the monitors list." + class="euiButtonIcon euiButtonIcon--text c3" + data-test-subj="xpack.uptime.monitorList.prevButton" + disabled="" + type="button" + > + <div + aria-hidden="true" + class="euiButtonIcon__icon" + data-euiicon-type="arrowLeft" + /> + </button> + </div> + <div + class="euiFlexItem euiFlexItem--flexGrowZero" + > + <button + aria-label="A disabled pagination button indicating that there cannot be any further navigation in the monitors list." + class="euiButtonIcon euiButtonIcon--text c3" + data-test-subj="xpack.uptime.monitorList.nextButton" + disabled="" + type="button" + > + <div + aria-hidden="true" + class="euiButtonIcon__icon" + data-euiicon-type="arrowRight" + /> + </button> + </div> + </div> + </div> + </div> +</div> +`; + +exports[`MonitorList component shallow renders the monitor list 1`] = ` +<ContextProvider + value={ + Object { + "history": Object { + "action": "POP", + "block": [Function], + "canGo": [Function], + "createHref": [Function], + "entries": Array [ + Object { + "hash": "", + "key": "TestKeyForTesting", + "pathname": "/", + "search": "", + "state": undefined, + }, + ], + "go": [Function], + "goBack": [Function], + "goForward": [Function], + "index": 0, + "length": 1, + "listen": [Function], + "location": Object { + "hash": "", + "key": "TestKeyForTesting", + "pathname": "/", + "search": "", + "state": undefined, + }, + "push": [Function], + "replace": [Function], + }, + "location": Object { + "hash": "", + "key": "TestKeyForTesting", + "pathname": "/", + "search": "", + "state": undefined, + }, + "match": Object { + "isExact": true, + "params": Object {}, + "path": "/", + "url": "/", + }, + "staticContext": undefined, + } + } +> + <MonitorListComponent + getMonitorList={[MockFunction]} + lastRefresh={123} + monitorList={ + Object { + "list": Object { + "nextPagePagination": null, + "prevPagePagination": null, + "summaries": Array [ + Object { + "monitor_id": "foo", + "state": Object { + "checks": Array [ + Object { + "monitor": Object { + "ip": "127.0.0.1", + "status": "up", + }, + "timestamp": 124, + }, + Object { + "monitor": Object { + "ip": "127.0.0.2", + "status": "down", + }, + "timestamp": 125, + }, + Object { + "monitor": Object { + "ip": "127.0.0.3", + "status": "down", + }, + "timestamp": 126, + }, + ], + "summary": Object { + "down": 2, + "up": 1, + }, + "timestamp": "123", + "url": Object {}, + }, + }, + Object { + "monitor_id": "bar", + "state": Object { + "checks": Array [ + Object { + "monitor": Object { + "ip": "127.0.0.1", + "status": "up", + }, + "timestamp": 125, + }, + Object { + "monitor": Object { + "ip": "127.0.0.2", + "status": "up", + }, + "timestamp": 126, + }, + ], + "summary": Object { + "down": 0, + "up": 2, + }, + "timestamp": "125", + "url": Object {}, + }, + }, + ], + "totalSummaryCount": 2, + }, + "loading": false, + } + } + /> +</ContextProvider> +`; diff --git a/x-pack/legacy/plugins/uptime/public/components/functional/monitor_list/__tests__/__snapshots__/monitor_list_status_column.test.tsx.snap b/x-pack/legacy/plugins/uptime/public/components/overview/monitor_list/__tests__/__snapshots__/monitor_list_status_column.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/functional/monitor_list/__tests__/__snapshots__/monitor_list_status_column.test.tsx.snap rename to x-pack/legacy/plugins/uptime/public/components/overview/monitor_list/__tests__/__snapshots__/monitor_list_status_column.test.tsx.snap diff --git a/x-pack/legacy/plugins/uptime/public/components/functional/monitor_list/__tests__/__snapshots__/monitor_page_link.test.tsx.snap b/x-pack/legacy/plugins/uptime/public/components/overview/monitor_list/__tests__/__snapshots__/monitor_page_link.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/functional/monitor_list/__tests__/__snapshots__/monitor_page_link.test.tsx.snap rename to x-pack/legacy/plugins/uptime/public/components/overview/monitor_list/__tests__/__snapshots__/monitor_page_link.test.tsx.snap diff --git a/x-pack/legacy/plugins/uptime/public/components/overview/monitor_list/__tests__/monitor_list.test.tsx b/x-pack/legacy/plugins/uptime/public/components/overview/monitor_list/__tests__/monitor_list.test.tsx new file mode 100644 index 0000000000000..9b1d799a23e37 --- /dev/null +++ b/x-pack/legacy/plugins/uptime/public/components/overview/monitor_list/__tests__/monitor_list.test.tsx @@ -0,0 +1,284 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { + MonitorSummaryResult, + CursorDirection, + SortOrder, +} from '../../../../../common/runtime_types'; +import { MonitorListComponent } from '../monitor_list'; +import { renderWithRouter, shallowWithRouter } from '../../../../lib'; + +describe('MonitorList component', () => { + let result: MonitorSummaryResult; + let localStorageMock: any; + + beforeEach(() => { + localStorageMock = { + getItem: jest.fn().mockImplementation(() => '25'), + setItem: jest.fn(), + }; + + // @ts-ignore replacing a call to localStorage we use for monitor list size + global.localStorage = localStorageMock; + result = { + nextPagePagination: null, + prevPagePagination: null, + summaries: [ + { + monitor_id: 'foo', + state: { + checks: [ + { + monitor: { + ip: '127.0.0.1', + status: 'up', + }, + timestamp: 124, + }, + { + monitor: { + ip: '127.0.0.2', + status: 'down', + }, + timestamp: 125, + }, + { + monitor: { + ip: '127.0.0.3', + status: 'down', + }, + timestamp: 126, + }, + ], + summary: { + up: 1, + down: 2, + }, + timestamp: '123', + url: {}, + }, + }, + { + monitor_id: 'bar', + state: { + checks: [ + { + monitor: { + ip: '127.0.0.1', + status: 'up', + }, + timestamp: 125, + }, + { + monitor: { + ip: '127.0.0.2', + status: 'up', + }, + timestamp: 126, + }, + ], + summary: { + up: 2, + down: 0, + }, + timestamp: '125', + url: {}, + }, + }, + ], + totalSummaryCount: 2, + }; + }); + + it('shallow renders the monitor list', () => { + const component = shallowWithRouter( + <MonitorListComponent + monitorList={{ list: result, loading: false }} + lastRefresh={123} + getMonitorList={jest.fn()} + /> + ); + + expect(component).toMatchSnapshot(); + }); + + it('renders a no items message when no data is provided', () => { + const component = shallowWithRouter( + <MonitorListComponent + monitorList={{ + list: { + summaries: [], + nextPagePagination: null, + prevPagePagination: null, + totalSummaryCount: 0, + }, + loading: true, + }} + lastRefresh={123} + getMonitorList={jest.fn()} + /> + ); + expect(component).toMatchSnapshot(); + }); + + it('renders the monitor list', () => { + const component = renderWithRouter( + <MonitorListComponent + monitorList={{ list: result, loading: false }} + lastRefresh={123} + getMonitorList={jest.fn()} + /> + ); + + expect(component).toMatchSnapshot(); + }); + + it('renders error list', () => { + const component = shallowWithRouter( + <MonitorListComponent + monitorList={{ list: result, error: new Error('foo message'), loading: false }} + lastRefresh={123} + getMonitorList={jest.fn()} + /> + ); + + expect(component).toMatchSnapshot(); + }); + + it('renders loading state', () => { + const component = shallowWithRouter( + <MonitorListComponent + monitorList={{ list: result, loading: true }} + lastRefresh={123} + getMonitorList={jest.fn()} + /> + ); + + expect(component).toMatchSnapshot(); + }); + + describe('MonitorListPagination component', () => { + let paginationResult: MonitorSummaryResult; + + beforeEach(() => { + paginationResult = { + prevPagePagination: JSON.stringify({ + cursorKey: { monitor_id: 123 }, + cursorDirection: CursorDirection.BEFORE, + sortOrder: SortOrder.ASC, + }), + nextPagePagination: JSON.stringify({ + cursorKey: { monitor_id: 456 }, + cursorDirection: CursorDirection.AFTER, + sortOrder: SortOrder.ASC, + }), + summaries: [ + { + monitor_id: 'foo', + state: { + checks: [ + { + monitor: { + ip: '127.0.0.1', + status: 'up', + }, + timestamp: 124, + }, + { + monitor: { + ip: '127.0.0.2', + status: 'down', + }, + timestamp: 125, + }, + { + monitor: { + ip: '127.0.0.3', + status: 'down', + }, + timestamp: 126, + }, + ], + summary: { + up: 1, + down: 2, + }, + timestamp: '123', + url: {}, + }, + }, + { + monitor_id: 'bar', + state: { + checks: [ + { + monitor: { + ip: '127.0.0.1', + status: 'up', + }, + timestamp: 125, + }, + { + monitor: { + ip: '127.0.0.2', + status: 'up', + }, + timestamp: 126, + }, + ], + summary: { + up: 2, + down: 0, + }, + timestamp: '125', + url: {}, + }, + }, + ], + totalSummaryCount: 2, + }; + }); + + it('renders the pagination', () => { + const component = shallowWithRouter( + <MonitorListComponent + monitorList={{ + list: { + ...paginationResult, + }, + loading: false, + }} + lastRefresh={123} + getMonitorList={jest.fn()} + /> + ); + + expect(component).toMatchSnapshot(); + }); + + it('renders a no items message when no data is provided', () => { + const component = shallowWithRouter( + <MonitorListComponent + monitorList={{ + list: { + summaries: [], + nextPagePagination: null, + prevPagePagination: null, + totalSummaryCount: 0, + }, + loading: false, + }} + lastRefresh={123} + getMonitorList={jest.fn()} + /> + ); + + expect(component).toMatchSnapshot(); + }); + }); +}); diff --git a/x-pack/legacy/plugins/uptime/public/components/functional/monitor_list/__tests__/monitor_list_page_size_select.test.tsx b/x-pack/legacy/plugins/uptime/public/components/overview/monitor_list/__tests__/monitor_list_page_size_select.test.tsx similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/functional/monitor_list/__tests__/monitor_list_page_size_select.test.tsx rename to x-pack/legacy/plugins/uptime/public/components/overview/monitor_list/__tests__/monitor_list_page_size_select.test.tsx diff --git a/x-pack/legacy/plugins/uptime/public/components/functional/monitor_list/__tests__/monitor_list_status_column.test.tsx b/x-pack/legacy/plugins/uptime/public/components/overview/monitor_list/__tests__/monitor_list_status_column.test.tsx similarity index 79% rename from x-pack/legacy/plugins/uptime/public/components/functional/monitor_list/__tests__/monitor_list_status_column.test.tsx rename to x-pack/legacy/plugins/uptime/public/components/overview/monitor_list/__tests__/monitor_list_status_column.test.tsx index 406e18535f34c..d765c0b33ea4b 100644 --- a/x-pack/legacy/plugins/uptime/public/components/functional/monitor_list/__tests__/monitor_list_status_column.test.tsx +++ b/x-pack/legacy/plugins/uptime/public/components/overview/monitor_list/__tests__/monitor_list_status_column.test.tsx @@ -8,7 +8,7 @@ import React from 'react'; import moment from 'moment'; import { renderWithIntl, shallowWithIntl } from 'test_utils/enzyme_helpers'; import { getLocationStatus, MonitorListStatusColumn } from '../monitor_list_status_column'; -import { Check } from '../../../../../common/graphql/types'; +import { Check } from '../../../../../common/runtime_types'; import { STATUS } from '../../../../../common/constants'; describe('MonitorListStatusColumn', () => { @@ -29,9 +29,6 @@ describe('MonitorListStatusColumn', () => { beforeEach(() => { upChecks = [ { - agent: { id: '6a2f2a1c-e346-49ed-8418-6d48af8841d6' }, - container: null, - kubernetes: null, monitor: { ip: '104.86.46.103', name: '', @@ -46,12 +43,9 @@ describe('MonitorListStatusColumn', () => { }, }, }, - timestamp: '1579794631464', + timestamp: 1579794631464, }, { - agent: { id: '1117fd01-bc1a-4aa5-bfab-40ab455eadf9' }, - container: null, - kubernetes: null, monitor: { ip: '104.86.46.103', name: '', @@ -66,12 +60,9 @@ describe('MonitorListStatusColumn', () => { }, }, }, - timestamp: '1579794634220', + timestamp: 1579794634220, }, { - agent: { id: 'eda59510-45e8-4dfe-b0f8-959c449e3565' }, - container: null, - kubernetes: null, monitor: { ip: '104.86.46.103', name: '', @@ -86,15 +77,12 @@ describe('MonitorListStatusColumn', () => { }, }, }, - timestamp: '1579794628368', + timestamp: 1579794628368, }, ]; downChecks = [ { - agent: { id: '6a2f2a1c-e346-49ed-8418-6d48af8841d6' }, - container: null, - kubernetes: null, monitor: { ip: '104.86.46.103', name: '', @@ -109,12 +97,9 @@ describe('MonitorListStatusColumn', () => { }, }, }, - timestamp: '1579794631464', + timestamp: 1579794631464, }, { - agent: { id: '1117fd01-bc1a-4aa5-bfab-40ab455eadf9' }, - container: null, - kubernetes: null, monitor: { ip: '104.86.46.103', name: '', @@ -129,12 +114,9 @@ describe('MonitorListStatusColumn', () => { }, }, }, - timestamp: '1579794634220', + timestamp: 1579794634220, }, { - agent: { id: 'eda59510-45e8-4dfe-b0f8-959c449e3565' }, - container: null, - kubernetes: null, monitor: { ip: '104.86.46.103', name: '', @@ -149,15 +131,12 @@ describe('MonitorListStatusColumn', () => { }, }, }, - timestamp: '1579794628368', + timestamp: 1579794628368, }, ]; checks = [ { - agent: { id: '6a2f2a1c-e346-49ed-8418-6d48af8841d6' }, - container: null, - kubernetes: null, monitor: { ip: '104.86.46.103', name: '', @@ -172,12 +151,9 @@ describe('MonitorListStatusColumn', () => { }, }, }, - timestamp: '1579794631464', + timestamp: 1579794631464, }, { - agent: { id: '1117fd01-bc1a-4aa5-bfab-40ab455eadf9' }, - container: null, - kubernetes: null, monitor: { ip: '104.86.46.103', name: '', @@ -192,12 +168,9 @@ describe('MonitorListStatusColumn', () => { }, }, }, - timestamp: '1579794634220', + timestamp: 1579794634220, }, { - agent: { id: 'eda59510-45e8-4dfe-b0f8-959c449e3565' }, - container: null, - kubernetes: null, monitor: { ip: '104.86.46.103', name: '', @@ -212,7 +185,7 @@ describe('MonitorListStatusColumn', () => { }, }, }, - timestamp: '1579794628368', + timestamp: 1579794628368, }, ]; }); diff --git a/x-pack/legacy/plugins/uptime/public/components/functional/monitor_list/__tests__/monitor_page_link.test.tsx b/x-pack/legacy/plugins/uptime/public/components/overview/monitor_list/__tests__/monitor_page_link.test.tsx similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/functional/monitor_list/__tests__/monitor_page_link.test.tsx rename to x-pack/legacy/plugins/uptime/public/components/overview/monitor_list/__tests__/monitor_page_link.test.tsx diff --git a/x-pack/legacy/plugins/uptime/public/components/functional/monitor_list/__tests__/parse_timestamp.test.ts b/x-pack/legacy/plugins/uptime/public/components/overview/monitor_list/__tests__/parse_timestamp.test.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/functional/monitor_list/__tests__/parse_timestamp.test.ts rename to x-pack/legacy/plugins/uptime/public/components/overview/monitor_list/__tests__/parse_timestamp.test.ts diff --git a/x-pack/legacy/plugins/uptime/public/components/overview/monitor_list/index.ts b/x-pack/legacy/plugins/uptime/public/components/overview/monitor_list/index.ts new file mode 100644 index 0000000000000..45e8822a317a4 --- /dev/null +++ b/x-pack/legacy/plugins/uptime/public/components/overview/monitor_list/index.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. + */ + +export { MonitorListComponent } from './monitor_list'; +export { Criteria, Pagination } from './types'; +export { LocationLink } from './monitor_list_drawer'; +export { MonitorListDrawer } from './monitor_list_drawer/list_drawer_container'; +export { ActionsPopover } from './monitor_list_drawer/actions_popover/actions_popover_container'; diff --git a/x-pack/legacy/plugins/uptime/public/components/overview/monitor_list/monitor_list.tsx b/x-pack/legacy/plugins/uptime/public/components/overview/monitor_list/monitor_list.tsx new file mode 100644 index 0000000000000..18e2e2437e147 --- /dev/null +++ b/x-pack/legacy/plugins/uptime/public/components/overview/monitor_list/monitor_list.tsx @@ -0,0 +1,240 @@ +/* + * 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 { + EuiButtonIcon, + EuiBasicTable, + EuiFlexGroup, + EuiFlexItem, + EuiIcon, + EuiLink, + EuiPanel, + EuiSpacer, + EuiTitle, +} from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n/react'; +import React, { useState, useEffect } from 'react'; +import styled from 'styled-components'; +import { HistogramPoint, FetchMonitorStatesQueryArgs } from '../../../../common/runtime_types'; +import { MonitorSummary } from '../../../../common/runtime_types'; +import { MonitorListStatusColumn } from './monitor_list_status_column'; +import { ExpandedRowMap } from './types'; +import { MonitorBarSeries } from '../../common/charts'; +import { MonitorPageLink } from './monitor_page_link'; +import { OverviewPageLink } from './overview_page_link'; +import * as labels from './translations'; +import { MonitorListPageSizeSelect } from './monitor_list_page_size_select'; +import { MonitorListDrawer } from './monitor_list_drawer/list_drawer_container'; +import { MonitorListProps } from './monitor_list_container'; +import { MonitorList } from '../../../state/reducers/monitor_list'; +import { useUrlParams } from '../../../hooks'; + +interface Props extends MonitorListProps { + lastRefresh: number; + monitorList: MonitorList; + getMonitorList: (params: FetchMonitorStatesQueryArgs) => void; +} + +const TruncatedEuiLink = styled(EuiLink)` + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +`; + +const DEFAULT_PAGE_SIZE = 10; +const LOCAL_STORAGE_KEY = 'xpack.uptime.monitorList.pageSize'; +const getPageSizeValue = () => { + const value = parseInt(localStorage.getItem(LOCAL_STORAGE_KEY) ?? '', 10); + if (isNaN(value)) { + return DEFAULT_PAGE_SIZE; + } + return value; +}; + +export const MonitorListComponent: React.FC<Props> = ({ + filters, + getMonitorList, + lastRefresh, + monitorList: { list, error, loading }, + linkParameters, +}) => { + const [pageSize, setPageSize] = useState<number>(getPageSizeValue()); + const [drawerIds, updateDrawerIds] = useState<string[]>([]); + + const [getUrlValues] = useUrlParams(); + const { dateRangeStart, dateRangeEnd, pagination, statusFilter } = getUrlValues(); + + useEffect(() => { + getMonitorList({ + dateRangeStart, + dateRangeEnd, + filters, + pageSize, + pagination, + statusFilter, + }); + }, [ + getMonitorList, + dateRangeStart, + dateRangeEnd, + filters, + lastRefresh, + pageSize, + pagination, + statusFilter, + ]); + + const items = list.summaries ?? []; + + const nextPagePagination = list.nextPagePagination ?? ''; + const prevPagePagination = list.prevPagePagination ?? ''; + + const getExpandedRowMap = () => { + return drawerIds.reduce((map: ExpandedRowMap, id: string) => { + return { + ...map, + [id]: ( + <MonitorListDrawer + summary={items.find(({ monitor_id: monitorId }) => monitorId === id)} + /> + ), + }; + }, {}); + }; + + const columns = [ + { + align: 'left' as const, + field: 'state.monitor.status', + name: labels.STATUS_COLUMN_LABEL, + mobileOptions: { + fullWidth: true, + }, + render: (status: string, { state: { timestamp, checks } }: MonitorSummary) => { + return ( + <MonitorListStatusColumn status={status} timestamp={timestamp} checks={checks ?? []} /> + ); + }, + }, + { + align: 'left' as const, + field: 'state.monitor.name', + name: labels.NAME_COLUMN_LABEL, + mobileOptions: { + fullWidth: true, + }, + render: (name: string, summary: MonitorSummary) => ( + <MonitorPageLink monitorId={summary.monitor_id} linkParameters={linkParameters}> + {name ? name : `Unnamed - ${summary.monitor_id}`} + </MonitorPageLink> + ), + sortable: true, + }, + { + align: 'left' as const, + field: 'state.url.full', + name: labels.URL, + render: (url: string, summary: MonitorSummary) => ( + <TruncatedEuiLink href={url} target="_blank" color="text"> + {url} <EuiIcon size="s" type="popout" color="subbdued" /> + </TruncatedEuiLink> + ), + }, + { + align: 'center' as const, + field: 'histogram.points', + name: labels.HISTORY_COLUMN_LABEL, + mobileOptions: { + show: false, + }, + render: (histogramSeries: HistogramPoint[] | null) => ( + <MonitorBarSeries histogramSeries={histogramSeries} /> + ), + }, + { + align: 'right' as const, + field: 'monitor_id', + name: '', + sortable: true, + isExpander: true, + width: '24px', + render: (id: string) => { + return ( + <EuiButtonIcon + aria-label={labels.getExpandDrawerLabel(id)} + iconType={drawerIds.includes(id) ? 'arrowUp' : 'arrowDown'} + onClick={() => { + if (drawerIds.includes(id)) { + updateDrawerIds(drawerIds.filter(p => p !== id)); + } else { + updateDrawerIds([...drawerIds, id]); + } + }} + /> + ); + }, + }, + ]; + + return ( + <EuiPanel> + <EuiTitle size="xs"> + <h5> + <FormattedMessage + id="xpack.uptime.monitorList.monitoringStatusTitle" + defaultMessage="Monitor status" + /> + </h5> + </EuiTitle> + <EuiSpacer size="s" /> + <EuiBasicTable + aria-label={labels.getDescriptionLabel(items.length)} + error={error?.message} + // Only set loading to true when there are no items present to prevent the bug outlined in + // in https://github.com/elastic/eui/issues/2393 . Once that is fixed we can simply set the value here to + // loading={loading} + loading={loading && (!items || items.length < 1)} + isExpandable={true} + hasActions={true} + itemId="monitor_id" + itemIdToExpandedRowMap={getExpandedRowMap()} + items={items} + // TODO: not needed without sorting and pagination + // onChange={onChange} + noItemsMessage={!!filters ? labels.NO_MONITOR_ITEM_SELECTED : labels.NO_DATA_MESSAGE} + // TODO: reintegrate pagination in future release + // pagination={pagination} + // TODO: reintegrate sorting in future release + // sorting={sorting} + columns={columns} + /> + <EuiSpacer size="m" /> + <EuiFlexGroup justifyContent="spaceBetween" responsive={false}> + <EuiFlexItem grow={false}> + <MonitorListPageSizeSelect size={pageSize} setSize={setPageSize} /> + </EuiFlexItem> + <EuiFlexItem grow={false}> + <EuiFlexGroup responsive={false}> + <EuiFlexItem grow={false}> + <OverviewPageLink + dataTestSubj="xpack.uptime.monitorList.prevButton" + direction="prev" + pagination={prevPagePagination} + /> + </EuiFlexItem> + <EuiFlexItem grow={false}> + <OverviewPageLink + dataTestSubj="xpack.uptime.monitorList.nextButton" + direction="next" + pagination={nextPagePagination} + /> + </EuiFlexItem> + </EuiFlexGroup> + </EuiFlexItem> + </EuiFlexGroup> + </EuiPanel> + ); +}; diff --git a/x-pack/legacy/plugins/uptime/public/components/overview/monitor_list/monitor_list_container.tsx b/x-pack/legacy/plugins/uptime/public/components/overview/monitor_list/monitor_list_container.tsx new file mode 100644 index 0000000000000..5bfe6ff0c5b4f --- /dev/null +++ b/x-pack/legacy/plugins/uptime/public/components/overview/monitor_list/monitor_list_container.tsx @@ -0,0 +1,34 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { useCallback } from 'react'; +import { useSelector, useDispatch } from 'react-redux'; +import { getMonitorList } from '../../../state/actions'; +import { FetchMonitorStatesQueryArgs } from '../../../../common/runtime_types'; +import { monitorListSelector } from '../../../state/selectors'; +import { MonitorListComponent } from './index'; + +export interface MonitorListProps { + filters?: string; + linkParameters?: string; +} + +export const MonitorList: React.FC<MonitorListProps> = props => { + const dispatch = useDispatch(); + + const dispatchCallback = useCallback( + (params: FetchMonitorStatesQueryArgs) => { + dispatch(getMonitorList(params)); + }, + [dispatch] + ); + + const monitorListState = useSelector(monitorListSelector); + + return ( + <MonitorListComponent {...props} {...monitorListState} getMonitorList={dispatchCallback} /> + ); +}; diff --git a/x-pack/legacy/plugins/uptime/public/components/functional/monitor_list/monitor_list_drawer/__tests__/__snapshots__/integration_group.test.tsx.snap b/x-pack/legacy/plugins/uptime/public/components/overview/monitor_list/monitor_list_drawer/__tests__/__snapshots__/integration_group.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/functional/monitor_list/monitor_list_drawer/__tests__/__snapshots__/integration_group.test.tsx.snap rename to x-pack/legacy/plugins/uptime/public/components/overview/monitor_list/monitor_list_drawer/__tests__/__snapshots__/integration_group.test.tsx.snap diff --git a/x-pack/legacy/plugins/uptime/public/components/functional/monitor_list/monitor_list_drawer/__tests__/__snapshots__/integration_link.test.tsx.snap b/x-pack/legacy/plugins/uptime/public/components/overview/monitor_list/monitor_list_drawer/__tests__/__snapshots__/integration_link.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/functional/monitor_list/monitor_list_drawer/__tests__/__snapshots__/integration_link.test.tsx.snap rename to x-pack/legacy/plugins/uptime/public/components/overview/monitor_list/monitor_list_drawer/__tests__/__snapshots__/integration_link.test.tsx.snap diff --git a/x-pack/legacy/plugins/uptime/public/components/functional/monitor_list/monitor_list_drawer/__tests__/__snapshots__/monitor_list_drawer.test.tsx.snap b/x-pack/legacy/plugins/uptime/public/components/overview/monitor_list/monitor_list_drawer/__tests__/__snapshots__/monitor_list_drawer.test.tsx.snap similarity index 97% rename from x-pack/legacy/plugins/uptime/public/components/functional/monitor_list/monitor_list_drawer/__tests__/__snapshots__/monitor_list_drawer.test.tsx.snap rename to x-pack/legacy/plugins/uptime/public/components/overview/monitor_list/monitor_list_drawer/__tests__/__snapshots__/monitor_list_drawer.test.tsx.snap index cf754581b1a33..4520b760be379 100644 --- a/x-pack/legacy/plugins/uptime/public/components/functional/monitor_list/monitor_list_drawer/__tests__/__snapshots__/monitor_list_drawer.test.tsx.snap +++ b/x-pack/legacy/plugins/uptime/public/components/overview/monitor_list/monitor_list_drawer/__tests__/__snapshots__/monitor_list_drawer.test.tsx.snap @@ -71,21 +71,21 @@ exports[`MonitorListDrawer component renders a MonitorListDrawer when there are "ip": "127.0.0.1", "status": "up", }, - "timestamp": "121", + "timestamp": 121, }, Object { "monitor": Object { "ip": "127.0.0.2", "status": "down", }, - "timestamp": "123", + "timestamp": 123, }, Object { "monitor": Object { "ip": "127.0.0.3", "status": "up", }, - "timestamp": "125", + "timestamp": 125, }, ], "summary": Object { @@ -175,7 +175,7 @@ exports[`MonitorListDrawer component renders a MonitorListDrawer when there is o "ip": "127.0.0.1", "status": "up", }, - "timestamp": "121", + "timestamp": 121, }, ], "summary": Object { diff --git a/x-pack/legacy/plugins/uptime/public/components/functional/monitor_list/monitor_list_drawer/__tests__/__snapshots__/monitor_status_list.test.tsx.snap b/x-pack/legacy/plugins/uptime/public/components/overview/monitor_list/monitor_list_drawer/__tests__/__snapshots__/monitor_status_list.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/functional/monitor_list/monitor_list_drawer/__tests__/__snapshots__/monitor_status_list.test.tsx.snap rename to x-pack/legacy/plugins/uptime/public/components/overview/monitor_list/monitor_list_drawer/__tests__/__snapshots__/monitor_status_list.test.tsx.snap diff --git a/x-pack/legacy/plugins/uptime/public/components/functional/monitor_list/monitor_list_drawer/__tests__/__snapshots__/monitor_status_row.test.tsx.snap b/x-pack/legacy/plugins/uptime/public/components/overview/monitor_list/monitor_list_drawer/__tests__/__snapshots__/monitor_status_row.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/functional/monitor_list/monitor_list_drawer/__tests__/__snapshots__/monitor_status_row.test.tsx.snap rename to x-pack/legacy/plugins/uptime/public/components/overview/monitor_list/monitor_list_drawer/__tests__/__snapshots__/monitor_status_row.test.tsx.snap diff --git a/x-pack/legacy/plugins/uptime/public/components/functional/monitor_list/monitor_list_drawer/__tests__/__snapshots__/most_recent_error.test.tsx.snap b/x-pack/legacy/plugins/uptime/public/components/overview/monitor_list/monitor_list_drawer/__tests__/__snapshots__/most_recent_error.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/functional/monitor_list/monitor_list_drawer/__tests__/__snapshots__/most_recent_error.test.tsx.snap rename to x-pack/legacy/plugins/uptime/public/components/overview/monitor_list/monitor_list_drawer/__tests__/__snapshots__/most_recent_error.test.tsx.snap diff --git a/x-pack/legacy/plugins/uptime/public/components/functional/monitor_list/monitor_list_drawer/__tests__/data.json b/x-pack/legacy/plugins/uptime/public/components/overview/monitor_list/monitor_list_drawer/__tests__/data.json similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/functional/monitor_list/monitor_list_drawer/__tests__/data.json rename to x-pack/legacy/plugins/uptime/public/components/overview/monitor_list/monitor_list_drawer/__tests__/data.json diff --git a/x-pack/legacy/plugins/uptime/public/components/functional/monitor_list/monitor_list_drawer/__tests__/integration_group.test.tsx b/x-pack/legacy/plugins/uptime/public/components/overview/monitor_list/monitor_list_drawer/__tests__/integration_group.test.tsx similarity index 87% rename from x-pack/legacy/plugins/uptime/public/components/functional/monitor_list/monitor_list_drawer/__tests__/integration_group.test.tsx rename to x-pack/legacy/plugins/uptime/public/components/overview/monitor_list/monitor_list_drawer/__tests__/integration_group.test.tsx index 723f8f9f4430a..25cf400bcd0fd 100644 --- a/x-pack/legacy/plugins/uptime/public/components/functional/monitor_list/monitor_list_drawer/__tests__/integration_group.test.tsx +++ b/x-pack/legacy/plugins/uptime/public/components/overview/monitor_list/monitor_list_drawer/__tests__/integration_group.test.tsx @@ -5,9 +5,9 @@ */ import React from 'react'; -import { MonitorSummary } from '../../../../../../common/graphql/types'; +import { MonitorSummary } from '../../../../../../common/runtime_types'; import { shallowWithIntl } from 'test_utils/enzyme_helpers'; -import { IntegrationGroup } from '../integration_group'; +import { IntegrationGroup } from '../actions_popover/integration_group'; describe('IntegrationGroup', () => { let summary: MonitorSummary; @@ -19,6 +19,7 @@ describe('IntegrationGroup', () => { summary: {}, checks: [], timestamp: '123', + url: {}, }, }; }); diff --git a/x-pack/legacy/plugins/uptime/public/components/functional/monitor_list/monitor_list_drawer/__tests__/integration_link.test.tsx b/x-pack/legacy/plugins/uptime/public/components/overview/monitor_list/monitor_list_drawer/__tests__/integration_link.test.tsx similarity index 93% rename from x-pack/legacy/plugins/uptime/public/components/functional/monitor_list/monitor_list_drawer/__tests__/integration_link.test.tsx rename to x-pack/legacy/plugins/uptime/public/components/overview/monitor_list/monitor_list_drawer/__tests__/integration_link.test.tsx index ba313f255f13d..8ee83bc38957b 100644 --- a/x-pack/legacy/plugins/uptime/public/components/functional/monitor_list/monitor_list_drawer/__tests__/integration_link.test.tsx +++ b/x-pack/legacy/plugins/uptime/public/components/overview/monitor_list/monitor_list_drawer/__tests__/integration_link.test.tsx @@ -5,7 +5,7 @@ */ import React from 'react'; -import { IntegrationLink } from '../integration_link'; +import { IntegrationLink } from '../actions_popover/integration_link'; import { shallowWithIntl } from 'test_utils/enzyme_helpers'; describe('IntegrationLink component', () => { diff --git a/x-pack/legacy/plugins/uptime/public/components/functional/monitor_list/monitor_list_drawer/__tests__/monitor_list_drawer.test.tsx b/x-pack/legacy/plugins/uptime/public/components/overview/monitor_list/monitor_list_drawer/__tests__/monitor_list_drawer.test.tsx similarity index 90% rename from x-pack/legacy/plugins/uptime/public/components/functional/monitor_list/monitor_list_drawer/__tests__/monitor_list_drawer.test.tsx rename to x-pack/legacy/plugins/uptime/public/components/overview/monitor_list/monitor_list_drawer/__tests__/monitor_list_drawer.test.tsx index d870acefaaea6..4bc0c3f0a40ba 100644 --- a/x-pack/legacy/plugins/uptime/public/components/functional/monitor_list/monitor_list_drawer/__tests__/monitor_list_drawer.test.tsx +++ b/x-pack/legacy/plugins/uptime/public/components/overview/monitor_list/monitor_list_drawer/__tests__/monitor_list_drawer.test.tsx @@ -4,10 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ import 'jest'; -import { MonitorSummary, Check } from '../../../../../../common/graphql/types'; import React from 'react'; import { MonitorListDrawerComponent } from '../monitor_list_drawer'; -import { MonitorDetails } from '../../../../../../common/runtime_types'; +import { Check, MonitorDetails, MonitorSummary } from '../../../../../../common/runtime_types'; import { shallowWithRouter } from '../../../../../lib'; describe('MonitorListDrawer component', () => { @@ -24,7 +23,7 @@ describe('MonitorListDrawer component', () => { ip: '127.0.0.1', status: 'up', }, - timestamp: '121', + timestamp: 121, }, ], summary: { @@ -77,21 +76,21 @@ describe('MonitorListDrawer component', () => { ip: '127.0.0.1', status: 'up', }, - timestamp: '121', + timestamp: 121, }, { monitor: { ip: '127.0.0.2', status: 'down', }, - timestamp: '123', + timestamp: 123, }, { monitor: { ip: '127.0.0.3', status: 'up', }, - timestamp: '125', + timestamp: 125, }, ]; summary.state.checks = checks; diff --git a/x-pack/legacy/plugins/uptime/public/components/overview/monitor_list/monitor_list_drawer/__tests__/monitor_status_list.test.tsx b/x-pack/legacy/plugins/uptime/public/components/overview/monitor_list/monitor_list_drawer/__tests__/monitor_status_list.test.tsx new file mode 100644 index 0000000000000..c7f3aef4075e1 --- /dev/null +++ b/x-pack/legacy/plugins/uptime/public/components/overview/monitor_list/monitor_list_drawer/__tests__/monitor_status_list.test.tsx @@ -0,0 +1,123 @@ +/* + * 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 { shallowWithIntl } from 'test_utils/enzyme_helpers'; +import React from 'react'; +import moment from 'moment'; +import { MonitorStatusList } from '../monitor_status_list'; +import { Check } from '../../../../../../common/runtime_types'; + +describe('MonitorStatusList component', () => { + let checks: Check[]; + + beforeAll(() => { + moment.prototype.toLocaleString = jest.fn(() => '2019-06-21 15:29:26'); + moment.prototype.from = jest.fn(() => 'a few moments ago'); + }); + + beforeEach(() => { + checks = [ + { + monitor: { + ip: '151.101.130.217', + name: 'elastic', + status: 'up', + }, + observer: { + geo: {}, + }, + timestamp: 1570538236414, + }, + { + monitor: { + ip: '151.101.194.217', + name: 'elastic', + status: 'up', + }, + observer: { + geo: {}, + }, + timestamp: 1570538236414, + }, + { + monitor: { + ip: '151.101.2.217', + name: 'elastic', + status: 'up', + }, + observer: { + geo: {}, + }, + timestamp: 1570538236414, + }, + { + monitor: { + ip: '151.101.66.217', + name: 'elastic', + status: 'up', + }, + observer: { + geo: {}, + }, + timestamp: 1570538236414, + }, + { + monitor: { + ip: '2a04:4e42:200::729', + name: 'elastic', + status: 'down', + }, + observer: { + geo: {}, + }, + timestamp: 1570538236414, + }, + { + monitor: { + ip: '2a04:4e42:400::729', + name: 'elastic', + status: 'down', + }, + observer: { + geo: {}, + }, + timestamp: 1570538236414, + }, + { + monitor: { + ip: '2a04:4e42:600::729', + name: 'elastic', + status: 'down', + }, + observer: { + geo: {}, + }, + timestamp: 1570538236414, + }, + { + monitor: { + ip: '2a04:4e42::729', + name: 'elastic', + status: 'down', + }, + observer: { + geo: {}, + }, + timestamp: 1570538236414, + }, + ]; + }); + + it('renders checks', () => { + const component = shallowWithIntl(<MonitorStatusList checks={checks} />); + expect(component).toMatchSnapshot(); + }); + + it('renders null in place of child status with missing ip', () => { + const component = shallowWithIntl(<MonitorStatusList checks={checks} />); + expect(component).toMatchSnapshot(); + }); +}); diff --git a/x-pack/legacy/plugins/uptime/public/components/functional/monitor_list/monitor_list_drawer/__tests__/monitor_status_row.test.tsx b/x-pack/legacy/plugins/uptime/public/components/overview/monitor_list/monitor_list_drawer/__tests__/monitor_status_row.test.tsx similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/functional/monitor_list/monitor_list_drawer/__tests__/monitor_status_row.test.tsx rename to x-pack/legacy/plugins/uptime/public/components/overview/monitor_list/monitor_list_drawer/__tests__/monitor_status_row.test.tsx diff --git a/x-pack/legacy/plugins/uptime/public/components/functional/monitor_list/monitor_list_drawer/__tests__/most_recent_error.test.tsx b/x-pack/legacy/plugins/uptime/public/components/overview/monitor_list/monitor_list_drawer/__tests__/most_recent_error.test.tsx similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/functional/monitor_list/monitor_list_drawer/__tests__/most_recent_error.test.tsx rename to x-pack/legacy/plugins/uptime/public/components/overview/monitor_list/monitor_list_drawer/__tests__/most_recent_error.test.tsx diff --git a/x-pack/legacy/plugins/uptime/public/components/overview/monitor_list/monitor_list_drawer/actions_popover/actions_popover.tsx b/x-pack/legacy/plugins/uptime/public/components/overview/monitor_list/monitor_list_drawer/actions_popover/actions_popover.tsx new file mode 100644 index 0000000000000..e86e6b309214f --- /dev/null +++ b/x-pack/legacy/plugins/uptime/public/components/overview/monitor_list/monitor_list_drawer/actions_popover/actions_popover.tsx @@ -0,0 +1,58 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { get } from 'lodash'; +import { i18n } from '@kbn/i18n'; +import React from 'react'; +import { EuiPopover, EuiButton } from '@elastic/eui'; +import { IntegrationGroup } from './integration_group'; +import { MonitorSummary } from '../../../../../../common/runtime_types'; +import { toggleIntegrationsPopover, PopoverState } from '../../../../../state/actions'; + +interface ActionsPopoverProps { + summary: MonitorSummary; + popoverState: PopoverState | null; + togglePopoverIsVisible: typeof toggleIntegrationsPopover; +} + +export const ActionsPopoverComponent = ({ + summary, + popoverState, + togglePopoverIsVisible, +}: ActionsPopoverProps) => { + const popoverId = `${summary.monitor_id}_popover`; + + const monitorUrl: string | undefined = get(summary, 'state.url.full', undefined); + const isPopoverOpen: boolean = + !!popoverState && popoverState.open && popoverState.id === popoverId; + return ( + <EuiPopover + button={ + <EuiButton + aria-label={i18n.translate( + 'xpack.uptime.monitorList.observabilityIntegrationsColumn.popoverIconButton.ariaLabel', + { + defaultMessage: 'Opens integrations popover for monitor with url {monitorUrl}', + description: + 'A message explaining that this button opens a popover with links to other apps for a given monitor', + values: { monitorUrl }, + } + )} + onClick={() => togglePopoverIsVisible({ id: popoverId, open: true })} + iconType="arrowDown" + iconSide="right" + > + Integrations + </EuiButton> + } + closePopover={() => togglePopoverIsVisible({ id: popoverId, open: false })} + id={popoverId} + isOpen={isPopoverOpen} + > + <IntegrationGroup summary={summary} /> + </EuiPopover> + ); +}; diff --git a/x-pack/legacy/plugins/uptime/public/components/overview/monitor_list/monitor_list_drawer/actions_popover/actions_popover_container.tsx b/x-pack/legacy/plugins/uptime/public/components/overview/monitor_list/monitor_list_drawer/actions_popover/actions_popover_container.tsx new file mode 100644 index 0000000000000..b1c25ddd7a338 --- /dev/null +++ b/x-pack/legacy/plugins/uptime/public/components/overview/monitor_list/monitor_list_drawer/actions_popover/actions_popover_container.tsx @@ -0,0 +1,23 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { connect } from 'react-redux'; +import { AppState } from '../../../../../state'; +import { isIntegrationsPopupOpen } from '../../../../../state/selectors'; +import { PopoverState, toggleIntegrationsPopover } from '../../../../../state/actions'; +import { ActionsPopoverComponent } from '../index'; + +const mapStateToProps = (state: AppState) => ({ + popoverState: isIntegrationsPopupOpen(state), +}); + +const mapDispatchToProps = (dispatch: any) => ({ + togglePopoverIsVisible: (popoverState: PopoverState) => { + return dispatch(toggleIntegrationsPopover(popoverState)); + }, +}); + +export const ActionsPopover = connect(mapStateToProps, mapDispatchToProps)(ActionsPopoverComponent); diff --git a/x-pack/legacy/plugins/uptime/public/components/functional/monitor_list/monitor_list_drawer/integration_group.tsx b/x-pack/legacy/plugins/uptime/public/components/overview/monitor_list/monitor_list_drawer/actions_popover/integration_group.tsx similarity index 98% rename from x-pack/legacy/plugins/uptime/public/components/functional/monitor_list/monitor_list_drawer/integration_group.tsx rename to x-pack/legacy/plugins/uptime/public/components/overview/monitor_list/monitor_list_drawer/actions_popover/integration_group.tsx index 34bff58a3e2d9..bbcba7238748d 100644 --- a/x-pack/legacy/plugins/uptime/public/components/functional/monitor_list/monitor_list_drawer/integration_group.tsx +++ b/x-pack/legacy/plugins/uptime/public/components/overview/monitor_list/monitor_list_drawer/actions_popover/integration_group.tsx @@ -18,9 +18,9 @@ import { getLoggingContainerHref, getLoggingIpHref, getLoggingKubernetesHref, -} from '../../../../lib/helper'; -import { MonitorSummary } from '../../../../../common/graphql/types'; -import { UptimeSettingsContext } from '../../../../contexts'; +} from '../../../../../lib/helper'; +import { MonitorSummary } from '../../../../../../common/runtime_types'; +import { UptimeSettingsContext } from '../../../../../contexts'; interface IntegrationGroupProps { summary: MonitorSummary; diff --git a/x-pack/legacy/plugins/uptime/public/components/functional/monitor_list/monitor_list_drawer/integration_link.tsx b/x-pack/legacy/plugins/uptime/public/components/overview/monitor_list/monitor_list_drawer/actions_popover/integration_link.tsx similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/functional/monitor_list/monitor_list_drawer/integration_link.tsx rename to x-pack/legacy/plugins/uptime/public/components/overview/monitor_list/monitor_list_drawer/actions_popover/integration_link.tsx diff --git a/x-pack/legacy/plugins/uptime/public/components/overview/monitor_list/monitor_list_drawer/index.ts b/x-pack/legacy/plugins/uptime/public/components/overview/monitor_list/monitor_list_drawer/index.ts new file mode 100644 index 0000000000000..32c722b806f2b --- /dev/null +++ b/x-pack/legacy/plugins/uptime/public/components/overview/monitor_list/monitor_list_drawer/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 { LocationLink } from '../../../common/location_link'; +export { ActionsPopoverComponent } from './actions_popover/actions_popover'; diff --git a/x-pack/legacy/plugins/uptime/public/components/overview/monitor_list/monitor_list_drawer/list_drawer_container.tsx b/x-pack/legacy/plugins/uptime/public/components/overview/monitor_list/monitor_list_drawer/list_drawer_container.tsx new file mode 100644 index 0000000000000..bec32ace27f2b --- /dev/null +++ b/x-pack/legacy/plugins/uptime/public/components/overview/monitor_list/monitor_list_drawer/list_drawer_container.tsx @@ -0,0 +1,47 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { useEffect } from 'react'; +import { connect } from 'react-redux'; +import { AppState } from '../../../../state'; +import { monitorDetailsSelector } from '../../../../state/selectors'; +import { MonitorDetailsActionPayload } from '../../../../state/actions/types'; +import { getMonitorDetailsAction } from '../../../../state/actions/monitor'; +import { MonitorListDrawerComponent } from './monitor_list_drawer'; +import { useGetUrlParams } from '../../../../hooks'; +import { MonitorDetails, MonitorSummary } from '../../../../../common/runtime_types'; + +interface ContainerProps { + summary: MonitorSummary; + monitorDetails: MonitorDetails; + loadMonitorDetails: typeof getMonitorDetailsAction; +} + +const Container: React.FC<ContainerProps> = ({ summary, loadMonitorDetails, monitorDetails }) => { + const monitorId = summary?.monitor_id; + + const { dateRangeStart: dateStart, dateRangeEnd: dateEnd } = useGetUrlParams(); + + useEffect(() => { + loadMonitorDetails({ + dateStart, + dateEnd, + monitorId, + }); + }, [dateStart, dateEnd, monitorId, loadMonitorDetails]); + return <MonitorListDrawerComponent monitorDetails={monitorDetails} summary={summary} />; +}; + +const mapStateToProps = (state: AppState, { summary }: any) => ({ + monitorDetails: monitorDetailsSelector(state, summary), +}); + +const mapDispatchToProps = (dispatch: any) => ({ + loadMonitorDetails: (actionPayload: MonitorDetailsActionPayload) => + dispatch(getMonitorDetailsAction(actionPayload)), +}); + +export const MonitorListDrawer = connect(mapStateToProps, mapDispatchToProps)(Container); diff --git a/x-pack/legacy/plugins/uptime/public/components/functional/monitor_list/monitor_list_drawer/monitor_list_drawer.tsx b/x-pack/legacy/plugins/uptime/public/components/overview/monitor_list/monitor_list_drawer/monitor_list_drawer.tsx similarity index 87% rename from x-pack/legacy/plugins/uptime/public/components/functional/monitor_list/monitor_list_drawer/monitor_list_drawer.tsx rename to x-pack/legacy/plugins/uptime/public/components/overview/monitor_list/monitor_list_drawer/monitor_list_drawer.tsx index 8383596ccc346..8e97ce4d692d7 100644 --- a/x-pack/legacy/plugins/uptime/public/components/functional/monitor_list/monitor_list_drawer/monitor_list_drawer.tsx +++ b/x-pack/legacy/plugins/uptime/public/components/overview/monitor_list/monitor_list_drawer/monitor_list_drawer.tsx @@ -7,11 +7,10 @@ import React from 'react'; import styled from 'styled-components'; import { EuiLink, EuiSpacer, EuiFlexGroup, EuiFlexItem, EuiIcon, EuiText } from '@elastic/eui'; -import { MonitorSummary } from '../../../../../common/graphql/types'; import { MostRecentError } from './most_recent_error'; import { MonitorStatusList } from './monitor_status_list'; -import { MonitorDetails } from '../../../../../common/runtime_types'; -import { MonitorListActionsPopover } from '../../../connected'; +import { MonitorDetails, MonitorSummary } from '../../../../../common/runtime_types'; +import { ActionsPopover } from './actions_popover/actions_popover_container'; const ContainerDiv = styled.div` padding: 10px; @@ -49,7 +48,7 @@ export function MonitorListDrawerComponent({ summary, monitorDetails }: MonitorL </EuiText> </EuiFlexItem> <EuiFlexItem grow={false}> - <MonitorListActionsPopover summary={summary} /> + <ActionsPopover summary={summary} /> </EuiFlexItem> </EuiFlexGroup> <EuiSpacer size="m" /> diff --git a/x-pack/legacy/plugins/uptime/public/components/functional/monitor_list/monitor_list_drawer/monitor_status_list.tsx b/x-pack/legacy/plugins/uptime/public/components/overview/monitor_list/monitor_list_drawer/monitor_status_list.tsx similarity index 94% rename from x-pack/legacy/plugins/uptime/public/components/functional/monitor_list/monitor_list_drawer/monitor_status_list.tsx rename to x-pack/legacy/plugins/uptime/public/components/overview/monitor_list/monitor_list_drawer/monitor_status_list.tsx index a2042e379dd80..cd1a5a95b8adb 100644 --- a/x-pack/legacy/plugins/uptime/public/components/functional/monitor_list/monitor_list_drawer/monitor_status_list.tsx +++ b/x-pack/legacy/plugins/uptime/public/components/overview/monitor_list/monitor_list_drawer/monitor_status_list.tsx @@ -8,9 +8,9 @@ import React from 'react'; import { get, capitalize } from 'lodash'; import { EuiCallOut, EuiSpacer } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; -import { Check } from '../../../../../common/graphql/types'; -import { LocationLink } from './location_link'; +import { LocationLink } from '../../../common/location_link'; import { MonitorStatusRow } from './monitor_status_row'; +import { Check } from '../../../../../common/runtime_types'; import { STATUS, UNNAMED_LOCATION } from '../../../../../common/constants'; interface MonitorStatusListProps { diff --git a/x-pack/legacy/plugins/uptime/public/components/functional/monitor_list/monitor_list_drawer/monitor_status_row.tsx b/x-pack/legacy/plugins/uptime/public/components/overview/monitor_list/monitor_list_drawer/monitor_status_row.tsx similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/functional/monitor_list/monitor_list_drawer/monitor_status_row.tsx rename to x-pack/legacy/plugins/uptime/public/components/overview/monitor_list/monitor_list_drawer/monitor_status_row.tsx diff --git a/x-pack/legacy/plugins/uptime/public/components/functional/monitor_list/monitor_list_drawer/most_recent_error.tsx b/x-pack/legacy/plugins/uptime/public/components/overview/monitor_list/monitor_list_drawer/most_recent_error.tsx similarity index 94% rename from x-pack/legacy/plugins/uptime/public/components/functional/monitor_list/monitor_list_drawer/most_recent_error.tsx rename to x-pack/legacy/plugins/uptime/public/components/overview/monitor_list/monitor_list_drawer/most_recent_error.tsx index 036882b49359f..1963a9c852b11 100644 --- a/x-pack/legacy/plugins/uptime/public/components/functional/monitor_list/monitor_list_drawer/most_recent_error.tsx +++ b/x-pack/legacy/plugins/uptime/public/components/overview/monitor_list/monitor_list_drawer/most_recent_error.tsx @@ -8,7 +8,7 @@ import { EuiText, EuiSpacer } from '@elastic/eui'; import moment from 'moment'; import { i18n } from '@kbn/i18n'; import { MonitorPageLink } from '../monitor_page_link'; -import { useUrlParams } from '../../../../hooks'; +import { useGetUrlParams } from '../../../../hooks'; import { stringifyUrlParams } from '../../../../lib/helper/stringify_url_params'; import { MonitorError } from '../../../../../common/runtime_types'; @@ -30,8 +30,7 @@ interface MostRecentErrorProps { } export const MostRecentError = ({ error, monitorId, timestamp }: MostRecentErrorProps) => { - const [getUrlParams] = useUrlParams(); - const { absoluteDateRangeStart, absoluteDateRangeEnd, ...params } = getUrlParams(); + const { absoluteDateRangeStart, absoluteDateRangeEnd, ...params } = useGetUrlParams(); params.selectedPingStatus = 'down'; const linkParameters = stringifyUrlParams(params, true); diff --git a/x-pack/legacy/plugins/uptime/public/components/functional/monitor_list/monitor_list_page_size_select.tsx b/x-pack/legacy/plugins/uptime/public/components/overview/monitor_list/monitor_list_page_size_select.tsx similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/functional/monitor_list/monitor_list_page_size_select.tsx rename to x-pack/legacy/plugins/uptime/public/components/overview/monitor_list/monitor_list_page_size_select.tsx diff --git a/x-pack/legacy/plugins/uptime/public/components/functional/monitor_list/monitor_list_status_column.tsx b/x-pack/legacy/plugins/uptime/public/components/overview/monitor_list/monitor_list_status_column.tsx similarity index 98% rename from x-pack/legacy/plugins/uptime/public/components/functional/monitor_list/monitor_list_status_column.tsx rename to x-pack/legacy/plugins/uptime/public/components/overview/monitor_list/monitor_list_status_column.tsx index 7e23be572a6f9..8076fe66cc208 100644 --- a/x-pack/legacy/plugins/uptime/public/components/functional/monitor_list/monitor_list_status_column.tsx +++ b/x-pack/legacy/plugins/uptime/public/components/overview/monitor_list/monitor_list_status_column.tsx @@ -11,7 +11,7 @@ import { capitalize } from 'lodash'; import styled from 'styled-components'; import { EuiHealth, EuiFlexGroup, EuiFlexItem, EuiText, EuiToolTip } from '@elastic/eui'; import { parseTimestamp } from './parse_timestamp'; -import { Check } from '../../../../common/graphql/types'; +import { Check } from '../../../../common/runtime_types'; import { STATUS, SHORT_TIMESPAN_LOCALE, diff --git a/x-pack/legacy/plugins/uptime/public/components/functional/monitor_list/monitor_page_link.tsx b/x-pack/legacy/plugins/uptime/public/components/overview/monitor_list/monitor_page_link.tsx similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/functional/monitor_list/monitor_page_link.tsx rename to x-pack/legacy/plugins/uptime/public/components/overview/monitor_list/monitor_page_link.tsx diff --git a/x-pack/legacy/plugins/uptime/public/components/functional/monitor_list/overview_page_link.tsx b/x-pack/legacy/plugins/uptime/public/components/overview/monitor_list/overview_page_link.tsx similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/functional/monitor_list/overview_page_link.tsx rename to x-pack/legacy/plugins/uptime/public/components/overview/monitor_list/overview_page_link.tsx diff --git a/x-pack/legacy/plugins/uptime/public/components/functional/monitor_list/parse_timestamp.ts b/x-pack/legacy/plugins/uptime/public/components/overview/monitor_list/parse_timestamp.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/functional/monitor_list/parse_timestamp.ts rename to x-pack/legacy/plugins/uptime/public/components/overview/monitor_list/parse_timestamp.ts diff --git a/x-pack/legacy/plugins/uptime/public/components/functional/monitor_list/translations.ts b/x-pack/legacy/plugins/uptime/public/components/overview/monitor_list/translations.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/functional/monitor_list/translations.ts rename to x-pack/legacy/plugins/uptime/public/components/overview/monitor_list/translations.ts diff --git a/x-pack/legacy/plugins/uptime/public/components/overview/monitor_list/types.ts b/x-pack/legacy/plugins/uptime/public/components/overview/monitor_list/types.ts new file mode 100644 index 0000000000000..6a6cee4a7d96d --- /dev/null +++ b/x-pack/legacy/plugins/uptime/public/components/overview/monitor_list/types.ts @@ -0,0 +1,35 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export interface CondensedCheckStatus { + ip?: string | null; + status: string; + timestamp: string; +} + +export interface Criteria { + page?: { + index: number; + size: number; + }; + sort?: { + field: string; + direction: 'asc' | 'desc'; + }; +} + +export interface ExpandedRowMap { + [key: string]: JSX.Element; +} + +export interface Pagination { + hidePerPageOptions?: boolean; + initialPageSize: number; + pageIndex: number; + pageSize: number; + pageSizeOptions: number[]; + totalItemCount: number; +} diff --git a/x-pack/legacy/plugins/uptime/public/components/overview/overview_container.tsx b/x-pack/legacy/plugins/uptime/public/components/overview/overview_container.tsx new file mode 100644 index 0000000000000..d64e489c48076 --- /dev/null +++ b/x-pack/legacy/plugins/uptime/public/components/overview/overview_container.tsx @@ -0,0 +1,23 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { connect } from 'react-redux'; +import { OverviewPageComponent } from '../../pages/overview'; +import { selectIndexPattern } from '../../state/selectors'; +import { AppState } from '../../state'; +import { setEsKueryString } from '../../state/actions'; + +interface DispatchProps { + setEsKueryFilters: typeof setEsKueryString; +} + +const mapDispatchToProps = (dispatch: any): DispatchProps => ({ + setEsKueryFilters: (esFilters: string) => dispatch(setEsKueryString(esFilters)), +}); + +const mapStateToProps = (state: AppState) => ({ ...selectIndexPattern(state) }); + +export const OverviewPage = connect(mapStateToProps, mapDispatchToProps)(OverviewPageComponent); diff --git a/x-pack/legacy/plugins/uptime/public/components/overview/parsing_error_callout.tsx b/x-pack/legacy/plugins/uptime/public/components/overview/parsing_error_callout.tsx new file mode 100644 index 0000000000000..96ea14cdf9f37 --- /dev/null +++ b/x-pack/legacy/plugins/uptime/public/components/overview/parsing_error_callout.tsx @@ -0,0 +1,47 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { EuiCallOut, EuiCodeBlock } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; +import React from 'react'; + +interface HasMessage { + message: string; +} + +interface ParsingErrorCalloutProps { + error: HasMessage; +} + +export const ParsingErrorCallout = ({ error }: ParsingErrorCalloutProps) => ( + <EuiCallOut + title={i18n.translate('xpack.uptime.overviewPageParsingErrorCallout.title', { + defaultMessage: 'Parsing error', + })} + color="danger" + iconType="alert" + style={{ width: '100%' }} + > + <p> + <FormattedMessage + id="xpack.uptime.overviewPageParsingErrorCallout.content" + defaultMessage="There was an error parsing the filter query. {content}" + values={{ + content: ( + <EuiCodeBlock> + {error.message + ? error.message + : i18n.translate('xpack.uptime.overviewPageParsingErrorCallout.noMessage', { + defaultMessage: 'There was no error message', + })} + </EuiCodeBlock> + ), + }} + /> + </p> + </EuiCallOut> +); diff --git a/x-pack/legacy/plugins/uptime/public/components/overview/snapshot/index.ts b/x-pack/legacy/plugins/uptime/public/components/overview/snapshot/index.ts new file mode 100644 index 0000000000000..f07bb23f16f06 --- /dev/null +++ b/x-pack/legacy/plugins/uptime/public/components/overview/snapshot/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 { SnapshotComponent } from './snapshot'; +export { Snapshot } from './snapshot_container'; diff --git a/x-pack/legacy/plugins/uptime/public/components/functional/snapshot.tsx b/x-pack/legacy/plugins/uptime/public/components/overview/snapshot/snapshot.tsx similarity index 85% rename from x-pack/legacy/plugins/uptime/public/components/functional/snapshot.tsx rename to x-pack/legacy/plugins/uptime/public/components/overview/snapshot/snapshot.tsx index 999ade9dccdd9..8d6933ad18ced 100644 --- a/x-pack/legacy/plugins/uptime/public/components/functional/snapshot.tsx +++ b/x-pack/legacy/plugins/uptime/public/components/overview/snapshot/snapshot.tsx @@ -6,10 +6,10 @@ import { EuiSpacer } from '@elastic/eui'; import React from 'react'; -import { DonutChart } from './charts'; -import { ChartWrapper } from './charts/chart_wrapper'; +import { DonutChart } from '../../common/charts'; +import { ChartWrapper } from '../../common/charts/chart_wrapper'; import { SnapshotHeading } from './snapshot_heading'; -import { Snapshot as SnapshotType } from '../../../common/runtime_types'; +import { Snapshot as SnapshotType } from '../../../../common/runtime_types'; const SNAPSHOT_CHART_WIDTH = 144; const SNAPSHOT_CHART_HEIGHT = 144; diff --git a/x-pack/legacy/plugins/uptime/public/components/overview/snapshot/snapshot_container.tsx b/x-pack/legacy/plugins/uptime/public/components/overview/snapshot/snapshot_container.tsx new file mode 100644 index 0000000000000..09d30e049175c --- /dev/null +++ b/x-pack/legacy/plugins/uptime/public/components/overview/snapshot/snapshot_container.tsx @@ -0,0 +1,34 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { useEffect } from 'react'; +import { useDispatch, useSelector } from 'react-redux'; +import { useGetUrlParams } from '../../../hooks'; +import { getSnapshotCountAction } from '../../../state/actions'; +import { SnapshotComponent } from './snapshot'; +import { snapshotDataSelector } from '../../../state/selectors'; + +interface Props { + /** + * Height is needed, since by default charts takes height of 100% + */ + height?: string; +} + +export const Snapshot: React.FC<Props> = ({ height }: Props) => { + const { dateRangeStart, dateRangeEnd, statusFilter } = useGetUrlParams(); + + const { count, lastRefresh, loading, esKuery } = useSelector(snapshotDataSelector); + + const dispatch = useDispatch(); + + useEffect(() => { + dispatch( + getSnapshotCountAction({ dateRangeStart, dateRangeEnd, filters: esKuery, statusFilter }) + ); + }, [dateRangeStart, dateRangeEnd, esKuery, lastRefresh, statusFilter, dispatch]); + return <SnapshotComponent count={count} height={height} loading={loading} />; +}; diff --git a/x-pack/legacy/plugins/uptime/public/components/functional/snapshot_heading.tsx b/x-pack/legacy/plugins/uptime/public/components/overview/snapshot/snapshot_heading.tsx similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/functional/snapshot_heading.tsx rename to x-pack/legacy/plugins/uptime/public/components/overview/snapshot/snapshot_heading.tsx diff --git a/x-pack/legacy/plugins/uptime/public/components/functional/status_panel.tsx b/x-pack/legacy/plugins/uptime/public/components/overview/status_panel.tsx similarity index 87% rename from x-pack/legacy/plugins/uptime/public/components/functional/status_panel.tsx rename to x-pack/legacy/plugins/uptime/public/components/overview/status_panel.tsx index 2c0be2aa15d6f..9edcb08a6d5b1 100644 --- a/x-pack/legacy/plugins/uptime/public/components/functional/status_panel.tsx +++ b/x-pack/legacy/plugins/uptime/public/components/overview/status_panel.tsx @@ -6,7 +6,8 @@ import React from 'react'; import { EuiFlexGroup, EuiFlexItem, EuiPanel } from '@elastic/eui'; -import { PingHistogram, Snapshot } from '../connected'; +import { PingHistogram } from '../monitor'; +import { Snapshot } from './snapshot/snapshot_container'; const STATUS_CHART_HEIGHT = '160px'; diff --git a/x-pack/legacy/plugins/uptime/public/components/settings/__tests__/__snapshots__/certificate_form.test.tsx.snap b/x-pack/legacy/plugins/uptime/public/components/settings/__tests__/__snapshots__/certificate_form.test.tsx.snap new file mode 100644 index 0000000000000..36bc9bb860211 --- /dev/null +++ b/x-pack/legacy/plugins/uptime/public/components/settings/__tests__/__snapshots__/certificate_form.test.tsx.snap @@ -0,0 +1,69 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`CertificateForm shallow renders expected elements for valid props 1`] = ` +<ContextProvider + value={ + Object { + "history": Object { + "action": "POP", + "block": [Function], + "canGo": [Function], + "createHref": [Function], + "entries": Array [ + Object { + "hash": "", + "key": "TestKeyForTesting", + "pathname": "/", + "search": "", + "state": undefined, + }, + ], + "go": [Function], + "goBack": [Function], + "goForward": [Function], + "index": 0, + "length": 1, + "listen": [Function], + "location": Object { + "hash": "", + "key": "TestKeyForTesting", + "pathname": "/", + "search": "", + "state": undefined, + }, + "push": [Function], + "replace": [Function], + }, + "location": Object { + "hash": "", + "key": "TestKeyForTesting", + "pathname": "/", + "search": "", + "state": undefined, + }, + "match": Object { + "isExact": true, + "params": Object {}, + "path": "/", + "url": "/", + }, + "staticContext": undefined, + } + } +> + <CertificateExpirationForm + fieldErrors={Object {}} + formFields={ + Object { + "certificatesThresholds": Object { + "errorState": 7, + "warningState": 36, + }, + "heartbeatIndices": "heartbeat-8*", + } + } + isDisabled={false} + onChange={[MockFunction]} + /> +</ContextProvider> +`; diff --git a/x-pack/legacy/plugins/uptime/public/components/settings/__tests__/__snapshots__/indices_form.test.tsx.snap b/x-pack/legacy/plugins/uptime/public/components/settings/__tests__/__snapshots__/indices_form.test.tsx.snap new file mode 100644 index 0000000000000..93151198c0f49 --- /dev/null +++ b/x-pack/legacy/plugins/uptime/public/components/settings/__tests__/__snapshots__/indices_form.test.tsx.snap @@ -0,0 +1,69 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`CertificateForm shallow renders expected elements for valid props 1`] = ` +<ContextProvider + value={ + Object { + "history": Object { + "action": "POP", + "block": [Function], + "canGo": [Function], + "createHref": [Function], + "entries": Array [ + Object { + "hash": "", + "key": "TestKeyForTesting", + "pathname": "/", + "search": "", + "state": undefined, + }, + ], + "go": [Function], + "goBack": [Function], + "goForward": [Function], + "index": 0, + "length": 1, + "listen": [Function], + "location": Object { + "hash": "", + "key": "TestKeyForTesting", + "pathname": "/", + "search": "", + "state": undefined, + }, + "push": [Function], + "replace": [Function], + }, + "location": Object { + "hash": "", + "key": "TestKeyForTesting", + "pathname": "/", + "search": "", + "state": undefined, + }, + "match": Object { + "isExact": true, + "params": Object {}, + "path": "/", + "url": "/", + }, + "staticContext": undefined, + } + } +> + <IndicesForm + fieldErrors={Object {}} + formFields={ + Object { + "certificatesThresholds": Object { + "errorState": 7, + "warningState": 36, + }, + "heartbeatIndices": "heartbeat-8*", + } + } + isDisabled={false} + onChange={[MockFunction]} + /> +</ContextProvider> +`; diff --git a/x-pack/legacy/plugins/uptime/public/components/settings/__tests__/certificate_form.test.tsx b/x-pack/legacy/plugins/uptime/public/components/settings/__tests__/certificate_form.test.tsx new file mode 100644 index 0000000000000..a3158f3d72445 --- /dev/null +++ b/x-pack/legacy/plugins/uptime/public/components/settings/__tests__/certificate_form.test.tsx @@ -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 React from 'react'; +import { CertificateExpirationForm } from '../certificate_form'; +import { shallowWithRouter } from '../../../lib'; + +describe('CertificateForm', () => { + it('shallow renders expected elements for valid props', () => { + expect( + shallowWithRouter( + <CertificateExpirationForm + onChange={jest.fn()} + formFields={{ + heartbeatIndices: 'heartbeat-8*', + certificatesThresholds: { errorState: 7, warningState: 36 }, + }} + fieldErrors={{}} + isDisabled={false} + /> + ) + ).toMatchSnapshot(); + }); +}); diff --git a/x-pack/legacy/plugins/uptime/public/components/settings/__tests__/indices_form.test.tsx b/x-pack/legacy/plugins/uptime/public/components/settings/__tests__/indices_form.test.tsx new file mode 100644 index 0000000000000..654d51019d4e5 --- /dev/null +++ b/x-pack/legacy/plugins/uptime/public/components/settings/__tests__/indices_form.test.tsx @@ -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 React from 'react'; +import { IndicesForm } from '../indices_form'; +import { shallowWithRouter } from '../../../lib'; + +describe('CertificateForm', () => { + it('shallow renders expected elements for valid props', () => { + expect( + shallowWithRouter( + <IndicesForm + onChange={jest.fn()} + formFields={{ + heartbeatIndices: 'heartbeat-8*', + certificatesThresholds: { errorState: 7, warningState: 36 }, + }} + fieldErrors={{}} + isDisabled={false} + /> + ) + ).toMatchSnapshot(); + }); +}); diff --git a/x-pack/legacy/plugins/uptime/public/components/settings/certificate_form.tsx b/x-pack/legacy/plugins/uptime/public/components/settings/certificate_form.tsx new file mode 100644 index 0000000000000..5103caee1e1c0 --- /dev/null +++ b/x-pack/legacy/plugins/uptime/public/components/settings/certificate_form.tsx @@ -0,0 +1,160 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { useSelector } from 'react-redux'; +import { + EuiDescribedFormGroup, + EuiFormRow, + EuiCode, + EuiFieldNumber, + EuiTitle, + EuiSpacer, + EuiSelect, + EuiFlexGroup, + EuiFlexItem, +} from '@elastic/eui'; +import { defaultDynamicSettings, DynamicSettings } from '../../../common/runtime_types'; +import { selectDynamicSettings } from '../../state/selectors'; + +type NumStr = string | number; + +export type OnFieldChangeType = (field: string, value?: NumStr) => void; + +export interface SettingsFormProps { + onChange: OnFieldChangeType; + formFields: DynamicSettings | null; + fieldErrors: any; + isDisabled: boolean; +} + +export const CertificateExpirationForm: React.FC<SettingsFormProps> = ({ + onChange, + formFields, + fieldErrors, + isDisabled, +}) => { + const dss = useSelector(selectDynamicSettings); + + return ( + <> + <EuiTitle size="s"> + <h3> + <FormattedMessage + id="xpack.uptime.sourceConfiguration.certificationSectionTitle" + defaultMessage="Certificate Expiration" + /> + </h3> + </EuiTitle> + <EuiSpacer size="m" /> + <EuiDescribedFormGroup + title={ + <h4> + <FormattedMessage + id="xpack.uptime.sourceConfiguration.stateThresholds" + defaultMessage="Expiration State Thresholds" + /> + </h4> + } + description={ + <FormattedMessage + id="xpack.uptime.sourceConfiguration.stateThresholdsDescription" + defaultMessage="Set certificate expiration warning/error thresholds" + /> + } + > + <EuiFormRow + describedByIds={['errorState']} + error={fieldErrors?.certificatesThresholds?.errorState} + fullWidth + helpText={ + <FormattedMessage + id="xpack.uptime.sourceConfiguration.errorStateDefaultValue" + defaultMessage="The default value is {defaultValue}" + values={{ + defaultValue: ( + <EuiCode>{defaultDynamicSettings?.certificatesThresholds?.errorState}</EuiCode> + ), + }} + /> + } + isInvalid={!!fieldErrors?.certificatesThresholds?.errorState} + label={ + <FormattedMessage + id="xpack.uptime.sourceConfiguration.errorStateLabel" + defaultMessage="Error state" + /> + } + > + <EuiFlexGroup> + <EuiFlexItem grow={2}> + <EuiFieldNumber + data-test-subj={`error-state-threshold-input-${dss.loading ? 'loading' : 'loaded'}`} + fullWidth + disabled={isDisabled} + isLoading={dss.loading} + value={formFields?.certificatesThresholds?.errorState || ''} + onChange={({ currentTarget: { value } }: any) => + onChange( + 'certificatesThresholds.errorState', + value === '' ? undefined : Number(value) + ) + } + /> + </EuiFlexItem> + <EuiFlexItem grow={1}> + <EuiSelect options={[{ value: 'day', text: 'Days' }]} /> + </EuiFlexItem> + </EuiFlexGroup> + </EuiFormRow> + <EuiFormRow + describedByIds={['warningState']} + error={fieldErrors?.certificatesThresholds?.warningState} + fullWidth + helpText={ + <FormattedMessage + id="xpack.uptime.sourceConfiguration.warningStateDefaultValue" + defaultMessage="The default value is {defaultValue}" + values={{ + defaultValue: ( + <EuiCode>{defaultDynamicSettings?.certificatesThresholds?.warningState}</EuiCode> + ), + }} + /> + } + isInvalid={!!fieldErrors?.certificatesThresholds?.warningState} + label={ + <FormattedMessage + id="xpack.uptime.sourceConfiguration.warningStateLabel" + defaultMessage="Warning state" + /> + } + > + <EuiFlexGroup> + <EuiFlexItem grow={2}> + <EuiFieldNumber + data-test-subj={`warning-state-threshold-input-${ + dss.loading ? 'loading' : 'loaded' + }`} + fullWidth + disabled={isDisabled} + isLoading={dss.loading} + value={formFields?.certificatesThresholds?.warningState || ''} + onChange={(event: any) => + onChange('certificatesThresholds.warningState', Number(event.currentTarget.value)) + } + /> + </EuiFlexItem> + <EuiFlexItem grow={1}> + <EuiSelect options={[{ value: 'day', text: 'Days' }]} /> + </EuiFlexItem> + </EuiFlexGroup> + </EuiFormRow> + </EuiDescribedFormGroup> + </> + ); +}; diff --git a/x-pack/legacy/plugins/uptime/public/components/settings/indices_form.tsx b/x-pack/legacy/plugins/uptime/public/components/settings/indices_form.tsx new file mode 100644 index 0000000000000..c28eca2ea229e --- /dev/null +++ b/x-pack/legacy/plugins/uptime/public/components/settings/indices_form.tsx @@ -0,0 +1,90 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { useSelector } from 'react-redux'; +import { + EuiDescribedFormGroup, + EuiFormRow, + EuiCode, + EuiFieldText, + EuiTitle, + EuiSpacer, +} from '@elastic/eui'; +import { defaultDynamicSettings } from '../../../common/runtime_types'; +import { selectDynamicSettings } from '../../state/selectors'; +import { SettingsFormProps } from './certificate_form'; + +export const IndicesForm: React.FC<SettingsFormProps> = ({ + onChange, + formFields, + fieldErrors, + isDisabled, +}) => { + const dss = useSelector(selectDynamicSettings); + + return ( + <> + <EuiTitle size="s"> + <h3> + <FormattedMessage + id="xpack.uptime.sourceConfiguration.indicesSectionTitle" + defaultMessage="Indices" + /> + </h3> + </EuiTitle> + <EuiSpacer size="m" /> + <EuiDescribedFormGroup + title={ + <h4> + <FormattedMessage + id="xpack.uptime.sourceConfiguration.heartbeatIndicesTitle" + defaultMessage="Uptime indices" + /> + </h4> + } + description={ + <FormattedMessage + id="xpack.uptime.sourceConfiguration.heartbeatIndicesDescription" + defaultMessage="Index pattern for matching indices that contain Heartbeat data" + /> + } + > + <EuiFormRow + describedByIds={['heartbeatIndices']} + error={fieldErrors?.heartbeatIndices} + fullWidth + helpText={ + <FormattedMessage + id="xpack.uptime.sourceConfiguration.heartbeatIndicesDefaultValue" + defaultMessage="The default value is {defaultValue}" + values={{ + defaultValue: <EuiCode>{defaultDynamicSettings.heartbeatIndices}</EuiCode>, + }} + /> + } + isInvalid={!!fieldErrors?.heartbeatIndices} + label={ + <FormattedMessage + id="xpack.uptime.sourceConfiguration.heartbeatIndicesLabel" + defaultMessage="Heartbeat indices" + /> + } + > + <EuiFieldText + data-test-subj={`heartbeat-indices-input-${dss.loading ? 'loading' : 'loaded'}`} + fullWidth + disabled={isDisabled} + isLoading={dss.loading} + value={formFields?.heartbeatIndices || ''} + onChange={(event: any) => onChange('heartbeatIndices', event.currentTarget.value)} + /> + </EuiFormRow> + </EuiDescribedFormGroup> + </> + ); +}; diff --git a/x-pack/legacy/plugins/uptime/public/contexts/uptime_settings_context.tsx b/x-pack/legacy/plugins/uptime/public/contexts/uptime_settings_context.tsx index 44a87d310c9c7..137846de103b4 100644 --- a/x-pack/legacy/plugins/uptime/public/contexts/uptime_settings_context.tsx +++ b/x-pack/legacy/plugins/uptime/public/contexts/uptime_settings_context.tsx @@ -7,8 +7,8 @@ import React, { createContext, useMemo } from 'react'; import { UptimeAppProps } from '../uptime_app'; import { CLIENT_DEFAULTS, CONTEXT_DEFAULTS } from '../../common/constants'; -import { CommonlyUsedRange } from '../components/functional/uptime_date_picker'; -import { useUrlParams } from '../hooks'; +import { CommonlyUsedRange } from '../components/common/uptime_date_picker'; +import { useGetUrlParams } from '../hooks'; import { ILicense } from '../../../../../plugins/licensing/common/types'; export interface UptimeSettingsContextValues { @@ -50,9 +50,7 @@ export const UptimeSettingsContextProvider: React.FC<UptimeAppProps> = ({ childr plugins, } = props; - const [getUrlParams] = useUrlParams(); - - const { dateRangeStart, dateRangeEnd } = getUrlParams(); + const { dateRangeStart, dateRangeEnd } = useGetUrlParams(); let license: ILicense | null = null; diff --git a/x-pack/legacy/plugins/uptime/public/hooks/__tests__/use_breadcrumbs.test.tsx b/x-pack/legacy/plugins/uptime/public/hooks/__tests__/use_breadcrumbs.test.tsx index 85961003fce72..1ce00fe7ce3af 100644 --- a/x-pack/legacy/plugins/uptime/public/hooks/__tests__/use_breadcrumbs.test.tsx +++ b/x-pack/legacy/plugins/uptime/public/hooks/__tests__/use_breadcrumbs.test.tsx @@ -11,7 +11,7 @@ import { mountWithRouter } from '../../lib'; import { OVERVIEW_ROUTE } from '../../../common/constants'; import { KibanaContextProvider } from '../../../../../../../src/plugins/kibana_react/public'; import { UptimeUrlParams, getSupportedUrlParams } from '../../lib/helper'; -import { makeBaseBreadcrumb, useBreadcrumbs } from '../../hooks/use_breadcrumbs'; +import { makeBaseBreadcrumb, useBreadcrumbs } from '../use_breadcrumbs'; describe('useBreadcrumbs', () => { it('sets the given breadcrumbs', () => { diff --git a/x-pack/legacy/plugins/uptime/public/hooks/__tests__/use_url_params.test.tsx b/x-pack/legacy/plugins/uptime/public/hooks/__tests__/use_url_params.test.tsx index a8999a50927d2..deb1f163c1326 100644 --- a/x-pack/legacy/plugins/uptime/public/hooks/__tests__/use_url_params.test.tsx +++ b/x-pack/legacy/plugins/uptime/public/hooks/__tests__/use_url_params.test.tsx @@ -19,6 +19,7 @@ interface MockUrlParamsComponentProps { const UseUrlParamsTestComponent = ({ hook, updateParams }: MockUrlParamsComponentProps) => { const [params, setParams] = useState({}); const [getUrlParams, updateUrlParams] = hook(); + const queryParams = getUrlParams(); return ( <Fragment> {Object.keys(params).length > 0 ? <div>{JSON.stringify(params)}</div> : null} @@ -30,7 +31,7 @@ const UseUrlParamsTestComponent = ({ hook, updateParams }: MockUrlParamsComponen > Set url params </button> - <button id="getUrlParams" onClick={() => setParams(getUrlParams())}> + <button id="getUrlParams" onClick={() => setParams(queryParams)}> Get url params </button> </Fragment> diff --git a/x-pack/legacy/plugins/uptime/public/hooks/index.ts b/x-pack/legacy/plugins/uptime/public/hooks/index.ts index e022248df407a..1f50e995eda49 100644 --- a/x-pack/legacy/plugins/uptime/public/hooks/index.ts +++ b/x-pack/legacy/plugins/uptime/public/hooks/index.ts @@ -4,6 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ +export * from './use_monitor'; export * from './use_url_params'; export * from './use_telemetry'; export * from './update_kuery_string'; diff --git a/x-pack/legacy/plugins/uptime/public/hooks/use_monitor.ts b/x-pack/legacy/plugins/uptime/public/hooks/use_monitor.ts new file mode 100644 index 0000000000000..8080ce2696a3c --- /dev/null +++ b/x-pack/legacy/plugins/uptime/public/hooks/use_monitor.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 { useParams } from 'react-router-dom'; + +export const useMonitorId = (): string => { + const { monitorId } = useParams(); + + // decode 64 base string, it was decoded to make it a valid url, since monitor id can be a url + return atob(monitorId || ''); +}; diff --git a/x-pack/legacy/plugins/uptime/public/hooks/use_telemetry.ts b/x-pack/legacy/plugins/uptime/public/hooks/use_telemetry.ts index 13fe523332ae5..a2012b8ac5636 100644 --- a/x-pack/legacy/plugins/uptime/public/hooks/use_telemetry.ts +++ b/x-pack/legacy/plugins/uptime/public/hooks/use_telemetry.ts @@ -5,7 +5,7 @@ */ import { useEffect } from 'react'; -import { useUrlParams } from './use_url_params'; +import { useGetUrlParams } from './use_url_params'; import { apiService } from '../state/api/utils'; import { API_URLS } from '../../common/constants'; @@ -17,8 +17,12 @@ export enum UptimePage { } export const useUptimeTelemetry = (page?: UptimePage) => { - const [getUrlParams] = useUrlParams(); - const { dateRangeStart, dateRangeEnd, autorefreshInterval, autorefreshIsPaused } = getUrlParams(); + const { + dateRangeStart, + dateRangeEnd, + autorefreshInterval, + autorefreshIsPaused, + } = useGetUrlParams(); useEffect(() => { if (!apiService.http) throw new Error('Core http services are not defined'); diff --git a/x-pack/legacy/plugins/uptime/public/hooks/use_url_params.ts b/x-pack/legacy/plugins/uptime/public/hooks/use_url_params.ts index 20063b2c1bc93..8b13e9e480559 100644 --- a/x-pack/legacy/plugins/uptime/public/hooks/use_url_params.ts +++ b/x-pack/legacy/plugins/uptime/public/hooks/use_url_params.ts @@ -15,27 +15,26 @@ export type UpdateUrlParams = (updatedParams: { export type UptimeUrlParamsHook = () => [GetUrlParams, UpdateUrlParams]; -export const useUrlParams: UptimeUrlParamsHook = () => { +const getParsedParams = (search: string) => { + return search ? parse(search[0] === '?' ? search.slice(1) : search, { sort: false }) : {}; +}; + +export const useGetUrlParams: GetUrlParams = () => { const location = useLocation(); - const history = useHistory(); - const getUrlParams: GetUrlParams = () => { - let search: string | undefined; - if (location) { - search = location.search; - } + const params = getParsedParams(location?.search); - const params = search - ? parse(search[0] === '?' ? search.slice(1) : search, { sort: false }) - : {}; + return getSupportedUrlParams(params); +}; - return getSupportedUrlParams(params); - }; +export const useUrlParams: UptimeUrlParamsHook = () => { + const location = useLocation(); + const history = useHistory(); const updateUrlParams: UpdateUrlParams = updatedParams => { if (!history || !location) return; const { pathname, search } = location; - const currentParams = parse(search[0] === '?' ? search.slice(1) : search, { sort: false }); + const currentParams = getParsedParams(search); const mergedParams = { ...currentParams, ...updatedParams, @@ -60,5 +59,5 @@ export const useUrlParams: UptimeUrlParamsHook = () => { }); }; - return [getUrlParams, updateUrlParams]; + return [useGetUrlParams, updateUrlParams]; }; diff --git a/x-pack/legacy/plugins/uptime/public/lib/adapters/framework/apollo_client_adapter.ts b/x-pack/legacy/plugins/uptime/public/lib/adapters/framework/apollo_client_adapter.ts deleted file mode 100644 index 2cec0d5fc8c64..0000000000000 --- a/x-pack/legacy/plugins/uptime/public/lib/adapters/framework/apollo_client_adapter.ts +++ /dev/null @@ -1,16 +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 { InMemoryCache } from 'apollo-cache-inmemory'; -import { ApolloClient } from 'apollo-client'; -import { HttpLink } from 'apollo-link-http'; -import { CreateGraphQLClient } from './framework_adapter_types'; - -export const createApolloClient: CreateGraphQLClient = (uri: string, xsrfHeader: string) => - new ApolloClient({ - link: new HttpLink({ uri, credentials: 'same-origin', headers: { 'kbn-xsrf': xsrfHeader } }), - cache: new InMemoryCache({ dataIdFromObject: () => undefined }), - }); diff --git a/x-pack/legacy/plugins/uptime/public/lib/adapters/framework/framework_adapter_types.ts b/x-pack/legacy/plugins/uptime/public/lib/adapters/framework/framework_adapter_types.ts deleted file mode 100644 index 34cf48514c932..0000000000000 --- a/x-pack/legacy/plugins/uptime/public/lib/adapters/framework/framework_adapter_types.ts +++ /dev/null @@ -1,12 +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 { NormalizedCacheObject } from 'apollo-cache-inmemory'; -import { ApolloClient } from 'apollo-client'; - -export type GraphQLClient = ApolloClient<NormalizedCacheObject>; - -export type CreateGraphQLClient = (url: string, xsrfHeader: string) => GraphQLClient; diff --git a/x-pack/legacy/plugins/uptime/public/lib/adapters/framework/new_platform_adapter.tsx b/x-pack/legacy/plugins/uptime/public/lib/adapters/framework/new_platform_adapter.tsx index a2f3328b98612..71c73bf5ba5d4 100644 --- a/x-pack/legacy/plugins/uptime/public/lib/adapters/framework/new_platform_adapter.tsx +++ b/x-pack/legacy/plugins/uptime/public/lib/adapters/framework/new_platform_adapter.tsx @@ -20,7 +20,6 @@ import { DEFAULT_TIMEPICKER_QUICK_RANGES, } from '../../../../common/constants'; import { UMFrameworkAdapter } from '../../lib'; -import { createApolloClient } from './apollo_client_adapter'; export const getKibanaFrameworkAdapter = ( core: CoreStart, @@ -60,7 +59,6 @@ export const getKibanaFrameworkAdapter = ( const props: UptimeAppProps = { basePath: basePath.get(), canSave, - client: createApolloClient(`${basePath.get()}/api/uptime/graphql`, 'true'), core, darkMode: core.uiSettings.get(DEFAULT_DARK_MODE), commonlyUsedRanges: core.uiSettings.get(DEFAULT_TIMEPICKER_QUICK_RANGES), diff --git a/x-pack/legacy/plugins/uptime/public/lib/alert_types/index.ts b/x-pack/legacy/plugins/uptime/public/lib/alert_types/index.ts index f764505a6d683..74160577cb0b1 100644 --- a/x-pack/legacy/plugins/uptime/public/lib/alert_types/index.ts +++ b/x-pack/legacy/plugins/uptime/public/lib/alert_types/index.ts @@ -6,7 +6,7 @@ // TODO: after NP migration is complete we should be able to remove this lint ignore comment // eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { AlertTypeModel } from '../../../../../../plugins/triggers_actions_ui/public/types'; +import { AlertTypeModel } from '../../../../../../plugins/triggers_actions_ui/public'; import { initMonitorStatusAlertType } from './monitor_status'; export type AlertTypeInitializer = (dependenies: { autocomplete: any }) => AlertTypeModel; diff --git a/x-pack/legacy/plugins/uptime/public/lib/alert_types/monitor_status.tsx b/x-pack/legacy/plugins/uptime/public/lib/alert_types/monitor_status.tsx index d059274159c7f..0624d20b197c0 100644 --- a/x-pack/legacy/plugins/uptime/public/lib/alert_types/monitor_status.tsx +++ b/x-pack/legacy/plugins/uptime/public/lib/alert_types/monitor_status.tsx @@ -16,7 +16,7 @@ import { } from '../../../../../../plugins/triggers_actions_ui/public/types'; import { AlertTypeInitializer } from '.'; import { StatusCheckExecutorParamsType } from '../../../common/runtime_types'; -import { AlertMonitorStatus } from '../../components/connected/alerts'; +import { AlertMonitorStatus } from '../../components/overview/alerts/alerts_containers'; export const validate = (alertParams: any): ValidationResult => { const errors: Record<string, any> = {}; diff --git a/x-pack/legacy/plugins/uptime/public/lib/helper/__tests__/__snapshots__/format_error_string.test.ts.snap b/x-pack/legacy/plugins/uptime/public/lib/helper/__tests__/__snapshots__/format_error_string.test.ts.snap deleted file mode 100644 index 8519157e4039e..0000000000000 --- a/x-pack/legacy/plugins/uptime/public/lib/helper/__tests__/__snapshots__/format_error_string.test.ts.snap +++ /dev/null @@ -1,7 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`formatErrorString returns a formatted string containing each error 1`] = ` -"Error: foo is bar -Error: bar is not foo -" -`; diff --git a/x-pack/legacy/plugins/uptime/public/lib/helper/__tests__/format_error_string.test.ts b/x-pack/legacy/plugins/uptime/public/lib/helper/__tests__/format_error_string.test.ts deleted file mode 100644 index ba437c05cbe2b..0000000000000 --- a/x-pack/legacy/plugins/uptime/public/lib/helper/__tests__/format_error_string.test.ts +++ /dev/null @@ -1,41 +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 { formatUptimeGraphQLErrorList } from '../format_error_list'; - -describe('formatErrorString', () => { - it('returns an empty string for empty array', () => { - const result = formatUptimeGraphQLErrorList([]); - expect(result).toEqual(''); - }); - it('returns a formatted string containing each error', () => { - const result = formatUptimeGraphQLErrorList([ - { - message: 'foo is bar', - locations: undefined, - path: undefined, - nodes: undefined, - source: undefined, - positions: undefined, - originalError: undefined, - extensions: undefined, - name: 'test error', - }, - { - message: 'bar is not foo', - locations: undefined, - path: undefined, - nodes: undefined, - source: undefined, - positions: undefined, - originalError: undefined, - extensions: undefined, - name: 'test error', - }, - ]); - expect(result).toMatchSnapshot(); - }); -}); diff --git a/x-pack/legacy/plugins/uptime/public/lib/helper/charts/get_chart_date_label.ts b/x-pack/legacy/plugins/uptime/public/lib/helper/charts/get_chart_date_label.ts index 126b1d85f749f..aa5a2b0f60e4f 100644 --- a/x-pack/legacy/plugins/uptime/public/lib/helper/charts/get_chart_date_label.ts +++ b/x-pack/legacy/plugins/uptime/public/lib/helper/charts/get_chart_date_label.ts @@ -11,7 +11,7 @@ import { CHART_FORMAT_LIMITS } from '../../../../common/constants'; /** * Generates an appropriate date formatting string intended for the y-axis * label of timeseries charts. The function will return day/month values for shorter - * timespans that cross the local date threshold, otherwise it estimates an appropriate + * time spans that cross the local date threshold, otherwise it estimates an appropriate * label for several different stops. * @param dateRangeStart the beginning of the date range * @param dateRangeEnd the end of the date range diff --git a/x-pack/legacy/plugins/uptime/public/lib/helper/charts/get_label_format.ts b/x-pack/legacy/plugins/uptime/public/lib/helper/charts/get_label_format.ts index 668147fee8055..5957123e9257d 100644 --- a/x-pack/legacy/plugins/uptime/public/lib/helper/charts/get_label_format.ts +++ b/x-pack/legacy/plugins/uptime/public/lib/helper/charts/get_label_format.ts @@ -49,7 +49,7 @@ const dateStops: Array<{ key: number; value: string }> = [ ]; /** - * Returns an appropriate label format bbased on pre-defined intervals. + * Returns an appropriate label format based on pre-defined intervals. * @param delta The length of the timespan in milliseconds */ export const getLabelFormat = (delta: number): string => { diff --git a/x-pack/legacy/plugins/uptime/public/lib/helper/convert_measurements.ts b/x-pack/legacy/plugins/uptime/public/lib/helper/convert_measurements.ts index 4ad9b81b9e660..da97b6400a9a5 100644 --- a/x-pack/legacy/plugins/uptime/public/lib/helper/convert_measurements.ts +++ b/x-pack/legacy/plugins/uptime/public/lib/helper/convert_measurements.ts @@ -8,7 +8,7 @@ const NUM_MICROSECONDS_IN_MILLISECOND = 1000; /** * This simply converts microseconds to milliseconds. People tend to prefer ms to us - * when visualizaing request duration times. + * when visualizing request duration times. */ export const convertMicrosecondsToMilliseconds = (microseconds: number | null): number | null => { if (!microseconds && microseconds !== 0) return null; diff --git a/x-pack/legacy/plugins/uptime/public/lib/helper/format_error_list.ts b/x-pack/legacy/plugins/uptime/public/lib/helper/format_error_list.ts deleted file mode 100644 index a23122c5eead5..0000000000000 --- a/x-pack/legacy/plugins/uptime/public/lib/helper/format_error_list.ts +++ /dev/null @@ -1,20 +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 { i18n } from '@kbn/i18n'; -import { GraphQLError } from 'graphql'; - -export const formatUptimeGraphQLErrorList = (errors: GraphQLError[]) => - errors.reduce( - (errorString, error) => - errorString.concat( - `${i18n.translate('xpack.uptime.errorMessage', { - values: { message: error.message }, - defaultMessage: 'Error: {message}', - })}\n` - ), - '' - ); diff --git a/x-pack/legacy/plugins/uptime/public/lib/helper/observability_integration/__tests__/__snapshots__/get_apm_href.test.ts.snap b/x-pack/legacy/plugins/uptime/public/lib/helper/observability_integration/__tests__/__snapshots__/get_apm_href.test.ts.snap deleted file mode 100644 index 53d336b52bd24..0000000000000 --- a/x-pack/legacy/plugins/uptime/public/lib/helper/observability_integration/__tests__/__snapshots__/get_apm_href.test.ts.snap +++ /dev/null @@ -1,5 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`getApmHref creates href with base path when present 1`] = `"foo/app/apm#/services?kuery=url.domain:%20%22www.elastic.co%22&rangeFrom=now-15m&rangeTo=now"`; - -exports[`getApmHref does not add a base path or extra slash when base path is empty string 1`] = `"/app/apm#/services?kuery=url.domain:%20%22www.elastic.co%22&rangeFrom=now-15m&rangeTo=now"`; diff --git a/x-pack/legacy/plugins/uptime/public/lib/helper/observability_integration/__tests__/__snapshots__/get_infra_href.test.ts.snap b/x-pack/legacy/plugins/uptime/public/lib/helper/observability_integration/__tests__/__snapshots__/get_infra_href.test.ts.snap deleted file mode 100644 index e79eb50d384a8..0000000000000 --- a/x-pack/legacy/plugins/uptime/public/lib/helper/observability_integration/__tests__/__snapshots__/get_infra_href.test.ts.snap +++ /dev/null @@ -1,19 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`getInfraHref getInfraContainerHref creates a link for valid parameters 1`] = `"foo/app/metrics/link-to/container-detail/test-container-id"`; - -exports[`getInfraHref getInfraContainerHref does not specify a base path when none is available 1`] = `"/app/metrics/link-to/container-detail/test-container-id"`; - -exports[`getInfraHref getInfraContainerHref returns the first item when multiple container ids are supplied 1`] = `"bar/app/metrics/link-to/container-detail/test-container-id"`; - -exports[`getInfraHref getInfraIpHref creates a link for valid parameters 1`] = `"bar/app/metrics/inventory?waffleFilter=(expression:'host.ip%20%3A%20151.101.202.217',kind:kuery)"`; - -exports[`getInfraHref getInfraIpHref does not specify a base path when none is available 1`] = `"/app/metrics/inventory?waffleFilter=(expression:'host.ip%20%3A%20151.101.202.217',kind:kuery)"`; - -exports[`getInfraHref getInfraIpHref returns a url for ors between multiple ips 1`] = `"foo/app/metrics/inventory?waffleFilter=(expression:'host.ip%20%3A%20152.151.23.192%20or%20host.ip%20%3A%20151.101.202.217',kind:kuery)"`; - -exports[`getInfraHref getInfraKubernetesHref creates a link for valid parameters 1`] = `"foo/app/metrics/link-to/pod-detail/test-pod-uid"`; - -exports[`getInfraHref getInfraKubernetesHref does not specify a base path when none is available 1`] = `"/app/metrics/link-to/pod-detail/test-pod-uid"`; - -exports[`getInfraHref getInfraKubernetesHref selects the first pod uid when there are multiple 1`] = `"/app/metrics/link-to/pod-detail/test-pod-uid"`; diff --git a/x-pack/legacy/plugins/uptime/public/lib/helper/observability_integration/__tests__/__snapshots__/get_logging_href.test.ts.snap b/x-pack/legacy/plugins/uptime/public/lib/helper/observability_integration/__tests__/__snapshots__/get_logging_href.test.ts.snap deleted file mode 100644 index cfac6ce133c8a..0000000000000 --- a/x-pack/legacy/plugins/uptime/public/lib/helper/observability_integration/__tests__/__snapshots__/get_logging_href.test.ts.snap +++ /dev/null @@ -1,13 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`getLoggingHref creates a container href with base path when present 1`] = `"bar/app/logs?logFilter=(expression:'container.id%20:%20test-container-id',kind:kuery)"`; - -exports[`getLoggingHref creates a container href without a base path if it's an empty string 1`] = `"/app/logs?logFilter=(expression:'container.id%20:%20test-container-id',kind:kuery)"`; - -exports[`getLoggingHref creates a pod href with base path when present 1`] = `"bar/app/logs?logFilter=(expression:'pod.uid%20:%20test-pod-id',kind:kuery)"`; - -exports[`getLoggingHref creates a pod href without a base path when it's an empty string 1`] = `"/app/logs?logFilter=(expression:'pod.uid%20:%20test-pod-id',kind:kuery)"`; - -exports[`getLoggingHref creates an ip href with base path when present 1`] = `"bar/app/logs?logFilter=(expression:'pod.uid%20:%20test-pod-id',kind:kuery)"`; - -exports[`getLoggingHref creates an ip href without a base path when it's an empty string 1`] = `"/app/logs?logFilter=(expression:'host.ip%20%3A%20151.101.202.217',kind:kuery)"`; diff --git a/x-pack/legacy/plugins/uptime/public/lib/helper/observability_integration/__tests__/get_apm_href.test.ts b/x-pack/legacy/plugins/uptime/public/lib/helper/observability_integration/__tests__/get_apm_href.test.ts index db49e95896ac1..f27ed78d593ac 100644 --- a/x-pack/legacy/plugins/uptime/public/lib/helper/observability_integration/__tests__/get_apm_href.test.ts +++ b/x-pack/legacy/plugins/uptime/public/lib/helper/observability_integration/__tests__/get_apm_href.test.ts @@ -5,7 +5,7 @@ */ import { getApmHref } from '../get_apm_href'; -import { MonitorSummary } from '../../../../../common/graphql/types'; +import { MonitorSummary } from '../../../../../common/runtime_types'; describe('getApmHref', () => { let summary: MonitorSummary; @@ -29,7 +29,7 @@ describe('getApmHref', () => { uid: 'test-pod-id', }, }, - timestamp: '123', + timestamp: 123, }, ], timestamp: '123', @@ -43,11 +43,15 @@ describe('getApmHref', () => { it('creates href with base path when present', () => { const result = getApmHref(summary, 'foo', 'now-15m', 'now'); - expect(result).toMatchSnapshot(); + expect(result).toMatchInlineSnapshot( + `"foo/app/apm#/services?kuery=url.domain:%20%22www.elastic.co%22&rangeFrom=now-15m&rangeTo=now"` + ); }); it('does not add a base path or extra slash when base path is empty string', () => { const result = getApmHref(summary, '', 'now-15m', 'now'); - expect(result).toMatchSnapshot(); + expect(result).toMatchInlineSnapshot( + `"/app/apm#/services?kuery=url.domain:%20%22www.elastic.co%22&rangeFrom=now-15m&rangeTo=now"` + ); }); }); diff --git a/x-pack/legacy/plugins/uptime/public/lib/helper/observability_integration/__tests__/get_infra_href.test.ts b/x-pack/legacy/plugins/uptime/public/lib/helper/observability_integration/__tests__/get_infra_href.test.ts index c2360c321da8f..ee5db74af22c2 100644 --- a/x-pack/legacy/plugins/uptime/public/lib/helper/observability_integration/__tests__/get_infra_href.test.ts +++ b/x-pack/legacy/plugins/uptime/public/lib/helper/observability_integration/__tests__/get_infra_href.test.ts @@ -5,7 +5,7 @@ */ import { getInfraContainerHref, getInfraKubernetesHref, getInfraIpHref } from '../get_infra_href'; -import { MonitorSummary } from '../../../../../common/graphql/types'; +import { MonitorSummary } from '../../../../../common/runtime_types'; describe('getInfraHref', () => { let summary: MonitorSummary; @@ -13,7 +13,6 @@ describe('getInfraHref', () => { summary = { monitor_id: 'foo', state: { - summary: {}, checks: [ { monitor: { @@ -28,9 +27,11 @@ describe('getInfraHref', () => { uid: 'test-pod-uid', }, }, - timestamp: '123', + timestamp: 123, }, ], + summary: {}, + url: {}, timestamp: '123', }, }; @@ -38,11 +39,15 @@ describe('getInfraHref', () => { it('getInfraContainerHref creates a link for valid parameters', () => { const result = getInfraContainerHref(summary, 'foo'); - expect(result).toMatchSnapshot(); + expect(result).toMatchInlineSnapshot( + `"foo/app/metrics/link-to/container-detail/test-container-id"` + ); }); it('getInfraContainerHref does not specify a base path when none is available', () => { - expect(getInfraContainerHref(summary, '')).toMatchSnapshot(); + expect(getInfraContainerHref(summary, '')).toMatchInlineSnapshot( + `"/app/metrics/link-to/container-detail/test-container-id"` + ); }); it('getInfraContainerHref returns undefined when no container id is present', () => { @@ -65,7 +70,7 @@ describe('getInfraHref', () => { uid: 'test-pod-uid', }, }, - timestamp: '123', + timestamp: 123, }, { monitor: { @@ -80,10 +85,12 @@ describe('getInfraHref', () => { uid: 'test-pod-uid-bar', }, }, - timestamp: '123', + timestamp: 123, }, ]; - expect(getInfraContainerHref(summary, 'bar')).toMatchSnapshot(); + expect(getInfraContainerHref(summary, 'bar')).toMatchInlineSnapshot( + `"bar/app/metrics/link-to/container-detail/test-container-id"` + ); }); it('getInfraContainerHref returns undefined when checks are undefined', () => { @@ -94,11 +101,13 @@ describe('getInfraHref', () => { it('getInfraKubernetesHref creates a link for valid parameters', () => { const result = getInfraKubernetesHref(summary, 'foo'); expect(result).not.toBeUndefined(); - expect(result).toMatchSnapshot(); + expect(result).toMatchInlineSnapshot(`"foo/app/metrics/link-to/pod-detail/test-pod-uid"`); }); it('getInfraKubernetesHref does not specify a base path when none is available', () => { - expect(getInfraKubernetesHref(summary, '')).toMatchSnapshot(); + expect(getInfraKubernetesHref(summary, '')).toMatchInlineSnapshot( + `"/app/metrics/link-to/pod-detail/test-pod-uid"` + ); }); it('getInfraKubernetesHref returns undefined when no pod data is present', () => { @@ -121,7 +130,7 @@ describe('getInfraHref', () => { uid: 'test-pod-uid', }, }, - timestamp: '123', + timestamp: 123, }, { monitor: { @@ -136,10 +145,12 @@ describe('getInfraHref', () => { uid: 'test-pod-uid-bar', }, }, - timestamp: '123', + timestamp: 123, }, ]; - expect(getInfraKubernetesHref(summary, '')).toMatchSnapshot(); + expect(getInfraKubernetesHref(summary, '')).toMatchInlineSnapshot( + `"/app/metrics/link-to/pod-detail/test-pod-uid"` + ); }); it('getInfraKubernetesHref returns undefined when checks are undefined', () => { @@ -148,17 +159,21 @@ describe('getInfraHref', () => { }); it('getInfraKubernetesHref returns undefined when checks are null', () => { - summary.state.checks![0]!.kubernetes!.pod!.uid = null; + delete summary.state.checks![0]!.kubernetes!.pod!.uid; expect(getInfraKubernetesHref(summary, '')).toBeUndefined(); }); it('getInfraIpHref creates a link for valid parameters', () => { const result = getInfraIpHref(summary, 'bar'); - expect(result).toMatchSnapshot(); + expect(result).toMatchInlineSnapshot( + `"bar/app/metrics/inventory?waffleFilter=(expression:'host.ip%20%3A%20151.101.202.217',kind:kuery)"` + ); }); it('getInfraIpHref does not specify a base path when none is available', () => { - expect(getInfraIpHref(summary, '')).toMatchSnapshot(); + expect(getInfraIpHref(summary, '')).toMatchInlineSnapshot( + `"/app/metrics/inventory?waffleFilter=(expression:'host.ip%20%3A%20151.101.202.217',kind:kuery)"` + ); }); it('getInfraIpHref returns undefined when ip is undefined', () => { @@ -167,14 +182,14 @@ describe('getInfraHref', () => { }); it('getInfraIpHref returns undefined when ip is null', () => { - summary.state.checks![0].monitor.ip = null; + delete summary.state.checks![0].monitor.ip; expect(getInfraIpHref(summary, 'foo')).toBeUndefined(); }); it('getInfraIpHref returns a url for ors between multiple ips', () => { summary.state.checks = [ { - timestamp: '123', + timestamp: 123, monitor: { ip: '152.151.23.192', status: 'up', @@ -193,10 +208,12 @@ describe('getInfraHref', () => { uid: 'test-pod-uid', }, }, - timestamp: '123', + timestamp: 123, }, ]; - expect(getInfraIpHref(summary, 'foo')).toMatchSnapshot(); + expect(getInfraIpHref(summary, 'foo')).toMatchInlineSnapshot( + `"foo/app/metrics/inventory?waffleFilter=(expression:'host.ip%20%3A%20152.151.23.192%20or%20host.ip%20%3A%20151.101.202.217',kind:kuery)"` + ); }); it('getInfraIpHref returns undefined if checks are undefined', () => { diff --git a/x-pack/legacy/plugins/uptime/public/lib/helper/observability_integration/__tests__/get_logging_href.test.ts b/x-pack/legacy/plugins/uptime/public/lib/helper/observability_integration/__tests__/get_logging_href.test.ts index 1117fa1429962..b188a8d1b8ef6 100644 --- a/x-pack/legacy/plugins/uptime/public/lib/helper/observability_integration/__tests__/get_logging_href.test.ts +++ b/x-pack/legacy/plugins/uptime/public/lib/helper/observability_integration/__tests__/get_logging_href.test.ts @@ -9,7 +9,7 @@ import { getLoggingKubernetesHref, getLoggingIpHref, } from '../get_logging_href'; -import { MonitorSummary } from '../../../../../common/graphql/types'; +import { MonitorSummary } from '../../../../../common/runtime_types'; describe('getLoggingHref', () => { let summary: MonitorSummary; @@ -33,10 +33,11 @@ describe('getLoggingHref', () => { uid: 'test-pod-id', }, }, - timestamp: '123', + timestamp: 123, }, ], timestamp: '123', + url: {}, }, }; }); @@ -44,37 +45,49 @@ describe('getLoggingHref', () => { it('creates a container href with base path when present', () => { const result = getLoggingContainerHref(summary, 'bar'); expect(result).not.toBeUndefined(); - expect(result).toMatchSnapshot(); + expect(result).toMatchInlineSnapshot( + `"bar/app/logs?logFilter=(expression:'container.id%20:%20test-container-id',kind:kuery)"` + ); }); it(`creates a container href without a base path if it's an empty string`, () => { const result = getLoggingContainerHref(summary, ''); expect(result).not.toBeUndefined(); - expect(result).toMatchSnapshot(); + expect(result).toMatchInlineSnapshot( + `"/app/logs?logFilter=(expression:'container.id%20:%20test-container-id',kind:kuery)"` + ); }); it(`creates an ip href with base path when present`, () => { const result = getLoggingKubernetesHref(summary, 'bar'); expect(result).not.toBeUndefined(); - expect(result).toMatchSnapshot(); + expect(result).toMatchInlineSnapshot( + `"bar/app/logs?logFilter=(expression:'pod.uid%20:%20test-pod-id',kind:kuery)"` + ); }); it('creates a pod href with base path when present', () => { const result = getLoggingKubernetesHref(summary, 'bar'); expect(result).not.toBeUndefined(); - expect(result).toMatchSnapshot(); + expect(result).toMatchInlineSnapshot( + `"bar/app/logs?logFilter=(expression:'pod.uid%20:%20test-pod-id',kind:kuery)"` + ); }); it(`creates a pod href without a base path when it's an empty string`, () => { const result = getLoggingKubernetesHref(summary, ''); expect(result).not.toBeUndefined(); - expect(result).toMatchSnapshot(); + expect(result).toMatchInlineSnapshot( + `"/app/logs?logFilter=(expression:'pod.uid%20:%20test-pod-id',kind:kuery)"` + ); }); it(`creates an ip href without a base path when it's an empty string`, () => { const result = getLoggingIpHref(summary, ''); expect(result).not.toBeUndefined(); - expect(result).toMatchSnapshot(); + expect(result).toMatchInlineSnapshot( + `"/app/logs?logFilter=(expression:'host.ip%20%3A%20151.101.202.217',kind:kuery)"` + ); }); it('returns undefined if necessary container is not present', () => { @@ -83,7 +96,7 @@ describe('getLoggingHref', () => { }); it('returns undefined if necessary container is null', () => { - summary.state.checks![0].container!.id = null; + delete summary.state.checks![0].container!.id; expect(getLoggingContainerHref(summary, '')).toBeUndefined(); }); @@ -93,7 +106,7 @@ describe('getLoggingHref', () => { }); it('returns undefined if necessary pod is null', () => { - summary.state.checks![0].kubernetes!.pod!.uid = null; + delete summary.state.checks![0].kubernetes!.pod!.uid; expect(getLoggingKubernetesHref(summary, '')).toBeUndefined(); }); @@ -103,7 +116,7 @@ describe('getLoggingHref', () => { }); it('returns undefined ip href if ip is null', () => { - summary.state.checks![0].monitor.ip = null; + delete summary.state.checks![0].monitor.ip; expect(getLoggingIpHref(summary, '')).toBeUndefined(); }); }); diff --git a/x-pack/legacy/plugins/uptime/public/lib/helper/observability_integration/build_href.ts b/x-pack/legacy/plugins/uptime/public/lib/helper/observability_integration/build_href.ts index 19e437651090b..0f830435be89d 100644 --- a/x-pack/legacy/plugins/uptime/public/lib/helper/observability_integration/build_href.ts +++ b/x-pack/legacy/plugins/uptime/public/lib/helper/observability_integration/build_href.ts @@ -5,7 +5,7 @@ */ import { get } from 'lodash'; -import { Check } from '../../../../common/graphql/types'; +import { Check } from '../../../../common/runtime_types'; /** * Builds URLs to the designated features by extracting values from the provided diff --git a/x-pack/legacy/plugins/uptime/public/lib/helper/observability_integration/get_apm_href.ts b/x-pack/legacy/plugins/uptime/public/lib/helper/observability_integration/get_apm_href.ts index aaa57acee6c6a..0ff5a8acb3367 100644 --- a/x-pack/legacy/plugins/uptime/public/lib/helper/observability_integration/get_apm_href.ts +++ b/x-pack/legacy/plugins/uptime/public/lib/helper/observability_integration/get_apm_href.ts @@ -6,7 +6,7 @@ import { get } from 'lodash'; import { addBasePath } from './add_base_path'; -import { MonitorSummary } from '../../../../common/graphql/types'; +import { MonitorSummary } from '../../../../common/runtime_types'; export const getApmHref = ( summary: MonitorSummary, diff --git a/x-pack/legacy/plugins/uptime/public/lib/helper/observability_integration/get_infra_href.ts b/x-pack/legacy/plugins/uptime/public/lib/helper/observability_integration/get_infra_href.ts index 73065be395c76..384067e4b033b 100644 --- a/x-pack/legacy/plugins/uptime/public/lib/helper/observability_integration/get_infra_href.ts +++ b/x-pack/legacy/plugins/uptime/public/lib/helper/observability_integration/get_infra_href.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { MonitorSummary } from '../../../../common/graphql/types'; +import { MonitorSummary } from '../../../../common/runtime_types'; import { addBasePath } from './add_base_path'; import { buildHref } from './build_href'; diff --git a/x-pack/legacy/plugins/uptime/public/lib/helper/observability_integration/get_logging_href.ts b/x-pack/legacy/plugins/uptime/public/lib/helper/observability_integration/get_logging_href.ts index b97b5a34855fb..222c7b57c9272 100644 --- a/x-pack/legacy/plugins/uptime/public/lib/helper/observability_integration/get_logging_href.ts +++ b/x-pack/legacy/plugins/uptime/public/lib/helper/observability_integration/get_logging_href.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { MonitorSummary } from '../../../../common/graphql/types'; +import { MonitorSummary } from '../../../../common/runtime_types'; import { addBasePath } from './add_base_path'; import { buildHref } from './build_href'; diff --git a/x-pack/legacy/plugins/uptime/public/lib/helper/series_has_down_values.ts b/x-pack/legacy/plugins/uptime/public/lib/helper/series_has_down_values.ts index 13079b912a147..4ebeb350ed892 100644 --- a/x-pack/legacy/plugins/uptime/public/lib/helper/series_has_down_values.ts +++ b/x-pack/legacy/plugins/uptime/public/lib/helper/series_has_down_values.ts @@ -4,8 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ -import { SummaryHistogramPoint } from '../../../common/graphql/types'; +import { HistogramPoint } from '../../../common/runtime_types'; -export const seriesHasDownValues = (series: SummaryHistogramPoint[] | null): boolean => { +export const seriesHasDownValues = (series: HistogramPoint[] | null): boolean => { return series ? series.some(point => !!point.down) : false; }; diff --git a/x-pack/legacy/plugins/uptime/public/lib/helper/url_params/__tests__/parse_absolute_date.test.ts b/x-pack/legacy/plugins/uptime/public/lib/helper/url_params/__tests__/parse_absolute_date.test.ts index 691b38bdf9ca2..16888aec21cfe 100644 --- a/x-pack/legacy/plugins/uptime/public/lib/helper/url_params/__tests__/parse_absolute_date.test.ts +++ b/x-pack/legacy/plugins/uptime/public/lib/helper/url_params/__tests__/parse_absolute_date.test.ts @@ -24,7 +24,7 @@ describe('parseAbsoluteDate', () => { it('returns the default value if the parser provides `undefined`', () => { dateMathSpy.mockReturnValue(undefined); - const result = parseAbsoluteDate('this is not a valid datae', 12345); + const result = parseAbsoluteDate('this is not a valid date', 12345); expect(result).toBe(12345); }); }); diff --git a/x-pack/legacy/plugins/uptime/public/lib/helper/url_params/__tests__/parse_autorefresh_interval.test.ts b/x-pack/legacy/plugins/uptime/public/lib/helper/url_params/__tests__/parse_autorefresh_interval.test.ts deleted file mode 100644 index a5c2168378089..0000000000000 --- a/x-pack/legacy/plugins/uptime/public/lib/helper/url_params/__tests__/parse_autorefresh_interval.test.ts +++ /dev/null @@ -1,24 +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 { parseUrlInt } from '../parse_url_int'; - -describe('parseUrlInt', () => { - it('parses a number', () => { - const result = parseUrlInt('23', 50); - expect(result).toBe(23); - }); - - it('returns default value for empty string', () => { - const result = parseUrlInt('', 50); - expect(result).toBe(50); - }); - - it('returns default value for non-numeric string', () => { - const result = parseUrlInt('abc', 50); - expect(result).toBe(50); - }); -}); diff --git a/x-pack/legacy/plugins/uptime/public/lib/lib.ts b/x-pack/legacy/plugins/uptime/public/lib/lib.ts index aba151bf5aab3..7dd3aa9eed5ce 100644 --- a/x-pack/legacy/plugins/uptime/public/lib/lib.ts +++ b/x-pack/legacy/plugins/uptime/public/lib/lib.ts @@ -4,10 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { NormalizedCacheObject } from 'apollo-cache-inmemory'; -import ApolloClient from 'apollo-client'; -import React from 'react'; -import { ChromeBreadcrumb } from 'src/core/public'; +import { ReactElement } from 'react'; import { UMBadge } from '../badge'; import { UptimeAppProps } from '../uptime_app'; @@ -15,13 +12,9 @@ export interface UMFrontendLibs { framework: UMFrameworkAdapter; } -export type UMUpdateBreadcrumbs = (breadcrumbs: ChromeBreadcrumb[]) => void; - export type UMUpdateBadge = (badge: UMBadge) => void; -export type UMGraphQLClient = ApolloClient<NormalizedCacheObject>; // | OtherClientType - -export type BootstrapUptimeApp = (props: UptimeAppProps) => React.ReactElement<any>; +export type BootstrapUptimeApp = (props: UptimeAppProps) => ReactElement<any>; export interface UMFrameworkAdapter { render(element: any): void; diff --git a/x-pack/legacy/plugins/uptime/public/pages/monitor.tsx b/x-pack/legacy/plugins/uptime/public/pages/monitor.tsx index 21124b7323d68..4495be9b24dc1 100644 --- a/x-pack/legacy/plugins/uptime/public/pages/monitor.tsx +++ b/x-pack/legacy/plugins/uptime/public/pages/monitor.tsx @@ -5,41 +5,22 @@ */ import { EuiSpacer } from '@elastic/eui'; -import React, { useContext, useState } from 'react'; -import { useParams } from 'react-router-dom'; +import React from 'react'; import { useSelector } from 'react-redux'; -import { MonitorCharts, PingList } from '../components/functional'; -import { UptimeRefreshContext } from '../contexts'; -import { useUptimeTelemetry, useUrlParams, UptimePage } from '../hooks'; import { useTrackPageview } from '../../../../../plugins/observability/public'; -import { MonitorStatusDetails } from '../components/connected'; import { monitorStatusSelector } from '../state/selectors'; import { PageHeader } from './page_header'; import { useBreadcrumbs } from '../hooks/use_breadcrumbs'; +import { useMonitorId, useUptimeTelemetry, UptimePage } from '../hooks'; +import { MonitorCharts } from '../components/monitor'; +import { MonitorStatusDetails } from '../components/monitor'; +import { PingList } from '../components/monitor'; export const MonitorPage: React.FC = () => { - // decode 64 base string, it was decoded to make it a valid url, since monitor id can be a url - let { monitorId } = useParams(); - monitorId = atob(monitorId || ''); - - const [pingListPageCount, setPingListPageCount] = useState<number>(10); - const { refreshApp } = useContext(UptimeRefreshContext); - const [getUrlParams, updateUrlParams] = useUrlParams(); - const { absoluteDateRangeStart, absoluteDateRangeEnd, ...params } = getUrlParams(); - const { dateRangeStart, dateRangeEnd, selectedPingStatus } = params; - - const [selectedLocation, setSelectedLocation] = useState(undefined); - const [pingListIndex, setPingListIndex] = useState(0); + const monitorId = useMonitorId(); const selectedMonitor = useSelector(monitorStatusSelector); - const sharedVariables = { - dateRangeStart, - dateRangeEnd, - monitorId, - location: selectedLocation, - }; - useUptimeTelemetry(UptimePage.Monitor); useTrackPageview({ app: 'uptime', path: 'monitor' }); @@ -55,25 +36,7 @@ export const MonitorPage: React.FC = () => { <EuiSpacer size="s" /> <MonitorCharts monitorId={monitorId} /> <EuiSpacer size="s" /> - <PingList - onPageCountChange={setPingListPageCount} - onSelectedLocationChange={setSelectedLocation} - onSelectedStatusChange={(selectedStatus: string | undefined) => { - updateUrlParams({ selectedPingStatus: selectedStatus || '' }); - refreshApp(); - }} - onPageIndexChange={(index: number) => setPingListIndex(index)} - pageIndex={pingListIndex} - pageSize={pingListPageCount} - selectedOption={selectedPingStatus} - selectedLocation={selectedLocation} - variables={{ - ...sharedVariables, - page: pingListIndex, - size: pingListPageCount, - status: selectedPingStatus, - }} - /> + <PingList monitorId={monitorId} /> </> ); }; diff --git a/x-pack/legacy/plugins/uptime/public/pages/overview.tsx b/x-pack/legacy/plugins/uptime/public/pages/overview.tsx index 943dbd6bd57ba..adc36efa6f7db 100644 --- a/x-pack/legacy/plugins/uptime/public/pages/overview.tsx +++ b/x-pack/legacy/plugins/uptime/public/pages/overview.tsx @@ -5,23 +5,19 @@ */ import { EuiFlexGroup, EuiFlexItem, EuiSpacer } from '@elastic/eui'; -import React, { useContext, useEffect, useState } from 'react'; +import React, { useEffect } from 'react'; import styled from 'styled-components'; import { i18n } from '@kbn/i18n'; -import { - MonitorList, - OverviewPageParsingErrorCallout, - StatusPanel, -} from '../components/functional'; -import { useUrlParams, useUptimeTelemetry, UptimePage } from '../hooks'; +import { useUptimeTelemetry, UptimePage, useGetUrlParams } from '../hooks'; import { stringifyUrlParams } from '../lib/helper/stringify_url_params'; import { useTrackPageview } from '../../../../../plugins/observability/public'; import { DataPublicPluginSetup, IIndexPattern } from '../../../../../../src/plugins/data/public'; -import { UptimeThemeContext } from '../contexts'; -import { EmptyState, FilterGroup, KueryBar } from '../components/connected'; import { useUpdateKueryString } from '../hooks'; import { PageHeader } from './page_header'; import { useBreadcrumbs } from '../hooks/use_breadcrumbs'; +import { MonitorList } from '../components/overview/monitor_list/monitor_list_container'; +import { EmptyState, FilterGroup, KueryBar, ParsingErrorCallout } from '../components/overview'; +import { StatusPanel } from '../components/overview/status_panel'; interface OverviewPageProps { autocomplete: DataPublicPluginSetup['autocomplete']; @@ -40,35 +36,9 @@ const EuiFlexItemStyled = styled(EuiFlexItem)` } `; -// TODO: these values belong deeper down in the monitor -// list pagination control, but are here temporarily until we -// are done removing GraphQL -const DEFAULT_PAGE_SIZE = 10; -const LOCAL_STORAGE_KEY = 'xpack.uptime.monitorList.pageSize'; -const getMonitorListPageSizeValue = () => { - const value = parseInt(localStorage.getItem(LOCAL_STORAGE_KEY) ?? '', 10); - if (isNaN(value)) { - return DEFAULT_PAGE_SIZE; - } - return value; -}; - export const OverviewPageComponent = ({ autocomplete, indexPattern, setEsKueryFilters }: Props) => { - const { colors } = useContext(UptimeThemeContext); - const [getUrlParams] = useUrlParams(); - // TODO: this is temporary until we migrate the monitor list to our Redux implementation - const [monitorListPageSize, setMonitorListPageSize] = useState<number>( - getMonitorListPageSizeValue() - ); - const { absoluteDateRangeStart, absoluteDateRangeEnd, ...params } = getUrlParams(); - const { - dateRangeStart, - dateRangeEnd, - pagination, - statusFilter, - search, - filters: urlFilters, - } = params; + const { absoluteDateRangeStart, absoluteDateRangeEnd, ...params } = useGetUrlParams(); + const { search, filters: urlFilters } = params; useUptimeTelemetry(UptimePage.Overview); @@ -81,13 +51,6 @@ export const OverviewPageComponent = ({ autocomplete, indexPattern, setEsKueryFi setEsKueryFilters(esFilters ?? ''); }, [esFilters, setEsKueryFilters]); - const sharedProps = { - dateRangeStart, - dateRangeEnd, - statusFilter, - filters: esFilters, - }; - const linkParameters = stringifyUrlParams(params, true); const heading = i18n.translate('xpack.uptime.overviewPage.headerText', { @@ -113,25 +76,12 @@ export const OverviewPageComponent = ({ autocomplete, indexPattern, setEsKueryFi <EuiFlexItemStyled grow={true}> <FilterGroup esFilters={esFilters} /> </EuiFlexItemStyled> - {error && <OverviewPageParsingErrorCallout error={error} />} + {error && <ParsingErrorCallout error={error} />} </EuiFlexGroup> <EuiSpacer size="s" /> <StatusPanel /> <EuiSpacer size="s" /> - <MonitorList - dangerColor={colors.danger} - hasActiveFilters={!!esFilters} - implementsCustomErrorState={true} - linkParameters={linkParameters} - pageSize={monitorListPageSize} - setPageSize={setMonitorListPageSize} - successColor={colors.success} - variables={{ - ...sharedProps, - pagination, - pageSize: monitorListPageSize, - }} - /> + <MonitorList filters={esFilters} linkParameters={linkParameters} /> </EmptyState> </> ); diff --git a/x-pack/legacy/plugins/uptime/public/pages/page_header.tsx b/x-pack/legacy/plugins/uptime/public/pages/page_header.tsx index 821a70c85dc7c..b10bc6ba44f8a 100644 --- a/x-pack/legacy/plugins/uptime/public/pages/page_header.tsx +++ b/x-pack/legacy/plugins/uptime/public/pages/page_header.tsx @@ -8,16 +8,18 @@ import React from 'react'; import { EuiFlexGroup, EuiFlexItem, EuiTitle, EuiSpacer, EuiButtonEmpty } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { Link } from 'react-router-dom'; -import { UptimeDatePicker } from '../components/functional/uptime_date_picker'; +import { UptimeDatePicker } from '../components/common/uptime_date_picker'; import { SETTINGS_ROUTE } from '../../common/constants'; -import { ToggleAlertFlyoutButton } from '../components/connected'; +import { ToggleAlertFlyoutButton } from '../components/overview/alerts/alerts_containers'; interface PageHeaderProps { headingText: string; extraLinks?: boolean; datePicker?: boolean; } - +const SETTINGS_LINK_TEXT = i18n.translate('xpack.uptime.page_header.settingsLink', { + defaultMessage: 'Settings', +}); export const PageHeader = React.memo( ({ headingText, extraLinks = false, datePicker = true }: PageHeaderProps) => { const datePickerComponent = datePicker ? ( @@ -26,9 +28,6 @@ export const PageHeader = React.memo( </EuiFlexItem> ) : null; - const settingsLinkText = i18n.translate('xpack.uptime.page_header.settingsLink', { - defaultMessage: 'Settings', - }); const extraLinkComponents = !extraLinks ? null : ( <EuiFlexGroup alignItems="flexEnd"> <EuiFlexItem grow={false}> @@ -37,7 +36,7 @@ export const PageHeader = React.memo( <EuiFlexItem grow={false}> <Link to={SETTINGS_ROUTE}> <EuiButtonEmpty data-test-subj="settings-page-link" iconType="gear"> - {settingsLinkText} + {SETTINGS_LINK_TEXT} </EuiButtonEmpty> </Link> </EuiFlexItem> diff --git a/x-pack/legacy/plugins/uptime/public/pages/settings.tsx b/x-pack/legacy/plugins/uptime/public/pages/settings.tsx index d3e17a15ee0e0..6defb96e0da3d 100644 --- a/x-pack/legacy/plugins/uptime/public/pages/settings.tsx +++ b/x-pack/legacy/plugins/uptime/public/pages/settings.tsx @@ -9,46 +9,54 @@ import { EuiButton, EuiButtonEmpty, EuiCallOut, - EuiCode, - EuiDescribedFormGroup, - EuiFieldText, EuiFlexGroup, EuiFlexItem, EuiForm, - EuiFormRow, EuiPanel, EuiSpacer, - EuiTitle, } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; -import { connect } from 'react-redux'; -import { isEqual } from 'lodash'; +import { useDispatch, useSelector } from 'react-redux'; +import { cloneDeep, isEqual, set } from 'lodash'; import { i18n } from '@kbn/i18n'; import { Link } from 'react-router-dom'; -import { AppState } from '../state'; import { selectDynamicSettings } from '../state/selectors'; -import { DynamicSettingsState } from '../state/reducers/dynamic_settings'; import { getDynamicSettings, setDynamicSettings } from '../state/actions/dynamic_settings'; -import { defaultDynamicSettings, DynamicSettings } from '../../common/runtime_types'; +import { DynamicSettings } from '../../common/runtime_types'; import { useBreadcrumbs } from '../hooks/use_breadcrumbs'; import { OVERVIEW_ROUTE } from '../../common/constants'; import { useKibana } from '../../../../../../src/plugins/kibana_react/public'; import { UptimePage, useUptimeTelemetry } from '../hooks'; +import { IndicesForm } from '../components/settings/indices_form'; +import { + CertificateExpirationForm, + OnFieldChangeType, +} from '../components/settings/certificate_form'; + +const getFieldErrors = (formFields: DynamicSettings | null) => { + if (formFields) { + const blankStr = 'May not be blank'; + const { certificatesThresholds, heartbeatIndices } = formFields; + const heartbeatIndErr = heartbeatIndices.match(/^\S+$/) ? '' : blankStr; + const errorStateErr = certificatesThresholds?.errorState ? null : blankStr; + const warningStateErr = certificatesThresholds?.warningState ? null : blankStr; + return { + heartbeatIndices: heartbeatIndErr, + certificatesThresholds: + errorStateErr || warningStateErr + ? { + errorState: errorStateErr, + warningState: warningStateErr, + } + : null, + }; + } + return null; +}; -interface Props { - dynamicSettingsState: DynamicSettingsState; -} - -interface DispatchProps { - dispatchGetDynamicSettings: typeof getDynamicSettings; - dispatchSetDynamicSettings: typeof setDynamicSettings; -} +export const SettingsPage = () => { + const dss = useSelector(selectDynamicSettings); -export const SettingsPageComponent = ({ - dynamicSettingsState: dss, - dispatchGetDynamicSettings, - dispatchSetDynamicSettings, -}: Props & DispatchProps) => { const settingsBreadcrumbText = i18n.translate('xpack.uptime.settingsBreadcrumbText', { defaultMessage: 'Settings', }); @@ -56,9 +64,11 @@ export const SettingsPageComponent = ({ useUptimeTelemetry(UptimePage.Settings); + const dispatch = useDispatch(); + useEffect(() => { - dispatchGetDynamicSettings({}); - }, [dispatchGetDynamicSettings]); + dispatch(getDynamicSettings()); + }, [dispatch]); const [formFields, setFormFields] = useState<DynamicSettings | null>(dss.settings || null); @@ -66,22 +76,22 @@ export const SettingsPageComponent = ({ setFormFields({ ...dss.settings }); } - const fieldErrors = formFields && { - heartbeatIndices: formFields.heartbeatIndices.match(/^\S+$/) ? null : 'May not be blank', - }; + const fieldErrors = getFieldErrors(formFields); + const isFormValid = !(fieldErrors && Object.values(fieldErrors).find(v => !!v)); - const onChangeFormField = (field: keyof DynamicSettings, value: any) => { + const onChangeFormField: OnFieldChangeType = (field, value) => { if (formFields) { - formFields[field] = value; - setFormFields({ ...formFields }); + const newFormFields = cloneDeep(formFields); + set(newFormFields, field, value); + setFormFields(cloneDeep(newFormFields)); } }; const onApply = (event: React.FormEvent) => { event.preventDefault(); if (formFields) { - dispatchSetDynamicSettings(formFields); + dispatch(setDynamicSettings(formFields)); } }; @@ -112,7 +122,7 @@ export const SettingsPageComponent = ({ return ( <> - <Link to={OVERVIEW_ROUTE}> + <Link to={OVERVIEW_ROUTE} data-test-subj="uptimeSettingsToOverviewLink"> <EuiButtonEmpty size="s" color="primary" iconType="arrowLeft"> {i18n.translate('xpack.uptime.settings.returnToOverviewLinkLabel', { defaultMessage: 'Return to overview', @@ -128,68 +138,18 @@ export const SettingsPageComponent = ({ <EuiFlexItem grow={false}> <form onSubmit={onApply}> <EuiForm> - <EuiTitle size="s"> - <h3> - <FormattedMessage - id="xpack.uptime.sourceConfiguration.indicesSectionTitle" - defaultMessage="Indices" - /> - </h3> - </EuiTitle> - <EuiSpacer size="m" /> - <EuiDescribedFormGroup - title={ - <h4> - <FormattedMessage - id="xpack.uptime.sourceConfiguration.heartbeatIndicesTitle" - defaultMessage="Uptime indices" - /> - </h4> - } - description={ - <FormattedMessage - id="xpack.uptime.sourceConfiguration.heartbeatIndicesDescription" - defaultMessage="Index pattern for matching indices that contain Heartbeat data" - /> - } - > - <EuiFormRow - describedByIds={['heartbeatIndices']} - error={fieldErrors?.heartbeatIndices} - fullWidth - helpText={ - <FormattedMessage - id="xpack.uptime.sourceConfiguration.heartbeatIndicesDefaultValue" - defaultMessage="The default value is {defaultValue}" - values={{ - defaultValue: ( - <EuiCode>{defaultDynamicSettings.heartbeatIndices}</EuiCode> - ), - }} - /> - } - isInvalid={!!fieldErrors?.heartbeatIndices} - label={ - <FormattedMessage - id="xpack.uptime.sourceConfiguration.heartbeatIndicesLabel" - defaultMessage="Heartbeat indices" - /> - } - > - <EuiFieldText - data-test-subj={`heartbeat-indices-input-${ - dss.loading ? 'loading' : 'loaded' - }`} - fullWidth - disabled={isFormDisabled} - isLoading={dss.loading} - value={formFields?.heartbeatIndices || ''} - onChange={(event: any) => - onChangeFormField('heartbeatIndices', event.currentTarget.value) - } - /> - </EuiFormRow> - </EuiDescribedFormGroup> + <IndicesForm + onChange={onChangeFormField} + formFields={formFields} + fieldErrors={fieldErrors} + isDisabled={isFormDisabled} + /> + <CertificateExpirationForm + onChange={onChangeFormField} + formFields={formFields} + fieldErrors={fieldErrors} + isDisabled={isFormDisabled} + /> <EuiSpacer size="m" /> <EuiFlexGroup justifyContent="flexEnd" gutterSize="s"> @@ -230,18 +190,3 @@ export const SettingsPageComponent = ({ </> ); }; - -const mapStateToProps = (state: AppState) => ({ - dynamicSettingsState: selectDynamicSettings(state), -}); - -const mapDispatchToProps = (dispatch: any) => ({ - dispatchGetDynamicSettings: () => { - return dispatch(getDynamicSettings({})); - }, - dispatchSetDynamicSettings: (settings: DynamicSettings) => { - return dispatch(setDynamicSettings(settings)); - }, -}); - -export const SettingsPage = connect(mapStateToProps, mapDispatchToProps)(SettingsPageComponent); diff --git a/x-pack/legacy/plugins/uptime/public/queries/index.ts b/x-pack/legacy/plugins/uptime/public/queries/index.ts deleted file mode 100644 index 283382ec1b7ba..0000000000000 --- a/x-pack/legacy/plugins/uptime/public/queries/index.ts +++ /dev/null @@ -1,7 +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. - */ - -export { pingsQuery, pingsQueryString } from './pings_query'; diff --git a/x-pack/legacy/plugins/uptime/public/queries/monitor_states_query.ts b/x-pack/legacy/plugins/uptime/public/queries/monitor_states_query.ts deleted file mode 100644 index 676e638c239de..0000000000000 --- a/x-pack/legacy/plugins/uptime/public/queries/monitor_states_query.ts +++ /dev/null @@ -1,110 +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 gql from 'graphql-tag'; - -export const monitorStatesQueryString = ` -query MonitorStates($dateRangeStart: String!, $dateRangeEnd: String!, $pagination: String, $filters: String, $statusFilter: String, $pageSize: Int) { - monitorStates: getMonitorStates( - dateRangeStart: $dateRangeStart - dateRangeEnd: $dateRangeEnd - pagination: $pagination - filters: $filters - statusFilter: $statusFilter - pageSize: $pageSize - ) { - prevPagePagination - nextPagePagination - totalSummaryCount - summaries { - monitor_id - histogram { - count - points { - timestamp - up - down - } - } - state { - agent { - id - } - checks { - agent { - id - } - container { - id - } - kubernetes { - pod { - uid - } - } - monitor { - ip - name - status - } - observer { - geo { - name - location { - lat - lon - } - } - } - timestamp - } - geo { - name - location { - lat - lon - } - } - observer { - geo { - name - location { - lat - lon - } - } - } - monitor { - id - name - status - type - } - summary { - up - down - geo { - name - location { - lat - lon - } - } - } - url { - full - domain - } - timestamp - } - } - } -} -`; - -export const monitorStatesQuery = gql` - ${monitorStatesQueryString} -`; diff --git a/x-pack/legacy/plugins/uptime/public/queries/pings_query.ts b/x-pack/legacy/plugins/uptime/public/queries/pings_query.ts deleted file mode 100644 index ed20fe8eb2931..0000000000000 --- a/x-pack/legacy/plugins/uptime/public/queries/pings_query.ts +++ /dev/null @@ -1,73 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import gql from 'graphql-tag'; - -export const pingsQueryString = ` -query PingList( - $dateRangeStart: String! - $dateRangeEnd: String! - $monitorId: String - $status: String - $sort: String - $size: Int - $location: String - $page: Int -) { - allPings( - dateRangeStart: $dateRangeStart - dateRangeEnd: $dateRangeEnd - monitorId: $monitorId - status: $status - sort: $sort - size: $size - location: $location - page: $page - ) { - total - locations - pings { - id - timestamp - http { - response { - status_code - body { - bytes - hash - content - content_bytes - } - } - } - error { - message - type - } - monitor { - duration { - us - } - id - ip - name - scheme - status - type - } - observer { - geo { - name - } - } - } - } - } -`; - -export const pingsQuery = gql` - ${pingsQueryString} -`; diff --git a/x-pack/legacy/plugins/uptime/public/routes.tsx b/x-pack/legacy/plugins/uptime/public/routes.tsx index bb0700287dbf1..b5e20ef8a70a9 100644 --- a/x-pack/legacy/plugins/uptime/public/routes.tsx +++ b/x-pack/legacy/plugins/uptime/public/routes.tsx @@ -7,7 +7,7 @@ import React, { FC } from 'react'; import { Route, Switch } from 'react-router-dom'; import { DataPublicPluginSetup } from '../../../../../src/plugins/data/public'; -import { OverviewPage } from './components/connected/pages/overview_container'; +import { OverviewPage } from './components/overview/overview_container'; import { MONITOR_ROUTE, OVERVIEW_ROUTE, SETTINGS_ROUTE } from '../common/constants'; import { MonitorPage, NotFoundPage, SettingsPage } from './pages'; diff --git a/x-pack/legacy/plugins/uptime/public/state/actions/dynamic_settings.ts b/x-pack/legacy/plugins/uptime/public/state/actions/dynamic_settings.ts index d78c725c4b599..3dbb1aa234621 100644 --- a/x-pack/legacy/plugins/uptime/public/state/actions/dynamic_settings.ts +++ b/x-pack/legacy/plugins/uptime/public/state/actions/dynamic_settings.ts @@ -6,7 +6,7 @@ import { createAction } from 'redux-actions'; import { DynamicSettings } from '../../../common/runtime_types'; -export const getDynamicSettings = createAction<{}>('GET_DYNAMIC_SETTINGS'); +export const getDynamicSettings = createAction('GET_DYNAMIC_SETTINGS'); export const getDynamicSettingsSuccess = createAction<DynamicSettings>( 'GET_DYNAMIC_SETTINGS_SUCCESS' ); @@ -17,4 +17,3 @@ export const setDynamicSettingsSuccess = createAction<DynamicSettings>( 'SET_DYNAMIC_SETTINGS_SUCCESS' ); export const setDynamicSettingsFail = createAction<Error>('SET_DYNAMIC_SETTINGS_FAIL'); -export const acknowledgeSetDynamicSettings = createAction<{}>('ACKNOWLEDGE_SET_DYNAMIC_SETTINGS'); diff --git a/x-pack/legacy/plugins/uptime/public/state/actions/index.ts b/x-pack/legacy/plugins/uptime/public/state/actions/index.ts index 4563e6bfc4f0e..0dc6baa0b2e50 100644 --- a/x-pack/legacy/plugins/uptime/public/state/actions/index.ts +++ b/x-pack/legacy/plugins/uptime/public/state/actions/index.ts @@ -7,6 +7,7 @@ export * from './overview_filters'; export * from './snapshot'; export * from './ui'; +export * from './monitor_list'; export * from './monitor_status'; export * from './index_patternts'; export * from './ping'; diff --git a/x-pack/legacy/plugins/uptime/public/state/actions/monitor_list.ts b/x-pack/legacy/plugins/uptime/public/state/actions/monitor_list.ts new file mode 100644 index 0000000000000..ee2267a9058af --- /dev/null +++ b/x-pack/legacy/plugins/uptime/public/state/actions/monitor_list.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 { createAction } from 'redux-actions'; +import { FetchMonitorStatesQueryArgs, MonitorSummaryResult } from '../../../common/runtime_types'; + +export const getMonitorList = createAction<FetchMonitorStatesQueryArgs>('GET_MONITOR_LIST'); +export const getMonitorListSuccess = createAction<MonitorSummaryResult>('GET_MONITOR_LIST_SUCCESS'); +export const getMonitorListFailure = createAction<Error>('GET_MONITOR_LIST_FAIL'); diff --git a/x-pack/legacy/plugins/uptime/public/state/actions/monitor_status.ts b/x-pack/legacy/plugins/uptime/public/state/actions/monitor_status.ts index a8f37d38ebae6..3d480e66c9e0b 100644 --- a/x-pack/legacy/plugins/uptime/public/state/actions/monitor_status.ts +++ b/x-pack/legacy/plugins/uptime/public/state/actions/monitor_status.ts @@ -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 { createAction } from 'redux-actions'; import { QueryParams } from './types'; -import { Ping } from '../../../common/graphql/types'; +import { Ping } from '../../../common/runtime_types'; export const getMonitorStatusAction = createAction<QueryParams>('GET_MONITOR_STATUS'); export const getMonitorStatusActionSuccess = createAction<Ping>('GET_MONITOR_STATUS_SUCCESS'); diff --git a/x-pack/legacy/plugins/uptime/public/state/actions/ping.ts b/x-pack/legacy/plugins/uptime/public/state/actions/ping.ts index bb7258d9a54b2..70918a4cc70e5 100644 --- a/x-pack/legacy/plugins/uptime/public/state/actions/ping.ts +++ b/x-pack/legacy/plugins/uptime/public/state/actions/ping.ts @@ -5,8 +5,17 @@ */ import { createAction } from 'redux-actions'; -import { GetPingHistogramParams, HistogramResult } from '../../../common/types'; +import { + GetPingHistogramParams, + HistogramResult, + PingsResponse, + GetPingsParams, +} from '../../../common/runtime_types'; export const getPingHistogram = createAction<GetPingHistogramParams>('GET_PING_HISTOGRAM'); export const getPingHistogramSuccess = createAction<HistogramResult>('GET_PING_HISTOGRAM_SUCCESS'); export const getPingHistogramFail = createAction<Error>('GET_PING_HISTOGRAM_FAIL'); + +export const getPings = createAction<GetPingsParams>('GET PINGS'); +export const getPingsSuccess = createAction<PingsResponse>('GET PINGS SUCCESS'); +export const getPingsFail = createAction<Error>('GET PINGS FAIL'); diff --git a/x-pack/legacy/plugins/uptime/public/state/api/dynamic_settings.ts b/x-pack/legacy/plugins/uptime/public/state/api/dynamic_settings.ts index 8ade2aa4595dc..e52e40c53513c 100644 --- a/x-pack/legacy/plugins/uptime/public/state/api/dynamic_settings.ts +++ b/x-pack/legacy/plugins/uptime/public/state/api/dynamic_settings.ts @@ -14,22 +14,15 @@ import { apiService } from './utils'; const apiPath = '/api/uptime/dynamic_settings'; -interface BaseApiRequest { - basePath: string; -} - -type SaveApiRequest = BaseApiRequest & { +interface SaveApiRequest { settings: DynamicSettings; -}; +} -export const getDynamicSettings = async ({ - basePath, -}: BaseApiRequest): Promise<DynamicSettings> => { +export const getDynamicSettings = async (): Promise<DynamicSettings> => { return await apiService.get(apiPath, undefined, DynamicSettingsType); }; export const setDynamicSettings = async ({ - basePath, settings, }: SaveApiRequest): Promise<DynamicSettingsSaveResponse> => { return await apiService.post(apiPath, settings, DynamicSettingsSaveType); diff --git a/x-pack/legacy/plugins/uptime/public/state/api/index.ts b/x-pack/legacy/plugins/uptime/public/state/api/index.ts index 793762c0f4a10..a50afb3f866de 100644 --- a/x-pack/legacy/plugins/uptime/public/state/api/index.ts +++ b/x-pack/legacy/plugins/uptime/public/state/api/index.ts @@ -5,6 +5,7 @@ */ export * from './monitor'; +export * from './monitor_list'; export * from './overview_filters'; export * from './snapshot'; export * from './monitor_status'; diff --git a/x-pack/legacy/plugins/uptime/public/state/api/ml_anomaly.ts b/x-pack/legacy/plugins/uptime/public/state/api/ml_anomaly.ts index bcd2582fe18b9..f10745a50f56a 100644 --- a/x-pack/legacy/plugins/uptime/public/state/api/ml_anomaly.ts +++ b/x-pack/legacy/plugins/uptime/public/state/api/ml_anomaly.ts @@ -48,11 +48,8 @@ export const createMLJob = async ({ query: { bool: { filter: [ - { - term: { - 'monitor.id': lowerCaseMonitorId, - }, - }, + { term: { 'monitor.id': lowerCaseMonitorId } }, + { range: { 'monitor.duration.us': { gt: 0 } } }, ], }, }, diff --git a/x-pack/legacy/plugins/uptime/public/state/api/monitor.ts b/x-pack/legacy/plugins/uptime/public/state/api/monitor.ts index b36eccca98da9..c3d0a0180cf51 100644 --- a/x-pack/legacy/plugins/uptime/public/state/api/monitor.ts +++ b/x-pack/legacy/plugins/uptime/public/state/api/monitor.ts @@ -8,7 +8,7 @@ import { BaseParams } from './types'; import { MonitorDetailsType, MonitorLocationsType } from '../../../common/runtime_types'; import { QueryParams } from '../actions/types'; import { apiService } from './utils'; -import { API_URLS } from '../../../common/constants/rest_api'; +import { API_URLS } from '../../../common/constants'; interface ApiRequest { monitorId: string; diff --git a/x-pack/legacy/plugins/uptime/public/state/api/monitor_duration.ts b/x-pack/legacy/plugins/uptime/public/state/api/monitor_duration.ts index daf725119fcf3..91034f1784b15 100644 --- a/x-pack/legacy/plugins/uptime/public/state/api/monitor_duration.ts +++ b/x-pack/legacy/plugins/uptime/public/state/api/monitor_duration.ts @@ -6,7 +6,7 @@ import { BaseParams } from './types'; import { apiService } from './utils'; -import { API_URLS } from '../../../common/constants/rest_api'; +import { API_URLS } from '../../../common/constants'; export const fetchMonitorDuration = async ({ monitorId, dateStart, dateEnd }: BaseParams) => { const queryParams = { diff --git a/x-pack/legacy/plugins/uptime/public/state/api/monitor_list.ts b/x-pack/legacy/plugins/uptime/public/state/api/monitor_list.ts new file mode 100644 index 0000000000000..084bcb4bd2a91 --- /dev/null +++ b/x-pack/legacy/plugins/uptime/public/state/api/monitor_list.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. + */ + +import { API_URLS } from '../../../common/constants'; +import { apiService } from './utils'; +import { + FetchMonitorStatesQueryArgs, + MonitorSummaryResult, + MonitorSummaryResultType, +} from '../../../common/runtime_types'; + +export const fetchMonitorList = async ( + params: FetchMonitorStatesQueryArgs +): Promise<MonitorSummaryResult> => { + return await apiService.get(API_URLS.MONITOR_LIST, params, MonitorSummaryResultType); +}; diff --git a/x-pack/legacy/plugins/uptime/public/state/api/monitor_status.ts b/x-pack/legacy/plugins/uptime/public/state/api/monitor_status.ts index f9e171adda334..7c8ab3518b5a0 100644 --- a/x-pack/legacy/plugins/uptime/public/state/api/monitor_status.ts +++ b/x-pack/legacy/plugins/uptime/public/state/api/monitor_status.ts @@ -5,9 +5,9 @@ */ import { QueryParams } from '../actions/types'; -import { Ping } from '../../../common/graphql/types'; -import { apiService } from './utils'; +import { Ping } from '../../../common/runtime_types'; import { API_URLS } from '../../../common/constants'; +import { apiService } from './utils'; export const fetchMonitorStatus = async ({ monitorId, diff --git a/x-pack/legacy/plugins/uptime/public/state/api/overview_filters.ts b/x-pack/legacy/plugins/uptime/public/state/api/overview_filters.ts index 9943bc27f11f0..6330d8a912210 100644 --- a/x-pack/legacy/plugins/uptime/public/state/api/overview_filters.ts +++ b/x-pack/legacy/plugins/uptime/public/state/api/overview_filters.ts @@ -4,10 +4,10 @@ * you may not use this file except in compliance with the Elastic License. */ -import { GetOverviewFiltersPayload } from '../actions/overview_filters'; +import { GetOverviewFiltersPayload } from '../actions'; import { OverviewFiltersType } from '../../../common/runtime_types'; import { apiService } from './utils'; -import { API_URLS } from '../../../common/constants/rest_api'; +import { API_URLS } from '../../../common/constants'; export const fetchOverviewFilters = async ({ dateRangeStart, diff --git a/x-pack/legacy/plugins/uptime/public/state/api/ping.ts b/x-pack/legacy/plugins/uptime/public/state/api/ping.ts index df71cc8d67bd0..6de27879a49f5 100644 --- a/x-pack/legacy/plugins/uptime/public/state/api/ping.ts +++ b/x-pack/legacy/plugins/uptime/public/state/api/ping.ts @@ -5,9 +5,20 @@ */ import { APIFn } from './types'; -import { GetPingHistogramParams, HistogramResult } from '../../../common/types'; +import { + PingsResponseType, + PingsResponse, + GetPingsParams, + GetPingHistogramParams, + HistogramResult, +} from '../../../common/runtime_types'; import { apiService } from './utils'; -import { API_URLS } from '../../../common/constants/rest_api'; +import { API_URLS } from '../../../common/constants'; + +export const fetchPings: APIFn<GetPingsParams, PingsResponse> = async ({ + dateRange: { from, to }, + ...optional +}) => await apiService.get(API_URLS.PINGS, { from, to, ...optional }, PingsResponseType); export const fetchPingHistogram: APIFn<GetPingHistogramParams, HistogramResult> = async ({ monitorId, @@ -19,9 +30,9 @@ export const fetchPingHistogram: APIFn<GetPingHistogramParams, HistogramResult> const queryParams = { dateStart, dateEnd, - ...(monitorId && { monitorId }), - ...(statusFilter && { statusFilter }), - ...(filters && { filters }), + monitorId, + statusFilter, + filters, }; return await apiService.get(API_URLS.PING_HISTOGRAM, queryParams); diff --git a/x-pack/legacy/plugins/uptime/public/state/api/snapshot.ts b/x-pack/legacy/plugins/uptime/public/state/api/snapshot.ts index e663d0241d688..9ee53dd2cbcef 100644 --- a/x-pack/legacy/plugins/uptime/public/state/api/snapshot.ts +++ b/x-pack/legacy/plugins/uptime/public/state/api/snapshot.ts @@ -6,7 +6,7 @@ import { SnapshotType, Snapshot } from '../../../common/runtime_types'; import { apiService } from './utils'; -import { API_URLS } from '../../../common/constants/rest_api'; +import { API_URLS } from '../../../common/constants'; export interface SnapShotQueryParams { dateRangeStart: string; diff --git a/x-pack/legacy/plugins/uptime/public/state/effects/dynamic_settings.ts b/x-pack/legacy/plugins/uptime/public/state/effects/dynamic_settings.ts index 9bc8bd95be68c..bee92813aa1f0 100644 --- a/x-pack/legacy/plugins/uptime/public/state/effects/dynamic_settings.ts +++ b/x-pack/legacy/plugins/uptime/public/state/effects/dynamic_settings.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { takeLatest, put, call, select } from 'redux-saga/effects'; +import { takeLatest, put, call } from 'redux-saga/effects'; import { Action } from 'redux-actions'; import { i18n } from '@kbn/i18n'; import { fetchEffectFactory } from './fetch_effect'; @@ -21,7 +21,6 @@ import { setDynamicSettings as setDynamicSettingsAPI, } from '../api'; import { DynamicSettings } from '../../../common/runtime_types'; -import { getBasePath } from '../selectors'; import { kibanaService } from '../kibana_service'; export function* fetchDynamicSettingsEffect() { @@ -46,8 +45,7 @@ export function* setDynamicSettingsEffect() { }); return; } - const basePath = yield select(getBasePath); - yield call(setDynamicSettingsAPI, { settings: action.payload, basePath }); + yield call(setDynamicSettingsAPI, { settings: action.payload }); yield put(setDynamicSettingsSuccess(action.payload)); kibanaService.core.notifications.toasts.addSuccess('Settings saved!'); } catch (err) { diff --git a/x-pack/legacy/plugins/uptime/public/state/effects/fetch_effect.ts b/x-pack/legacy/plugins/uptime/public/state/effects/fetch_effect.ts index 49e497952ea44..b0734cb5ccabb 100644 --- a/x-pack/legacy/plugins/uptime/public/state/effects/fetch_effect.ts +++ b/x-pack/legacy/plugins/uptime/public/state/effects/fetch_effect.ts @@ -16,7 +16,7 @@ import { IHttpFetchError } from '../../../../../../../target/types/core/public/h * @param fail creates a failure action * @template T the action type expected by the fetch action * @template R the type that the API request should return on success - * @template S tye type of the success action + * @template S the type of the success action * @template F the type of the failure action */ export function fetchEffectFactory<T, R, S, F>( diff --git a/x-pack/legacy/plugins/uptime/public/state/effects/index.ts b/x-pack/legacy/plugins/uptime/public/state/effects/index.ts index 8d457be1d1c78..739179c5bbeae 100644 --- a/x-pack/legacy/plugins/uptime/public/state/effects/index.ts +++ b/x-pack/legacy/plugins/uptime/public/state/effects/index.ts @@ -8,10 +8,11 @@ import { fork } from 'redux-saga/effects'; import { fetchMonitorDetailsEffect } from './monitor'; import { fetchOverviewFiltersEffect } from './overview_filters'; import { fetchSnapshotCountEffect } from './snapshot'; +import { fetchMonitorListEffect } from './monitor_list'; import { fetchMonitorStatusEffect } from './monitor_status'; import { fetchDynamicSettingsEffect, setDynamicSettingsEffect } from './dynamic_settings'; import { fetchIndexPatternEffect } from './index_pattern'; -import { fetchPingHistogramEffect } from './ping'; +import { fetchPingsEffect, fetchPingHistogramEffect } from './ping'; import { fetchMonitorDurationEffect } from './monitor_duration'; import { fetchMLJobEffect } from './ml_anomaly'; import { fetchIndexStatusEffect } from './index_status'; @@ -20,10 +21,12 @@ export function* rootEffect() { yield fork(fetchMonitorDetailsEffect); yield fork(fetchSnapshotCountEffect); yield fork(fetchOverviewFiltersEffect); + yield fork(fetchMonitorListEffect); yield fork(fetchMonitorStatusEffect); yield fork(fetchDynamicSettingsEffect); yield fork(setDynamicSettingsEffect); yield fork(fetchIndexPatternEffect); + yield fork(fetchPingsEffect); yield fork(fetchPingHistogramEffect); yield fork(fetchMLJobEffect); yield fork(fetchMonitorDurationEffect); diff --git a/x-pack/legacy/plugins/uptime/public/state/effects/monitor_list.ts b/x-pack/legacy/plugins/uptime/public/state/effects/monitor_list.ts new file mode 100644 index 0000000000000..b607641ecd7d0 --- /dev/null +++ b/x-pack/legacy/plugins/uptime/public/state/effects/monitor_list.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 { takeLatest } from 'redux-saga/effects'; +import { getMonitorList, getMonitorListSuccess, getMonitorListFailure } from '../actions'; +import { fetchMonitorList } from '../api'; +import { fetchEffectFactory } from './fetch_effect'; + +export function* fetchMonitorListEffect() { + yield takeLatest( + getMonitorList, + fetchEffectFactory(fetchMonitorList, getMonitorListSuccess, getMonitorListFailure) + ); +} diff --git a/x-pack/legacy/plugins/uptime/public/state/effects/ping.ts b/x-pack/legacy/plugins/uptime/public/state/effects/ping.ts index acb9b31915fa9..dec67ed8cf979 100644 --- a/x-pack/legacy/plugins/uptime/public/state/effects/ping.ts +++ b/x-pack/legacy/plugins/uptime/public/state/effects/ping.ts @@ -5,10 +5,21 @@ */ import { takeLatest } from 'redux-saga/effects'; -import { getPingHistogram, getPingHistogramSuccess, getPingHistogramFail } from '../actions'; -import { fetchPingHistogram } from '../api'; +import { + getPingHistogram, + getPingHistogramSuccess, + getPingHistogramFail, + getPings, + getPingsSuccess, + getPingsFail, +} from '../actions'; +import { fetchPingHistogram, fetchPings } from '../api'; import { fetchEffectFactory } from './fetch_effect'; +export function* fetchPingsEffect() { + yield takeLatest(String(getPings), fetchEffectFactory(fetchPings, getPingsSuccess, getPingsFail)); +} + export function* fetchPingHistogramEffect() { yield takeLatest( String(getPingHistogram), diff --git a/x-pack/legacy/plugins/uptime/public/state/reducers/index.ts b/x-pack/legacy/plugins/uptime/public/state/reducers/index.ts index 7bf8af5dd4d03..294bde2f277ec 100644 --- a/x-pack/legacy/plugins/uptime/public/state/reducers/index.ts +++ b/x-pack/legacy/plugins/uptime/public/state/reducers/index.ts @@ -10,9 +10,11 @@ import { overviewFiltersReducer } from './overview_filters'; import { snapshotReducer } from './snapshot'; import { uiReducer } from './ui'; import { monitorStatusReducer } from './monitor_status'; +import { monitorListReducer } from './monitor_list'; import { dynamicSettingsReducer } from './dynamic_settings'; import { indexPatternReducer } from './index_pattern'; import { pingReducer } from './ping'; +import { pingListReducer } from './ping_list'; import { monitorDurationReducer } from './monitor_duration'; import { indexStatusReducer } from './index_status'; import { mlJobsReducer } from './ml_anomaly'; @@ -22,10 +24,12 @@ export const rootReducer = combineReducers({ overviewFilters: overviewFiltersReducer, snapshot: snapshotReducer, ui: uiReducer, + monitorList: monitorListReducer, monitorStatus: monitorStatusReducer, dynamicSettings: dynamicSettingsReducer, indexPattern: indexPatternReducer, ping: pingReducer, + pingList: pingListReducer, ml: mlJobsReducer, monitorDuration: monitorDurationReducer, indexStatus: indexStatusReducer, diff --git a/x-pack/legacy/plugins/uptime/public/state/reducers/monitor_duration.ts b/x-pack/legacy/plugins/uptime/public/state/reducers/monitor_duration.ts index 0e1771c393e50..ec6b374c1057c 100644 --- a/x-pack/legacy/plugins/uptime/public/state/reducers/monitor_duration.ts +++ b/x-pack/legacy/plugins/uptime/public/state/reducers/monitor_duration.ts @@ -24,9 +24,9 @@ const initialState: MonitorDuration = { errors: [], }; -type PayLoad = MonitorDurationResult & Error; +type Payload = MonitorDurationResult & Error; -export const monitorDurationReducer = handleActions<MonitorDuration, PayLoad>( +export const monitorDurationReducer = handleActions<MonitorDuration, Payload>( { [String(getMonitorDurationAction)]: (state: MonitorDuration) => ({ ...state, diff --git a/x-pack/legacy/plugins/uptime/public/state/reducers/monitor_list.ts b/x-pack/legacy/plugins/uptime/public/state/reducers/monitor_list.ts new file mode 100644 index 0000000000000..cf895aebeb755 --- /dev/null +++ b/x-pack/legacy/plugins/uptime/public/state/reducers/monitor_list.ts @@ -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. + */ + +import { handleActions, Action } from 'redux-actions'; +import { getMonitorList, getMonitorListSuccess, getMonitorListFailure } from '../actions'; +import { MonitorSummaryResult } from '../../../common/runtime_types'; + +export interface MonitorList { + list: MonitorSummaryResult; + error?: Error; + loading: boolean; +} + +export const initialState: MonitorList = { + list: { + nextPagePagination: null, + prevPagePagination: null, + summaries: [], + totalSummaryCount: 0, + }, + loading: false, +}; + +type Payload = MonitorSummaryResult & Error; + +export const monitorListReducer = handleActions<MonitorList, Payload>( + { + [String(getMonitorList)]: (state: MonitorList) => ({ + ...state, + loading: true, + }), + [String(getMonitorListSuccess)]: ( + state: MonitorList, + action: Action<MonitorSummaryResult> + ) => ({ + ...state, + loading: false, + error: undefined, + list: { ...action.payload }, + }), + [String(getMonitorListFailure)]: (state: MonitorList, action: Action<Error>) => ({ + ...state, + error: action.payload, + loading: false, + }), + }, + initialState +); diff --git a/x-pack/legacy/plugins/uptime/public/state/reducers/monitor_status.ts b/x-pack/legacy/plugins/uptime/public/state/reducers/monitor_status.ts index 6cfaa9f8f59c1..a98e89a27a711 100644 --- a/x-pack/legacy/plugins/uptime/public/state/reducers/monitor_status.ts +++ b/x-pack/legacy/plugins/uptime/public/state/reducers/monitor_status.ts @@ -9,7 +9,7 @@ import { getMonitorStatusActionSuccess, getMonitorStatusActionFail, } from '../actions'; -import { Ping } from '../../../common/graphql/types'; +import { Ping } from '../../../common/runtime_types'; import { QueryParams } from '../actions/types'; export interface MonitorStatusState { @@ -26,7 +26,7 @@ type MonitorStatusPayload = QueryParams & Ping; export const monitorStatusReducer = handleActions<MonitorStatusState, MonitorStatusPayload>( { - [String(getMonitorStatusAction)]: (state, action: Action<QueryParams>) => ({ + [String(getMonitorStatusAction)]: state => ({ ...state, loading: true, }), @@ -43,7 +43,7 @@ export const monitorStatusReducer = handleActions<MonitorStatusState, MonitorSta }; }, - [String(getMonitorStatusActionFail)]: (state, action: Action<any>) => ({ + [String(getMonitorStatusActionFail)]: state => ({ ...state, loading: false, }), diff --git a/x-pack/legacy/plugins/uptime/public/state/reducers/ping.ts b/x-pack/legacy/plugins/uptime/public/state/reducers/ping.ts index 76775e6a0a355..4c8715038ce36 100644 --- a/x-pack/legacy/plugins/uptime/public/state/reducers/ping.ts +++ b/x-pack/legacy/plugins/uptime/public/state/reducers/ping.ts @@ -6,7 +6,7 @@ import { handleActions, Action } from 'redux-actions'; import { getPingHistogram, getPingHistogramSuccess, getPingHistogramFail } from '../actions'; -import { HistogramResult } from '../../../common/types'; +import { HistogramResult } from '../../../common/runtime_types'; export interface PingState { pingHistogram: HistogramResult | null; diff --git a/x-pack/legacy/plugins/uptime/public/state/reducers/ping_list.ts b/x-pack/legacy/plugins/uptime/public/state/reducers/ping_list.ts new file mode 100644 index 0000000000000..e3ccb1e663eda --- /dev/null +++ b/x-pack/legacy/plugins/uptime/public/state/reducers/ping_list.ts @@ -0,0 +1,48 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { handleActions, Action } from 'redux-actions'; +import { PingsResponse } from '../../../common/runtime_types'; +import { getPings, getPingsSuccess, getPingsFail } from '../actions'; + +export interface PingListState { + pingList: PingsResponse; + error?: Error; + loading: boolean; +} + +const initialState: PingListState = { + pingList: { + total: 0, + locations: [], + pings: [], + }, + loading: false, +}; + +type PingListPayload = PingsResponse & Error; + +export const pingListReducer = handleActions<PingListState, PingListPayload>( + { + [String(getPings)]: state => ({ + ...state, + loading: true, + }), + + [String(getPingsSuccess)]: (state, action: Action<PingsResponse>) => ({ + ...state, + pingList: { ...action.payload }, + loading: false, + }), + + [String(getPingsFail)]: (state, action: Action<Error>) => ({ + ...state, + error: action.payload, + loading: false, + }), + }, + initialState +); diff --git a/x-pack/legacy/plugins/uptime/public/state/reducers/ui.ts b/x-pack/legacy/plugins/uptime/public/state/reducers/ui.ts index 702d314250521..c533f293fc940 100644 --- a/x-pack/legacy/plugins/uptime/public/state/reducers/ui.ts +++ b/x-pack/legacy/plugins/uptime/public/state/reducers/ui.ts @@ -13,7 +13,7 @@ import { triggerAppRefresh, UiPayload, setAlertFlyoutVisible, -} from '../actions/ui'; +} from '../actions'; export interface UiState { alertFlyoutVisible: boolean; diff --git a/x-pack/legacy/plugins/uptime/public/state/selectors/__tests__/index.test.ts b/x-pack/legacy/plugins/uptime/public/state/selectors/__tests__/index.test.ts index 3b4547514a11e..2b7c04178e9b4 100644 --- a/x-pack/legacy/plugins/uptime/public/state/selectors/__tests__/index.test.ts +++ b/x-pack/legacy/plugins/uptime/public/state/selectors/__tests__/index.test.ts @@ -58,11 +58,28 @@ describe('state selectors', () => { loading: false, errors: [], }, + pingList: { + loading: false, + pingList: { + total: 0, + locations: [], + pings: [], + }, + }, monitorDuration: { durationLines: null, loading: false, errors: [], }, + monitorList: { + list: { + prevPagePagination: null, + nextPagePagination: null, + summaries: [], + totalSummaryCount: 0, + }, + loading: false, + }, ml: { mlJob: { data: null, diff --git a/x-pack/legacy/plugins/uptime/public/state/selectors/index.ts b/x-pack/legacy/plugins/uptime/public/state/selectors/index.ts index 0fc3c7151cb3b..7260c61f44147 100644 --- a/x-pack/legacy/plugins/uptime/public/state/selectors/index.ts +++ b/x-pack/legacy/plugins/uptime/public/state/selectors/index.ts @@ -41,6 +41,21 @@ export const selectPingHistogram = ({ ping, ui }: AppState) => { }; }; +export const selectPingList = ({ pingList, ui: { lastRefresh } }: AppState) => ({ + pings: pingList, + lastRefresh, +}); + +export const snapshotDataSelector = ({ + snapshot: { count, loading }, + ui: { lastRefresh, esKuery }, +}: AppState) => ({ + count, + lastRefresh, + loading, + esKuery, +}); + const mlCapabilitiesSelector = (state: AppState) => state.ml.mlCapabilities.data; export const hasMLFeatureAvailable = createSelector( @@ -87,3 +102,8 @@ export const selectMonitorStatusAlert = ({ indexPattern, overviewFilters, ui }: export const indexStatusSelector = ({ indexStatus }: AppState) => { return indexStatus.indexStatus; }; + +export const monitorListSelector = ({ monitorList, ui: { lastRefresh } }: AppState) => ({ + monitorList, + lastRefresh, +}); diff --git a/x-pack/legacy/plugins/uptime/public/uptime_app.tsx b/x-pack/legacy/plugins/uptime/public/uptime_app.tsx index dafb20dc9c323..92775a2663863 100644 --- a/x-pack/legacy/plugins/uptime/public/uptime_app.tsx +++ b/x-pack/legacy/plugins/uptime/public/uptime_app.tsx @@ -7,24 +7,25 @@ import { EuiPage, EuiErrorBoundary } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import React, { useEffect } from 'react'; -import { ApolloProvider } from 'react-apollo'; import { Provider as ReduxProvider } from 'react-redux'; import { BrowserRouter as Router } from 'react-router-dom'; import { I18nStart, ChromeBreadcrumb, CoreStart } from 'src/core/public'; import { PluginsSetup } from 'ui/new_platform/new_platform'; import { KibanaContextProvider } from '../../../../../src/plugins/kibana_react/public'; -import { UMGraphQLClient, UMUpdateBadge } from './lib/lib'; +import { UMUpdateBadge } from './lib/lib'; import { UptimeRefreshContextProvider, UptimeSettingsContextProvider, UptimeThemeContextProvider, } from './contexts'; -import { CommonlyUsedRange } from './components/functional/uptime_date_picker'; +import { CommonlyUsedRange } from './components/common/uptime_date_picker'; import { store } from './state'; import { setBasePath } from './state/actions'; import { PageRouter } from './routes'; -import { UptimeAlertsFlyoutWrapper } from './components/connected'; -import { UptimeAlertsContextProvider } from './components/functional/alerts'; +import { + UptimeAlertsContextProvider, + UptimeAlertsFlyoutWrapper, +} from './components/overview/alerts'; import { kibanaService } from './state/kibana_service'; export interface UptimeAppColors { @@ -39,7 +40,6 @@ export interface UptimeAppColors { export interface UptimeAppProps { basePath: string; canSave: boolean; - client: UMGraphQLClient; core: CoreStart; darkMode: boolean; i18n: I18nStart; @@ -59,7 +59,6 @@ const Application = (props: UptimeAppProps) => { const { basePath, canSave, - client, core, darkMode, i18n: i18nCore, @@ -97,25 +96,23 @@ const Application = (props: UptimeAppProps) => { <ReduxProvider store={store}> <KibanaContextProvider services={{ ...core, ...plugins }}> <Router basename={routerBasename}> - <ApolloProvider client={client}> - <UptimeRefreshContextProvider> - <UptimeSettingsContextProvider {...props}> - <UptimeThemeContextProvider darkMode={darkMode}> - <UptimeAlertsContextProvider> - <EuiPage className="app-wrapper-panel " data-test-subj="uptimeApp"> - <main> - <UptimeAlertsFlyoutWrapper - alertTypeId="xpack.uptime.alerts.monitorStatus" - canChangeTrigger={false} - /> - <PageRouter autocomplete={plugins.data.autocomplete} /> - </main> - </EuiPage> - </UptimeAlertsContextProvider> - </UptimeThemeContextProvider> - </UptimeSettingsContextProvider> - </UptimeRefreshContextProvider> - </ApolloProvider> + <UptimeRefreshContextProvider> + <UptimeSettingsContextProvider {...props}> + <UptimeThemeContextProvider darkMode={darkMode}> + <UptimeAlertsContextProvider> + <EuiPage className="app-wrapper-panel " data-test-subj="uptimeApp"> + <main> + <UptimeAlertsFlyoutWrapper + alertTypeId="xpack.uptime.alerts.monitorStatus" + canChangeTrigger={false} + /> + <PageRouter autocomplete={plugins.data.autocomplete} /> + </main> + </EuiPage> + </UptimeAlertsContextProvider> + </UptimeThemeContextProvider> + </UptimeSettingsContextProvider> + </UptimeRefreshContextProvider> </Router> </KibanaContextProvider> </ReduxProvider> diff --git a/x-pack/legacy/plugins/uptime/scripts/gql_gen.json b/x-pack/legacy/plugins/uptime/scripts/gql_gen.json deleted file mode 100644 index 87b8233dd1eeb..0000000000000 --- a/x-pack/legacy/plugins/uptime/scripts/gql_gen.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "flattenTypes": true, - "generatorConfig": {}, - "primitives": { - "String": "string", - "Int": "number", - "Float": "number", - "Boolean": "boolean", - "ID": "string" - } -} diff --git a/x-pack/legacy/plugins/uptime/scripts/infer_graphql_types.js b/x-pack/legacy/plugins/uptime/scripts/infer_graphql_types.js deleted file mode 100644 index 2499e15bf4e23..0000000000000 --- a/x-pack/legacy/plugins/uptime/scripts/infer_graphql_types.js +++ /dev/null @@ -1,45 +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. - */ - -require('../../../../../src/setup_node_env'); - -const { resolve } = require('path'); -// eslint-disable-next-line import/no-extraneous-dependencies, import/no-unresolved -const { generate } = require('graphql-code-generator'); - -const CONFIG_PATH = resolve(__dirname, 'gql_gen.json'); -const OUTPUT_INTROSPECTION_PATH = resolve('common', 'graphql', 'introspection.json'); -const OUTPUT_TYPES_PATH = resolve('common', 'graphql', 'types.ts'); -const SCHEMA_PATH = resolve(__dirname, 'graphql_schemas.ts'); - -async function main() { - await generate( - { - args: [], - config: CONFIG_PATH, - out: OUTPUT_INTROSPECTION_PATH, - overwrite: true, - schema: SCHEMA_PATH, - template: 'graphql-codegen-introspection-template', - }, - true - ); - await generate( - { - args: [], - config: CONFIG_PATH, - out: OUTPUT_TYPES_PATH, - overwrite: true, - schema: SCHEMA_PATH, - template: 'graphql-codegen-typescript-template', - }, - true - ); -} - -if (require.main === module) { - main(); -} diff --git a/x-pack/package.json b/x-pack/package.json index b2ec4c3dc3f6f..3c6146b491f60 100644 --- a/x-pack/package.json +++ b/x-pack/package.json @@ -85,7 +85,7 @@ "@types/node-fetch": "^2.5.0", "@types/nodemailer": "^6.2.1", "@types/object-hash": "^1.3.0", - "@types/papaparse": "^4.5.11", + "@types/papaparse": "^5.0.3", "@types/pngjs": "^3.3.2", "@types/prop-types": "^15.5.3", "@types/proper-lockfile": "^3.0.1", @@ -179,7 +179,7 @@ "@babel/core": "^7.9.0", "@babel/register": "^7.9.0", "@babel/runtime": "^7.9.2", - "@elastic/apm-rum-react": "^0.3.2", + "@elastic/apm-rum-react": "^1.1.1", "@elastic/datemath": "5.0.3", "@elastic/ems-client": "7.8.0", "@elastic/eui": "21.0.1", @@ -290,7 +290,7 @@ "oboe": "^2.1.4", "oppsy": "^2.0.0", "p-retry": "^4.2.0", - "papaparse": "^4.6.3", + "papaparse": "^5.2.0", "pdfmake": "^0.1.63", "pluralize": "3.1.0", "pngjs": "3.4.0", diff --git a/x-pack/plugins/actions/server/builtin_action_types/email.test.ts b/x-pack/plugins/actions/server/builtin_action_types/email.test.ts index 469df4fd86e2c..658f8f3fd8cf9 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/email.test.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/email.test.ts @@ -255,7 +255,14 @@ describe('execute()', () => { services, }; sendEmailMock.mockReset(); - await actionType.executor(executorOptions); + const result = await actionType.executor(executorOptions); + expect(result).toMatchInlineSnapshot(` + Object { + "actionId": "some-id", + "data": undefined, + "status": "ok", + } + `); expect(sendEmailMock.mock.calls[0][1]).toMatchInlineSnapshot(` Object { "content": Object { @@ -282,4 +289,102 @@ describe('execute()', () => { } `); }); + + test('parameters are as expected with no auth', async () => { + const config: ActionTypeConfigType = { + service: null, + host: 'a host', + port: 42, + secure: true, + from: 'bob@example.com', + }; + const secrets: ActionTypeSecretsType = { + user: null, + password: null, + }; + const params: ActionParamsType = { + to: ['jim@example.com'], + cc: ['james@example.com'], + bcc: ['jimmy@example.com'], + subject: 'the subject', + message: 'a message to you', + }; + + const actionId = 'some-id'; + const executorOptions: ActionTypeExecutorOptions = { + actionId, + config, + params, + secrets, + services, + }; + sendEmailMock.mockReset(); + await actionType.executor(executorOptions); + expect(sendEmailMock.mock.calls[0][1]).toMatchInlineSnapshot(` + Object { + "content": Object { + "message": "a message to you", + "subject": "the subject", + }, + "routing": Object { + "bcc": Array [ + "jimmy@example.com", + ], + "cc": Array [ + "james@example.com", + ], + "from": "bob@example.com", + "to": Array [ + "jim@example.com", + ], + }, + "transport": Object { + "host": "a host", + "port": 42, + "secure": true, + }, + } + `); + }); + + test('returns expected result when an error is thrown', async () => { + const config: ActionTypeConfigType = { + service: null, + host: 'a host', + port: 42, + secure: true, + from: 'bob@example.com', + }; + const secrets: ActionTypeSecretsType = { + user: null, + password: null, + }; + const params: ActionParamsType = { + to: ['jim@example.com'], + cc: ['james@example.com'], + bcc: ['jimmy@example.com'], + subject: 'the subject', + message: 'a message to you', + }; + + const actionId = 'some-id'; + const executorOptions: ActionTypeExecutorOptions = { + actionId, + config, + params, + secrets, + services, + }; + sendEmailMock.mockReset(); + sendEmailMock.mockRejectedValue(new Error('wops')); + const result = await actionType.executor(executorOptions); + expect(result).toMatchInlineSnapshot(` + Object { + "actionId": "some-id", + "message": "error sending email", + "serviceMessage": "wops", + "status": "error", + } + `); + }); }); diff --git a/x-pack/plugins/actions/server/builtin_action_types/email.ts b/x-pack/plugins/actions/server/builtin_action_types/email.ts index 7992920fdfcb4..ca8d089ad2946 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/email.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/email.ts @@ -9,7 +9,7 @@ import { i18n } from '@kbn/i18n'; import { schema, TypeOf } from '@kbn/config-schema'; import nodemailerGetService from 'nodemailer/lib/well-known'; -import { sendEmail, JSON_TRANSPORT_SERVICE } from './lib/send_email'; +import { sendEmail, JSON_TRANSPORT_SERVICE, SendEmailOptions, Transport } from './lib/send_email'; import { portSchema } from './lib/schemas'; import { Logger } from '../../../../../src/core/server'; import { ActionType, ActionTypeExecutorOptions, ActionTypeExecutorResult } from '../types'; @@ -143,7 +143,7 @@ async function executor( const secrets = execOptions.secrets as ActionTypeSecretsType; const params = execOptions.params as ActionParamsType; - const transport: any = {}; + const transport: Transport = {}; if (secrets.user != null) { transport.user = secrets.user; @@ -155,12 +155,13 @@ async function executor( if (config.service !== null) { transport.service = config.service; } else { - transport.host = config.host; - transport.port = config.port; + // already validated service or host/port is not null ... + transport.host = config.host!; + transport.port = config.port!; transport.secure = getSecureValue(config.secure, config.port); } - const sendEmailOptions = { + const sendEmailOptions: SendEmailOptions = { transport, routing: { from: config.from, diff --git a/x-pack/plugins/actions/server/builtin_action_types/lib/send_email.test.ts b/x-pack/plugins/actions/server/builtin_action_types/lib/send_email.test.ts new file mode 100644 index 0000000000000..42160dc2fc22b --- /dev/null +++ b/x-pack/plugins/actions/server/builtin_action_types/lib/send_email.test.ts @@ -0,0 +1,175 @@ +/* + * 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('nodemailer', () => ({ + createTransport: jest.fn(), +})); + +import { Logger } from '../../../../../../src/core/server'; +import { sendEmail } from './send_email'; +import { loggingServiceMock } from '../../../../../../src/core/server/mocks'; +import nodemailer from 'nodemailer'; + +const createTransportMock = nodemailer.createTransport as jest.Mock; +const sendMailMockResult = { result: 'does not matter' }; +const sendMailMock = jest.fn(); + +const mockLogger = loggingServiceMock.create().get() as jest.Mocked<Logger>; + +describe('send_email module', () => { + beforeEach(() => { + jest.resetAllMocks(); + createTransportMock.mockReturnValue({ sendMail: sendMailMock }); + sendMailMock.mockResolvedValue(sendMailMockResult); + }); + + test('handles authenticated email using service', async () => { + const sendEmailOptions = getSendEmailOptions(); + const result = await sendEmail(mockLogger, sendEmailOptions); + expect(result).toBe(sendMailMockResult); + expect(createTransportMock.mock.calls[0]).toMatchInlineSnapshot(` + Array [ + Object { + "auth": Object { + "pass": "changeme", + "user": "elastic", + }, + "service": "whatever", + }, + ] + `); + expect(sendMailMock.mock.calls[0]).toMatchInlineSnapshot(` + Array [ + Object { + "bcc": Array [], + "cc": Array [ + "bob@example.com", + "robert@example.com", + ], + "from": "fred@example.com", + "html": "<p>a message</p> + ", + "subject": "a subject", + "text": "a message", + "to": Array [ + "jim@example.com", + ], + }, + ] + `); + }); + + test('handles unauthenticated email using not secure host/port', async () => { + const sendEmailOptions = getSendEmailOptions(); + delete sendEmailOptions.transport.service; + delete sendEmailOptions.transport.user; + delete sendEmailOptions.transport.password; + sendEmailOptions.transport.host = 'example.com'; + sendEmailOptions.transport.port = 1025; + const result = await sendEmail(mockLogger, sendEmailOptions); + expect(result).toBe(sendMailMockResult); + expect(createTransportMock.mock.calls[0]).toMatchInlineSnapshot(` + Array [ + Object { + "host": "example.com", + "port": 1025, + "secure": false, + "tls": Object { + "rejectUnauthorized": false, + }, + }, + ] + `); + expect(sendMailMock.mock.calls[0]).toMatchInlineSnapshot(` + Array [ + Object { + "bcc": Array [], + "cc": Array [ + "bob@example.com", + "robert@example.com", + ], + "from": "fred@example.com", + "html": "<p>a message</p> + ", + "subject": "a subject", + "text": "a message", + "to": Array [ + "jim@example.com", + ], + }, + ] + `); + }); + + test('handles unauthenticated email using secure host/port', async () => { + const sendEmailOptions = getSendEmailOptions(); + delete sendEmailOptions.transport.service; + delete sendEmailOptions.transport.user; + delete sendEmailOptions.transport.password; + sendEmailOptions.transport.host = 'example.com'; + sendEmailOptions.transport.port = 1025; + sendEmailOptions.transport.secure = true; + const result = await sendEmail(mockLogger, sendEmailOptions); + expect(result).toBe(sendMailMockResult); + expect(createTransportMock.mock.calls[0]).toMatchInlineSnapshot(` + Array [ + Object { + "host": "example.com", + "port": 1025, + "secure": true, + }, + ] + `); + expect(sendMailMock.mock.calls[0]).toMatchInlineSnapshot(` + Array [ + Object { + "bcc": Array [], + "cc": Array [ + "bob@example.com", + "robert@example.com", + ], + "from": "fred@example.com", + "html": "<p>a message</p> + ", + "subject": "a subject", + "text": "a message", + "to": Array [ + "jim@example.com", + ], + }, + ] + `); + }); + + test('passes nodemailer exceptions to caller', async () => { + const sendEmailOptions = getSendEmailOptions(); + + sendMailMock.mockReset(); + sendMailMock.mockRejectedValue(new Error('wops')); + + await expect(sendEmail(mockLogger, sendEmailOptions)).rejects.toThrow('wops'); + }); +}); + +function getSendEmailOptions(): any { + return { + content: { + message: 'a message', + subject: 'a subject', + }, + routing: { + from: 'fred@example.com', + to: ['jim@example.com'], + cc: ['bob@example.com', 'robert@example.com'], + bcc: [], + }, + transport: { + service: 'whatever', + user: 'elastic', + password: 'changeme', + }, + }; +} diff --git a/x-pack/plugins/actions/server/builtin_action_types/lib/send_email.ts b/x-pack/plugins/actions/server/builtin_action_types/lib/send_email.ts index 47d7aff8022ce..ffbf7485a8b0b 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/lib/send_email.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/lib/send_email.ts @@ -14,30 +14,30 @@ import { Logger } from '../../../../../../src/core/server'; // an email "service" which doesn't actually send, just returns what it would send export const JSON_TRANSPORT_SERVICE = '__json'; -interface SendEmailOptions { +export interface SendEmailOptions { transport: Transport; routing: Routing; content: Content; } // config validation ensures either service is set or host/port are set -interface Transport { - user: string; - password: string; +export interface Transport { + user?: string; + password?: string; service?: string; // see: https://nodemailer.com/smtp/well-known/ host?: string; port?: number; secure?: boolean; // see: https://nodemailer.com/smtp/#tls-options } -interface Routing { +export interface Routing { from: string; to: string[]; cc: string[]; bcc: string[]; } -interface Content { +export interface Content { subject: string; message: string; } @@ -49,12 +49,14 @@ export async function sendEmail(logger: Logger, options: SendEmailOptions): Prom const { from, to, cc, bcc } = routing; const { subject, message } = content; - const transportConfig: Record<string, any> = { - auth: { + const transportConfig: Record<string, any> = {}; + + if (user != null && password != null) { + transportConfig.auth = { user, pass: password, - }, - }; + }; + } if (service === JSON_TRANSPORT_SERVICE) { transportConfig.jsonTransport = true; diff --git a/x-pack/plugins/actions/server/builtin_action_types/servicenow/lib/index.ts b/x-pack/plugins/actions/server/builtin_action_types/servicenow/lib/index.ts index 1acb6c563801c..cc07a0b90330d 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/servicenow/lib/index.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/servicenow/lib/index.ts @@ -147,7 +147,14 @@ class ServiceNow { comments: Comment[], field: string ): Promise<CommentResponse[]> { - const res = await Promise.all(comments.map(c => this.createComment(incidentId, c, field))); + // Create comments sequentially. + const promises = comments.reduce(async (prevPromise, currentComment) => { + const totalComments = await prevPromise; + const res = await this.createComment(incidentId, currentComment, field); + return [...totalComments, res]; + }, Promise.resolve([] as CommentResponse[])); + + const res = await promises; return res; } diff --git a/x-pack/plugins/actions/server/create_execute_function.test.ts b/x-pack/plugins/actions/server/create_execute_function.test.ts index 68c3967359ff4..6bdd30848e4b7 100644 --- a/x-pack/plugins/actions/server/create_execute_function.test.ts +++ b/x-pack/plugins/actions/server/create_execute_function.test.ts @@ -23,6 +23,7 @@ describe('execute()', () => { actionTypeRegistry: actionTypeRegistryMock.create(), getScopedSavedObjectsClient: jest.fn().mockReturnValueOnce(savedObjectsClient), isESOUsingEphemeralEncryptionKey: false, + preconfiguredActions: [], }); savedObjectsClient.get.mockResolvedValueOnce({ id: '123', @@ -68,6 +69,68 @@ describe('execute()', () => { }); }); + test('schedules the action with all given parameters with a preconfigured action', async () => { + const executeFn = createExecuteFunction({ + getBasePath, + taskManager: mockTaskManager, + actionTypeRegistry: actionTypeRegistryMock.create(), + getScopedSavedObjectsClient: jest.fn().mockReturnValueOnce(savedObjectsClient), + isESOUsingEphemeralEncryptionKey: false, + preconfiguredActions: [ + { + id: '123', + actionTypeId: 'mock-action-preconfigured', + config: {}, + isPreconfigured: true, + name: 'x', + secrets: {}, + }, + ], + }); + savedObjectsClient.get.mockResolvedValueOnce({ + id: '123', + type: 'action', + attributes: { + actionTypeId: 'mock-action', + }, + references: [], + }); + savedObjectsClient.create.mockResolvedValueOnce({ + id: '234', + type: 'action_task_params', + attributes: {}, + references: [], + }); + await executeFn({ + id: '123', + params: { baz: false }, + spaceId: 'default', + apiKey: Buffer.from('123:abc').toString('base64'), + }); + expect(mockTaskManager.schedule).toHaveBeenCalledTimes(1); + expect(mockTaskManager.schedule.mock.calls[0]).toMatchInlineSnapshot(` + Array [ + Object { + "params": Object { + "actionTaskParamsId": "234", + "spaceId": "default", + }, + "scope": Array [ + "actions", + ], + "state": Object {}, + "taskType": "actions:mock-action-preconfigured", + }, + ] + `); + expect(savedObjectsClient.get).not.toHaveBeenCalled(); + expect(savedObjectsClient.create).toHaveBeenCalledWith('action_task_params', { + actionId: '123', + params: { baz: false }, + apiKey: Buffer.from('123:abc').toString('base64'), + }); + }); + test('uses API key when provided', async () => { const getScopedSavedObjectsClient = jest.fn().mockReturnValueOnce(savedObjectsClient); const executeFn = createExecuteFunction({ @@ -76,6 +139,7 @@ describe('execute()', () => { getScopedSavedObjectsClient, isESOUsingEphemeralEncryptionKey: false, actionTypeRegistry: actionTypeRegistryMock.create(), + preconfiguredActions: [], }); savedObjectsClient.get.mockResolvedValueOnce({ id: '123', @@ -125,6 +189,7 @@ describe('execute()', () => { getScopedSavedObjectsClient, isESOUsingEphemeralEncryptionKey: false, actionTypeRegistry: actionTypeRegistryMock.create(), + preconfiguredActions: [], }); savedObjectsClient.get.mockResolvedValueOnce({ id: '123', @@ -171,6 +236,7 @@ describe('execute()', () => { getScopedSavedObjectsClient, isESOUsingEphemeralEncryptionKey: true, actionTypeRegistry: actionTypeRegistryMock.create(), + preconfiguredActions: [], }); await expect( executeFn({ @@ -193,6 +259,7 @@ describe('execute()', () => { getScopedSavedObjectsClient, isESOUsingEphemeralEncryptionKey: false, actionTypeRegistry: mockedActionTypeRegistry, + preconfiguredActions: [], }); mockedActionTypeRegistry.ensureActionTypeEnabled.mockImplementation(() => { throw new Error('Fail'); diff --git a/x-pack/plugins/actions/server/create_execute_function.ts b/x-pack/plugins/actions/server/create_execute_function.ts index 4bbcda4cba7fc..4a9ddf412b7cc 100644 --- a/x-pack/plugins/actions/server/create_execute_function.ts +++ b/x-pack/plugins/actions/server/create_execute_function.ts @@ -6,7 +6,12 @@ import { SavedObjectsClientContract } from '../../../../src/core/server'; import { TaskManagerStartContract } from '../../task_manager/server'; -import { GetBasePathFunction, RawAction, ActionTypeRegistryContract } from './types'; +import { + GetBasePathFunction, + RawAction, + ActionTypeRegistryContract, + PreConfiguredAction, +} from './types'; interface CreateExecuteFunctionOptions { taskManager: TaskManagerStartContract; @@ -14,6 +19,7 @@ interface CreateExecuteFunctionOptions { getBasePath: GetBasePathFunction; isESOUsingEphemeralEncryptionKey: boolean; actionTypeRegistry: ActionTypeRegistryContract; + preconfiguredActions: PreConfiguredAction[]; } export interface ExecuteOptions { @@ -29,6 +35,7 @@ export function createExecuteFunction({ actionTypeRegistry, getScopedSavedObjectsClient, isESOUsingEphemeralEncryptionKey, + preconfiguredActions, }: CreateExecuteFunctionOptions) { return async function execute({ id, params, spaceId, apiKey }: ExecuteOptions) { if (isESOUsingEphemeralEncryptionKey === true) { @@ -61,9 +68,9 @@ export function createExecuteFunction({ }; const savedObjectsClient = getScopedSavedObjectsClient(fakeRequest); - const actionSavedObject = await savedObjectsClient.get<RawAction>('action', id); + const actionTypeId = await getActionTypeId(id); - actionTypeRegistry.ensureActionTypeEnabled(actionSavedObject.attributes.actionTypeId); + actionTypeRegistry.ensureActionTypeEnabled(actionTypeId); const actionTaskParamsRecord = await savedObjectsClient.create('action_task_params', { actionId: id, @@ -72,7 +79,7 @@ export function createExecuteFunction({ }); await taskManager.schedule({ - taskType: `actions:${actionSavedObject.attributes.actionTypeId}`, + taskType: `actions:${actionTypeId}`, params: { spaceId, actionTaskParamsId: actionTaskParamsRecord.id, @@ -80,5 +87,15 @@ export function createExecuteFunction({ state: {}, scope: ['actions'], }); + + async function getActionTypeId(actionId: string): Promise<string> { + const pcAction = preconfiguredActions.find(action => action.id === actionId); + if (pcAction) { + return pcAction.actionTypeId; + } + + const actionSO = await savedObjectsClient.get<RawAction>('action', actionId); + return actionSO.attributes.actionTypeId; + } }; } diff --git a/x-pack/plugins/actions/server/lib/action_executor.test.ts b/x-pack/plugins/actions/server/lib/action_executor.test.ts index bbcb0457fc1d1..124e5951c714b 100644 --- a/x-pack/plugins/actions/server/lib/action_executor.test.ts +++ b/x-pack/plugins/actions/server/lib/action_executor.test.ts @@ -43,6 +43,7 @@ actionExecutor.initialize({ actionTypeRegistry, encryptedSavedObjectsPlugin, eventLogger: eventLoggerMock.create(), + preconfiguredActions: [], }); beforeEach(() => { @@ -232,6 +233,7 @@ test('throws an error when passing isESOUsingEphemeralEncryptionKey with value o actionTypeRegistry, encryptedSavedObjectsPlugin, eventLogger: eventLoggerMock.create(), + preconfiguredActions: [], }); await expect( customActionExecutor.execute(executeParams) diff --git a/x-pack/plugins/actions/server/lib/action_executor.ts b/x-pack/plugins/actions/server/lib/action_executor.ts index af0353247d99f..a33fb8830a930 100644 --- a/x-pack/plugins/actions/server/lib/action_executor.ts +++ b/x-pack/plugins/actions/server/lib/action_executor.ts @@ -11,6 +11,8 @@ import { ActionTypeRegistryContract, GetServicesFunction, RawAction, + PreConfiguredAction, + Services, } from '../types'; import { EncryptedSavedObjectsPluginStart } from '../../../encrypted_saved_objects/server'; import { SpacesServiceSetup } from '../../../spaces/server'; @@ -24,6 +26,7 @@ export interface ActionExecutorContext { encryptedSavedObjectsPlugin: EncryptedSavedObjectsPluginStart; actionTypeRegistry: ActionTypeRegistryContract; eventLogger: IEventLogger; + preconfiguredActions: PreConfiguredAction[]; } export interface ExecuteOptions { @@ -72,29 +75,22 @@ export class ActionExecutor { encryptedSavedObjectsPlugin, actionTypeRegistry, eventLogger, + preconfiguredActions, } = this.actionExecutorContext!; const services = getServices(request); - const namespace = spaces && spaces.getSpaceId(request); + const spaceId = spaces && spaces.getSpaceId(request); + const namespace = spaceId && spaceId !== 'default' ? { namespace: spaceId } : {}; - // Ensure user can read the action before processing - const { - attributes: { actionTypeId, config, name }, - } = await services.savedObjectsClient.get<RawAction>('action', actionId); - - actionTypeRegistry.ensureActionTypeEnabled(actionTypeId); - - // Only get encrypted attributes here, the remaining attributes can be fetched in - // the savedObjectsClient call - const { - attributes: { secrets }, - } = await encryptedSavedObjectsPlugin.getDecryptedAsInternalUser<RawAction>( - 'action', + const { actionTypeId, name, config, secrets } = await getActionInfo( + services, + encryptedSavedObjectsPlugin, + preconfiguredActions, actionId, - { - namespace: namespace === 'default' ? undefined : namespace, - } + namespace.namespace ); + + actionTypeRegistry.ensureActionTypeEnabled(actionTypeId); const actionType = actionTypeRegistry.get(actionTypeId); let validatedParams: Record<string, any>; @@ -112,7 +108,7 @@ export class ActionExecutor { const actionLabel = `${actionTypeId}:${actionId}: ${name}`; const event: IEvent = { event: { action: EVENT_LOG_ACTIONS.execute }, - kibana: { namespace, saved_objects: [{ type: 'action', id: actionId }] }, + kibana: { saved_objects: [{ type: 'action', id: actionId, ...namespace }] }, }; eventLogger.startTiming(event); @@ -174,3 +170,50 @@ function actionErrorToMessage(result: ActionTypeExecutorResult): string { return message; } + +interface ActionInfo { + actionTypeId: string; + name: string; + config: any; + secrets: any; +} + +async function getActionInfo( + services: Services, + encryptedSavedObjectsPlugin: EncryptedSavedObjectsPluginStart, + preconfiguredActions: PreConfiguredAction[], + actionId: string, + namespace: string | undefined +): Promise<ActionInfo> { + // check to see if it's a pre-configured action first + const pcAction = preconfiguredActions.find( + preconfiguredAction => preconfiguredAction.id === actionId + ); + if (pcAction) { + return { + actionTypeId: pcAction.actionTypeId, + name: pcAction.name, + config: pcAction.config, + secrets: pcAction.secrets, + }; + } + + // if not pre-configured action, should be a saved object + // ensure user can read the action before processing + const { + attributes: { actionTypeId, config, name }, + } = await services.savedObjectsClient.get<RawAction>('action', actionId); + + const { + attributes: { secrets }, + } = await encryptedSavedObjectsPlugin.getDecryptedAsInternalUser<RawAction>('action', actionId, { + namespace: namespace === 'default' ? undefined : namespace, + }); + + return { + actionTypeId, + name, + config, + secrets, + }; +} diff --git a/x-pack/plugins/actions/server/lib/task_runner_factory.test.ts b/x-pack/plugins/actions/server/lib/task_runner_factory.test.ts index 43882cef21170..f070f714ee508 100644 --- a/x-pack/plugins/actions/server/lib/task_runner_factory.test.ts +++ b/x-pack/plugins/actions/server/lib/task_runner_factory.test.ts @@ -61,6 +61,7 @@ const actionExecutorInitializerParams = { actionTypeRegistry, encryptedSavedObjectsPlugin: mockedEncryptedSavedObjectsPlugin, eventLogger: eventLoggerMock.create(), + preconfiguredActions: [], }; const taskRunnerFactoryInitializerParams = { spaceIdToNamespace, diff --git a/x-pack/plugins/actions/server/plugin.test.ts b/x-pack/plugins/actions/server/plugin.test.ts index 6215b08df81d4..fa5b2f9399a4d 100644 --- a/x-pack/plugins/actions/server/plugin.test.ts +++ b/x-pack/plugins/actions/server/plugin.test.ts @@ -99,6 +99,9 @@ describe('Actions Plugin', () => { savedObjects: { client: {}, }, + elasticsearch: { + adminClient: jest.fn(), + }, }, } as any, httpServerMock.createKibanaRequest(), diff --git a/x-pack/plugins/actions/server/plugin.ts b/x-pack/plugins/actions/server/plugin.ts index 34c9e7aa9e8b8..a8ab3bbb2fad2 100644 --- a/x-pack/plugins/actions/server/plugin.ts +++ b/x-pack/plugins/actions/server/plugin.ts @@ -11,13 +11,13 @@ import { Plugin, CoreSetup, CoreStart, - IClusterClient, KibanaRequest, Logger, SharedGlobalConfig, RequestHandler, IContextProvider, SavedObjectsServiceStart, + ElasticsearchServiceStart, } from '../../../../src/core/server'; import { @@ -89,7 +89,6 @@ export class ActionsPlugin implements Plugin<Promise<PluginSetupContract>, Plugi private readonly logger: Logger; private serverBasePath?: string; - private adminClient?: IClusterClient; private taskRunnerFactory?: TaskRunnerFactory; private actionTypeRegistry?: ActionTypeRegistry; private actionExecutor?: ActionExecutor; @@ -173,7 +172,6 @@ export class ActionsPlugin implements Plugin<Promise<PluginSetupContract>, Plugi this.actionTypeRegistry = actionTypeRegistry; this.serverBasePath = core.http.basePath.serverBasePath; this.actionExecutor = actionExecutor; - this.adminClient = core.elasticsearch.adminClient; this.spaces = plugins.spaces?.spacesService; registerBuiltInActionTypes({ @@ -233,7 +231,6 @@ export class ActionsPlugin implements Plugin<Promise<PluginSetupContract>, Plugi actionTypeRegistry, taskRunnerFactory, kibanaIndex, - adminClient, isESOUsingEphemeralEncryptionKey, preconfiguredActions, } = this; @@ -242,9 +239,10 @@ export class ActionsPlugin implements Plugin<Promise<PluginSetupContract>, Plugi logger, eventLogger: this.eventLogger!, spaces: this.spaces, - getServices: this.getServicesFactory(core.savedObjects), + getServices: this.getServicesFactory(core.savedObjects, core.elasticsearch), encryptedSavedObjectsPlugin: plugins.encryptedSavedObjects, actionTypeRegistry: actionTypeRegistry!, + preconfiguredActions, }); taskRunnerFactory!.initialize({ @@ -265,6 +263,7 @@ export class ActionsPlugin implements Plugin<Promise<PluginSetupContract>, Plugi getScopedSavedObjectsClient: core.savedObjects.getScopedClient, getBasePath: this.getBasePath, isESOUsingEphemeralEncryptionKey: isESOUsingEphemeralEncryptionKey!, + preconfiguredActions, }), isActionTypeEnabled: id => { return this.actionTypeRegistry!.isActionTypeEnabled(id); @@ -280,7 +279,7 @@ export class ActionsPlugin implements Plugin<Promise<PluginSetupContract>, Plugi savedObjectsClient: core.savedObjects.getScopedClient(request), actionTypeRegistry: actionTypeRegistry!, defaultKibanaIndex: await kibanaIndex, - scopedClusterClient: adminClient!.asScoped(request), + scopedClusterClient: core.elasticsearch.legacy.client.asScoped(request), preconfiguredActions, }); }, @@ -289,11 +288,11 @@ export class ActionsPlugin implements Plugin<Promise<PluginSetupContract>, Plugi } private getServicesFactory( - savedObjects: SavedObjectsServiceStart + savedObjects: SavedObjectsServiceStart, + elasticsearch: ElasticsearchServiceStart ): (request: KibanaRequest) => Services { - const { adminClient } = this; return request => ({ - callCluster: adminClient!.asScoped(request).callAsCurrentUser, + callCluster: elasticsearch.legacy.client.asScoped(request).callAsCurrentUser, savedObjectsClient: savedObjects.getScopedClient(request), }); } @@ -301,12 +300,8 @@ export class ActionsPlugin implements Plugin<Promise<PluginSetupContract>, Plugi private createRouteHandlerContext = ( defaultKibanaIndex: string ): IContextProvider<RequestHandler<any, any, any>, 'actions'> => { - const { - actionTypeRegistry, - adminClient, - isESOUsingEphemeralEncryptionKey, - preconfiguredActions, - } = this; + const { actionTypeRegistry, isESOUsingEphemeralEncryptionKey, preconfiguredActions } = this; + return async function actionsRouteHandlerContext(context, request) { return { getActionsClient: () => { @@ -319,7 +314,7 @@ export class ActionsPlugin implements Plugin<Promise<PluginSetupContract>, Plugi savedObjectsClient: context.core.savedObjects.client, actionTypeRegistry: actionTypeRegistry!, defaultKibanaIndex, - scopedClusterClient: adminClient!.asScoped(request), + scopedClusterClient: context.core.elasticsearch.adminClient, preconfiguredActions, }); }, diff --git a/x-pack/plugins/actions/server/usage/task.ts b/x-pack/plugins/actions/server/usage/task.ts index a07a2aa8f1c70..ed0d876ed0208 100644 --- a/x-pack/plugins/actions/server/usage/task.ts +++ b/x-pack/plugins/actions/server/usage/task.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { Logger, CoreSetup } from 'kibana/server'; +import { Logger, CoreSetup, APICaller } from 'kibana/server'; import moment from 'moment'; import { RunContext, @@ -62,7 +62,11 @@ async function scheduleTasks(logger: Logger, taskManager: TaskManagerStartContra export function telemetryTaskRunner(logger: Logger, core: CoreSetup, kibanaIndex: string) { return ({ taskInstance }: RunContext) => { const { state } = taskInstance; - const callCluster = core.elasticsearch.adminClient.callAsInternalUser; + const callCluster = (...args: Parameters<APICaller>) => { + return core.getStartServices().then(([{ elasticsearch: { legacy: { client } } }]) => + client.callAsInternalUser(...args) + ); + }; return { async run() { return Promise.all([ diff --git a/x-pack/plugins/alerting/server/alerts_client.ts b/x-pack/plugins/alerting/server/alerts_client.ts index 6f8478df58a53..3e4c26d3444c9 100644 --- a/x-pack/plugins/alerting/server/alerts_client.ts +++ b/x-pack/plugins/alerting/server/alerts_client.ts @@ -34,6 +34,7 @@ import { import { EncryptedSavedObjectsPluginStart } from '../../../plugins/encrypted_saved_objects/server'; import { TaskManagerStartContract } from '../../../plugins/task_manager/server'; import { taskInstanceToAlertTaskInstance } from './task_runner/alert_task_instance'; +import { deleteTaskIfItExists } from './lib/delete_task_if_it_exists'; type NormalizedAlertAction = Omit<AlertAction, 'actionTypeId'>; export type CreateAPIKeyResult = @@ -268,7 +269,7 @@ export class AlertsClient { const removeResult = await this.savedObjectsClient.delete('alert', id); await Promise.all([ - taskIdToRemove ? this.taskManager.remove(taskIdToRemove) : null, + taskIdToRemove ? deleteTaskIfItExists(this.taskManager, taskIdToRemove) : null, apiKeyToInvalidate ? this.invalidateApiKey({ apiKey: apiKeyToInvalidate }) : null, ]); @@ -510,7 +511,9 @@ export class AlertsClient { ); await Promise.all([ - attributes.scheduledTaskId ? this.taskManager.remove(attributes.scheduledTaskId) : null, + attributes.scheduledTaskId + ? deleteTaskIfItExists(this.taskManager, attributes.scheduledTaskId) + : null, apiKeyToInvalidate ? this.invalidateApiKey({ apiKey: apiKeyToInvalidate }) : null, ]); } diff --git a/x-pack/plugins/alerting/server/lib/delete_task_if_it_exists.test.ts b/x-pack/plugins/alerting/server/lib/delete_task_if_it_exists.test.ts new file mode 100644 index 0000000000000..84a1743387c9c --- /dev/null +++ b/x-pack/plugins/alerting/server/lib/delete_task_if_it_exists.test.ts @@ -0,0 +1,44 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import uuid from 'uuid'; +import { taskManagerMock } from '../../../task_manager/server/mocks'; +import { SavedObjectsErrorHelpers } from '../../../../../src/core/server'; +import { deleteTaskIfItExists } from './delete_task_if_it_exists'; + +describe('deleteTaskIfItExists', () => { + test('removes the task by its ID', async () => { + const tm = taskManagerMock.createStart(); + const id = uuid.v4(); + + expect(await deleteTaskIfItExists(tm, id)).toBe(undefined); + + expect(tm.remove).toHaveBeenCalledWith(id); + }); + + test('handles 404 errors caused by the task not existing', async () => { + const tm = taskManagerMock.createStart(); + const id = uuid.v4(); + + tm.remove.mockRejectedValue(SavedObjectsErrorHelpers.createGenericNotFoundError('task', id)); + + expect(await deleteTaskIfItExists(tm, id)).toBe(undefined); + + expect(tm.remove).toHaveBeenCalledWith(id); + }); + + test('throws if any other errro is caused by task removal', async () => { + const tm = taskManagerMock.createStart(); + const id = uuid.v4(); + + const error = SavedObjectsErrorHelpers.createInvalidVersionError(uuid.v4()); + tm.remove.mockRejectedValue(error); + + expect(deleteTaskIfItExists(tm, id)).rejects.toBe(error); + + expect(tm.remove).toHaveBeenCalledWith(id); + }); +}); diff --git a/x-pack/plugins/alerting/server/lib/delete_task_if_it_exists.ts b/x-pack/plugins/alerting/server/lib/delete_task_if_it_exists.ts new file mode 100644 index 0000000000000..53bb1b5cb5d53 --- /dev/null +++ b/x-pack/plugins/alerting/server/lib/delete_task_if_it_exists.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 { TaskManagerStartContract } from '../../../task_manager/server'; +import { SavedObjectsErrorHelpers } from '../../../../../src/core/server'; + +export async function deleteTaskIfItExists(taskManager: TaskManagerStartContract, taskId: string) { + try { + await taskManager.remove(taskId); + } catch (err) { + if (!SavedObjectsErrorHelpers.isNotFoundError(err)) { + throw err; + } + } +} diff --git a/x-pack/plugins/alerting/server/lib/is_alert_not_found_error.test.ts b/x-pack/plugins/alerting/server/lib/is_alert_not_found_error.test.ts new file mode 100644 index 0000000000000..46ceee3ce420b --- /dev/null +++ b/x-pack/plugins/alerting/server/lib/is_alert_not_found_error.test.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 { isAlertSavedObjectNotFoundError } from './is_alert_not_found_error'; +import { SavedObjectsErrorHelpers } from '../../../../../src/core/server'; +import uuid from 'uuid'; + +describe('isAlertSavedObjectNotFoundError', () => { + test('identifies SavedObjects Not Found errors', () => { + const id = uuid.v4(); + // ensure the error created by SO parses as a string with the format we expect + expect( + `${SavedObjectsErrorHelpers.createGenericNotFoundError('alert', id)}`.includes(`alert/${id}`) + ).toBe(true); + + const errorBySavedObjectsHelper = SavedObjectsErrorHelpers.createGenericNotFoundError( + 'alert', + id + ); + + expect(isAlertSavedObjectNotFoundError(errorBySavedObjectsHelper, id)).toBe(true); + }); + + test('identifies generic errors', () => { + const id = uuid.v4(); + expect(isAlertSavedObjectNotFoundError(new Error(`not found`), id)).toBe(false); + }); +}); diff --git a/x-pack/plugins/alerting/server/lib/is_alert_not_found_error.ts b/x-pack/plugins/alerting/server/lib/is_alert_not_found_error.ts new file mode 100644 index 0000000000000..0aa83ad0e883c --- /dev/null +++ b/x-pack/plugins/alerting/server/lib/is_alert_not_found_error.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 { SavedObjectsErrorHelpers } from '../../../../../src/core/server'; + +export function isAlertSavedObjectNotFoundError(err: Error, alertId: string) { + return SavedObjectsErrorHelpers.isNotFoundError(err) && `${err}`.includes(alertId); +} diff --git a/x-pack/plugins/alerting/server/mocks.ts b/x-pack/plugins/alerting/server/mocks.ts index 55ad722dcf881..a9e224142a632 100644 --- a/x-pack/plugins/alerting/server/mocks.ts +++ b/x-pack/plugins/alerting/server/mocks.ts @@ -6,6 +6,8 @@ import { alertsClientMock } from './alerts_client.mock'; import { PluginSetupContract, PluginStartContract } from './plugin'; +import { savedObjectsClientMock } from '../../../../src/core/server/mocks'; +import { AlertInstance } from './alert_instance'; export { alertsClientMock }; @@ -24,7 +26,44 @@ const createStartMock = () => { return mock; }; +export type AlertInstanceMock = jest.Mocked<AlertInstance>; +const createAlertInstanceFactoryMock = () => { + const mock = { + hasScheduledActions: jest.fn(), + isThrottled: jest.fn(), + getScheduledActionOptions: jest.fn(), + unscheduleActions: jest.fn(), + getState: jest.fn(), + scheduleActions: jest.fn(), + replaceState: jest.fn(), + updateLastScheduledActions: jest.fn(), + toJSON: jest.fn(), + toRaw: jest.fn(), + }; + + // support chaining + mock.replaceState.mockReturnValue(mock); + mock.unscheduleActions.mockReturnValue(mock); + mock.scheduleActions.mockReturnValue(mock); + + return (mock as unknown) as AlertInstanceMock; +}; + +const createAlertServicesMock = () => { + const alertInstanceFactoryMock = createAlertInstanceFactoryMock(); + return { + alertInstanceFactory: jest + .fn<jest.Mocked<AlertInstance>, [string]>() + .mockReturnValue(alertInstanceFactoryMock), + callCluster: jest.fn(), + savedObjectsClient: savedObjectsClientMock.create(), + }; +}; +export type AlertServicesMock = ReturnType<typeof createAlertServicesMock>; + export const alertsMock = { + createAlertInstanceFactory: createAlertInstanceFactoryMock, createSetup: createSetupMock, createStart: createStartMock, + createAlertServices: createAlertServicesMock, }; diff --git a/x-pack/plugins/alerting/server/plugin.ts b/x-pack/plugins/alerting/server/plugin.ts index fdca6c0a9b503..ad39d09bd6d3d 100644 --- a/x-pack/plugins/alerting/server/plugin.ts +++ b/x-pack/plugins/alerting/server/plugin.ts @@ -19,7 +19,6 @@ import { TaskRunnerFactory } from './task_runner'; import { AlertsClientFactory } from './alerts_client_factory'; import { LicenseState } from './lib/license_state'; import { - IClusterClient, KibanaRequest, Logger, PluginInitializerContext, @@ -29,6 +28,7 @@ import { IContextProvider, RequestHandler, SharedGlobalConfig, + ElasticsearchServiceStart, } from '../../../../src/core/server'; import { @@ -94,7 +94,6 @@ export class AlertingPlugin { private readonly logger: Logger; private alertTypeRegistry?: AlertTypeRegistry; private readonly taskRunnerFactory: TaskRunnerFactory; - private adminClient?: IClusterClient; private serverBasePath?: string; private licenseState: LicenseState | null = null; private isESOUsingEphemeralEncryptionKey?: boolean; @@ -119,7 +118,6 @@ export class AlertingPlugin { } public async setup(core: CoreSetup, plugins: AlertingPluginsSetup): Promise<PluginSetupContract> { - this.adminClient = core.elasticsearch.adminClient; this.licenseState = new LicenseState(plugins.licensing.license$); this.spaces = plugins.spaces?.spacesService; this.security = plugins.security; @@ -223,7 +221,7 @@ export class AlertingPlugin { taskRunnerFactory.initialize({ logger, - getServices: this.getServicesFactory(core.savedObjects), + getServices: this.getServicesFactory(core.savedObjects, core.elasticsearch), spaceIdToNamespace: this.spaceIdToNamespace, actionsPlugin: plugins.actions, encryptedSavedObjectsPlugin: plugins.encryptedSavedObjects, @@ -263,11 +261,11 @@ export class AlertingPlugin { }; private getServicesFactory( - savedObjects: SavedObjectsServiceStart + savedObjects: SavedObjectsServiceStart, + elasticsearch: ElasticsearchServiceStart ): (request: KibanaRequest) => Services { - const { adminClient } = this; return request => ({ - callCluster: adminClient!.asScoped(request).callAsCurrentUser, + callCluster: elasticsearch.legacy.client.asScoped(request).callAsCurrentUser, savedObjectsClient: savedObjects.getScopedClient(request), }); } diff --git a/x-pack/plugins/alerting/server/task_runner/create_execution_handler.test.ts b/x-pack/plugins/alerting/server/task_runner/create_execution_handler.test.ts index 8d037a1ecee91..756080baba626 100644 --- a/x-pack/plugins/alerting/server/task_runner/create_execution_handler.test.ts +++ b/x-pack/plugins/alerting/server/task_runner/create_execution_handler.test.ts @@ -91,7 +91,6 @@ test('calls actionsPlugin.execute per selected action', async () => { "alerting": Object { "instance_id": "2", }, - "namespace": "default", "saved_objects": Array [ Object { "id": "1", diff --git a/x-pack/plugins/alerting/server/task_runner/create_execution_handler.ts b/x-pack/plugins/alerting/server/task_runner/create_execution_handler.ts index de06c8bbb374a..72f9e70905dc2 100644 --- a/x-pack/plugins/alerting/server/task_runner/create_execution_handler.ts +++ b/x-pack/plugins/alerting/server/task_runner/create_execution_handler.ts @@ -87,16 +87,17 @@ export function createExecutionHandler({ apiKey, }); + const namespace = spaceId === 'default' ? {} : { namespace: spaceId }; + const event: IEvent = { event: { action: EVENT_LOG_ACTIONS.executeAction }, kibana: { alerting: { instance_id: alertInstanceId, }, - namespace: spaceId, saved_objects: [ - { type: 'alert', id: alertId }, - { type: 'action', id: action.id }, + { type: 'alert', id: alertId, ...namespace }, + { type: 'action', id: action.id, ...namespace }, ], }, }; diff --git a/x-pack/plugins/alerting/server/task_runner/task_runner.test.ts b/x-pack/plugins/alerting/server/task_runner/task_runner.test.ts index 520f8d5c99b16..31cc893f785cb 100644 --- a/x-pack/plugins/alerting/server/task_runner/task_runner.test.ts +++ b/x-pack/plugins/alerting/server/task_runner/task_runner.test.ts @@ -16,6 +16,7 @@ import { PluginStartContract as ActionsPluginStart } from '../../../actions/serv import { actionsMock } from '../../../actions/server/mocks'; import { eventLoggerMock } from '../../../event_log/server/event_logger.mock'; import { IEventLogger } from '../../../event_log/server'; +import { SavedObjectsErrorHelpers } from '../../../../../src/core/server'; const alertType = { id: 'test', @@ -169,10 +170,10 @@ describe('Task Runner', () => { "action": "execute", }, "kibana": Object { - "namespace": undefined, "saved_objects": Array [ Object { "id": "1", + "namespace": undefined, "type": "alert", }, ], @@ -229,10 +230,10 @@ describe('Task Runner', () => { "action": "execute", }, "kibana": Object { - "namespace": undefined, "saved_objects": Array [ Object { "id": "1", + "namespace": undefined, "type": "alert", }, ], @@ -249,10 +250,10 @@ describe('Task Runner', () => { "alerting": Object { "instance_id": "1", }, - "namespace": undefined, "saved_objects": Array [ Object { "id": "1", + "namespace": undefined, "type": "alert", }, ], @@ -269,14 +270,15 @@ describe('Task Runner', () => { "alerting": Object { "instance_id": "1", }, - "namespace": undefined, "saved_objects": Array [ Object { "id": "1", + "namespace": undefined, "type": "alert", }, Object { "id": "1", + "namespace": undefined, "type": "action", }, ], @@ -344,10 +346,10 @@ describe('Task Runner', () => { "action": "execute", }, "kibana": Object { - "namespace": undefined, "saved_objects": Array [ Object { "id": "1", + "namespace": undefined, "type": "alert", }, ], @@ -364,10 +366,10 @@ describe('Task Runner', () => { "alerting": Object { "instance_id": "2", }, - "namespace": undefined, "saved_objects": Array [ Object { "id": "1", + "namespace": undefined, "type": "alert", }, ], @@ -560,10 +562,10 @@ describe('Task Runner', () => { "action": "execute", }, "kibana": Object { - "namespace": undefined, "saved_objects": Array [ Object { "id": "1", + "namespace": undefined, "type": "alert", }, ], @@ -664,4 +666,36 @@ describe('Task Runner', () => { } `); }); + + test('avoids rescheduling a failed Alert Task Runner when it throws due to failing to fetch the alert', async () => { + savedObjectsClient.get.mockImplementation(() => { + throw SavedObjectsErrorHelpers.createGenericNotFoundError('task', '1'); + }); + + const taskRunner = new TaskRunner( + alertType, + mockedTaskInstance, + taskRunnerFactoryInitializerParams + ); + + encryptedSavedObjectsPlugin.getDecryptedAsInternalUser.mockResolvedValueOnce({ + id: '1', + type: 'alert', + attributes: { + apiKey: Buffer.from('123:abc').toString('base64'), + }, + references: [], + }); + + const runnerResult = await taskRunner.run(); + + expect(runnerResult).toMatchInlineSnapshot(` + Object { + "runAt": undefined, + "state": Object { + "previousStartedAt": 1970-01-01T00:00:00.000Z, + }, + } + `); + }); }); diff --git a/x-pack/plugins/alerting/server/task_runner/task_runner.ts b/x-pack/plugins/alerting/server/task_runner/task_runner.ts index 2ba56396279ea..1d4b12e96bc76 100644 --- a/x-pack/plugins/alerting/server/task_runner/task_runner.ts +++ b/x-pack/plugins/alerting/server/task_runner/task_runner.ts @@ -26,12 +26,13 @@ import { taskInstanceToAlertTaskInstance } from './alert_task_instance'; import { AlertInstances } from '../alert_instance/alert_instance'; import { EVENT_LOG_ACTIONS } from '../plugin'; import { IEvent, IEventLogger } from '../../../event_log/server'; +import { isAlertSavedObjectNotFoundError } from '../lib/is_alert_not_found_error'; const FALLBACK_RETRY_INTERVAL: IntervalSchedule = { interval: '5m' }; interface AlertTaskRunResult { state: AlertTaskState; - runAt: Date; + runAt: Date | undefined; } interface AlertTaskInstance extends ConcreteTaskInstance { @@ -173,7 +174,7 @@ export class TaskRunner { const alertLabel = `${this.alertType.id}:${alertId}: '${name}'`; const event: IEvent = { event: { action: EVENT_LOG_ACTIONS.execute }, - kibana: { namespace, saved_objects: [{ type: 'alert', id: alertId }] }, + kibana: { saved_objects: [{ type: 'alert', id: alertId, namespace }] }, }; eventLogger.startTiming(event); @@ -328,22 +329,29 @@ export class TaskRunner { }; }, (err: Error) => { - this.logger.error(`Executing Alert "${alertId}" has resulted in Error: ${err.message}`); + const message = `Executing Alert "${alertId}" has resulted in Error: ${err.message}`; + if (isAlertSavedObjectNotFoundError(err, alertId)) { + this.logger.debug(message); + } else { + this.logger.error(message); + } return { ...originalState, previousStartedAt, }; } ), - runAt: resolveErr<Date, Error>(runAt, () => - getNextRunAt( - new Date(), - // if we fail at this point we wish to recover but don't have access to the Alert's - // attributes, so we'll use a default interval to prevent the underlying task from - // falling into a failed state - FALLBACK_RETRY_INTERVAL - ) - ), + runAt: resolveErr<Date | undefined, Error>(runAt, err => { + return isAlertSavedObjectNotFoundError(err, alertId) + ? undefined + : getNextRunAt( + new Date(), + // if we fail at this point we wish to recover but don't have access to the Alert's + // attributes, so we'll use a default interval to prevent the underlying task from + // falling into a failed state + FALLBACK_RETRY_INTERVAL + ); + }), }; } } @@ -378,11 +386,10 @@ function generateNewAndResolvedInstanceEvents(params: GenerateNewAndResolvedInst action, }, kibana: { - namespace: params.namespace, alerting: { instance_id: id, }, - saved_objects: [{ type: 'alert', id: params.alertId }], + saved_objects: [{ type: 'alert', id: params.alertId, namespace: params.namespace }], }, message, }; diff --git a/x-pack/plugins/alerting/server/usage/task.ts b/x-pack/plugins/alerting/server/usage/task.ts index 3da60aef301e2..ab62d81d44f8a 100644 --- a/x-pack/plugins/alerting/server/usage/task.ts +++ b/x-pack/plugins/alerting/server/usage/task.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { Logger, CoreSetup } from 'kibana/server'; +import { Logger, CoreSetup, APICaller } from 'kibana/server'; import moment from 'moment'; import { RunContext, @@ -65,7 +65,12 @@ async function scheduleTasks(logger: Logger, taskManager: TaskManagerStartContra export function telemetryTaskRunner(logger: Logger, core: CoreSetup, kibanaIndex: string) { return ({ taskInstance }: RunContext) => { const { state } = taskInstance; - const callCluster = core.elasticsearch.adminClient.callAsInternalUser; + const callCluster = (...args: Parameters<APICaller>) => { + return core.getStartServices().then(([{ elasticsearch: { legacy: { client } } }]) => + client.callAsInternalUser(...args) + ); + }; + return { async run() { return Promise.all([ diff --git a/x-pack/plugins/apm/common/agent_configuration/setting_definitions/__snapshots__/index.test.ts.snap b/x-pack/plugins/apm/common/agent_configuration/setting_definitions/__snapshots__/index.test.ts.snap index 49840d2157af7..ea706be9f584a 100644 --- a/x-pack/plugins/apm/common/agent_configuration/setting_definitions/__snapshots__/index.test.ts.snap +++ b/x-pack/plugins/apm/common/agent_configuration/setting_definitions/__snapshots__/index.test.ts.snap @@ -29,15 +29,19 @@ Array [ "options": Array [ Object { "text": "off", + "value": "off", }, Object { "text": "errors", + "value": "errors", }, Object { "text": "transactions", + "value": "transactions", }, Object { "text": "all", + "value": "all", }, ], "type": "select", diff --git a/x-pack/plugins/apm/common/agent_configuration/setting_definitions/general_settings.ts b/x-pack/plugins/apm/common/agent_configuration/setting_definitions/general_settings.ts index e73aed35e87f9..7477238ba79ae 100644 --- a/x-pack/plugins/apm/common/agent_configuration/setting_definitions/general_settings.ts +++ b/x-pack/plugins/apm/common/agent_configuration/setting_definitions/general_settings.ts @@ -67,10 +67,10 @@ export const generalSettings: RawSettingDefinition[] = [ } ), options: [ - { text: 'off' }, - { text: 'errors' }, - { text: 'transactions' }, - { text: 'all' } + { text: 'off', value: 'off' }, + { text: 'errors', value: 'errors' }, + { text: 'transactions', value: 'transactions' }, + { text: 'all', value: 'all' } ], excludeAgents: ['js-base', 'rum-js'] }, diff --git a/x-pack/plugins/apm/common/agent_configuration/setting_definitions/types.d.ts b/x-pack/plugins/apm/common/agent_configuration/setting_definitions/types.d.ts index 282ced346dda0..815b8cb3d4e83 100644 --- a/x-pack/plugins/apm/common/agent_configuration/setting_definitions/types.d.ts +++ b/x-pack/plugins/apm/common/agent_configuration/setting_definitions/types.d.ts @@ -76,7 +76,7 @@ interface FloatSetting extends BaseSetting { interface SelectSetting extends BaseSetting { type: 'select'; - options: Array<{ text: string }>; + options: Array<{ text: string; value: string }>; } interface BooleanSetting extends BaseSetting { diff --git a/x-pack/plugins/apm/common/ml_job_constants.test.ts b/x-pack/plugins/apm/common/ml_job_constants.test.ts index cea66f31303be..2aa50a305f7c8 100644 --- a/x-pack/plugins/apm/common/ml_job_constants.test.ts +++ b/x-pack/plugins/apm/common/ml_job_constants.test.ts @@ -21,6 +21,12 @@ describe('ml_job_constants', () => { expect(getMlJobId('myServiceName', 'myTransactionType')).toBe( 'myservicename-mytransactiontype-high_mean_response_time' ); + expect(getMlJobId('my service name')).toBe( + 'my_service_name-high_mean_response_time' + ); + expect(getMlJobId('my service name', 'my transaction type')).toBe( + 'my_service_name-my_transaction_type-high_mean_response_time' + ); }); it('getMlIndex', () => { diff --git a/x-pack/plugins/apm/common/ml_job_constants.ts b/x-pack/plugins/apm/common/ml_job_constants.ts index 37487d09e4f4c..01f5762e2dc4b 100644 --- a/x-pack/plugins/apm/common/ml_job_constants.ts +++ b/x-pack/plugins/apm/common/ml_job_constants.ts @@ -6,7 +6,7 @@ export function getMlPrefix(serviceName: string, transactionType?: string) { const maybeTransactionType = transactionType ? `${transactionType}-` : ''; - return `${serviceName}-${maybeTransactionType}`.toLowerCase(); + return encodeForMlApi(`${serviceName}-${maybeTransactionType}`); } export function getMlJobId(serviceName: string, transactionType?: string) { @@ -16,3 +16,7 @@ export function getMlJobId(serviceName: string, transactionType?: string) { export function getMlIndex(serviceName: string, transactionType?: string) { return `.ml-anomalies-${getMlJobId(serviceName, transactionType)}`; } + +export function encodeForMlApi(value: string) { + return value.replace(/\s+/g, '_').toLowerCase(); +} 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 85f233de2086d..e9801272cd57b 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 @@ -61,10 +61,10 @@ export const tasks: TelemetryTask[] = [ return prevJob.then(async data => { const { processorEvent, timeRange } = current; - const response = await search({ + const totalHitsResponse = await search({ index: indicesByProcessorEvent[processorEvent], body: { - size: 1, + size: 0, query: { bool: { filter: [ @@ -83,25 +83,43 @@ export const tasks: TelemetryTask[] = [ ] } }, - sort: { - '@timestamp': 'asc' - }, - _source: ['@timestamp'], track_total_hits: true } }); - const event = response.hits.hits[0]?._source as { - '@timestamp': number; - }; + const retainmentResponse = + timeRange === 'all' + ? await search({ + index: indicesByProcessorEvent[processorEvent], + body: { + query: { + bool: { + filter: [ + { term: { [PROCESSOR_EVENT]: processorEvent } } + ] + } + }, + sort: { + '@timestamp': 'asc' + }, + _source: ['@timestamp'] + } + }) + : null; + + const event = retainmentResponse?.hits.hits[0]?._source as + | { + '@timestamp': number; + } + | undefined; return merge({}, data, { counts: { [processorEvent]: { - [timeRange]: response.hits.total.value + [timeRange]: totalHitsResponse.hits.total.value } }, - ...(timeRange === 'all' && event + ...(event ? { retainment: { [processorEvent]: { diff --git a/x-pack/plugins/apm/server/lib/apm_telemetry/index.ts b/x-pack/plugins/apm/server/lib/apm_telemetry/index.ts index a78b2b93a250d..3eb61bb130725 100644 --- a/x-pack/plugins/apm/server/lib/apm_telemetry/index.ts +++ b/x-pack/plugins/apm/server/lib/apm_telemetry/index.ts @@ -87,7 +87,8 @@ export async function createApmTelemetry({ return { run: async () => { await collectAndStore(); - } + }, + cancel: async () => {} }; } } diff --git a/x-pack/plugins/apm/server/plugin.ts b/x-pack/plugins/apm/server/plugin.ts index e18b6d33ca419..b434d41982f4c 100644 --- a/x-pack/plugins/apm/server/plugin.ts +++ b/x-pack/plugins/apm/server/plugin.ts @@ -3,7 +3,13 @@ * 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, Plugin, CoreSetup } from 'src/core/server'; +import { + PluginInitializerContext, + Plugin, + CoreSetup, + CoreStart, + Logger +} from 'src/core/server'; import { Observable, combineLatest, AsyncSubject } from 'rxjs'; import { map, take } from 'rxjs/operators'; import { Server } from 'hapi'; @@ -37,6 +43,8 @@ export interface APMPluginContract { } export class APMPlugin implements Plugin<APMPluginContract> { + private currentConfig?: APMConfig; + private logger?: Logger; legacySetup$: AsyncSubject<LegacySetup>; constructor(private readonly initContext: PluginInitializerContext) { this.initContext = initContext; @@ -56,7 +64,7 @@ export class APMPlugin implements Plugin<APMPluginContract> { actions?: ActionsPlugin['setup']; } ) { - const logger = this.initContext.logger.get(); + this.logger = this.initContext.logger.get(); const config$ = this.initContext.config.create<APMXPackConfig>(); const mergedConfig$ = combineLatest(plugins.apm_oss.config$, config$).pipe( map(([apmOssConfig, apmConfig]) => mergeConfigs(apmOssConfig, apmConfig)) @@ -71,49 +79,40 @@ export class APMPlugin implements Plugin<APMPluginContract> { } this.legacySetup$.subscribe(__LEGACY => { - createApmApi().init(core, { config$: mergedConfig$, logger, __LEGACY }); + createApmApi().init(core, { + config$: mergedConfig$, + logger: this.logger!, + __LEGACY + }); }); - const currentConfig = await mergedConfig$.pipe(take(1)).toPromise(); + this.currentConfig = await mergedConfig$.pipe(take(1)).toPromise(); if ( plugins.taskManager && plugins.usageCollection && - currentConfig['xpack.apm.telemetryCollectionEnabled'] + this.currentConfig['xpack.apm.telemetryCollectionEnabled'] ) { createApmTelemetry({ core, config$: mergedConfig$, usageCollector: plugins.usageCollection, taskManager: plugins.taskManager, - logger + logger: this.logger }); } - // create agent configuration index without blocking setup lifecycle - createApmAgentConfigurationIndex({ - esClient: core.elasticsearch.dataClient, - config: currentConfig, - logger - }); - // create custom action index without blocking setup lifecycle - createApmCustomLinkIndex({ - esClient: core.elasticsearch.dataClient, - config: currentConfig, - logger - }); - plugins.home.tutorials.registerTutorial( tutorialProvider({ - isEnabled: currentConfig['xpack.apm.ui.enabled'], - indexPatternTitle: currentConfig['apm_oss.indexPattern'], + isEnabled: this.currentConfig['xpack.apm.ui.enabled'], + indexPatternTitle: this.currentConfig['apm_oss.indexPattern'], cloud: plugins.cloud, indices: { - errorIndices: currentConfig['apm_oss.errorIndices'], - metricsIndices: currentConfig['apm_oss.metricsIndices'], - onboardingIndices: currentConfig['apm_oss.onboardingIndices'], - sourcemapIndices: currentConfig['apm_oss.sourcemapIndices'], - transactionIndices: currentConfig['apm_oss.transactionIndices'] + errorIndices: this.currentConfig['apm_oss.errorIndices'], + metricsIndices: this.currentConfig['apm_oss.metricsIndices'], + onboardingIndices: this.currentConfig['apm_oss.onboardingIndices'], + sourcemapIndices: this.currentConfig['apm_oss.sourcemapIndices'], + transactionIndices: this.currentConfig['apm_oss.transactionIndices'] } }) ); @@ -127,12 +126,29 @@ export class APMPlugin implements Plugin<APMPluginContract> { getApmIndices: async () => getApmIndices({ savedObjectsClient: await getInternalSavedObjectsClient(core), - config: currentConfig + config: await mergedConfig$.pipe(take(1)).toPromise() }) }; } - public async start() {} + public start(core: CoreStart) { + if (this.currentConfig == null || this.logger == null) { + throw new Error('APMPlugin needs to be setup before calling start()'); + } + + // create agent configuration index without blocking start lifecycle + createApmAgentConfigurationIndex({ + esClient: core.elasticsearch.legacy.client, + config: this.currentConfig, + logger: this.logger + }); + // create custom action index without blocking start lifecycle + createApmCustomLinkIndex({ + esClient: core.elasticsearch.legacy.client, + config: this.currentConfig, + logger: this.logger + }); + } public stop() {} } diff --git a/x-pack/plugins/apm/typings/es_schemas/raw/span_raw.ts b/x-pack/plugins/apm/typings/es_schemas/raw/span_raw.ts index 4d5d2c5c4a12e..e98b2f52089b3 100644 --- a/x-pack/plugins/apm/typings/es_schemas/raw/span_raw.ts +++ b/x-pack/plugins/apm/typings/es_schemas/raw/span_raw.ts @@ -52,4 +52,5 @@ export interface SpanRaw extends APMBaseDoc { id: string; }; observer?: Observer; + child_ids?: string[]; } diff --git a/x-pack/plugins/canvas/server/plugin.ts b/x-pack/plugins/canvas/server/plugin.ts index 0325de9cf29e2..91a5634734559 100644 --- a/x-pack/plugins/canvas/server/plugin.ts +++ b/x-pack/plugins/canvas/server/plugin.ts @@ -14,6 +14,7 @@ import { initRoutes } from './routes'; import { registerCanvasUsageCollector } from './collectors'; import { loadSampleData } from './sample_data'; import { setupInterpreter } from './setup_interpreter'; +import { customElementType, workpadType } from './saved_objects'; interface PluginsSetup { expressions: ExpressionsServerSetup; @@ -29,6 +30,9 @@ export class CanvasPlugin implements Plugin { } public async setup(coreSetup: CoreSetup, plugins: PluginsSetup) { + coreSetup.savedObjects.registerType(customElementType); + coreSetup.savedObjects.registerType(workpadType); + plugins.features.registerFeature({ id: 'canvas', name: 'Canvas', diff --git a/x-pack/plugins/canvas/server/saved_objects/custom_element.ts b/x-pack/plugins/canvas/server/saved_objects/custom_element.ts new file mode 100644 index 0000000000000..14223455cbc21 --- /dev/null +++ b/x-pack/plugins/canvas/server/saved_objects/custom_element.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 { SavedObjectsType } from 'src/core/server'; +import { CUSTOM_ELEMENT_TYPE } from '../../../../legacy/plugins/canvas/common/lib/constants'; + +export const customElementType: SavedObjectsType = { + name: CUSTOM_ELEMENT_TYPE, + hidden: false, + namespaceType: 'single', + mappings: { + dynamic: false, + properties: { + name: { + type: 'text', + fields: { + keyword: { + type: 'keyword', + }, + }, + }, + help: { type: 'text' }, + content: { type: 'text' }, + image: { type: 'text' }, + '@timestamp': { type: 'date' }, + '@created': { type: 'date' }, + }, + }, + migrations: {}, +}; diff --git a/x-pack/plugins/canvas/server/saved_objects/index.ts b/x-pack/plugins/canvas/server/saved_objects/index.ts new file mode 100644 index 0000000000000..dd7e74b87e2f4 --- /dev/null +++ b/x-pack/plugins/canvas/server/saved_objects/index.ts @@ -0,0 +1,10 @@ +/* + * 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 { workpadType } from './workpad'; +import { customElementType } from './custom_element'; + +export { customElementType, workpadType }; diff --git a/x-pack/plugins/canvas/server/saved_objects/migrations/remove_attributes_id.test.ts b/x-pack/plugins/canvas/server/saved_objects/migrations/remove_attributes_id.test.ts new file mode 100644 index 0000000000000..a7112504e9980 --- /dev/null +++ b/x-pack/plugins/canvas/server/saved_objects/migrations/remove_attributes_id.test.ts @@ -0,0 +1,39 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { removeAttributesId } from './remove_attributes_id'; + +const context: any = { + log: jest.fn(), +}; + +describe(`removeAttributesId`, () => { + it('does not throw error on empty object', () => { + const migratedDoc = removeAttributesId({} as any, context); + expect(migratedDoc).toMatchInlineSnapshot(`Object {}`); + }); + + it('removes id from "attributes"', () => { + const migratedDoc = removeAttributesId( + { + foo: true, + attributes: { + id: '123', + bar: true, + }, + } as any, + context + ); + expect(migratedDoc).toMatchInlineSnapshot(` +Object { + "attributes": Object { + "bar": true, + }, + "foo": true, +} +`); + }); +}); diff --git a/x-pack/plugins/canvas/server/saved_objects/migrations/remove_attributes_id.ts b/x-pack/plugins/canvas/server/saved_objects/migrations/remove_attributes_id.ts new file mode 100644 index 0000000000000..893a73d7b5913 --- /dev/null +++ b/x-pack/plugins/canvas/server/saved_objects/migrations/remove_attributes_id.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 { SavedObjectMigrationFn } from 'src/core/server'; + +export const removeAttributesId: SavedObjectMigrationFn = doc => { + if (typeof doc.attributes === 'object' && doc.attributes !== null) { + delete (doc.attributes as any).id; + } + return doc; +}; diff --git a/x-pack/plugins/canvas/server/saved_objects/workpad.ts b/x-pack/plugins/canvas/server/saved_objects/workpad.ts new file mode 100644 index 0000000000000..918f4bf991076 --- /dev/null +++ b/x-pack/plugins/canvas/server/saved_objects/workpad.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 { SavedObjectsType } from 'src/core/server'; +import { CANVAS_TYPE } from '../../../../legacy/plugins/canvas/common/lib/constants'; +import { removeAttributesId } from './migrations/remove_attributes_id'; + +export const workpadType: SavedObjectsType = { + name: CANVAS_TYPE, + hidden: false, + namespaceType: 'single', + mappings: { + dynamic: false, + properties: { + name: { + type: 'text', + fields: { + keyword: { + type: 'keyword', + }, + }, + }, + '@timestamp': { type: 'date' }, + '@created': { type: 'date' }, + }, + }, + migrations: { + '7.0.0': removeAttributesId, + }, +}; diff --git a/x-pack/plugins/case/common/api/cases/case.ts b/x-pack/plugins/case/common/api/cases/case.ts index 3c5d3405f395e..1f08a41024905 100644 --- a/x-pack/plugins/case/common/api/cases/case.ts +++ b/x-pack/plugins/case/common/api/cases/case.ts @@ -167,6 +167,7 @@ export type CasesResponse = rt.TypeOf<typeof CasesResponseRt>; export type CasesFindResponse = rt.TypeOf<typeof CasesFindResponseRt>; export type CasePatchRequest = rt.TypeOf<typeof CasePatchRequestRt>; export type CasesPatchRequest = rt.TypeOf<typeof CasesPatchRequestRt>; +export type Status = rt.TypeOf<typeof StatusRt>; export type CaseExternalServiceRequest = rt.TypeOf<typeof CaseExternalServiceRequestRt>; export type ServiceConnectorCaseParams = rt.TypeOf<typeof ServiceConnectorCaseParamsRt>; export type ServiceConnectorCaseResponse = rt.TypeOf<typeof ServiceConnectorCaseResponseRt>; diff --git a/x-pack/plugins/case/server/routes/api/__fixtures__/create_mock_so_repository.ts b/x-pack/plugins/case/server/routes/api/__fixtures__/create_mock_so_repository.ts index 95cd66a9c51a2..e83dafc68ee69 100644 --- a/x-pack/plugins/case/server/routes/api/__fixtures__/create_mock_so_repository.ts +++ b/x-pack/plugins/case/server/routes/api/__fixtures__/create_mock_so_repository.ts @@ -11,14 +11,20 @@ import { SavedObjectsBulkUpdateObject, } from 'src/core/server'; -import { CASE_COMMENT_SAVED_OBJECT, CASE_SAVED_OBJECT } from '../../../saved_object_types'; +import { + CASE_COMMENT_SAVED_OBJECT, + CASE_SAVED_OBJECT, + CASE_CONFIGURE_SAVED_OBJECT, +} from '../../../saved_object_types'; export const createMockSavedObjectsRepository = ({ caseSavedObject = [], caseCommentSavedObject = [], + caseConfigureSavedObject = [], }: { caseSavedObject?: any[]; caseCommentSavedObject?: any[]; + caseConfigureSavedObject?: any[]; }) => { const mockSavedObjectsClientContract = ({ bulkGet: jest.fn((objects: SavedObjectsBulkGetObject[]) => { @@ -70,6 +76,7 @@ export const createMockSavedObjectsRepository = ({ } return result[0]; } + const result = caseSavedObject.filter(s => s.id === id); if (!result.length) { throw SavedObjectsErrorHelpers.createGenericNotFoundError(type, id); @@ -81,6 +88,23 @@ export const createMockSavedObjectsRepository = ({ throw SavedObjectsErrorHelpers.createBadRequestError('Error thrown for testing'); } + if ( + findArgs.type === CASE_CONFIGURE_SAVED_OBJECT && + caseConfigureSavedObject[0] && + caseConfigureSavedObject[0].id === 'throw-error-find' + ) { + throw SavedObjectsErrorHelpers.createGenericNotFoundError('Error thrown for testing'); + } + + if (findArgs.type === CASE_CONFIGURE_SAVED_OBJECT) { + return { + page: 1, + per_page: 5, + total: caseConfigureSavedObject.length, + saved_objects: caseConfigureSavedObject, + }; + } + if (findArgs.type === CASE_COMMENT_SAVED_OBJECT) { return { page: 1, @@ -101,6 +125,13 @@ export const createMockSavedObjectsRepository = ({ throw SavedObjectsErrorHelpers.createBadRequestError('Error thrown for testing'); } + if ( + type === CASE_CONFIGURE_SAVED_OBJECT && + attributes.connector_id === 'throw-error-create' + ) { + throw SavedObjectsErrorHelpers.createBadRequestError('Error thrown for testing'); + } + if (type === CASE_COMMENT_SAVED_OBJECT) { const newCommentObj = { type, @@ -113,6 +144,20 @@ export const createMockSavedObjectsRepository = ({ caseCommentSavedObject = [...caseCommentSavedObject, newCommentObj]; return newCommentObj; } + + if (type === CASE_CONFIGURE_SAVED_OBJECT) { + const newConfiguration = { + type, + id: 'mock-configuration', + attributes, + updated_at: '2020-04-09T09:43:51.778Z', + version: attributes.connector_id === 'no-version' ? undefined : 'WzksMV0=', + }; + + caseConfigureSavedObject = [newConfiguration]; + return newConfiguration; + } + return { type, id: 'mock-it', @@ -143,6 +188,16 @@ export const createMockSavedObjectsRepository = ({ } } + if (type === CASE_CONFIGURE_SAVED_OBJECT) { + return { + id, + type, + updated_at: '2019-11-22T22:50:55.191Z', + attributes, + version: attributes.connector_id === 'no-version' ? undefined : 'WzE3LDFd', + }; + } + return { id, type, @@ -153,16 +208,29 @@ export const createMockSavedObjectsRepository = ({ }), delete: jest.fn((type: string, id: string) => { let result = caseSavedObject.filter(s => s.id === id); + if (type === CASE_COMMENT_SAVED_OBJECT) { result = caseCommentSavedObject.filter(s => s.id === id); } + + if (type === CASE_CONFIGURE_SAVED_OBJECT) { + result = caseConfigureSavedObject.filter(s => s.id === id); + } + if (type === CASE_COMMENT_SAVED_OBJECT && id === 'bad-guy') { throw SavedObjectsErrorHelpers.createBadRequestError('Error thrown for testing'); } + if (!result.length) { throw SavedObjectsErrorHelpers.createGenericNotFoundError(type, id); } + if ( + type === CASE_CONFIGURE_SAVED_OBJECT && + caseConfigureSavedObject[0].id === 'throw-error-delete' + ) { + throw new Error('Error thrown for testing'); + } return {}; }), deleteByNamespace: jest.fn(), diff --git a/x-pack/plugins/case/server/routes/api/__fixtures__/mock_saved_objects.ts b/x-pack/plugins/case/server/routes/api/__fixtures__/mock_saved_objects.ts index 03da50f886fd5..75e793a80272f 100644 --- a/x-pack/plugins/case/server/routes/api/__fixtures__/mock_saved_objects.ts +++ b/x-pack/plugins/case/server/routes/api/__fixtures__/mock_saved_objects.ts @@ -5,7 +5,11 @@ */ import { SavedObject } from 'kibana/server'; -import { CaseAttributes, CommentAttributes } from '../../../../common/api'; +import { + CaseAttributes, + CommentAttributes, + CasesConfigureAttributes, +} from '../../../../common/api'; export const mockCases: Array<SavedObject<CaseAttributes>> = [ { @@ -225,7 +229,33 @@ export const mockCaseComments: Array<SavedObject<CommentAttributes>> = [ }, ], updated_at: '2019-11-25T22:32:30.608Z', + version: 'WzYsMV0=', + }, +]; +export const mockCaseConfigure: Array<SavedObject<CasesConfigureAttributes>> = [ + { + type: 'cases-configure', + id: 'mock-configuration-1', + attributes: { + connector_id: '123', + connector_name: 'My connector', + closure_type: 'close-by-user', + created_at: '2020-04-09T09:43:51.778Z', + created_by: { + full_name: 'elastic', + email: 'testemail@elastic.co', + username: 'elastic', + }, + updated_at: '2020-04-09T09:43:51.778Z', + updated_by: { + full_name: 'elastic', + email: 'testemail@elastic.co', + username: 'elastic', + }, + }, + references: [], + updated_at: '2020-04-09T09:43:51.778Z', version: 'WzYsMV0=', }, ]; diff --git a/x-pack/plugins/case/server/routes/api/__fixtures__/route_contexts.ts b/x-pack/plugins/case/server/routes/api/__fixtures__/route_contexts.ts index b1881e394e796..d947ffbaf181d 100644 --- a/x-pack/plugins/case/server/routes/api/__fixtures__/route_contexts.ts +++ b/x-pack/plugins/case/server/routes/api/__fixtures__/route_contexts.ts @@ -5,13 +5,19 @@ */ import { RequestHandlerContext } from 'src/core/server'; +import { actionsClientMock } from '../../../../../actions/server/mocks'; +import { getActions } from '../__mocks__/request_responses'; export const createRouteContext = (client: any) => { + const actionsMock = actionsClientMock.create(); + actionsMock.getAll.mockImplementation(() => Promise.resolve(getActions())); + return ({ core: { savedObjects: { client, }, }, + actions: { getActionsClient: () => actionsMock }, } as unknown) as RequestHandlerContext; }; diff --git a/x-pack/plugins/case/server/routes/api/__mocks__/request_responses.ts b/x-pack/plugins/case/server/routes/api/__mocks__/request_responses.ts new file mode 100644 index 0000000000000..846013674986e --- /dev/null +++ b/x-pack/plugins/case/server/routes/api/__mocks__/request_responses.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 { CasePostRequest, CasesConfigureRequest } from '../../../../common/api'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { FindActionResult } from '../../../../../actions/server/types'; + +export const newCase: CasePostRequest = { + title: 'My new case', + description: 'A description', + tags: ['new', 'case'], +}; + +export const getActions = (): FindActionResult[] => [ + { + id: 'e90075a5-c386-41e3-ae21-ba4e61510695', + actionTypeId: '.webhook', + name: 'Test', + config: { + method: 'post', + url: 'https://example.com', + headers: null, + }, + isPreconfigured: false, + referencedByCount: 0, + }, + { + id: 'd611af27-3532-4da9-8034-271fee81d634', + actionTypeId: '.servicenow', + name: 'ServiceNow', + config: { + casesConfiguration: { + mapping: [ + { + source: 'title', + target: 'short_description', + actionType: 'overwrite', + }, + { + source: 'description', + target: 'description', + actionType: 'overwrite', + }, + { + source: 'comments', + target: 'comments', + actionType: 'append', + }, + ], + }, + apiUrl: 'https://dev102283.service-now.com', + }, + isPreconfigured: false, + referencedByCount: 0, + }, +]; + +export const newConfiguration: CasesConfigureRequest = { + connector_id: '456', + connector_name: 'My connector 2', + closure_type: 'close-by-pushing', +}; diff --git a/x-pack/plugins/case/server/routes/api/cases/configure/get_configure.test.ts b/x-pack/plugins/case/server/routes/api/cases/configure/get_configure.test.ts new file mode 100644 index 0000000000000..66d39c3f11d28 --- /dev/null +++ b/x-pack/plugins/case/server/routes/api/cases/configure/get_configure.test.ts @@ -0,0 +1,112 @@ +/* + * 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 { kibanaResponseFactory, RequestHandler } from 'src/core/server'; +import { httpServerMock } from 'src/core/server/mocks'; + +import { + createMockSavedObjectsRepository, + createRoute, + createRouteContext, +} from '../../__fixtures__'; + +import { mockCaseConfigure } from '../../__fixtures__/mock_saved_objects'; +import { initGetCaseConfigure } from './get_configure'; + +describe('GET configuration', () => { + let routeHandler: RequestHandler<any, any, any>; + beforeAll(async () => { + routeHandler = await createRoute(initGetCaseConfigure, 'get'); + }); + + it('returns the configuration', async () => { + const req = httpServerMock.createKibanaRequest({ + path: '/api/cases/configure', + method: 'get', + }); + + const context = createRouteContext( + createMockSavedObjectsRepository({ + caseConfigureSavedObject: mockCaseConfigure, + }) + ); + + const res = await routeHandler(context, req, kibanaResponseFactory); + expect(res.status).toEqual(200); + expect(res.payload).toEqual({ + ...mockCaseConfigure[0].attributes, + version: mockCaseConfigure[0].version, + }); + }); + + it('handles undefined version correctly', async () => { + const req = httpServerMock.createKibanaRequest({ + path: '/api/cases/configure', + method: 'get', + }); + + const context = createRouteContext( + createMockSavedObjectsRepository({ + caseConfigureSavedObject: [{ ...mockCaseConfigure[0], version: undefined }], + }) + ); + + const res = await routeHandler(context, req, kibanaResponseFactory); + expect(res.status).toEqual(200); + expect(res.payload).toEqual({ + connector_id: '123', + connector_name: 'My connector', + closure_type: 'close-by-user', + created_at: '2020-04-09T09:43:51.778Z', + created_by: { + full_name: 'elastic', + email: 'testemail@elastic.co', + username: 'elastic', + }, + updated_at: '2020-04-09T09:43:51.778Z', + updated_by: { + full_name: 'elastic', + email: 'testemail@elastic.co', + username: 'elastic', + }, + version: '', + }); + }); + + it('returns an empty object when there is no configuration', async () => { + const req = httpServerMock.createKibanaRequest({ + path: '/api/cases/configure', + method: 'get', + }); + + const context = createRouteContext( + createMockSavedObjectsRepository({ + caseConfigureSavedObject: [], + }) + ); + + const res = await routeHandler(context, req, kibanaResponseFactory); + expect(res.status).toEqual(200); + expect(res.payload).toEqual({}); + }); + + it('returns an error if find throws an error', async () => { + const req = httpServerMock.createKibanaRequest({ + path: '/api/cases/configure', + method: 'get', + }); + + const context = createRouteContext( + createMockSavedObjectsRepository({ + caseConfigureSavedObject: [{ ...mockCaseConfigure[0], id: 'throw-error-find' }], + }) + ); + + const res = await routeHandler(context, req, kibanaResponseFactory); + expect(res.status).toEqual(404); + expect(res.payload.isBoom).toEqual(true); + }); +}); diff --git a/x-pack/plugins/case/server/routes/api/cases/configure/get_connectors.test.ts b/x-pack/plugins/case/server/routes/api/cases/configure/get_connectors.test.ts new file mode 100644 index 0000000000000..62edaa0a4792a --- /dev/null +++ b/x-pack/plugins/case/server/routes/api/cases/configure/get_connectors.test.ts @@ -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 { kibanaResponseFactory, RequestHandler } from 'src/core/server'; +import { httpServerMock } from 'src/core/server/mocks'; + +import { + createMockSavedObjectsRepository, + createRoute, + createRouteContext, +} from '../../__fixtures__'; + +import { mockCaseConfigure } from '../../__fixtures__/mock_saved_objects'; +import { initCaseConfigureGetActionConnector } from './get_connectors'; +import { getActions } from '../../__mocks__/request_responses'; + +describe('GET connectors', () => { + let routeHandler: RequestHandler<any, any, any>; + beforeAll(async () => { + routeHandler = await createRoute(initCaseConfigureGetActionConnector, 'get'); + }); + + it('returns the connectors', async () => { + const req = httpServerMock.createKibanaRequest({ + path: '/api/cases/configure/connectors/_find', + method: 'get', + }); + + const context = createRouteContext( + createMockSavedObjectsRepository({ + caseConfigureSavedObject: mockCaseConfigure, + }) + ); + + const res = await routeHandler(context, req, kibanaResponseFactory); + expect(res.status).toEqual(200); + expect(res.payload).toEqual( + getActions().filter(action => action.actionTypeId === '.servicenow') + ); + }); + + it('it throws an error when actions client is null', async () => { + const req = httpServerMock.createKibanaRequest({ + path: '/api/cases/configure/connectors/_find', + method: 'get', + }); + + const context = createRouteContext( + createMockSavedObjectsRepository({ + caseConfigureSavedObject: mockCaseConfigure, + }) + ); + + context.actions = undefined; + + const res = await routeHandler(context, req, kibanaResponseFactory); + expect(res.status).toEqual(404); + expect(res.payload.isBoom).toEqual(true); + }); +}); diff --git a/x-pack/plugins/case/server/routes/api/cases/configure/patch_configure.test.ts b/x-pack/plugins/case/server/routes/api/cases/configure/patch_configure.test.ts new file mode 100644 index 0000000000000..5b3d68a258664 --- /dev/null +++ b/x-pack/plugins/case/server/routes/api/cases/configure/patch_configure.test.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 { kibanaResponseFactory, RequestHandler } from 'src/core/server'; +import { httpServerMock } from 'src/core/server/mocks'; + +import { + createMockSavedObjectsRepository, + createRoute, + createRouteContext, +} from '../../__fixtures__'; + +import { mockCaseConfigure } from '../../__fixtures__/mock_saved_objects'; +import { initPatchCaseConfigure } from './patch_configure'; + +describe('PATCH configuration', () => { + let routeHandler: RequestHandler<any, any, any>; + + beforeAll(async () => { + routeHandler = await createRoute(initPatchCaseConfigure, 'patch'); + const spyOnDate = jest.spyOn(global, 'Date') as jest.SpyInstance<{}, []>; + spyOnDate.mockImplementation(() => ({ + toISOString: jest.fn().mockReturnValue('2020-04-09T09:43:51.778Z'), + })); + }); + + it('patch configuration', async () => { + const req = httpServerMock.createKibanaRequest({ + path: '/api/cases/configure', + method: 'patch', + body: { + closure_type: 'close-by-pushing', + version: mockCaseConfigure[0].version, + }, + }); + + const context = createRouteContext( + createMockSavedObjectsRepository({ + caseConfigureSavedObject: mockCaseConfigure, + }) + ); + + const res = await routeHandler(context, req, kibanaResponseFactory); + + expect(res.status).toEqual(200); + expect(res.payload).toEqual( + expect.objectContaining({ + ...mockCaseConfigure[0].attributes, + closure_type: 'close-by-pushing', + updated_at: '2020-04-09T09:43:51.778Z', + updated_by: { email: 'd00d@awesome.com', full_name: 'Awesome D00d', username: 'awesome' }, + version: 'WzE3LDFd', + }) + ); + }); + + it('patch configuration without authentication', async () => { + routeHandler = await createRoute(initPatchCaseConfigure, 'patch', true); + + const req = httpServerMock.createKibanaRequest({ + path: '/api/cases/configure', + method: 'patch', + body: { + closure_type: 'close-by-pushing', + version: mockCaseConfigure[0].version, + }, + }); + + const context = createRouteContext( + createMockSavedObjectsRepository({ + caseConfigureSavedObject: mockCaseConfigure, + }) + ); + + const res = await routeHandler(context, req, kibanaResponseFactory); + + expect(res.status).toEqual(200); + expect(res.payload).toEqual( + expect.objectContaining({ + ...mockCaseConfigure[0].attributes, + closure_type: 'close-by-pushing', + updated_at: '2020-04-09T09:43:51.778Z', + updated_by: { email: null, full_name: null, username: null }, + version: 'WzE3LDFd', + }) + ); + }); + + it('throw error when configuration have not being created', async () => { + const req = httpServerMock.createKibanaRequest({ + path: '/api/cases/configure', + method: 'patch', + body: { + closure_type: 'close-by-pushing', + version: mockCaseConfigure[0].version, + }, + }); + + const context = createRouteContext( + createMockSavedObjectsRepository({ + caseConfigureSavedObject: [], + }) + ); + + const res = await routeHandler(context, req, kibanaResponseFactory); + + expect(res.status).toEqual(409); + expect(res.payload.isBoom).toEqual(true); + }); + + it('throw error when the versions are different', async () => { + const req = httpServerMock.createKibanaRequest({ + path: '/api/cases/configure', + method: 'patch', + body: { + closure_type: 'close-by-pushing', + version: 'different-version', + }, + }); + + const context = createRouteContext( + createMockSavedObjectsRepository({ + caseConfigureSavedObject: mockCaseConfigure, + }) + ); + + const res = await routeHandler(context, req, kibanaResponseFactory); + + expect(res.status).toEqual(409); + expect(res.payload.isBoom).toEqual(true); + }); + + it('handles undefined version correctly', async () => { + const req = httpServerMock.createKibanaRequest({ + path: '/api/cases/configure', + method: 'patch', + body: { connector_id: 'no-version', version: mockCaseConfigure[0].version }, + }); + + const context = createRouteContext( + createMockSavedObjectsRepository({ + caseConfigureSavedObject: mockCaseConfigure, + }) + ); + + const res = await routeHandler(context, req, kibanaResponseFactory); + expect(res.payload).toEqual( + expect.objectContaining({ + version: '', + }) + ); + }); +}); diff --git a/x-pack/plugins/case/server/routes/api/cases/configure/post_configure.test.ts b/x-pack/plugins/case/server/routes/api/cases/configure/post_configure.test.ts new file mode 100644 index 0000000000000..7e40cad5b1298 --- /dev/null +++ b/x-pack/plugins/case/server/routes/api/cases/configure/post_configure.test.ts @@ -0,0 +1,294 @@ +/* + * 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 { kibanaResponseFactory, RequestHandler } from 'src/core/server'; +import { httpServerMock } from 'src/core/server/mocks'; + +import { + createMockSavedObjectsRepository, + createRoute, + createRouteContext, +} from '../../__fixtures__'; + +import { mockCaseConfigure } from '../../__fixtures__/mock_saved_objects'; +import { initPostCaseConfigure } from './post_configure'; +import { newConfiguration } from '../../__mocks__/request_responses'; + +describe('POST configuration', () => { + let routeHandler: RequestHandler<any, any, any>; + + beforeAll(async () => { + routeHandler = await createRoute(initPostCaseConfigure, 'post'); + const spyOnDate = jest.spyOn(global, 'Date') as jest.SpyInstance<{}, []>; + spyOnDate.mockImplementation(() => ({ + toISOString: jest.fn().mockReturnValue('2020-04-09T09:43:51.778Z'), + })); + }); + + it('create configuration', async () => { + const req = httpServerMock.createKibanaRequest({ + path: '/api/cases/configure', + method: 'post', + body: newConfiguration, + }); + + const context = createRouteContext( + createMockSavedObjectsRepository({ + caseConfigureSavedObject: mockCaseConfigure, + }) + ); + + const res = await routeHandler(context, req, kibanaResponseFactory); + + expect(res.status).toEqual(200); + expect(res.payload).toEqual( + expect.objectContaining({ + connector_id: '456', + connector_name: 'My connector 2', + closure_type: 'close-by-pushing', + created_at: '2020-04-09T09:43:51.778Z', + created_by: { email: 'd00d@awesome.com', full_name: 'Awesome D00d', username: 'awesome' }, + updated_at: null, + updated_by: null, + }) + ); + }); + + it('create configuration without authentication', async () => { + routeHandler = await createRoute(initPostCaseConfigure, 'post', true); + + const req = httpServerMock.createKibanaRequest({ + path: '/api/cases/configure', + method: 'post', + body: newConfiguration, + }); + + const context = createRouteContext( + createMockSavedObjectsRepository({ + caseConfigureSavedObject: mockCaseConfigure, + }) + ); + + const res = await routeHandler(context, req, kibanaResponseFactory); + + expect(res.status).toEqual(200); + expect(res.payload).toEqual( + expect.objectContaining({ + connector_id: '456', + connector_name: 'My connector 2', + closure_type: 'close-by-pushing', + created_at: '2020-04-09T09:43:51.778Z', + created_by: { email: null, full_name: null, username: null }, + updated_at: null, + updated_by: null, + }) + ); + }); + + it('throws when missing connector_id', async () => { + const req = httpServerMock.createKibanaRequest({ + path: '/api/cases/configure', + method: 'post', + body: { + connector_name: 'My connector 2', + closure_type: 'close-by-pushing', + }, + }); + + const context = createRouteContext( + createMockSavedObjectsRepository({ + caseConfigureSavedObject: mockCaseConfigure, + }) + ); + + const res = await routeHandler(context, req, kibanaResponseFactory); + expect(res.status).toEqual(400); + expect(res.payload.isBoom).toEqual(true); + }); + + it('throws when missing connector_name', async () => { + const req = httpServerMock.createKibanaRequest({ + path: '/api/cases/configure', + method: 'post', + body: { + connector_id: '456', + closure_type: 'close-by-pushing', + }, + }); + + const context = createRouteContext( + createMockSavedObjectsRepository({ + caseConfigureSavedObject: mockCaseConfigure, + }) + ); + + const res = await routeHandler(context, req, kibanaResponseFactory); + expect(res.status).toEqual(400); + expect(res.payload.isBoom).toEqual(true); + }); + + it('throws when missing closure_type', async () => { + const req = httpServerMock.createKibanaRequest({ + path: '/api/cases/configure', + method: 'post', + body: { + connector_id: '456', + connector_name: 'My connector 2', + }, + }); + + const context = createRouteContext( + createMockSavedObjectsRepository({ + caseConfigureSavedObject: mockCaseConfigure, + }) + ); + + const res = await routeHandler(context, req, kibanaResponseFactory); + expect(res.status).toEqual(400); + expect(res.payload.isBoom).toEqual(true); + }); + + it('it deletes the previous configuration', async () => { + const req = httpServerMock.createKibanaRequest({ + path: '/api/cases/configure', + method: 'post', + body: newConfiguration, + }); + + const savedObjectRepository = createMockSavedObjectsRepository({ + caseConfigureSavedObject: mockCaseConfigure, + }); + + const context = createRouteContext(savedObjectRepository); + + const res = await routeHandler(context, req, kibanaResponseFactory); + + expect(res.status).toEqual(200); + expect(savedObjectRepository.delete.mock.calls[0][1]).toBe(mockCaseConfigure[0].id); + }); + + it('it does NOT delete when not found', async () => { + const req = httpServerMock.createKibanaRequest({ + path: '/api/cases/configure', + method: 'post', + body: newConfiguration, + }); + + const savedObjectRepository = createMockSavedObjectsRepository({ + caseConfigureSavedObject: [], + }); + + const context = createRouteContext(savedObjectRepository); + + const res = await routeHandler(context, req, kibanaResponseFactory); + + expect(res.status).toEqual(200); + expect(savedObjectRepository.delete).not.toHaveBeenCalled(); + }); + + it('it deletes all configuration', async () => { + const req = httpServerMock.createKibanaRequest({ + path: '/api/cases/configure', + method: 'post', + body: newConfiguration, + }); + + const savedObjectRepository = createMockSavedObjectsRepository({ + caseConfigureSavedObject: [ + mockCaseConfigure[0], + { ...mockCaseConfigure[0], id: 'mock-configuration-2' }, + ], + }); + + const context = createRouteContext(savedObjectRepository); + + const res = await routeHandler(context, req, kibanaResponseFactory); + + expect(res.status).toEqual(200); + expect(savedObjectRepository.delete.mock.calls[0][1]).toBe(mockCaseConfigure[0].id); + expect(savedObjectRepository.delete.mock.calls[1][1]).toBe('mock-configuration-2'); + }); + + it('returns an error if find throws an error', async () => { + const req = httpServerMock.createKibanaRequest({ + path: '/api/cases/configure', + method: 'post', + body: newConfiguration, + }); + + const context = createRouteContext( + createMockSavedObjectsRepository({ + caseConfigureSavedObject: [{ ...mockCaseConfigure[0], id: 'throw-error-find' }], + }) + ); + + const res = await routeHandler(context, req, kibanaResponseFactory); + expect(res.status).toEqual(404); + expect(res.payload.isBoom).toEqual(true); + }); + + it('returns an error if delete throws an error', async () => { + const req = httpServerMock.createKibanaRequest({ + path: '/api/cases/configure', + method: 'post', + body: newConfiguration, + }); + + const context = createRouteContext( + createMockSavedObjectsRepository({ + caseConfigureSavedObject: [{ ...mockCaseConfigure[0], id: 'throw-error-delete' }], + }) + ); + + const res = await routeHandler(context, req, kibanaResponseFactory); + expect(res.status).toEqual(500); + expect(res.payload.isBoom).toEqual(true); + }); + + it('returns an error if post throws an error', async () => { + const req = httpServerMock.createKibanaRequest({ + path: '/api/cases/configure', + method: 'post', + body: { + connector_id: 'throw-error-create', + connector_name: 'My connector 2', + closure_type: 'close-by-pushing', + }, + }); + + const context = createRouteContext( + createMockSavedObjectsRepository({ + caseConfigureSavedObject: mockCaseConfigure, + }) + ); + + const res = await routeHandler(context, req, kibanaResponseFactory); + expect(res.status).toEqual(400); + expect(res.payload.isBoom).toEqual(true); + }); + + it('handles undefined version correctly', async () => { + const req = httpServerMock.createKibanaRequest({ + path: '/api/cases/configure', + method: 'post', + body: { ...newConfiguration, connector_id: 'no-version' }, + }); + + const context = createRouteContext( + createMockSavedObjectsRepository({ + caseConfigureSavedObject: mockCaseConfigure, + }) + ); + + const res = await routeHandler(context, req, kibanaResponseFactory); + expect(res.status).toEqual(200); + expect(res.payload).toEqual( + expect.objectContaining({ + version: '', + }) + ); + }); +}); diff --git a/x-pack/plugins/case/server/routes/api/cases/get_case.ts b/x-pack/plugins/case/server/routes/api/cases/get_case.ts index e947118a39e8e..ac32b20541a9c 100644 --- a/x-pack/plugins/case/server/routes/api/cases/get_case.ts +++ b/x-pack/plugins/case/server/routes/api/cases/get_case.ts @@ -40,6 +40,10 @@ export function initGetCaseApi({ caseService, router }: RouteDeps) { const theComments = await caseService.getAllCaseComments({ client, caseId: request.params.case_id, + options: { + sortField: 'created_at', + sortOrder: 'asc', + }, }); return response.ok({ diff --git a/x-pack/plugins/case/server/routes/api/utils.test.ts b/x-pack/plugins/case/server/routes/api/utils.test.ts new file mode 100644 index 0000000000000..a22f4db30bf8d --- /dev/null +++ b/x-pack/plugins/case/server/routes/api/utils.test.ts @@ -0,0 +1,418 @@ +/* + * 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 { + transformNewCase, + transformNewComment, + wrapError, + transformCases, + flattenCaseSavedObjects, + flattenCaseSavedObject, + flattenCommentSavedObjects, + transformComments, + flattenCommentSavedObject, + sortToSnake, +} from './utils'; +import { newCase } from './__mocks__/request_responses'; +import { isBoom, boomify } from 'boom'; +import { mockCases, mockCaseComments } from './__fixtures__/mock_saved_objects'; + +describe('Utils', () => { + describe('transformNewCase', () => { + it('transform correctly', () => { + const myCase = { + newCase, + createdDate: '2020-04-09T09:43:51.778Z', + email: 'elastic@elastic.co', + full_name: 'Elastic', + username: 'elastic', + }; + + const res = transformNewCase(myCase); + + expect(res).toEqual({ + ...myCase.newCase, + closed_at: null, + closed_by: null, + created_at: '2020-04-09T09:43:51.778Z', + created_by: { email: 'elastic@elastic.co', full_name: 'Elastic', username: 'elastic' }, + external_service: null, + status: 'open', + updated_at: null, + updated_by: null, + }); + }); + + it('transform correctly without optional fields', () => { + const myCase = { + newCase, + createdDate: '2020-04-09T09:43:51.778Z', + }; + + const res = transformNewCase(myCase); + + expect(res).toEqual({ + ...myCase.newCase, + closed_at: null, + closed_by: null, + created_at: '2020-04-09T09:43:51.778Z', + created_by: { email: undefined, full_name: undefined, username: undefined }, + external_service: null, + status: 'open', + updated_at: null, + updated_by: null, + }); + }); + + it('transform correctly with optional fields as null', () => { + const myCase = { + newCase, + createdDate: '2020-04-09T09:43:51.778Z', + email: null, + full_name: null, + username: null, + }; + + const res = transformNewCase(myCase); + + expect(res).toEqual({ + ...myCase.newCase, + closed_at: null, + closed_by: null, + created_at: '2020-04-09T09:43:51.778Z', + created_by: { email: null, full_name: null, username: null }, + external_service: null, + status: 'open', + updated_at: null, + updated_by: null, + }); + }); + }); + + describe('transformNewComment', () => { + it('transforms correctly', () => { + const comment = { + comment: 'A comment', + createdDate: '2020-04-09T09:43:51.778Z', + email: 'elastic@elastic.co', + full_name: 'Elastic', + username: 'elastic', + }; + + const res = transformNewComment(comment); + expect(res).toEqual({ + comment: 'A comment', + created_at: '2020-04-09T09:43:51.778Z', + created_by: { email: 'elastic@elastic.co', full_name: 'Elastic', username: 'elastic' }, + pushed_at: null, + pushed_by: null, + updated_at: null, + updated_by: null, + }); + }); + + it('transform correctly without optional fields', () => { + const comment = { + comment: 'A comment', + createdDate: '2020-04-09T09:43:51.778Z', + }; + + const res = transformNewComment(comment); + + expect(res).toEqual({ + comment: 'A comment', + created_at: '2020-04-09T09:43:51.778Z', + created_by: { email: undefined, full_name: undefined, username: undefined }, + pushed_at: null, + pushed_by: null, + updated_at: null, + updated_by: null, + }); + }); + + it('transform correctly with optional fields as null', () => { + const comment = { + comment: 'A comment', + createdDate: '2020-04-09T09:43:51.778Z', + email: null, + full_name: null, + username: null, + }; + + const res = transformNewComment(comment); + + expect(res).toEqual({ + comment: 'A comment', + created_at: '2020-04-09T09:43:51.778Z', + created_by: { email: null, full_name: null, username: null }, + pushed_at: null, + pushed_by: null, + updated_at: null, + updated_by: null, + }); + }); + }); + + describe('wrapError', () => { + it('wraps an error', () => { + const error = new Error('Something happened'); + const res = wrapError(error); + + expect(isBoom(res.body as Error)).toBe(true); + }); + + it('it set statusCode to 500', () => { + const error = new Error('Something happened'); + const res = wrapError(error); + + expect(res.statusCode).toBe(500); + }); + + it('it set statusCode to errors status code', () => { + const error = new Error('Something happened') as any; + error.statusCode = 404; + const res = wrapError(error); + + expect(res.statusCode).toBe(404); + }); + + it('it accepts a boom error', () => { + const error = boomify(new Error('Something happened')); + const res = wrapError(error); + + // Utils returns the same boom error as body + expect(res.body).toBe(error); + }); + + it('it accepts a boom error with status code', () => { + const error = boomify(new Error('Something happened'), { statusCode: 404 }); + const res = wrapError(error); + + expect(res.statusCode).toBe(404); + }); + + it('it returns empty headers', () => { + const error = new Error('Something happened'); + const res = wrapError(error); + + expect(res.headers).toEqual({}); + }); + }); + + describe('transformCases', () => { + it('transforms correctly', () => { + const totalCommentsByCase = [ + { caseId: mockCases[0].id, totalComments: 2 }, + { caseId: mockCases[1].id, totalComments: 2 }, + { caseId: mockCases[2].id, totalComments: 2 }, + { caseId: mockCases[3].id, totalComments: 2 }, + ]; + + const res = transformCases( + { saved_objects: mockCases, total: mockCases.length, per_page: 10, page: 1 }, + 2, + 2, + totalCommentsByCase + ); + expect(res).toEqual({ + page: 1, + per_page: 10, + total: mockCases.length, + cases: flattenCaseSavedObjects(mockCases, totalCommentsByCase), + count_open_cases: 2, + count_closed_cases: 2, + }); + }); + }); + + describe('flattenCaseSavedObjects', () => { + it('flattens correctly', () => { + const totalCommentsByCase = [{ caseId: mockCases[0].id, totalComments: 2 }]; + + const res = flattenCaseSavedObjects([mockCases[0]], totalCommentsByCase); + expect(res).toEqual([ + { + id: 'mock-id-1', + closed_at: null, + closed_by: null, + created_at: '2019-11-25T21:54:48.952Z', + created_by: { + full_name: 'elastic', + email: 'testemail@elastic.co', + username: 'elastic', + }, + description: 'This is a brand new case of a bad meanie defacing data', + external_service: null, + title: 'Super Bad Security Issue', + status: 'open', + tags: ['defacement'], + updated_at: '2019-11-25T21:54:48.952Z', + updated_by: { + full_name: 'elastic', + email: 'testemail@elastic.co', + username: 'elastic', + }, + comments: [], + totalComment: 2, + version: 'WzAsMV0=', + }, + ]); + }); + + it('it handles total comments correctly', () => { + const totalCommentsByCase = [{ caseId: 'not-exist', totalComments: 2 }]; + + const res = flattenCaseSavedObjects([mockCases[0]], totalCommentsByCase); + + expect(res).toEqual([ + { + id: 'mock-id-1', + closed_at: null, + closed_by: null, + created_at: '2019-11-25T21:54:48.952Z', + created_by: { + full_name: 'elastic', + email: 'testemail@elastic.co', + username: 'elastic', + }, + description: 'This is a brand new case of a bad meanie defacing data', + external_service: null, + title: 'Super Bad Security Issue', + status: 'open', + tags: ['defacement'], + updated_at: '2019-11-25T21:54:48.952Z', + updated_by: { + full_name: 'elastic', + email: 'testemail@elastic.co', + username: 'elastic', + }, + comments: [], + totalComment: 0, + version: 'WzAsMV0=', + }, + ]); + }); + }); + + describe('flattenCaseSavedObject', () => { + it('flattens correctly', () => { + const myCase = { ...mockCases[0] }; + const res = flattenCaseSavedObject(myCase, [], 2); + expect(res).toEqual({ + id: myCase.id, + version: myCase.version, + comments: [], + totalComment: 2, + ...myCase.attributes, + }); + }); + + it('flattens correctly without version', () => { + const myCase = { ...mockCases[0] }; + myCase.version = undefined; + const res = flattenCaseSavedObject(myCase, [], 2); + expect(res).toEqual({ + id: myCase.id, + version: '0', + comments: [], + totalComment: 2, + ...myCase.attributes, + }); + }); + + it('flattens correctly with comments', () => { + const myCase = { ...mockCases[0] }; + const comments = [{ ...mockCaseComments[0] }]; + const res = flattenCaseSavedObject(myCase, comments, 2); + expect(res).toEqual({ + id: myCase.id, + version: myCase.version, + comments: flattenCommentSavedObjects(comments), + totalComment: 2, + ...myCase.attributes, + }); + }); + }); + + describe('transformComments', () => { + it('transforms correctly', () => { + const comments = { + saved_objects: mockCaseComments, + total: mockCaseComments.length, + per_page: 10, + page: 1, + }; + + const res = transformComments(comments); + expect(res).toEqual({ + page: 1, + per_page: 10, + total: mockCaseComments.length, + comments: flattenCommentSavedObjects(comments.saved_objects), + }); + }); + }); + + describe('flattenCommentSavedObjects', () => { + it('flattens correctly', () => { + const comments = [{ ...mockCaseComments[0] }, { ...mockCaseComments[1] }]; + const res = flattenCommentSavedObjects(comments); + expect(res).toEqual([ + flattenCommentSavedObject(comments[0]), + flattenCommentSavedObject(comments[1]), + ]); + }); + }); + + describe('flattenCommentSavedObject', () => { + it('flattens correctly', () => { + const comment = { ...mockCaseComments[0] }; + const res = flattenCommentSavedObject(comment); + expect(res).toEqual({ + id: comment.id, + version: comment.version, + ...comment.attributes, + }); + }); + + it('flattens correctly without version', () => { + const comment = { ...mockCaseComments[0] }; + comment.version = undefined; + const res = flattenCommentSavedObject(comment); + expect(res).toEqual({ + id: comment.id, + version: '0', + ...comment.attributes, + }); + }); + }); + + describe('sortToSnake', () => { + it('it transforms status correctly', () => { + expect(sortToSnake('status')).toBe('status'); + }); + + it('it transforms createdAt correctly', () => { + expect(sortToSnake('createdAt')).toBe('created_at'); + }); + + it('it transforms created_at correctly', () => { + expect(sortToSnake('created_at')).toBe('created_at'); + }); + + it('it transforms closedAt correctly', () => { + expect(sortToSnake('closedAt')).toBe('closed_at'); + }); + + it('it transforms closed_at correctly', () => { + expect(sortToSnake('closed_at')).toBe('closed_at'); + }); + + it('it transforms default correctly', () => { + expect(sortToSnake('not-exist')).toBe('created_at'); + }); + }); +}); diff --git a/x-pack/plugins/case/server/saved_object_types/cases.ts b/x-pack/plugins/case/server/saved_object_types/cases.ts index a4c5dab0feeb7..cc2b1e74b38c4 100644 --- a/x-pack/plugins/case/server/saved_object_types/cases.ts +++ b/x-pack/plugins/case/server/saved_object_types/cases.ts @@ -11,7 +11,7 @@ export const CASE_SAVED_OBJECT = 'cases'; export const caseSavedObjectType: SavedObjectsType = { name: CASE_SAVED_OBJECT, hidden: false, - namespaceAgnostic: false, + namespaceType: 'single', mappings: { properties: { closed_at: { diff --git a/x-pack/plugins/case/server/saved_object_types/comments.ts b/x-pack/plugins/case/server/saved_object_types/comments.ts index 73b1852bafe58..8b69f272d5b0d 100644 --- a/x-pack/plugins/case/server/saved_object_types/comments.ts +++ b/x-pack/plugins/case/server/saved_object_types/comments.ts @@ -11,7 +11,7 @@ export const CASE_COMMENT_SAVED_OBJECT = 'cases-comments'; export const caseCommentSavedObjectType: SavedObjectsType = { name: CASE_COMMENT_SAVED_OBJECT, hidden: false, - namespaceAgnostic: false, + namespaceType: 'single', mappings: { properties: { comment: { diff --git a/x-pack/plugins/case/server/saved_object_types/configure.ts b/x-pack/plugins/case/server/saved_object_types/configure.ts index d66c38b6ea8ff..d6bc3c9f2e227 100644 --- a/x-pack/plugins/case/server/saved_object_types/configure.ts +++ b/x-pack/plugins/case/server/saved_object_types/configure.ts @@ -11,7 +11,7 @@ export const CASE_CONFIGURE_SAVED_OBJECT = 'cases-configure'; export const caseConfigureSavedObjectType: SavedObjectsType = { name: CASE_CONFIGURE_SAVED_OBJECT, hidden: false, - namespaceAgnostic: false, + namespaceType: 'single', mappings: { properties: { created_at: { diff --git a/x-pack/plugins/case/server/saved_object_types/user_actions.ts b/x-pack/plugins/case/server/saved_object_types/user_actions.ts index b61bfafc3b33c..826c6907efea6 100644 --- a/x-pack/plugins/case/server/saved_object_types/user_actions.ts +++ b/x-pack/plugins/case/server/saved_object_types/user_actions.ts @@ -11,7 +11,7 @@ export const CASE_USER_ACTION_SAVED_OBJECT = 'cases-user-actions'; export const caseUserActionSavedObjectType: SavedObjectsType = { name: CASE_USER_ACTION_SAVED_OBJECT, hidden: false, - namespaceAgnostic: false, + namespaceType: 'single', mappings: { properties: { action_field: { diff --git a/x-pack/plugins/case/server/scripts/generate_case_and_comment_data.sh b/x-pack/plugins/case/server/scripts/generate_case_and_comment_data.sh index 9b6f472d798e0..7ec7dc5a70e92 100755 --- a/x-pack/plugins/case/server/scripts/generate_case_and_comment_data.sh +++ b/x-pack/plugins/case/server/scripts/generate_case_and_comment_data.sh @@ -22,9 +22,9 @@ POSTED_COMMENT="$(curl -s -k \ -u ${ELASTICSEARCH_USERNAME}:${ELASTICSEARCH_PASSWORD} \ -X POST "${KIBANA_URL}${SPACE_URL}/api/cases/$CASE_ID/comments" \ -d @${COMMENT} \ - | jq '{ commentId: .id, commentVersion: .version }' -)" + | jq '{ commentId: .comments[0].id, commentVersion: .comments[0].version }' \ +-j)" POSTED_CASE=$(./get_case.sh $CASE_ID | jq '{ caseId: .id, caseVersion: .version }' -j) echo ${POSTED_COMMENT} ${POSTED_CASE} \ - | jq -s add; \ No newline at end of file +| jq -s add; diff --git a/x-pack/plugins/case/server/scripts/generate_case_data.sh b/x-pack/plugins/case/server/scripts/generate_case_data.sh index f8f6142a5d733..d3a4d3833ad2e 100755 --- a/x-pack/plugins/case/server/scripts/generate_case_data.sh +++ b/x-pack/plugins/case/server/scripts/generate_case_data.sh @@ -11,6 +11,6 @@ # ./generate_case_data.sh set -e -./check_env_variables.sh -./post_case.sh | jq '{ id: .id, version: .version }' -j; + ./check_env_variables.sh + ./post_case.sh | jq '{ id: .id, version: .version }'; diff --git a/x-pack/plugins/encrypted_saved_objects/server/saved_objects/encrypted_saved_objects_client_wrapper.test.ts b/x-pack/plugins/encrypted_saved_objects/server/saved_objects/encrypted_saved_objects_client_wrapper.test.ts index f691e62b9352a..2caf3a7055df9 100644 --- a/x-pack/plugins/encrypted_saved_objects/server/saved_objects/encrypted_saved_objects_client_wrapper.test.ts +++ b/x-pack/plugins/encrypted_saved_objects/server/saved_objects/encrypted_saved_objects_client_wrapper.test.ts @@ -10,14 +10,16 @@ import { SavedObjectsClientContract } from 'src/core/server'; import { EncryptedSavedObjectsService } from '../crypto'; import { EncryptedSavedObjectsClientWrapper } from './encrypted_saved_objects_client_wrapper'; -import { savedObjectsClientMock } from 'src/core/server/mocks'; +import { savedObjectsClientMock, savedObjectsTypeRegistryMock } from 'src/core/server/mocks'; import { encryptedSavedObjectsServiceMock } from '../crypto/index.mock'; let wrapper: EncryptedSavedObjectsClientWrapper; let mockBaseClient: jest.Mocked<SavedObjectsClientContract>; +let mockBaseTypeRegistry: ReturnType<typeof savedObjectsTypeRegistryMock.create>; let encryptedSavedObjectsServiceMockInstance: jest.Mocked<EncryptedSavedObjectsService>; beforeEach(() => { mockBaseClient = savedObjectsClientMock.create(); + mockBaseTypeRegistry = savedObjectsTypeRegistryMock.create(); encryptedSavedObjectsServiceMockInstance = encryptedSavedObjectsServiceMock.create([ { type: 'known-type', @@ -28,6 +30,7 @@ beforeEach(() => { wrapper = new EncryptedSavedObjectsClientWrapper({ service: encryptedSavedObjectsServiceMockInstance, baseClient: mockBaseClient, + baseTypeRegistry: mockBaseTypeRegistry, } as any); }); @@ -91,35 +94,50 @@ describe('#create', () => { ); }); - it('uses `namespace` to encrypt attributes if it is specified', async () => { - const attributes = { attrOne: 'one', attrSecret: 'secret', attrThree: 'three' }; - const options = { overwrite: true, namespace: 'some-namespace' }; - const mockedResponse = { - id: 'uuid-v4-id', - type: 'known-type', - attributes: { attrOne: 'one', attrSecret: '*secret*', attrThree: 'three' }, - references: [], - }; + describe('namespace', () => { + const doTest = async (namespace: string, expectNamespaceInDescriptor: boolean) => { + const attributes = { attrOne: 'one', attrSecret: 'secret', attrThree: 'three' }; + const options = { overwrite: true, namespace }; + const mockedResponse = { + id: 'uuid-v4-id', + type: 'known-type', + attributes: { attrOne: 'one', attrSecret: '*secret*', attrThree: 'three' }, + references: [], + }; - mockBaseClient.create.mockResolvedValue(mockedResponse); + mockBaseClient.create.mockResolvedValue(mockedResponse); - expect(await wrapper.create('known-type', attributes, options)).toEqual({ - ...mockedResponse, - attributes: { attrOne: 'one', attrThree: 'three' }, - }); + expect(await wrapper.create('known-type', attributes, options)).toEqual({ + ...mockedResponse, + attributes: { attrOne: 'one', attrThree: 'three' }, + }); - expect(encryptedSavedObjectsServiceMockInstance.encryptAttributes).toHaveBeenCalledTimes(1); - expect(encryptedSavedObjectsServiceMockInstance.encryptAttributes).toHaveBeenCalledWith( - { type: 'known-type', id: 'uuid-v4-id', namespace: 'some-namespace' }, - { attrOne: 'one', attrSecret: 'secret', attrThree: 'three' } - ); + expect(encryptedSavedObjectsServiceMockInstance.encryptAttributes).toHaveBeenCalledTimes(1); + expect(encryptedSavedObjectsServiceMockInstance.encryptAttributes).toHaveBeenCalledWith( + { + type: 'known-type', + id: 'uuid-v4-id', + namespace: expectNamespaceInDescriptor ? namespace : undefined, + }, + { attrOne: 'one', attrSecret: 'secret', attrThree: 'three' } + ); - expect(mockBaseClient.create).toHaveBeenCalledTimes(1); - expect(mockBaseClient.create).toHaveBeenCalledWith( - 'known-type', - { attrOne: 'one', attrSecret: '*secret*', attrThree: 'three' }, - { id: 'uuid-v4-id', overwrite: true, namespace: 'some-namespace' } - ); + expect(mockBaseClient.create).toHaveBeenCalledTimes(1); + expect(mockBaseClient.create).toHaveBeenCalledWith( + 'known-type', + { attrOne: 'one', attrSecret: '*secret*', attrThree: 'three' }, + { id: 'uuid-v4-id', overwrite: true, namespace } + ); + }; + + it('uses `namespace` to encrypt attributes if it is specified when type is single-namespace', async () => { + await doTest('some-namespace', true); + }); + + it('does not use `namespace` to encrypt attributes if it is specified when type is not single-namespace', async () => { + mockBaseTypeRegistry.isSingleNamespace.mockReturnValue(false); + await doTest('some-namespace', false); + }); }); it('fails if base client fails', async () => { @@ -190,14 +208,13 @@ describe('#bulkCreate', () => { it('fails if ID is specified for registered type', async () => { const attributes = { attrOne: 'one', attrSecret: 'secret', attrThree: 'three' }; - const options = { namespace: 'some-namespace' }; const bulkCreateParams = [ { id: 'some-id', type: 'known-type', attributes }, { type: 'unknown-type', attributes }, ]; - await expect(wrapper.bulkCreate(bulkCreateParams, options)).rejects.toThrowError( + await expect(wrapper.bulkCreate(bulkCreateParams)).rejects.toThrowError( 'Predefined IDs are not allowed for saved objects with encrypted attributes.' ); @@ -257,39 +274,57 @@ describe('#bulkCreate', () => { ); }); - it('uses `namespace` to encrypt attributes if it is specified', async () => { - const attributes = { attrOne: 'one', attrSecret: 'secret', attrThree: 'three' }; - const options = { namespace: 'some-namespace' }; - const mockedResponse = { - saved_objects: [{ id: 'uuid-v4-id', type: 'known-type', attributes, references: [] }], - }; + describe('namespace', () => { + const doTest = async (namespace: string, expectNamespaceInDescriptor: boolean) => { + const attributes = { attrOne: 'one', attrSecret: 'secret', attrThree: 'three' }; + const options = { namespace }; + const mockedResponse = { + saved_objects: [{ id: 'uuid-v4-id', type: 'known-type', attributes, references: [] }], + }; + + mockBaseClient.bulkCreate.mockResolvedValue(mockedResponse); + + const bulkCreateParams = [{ type: 'known-type', attributes }]; + await expect(wrapper.bulkCreate(bulkCreateParams, options)).resolves.toEqual({ + saved_objects: [ + { + ...mockedResponse.saved_objects[0], + attributes: { attrOne: 'one', attrThree: 'three' }, + }, + ], + }); - mockBaseClient.bulkCreate.mockResolvedValue(mockedResponse); + expect(encryptedSavedObjectsServiceMockInstance.encryptAttributes).toHaveBeenCalledTimes(1); + expect(encryptedSavedObjectsServiceMockInstance.encryptAttributes).toHaveBeenCalledWith( + { + type: 'known-type', + id: 'uuid-v4-id', + namespace: expectNamespaceInDescriptor ? namespace : undefined, + }, + { attrOne: 'one', attrSecret: 'secret', attrThree: 'three' } + ); + + expect(mockBaseClient.bulkCreate).toHaveBeenCalledTimes(1); + expect(mockBaseClient.bulkCreate).toHaveBeenCalledWith( + [ + { + ...bulkCreateParams[0], + id: 'uuid-v4-id', + attributes: { attrOne: 'one', attrSecret: '*secret*', attrThree: 'three' }, + }, + ], + options + ); + }; - const bulkCreateParams = [{ type: 'known-type', attributes }]; - await expect(wrapper.bulkCreate(bulkCreateParams, options)).resolves.toEqual({ - saved_objects: [ - { ...mockedResponse.saved_objects[0], attributes: { attrOne: 'one', attrThree: 'three' } }, - ], + it('uses `namespace` to encrypt attributes if it is specified when type is single-namespace', async () => { + await doTest('some-namespace', true); }); - expect(encryptedSavedObjectsServiceMockInstance.encryptAttributes).toHaveBeenCalledTimes(1); - expect(encryptedSavedObjectsServiceMockInstance.encryptAttributes).toHaveBeenCalledWith( - { type: 'known-type', id: 'uuid-v4-id', namespace: 'some-namespace' }, - { attrOne: 'one', attrSecret: 'secret', attrThree: 'three' } - ); - - expect(mockBaseClient.bulkCreate).toHaveBeenCalledTimes(1); - expect(mockBaseClient.bulkCreate).toHaveBeenCalledWith( - [ - { - ...bulkCreateParams[0], - id: 'uuid-v4-id', - attributes: { attrOne: 'one', attrSecret: '*secret*', attrThree: 'three' }, - }, - ], - options - ); + it('does not use `namespace` to encrypt attributes if it is specified when type is not single-namespace', async () => { + mockBaseTypeRegistry.isSingleNamespace.mockReturnValue(false); + await doTest('some-namespace', false); + }); }); it('fails if base client fails', async () => { @@ -432,63 +467,79 @@ describe('#bulkUpdate', () => { ); }); - it('uses `namespace` to encrypt attributes if it is specified', async () => { - const docs = [ - { - id: 'some-id', - type: 'known-type', - attributes: { - attrOne: 'one', - attrSecret: 'secret', - attrThree: 'three', - }, - version: 'some-version', - }, - ]; - - mockBaseClient.bulkUpdate.mockResolvedValue({ - saved_objects: docs.map(doc => ({ ...doc, references: undefined })), - }); - - await expect(wrapper.bulkUpdate(docs, { namespace: 'some-namespace' })).resolves.toEqual({ - saved_objects: [ + describe('namespace', () => { + const doTest = async (namespace: string, expectNamespaceInDescriptor: boolean) => { + const docs = [ { id: 'some-id', type: 'known-type', attributes: { attrOne: 'one', + attrSecret: 'secret', attrThree: 'three', }, version: 'some-version', - references: undefined, }, - ], - }); - - expect(encryptedSavedObjectsServiceMockInstance.encryptAttributes).toHaveBeenCalledTimes(1); - expect(encryptedSavedObjectsServiceMockInstance.encryptAttributes).toHaveBeenCalledWith( - { type: 'known-type', id: 'some-id', namespace: 'some-namespace' }, - { attrOne: 'one', attrSecret: 'secret', attrThree: 'three' } - ); + ]; + const options = { namespace }; + + mockBaseClient.bulkUpdate.mockResolvedValue({ + saved_objects: docs.map(doc => ({ ...doc, references: undefined })), + }); + + await expect(wrapper.bulkUpdate(docs, options)).resolves.toEqual({ + saved_objects: [ + { + id: 'some-id', + type: 'known-type', + attributes: { + attrOne: 'one', + attrThree: 'three', + }, + version: 'some-version', + references: undefined, + }, + ], + }); - expect(mockBaseClient.bulkUpdate).toHaveBeenCalledTimes(1); - expect(mockBaseClient.bulkUpdate).toHaveBeenCalledWith( - [ + expect(encryptedSavedObjectsServiceMockInstance.encryptAttributes).toHaveBeenCalledTimes(1); + expect(encryptedSavedObjectsServiceMockInstance.encryptAttributes).toHaveBeenCalledWith( { - id: 'some-id', type: 'known-type', - attributes: { - attrOne: 'one', - attrSecret: '*secret*', - attrThree: 'three', + id: 'some-id', + namespace: expectNamespaceInDescriptor ? namespace : undefined, + }, + { attrOne: 'one', attrSecret: 'secret', attrThree: 'three' } + ); + + expect(mockBaseClient.bulkUpdate).toHaveBeenCalledTimes(1); + expect(mockBaseClient.bulkUpdate).toHaveBeenCalledWith( + [ + { + id: 'some-id', + type: 'known-type', + attributes: { + attrOne: 'one', + attrSecret: '*secret*', + attrThree: 'three', + }, + version: 'some-version', + + references: undefined, }, - version: 'some-version', + ], + options + ); + }; - references: undefined, - }, - ], - { namespace: 'some-namespace' } - ); + it('uses `namespace` to encrypt attributes if it is specified when type is single-namespace', async () => { + await doTest('some-namespace', true); + }); + + it('does not use `namespace` to encrypt attributes if it is specified when type is not single-namespace', async () => { + mockBaseTypeRegistry.isSingleNamespace.mockReturnValue(false); + await doTest('some-namespace', false); + }); }); it('fails if base client fails', async () => { @@ -871,31 +922,46 @@ describe('#update', () => { ); }); - it('uses `namespace` to encrypt attributes if it is specified', async () => { - const attributes = { attrOne: 'one', attrSecret: 'secret', attrThree: 'three' }; - const options = { version: 'some-version', namespace: 'some-namespace' }; - const mockedResponse = { id: 'some-id', type: 'known-type', attributes, references: [] }; + describe('namespace', () => { + const doTest = async (namespace: string, expectNamespaceInDescriptor: boolean) => { + const attributes = { attrOne: 'one', attrSecret: 'secret', attrThree: 'three' }; + const options = { version: 'some-version', namespace }; + const mockedResponse = { id: 'some-id', type: 'known-type', attributes, references: [] }; - mockBaseClient.update.mockResolvedValue(mockedResponse); + mockBaseClient.update.mockResolvedValue(mockedResponse); - await expect(wrapper.update('known-type', 'some-id', attributes, options)).resolves.toEqual({ - ...mockedResponse, - attributes: { attrOne: 'one', attrThree: 'three' }, - }); + await expect(wrapper.update('known-type', 'some-id', attributes, options)).resolves.toEqual({ + ...mockedResponse, + attributes: { attrOne: 'one', attrThree: 'three' }, + }); - expect(encryptedSavedObjectsServiceMockInstance.encryptAttributes).toHaveBeenCalledTimes(1); - expect(encryptedSavedObjectsServiceMockInstance.encryptAttributes).toHaveBeenCalledWith( - { type: 'known-type', id: 'some-id', namespace: 'some-namespace' }, - { attrOne: 'one', attrSecret: 'secret', attrThree: 'three' } - ); + expect(encryptedSavedObjectsServiceMockInstance.encryptAttributes).toHaveBeenCalledTimes(1); + expect(encryptedSavedObjectsServiceMockInstance.encryptAttributes).toHaveBeenCalledWith( + { + type: 'known-type', + id: 'some-id', + namespace: expectNamespaceInDescriptor ? namespace : undefined, + }, + { attrOne: 'one', attrSecret: 'secret', attrThree: 'three' } + ); + + expect(mockBaseClient.update).toHaveBeenCalledTimes(1); + expect(mockBaseClient.update).toHaveBeenCalledWith( + 'known-type', + 'some-id', + { attrOne: 'one', attrSecret: '*secret*', attrThree: 'three' }, + options + ); + }; - expect(mockBaseClient.update).toHaveBeenCalledTimes(1); - expect(mockBaseClient.update).toHaveBeenCalledWith( - 'known-type', - 'some-id', - { attrOne: 'one', attrSecret: '*secret*', attrThree: 'three' }, - options - ); + it('uses `namespace` to encrypt attributes if it is specified when type is single-namespace', async () => { + await doTest('some-namespace', true); + }); + + it('does not use `namespace` to encrypt attributes if it is specified when type is not single-namespace', async () => { + mockBaseTypeRegistry.isSingleNamespace.mockReturnValue(false); + await doTest('some-namespace', false); + }); }); it('fails if base client fails', async () => { diff --git a/x-pack/plugins/encrypted_saved_objects/server/saved_objects/encrypted_saved_objects_client_wrapper.ts b/x-pack/plugins/encrypted_saved_objects/server/saved_objects/encrypted_saved_objects_client_wrapper.ts index b4f1de52c9ce3..e8197536d29d9 100644 --- a/x-pack/plugins/encrypted_saved_objects/server/saved_objects/encrypted_saved_objects_client_wrapper.ts +++ b/x-pack/plugins/encrypted_saved_objects/server/saved_objects/encrypted_saved_objects_client_wrapper.ts @@ -19,11 +19,15 @@ import { SavedObjectsFindResponse, SavedObjectsUpdateOptions, SavedObjectsUpdateResponse, + SavedObjectsAddToNamespacesOptions, + SavedObjectsDeleteFromNamespacesOptions, + ISavedObjectTypeRegistry, } from 'src/core/server'; import { EncryptedSavedObjectsService } from '../crypto'; interface EncryptedSavedObjectsClientOptions { baseClient: SavedObjectsClientContract; + baseTypeRegistry: ISavedObjectTypeRegistry; service: Readonly<EncryptedSavedObjectsService>; } @@ -41,6 +45,10 @@ export class EncryptedSavedObjectsClientWrapper implements SavedObjectsClientCon public readonly errors = options.baseClient.errors ) {} + // only include namespace in AAD descriptor if the specified type is single-namespace + private getDescriptorNamespace = (type: string, namespace?: string) => + this.options.baseTypeRegistry.isSingleNamespace(type) ? namespace : undefined; + public async create<T = unknown>( type: string, attributes: T = {} as T, @@ -60,11 +68,12 @@ export class EncryptedSavedObjectsClientWrapper implements SavedObjectsClientCon } const id = generateID(); + const namespace = this.getDescriptorNamespace(type, options.namespace); return this.stripEncryptedAttributesFromResponse( await this.options.baseClient.create( type, await this.options.service.encryptAttributes( - { type, id, namespace: options.namespace }, + { type, id, namespace }, attributes as Record<string, unknown> ), { ...options, id } @@ -95,11 +104,12 @@ export class EncryptedSavedObjectsClientWrapper implements SavedObjectsClientCon } const id = generateID(); + const namespace = this.getDescriptorNamespace(object.type, options?.namespace); return { ...object, id, attributes: await this.options.service.encryptAttributes( - { type: object.type, id, namespace: options && options.namespace }, + { type: object.type, id, namespace }, object.attributes as Record<string, unknown> ), } as SavedObjectsBulkCreateObject<T>; @@ -124,10 +134,11 @@ export class EncryptedSavedObjectsClientWrapper implements SavedObjectsClientCon if (!this.options.service.isRegistered(type)) { return object; } + const namespace = this.getDescriptorNamespace(type, options?.namespace); return { ...object, attributes: await this.options.service.encryptAttributes( - { type, id, namespace: options && options.namespace }, + { type, id, namespace }, attributes ), }; @@ -173,20 +184,35 @@ export class EncryptedSavedObjectsClientWrapper implements SavedObjectsClientCon if (!this.options.service.isRegistered(type)) { return await this.options.baseClient.update(type, id, attributes, options); } - + const namespace = this.getDescriptorNamespace(type, options?.namespace); return this.stripEncryptedAttributesFromResponse( await this.options.baseClient.update( type, id, - await this.options.service.encryptAttributes( - { type, id, namespace: options && options.namespace }, - attributes - ), + await this.options.service.encryptAttributes({ type, id, namespace }, attributes), options ) ); } + public async addToNamespaces( + type: string, + id: string, + namespaces: string[], + options?: SavedObjectsAddToNamespacesOptions + ) { + return await this.options.baseClient.addToNamespaces(type, id, namespaces, options); + } + + public async deleteFromNamespaces( + type: string, + id: string, + namespaces: string[], + options?: SavedObjectsDeleteFromNamespacesOptions + ) { + return await this.options.baseClient.deleteFromNamespaces(type, id, namespaces, options); + } + /** * Strips encrypted attributes from any non-bulk Saved Objects API response. If type isn't * registered, response is returned as is. diff --git a/x-pack/plugins/encrypted_saved_objects/server/saved_objects/index.ts b/x-pack/plugins/encrypted_saved_objects/server/saved_objects/index.ts index c76477cd8da43..10599ae3a1798 100644 --- a/x-pack/plugins/encrypted_saved_objects/server/saved_objects/index.ts +++ b/x-pack/plugins/encrypted_saved_objects/server/saved_objects/index.ts @@ -40,7 +40,8 @@ export function setupSavedObjects({ savedObjects.addClientWrapper( Number.MAX_SAFE_INTEGER, 'encryptedSavedObjects', - ({ client: baseClient }) => new EncryptedSavedObjectsClientWrapper({ baseClient, service }) + ({ client: baseClient, typeRegistry: baseTypeRegistry }) => + new EncryptedSavedObjectsClientWrapper({ baseClient, baseTypeRegistry, service }) ); const internalRepositoryPromise = getStartServices().then(([core]) => diff --git a/x-pack/plugins/endpoint/common/generate_data.ts b/x-pack/plugins/endpoint/common/generate_data.ts index 7c24bd9d77148..1978d780f54f5 100644 --- a/x-pack/plugins/endpoint/common/generate_data.ts +++ b/x-pack/plugins/endpoint/common/generate_data.ts @@ -6,12 +6,8 @@ import uuid from 'uuid'; import seedrandom from 'seedrandom'; -import { AlertEvent, EndpointEvent, HostMetadata, OSFields, HostFields } from './types'; -// FIXME: move types/model to top-level -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { PolicyData } from '../public/applications/endpoint/types'; -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { generatePolicy } from '../public/applications/endpoint/models/policy'; +import { AlertEvent, EndpointEvent, HostMetadata, OSFields, HostFields, PolicyData } from './types'; +import { factory as policyFactory } from './models/policy_config'; export type Event = AlertEvent | EndpointEvent; @@ -475,7 +471,7 @@ export class EndpointDocGenerator { streams: [], config: { policy: { - value: generatePolicy(), + value: policyFactory(), }, }, }, diff --git a/x-pack/plugins/endpoint/common/models/policy_config.ts b/x-pack/plugins/endpoint/common/models/policy_config.ts new file mode 100644 index 0000000000000..199b8a91e4307 --- /dev/null +++ b/x-pack/plugins/endpoint/common/models/policy_config.ts @@ -0,0 +1,97 @@ +/* + * 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 { PolicyConfig, ProtectionModes } from '../types'; + +/** + * Return a new default `PolicyConfig`. + */ +export const factory = (): PolicyConfig => { + return { + windows: { + events: { + dll_and_driver_load: true, + dns: true, + file: true, + network: true, + process: true, + registry: true, + security: true, + }, + malware: { + mode: ProtectionModes.prevent, + }, + logging: { + stdout: 'debug', + file: 'info', + }, + advanced: { + elasticsearch: { + indices: { + control: 'control-index', + event: 'event-index', + logging: 'logging-index', + }, + kernel: { + connect: true, + process: true, + }, + }, + }, + }, + mac: { + events: { + process: true, + file: true, + network: true, + }, + malware: { + mode: ProtectionModes.detect, + }, + logging: { + stdout: 'debug', + file: 'info', + }, + advanced: { + elasticsearch: { + indices: { + control: 'control-index', + event: 'event-index', + logging: 'logging-index', + }, + kernel: { + connect: true, + process: true, + }, + }, + }, + }, + linux: { + events: { + process: true, + file: true, + network: true, + }, + logging: { + stdout: 'debug', + file: 'info', + }, + advanced: { + elasticsearch: { + indices: { + control: 'control-index', + event: 'event-index', + logging: 'logging-index', + }, + kernel: { + connect: true, + process: true, + }, + }, + }, + }, + }; +}; diff --git a/x-pack/plugins/endpoint/common/schema/index_pattern.ts b/x-pack/plugins/endpoint/common/schema/index_pattern.ts new file mode 100644 index 0000000000000..2809004f88c6e --- /dev/null +++ b/x-pack/plugins/endpoint/common/schema/index_pattern.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. + */ + +import { schema } from '@kbn/config-schema'; + +export const indexPatternGetParamsSchema = schema.object({ datasetPath: schema.string() }); diff --git a/x-pack/plugins/endpoint/common/types.ts b/x-pack/plugins/endpoint/common/types.ts index a614526d92a3f..49f8ebbd580d8 100644 --- a/x-pack/plugins/endpoint/common/types.ts +++ b/x-pack/plugins/endpoint/common/types.ts @@ -7,12 +7,16 @@ import { SearchResponse } from 'elasticsearch'; import { TypeOf } from '@kbn/config-schema'; import { alertingIndexGetQuerySchema } from './schema/alert_index'; +import { indexPatternGetParamsSchema } from './schema/index_pattern'; +import { Datasource, NewDatasource } from '../../ingest_manager/common'; /** * A deep readonly type that will make all children of a given object readonly recursively */ export type Immutable<T> = T extends undefined | null | boolean | string | number ? T + : unknown extends T + ? unknown : T extends Array<infer U> ? ImmutableArray<U> : T extends Map<infer K, infer V> @@ -30,9 +34,9 @@ export type Direction = 'asc' | 'desc'; export class EndpointAppConstants { static BASE_API_URL = '/api/endpoint'; - static ENDPOINT_INDEX_NAME = 'endpoint-agent*'; + static INDEX_PATTERN_ROUTE = `${EndpointAppConstants.BASE_API_URL}/index_pattern`; static ALERT_INDEX_NAME = 'events-endpoint-1'; - static EVENT_INDEX_NAME = 'events-endpoint-*'; + static EVENT_DATASET = 'events'; static DEFAULT_TOTAL_HITS = 10000; /** * Legacy events are stored in indices with endgame-* prefix @@ -375,11 +379,6 @@ export interface EndpointEvent { export type ResolverEvent = EndpointEvent | LegacyEndpointEvent; -/** - * The PageId type is used for the payload when firing userNavigatedToPage actions - */ -export type PageId = 'alertsPage' | 'managementPage' | 'policyListPage'; - /** * Takes a @kbn/config-schema 'schema' type and returns a type that represents valid inputs. * Similar to `TypeOf`, but allows strings as input for `schema.number()` (which is inline @@ -447,3 +446,130 @@ export type AlertingIndexGetQueryInput = KbnConfigSchemaInputTypeOf< * Result of the validated query params when handling alert index requests. */ export type AlertingIndexGetQueryResult = TypeOf<typeof alertingIndexGetQuerySchema>; + +/** + * Result of the validated params when handling an index pattern request. + */ +export type IndexPatternGetParamsResult = TypeOf<typeof indexPatternGetParamsSchema>; + +/** + * Endpoint Policy configuration + */ +export interface PolicyConfig { + windows: { + events: { + dll_and_driver_load: boolean; + dns: boolean; + file: boolean; + network: boolean; + process: boolean; + registry: boolean; + security: boolean; + }; + malware: MalwareFields; + logging: { + stdout: string; + file: string; + }; + advanced: PolicyConfigAdvancedOptions; + }; + mac: { + events: { + file: boolean; + process: boolean; + network: boolean; + }; + malware: MalwareFields; + logging: { + stdout: string; + file: string; + }; + advanced: PolicyConfigAdvancedOptions; + }; + linux: { + events: { + file: boolean; + process: boolean; + network: boolean; + }; + logging: { + stdout: string; + file: string; + }; + advanced: PolicyConfigAdvancedOptions; + }; +} + +/** + * Windows-specific policy configuration that is supported via the UI + */ +type WindowsPolicyConfig = Pick<PolicyConfig['windows'], 'events' | 'malware'>; + +/** + * Mac-specific policy configuration that is supported via the UI + */ +type MacPolicyConfig = Pick<PolicyConfig['mac'], 'malware' | 'events'>; + +/** + * Linux-specific policy configuration that is supported via the UI + */ +type LinuxPolicyConfig = Pick<PolicyConfig['linux'], 'events'>; + +/** + * The set of Policy configuration settings that are show/edited via the UI + */ +export interface UIPolicyConfig { + windows: WindowsPolicyConfig; + mac: MacPolicyConfig; + linux: LinuxPolicyConfig; +} + +interface PolicyConfigAdvancedOptions { + elasticsearch: { + indices: { + control: string; + event: string; + logging: string; + }; + kernel: { + connect: boolean; + process: boolean; + }; + }; +} + +/** Policy: Malware protection fields */ +export interface MalwareFields { + mode: ProtectionModes; +} + +/** Policy protection mode options */ +export enum ProtectionModes { + detect = 'detect', + prevent = 'prevent', + preventNotify = 'preventNotify', + off = 'off', +} + +/** + * Endpoint Policy data, which extends Ingest's `Datasource` type + */ +export type PolicyData = Datasource & NewPolicyData; + +/** + * New policy data. Used when updating the policy record via ingest APIs + */ +export type NewPolicyData = NewDatasource & { + inputs: [ + { + type: 'endpoint'; + enabled: boolean; + streams: []; + config: { + policy: { + value: PolicyConfig; + }; + }; + } + ]; +}; diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/README.md b/x-pack/plugins/endpoint/public/applications/endpoint/README.md new file mode 100644 index 0000000000000..25bfd615d1d2c --- /dev/null +++ b/x-pack/plugins/endpoint/public/applications/endpoint/README.md @@ -0,0 +1,28 @@ +# Endpoint application +This application provides the user interface for the Elastic Endpoint + +# Architecture +The application consists of a _view_ written in React and a _model_ written in Redux. + +# Modules +We structure the modules to match the architecture. `view` contains the _view_ (all React) code. `store` contains the _model_. + +This section covers the conventions of each top level module. + +# `mocks` +This contains helper code for unit tests. + +## `models` +This contains domain models. By convention, each submodule here contains methods for a single type. Domain model classes would also live here. + +## `store` +This contains the _model_ of the application. All Redux and Redux middleware code (including API interactions) happen here. This module also contains the types and interfaces defining Redux actions. Each action type or interface should be commented and if it has fields, each field should be commented. Comments should be of `tsdoc` style. + +## `view` +This contains the code which renders elements to the DOM. All React code goes here. + +## `index.tsx` +This exports `renderApp` which instantiates the React view with the _model_. + +## `types.ts` +This contains the types and interfaces. All `export`ed types or interfaces (except ones defining Redux actions, which live in `store`) should be here. Each type or interface should have a `tsdoc` style comment. Interfaces should have `tsdoc` comments on each field and types which have fields should do the same. diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/components/header_nav.tsx b/x-pack/plugins/endpoint/public/applications/endpoint/components/header_nav.tsx deleted file mode 100644 index 3fb06d6b4a56e..0000000000000 --- a/x-pack/plugins/endpoint/public/applications/endpoint/components/header_nav.tsx +++ /dev/null @@ -1,79 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import React, { MouseEvent, useMemo } from 'react'; -import { i18n } from '@kbn/i18n'; -import { EuiTabs, EuiTab } from '@elastic/eui'; -import { useHistory, useLocation } from 'react-router-dom'; -import { useKibana } from '../../../../../../../src/plugins/kibana_react/public'; - -export interface NavTabs { - name: string; - id: string; - href: string; -} - -export const navTabs: NavTabs[] = [ - { - id: 'home', - name: i18n.translate('xpack.endpoint.headerNav.home', { - defaultMessage: 'Home', - }), - href: '/', - }, - { - id: 'hosts', - name: i18n.translate('xpack.endpoint.headerNav.hosts', { - defaultMessage: 'Hosts', - }), - href: '/hosts', - }, - { - id: 'alerts', - name: i18n.translate('xpack.endpoint.headerNav.alerts', { - defaultMessage: 'Alerts', - }), - href: '/alerts', - }, - { - id: 'policies', - name: i18n.translate('xpack.endpoint.headerNav.policies', { - defaultMessage: 'Policies', - }), - href: '/policy', - }, -]; - -export const HeaderNavigation: React.FunctionComponent = React.memo(() => { - const history = useHistory(); - const location = useLocation(); - const { services } = useKibana(); - const BASE_PATH = services.application.getUrlForApp('endpoint'); - - const tabList = useMemo(() => { - return navTabs.map((tab, index) => { - return ( - <EuiTab - data-test-subj={`${tab.id}EndpointTab`} - key={index} - href={`${BASE_PATH}${tab.href}`} - onClick={(event: MouseEvent) => { - event.preventDefault(); - history.push(tab.href); - }} - isSelected={ - tab.href === location.pathname || - (tab.href !== '/' && location.pathname.startsWith(tab.href)) - } - > - {tab.name} - </EuiTab> - ); - }); - }, [BASE_PATH, history, location.pathname]); - - return <EuiTabs>{tabList}</EuiTabs>; -}); diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/components/truncate_text.ts b/x-pack/plugins/endpoint/public/applications/endpoint/components/truncate_text.ts deleted file mode 100644 index 83f4bc1e79317..0000000000000 --- a/x-pack/plugins/endpoint/public/applications/endpoint/components/truncate_text.ts +++ /dev/null @@ -1,13 +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 styled from 'styled-components'; - -export const TruncateText = styled.div` - overflow: hidden; - white-space: nowrap; - text-overflow: ellipsis; -`; diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/index.tsx b/x-pack/plugins/endpoint/public/applications/endpoint/index.tsx index 82ac95160519c..a1999c056bf59 100644 --- a/x-pack/plugins/endpoint/public/applications/endpoint/index.tsx +++ b/x-pack/plugins/endpoint/public/applications/endpoint/index.tsx @@ -6,19 +6,10 @@ import * as React from 'react'; import ReactDOM from 'react-dom'; -import { CoreStart, AppMountParameters, ScopedHistory } from 'kibana/public'; -import { FormattedMessage } from '@kbn/i18n/react'; -import { Route, Switch } from 'react-router-dom'; -import { Store } from 'redux'; +import { CoreStart, AppMountParameters } from 'kibana/public'; import { EndpointPluginStartDependencies } from '../../plugin'; import { appStoreFactory } from './store'; -import { AlertIndex } from './view/alerts'; -import { HostList } from './view/hosts'; -import { PolicyList } from './view/policy'; -import { PolicyDetails } from './view/policy'; -import { HeaderNavigation } from './components/header_nav'; -import { AppRootProvider } from './view/app_root_provider'; -import { Setup } from './view/setup'; +import { AppRoot } from './view/app_root'; /** * This module will be loaded asynchronously to reduce the bundle size of your plugin's main bundle. @@ -37,41 +28,3 @@ export function renderApp( ReactDOM.unmountComponentAtNode(element); }; } - -interface RouterProps { - history: ScopedHistory; - store: Store; - coreStart: CoreStart; - depsStart: EndpointPluginStartDependencies; -} - -const AppRoot: React.FunctionComponent<RouterProps> = React.memo( - ({ history, store, coreStart, depsStart }) => { - return ( - <AppRootProvider store={store} history={history} coreStart={coreStart} depsStart={depsStart}> - <Setup ingestManager={depsStart.ingestManager} notifications={coreStart.notifications} /> - <HeaderNavigation /> - <Switch> - <Route - exact - path="/" - render={() => ( - <h1 data-test-subj="welcomeTitle"> - <FormattedMessage id="xpack.endpoint.welcomeTitle" defaultMessage="Hello World" /> - </h1> - )} - /> - <Route path="/hosts" component={HostList} /> - <Route path="/alerts" component={AlertIndex} /> - <Route path="/policy" exact component={PolicyList} /> - <Route path="/policy/:id" exact component={PolicyDetails} /> - <Route - render={() => ( - <FormattedMessage id="xpack.endpoint.notFound" defaultMessage="Page Not Found" /> - )} - /> - </Switch> - </AppRootProvider> - ); - } -); diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/lib/index.ts b/x-pack/plugins/endpoint/public/applications/endpoint/lib/index.ts deleted file mode 100644 index ba2e1ce8f9fe6..0000000000000 --- a/x-pack/plugins/endpoint/public/applications/endpoint/lib/index.ts +++ /dev/null @@ -1,7 +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. - */ - -export * from './saga'; diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/lib/saga.test.ts b/x-pack/plugins/endpoint/public/applications/endpoint/lib/saga.test.ts deleted file mode 100644 index 7c06681184085..0000000000000 --- a/x-pack/plugins/endpoint/public/applications/endpoint/lib/saga.test.ts +++ /dev/null @@ -1,114 +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 { createSagaMiddleware, SagaContext, SagaMiddleware } from './index'; -import { applyMiddleware, createStore, Reducer, Store } from 'redux'; - -describe('saga', () => { - const INCREMENT_COUNTER = 'INCREMENT'; - const DELAYED_INCREMENT_COUNTER = 'DELAYED INCREMENT COUNTER'; - const STOP_SAGA_PROCESSING = 'BREAK ASYNC ITERATOR'; - - const sleep = (ms = 100) => new Promise(resolve => setTimeout(resolve, ms)); - let store: Store; - let reducerA: Reducer; - let sideAffect: (a: unknown, s: unknown) => void; - let sagaExe: (sagaContext: SagaContext) => Promise<void>; - let sagaExeReduxMiddleware: SagaMiddleware; - - beforeEach(() => { - reducerA = jest.fn((prevState = { count: 0 }, { type }) => { - switch (type) { - case INCREMENT_COUNTER: - return { ...prevState, count: prevState.count + 1 }; - default: - return prevState; - } - }); - - sideAffect = jest.fn(); - - sagaExe = jest.fn(async ({ actionsAndState, dispatch }: SagaContext) => { - for await (const { action, state } of actionsAndState()) { - expect(action).toBeDefined(); - expect(state).toBeDefined(); - - if (action.type === STOP_SAGA_PROCESSING) { - break; - } - - sideAffect(action, state); - - if (action.type === DELAYED_INCREMENT_COUNTER) { - await sleep(1); - dispatch({ - type: INCREMENT_COUNTER, - }); - } - } - }); - - sagaExeReduxMiddleware = createSagaMiddleware(sagaExe); - store = createStore(reducerA, applyMiddleware(sagaExeReduxMiddleware)); - }); - - afterEach(() => { - sagaExeReduxMiddleware.stop(); - }); - - test('it does nothing if saga is not started', () => { - expect(sagaExe).not.toHaveBeenCalled(); - }); - - test('it can dispatch store actions once running', async () => { - sagaExeReduxMiddleware.start(); - expect(store.getState()).toEqual({ count: 0 }); - expect(sagaExe).toHaveBeenCalled(); - - store.dispatch({ type: DELAYED_INCREMENT_COUNTER }); - expect(store.getState()).toEqual({ count: 0 }); - - await sleep(); - - expect(sideAffect).toHaveBeenCalled(); - expect(store.getState()).toEqual({ count: 1 }); - }); - - test('it stops processing if break out of loop', async () => { - sagaExeReduxMiddleware.start(); - store.dispatch({ type: DELAYED_INCREMENT_COUNTER }); - await sleep(); - - expect(store.getState()).toEqual({ count: 1 }); - expect(sideAffect).toHaveBeenCalledTimes(2); - - store.dispatch({ type: STOP_SAGA_PROCESSING }); - await sleep(); - - store.dispatch({ type: DELAYED_INCREMENT_COUNTER }); - await sleep(); - - expect(store.getState()).toEqual({ count: 1 }); - expect(sideAffect).toHaveBeenCalledTimes(2); - }); - - test('it stops saga middleware when stop() is called', async () => { - sagaExeReduxMiddleware.start(); - store.dispatch({ type: DELAYED_INCREMENT_COUNTER }); - await sleep(); - - expect(store.getState()).toEqual({ count: 1 }); - expect(sideAffect).toHaveBeenCalledTimes(2); - - sagaExeReduxMiddleware.stop(); - - store.dispatch({ type: DELAYED_INCREMENT_COUNTER }); - await sleep(); - - expect(store.getState()).toEqual({ count: 1 }); - expect(sideAffect).toHaveBeenCalledTimes(2); - }); -}); diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/lib/saga.ts b/x-pack/plugins/endpoint/public/applications/endpoint/lib/saga.ts deleted file mode 100644 index 2a79827847f2e..0000000000000 --- a/x-pack/plugins/endpoint/public/applications/endpoint/lib/saga.ts +++ /dev/null @@ -1,159 +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 { AnyAction, Dispatch, Middleware, MiddlewareAPI } from 'redux'; -import { GlobalState } from '../types'; - -interface QueuedAction<TAction = AnyAction> { - /** - * The Redux action that was dispatched - */ - action: TAction; - /** - * The Global state at the time the action was dispatched - */ - state: GlobalState; -} - -interface IteratorInstance { - queue: QueuedAction[]; - nextResolve: null | ((inst: QueuedAction) => void); -} - -type Saga = (storeContext: SagaContext) => Promise<void>; - -type StoreActionsAndState<TAction = AnyAction> = AsyncIterableIterator<QueuedAction<TAction>>; - -export interface SagaContext<TAction extends AnyAction = AnyAction> { - /** - * A generator function that will `yield` `Promise`s that resolve with a `QueuedAction` - */ - actionsAndState: () => StoreActionsAndState<TAction>; - dispatch: Dispatch<TAction>; -} - -export interface SagaMiddleware extends Middleware { - /** - * Start the saga. Should be called after the `store` has been created - */ - start: () => void; - - /** - * Stop the saga by exiting the internal generator `for await...of` loop. - */ - stop: () => void; -} - -const noop = () => {}; -const STOP = Symbol('STOP'); - -/** - * Creates Saga Middleware for use with Redux. - * - * @param {Saga} saga The `saga` should initialize a long-running `for await...of` loop against - * the return value of the `actionsAndState()` method provided by the `SagaContext`. - * - * @return {SagaMiddleware} - * - * @example - * - * type TPossibleActions = { type: 'add', payload: any[] }; - * //... - * const endpointsSaga = async ({ actionsAndState, dispatch }: SagaContext<TPossibleActions>) => { - * for await (const { action, state } of actionsAndState()) { - * if (action.type === "userRequestedResource") { - * const resourceData = await doApiFetch('of/some/resource'); - * dispatch({ - * type: 'add', - * payload: [ resourceData ] - * }); - * } - * } - * } - * const endpointsSagaMiddleware = createSagaMiddleware(endpointsSaga); - * //.... - * const store = createStore(reducers, [ endpointsSagaMiddleware ]); - */ -export function createSagaMiddleware(saga: Saga): SagaMiddleware { - const iteratorInstances = new Set<IteratorInstance>(); - let runSaga: () => void = noop; - let stopSaga: () => void = noop; - let runningPromise: Promise<symbol>; - - async function* getActionsAndStateIterator(): StoreActionsAndState { - const instance: IteratorInstance = { queue: [], nextResolve: null }; - iteratorInstances.add(instance); - - try { - while (true) { - const actionAndState = await Promise.race([nextActionAndState(), runningPromise]); - - if (actionAndState === STOP) { - break; - } - - yield actionAndState as QueuedAction; - } - } finally { - // If the consumer stops consuming this (e.g. `break` or `return` is called in the `for await` - // then this `finally` block will run and unregister this instance and reset `runSaga` - iteratorInstances.delete(instance); - runSaga = stopSaga = noop; - } - - function nextActionAndState() { - if (instance.queue.length) { - return Promise.resolve(instance.queue.shift() as QueuedAction); - } else { - return new Promise<QueuedAction>(function(resolve) { - instance.nextResolve = resolve; - }); - } - } - } - - function enqueue(value: QueuedAction) { - for (const iteratorInstance of iteratorInstances) { - iteratorInstance.queue.push(value); - if (iteratorInstance.nextResolve !== null) { - iteratorInstance.nextResolve(iteratorInstance.queue.shift() as QueuedAction); - iteratorInstance.nextResolve = null; - } - } - } - - function middleware({ getState, dispatch }: MiddlewareAPI) { - if (runSaga === noop) { - runSaga = saga.bind<null, SagaContext, any[], Promise<void>>(null, { - actionsAndState: getActionsAndStateIterator, - dispatch, - }); - } - return (next: Dispatch<AnyAction>) => (action: AnyAction) => { - // Call the next dispatch method in the middleware chain. - const returnValue = next(action); - - enqueue({ - action, - state: getState(), - }); - - // This will likely be the action itself, unless a middleware further in chain changed it. - return returnValue; - }; - } - - middleware.start = () => { - runningPromise = new Promise(resolve => (stopSaga = () => resolve(STOP))); - runSaga(); - }; - - middleware.stop = () => { - stopSaga(); - }; - - return middleware; -} diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/mocks/app_context_render.tsx b/x-pack/plugins/endpoint/public/applications/endpoint/mocks/app_context_render.tsx index 7cb1031ef9a09..639b1f7252d7f 100644 --- a/x-pack/plugins/endpoint/public/applications/endpoint/mocks/app_context_render.tsx +++ b/x-pack/plugins/endpoint/public/applications/endpoint/mocks/app_context_render.tsx @@ -12,6 +12,7 @@ import { coreMock } from '../../../../../../../src/core/public/mocks'; import { EndpointPluginStartDependencies } from '../../../plugin'; import { depsStartMock } from './dependencies_start_mock'; import { AppRootProvider } from '../view/app_root_provider'; +import { createSpyMiddleware, MiddlewareActionSpyHelper } from '../store/test_utils'; type UiRender = (ui: React.ReactElement, options?: RenderOptions) => RenderResult; @@ -23,6 +24,7 @@ export interface AppContextTestRender { history: ReturnType<typeof createMemoryHistory>; coreStart: ReturnType<typeof coreMock.createStart>; depsStart: EndpointPluginStartDependencies; + middlewareSpy: MiddlewareActionSpyHelper; /** * A wrapper around `AppRootContext` component. Uses the mocked modules as input to the * `AppRootContext` @@ -45,7 +47,12 @@ export const createAppRootMockRenderer = (): AppContextTestRender => { const history = createMemoryHistory<never>(); const coreStart = coreMock.createStart({ basePath: '/mock' }); const depsStart = depsStartMock(); - const store = appStoreFactory({ coreStart, depsStart }); + const middlewareSpy = createSpyMiddleware(); + const store = appStoreFactory({ + coreStart, + depsStart, + additionalMiddleware: [middlewareSpy.actionSpyMiddleware], + }); const AppWrapper: React.FunctionComponent<{ children: React.ReactElement }> = ({ children }) => ( <AppRootProvider store={store} history={history} coreStart={coreStart} depsStart={depsStart}> {children} @@ -64,6 +71,7 @@ export const createAppRootMockRenderer = (): AppContextTestRender => { history, coreStart, depsStart, + middlewareSpy, AppWrapper, render, }; diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/models/index_pattern.ts b/x-pack/plugins/endpoint/public/applications/endpoint/models/index_pattern.ts new file mode 100644 index 0000000000000..0cae054432f96 --- /dev/null +++ b/x-pack/plugins/endpoint/public/applications/endpoint/models/index_pattern.ts @@ -0,0 +1,16 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { all } from 'deepmerge'; +import { Immutable } from '../../../../common/types'; +import { IIndexPattern } from '../../../../../../../src/plugins/data/common'; + +/** + * Model for the `IIndexPattern` interface exported by the `data` plugin. + */ +export function clone(value: IIndexPattern | Immutable<IIndexPattern>): IIndexPattern { + return all([value]) as IIndexPattern; +} diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/models/policy.ts b/x-pack/plugins/endpoint/public/applications/endpoint/models/policy.ts deleted file mode 100644 index 9ac53f9be609f..0000000000000 --- a/x-pack/plugins/endpoint/public/applications/endpoint/models/policy.ts +++ /dev/null @@ -1,90 +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 { PolicyConfig, ProtectionModes } from '../types'; - -/** - * Generate a new Policy model. - * NOTE: in the near future, this will likely be removed and an API call to EPM will be used to retrieve - * the latest from the Endpoint package - */ -export const generatePolicy = (): PolicyConfig => { - return { - windows: { - events: { - process: true, - network: true, - }, - malware: { - mode: ProtectionModes.prevent, - }, - logging: { - stdout: 'debug', - file: 'info', - }, - advanced: { - elasticsearch: { - indices: { - control: 'control-index', - event: 'event-index', - logging: 'logging-index', - }, - kernel: { - connect: true, - process: true, - }, - }, - }, - }, - mac: { - events: { - process: true, - }, - malware: { - mode: ProtectionModes.detect, - }, - logging: { - stdout: 'debug', - file: 'info', - }, - advanced: { - elasticsearch: { - indices: { - control: 'control-index', - event: 'event-index', - logging: 'logging-index', - }, - kernel: { - connect: true, - process: true, - }, - }, - }, - }, - linux: { - events: { - process: true, - }, - logging: { - stdout: 'debug', - file: 'info', - }, - advanced: { - elasticsearch: { - indices: { - control: 'control-index', - event: 'event-index', - logging: 'logging-index', - }, - kernel: { - connect: true, - process: true, - }, - }, - }, - }, - }; -}; diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/models/policy_details_config.ts b/x-pack/plugins/endpoint/public/applications/endpoint/models/policy_details_config.ts index 1145d1d19242a..3e56b1ff14d65 100644 --- a/x-pack/plugins/endpoint/public/applications/endpoint/models/policy_details_config.ts +++ b/x-pack/plugins/endpoint/public/applications/endpoint/models/policy_details_config.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { UIPolicyConfig } from '../types'; +import { UIPolicyConfig } from '../../../../common/types'; /** * A typed Object.entries() function where the keys and values are typed based on the given object @@ -43,3 +43,33 @@ export function clone(policyDetailsConfig: UIPolicyConfig): UIPolicyConfig { */ return clonedConfig as UIPolicyConfig; } + +/** + * Returns value from `configuration` + */ +export const getIn = (a: UIPolicyConfig) => <Key extends keyof UIPolicyConfig>(key: Key) => < + subKey extends keyof UIPolicyConfig[Key] +>( + subKey: subKey +) => <LeafKey extends keyof UIPolicyConfig[Key][subKey]>( + leafKey: LeafKey +): UIPolicyConfig[Key][subKey][LeafKey] => { + return a[key][subKey][leafKey]; +}; + +/** + * Returns cloned `configuration` with `value` set by the `keyPath`. + */ +export const setIn = (a: UIPolicyConfig) => <Key extends keyof UIPolicyConfig>(key: Key) => < + subKey extends keyof UIPolicyConfig[Key] +>( + subKey: subKey +) => <LeafKey extends keyof UIPolicyConfig[Key][subKey]>(leafKey: LeafKey) => < + V extends UIPolicyConfig[Key][subKey][LeafKey] +>( + v: V +): UIPolicyConfig => { + const c = clone(a); + c[key][subKey][leafKey] = v; + return c; +}; diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/services/ingest.ts b/x-pack/plugins/endpoint/public/applications/endpoint/services/ingest.ts deleted file mode 100644 index 583ebc55d896b..0000000000000 --- a/x-pack/plugins/endpoint/public/applications/endpoint/services/ingest.ts +++ /dev/null @@ -1,114 +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 { HttpFetchOptions, HttpStart } from 'kibana/public'; -import { - CreateDatasourceResponse, - GetAgentStatusResponse, - GetDatasourcesRequest, -} from '../../../../../ingest_manager/common/types/rest_spec'; -import { NewPolicyData, PolicyData } from '../types'; - -const INGEST_API_ROOT = `/api/ingest_manager`; -const INGEST_API_DATASOURCES = `${INGEST_API_ROOT}/datasources`; -const INGEST_API_FLEET = `${INGEST_API_ROOT}/fleet`; -const INGEST_API_FLEET_AGENT_STATUS = `${INGEST_API_FLEET}/agent-status`; - -// FIXME: Import from ingest after - https://github.com/elastic/kibana/issues/60677 -export interface GetDatasourcesResponse { - items: PolicyData[]; - total: number; - page: number; - perPage: number; - success: boolean; -} - -// FIXME: Import from Ingest after - https://github.com/elastic/kibana/issues/60677 -export interface GetDatasourceResponse { - item: PolicyData; - success: boolean; -} - -// FIXME: Import from Ingest after - https://github.com/elastic/kibana/issues/60677 -export type UpdateDatasourceResponse = CreateDatasourceResponse & { - item: PolicyData; -}; - -/** - * Retrieves a list of endpoint specific datasources (those created with a `package.name` of - * `endpoint`) from Ingest - * @param http - * @param options - */ -export const sendGetEndpointSpecificDatasources = ( - http: HttpStart, - options: HttpFetchOptions & Partial<GetDatasourcesRequest> = {} -): Promise<GetDatasourcesResponse> => { - return http.get<GetDatasourcesResponse>(INGEST_API_DATASOURCES, { - ...options, - query: { - ...options.query, - kuery: `${ - options?.query?.kuery ? options.query.kuery + ' and ' : '' - }datasources.package.name: endpoint`, - }, - }); -}; - -/** - * Retrieves a single datasource based on ID from ingest - * @param http - * @param datasourceId - * @param options - */ -export const sendGetDatasource = ( - http: HttpStart, - datasourceId: string, - options?: HttpFetchOptions -) => { - return http.get<GetDatasourceResponse>(`${INGEST_API_DATASOURCES}/${datasourceId}`, options); -}; - -/** - * Updates a datasources - * - * @param http - * @param datasourceId - * @param datasource - * @param options - */ -export const sendPutDatasource = ( - http: HttpStart, - datasourceId: string, - datasource: NewPolicyData, - options: Exclude<HttpFetchOptions, 'body'> = {} -): Promise<UpdateDatasourceResponse> => { - return http.put(`${INGEST_API_DATASOURCES}/${datasourceId}`, { - ...options, - body: JSON.stringify(datasource), - }); -}; - -/** - * Get a status summary for all Agents that are currently assigned to a given agent configuration - * - * @param http - * @param configId - * @param options - */ -export const sendGetFleetAgentStatusForConfig = ( - http: HttpStart, - /** the Agent (fleet) configuration id */ - configId: string, - options: Exclude<HttpFetchOptions, 'query'> = {} -): Promise<GetAgentStatusResponse> => { - return http.get(INGEST_API_FLEET_AGENT_STATUS, { - ...options, - query: { - configId, - }, - }); -}; diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/store/action.ts b/x-pack/plugins/endpoint/public/applications/endpoint/store/action.ts index 2dce8ead38584..a32ecb4b45561 100644 --- a/x-pack/plugins/endpoint/public/applications/endpoint/store/action.ts +++ b/x-pack/plugins/endpoint/public/applications/endpoint/store/action.ts @@ -10,6 +10,9 @@ import { RoutingAction } from './routing'; import { PolicyListAction } from './policy_list'; import { PolicyDetailsAction } from './policy_details'; +/** + * The entire set of redux actions recognized by our reducer. + */ export type AppAction = | HostAction | AlertAction diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/store/alerts/alert_details.test.ts b/x-pack/plugins/endpoint/public/applications/endpoint/store/alerts/alert_details.test.ts index 79e9de9c67352..feac8944f476b 100644 --- a/x-pack/plugins/endpoint/public/applications/endpoint/store/alerts/alert_details.test.ts +++ b/x-pack/plugins/endpoint/public/applications/endpoint/store/alerts/alert_details.test.ts @@ -14,16 +14,17 @@ import { coreMock } from 'src/core/public/mocks'; import { DepsStartMock, depsStartMock } from '../../mocks'; import { createBrowserHistory } from 'history'; import { mockAlertResultList } from './mock_alert_result_list'; +import { Immutable } from '../../../../../common/types'; describe('alert details tests', () => { - let store: Store<AlertListState, AppAction>; + let store: Store<Immutable<AlertListState>, Immutable<AppAction>>; let coreStart: ReturnType<typeof coreMock.createStart>; let depsStart: DepsStartMock; let history: History<never>; /** * A function that waits until a selector returns true. */ - let selectorIsTrue: (selector: (state: AlertListState) => boolean) => Promise<void>; + let selectorIsTrue: (selector: (state: Immutable<AlertListState>) => boolean) => Promise<void>; beforeEach(() => { coreStart = coreMock.createStart(); depsStart = depsStartMock(); diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/store/alerts/alert_list.test.ts b/x-pack/plugins/endpoint/public/applications/endpoint/store/alerts/alert_list.test.ts index b1cc2d46f614a..84281813312e0 100644 --- a/x-pack/plugins/endpoint/public/applications/endpoint/store/alerts/alert_list.test.ts +++ b/x-pack/plugins/endpoint/public/applications/endpoint/store/alerts/alert_list.test.ts @@ -12,20 +12,20 @@ import { alertMiddlewareFactory } from './middleware'; import { AppAction } from '../action'; import { coreMock } from 'src/core/public/mocks'; import { DepsStartMock, depsStartMock } from '../../mocks'; -import { AlertResultList } from '../../../../../common/types'; +import { AlertResultList, Immutable } from '../../../../../common/types'; import { isOnAlertPage } from './selectors'; import { createBrowserHistory } from 'history'; import { mockAlertResultList } from './mock_alert_result_list'; describe('alert list tests', () => { - let store: Store<AlertListState, AppAction>; + let store: Store<Immutable<AlertListState>, Immutable<AppAction>>; let coreStart: ReturnType<typeof coreMock.createStart>; let depsStart: DepsStartMock; let history: History<never>; /** * A function that waits until a selector returns true. */ - let selectorIsTrue: (selector: (state: AlertListState) => boolean) => Promise<void>; + let selectorIsTrue: (selector: (state: Immutable<AlertListState>) => boolean) => Promise<void>; beforeEach(() => { coreStart = coreMock.createStart(); depsStart = depsStartMock(); diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/store/alerts/alert_list_pagination.test.ts b/x-pack/plugins/endpoint/public/applications/endpoint/store/alerts/alert_list_pagination.test.ts index bb5893f14287b..4cc86e9c0449c 100644 --- a/x-pack/plugins/endpoint/public/applications/endpoint/store/alerts/alert_list_pagination.test.ts +++ b/x-pack/plugins/endpoint/public/applications/endpoint/store/alerts/alert_list_pagination.test.ts @@ -15,9 +15,10 @@ import { DepsStartMock, depsStartMock } from '../../mocks'; import { createBrowserHistory } from 'history'; import { uiQueryParams } from './selectors'; import { urlFromQueryParams } from '../../view/alerts/url_from_query_params'; +import { Immutable } from '../../../../../common/types'; describe('alert list pagination', () => { - let store: Store<AlertListState, AppAction>; + let store: Store<Immutable<AlertListState>, Immutable<AppAction>>; let coreStart: ReturnType<typeof coreMock.createStart>; let depsStart: DepsStartMock; let history: History<never>; diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/store/alerts/index.ts b/x-pack/plugins/endpoint/public/applications/endpoint/store/alerts/index.ts index f63910a1c305e..5545218d9abd6 100644 --- a/x-pack/plugins/endpoint/public/applications/endpoint/store/alerts/index.ts +++ b/x-pack/plugins/endpoint/public/applications/endpoint/store/alerts/index.ts @@ -6,4 +6,3 @@ export { alertListReducer } from './reducer'; export { AlertAction } from './action'; -export * from '../../types'; diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/store/alerts/middleware.ts b/x-pack/plugins/endpoint/public/applications/endpoint/store/alerts/middleware.ts index 2c6ebf52189f5..90d6b8b82198a 100644 --- a/x-pack/plugins/endpoint/public/applications/endpoint/store/alerts/middleware.ts +++ b/x-pack/plugins/endpoint/public/applications/endpoint/store/alerts/middleware.ts @@ -6,26 +6,32 @@ import { IIndexPattern } from 'src/plugins/data/public'; import { AlertResultList, AlertDetails } from '../../../../../common/types'; -import { AppAction } from '../action'; -import { MiddlewareFactory, AlertListState } from '../../types'; +import { ImmutableMiddlewareFactory, AlertListState } from '../../types'; import { isOnAlertPage, apiQueryParams, hasSelectedAlert, uiQueryParams } from './selectors'; import { cloneHttpFetchQuery } from '../../../../common/clone_http_fetch_query'; import { EndpointAppConstants } from '../../../../../common/types'; -export const alertMiddlewareFactory: MiddlewareFactory<AlertListState> = (coreStart, depsStart) => { +export const alertMiddlewareFactory: ImmutableMiddlewareFactory<AlertListState> = ( + coreStart, + depsStart +) => { async function fetchIndexPatterns(): Promise<IIndexPattern[]> { const { indexPatterns } = depsStart.data; - const indexName = EndpointAppConstants.ALERT_INDEX_NAME; - const fields = await indexPatterns.getFieldsForWildcard({ pattern: indexName }); + const eventsPattern: { indexPattern: string } = await coreStart.http.get( + `${EndpointAppConstants.INDEX_PATTERN_ROUTE}/${EndpointAppConstants.EVENT_DATASET}` + ); + const fields = await indexPatterns.getFieldsForWildcard({ + pattern: eventsPattern.indexPattern, + }); const indexPattern: IIndexPattern = { - title: indexName, + title: eventsPattern.indexPattern, fields, }; return [indexPattern]; } - return api => next => async (action: AppAction) => { + return api => next => async action => { next(action); const state = api.getState(); if (action.type === 'userChangedUrl' && isOnAlertPage(state)) { diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/store/alerts/reducer.ts b/x-pack/plugins/endpoint/public/applications/endpoint/store/alerts/reducer.ts index 4430a4d39cf4a..52b91dcae7d70 100644 --- a/x-pack/plugins/endpoint/public/applications/endpoint/store/alerts/reducer.ts +++ b/x-pack/plugins/endpoint/public/applications/endpoint/store/alerts/reducer.ts @@ -4,11 +4,11 @@ * you may not use this file except in compliance with the Elastic License. */ -import { Reducer } from 'redux'; -import { AlertListState } from '../../types'; +import { AlertListState, ImmutableReducer } from '../../types'; import { AppAction } from '../action'; +import { Immutable } from '../../../../../common/types'; -const initialState = (): AlertListState => { +const initialState = (): Immutable<AlertListState> => { return { alerts: [], alertDetails: undefined, @@ -22,7 +22,7 @@ const initialState = (): AlertListState => { }; }; -export const alertListReducer: Reducer<AlertListState, AppAction> = ( +export const alertListReducer: ImmutableReducer<AlertListState, AppAction> = ( state = initialState(), action ) => { diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/store/alerts/selectors.ts b/x-pack/plugins/endpoint/public/applications/endpoint/store/alerts/selectors.ts index 5e9b08c09c2c7..cc362c3701956 100644 --- a/x-pack/plugins/endpoint/public/applications/endpoint/store/alerts/selectors.ts +++ b/x-pack/plugins/endpoint/public/applications/endpoint/store/alerts/selectors.ts @@ -19,23 +19,23 @@ const createStructuredSelector: CreateStructuredSelector = createStructuredSelec /** * Returns the Alert Data array from state */ -export const alertListData = (state: AlertListState) => state.alerts; +export const alertListData = (state: Immutable<AlertListState>) => state.alerts; -export const selectedAlertDetailsData = (state: AlertListState) => state.alertDetails; +export const selectedAlertDetailsData = (state: Immutable<AlertListState>) => state.alertDetails; /** * Returns the alert list pagination data from state */ export const alertListPagination = createStructuredSelector({ - pageIndex: (state: AlertListState) => state.pageIndex, - pageSize: (state: AlertListState) => state.pageSize, - total: (state: AlertListState) => state.total, + pageIndex: (state: Immutable<AlertListState>) => state.pageIndex, + pageSize: (state: Immutable<AlertListState>) => state.pageSize, + total: (state: Immutable<AlertListState>) => state.total, }); /** * Returns a boolean based on whether or not the user is on the alerts page */ -export const isOnAlertPage = (state: AlertListState): boolean => { +export const isOnAlertPage = (state: Immutable<AlertListState>): boolean => { return state.location ? state.location.pathname === '/alerts' : false; }; @@ -44,10 +44,10 @@ export const isOnAlertPage = (state: AlertListState): boolean => { * Used to calculate urls for links and such. */ export const uiQueryParams: ( - state: AlertListState + state: Immutable<AlertListState> ) => Immutable<AlertingIndexUIQueryParams> = createSelector( - (state: AlertListState) => state.location, - (location: AlertListState['location']) => { + state => state.location, + (location: Immutable<AlertListState>['location']) => { const data: AlertingIndexUIQueryParams = {}; if (location) { // Removes the `?` from the beginning of query string if it exists @@ -82,7 +82,7 @@ export const uiQueryParams: ( * Parses the ui query params and returns a object that represents the query used by the SearchBar component. * If the query url param is undefined, a default is returned. */ -export const searchBarQuery: (state: AlertListState) => Query = createSelector( +export const searchBarQuery: (state: Immutable<AlertListState>) => Query = createSelector( uiQueryParams, ({ query }) => { if (query !== undefined) { @@ -97,21 +97,20 @@ export const searchBarQuery: (state: AlertListState) => Query = createSelector( * Parses the ui query params and returns a rison encoded string that represents the search bar's date range. * A default is provided if 'date_range' is not present in the url params. */ -export const encodedSearchBarDateRange: (state: AlertListState) => string = createSelector( - uiQueryParams, - ({ date_range: dateRange }) => { - if (dateRange === undefined) { - return encode({ from: 'now-24h', to: 'now' }); - } else { - return dateRange; - } +export const encodedSearchBarDateRange: ( + state: Immutable<AlertListState> +) => string = createSelector(uiQueryParams, ({ date_range: dateRange }) => { + if (dateRange === undefined) { + return encode({ from: 'now-24h', to: 'now' }); + } else { + return dateRange; } -); +}); /** * Parses the ui query params and returns a object that represents the dateRange used by the SearchBar component. */ -export const searchBarDateRange: (state: AlertListState) => TimeRange = createSelector( +export const searchBarDateRange: (state: Immutable<AlertListState>) => TimeRange = createSelector( encodedSearchBarDateRange, encodedDateRange => { return (decode(encodedDateRange) as unknown) as TimeRange; @@ -122,7 +121,7 @@ export const searchBarDateRange: (state: AlertListState) => TimeRange = createSe * Parses the ui query params and returns an array of filters used by the SearchBar component. * If the 'filters' param is not present, a default is returned. */ -export const searchBarFilters: (state: AlertListState) => Filter[] = createSelector( +export const searchBarFilters: (state: Immutable<AlertListState>) => Filter[] = createSelector( uiQueryParams, ({ filters }) => { if (filters !== undefined) { @@ -136,13 +135,14 @@ export const searchBarFilters: (state: AlertListState) => Filter[] = createSelec /** * Returns the indexPatterns used by the SearchBar component */ -export const searchBarIndexPatterns = (state: AlertListState) => state.searchBar.patterns; +export const searchBarIndexPatterns = (state: Immutable<AlertListState>) => + state.searchBar.patterns; /** * query params to use when requesting alert data. */ export const apiQueryParams: ( - state: AlertListState + state: Immutable<AlertListState> ) => Immutable<AlertingIndexGetQueryInput> = createSelector( uiQueryParams, encodedSearchBarDateRange, @@ -161,7 +161,7 @@ export const apiQueryParams: ( * True if the user has selected an alert to see details about. * Populated via the browsers query params. */ -export const hasSelectedAlert: (state: AlertListState) => boolean = createSelector( +export const hasSelectedAlert: (state: Immutable<AlertListState>) => boolean = createSelector( uiQueryParams, ({ selected_alert: selectedAlert }) => selectedAlert !== undefined ); diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/store/hosts/action.ts b/x-pack/plugins/endpoint/public/applications/endpoint/store/hosts/action.ts index 4dafa68ddb647..21871ec8ca849 100644 --- a/x-pack/plugins/endpoint/public/applications/endpoint/store/hosts/action.ts +++ b/x-pack/plugins/endpoint/public/applications/endpoint/store/hosts/action.ts @@ -27,14 +27,8 @@ interface UserPaginatedHostList { payload: HostListPagination; } -// Why is FakeActionWithNoPayload here, see: https://github.com/elastic/endpoint-app-team/issues/273 -interface FakeActionWithNoPayload { - type: 'fakeActionWithNoPayLoad'; -} - export type HostAction = | ServerReturnedHostList | ServerReturnedHostDetails | ServerFailedToReturnHostDetails - | UserPaginatedHostList - | FakeActionWithNoPayload; + | UserPaginatedHostList; diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/store/hosts/middleware.test.ts b/x-pack/plugins/endpoint/public/applications/endpoint/store/hosts/middleware.test.ts index 8c8578426aa29..8f39baddda00e 100644 --- a/x-pack/plugins/endpoint/public/applications/endpoint/store/hosts/middleware.test.ts +++ b/x-pack/plugins/endpoint/public/applications/endpoint/store/hosts/middleware.test.ts @@ -4,11 +4,11 @@ * you may not use this file except in compliance with the Elastic License. */ import { CoreStart, HttpSetup } from 'kibana/public'; -import { applyMiddleware, createStore, Dispatch, Store } from 'redux'; +import { applyMiddleware, createStore, Store } from 'redux'; import { coreMock } from '../../../../../../../../src/core/public/mocks'; import { History, createBrowserHistory } from 'history'; import { hostListReducer, hostMiddlewareFactory } from './index'; -import { HostResultList } from '../../../../../common/types'; +import { HostResultList, Immutable } from '../../../../../common/types'; import { HostListState } from '../../types'; import { AppAction } from '../action'; import { listData } from './selectors'; @@ -20,9 +20,10 @@ describe('host list middleware', () => { let fakeCoreStart: jest.Mocked<CoreStart>; let depsStart: DepsStartMock; let fakeHttpServices: jest.Mocked<HttpSetup>; - let store: Store<HostListState>; - let getState: typeof store['getState']; - let dispatch: Dispatch<AppAction>; + type HostListStore = Store<Immutable<HostListState>, Immutable<AppAction>>; + let store: HostListStore; + let getState: HostListStore['getState']; + let dispatch: HostListStore['dispatch']; let history: History<never>; const getEndpointListApiResponse = (): HostResultList => { diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/store/hosts/middleware.ts b/x-pack/plugins/endpoint/public/applications/endpoint/store/hosts/middleware.ts index 9481b6633f12e..83e11f5408bcd 100644 --- a/x-pack/plugins/endpoint/public/applications/endpoint/store/hosts/middleware.ts +++ b/x-pack/plugins/endpoint/public/applications/endpoint/store/hosts/middleware.ts @@ -4,13 +4,12 @@ * you may not use this file except in compliance with the Elastic License. */ -import { MiddlewareFactory } from '../../types'; +import { ImmutableMiddlewareFactory } from '../../types'; import { pageIndex, pageSize, isOnHostPage, hasSelectedHost, uiQueryParams } from './selectors'; import { HostListState } from '../../types'; -import { AppAction } from '../action'; -export const hostMiddlewareFactory: MiddlewareFactory<HostListState> = coreStart => { - return ({ getState, dispatch }) => next => async (action: AppAction) => { +export const hostMiddlewareFactory: ImmutableMiddlewareFactory<HostListState> = coreStart => { + return ({ getState, dispatch }) => next => async action => { next(action); const state = getState(); if ( diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/store/hosts/reducer.ts b/x-pack/plugins/endpoint/public/applications/endpoint/store/hosts/reducer.ts index ad6741dab7be7..298e819645dbe 100644 --- a/x-pack/plugins/endpoint/public/applications/endpoint/store/hosts/reducer.ts +++ b/x-pack/plugins/endpoint/public/applications/endpoint/store/hosts/reducer.ts @@ -4,8 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { Reducer } from 'redux'; -import { HostListState } from '../../types'; +import { HostListState, ImmutableReducer } from '../../types'; import { AppAction } from '../action'; const initialState = (): HostListState => { @@ -21,7 +20,7 @@ const initialState = (): HostListState => { }; }; -export const hostListReducer: Reducer<HostListState, AppAction> = ( +export const hostListReducer: ImmutableReducer<HostListState, AppAction> = ( state = initialState(), action ) => { diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/store/hosts/selectors.ts b/x-pack/plugins/endpoint/public/applications/endpoint/store/hosts/selectors.ts index ebe310cb51190..03cdba8505800 100644 --- a/x-pack/plugins/endpoint/public/applications/endpoint/store/hosts/selectors.ts +++ b/x-pack/plugins/endpoint/public/applications/endpoint/store/hosts/selectors.ts @@ -8,36 +8,36 @@ import { createSelector } from 'reselect'; import { Immutable } from '../../../../../common/types'; import { HostListState, HostIndexUIQueryParams } from '../../types'; -export const listData = (state: HostListState) => state.hosts; +export const listData = (state: Immutable<HostListState>) => state.hosts; -export const pageIndex = (state: HostListState) => state.pageIndex; +export const pageIndex = (state: Immutable<HostListState>) => state.pageIndex; -export const pageSize = (state: HostListState) => state.pageSize; +export const pageSize = (state: Immutable<HostListState>) => state.pageSize; -export const totalHits = (state: HostListState) => state.total; +export const totalHits = (state: Immutable<HostListState>) => state.total; -export const isLoading = (state: HostListState) => state.loading; +export const isLoading = (state: Immutable<HostListState>) => state.loading; -export const detailsError = (state: HostListState) => state.detailsError; +export const detailsError = (state: Immutable<HostListState>) => state.detailsError; -export const detailsData = (state: HostListState) => { +export const detailsData = (state: Immutable<HostListState>) => { return state.details; }; -export const isOnHostPage = (state: HostListState) => +export const isOnHostPage = (state: Immutable<HostListState>) => state.location ? state.location.pathname === '/hosts' : false; export const uiQueryParams: ( - state: HostListState + state: Immutable<HostListState> ) => Immutable<HostIndexUIQueryParams> = createSelector( - (state: HostListState) => state.location, - (location: HostListState['location']) => { + (state: Immutable<HostListState>) => state.location, + (location: Immutable<HostListState>['location']) => { const data: HostIndexUIQueryParams = {}; if (location) { // Removes the `?` from the beginning of query string if it exists const query = querystring.parse(location.search.slice(1)); - const keys: Array<keyof HostIndexUIQueryParams> = ['selected_host']; + const keys: Array<keyof HostIndexUIQueryParams> = ['selected_host', 'show']; for (const key of keys) { const value = query[key]; @@ -52,9 +52,17 @@ export const uiQueryParams: ( } ); -export const hasSelectedHost: (state: HostListState) => boolean = createSelector( +export const hasSelectedHost: (state: Immutable<HostListState>) => boolean = createSelector( uiQueryParams, ({ selected_host: selectedHost }) => { return selectedHost !== undefined; } ); + +/** What policy details panel view to show */ +export const showView: (state: HostListState) => 'policy_response' | 'details' = createSelector( + uiQueryParams, + searchParams => { + return searchParams.show === 'policy_response' ? 'policy_response' : 'details'; + } +); diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/store/immutable_combine_reducers.ts b/x-pack/plugins/endpoint/public/applications/endpoint/store/immutable_combine_reducers.ts new file mode 100644 index 0000000000000..6895f0106fb5d --- /dev/null +++ b/x-pack/plugins/endpoint/public/applications/endpoint/store/immutable_combine_reducers.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 { combineReducers } from 'redux'; +import { ImmutableCombineReducers } from '../types'; + +/** + * Works the same as `combineReducers` from `redux`, but uses the `ImmutableCombineReducers` type. + */ +export const immutableCombineReducers: ImmutableCombineReducers = combineReducers; diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/store/index.ts b/x-pack/plugins/endpoint/public/applications/endpoint/store/index.ts index efa79b163d3b6..a4d0b3a8b9815 100644 --- a/x-pack/plugins/endpoint/public/applications/endpoint/store/index.ts +++ b/x-pack/plugins/endpoint/public/applications/endpoint/store/index.ts @@ -4,44 +4,25 @@ * you may not use this file except in compliance with the Elastic License. */ -import { - createStore, - compose, - applyMiddleware, - Store, - MiddlewareAPI, - Dispatch, - Middleware, -} from 'redux'; +import { createStore, compose, applyMiddleware, Store } from 'redux'; import { CoreStart } from 'kibana/public'; import { appReducer } from './reducer'; import { alertMiddlewareFactory } from './alerts/middleware'; import { hostMiddlewareFactory } from './hosts'; import { policyListMiddlewareFactory } from './policy_list'; import { policyDetailsMiddlewareFactory } from './policy_details'; -import { GlobalState } from '../types'; -import { AppAction } from './action'; +import { ImmutableMiddlewareFactory, SubstateMiddlewareFactory } from '../types'; import { EndpointPluginStartDependencies } from '../../../plugin'; const composeWithReduxDevTools = (window as any).__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ ? (window as any).__REDUX_DEVTOOLS_EXTENSION_COMPOSE__({ name: 'EndpointApp' }) : compose; -export type Selector<S, R> = (state: S) => R; - -/** - * Wrap Redux Middleware and adjust 'getState()' to return the namespace from 'GlobalState that applies to the given Middleware concern. - * - * @param selector - * @param middleware - */ -export const substateMiddlewareFactory = <Substate>( - selector: Selector<GlobalState, Substate>, - middleware: Middleware<{}, Substate, Dispatch<AppAction>> -): Middleware<{}, GlobalState, Dispatch<AppAction>> => { +export const substateMiddlewareFactory: SubstateMiddlewareFactory = (selector, middleware) => { return api => { - const substateAPI: MiddlewareAPI<Dispatch<AppAction>, Substate> = { + const substateAPI = { ...api, + // Return just the substate instead of global state. getState() { return selector(api.getState()); }, @@ -62,10 +43,15 @@ export const appStoreFactory: (middlewareDeps?: { * Give middleware access to plugin start dependencies. */ depsStart: EndpointPluginStartDependencies; + /** + * Any additional Redux Middlewares + * (should only be used for testing - example: to inject the action spy middleware) + */ + additionalMiddleware?: Array<ReturnType<ImmutableMiddlewareFactory>>; }) => Store = middlewareDeps => { let middleware; if (middlewareDeps) { - const { coreStart, depsStart } = middlewareDeps; + const { coreStart, depsStart, additionalMiddleware = [] } = middlewareDeps; middleware = composeWithReduxDevTools( applyMiddleware( substateMiddlewareFactory( @@ -83,7 +69,9 @@ export const appStoreFactory: (middlewareDeps?: { substateMiddlewareFactory( globalState => globalState.alertList, alertMiddlewareFactory(coreStart, depsStart) - ) + ), + // Additional Middleware should go last + ...additionalMiddleware ) ); } else { diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/store/policy_details/action.ts b/x-pack/plugins/endpoint/public/applications/endpoint/store/policy_details/action.ts index 9905145048a8a..4de3dac02a8ec 100644 --- a/x-pack/plugins/endpoint/public/applications/endpoint/store/policy_details/action.ts +++ b/x-pack/plugins/endpoint/public/applications/endpoint/store/policy_details/action.ts @@ -4,8 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ -import { PolicyData, PolicyDetailsState, ServerApiError, UIPolicyConfig } from '../../types'; +import { PolicyDetailsState, ServerApiError } from '../../types'; import { GetAgentStatusResponse } from '../../../../../../ingest_manager/common/types/rest_spec'; +import { PolicyData, UIPolicyConfig } from '../../../../../common/types'; interface ServerReturnedPolicyDetailsData { type: 'serverReturnedPolicyDetailsData'; diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/store/policy_details/index.test.ts b/x-pack/plugins/endpoint/public/applications/endpoint/store/policy_details/index.test.ts index cf14092953227..a24687ebbcbbc 100644 --- a/x-pack/plugins/endpoint/public/applications/endpoint/store/policy_details/index.test.ts +++ b/x-pack/plugins/endpoint/public/applications/endpoint/store/policy_details/index.test.ts @@ -7,9 +7,9 @@ import { PolicyDetailsState } from '../../types'; import { createStore, Dispatch, Store } from 'redux'; import { policyDetailsReducer, PolicyDetailsAction } from './index'; -import { policyConfig, windowsEventing } from './selectors'; +import { policyConfig } from './selectors'; import { clone } from '../../models/policy_details_config'; -import { generatePolicy } from '../../models/policy'; +import { factory as policyConfigFactory } from '../../../../../common/models/policy_config'; describe('policy details: ', () => { let store: Store<PolicyDetailsState>; @@ -38,7 +38,7 @@ describe('policy details: ', () => { streams: [], config: { policy: { - value: generatePolicy(), + value: policyConfigFactory(), }, }, }, @@ -55,7 +55,7 @@ describe('policy details: ', () => { }); }); - describe('when the user has enabled windows process eventing', () => { + describe('when the user has enabled windows process events', () => { beforeEach(() => { const config = policyConfig(getState()); if (!config) { @@ -71,8 +71,53 @@ describe('policy details: ', () => { }); }); - it('windows process eventing is enabled', async () => { - expect(windowsEventing(getState())!.process).toEqual(true); + it('windows process events is enabled', () => { + const config = policyConfig(getState()); + expect(config!.windows.events.process).toEqual(true); + }); + }); + + describe('when the user has enabled mac file events', () => { + beforeEach(() => { + const config = policyConfig(getState()); + if (!config) { + throw new Error(); + } + + const newPayload1 = clone(config); + newPayload1.mac.events.file = true; + + dispatch({ + type: 'userChangedPolicyConfig', + payload: { policyConfig: newPayload1 }, + }); + }); + + it('mac file events is enabled', () => { + const config = policyConfig(getState()); + expect(config!.mac.events.file).toEqual(true); + }); + }); + + describe('when the user has enabled linux process events', () => { + beforeEach(() => { + const config = policyConfig(getState()); + if (!config) { + throw new Error(); + } + + const newPayload1 = clone(config); + newPayload1.linux.events.file = true; + + dispatch({ + type: 'userChangedPolicyConfig', + payload: { policyConfig: newPayload1 }, + }); + }); + + it('linux file events is enabled', () => { + const config = policyConfig(getState()); + expect(config!.linux.events.file).toEqual(true); }); }); }); diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/store/policy_details/middleware.ts b/x-pack/plugins/endpoint/public/applications/endpoint/store/policy_details/middleware.ts index 18248e272aada..7f17f5381fbda 100644 --- a/x-pack/plugins/endpoint/public/applications/endpoint/store/policy_details/middleware.ts +++ b/x-pack/plugins/endpoint/public/applications/endpoint/store/policy_details/middleware.ts @@ -4,17 +4,17 @@ * you may not use this file except in compliance with the Elastic License. */ -import { MiddlewareFactory, PolicyData, PolicyDetailsState } from '../../types'; +import { ImmutableMiddlewareFactory, PolicyDetailsState, UpdatePolicyResponse } from '../../types'; import { policyIdFromParams, isOnPolicyDetailsPage, policyDetails } from './selectors'; import { sendGetDatasource, sendGetFleetAgentStatusForConfig, sendPutDatasource, - UpdateDatasourceResponse, -} from '../../services/ingest'; -import { generatePolicy } from '../../models/policy'; +} from '../policy_list/services/ingest'; +import { PolicyData } from '../../../../../common/types'; +import { factory as policyConfigFactory } from '../../../../../common/models/policy_config'; -export const policyDetailsMiddlewareFactory: MiddlewareFactory<PolicyDetailsState> = coreStart => { +export const policyDetailsMiddlewareFactory: ImmutableMiddlewareFactory<PolicyDetailsState> = coreStart => { const http = coreStart.http; return ({ getState, dispatch }) => next => async action => { @@ -35,7 +35,6 @@ export const policyDetailsMiddlewareFactory: MiddlewareFactory<PolicyDetailsStat return; } - // FIXME: remove this code once the Default Policy is available in the endpoint package - see: https://github.com/elastic/endpoint-app-team/issues/295 // Until we get the Default configuration into the Endpoint package so that the datasource has // the expected data structure, we will add it here manually. if (!policyItem.inputs.length) { @@ -46,7 +45,7 @@ export const policyDetailsMiddlewareFactory: MiddlewareFactory<PolicyDetailsStat streams: [], config: { policy: { - value: generatePolicy(), + value: policyConfigFactory(), }, }, }, @@ -62,7 +61,6 @@ export const policyDetailsMiddlewareFactory: MiddlewareFactory<PolicyDetailsStat // Agent summary is secondary data, so its ok for it to come after the details // page is populated with the main content - // FIXME: need to only do this IF fleet is enabled - see: https://github.com/elastic/endpoint-app-team/issues/296 if (policyItem.config_id) { const { results } = await sendGetFleetAgentStatusForConfig(http, policyItem.config_id); dispatch({ @@ -75,7 +73,7 @@ export const policyDetailsMiddlewareFactory: MiddlewareFactory<PolicyDetailsStat } else if (action.type === 'userClickedPolicyDetailsSaveButton') { const { id, revision, ...updatedPolicyItem } = policyDetails(state) as PolicyData; - let apiResponse: UpdateDatasourceResponse; + let apiResponse: UpdatePolicyResponse; try { apiResponse = await sendPutDatasource(http, id, updatedPolicyItem); } catch (error) { diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/store/policy_details/reducer.ts b/x-pack/plugins/endpoint/public/applications/endpoint/store/policy_details/reducer.ts index fb3e26157ef32..9778f23d083a2 100644 --- a/x-pack/plugins/endpoint/public/applications/endpoint/store/policy_details/reducer.ts +++ b/x-pack/plugins/endpoint/public/applications/endpoint/store/policy_details/reducer.ts @@ -4,10 +4,10 @@ * you may not use this file except in compliance with the Elastic License. */ -import { Reducer } from 'redux'; -import { PolicyData, PolicyDetailsState, UIPolicyConfig } from '../../types'; import { AppAction } from '../action'; import { fullPolicy, isOnPolicyDetailsPage } from './selectors'; +import { PolicyDetailsState, ImmutableReducer } from '../../types'; +import { Immutable, PolicyConfig, UIPolicyConfig } from '../../../../../common/types'; const initialPolicyDetailsState = (): PolicyDetailsState => { return { @@ -23,7 +23,7 @@ const initialPolicyDetailsState = (): PolicyDetailsState => { }; }; -export const policyDetailsReducer: Reducer<PolicyDetailsState, AppAction> = ( +export const policyDetailsReducer: ImmutableReducer<PolicyDetailsState, AppAction> = ( state = initialPolicyDetailsState(), action ) => { @@ -70,7 +70,7 @@ export const policyDetailsReducer: Reducer<PolicyDetailsState, AppAction> = ( } if (action.type === 'userChangedUrl') { - const newState = { + const newState: Immutable<PolicyDetailsState> = { ...state, location: action.payload, }; @@ -79,8 +79,10 @@ export const policyDetailsReducer: Reducer<PolicyDetailsState, AppAction> = ( // Did user just enter the Detail page? if so, then set the loading indicator and return new state if (isCurrentlyOnDetailsPage && !wasPreviouslyOnDetailsPage) { - newState.isLoading = true; - return newState; + return { + ...newState, + isLoading: true, + }; } return { ...initialPolicyDetailsState(), @@ -89,12 +91,23 @@ export const policyDetailsReducer: Reducer<PolicyDetailsState, AppAction> = ( } if (action.type === 'userChangedPolicyConfig') { - const newState = { ...state, policyItem: { ...(state.policyItem as PolicyData) } }; - const newPolicy = (newState.policyItem.inputs[0].config.policy.value = { - ...fullPolicy(state), - }); + if (!state.policyItem) { + return state; + } + const newState = { ...state, policyItem: { ...state.policyItem } }; + const newPolicy: PolicyConfig = { ...fullPolicy(state) }; + + /** + * This is directly changing redux state because `policyItem.inputs` was copied over and not cloned. + */ + // @ts-ignore + newState.policyItem.inputs[0].config.policy.value = newPolicy; Object.entries(action.payload.policyConfig).forEach(([section, newSettings]) => { + /** + * this is not safe because `action.payload.policyConfig` may have excess keys + */ + // @ts-ignore newPolicy[section as keyof UIPolicyConfig] = { ...newPolicy[section as keyof UIPolicyConfig], ...newSettings, diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/store/policy_details/selectors.ts b/x-pack/plugins/endpoint/public/applications/endpoint/store/policy_details/selectors.ts index 0d505931c9ec5..4fd36d8d0a33f 100644 --- a/x-pack/plugins/endpoint/public/applications/endpoint/store/policy_details/selectors.ts +++ b/x-pack/plugins/endpoint/public/applications/endpoint/store/policy_details/selectors.ts @@ -5,14 +5,15 @@ */ import { createSelector } from 'reselect'; -import { PolicyConfig, PolicyDetailsState, UIPolicyConfig } from '../../types'; -import { generatePolicy } from '../../models/policy'; +import { PolicyDetailsState } from '../../types'; +import { Immutable, PolicyConfig, UIPolicyConfig } from '../../../../../common/types'; +import { factory as policyConfigFactory } from '../../../../../common/models/policy_config'; /** Returns the policy details */ -export const policyDetails = (state: PolicyDetailsState) => state.policyItem; +export const policyDetails = (state: Immutable<PolicyDetailsState>) => state.policyItem; /** Returns a boolean of whether the user is on the policy details page or not */ -export const isOnPolicyDetailsPage = (state: PolicyDetailsState) => { +export const isOnPolicyDetailsPage = (state: Immutable<PolicyDetailsState>) => { if (state.location) { const pathnameParts = state.location.pathname.split('/'); return pathnameParts[1] === 'policy' && pathnameParts[2]; @@ -22,8 +23,8 @@ export const isOnPolicyDetailsPage = (state: PolicyDetailsState) => { }; /** Returns the policyId from the url */ -export const policyIdFromParams: (state: PolicyDetailsState) => string = createSelector( - (state: PolicyDetailsState) => state.location, +export const policyIdFromParams: (state: Immutable<PolicyDetailsState>) => string = createSelector( + state => state.location, (location: PolicyDetailsState['location']) => { if (location) { return location.pathname.split('/')[2]; @@ -32,14 +33,16 @@ export const policyIdFromParams: (state: PolicyDetailsState) => string = createS } ); +const defaultFullPolicy: Immutable<PolicyConfig> = policyConfigFactory(); + /** * Returns the full Endpoint Policy, which will include private settings not shown on the UI. * Note: this will return a default full policy if the `policyItem` is `undefined` */ -export const fullPolicy: (s: PolicyDetailsState) => PolicyConfig = createSelector( +export const fullPolicy: (s: Immutable<PolicyDetailsState>) => PolicyConfig = createSelector( policyDetails, policyData => { - return policyData?.inputs[0]?.config?.policy?.value ?? generatePolicy(); + return policyData?.inputs[0]?.config?.policy?.value ?? defaultFullPolicy; } ); @@ -79,14 +82,8 @@ export const policyConfig: (s: PolicyDetailsState) => UIPolicyConfig = createSel } ); -/** Returns an object of all the windows eventing configuration */ -export const windowsEventing = (state: PolicyDetailsState) => { - const config = policyConfig(state); - return config && config.windows.events; -}; - /** Returns the total number of possible windows eventing configurations */ -export const totalWindowsEventing = (state: PolicyDetailsState): number => { +export const totalWindowsEvents = (state: PolicyDetailsState): number => { const config = policyConfig(state); if (config) { return Object.keys(config.windows.events).length; @@ -95,7 +92,7 @@ export const totalWindowsEventing = (state: PolicyDetailsState): number => { }; /** Returns the number of selected windows eventing configurations */ -export const selectedWindowsEventing = (state: PolicyDetailsState): number => { +export const selectedWindowsEvents = (state: PolicyDetailsState): number => { const config = policyConfig(state); if (config) { return Object.values(config.windows.events).reduce((count, event) => { @@ -105,6 +102,46 @@ export const selectedWindowsEventing = (state: PolicyDetailsState): number => { return 0; }; +/** Returns the total number of possible mac eventing configurations */ +export const totalMacEvents = (state: PolicyDetailsState): number => { + const config = policyConfig(state); + if (config) { + return Object.keys(config.mac.events).length; + } + return 0; +}; + +/** Returns the number of selected mac eventing configurations */ +export const selectedMacEvents = (state: PolicyDetailsState): number => { + const config = policyConfig(state); + if (config) { + return Object.values(config.mac.events).reduce((count, event) => { + return event === true ? count + 1 : count; + }, 0); + } + return 0; +}; + +/** Returns the total number of possible linux eventing configurations */ +export const totalLinuxEvents = (state: PolicyDetailsState): number => { + const config = policyConfig(state); + if (config) { + return Object.keys(config.linux.events).length; + } + return 0; +}; + +/** Returns the number of selected liinux eventing configurations */ +export const selectedLinuxEvents = (state: PolicyDetailsState): number => { + const config = policyConfig(state); + if (config) { + return Object.values(config.linux.events).reduce((count, event) => { + return event === true ? count + 1 : count; + }, 0); + } + return 0; +}; + /** is there an api call in flight */ export const isLoading = (state: PolicyDetailsState) => state.isLoading; diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/store/policy_list/action.ts b/x-pack/plugins/endpoint/public/applications/endpoint/store/policy_list/action.ts index 3f4f3f39e9be0..4c379b7426461 100644 --- a/x-pack/plugins/endpoint/public/applications/endpoint/store/policy_list/action.ts +++ b/x-pack/plugins/endpoint/public/applications/endpoint/store/policy_list/action.ts @@ -4,7 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ -import { PolicyData, ServerApiError } from '../../types'; +import { ServerApiError } from '../../types'; +import { PolicyData } from '../../../../../common/types'; interface ServerReturnedPolicyListData { type: 'serverReturnedPolicyListData'; @@ -21,15 +22,4 @@ interface ServerFailedToReturnPolicyListData { payload: ServerApiError; } -interface UserPaginatedPolicyListTable { - type: 'userPaginatedPolicyListTable'; - payload: { - pageSize: number; - pageIndex: number; - }; -} - -export type PolicyListAction = - | ServerReturnedPolicyListData - | UserPaginatedPolicyListTable - | ServerFailedToReturnPolicyListData; +export type PolicyListAction = ServerReturnedPolicyListData | ServerFailedToReturnPolicyListData; diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/store/policy_list/index.test.ts b/x-pack/plugins/endpoint/public/applications/endpoint/store/policy_list/index.test.ts index 0cf0eb8bfa3cd..69b11fb3c1f0e 100644 --- a/x-pack/plugins/endpoint/public/applications/endpoint/store/policy_list/index.test.ts +++ b/x-pack/plugins/endpoint/public/applications/endpoint/store/policy_list/index.test.ts @@ -4,71 +4,105 @@ * you may not use this file except in compliance with the Elastic License. */ -import { PolicyListState } from '../../types'; -import { applyMiddleware, createStore, Dispatch, Store } from 'redux'; +import { EndpointAppLocation, PolicyListState } from '../../types'; +import { applyMiddleware, createStore, Store } from 'redux'; import { AppAction } from '../action'; import { policyListReducer } from './reducer'; import { policyListMiddlewareFactory } from './middleware'; import { coreMock } from '../../../../../../../../src/core/public/mocks'; -import { CoreStart } from 'kibana/public'; -import { selectIsLoading } from './selectors'; +import { isOnPolicyListPage, selectIsLoading, urlSearchParams } from './selectors'; import { DepsStartMock, depsStartMock } from '../../mocks'; +import { setPolicyListApiMockImplementation } from './test_mock_utils'; +import { INGEST_API_DATASOURCES } from './services/ingest'; +import { Immutable } from '../../../../../common/types'; +import { createSpyMiddleware, MiddlewareActionSpyHelper } from '../test_utils'; describe('policy list store concerns', () => { - const sleep = () => new Promise(resolve => setTimeout(resolve, 1000)); - let fakeCoreStart: jest.Mocked<CoreStart>; + let fakeCoreStart: ReturnType<typeof coreMock.createStart>; let depsStart: DepsStartMock; - let store: Store<PolicyListState>; - let getState: typeof store['getState']; - let dispatch: Dispatch<AppAction>; + type PolicyListStore = Store<Immutable<PolicyListState>, Immutable<AppAction>>; + let store: PolicyListStore; + let getState: PolicyListStore['getState']; + let dispatch: PolicyListStore['dispatch']; + let waitForAction: MiddlewareActionSpyHelper['waitForAction']; beforeEach(() => { fakeCoreStart = coreMock.createStart({ basePath: '/mock' }); depsStart = depsStartMock(); + setPolicyListApiMockImplementation(fakeCoreStart.http); + let actionSpyMiddleware; + ({ actionSpyMiddleware, waitForAction } = createSpyMiddleware<PolicyListState>()); + store = createStore( policyListReducer, - applyMiddleware(policyListMiddlewareFactory(fakeCoreStart, depsStart)) + applyMiddleware(policyListMiddlewareFactory(fakeCoreStart, depsStart), actionSpyMiddleware) ); getState = store.getState; dispatch = store.dispatch; }); - // https://github.com/elastic/kibana/issues/58972 - test.skip('it sets `isLoading` when `userNavigatedToPage`', async () => { - expect(selectIsLoading(getState())).toBe(false); - dispatch({ type: 'userNavigatedToPage', payload: 'policyListPage' }); - expect(selectIsLoading(getState())).toBe(true); - await sleep(); - expect(selectIsLoading(getState())).toBe(false); + it('it does nothing on `userChangedUrl` if pathname is NOT `/policy`', async () => { + const state = getState(); + expect(isOnPolicyListPage(state)).toBe(false); + dispatch({ + type: 'userChangedUrl', + payload: { + pathname: '/foo', + search: '', + hash: '', + } as EndpointAppLocation, + }); + expect(getState()).toEqual(state); }); - // https://github.com/elastic/kibana/issues/58896 - test.skip('it sets `isLoading` when `userPaginatedPolicyListTable`', async () => { + it('it reports `isOnPolicyListPage` correctly when router pathname is `/policy`', async () => { + dispatch({ + type: 'userChangedUrl', + payload: { + pathname: '/policy', + search: '', + hash: '', + }, + }); + expect(isOnPolicyListPage(getState())).toBe(true); + }); + + it('it sets `isLoading` when `userChangedUrl`', async () => { expect(selectIsLoading(getState())).toBe(false); dispatch({ - type: 'userPaginatedPolicyListTable', + type: 'userChangedUrl', payload: { - pageSize: 10, - pageIndex: 1, + pathname: '/policy', + search: '', + hash: '', }, }); expect(selectIsLoading(getState())).toBe(true); - await sleep(); + await waitForAction('serverReturnedPolicyListData'); expect(selectIsLoading(getState())).toBe(false); }); - test('it resets state on `userNavigatedFromPage` action', async () => { + it('it resets state on `userChangedUrl` and pathname is NOT `/policy`', async () => { + dispatch({ + type: 'userChangedUrl', + payload: { + pathname: '/policy', + search: '', + hash: '', + }, + }); + await waitForAction('serverReturnedPolicyListData'); dispatch({ - type: 'serverReturnedPolicyListData', + type: 'userChangedUrl', payload: { - policyItems: [], - pageIndex: 20, - pageSize: 50, - total: 200, + pathname: '/foo', + search: '', + hash: '', }, }); - dispatch({ type: 'userNavigatedFromPage', payload: 'policyListPage' }); expect(getState()).toEqual({ + apiError: undefined, + location: undefined, policyItems: [], isLoading: false, pageIndex: 0, @@ -76,4 +110,85 @@ describe('policy list store concerns', () => { total: 0, }); }); + it('uses default pagination params when not included in url', async () => { + dispatch({ + type: 'userChangedUrl', + payload: { + pathname: '/policy', + search: '', + hash: '', + }, + }); + await waitForAction('serverReturnedPolicyListData'); + expect(fakeCoreStart.http.get).toHaveBeenCalledWith(INGEST_API_DATASOURCES, { + query: { kuery: 'datasources.package.name: endpoint', page: 1, perPage: 10 }, + }); + }); + + describe('when url contains search params', () => { + const dispatchUserChangedUrl = (searchParams: string = '') => + dispatch({ + type: 'userChangedUrl', + payload: { + pathname: '/policy', + search: searchParams, + hash: '', + }, + }); + + it('uses pagination params from url', async () => { + dispatchUserChangedUrl('?page_size=50&page_index=0'); + await waitForAction('serverReturnedPolicyListData'); + expect(fakeCoreStart.http.get).toHaveBeenCalledWith(INGEST_API_DATASOURCES, { + query: { kuery: 'datasources.package.name: endpoint', page: 1, perPage: 50 }, + }); + }); + it('uses defaults for params not in url', async () => { + dispatchUserChangedUrl('?page_index=99'); + expect(urlSearchParams(getState())).toEqual({ + page_index: 99, + page_size: 10, + }); + dispatchUserChangedUrl('?page_size=50'); + expect(urlSearchParams(getState())).toEqual({ + page_index: 0, + page_size: 50, + }); + }); + it('accepts only positive numbers for page_index and page_size', async () => { + dispatchUserChangedUrl('?page_size=-50&page_index=-99'); + await waitForAction('serverReturnedPolicyListData'); + expect(fakeCoreStart.http.get).toHaveBeenCalledWith(INGEST_API_DATASOURCES, { + query: { kuery: 'datasources.package.name: endpoint', page: 1, perPage: 10 }, + }); + }); + it('it ignores non-numeric values for page_index and page_size', async () => { + dispatchUserChangedUrl('?page_size=fifty&page_index=ten'); + await waitForAction('serverReturnedPolicyListData'); + expect(fakeCoreStart.http.get).toHaveBeenCalledWith(INGEST_API_DATASOURCES, { + query: { kuery: 'datasources.package.name: endpoint', page: 1, perPage: 10 }, + }); + }); + it('accepts only known values for `page_size`', async () => { + dispatchUserChangedUrl('?page_size=300&page_index=10'); + await waitForAction('serverReturnedPolicyListData'); + expect(fakeCoreStart.http.get).toHaveBeenCalledWith(INGEST_API_DATASOURCES, { + query: { kuery: 'datasources.package.name: endpoint', page: 11, perPage: 10 }, + }); + }); + it(`ignores unknown url search params`, async () => { + dispatchUserChangedUrl('?page_size=20&page_index=10&foo=bar'); + expect(urlSearchParams(getState())).toEqual({ + page_index: 10, + page_size: 20, + }); + }); + it(`uses last param value if param is defined multiple times`, async () => { + dispatchUserChangedUrl('?page_size=20&page_size=50&page_index=20&page_index=40'); + expect(urlSearchParams(getState())).toEqual({ + page_index: 20, + page_size: 20, + }); + }); + }); }); diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/store/policy_list/middleware.ts b/x-pack/plugins/endpoint/public/applications/endpoint/store/policy_list/middleware.ts index ebfee5dbe6a7e..78ebacd971840 100644 --- a/x-pack/plugins/endpoint/public/applications/endpoint/store/policy_list/middleware.ts +++ b/x-pack/plugins/endpoint/public/applications/endpoint/store/policy_list/middleware.ts @@ -4,32 +4,21 @@ * you may not use this file except in compliance with the Elastic License. */ -import { MiddlewareFactory, PolicyListState } from '../../types'; -import { GetDatasourcesResponse, sendGetEndpointSpecificDatasources } from '../../services/ingest'; +import { GetPolicyListResponse, ImmutableMiddlewareFactory, PolicyListState } from '../../types'; +import { sendGetEndpointSpecificDatasources } from './services/ingest'; +import { isOnPolicyListPage, urlSearchParams } from './selectors'; -export const policyListMiddlewareFactory: MiddlewareFactory<PolicyListState> = coreStart => { +export const policyListMiddlewareFactory: ImmutableMiddlewareFactory<PolicyListState> = coreStart => { const http = coreStart.http; return ({ getState, dispatch }) => next => async action => { next(action); - if ( - (action.type === 'userNavigatedToPage' && action.payload === 'policyListPage') || - action.type === 'userPaginatedPolicyListTable' - ) { - const state = getState(); - let pageSize: number; - let pageIndex: number; - - if (action.type === 'userPaginatedPolicyListTable') { - pageSize = action.payload.pageSize; - pageIndex = action.payload.pageIndex; - } else { - pageSize = state.pageSize; - pageIndex = state.pageIndex; - } + const state = getState(); - let response: GetDatasourcesResponse; + if (action.type === 'userChangedUrl' && isOnPolicyListPage(state)) { + const { page_index: pageIndex, page_size: pageSize } = urlSearchParams(state); + let response: GetPolicyListResponse; try { response = await sendGetEndpointSpecificDatasources(http, { diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/store/policy_list/reducer.ts b/x-pack/plugins/endpoint/public/applications/endpoint/store/policy_list/reducer.ts index b964f4f023866..ccd3f84dd060c 100644 --- a/x-pack/plugins/endpoint/public/applications/endpoint/store/policy_list/reducer.ts +++ b/x-pack/plugins/endpoint/public/applications/endpoint/store/policy_list/reducer.ts @@ -4,9 +4,10 @@ * you may not use this file except in compliance with the Elastic License. */ -import { Reducer } from 'redux'; -import { PolicyListState } from '../../types'; +import { PolicyListState, ImmutableReducer } from '../../types'; import { AppAction } from '../action'; +import { isOnPolicyListPage } from './selectors'; +import { Immutable } from '../../../../../common/types'; const initialPolicyListState = (): PolicyListState => { return { @@ -16,10 +17,11 @@ const initialPolicyListState = (): PolicyListState => { pageIndex: 0, pageSize: 10, total: 0, + location: undefined, }; }; -export const policyListReducer: Reducer<PolicyListState, AppAction> = ( +export const policyListReducer: ImmutableReducer<PolicyListState, AppAction> = ( state = initialPolicyListState(), action ) => { @@ -39,18 +41,26 @@ export const policyListReducer: Reducer<PolicyListState, AppAction> = ( }; } - if ( - action.type === 'userPaginatedPolicyListTable' || - (action.type === 'userNavigatedToPage' && action.payload === 'policyListPage') - ) { - return { + if (action.type === 'userChangedUrl') { + const newState: Immutable<PolicyListState> = { ...state, - apiError: undefined, - isLoading: true, + location: action.payload, }; - } + const isCurrentlyOnListPage = isOnPolicyListPage(newState); + const wasPreviouslyOnListPage = isOnPolicyListPage(state); - if (action.type === 'userNavigatedFromPage' && action.payload === 'policyListPage') { + // If on the current page, then return new state with location information + // Also adjust some state if user is just entering the policy list view + if (isCurrentlyOnListPage) { + if (!wasPreviouslyOnListPage) { + return { + ...newState, + apiError: undefined, + isLoading: true, + }; + } + return newState; + } return initialPolicyListState(); } diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/store/policy_list/selectors.ts b/x-pack/plugins/endpoint/public/applications/endpoint/store/policy_list/selectors.ts index 7ca25e81ce75a..6d2e952fa07bb 100644 --- a/x-pack/plugins/endpoint/public/applications/endpoint/store/policy_list/selectors.ts +++ b/x-pack/plugins/endpoint/public/applications/endpoint/store/policy_list/selectors.ts @@ -4,16 +4,65 @@ * you may not use this file except in compliance with the Elastic License. */ -import { PolicyListState } from '../../types'; +import { createSelector } from 'reselect'; +import { parse } from 'query-string'; +import { PolicyListState, PolicyListUrlSearchParams } from '../../types'; +import { Immutable } from '../../../../../common/types'; -export const selectPolicyItems = (state: PolicyListState) => state.policyItems; +const PAGE_SIZES = Object.freeze([10, 20, 50]); -export const selectPageIndex = (state: PolicyListState) => state.pageIndex; +export const selectPolicyItems = (state: Immutable<PolicyListState>) => state.policyItems; -export const selectPageSize = (state: PolicyListState) => state.pageSize; +export const selectPageIndex = (state: Immutable<PolicyListState>) => state.pageIndex; -export const selectTotal = (state: PolicyListState) => state.total; +export const selectPageSize = (state: Immutable<PolicyListState>) => state.pageSize; -export const selectIsLoading = (state: PolicyListState) => state.isLoading; +export const selectTotal = (state: Immutable<PolicyListState>) => state.total; -export const selectApiError = (state: PolicyListState) => state.apiError; +export const selectIsLoading = (state: Immutable<PolicyListState>) => state.isLoading; + +export const selectApiError = (state: Immutable<PolicyListState>) => state.apiError; + +export const isOnPolicyListPage = (state: Immutable<PolicyListState>) => { + return state.location?.pathname === '/policy'; +}; + +const routeLocation = (state: Immutable<PolicyListState>) => state.location; + +/** + * Returns the supported URL search params, populated with defaults if none where present in the URL + */ +export const urlSearchParams: ( + state: Immutable<PolicyListState> +) => PolicyListUrlSearchParams = createSelector(routeLocation, location => { + const searchParams = { + page_index: 0, + page_size: 10, + }; + if (!location) { + return searchParams; + } + + const query = parse(location.search); + + // Search params can appear multiple times in the URL, in which case the value for them, + // once parsed, would be an array. In these case, we take the first value defined + searchParams.page_index = Number( + (Array.isArray(query.page_index) ? query.page_index[0] : query.page_index) ?? 0 + ); + searchParams.page_size = Number( + (Array.isArray(query.page_size) ? query.page_size[0] : query.page_size) ?? 10 + ); + + // If pageIndex is not a valid positive integer, set it to 0 + if (!Number.isFinite(searchParams.page_index) || searchParams.page_index < 0) { + searchParams.page_index = 0; + } + + // if pageSize is not one of the expected page sizes, reset it to 10 + if (!PAGE_SIZES.includes(searchParams.page_size)) { + searchParams.page_size = 10; + } + + return searchParams; +}); diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/services/ingest.test.ts b/x-pack/plugins/endpoint/public/applications/endpoint/store/policy_list/services/ingest.test.ts similarity index 95% rename from x-pack/plugins/endpoint/public/applications/endpoint/services/ingest.test.ts rename to x-pack/plugins/endpoint/public/applications/endpoint/store/policy_list/services/ingest.test.ts index a2c1dfbe09a48..c2865d36c95f2 100644 --- a/x-pack/plugins/endpoint/public/applications/endpoint/services/ingest.test.ts +++ b/x-pack/plugins/endpoint/public/applications/endpoint/store/policy_list/services/ingest.test.ts @@ -4,8 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ -import { httpServiceMock } from '../../../../../../../src/core/public/mocks'; import { sendGetDatasource, sendGetEndpointSpecificDatasources } from './ingest'; +import { httpServiceMock } from '../../../../../../../../../src/core/public/mocks'; describe('ingest service', () => { let http: ReturnType<typeof httpServiceMock.createStartContract>; diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/store/policy_list/services/ingest.ts b/x-pack/plugins/endpoint/public/applications/endpoint/store/policy_list/services/ingest.ts new file mode 100644 index 0000000000000..4356517e43c2c --- /dev/null +++ b/x-pack/plugins/endpoint/public/applications/endpoint/store/policy_list/services/ingest.ts @@ -0,0 +1,94 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { HttpFetchOptions, HttpStart } from 'kibana/public'; +import { + GetDatasourcesRequest, + GetAgentStatusResponse, +} from '../../../../../../../ingest_manager/common'; +import { GetPolicyListResponse, GetPolicyResponse, UpdatePolicyResponse } from '../../../types'; +import { NewPolicyData } from '../../../../../../common/types'; + +const INGEST_API_ROOT = `/api/ingest_manager`; +export const INGEST_API_DATASOURCES = `${INGEST_API_ROOT}/datasources`; +const INGEST_API_FLEET = `${INGEST_API_ROOT}/fleet`; +const INGEST_API_FLEET_AGENT_STATUS = `${INGEST_API_FLEET}/agent-status`; + +/** + * Retrieves a list of endpoint specific datasources (those created with a `package.name` of + * `endpoint`) from Ingest + * @param http + * @param options + */ +export const sendGetEndpointSpecificDatasources = ( + http: HttpStart, + options: HttpFetchOptions & Partial<GetDatasourcesRequest> = {} +): Promise<GetPolicyListResponse> => { + return http.get<GetPolicyListResponse>(INGEST_API_DATASOURCES, { + ...options, + query: { + ...options.query, + kuery: `${ + options?.query?.kuery ? options.query.kuery + ' and ' : '' + }datasources.package.name: endpoint`, + }, + }); +}; + +/** + * Retrieves a single datasource based on ID from ingest + * @param http + * @param datasourceId + * @param options + */ +export const sendGetDatasource = ( + http: HttpStart, + datasourceId: string, + options?: HttpFetchOptions +) => { + return http.get<GetPolicyResponse>(`${INGEST_API_DATASOURCES}/${datasourceId}`, options); +}; + +/** + * Updates a datasources + * + * @param http + * @param datasourceId + * @param datasource + * @param options + */ +export const sendPutDatasource = ( + http: HttpStart, + datasourceId: string, + datasource: NewPolicyData, + options: Exclude<HttpFetchOptions, 'body'> = {} +): Promise<UpdatePolicyResponse> => { + return http.put(`${INGEST_API_DATASOURCES}/${datasourceId}`, { + ...options, + body: JSON.stringify(datasource), + }); +}; + +/** + * Get a status summary for all Agents that are currently assigned to a given agent configuration + * + * @param http + * @param configId + * @param options + */ +export const sendGetFleetAgentStatusForConfig = ( + http: HttpStart, + /** the Agent (fleet) configuration id */ + configId: string, + options: Exclude<HttpFetchOptions, 'query'> = {} +): Promise<GetAgentStatusResponse> => { + return http.get(INGEST_API_FLEET_AGENT_STATUS, { + ...options, + query: { + configId, + }, + }); +}; diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/store/policy_list/test_mock_utils.ts b/x-pack/plugins/endpoint/public/applications/endpoint/store/policy_list/test_mock_utils.ts new file mode 100644 index 0000000000000..a1788b8f8021d --- /dev/null +++ b/x-pack/plugins/endpoint/public/applications/endpoint/store/policy_list/test_mock_utils.ts @@ -0,0 +1,38 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { HttpStart } from 'kibana/public'; +import { INGEST_API_DATASOURCES } from './services/ingest'; +import { EndpointDocGenerator } from '../../../../../common/generate_data'; +import { GetPolicyListResponse } from '../../types'; + +const generator = new EndpointDocGenerator('policy-list'); + +/** + * It sets the mock implementation on the necessary http methods to support the policy list view + * @param mockedHttpService + * @param responseItems + */ +export const setPolicyListApiMockImplementation = ( + mockedHttpService: jest.Mocked<HttpStart>, + responseItems: GetPolicyListResponse['items'] = [generator.generatePolicyDatasource()] +): void => { + mockedHttpService.get.mockImplementation((...args) => { + const [path] = args; + if (typeof path === 'string') { + if (path === INGEST_API_DATASOURCES) { + return Promise.resolve<GetPolicyListResponse>({ + items: responseItems, + total: 10, + page: 1, + perPage: 10, + success: true, + }); + } + } + return Promise.reject(new Error(`MOCK: unknown policy list api: ${path}`)); + }); +}; diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/store/reducer.ts b/x-pack/plugins/endpoint/public/applications/endpoint/store/reducer.ts index c8b2d08676724..2f77c380d9387 100644 --- a/x-pack/plugins/endpoint/public/applications/endpoint/store/reducer.ts +++ b/x-pack/plugins/endpoint/public/applications/endpoint/store/reducer.ts @@ -3,15 +3,16 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -import { combineReducers, Reducer } from 'redux'; + import { hostListReducer } from './hosts'; import { AppAction } from './action'; import { alertListReducer } from './alerts'; -import { GlobalState } from '../types'; +import { GlobalState, ImmutableReducer } from '../types'; import { policyListReducer } from './policy_list'; import { policyDetailsReducer } from './policy_details'; +import { immutableCombineReducers } from './immutable_combine_reducers'; -export const appReducer: Reducer<GlobalState, AppAction> = combineReducers({ +export const appReducer: ImmutableReducer<GlobalState, AppAction> = immutableCombineReducers({ hostList: hostListReducer, alertList: alertListReducer, policyList: policyListReducer, diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/store/routing/action.ts b/x-pack/plugins/endpoint/public/applications/endpoint/store/routing/action.ts index c7e9970e58c30..fd72a02b33588 100644 --- a/x-pack/plugins/endpoint/public/applications/endpoint/store/routing/action.ts +++ b/x-pack/plugins/endpoint/public/applications/endpoint/store/routing/action.ts @@ -4,22 +4,12 @@ * you may not use this file except in compliance with the Elastic License. */ -import { PageId, Immutable } from '../../../../../common/types'; -import { EndpointAppLocation } from '../alerts'; - -interface UserNavigatedToPage { - readonly type: 'userNavigatedToPage'; - readonly payload: PageId; -} - -interface UserNavigatedFromPage { - readonly type: 'userNavigatedFromPage'; - readonly payload: PageId; -} +import { Immutable } from '../../../../../common/types'; +import { EndpointAppLocation } from '../../types'; interface UserChangedUrl { readonly type: 'userChangedUrl'; readonly payload: Immutable<EndpointAppLocation>; } -export type RoutingAction = UserNavigatedToPage | UserNavigatedFromPage | UserChangedUrl; +export type RoutingAction = UserChangedUrl; diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/store/test_utils.ts b/x-pack/plugins/endpoint/public/applications/endpoint/store/test_utils.ts new file mode 100644 index 0000000000000..df17cf8cf6638 --- /dev/null +++ b/x-pack/plugins/endpoint/public/applications/endpoint/store/test_utils.ts @@ -0,0 +1,126 @@ +/* + * 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 { Dispatch } from 'redux'; +import { AppAction, GlobalState, ImmutableMiddlewareFactory } from '../types'; + +/** + * Utilities for testing Redux middleware + */ +export interface MiddlewareActionSpyHelper<S = GlobalState, A extends AppAction = AppAction> { + /** + * Returns a promise that is fulfilled when the given action is dispatched or a timeout occurs. + * The `action` will given to the promise `resolve` thus allowing for checks to be done. + * The use of this method instead of a `sleep()` type of delay should avoid test case instability + * especially when run in a CI environment. + * + * @param actionType + */ + waitForAction: <T extends A['type']>(actionType: T) => Promise<A extends { type: T } ? A : never>; + /** + * A property holding the information around the calls that were processed by the internal + * `actionSpyMiddelware`. This property holds the information typically found in Jets's mocked + * function `mock` property - [see here for more information](https://jestjs.io/docs/en/mock-functions#mock-property) + * + * **Note**: this property will only be set **after* the `actionSpyMiddlware` has been + * initialized (ex. via `createStore()`. Attempting to reference this property before that time + * will throw an error. + * Also - do not hold on to references to this property value if `jest.clearAllMocks()` or + * `jest.resetAllMocks()` is called between usages of the value. + */ + dispatchSpy: jest.Mock<Dispatch<A>>['mock']; + /** + * Redux middleware that enables spying on the action that are dispatched through the store + */ + actionSpyMiddleware: ReturnType<ImmutableMiddlewareFactory<S>>; +} + +/** + * Creates a new instance of middleware action helpers + * Note: in most cases (testing concern specific middleware) this function should be given + * the state type definition, else, the global state will be used. + * + * @example + * // Use in Policy List middleware testing + * const middlewareSpyUtils = createSpyMiddleware<PolicyListState>(); + * store = createStore( + * policyListReducer, + * applyMiddleware( + * policyListMiddlewareFactory(fakeCoreStart, depsStart), + * middlewareSpyUtils.actionSpyMiddleware + * ) + * ); + * // Reference `dispatchSpy` ONLY after creating the store that includes `actionSpyMiddleware` + * const { waitForAction, dispatchSpy } = middlewareSpyUtils; + * // + * // later in test + * // + * it('...', async () => { + * //... + * await waitForAction('serverReturnedPolicyListData'); + * // do assertions + * // or check how action was called + * expect(dispatchSpy.calls.length).toBe(2) + * }); + */ +export const createSpyMiddleware = < + S = GlobalState, + A extends AppAction = AppAction +>(): MiddlewareActionSpyHelper<S, A> => { + type ActionWatcher = (action: A) => void; + + const watchers = new Set<ActionWatcher>(); + let spyDispatch: jest.Mock<Dispatch<A>>; + + return { + waitForAction: async actionType => { + type ResolvedAction = A extends { type: typeof actionType } ? A : never; + + // Error is defined here so that we get a better stack trace that points to the test from where it was used + const err = new Error(`action '${actionType}' was not dispatched within the allocated time`); + + return new Promise<ResolvedAction>((resolve, reject) => { + const watch: ActionWatcher = action => { + if (action.type === actionType) { + watchers.delete(watch); + clearTimeout(timeout); + resolve(action as ResolvedAction); + } + }; + + // We timeout before jest's default 5s, so that a better error stack is returned + const timeout = setTimeout(() => { + watchers.delete(watch); + reject(err); + }, 4500); + watchers.add(watch); + }); + }, + + get dispatchSpy() { + if (!spyDispatch) { + throw new Error( + 'Spy Middleware has not been initialized. Access this property only after using `actionSpyMiddleware` in a redux store' + ); + } + return spyDispatch.mock; + }, + + actionSpyMiddleware: () => { + return next => { + spyDispatch = jest.fn(action => { + next(action); + // loop through the list of watcher (if any) and call them with this action + for (const watch of watchers) { + watch(action); + } + return action; + }); + return spyDispatch; + }; + }, + }; +}; diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/types.ts b/x-pack/plugins/endpoint/public/applications/endpoint/types.ts index d4f6d2457254e..f407d32cb3b42 100644 --- a/x-pack/plugins/endpoint/public/applications/endpoint/types.ts +++ b/x-pack/plugins/endpoint/public/applications/endpoint/types.ts @@ -4,7 +4,13 @@ * you may not use this file except in compliance with the Elastic License. */ -import { Dispatch, MiddlewareAPI } from 'redux'; +import { + Dispatch, + Action as ReduxAction, + AnyAction as ReduxAnyAction, + Action, + Middleware, +} from 'redux'; import { IIndexPattern } from 'src/plugins/data/public'; import { HostMetadata, @@ -13,20 +19,74 @@ import { Immutable, ImmutableArray, AlertDetails, + MalwareFields, + UIPolicyConfig, + PolicyData, } from '../../../common/types'; import { EndpointPluginStartDependencies } from '../../plugin'; import { AppAction } from './store/action'; import { CoreStart } from '../../../../../../src/core/public'; -import { Datasource, NewDatasource } from '../../../../ingest_manager/common/types/models'; -import { GetAgentStatusResponse } from '../../../../ingest_manager/common/types/rest_spec'; +import { + GetAgentStatusResponse, + GetDatasourcesResponse, + GetOneDatasourceResponse, + UpdateDatasourceResponse, +} from '../../../../ingest_manager/common'; export { AppAction }; -export type MiddlewareFactory<S = GlobalState> = ( + +/** + * like redux's `MiddlewareAPI` but `getState` returns an `Immutable` version of + * state and `dispatch` accepts `Immutable` versions of actions. + */ +export interface ImmutableMiddlewareAPI<S, A extends Action> { + dispatch: Dispatch<A | Immutable<A>>; + getState(): Immutable<S>; +} + +/** + * Like redux's `Middleware` but without the ability to mutate actions or state. + * Differences: + * * `getState` returns an `Immutable` version of state + * * `dispatch` accepts `Immutable` versions of actions + * * `action`s received will be `Immutable` + */ +export type ImmutableMiddleware<S, A extends Action> = ( + api: ImmutableMiddlewareAPI<S, A> +) => (next: Dispatch<A | Immutable<A>>) => (action: Immutable<A>) => unknown; + +/** + * Takes application-standard middleware dependencies + * and returns a redux middleware. + * Middleware will be of the `ImmutableMiddleware` variety. Not able to directly + * change actions or state. + */ +export type ImmutableMiddlewareFactory<S = GlobalState> = ( coreStart: CoreStart, depsStart: EndpointPluginStartDependencies -) => ( - api: MiddlewareAPI<Dispatch<AppAction>, S> -) => (next: Dispatch<AppAction>) => (action: AppAction) => unknown; +) => ImmutableMiddleware<S, AppAction>; + +/** + * Simple type for a redux selector. + */ +type Selector<S, R> = (state: S) => R; + +/** + * Takes a selector and an `ImmutableMiddleware`. The + * middleware's version of `getState` will receive + * the result of the selector instead of the global state. + * + * This allows middleware to have knowledge of only a subsection of state. + * + * `selector` returns an `Immutable` version of the substate. + * `middleware` must be an `ImmutableMiddleware`. + * + * Returns a regular middleware, meant to be used with `applyMiddleware`. + */ +export type SubstateMiddlewareFactory = <Substate>( + selector: Selector<GlobalState, Immutable<Substate>>, + middleware: ImmutableMiddleware<Substate, AppAction> +) => Middleware<{}, GlobalState, Dispatch<AppAction | Immutable<AppAction>>>; export interface HostListState { hosts: HostMetadata[]; @@ -45,6 +105,7 @@ export interface HostListPagination { } export interface HostIndexUIQueryParams { selected_host?: string; + show?: string; } export interface ServerApiError { @@ -53,29 +114,6 @@ export interface ServerApiError { message: string; } -/** - * New policy data. Used when updating the policy record via ingest APIs - */ -export type NewPolicyData = NewDatasource & { - inputs: [ - { - type: 'endpoint'; - enabled: boolean; - streams: []; - config: { - policy: { - value: PolicyConfig; - }; - }; - } - ]; -}; - -/** - * Endpoint Policy data, which extends Ingest's `Datasource` type - */ -export type PolicyData = Datasource & NewPolicyData; - /** * Policy list store state */ @@ -92,6 +130,8 @@ export interface PolicyListState { pageIndex: number; /** data is being retrieved from server */ isLoading: boolean; + /** current location information */ + location?: Immutable<EndpointAppLocation>; } /** @@ -114,16 +154,28 @@ export interface PolicyDetailsState { }; } +/** + * The URL search params that are supported by the Policy List page view + */ +export interface PolicyListUrlSearchParams { + page_index: number; + page_size: number; +} + /** * Endpoint Policy configuration */ export interface PolicyConfig { windows: { events: { - process: boolean; + dll_and_driver_load: boolean; + dns: boolean; + file: boolean; network: boolean; + process: boolean; + registry: boolean; + security: boolean; }; - /** malware mode can be off, detect, prevent or prevent and notify user */ malware: MalwareFields; logging: { stdout: string; @@ -133,7 +185,9 @@ export interface PolicyConfig { }; mac: { events: { + file: boolean; process: boolean; + network: boolean; }; malware: MalwareFields; logging: { @@ -144,7 +198,9 @@ export interface PolicyConfig { }; linux: { events: { + file: boolean; process: boolean; + network: boolean; }; logging: { stdout: string; @@ -168,30 +224,6 @@ interface PolicyConfigAdvancedOptions { }; } -/** - * Windows-specific policy configuration that is supported via the UI - */ -type WindowsPolicyConfig = Pick<PolicyConfig['windows'], 'events' | 'malware'>; - -/** - * Mac-specific policy configuration that is supported via the UI - */ -type MacPolicyConfig = Pick<PolicyConfig['mac'], 'malware' | 'events'>; - -/** - * Linux-specific policy configuration that is supported via the UI - */ -type LinuxPolicyConfig = Pick<PolicyConfig['linux'], 'events'>; - -/** - * The set of Policy configuration settings that are show/edited via the UI - */ -export interface UIPolicyConfig { - windows: WindowsPolicyConfig; - mac: MacPolicyConfig; - linux: LinuxPolicyConfig; -} - /** OS used in Policy */ export enum OS { windows = 'windows', @@ -199,12 +231,6 @@ export enum OS { linux = 'linux', } -/** Used in Policy */ -export enum EventingFields { - process = 'process', - network = 'network', -} - /** * Returns the keys of an object whose values meet a criteria. * Ex) interface largeNestedObject = { @@ -228,20 +254,7 @@ export type KeysByValueCriteria<O, Criteria> = { }[keyof O]; /** Returns an array of the policy OSes that have a malware protection field */ - export type MalwareProtectionOSes = KeysByValueCriteria<UIPolicyConfig, { malware: MalwareFields }>; -/** Policy: Malware protection fields */ -export interface MalwareFields { - mode: ProtectionModes; -} - -/** Policy protection mode options */ -export enum ProtectionModes { - detect = 'detect', - prevent = 'prevent', - preventNotify = 'preventNotify', - off = 'off', -} export interface GlobalState { readonly hostList: HostListState; @@ -319,3 +332,40 @@ export interface AlertingIndexUIQueryParams { date_range?: string; filters?: string; } + +export interface GetPolicyListResponse extends GetDatasourcesResponse { + items: PolicyData[]; +} + +export interface GetPolicyResponse extends GetOneDatasourceResponse { + item: PolicyData; +} + +export interface UpdatePolicyResponse extends UpdateDatasourceResponse { + item: PolicyData; +} + +/** + * Like `Reducer` from `redux` but it accepts immutable versions of `state` and `action`. + * Use this type for all Reducers in order to help enforce our pattern of immutable state. + */ +export type ImmutableReducer<State, Action> = ( + state: Immutable<State> | undefined, + action: Immutable<Action> +) => State | Immutable<State>; + +/** + * A alternate interface for `redux`'s `combineReducers`. Will work with the same underlying implementation, + * but will enforce that `Immutable` versions of `state` and `action` are received. + */ +export type ImmutableCombineReducers = <S, A extends ReduxAction = ReduxAnyAction>( + reducers: ImmutableReducersMapObject<S, A> +) => ImmutableReducer<S, A>; + +/** + * Like `redux`'s `ReducersMapObject` (which is used by `combineReducers`) but enforces that + * the `state` and `action` received are `Immutable` versions. + */ +type ImmutableReducersMapObject<S, A extends ReduxAction = ReduxAction> = { + [K in keyof S]: ImmutableReducer<S[K], A>; +}; diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/view/alerts/index_search_bar.tsx b/x-pack/plugins/endpoint/public/applications/endpoint/view/alerts/index_search_bar.tsx index 5b872962a5dc0..1ede06c086517 100644 --- a/x-pack/plugins/endpoint/public/applications/endpoint/view/alerts/index_search_bar.tsx +++ b/x-pack/plugins/endpoint/public/applications/endpoint/view/alerts/index_search_bar.tsx @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import React from 'react'; +import React, { useMemo } from 'react'; import { memo, useEffect, useCallback } from 'react'; import { useHistory } from 'react-router-dom'; import { encode, RisonValue } from 'rison-node'; @@ -14,11 +14,18 @@ import { urlFromQueryParams } from './url_from_query_params'; import { useAlertListSelector } from './hooks/use_alerts_selector'; import * as selectors from '../../store/alerts/selectors'; import { EndpointPluginServices } from '../../../../plugin'; +import { clone } from '../../models/index_pattern'; export const AlertIndexSearchBar = memo(() => { const history = useHistory(); const queryParams = useAlertListSelector(selectors.uiQueryParams); const searchBarIndexPatterns = useAlertListSelector(selectors.searchBarIndexPatterns); + + // Deeply clone the search bar index patterns as the receiving component may mutate them + const clonedSearchBarIndexPatterns = useMemo( + () => searchBarIndexPatterns.map(pattern => clone(pattern)), + [searchBarIndexPatterns] + ); const searchBarQuery = useAlertListSelector(selectors.searchBarQuery); const searchBarDateRange = useAlertListSelector(selectors.searchBarDateRange); const searchBarFilters = useAlertListSelector(selectors.searchBarFilters); @@ -68,7 +75,7 @@ export const AlertIndexSearchBar = memo(() => { dataTestSubj="alertsSearchBar" appName="endpoint" isLoading={false} - indexPatterns={searchBarIndexPatterns} + indexPatterns={clonedSearchBarIndexPatterns} query={searchBarQuery} dateRangeFrom={searchBarDateRange.from} dateRangeTo={searchBarDateRange.to} diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/view/app_root.tsx b/x-pack/plugins/endpoint/public/applications/endpoint/view/app_root.tsx new file mode 100644 index 0000000000000..f9634c63deefb --- /dev/null +++ b/x-pack/plugins/endpoint/public/applications/endpoint/view/app_root.tsx @@ -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 * as React from 'react'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { Route, Switch } from 'react-router-dom'; +import { Store } from 'redux'; +import { AlertIndex } from './alerts'; +import { HostList } from './hosts'; +import { PolicyList } from './policy'; +import { PolicyDetails } from './policy'; +import { HeaderNavigation } from './components/header_navigation'; +import { AppRootProvider } from './app_root_provider'; +import { Setup } from './setup'; +import { EndpointPluginStartDependencies } from '../../../plugin'; +import { ScopedHistory, CoreStart } from '../../../../../../../src/core/public'; + +interface RouterProps { + history: ScopedHistory; + store: Store; + coreStart: CoreStart; + depsStart: EndpointPluginStartDependencies; +} + +/** + * The root of the Endpoint application react view. + */ +export const AppRoot: React.FunctionComponent<RouterProps> = React.memo( + ({ history, store, coreStart, depsStart }) => { + return ( + <AppRootProvider store={store} history={history} coreStart={coreStart} depsStart={depsStart}> + <Setup ingestManager={depsStart.ingestManager} notifications={coreStart.notifications} /> + <HeaderNavigation /> + <Switch> + <Route + exact + path="/" + render={() => ( + <h1 data-test-subj="welcomeTitle"> + <FormattedMessage id="xpack.endpoint.welcomeTitle" defaultMessage="Hello World" /> + </h1> + )} + /> + <Route path="/hosts" component={HostList} /> + <Route path="/alerts" component={AlertIndex} /> + <Route path="/policy" exact component={PolicyList} /> + <Route path="/policy/:id" exact component={PolicyDetails} /> + <Route + render={() => ( + <FormattedMessage id="xpack.endpoint.notFound" defaultMessage="Page Not Found" /> + )} + /> + </Switch> + </AppRootProvider> + ); + } +); diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/components/__snapshots__/link_to_app.test.tsx.snap b/x-pack/plugins/endpoint/public/applications/endpoint/view/components/__snapshots__/link_to_app.test.tsx.snap similarity index 100% rename from x-pack/plugins/endpoint/public/applications/endpoint/components/__snapshots__/link_to_app.test.tsx.snap rename to x-pack/plugins/endpoint/public/applications/endpoint/view/components/__snapshots__/link_to_app.test.tsx.snap diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/components/__snapshots__/page_view.test.tsx.snap b/x-pack/plugins/endpoint/public/applications/endpoint/view/components/__snapshots__/page_view.test.tsx.snap similarity index 99% rename from x-pack/plugins/endpoint/public/applications/endpoint/components/__snapshots__/page_view.test.tsx.snap rename to x-pack/plugins/endpoint/public/applications/endpoint/view/components/__snapshots__/page_view.test.tsx.snap index dfc69fc46ebdc..36b602a1e6784 100644 --- a/x-pack/plugins/endpoint/public/applications/endpoint/components/__snapshots__/page_view.test.tsx.snap +++ b/x-pack/plugins/endpoint/public/applications/endpoint/view/components/__snapshots__/page_view.test.tsx.snap @@ -7,6 +7,7 @@ exports[`PageView component should display body header custom element 1`] = ` .c0.endpoint--isListView .endpoint-header { padding: 24px; + margin-bottom: 0; } .c0.endpoint--isListView .endpoint-page-content { @@ -97,6 +98,7 @@ exports[`PageView component should display body header wrapped in EuiTitle 1`] = .c0.endpoint--isListView .endpoint-header { padding: 24px; + margin-bottom: 0; } .c0.endpoint--isListView .endpoint-page-content { @@ -190,6 +192,7 @@ exports[`PageView component should display header left and right 1`] = ` .c0.endpoint--isListView .endpoint-header { padding: 24px; + margin-bottom: 0; } .c0.endpoint--isListView .endpoint-page-content { @@ -298,6 +301,7 @@ exports[`PageView component should display only body if not header props used 1` .c0.endpoint--isListView .endpoint-header { padding: 24px; + margin-bottom: 0; } .c0.endpoint--isListView .endpoint-page-content { @@ -365,6 +369,7 @@ exports[`PageView component should display only header left 1`] = ` .c0.endpoint--isListView .endpoint-header { padding: 24px; + margin-bottom: 0; } .c0.endpoint--isListView .endpoint-page-content { @@ -462,6 +467,7 @@ exports[`PageView component should display only header right but include an empt .c0.endpoint--isListView .endpoint-header { padding: 24px; + margin-bottom: 0; } .c0.endpoint--isListView .endpoint-page-content { @@ -556,6 +562,7 @@ exports[`PageView component should pass through EuiPage props 1`] = ` .c0.endpoint--isListView .endpoint-header { padding: 24px; + margin-bottom: 0; } .c0.endpoint--isListView .endpoint-page-content { @@ -640,6 +647,7 @@ exports[`PageView component should use custom element for header left and not wr .c0.endpoint--isListView .endpoint-header { padding: 24px; + margin-bottom: 0; } .c0.endpoint--isListView .endpoint-page-content { diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/view/components/header_navigation.tsx b/x-pack/plugins/endpoint/public/applications/endpoint/view/components/header_navigation.tsx new file mode 100644 index 0000000000000..6c294d9c86548 --- /dev/null +++ b/x-pack/plugins/endpoint/public/applications/endpoint/view/components/header_navigation.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, { MouseEvent, useMemo } from 'react'; +import { i18n } from '@kbn/i18n'; +import { EuiTabs, EuiTab } from '@elastic/eui'; +import { useHistory, useLocation } from 'react-router-dom'; +import { useKibana } from '../../../../../../../../src/plugins/kibana_react/public'; +import { Immutable } from '../../../../../common/types'; + +interface NavTabs { + name: string; + id: string; + href: string; +} + +const navTabs: Immutable<NavTabs[]> = [ + { + id: 'home', + name: i18n.translate('xpack.endpoint.headerNav.home', { + defaultMessage: 'Home', + }), + href: '/', + }, + { + id: 'hosts', + name: i18n.translate('xpack.endpoint.headerNav.hosts', { + defaultMessage: 'Hosts', + }), + href: '/hosts', + }, + { + id: 'alerts', + name: i18n.translate('xpack.endpoint.headerNav.alerts', { + defaultMessage: 'Alerts', + }), + href: '/alerts', + }, + { + id: 'policies', + name: i18n.translate('xpack.endpoint.headerNav.policies', { + defaultMessage: 'Policies', + }), + href: '/policy', + }, +]; + +export const HeaderNavigation: React.FunctionComponent = React.memo(() => { + const history = useHistory(); + const location = useLocation(); + const { services } = useKibana(); + const BASE_PATH = services.application.getUrlForApp('endpoint'); + + const tabList = useMemo(() => { + return navTabs.map((tab, index) => { + return ( + <EuiTab + data-test-subj={`${tab.id}EndpointTab`} + key={index} + href={`${BASE_PATH}${tab.href}`} + onClick={(event: MouseEvent) => { + event.preventDefault(); + history.push(tab.href); + }} + isSelected={ + tab.href === location.pathname || + (tab.href !== '/' && location.pathname.startsWith(tab.href)) + } + > + {tab.name} + </EuiTab> + ); + }); + }, [BASE_PATH, history, location.pathname]); + + return <EuiTabs>{tabList}</EuiTabs>; +}); diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/components/link_to_app.test.tsx b/x-pack/plugins/endpoint/public/applications/endpoint/view/components/link_to_app.test.tsx similarity index 86% rename from x-pack/plugins/endpoint/public/applications/endpoint/components/link_to_app.test.tsx rename to x-pack/plugins/endpoint/public/applications/endpoint/view/components/link_to_app.test.tsx index 902c215434aac..d0a8f9690dafb 100644 --- a/x-pack/plugins/endpoint/public/applications/endpoint/components/link_to_app.test.tsx +++ b/x-pack/plugins/endpoint/public/applications/endpoint/view/components/link_to_app.test.tsx @@ -7,9 +7,14 @@ import React from 'react'; import { mount } from 'enzyme'; import { LinkToApp } from './link_to_app'; -import { KibanaContextProvider } from '../../../../../../../src/plugins/kibana_react/public'; -import { coreMock } from '../../../../../../../src/core/public/mocks'; import { CoreStart } from 'kibana/public'; +import { KibanaContextProvider } from '../../../../../../../../src/plugins/kibana_react/public'; +import { coreMock } from '../../../../../../../../src/core/public/mocks'; + +type LinkToAppOnClickMock<Return = void> = jest.Mock< + Return, + [React.MouseEvent<HTMLAnchorElement, MouseEvent>] +>; describe('LinkToApp component', () => { let fakeCoreStart: jest.Mocked<CoreStart>; @@ -38,7 +43,8 @@ describe('LinkToApp component', () => { ).toMatchSnapshot(); }); it('should support onClick prop', () => { - const spyOnClickHandler = jest.fn(); + // Take `_event` (even though it is not used) so that `jest.fn` will have a type that expects to be called with an event + const spyOnClickHandler: LinkToAppOnClickMock = jest.fn(_event => {}); const renderResult = render( <LinkToApp appId="ingestManager" href="/app/ingest" onClick={spyOnClickHandler}> link @@ -91,7 +97,8 @@ describe('LinkToApp component', () => { }); }); it('should still preventDefault if onClick callback throws', () => { - const spyOnClickHandler = jest.fn(ev => { + // Take `_event` (even though it is not used) so that `jest.fn` will have a type that expects to be called with an event + const spyOnClickHandler: LinkToAppOnClickMock<never> = jest.fn(_event => { throw new Error('test'); }); const renderResult = render( @@ -104,7 +111,7 @@ describe('LinkToApp component', () => { expect(clickEventArg.isDefaultPrevented()).toBe(true); }); it('should not navigate if onClick callback prevents defalut', () => { - const spyOnClickHandler = jest.fn(ev => { + const spyOnClickHandler: LinkToAppOnClickMock = jest.fn(ev => { ev.preventDefault(); }); const renderResult = render( diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/components/link_to_app.tsx b/x-pack/plugins/endpoint/public/applications/endpoint/view/components/link_to_app.tsx similarity index 96% rename from x-pack/plugins/endpoint/public/applications/endpoint/components/link_to_app.tsx rename to x-pack/plugins/endpoint/public/applications/endpoint/view/components/link_to_app.tsx index 858dac864b58a..6a3cf5e78f4bf 100644 --- a/x-pack/plugins/endpoint/public/applications/endpoint/components/link_to_app.tsx +++ b/x-pack/plugins/endpoint/public/applications/endpoint/view/components/link_to_app.tsx @@ -9,7 +9,7 @@ import { EuiLink } from '@elastic/eui'; import { EuiLinkProps } from '@elastic/eui'; import { useNavigateToAppEventHandler } from '../hooks/use_navigate_to_app_event_handler'; -export type LinkToAppProps = EuiLinkProps & { +type LinkToAppProps = EuiLinkProps & { /** the app id - normally the value of the `id` in that plugin's `kibana.json` */ appId: string; /** Any app specific path (route) */ diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/components/page_view.test.tsx b/x-pack/plugins/endpoint/public/applications/endpoint/view/components/page_view.test.tsx similarity index 96% rename from x-pack/plugins/endpoint/public/applications/endpoint/components/page_view.test.tsx rename to x-pack/plugins/endpoint/public/applications/endpoint/view/components/page_view.test.tsx index 0d4d26737d355..4007477a088fa 100644 --- a/x-pack/plugins/endpoint/public/applications/endpoint/components/page_view.test.tsx +++ b/x-pack/plugins/endpoint/public/applications/endpoint/view/components/page_view.test.tsx @@ -7,7 +7,7 @@ import React from 'react'; import { mount } from 'enzyme'; import { PageView } from './page_view'; -import { EuiThemeProvider } from '../../../../../../legacy/common/eui_styled_components'; +import { EuiThemeProvider } from '../../../../../../../legacy/common/eui_styled_components'; describe('PageView component', () => { const render = (ui: Parameters<typeof mount>[0]) => diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/components/page_view.tsx b/x-pack/plugins/endpoint/public/applications/endpoint/view/components/page_view.tsx similarity index 99% rename from x-pack/plugins/endpoint/public/applications/endpoint/components/page_view.tsx rename to x-pack/plugins/endpoint/public/applications/endpoint/view/components/page_view.tsx index 561d671e18e07..6da352b68f890 100644 --- a/x-pack/plugins/endpoint/public/applications/endpoint/components/page_view.tsx +++ b/x-pack/plugins/endpoint/public/applications/endpoint/view/components/page_view.tsx @@ -25,6 +25,7 @@ const StyledEuiPage = styled(EuiPage)` .endpoint-header { padding: ${props => props.theme.eui.euiSizeL}; + margin-bottom: 0; } .endpoint-page-content { border-left: none; diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/hooks/use_navigate_to_app_event_handler.ts b/x-pack/plugins/endpoint/public/applications/endpoint/view/hooks/use_navigate_to_app_event_handler.ts similarity index 96% rename from x-pack/plugins/endpoint/public/applications/endpoint/hooks/use_navigate_to_app_event_handler.ts rename to x-pack/plugins/endpoint/public/applications/endpoint/view/hooks/use_navigate_to_app_event_handler.ts index 5fbfa5e0e58a8..ec9a8691c481e 100644 --- a/x-pack/plugins/endpoint/public/applications/endpoint/hooks/use_navigate_to_app_event_handler.ts +++ b/x-pack/plugins/endpoint/public/applications/endpoint/view/hooks/use_navigate_to_app_event_handler.ts @@ -6,7 +6,7 @@ import { MouseEventHandler, useCallback } from 'react'; import { ApplicationStart } from 'kibana/public'; -import { useKibana } from '../../../../../../../src/plugins/kibana_react/public'; +import { useKibana } from '../../../../../../../../src/plugins/kibana_react/public'; type NavigateToAppHandlerProps = Parameters<ApplicationStart['navigateToApp']>; type EventHandlerCallback = MouseEventHandler<HTMLButtonElement | HTMLAnchorElement>; diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/view/hosts/details.tsx b/x-pack/plugins/endpoint/public/applications/endpoint/view/hosts/details.tsx deleted file mode 100644 index 90829f7ad4cbe..0000000000000 --- a/x-pack/plugins/endpoint/public/applications/endpoint/view/hosts/details.tsx +++ /dev/null @@ -1,200 +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, { useCallback, useMemo, memo, useEffect } from 'react'; -import { - EuiFlyout, - EuiFlyoutBody, - EuiFlyoutHeader, - EuiTitle, - EuiDescriptionList, - EuiLoadingContent, - EuiHorizontalRule, - EuiHealth, - EuiSpacer, - EuiListGroup, - EuiListGroupItem, -} from '@elastic/eui'; -import { useHistory } from 'react-router-dom'; -import styled from 'styled-components'; -import { i18n } from '@kbn/i18n'; -import { FormattedMessage } from '@kbn/i18n/react'; -import { useKibana } from '../../../../../../../../src/plugins/kibana_react/public'; -import { HostMetadata } from '../../../../../common/types'; -import { useHostListSelector } from './hooks'; -import { urlFromQueryParams } from './url_from_query_params'; -import { FormattedDateAndTime } from '../formatted_date_time'; -import { uiQueryParams, detailsData, detailsError } from './../../store/hosts/selectors'; -import { LinkToApp } from '../../components/link_to_app'; - -const HostIds = styled(EuiListGroupItem)` - margin-top: 0; - .euiListGroupItem__text { - padding: 0; - } -`; - -const HostDetails = memo(({ details }: { details: HostMetadata }) => { - const { appId, appPath, url } = useHostLogsUrl(details.host.id); - const detailsResultsUpper = useMemo(() => { - return [ - { - title: i18n.translate('xpack.endpoint.host.details.os', { - defaultMessage: 'OS', - }), - description: details.host.os.full, - }, - { - title: i18n.translate('xpack.endpoint.host.details.lastSeen', { - defaultMessage: 'Last Seen', - }), - description: <FormattedDateAndTime date={new Date(details['@timestamp'])} />, - }, - { - title: i18n.translate('xpack.endpoint.host.details.alerts', { - defaultMessage: 'Alerts', - }), - description: '0', - }, - ]; - }, [details]); - - const detailsResultsLower = useMemo(() => { - return [ - { - title: i18n.translate('xpack.endpoint.host.details.policy', { - defaultMessage: 'Policy', - }), - description: details.endpoint.policy.id, - }, - { - title: i18n.translate('xpack.endpoint.host.details.policyStatus', { - defaultMessage: 'Policy Status', - }), - description: <EuiHealth color="success">active</EuiHealth>, - }, - { - title: i18n.translate('xpack.endpoint.host.details.ipAddress', { - defaultMessage: 'IP Address', - }), - description: ( - <EuiListGroup flush> - {details.host.ip.map((ip: string, index: number) => ( - <HostIds key={index} label={ip} /> - ))} - </EuiListGroup> - ), - }, - { - title: i18n.translate('xpack.endpoint.host.details.hostname', { - defaultMessage: 'Hostname', - }), - description: details.host.hostname, - }, - { - title: i18n.translate('xpack.endpoint.host.details.sensorVersion', { - defaultMessage: 'Sensor Version', - }), - description: details.agent.version, - }, - ]; - }, [details.agent.version, details.endpoint.policy.id, details.host.hostname, details.host.ip]); - return ( - <> - <EuiDescriptionList - type="column" - listItems={detailsResultsUpper} - data-test-subj="hostDetailsUpperList" - /> - <EuiHorizontalRule margin="s" /> - <EuiDescriptionList - type="column" - listItems={detailsResultsLower} - data-test-subj="hostDetailsLowerList" - /> - <EuiHorizontalRule margin="s" /> - <p> - <LinkToApp - appId={appId} - appPath={appPath} - href={url} - data-test-subj="hostDetailsLinkToLogs" - > - <FormattedMessage - id="xpack.endpoint.host.details.linkToLogsTitle" - defaultMessage="Endpoint Logs" - /> - </LinkToApp> - </p> - </> - ); -}); - -export const HostDetailsFlyout = () => { - const history = useHistory(); - const { notifications } = useKibana(); - const queryParams = useHostListSelector(uiQueryParams); - const { selected_host: selectedHost, ...queryParamsWithoutSelectedHost } = queryParams; - const details = useHostListSelector(detailsData); - const error = useHostListSelector(detailsError); - - const handleFlyoutClose = useCallback(() => { - history.push(urlFromQueryParams(queryParamsWithoutSelectedHost)); - }, [history, queryParamsWithoutSelectedHost]); - - useEffect(() => { - if (error !== undefined) { - notifications.toasts.danger({ - title: ( - <FormattedMessage - id="xpack.endpoint.host.details.errorTitle" - defaultMessage="Could not find host" - /> - ), - body: ( - <FormattedMessage - id="xpack.endpoint.host.details.errorBody" - defaultMessage="Please exit the flyout and select an available host." - /> - ), - toastLifeTimeMs: 10000, - }); - } - }, [error, notifications.toasts]); - - return ( - <EuiFlyout onClose={handleFlyoutClose} data-test-subj="hostDetailsFlyout"> - <EuiFlyoutHeader hasBorder> - <EuiTitle size="s"> - <h2 data-test-subj="hostDetailsFlyoutTitle"> - {details === undefined ? <EuiLoadingContent lines={1} /> : details.host.hostname} - </h2> - </EuiTitle> - </EuiFlyoutHeader> - <EuiFlyoutBody> - {details === undefined ? ( - <> - <EuiLoadingContent lines={3} /> <EuiSpacer size="l" /> <EuiLoadingContent lines={3} /> - </> - ) : ( - <HostDetails details={details} /> - )} - </EuiFlyoutBody> - </EuiFlyout> - ); -}; - -const useHostLogsUrl = (hostId: string): { url: string; appId: string; appPath: string } => { - const { services } = useKibana(); - return useMemo(() => { - const appPath = `/stream?logFilter=(expression:'host.id:${hostId}',kind:kuery)`; - return { - url: `${services.application.getUrlForApp('logs')}${appPath}`, - appId: 'logs', - appPath, - }; - }, [hostId, services.application]); -}; diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/view/hosts/details/components/flyout_sub_header.tsx b/x-pack/plugins/endpoint/public/applications/endpoint/view/hosts/details/components/flyout_sub_header.tsx new file mode 100644 index 0000000000000..26f2203790a9e --- /dev/null +++ b/x-pack/plugins/endpoint/public/applications/endpoint/view/hosts/details/components/flyout_sub_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, { memo } from 'react'; +import { EuiFlyoutHeader, CommonProps, EuiButtonEmpty } from '@elastic/eui'; +import styled from 'styled-components'; + +export type FlyoutSubHeaderProps = CommonProps & { + children: React.ReactNode; + backButton?: { + title: string; + onClick: (event: React.MouseEvent) => void; + href?: string; + }; +}; + +const StyledEuiFlyoutHeader = styled(EuiFlyoutHeader)` + padding: ${props => props.theme.eui.paddingSizes.s}; + + &.hasButtons { + .buttons { + padding-bottom: ${props => props.theme.eui.paddingSizes.s}; + } + + .back-button-content { + padding-left: 0; + &-text { + margin-left: 0; + } + } + } + + .flyout-content { + padding-left: ${props => props.theme.eui.paddingSizes.m}; + } +`; + +const BUTTON_CONTENT_PROPS = Object.freeze({ className: 'back-button-content' }); +const BUTTON_TEXT_PROPS = Object.freeze({ className: 'back-button-content-text' }); + +/** + * A Eui Flyout Header component that has its styles adjusted to display a panel sub-header. + * Component also provides a way to display a "back" button above the header title. + */ +export const FlyoutSubHeader = memo<FlyoutSubHeaderProps>( + ({ children, backButton, ...otherProps }) => { + return ( + <StyledEuiFlyoutHeader hasBorder {...otherProps} className={backButton && `hasButtons`}> + {backButton && ( + <div className="buttons"> + {/* eslint-disable-next-line @elastic/eui/href-or-on-click */} + <EuiButtonEmpty + data-test-subj="flyoutSubHeaderBackButton" + iconType="arrowLeft" + contentProps={BUTTON_CONTENT_PROPS} + textProps={BUTTON_TEXT_PROPS} + size="xs" + href={backButton?.href ?? ''} + onClick={backButton?.onClick} + > + {backButton?.title} + </EuiButtonEmpty> + </div> + )} + <div className={'flyout-content'}>{children}</div> + </StyledEuiFlyoutHeader> + ); + } +); diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/view/hosts/details/host_details.tsx b/x-pack/plugins/endpoint/public/applications/endpoint/view/hosts/details/host_details.tsx new file mode 100644 index 0000000000000..32c69426b03f3 --- /dev/null +++ b/x-pack/plugins/endpoint/public/applications/endpoint/view/hosts/details/host_details.tsx @@ -0,0 +1,163 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import styled from 'styled-components'; +import { + EuiDescriptionList, + EuiHealth, + EuiHorizontalRule, + EuiLink, + EuiListGroup, + EuiListGroupItem, +} from '@elastic/eui'; +import React, { memo, useMemo } from 'react'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { i18n } from '@kbn/i18n'; +import { useHistory } from 'react-router-dom'; +import { HostMetadata } from '../../../../../../common/types'; +import { FormattedDateAndTime } from '../../formatted_date_time'; +import { LinkToApp } from '../../components/link_to_app'; +import { useHostListSelector, useHostLogsUrl } from '../hooks'; +import { urlFromQueryParams } from '../url_from_query_params'; +import { uiQueryParams } from '../../../store/hosts/selectors'; + +const HostIds = styled(EuiListGroupItem)` + margin-top: 0; + .euiListGroupItem__text { + padding: 0; + } +`; + +export const HostDetails = memo(({ details }: { details: HostMetadata }) => { + const { appId, appPath, url } = useHostLogsUrl(details.host.id); + const queryParams = useHostListSelector(uiQueryParams); + const history = useHistory(); + const detailsResultsUpper = useMemo(() => { + return [ + { + title: i18n.translate('xpack.endpoint.host.details.os', { + defaultMessage: 'OS', + }), + description: details.host.os.full, + }, + { + title: i18n.translate('xpack.endpoint.host.details.lastSeen', { + defaultMessage: 'Last Seen', + }), + description: <FormattedDateAndTime date={new Date(details['@timestamp'])} />, + }, + { + title: i18n.translate('xpack.endpoint.host.details.alerts', { + defaultMessage: 'Alerts', + }), + description: '0', + }, + ]; + }, [details]); + + const policyResponseUri = useMemo(() => { + return urlFromQueryParams({ + ...queryParams, + selected_host: details.host.id, + show: 'policy_response', + }); + }, [details.host.id, queryParams]); + + const detailsResultsLower = useMemo(() => { + return [ + { + title: i18n.translate('xpack.endpoint.host.details.policy', { + defaultMessage: 'Policy', + }), + description: details.endpoint.policy.id, + }, + { + title: i18n.translate('xpack.endpoint.host.details.policyStatus', { + defaultMessage: 'Policy Status', + }), + description: ( + <EuiHealth color="success"> + {/* eslint-disable-next-line @elastic/eui/href-or-on-click */} + <EuiLink + data-test-subj="policyStatusValue" + href={'?' + policyResponseUri.search} + onClick={(ev: React.MouseEvent) => { + ev.preventDefault(); + history.push(policyResponseUri); + }} + > + <FormattedMessage + id="xpack.endpoint.host.details.policyStatus.success" + defaultMessage="Successful" + /> + </EuiLink> + </EuiHealth> + ), + }, + { + title: i18n.translate('xpack.endpoint.host.details.ipAddress', { + defaultMessage: 'IP Address', + }), + description: ( + <EuiListGroup flush> + {details.host.ip.map((ip: string, index: number) => ( + <HostIds key={index} label={ip} /> + ))} + </EuiListGroup> + ), + }, + { + title: i18n.translate('xpack.endpoint.host.details.hostname', { + defaultMessage: 'Hostname', + }), + description: details.host.hostname, + }, + { + title: i18n.translate('xpack.endpoint.host.details.sensorVersion', { + defaultMessage: 'Sensor Version', + }), + description: details.agent.version, + }, + ]; + }, [ + details.agent.version, + details.endpoint.policy.id, + details.host.hostname, + details.host.ip, + history, + policyResponseUri, + ]); + + return ( + <> + <EuiDescriptionList + type="column" + listItems={detailsResultsUpper} + data-test-subj="hostDetailsUpperList" + /> + <EuiHorizontalRule margin="s" /> + <EuiDescriptionList + type="column" + listItems={detailsResultsLower} + data-test-subj="hostDetailsLowerList" + /> + <EuiHorizontalRule margin="s" /> + <p> + <LinkToApp + appId={appId} + appPath={appPath} + href={url} + data-test-subj="hostDetailsLinkToLogs" + > + <FormattedMessage + id="xpack.endpoint.host.details.linkToLogsTitle" + defaultMessage="Endpoint Logs" + /> + </LinkToApp> + </p> + </> + ); +}); diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/view/hosts/details/index.tsx b/x-pack/plugins/endpoint/public/applications/endpoint/view/hosts/details/index.tsx new file mode 100644 index 0000000000000..a41d4a968f177 --- /dev/null +++ b/x-pack/plugins/endpoint/public/applications/endpoint/view/hosts/details/index.tsx @@ -0,0 +1,134 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { useCallback, useEffect, memo, useMemo } from 'react'; +import { + EuiFlyout, + EuiFlyoutBody, + EuiFlyoutHeader, + EuiTitle, + EuiLoadingContent, + EuiSpacer, +} from '@elastic/eui'; +import { useHistory } from 'react-router-dom'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { i18n } from '@kbn/i18n'; +import { useKibana } from '../../../../../../../../../src/plugins/kibana_react/public'; +import { useHostListSelector } from '../hooks'; +import { urlFromQueryParams } from '../url_from_query_params'; +import { uiQueryParams, detailsData, detailsError, showView } from '../../../store/hosts/selectors'; +import { HostDetails } from './host_details'; +import { PolicyResponse } from './policy_response'; +import { HostMetadata } from '../../../../../../common/types'; +import { FlyoutSubHeader, FlyoutSubHeaderProps } from './components/flyout_sub_header'; + +export const HostDetailsFlyout = memo(() => { + const history = useHistory(); + const { notifications } = useKibana(); + const queryParams = useHostListSelector(uiQueryParams); + const { selected_host: selectedHost, ...queryParamsWithoutSelectedHost } = queryParams; + const details = useHostListSelector(detailsData); + const error = useHostListSelector(detailsError); + const show = useHostListSelector(showView); + + const handleFlyoutClose = useCallback(() => { + history.push(urlFromQueryParams(queryParamsWithoutSelectedHost)); + }, [history, queryParamsWithoutSelectedHost]); + + useEffect(() => { + if (error !== undefined) { + notifications.toasts.danger({ + title: ( + <FormattedMessage + id="xpack.endpoint.host.details.errorTitle" + defaultMessage="Could not find host" + /> + ), + body: ( + <FormattedMessage + id="xpack.endpoint.host.details.errorBody" + defaultMessage="Please exit the flyout and select an available host." + /> + ), + toastLifeTimeMs: 10000, + }); + } + }, [error, notifications.toasts]); + + return ( + <EuiFlyout onClose={handleFlyoutClose} data-test-subj="hostDetailsFlyout" size="s"> + <EuiFlyoutHeader hasBorder> + <EuiTitle size="s"> + <h2 data-test-subj="hostDetailsFlyoutTitle"> + {details === undefined ? <EuiLoadingContent lines={1} /> : details.host.hostname} + </h2> + </EuiTitle> + </EuiFlyoutHeader> + {details === undefined ? ( + <> + <EuiFlyoutBody> + <EuiLoadingContent lines={3} /> <EuiSpacer size="l" /> <EuiLoadingContent lines={3} /> + </EuiFlyoutBody> + </> + ) : ( + <> + {show === 'details' && ( + <> + <EuiFlyoutBody data-test-subj="hostDetailsFlyoutBody"> + <HostDetails details={details} /> + </EuiFlyoutBody> + </> + )} + {show === 'policy_response' && <PolicyResponseFlyoutPanel hostMeta={details} />} + </> + )} + </EuiFlyout> + ); +}); + +const PolicyResponseFlyoutPanel = memo<{ + hostMeta: HostMetadata; +}>(({ hostMeta }) => { + const history = useHistory(); + const { show, ...queryParams } = useHostListSelector(uiQueryParams); + const backButtonProp = useMemo((): FlyoutSubHeaderProps['backButton'] => { + const detailsUri = urlFromQueryParams({ + ...queryParams, + selected_host: hostMeta.host.id, + }); + return { + title: i18n.translate('xpack.endpoint.host.policyResponse.backLinkTitle', { + defaultMessage: 'Endpoint Details', + }), + href: '?' + detailsUri.search, + onClick: ev => { + ev.preventDefault(); + history.push(detailsUri); + }, + }; + }, [history, hostMeta.host.id, queryParams]); + + return ( + <> + <FlyoutSubHeader + backButton={backButtonProp} + data-test-subj="hostDetailsPolicyResponseFlyoutHeader" + > + <EuiTitle size="xxs" data-test-subj="hostDetailsPolicyResponseFlyoutTitle"> + <h3> + <FormattedMessage + id="xpack.endpoint.host.policyResponse.title" + defaultMessage="Policy Response" + /> + </h3> + </EuiTitle> + </FlyoutSubHeader> + <EuiFlyoutBody data-test-subj="hostDetailsPolicyResponseFlyoutBody"> + <PolicyResponse /> + </EuiFlyoutBody> + </> + ); +}); diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/view/hosts/details/policy_response.tsx b/x-pack/plugins/endpoint/public/applications/endpoint/view/hosts/details/policy_response.tsx new file mode 100644 index 0000000000000..eacb6a52d3184 --- /dev/null +++ b/x-pack/plugins/endpoint/public/applications/endpoint/view/hosts/details/policy_response.tsx @@ -0,0 +1,10 @@ +/* + * 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'; + +export const PolicyResponse = memo(() => { + return <div>Policy Status to be displayed here soon.</div>; +}); diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/view/hosts/hooks.ts b/x-pack/plugins/endpoint/public/applications/endpoint/view/hosts/hooks.ts index 99a0073f46c74..7eb51f3a7b294 100644 --- a/x-pack/plugins/endpoint/public/applications/endpoint/view/hosts/hooks.ts +++ b/x-pack/plugins/endpoint/public/applications/endpoint/view/hosts/hooks.ts @@ -5,10 +5,28 @@ */ import { useSelector } from 'react-redux'; +import { useMemo } from 'react'; import { GlobalState, HostListState } from '../../types'; +import { useKibana } from '../../../../../../../../src/plugins/kibana_react/public'; export function useHostListSelector<TSelected>(selector: (state: HostListState) => TSelected) { return useSelector(function(state: GlobalState) { return selector(state.hostList); }); } + +/** + * Returns an object that contains Kibana Logs app and URL information for a given host id + * @param hostId + */ +export const useHostLogsUrl = (hostId: string): { url: string; appId: string; appPath: string } => { + const { services } = useKibana(); + return useMemo(() => { + const appPath = `/stream?logFilter=(expression:'host.id:${hostId}',kind:kuery)`; + return { + url: `${services.application.getUrlForApp('logs')}${appPath}`, + appId: 'logs', + appPath, + }; + }, [hostId, services.application]); +}; diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/view/hosts/index.test.tsx b/x-pack/plugins/endpoint/public/applications/endpoint/view/hosts/index.test.tsx index c3ff41268e3db..88416b577ed0c 100644 --- a/x-pack/plugins/endpoint/public/applications/endpoint/view/hosts/index.test.tsx +++ b/x-pack/plugins/endpoint/public/applications/endpoint/view/hosts/index.test.tsx @@ -21,10 +21,11 @@ describe('when on the hosts page', () => { let history: AppContextTestRender['history']; let store: AppContextTestRender['store']; let coreStart: AppContextTestRender['coreStart']; + let middlewareSpy: AppContextTestRender['middlewareSpy']; beforeEach(async () => { const mockedContext = createAppRootMockRenderer(); - ({ history, store, coreStart } = mockedContext); + ({ history, store, coreStart, middlewareSpy } = mockedContext); render = () => mockedContext.render(<HostList />); }); @@ -132,6 +133,25 @@ describe('when on the hosts page', () => { expect(flyout).not.toBeNull(); }); }); + it('should display policy status value as a link', async () => { + const renderResult = render(); + const policyStatusLink = await renderResult.findByTestId('policyStatusValue'); + expect(policyStatusLink).not.toBeNull(); + expect(policyStatusLink.textContent).toEqual('Successful'); + expect(policyStatusLink.getAttribute('href')).toEqual( + '?selected_host=1&show=policy_response' + ); + }); + it('should update the URL when policy status link is clicked', async () => { + const renderResult = render(); + const policyStatusLink = await renderResult.findByTestId('policyStatusValue'); + const userChangedUrlChecker = middlewareSpy.waitForAction('userChangedUrl'); + reactTestingLibrary.act(() => { + fireEvent.click(policyStatusLink); + }); + const changedUrlAction = await userChangedUrlChecker; + expect(changedUrlAction.payload.search).toEqual('?selected_host=1&show=policy_response'); + }); it('should include the link to logs', async () => { const renderResult = render(); const linkToLogs = await renderResult.findByTestId('hostDetailsLinkToLogs'); @@ -151,9 +171,51 @@ describe('when on the hosts page', () => { }); it('should navigate to logs without full page refresh', async () => { - // FIXME: this is not working :( expect(coreStart.application.navigateToApp.mock.calls).toHaveLength(1); }); }); + describe('when showing host Policy Response', () => { + let renderResult: ReturnType<typeof render>; + beforeEach(async () => { + renderResult = render(); + const policyStatusLink = await renderResult.findByTestId('policyStatusValue'); + const userChangedUrlChecker = middlewareSpy.waitForAction('userChangedUrl'); + reactTestingLibrary.act(() => { + fireEvent.click(policyStatusLink); + }); + await userChangedUrlChecker; + }); + it('should hide the host details panel', async () => { + const hostDetailsFlyout = await renderResult.queryByTestId('hostDetailsFlyoutBody'); + expect(hostDetailsFlyout).toBeNull(); + }); + it('should display policy response sub-panel', async () => { + expect( + await renderResult.findByTestId('hostDetailsPolicyResponseFlyoutHeader') + ).not.toBeNull(); + expect( + await renderResult.findByTestId('hostDetailsPolicyResponseFlyoutBody') + ).not.toBeNull(); + }); + it('should include the sub-panel title', async () => { + expect( + (await renderResult.findByTestId('hostDetailsPolicyResponseFlyoutTitle')).textContent + ).toBe('Policy Response'); + }); + it('should include the back to details link', async () => { + const subHeaderBackLink = await renderResult.findByTestId('flyoutSubHeaderBackButton'); + expect(subHeaderBackLink.textContent).toBe('Endpoint Details'); + expect(subHeaderBackLink.getAttribute('href')).toBe('?selected_host=1'); + }); + it('should update URL when back to details link is clicked', async () => { + const subHeaderBackLink = await renderResult.findByTestId('flyoutSubHeaderBackButton'); + const userChangedUrlChecker = middlewareSpy.waitForAction('userChangedUrl'); + reactTestingLibrary.act(() => { + fireEvent.click(subHeaderBackLink); + }); + const changedUrlAction = await userChangedUrlChecker; + expect(changedUrlAction.payload.search).toEqual('?selected_host=1'); + }); + }); }); }); diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/view/hosts/index.tsx b/x-pack/plugins/endpoint/public/applications/endpoint/view/hosts/index.tsx index 94625b8c66191..1d81d6e8a16db 100644 --- a/x-pack/plugins/endpoint/public/applications/endpoint/view/hosts/index.tsx +++ b/x-pack/plugins/endpoint/public/applications/endpoint/view/hosts/index.tsx @@ -23,12 +23,14 @@ import { i18n } from '@kbn/i18n'; import styled from 'styled-components'; import { FormattedMessage } from '@kbn/i18n/react'; import { createStructuredSelector } from 'reselect'; +import { EuiBasicTableColumn } from '@elastic/eui'; import { HostDetailsFlyout } from './details'; import * as selectors from '../../store/hosts/selectors'; import { HostAction } from '../../store/hosts/action'; import { useHostListSelector } from './hooks'; import { CreateStructuredSelector } from '../../types'; import { urlFromQueryParams } from './url_from_query_params'; +import { HostMetadata, Immutable } from '../../../../../common/types'; const selector = (createStructuredSelector as CreateStructuredSelector)(selectors); export const HostList = () => { @@ -65,7 +67,7 @@ export const HostList = () => { [dispatch] ); - const columns = useMemo(() => { + const columns: Array<EuiBasicTableColumn<Immutable<HostMetadata>>> = useMemo(() => { return [ { field: '', @@ -174,7 +176,7 @@ export const HostList = () => { <EuiHorizontalRule margin="xs" /> <EuiBasicTable data-test-subj="hostListTable" - items={listData} + items={useMemo(() => [...listData], [listData])} columns={columns} loading={isLoading} pagination={paginationSetup} diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/view/policy/policy_details.tsx b/x-pack/plugins/endpoint/public/applications/endpoint/view/policy/policy_details.tsx index bc56e5e6f6329..076de7b57b44b 100644 --- a/x-pack/plugins/endpoint/public/applications/endpoint/view/policy/policy_details.tsx +++ b/x-pack/plugins/endpoint/public/applications/endpoint/view/policy/policy_details.tsx @@ -29,12 +29,12 @@ import { isLoading, apiError, } from '../../store/policy_details/selectors'; -import { WindowsEventing } from './policy_forms/eventing/windows'; -import { PageView, PageViewHeaderTitle } from '../../components/page_view'; +import { PageView, PageViewHeaderTitle } from '../components/page_view'; import { AppAction } from '../../types'; import { useKibana } from '../../../../../../../../src/plugins/kibana_react/public'; import { AgentsSummary } from './agents_summary'; import { VerticalDivider } from './vertical_divider'; +import { WindowsEvents, MacEvents, LinuxEvents } from './policy_forms/events'; import { MalwareProtections } from './policy_forms/protections/malware'; export const PolicyDetails = React.memo(() => { @@ -82,7 +82,7 @@ export const PolicyDetails = React.memo(() => { } }, [notifications.toasts, policyItem, policyName, policyUpdateStatus]); - const handleBackToListOnClick = useCallback( + const handleBackToListOnClick: React.MouseEventHandler = useCallback( ev => { ev.preventDefault(); history.push(`/policy`); @@ -161,7 +161,6 @@ export const PolicyDetails = React.memo(() => { fill={true} iconType="save" data-test-subj="policyDetailsSaveButton" - // FIXME: need to disable if User has no write permissions to ingest - see: https://github.com/elastic/endpoint-app-team/issues/296 onClick={handleSaveOnClick} isLoading={isPolicyLoading} > @@ -206,7 +205,11 @@ export const PolicyDetails = React.memo(() => { </h4> </EuiText> <EuiSpacer size="xs" /> - <WindowsEventing /> + <WindowsEvents /> + <EuiSpacer size="l" /> + <MacEvents /> + <EuiSpacer size="l" /> + <LinuxEvents /> </PageView> </> ); diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/view/policy/policy_forms/config_form.tsx b/x-pack/plugins/endpoint/public/applications/endpoint/view/policy/policy_forms/config_form.tsx index 8b6c32c3277ed..341086c7cf75c 100644 --- a/x-pack/plugins/endpoint/public/applications/endpoint/view/policy/policy_forms/config_form.tsx +++ b/x-pack/plugins/endpoint/public/applications/endpoint/view/policy/policy_forms/config_form.tsx @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import React from 'react'; +import React, { useMemo } from 'react'; import { EuiCard, EuiFlexGroup, @@ -25,14 +25,27 @@ const PolicyDetailCard = styled.div` } `; export const ConfigForm: React.FC<{ + /** + * A subtitle for this component. + **/ type: string; - supportedOss: string[]; + /** + * Types of supported operating systems. + */ + supportedOss: React.ReactNode; children: React.ReactNode; - id: string; - /** Takes a react component to be put on the right corner of the card */ + /** + * A description for the component. + */ + description: string; + /** + * The `data-test-subj` attribute to append to a certain child element. + */ + dataTestSubj: string; + /** React Node to be put on the right corner of the card */ rightCorner: React.ReactNode; -}> = React.memo(({ type, supportedOss, children, id, rightCorner }) => { - const typeTitle = () => { +}> = React.memo(({ type, supportedOss, children, dataTestSubj, rightCorner, description }) => { + const typeTitle = useMemo(() => { return ( <EuiFlexGroup direction="row" gutterSize="none" alignItems="center"> <EuiFlexGroup direction="column" gutterSize="none"> @@ -59,28 +72,25 @@ export const ConfigForm: React.FC<{ </EuiTitle> </EuiFlexItem> <EuiFlexItem className="policyDetailTitleFlexItem"> - <EuiText>{supportedOss.join(', ')}</EuiText> + <EuiText>{supportedOss}</EuiText> </EuiFlexItem> </EuiFlexGroup> <EuiFlexItem grow={false}>{rightCorner}</EuiFlexItem> </EuiFlexGroup> ); - }; + }, [rightCorner, supportedOss, type]); return ( <PolicyDetailCard> <EuiCard - data-test-subj={id} + description={description} + data-test-subj={dataTestSubj} textAlign="left" - title={typeTitle()} - description="" - children={ - <> - <EuiHorizontalRule margin="m" /> - {children} - </> - } - /> + title={typeTitle} + > + <EuiHorizontalRule margin="m" /> + {children} + </EuiCard> </PolicyDetailCard> ); }); diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/view/policy/policy_forms/eventing/checkbox.tsx b/x-pack/plugins/endpoint/public/applications/endpoint/view/policy/policy_forms/eventing/checkbox.tsx deleted file mode 100644 index 8b7fb89ed1646..0000000000000 --- a/x-pack/plugins/endpoint/public/applications/endpoint/view/policy/policy_forms/eventing/checkbox.tsx +++ /dev/null @@ -1,53 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import React, { useCallback } from 'react'; -import { EuiCheckbox } from '@elastic/eui'; -import { useDispatch } from 'react-redux'; -import { usePolicyDetailsSelector } from '../../policy_hooks'; -import { policyConfig, windowsEventing } from '../../../../store/policy_details/selectors'; -import { PolicyDetailsAction } from '../../../../store/policy_details'; -import { OS, EventingFields } from '../../../../types'; -import { clone } from '../../../../models/policy_details_config'; - -export const EventingCheckbox: React.FC<{ - id: string; - name: string; - os: OS; - protectionField: EventingFields; -}> = React.memo(({ id, name, os, protectionField }) => { - const policyDetailsConfig = usePolicyDetailsSelector(policyConfig); - const eventing = usePolicyDetailsSelector(windowsEventing); - const dispatch = useDispatch<(action: PolicyDetailsAction) => void>(); - - const handleRadioChange = useCallback( - (event: React.ChangeEvent<HTMLInputElement>) => { - if (policyDetailsConfig) { - const newPayload = clone(policyDetailsConfig); - if (os === OS.linux || os === OS.mac) { - newPayload[os].events.process = event.target.checked; - } else { - newPayload[os].events[protectionField] = event.target.checked; - } - - dispatch({ - type: 'userChangedPolicyConfig', - payload: { policyConfig: newPayload }, - }); - } - }, - [dispatch, os, policyDetailsConfig, protectionField] - ); - - return ( - <EuiCheckbox - id={id} - label={name} - checked={eventing && eventing[protectionField]} - onChange={handleRadioChange} - /> - ); -}); diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/view/policy/policy_forms/eventing/windows.tsx b/x-pack/plugins/endpoint/public/applications/endpoint/view/policy/policy_forms/eventing/windows.tsx deleted file mode 100644 index 7bec2c4c742d2..0000000000000 --- a/x-pack/plugins/endpoint/public/applications/endpoint/view/policy/policy_forms/eventing/windows.tsx +++ /dev/null @@ -1,96 +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, { useMemo } from 'react'; -import { i18n } from '@kbn/i18n'; -import { FormattedMessage } from '@kbn/i18n/react'; -import { EuiTitle, EuiText, EuiSpacer } from '@elastic/eui'; -import { EventingCheckbox } from './checkbox'; -import { OS, EventingFields } from '../../../../types'; -import { usePolicyDetailsSelector } from '../../policy_hooks'; -import { - selectedWindowsEventing, - totalWindowsEventing, -} from '../../../../store/policy_details/selectors'; -import { ConfigForm } from '../config_form'; - -export const WindowsEventing = React.memo(() => { - const selected = usePolicyDetailsSelector(selectedWindowsEventing); - const total = usePolicyDetailsSelector(totalWindowsEventing); - - const checkboxes = useMemo( - () => [ - { - name: i18n.translate('xpack.endpoint.policyDetailsConfig.eventingProcess', { - defaultMessage: 'Process', - }), - os: OS.windows, - protectionField: EventingFields.process, - }, - { - name: i18n.translate('xpack.endpoint.policyDetailsConfig.eventingNetwork', { - defaultMessage: 'Network', - }), - os: OS.windows, - protectionField: EventingFields.network, - }, - ], - [] - ); - - const renderCheckboxes = () => { - return ( - <> - <EuiTitle size="xxs"> - <h5> - <FormattedMessage - id="xpack.endpoint.policyDetailsConfig.eventingEvents" - defaultMessage="Events" - /> - </h5> - </EuiTitle> - <EuiSpacer size="s" /> - {checkboxes.map((item, index) => { - return ( - <EventingCheckbox - id={`eventing${item.name}`} - name={item.name} - key={index} - os={item.os} - protectionField={item.protectionField} - /> - ); - })} - </> - ); - }; - - const collectionsEnabled = () => { - return ( - <EuiText size="s" color="subdued"> - <FormattedMessage - id="xpack.endpoint.policy.details.eventCollectionsEnabled" - defaultMessage="{selected} / {total} event collections enabled" - values={{ selected, total }} - /> - </EuiText> - ); - }; - - return ( - <ConfigForm - type={i18n.translate('xpack.endpoint.policy.details.eventCollection', { - defaultMessage: 'Event Collection', - })} - supportedOss={[ - i18n.translate('xpack.endpoint.policy.details.windows', { defaultMessage: 'Windows' }), - ]} - id="windowsEventingForm" - rightCorner={collectionsEnabled()} - children={renderCheckboxes()} - /> - ); -}); diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/view/policy/policy_forms/events/checkbox.tsx b/x-pack/plugins/endpoint/public/applications/endpoint/view/policy/policy_forms/events/checkbox.tsx new file mode 100644 index 0000000000000..74322ac8b993b --- /dev/null +++ b/x-pack/plugins/endpoint/public/applications/endpoint/view/policy/policy_forms/events/checkbox.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, { useCallback, useMemo } from 'react'; +import { EuiCheckbox } from '@elastic/eui'; +import { useDispatch } from 'react-redux'; +import { htmlIdGenerator } from '@elastic/eui'; +import { usePolicyDetailsSelector } from '../../policy_hooks'; +import { policyConfig } from '../../../../store/policy_details/selectors'; +import { PolicyDetailsAction } from '../../../../store/policy_details'; +import { UIPolicyConfig } from '../../../../../../../common/types'; + +export const EventsCheckbox = React.memo(function({ + name, + setter, + getter, +}: { + name: string; + setter: (config: UIPolicyConfig, checked: boolean) => UIPolicyConfig; + getter: (config: UIPolicyConfig) => boolean; +}) { + const policyDetailsConfig = usePolicyDetailsSelector(policyConfig); + const selected = getter(policyDetailsConfig); + const dispatch = useDispatch<(action: PolicyDetailsAction) => void>(); + + const handleCheckboxChange = useCallback( + (event: React.ChangeEvent<HTMLInputElement>) => { + if (policyDetailsConfig) { + dispatch({ + type: 'userChangedPolicyConfig', + payload: { policyConfig: setter(policyDetailsConfig, event.target.checked) }, + }); + } + }, + [dispatch, policyDetailsConfig, setter] + ); + + return ( + <EuiCheckbox + id={useMemo(() => htmlIdGenerator()(), [])} + label={name} + checked={selected} + onChange={handleCheckboxChange} + /> + ); +}); diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/view/policy/policy_forms/events/index.tsx b/x-pack/plugins/endpoint/public/applications/endpoint/view/policy/policy_forms/events/index.tsx new file mode 100644 index 0000000000000..927456fb671d8 --- /dev/null +++ b/x-pack/plugins/endpoint/public/applications/endpoint/view/policy/policy_forms/events/index.tsx @@ -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 { WindowsEvents } from './windows'; +export { MacEvents } from './mac'; +export { LinuxEvents } from './linux'; diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/view/policy/policy_forms/events/linux.tsx b/x-pack/plugins/endpoint/public/applications/endpoint/view/policy/policy_forms/events/linux.tsx new file mode 100644 index 0000000000000..c3d6bdba7c852 --- /dev/null +++ b/x-pack/plugins/endpoint/public/applications/endpoint/view/policy/policy_forms/events/linux.tsx @@ -0,0 +1,107 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { useMemo } from 'react'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { EuiTitle, EuiText, EuiSpacer } from '@elastic/eui'; +import { EventsCheckbox } from './checkbox'; +import { OS } from '../../../../types'; +import { usePolicyDetailsSelector } from '../../policy_hooks'; +import { selectedLinuxEvents, totalLinuxEvents } from '../../../../store/policy_details/selectors'; +import { ConfigForm } from '../config_form'; +import { getIn, setIn } from '../../../../models/policy_details_config'; +import { UIPolicyConfig } from '../../../../../../../common/types'; + +export const LinuxEvents = React.memo(() => { + const selected = usePolicyDetailsSelector(selectedLinuxEvents); + const total = usePolicyDetailsSelector(totalLinuxEvents); + + const checkboxes = useMemo(() => { + const items: Array<{ + name: string; + os: 'linux'; + protectionField: keyof UIPolicyConfig['linux']['events']; + }> = [ + { + name: i18n.translate('xpack.endpoint.policyDetailsConfig.linux.events.file', { + defaultMessage: 'File', + }), + os: OS.linux, + protectionField: 'file', + }, + { + name: i18n.translate('xpack.endpoint.policyDetailsConfig.linux.events.process', { + defaultMessage: 'Process', + }), + os: OS.linux, + protectionField: 'process', + }, + { + name: i18n.translate('xpack.endpoint.policyDetailsConfig.linux.events.network', { + defaultMessage: 'Network', + }), + os: OS.linux, + protectionField: 'network', + }, + ]; + return ( + <> + <EuiTitle size="xxs"> + <h5> + <FormattedMessage + id="xpack.endpoint.policyDetailsConfig.eventingEvents" + defaultMessage="Events" + /> + </h5> + </EuiTitle> + <EuiSpacer size="s" /> + {items.map((item, index) => { + return ( + <EventsCheckbox + name={item.name} + key={index} + setter={(config, checked) => + setIn(config)(item.os)('events')(item.protectionField)(checked) + } + getter={config => getIn(config)(item.os)('events')(item.protectionField)} + /> + ); + })} + </> + ); + }, []); + + const collectionsEnabled = useMemo(() => { + return ( + <EuiText size="s" color="subdued"> + <FormattedMessage + id="xpack.endpoint.policy.details.eventCollectionsEnabled" + defaultMessage="{selected} / {total} event collections enabled" + values={{ selected, total }} + /> + </EuiText> + ); + }, [selected, total]); + + return ( + <ConfigForm + type={i18n.translate('xpack.endpoint.policy.details.eventCollection', { + defaultMessage: 'Event Collection', + })} + description={i18n.translate('xpack.endpoint.policy.details.eventCollectionLabel', { + defaultMessage: 'Event Collection', + })} + supportedOss={i18n.translate('xpack.endpoint.policy.details.linux', { + defaultMessage: 'Linux', + })} + dataTestSubj="linuxEventingForm" + rightCorner={collectionsEnabled} + > + {checkboxes} + </ConfigForm> + ); +}); diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/view/policy/policy_forms/events/mac.tsx b/x-pack/plugins/endpoint/public/applications/endpoint/view/policy/policy_forms/events/mac.tsx new file mode 100644 index 0000000000000..40b80b9af0f65 --- /dev/null +++ b/x-pack/plugins/endpoint/public/applications/endpoint/view/policy/policy_forms/events/mac.tsx @@ -0,0 +1,105 @@ +/* + * 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, { useMemo } from 'react'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { EuiTitle, EuiText, EuiSpacer } from '@elastic/eui'; +import { EventsCheckbox } from './checkbox'; +import { OS } from '../../../../types'; +import { usePolicyDetailsSelector } from '../../policy_hooks'; +import { selectedMacEvents, totalMacEvents } from '../../../../store/policy_details/selectors'; +import { ConfigForm } from '../config_form'; +import { getIn, setIn } from '../../../../models/policy_details_config'; +import { UIPolicyConfig } from '../../../../../../../common/types'; + +export const MacEvents = React.memo(() => { + const selected = usePolicyDetailsSelector(selectedMacEvents); + const total = usePolicyDetailsSelector(totalMacEvents); + + const checkboxes = useMemo(() => { + const items: Array<{ + name: string; + os: 'mac'; + protectionField: keyof UIPolicyConfig['mac']['events']; + }> = [ + { + name: i18n.translate('xpack.endpoint.policyDetailsConfig.mac.events.file', { + defaultMessage: 'File', + }), + os: OS.mac, + protectionField: 'file', + }, + { + name: i18n.translate('xpack.endpoint.policyDetailsConfig.mac.events.process', { + defaultMessage: 'Process', + }), + os: OS.mac, + protectionField: 'process', + }, + { + name: i18n.translate('xpack.endpoint.policyDetailsConfig.mac.events.network', { + defaultMessage: 'Network', + }), + os: OS.mac, + protectionField: 'network', + }, + ]; + return ( + <> + <EuiTitle size="xxs"> + <h5> + <FormattedMessage + id="xpack.endpoint.policyDetailsConfig.eventingEvents" + defaultMessage="Events" + /> + </h5> + </EuiTitle> + <EuiSpacer size="s" /> + {items.map((item, index) => { + return ( + <EventsCheckbox + name={item.name} + key={index} + setter={(config, checked) => + setIn(config)(item.os)('events')(item.protectionField)(checked) + } + getter={config => getIn(config)(item.os)('events')(item.protectionField)} + /> + ); + })} + </> + ); + }, []); + + const collectionsEnabled = useMemo(() => { + return ( + <EuiText size="s" color="subdued"> + <FormattedMessage + id="xpack.endpoint.policy.details.eventCollectionsEnabled" + defaultMessage="{selected} / {total} event collections enabled" + values={{ selected, total }} + /> + </EuiText> + ); + }, [selected, total]); + + return ( + <ConfigForm + type={i18n.translate('xpack.endpoint.policy.details.eventCollection', { + defaultMessage: 'Event Collection', + })} + description={i18n.translate('xpack.endpoint.policy.details.eventCollectionLabel', { + defaultMessage: 'Event Collection', + })} + supportedOss={i18n.translate('xpack.endpoint.policy.details.mac', { defaultMessage: 'Mac' })} + dataTestSubj="macEventingForm" + rightCorner={collectionsEnabled} + > + {checkboxes} + </ConfigForm> + ); +}); diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/view/policy/policy_forms/events/windows.tsx b/x-pack/plugins/endpoint/public/applications/endpoint/view/policy/policy_forms/events/windows.tsx new file mode 100644 index 0000000000000..7f946de9614ca --- /dev/null +++ b/x-pack/plugins/endpoint/public/applications/endpoint/view/policy/policy_forms/events/windows.tsx @@ -0,0 +1,138 @@ +/* + * 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, { useMemo } from 'react'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { EuiTitle, EuiText, EuiSpacer } from '@elastic/eui'; +import { EventsCheckbox } from './checkbox'; +import { OS } from '../../../../types'; +import { usePolicyDetailsSelector } from '../../policy_hooks'; +import { + selectedWindowsEvents, + totalWindowsEvents, +} from '../../../../store/policy_details/selectors'; +import { ConfigForm } from '../config_form'; +import { setIn, getIn } from '../../../../models/policy_details_config'; +import { UIPolicyConfig, ImmutableArray } from '../../../../../../../common/types'; + +export const WindowsEvents = React.memo(() => { + const selected = usePolicyDetailsSelector(selectedWindowsEvents); + const total = usePolicyDetailsSelector(totalWindowsEvents); + + const checkboxes = useMemo(() => { + const items: ImmutableArray<{ + name: string; + os: 'windows'; + protectionField: keyof UIPolicyConfig['windows']['events']; + }> = [ + { + name: i18n.translate('xpack.endpoint.policyDetailsConfig.windows.events.dllDriverLoad', { + defaultMessage: 'DLL and Driver Load', + }), + os: OS.windows, + protectionField: 'dll_and_driver_load', + }, + { + name: i18n.translate('xpack.endpoint.policyDetailsConfig.windows.events.dns', { + defaultMessage: 'DNS', + }), + os: OS.windows, + protectionField: 'dns', + }, + { + name: i18n.translate('xpack.endpoint.policyDetailsConfig.windows.events.file', { + defaultMessage: 'File', + }), + os: OS.windows, + protectionField: 'file', + }, + { + name: i18n.translate('xpack.endpoint.policyDetailsConfig.windows.events.network', { + defaultMessage: 'Network', + }), + os: OS.windows, + protectionField: 'network', + }, + { + name: i18n.translate('xpack.endpoint.policyDetailsConfig.windows.events.process', { + defaultMessage: 'Process', + }), + os: OS.windows, + protectionField: 'process', + }, + { + name: i18n.translate('xpack.endpoint.policyDetailsConfig.windows.events.registry', { + defaultMessage: 'Registry', + }), + os: OS.windows, + protectionField: 'registry', + }, + { + name: i18n.translate('xpack.endpoint.policyDetailsConfig.windows.events.security', { + defaultMessage: 'Security', + }), + os: OS.windows, + protectionField: 'security', + }, + ]; + return ( + <> + <EuiTitle size="xxs"> + <h5> + <FormattedMessage + id="xpack.endpoint.policyDetailsConfig.eventingEvents" + defaultMessage="Events" + /> + </h5> + </EuiTitle> + <EuiSpacer size="s" /> + {items.map((item, index) => { + return ( + <EventsCheckbox + name={item.name} + key={index} + setter={(config, checked) => + setIn(config)(item.os)('events')(item.protectionField)(checked) + } + getter={config => getIn(config)(item.os)('events')(item.protectionField)} + /> + ); + })} + </> + ); + }, []); + + const collectionsEnabled = useMemo(() => { + return ( + <EuiText size="s" color="subdued"> + <FormattedMessage + id="xpack.endpoint.policy.details.eventCollectionsEnabled" + defaultMessage="{selected} / {total} event collections enabled" + values={{ selected, total }} + /> + </EuiText> + ); + }, [selected, total]); + + return ( + <ConfigForm + type={i18n.translate('xpack.endpoint.policy.details.eventCollection', { + defaultMessage: 'Event Collection', + })} + description={i18n.translate('xpack.endpoint.policy.details.windowsLabel', { + defaultMessage: 'Windows', + })} + supportedOss={i18n.translate('xpack.endpoint.policy.details.windows', { + defaultMessage: 'Windows', + })} + dataTestSubj="windowsEventingForm" + rightCorner={collectionsEnabled} + > + {checkboxes} + </ConfigForm> + ); +}); diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/view/policy/policy_forms/protections/malware.tsx b/x-pack/plugins/endpoint/public/applications/endpoint/view/policy/policy_forms/protections/malware.tsx index 66b22178607b9..14871c71ec038 100644 --- a/x-pack/plugins/endpoint/public/applications/endpoint/view/policy/policy_forms/protections/malware.tsx +++ b/x-pack/plugins/endpoint/public/applications/endpoint/view/policy/policy_forms/protections/malware.tsx @@ -11,8 +11,8 @@ import { EuiRadio, EuiSwitch, EuiTitle, EuiSpacer } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; import { htmlIdGenerator } from '@elastic/eui'; -import { Immutable } from '../../../../../../../common/types'; -import { OS, ProtectionModes, MalwareProtectionOSes } from '../../../../types'; +import { Immutable, ProtectionModes, ImmutableArray } from '../../../../../../../common/types'; +import { OS, MalwareProtectionOSes } from '../../../../types'; import { ConfigForm } from '../config_form'; import { policyConfig } from '../../../../store/policy_details/selectors'; import { usePolicyDetailsSelector } from '../../policy_hooks'; @@ -73,7 +73,7 @@ export const MalwareProtections = React.memo(() => { // currently just taking windows.malware, but both windows.malware and mac.malware should be the same value const selected = policyDetailsConfig && policyDetailsConfig.windows.malware.mode; - const radios: Array<{ + const radios: ImmutableArray<{ id: ProtectionModes; label: string; protection: 'malware'; @@ -123,7 +123,7 @@ export const MalwareProtections = React.memo(() => { [dispatch, policyDetailsConfig] ); - const RadioButtons = () => { + const radioButtons = useMemo(() => { return ( <> <EuiTitle size="xxxs"> @@ -148,9 +148,9 @@ export const MalwareProtections = React.memo(() => { </ProtectionRadioGroup> </> ); - }; + }, [radios]); - const ProtectionSwitch = () => { + const protectionSwitch = useMemo(() => { return ( <EuiSwitch label={i18n.translate('xpack.endpoint.policy.details.malwareProtectionsEnabled', { @@ -163,18 +163,21 @@ export const MalwareProtections = React.memo(() => { onChange={handleSwitchChange} /> ); - }; + }, [handleSwitchChange, selected]); return ( <ConfigForm type={i18n.translate('xpack.endpoint.policy.details.malware', { defaultMessage: 'Malware' })} - supportedOss={[ - i18n.translate('xpack.endpoint.policy.details.windows', { defaultMessage: 'Windows' }), - i18n.translate('xpack.endpoint.policy.details.mac', { defaultMessage: 'Mac' }), - ]} - id="malwareProtectionsForm" - rightCorner={ProtectionSwitch()} - children={RadioButtons()} - /> + supportedOss={i18n.translate('xpack.endpoint.policy.details.windowsAndMac', { + defaultMessage: 'Windows, Mac', + })} + dataTestSubj="malwareProtectionsForm" + description={i18n.translate('xpack.endpoint.policy.details.malwareLabel', { + defaultMessage: 'Malware', + })} + rightCorner={protectionSwitch} + > + {radioButtons} + </ConfigForm> ); }); diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/view/policy/policy_list.tsx b/x-pack/plugins/endpoint/public/applications/endpoint/view/policy/policy_list.tsx index 5ee1539ce9788..062c7afb6706d 100644 --- a/x-pack/plugins/endpoint/public/applications/endpoint/view/policy/policy_list.tsx +++ b/x-pack/plugins/endpoint/public/applications/endpoint/view/policy/policy_list.tsx @@ -9,8 +9,7 @@ import { EuiBasicTable, EuiText, EuiTableFieldDataColumnType, EuiLink } from '@e import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; import { useDispatch } from 'react-redux'; -import { useHistory } from 'react-router-dom'; -import { usePageId } from '../use_page_id'; +import { useHistory, useLocation } from 'react-router-dom'; import { selectApiError, selectIsLoading, @@ -21,10 +20,10 @@ import { } from '../../store/policy_list/selectors'; import { usePolicyListSelector } from './policy_hooks'; import { PolicyListAction } from '../../store/policy_list'; -import { PolicyData } from '../../types'; import { useKibana } from '../../../../../../../../src/plugins/kibana_react/public'; -import { PageView } from '../../components/page_view'; -import { LinkToApp } from '../../components/link_to_app'; +import { PageView } from '../components/page_view'; +import { LinkToApp } from '../components/link_to_app'; +import { Immutable, PolicyData } from '../../../../../common/types'; interface TableChangeCallbackArguments { page: { index: number; size: number }; @@ -45,14 +44,14 @@ const PolicyLink: React.FC<{ name: string; route: string }> = ({ name, route }) ); }; -const renderPolicyNameLink = (value: string, _item: PolicyData) => { - return <PolicyLink name={value} route={`/policy/${_item.id}`} />; +const renderPolicyNameLink = (value: string, item: Immutable<PolicyData>) => { + return <PolicyLink name={value} route={`/policy/${item.id}`} />; }; export const PolicyList = React.memo(() => { - usePageId('policyListPage'); - const { services, notifications } = useKibana(); + const history = useHistory(); + const location = useLocation(); const dispatch = useDispatch<(action: PolicyListAction) => void>(); const policyItems = usePolicyListSelector(selectPolicyItems); @@ -84,18 +83,12 @@ export const PolicyList = React.memo(() => { const handleTableChange = useCallback( ({ page: { index, size } }: TableChangeCallbackArguments) => { - dispatch({ - type: 'userPaginatedPolicyListTable', - payload: { - pageIndex: index, - pageSize: size, - }, - }); + history.push(`${location.pathname}?page_index=${index}&page_size=${size}`); }, - [dispatch] + [history, location.pathname] ); - const columns: Array<EuiTableFieldDataColumnType<PolicyData>> = useMemo( + const columns: Array<EuiTableFieldDataColumnType<Immutable<PolicyData>>> = useMemo( () => [ { field: 'name', @@ -167,7 +160,7 @@ export const PolicyList = React.memo(() => { } > <EuiBasicTable - items={policyItems} + items={useMemo(() => [...policyItems], [policyItems])} columns={columns} loading={loading} pagination={paginationSetup} diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/view/use_page_id.ts b/x-pack/plugins/endpoint/public/applications/endpoint/view/use_page_id.ts deleted file mode 100644 index 49c39064c8d9a..0000000000000 --- a/x-pack/plugins/endpoint/public/applications/endpoint/view/use_page_id.ts +++ /dev/null @@ -1,28 +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 { useEffect } from 'react'; -import { useDispatch } from 'react-redux'; -import { PageId } from '../../../../common/types'; -import { RoutingAction } from '../store/routing'; - -/** - * Dispatches a 'userNavigatedToPage' action with the given 'pageId' as the action payload. - * When the component is un-mounted, a `userNavigatedFromPage` action will be dispatched - * with the given `pageId`. - * - * @param pageId A page id - */ -export function usePageId(pageId: PageId) { - const dispatch: (action: RoutingAction) => unknown = useDispatch(); - useEffect(() => { - dispatch({ type: 'userNavigatedToPage', payload: pageId }); - - return () => { - dispatch({ type: 'userNavigatedFromPage', payload: pageId }); - }; - }, [dispatch, pageId]); -} diff --git a/x-pack/plugins/endpoint/public/embeddables/resolver/view/defs.tsx b/x-pack/plugins/endpoint/public/embeddables/resolver/view/defs.tsx index 8ee9bfafc630e..de9c3c7e8f8f3 100644 --- a/x-pack/plugins/endpoint/public/embeddables/resolver/view/defs.tsx +++ b/x-pack/plugins/endpoint/public/embeddables/resolver/view/defs.tsx @@ -5,7 +5,7 @@ */ import React, { memo } from 'react'; -import { saturate, lighten } from 'polished'; +import { saturate } from 'polished'; import { htmlIdGenerator, @@ -79,8 +79,6 @@ const idGenerator = htmlIdGenerator(); * Ids of paint servers to be referenced by fill and stroke attributes */ export const PaintServerIds = { - runningProcess: idGenerator('psRunningProcess'), - runningTrigger: idGenerator('psRunningTrigger'), runningProcessCube: idGenerator('psRunningProcessCube'), runningTriggerCube: idGenerator('psRunningTriggerCube'), terminatedProcessCube: idGenerator('psTerminatedProcessCube'), @@ -93,46 +91,6 @@ export const PaintServerIds = { */ const PaintServers = memo(() => ( <> - <linearGradient - id={PaintServerIds.runningProcess} - x1="0" - y1="0" - x2="1" - y2="0" - spreadMethod="reflect" - gradientUnits="objectBoundingBox" - > - <stop - offset="0%" - stopColor={saturate(0.7, lighten(0.05, NamedColors.runningProcessStart))} - stopOpacity="1" - /> - <stop - offset="100%" - stopColor={saturate(0.7, lighten(0.05, NamedColors.runningProcessEnd))} - stopOpacity="1" - /> - </linearGradient> - <linearGradient - id={PaintServerIds.runningTrigger} - x1="0" - y1="0" - x2="1" - y2="0" - spreadMethod="reflect" - gradientUnits="objectBoundingBox" - > - <stop - offset="0%" - stopColor={saturate(0.7, lighten(0.05, NamedColors.runningTriggerStart))} - stopOpacity="1" - /> - <stop - offset="100%" - stopColor={saturate(0.7, lighten(0.05, NamedColors.runningTriggerEnd))} - stopOpacity="1" - /> - </linearGradient> <linearGradient id={PaintServerIds.terminatedProcessCube} x1="-381.23752" diff --git a/x-pack/plugins/endpoint/public/embeddables/resolver/view/process_event_dot.tsx b/x-pack/plugins/endpoint/public/embeddables/resolver/view/process_event_dot.tsx index 2e3981de74d34..10e331ffff02d 100644 --- a/x-pack/plugins/endpoint/public/embeddables/resolver/view/process_event_dot.tsx +++ b/x-pack/plugins/endpoint/public/embeddables/resolver/view/process_event_dot.tsx @@ -11,7 +11,7 @@ import { htmlIdGenerator, EuiKeyboardAccessible } from '@elastic/eui'; import { useSelector } from 'react-redux'; import { applyMatrix3 } from '../lib/vector2'; import { Vector2, Matrix3, AdjacentProcessMap, ResolverProcessType } from '../types'; -import { SymbolIds, NamedColors, PaintServerIds } from './defs'; +import { SymbolIds, NamedColors } from './defs'; import { ResolverEvent } from '../../../../common/types'; import { useResolverDispatch } from './use_resolver_dispatch'; import * as eventModel from '../../../../common/models/event'; @@ -21,7 +21,7 @@ import * as selectors from '../store/selectors'; const nodeAssets = { runningProcessCube: { cubeSymbol: `#${SymbolIds.runningProcessCube}`, - labelFill: `url(#${PaintServerIds.runningProcess})`, + labelBackground: NamedColors.fullLabelBackground, descriptionFill: NamedColors.empty, descriptionText: i18n.translate('xpack.endpoint.resolver.runningProcess', { defaultMessage: 'Running Process', @@ -29,7 +29,7 @@ const nodeAssets = { }, runningTriggerCube: { cubeSymbol: `#${SymbolIds.runningTriggerCube}`, - labelFill: `url(#${PaintServerIds.runningTrigger})`, + labelBackground: NamedColors.fullLabelBackground, descriptionFill: NamedColors.empty, descriptionText: i18n.translate('xpack.endpoint.resolver.runningTrigger', { defaultMessage: 'Running Trigger', @@ -37,7 +37,7 @@ const nodeAssets = { }, terminatedProcessCube: { cubeSymbol: `#${SymbolIds.terminatedProcessCube}`, - labelFill: NamedColors.fullLabelBackground, + labelBackground: NamedColors.fullLabelBackground, descriptionFill: NamedColors.empty, descriptionText: i18n.translate('xpack.endpoint.resolver.terminatedProcess', { defaultMessage: 'Terminated Process', @@ -45,7 +45,7 @@ const nodeAssets = { }, terminatedTriggerCube: { cubeSymbol: `#${SymbolIds.terminatedTriggerCube}`, - labelFill: NamedColors.fullLabelBackground, + labelBackground: NamedColors.fullLabelBackground, descriptionFill: NamedColors.empty, descriptionText: i18n.translate('xpack.endpoint.resolver.terminatedTrigger', { defaultMessage: 'Terminated Trigger', @@ -114,14 +114,21 @@ export const ProcessEventDot = styled( [left, magFactorX, top] ); + /** + * Type in non-SVG components scales as follows: + * (These values were adjusted to match the proportions in the comps provided by UX/Design) + * 18.75 : The smallest readable font size at which labels/descriptions can be read. Font size will not scale below this. + * 12.5 : A 'slope' at which the font size will scale w.r.t. to zoom level otherwise + */ + const minimumFontSize = 18.75; + const slopeOfFontScale = 12.5; + const fontSizeAdjustmentForScale = magFactorX > 1 ? slopeOfFontScale * (magFactorX - 1) : 0; + const scaledTypeSize = minimumFontSize + fontSizeAdjustmentForScale; + const markerBaseSize = 15; const markerSize = markerBaseSize; const markerPositionOffset = -markerBaseSize / 2; - const labelYOffset = markerPositionOffset + 0.25 * markerSize - 0.5; - - const labelYHeight = markerSize / 1.7647; - /** * An element that should be animated when the node is clicked. */ @@ -136,9 +143,7 @@ export const ProcessEventDot = styled( }) | null; } = React.createRef(); - const { cubeSymbol, labelFill, descriptionFill, descriptionText } = nodeAssets[ - nodeType(event) - ]; + const { cubeSymbol, labelBackground, descriptionText } = nodeAssets[nodeType(event)]; const resolverNodeIdGenerator = useMemo(() => htmlIdGenerator('resolverNode'), []); const nodeId = useMemo(() => resolverNodeIdGenerator(selfId), [ @@ -154,7 +159,7 @@ export const ProcessEventDot = styled( const dispatch = useResolverDispatch(); const handleFocus = useCallback( - (focusEvent: React.FocusEvent<SVGSVGElement>) => { + (focusEvent: React.FocusEvent<HTMLDivElement>) => { dispatch({ type: 'userFocusedOnResolverNode', payload: { @@ -166,7 +171,7 @@ export const ProcessEventDot = styled( ); const handleClick = useCallback( - (clickEvent: React.MouseEvent<SVGSVGElement, MouseEvent>) => { + (clickEvent: React.MouseEvent<HTMLDivElement, MouseEvent>) => { if (animationTarget.current !== null) { (animationTarget.current as any).beginElement(); } @@ -179,14 +184,15 @@ export const ProcessEventDot = styled( }, [animationTarget, dispatch, nodeId] ); - + /* eslint-disable jsx-a11y/click-events-have-key-events */ + /** + * Key event handling (e.g. 'Enter'/'Space') is provisioned by the `EuiKeyboardAccessible` component + */ return ( <EuiKeyboardAccessible> - <svg + <div data-test-subj={'resolverNode'} className={className + ' kbn-resetFocusState'} - viewBox="-15 -15 90 30" - preserveAspectRatio="xMidYMid meet" role="treeitem" aria-level={adjacentNodeMap.level} aria-flowto={ @@ -203,81 +209,100 @@ export const ProcessEventDot = styled( onFocus={handleFocus} tabIndex={-1} > - <g> - <use - xlinkHref={`#${SymbolIds.processCubeActiveBacking}`} - x={-11.35} - y={-11.35} - width={markerSize * 1.5} - height={markerSize * 1.5} - className="backing" - /> - <rect x="7" y="-12.75" width="15" height="10" fill={NamedColors.resolverBackground} /> - <use - role="presentation" - xlinkHref={cubeSymbol} - x={markerPositionOffset} - y={markerPositionOffset} - width={markerSize} - height={markerSize} - opacity="1" - className="cube" - > - <animateTransform - attributeType="XML" - attributeName="transform" - type="scale" - values="1 1; 1 .83; 1 .8; 1 .83; 1 1" - dur="0.2s" - begin="click" - repeatCount="1" - className="squish" - ref={animationTarget} + <svg + viewBox="-15 -15 90 30" + preserveAspectRatio="xMidYMid meet" + style={{ + display: 'block', + width: '100%', + height: '100%', + position: 'absolute', + top: '0', + left: '0', + }} + > + <g> + <use + xlinkHref={`#${SymbolIds.processCubeActiveBacking}`} + x={-11.35} + y={-11.35} + width={markerSize * 1.5} + height={markerSize * 1.5} + className="backing" /> - </use> - <use - role="presentation" - xlinkHref={`#${SymbolIds.processNodeLabel}`} - x={markerPositionOffset + markerSize - 0.5} - y={labelYOffset} - width={(markerSize / 1.7647) * 5} - height={markerSize / 1.7647} - opacity="1" - fill={labelFill} - /> - <text - x={markerPositionOffset + 0.7 * markerSize + 50 / 2} - y={labelYOffset + labelYHeight / 2} - textAnchor="middle" - dominantBaseline="middle" - fontSize="3.75" - fontWeight="bold" - fill={NamedColors.empty} - paintOrder="stroke" - tabIndex={-1} - style={{ letterSpacing: '-0.02px' }} - id={labelId} - > - {eventModel.eventName(event)} - </text> - <text - x={markerPositionOffset + markerSize} - y={labelYOffset - 1} - textAnchor="start" - dominantBaseline="middle" - fontSize="2.67" - fill={descriptionFill} + <use + role="presentation" + xlinkHref={cubeSymbol} + x={markerPositionOffset} + y={markerPositionOffset} + width={markerSize} + height={markerSize} + opacity="1" + className="cube" + > + <animateTransform + attributeType="XML" + attributeName="transform" + type="scale" + values="1 1; 1 .83; 1 .8; 1 .83; 1 1" + dur="0.2s" + begin="click" + repeatCount="1" + className="squish" + ref={animationTarget} + /> + </use> + </g> + </svg> + <div + style={{ + left: '25%', + top: '30%', + position: 'absolute', + width: '50%', + color: 'white', + fontSize: `${scaledTypeSize}px`, + lineHeight: '140%', + }} + > + <div id={descriptionId} - paintOrder="stroke" - fontWeight="bold" - style={{ textTransform: 'uppercase', letterSpacing: '-0.01px' }} + style={{ + textTransform: 'uppercase', + letterSpacing: '-0.01px', + backgroundColor: NamedColors.resolverBackground, + lineHeight: '1.2', + fontWeight: 'bold', + fontSize: '.5em', + width: '100%', + margin: '0 0 .05em 0', + textAlign: 'left', + padding: '0', + }} > {descriptionText} - </text> - </g> - </svg> + </div> + <div + data-test-subject="nodeLabel" + id={labelId} + style={{ + backgroundColor: labelBackground, + padding: '.15em 0', + textAlign: 'center', + maxWidth: '100%', + overflow: 'hidden', + whiteSpace: 'nowrap', + textOverflow: 'ellipsis', + contain: 'content', + }} + > + {eventModel.eventName(event)} + </div> + </div> + </div> </EuiKeyboardAccessible> ); + /* eslint-enable jsx-a11y/click-events-have-key-events */ } ) )` diff --git a/x-pack/plugins/endpoint/scripts/alert_mapping.json b/x-pack/plugins/endpoint/scripts/alert_mapping.json index a21e48b4bc95f..2e0041d0af986 100644 --- a/x-pack/plugins/endpoint/scripts/alert_mapping.json +++ b/x-pack/plugins/endpoint/scripts/alert_mapping.json @@ -394,7 +394,8 @@ "type": "nested" }, "file_extension": { - "type": "long" + "ignore_above": 1024, + "type": "keyword" }, "project_file": { "properties": { diff --git a/x-pack/plugins/endpoint/scripts/event_mapping.json b/x-pack/plugins/endpoint/scripts/event_mapping.json index 59d1ed17852b1..f410edc7abe5e 100644 --- a/x-pack/plugins/endpoint/scripts/event_mapping.json +++ b/x-pack/plugins/endpoint/scripts/event_mapping.json @@ -386,7 +386,8 @@ "type": "nested" }, "file_extension": { - "type": "long" + "ignore_above": 1024, + "type": "keyword" }, "project_file": { "properties": { diff --git a/x-pack/plugins/endpoint/scripts/resolver_generator.ts b/x-pack/plugins/endpoint/scripts/resolver_generator.ts index 333846bde6ce4..dd9e591f4b034 100644 --- a/x-pack/plugins/endpoint/scripts/resolver_generator.ts +++ b/x-pack/plugins/endpoint/scripts/resolver_generator.ts @@ -41,7 +41,7 @@ async function main() { metadataIndex: { alias: 'mi', describe: 'index to store host metadata in', - default: 'endpoint-agent-1', + default: 'metrics-endpoint-default-1', type: 'string', }, auth: { diff --git a/x-pack/plugins/endpoint/server/index_pattern.ts b/x-pack/plugins/endpoint/server/index_pattern.ts new file mode 100644 index 0000000000000..ea612bfd75441 --- /dev/null +++ b/x-pack/plugins/endpoint/server/index_pattern.ts @@ -0,0 +1,77 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { Logger, LoggerFactory, RequestHandlerContext } from 'kibana/server'; +import { ESIndexPatternService } from '../../ingest_manager/server'; +import { EndpointAppConstants } from '../common/types'; + +export interface IndexPatternRetriever { + getIndexPattern(ctx: RequestHandlerContext, datasetPath: string): Promise<string>; + getEventIndexPattern(ctx: RequestHandlerContext): Promise<string>; + getMetadataIndexPattern(ctx: RequestHandlerContext): Promise<string>; +} + +/** + * This class is used to retrieve an index pattern. It should be used in the server side code whenever + * an index pattern is needed to query data within ES. The index pattern is constructed by the Ingest Manager + * based on the contents of the Endpoint Package in the Package Registry. + */ +export class IngestIndexPatternRetriever implements IndexPatternRetriever { + private static endpointPackageName = 'endpoint'; + private static metadataDataset = 'metadata'; + private readonly log: Logger; + constructor(private readonly service: ESIndexPatternService, loggerFactory: LoggerFactory) { + this.log = loggerFactory.get('index-pattern-retriever'); + } + + /** + * Retrieves the index pattern for querying events within elasticsearch. + * + * @param ctx a RequestHandlerContext from a route handler + * @returns a string representing the index pattern (e.g. `events-endpoint-*`) + */ + async getEventIndexPattern(ctx: RequestHandlerContext) { + return await this.getIndexPattern(ctx, EndpointAppConstants.EVENT_DATASET); + } + + /** + * Retrieves the index pattern for querying endpoint metadata within elasticsearch. + * + * @param ctx a RequestHandlerContext from a route handler + * @returns a string representing the index pattern (e.g. `metrics-endpoint-*`) + */ + async getMetadataIndexPattern(ctx: RequestHandlerContext) { + return await this.getIndexPattern(ctx, IngestIndexPatternRetriever.metadataDataset); + } + + /** + * Retrieves the index pattern for a specific dataset for querying endpoint data. + * + * @param ctx a RequestHandlerContext from a route handler + * @param datasetPath a string of the path being used for a dataset within the Endpoint Package + * (e.g. `events`, `metadata`) + * @returns a string representing the index pattern (e.g. `metrics-endpoint-*`) + */ + async getIndexPattern(ctx: RequestHandlerContext, datasetPath: string) { + try { + const pattern = await this.service.getESIndexPattern( + ctx.core.savedObjects.client, + IngestIndexPatternRetriever.endpointPackageName, + datasetPath + ); + + if (!pattern) { + const msg = `Unable to retrieve the index pattern for dataset: ${datasetPath}`; + this.log.warn(msg); + throw new Error(msg); + } + return pattern; + } catch (error) { + const errMsg = `Error occurred while retrieving pattern for: ${datasetPath} error: ${error}`; + this.log.warn(errMsg); + throw new Error(errMsg); + } + } +} diff --git a/x-pack/plugins/endpoint/server/mocks.ts b/x-pack/plugins/endpoint/server/mocks.ts new file mode 100644 index 0000000000000..903aa19cd8843 --- /dev/null +++ b/x-pack/plugins/endpoint/server/mocks.ts @@ -0,0 +1,44 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +/** + * Creates a mock IndexPatternRetriever for use in tests. + * + * @param indexPattern a string index pattern to return when any of the mock's public methods are called. + * @returns the same string passed in via `indexPattern` + */ +export const createMockIndexPatternRetriever = (indexPattern: string) => { + const mockGetFunc = jest.fn().mockResolvedValue(indexPattern); + return { + getIndexPattern: mockGetFunc, + getEventIndexPattern: mockGetFunc, + getMetadataIndexPattern: mockGetFunc, + }; +}; + +export const MetadataIndexPattern = 'metrics-endpoint-*'; + +/** + * Creates a mock IndexPatternRetriever for use in tests that returns `metrics-endpoint-*` + */ +export const createMockMetadataIndexPatternRetriever = () => { + return createMockIndexPatternRetriever(MetadataIndexPattern); +}; + +/** + * Creates a mock IndexPatternService for use in tests that need to interact with the Ingest Manager's + * ESIndexPatternService. + * + * @param indexPattern a string index pattern to return when called by a test + * @returns the same value as `indexPattern` parameter + */ +export const createMockIndexPatternService = (indexPattern: string) => { + return { + esIndexPatternService: { + getESIndexPattern: jest.fn().mockResolvedValue(indexPattern), + }, + }; +}; diff --git a/x-pack/plugins/endpoint/server/plugin.test.ts b/x-pack/plugins/endpoint/server/plugin.test.ts index 7dd878d579043..8d55e64f16dcf 100644 --- a/x-pack/plugins/endpoint/server/plugin.test.ts +++ b/x-pack/plugins/endpoint/server/plugin.test.ts @@ -7,6 +7,7 @@ import { EndpointPlugin, EndpointPluginSetupDependencies } from './plugin'; import { coreMock } from '../../../../src/core/server/mocks'; import { PluginSetupContract } from '../../features/server'; +import { createMockIndexPatternService } from './mocks'; describe('test endpoint plugin', () => { let plugin: EndpointPlugin; @@ -28,7 +29,10 @@ describe('test endpoint plugin', () => { getFeaturesUICapabilities: jest.fn(), registerLegacyAPI: jest.fn(), }; - mockedEndpointPluginSetupDependencies = { features: mockedPluginSetupContract }; + mockedEndpointPluginSetupDependencies = { + features: mockedPluginSetupContract, + ingestManager: createMockIndexPatternService(''), + }; }); it('test properly setup plugin', async () => { diff --git a/x-pack/plugins/endpoint/server/plugin.ts b/x-pack/plugins/endpoint/server/plugin.ts index d3a399124124f..6a42014e91130 100644 --- a/x-pack/plugins/endpoint/server/plugin.ts +++ b/x-pack/plugins/endpoint/server/plugin.ts @@ -6,12 +6,15 @@ import { Plugin, CoreSetup, PluginInitializerContext, Logger } from 'kibana/server'; import { first } from 'rxjs/operators'; import { PluginSetupContract as FeaturesPluginSetupContract } from '../../features/server'; +import { IngestManagerSetupContract } from '../../ingest_manager/server'; import { createConfig$, EndpointConfigType } from './config'; import { EndpointAppContext } from './types'; import { registerAlertRoutes } from './routes/alerts'; import { registerResolverRoutes } from './routes/resolver'; +import { registerIndexPatternRoute } from './routes/index_pattern'; import { registerEndpointRoutes } from './routes/metadata'; +import { IngestIndexPatternRetriever } from './index_pattern'; export type EndpointPluginStart = void; export type EndpointPluginSetup = void; @@ -19,6 +22,7 @@ export interface EndpointPluginStartDependencies {} // eslint-disable-line @type export interface EndpointPluginSetupDependencies { features: FeaturesPluginSetupContract; + ingestManager: IngestManagerSetupContract; } export class EndpointPlugin @@ -62,6 +66,10 @@ export class EndpointPlugin }, }); const endpointContext = { + indexPatternRetriever: new IngestIndexPatternRetriever( + plugins.ingestManager.esIndexPatternService, + this.initializerContext.logger + ), logFactory: this.initializerContext.logger, config: (): Promise<EndpointConfigType> => { return createConfig$(this.initializerContext) @@ -73,6 +81,7 @@ export class EndpointPlugin registerEndpointRoutes(router, endpointContext); registerResolverRoutes(router, endpointContext); registerAlertRoutes(router, endpointContext); + registerIndexPatternRoute(router, endpointContext); } public start() { diff --git a/x-pack/plugins/endpoint/server/routes/alerts/alerts.test.ts b/x-pack/plugins/endpoint/server/routes/alerts/alerts.test.ts index 5f5e4be4ecd0a..6be7b26898206 100644 --- a/x-pack/plugins/endpoint/server/routes/alerts/alerts.test.ts +++ b/x-pack/plugins/endpoint/server/routes/alerts/alerts.test.ts @@ -12,6 +12,7 @@ import { import { registerAlertRoutes } from './index'; import { EndpointConfigSchema } from '../../config'; import { alertingIndexGetQuerySchema } from '../../../common/schema/alert_index'; +import { createMockIndexPatternRetriever } from '../../mocks'; describe('test alerts route', () => { let routerMock: jest.Mocked<IRouter>; @@ -24,6 +25,7 @@ describe('test alerts route', () => { mockClusterClient.asScoped.mockReturnValue(mockScopedClient); routerMock = httpServiceMock.createRouter(); registerAlertRoutes(routerMock, { + indexPatternRetriever: createMockIndexPatternRetriever('events-endpoint-*'), logFactory: loggingServiceMock.create(), config: () => Promise.resolve(EndpointConfigSchema.validate({})), }); diff --git a/x-pack/plugins/endpoint/server/routes/alerts/details/handlers.ts b/x-pack/plugins/endpoint/server/routes/alerts/details/handlers.ts index 0f32deb4fad9b..86e9f55da5697 100644 --- a/x-pack/plugins/endpoint/server/routes/alerts/details/handlers.ts +++ b/x-pack/plugins/endpoint/server/routes/alerts/details/handlers.ts @@ -26,15 +26,18 @@ export const alertDetailsHandlerWrapper = function( id: alertId, })) as GetResponse<AlertEvent>; + const indexPattern = await endpointAppContext.indexPatternRetriever.getEventIndexPattern(ctx); + const config = await endpointAppContext.config(); const pagination: AlertDetailsPagination = new AlertDetailsPagination( config, ctx, req.params, - response + response, + indexPattern ); - const currentHostInfo = await getHostData(ctx, response._source.host.id); + const currentHostInfo = await getHostData(ctx, response._source.host.id, indexPattern); return res.ok({ body: { diff --git a/x-pack/plugins/endpoint/server/routes/alerts/details/lib/pagination.ts b/x-pack/plugins/endpoint/server/routes/alerts/details/lib/pagination.ts index 446d61080e650..d482da03872c6 100644 --- a/x-pack/plugins/endpoint/server/routes/alerts/details/lib/pagination.ts +++ b/x-pack/plugins/endpoint/server/routes/alerts/details/lib/pagination.ts @@ -29,7 +29,8 @@ export class AlertDetailsPagination extends Pagination< config: EndpointConfigType, requestContext: RequestHandlerContext, state: AlertDetailsRequestParams, - data: GetResponse<AlertEvent> + data: GetResponse<AlertEvent>, + private readonly indexPattern: string ) { super(config, requestContext, state, data); } @@ -54,7 +55,8 @@ export class AlertDetailsPagination extends Pagination< const response = await searchESForAlerts( this.requestContext.core.elasticsearch.dataClient, - reqData + reqData, + this.indexPattern ); return response; } diff --git a/x-pack/plugins/endpoint/server/routes/alerts/lib/index.ts b/x-pack/plugins/endpoint/server/routes/alerts/lib/index.ts index dc1ce767a715b..74db24c85eab5 100644 --- a/x-pack/plugins/endpoint/server/routes/alerts/lib/index.ts +++ b/x-pack/plugins/endpoint/server/routes/alerts/lib/index.ts @@ -97,7 +97,8 @@ function buildSort(query: AlertSearchQuery): AlertSort { * Builds a request body for Elasticsearch, given a set of query params. **/ const buildAlertSearchQuery = async ( - query: AlertSearchQuery + query: AlertSearchQuery, + indexPattern: string ): Promise<AlertSearchRequestWrapper> => { let totalHitsMin: number = EndpointAppConstants.DEFAULT_TOTAL_HITS; @@ -125,7 +126,7 @@ const buildAlertSearchQuery = async ( const reqWrapper: AlertSearchRequestWrapper = { size: query.pageSize, - index: EndpointAppConstants.ALERT_INDEX_NAME, + index: indexPattern, body: reqBody, }; @@ -141,9 +142,10 @@ const buildAlertSearchQuery = async ( **/ export const searchESForAlerts = async ( dataClient: IScopedClusterClient, - query: AlertSearchQuery + query: AlertSearchQuery, + indexPattern: string ): Promise<SearchResponse<AlertEvent>> => { - const reqWrapper = await buildAlertSearchQuery(query); + const reqWrapper = await buildAlertSearchQuery(query, indexPattern); const response = (await dataClient.callAsCurrentUser('search', reqWrapper)) as SearchResponse< AlertEvent >; diff --git a/x-pack/plugins/endpoint/server/routes/alerts/list/handlers.ts b/x-pack/plugins/endpoint/server/routes/alerts/list/handlers.ts index 93ec8d7aa3e67..f23dffd13db4f 100644 --- a/x-pack/plugins/endpoint/server/routes/alerts/list/handlers.ts +++ b/x-pack/plugins/endpoint/server/routes/alerts/list/handlers.ts @@ -18,8 +18,13 @@ export const alertListHandlerWrapper = function( res ) => { try { + const indexPattern = await endpointAppContext.indexPatternRetriever.getEventIndexPattern(ctx); const reqData = await getRequestData(req, endpointAppContext); - const response = await searchESForAlerts(ctx.core.elasticsearch.dataClient, reqData); + const response = await searchESForAlerts( + ctx.core.elasticsearch.dataClient, + reqData, + indexPattern + ); const mappedBody = await mapToAlertResultList(ctx, endpointAppContext, reqData, response); return res.ok({ body: mappedBody }); } catch (err) { diff --git a/x-pack/plugins/endpoint/server/routes/index_pattern.ts b/x-pack/plugins/endpoint/server/routes/index_pattern.ts new file mode 100644 index 0000000000000..3b71f6a6957ba --- /dev/null +++ b/x-pack/plugins/endpoint/server/routes/index_pattern.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 { IRouter, Logger, RequestHandler } from 'kibana/server'; +import { EndpointAppContext } from '../types'; +import { IndexPatternGetParamsResult, EndpointAppConstants } from '../../common/types'; +import { indexPatternGetParamsSchema } from '../../common/schema/index_pattern'; +import { IndexPatternRetriever } from '../index_pattern'; + +function handleIndexPattern( + log: Logger, + indexRetriever: IndexPatternRetriever +): RequestHandler<IndexPatternGetParamsResult> { + return async (context, req, res) => { + try { + return res.ok({ + body: { + indexPattern: await indexRetriever.getIndexPattern(context, req.params.datasetPath), + }, + }); + } catch (error) { + log.warn(error); + return res.notFound({ body: error }); + } + }; +} + +export function registerIndexPatternRoute(router: IRouter, endpointAppContext: EndpointAppContext) { + const log = endpointAppContext.logFactory.get('index_pattern'); + + router.get( + { + path: `${EndpointAppConstants.INDEX_PATTERN_ROUTE}/{datasetPath}`, + validate: { params: indexPatternGetParamsSchema }, + options: { authRequired: true }, + }, + handleIndexPattern(log, endpointAppContext.indexPatternRetriever) + ); +} diff --git a/x-pack/plugins/endpoint/server/routes/metadata/index.ts b/x-pack/plugins/endpoint/server/routes/metadata/index.ts index 450469914bc50..883bb88204fd4 100644 --- a/x-pack/plugins/endpoint/server/routes/metadata/index.ts +++ b/x-pack/plugins/endpoint/server/routes/metadata/index.ts @@ -8,9 +8,9 @@ import { IRouter, RequestHandlerContext } from 'kibana/server'; import { SearchResponse } from 'elasticsearch'; import { schema } from '@kbn/config-schema'; +import { kibanaRequestToMetadataListESQuery, getESQueryHostMetadataByID } from './query_builders'; import { HostInfo, HostMetadata, HostResultList, HostStatus } from '../../../common/types'; import { EndpointAppContext } from '../../types'; -import { getESQueryHostMetadataByID, kibanaRequestToMetadataListESQuery } from './query_builders'; interface HitSource { _source: HostMetadata; @@ -50,7 +50,14 @@ export function registerEndpointRoutes(router: IRouter, endpointAppContext: Endp }, async (context, req, res) => { try { - const queryParams = await kibanaRequestToMetadataListESQuery(req, endpointAppContext); + const index = await endpointAppContext.indexPatternRetriever.getMetadataIndexPattern( + context + ); + const queryParams = await kibanaRequestToMetadataListESQuery( + req, + endpointAppContext, + index + ); const response = (await context.core.elasticsearch.dataClient.callAsCurrentUser( 'search', queryParams @@ -72,7 +79,11 @@ export function registerEndpointRoutes(router: IRouter, endpointAppContext: Endp }, async (context, req, res) => { try { - const doc = await getHostData(context, req.params.id); + const index = await endpointAppContext.indexPatternRetriever.getMetadataIndexPattern( + context + ); + + const doc = await getHostData(context, req.params.id, index); if (doc) { return res.ok({ body: doc }); } @@ -86,9 +97,10 @@ export function registerEndpointRoutes(router: IRouter, endpointAppContext: Endp export async function getHostData( context: RequestHandlerContext, - id: string + id: string, + index: string ): Promise<HostInfo | undefined> { - const query = getESQueryHostMetadataByID(id); + const query = getESQueryHostMetadataByID(id, index); const response = (await context.core.elasticsearch.dataClient.callAsCurrentUser( 'search', query diff --git a/x-pack/plugins/endpoint/server/routes/metadata/metadata.test.ts b/x-pack/plugins/endpoint/server/routes/metadata/metadata.test.ts index 9bd251735cc04..9a7d3fb3188a6 100644 --- a/x-pack/plugins/endpoint/server/routes/metadata/metadata.test.ts +++ b/x-pack/plugins/endpoint/server/routes/metadata/metadata.test.ts @@ -11,24 +11,28 @@ import { RequestHandler, RequestHandlerContext, RouteConfig, + SavedObjectsClientContract, } from 'kibana/server'; import { elasticsearchServiceMock, httpServerMock, httpServiceMock, loggingServiceMock, + savedObjectsClientMock, } from '../../../../../../src/core/server/mocks'; import { HostInfo, HostMetadata, HostResultList, HostStatus } from '../../../common/types'; import { SearchResponse } from 'elasticsearch'; +import { registerEndpointRoutes } from './index'; import { EndpointConfigSchema } from '../../config'; import * as data from '../../test_data/all_metadata_data.json'; -import { registerEndpointRoutes } from './index'; +import { createMockMetadataIndexPatternRetriever } from '../../mocks'; describe('test endpoint route', () => { let routerMock: jest.Mocked<IRouter>; let mockResponse: jest.Mocked<KibanaResponseFactory>; let mockClusterClient: jest.Mocked<IClusterClient>; let mockScopedClient: jest.Mocked<IScopedClusterClient>; + let mockSavedObjectClient: jest.Mocked<SavedObjectsClientContract>; let routeHandler: RequestHandler<any, any, any>; let routeConfig: RouteConfig<any, any, any, any>; @@ -37,15 +41,38 @@ describe('test endpoint route', () => { IClusterClient >; mockScopedClient = elasticsearchServiceMock.createScopedClusterClient(); + mockSavedObjectClient = savedObjectsClientMock.create(); mockClusterClient.asScoped.mockReturnValue(mockScopedClient); routerMock = httpServiceMock.createRouter(); mockResponse = httpServerMock.createResponseFactory(); registerEndpointRoutes(routerMock, { + indexPatternRetriever: createMockMetadataIndexPatternRetriever(), logFactory: loggingServiceMock.create(), config: () => Promise.resolve(EndpointConfigSchema.validate({})), }); }); + function createRouteHandlerContext( + dataClient: jest.Mocked<IScopedClusterClient>, + savedObjectsClient: jest.Mocked<SavedObjectsClientContract> + ) { + return ({ + core: { + elasticsearch: { + dataClient, + }, + savedObjects: { + client: savedObjectsClient, + }, + }, + /** + * Using unknown here because the object defined is not a full `RequestHandlerContext`. We don't + * need all of the fields required to run the tests, but the `routeHandler` function requires a + * `RequestHandlerContext`. + */ + } as unknown) as RequestHandlerContext; + } + it('test find the latest of all endpoints', async () => { const mockRequest = httpServerMock.createKibanaRequest({}); @@ -58,13 +85,7 @@ describe('test endpoint route', () => { )!; await routeHandler( - ({ - core: { - elasticsearch: { - dataClient: mockScopedClient, - }, - }, - } as unknown) as RequestHandlerContext, + createRouteHandlerContext(mockScopedClient, mockSavedObjectClient), mockRequest, mockResponse ); @@ -100,13 +121,7 @@ describe('test endpoint route', () => { )!; await routeHandler( - ({ - core: { - elasticsearch: { - dataClient: mockScopedClient, - }, - }, - } as unknown) as RequestHandlerContext, + createRouteHandlerContext(mockScopedClient, mockSavedObjectClient), mockRequest, mockResponse ); @@ -147,13 +162,7 @@ describe('test endpoint route', () => { )!; await routeHandler( - ({ - core: { - elasticsearch: { - dataClient: mockScopedClient, - }, - }, - } as unknown) as RequestHandlerContext, + createRouteHandlerContext(mockScopedClient, mockSavedObjectClient), mockRequest, mockResponse ); @@ -212,13 +221,7 @@ describe('test endpoint route', () => { )!; await routeHandler( - ({ - core: { - elasticsearch: { - dataClient: mockScopedClient, - }, - }, - } as unknown) as RequestHandlerContext, + createRouteHandlerContext(mockScopedClient, mockSavedObjectClient), mockRequest, mockResponse ); @@ -243,13 +246,7 @@ describe('test endpoint route', () => { )!; await routeHandler( - ({ - core: { - elasticsearch: { - dataClient: mockScopedClient, - }, - }, - } as unknown) as RequestHandlerContext, + createRouteHandlerContext(mockScopedClient, mockSavedObjectClient), mockRequest, mockResponse ); diff --git a/x-pack/plugins/endpoint/server/routes/metadata/query_builders.test.ts b/x-pack/plugins/endpoint/server/routes/metadata/query_builders.test.ts index 2514d5aa85811..c8143fbdda1ea 100644 --- a/x-pack/plugins/endpoint/server/routes/metadata/query_builders.test.ts +++ b/x-pack/plugins/endpoint/server/routes/metadata/query_builders.test.ts @@ -6,7 +6,7 @@ import { httpServerMock, loggingServiceMock } from '../../../../../../src/core/server/mocks'; import { EndpointConfigSchema } from '../../config'; import { kibanaRequestToMetadataListESQuery, getESQueryHostMetadataByID } from './query_builders'; -import { EndpointAppConstants } from '../../../common/types'; +import { createMockMetadataIndexPatternRetriever, MetadataIndexPattern } from '../../mocks'; describe('query builder', () => { describe('MetadataListESQuery', () => { @@ -14,17 +14,22 @@ describe('query builder', () => { const mockRequest = httpServerMock.createKibanaRequest({ body: {}, }); - const query = await kibanaRequestToMetadataListESQuery(mockRequest, { - logFactory: loggingServiceMock.create(), - config: () => Promise.resolve(EndpointConfigSchema.validate({})), - }); + const query = await kibanaRequestToMetadataListESQuery( + mockRequest, + { + indexPatternRetriever: createMockMetadataIndexPatternRetriever(), + logFactory: loggingServiceMock.create(), + config: () => Promise.resolve(EndpointConfigSchema.validate({})), + }, + MetadataIndexPattern + ); expect(query).toEqual({ body: { query: { match_all: {}, }, collapse: { - field: 'host.id.keyword', + field: 'host.id', inner_hits: { name: 'most_recent', size: 1, @@ -34,7 +39,7 @@ describe('query builder', () => { aggs: { total: { cardinality: { - field: 'host.id.keyword', + field: 'host.id', }, }, }, @@ -48,7 +53,7 @@ describe('query builder', () => { }, from: 0, size: 10, - index: EndpointAppConstants.ENDPOINT_INDEX_NAME, + index: MetadataIndexPattern, } as Record<string, any>); }); }); @@ -60,10 +65,15 @@ describe('query builder', () => { filter: 'not host.ip:10.140.73.246', }, }); - const query = await kibanaRequestToMetadataListESQuery(mockRequest, { - logFactory: loggingServiceMock.create(), - config: () => Promise.resolve(EndpointConfigSchema.validate({})), - }); + const query = await kibanaRequestToMetadataListESQuery( + mockRequest, + { + indexPatternRetriever: createMockMetadataIndexPatternRetriever(), + logFactory: loggingServiceMock.create(), + config: () => Promise.resolve(EndpointConfigSchema.validate({})), + }, + MetadataIndexPattern + ); expect(query).toEqual({ body: { query: { @@ -83,7 +93,7 @@ describe('query builder', () => { }, }, collapse: { - field: 'host.id.keyword', + field: 'host.id', inner_hits: { name: 'most_recent', size: 1, @@ -93,7 +103,7 @@ describe('query builder', () => { aggs: { total: { cardinality: { - field: 'host.id.keyword', + field: 'host.id', }, }, }, @@ -107,7 +117,7 @@ describe('query builder', () => { }, from: 0, size: 10, - index: EndpointAppConstants.ENDPOINT_INDEX_NAME, + index: MetadataIndexPattern, } as Record<string, any>); }); }); @@ -115,14 +125,15 @@ describe('query builder', () => { describe('MetadataGetQuery', () => { it('searches for the correct ID', () => { const mockID = 'AABBCCDD-0011-2233-AA44-DEADBEEF8899'; - const query = getESQueryHostMetadataByID(mockID); + const query = getESQueryHostMetadataByID(mockID, MetadataIndexPattern); + expect(query).toEqual({ body: { - query: { match: { 'host.id.keyword': mockID } }, + query: { match: { 'host.id': mockID } }, sort: [{ 'event.created': { order: 'desc' } }], size: 1, }, - index: EndpointAppConstants.ENDPOINT_INDEX_NAME, + index: MetadataIndexPattern, }); }); }); diff --git a/x-pack/plugins/endpoint/server/routes/metadata/query_builders.ts b/x-pack/plugins/endpoint/server/routes/metadata/query_builders.ts index bd07604fe9ad2..abcc293985f9f 100644 --- a/x-pack/plugins/endpoint/server/routes/metadata/query_builders.ts +++ b/x-pack/plugins/endpoint/server/routes/metadata/query_builders.ts @@ -6,18 +6,18 @@ import { KibanaRequest } from 'kibana/server'; import { esKuery } from '../../../../../../src/plugins/data/server'; import { EndpointAppContext } from '../../types'; -import { EndpointAppConstants } from '../../../common/types'; export const kibanaRequestToMetadataListESQuery = async ( request: KibanaRequest<any, any, any>, - endpointAppContext: EndpointAppContext + endpointAppContext: EndpointAppContext, + index: string ): Promise<Record<string, any>> => { const pagingProperties = await getPagingProperties(request, endpointAppContext); return { body: { query: buildQueryBody(request), collapse: { - field: 'host.id.keyword', + field: 'host.id', inner_hits: { name: 'most_recent', size: 1, @@ -27,7 +27,7 @@ export const kibanaRequestToMetadataListESQuery = async ( aggs: { total: { cardinality: { - field: 'host.id.keyword', + field: 'host.id', }, }, }, @@ -41,7 +41,7 @@ export const kibanaRequestToMetadataListESQuery = async ( }, from: pagingProperties.pageIndex * pagingProperties.pageSize, size: pagingProperties.pageSize, - index: EndpointAppConstants.ENDPOINT_INDEX_NAME, + index, }; }; @@ -74,12 +74,12 @@ function buildQueryBody(request: KibanaRequest<any, any, any>): Record<string, a }; } -export function getESQueryHostMetadataByID(hostID: string) { +export function getESQueryHostMetadataByID(hostID: string, index: string) { return { body: { query: { match: { - 'host.id.keyword': hostID, + 'host.id': hostID, }, }, sort: [ @@ -91,6 +91,6 @@ export function getESQueryHostMetadataByID(hostID: string) { ], size: 1, }, - index: EndpointAppConstants.ENDPOINT_INDEX_NAME, + index, }; } diff --git a/x-pack/plugins/endpoint/server/routes/resolver.ts b/x-pack/plugins/endpoint/server/routes/resolver.ts index 946ada51c40e9..77fcbc87baeb1 100644 --- a/x-pack/plugins/endpoint/server/routes/resolver.ts +++ b/x-pack/plugins/endpoint/server/routes/resolver.ts @@ -12,6 +12,7 @@ import { handleLifecycle, validateLifecycle } from './resolver/lifecycle'; export function registerResolverRoutes(router: IRouter, endpointAppContext: EndpointAppContext) { const log = endpointAppContext.logFactory.get('resolver'); + const indexPatternService = endpointAppContext.indexPatternRetriever; router.get( { @@ -19,7 +20,7 @@ export function registerResolverRoutes(router: IRouter, endpointAppContext: Endp validate: validateRelatedEvents, options: { authRequired: true }, }, - handleRelatedEvents(log) + handleRelatedEvents(log, indexPatternService) ); router.get( @@ -28,7 +29,7 @@ export function registerResolverRoutes(router: IRouter, endpointAppContext: Endp validate: validateChildren, options: { authRequired: true }, }, - handleChildren(log) + handleChildren(log, indexPatternService) ); router.get( @@ -37,6 +38,6 @@ export function registerResolverRoutes(router: IRouter, endpointAppContext: Endp validate: validateLifecycle, options: { authRequired: true }, }, - handleLifecycle(log) + handleLifecycle(log, indexPatternService) ); } diff --git a/x-pack/plugins/endpoint/server/routes/resolver/children.ts b/x-pack/plugins/endpoint/server/routes/resolver/children.ts index f97c742b18d67..05b8f0b5f8608 100644 --- a/x-pack/plugins/endpoint/server/routes/resolver/children.ts +++ b/x-pack/plugins/endpoint/server/routes/resolver/children.ts @@ -11,6 +11,7 @@ import { extractEntityID } from './utils/normalize'; import { getPaginationParams } from './utils/pagination'; import { LifecycleQuery } from './queries/lifecycle'; import { ChildrenQuery } from './queries/children'; +import { IndexPatternRetriever } from '../../index_pattern'; interface ChildrenQueryParams { after?: string; @@ -45,7 +46,8 @@ export const validateChildren = { }; export function handleChildren( - log: Logger + log: Logger, + indexRetriever: IndexPatternRetriever ): RequestHandler<ChildrenPathParams, ChildrenQueryParams> { return async (context, req, res) => { const { @@ -54,10 +56,11 @@ export function handleChildren( } = req; try { const pagination = getPaginationParams(limit, after); + const indexPattern = await indexRetriever.getEventIndexPattern(context); const client = context.core.elasticsearch.dataClient; - const childrenQuery = new ChildrenQuery(legacyEndpointID, pagination); - const lifecycleQuery = new LifecycleQuery(legacyEndpointID); + const childrenQuery = new ChildrenQuery(indexPattern, legacyEndpointID, pagination); + const lifecycleQuery = new LifecycleQuery(indexPattern, legacyEndpointID); // Retrieve the related child process events for a given process const { total, results: events, nextCursor } = await childrenQuery.search(client, id); diff --git a/x-pack/plugins/endpoint/server/routes/resolver/lifecycle.ts b/x-pack/plugins/endpoint/server/routes/resolver/lifecycle.ts index 9895344174014..6d155b79651a7 100644 --- a/x-pack/plugins/endpoint/server/routes/resolver/lifecycle.ts +++ b/x-pack/plugins/endpoint/server/routes/resolver/lifecycle.ts @@ -10,6 +10,7 @@ import { RequestHandler, Logger } from 'kibana/server'; import { extractParentEntityID } from './utils/normalize'; import { LifecycleQuery } from './queries/lifecycle'; import { ResolverEvent } from '../../../common/types'; +import { IndexPatternRetriever } from '../../index_pattern'; interface LifecycleQueryParams { ancestors: number; @@ -46,7 +47,8 @@ function getParentEntityID(results: ResolverEvent[]) { } export function handleLifecycle( - log: Logger + log: Logger, + indexRetriever: IndexPatternRetriever ): RequestHandler<LifecyclePathParams, LifecycleQueryParams> { return async (context, req, res) => { const { @@ -56,8 +58,8 @@ export function handleLifecycle( try { const ancestorLifecycles = []; const client = context.core.elasticsearch.dataClient; - - const lifecycleQuery = new LifecycleQuery(legacyEndpointID); + const indexPattern = await indexRetriever.getEventIndexPattern(context); + const lifecycleQuery = new LifecycleQuery(indexPattern, legacyEndpointID); const { results: processLifecycle } = await lifecycleQuery.search(client, id); let nextParentID = getParentEntityID(processLifecycle); diff --git a/x-pack/plugins/endpoint/server/routes/resolver/queries/base.ts b/x-pack/plugins/endpoint/server/routes/resolver/queries/base.ts index be83efc39ca4c..b049439207e50 100644 --- a/x-pack/plugins/endpoint/server/routes/resolver/queries/base.ts +++ b/x-pack/plugins/endpoint/server/routes/resolver/queries/base.ts @@ -11,6 +11,7 @@ import { JsonObject } from '../../../../../../../src/plugins/kibana_utils/public export abstract class ResolverQuery { constructor( + private readonly indexPattern: string, private readonly endpointID?: string, private readonly pagination?: PaginationParams ) {} @@ -26,7 +27,7 @@ export abstract class ResolverQuery { if (this.endpointID) { return this.legacyQuery(this.endpointID, ids, EndpointAppConstants.LEGACY_EVENT_INDEX_NAME); } - return this.query(ids, EndpointAppConstants.EVENT_INDEX_NAME); + return this.query(ids, this.indexPattern); } async search(client: IScopedClusterClient, ...ids: string[]) { diff --git a/x-pack/plugins/endpoint/server/routes/resolver/queries/children.test.ts b/x-pack/plugins/endpoint/server/routes/resolver/queries/children.test.ts index 08a906e2884d6..e73053d53dee0 100644 --- a/x-pack/plugins/endpoint/server/routes/resolver/queries/children.test.ts +++ b/x-pack/plugins/endpoint/server/routes/resolver/queries/children.test.ts @@ -6,11 +6,17 @@ import { ChildrenQuery } from './children'; import { EndpointAppConstants } from '../../../../common/types'; +export const fakeEventIndexPattern = 'events-endpoint-*'; + describe('children events query', () => { it('generates the correct legacy queries', () => { const timestamp = new Date().getTime(); expect( - new ChildrenQuery('awesome-id', { size: 1, timestamp, eventID: 'foo' }).build('5') + new ChildrenQuery(EndpointAppConstants.LEGACY_EVENT_INDEX_NAME, 'awesome-id', { + size: 1, + timestamp, + eventID: 'foo', + }).build('5') ).toStrictEqual({ body: { query: { @@ -50,7 +56,11 @@ describe('children events query', () => { const timestamp = new Date().getTime(); expect( - new ChildrenQuery(undefined, { size: 1, timestamp, eventID: 'bar' }).build('baz') + new ChildrenQuery(fakeEventIndexPattern, undefined, { + size: 1, + timestamp, + eventID: 'bar', + }).build('baz') ).toStrictEqual({ body: { query: { @@ -88,7 +98,7 @@ describe('children events query', () => { size: 1, sort: [{ '@timestamp': 'asc' }, { 'event.id': 'asc' }], }, - index: EndpointAppConstants.EVENT_INDEX_NAME, + index: fakeEventIndexPattern, }); }); }); diff --git a/x-pack/plugins/endpoint/server/routes/resolver/queries/lifecycle.test.ts b/x-pack/plugins/endpoint/server/routes/resolver/queries/lifecycle.test.ts index b1b47bfb9de7f..8a3955706b278 100644 --- a/x-pack/plugins/endpoint/server/routes/resolver/queries/lifecycle.test.ts +++ b/x-pack/plugins/endpoint/server/routes/resolver/queries/lifecycle.test.ts @@ -5,10 +5,13 @@ */ import { EndpointAppConstants } from '../../../../common/types'; import { LifecycleQuery } from './lifecycle'; +import { fakeEventIndexPattern } from './children.test'; describe('lifecycle query', () => { it('generates the correct legacy queries', () => { - expect(new LifecycleQuery('awesome-id').build('5')).toStrictEqual({ + expect( + new LifecycleQuery(EndpointAppConstants.LEGACY_EVENT_INDEX_NAME, 'awesome-id').build('5') + ).toStrictEqual({ body: { query: { bool: { @@ -32,7 +35,7 @@ describe('lifecycle query', () => { }); it('generates the correct non-legacy queries', () => { - expect(new LifecycleQuery().build('baz')).toStrictEqual({ + expect(new LifecycleQuery(fakeEventIndexPattern).build('baz')).toStrictEqual({ body: { query: { bool: { @@ -57,7 +60,7 @@ describe('lifecycle query', () => { }, sort: [{ '@timestamp': 'asc' }], }, - index: EndpointAppConstants.EVENT_INDEX_NAME, + index: fakeEventIndexPattern, }); }); }); diff --git a/x-pack/plugins/endpoint/server/routes/resolver/queries/related_events.test.ts b/x-pack/plugins/endpoint/server/routes/resolver/queries/related_events.test.ts index a91c87274b8dd..5caef935ce621 100644 --- a/x-pack/plugins/endpoint/server/routes/resolver/queries/related_events.test.ts +++ b/x-pack/plugins/endpoint/server/routes/resolver/queries/related_events.test.ts @@ -5,12 +5,17 @@ */ import { RelatedEventsQuery } from './related_events'; import { EndpointAppConstants } from '../../../../common/types'; +import { fakeEventIndexPattern } from './children.test'; describe('related events query', () => { it('generates the correct legacy queries', () => { const timestamp = new Date().getTime(); expect( - new RelatedEventsQuery('awesome-id', { size: 1, timestamp, eventID: 'foo' }).build('5') + new RelatedEventsQuery(EndpointAppConstants.LEGACY_EVENT_INDEX_NAME, 'awesome-id', { + size: 1, + timestamp, + eventID: 'foo', + }).build('5') ).toStrictEqual({ body: { query: { @@ -51,7 +56,11 @@ describe('related events query', () => { const timestamp = new Date().getTime(); expect( - new RelatedEventsQuery(undefined, { size: 1, timestamp, eventID: 'bar' }).build('baz') + new RelatedEventsQuery(fakeEventIndexPattern, undefined, { + size: 1, + timestamp, + eventID: 'bar', + }).build('baz') ).toStrictEqual({ body: { query: { @@ -90,7 +99,7 @@ describe('related events query', () => { size: 1, sort: [{ '@timestamp': 'asc' }, { 'event.id': 'asc' }], }, - index: EndpointAppConstants.EVENT_INDEX_NAME, + index: fakeEventIndexPattern, }); }); }); diff --git a/x-pack/plugins/endpoint/server/routes/resolver/related_events.ts b/x-pack/plugins/endpoint/server/routes/resolver/related_events.ts index 804400522065c..46e205464f53c 100644 --- a/x-pack/plugins/endpoint/server/routes/resolver/related_events.ts +++ b/x-pack/plugins/endpoint/server/routes/resolver/related_events.ts @@ -8,6 +8,7 @@ import { schema } from '@kbn/config-schema'; import { RequestHandler, Logger } from 'kibana/server'; import { getPaginationParams } from './utils/pagination'; import { RelatedEventsQuery } from './queries/related_events'; +import { IndexPatternRetriever } from '../../index_pattern'; interface RelatedEventsQueryParams { after?: string; @@ -42,7 +43,8 @@ export const validateRelatedEvents = { }; export function handleRelatedEvents( - log: Logger + log: Logger, + indexRetriever: IndexPatternRetriever ): RequestHandler<RelatedEventsPathParams, RelatedEventsQueryParams> { return async (context, req, res) => { const { @@ -53,8 +55,9 @@ export function handleRelatedEvents( const pagination = getPaginationParams(limit, after); const client = context.core.elasticsearch.dataClient; + const indexPattern = await indexRetriever.getEventIndexPattern(context); // Retrieve the related non-process events for a given process - const relatedEventsQuery = new RelatedEventsQuery(legacyEndpointID, pagination); + const relatedEventsQuery = new RelatedEventsQuery(indexPattern, legacyEndpointID, pagination); const relatedEvents = await relatedEventsQuery.search(client, id); const { total, results: events, nextCursor } = relatedEvents; diff --git a/x-pack/plugins/endpoint/server/test_data/all_metadata_data.json b/x-pack/plugins/endpoint/server/test_data/all_metadata_data.json index 3c8486aa127ea..48952afb33f68 100644 --- a/x-pack/plugins/endpoint/server/test_data/all_metadata_data.json +++ b/x-pack/plugins/endpoint/server/test_data/all_metadata_data.json @@ -1,115 +1,109 @@ { - "took" : 343, - "timed_out" : false, - "_shards" : { - "total" : 1, - "successful" : 1, - "skipped" : 0, - "failed" : 0 + "took": 343, + "timed_out": false, + "_shards": { + "total": 1, + "successful": 1, + "skipped": 0, + "failed": 0 }, - "hits" : { - "total" : { - "value" : 4, - "relation" : "eq" + "hits": { + "total": { + "value": 4, + "relation": "eq" }, - "max_score" : null, - "hits" : [ + "max_score": null, + "hits": [ { - "_index" : "endpoint-agent", - "_id" : "WqVo1G8BYQH1gtPUgYkC", - "_score" : null, - "_source" : { - "@timestamp" : 1579816615336, - "event" : { - "created" : "2020-01-23T21:56:55.336Z" + "_index": "metadata-endpoint-default-1", + "_id": "WqVo1G8BYQH1gtPUgYkC", + "_score": null, + "_source": { + "@timestamp": 1579816615336, + "event": { + "created": "2020-01-23T21:56:55.336Z" }, "elastic": { "agent": { "id": "56a75650-3c8a-4e4f-ac17-6dd729c650e2" } }, - "endpoint" : { - "policy" : { - "id" : "C2A9093E-E289-4C0A-AA44-8C32A414FA7A" + "endpoint": { + "policy": { + "id": "C2A9093E-E289-4C0A-AA44-8C32A414FA7A" } }, - "agent" : { - "version" : "6.8.3", - "id" : "56a75650-3c8a-4e4f-ac17-6dd729c650e2", + "agent": { + "version": "6.8.3", + "id": "56a75650-3c8a-4e4f-ac17-6dd729c650e2", "name": "Elastic Endpoint" }, - "host" : { - "id" : "7141a48b-e19f-4ae3-89a0-6e7179a84265", - "hostname" : "larimer-0.example.com", - "ip" : "10.21.48.136", - "mac" : "77-be-30-f0-e8-d6", - "architecture" : "x86_64", - "os" : { - "name" : "windows 6.2", - "full" : "Windows Server 2012", - "version" : "6.2", - "variant" : "Windows Server" + "host": { + "id": "7141a48b-e19f-4ae3-89a0-6e7179a84265", + "hostname": "larimer-0.example.com", + "ip": "10.21.48.136", + "mac": "77-be-30-f0-e8-d6", + "architecture": "x86_64", + "os": { + "name": "windows 6.2", + "full": "Windows Server 2012", + "version": "6.2", + "variant": "Windows Server" } } }, - "fields" : { - "host.id.keyword" : [ - "7141a48b-e19f-4ae3-89a0-6e7179a84265" - ] + "fields": { + "host.id.keyword": ["7141a48b-e19f-4ae3-89a0-6e7179a84265"] }, - "sort" : [ - 1579816615336 - ], - "inner_hits" : { - "most_recent" : { - "hits" : { - "total" : { - "value" : 2, - "relation" : "eq" + "sort": [1579816615336], + "inner_hits": { + "most_recent": { + "hits": { + "total": { + "value": 2, + "relation": "eq" }, - "max_score" : null, - "hits" : [ + "max_score": null, + "hits": [ { - "_index" : "endpoint-agent", - "_id" : "WqVo1G8BYQH1gtPUgYkC", - "_score" : null, - "_source" : { - "@timestamp" : 1579816615336, - "event" : { - "created" : "2020-01-23T21:56:55.336Z" + "_index": "metadata-endpoint-default-1", + "_id": "WqVo1G8BYQH1gtPUgYkC", + "_score": null, + "_source": { + "@timestamp": 1579816615336, + "event": { + "created": "2020-01-23T21:56:55.336Z" }, "elastic": { "agent": { "id": "56a75650-3c8a-4e4f-ac17-6dd729c650e2" } }, - "endpoint" : { - "policy" : { - "id" : "C2A9093E-E289-4C0A-AA44-8C32A414FA7A" + "endpoint": { + "policy": { + "id": "C2A9093E-E289-4C0A-AA44-8C32A414FA7A" } }, - "agent" : { - "version" : "6.8.3", - "id" : "56a75650-3c8a-4e4f-ac17-6dd729c650e2", + "agent": { + "version": "6.8.3", + "id": "56a75650-3c8a-4e4f-ac17-6dd729c650e2", "name": "Elastic Endpoint" }, - "host" : { - "id" : "7141a48b-e19f-4ae3-89a0-6e7179a84265", - "hostname" : "larimer-0.example.com", - "ip" : "10.21.48.136", - "mac" : "77-be-30-f0-e8-d6", - "architecture" : "x86_64", - "os" : { - "name" : "windows 6.2", - "full" : "Windows Server 2012", - "version" : "6.2", - "variant" : "Windows Server" + "host": { + "id": "7141a48b-e19f-4ae3-89a0-6e7179a84265", + "hostname": "larimer-0.example.com", + "ip": "10.21.48.136", + "mac": "77-be-30-f0-e8-d6", + "architecture": "x86_64", + "os": { + "name": "windows 6.2", + "full": "Windows Server 2012", + "version": "6.2", + "variant": "Windows Server" } } }, - "sort" : [ - 1579816615336 - ] + "sort": [1579816615336] } ] } @@ -117,101 +111,95 @@ } }, { - "_index" : "endpoint-agent", - "_id" : "W6Vo1G8BYQH1gtPUgYkC", - "_score" : null, - "_source" : { - "@timestamp" : 1579816615336, - "event" : { - "created" : "2020-01-23T21:56:55.336Z" + "_index": "metadata-endpoint-default-1", + "_id": "W6Vo1G8BYQH1gtPUgYkC", + "_score": null, + "_source": { + "@timestamp": 1579816615336, + "event": { + "created": "2020-01-23T21:56:55.336Z" }, "elastic": { "agent": { "id": "c2d84d8f-d355-40de-8b54-5d318d4d1312" } }, - "endpoint" : { - "policy" : { - "id" : "C2A9093E-E289-4C0A-AA44-8C32A414FA7A" + "endpoint": { + "policy": { + "id": "C2A9093E-E289-4C0A-AA44-8C32A414FA7A" } }, - "agent" : { - "version" : "6.4.3", - "id" : "c2d84d8f-d355-40de-8b54-5d318d4d1312", + "agent": { + "version": "6.4.3", + "id": "c2d84d8f-d355-40de-8b54-5d318d4d1312", "name": "Elastic Endpoint" }, - "host" : { - "id" : "f35ec6c1-6562-45b1-818f-2f14c0854adf", - "hostname" : "hildebrandt-6.example.com", - "ip" : "10.53.92.84", - "mac" : "af-f1-8f-51-25-2a", - "architecture" : "x86_64", - "os" : { - "name" : "windows 10.0", - "full" : "Windows 10", - "version" : "10.0", - "variant" : "Windows Pro" + "host": { + "id": "f35ec6c1-6562-45b1-818f-2f14c0854adf", + "hostname": "hildebrandt-6.example.com", + "ip": "10.53.92.84", + "mac": "af-f1-8f-51-25-2a", + "architecture": "x86_64", + "os": { + "name": "windows 10.0", + "full": "Windows 10", + "version": "10.0", + "variant": "Windows Pro" } } }, - "fields" : { - "host.id.keyword" : [ - "f35ec6c1-6562-45b1-818f-2f14c0854adf" - ] + "fields": { + "host.id.keyword": ["f35ec6c1-6562-45b1-818f-2f14c0854adf"] }, - "sort" : [ - 1579816615336 - ], - "inner_hits" : { - "most_recent" : { - "hits" : { - "total" : { - "value" : 2, - "relation" : "eq" + "sort": [1579816615336], + "inner_hits": { + "most_recent": { + "hits": { + "total": { + "value": 2, + "relation": "eq" }, - "max_score" : null, - "hits" : [ + "max_score": null, + "hits": [ { - "_index" : "endpoint-agent", - "_id" : "W6Vo1G8BYQH1gtPUgYkC", - "_score" : null, - "_source" : { - "@timestamp" : 1579816615336, - "event" : { - "created" : "2020-01-23T21:56:55.336Z" + "_index": "metadata-endpoint-default-1", + "_id": "W6Vo1G8BYQH1gtPUgYkC", + "_score": null, + "_source": { + "@timestamp": 1579816615336, + "event": { + "created": "2020-01-23T21:56:55.336Z" }, "elastic": { "agent": { "id": "c2d84d8f-d355-40de-8b54-5d318d4d1312" } }, - "endpoint" : { - "policy" : { - "id" : "C2A9093E-E289-4C0A-AA44-8C32A414FA7A" + "endpoint": { + "policy": { + "id": "C2A9093E-E289-4C0A-AA44-8C32A414FA7A" } }, - "agent" : { - "version" : "6.4.3", - "id" : "c2d84d8f-d355-40de-8b54-5d318d4d1312", + "agent": { + "version": "6.4.3", + "id": "c2d84d8f-d355-40de-8b54-5d318d4d1312", "name": "Elastic Endpoint" }, - "host" : { - "id" : "f35ec6c1-6562-45b1-818f-2f14c0854adf", - "hostname" : "hildebrandt-6.example.com", - "ip" : "10.53.92.84", - "mac" : "af-f1-8f-51-25-2a", - "architecture" : "x86_64", - "os" : { - "name" : "windows 10.0", - "full" : "Windows 10", - "version" : "10.0", - "variant" : "Windows Pro" + "host": { + "id": "f35ec6c1-6562-45b1-818f-2f14c0854adf", + "hostname": "hildebrandt-6.example.com", + "ip": "10.53.92.84", + "mac": "af-f1-8f-51-25-2a", + "architecture": "x86_64", + "os": { + "name": "windows 10.0", + "full": "Windows 10", + "version": "10.0", + "variant": "Windows Pro" } } }, - "sort" : [ - 1579816615336 - ] + "sort": [1579816615336] } ] } @@ -220,9 +208,9 @@ } ] }, - "aggregations" : { - "total" : { - "value" : 2 + "aggregations": { + "total": { + "value": 2 } } } diff --git a/x-pack/plugins/endpoint/server/types.ts b/x-pack/plugins/endpoint/server/types.ts index 6dc128bd3d61e..46a23060339f4 100644 --- a/x-pack/plugins/endpoint/server/types.ts +++ b/x-pack/plugins/endpoint/server/types.ts @@ -5,11 +5,13 @@ */ import { LoggerFactory } from 'kibana/server'; import { EndpointConfigType } from './config'; +import { IndexPatternRetriever } from './index_pattern'; /** * The context for Endpoint apps. */ export interface EndpointAppContext { + indexPatternRetriever: IndexPatternRetriever; logFactory: LoggerFactory; config(): Promise<EndpointConfigType>; } diff --git a/x-pack/plugins/es_ui_shared/console_lang/ace/modes/index.ts b/x-pack/plugins/es_ui_shared/console_lang/ace/modes/index.ts deleted file mode 100644 index ae3c9962ecadb..0000000000000 --- a/x-pack/plugins/es_ui_shared/console_lang/ace/modes/index.ts +++ /dev/null @@ -1,7 +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. - */ - -export { installXJsonMode, XJsonMode } from './x_json'; diff --git a/x-pack/plugins/es_ui_shared/console_lang/ace/modes/x_json/index.ts b/x-pack/plugins/es_ui_shared/console_lang/ace/modes/x_json/index.ts deleted file mode 100644 index ae3c9962ecadb..0000000000000 --- a/x-pack/plugins/es_ui_shared/console_lang/ace/modes/x_json/index.ts +++ /dev/null @@ -1,7 +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. - */ - -export { installXJsonMode, XJsonMode } from './x_json'; diff --git a/x-pack/plugins/es_ui_shared/console_lang/ace/modes/x_json/worker/index.ts b/x-pack/plugins/es_ui_shared/console_lang/ace/modes/x_json/worker/index.ts deleted file mode 100644 index 0e40fd335dd31..0000000000000 --- a/x-pack/plugins/es_ui_shared/console_lang/ace/modes/x_json/worker/index.ts +++ /dev/null @@ -1,13 +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. - */ - -// @ts-ignore -import src from '!!raw-loader!./worker.js'; - -export const workerModule = { - id: 'ace/mode/json_worker', - src, -}; diff --git a/x-pack/plugins/es_ui_shared/console_lang/ace/modes/x_json/worker/worker.d.ts b/x-pack/plugins/es_ui_shared/console_lang/ace/modes/x_json/worker/worker.d.ts deleted file mode 100644 index 4ebad4f2ef91c..0000000000000 --- a/x-pack/plugins/es_ui_shared/console_lang/ace/modes/x_json/worker/worker.d.ts +++ /dev/null @@ -1,12 +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. - */ - -// Satisfy TS's requirements that the module be declared per './index.ts'. -declare module '!!raw-loader!./worker.js' { - const content: string; - // eslint-disable-next-line import/no-default-export - export default content; -} diff --git a/x-pack/plugins/es_ui_shared/console_lang/ace/modes/x_json/x_json.ts b/x-pack/plugins/es_ui_shared/console_lang/ace/modes/x_json/x_json.ts deleted file mode 100644 index bfeca045bea02..0000000000000 --- a/x-pack/plugins/es_ui_shared/console_lang/ace/modes/x_json/x_json.ts +++ /dev/null @@ -1,34 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import ace from 'brace'; -import { XJsonMode } from '../../../../../../../src/plugins/es_ui_shared/console_lang'; -import { workerModule } from './worker'; -const { WorkerClient } = ace.acequire('ace/worker/worker_client'); - -// Then clobber `createWorker` method to install our worker source. Per ace's wiki: https://github.com/ajaxorg/ace/wiki/Syntax-validation -(XJsonMode.prototype as any).createWorker = function(session: ace.IEditSession) { - const xJsonWorker = new WorkerClient(['ace'], workerModule, 'JsonWorker'); - - xJsonWorker.attachToDocument(session.getDocument()); - - xJsonWorker.on('annotate', function(e: { data: any }) { - session.setAnnotations(e.data); - }); - - xJsonWorker.on('terminate', function() { - session.clearAnnotations(); - }); - - return xJsonWorker; -}; - -export { XJsonMode }; - -export function installXJsonMode(editor: ace.Editor) { - const session = editor.getSession(); - session.setMode(new (XJsonMode as any)()); -} diff --git a/x-pack/plugins/es_ui_shared/console_lang/index.ts b/x-pack/plugins/es_ui_shared/console_lang/index.ts deleted file mode 100644 index b5fe3a554e34d..0000000000000 --- a/x-pack/plugins/es_ui_shared/console_lang/index.ts +++ /dev/null @@ -1,7 +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. - */ - -export { XJsonMode, installXJsonMode } from './ace/modes'; diff --git a/x-pack/plugins/es_ui_shared/console_lang/mocks.ts b/x-pack/plugins/es_ui_shared/console_lang/mocks.ts deleted file mode 100644 index 68480282ddc03..0000000000000 --- a/x-pack/plugins/es_ui_shared/console_lang/mocks.ts +++ /dev/null @@ -1,9 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -jest.mock('./ace/modes/x_json/worker', () => ({ - workerModule: { id: 'ace/mode/json_worker', src: '' }, -})); diff --git a/x-pack/plugins/event_log/README.md b/x-pack/plugins/event_log/README.md index 027bbc694801f..38364033cb70b 100644 --- a/x-pack/plugins/event_log/README.md +++ b/x-pack/plugins/event_log/README.md @@ -125,7 +125,6 @@ Here's the event written to the event log index: "duration": 1000000 }, "kibana": { - "namespace": "default", "saved_objects": [ { "type": "action", diff --git a/x-pack/plugins/event_log/generated/mappings.json b/x-pack/plugins/event_log/generated/mappings.json index ab1b4096d17f2..9c1dff60f9727 100644 --- a/x-pack/plugins/event_log/generated/mappings.json +++ b/x-pack/plugins/event_log/generated/mappings.json @@ -72,10 +72,6 @@ "type": "keyword", "ignore_above": 1024 }, - "namespace": { - "type": "keyword", - "ignore_above": 1024 - }, "alerting": { "properties": { "instance_id": { @@ -86,7 +82,7 @@ }, "saved_objects": { "properties": { - "store": { + "namespace": { "type": "keyword", "ignore_above": 1024 }, diff --git a/x-pack/plugins/event_log/generated/schemas.ts b/x-pack/plugins/event_log/generated/schemas.ts index b731093b33b06..5e93f320c009f 100644 --- a/x-pack/plugins/event_log/generated/schemas.ts +++ b/x-pack/plugins/event_log/generated/schemas.ts @@ -56,7 +56,6 @@ export const EventSchema = schema.maybe( kibana: schema.maybe( schema.object({ server_uuid: ecsString(), - namespace: ecsString(), alerting: schema.maybe( schema.object({ instance_id: ecsString(), @@ -65,7 +64,7 @@ export const EventSchema = schema.maybe( saved_objects: schema.maybe( schema.arrayOf( schema.object({ - store: ecsString(), + namespace: ecsString(), id: ecsString(), type: ecsString(), }) diff --git a/x-pack/plugins/event_log/scripts/mappings.js b/x-pack/plugins/event_log/scripts/mappings.js index 9e721b06ec335..de3c9d631fbca 100644 --- a/x-pack/plugins/event_log/scripts/mappings.js +++ b/x-pack/plugins/event_log/scripts/mappings.js @@ -20,17 +20,12 @@ exports.EcsKibanaExtensionsMappings = { }, }, }, - // relevant kibana space - namespace: { - type: 'keyword', - ignore_above: 1024, - }, // array of saved object references, for "linking" via search saved_objects: { type: 'nested', properties: { - // 'kibana' for typical saved object, 'task_manager' for TM, etc - store: { + // relevant kibana space + namespace: { type: 'keyword', ignore_above: 1024, }, @@ -61,9 +56,8 @@ exports.EcsEventLogProperties = [ 'error.message', 'user.name', 'kibana.server_uuid', - 'kibana.namespace', 'kibana.alerting.instance_id', - 'kibana.saved_objects.store', + 'kibana.saved_objects.namespace', 'kibana.saved_objects.id', 'kibana.saved_objects.name', 'kibana.saved_objects.type', diff --git a/x-pack/plugins/event_log/server/es/cluster_client_adapter.test.ts b/x-pack/plugins/event_log/server/es/cluster_client_adapter.test.ts index ae26d7a7ece07..986486902c3fa 100644 --- a/x-pack/plugins/event_log/server/es/cluster_client_adapter.test.ts +++ b/x-pack/plugins/event_log/server/es/cluster_client_adapter.test.ts @@ -21,7 +21,7 @@ beforeEach(() => { clusterClient = elasticsearchServiceMock.createClusterClient(); clusterClientAdapter = new ClusterClientAdapter({ logger, - clusterClient, + clusterClientPromise: Promise.resolve(clusterClient), }); }); diff --git a/x-pack/plugins/event_log/server/es/cluster_client_adapter.ts b/x-pack/plugins/event_log/server/es/cluster_client_adapter.ts index 36bc94edfca4e..409bb2d00e161 100644 --- a/x-pack/plugins/event_log/server/es/cluster_client_adapter.ts +++ b/x-pack/plugins/event_log/server/es/cluster_client_adapter.ts @@ -14,7 +14,7 @@ export type IClusterClientAdapter = PublicMethodsOf<ClusterClientAdapter>; export interface ConstructorOpts { logger: Logger; - clusterClient: EsClusterClient; + clusterClientPromise: Promise<EsClusterClient>; } export interface QueryEventsBySavedObjectResult { @@ -26,11 +26,11 @@ export interface QueryEventsBySavedObjectResult { export class ClusterClientAdapter { private readonly logger: Logger; - private readonly clusterClient: EsClusterClient; + private readonly clusterClientPromise: Promise<EsClusterClient>; constructor(opts: ConstructorOpts) { this.logger = opts.logger; - this.clusterClient = opts.clusterClient; + this.clusterClientPromise = opts.clusterClientPromise; } public async indexDocument(doc: any): Promise<void> { @@ -201,7 +201,8 @@ export class ClusterClientAdapter { private async callEs(operation: string, body?: any): Promise<any> { try { this.debug(`callEs(${operation}) calls:`, body); - const result = await this.clusterClient.callAsInternalUser(operation, body); + const clusterClient = await this.clusterClientPromise; + const result = await clusterClient.callAsInternalUser(operation, body); this.debug(`callEs(${operation}) result:`, result); return result; } catch (err) { diff --git a/x-pack/plugins/event_log/server/es/context.mock.ts b/x-pack/plugins/event_log/server/es/context.mock.ts index 6581cd689e43d..c15fee803fb71 100644 --- a/x-pack/plugins/event_log/server/es/context.mock.ts +++ b/x-pack/plugins/event_log/server/es/context.mock.ts @@ -19,6 +19,7 @@ const createContextMock = () => { initialize: jest.fn(), waitTillReady: jest.fn(), esAdapter: clusterClientAdapterMock.create(), + initialized: true, }; return mock; }; diff --git a/x-pack/plugins/event_log/server/es/context.test.ts b/x-pack/plugins/event_log/server/es/context.test.ts new file mode 100644 index 0000000000000..09fe676a5762e --- /dev/null +++ b/x-pack/plugins/event_log/server/es/context.test.ts @@ -0,0 +1,95 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { createEsContext } from './context'; +import { ClusterClient, Logger } from '../../../../../src/core/server'; +import { elasticsearchServiceMock, loggingServiceMock } from '../../../../../src/core/server/mocks'; +jest.mock('../lib/../../../../package.json', () => ({ + version: '1.2.3', +})); +type EsClusterClient = Pick<jest.Mocked<ClusterClient>, 'callAsInternalUser' | 'asScoped'>; + +let logger: Logger; +let clusterClient: EsClusterClient; + +beforeEach(() => { + logger = loggingServiceMock.createLogger(); + clusterClient = elasticsearchServiceMock.createClusterClient(); +}); + +describe('createEsContext', () => { + test('should return is ready state as falsy if not initialized', () => { + const context = createEsContext({ + logger, + clusterClientPromise: Promise.resolve(clusterClient), + indexNameRoot: 'test0', + }); + + expect(context.initialized).toBeFalsy(); + + context.initialize(); + expect(context.initialized).toBeTruthy(); + }); + + test('should return esNames', () => { + const context = createEsContext({ + logger, + clusterClientPromise: Promise.resolve(clusterClient), + indexNameRoot: 'test-index', + }); + + const esNames = context.esNames; + expect(esNames).toStrictEqual({ + base: 'test-index', + alias: 'test-index-event-log-1.2.3', + ilmPolicy: 'test-index-event-log-policy', + indexPattern: 'test-index-event-log-*', + indexPatternWithVersion: 'test-index-event-log-1.2.3-*', + indexTemplate: 'test-index-event-log-1.2.3-template', + initialIndex: 'test-index-event-log-1.2.3-000001', + }); + }); + + test('should return exist false for esAdapter ilm policy, index template and alias before initialize', async () => { + const context = createEsContext({ + logger, + clusterClientPromise: Promise.resolve(clusterClient), + indexNameRoot: 'test1', + }); + clusterClient.callAsInternalUser.mockResolvedValue(false); + + const doesAliasExist = await context.esAdapter.doesAliasExist(context.esNames.alias); + expect(doesAliasExist).toBeFalsy(); + + const doesIndexTemplateExist = await context.esAdapter.doesIndexTemplateExist( + context.esNames.indexTemplate + ); + expect(doesIndexTemplateExist).toBeFalsy(); + }); + + test('should return exist true for esAdapter ilm policy, index template and alias after initialize', async () => { + const context = createEsContext({ + logger, + clusterClientPromise: Promise.resolve(clusterClient), + indexNameRoot: 'test2', + }); + clusterClient.callAsInternalUser.mockResolvedValue(true); + context.initialize(); + + const doesIlmPolicyExist = await context.esAdapter.doesIlmPolicyExist( + context.esNames.ilmPolicy + ); + expect(doesIlmPolicyExist).toBeTruthy(); + + const doesAliasExist = await context.esAdapter.doesAliasExist(context.esNames.alias); + expect(doesAliasExist).toBeTruthy(); + + const doesIndexTemplateExist = await context.esAdapter.doesIndexTemplateExist( + context.esNames.indexTemplate + ); + expect(doesIndexTemplateExist).toBeTruthy(); + }); +}); diff --git a/x-pack/plugins/event_log/server/es/context.ts b/x-pack/plugins/event_log/server/es/context.ts index 144f44ac8e5ea..0b3f22c6eecc0 100644 --- a/x-pack/plugins/event_log/server/es/context.ts +++ b/x-pack/plugins/event_log/server/es/context.ts @@ -19,6 +19,7 @@ export interface EsContext { esAdapter: IClusterClientAdapter; initialize(): void; waitTillReady(): Promise<boolean>; + initialized: boolean; } export interface EsError { @@ -32,7 +33,7 @@ export function createEsContext(params: EsContextCtorParams): EsContext { export interface EsContextCtorParams { logger: Logger; - clusterClient: EsClusterClient; + clusterClientPromise: Promise<EsClusterClient>; indexNameRoot: string; } @@ -41,7 +42,7 @@ class EsContextImpl implements EsContext { public readonly esNames: EsNames; public esAdapter: IClusterClientAdapter; private readonly readySignal: ReadySignal<boolean>; - private initialized: boolean; + public initialized: boolean; constructor(params: EsContextCtorParams) { this.logger = params.logger; @@ -50,7 +51,7 @@ class EsContextImpl implements EsContext { this.initialized = false; this.esAdapter = new ClusterClientAdapter({ logger: params.logger, - clusterClient: params.clusterClient, + clusterClientPromise: params.clusterClientPromise, }); } diff --git a/x-pack/plugins/event_log/server/plugin.ts b/x-pack/plugins/event_log/server/plugin.ts index 2cc41354b4fbc..e5034f599f118 100644 --- a/x-pack/plugins/event_log/server/plugin.ts +++ b/x-pack/plugins/event_log/server/plugin.ts @@ -66,7 +66,9 @@ export class Plugin implements CorePlugin<IEventLogService, IEventLogClientServi logger: this.systemLogger, // TODO: get index prefix from config.get(kibana.index) indexNameRoot: kibanaIndex, - clusterClient: core.elasticsearch.adminClient, + clusterClientPromise: core + .getStartServices() + .then(([{ elasticsearch }]) => elasticsearch.legacy.client), }); this.eventLogService = new EventLogService({ diff --git a/x-pack/plugins/event_log/server/routes/_mock_handler_arguments.ts b/x-pack/plugins/event_log/server/routes/_mock_handler_arguments.ts index 6640683bf6005..19933649277aa 100644 --- a/x-pack/plugins/event_log/server/routes/_mock_handler_arguments.ts +++ b/x-pack/plugins/event_log/server/routes/_mock_handler_arguments.ts @@ -50,9 +50,9 @@ export function fakeEvent(overrides = {}) { duration: 1000000, }, kibana: { - namespace: 'default', saved_objects: [ { + namespace: 'default', type: 'action', id: '968f1b82-0414-4a10-becc-56b6473e4a29', }, diff --git a/x-pack/plugins/features/kibana.json b/x-pack/plugins/features/kibana.json index 325e5a0407493..6e51f3b650710 100644 --- a/x-pack/plugins/features/kibana.json +++ b/x-pack/plugins/features/kibana.json @@ -2,7 +2,7 @@ "id": "features", "version": "8.0.0", "kibanaVersion": "kibana", - "optionalPlugins": ["timelion"], + "optionalPlugins": ["visTypeTimelion"], "configPath": ["xpack", "features"], "server": true, "ui": true diff --git a/x-pack/plugins/features/server/plugin.ts b/x-pack/plugins/features/server/plugin.ts index cebf67243fb28..83cc9e1eb7cc8 100644 --- a/x-pack/plugins/features/server/plugin.ts +++ b/x-pack/plugins/features/server/plugin.ts @@ -13,7 +13,7 @@ import { import { Capabilities as UICapabilities } from '../../../../src/core/server'; import { deepFreeze } from '../../../../src/core/utils'; import { XPackInfo } from '../../../legacy/plugins/xpack_main/server/lib/xpack_info'; -import { PluginSetupContract as TimelionSetupContract } from '../../../../src/plugins/timelion/server'; +import { PluginSetupContract as TimelionSetupContract } from '../../../../src/plugins/vis_type_timelion/server'; import { FeatureRegistry } from './feature_registry'; import { Feature, FeatureConfig } from '../common/feature'; import { uiCapabilitiesForFeatures } from './ui_capabilities_for_features'; @@ -65,7 +65,7 @@ export class Plugin { public async setup( core: CoreSetup, - { timelion }: { timelion?: TimelionSetupContract } + { visTypeTimelion }: { visTypeTimelion?: TimelionSetupContract } ): Promise<RecursiveReadonly<PluginSetupContract>> { defineRoutes({ router: core.http.createRouter(), @@ -84,7 +84,7 @@ export class Plugin { // Register OSS features. for (const feature of buildOSSFeatures({ savedObjectTypes: this.legacyAPI.savedObjectTypes, - includeTimelion: timelion !== undefined && timelion.uiEnabled, + includeTimelion: visTypeTimelion !== undefined && visTypeTimelion.uiEnabled, })) { this.featureRegistry.register(feature); } diff --git a/x-pack/plugins/features/server/ui_capabilities_for_features.test.ts b/x-pack/plugins/features/server/ui_capabilities_for_features.test.ts index 73c399878b17b..35dcc4cf42b37 100644 --- a/x-pack/plugins/features/server/ui_capabilities_for_features.test.ts +++ b/x-pack/plugins/features/server/ui_capabilities_for_features.test.ts @@ -135,6 +135,42 @@ describe('populateUICapabilities', () => { }); }); + it(`supports capabilities from reserved privileges`, () => { + expect( + uiCapabilitiesForFeatures([ + new Feature({ + id: 'newFeature', + name: 'my new feature', + navLinkId: 'newFeatureNavLink', + app: ['bar-app'], + privileges: null, + reserved: { + description: '', + privileges: [ + { + id: 'rp_1', + privilege: createFeaturePrivilege(['capability1', 'capability2']), + }, + { + id: 'rp_2', + privilege: createFeaturePrivilege(['capability3', 'capability4', 'capability5']), + }, + ], + }, + }), + ]) + ).toEqual({ + catalogue: {}, + newFeature: { + capability1: true, + capability2: true, + capability3: true, + capability4: true, + capability5: true, + }, + }); + }); + it(`supports merging features with sub privileges`, () => { expect( uiCapabilitiesForFeatures([ diff --git a/x-pack/plugins/features/server/ui_capabilities_for_features.ts b/x-pack/plugins/features/server/ui_capabilities_for_features.ts index d3d3230822749..e6ff3ad4383d2 100644 --- a/x-pack/plugins/features/server/ui_capabilities_for_features.ts +++ b/x-pack/plugins/features/server/ui_capabilities_for_features.ts @@ -45,6 +45,9 @@ function getCapabilitiesFromFeature(feature: Feature): FeatureCapabilities { ...feature.subFeatures.map(sf => sf.privilegeGroups.map(pg => pg.privileges)).flat(2) ); } + if (feature.reserved?.privileges) { + featurePrivileges.push(...feature.reserved.privileges.map(rp => rp.privilege)); + } featurePrivileges.forEach(privilege => { UIFeatureCapabilities[feature.id] = { diff --git a/x-pack/plugins/file_upload/kibana.json b/x-pack/plugins/file_upload/kibana.json index 3fda32fb6ebe5..7676a01d0b0f9 100644 --- a/x-pack/plugins/file_upload/kibana.json +++ b/x-pack/plugins/file_upload/kibana.json @@ -1,8 +1,7 @@ { - "id": "file_upload", + "id": "fileUpload", "version": "8.0.0", "kibanaVersion": "kibana", - "configPath": ["xpack", "file_upload"], "server": true, "ui": true, "requiredPlugins": ["data", "usageCollection"] diff --git a/x-pack/plugins/file_upload/server/telemetry/mappings.ts b/x-pack/plugins/file_upload/server/telemetry/mappings.ts index ca935fea3449a..97a5ed9eeef82 100644 --- a/x-pack/plugins/file_upload/server/telemetry/mappings.ts +++ b/x-pack/plugins/file_upload/server/telemetry/mappings.ts @@ -10,7 +10,7 @@ import { TELEMETRY_DOC_ID } from './telemetry'; export const fileUploadTelemetryMappingsType: SavedObjectsType = { name: TELEMETRY_DOC_ID, hidden: false, - namespaceAgnostic: true, + namespaceType: 'agnostic', mappings: { properties: { filesUploadedTotalCount: { diff --git a/x-pack/legacy/plugins/index_lifecycle_management/__jest__/__snapshots__/extend_index_management.test.js.snap b/x-pack/plugins/index_lifecycle_management/__jest__/__snapshots__/extend_index_management.test.js.snap similarity index 100% rename from x-pack/legacy/plugins/index_lifecycle_management/__jest__/__snapshots__/extend_index_management.test.js.snap rename to x-pack/plugins/index_lifecycle_management/__jest__/__snapshots__/extend_index_management.test.js.snap diff --git a/x-pack/legacy/plugins/index_lifecycle_management/__jest__/components/__snapshots__/policy_table.test.js.snap b/x-pack/plugins/index_lifecycle_management/__jest__/components/__snapshots__/policy_table.test.js.snap similarity index 100% rename from x-pack/legacy/plugins/index_lifecycle_management/__jest__/components/__snapshots__/policy_table.test.js.snap rename to x-pack/plugins/index_lifecycle_management/__jest__/components/__snapshots__/policy_table.test.js.snap diff --git a/x-pack/legacy/plugins/index_lifecycle_management/__jest__/components/edit_policy.test.js b/x-pack/plugins/index_lifecycle_management/__jest__/components/edit_policy.test.js similarity index 96% rename from x-pack/legacy/plugins/index_lifecycle_management/__jest__/components/edit_policy.test.js rename to x-pack/plugins/index_lifecycle_management/__jest__/components/edit_policy.test.js index 9d143c4d3fc8e..bf4de823f1833 100644 --- a/x-pack/legacy/plugins/index_lifecycle_management/__jest__/components/edit_policy.test.js +++ b/x-pack/plugins/index_lifecycle_management/__jest__/components/edit_policy.test.js @@ -13,13 +13,13 @@ import axiosXhrAdapter from 'axios/lib/adapters/xhr'; import sinon from 'sinon'; import { findTestSubject } from '@elastic/eui/lib/test'; -import { mountWithIntl } from '../../../../../test_utils/enzyme_helpers'; -import { fetchedPolicies, fetchedNodes } from '../../public/np_ready/application/store/actions'; -import { indexLifecycleManagementStore } from '../../public/np_ready/application/store'; -import { EditPolicy } from '../../public/np_ready/application/sections/edit_policy'; -import { init as initHttp } from '../../public/np_ready/application/services/http'; -import { init as initUiMetric } from '../../public/np_ready/application/services/ui_metric'; -import { init as initNotification } from '../../public/np_ready/application/services/notification'; +import { mountWithIntl } from '../../../../test_utils/enzyme_helpers'; +import { fetchedPolicies, fetchedNodes } from '../../public/application/store/actions'; +import { indexLifecycleManagementStore } from '../../public/application/store'; +import { EditPolicy } from '../../public/application/sections/edit_policy'; +import { init as initHttp } from '../../public/application/services/http'; +import { init as initUiMetric } from '../../public/application/services/ui_metric'; +import { init as initNotification } from '../../public/application/services/notification'; import { positiveNumbersAboveZeroErrorMessage, positiveNumberRequiredMessage, @@ -33,16 +33,14 @@ import { policyNameMustBeDifferentErrorMessage, policyNameAlreadyUsedErrorMessage, maximumDocumentsRequiredMessage, -} from '../../public/np_ready/application/store/selectors/lifecycle'; +} from '../../public/application/store/selectors/lifecycle'; initHttp(axios.create({ adapter: axiosXhrAdapter }), path => path); -initUiMetric(() => () => {}); +initUiMetric({ reportUiStats: () => {} }); initNotification({ addDanger: () => {}, }); -jest.mock('ui/new_platform'); - let server; let store; const policy = { diff --git a/x-pack/legacy/plugins/index_lifecycle_management/__jest__/components/policy_table.test.js b/x-pack/plugins/index_lifecycle_management/__jest__/components/policy_table.test.js similarity index 91% rename from x-pack/legacy/plugins/index_lifecycle_management/__jest__/components/policy_table.test.js rename to x-pack/plugins/index_lifecycle_management/__jest__/components/policy_table.test.js index a3a9c5e59bfa4..78c5c181eea62 100644 --- a/x-pack/legacy/plugins/index_lifecycle_management/__jest__/components/policy_table.test.js +++ b/x-pack/plugins/index_lifecycle_management/__jest__/components/policy_table.test.js @@ -12,17 +12,15 @@ import axiosXhrAdapter from 'axios/lib/adapters/xhr'; import sinon from 'sinon'; import { findTestSubject, takeMountedSnapshot } from '@elastic/eui/lib/test'; -import { mountWithIntl } from '../../../../../test_utils/enzyme_helpers'; -import { fetchedPolicies } from '../../public/np_ready/application/store/actions'; -import { indexLifecycleManagementStore } from '../../public/np_ready/application/store'; -import { PolicyTable } from '../../public/np_ready/application/sections/policy_table'; -import { init as initHttp } from '../../public/np_ready/application/services/http'; -import { init as initUiMetric } from '../../public/np_ready/application/services/ui_metric'; +import { mountWithIntl } from '../../../../test_utils/enzyme_helpers'; +import { fetchedPolicies } from '../../public/application/store/actions'; +import { indexLifecycleManagementStore } from '../../public/application/store'; +import { PolicyTable } from '../../public/application/sections/policy_table'; +import { init as initHttp } from '../../public/application/services/http'; +import { init as initUiMetric } from '../../public/application/services/ui_metric'; initHttp(axios.create({ adapter: axiosXhrAdapter }), path => path); -initUiMetric(() => () => {}); - -jest.mock('ui/new_platform'); +initUiMetric({ reportUiStats: () => {} }); let server = null; diff --git a/x-pack/legacy/plugins/index_lifecycle_management/__jest__/extend_index_management.test.js b/x-pack/plugins/index_lifecycle_management/__jest__/extend_index_management.test.js similarity index 93% rename from x-pack/legacy/plugins/index_lifecycle_management/__jest__/extend_index_management.test.js rename to x-pack/plugins/index_lifecycle_management/__jest__/extend_index_management.test.js index d2619778617c3..900de27ca36ab 100644 --- a/x-pack/legacy/plugins/index_lifecycle_management/__jest__/extend_index_management.test.js +++ b/x-pack/plugins/index_lifecycle_management/__jest__/extend_index_management.test.js @@ -8,7 +8,7 @@ import moment from 'moment-timezone'; import axios from 'axios'; import axiosXhrAdapter from 'axios/lib/adapters/xhr'; -import { mountWithIntl } from '../../../../test_utils/enzyme_helpers'; +import { mountWithIntl } from '../../../test_utils/enzyme_helpers'; import { retryLifecycleActionExtension, removeLifecyclePolicyActionExtension, @@ -16,21 +16,18 @@ import { ilmBannerExtension, ilmFilterExtension, ilmSummaryExtension, -} from '../public/np_ready/extend_index_management'; -import { init as initHttp } from '../public/np_ready/application/services/http'; -import { init as initUiMetric } from '../public/np_ready/application/services/ui_metric'; +} from '../public/extend_index_management'; +import { init as initHttp } from '../public/application/services/http'; +import { init as initUiMetric } from '../public/application/services/ui_metric'; // We need to init the http with a mock for any tests that depend upon the http service. // For example, add_lifecycle_confirm_modal makes an API request in its componentDidMount // lifecycle method. If we don't mock this, CI will fail with "Call retries were exceeded". initHttp(axios.create({ adapter: axiosXhrAdapter }), path => path); -initUiMetric(() => () => {}); +initUiMetric({ reportUiStats: () => {} }); -jest.mock('ui/new_platform'); -jest.mock('../../../../plugins/index_management/public', async () => { - const { indexManagementMock } = await import( - '../../../../plugins/index_management/public/mocks.ts' - ); +jest.mock('../../../plugins/index_management/public', async () => { + const { indexManagementMock } = await import('../../../plugins/index_management/public/mocks.ts'); return indexManagementMock.createSetup(); }); diff --git a/x-pack/plugins/index_lifecycle_management/common/constants/index.ts b/x-pack/plugins/index_lifecycle_management/common/constants/index.ts new file mode 100644 index 0000000000000..700039985eaf5 --- /dev/null +++ b/x-pack/plugins/index_lifecycle_management/common/constants/index.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 { i18n } from '@kbn/i18n'; +import { LicenseType } from '../../../licensing/common/types'; + +const basicLicense: LicenseType = 'basic'; + +export const PLUGIN = { + ID: 'index_lifecycle_management', + minimumLicenseType: basicLicense, + TITLE: i18n.translate('xpack.indexLifecycleMgmt.appTitle', { + defaultMessage: 'Index Lifecycle Policies', + }), +}; + +export const BASE_PATH = '/management/elasticsearch/index_lifecycle_management/'; + +export const API_BASE_PATH = '/api/index_lifecycle_management'; diff --git a/x-pack/plugins/index_lifecycle_management/kibana.json b/x-pack/plugins/index_lifecycle_management/kibana.json new file mode 100644 index 0000000000000..6385646b95789 --- /dev/null +++ b/x-pack/plugins/index_lifecycle_management/kibana.json @@ -0,0 +1,16 @@ +{ + "id": "indexLifecycleManagement", + "version": "kibana", + "server": true, + "ui": true, + "requiredPlugins": [ + "home", + "licensing", + "management" + ], + "optionalPlugins": [ + "usageCollection", + "indexManagement" + ], + "configPath": ["xpack", "ilm"] +} diff --git a/x-pack/plugins/index_lifecycle_management/public/application/app.tsx b/x-pack/plugins/index_lifecycle_management/public/application/app.tsx new file mode 100644 index 0000000000000..993dced20bbe6 --- /dev/null +++ b/x-pack/plugins/index_lifecycle_management/public/application/app.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, { useEffect } from 'react'; +import { HashRouter, Switch, Route, Redirect } from 'react-router-dom'; +import { METRIC_TYPE } from '@kbn/analytics'; + +import { BASE_PATH } from '../../common/constants'; +import { UIM_APP_LOAD } from './constants'; +import { EditPolicy } from './sections/edit_policy'; +import { PolicyTable } from './sections/policy_table'; +import { trackUiMetric } from './services/ui_metric'; + +export const App = () => { + useEffect(() => trackUiMetric(METRIC_TYPE.LOADED, UIM_APP_LOAD), []); + + return ( + <HashRouter> + <Switch> + <Redirect exact from={`${BASE_PATH}`} to={`${BASE_PATH}policies`} /> + <Route exact path={`${BASE_PATH}policies`} component={PolicyTable} /> + <Route path={`${BASE_PATH}policies/edit/:policyName?`} component={EditPolicy} /> + </Switch> + </HashRouter> + ); +}; diff --git a/x-pack/legacy/plugins/index_lifecycle_management/public/np_ready/application/constants/index.ts b/x-pack/plugins/index_lifecycle_management/public/application/constants/index.ts similarity index 100% rename from x-pack/legacy/plugins/index_lifecycle_management/public/np_ready/application/constants/index.ts rename to x-pack/plugins/index_lifecycle_management/public/application/constants/index.ts diff --git a/x-pack/legacy/plugins/index_lifecycle_management/public/np_ready/application/constants/ui_metric.ts b/x-pack/plugins/index_lifecycle_management/public/application/constants/ui_metric.ts similarity index 100% rename from x-pack/legacy/plugins/index_lifecycle_management/public/np_ready/application/constants/ui_metric.ts rename to x-pack/plugins/index_lifecycle_management/public/application/constants/ui_metric.ts diff --git a/x-pack/plugins/index_lifecycle_management/public/application/index.tsx b/x-pack/plugins/index_lifecycle_management/public/application/index.tsx new file mode 100644 index 0000000000000..a7d88d31e58fc --- /dev/null +++ b/x-pack/plugins/index_lifecycle_management/public/application/index.tsx @@ -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 React from 'react'; +import { render, unmountComponentAtNode } from 'react-dom'; +import { Provider } from 'react-redux'; +import { I18nStart } from 'kibana/public'; +import { UnmountCallback } from 'src/core/public'; + +import { App } from './app'; +import { indexLifecycleManagementStore } from './store'; + +export const renderApp = (element: Element, I18nContext: I18nStart['Context']): UnmountCallback => { + render( + <I18nContext> + <Provider store={indexLifecycleManagementStore()}> + <App /> + </Provider> + </I18nContext>, + element + ); + + return () => unmountComponentAtNode(element); +}; diff --git a/x-pack/legacy/plugins/index_lifecycle_management/public/np_ready/application/sections/components/active_badge.js b/x-pack/plugins/index_lifecycle_management/public/application/sections/components/active_badge.js similarity index 100% rename from x-pack/legacy/plugins/index_lifecycle_management/public/np_ready/application/sections/components/active_badge.js rename to x-pack/plugins/index_lifecycle_management/public/application/sections/components/active_badge.js diff --git a/x-pack/legacy/plugins/index_lifecycle_management/public/np_ready/application/sections/components/index.js b/x-pack/plugins/index_lifecycle_management/public/application/sections/components/index.js similarity index 100% rename from x-pack/legacy/plugins/index_lifecycle_management/public/np_ready/application/sections/components/index.js rename to x-pack/plugins/index_lifecycle_management/public/application/sections/components/index.js diff --git a/x-pack/legacy/plugins/index_lifecycle_management/public/np_ready/application/sections/components/learn_more_link.js b/x-pack/plugins/index_lifecycle_management/public/application/sections/components/learn_more_link.js similarity index 100% rename from x-pack/legacy/plugins/index_lifecycle_management/public/np_ready/application/sections/components/learn_more_link.js rename to x-pack/plugins/index_lifecycle_management/public/application/sections/components/learn_more_link.js diff --git a/x-pack/legacy/plugins/index_lifecycle_management/public/np_ready/application/sections/components/optional_label.js b/x-pack/plugins/index_lifecycle_management/public/application/sections/components/optional_label.js similarity index 100% rename from x-pack/legacy/plugins/index_lifecycle_management/public/np_ready/application/sections/components/optional_label.js rename to x-pack/plugins/index_lifecycle_management/public/application/sections/components/optional_label.js diff --git a/x-pack/legacy/plugins/index_lifecycle_management/public/np_ready/application/sections/components/phase_error_message.js b/x-pack/plugins/index_lifecycle_management/public/application/sections/components/phase_error_message.js similarity index 100% rename from x-pack/legacy/plugins/index_lifecycle_management/public/np_ready/application/sections/components/phase_error_message.js rename to x-pack/plugins/index_lifecycle_management/public/application/sections/components/phase_error_message.js diff --git a/x-pack/legacy/plugins/index_lifecycle_management/public/np_ready/application/sections/edit_policy/components/cold_phase/cold_phase.container.js b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/cold_phase/cold_phase.container.js similarity index 100% rename from x-pack/legacy/plugins/index_lifecycle_management/public/np_ready/application/sections/edit_policy/components/cold_phase/cold_phase.container.js rename to x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/cold_phase/cold_phase.container.js diff --git a/x-pack/legacy/plugins/index_lifecycle_management/public/np_ready/application/sections/edit_policy/components/cold_phase/cold_phase.js b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/cold_phase/cold_phase.js similarity index 100% rename from x-pack/legacy/plugins/index_lifecycle_management/public/np_ready/application/sections/edit_policy/components/cold_phase/cold_phase.js rename to x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/cold_phase/cold_phase.js diff --git a/x-pack/legacy/plugins/index_lifecycle_management/public/np_ready/application/sections/edit_policy/components/cold_phase/index.js b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/cold_phase/index.js similarity index 100% rename from x-pack/legacy/plugins/index_lifecycle_management/public/np_ready/application/sections/edit_policy/components/cold_phase/index.js rename to x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/cold_phase/index.js diff --git a/x-pack/legacy/plugins/index_lifecycle_management/public/np_ready/application/sections/edit_policy/components/delete_phase/delete_phase.container.js b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/delete_phase/delete_phase.container.js similarity index 100% rename from x-pack/legacy/plugins/index_lifecycle_management/public/np_ready/application/sections/edit_policy/components/delete_phase/delete_phase.container.js rename to x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/delete_phase/delete_phase.container.js diff --git a/x-pack/legacy/plugins/index_lifecycle_management/public/np_ready/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 similarity index 100% rename from x-pack/legacy/plugins/index_lifecycle_management/public/np_ready/application/sections/edit_policy/components/delete_phase/delete_phase.js rename to x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/delete_phase/delete_phase.js diff --git a/x-pack/legacy/plugins/index_lifecycle_management/public/np_ready/application/sections/edit_policy/components/delete_phase/index.js b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/delete_phase/index.js similarity index 100% rename from x-pack/legacy/plugins/index_lifecycle_management/public/np_ready/application/sections/edit_policy/components/delete_phase/index.js rename to x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/delete_phase/index.js diff --git a/x-pack/legacy/plugins/index_lifecycle_management/public/np_ready/application/sections/edit_policy/components/hot_phase/hot_phase.container.js b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/hot_phase/hot_phase.container.js similarity index 100% rename from x-pack/legacy/plugins/index_lifecycle_management/public/np_ready/application/sections/edit_policy/components/hot_phase/hot_phase.container.js rename to x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/hot_phase/hot_phase.container.js diff --git a/x-pack/legacy/plugins/index_lifecycle_management/public/np_ready/application/sections/edit_policy/components/hot_phase/hot_phase.js b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/hot_phase/hot_phase.js similarity index 100% rename from x-pack/legacy/plugins/index_lifecycle_management/public/np_ready/application/sections/edit_policy/components/hot_phase/hot_phase.js rename to x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/hot_phase/hot_phase.js diff --git a/x-pack/legacy/plugins/index_lifecycle_management/public/np_ready/application/sections/edit_policy/components/hot_phase/index.js b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/hot_phase/index.js similarity index 100% rename from x-pack/legacy/plugins/index_lifecycle_management/public/np_ready/application/sections/edit_policy/components/hot_phase/index.js rename to x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/hot_phase/index.js diff --git a/x-pack/legacy/plugins/index_lifecycle_management/public/np_ready/application/sections/edit_policy/components/min_age_input.js b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/min_age_input.js similarity index 100% rename from x-pack/legacy/plugins/index_lifecycle_management/public/np_ready/application/sections/edit_policy/components/min_age_input.js rename to x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/min_age_input.js diff --git a/x-pack/legacy/plugins/index_lifecycle_management/public/np_ready/application/sections/edit_policy/components/node_allocation/index.js b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/node_allocation/index.js similarity index 100% rename from x-pack/legacy/plugins/index_lifecycle_management/public/np_ready/application/sections/edit_policy/components/node_allocation/index.js rename to x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/node_allocation/index.js diff --git a/x-pack/legacy/plugins/index_lifecycle_management/public/np_ready/application/sections/edit_policy/components/node_allocation/node_allocation.container.js b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/node_allocation/node_allocation.container.js similarity index 100% rename from x-pack/legacy/plugins/index_lifecycle_management/public/np_ready/application/sections/edit_policy/components/node_allocation/node_allocation.container.js rename to x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/node_allocation/node_allocation.container.js diff --git a/x-pack/legacy/plugins/index_lifecycle_management/public/np_ready/application/sections/edit_policy/components/node_allocation/node_allocation.js b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/node_allocation/node_allocation.js similarity index 100% rename from x-pack/legacy/plugins/index_lifecycle_management/public/np_ready/application/sections/edit_policy/components/node_allocation/node_allocation.js rename to x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/node_allocation/node_allocation.js diff --git a/x-pack/legacy/plugins/index_lifecycle_management/public/np_ready/application/sections/edit_policy/components/node_attrs_details/index.js b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/node_attrs_details/index.js similarity index 100% rename from x-pack/legacy/plugins/index_lifecycle_management/public/np_ready/application/sections/edit_policy/components/node_attrs_details/index.js rename to x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/node_attrs_details/index.js diff --git a/x-pack/legacy/plugins/index_lifecycle_management/public/np_ready/application/sections/edit_policy/components/node_attrs_details/node_attrs_details.container.js b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/node_attrs_details/node_attrs_details.container.js similarity index 100% rename from x-pack/legacy/plugins/index_lifecycle_management/public/np_ready/application/sections/edit_policy/components/node_attrs_details/node_attrs_details.container.js rename to x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/node_attrs_details/node_attrs_details.container.js diff --git a/x-pack/legacy/plugins/index_lifecycle_management/public/np_ready/application/sections/edit_policy/components/node_attrs_details/node_attrs_details.js b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/node_attrs_details/node_attrs_details.js similarity index 100% rename from x-pack/legacy/plugins/index_lifecycle_management/public/np_ready/application/sections/edit_policy/components/node_attrs_details/node_attrs_details.js rename to x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/node_attrs_details/node_attrs_details.js diff --git a/x-pack/legacy/plugins/index_lifecycle_management/public/np_ready/application/sections/edit_policy/components/policy_json_flyout.js b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/policy_json_flyout.js similarity index 100% rename from x-pack/legacy/plugins/index_lifecycle_management/public/np_ready/application/sections/edit_policy/components/policy_json_flyout.js rename to x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/policy_json_flyout.js diff --git a/x-pack/legacy/plugins/index_lifecycle_management/public/np_ready/application/sections/edit_policy/components/set_priority_input.js b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/set_priority_input.js similarity index 100% rename from x-pack/legacy/plugins/index_lifecycle_management/public/np_ready/application/sections/edit_policy/components/set_priority_input.js rename to x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/set_priority_input.js diff --git a/x-pack/legacy/plugins/index_lifecycle_management/public/np_ready/application/sections/edit_policy/components/warm_phase/index.js b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/warm_phase/index.js similarity index 100% rename from x-pack/legacy/plugins/index_lifecycle_management/public/np_ready/application/sections/edit_policy/components/warm_phase/index.js rename to x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/warm_phase/index.js diff --git a/x-pack/legacy/plugins/index_lifecycle_management/public/np_ready/application/sections/edit_policy/components/warm_phase/warm_phase.container.js b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/warm_phase/warm_phase.container.js similarity index 100% rename from x-pack/legacy/plugins/index_lifecycle_management/public/np_ready/application/sections/edit_policy/components/warm_phase/warm_phase.container.js rename to x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/warm_phase/warm_phase.container.js diff --git a/x-pack/legacy/plugins/index_lifecycle_management/public/np_ready/application/sections/edit_policy/components/warm_phase/warm_phase.js b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/warm_phase/warm_phase.js similarity index 100% rename from x-pack/legacy/plugins/index_lifecycle_management/public/np_ready/application/sections/edit_policy/components/warm_phase/warm_phase.js rename to x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/warm_phase/warm_phase.js diff --git a/x-pack/legacy/plugins/index_lifecycle_management/public/np_ready/application/sections/edit_policy/edit_policy.container.js b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/edit_policy.container.js similarity index 100% rename from x-pack/legacy/plugins/index_lifecycle_management/public/np_ready/application/sections/edit_policy/edit_policy.container.js rename to x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/edit_policy.container.js diff --git a/x-pack/legacy/plugins/index_lifecycle_management/public/np_ready/application/sections/edit_policy/edit_policy.js b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/edit_policy.js similarity index 100% rename from x-pack/legacy/plugins/index_lifecycle_management/public/np_ready/application/sections/edit_policy/edit_policy.js rename to x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/edit_policy.js diff --git a/x-pack/legacy/plugins/index_lifecycle_management/public/np_ready/application/sections/edit_policy/form_errors.js b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form_errors.js similarity index 100% rename from x-pack/legacy/plugins/index_lifecycle_management/public/np_ready/application/sections/edit_policy/form_errors.js rename to x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form_errors.js diff --git a/x-pack/legacy/plugins/index_lifecycle_management/public/np_ready/application/sections/edit_policy/index.d.ts b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/index.d.ts similarity index 100% rename from x-pack/legacy/plugins/index_lifecycle_management/public/np_ready/application/sections/edit_policy/index.d.ts rename to x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/index.d.ts diff --git a/x-pack/legacy/plugins/index_lifecycle_management/public/np_ready/application/sections/edit_policy/index.js b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/index.js similarity index 100% rename from x-pack/legacy/plugins/index_lifecycle_management/public/np_ready/application/sections/edit_policy/index.js rename to x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/index.js diff --git a/x-pack/legacy/plugins/index_lifecycle_management/public/np_ready/application/sections/policy_table/components/no_match/components/no_match/index.js b/x-pack/plugins/index_lifecycle_management/public/application/sections/policy_table/components/no_match/components/no_match/index.js similarity index 100% rename from x-pack/legacy/plugins/index_lifecycle_management/public/np_ready/application/sections/policy_table/components/no_match/components/no_match/index.js rename to x-pack/plugins/index_lifecycle_management/public/application/sections/policy_table/components/no_match/components/no_match/index.js diff --git a/x-pack/legacy/plugins/index_lifecycle_management/public/np_ready/application/sections/policy_table/components/no_match/components/no_match/no_match.js b/x-pack/plugins/index_lifecycle_management/public/application/sections/policy_table/components/no_match/components/no_match/no_match.js similarity index 100% rename from x-pack/legacy/plugins/index_lifecycle_management/public/np_ready/application/sections/policy_table/components/no_match/components/no_match/no_match.js rename to x-pack/plugins/index_lifecycle_management/public/application/sections/policy_table/components/no_match/components/no_match/no_match.js diff --git a/x-pack/legacy/plugins/index_lifecycle_management/public/np_ready/application/sections/policy_table/components/no_match/index.js b/x-pack/plugins/index_lifecycle_management/public/application/sections/policy_table/components/no_match/index.js similarity index 100% rename from x-pack/legacy/plugins/index_lifecycle_management/public/np_ready/application/sections/policy_table/components/no_match/index.js rename to x-pack/plugins/index_lifecycle_management/public/application/sections/policy_table/components/no_match/index.js diff --git a/x-pack/legacy/plugins/index_lifecycle_management/public/np_ready/application/sections/policy_table/components/policy_table/add_policy_to_template_confirm_modal.js b/x-pack/plugins/index_lifecycle_management/public/application/sections/policy_table/components/policy_table/add_policy_to_template_confirm_modal.js similarity index 100% rename from x-pack/legacy/plugins/index_lifecycle_management/public/np_ready/application/sections/policy_table/components/policy_table/add_policy_to_template_confirm_modal.js rename to x-pack/plugins/index_lifecycle_management/public/application/sections/policy_table/components/policy_table/add_policy_to_template_confirm_modal.js diff --git a/x-pack/legacy/plugins/index_lifecycle_management/public/np_ready/application/sections/policy_table/components/policy_table/confirm_delete.js b/x-pack/plugins/index_lifecycle_management/public/application/sections/policy_table/components/policy_table/confirm_delete.js similarity index 100% rename from x-pack/legacy/plugins/index_lifecycle_management/public/np_ready/application/sections/policy_table/components/policy_table/confirm_delete.js rename to x-pack/plugins/index_lifecycle_management/public/application/sections/policy_table/components/policy_table/confirm_delete.js diff --git a/x-pack/legacy/plugins/index_lifecycle_management/public/np_ready/application/sections/policy_table/components/policy_table/index.js b/x-pack/plugins/index_lifecycle_management/public/application/sections/policy_table/components/policy_table/index.js similarity index 100% rename from x-pack/legacy/plugins/index_lifecycle_management/public/np_ready/application/sections/policy_table/components/policy_table/index.js rename to x-pack/plugins/index_lifecycle_management/public/application/sections/policy_table/components/policy_table/index.js diff --git a/x-pack/legacy/plugins/index_lifecycle_management/public/np_ready/application/sections/policy_table/components/policy_table/policy_table.container.js b/x-pack/plugins/index_lifecycle_management/public/application/sections/policy_table/components/policy_table/policy_table.container.js similarity index 100% rename from x-pack/legacy/plugins/index_lifecycle_management/public/np_ready/application/sections/policy_table/components/policy_table/policy_table.container.js rename to x-pack/plugins/index_lifecycle_management/public/application/sections/policy_table/components/policy_table/policy_table.container.js diff --git a/x-pack/legacy/plugins/index_lifecycle_management/public/np_ready/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 similarity index 98% rename from x-pack/legacy/plugins/index_lifecycle_management/public/np_ready/application/sections/policy_table/components/policy_table/policy_table.js rename to x-pack/plugins/index_lifecycle_management/public/application/sections/policy_table/components/policy_table/policy_table.js index 903161fe094fc..d406d86bc6ce7 100644 --- a/x-pack/legacy/plugins/index_lifecycle_management/public/np_ready/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 @@ -37,8 +37,8 @@ import { import { RIGHT_ALIGNMENT } from '@elastic/eui/lib/services'; -import { getIndexListUri } from '../../../../../../../../../../plugins/index_management/public'; -import { BASE_PATH } from '../../../../../../../common/constants'; +import { getIndexListUri } from '../../../../../../../index_management/public'; +import { BASE_PATH } from '../../../../../../common/constants'; import { UIM_EDIT_CLICK } from '../../../../constants'; import { getPolicyPath } from '../../../../services/navigation'; import { flattenPanelTree } from '../../../../services/flatten_panel_tree'; @@ -52,6 +52,7 @@ const COLUMNS = { label: i18n.translate('xpack.indexLifecycleMgmt.policyTable.headers.nameHeader', { defaultMessage: 'Name', }), + width: 200, }, linkedIndices: { label: i18n.translate('xpack.indexLifecycleMgmt.policyTable.headers.linkedIndicesHeader', { @@ -179,7 +180,6 @@ export class PolicyTable extends Component { return ( /* eslint-disable-next-line @elastic/eui/href-or-on-click */ <EuiLink - className="policyTable__link" data-test-subj="policyTablePolicyNameLink" href={getPolicyPath(value)} onClick={() => trackUiMetric('click', UIM_EDIT_CLICK)} @@ -415,7 +415,7 @@ export class PolicyTable extends Component { tableContent = <EuiLoadingSpinner size="m" />; } else if (totalNumberOfPolicies > 0) { tableContent = ( - <EuiTable className="policyTable__horizontalScroll"> + <EuiTable> <EuiScreenReaderOnly> <caption role="status" aria-relevant="text" aria-live="polite"> <FormattedMessage diff --git a/x-pack/legacy/plugins/index_lifecycle_management/public/np_ready/application/sections/policy_table/index.d.ts b/x-pack/plugins/index_lifecycle_management/public/application/sections/policy_table/index.d.ts similarity index 100% rename from x-pack/legacy/plugins/index_lifecycle_management/public/np_ready/application/sections/policy_table/index.d.ts rename to x-pack/plugins/index_lifecycle_management/public/application/sections/policy_table/index.d.ts diff --git a/x-pack/legacy/plugins/index_lifecycle_management/public/np_ready/application/sections/policy_table/index.js b/x-pack/plugins/index_lifecycle_management/public/application/sections/policy_table/index.js similarity index 100% rename from x-pack/legacy/plugins/index_lifecycle_management/public/np_ready/application/sections/policy_table/index.js rename to x-pack/plugins/index_lifecycle_management/public/application/sections/policy_table/index.js 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.js new file mode 100644 index 0000000000000..1cb2089ab66db --- /dev/null +++ b/x-pack/plugins/index_lifecycle_management/public/application/services/api.js @@ -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 { + UIM_POLICY_DELETE, + UIM_POLICY_ATTACH_INDEX, + UIM_POLICY_ATTACH_INDEX_TEMPLATE, + UIM_POLICY_DETACH_INDEX, + UIM_INDEX_RETRY_STEP, +} from '../constants'; + +import { trackUiMetric } from './ui_metric'; +import { sendGet, sendPost, sendDelete } from './http'; + +export async function loadNodes() { + return await sendGet(`nodes/list`); +} + +export async function loadNodeDetails(selectedNodeAttrs) { + return await sendGet(`nodes/${selectedNodeAttrs}/details`); +} + +export async function loadIndexTemplates() { + return await sendGet(`templates`); +} + +export async function loadPolicies(withIndices) { + const query = withIndices ? '?withIndices=true' : ''; + return await sendGet('policies', query); +} + +export async function savePolicy(policy) { + return await sendPost(`policies`, policy); +} + +export async function deletePolicy(policyName) { + const response = await sendDelete(`policies/${encodeURIComponent(policyName)}`); + // Only track successful actions. + trackUiMetric('count', UIM_POLICY_DELETE); + return response; +} + +export const retryLifecycleForIndex = async indexNames => { + const response = await sendPost(`index/retry`, { indexNames }); + // Only track successful actions. + trackUiMetric('count', UIM_INDEX_RETRY_STEP); + return response; +}; + +export const removeLifecycleForIndex = async indexNames => { + const response = await sendPost(`index/remove`, { indexNames }); + // Only track successful actions. + trackUiMetric('count', UIM_POLICY_DETACH_INDEX); + return response; +}; + +export const addLifecyclePolicyToIndex = async body => { + const response = await sendPost(`index/add`, body); + // Only track successful actions. + trackUiMetric('count', UIM_POLICY_ATTACH_INDEX); + return response; +}; + +export const addLifecyclePolicyToTemplate = async body => { + const response = await sendPost(`template`, body); + // Only track successful actions. + trackUiMetric('count', UIM_POLICY_ATTACH_INDEX_TEMPLATE); + return response; +}; diff --git a/x-pack/legacy/plugins/index_lifecycle_management/public/np_ready/application/services/api_errors.js b/x-pack/plugins/index_lifecycle_management/public/application/services/api_errors.js similarity index 100% rename from x-pack/legacy/plugins/index_lifecycle_management/public/np_ready/application/services/api_errors.js rename to x-pack/plugins/index_lifecycle_management/public/application/services/api_errors.js diff --git a/x-pack/legacy/plugins/index_lifecycle_management/public/np_ready/application/services/documentation.ts b/x-pack/plugins/index_lifecycle_management/public/application/services/documentation.ts similarity index 100% rename from x-pack/legacy/plugins/index_lifecycle_management/public/np_ready/application/services/documentation.ts rename to x-pack/plugins/index_lifecycle_management/public/application/services/documentation.ts diff --git a/x-pack/legacy/plugins/index_lifecycle_management/public/np_ready/application/services/filter_items.js b/x-pack/plugins/index_lifecycle_management/public/application/services/filter_items.js similarity index 100% rename from x-pack/legacy/plugins/index_lifecycle_management/public/np_ready/application/services/filter_items.js rename to x-pack/plugins/index_lifecycle_management/public/application/services/filter_items.js diff --git a/x-pack/legacy/plugins/index_lifecycle_management/public/np_ready/application/services/find_errors.js b/x-pack/plugins/index_lifecycle_management/public/application/services/find_errors.js similarity index 100% rename from x-pack/legacy/plugins/index_lifecycle_management/public/np_ready/application/services/find_errors.js rename to x-pack/plugins/index_lifecycle_management/public/application/services/find_errors.js diff --git a/x-pack/legacy/plugins/index_lifecycle_management/public/np_ready/application/services/flatten_panel_tree.js b/x-pack/plugins/index_lifecycle_management/public/application/services/flatten_panel_tree.js similarity index 100% rename from x-pack/legacy/plugins/index_lifecycle_management/public/np_ready/application/services/flatten_panel_tree.js rename to x-pack/plugins/index_lifecycle_management/public/application/services/flatten_panel_tree.js 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 new file mode 100644 index 0000000000000..47e96ea28bb8c --- /dev/null +++ b/x-pack/plugins/index_lifecycle_management/public/application/services/http.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. + */ + +let _httpClient: any; + +export function init(httpClient: any): void { + _httpClient = httpClient; +} + +function getFullPath(path: string): string { + const apiPrefix = '/api/index_lifecycle_management'; + + if (path) { + return `${apiPrefix}/${path}`; + } + + return apiPrefix; +} + +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 { + return _httpClient.get(getFullPath(path), { query }); +} + +export function sendDelete(path: string): any { + return _httpClient.delete(getFullPath(path)); +} diff --git a/x-pack/legacy/plugins/index_lifecycle_management/public/np_ready/application/services/index.js b/x-pack/plugins/index_lifecycle_management/public/application/services/index.js similarity index 100% rename from x-pack/legacy/plugins/index_lifecycle_management/public/np_ready/application/services/index.js rename to x-pack/plugins/index_lifecycle_management/public/application/services/index.js diff --git a/x-pack/plugins/index_lifecycle_management/public/application/services/navigation.ts b/x-pack/plugins/index_lifecycle_management/public/application/services/navigation.ts new file mode 100644 index 0000000000000..2d518ebb3015e --- /dev/null +++ b/x-pack/plugins/index_lifecycle_management/public/application/services/navigation.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 { BASE_PATH } from '../../../common/constants'; + +export const goToPolicyList = () => { + window.location.hash = `${BASE_PATH}policies`; +}; + +export const getPolicyPath = (policyName: string): string => { + return encodeURI(`#${BASE_PATH}policies/edit/${encodeURIComponent(policyName)}`); +}; diff --git a/x-pack/legacy/plugins/index_lifecycle_management/public/np_ready/application/services/notification.ts b/x-pack/plugins/index_lifecycle_management/public/application/services/notification.ts similarity index 100% rename from x-pack/legacy/plugins/index_lifecycle_management/public/np_ready/application/services/notification.ts rename to x-pack/plugins/index_lifecycle_management/public/application/services/notification.ts diff --git a/x-pack/legacy/plugins/index_lifecycle_management/public/np_ready/application/services/sort_table.js b/x-pack/plugins/index_lifecycle_management/public/application/services/sort_table.js similarity index 100% rename from x-pack/legacy/plugins/index_lifecycle_management/public/np_ready/application/services/sort_table.js rename to x-pack/plugins/index_lifecycle_management/public/application/services/sort_table.js diff --git a/x-pack/legacy/plugins/index_lifecycle_management/public/np_ready/application/services/ui_metric.test.js b/x-pack/plugins/index_lifecycle_management/public/application/services/ui_metric.test.js similarity index 100% rename from x-pack/legacy/plugins/index_lifecycle_management/public/np_ready/application/services/ui_metric.test.js rename to x-pack/plugins/index_lifecycle_management/public/application/services/ui_metric.test.js diff --git a/x-pack/legacy/plugins/index_lifecycle_management/public/np_ready/application/services/ui_metric.ts b/x-pack/plugins/index_lifecycle_management/public/application/services/ui_metric.ts similarity index 84% rename from x-pack/legacy/plugins/index_lifecycle_management/public/np_ready/application/services/ui_metric.ts rename to x-pack/plugins/index_lifecycle_management/public/application/services/ui_metric.ts index d9f2c26048317..ca987441c7ce9 100644 --- a/x-pack/legacy/plugins/index_lifecycle_management/public/np_ready/application/services/ui_metric.ts +++ b/x-pack/plugins/index_lifecycle_management/public/application/services/ui_metric.ts @@ -6,7 +6,8 @@ import { get } from 'lodash'; -import { createUiStatsReporter } from '../../../../../../../../src/legacy/core_plugins/ui_metric/public'; +import { UsageCollectionSetup } from 'src/plugins/usage_collection/public'; +import { UiStatsMetricType } from '@kbn/analytics'; import { UIM_APP_NAME, @@ -22,11 +23,11 @@ import { import { defaultColdPhase, defaultWarmPhase, defaultHotPhase } from '../store/defaults'; -export let trackUiMetric: ReturnType<typeof createUiStatsReporter>; +export let trackUiMetric = (metricType: UiStatsMetricType, eventName: string) => {}; -export function init(getReporter: typeof createUiStatsReporter): void { - if (getReporter) { - trackUiMetric = getReporter(UIM_APP_NAME); +export function init(usageCollection?: UsageCollectionSetup): void { + if (usageCollection) { + trackUiMetric = usageCollection.reportUiStats.bind(usageCollection, UIM_APP_NAME); } } diff --git a/x-pack/legacy/plugins/index_lifecycle_management/public/np_ready/application/store/actions/general.js b/x-pack/plugins/index_lifecycle_management/public/application/store/actions/general.js similarity index 100% rename from x-pack/legacy/plugins/index_lifecycle_management/public/np_ready/application/store/actions/general.js rename to x-pack/plugins/index_lifecycle_management/public/application/store/actions/general.js diff --git a/x-pack/legacy/plugins/index_lifecycle_management/public/np_ready/application/store/actions/index.js b/x-pack/plugins/index_lifecycle_management/public/application/store/actions/index.js similarity index 100% rename from x-pack/legacy/plugins/index_lifecycle_management/public/np_ready/application/store/actions/index.js rename to x-pack/plugins/index_lifecycle_management/public/application/store/actions/index.js diff --git a/x-pack/legacy/plugins/index_lifecycle_management/public/np_ready/application/store/actions/lifecycle.js b/x-pack/plugins/index_lifecycle_management/public/application/store/actions/lifecycle.js similarity index 100% rename from x-pack/legacy/plugins/index_lifecycle_management/public/np_ready/application/store/actions/lifecycle.js rename to x-pack/plugins/index_lifecycle_management/public/application/store/actions/lifecycle.js diff --git a/x-pack/legacy/plugins/index_lifecycle_management/public/np_ready/application/store/actions/nodes.js b/x-pack/plugins/index_lifecycle_management/public/application/store/actions/nodes.js similarity index 100% rename from x-pack/legacy/plugins/index_lifecycle_management/public/np_ready/application/store/actions/nodes.js rename to x-pack/plugins/index_lifecycle_management/public/application/store/actions/nodes.js diff --git a/x-pack/legacy/plugins/index_lifecycle_management/public/np_ready/application/store/actions/policies.js b/x-pack/plugins/index_lifecycle_management/public/application/store/actions/policies.js similarity index 100% rename from x-pack/legacy/plugins/index_lifecycle_management/public/np_ready/application/store/actions/policies.js rename to x-pack/plugins/index_lifecycle_management/public/application/store/actions/policies.js diff --git a/x-pack/legacy/plugins/index_lifecycle_management/public/np_ready/application/store/defaults/cold_phase.js b/x-pack/plugins/index_lifecycle_management/public/application/store/defaults/cold_phase.js similarity index 100% rename from x-pack/legacy/plugins/index_lifecycle_management/public/np_ready/application/store/defaults/cold_phase.js rename to x-pack/plugins/index_lifecycle_management/public/application/store/defaults/cold_phase.js diff --git a/x-pack/legacy/plugins/index_lifecycle_management/public/np_ready/application/store/defaults/delete_phase.js b/x-pack/plugins/index_lifecycle_management/public/application/store/defaults/delete_phase.js similarity index 100% rename from x-pack/legacy/plugins/index_lifecycle_management/public/np_ready/application/store/defaults/delete_phase.js rename to x-pack/plugins/index_lifecycle_management/public/application/store/defaults/delete_phase.js diff --git a/x-pack/legacy/plugins/index_lifecycle_management/public/np_ready/application/store/defaults/hot_phase.js b/x-pack/plugins/index_lifecycle_management/public/application/store/defaults/hot_phase.js similarity index 100% rename from x-pack/legacy/plugins/index_lifecycle_management/public/np_ready/application/store/defaults/hot_phase.js rename to x-pack/plugins/index_lifecycle_management/public/application/store/defaults/hot_phase.js diff --git a/x-pack/legacy/plugins/index_lifecycle_management/public/np_ready/application/store/defaults/index.d.ts b/x-pack/plugins/index_lifecycle_management/public/application/store/defaults/index.d.ts similarity index 100% rename from x-pack/legacy/plugins/index_lifecycle_management/public/np_ready/application/store/defaults/index.d.ts rename to x-pack/plugins/index_lifecycle_management/public/application/store/defaults/index.d.ts diff --git a/x-pack/legacy/plugins/index_lifecycle_management/public/np_ready/application/store/defaults/index.js b/x-pack/plugins/index_lifecycle_management/public/application/store/defaults/index.js similarity index 100% rename from x-pack/legacy/plugins/index_lifecycle_management/public/np_ready/application/store/defaults/index.js rename to x-pack/plugins/index_lifecycle_management/public/application/store/defaults/index.js diff --git a/x-pack/legacy/plugins/index_lifecycle_management/public/np_ready/application/store/defaults/warm_phase.js b/x-pack/plugins/index_lifecycle_management/public/application/store/defaults/warm_phase.js similarity index 100% rename from x-pack/legacy/plugins/index_lifecycle_management/public/np_ready/application/store/defaults/warm_phase.js rename to x-pack/plugins/index_lifecycle_management/public/application/store/defaults/warm_phase.js diff --git a/x-pack/legacy/plugins/index_lifecycle_management/public/np_ready/application/store/index.d.ts b/x-pack/plugins/index_lifecycle_management/public/application/store/index.d.ts similarity index 100% rename from x-pack/legacy/plugins/index_lifecycle_management/public/np_ready/application/store/index.d.ts rename to x-pack/plugins/index_lifecycle_management/public/application/store/index.d.ts diff --git a/x-pack/legacy/plugins/index_lifecycle_management/public/np_ready/application/store/index.js b/x-pack/plugins/index_lifecycle_management/public/application/store/index.js similarity index 100% rename from x-pack/legacy/plugins/index_lifecycle_management/public/np_ready/application/store/index.js rename to x-pack/plugins/index_lifecycle_management/public/application/store/index.js diff --git a/x-pack/legacy/plugins/index_lifecycle_management/public/np_ready/application/store/reducers/general.js b/x-pack/plugins/index_lifecycle_management/public/application/store/reducers/general.js similarity index 100% rename from x-pack/legacy/plugins/index_lifecycle_management/public/np_ready/application/store/reducers/general.js rename to x-pack/plugins/index_lifecycle_management/public/application/store/reducers/general.js diff --git a/x-pack/legacy/plugins/index_lifecycle_management/public/np_ready/application/store/reducers/index.js b/x-pack/plugins/index_lifecycle_management/public/application/store/reducers/index.js similarity index 100% rename from x-pack/legacy/plugins/index_lifecycle_management/public/np_ready/application/store/reducers/index.js rename to x-pack/plugins/index_lifecycle_management/public/application/store/reducers/index.js diff --git a/x-pack/legacy/plugins/index_lifecycle_management/public/np_ready/application/store/reducers/nodes.js b/x-pack/plugins/index_lifecycle_management/public/application/store/reducers/nodes.js similarity index 100% rename from x-pack/legacy/plugins/index_lifecycle_management/public/np_ready/application/store/reducers/nodes.js rename to x-pack/plugins/index_lifecycle_management/public/application/store/reducers/nodes.js diff --git a/x-pack/legacy/plugins/index_lifecycle_management/public/np_ready/application/store/reducers/policies.js b/x-pack/plugins/index_lifecycle_management/public/application/store/reducers/policies.js similarity index 100% rename from x-pack/legacy/plugins/index_lifecycle_management/public/np_ready/application/store/reducers/policies.js rename to x-pack/plugins/index_lifecycle_management/public/application/store/reducers/policies.js diff --git a/x-pack/legacy/plugins/index_lifecycle_management/public/np_ready/application/store/selectors/general.js b/x-pack/plugins/index_lifecycle_management/public/application/store/selectors/general.js similarity index 100% rename from x-pack/legacy/plugins/index_lifecycle_management/public/np_ready/application/store/selectors/general.js rename to x-pack/plugins/index_lifecycle_management/public/application/store/selectors/general.js diff --git a/x-pack/legacy/plugins/index_lifecycle_management/public/np_ready/application/store/selectors/index.js b/x-pack/plugins/index_lifecycle_management/public/application/store/selectors/index.js similarity index 100% rename from x-pack/legacy/plugins/index_lifecycle_management/public/np_ready/application/store/selectors/index.js rename to x-pack/plugins/index_lifecycle_management/public/application/store/selectors/index.js diff --git a/x-pack/legacy/plugins/index_lifecycle_management/public/np_ready/application/store/selectors/lifecycle.js b/x-pack/plugins/index_lifecycle_management/public/application/store/selectors/lifecycle.js similarity index 100% rename from x-pack/legacy/plugins/index_lifecycle_management/public/np_ready/application/store/selectors/lifecycle.js rename to x-pack/plugins/index_lifecycle_management/public/application/store/selectors/lifecycle.js diff --git a/x-pack/legacy/plugins/index_lifecycle_management/public/np_ready/application/store/selectors/nodes.js b/x-pack/plugins/index_lifecycle_management/public/application/store/selectors/nodes.js similarity index 100% rename from x-pack/legacy/plugins/index_lifecycle_management/public/np_ready/application/store/selectors/nodes.js rename to x-pack/plugins/index_lifecycle_management/public/application/store/selectors/nodes.js diff --git a/x-pack/legacy/plugins/index_lifecycle_management/public/np_ready/application/store/selectors/policies.js b/x-pack/plugins/index_lifecycle_management/public/application/store/selectors/policies.js similarity index 100% rename from x-pack/legacy/plugins/index_lifecycle_management/public/np_ready/application/store/selectors/policies.js rename to x-pack/plugins/index_lifecycle_management/public/application/store/selectors/policies.js diff --git a/x-pack/legacy/plugins/index_lifecycle_management/public/np_ready/application/store/store.js b/x-pack/plugins/index_lifecycle_management/public/application/store/store.js similarity index 100% rename from x-pack/legacy/plugins/index_lifecycle_management/public/np_ready/application/store/store.js rename to x-pack/plugins/index_lifecycle_management/public/application/store/store.js diff --git a/x-pack/legacy/plugins/index_lifecycle_management/public/np_ready/extend_index_management/components/add_lifecycle_confirm_modal.js b/x-pack/plugins/index_lifecycle_management/public/extend_index_management/components/add_lifecycle_confirm_modal.js similarity index 97% rename from x-pack/legacy/plugins/index_lifecycle_management/public/np_ready/extend_index_management/components/add_lifecycle_confirm_modal.js rename to x-pack/plugins/index_lifecycle_management/public/extend_index_management/components/add_lifecycle_confirm_modal.js index 5b8f2d197daf4..143895150172d 100644 --- a/x-pack/legacy/plugins/index_lifecycle_management/public/np_ready/extend_index_management/components/add_lifecycle_confirm_modal.js +++ b/x-pack/plugins/index_lifecycle_management/public/extend_index_management/components/add_lifecycle_confirm_modal.js @@ -23,7 +23,7 @@ import { EuiModalHeaderTitle, } from '@elastic/eui'; -import { BASE_PATH } from '../../../../common/constants'; +import { BASE_PATH } from '../../../common/constants'; import { loadPolicies, addLifecyclePolicyToIndex } from '../../application/services/api'; import { showApiError } from '../../application/services/api_errors'; import { toasts } from '../../application/services/notification'; @@ -38,7 +38,7 @@ export class AddLifecyclePolicyConfirmModal extends Component { }; } addPolicy = async () => { - const { indexName, httpClient, closeModal, reloadIndices } = this.props; + const { indexName, closeModal, reloadIndices } = this.props; const { selectedPolicyName, selectedAlias } = this.state; if (!selectedPolicyName) { this.setState({ @@ -55,7 +55,7 @@ export class AddLifecyclePolicyConfirmModal extends Component { policyName: selectedPolicyName, alias: selectedAlias, }; - await addLifecyclePolicyToIndex(body, httpClient); + await addLifecyclePolicyToIndex(body); closeModal(); toasts.addSuccess( i18n.translate( diff --git a/x-pack/legacy/plugins/index_lifecycle_management/public/np_ready/extend_index_management/components/index_lifecycle_summary.js b/x-pack/plugins/index_lifecycle_management/public/extend_index_management/components/index_lifecycle_summary.js similarity index 100% rename from x-pack/legacy/plugins/index_lifecycle_management/public/np_ready/extend_index_management/components/index_lifecycle_summary.js rename to x-pack/plugins/index_lifecycle_management/public/extend_index_management/components/index_lifecycle_summary.js diff --git a/x-pack/legacy/plugins/index_lifecycle_management/public/np_ready/extend_index_management/components/remove_lifecycle_confirm_modal.js b/x-pack/plugins/index_lifecycle_management/public/extend_index_management/components/remove_lifecycle_confirm_modal.js similarity index 96% rename from x-pack/legacy/plugins/index_lifecycle_management/public/np_ready/extend_index_management/components/remove_lifecycle_confirm_modal.js rename to x-pack/plugins/index_lifecycle_management/public/extend_index_management/components/remove_lifecycle_confirm_modal.js index 0ba5ed1720084..4e0d2383c7d79 100644 --- a/x-pack/legacy/plugins/index_lifecycle_management/public/np_ready/extend_index_management/components/remove_lifecycle_confirm_modal.js +++ b/x-pack/plugins/index_lifecycle_management/public/extend_index_management/components/remove_lifecycle_confirm_modal.js @@ -24,10 +24,10 @@ export class RemoveLifecyclePolicyConfirmModal extends Component { } removePolicy = async () => { - const { indexNames, httpClient, closeModal, reloadIndices } = this.props; + const { indexNames, closeModal, reloadIndices } = this.props; try { - await removeLifecycleForIndex(indexNames, httpClient); + await removeLifecycleForIndex(indexNames); closeModal(); toasts.addSuccess( i18n.translate( diff --git a/x-pack/legacy/plugins/index_lifecycle_management/public/np_ready/extend_index_management/index.d.ts b/x-pack/plugins/index_lifecycle_management/public/extend_index_management/index.d.ts similarity index 100% rename from x-pack/legacy/plugins/index_lifecycle_management/public/np_ready/extend_index_management/index.d.ts rename to x-pack/plugins/index_lifecycle_management/public/extend_index_management/index.d.ts diff --git a/x-pack/plugins/index_lifecycle_management/public/extend_index_management/index.js b/x-pack/plugins/index_lifecycle_management/public/extend_index_management/index.js new file mode 100644 index 0000000000000..40ff04408002f --- /dev/null +++ b/x-pack/plugins/index_lifecycle_management/public/extend_index_management/index.js @@ -0,0 +1,204 @@ +/* + * 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 { get, every, any } from 'lodash'; +import { i18n } from '@kbn/i18n'; +import { EuiSearchBar } from '@elastic/eui'; + +import { retryLifecycleForIndex } from '../application/services/api'; +import { IndexLifecycleSummary } from './components/index_lifecycle_summary'; +import { AddLifecyclePolicyConfirmModal } from './components/add_lifecycle_confirm_modal'; +import { RemoveLifecyclePolicyConfirmModal } from './components/remove_lifecycle_confirm_modal'; + +const stepPath = 'ilm.step'; + +export const retryLifecycleActionExtension = ({ indices }) => { + const allHaveErrors = every(indices, index => { + return index.ilm && index.ilm.failed_step; + }); + if (!allHaveErrors) { + return null; + } + const indexNames = indices.map(({ name }) => name); + return { + requestMethod: retryLifecycleForIndex, + icon: 'play', + indexNames: [indexNames], + buttonLabel: i18n.translate('xpack.indexLifecycleMgmt.retryIndexLifecycleActionButtonLabel', { + defaultMessage: 'Retry lifecycle step', + }), + successMessage: i18n.translate( + 'xpack.indexLifecycleMgmt.retryIndexLifecycleAction.retriedLifecycleMessage', + { + defaultMessage: 'Called retry lifecycle step for: {indexNames}', + values: { indexNames: indexNames.map(indexName => `"${indexName}"`).join(', ') }, + } + ), + }; +}; + +export const removeLifecyclePolicyActionExtension = ({ indices, reloadIndices }) => { + const allHaveIlm = every(indices, index => { + return index.ilm && index.ilm.managed; + }); + if (!allHaveIlm) { + return null; + } + const indexNames = indices.map(({ name }) => name); + return { + renderConfirmModal: closeModal => { + return ( + <RemoveLifecyclePolicyConfirmModal + indexNames={indexNames} + closeModal={closeModal} + reloadIndices={reloadIndices} + /> + ); + }, + icon: 'stopFilled', + indexNames: [indexNames], + buttonLabel: i18n.translate('xpack.indexLifecycleMgmt.removeIndexLifecycleActionButtonLabel', { + defaultMessage: 'Remove lifecycle policy', + }), + }; +}; + +export const addLifecyclePolicyActionExtension = ({ indices, reloadIndices }) => { + if (indices.length !== 1) { + return null; + } + const index = indices[0]; + const hasIlm = index.ilm && index.ilm.managed; + + if (hasIlm) { + return null; + } + const indexName = index.name; + return { + renderConfirmModal: closeModal => { + return ( + <AddLifecyclePolicyConfirmModal + indexName={indexName} + closeModal={closeModal} + index={index} + reloadIndices={reloadIndices} + /> + ); + }, + icon: 'plusInCircle', + buttonLabel: i18n.translate('xpack.indexLifecycleMgmt.addLifecyclePolicyActionButtonLabel', { + defaultMessage: 'Add lifecycle policy', + }), + }; +}; + +export const ilmBannerExtension = indices => { + const { Query } = EuiSearchBar; + if (!indices.length) { + return null; + } + const indicesWithLifecycleErrors = indices.filter(index => { + return get(index, stepPath) === 'ERROR'; + }); + const numIndicesWithLifecycleErrors = indicesWithLifecycleErrors.length; + if (!numIndicesWithLifecycleErrors) { + return null; + } + return { + type: 'warning', + filter: Query.parse(`${stepPath}:ERROR`), + filterLabel: i18n.translate('xpack.indexLifecycleMgmt.indexMgmtBanner.filterLabel', { + defaultMessage: 'Show errors', + }), + title: i18n.translate('xpack.indexLifecycleMgmt.indexMgmtBanner.errorMessage', { + defaultMessage: `{ numIndicesWithLifecycleErrors, number} + {numIndicesWithLifecycleErrors, plural, one {index has} other {indices have} } + lifecycle errors`, + values: { numIndicesWithLifecycleErrors }, + }), + }; +}; + +export const ilmSummaryExtension = index => { + return <IndexLifecycleSummary index={index} />; +}; + +export const ilmFilterExtension = indices => { + const hasIlm = any(indices, index => index.ilm && index.ilm.managed); + if (!hasIlm) { + return []; + } else { + return [ + { + type: 'field_value_selection', + name: i18n.translate('xpack.indexLifecycleMgmt.indexMgmtFilter.lifecycleStatusLabel', { + defaultMessage: 'Lifecycle status', + }), + multiSelect: false, + field: 'ilm.managed', + options: [ + { + value: true, + view: i18n.translate('xpack.indexLifecycleMgmt.indexMgmtFilter.managedLabel', { + defaultMessage: 'Managed', + }), + }, + { + value: false, + view: i18n.translate('xpack.indexLifecycleMgmt.indexMgmtFilter.unmanagedLabel', { + defaultMessage: 'Unmanaged', + }), + }, + ], + }, + { + type: 'field_value_selection', + field: 'ilm.phase', + name: i18n.translate('xpack.indexLifecycleMgmt.indexMgmtFilter.lifecyclePhaseLabel', { + defaultMessage: 'Lifecycle phase', + }), + multiSelect: 'or', + options: [ + { + value: 'hot', + view: i18n.translate('xpack.indexLifecycleMgmt.indexMgmtFilter.hotLabel', { + defaultMessage: 'Hot', + }), + }, + { + value: 'warm', + view: i18n.translate('xpack.indexLifecycleMgmt.indexMgmtFilter.warmLabel', { + defaultMessage: 'Warm', + }), + }, + { + value: 'cold', + view: i18n.translate('xpack.indexLifecycleMgmt.indexMgmtFilter.coldLabel', { + defaultMessage: 'Cold', + }), + }, + { + value: 'delete', + view: i18n.translate('xpack.indexLifecycleMgmt.indexMgmtFilter.deleteLabel', { + defaultMessage: 'Delete', + }), + }, + ], + }, + ]; + } +}; + +export const addAllExtensions = extensionsService => { + extensionsService.addAction(retryLifecycleActionExtension); + extensionsService.addAction(removeLifecyclePolicyActionExtension); + extensionsService.addAction(addLifecyclePolicyActionExtension); + + extensionsService.addBanner(ilmBannerExtension); + extensionsService.addSummary(ilmSummaryExtension); + extensionsService.addFilter(ilmFilterExtension); +}; diff --git a/x-pack/plugins/index_lifecycle_management/public/index.ts b/x-pack/plugins/index_lifecycle_management/public/index.ts new file mode 100644 index 0000000000000..586763188a54b --- /dev/null +++ b/x-pack/plugins/index_lifecycle_management/public/index.ts @@ -0,0 +1,14 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { PluginInitializerContext } from 'kibana/public'; + +import { IndexLifecycleManagementPlugin } from './plugin'; + +/** @public */ +export const plugin = (initializerContext: PluginInitializerContext) => { + return new IndexLifecycleManagementPlugin(initializerContext); +}; diff --git a/x-pack/plugins/index_lifecycle_management/public/plugin.tsx b/x-pack/plugins/index_lifecycle_management/public/plugin.tsx new file mode 100644 index 0000000000000..ca93646e20fcf --- /dev/null +++ b/x-pack/plugins/index_lifecycle_management/public/plugin.tsx @@ -0,0 +1,69 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { CoreSetup, PluginInitializerContext } from 'src/core/public'; + +import { PLUGIN } from '../common/constants'; +import { init as initHttp } from './application/services/http'; +import { init as initDocumentation } from './application/services/documentation'; +import { init as initUiMetric } from './application/services/ui_metric'; +import { init as initNotification } from './application/services/notification'; +import { addAllExtensions } from './extend_index_management'; +import { PluginsDependencies, ClientConfigType } from './types'; + +export class IndexLifecycleManagementPlugin { + constructor(private readonly initializerContext: PluginInitializerContext) {} + + public setup(coreSetup: CoreSetup, plugins: PluginsDependencies) { + const { + ui: { enabled: isIndexLifecycleManagementUiEnabled }, + } = this.initializerContext.config.get<ClientConfigType>(); + + if (isIndexLifecycleManagementUiEnabled) { + const { + http, + notifications: { toasts }, + fatalErrors, + getStartServices, + } = coreSetup; + + const { usageCollection, management, indexManagement } = plugins; + + // Initialize services even if the app isn't mounted, because they're used by index management extensions. + initHttp(http); + initUiMetric(usageCollection); + initNotification(toasts, fatalErrors); + + management.sections.getSection('elasticsearch')!.registerApp({ + id: PLUGIN.ID, + title: PLUGIN.TITLE, + order: 2, + mount: async ({ element }) => { + const [coreStart] = await getStartServices(); + const { + i18n: { Context: I18nContext }, + docLinks: { ELASTIC_WEBSITE_URL, DOC_LINK_VERSION }, + } = coreStart; + + // Initialize additional services. + initDocumentation( + `${ELASTIC_WEBSITE_URL}guide/en/elasticsearch/reference/${DOC_LINK_VERSION}/` + ); + + const { renderApp } = await import('./application'); + return renderApp(element, I18nContext); + }, + }); + + if (indexManagement) { + addAllExtensions(indexManagement.extensionsService); + } + } + } + + public start() {} + public stop() {} +} diff --git a/x-pack/plugins/index_lifecycle_management/public/types.ts b/x-pack/plugins/index_lifecycle_management/public/types.ts new file mode 100644 index 0000000000000..178884a7ee679 --- /dev/null +++ b/x-pack/plugins/index_lifecycle_management/public/types.ts @@ -0,0 +1,21 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { UsageCollectionSetup } from '../../../../src/plugins/usage_collection/public'; +import { ManagementSetup } from '../../../../src/plugins/management/public'; +import { IndexManagementPluginSetup } from '../../index_management/public'; + +export interface PluginsDependencies { + usageCollection?: UsageCollectionSetup; + management: ManagementSetup; + indexManagement?: IndexManagementPluginSetup; +} + +export interface ClientConfigType { + ui: { + enabled: boolean; + }; +} diff --git a/x-pack/plugins/index_lifecycle_management/server/config.ts b/x-pack/plugins/index_lifecycle_management/server/config.ts new file mode 100644 index 0000000000000..9728e31a8a148 --- /dev/null +++ b/x-pack/plugins/index_lifecycle_management/server/config.ts @@ -0,0 +1,18 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { schema, TypeOf } from '@kbn/config-schema'; + +export const configSchema = schema.object({ + enabled: schema.boolean({ defaultValue: true }), + ui: schema.object({ + enabled: schema.boolean({ defaultValue: true }), + }), + // Cloud requires the ability to hide internal node attributes from users. + filteredNodeAttributes: schema.arrayOf(schema.string(), { defaultValue: [] }), +}); + +export type IndexLifecycleManagementConfig = TypeOf<typeof configSchema>; diff --git a/x-pack/plugins/index_lifecycle_management/server/index.ts b/x-pack/plugins/index_lifecycle_management/server/index.ts new file mode 100644 index 0000000000000..8a5f0fe19f9b0 --- /dev/null +++ b/x-pack/plugins/index_lifecycle_management/server/index.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. + */ + +import { PluginInitializerContext, PluginConfigDescriptor } from 'kibana/server'; +import { IndexLifecycleManagementServerPlugin } from './plugin'; +import { configSchema, IndexLifecycleManagementConfig } from './config'; + +export const plugin = (ctx: PluginInitializerContext) => + new IndexLifecycleManagementServerPlugin(ctx); + +export const config: PluginConfigDescriptor<IndexLifecycleManagementConfig> = { + schema: configSchema, + exposeToBrowser: { + ui: true, + }, +}; diff --git a/x-pack/legacy/plugins/index_lifecycle_management/server/lib/is_es_error/is_es_error.ts b/x-pack/plugins/index_lifecycle_management/server/lib/is_es_error.ts similarity index 100% rename from x-pack/legacy/plugins/index_lifecycle_management/server/lib/is_es_error/is_es_error.ts rename to x-pack/plugins/index_lifecycle_management/server/lib/is_es_error.ts diff --git a/x-pack/plugins/index_lifecycle_management/server/plugin.ts b/x-pack/plugins/index_lifecycle_management/server/plugin.ts new file mode 100644 index 0000000000000..faeac67f62a21 --- /dev/null +++ b/x-pack/plugins/index_lifecycle_management/server/plugin.ts @@ -0,0 +1,86 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { Observable } from 'rxjs'; +import { first } from 'rxjs/operators'; +import { i18n } from '@kbn/i18n'; +import { CoreSetup, Plugin, Logger, PluginInitializerContext, APICaller } from 'src/core/server'; + +import { PLUGIN } from '../common/constants'; +import { Dependencies } from './types'; +import { registerApiRoutes } from './routes'; +import { License } from './services'; +import { IndexLifecycleManagementConfig } from './config'; +import { isEsError } from './lib/is_es_error'; + +const indexLifecycleDataEnricher = async (indicesList: any, callAsCurrentUser: APICaller) => { + if (!indicesList || !indicesList.length) { + return; + } + + const params = { + path: '/*/_ilm/explain', + method: 'GET', + }; + + const { indices: ilmIndicesData } = await callAsCurrentUser('transport.request', params); + + return indicesList.map((index: any): any => { + return { + ...index, + ilm: { ...(ilmIndicesData[index.name] || {}) }, + }; + }); +}; + +export class IndexLifecycleManagementServerPlugin implements Plugin<void, void, any, any> { + private readonly config$: Observable<IndexLifecycleManagementConfig>; + private readonly license: License; + private readonly logger: Logger; + + constructor(initializerContext: PluginInitializerContext) { + this.logger = initializerContext.logger.get(); + this.config$ = initializerContext.config.create(); + this.license = new License(); + } + + async setup({ http }: CoreSetup, { licensing, indexManagement }: Dependencies): Promise<void> { + const router = http.createRouter(); + const config = await this.config$.pipe(first()).toPromise(); + + this.license.setup( + { + pluginId: PLUGIN.ID, + minimumLicenseType: PLUGIN.minimumLicenseType, + defaultErrorMessage: i18n.translate('xpack.indexLifecycleMgmt.licenseCheckErrorMessage', { + defaultMessage: 'License check failed', + }), + }, + { + licensing, + logger: this.logger, + } + ); + + registerApiRoutes({ + router, + config, + license: this.license, + lib: { + isEsError, + }, + }); + + if (config.ui.enabled) { + if (indexManagement && indexManagement.indexDataEnricher) { + indexManagement.indexDataEnricher.add(indexLifecycleDataEnricher); + } + } + } + + start() {} + stop() {} +} diff --git a/x-pack/plugins/index_lifecycle_management/server/routes/api/index/index.ts b/x-pack/plugins/index_lifecycle_management/server/routes/api/index/index.ts new file mode 100644 index 0000000000000..abe00af74b63a --- /dev/null +++ b/x-pack/plugins/index_lifecycle_management/server/routes/api/index/index.ts @@ -0,0 +1,16 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { RouteDependencies } from '../../../types'; +import { registerRetryRoute } from './register_retry_route'; +import { registerRemoveRoute } from './register_remove_route'; +import { registerAddPolicyRoute } from './register_add_policy_route'; + +export function registerIndexRoutes(dependencies: RouteDependencies) { + registerRetryRoute(dependencies); + registerRemoveRoute(dependencies); + registerAddPolicyRoute(dependencies); +} diff --git a/x-pack/plugins/index_lifecycle_management/server/routes/api/index/register_add_policy_route.ts b/x-pack/plugins/index_lifecycle_management/server/routes/api/index/register_add_policy_route.ts new file mode 100644 index 0000000000000..9627f6399eaaf --- /dev/null +++ b/x-pack/plugins/index_lifecycle_management/server/routes/api/index/register_add_policy_route.ts @@ -0,0 +1,66 @@ +/* + * 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 { APICaller } from 'src/core/server'; + +import { RouteDependencies } from '../../../types'; +import { addBasePath } from '../../../services'; + +async function addLifecyclePolicy( + callAsCurrentUser: APICaller, + indexName: string, + policyName: string, + alias: string +) { + const params = { + method: 'PUT', + path: `/${encodeURIComponent(indexName)}/_settings`, + body: { + lifecycle: { + name: policyName, + rollover_alias: alias, + }, + }, + }; + + return callAsCurrentUser('transport.request', params); +} + +const bodySchema = schema.object({ + indexName: schema.string(), + policyName: schema.string(), + alias: schema.maybe(schema.string()), +}); + +export function registerAddPolicyRoute({ router, license, lib }: RouteDependencies) { + router.post( + { path: addBasePath('/index/add'), validate: { body: bodySchema } }, + license.guardApiRoute(async (context, request, response) => { + const body = request.body as typeof bodySchema.type; + const { indexName, policyName, alias = '' } = body; + + try { + await addLifecyclePolicy( + context.core.elasticsearch.dataClient.callAsCurrentUser, + indexName, + policyName, + alias + ); + return response.ok(); + } 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/api/index/register_remove_route.ts b/x-pack/plugins/index_lifecycle_management/server/routes/api/index/register_remove_route.ts new file mode 100644 index 0000000000000..8ec94a8591785 --- /dev/null +++ b/x-pack/plugins/index_lifecycle_management/server/routes/api/index/register_remove_route.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 { schema } from '@kbn/config-schema'; +import { APICaller } from 'src/core/server'; + +import { RouteDependencies } from '../../../types'; +import { addBasePath } from '../../../services'; + +async function removeLifecycle(callAsCurrentUser: APICaller, indexNames: string[]) { + const responses = []; + for (let i = 0; i < indexNames.length; i++) { + const indexName = indexNames[i]; + const params = { + method: 'POST', + path: `/${encodeURIComponent(indexName)}/_ilm/remove`, + ignore: [404], + }; + + responses.push(callAsCurrentUser('transport.request', params)); + } + return Promise.all(responses); +} + +const bodySchema = schema.object({ + indexNames: schema.arrayOf(schema.string()), +}); + +export function registerRemoveRoute({ router, license, lib }: RouteDependencies) { + router.post( + { path: addBasePath('/index/remove'), validate: { body: bodySchema } }, + license.guardApiRoute(async (context, request, response) => { + const body = request.body as typeof bodySchema.type; + const { indexNames } = body; + + try { + await removeLifecycle(context.core.elasticsearch.dataClient.callAsCurrentUser, indexNames); + return response.ok(); + } 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/api/index/register_retry_route.ts b/x-pack/plugins/index_lifecycle_management/server/routes/api/index/register_retry_route.ts new file mode 100644 index 0000000000000..1e2d621cab173 --- /dev/null +++ b/x-pack/plugins/index_lifecycle_management/server/routes/api/index/register_retry_route.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 { schema } from '@kbn/config-schema'; +import { APICaller } from 'src/core/server'; + +import { RouteDependencies } from '../../../types'; +import { addBasePath } from '../../../services'; + +async function retryLifecycle(callAsCurrentUser: APICaller, indexNames: string[]) { + const responses = []; + for (let i = 0; i < indexNames.length; i++) { + const indexName = indexNames[i]; + const params = { + method: 'POST', + path: `/${encodeURIComponent(indexName)}/_ilm/retry`, + ignore: [404], + }; + + responses.push(callAsCurrentUser('transport.request', params)); + } + return Promise.all(responses); +} + +const bodySchema = schema.object({ + indexNames: schema.arrayOf(schema.string()), +}); + +export function registerRetryRoute({ router, license, lib }: RouteDependencies) { + router.post( + { path: addBasePath('/index/retry'), validate: { body: bodySchema } }, + license.guardApiRoute(async (context, request, response) => { + const body = request.body as typeof bodySchema.type; + const { indexNames } = body; + + try { + await retryLifecycle(context.core.elasticsearch.dataClient.callAsCurrentUser, indexNames); + return response.ok(); + } 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/api/nodes/index.ts b/x-pack/plugins/index_lifecycle_management/server/routes/api/nodes/index.ts new file mode 100644 index 0000000000000..bde56f0318bbd --- /dev/null +++ b/x-pack/plugins/index_lifecycle_management/server/routes/api/nodes/index.ts @@ -0,0 +1,14 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { RouteDependencies } from '../../../types'; +import { registerListRoute } from './register_list_route'; +import { registerDetailsRoute } from './register_details_route'; + +export function registerNodesRoutes(dependencies: RouteDependencies) { + registerListRoute(dependencies); + registerDetailsRoute(dependencies); +} diff --git a/x-pack/plugins/index_lifecycle_management/server/routes/api/nodes/register_details_route.ts b/x-pack/plugins/index_lifecycle_management/server/routes/api/nodes/register_details_route.ts new file mode 100644 index 0000000000000..6ff1f147e7ea7 --- /dev/null +++ b/x-pack/plugins/index_lifecycle_management/server/routes/api/nodes/register_details_route.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 { schema } from '@kbn/config-schema'; +import { APICaller } from 'src/core/server'; + +import { RouteDependencies } from '../../../types'; +import { addBasePath } from '../../../services'; + +function findMatchingNodes(stats: any, nodeAttrs: string): any { + return Object.entries(stats.nodes).reduce((accum: any[], [nodeId, nodeStats]: [any, any]) => { + const attributes = nodeStats.attributes || {}; + for (const [key, value] of Object.entries(attributes)) { + if (`${key}:${value}` === nodeAttrs) { + accum.push({ + nodeId, + stats: nodeStats, + }); + break; + } + } + return accum; + }, []); +} + +async function fetchNodeStats(callAsCurrentUser: APICaller): Promise<any> { + const params = { + format: 'json', + }; + + return await callAsCurrentUser('nodes.stats', params); +} + +const paramsSchema = schema.object({ + nodeAttrs: schema.string(), +}); + +export function registerDetailsRoute({ router, license, lib }: RouteDependencies) { + router.get( + { path: addBasePath('/nodes/{nodeAttrs}/details'), validate: { params: paramsSchema } }, + license.guardApiRoute(async (context, request, response) => { + const params = request.params as typeof paramsSchema.type; + const { nodeAttrs } = params; + + try { + const stats = await fetchNodeStats(context.core.elasticsearch.dataClient.callAsCurrentUser); + const okResponse = { body: findMatchingNodes(stats, nodeAttrs) }; + return response.ok(okResponse); + } 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/api/nodes/register_list_route.ts b/x-pack/plugins/index_lifecycle_management/server/routes/api/nodes/register_list_route.ts new file mode 100644 index 0000000000000..73d85c78d3b11 --- /dev/null +++ b/x-pack/plugins/index_lifecycle_management/server/routes/api/nodes/register_list_route.ts @@ -0,0 +1,69 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { APICaller } from 'src/core/server'; + +import { RouteDependencies } from '../../../types'; +import { addBasePath } from '../../../services'; + +function convertStatsIntoList(stats: any, disallowedNodeAttributes: string[]): any { + return Object.entries(stats.nodes).reduce((accum: any, [nodeId, nodeStats]: [any, any]) => { + const attributes = nodeStats.attributes || {}; + for (const [key, value] of Object.entries(attributes)) { + const isNodeAttributeAllowed = !disallowedNodeAttributes.includes(key); + if (isNodeAttributeAllowed) { + const attributeString = `${key}:${value}`; + accum[attributeString] = accum[attributeString] || []; + accum[attributeString].push(nodeId); + } + } + return accum; + }, {}); +} + +async function fetchNodeStats(callAsCurrentUser: APICaller): Promise<any> { + const params = { + format: 'json', + }; + + return await callAsCurrentUser('nodes.stats', params); +} + +export function registerListRoute({ router, config, license, lib }: RouteDependencies) { + const { filteredNodeAttributes } = config; + + const NODE_ATTRS_KEYS_TO_IGNORE: string[] = [ + 'ml.enabled', + 'ml.machine_memory', + 'ml.max_open_jobs', + // Used by ML to identify nodes that have transform enabled: + // https://github.com/elastic/elasticsearch/pull/52712/files#diff-225cc2c1291b4c60a8c3412a619094e1R147 + 'transform.node', + 'xpack.installed', + ]; + + const disallowedNodeAttributes = [...NODE_ATTRS_KEYS_TO_IGNORE, ...filteredNodeAttributes]; + + router.get( + { path: addBasePath('/nodes/list'), validate: false }, + license.guardApiRoute(async (context, request, response) => { + try { + const stats = await fetchNodeStats(context.core.elasticsearch.dataClient.callAsCurrentUser); + const okResponse = { body: convertStatsIntoList(stats, disallowedNodeAttributes) }; + return response.ok(okResponse); + } 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/api/policies/index.ts b/x-pack/plugins/index_lifecycle_management/server/routes/api/policies/index.ts new file mode 100644 index 0000000000000..c30dc04c61169 --- /dev/null +++ b/x-pack/plugins/index_lifecycle_management/server/routes/api/policies/index.ts @@ -0,0 +1,16 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { RouteDependencies } from '../../../types'; +import { registerFetchRoute } from './register_fetch_route'; +import { registerCreateRoute } from './register_create_route'; +import { registerDeleteRoute } from './register_delete_route'; + +export function registerPoliciesRoutes(dependencies: RouteDependencies) { + registerFetchRoute(dependencies); + registerCreateRoute(dependencies); + registerDeleteRoute(dependencies); +} diff --git a/x-pack/plugins/index_lifecycle_management/server/routes/api/policies/register_create_route.ts b/x-pack/plugins/index_lifecycle_management/server/routes/api/policies/register_create_route.ts new file mode 100644 index 0000000000000..a9c6bab58fdd9 --- /dev/null +++ b/x-pack/plugins/index_lifecycle_management/server/routes/api/policies/register_create_route.ts @@ -0,0 +1,145 @@ +/* + * 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 { APICaller } from 'src/core/server'; + +import { RouteDependencies } from '../../../types'; +import { addBasePath } from '../../../services'; + +async function createPolicy(callAsCurrentUser: APICaller, name: string, phases: any): Promise<any> { + const body = { + policy: { + phases, + }, + }; + const params = { + method: 'PUT', + path: `/_ilm/policy/${encodeURIComponent(name)}`, + ignore: [404], + body, + }; + + return await callAsCurrentUser('transport.request', params); +} + +const minAgeSchema = schema.maybe(schema.string()); + +const setPrioritySchema = schema.maybe( + schema.object({ + priority: schema.number(), + }) +); + +const unfollowSchema = schema.maybe(schema.object({})); // Unfollow has no options + +const allocateNodeSchema = schema.maybe(schema.recordOf(schema.string(), schema.string())); +const allocateSchema = schema.maybe( + schema.object({ + number_of_replicas: schema.maybe(schema.number()), + include: allocateNodeSchema, + exclude: allocateNodeSchema, + require: allocateNodeSchema, + }) +); + +const hotPhaseSchema = schema.object({ + min_age: minAgeSchema, + actions: schema.object({ + set_priority: setPrioritySchema, + unfollow: unfollowSchema, + rollover: schema.maybe( + schema.object({ + max_age: schema.maybe(schema.string()), + max_size: schema.maybe(schema.string()), + max_docs: schema.maybe(schema.number()), + }) + ), + }), +}); + +const warmPhaseSchema = schema.maybe( + schema.object({ + min_age: minAgeSchema, + actions: schema.object({ + set_priority: setPrioritySchema, + unfollow: unfollowSchema, + read_only: schema.maybe(schema.object({})), // Readonly has no options + allocate: allocateSchema, + shrink: schema.maybe( + schema.object({ + number_of_shards: schema.number(), + }) + ), + forcemerge: schema.maybe( + schema.object({ + max_num_segments: schema.number(), + }) + ), + }), + }) +); + +const coldPhaseSchema = schema.maybe( + schema.object({ + min_age: minAgeSchema, + actions: schema.object({ + set_priority: setPrioritySchema, + unfollow: unfollowSchema, + allocate: allocateSchema, + freeze: schema.maybe(schema.object({})), // Freeze has no options + }), + }) +); + +const deletePhaseSchema = schema.maybe( + schema.object({ + min_age: minAgeSchema, + actions: schema.object({ + wait_for_snapshot: schema.maybe( + schema.object({ + policy: schema.string(), + }) + ), + delete: schema.maybe(schema.object({})), // Delete has no options + }), + }) +); + +// Per https://www.elastic.co/guide/en/elasticsearch/reference/current/_actions.html +const bodySchema = schema.object({ + name: schema.string(), + phases: schema.object({ + hot: hotPhaseSchema, + warm: warmPhaseSchema, + cold: coldPhaseSchema, + delete: deletePhaseSchema, + }), +}); + +export function registerCreateRoute({ router, license, lib }: RouteDependencies) { + router.post( + { path: addBasePath('/policies'), validate: { body: bodySchema } }, + license.guardApiRoute(async (context, request, response) => { + const body = request.body as typeof bodySchema.type; + const { name, phases } = body; + + try { + await createPolicy(context.core.elasticsearch.dataClient.callAsCurrentUser, name, phases); + return response.ok(); + } 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/api/policies/register_delete_route.ts b/x-pack/plugins/index_lifecycle_management/server/routes/api/policies/register_delete_route.ts new file mode 100644 index 0000000000000..e08297f4d7bc4 --- /dev/null +++ b/x-pack/plugins/index_lifecycle_management/server/routes/api/policies/register_delete_route.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 { APICaller } from 'src/core/server'; + +import { RouteDependencies } from '../../../types'; +import { addBasePath } from '../../../services'; + +async function deletePolicies(callAsCurrentUser: APICaller, policyNames: string): Promise<any> { + const params = { + method: 'DELETE', + path: `/_ilm/policy/${encodeURIComponent(policyNames)}`, + // we allow 404 since they may have no policies + ignore: [404], + }; + + return await callAsCurrentUser('transport.request', params); +} + +const paramsSchema = schema.object({ + policyNames: schema.string(), +}); + +export function registerDeleteRoute({ router, license, lib }: RouteDependencies) { + router.delete( + { path: addBasePath('/policies/{policyNames}'), validate: { params: paramsSchema } }, + license.guardApiRoute(async (context, request, response) => { + const params = request.params as typeof paramsSchema.type; + const { policyNames } = params; + + try { + await deletePolicies(context.core.elasticsearch.dataClient.callAsCurrentUser, policyNames); + return response.ok(); + } 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/api/policies/register_fetch_route.ts b/x-pack/plugins/index_lifecycle_management/server/routes/api/policies/register_fetch_route.ts new file mode 100644 index 0000000000000..294b7c4c65cba --- /dev/null +++ b/x-pack/plugins/index_lifecycle_management/server/routes/api/policies/register_fetch_route.ts @@ -0,0 +1,90 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { schema } from '@kbn/config-schema'; +import { APICaller } from 'src/core/server'; + +import { RouteDependencies } from '../../../types'; +import { addBasePath } from '../../../services'; + +function formatPolicies(policiesMap: any): any { + if (policiesMap.status === 404) { + return []; + } + + return Object.keys(policiesMap).reduce((accum: any[], lifecycleName: string) => { + const policyEntry = policiesMap[lifecycleName]; + accum.push({ + ...policyEntry, + name: lifecycleName, + }); + return accum; + }, []); +} + +async function fetchPolicies(callAsCurrentUser: APICaller): Promise<any> { + const params = { + method: 'GET', + path: '/_ilm/policy', + // we allow 404 since they may have no policies + ignore: [404], + }; + + return await callAsCurrentUser('transport.request', params); +} + +async function addLinkedIndices(callAsCurrentUser: APICaller, policiesMap: any) { + if (policiesMap.status === 404) { + return policiesMap; + } + const params = { + method: 'GET', + path: '/*/_ilm/explain', + // we allow 404 since they may have no policies + ignore: [404], + }; + + const policyExplanation: any = await callAsCurrentUser('transport.request', params); + Object.entries(policyExplanation.indices).forEach(([indexName, { policy }]: [string, any]) => { + if (policy && policiesMap[policy]) { + policiesMap[policy].linkedIndices = policiesMap[policy].linkedIndices || []; + policiesMap[policy].linkedIndices.push(indexName); + } + }); +} + +const querySchema = schema.object({ + withIndices: schema.boolean({ defaultValue: false }), +}); + +export function registerFetchRoute({ router, license, lib }: RouteDependencies) { + router.get( + { path: addBasePath('/policies'), validate: { query: querySchema } }, + license.guardApiRoute(async (context, request, response) => { + const query = request.query as typeof querySchema.type; + const { withIndices } = query; + const { callAsCurrentUser } = context.core.elasticsearch.dataClient; + + try { + const policiesMap = await fetchPolicies(callAsCurrentUser); + if (withIndices) { + await addLinkedIndices(callAsCurrentUser, policiesMap); + } + const okResponse = { body: formatPolicies(policiesMap) }; + return response.ok(okResponse); + } 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/api/templates/index.ts b/x-pack/plugins/index_lifecycle_management/server/routes/api/templates/index.ts new file mode 100644 index 0000000000000..a2d885c3170b9 --- /dev/null +++ b/x-pack/plugins/index_lifecycle_management/server/routes/api/templates/index.ts @@ -0,0 +1,14 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { RouteDependencies } from '../../../types'; +import { registerFetchRoute } from './register_fetch_route'; +import { registerAddPolicyRoute } from './register_add_policy_route'; + +export function registerTemplatesRoutes(dependencies: RouteDependencies) { + registerFetchRoute(dependencies); + registerAddPolicyRoute(dependencies); +} diff --git a/x-pack/plugins/index_lifecycle_management/server/routes/api/templates/register_add_policy_route.ts b/x-pack/plugins/index_lifecycle_management/server/routes/api/templates/register_add_policy_route.ts new file mode 100644 index 0000000000000..0da8535f8d4ec --- /dev/null +++ b/x-pack/plugins/index_lifecycle_management/server/routes/api/templates/register_add_policy_route.ts @@ -0,0 +1,81 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { merge } from 'lodash'; +import { schema } from '@kbn/config-schema'; +import { APICaller } from 'src/core/server'; + +import { RouteDependencies } from '../../../types'; +import { addBasePath } from '../../../services'; + +async function getIndexTemplate(callAsCurrentUser: APICaller, templateName: string): Promise<any> { + const response = await callAsCurrentUser('indices.getTemplate', { name: templateName }); + return response[templateName]; +} + +async function updateIndexTemplate( + callAsCurrentUser: APICaller, + templateName: string, + policyName: string, + aliasName?: string +): Promise<any> { + // Fetch existing template + const template = await getIndexTemplate(callAsCurrentUser, templateName); + merge(template, { + settings: { + index: { + lifecycle: { + name: policyName, + rollover_alias: aliasName, + }, + }, + }, + }); + + const params = { + method: 'PUT', + path: `/_template/${encodeURIComponent(templateName)}`, + ignore: [404], + body: template, + }; + + return await callAsCurrentUser('transport.request', params); +} + +const bodySchema = schema.object({ + templateName: schema.string(), + policyName: schema.string(), + aliasName: schema.maybe(schema.string()), +}); + +export function registerAddPolicyRoute({ router, license, lib }: RouteDependencies) { + router.post( + { path: addBasePath('/template'), validate: { body: bodySchema } }, + license.guardApiRoute(async (context, request, response) => { + const body = request.body as typeof bodySchema.type; + const { templateName, policyName, aliasName } = body; + + try { + await updateIndexTemplate( + context.core.elasticsearch.dataClient.callAsCurrentUser, + templateName, + policyName, + aliasName + ); + return response.ok(); + } 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/api/templates/register_fetch_route.ts b/x-pack/plugins/index_lifecycle_management/server/routes/api/templates/register_fetch_route.ts new file mode 100644 index 0000000000000..a2dc67cb77afe --- /dev/null +++ b/x-pack/plugins/index_lifecycle_management/server/routes/api/templates/register_fetch_route.ts @@ -0,0 +1,85 @@ +/* + * 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 { APICaller } from 'src/core/server'; + +import { RouteDependencies } from '../../../types'; +import { addBasePath } from '../../../services'; + +/** + * We don't want to output system template (whose name starts with a ".") which don't + * have a time base index pattern (with a wildcard in it) as those templates are already + * assigned to a single index. + * + * @param {String} templateName The index template + * @param {Array} indexPatterns Index patterns + */ +function isReservedSystemTemplate(templateName: string, indexPatterns: string[]): boolean { + return ( + templateName.startsWith('kibana_index_template') || + (templateName.startsWith('.') && + indexPatterns.every(pattern => { + return !pattern.includes('*'); + })) + ); +} + +function filterAndFormatTemplates(templates: any): any { + const formattedTemplates = []; + const templateNames = Object.keys(templates); + for (const templateName of templateNames) { + const { settings, index_patterns } = templates[templateName]; // eslint-disable-line camelcase + if (isReservedSystemTemplate(templateName, index_patterns)) { + continue; + } + const formattedTemplate = { + index_lifecycle_name: + settings.index && settings.index.lifecycle ? settings.index.lifecycle.name : undefined, + index_patterns, + allocation_rules: + settings.index && settings.index.routing ? settings.index.routing : undefined, + settings, + name: templateName, + }; + formattedTemplates.push(formattedTemplate); + } + return formattedTemplates; +} + +async function fetchTemplates(callAsCurrentUser: APICaller): Promise<any> { + const params = { + method: 'GET', + path: '/_template', + // we allow 404 incase the user shutdown security in-between the check and now + ignore: [404], + }; + + return await callAsCurrentUser('transport.request', params); +} + +export function registerFetchRoute({ router, license, lib }: RouteDependencies) { + router.get( + { path: addBasePath('/templates'), validate: false }, + license.guardApiRoute(async (context, request, response) => { + try { + const templates = await fetchTemplates( + context.core.elasticsearch.dataClient.callAsCurrentUser + ); + const okResponse = { body: filterAndFormatTemplates(templates) }; + return response.ok(okResponse); + } 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 new file mode 100644 index 0000000000000..35996721854c6 --- /dev/null +++ b/x-pack/plugins/index_lifecycle_management/server/routes/index.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. + */ + +import { RouteDependencies } from '../types'; + +import { registerIndexRoutes } from './api/index'; +import { registerNodesRoutes } from './api/nodes'; +import { registerPoliciesRoutes } from './api/policies'; +import { registerTemplatesRoutes } from './api/templates'; + +export function registerApiRoutes(dependencies: RouteDependencies) { + registerIndexRoutes(dependencies); + registerNodesRoutes(dependencies); + registerPoliciesRoutes(dependencies); + registerTemplatesRoutes(dependencies); +} diff --git a/x-pack/plugins/index_lifecycle_management/server/services/add_base_path.ts b/x-pack/plugins/index_lifecycle_management/server/services/add_base_path.ts new file mode 100644 index 0000000000000..3f3dd131df7c7 --- /dev/null +++ b/x-pack/plugins/index_lifecycle_management/server/services/add_base_path.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. + */ + +import { API_BASE_PATH } from '../../common/constants'; + +export const addBasePath = (uri: string): string => `${API_BASE_PATH}${uri}`; diff --git a/x-pack/plugins/index_lifecycle_management/server/services/index.ts b/x-pack/plugins/index_lifecycle_management/server/services/index.ts new file mode 100644 index 0000000000000..d7b544b290c39 --- /dev/null +++ b/x-pack/plugins/index_lifecycle_management/server/services/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 { License } from './license'; +export { addBasePath } from './add_base_path'; diff --git a/x-pack/plugins/index_lifecycle_management/server/services/license.ts b/x-pack/plugins/index_lifecycle_management/server/services/license.ts new file mode 100644 index 0000000000000..31d3654c51e3e --- /dev/null +++ b/x-pack/plugins/index_lifecycle_management/server/services/license.ts @@ -0,0 +1,82 @@ +/* + * 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 'src/core/server'; +import { + KibanaRequest, + KibanaResponseFactory, + RequestHandler, + RequestHandlerContext, +} from 'kibana/server'; + +import { LicensingPluginSetup } from '../../../licensing/server'; +import { LicenseType } from '../../../licensing/common/types'; + +export interface LicenseStatus { + isValid: boolean; + message?: string; +} + +interface SetupSettings { + pluginId: string; + minimumLicenseType: LicenseType; + defaultErrorMessage: string; +} + +export class License { + private licenseStatus: LicenseStatus = { + isValid: false, + message: 'Invalid License', + }; + + setup( + { pluginId, minimumLicenseType, defaultErrorMessage }: SetupSettings, + { licensing, logger }: { licensing: LicensingPluginSetup; logger: Logger } + ) { + licensing.license$.subscribe(license => { + const { state, message } = license.check(pluginId, minimumLicenseType); + const hasRequiredLicense = state === 'valid'; + + if (hasRequiredLicense) { + this.licenseStatus = { isValid: true }; + } else { + this.licenseStatus = { + isValid: false, + message: message || defaultErrorMessage, + }; + if (message) { + logger.info(message); + } + } + }); + } + + guardApiRoute(handler: RequestHandler) { + const license = this; + + return function licenseCheck( + ctx: RequestHandlerContext, + request: KibanaRequest, + response: KibanaResponseFactory + ) { + const licenseStatus = license.getStatus(); + + if (!licenseStatus.isValid) { + return response.customError({ + body: { + message: licenseStatus.message || '', + }, + statusCode: 403, + }); + } + + return handler(ctx, request, response); + }; + } + + getStatus() { + return this.licenseStatus; + } +} diff --git a/x-pack/plugins/index_lifecycle_management/server/types.ts b/x-pack/plugins/index_lifecycle_management/server/types.ts new file mode 100644 index 0000000000000..734d05a82000e --- /dev/null +++ b/x-pack/plugins/index_lifecycle_management/server/types.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 { IRouter } from 'src/core/server'; + +import { LicensingPluginSetup } from '../../licensing/server'; +import { IndexManagementPluginSetup } from '../../index_management/server'; +import { License } from './services'; +import { IndexLifecycleManagementConfig } from './config'; +import { isEsError } from './lib/is_es_error'; + +export interface Dependencies { + licensing: LicensingPluginSetup; + indexManagement?: IndexManagementPluginSetup; +} + +export interface RouteDependencies { + router: IRouter; + config: IndexLifecycleManagementConfig; + license: License; + lib: { + isEsError: typeof isEsError; + }; +} diff --git a/x-pack/plugins/index_management/public/application/sections/home/index_list/index_actions_context_menu/index_actions_context_menu.js b/x-pack/plugins/index_management/public/application/sections/home/index_list/index_actions_context_menu/index_actions_context_menu.js index 8f794ce1ed612..a351d39b123a8 100644 --- a/x-pack/plugins/index_management/public/application/sections/home/index_list/index_actions_context_menu/index_actions_context_menu.js +++ b/x-pack/plugins/index_management/public/application/sections/home/index_list/index_actions_context_menu/index_actions_context_menu.js @@ -46,11 +46,7 @@ export class IndexActionsContextMenu extends Component { confirmAction = isActionConfirmed => { this.setState({ isActionConfirmed }); }; - panels({ - core: { fatalErrors }, - services: { extensionsService, httpService, notificationService }, - plugins: { usageCollection }, - }) { + panels({ services: { extensionsService } }) { const { closeIndices, openIndices, @@ -218,15 +214,6 @@ export class IndexActionsContextMenu extends Component { const actionExtensionDefinition = actionExtension({ indices, reloadIndices, - // These config options can be removed once the NP migration out of legacy is complete. - // They're needed for now because ILM's extensions make API calls which require these - // dependencies, but they're not available unless the app's "setup" lifecycle stage occurs. - // Within the old platform, "setup" only occurs once the user actually visits the app. - // Once ILM and IM have been moved out of legacy this hack won't be necessary. - usageCollection, - toasts: notificationService.toasts, - fatalErrors, - httpClient: httpService.httpClient, }); if (actionExtensionDefinition) { const { diff --git a/x-pack/plugins/index_management/public/index.ts b/x-pack/plugins/index_management/public/index.ts index 6bb921ef648f3..7a76fff7f3ec6 100644 --- a/x-pack/plugins/index_management/public/index.ts +++ b/x-pack/plugins/index_management/public/index.ts @@ -4,13 +4,13 @@ * you may not use this file except in compliance with the Elastic License. */ import './index.scss'; -import { IndexMgmtUIPlugin, IndexMgmtSetup } from './plugin'; +import { IndexMgmtUIPlugin, IndexManagementPluginSetup } from './plugin'; /** @public */ export const plugin = () => { return new IndexMgmtUIPlugin(); }; -export { IndexMgmtSetup }; +export { IndexManagementPluginSetup }; export { getIndexListUri } from './application/services/navigation'; diff --git a/x-pack/plugins/index_management/public/plugin.ts b/x-pack/plugins/index_management/public/plugin.ts index 4aa06d286e3c4..f9e2a47170b3d 100644 --- a/x-pack/plugins/index_management/public/plugin.ts +++ b/x-pack/plugins/index_management/public/plugin.ts @@ -20,7 +20,7 @@ import { setUiMetricService } from './application/services/api'; import { IndexMgmtMetricsType } from './types'; import { ExtensionsService, ExtensionsSetup } from './services'; -export interface IndexMgmtSetup { +export interface IndexManagementPluginSetup { extensionsService: ExtensionsSetup; } @@ -40,7 +40,7 @@ export class IndexMgmtUIPlugin { setUiMetricService(this.uiMetricService); } - public setup(coreSetup: CoreSetup, plugins: PluginsDependencies): IndexMgmtSetup { + public setup(coreSetup: CoreSetup, plugins: PluginsDependencies): IndexManagementPluginSetup { const { http, notifications } = coreSetup; const { usageCollection, management } = plugins; diff --git a/x-pack/plugins/index_management/server/index.ts b/x-pack/plugins/index_management/server/index.ts index e4102711708cb..4d9409e4a516c 100644 --- a/x-pack/plugins/index_management/server/index.ts +++ b/x-pack/plugins/index_management/server/index.ts @@ -17,6 +17,6 @@ export const config = { /** @public */ export { Dependencies } from './types'; -export { IndexMgmtSetup } from './plugin'; +export { IndexManagementPluginSetup } from './plugin'; export { Index } from './types'; export { IndexManagementConfig } from './config'; diff --git a/x-pack/plugins/index_management/server/plugin.ts b/x-pack/plugins/index_management/server/plugin.ts index a0a9151cdb71f..e5bd7451b028f 100644 --- a/x-pack/plugins/index_management/server/plugin.ts +++ b/x-pack/plugins/index_management/server/plugin.ts @@ -12,13 +12,13 @@ import { ApiRoutes } from './routes'; import { License, IndexDataEnricher } from './services'; import { isEsError } from './lib/is_es_error'; -export interface IndexMgmtSetup { +export interface IndexManagementPluginSetup { indexDataEnricher: { add: IndexDataEnricher['add']; }; } -export class IndexMgmtServerPlugin implements Plugin<IndexMgmtSetup, void, any, any> { +export class IndexMgmtServerPlugin implements Plugin<IndexManagementPluginSetup, void, any, any> { private readonly apiRoutes: ApiRoutes; private readonly license: License; private readonly logger: Logger; @@ -31,7 +31,7 @@ export class IndexMgmtServerPlugin implements Plugin<IndexMgmtSetup, void, any, this.indexDataEnricher = new IndexDataEnricher(); } - setup({ http }: CoreSetup, { licensing }: Dependencies): IndexMgmtSetup { + setup({ http }: CoreSetup, { licensing }: Dependencies): IndexManagementPluginSetup { const router = http.createRouter(); this.license.setup( diff --git a/x-pack/plugins/infra/common/http_api/metadata_api.ts b/x-pack/plugins/infra/common/http_api/metadata_api.ts index 7fc3c3e876f08..5ee96b479be8e 100644 --- a/x-pack/plugins/infra/common/http_api/metadata_api.ts +++ b/x-pack/plugins/infra/common/http_api/metadata_api.ts @@ -11,6 +11,10 @@ export const InfraMetadataRequestRT = rt.type({ nodeId: rt.string, nodeType: ItemTypeRT, sourceId: rt.string, + timeRange: rt.type({ + from: rt.number, + to: rt.number, + }), }); export const InfraMetadataFeatureRT = rt.type({ diff --git a/x-pack/plugins/infra/common/http_api/source_api.ts b/x-pack/plugins/infra/common/http_api/source_api.ts new file mode 100644 index 0000000000000..218f8cebc9869 --- /dev/null +++ b/x-pack/plugins/infra/common/http_api/source_api.ts @@ -0,0 +1,188 @@ +/* + * 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. + */ + +/* eslint-disable @typescript-eslint/no-empty-interface */ + +import * as rt from 'io-ts'; +import moment from 'moment'; +import { pipe } from 'fp-ts/lib/pipeable'; +import { chain } from 'fp-ts/lib/Either'; + +export const TimestampFromString = new rt.Type<number, string>( + 'TimestampFromString', + (input): input is number => typeof input === 'number', + (input, context) => + pipe( + rt.string.validate(input, context), + chain(stringInput => { + const momentValue = moment(stringInput); + return momentValue.isValid() + ? rt.success(momentValue.valueOf()) + : rt.failure(stringInput, context); + }) + ), + output => new Date(output).toISOString() +); + +/** + * Stored source configuration as read from and written to saved objects + */ + +const SavedSourceConfigurationFieldsRuntimeType = rt.partial({ + container: rt.string, + host: rt.string, + pod: rt.string, + tiebreaker: rt.string, + timestamp: rt.string, +}); + +export const SavedSourceConfigurationTimestampColumnRuntimeType = rt.type({ + timestampColumn: rt.type({ + id: rt.string, + }), +}); + +export const SavedSourceConfigurationMessageColumnRuntimeType = rt.type({ + messageColumn: rt.type({ + id: rt.string, + }), +}); + +export const SavedSourceConfigurationFieldColumnRuntimeType = rt.type({ + fieldColumn: rt.type({ + id: rt.string, + field: rt.string, + }), +}); + +export const SavedSourceConfigurationColumnRuntimeType = rt.union([ + SavedSourceConfigurationTimestampColumnRuntimeType, + SavedSourceConfigurationMessageColumnRuntimeType, + SavedSourceConfigurationFieldColumnRuntimeType, +]); + +export const SavedSourceConfigurationRuntimeType = rt.partial({ + name: rt.string, + description: rt.string, + metricAlias: rt.string, + logAlias: rt.string, + fields: SavedSourceConfigurationFieldsRuntimeType, + logColumns: rt.array(SavedSourceConfigurationColumnRuntimeType), +}); + +export interface InfraSavedSourceConfiguration + extends rt.TypeOf<typeof SavedSourceConfigurationRuntimeType> {} + +export const pickSavedSourceConfiguration = ( + value: InfraSourceConfiguration +): InfraSavedSourceConfiguration => { + const { name, description, metricAlias, logAlias, fields, logColumns } = value; + const { container, host, pod, tiebreaker, timestamp } = fields; + + return { + name, + description, + metricAlias, + logAlias, + fields: { container, host, pod, tiebreaker, timestamp }, + logColumns, + }; +}; + +/** + * Static source configuration as read from the configuration file + */ + +const StaticSourceConfigurationFieldsRuntimeType = rt.partial({ + ...SavedSourceConfigurationFieldsRuntimeType.props, + message: rt.array(rt.string), +}); + +export const StaticSourceConfigurationRuntimeType = rt.partial({ + name: rt.string, + description: rt.string, + metricAlias: rt.string, + logAlias: rt.string, + fields: StaticSourceConfigurationFieldsRuntimeType, + logColumns: rt.array(SavedSourceConfigurationColumnRuntimeType), +}); + +export interface InfraStaticSourceConfiguration + extends rt.TypeOf<typeof StaticSourceConfigurationRuntimeType> {} + +/** + * Full source configuration type after all cleanup has been done at the edges + */ + +const SourceConfigurationFieldsRuntimeType = rt.type({ + ...StaticSourceConfigurationFieldsRuntimeType.props, +}); + +export const SourceConfigurationRuntimeType = rt.type({ + ...SavedSourceConfigurationRuntimeType.props, + fields: SourceConfigurationFieldsRuntimeType, + logColumns: rt.array(SavedSourceConfigurationColumnRuntimeType), +}); + +export const SourceRuntimeType = rt.intersection([ + rt.type({ + id: rt.string, + origin: rt.keyof({ + fallback: null, + internal: null, + stored: null, + }), + configuration: SourceConfigurationRuntimeType, + }), + rt.partial({ + version: rt.string, + updatedAt: rt.number, + }), +]); + +export interface InfraSourceConfiguration + extends rt.TypeOf<typeof SourceConfigurationRuntimeType> {} + +export interface InfraSource extends rt.TypeOf<typeof SourceRuntimeType> {} + +const SourceStatusFieldRuntimeType = rt.type({ + name: rt.string, + type: rt.string, + searchable: rt.boolean, + aggregatable: rt.boolean, + displayable: rt.boolean, +}); + +const SourceStatusRuntimeType = rt.type({ + logIndicesExist: rt.boolean, + metricIndicesExist: rt.boolean, + indexFields: rt.array(SourceStatusFieldRuntimeType), +}); + +export const SourceResponseRuntimeType = rt.type({ + source: SourceRuntimeType, + status: SourceStatusRuntimeType, +}); + +export type SourceResponse = rt.TypeOf<typeof SourceResponseRuntimeType>; + +/** + * Saved object type with metadata + */ + +export const SourceConfigurationSavedObjectRuntimeType = rt.intersection([ + rt.type({ + id: rt.string, + attributes: SavedSourceConfigurationRuntimeType, + }), + rt.partial({ + version: rt.string, + updated_at: TimestampFromString, + }), +]); + +export interface SourceConfigurationSavedObject + extends rt.TypeOf<typeof SourceConfigurationSavedObjectRuntimeType> {} diff --git a/x-pack/plugins/infra/common/inventory_models/aws_ec2/layout.tsx b/x-pack/plugins/infra/common/inventory_models/aws_ec2/layout.tsx index c8e0680287526..68bfe41fd538e 100644 --- a/x-pack/plugins/infra/common/inventory_models/aws_ec2/layout.tsx +++ b/x-pack/plugins/infra/common/inventory_models/aws_ec2/layout.tsx @@ -20,7 +20,7 @@ import { withTheme } from '../../../../observability/public'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths import { MetadataDetails } from '../../../public/pages/metrics/components/metadata_details'; -export const Layout = withTheme(({ metrics, theme }: LayoutPropsWithTheme) => ( +export const Layout = withTheme(({ metrics, theme, onChangeRangeTime }: LayoutPropsWithTheme) => ( <React.Fragment> <MetadataDetails fields={[ @@ -42,6 +42,7 @@ export const Layout = withTheme(({ metrics, theme }: LayoutPropsWithTheme) => ( } )} metrics={metrics} + onChangeRangeTime={onChangeRangeTime} > <SubSection id="awsEC2CpuUtilization" diff --git a/x-pack/plugins/infra/common/inventory_models/aws_rds/layout.tsx b/x-pack/plugins/infra/common/inventory_models/aws_rds/layout.tsx index 4bc4562ab2760..220c6c67f4aea 100644 --- a/x-pack/plugins/infra/common/inventory_models/aws_rds/layout.tsx +++ b/x-pack/plugins/infra/common/inventory_models/aws_rds/layout.tsx @@ -18,7 +18,7 @@ import { withTheme } from '../../../../observability/public'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths import { LayoutContent } from '../../../public/pages/metrics/components/layout_content'; -export const Layout = withTheme(({ metrics, theme }: LayoutPropsWithTheme) => ( +export const Layout = withTheme(({ metrics, onChangeRangeTime, theme }: LayoutPropsWithTheme) => ( <React.Fragment> <LayoutContent> <Section @@ -30,6 +30,7 @@ export const Layout = withTheme(({ metrics, theme }: LayoutPropsWithTheme) => ( } )} metrics={metrics} + onChangeRangeTime={onChangeRangeTime} > <SubSection id="awsRDSCpuTotal" diff --git a/x-pack/plugins/infra/common/inventory_models/aws_s3/layout.tsx b/x-pack/plugins/infra/common/inventory_models/aws_s3/layout.tsx index c40c8e7b8c6d3..805236cf47082 100644 --- a/x-pack/plugins/infra/common/inventory_models/aws_s3/layout.tsx +++ b/x-pack/plugins/infra/common/inventory_models/aws_s3/layout.tsx @@ -18,7 +18,7 @@ import { withTheme } from '../../../../observability/public'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths import { LayoutContent } from '../../../public/pages/metrics/components/layout_content'; -export const Layout = withTheme(({ metrics, theme }: LayoutPropsWithTheme) => ( +export const Layout = withTheme(({ metrics, onChangeRangeTime, theme }: LayoutPropsWithTheme) => ( <React.Fragment> <LayoutContent> <Section @@ -30,6 +30,7 @@ export const Layout = withTheme(({ metrics, theme }: LayoutPropsWithTheme) => ( } )} metrics={metrics} + onChangeRangeTime={onChangeRangeTime} > <SubSection id="awsS3BucketSize" diff --git a/x-pack/plugins/infra/common/inventory_models/aws_sqs/layout.tsx b/x-pack/plugins/infra/common/inventory_models/aws_sqs/layout.tsx index 7f2dc92f42205..d581ac751682d 100644 --- a/x-pack/plugins/infra/common/inventory_models/aws_sqs/layout.tsx +++ b/x-pack/plugins/infra/common/inventory_models/aws_sqs/layout.tsx @@ -18,7 +18,7 @@ import { withTheme } from '../../../../observability/public'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths import { LayoutContent } from '../../../public/pages/metrics/components/layout_content'; -export const Layout = withTheme(({ metrics, theme }: LayoutPropsWithTheme) => ( +export const Layout = withTheme(({ metrics, onChangeRangeTime, theme }: LayoutPropsWithTheme) => ( <React.Fragment> <LayoutContent> <Section @@ -30,6 +30,7 @@ export const Layout = withTheme(({ metrics, theme }: LayoutPropsWithTheme) => ( } )} metrics={metrics} + onChangeRangeTime={onChangeRangeTime} > <SubSection id="awsSQSMessagesVisible" diff --git a/x-pack/plugins/infra/common/inventory_models/container/layout.tsx b/x-pack/plugins/infra/common/inventory_models/container/layout.tsx index b61e8eed09cce..9956b2c9a2ce4 100644 --- a/x-pack/plugins/infra/common/inventory_models/container/layout.tsx +++ b/x-pack/plugins/infra/common/inventory_models/container/layout.tsx @@ -22,7 +22,7 @@ import { LayoutContent } from '../../../public/pages/metrics/components/layout_c // eslint-disable-next-line @kbn/eslint/no-restricted-paths import { MetadataDetails } from '../../../public/pages/metrics/components/metadata_details'; -export const Layout = withTheme(({ metrics, theme }: LayoutPropsWithTheme) => ( +export const Layout = withTheme(({ metrics, onChangeRangeTime, theme }: LayoutPropsWithTheme) => ( <React.Fragment> <MetadataDetails /> <LayoutContent> @@ -40,6 +40,7 @@ export const Layout = withTheme(({ metrics, theme }: LayoutPropsWithTheme) => ( } )} metrics={metrics} + onChangeRangeTime={onChangeRangeTime} > <SubSection id="containerOverview"> <GaugesSectionVis diff --git a/x-pack/plugins/infra/common/inventory_models/host/layout.tsx b/x-pack/plugins/infra/common/inventory_models/host/layout.tsx index 15e5530e8db53..6d7d361254220 100644 --- a/x-pack/plugins/infra/common/inventory_models/host/layout.tsx +++ b/x-pack/plugins/infra/common/inventory_models/host/layout.tsx @@ -24,7 +24,7 @@ import { MetadataDetails } from '../../../public/pages/metrics/components/metada // eslint-disable-next-line @kbn/eslint/no-restricted-paths import { LayoutContent } from '../../../public/pages/metrics/components/layout_content'; -export const Layout = withTheme(({ metrics, theme }: LayoutPropsWithTheme) => ( +export const Layout = withTheme(({ metrics, onChangeRangeTime, theme }: LayoutPropsWithTheme) => ( <React.Fragment> <MetadataDetails fields={[ @@ -52,6 +52,7 @@ export const Layout = withTheme(({ metrics, theme }: LayoutPropsWithTheme) => ( } )} metrics={metrics} + onChangeRangeTime={onChangeRangeTime} > <SubSection id="hostSystemOverview"> <GaugesSectionVis @@ -242,6 +243,7 @@ export const Layout = withTheme(({ metrics, theme }: LayoutPropsWithTheme) => ( } )} metrics={metrics} + onChangeRangeTime={onChangeRangeTime} > <SubSection id="hostK8sOverview"> <GaugesSectionVis @@ -371,8 +373,8 @@ export const Layout = withTheme(({ metrics, theme }: LayoutPropsWithTheme) => ( /> </SubSection> </Section> - <Aws.Layout metrics={metrics} /> - <Ngnix.Layout metrics={metrics} /> + <Aws.Layout metrics={metrics} onChangeRangeTime={onChangeRangeTime} /> + <Ngnix.Layout metrics={metrics} onChangeRangeTime={onChangeRangeTime} /> </LayoutContent> </React.Fragment> )); diff --git a/x-pack/plugins/infra/common/inventory_models/pod/layout.tsx b/x-pack/plugins/infra/common/inventory_models/pod/layout.tsx index 43b95d73f6d95..8bc2f3ee8b4b3 100644 --- a/x-pack/plugins/infra/common/inventory_models/pod/layout.tsx +++ b/x-pack/plugins/infra/common/inventory_models/pod/layout.tsx @@ -23,7 +23,7 @@ import { MetadataDetails } from '../../../public/pages/metrics/components/metada // eslint-disable-next-line @kbn/eslint/no-restricted-paths import { LayoutContent } from '../../../public/pages/metrics/components/layout_content'; -export const Layout = withTheme(({ metrics, theme }: LayoutPropsWithTheme) => ( +export const Layout = withTheme(({ metrics, onChangeRangeTime, theme }: LayoutPropsWithTheme) => ( <React.Fragment> <MetadataDetails /> <LayoutContent> @@ -38,6 +38,7 @@ export const Layout = withTheme(({ metrics, theme }: LayoutPropsWithTheme) => ( } )} metrics={metrics} + onChangeRangeTime={onChangeRangeTime} > <SubSection id="podOverview"> <GaugesSectionVis @@ -161,7 +162,7 @@ export const Layout = withTheme(({ metrics, theme }: LayoutPropsWithTheme) => ( /> </SubSection> </Section> - <Nginx.Layout metrics={metrics} /> + <Nginx.Layout metrics={metrics} onChangeRangeTime={onChangeRangeTime} /> </LayoutContent> </React.Fragment> )); diff --git a/x-pack/plugins/infra/common/inventory_models/shared/layouts/aws.tsx b/x-pack/plugins/infra/common/inventory_models/shared/layouts/aws.tsx index fba48c4224e6b..7a0b898d406ce 100644 --- a/x-pack/plugins/infra/common/inventory_models/shared/layouts/aws.tsx +++ b/x-pack/plugins/infra/common/inventory_models/shared/layouts/aws.tsx @@ -18,7 +18,7 @@ import { ChartSectionVis } from '../../../../public/pages/metrics/components/cha // eslint-disable-next-line @kbn/eslint/no-restricted-paths import { withTheme } from '../../../../../observability/public'; -export const Layout = withTheme(({ metrics, theme }: LayoutPropsWithTheme) => ( +export const Layout = withTheme(({ metrics, onChangeRangeTime, theme }: LayoutPropsWithTheme) => ( <React.Fragment> <Section navLabel="AWS" @@ -29,6 +29,7 @@ export const Layout = withTheme(({ metrics, theme }: LayoutPropsWithTheme) => ( } )} metrics={metrics} + onChangeRangeTime={onChangeRangeTime} > <SubSection id="awsOverview"> <GaugesSectionVis diff --git a/x-pack/plugins/infra/common/inventory_models/shared/layouts/nginx.tsx b/x-pack/plugins/infra/common/inventory_models/shared/layouts/nginx.tsx index eff0a9bbca85a..79cea5150d498 100644 --- a/x-pack/plugins/infra/common/inventory_models/shared/layouts/nginx.tsx +++ b/x-pack/plugins/infra/common/inventory_models/shared/layouts/nginx.tsx @@ -16,9 +16,14 @@ import { ChartSectionVis } from '../../../../public/pages/metrics/components/cha // eslint-disable-next-line @kbn/eslint/no-restricted-paths import { withTheme } from '../../../../../observability/public'; -export const Layout = withTheme(({ metrics, theme }: LayoutPropsWithTheme) => ( +export const Layout = withTheme(({ metrics, onChangeRangeTime, theme }: LayoutPropsWithTheme) => ( <React.Fragment> - <Section navLabel="Nginx" sectionLabel="Nginx" metrics={metrics}> + <Section + navLabel="Nginx" + sectionLabel="Nginx" + metrics={metrics} + onChangeRangeTime={onChangeRangeTime} + > <SubSection id="nginxHits" label={i18n.translate( diff --git a/x-pack/plugins/infra/common/saved_objects/inventory_view.ts b/x-pack/plugins/infra/common/saved_objects/inventory_view.ts index bccffadc0a1ba..8ae765f379add 100644 --- a/x-pack/plugins/infra/common/saved_objects/inventory_view.ts +++ b/x-pack/plugins/infra/common/saved_objects/inventory_view.ts @@ -7,7 +7,7 @@ // eslint-disable-next-line @kbn/eslint/no-restricted-paths import { ElasticsearchMappingOf } from '../../server/utils/typed_elasticsearch_mappings'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { WaffleViewState } from '../../public/containers/waffle/with_waffle_view_state'; +import { WaffleViewState } from '../../public/pages/inventory_view/hooks/use_waffle_view_state'; export const inventoryViewSavedObjectType = 'inventory-view'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths @@ -102,7 +102,7 @@ export const inventoryViewSavedObjectMappings: { type: 'boolean', }, time: { - type: 'integer', + type: 'long', }, autoReload: { type: 'boolean', @@ -117,6 +117,12 @@ export const inventoryViewSavedObjectMappings: { }, }, }, + accountId: { + type: 'keyword', + }, + region: { + type: 'keyword', + }, }, }, }; diff --git a/x-pack/plugins/infra/kibana.json b/x-pack/plugins/infra/kibana.json index b8796ad7a358e..a15465a0cde66 100644 --- a/x-pack/plugins/infra/kibana.json +++ b/x-pack/plugins/infra/kibana.json @@ -10,7 +10,7 @@ "home", "data", "dataEnhanced", - "metrics", + "visTypeTimeseries", "alerting", "triggers_actions_ui" ], diff --git a/x-pack/plugins/infra/public/apps/start_app.tsx b/x-pack/plugins/infra/public/apps/start_app.tsx index a986ee6ece352..ebf9562c38d7a 100644 --- a/x-pack/plugins/infra/public/apps/start_app.tsx +++ b/x-pack/plugins/infra/public/apps/start_app.tsx @@ -8,9 +8,6 @@ import { createBrowserHistory } from 'history'; import React from 'react'; import ReactDOM from 'react-dom'; import { ApolloProvider } from 'react-apollo'; -import { Provider as ReduxStoreProvider } from 'react-redux'; -import { BehaviorSubject } from 'rxjs'; -import { pluck } from 'rxjs/operators'; import { CoreStart, AppMountParameters } from 'kibana/public'; // TODO use theme provided from parentApp when kibana supports it @@ -18,9 +15,7 @@ import { EuiErrorBoundary } from '@elastic/eui'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths import { EuiThemeProvider } from '../../../observability/public/typings/eui_styled_components'; import { InfraFrontendLibs } from '../lib/lib'; -import { createStore } from '../store'; import { ApolloClientContext } from '../utils/apollo_context'; -import { ReduxStateContextProvider } from '../utils/redux_context'; import { HistoryContext } from '../utils/history_context'; import { useUiSetting$, @@ -43,12 +38,6 @@ export async function startApp( ) { const { element, appBasePath } = params; const history = createBrowserHistory({ basename: appBasePath }); - const libs$ = new BehaviorSubject(libs); - const store = createStore({ - apolloClient: libs$.pipe(pluck('apolloClient')), - observableApi: libs$.pipe(pluck('observableApi')), - }); - const InfraPluginRoot: React.FunctionComponent = () => { const [darkMode] = useUiSetting$<boolean>('theme:darkMode'); @@ -56,19 +45,15 @@ export async function startApp( <core.i18n.Context> <EuiErrorBoundary> <TriggersActionsProvider triggersActionsUI={triggersActionsUI}> - <ReduxStoreProvider store={store}> - <ReduxStateContextProvider> - <ApolloProvider client={libs.apolloClient}> - <ApolloClientContext.Provider value={libs.apolloClient}> - <EuiThemeProvider darkMode={darkMode}> - <HistoryContext.Provider value={history}> - <Router history={history} /> - </HistoryContext.Provider> - </EuiThemeProvider> - </ApolloClientContext.Provider> - </ApolloProvider> - </ReduxStateContextProvider> - </ReduxStoreProvider> + <ApolloProvider client={libs.apolloClient}> + <ApolloClientContext.Provider value={libs.apolloClient}> + <EuiThemeProvider darkMode={darkMode}> + <HistoryContext.Provider value={history}> + <Router history={history} /> + </HistoryContext.Provider> + </EuiThemeProvider> + </ApolloClientContext.Provider> + </ApolloProvider> </TriggersActionsProvider> </EuiErrorBoundary> </core.i18n.Context> diff --git a/x-pack/plugins/infra/public/components/alerting/metrics/expression.tsx b/x-pack/plugins/infra/public/components/alerting/metrics/expression.tsx index 2e43ede2480ce..0e9da32aaa509 100644 --- a/x-pack/plugins/infra/public/components/alerting/metrics/expression.tsx +++ b/x-pack/plugins/infra/public/components/alerting/metrics/expression.tsx @@ -38,8 +38,8 @@ import { AlertsContextValue } from '../../../../../triggers_actions_ui/public/ap import { MetricsExplorerOptions } from '../../../containers/metrics_explorer/use_metrics_explorer_options'; import { MetricsExplorerKueryBar } from '../../metrics_explorer/kuery_bar'; import { MetricsExplorerSeries } from '../../../../common/http_api/metrics_explorer'; -import { useSource } from '../../../containers/source'; import { MetricsExplorerGroupBy } from '../../metrics_explorer/group_by'; +import { useSourceViaHttp } from '../../../containers/source/use_source_via_http'; interface AlertContextMeta { currentOptions?: Partial<MetricsExplorerOptions>; @@ -84,7 +84,12 @@ const defaultExpression = { export const Expressions: React.FC<Props> = props => { const { setAlertParams, alertParams, errors, alertsContext } = props; - const { source, createDerivedIndexPattern } = useSource({ sourceId: 'default' }); + const { source, createDerivedIndexPattern } = useSourceViaHttp({ + sourceId: 'default', + type: 'metrics', + fetch: alertsContext.http.fetch, + toastWarning: alertsContext.toastNotifications.addWarning, + }); const [timeSize, setTimeSize] = useState<number | undefined>(1); const [timeUnit, setTimeUnit] = useState<TimeUnit>('m'); @@ -205,6 +210,8 @@ export const Expressions: React.FC<Props> = props => { setAlertParams('groupBy', md.currentOptions.groupBy); } setAlertParams('sourceId', source?.id); + } else { + setAlertParams('criteria', [defaultExpression]); } }, [alertsContext.metadata, defaultExpression, source]); // eslint-disable-line react-hooks/exhaustive-deps @@ -262,45 +269,47 @@ export const Expressions: React.FC<Props> = props => { <EuiSpacer size={'m'} /> - <EuiFormRow - label={i18n.translate('xpack.infra.metrics.alertFlyout.filterLabel', { - defaultMessage: 'Filter', - })} - helpText={i18n.translate('xpack.infra.metrics.alertFlyout.filterHelpText', { - defaultMessage: 'Filter help text', - })} - fullWidth - compressed - > - <MetricsExplorerKueryBar - derivedIndexPattern={derivedIndexPattern} - onSubmit={onFilterQuerySubmit} - value={alertParams.filterQuery} - /> - </EuiFormRow> - - <EuiSpacer size={'m'} /> - {alertsContext.metadata && ( - <EuiFormRow - label={i18n.translate('xpack.infra.metrics.alertFlyout.createAlertPerText', { - defaultMessage: 'Create alert per', - })} - helpText={i18n.translate('xpack.infra.metrics.alertFlyout.createAlertPerHelpText', { - defaultMessage: 'Create alert help text', - })} - fullWidth - compressed - > - <MetricsExplorerGroupBy - onChange={onGroupByChange} - fields={derivedIndexPattern.fields} - options={{ - ...options, - groupBy: alertParams.groupBy || undefined, - }} - /> - </EuiFormRow> + <> + <EuiFormRow + label={i18n.translate('xpack.infra.metrics.alertFlyout.filterLabel', { + defaultMessage: 'Filter (optional)', + })} + helpText={i18n.translate('xpack.infra.metrics.alertFlyout.filterHelpText', { + defaultMessage: 'Use a KQL expression to limit the scope of your alert trigger.', + })} + fullWidth + compressed + > + <MetricsExplorerKueryBar + derivedIndexPattern={derivedIndexPattern} + onSubmit={onFilterQuerySubmit} + value={alertParams.filterQuery} + /> + </EuiFormRow> + + <EuiSpacer size={'m'} /> + <EuiFormRow + label={i18n.translate('xpack.infra.metrics.alertFlyout.createAlertPerText', { + defaultMessage: 'Create alert per (optional)', + })} + helpText={i18n.translate('xpack.infra.metrics.alertFlyout.createAlertPerHelpText', { + defaultMessage: + 'Create an alert for every unique value. For example: "host.id" or "cloud.region".', + })} + fullWidth + compressed + > + <MetricsExplorerGroupBy + onChange={onGroupByChange} + fields={derivedIndexPattern.fields} + options={{ + ...options, + groupBy: alertParams.groupBy || undefined, + }} + /> + </EuiFormRow> + </> )} </> ); @@ -338,7 +347,11 @@ export const ExpressionRow: React.FC<ExpressionRowProps> = props => { const updateAggType = useCallback( (at: string) => { - setAlertParams(expressionId, { ...expression, aggType: at as MetricExpression['aggType'] }); + setAlertParams(expressionId, { + ...expression, + aggType: at as MetricExpression['aggType'], + metric: at === 'count' ? undefined : expression.metric, + }); }, [expressionId, expression, setAlertParams] ); @@ -359,7 +372,9 @@ export const ExpressionRow: React.FC<ExpressionRowProps> = props => { const updateThreshold = useCallback( t => { - setAlertParams(expressionId, { ...expression, threshold: t }); + if (t.join() !== expression.threshold.join()) { + setAlertParams(expressionId, { ...expression, threshold: t }); + } }, [expressionId, expression, setAlertParams] ); diff --git a/x-pack/plugins/infra/public/components/inventory/layout.tsx b/x-pack/plugins/infra/public/components/inventory/layout.tsx index 4dd9803c7bfce..3c91f9fa5946f 100644 --- a/x-pack/plugins/infra/public/components/inventory/layout.tsx +++ b/x-pack/plugins/infra/public/components/inventory/layout.tsx @@ -5,64 +5,89 @@ */ import React from 'react'; -import { InfraWaffleMapOptions, InfraWaffleMapBounds } from '../../lib/lib'; -import { KueryFilterQuery } from '../../store/local/waffle_filter'; +import { useInterval } from 'react-use'; +import { euiPaletteColorBlind } from '@elastic/eui'; import { NodesOverview } from '../nodes_overview'; import { Toolbar } from './toolbars/toolbar'; import { PageContent } from '../page'; import { useSnapshot } from '../../containers/waffle/use_snaphot'; import { useInventoryMeta } from '../../containers/inventory_metadata/use_inventory_meta'; -import { SnapshotMetricInput, SnapshotGroupBy } from '../../../common/http_api/snapshot_api'; -import { InventoryItemType } from '../../../common/inventory_models/types'; +import { useWaffleTimeContext } from '../../pages/inventory_view/hooks/use_waffle_time'; +import { useWaffleFiltersContext } from '../../pages/inventory_view/hooks/use_waffle_filters'; +import { useWaffleOptionsContext } from '../../pages/inventory_view/hooks/use_waffle_options'; +import { useSourceContext } from '../../containers/source'; +import { InfraFormatterType, InfraWaffleMapGradientLegend } from '../../lib/lib'; -export interface LayoutProps { - options: InfraWaffleMapOptions; - nodeType: InventoryItemType; - onDrilldown: (filter: KueryFilterQuery) => void; - currentTime: number; - onViewChange: (view: string) => void; - view: string; - boundsOverride: InfraWaffleMapBounds; - autoBounds: boolean; +const euiVisColorPalette = euiPaletteColorBlind(); - filterQuery: string | null | undefined; - metric: SnapshotMetricInput; - groupBy: SnapshotGroupBy; - sourceId: string; - accountId: string; - region: string; -} - -export const Layout = (props: LayoutProps) => { - const { accounts, regions } = useInventoryMeta(props.sourceId, props.nodeType); +export const Layout = () => { + const { sourceId, source } = useSourceContext(); + const { + metric, + groupBy, + nodeType, + accountId, + region, + changeView, + view, + autoBounds, + boundsOverride, + } = useWaffleOptionsContext(); + const { accounts, regions } = useInventoryMeta(sourceId, nodeType); + const { currentTime, jumpToTime, isAutoReloading } = useWaffleTimeContext(); + const { filterQueryAsJson, applyFilterQuery } = useWaffleFiltersContext(); const { loading, nodes, reload, interval } = useSnapshot( - props.filterQuery, - props.metric, - props.groupBy, - props.nodeType, - props.sourceId, - props.currentTime, - props.accountId, - props.region + filterQueryAsJson, + metric, + groupBy, + nodeType, + sourceId, + currentTime, + accountId, + region + ); + + const options = { + formatter: InfraFormatterType.percent, + formatTemplate: '{{value}}', + legend: { + type: 'gradient', + rules: [ + { value: 0, color: '#D3DAE6' }, + { value: 1, color: euiVisColorPalette[1] }, + ], + } as InfraWaffleMapGradientLegend, + metric, + fields: source?.configuration?.fields, + groupBy, + }; + + useInterval( + () => { + if (!loading) { + jumpToTime(Date.now()); + } + }, + isAutoReloading ? 5000 : null ); return ( <> - <Toolbar accounts={accounts} regions={regions} nodeType={props.nodeType} /> + <Toolbar accounts={accounts} regions={regions} nodeType={nodeType} /> <PageContent> <NodesOverview nodes={nodes} - options={props.options} - nodeType={props.nodeType} + options={options} + nodeType={nodeType} loading={loading} reload={reload} - onDrilldown={props.onDrilldown} - currentTime={props.currentTime} - onViewChange={props.onViewChange} - view={props.view} - autoBounds={props.autoBounds} - boundsOverride={props.boundsOverride} + onDrilldown={applyFilterQuery} + currentTime={currentTime} + onViewChange={changeView} + view={view} + autoBounds={autoBounds} + boundsOverride={boundsOverride} interval={interval} /> </PageContent> diff --git a/x-pack/plugins/infra/public/components/inventory/toolbars/save_views.tsx b/x-pack/plugins/infra/public/components/inventory/toolbars/save_views.tsx new file mode 100644 index 0000000000000..cb315d3e17b03 --- /dev/null +++ b/x-pack/plugins/infra/public/components/inventory/toolbars/save_views.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 { SavedViewsToolbarControls } from '../../saved_views/toolbar_control'; +import { inventoryViewSavedObjectType } from '../../../../common/saved_objects/inventory_view'; +import { useWaffleViewState } from '../../../pages/inventory_view/hooks/use_waffle_view_state'; + +export const SavedViews = () => { + const { viewState, defaultViewState, onViewChange } = useWaffleViewState(); + return ( + <SavedViewsToolbarControls + defaultViewState={defaultViewState} + viewState={viewState} + onViewChange={onViewChange} + viewType={inventoryViewSavedObjectType} + /> + ); +}; diff --git a/x-pack/plugins/infra/public/components/inventory/toolbars/toolbar.tsx b/x-pack/plugins/infra/public/components/inventory/toolbars/toolbar.tsx index c59ab994a018c..63ab6d2f4465a 100644 --- a/x-pack/plugins/infra/public/components/inventory/toolbars/toolbar.tsx +++ b/x-pack/plugins/infra/public/components/inventory/toolbars/toolbar.tsx @@ -5,7 +5,6 @@ */ import React, { FunctionComponent } from 'react'; -import { Action } from 'typescript-fsa'; import { EuiFlexItem } from '@elastic/eui'; import { SnapshotMetricInput, @@ -16,33 +15,23 @@ import { InventoryCloudAccount } from '../../../../common/http_api/inventory_met import { findToolbar } from '../../../../common/inventory_models/toolbars'; import { ToolbarWrapper } from './toolbar_wrapper'; -import { waffleOptionsSelectors } from '../../../store'; import { InfraGroupByOptions } from '../../../lib/lib'; -import { WithWaffleViewState } from '../../../containers/waffle/with_waffle_view_state'; -import { SavedViewsToolbarControls } from '../../saved_views/toolbar_control'; -import { inventoryViewSavedObjectType } from '../../../../common/saved_objects/inventory_view'; import { IIndexPattern } from '../../../../../../../src/plugins/data/public'; import { InventoryItemType } from '../../../../common/inventory_models/types'; +import { WaffleOptionsState } from '../../../pages/inventory_view/hooks/use_waffle_options'; +import { SavedViews } from './save_views'; -export interface ToolbarProps { +export interface ToolbarProps + extends Omit<WaffleOptionsState, 'view' | 'boundsOverride' | 'autoBounds'> { createDerivedIndexPattern: (type: 'logs' | 'metrics' | 'both') => IIndexPattern; - changeMetric: (payload: SnapshotMetricInput) => Action<SnapshotMetricInput>; - changeGroupBy: (payload: SnapshotGroupBy) => Action<SnapshotGroupBy>; - changeCustomOptions: (payload: InfraGroupByOptions[]) => Action<InfraGroupByOptions[]>; - changeAccount: (id: string) => Action<string>; - changeRegion: (name: string) => Action<string>; - customOptions: ReturnType<typeof waffleOptionsSelectors.selectCustomOptions>; - groupBy: ReturnType<typeof waffleOptionsSelectors.selectGroupBy>; - metric: ReturnType<typeof waffleOptionsSelectors.selectMetric>; - nodeType: ReturnType<typeof waffleOptionsSelectors.selectNodeType>; - accountId: ReturnType<typeof waffleOptionsSelectors.selectAccountId>; - region: ReturnType<typeof waffleOptionsSelectors.selectRegion>; + changeMetric: (payload: SnapshotMetricInput) => void; + changeGroupBy: (payload: SnapshotGroupBy) => void; + changeCustomOptions: (payload: InfraGroupByOptions[]) => void; + changeAccount: (id: string) => void; + changeRegion: (name: string) => void; accounts: InventoryCloudAccount[]; regions: string[]; - customMetrics: ReturnType<typeof waffleOptionsSelectors.selectCustomMetrics>; - changeCustomMetrics: ( - payload: SnapshotCustomMetricInput[] - ) => Action<SnapshotCustomMetricInput[]>; + changeCustomMetrics: (payload: SnapshotCustomMetricInput[]) => void; } const wrapToolbarItems = ( @@ -57,16 +46,7 @@ const wrapToolbarItems = ( <ToolbarItems {...props} accounts={accounts} regions={regions} /> <EuiFlexItem grow={true} /> <EuiFlexItem grow={false}> - <WithWaffleViewState indexPattern={props.createDerivedIndexPattern('metrics')}> - {({ defaultViewState, viewState, onViewChange }) => ( - <SavedViewsToolbarControls - defaultViewState={defaultViewState} - viewState={viewState} - onViewChange={onViewChange} - viewType={inventoryViewSavedObjectType} - /> - )} - </WithWaffleViewState> + <SavedViews /> </EuiFlexItem> </> )} diff --git a/x-pack/plugins/infra/public/components/inventory/toolbars/toolbar_wrapper.tsx b/x-pack/plugins/infra/public/components/inventory/toolbars/toolbar_wrapper.tsx index 735539d063b01..fefda94372cfb 100644 --- a/x-pack/plugins/infra/public/components/inventory/toolbars/toolbar_wrapper.tsx +++ b/x-pack/plugins/infra/public/components/inventory/toolbars/toolbar_wrapper.tsx @@ -8,58 +8,52 @@ import React from 'react'; import { EuiFlexGroup } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { SnapshotMetricType } from '../../../../common/inventory_models/types'; -import { WithSource } from '../../../containers/with_source'; -import { WithWaffleOptions } from '../../../containers/waffle/with_waffle_options'; import { Toolbar } from '../../eui/toolbar'; import { ToolbarProps } from './toolbar'; import { fieldToName } from '../../waffle/lib/field_to_display_name'; +import { useSourceContext } from '../../../containers/source'; +import { useWaffleOptionsContext } from '../../../pages/inventory_view/hooks/use_waffle_options'; interface Props { children: (props: Omit<ToolbarProps, 'accounts' | 'regions'>) => React.ReactElement; } export const ToolbarWrapper = (props: Props) => { + const { + changeMetric, + changeGroupBy, + changeCustomOptions, + changeAccount, + changeRegion, + customOptions, + groupBy, + metric, + nodeType, + accountId, + region, + customMetrics, + changeCustomMetrics, + } = useWaffleOptionsContext(); + const { createDerivedIndexPattern } = useSourceContext(); return ( <Toolbar> <EuiFlexGroup alignItems="center" gutterSize="m"> - <WithSource> - {({ createDerivedIndexPattern }) => ( - <WithWaffleOptions> - {({ - changeMetric, - changeGroupBy, - changeCustomOptions, - changeAccount, - changeRegion, - customOptions, - groupBy, - metric, - nodeType, - accountId, - region, - customMetrics, - changeCustomMetrics, - }) => - props.children({ - createDerivedIndexPattern, - changeMetric, - changeGroupBy, - changeAccount, - changeRegion, - changeCustomOptions, - customOptions, - groupBy, - metric, - nodeType, - region, - accountId, - customMetrics, - changeCustomMetrics, - }) - } - </WithWaffleOptions> - )} - </WithSource> + {props.children({ + createDerivedIndexPattern, + changeMetric, + changeGroupBy, + changeAccount, + changeRegion, + changeCustomOptions, + customOptions, + groupBy, + metric, + nodeType, + region, + accountId, + customMetrics, + changeCustomMetrics, + })} </EuiFlexGroup> </Toolbar> ); diff --git a/x-pack/plugins/infra/public/components/metrics_explorer/group_by.tsx b/x-pack/plugins/infra/public/components/metrics_explorer/group_by.tsx index 750894fd0188b..3246a2aa4a731 100644 --- a/x-pack/plugins/infra/public/components/metrics_explorer/group_by.tsx +++ b/x-pack/plugins/infra/public/components/metrics_explorer/group_by.tsx @@ -10,7 +10,6 @@ import { i18n } from '@kbn/i18n'; import React, { useCallback } from 'react'; import { IFieldType } from 'src/plugins/data/public'; import { MetricsExplorerOptions } from '../../containers/metrics_explorer/use_metrics_explorer_options'; -import { isDisplayable } from '../../utils/is_displayable'; interface Props { options: MetricsExplorerOptions; @@ -27,18 +26,6 @@ export const MetricsExplorerGroupBy = ({ options, onChange, fields }: Props) => [onChange] ); - const metricPrefixes = options.metrics - .map( - metric => - (metric.field && - metric.field - .split(/\./) - .slice(0, 2) - .join('.')) || - null - ) - .filter(metric => metric) as string[]; - return ( <EuiComboBox placeholder={i18n.translate('xpack.infra.metricsExplorer.groupByLabel', { @@ -51,7 +38,7 @@ export const MetricsExplorerGroupBy = ({ options, onChange, fields }: Props) => singleSelection={true} selectedOptions={(options.groupBy && [{ label: options.groupBy }]) || []} options={fields - .filter(f => isDisplayable(f, metricPrefixes) && f.aggregatable && f.type === 'string') + .filter(f => f.aggregatable && f.type === 'string') .map(f => ({ label: f.name }))} onChange={handleChange} isClearable={true} diff --git a/x-pack/plugins/infra/public/components/metrics_explorer/kuery_bar.tsx b/x-pack/plugins/infra/public/components/metrics_explorer/kuery_bar.tsx index dcc160d05b6ad..7c3e0444dbeea 100644 --- a/x-pack/plugins/infra/public/components/metrics_explorer/kuery_bar.tsx +++ b/x-pack/plugins/infra/public/components/metrics_explorer/kuery_bar.tsx @@ -9,7 +9,6 @@ import { i18n } from '@kbn/i18n'; import React, { useEffect, useState } from 'react'; import { WithKueryAutocompletion } from '../../containers/with_kuery_autocompletion'; import { AutocompleteField } from '../autocomplete_field'; -import { isDisplayable } from '../../utils/is_displayable'; import { esKuery, IIndexPattern } from '../../../../../../src/plugins/data/public'; interface Props { @@ -51,7 +50,7 @@ export const MetricsExplorerKueryBar = ({ const filteredDerivedIndexPattern = { ...derivedIndexPattern, - fields: derivedIndexPattern.fields.filter(field => isDisplayable(field)), + fields: derivedIndexPattern.fields, }; const defaultPlaceholder = i18n.translate( diff --git a/x-pack/plugins/infra/public/components/metrics_explorer/metrics.tsx b/x-pack/plugins/infra/public/components/metrics_explorer/metrics.tsx index 79d4122733c55..a49e42c9cac0e 100644 --- a/x-pack/plugins/infra/public/components/metrics_explorer/metrics.tsx +++ b/x-pack/plugins/infra/public/components/metrics_explorer/metrics.tsx @@ -12,7 +12,6 @@ import { IFieldType } from 'src/plugins/data/public'; import { colorTransformer, MetricsExplorerColor } from '../../../common/color_palette'; import { MetricsExplorerMetric } from '../../../common/http_api/metrics_explorer'; import { MetricsExplorerOptions } from '../../containers/metrics_explorer/use_metrics_explorer_options'; -import { isDisplayable } from '../../utils/is_displayable'; interface Props { autoFocus?: boolean; @@ -54,9 +53,7 @@ export const MetricsExplorerMetrics = ({ options, onChange, fields, autoFocus = [onChange, options.aggregation, colors] ); - const comboOptions = fields - .filter(field => isDisplayable(field)) - .map(field => ({ label: field.name, value: field.name })); + const comboOptions = fields.map(field => ({ label: field.name, value: field.name })); const selectedOptions = options.metrics .filter(m => m.aggregation !== 'count') .map(metric => ({ diff --git a/x-pack/plugins/infra/public/components/nodes_overview/index.tsx b/x-pack/plugins/infra/public/components/nodes_overview/index.tsx index 4d61568a63b9f..ef22e0486f892 100644 --- a/x-pack/plugins/infra/public/components/nodes_overview/index.tsx +++ b/x-pack/plugins/infra/public/components/nodes_overview/index.tsx @@ -12,7 +12,6 @@ import React from 'react'; import { euiStyled } from '../../../../observability/public'; import { InfraFormatterType, InfraWaffleMapBounds, InfraWaffleMapOptions } from '../../lib/lib'; -import { KueryFilterQuery } from '../../store/local/waffle_filter'; import { createFormatter } from '../../utils/formatters'; import { NoData } from '../empty_states'; import { InfraLoadingPanel } from '../loading'; @@ -24,6 +23,11 @@ import { convertIntervalToString } from '../../utils/convert_interval_to_string' import { InventoryItemType } from '../../../common/inventory_models/types'; import { createFormatterForMetric } from '../metrics_explorer/helpers/create_formatter_for_metric'; +export interface KueryFilterQuery { + kind: 'kuery'; + expression: string; +} + interface Props { options: InfraWaffleMapOptions; nodeType: InventoryItemType; diff --git a/x-pack/plugins/infra/public/components/waffle/legend.tsx b/x-pack/plugins/infra/public/components/waffle/legend.tsx index de070efb35a1f..13e533b225d4d 100644 --- a/x-pack/plugins/infra/public/components/waffle/legend.tsx +++ b/x-pack/plugins/infra/public/components/waffle/legend.tsx @@ -6,12 +6,12 @@ import React from 'react'; import { euiStyled } from '../../../../observability/public'; -import { WithWaffleOptions } from '../../containers/waffle/with_waffle_options'; import { InfraFormatter, InfraWaffleMapBounds, InfraWaffleMapLegend } from '../../lib/lib'; import { GradientLegend } from './gradient_legend'; import { LegendControls } from './legend_controls'; import { isInfraWaffleMapGradientLegend, isInfraWaffleMapStepLegend } from './lib/type_guards'; import { StepLegend } from './steps_legend'; +import { useWaffleOptionsContext } from '../../pages/inventory_view/hooks/use_waffle_options'; interface Props { legend: InfraWaffleMapLegend; bounds: InfraWaffleMapBounds; @@ -25,22 +25,24 @@ interface LegendControlOptions { } export const Legend: React.FC<Props> = ({ dataBounds, legend, bounds, formatter }) => { + const { + changeBoundsOverride, + changeAutoBounds, + autoBounds, + boundsOverride, + } = useWaffleOptionsContext(); return ( <LegendContainer> - <WithWaffleOptions> - {({ changeBoundsOverride, changeAutoBounds, autoBounds, boundsOverride }) => ( - <LegendControls - dataBounds={dataBounds} - bounds={bounds} - autoBounds={autoBounds} - boundsOverride={boundsOverride} - onChange={(options: LegendControlOptions) => { - changeBoundsOverride(options.bounds); - changeAutoBounds(options.auto); - }} - /> - )} - </WithWaffleOptions> + <LegendControls + dataBounds={dataBounds} + bounds={bounds} + autoBounds={autoBounds} + boundsOverride={boundsOverride} + onChange={(options: LegendControlOptions) => { + changeBoundsOverride(options.bounds); + changeAutoBounds(options.auto); + }} + /> {isInfraWaffleMapGradientLegend(legend) && ( <GradientLegend formatter={formatter} legend={legend} bounds={bounds} /> )} diff --git a/x-pack/plugins/infra/public/components/waffle/lib/create_uptime_link.test.ts b/x-pack/plugins/infra/public/components/waffle/lib/create_uptime_link.test.ts index 18e5838a15b56..902969c83ba39 100644 --- a/x-pack/plugins/infra/public/components/waffle/lib/create_uptime_link.test.ts +++ b/x-pack/plugins/infra/public/components/waffle/lib/create_uptime_link.test.ts @@ -5,11 +5,7 @@ */ import { createUptimeLink } from './create_uptime_link'; -import { - InfraWaffleMapOptions, - InfraWaffleMapLegendMode, - InfraFormatterType, -} from '../../../lib/lib'; +import { InfraWaffleMapOptions, InfraFormatterType } from '../../../lib/lib'; import { SnapshotMetricType } from '../../../../common/inventory_models/types'; const options: InfraWaffleMapOptions = { @@ -26,7 +22,7 @@ const options: InfraWaffleMapOptions = { metric: { type: 'cpu' }, groupBy: [], legend: { - type: InfraWaffleMapLegendMode.gradient, + type: 'gradient', rules: [], }, }; diff --git a/x-pack/plugins/infra/public/components/waffle/lib/type_guards.ts b/x-pack/plugins/infra/public/components/waffle/lib/type_guards.ts index aff16374ae262..f793afee1b948 100644 --- a/x-pack/plugins/infra/public/components/waffle/lib/type_guards.ts +++ b/x-pack/plugins/infra/public/components/waffle/lib/type_guards.ts @@ -4,16 +4,14 @@ * you may not use this file except in compliance with the Elastic License. */ -import { - InfraWaffleMapGradientLegend, - InfraWaffleMapLegendMode, - InfraWaffleMapStepLegend, -} from '../../../lib/lib'; +import { InfraWaffleMapGradientLegend, InfraWaffleMapStepLegend } from '../../../lib/lib'; + export function isInfraWaffleMapStepLegend(subject: any): subject is InfraWaffleMapStepLegend { - return subject.type && subject.type === InfraWaffleMapLegendMode.step; + return subject.type && subject.type === 'step'; } + export function isInfraWaffleMapGradientLegend( subject: any ): subject is InfraWaffleMapGradientLegend { - return subject.type && subject.type === InfraWaffleMapLegendMode.gradient; + return subject.type && subject.type === 'gradient'; } diff --git a/x-pack/plugins/infra/public/components/waffle/waffle_inventory_switcher.tsx b/x-pack/plugins/infra/public/components/waffle/waffle_inventory_switcher.tsx index d265b418f010d..21da10a0a7650 100644 --- a/x-pack/plugins/infra/public/components/waffle/waffle_inventory_switcher.tsx +++ b/x-pack/plugins/infra/public/components/waffle/waffle_inventory_switcher.tsx @@ -16,36 +16,23 @@ import React, { useCallback, useState, useMemo } from 'react'; import { FormattedMessage } from '@kbn/i18n/react'; import { findInventoryModel } from '../../../common/inventory_models'; import { InventoryItemType } from '../../../common/inventory_models/types'; -import { - SnapshotMetricInput, - SnapshotGroupBy, - SnapshotCustomMetricInput, -} from '../../../common/http_api/snapshot_api'; - -interface WaffleInventorySwitcherProps { - nodeType: InventoryItemType; - changeNodeType: (nodeType: InventoryItemType) => void; - changeGroupBy: (groupBy: SnapshotGroupBy) => void; - changeMetric: (metric: SnapshotMetricInput) => void; - changeCustomMetrics: (metrics: SnapshotCustomMetricInput[]) => void; - changeAccount: (id: string) => void; - changeRegion: (name: string) => void; -} +import { useWaffleOptionsContext } from '../../pages/inventory_view/hooks/use_waffle_options'; const getDisplayNameForType = (type: InventoryItemType) => { const inventoryModel = findInventoryModel(type); return inventoryModel.displayName; }; -export const WaffleInventorySwitcher: React.FC<WaffleInventorySwitcherProps> = ({ - changeNodeType, - changeGroupBy, - changeMetric, - changeAccount, - changeRegion, - changeCustomMetrics, - nodeType, -}) => { +export const WaffleInventorySwitcher: React.FC = () => { + const { + changeNodeType, + changeGroupBy, + changeMetric, + changeAccount, + changeRegion, + changeCustomMetrics, + nodeType, + } = useWaffleOptionsContext(); const [isOpen, setIsOpen] = useState(false); const closePopover = useCallback(() => setIsOpen(false), []); const openPopover = useCallback(() => setIsOpen(true), []); diff --git a/x-pack/plugins/infra/public/components/waffle/waffle_time_controls.tsx b/x-pack/plugins/infra/public/components/waffle/waffle_time_controls.tsx index 4f840336de8c3..458bb674afade 100644 --- a/x-pack/plugins/infra/public/components/waffle/waffle_time_controls.tsx +++ b/x-pack/plugins/infra/public/components/waffle/waffle_time_controls.tsx @@ -7,84 +7,60 @@ import { EuiButtonEmpty, EuiDatePicker, EuiFormControlLayout } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; import moment, { Moment } from 'moment'; -import React from 'react'; -import { Action } from 'typescript-fsa'; +import React, { useCallback } from 'react'; +import { useWaffleTimeContext } from '../../pages/inventory_view/hooks/use_waffle_time'; -interface WaffleTimeControlsProps { - currentTime: number; - isLiveStreaming?: boolean; - onChangeTime?: (time: number) => void; - startLiveStreaming?: (payload: void) => Action<void>; - stopLiveStreaming?: (payload: void) => Action<void>; -} +export const WaffleTimeControls = () => { + const { + currentTime, + isAutoReloading, + startAutoReload, + stopAutoReload, + jumpToTime, + } = useWaffleTimeContext(); -export class WaffleTimeControls extends React.Component<WaffleTimeControlsProps> { - public render() { - const { currentTime, isLiveStreaming } = this.props; + const currentMoment = moment(currentTime); - const currentMoment = moment(currentTime); + const liveStreamingButton = isAutoReloading ? ( + <EuiButtonEmpty color="primary" iconSide="left" iconType="pause" onClick={stopAutoReload}> + <FormattedMessage + id="xpack.infra.waffleTime.stopRefreshingButtonLabel" + defaultMessage="Stop refreshing" + /> + </EuiButtonEmpty> + ) : ( + <EuiButtonEmpty iconSide="left" iconType="play" onClick={startAutoReload}> + <FormattedMessage + id="xpack.infra.waffleTime.autoRefreshButtonLabel" + defaultMessage="Auto-refresh" + /> + </EuiButtonEmpty> + ); - const liveStreamingButton = isLiveStreaming ? ( - <EuiButtonEmpty - color="primary" - iconSide="left" - iconType="pause" - onClick={this.stopLiveStreaming} - > - <FormattedMessage - id="xpack.infra.waffleTime.stopRefreshingButtonLabel" - defaultMessage="Stop refreshing" - /> - </EuiButtonEmpty> - ) : ( - <EuiButtonEmpty iconSide="left" iconType="play" onClick={this.startLiveStreaming}> - <FormattedMessage - id="xpack.infra.waffleTime.autoRefreshButtonLabel" - defaultMessage="Auto-refresh" - /> - </EuiButtonEmpty> - ); + const handleChangeDate = useCallback( + (time: Moment | null) => { + if (time) { + jumpToTime(time.valueOf()); + } + }, + [jumpToTime] + ); - return ( - <EuiFormControlLayout append={liveStreamingButton} data-test-subj="waffleDatePicker"> - <EuiDatePicker - className="euiFieldText--inGroup" - dateFormat="L LTS" - disabled={isLiveStreaming} - injectTimes={currentMoment ? [currentMoment] : []} - isLoading={isLiveStreaming} - onChange={this.handleChangeDate} - popperPlacement="top-end" - selected={currentMoment} - shouldCloseOnSelect - showTimeSelect - timeFormat="LT" - /> - </EuiFormControlLayout> - ); - } - - private handleChangeDate = (time: Moment | null) => { - const { onChangeTime } = this.props; - - if (onChangeTime && time) { - onChangeTime(time.valueOf()); - } - }; - - private startLiveStreaming = () => { - const { startLiveStreaming } = this.props; - - if (startLiveStreaming) { - startLiveStreaming(); - } - }; - - private stopLiveStreaming = () => { - const { stopLiveStreaming } = this.props; - - if (stopLiveStreaming) { - stopLiveStreaming(); - } - }; -} + return ( + <EuiFormControlLayout append={liveStreamingButton} data-test-subj="waffleDatePicker"> + <EuiDatePicker + className="euiFieldText--inGroup" + dateFormat="L LTS" + disabled={isAutoReloading} + injectTimes={currentMoment ? [currentMoment] : []} + isLoading={isAutoReloading} + onChange={handleChangeDate} + popperPlacement="top-end" + selected={currentMoment} + shouldCloseOnSelect + showTimeSelect + timeFormat="LT" + /> + </EuiFormControlLayout> + ); +}; diff --git a/x-pack/plugins/infra/public/containers/metadata/use_metadata.ts b/x-pack/plugins/infra/public/containers/metadata/use_metadata.ts index 52c522ce8efd4..1ba016195bef4 100644 --- a/x-pack/plugins/infra/public/containers/metadata/use_metadata.ts +++ b/x-pack/plugins/infra/public/containers/metadata/use_metadata.ts @@ -13,17 +13,18 @@ import { useHTTPRequest } from '../../hooks/use_http_request'; import { throwErrors, createPlainError } from '../../../common/runtime_types'; import { InventoryMetric, InventoryItemType } from '../../../common/inventory_models/types'; import { getFilteredMetrics } from './lib/get_filtered_metrics'; +import { MetricsTimeInput } from '../../pages/metrics/hooks/use_metrics_time'; export function useMetadata( nodeId: string, nodeType: InventoryItemType, requiredMetrics: InventoryMetric[], - sourceId: string + sourceId: string, + timeRange: MetricsTimeInput ) { const decodeResponse = (response: any) => { return pipe(InfraMetadataRT.decode(response), fold(throwErrors(createPlainError), identity)); }; - const { error, loading, response, makeRequest } = useHTTPRequest<InfraMetadata>( '/api/infra/metadata', 'POST', @@ -31,6 +32,7 @@ export function useMetadata( nodeId, nodeType, sourceId, + timeRange: { from: timeRange.from, to: timeRange.to }, }), decodeResponse ); diff --git a/x-pack/plugins/infra/public/containers/source/source.tsx b/x-pack/plugins/infra/public/containers/source/source.tsx index a1310b1f33614..c2206769ef0ef 100644 --- a/x-pack/plugins/infra/public/containers/source/source.tsx +++ b/x-pack/plugins/infra/public/containers/source/source.tsx @@ -21,7 +21,7 @@ import { updateSourceMutation } from './update_source.gql_query'; type Source = SourceQuery.Query['source']; -const pickIndexPattern = (source: Source | undefined, type: 'logs' | 'metrics' | 'both') => { +export const pickIndexPattern = (source: Source | undefined, type: 'logs' | 'metrics' | 'both') => { if (!source) { return 'unknown-index'; } diff --git a/x-pack/plugins/infra/public/containers/source/use_source_via_http.ts b/x-pack/plugins/infra/public/containers/source/use_source_via_http.ts new file mode 100644 index 0000000000000..bc6374a6538e3 --- /dev/null +++ b/x-pack/plugins/infra/public/containers/source/use_source_via_http.ts @@ -0,0 +1,92 @@ +/* + * 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 { useEffect, useMemo } from 'react'; +import { fold } from 'fp-ts/lib/Either'; +import { identity } from 'fp-ts/lib/function'; +import { pipe } from 'fp-ts/lib/pipeable'; +import createContainer from 'constate'; +import { HttpHandler } from 'target/types/core/public/http'; +import { ToastInput } from 'target/types/core/public/notifications/toasts/toasts_api'; +import { + SourceResponseRuntimeType, + SourceResponse, + InfraSource, +} from '../../../common/http_api/source_api'; +import { useHTTPRequest } from '../../hooks/use_http_request'; +import { throwErrors, createPlainError } from '../../../common/runtime_types'; + +export const pickIndexPattern = ( + source: InfraSource | undefined, + type: 'logs' | 'metrics' | 'both' +) => { + if (!source) { + return 'unknown-index'; + } + if (type === 'logs') { + return source.configuration.logAlias; + } + if (type === 'metrics') { + return source.configuration.metricAlias; + } + return `${source.configuration.logAlias},${source.configuration.metricAlias}`; +}; + +interface Props { + sourceId: string; + type: 'logs' | 'metrics' | 'both'; + fetch?: HttpHandler; + toastWarning?: (input: ToastInput) => void; +} + +export const useSourceViaHttp = ({ + sourceId = 'default', + type = 'both', + fetch, + toastWarning, +}: Props) => { + const decodeResponse = (response: any) => { + return pipe( + SourceResponseRuntimeType.decode(response), + fold(throwErrors(createPlainError), identity) + ); + }; + + const { error, loading, response, makeRequest } = useHTTPRequest<SourceResponse>( + `/api/metrics/source/${sourceId}/${type}`, + 'GET', + null, + decodeResponse, + fetch, + toastWarning + ); + + useEffect(() => { + (async () => { + await makeRequest(); + })(); + }, [makeRequest]); + + const createDerivedIndexPattern = (indexType: 'logs' | 'metrics' | 'both' = type) => { + return { + fields: response?.source ? response.status.indexFields : [], + title: pickIndexPattern(response?.source, indexType), + }; + }; + + const source = useMemo(() => { + return response ? { ...response.source, status: response.status } : null; + }, [response]); + + return { + createDerivedIndexPattern, + source, + loading, + error, + }; +}; + +export const SourceViaHttp = createContainer(useSourceViaHttp); +export const [SourceViaHttpProvider, useSourceViaHttpContext] = SourceViaHttp; diff --git a/x-pack/plugins/infra/public/containers/waffle/index.ts b/x-pack/plugins/infra/public/containers/waffle/index.ts deleted file mode 100644 index 40c4bfc8cf678..0000000000000 --- a/x-pack/plugins/infra/public/containers/waffle/index.ts +++ /dev/null @@ -1,7 +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. - */ - -export * from './with_waffle_filters'; diff --git a/x-pack/plugins/infra/public/containers/waffle/waffle_nodes.gql_query.ts b/x-pack/plugins/infra/public/containers/waffle/waffle_nodes.gql_query.ts deleted file mode 100644 index 1ca6bc8c397e5..0000000000000 --- a/x-pack/plugins/infra/public/containers/waffle/waffle_nodes.gql_query.ts +++ /dev/null @@ -1,37 +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 gql from 'graphql-tag'; - -export const waffleNodesQuery = gql` - query WaffleNodesQuery( - $sourceId: ID! - $timerange: InfraTimerangeInput! - $filterQuery: String - $metric: InfraSnapshotMetricInput! - $groupBy: [InfraSnapshotGroupbyInput!]! - $type: InfraNodeType! - ) { - source(id: $sourceId) { - id - snapshot(timerange: $timerange, filterQuery: $filterQuery) { - nodes(groupBy: $groupBy, metric: $metric, type: $type) { - path { - value - label - ip - } - metric { - name - value - avg - max - } - } - } - } - } -`; diff --git a/x-pack/plugins/infra/public/containers/waffle/with_waffle_filters.tsx b/x-pack/plugins/infra/public/containers/waffle/with_waffle_filters.tsx deleted file mode 100644 index 0214237ef52d8..0000000000000 --- a/x-pack/plugins/infra/public/containers/waffle/with_waffle_filters.tsx +++ /dev/null @@ -1,96 +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 from 'react'; -import { connect } from 'react-redux'; - -import { IIndexPattern } from 'src/plugins/data/public'; -import { State, waffleFilterActions, waffleFilterSelectors } from '../../store'; -import { FilterQuery } from '../../store/local/waffle_filter'; -import { convertKueryToElasticSearchQuery } from '../../utils/kuery'; -import { asChildFunctionRenderer } from '../../utils/typed_react'; -import { bindPlainActionCreators } from '../../utils/typed_redux'; -import { UrlStateContainer } from '../../utils/url_state'; - -interface WithWaffleFilterProps { - indexPattern: IIndexPattern; -} - -export const withWaffleFilter = connect( - (state: State) => ({ - filterQuery: waffleFilterSelectors.selectWaffleFilterQuery(state), - filterQueryDraft: waffleFilterSelectors.selectWaffleFilterQueryDraft(state), - filterQueryAsJson: waffleFilterSelectors.selectWaffleFilterQueryAsJson(state), - isFilterQueryDraftValid: waffleFilterSelectors.selectIsWaffleFilterQueryDraftValid(state), - }), - (dispatch, ownProps: WithWaffleFilterProps) => - bindPlainActionCreators({ - applyFilterQuery: (query: FilterQuery) => - waffleFilterActions.applyWaffleFilterQuery({ - query, - serializedQuery: convertKueryToElasticSearchQuery( - query.expression, - ownProps.indexPattern - ), - }), - applyFilterQueryFromKueryExpression: (expression: string) => - waffleFilterActions.applyWaffleFilterQuery({ - query: { - kind: 'kuery', - expression, - }, - serializedQuery: convertKueryToElasticSearchQuery(expression, ownProps.indexPattern), - }), - setFilterQueryDraft: waffleFilterActions.setWaffleFilterQueryDraft, - setFilterQueryDraftFromKueryExpression: (expression: string) => - waffleFilterActions.setWaffleFilterQueryDraft({ - kind: 'kuery', - expression, - }), - }) -); - -export const WithWaffleFilter = asChildFunctionRenderer(withWaffleFilter); - -/** - * Url State - */ - -type WaffleFilterUrlState = ReturnType<typeof waffleFilterSelectors.selectWaffleFilterQuery>; - -type WithWaffleFilterUrlStateProps = WithWaffleFilterProps; - -export const WithWaffleFilterUrlState: React.FC<WithWaffleFilterUrlStateProps> = ({ - indexPattern, -}) => ( - <WithWaffleFilter indexPattern={indexPattern}> - {({ applyFilterQuery, filterQuery }) => ( - <UrlStateContainer - urlState={filterQuery} - urlStateKey="waffleFilter" - mapToUrlState={mapToUrlState} - onChange={urlState => { - if (urlState) { - applyFilterQuery(urlState); - } - }} - onInitialize={urlState => { - if (urlState) { - applyFilterQuery(urlState); - } - }} - /> - )} - </WithWaffleFilter> -); - -const mapToUrlState = (value: any): WaffleFilterUrlState | undefined => - value && value.kind === 'kuery' && typeof value.expression === 'string' - ? { - kind: value.kind, - expression: value.expression, - } - : undefined; diff --git a/x-pack/plugins/infra/public/containers/waffle/with_waffle_options.tsx b/x-pack/plugins/infra/public/containers/waffle/with_waffle_options.tsx deleted file mode 100644 index 47dd6a5a73a73..0000000000000 --- a/x-pack/plugins/infra/public/containers/waffle/with_waffle_options.tsx +++ /dev/null @@ -1,265 +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 from 'react'; -import { connect } from 'react-redux'; -import { createSelector } from 'reselect'; - -import { isBoolean, isNumber } from 'lodash'; -import { InfraGroupByOptions } from '../../lib/lib'; -import { State, waffleOptionsActions, waffleOptionsSelectors } from '../../store'; -import { asChildFunctionRenderer } from '../../utils/typed_react'; -import { bindPlainActionCreators } from '../../utils/typed_redux'; -import { UrlStateContainer } from '../../utils/url_state'; -import { - SnapshotMetricInput, - SnapshotGroupBy, - SnapshotCustomMetricInputRT, -} from '../../../common/http_api/snapshot_api'; -import { - SnapshotMetricTypeRT, - InventoryItemType, - ItemTypeRT, -} from '../../../common/inventory_models/types'; - -const selectOptionsUrlState = createSelector( - waffleOptionsSelectors.selectMetric, - waffleOptionsSelectors.selectView, - waffleOptionsSelectors.selectGroupBy, - waffleOptionsSelectors.selectNodeType, - waffleOptionsSelectors.selectCustomOptions, - waffleOptionsSelectors.selectBoundsOverride, - waffleOptionsSelectors.selectAutoBounds, - waffleOptionsSelectors.selectAccountId, - waffleOptionsSelectors.selectRegion, - waffleOptionsSelectors.selectCustomMetrics, - ( - metric, - view, - groupBy, - nodeType, - customOptions, - boundsOverride, - autoBounds, - accountId, - region, - customMetrics - ) => ({ - metric, - groupBy, - nodeType, - view, - customOptions, - boundsOverride, - autoBounds, - accountId, - region, - customMetrics, - }) -); - -export const withWaffleOptions = connect( - (state: State) => ({ - metric: waffleOptionsSelectors.selectMetric(state), - groupBy: waffleOptionsSelectors.selectGroupBy(state), - nodeType: waffleOptionsSelectors.selectNodeType(state), - view: waffleOptionsSelectors.selectView(state), - customOptions: waffleOptionsSelectors.selectCustomOptions(state), - boundsOverride: waffleOptionsSelectors.selectBoundsOverride(state), - autoBounds: waffleOptionsSelectors.selectAutoBounds(state), - accountId: waffleOptionsSelectors.selectAccountId(state), - region: waffleOptionsSelectors.selectRegion(state), - urlState: selectOptionsUrlState(state), - customMetrics: waffleOptionsSelectors.selectCustomMetrics(state), - }), - bindPlainActionCreators({ - changeMetric: waffleOptionsActions.changeMetric, - changeGroupBy: waffleOptionsActions.changeGroupBy, - changeNodeType: waffleOptionsActions.changeNodeType, - changeView: waffleOptionsActions.changeView, - changeCustomOptions: waffleOptionsActions.changeCustomOptions, - changeBoundsOverride: waffleOptionsActions.changeBoundsOverride, - changeAutoBounds: waffleOptionsActions.changeAutoBounds, - changeAccount: waffleOptionsActions.changeAccount, - changeRegion: waffleOptionsActions.changeRegion, - changeCustomMetrics: waffleOptionsActions.changeCustomMetrics, - }) -); - -export const WithWaffleOptions = asChildFunctionRenderer(withWaffleOptions); - -/** - * Url State - */ - -interface WaffleOptionsUrlState { - metric?: ReturnType<typeof waffleOptionsSelectors.selectMetric>; - groupBy?: ReturnType<typeof waffleOptionsSelectors.selectGroupBy>; - nodeType?: ReturnType<typeof waffleOptionsSelectors.selectNodeType>; - view?: ReturnType<typeof waffleOptionsSelectors.selectView>; - customOptions?: ReturnType<typeof waffleOptionsSelectors.selectCustomOptions>; - bounds?: ReturnType<typeof waffleOptionsSelectors.selectBoundsOverride>; - auto?: ReturnType<typeof waffleOptionsSelectors.selectAutoBounds>; - accountId?: ReturnType<typeof waffleOptionsSelectors.selectAccountId>; - region?: ReturnType<typeof waffleOptionsSelectors.selectRegion>; - customMetrics?: ReturnType<typeof waffleOptionsSelectors.selectCustomMetrics>; -} - -export const WithWaffleOptionsUrlState = () => ( - <WithWaffleOptions> - {({ - changeMetric, - urlState, - changeGroupBy, - changeNodeType, - changeView, - changeCustomOptions, - changeAutoBounds, - changeBoundsOverride, - changeAccount, - changeRegion, - changeCustomMetrics, - }) => ( - <UrlStateContainer<WaffleOptionsUrlState> - urlState={urlState} - urlStateKey="waffleOptions" - mapToUrlState={mapToUrlState} - onChange={newUrlState => { - if (newUrlState && newUrlState.metric) { - changeMetric(newUrlState.metric); - } - if (newUrlState && newUrlState.groupBy) { - changeGroupBy(newUrlState.groupBy); - } - if (newUrlState && newUrlState.nodeType) { - changeNodeType(newUrlState.nodeType); - } - if (newUrlState && newUrlState.view) { - changeView(newUrlState.view); - } - if (newUrlState && newUrlState.customOptions) { - changeCustomOptions(newUrlState.customOptions); - } - if (newUrlState && newUrlState.bounds) { - changeBoundsOverride(newUrlState.bounds); - } - if (newUrlState && newUrlState.auto) { - changeAutoBounds(newUrlState.auto); - } - if (newUrlState && newUrlState.accountId) { - changeAccount(newUrlState.accountId); - } - if (newUrlState && newUrlState.region) { - changeRegion(newUrlState.region); - } - if (newUrlState && newUrlState.customMetrics) { - changeCustomMetrics(newUrlState.customMetrics); - } - }} - onInitialize={initialUrlState => { - if (initialUrlState && initialUrlState.metric) { - changeMetric(initialUrlState.metric); - } - if (initialUrlState && initialUrlState.groupBy) { - changeGroupBy(initialUrlState.groupBy); - } - if (initialUrlState && initialUrlState.nodeType) { - changeNodeType(initialUrlState.nodeType); - } - if (initialUrlState && initialUrlState.view) { - changeView(initialUrlState.view); - } - if (initialUrlState && initialUrlState.customOptions) { - changeCustomOptions(initialUrlState.customOptions); - } - if (initialUrlState && initialUrlState.bounds) { - changeBoundsOverride(initialUrlState.bounds); - } - if (initialUrlState && initialUrlState.auto) { - changeAutoBounds(initialUrlState.auto); - } - if (initialUrlState && initialUrlState.accountId) { - changeAccount(initialUrlState.accountId); - } - if (initialUrlState && initialUrlState.region) { - changeRegion(initialUrlState.region); - } - if (initialUrlState && initialUrlState.customMetrics) { - changeCustomMetrics(initialUrlState.customMetrics); - } - }} - /> - )} - </WithWaffleOptions> -); - -const mapToUrlState = (value: any): WaffleOptionsUrlState | undefined => - value - ? { - metric: mapToMetricUrlState(value.metric), - groupBy: mapToGroupByUrlState(value.groupBy), - nodeType: mapToNodeTypeUrlState(value.nodeType), - view: mapToViewUrlState(value.view), - customOptions: mapToCustomOptionsUrlState(value.customOptions), - bounds: mapToBoundsOverideUrlState(value.boundsOverride), - auto: mapToAutoBoundsUrlState(value.autoBounds), - accountId: value.accountId, - region: value.region, - customMetrics: mapToCustomMetricsUrlState(value.customMetrics), - } - : undefined; - -const isInfraNodeType = (value: any): value is InventoryItemType => value in ItemTypeRT; - -const isInfraSnapshotMetricInput = (subject: any): subject is SnapshotMetricInput => { - return subject != null && subject.type in SnapshotMetricTypeRT; -}; - -const isInfraSnapshotGroupbyInput = (subject: any): subject is SnapshotGroupBy => { - return subject != null && subject.type != null; -}; - -const isInfraGroupByOption = (subject: any): subject is InfraGroupByOptions => { - return subject != null && subject.text != null && subject.field != null; -}; - -const mapToMetricUrlState = (subject: any) => { - return subject && isInfraSnapshotMetricInput(subject) ? subject : undefined; -}; - -const mapToGroupByUrlState = (subject: any) => { - return subject && Array.isArray(subject) && subject.every(isInfraSnapshotGroupbyInput) - ? subject - : undefined; -}; - -const mapToNodeTypeUrlState = (subject: any) => { - return isInfraNodeType(subject) ? subject : undefined; -}; - -const mapToViewUrlState = (subject: any) => { - return subject && ['map', 'table'].includes(subject) ? subject : undefined; -}; - -const mapToCustomOptionsUrlState = (subject: any) => { - return subject && Array.isArray(subject) && subject.every(isInfraGroupByOption) - ? subject - : undefined; -}; - -const mapToCustomMetricsUrlState = (subject: any) => { - return subject && Array.isArray(subject) && subject.every(s => SnapshotCustomMetricInputRT.is(s)) - ? subject - : []; -}; - -const mapToBoundsOverideUrlState = (subject: any) => { - return subject != null && isNumber(subject.max) && isNumber(subject.min) ? subject : undefined; -}; - -const mapToAutoBoundsUrlState = (subject: any) => { - return subject != null && isBoolean(subject) ? subject : undefined; -}; diff --git a/x-pack/plugins/infra/public/containers/waffle/with_waffle_time.tsx b/x-pack/plugins/infra/public/containers/waffle/with_waffle_time.tsx deleted file mode 100644 index 293f6184af21b..0000000000000 --- a/x-pack/plugins/infra/public/containers/waffle/with_waffle_time.tsx +++ /dev/null @@ -1,96 +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 from 'react'; -import { connect } from 'react-redux'; -import { createSelector } from 'reselect'; - -import { State, waffleTimeActions, waffleTimeSelectors } from '../../store'; -import { asChildFunctionRenderer } from '../../utils/typed_react'; -import { bindPlainActionCreators } from '../../utils/typed_redux'; -import { UrlStateContainer } from '../../utils/url_state'; - -export const withWaffleTime = connect( - (state: State) => ({ - currentTime: waffleTimeSelectors.selectCurrentTime(state), - currentTimeRange: waffleTimeSelectors.selectCurrentTimeRange(state), - isAutoReloading: waffleTimeSelectors.selectIsAutoReloading(state), - urlState: selectTimeUrlState(state), - }), - bindPlainActionCreators({ - jumpToTime: waffleTimeActions.jumpToTime, - startAutoReload: waffleTimeActions.startAutoReload, - stopAutoReload: waffleTimeActions.stopAutoReload, - }) -); - -export const WithWaffleTime = asChildFunctionRenderer(withWaffleTime, { - onCleanup: ({ stopAutoReload }) => stopAutoReload(), -}); - -/** - * Url State - */ - -interface WaffleTimeUrlState { - time?: ReturnType<typeof waffleTimeSelectors.selectCurrentTime>; - autoReload?: ReturnType<typeof waffleTimeSelectors.selectIsAutoReloading>; -} - -export const WithWaffleTimeUrlState = () => ( - <WithWaffleTime> - {({ jumpToTime, startAutoReload, stopAutoReload, urlState }) => ( - <UrlStateContainer - urlState={urlState} - urlStateKey="waffleTime" - mapToUrlState={mapToUrlState} - onChange={newUrlState => { - if (newUrlState && newUrlState.time) { - jumpToTime(newUrlState.time); - } - if (newUrlState && newUrlState.autoReload) { - startAutoReload(); - } else if ( - newUrlState && - typeof newUrlState.autoReload !== 'undefined' && - !newUrlState.autoReload - ) { - stopAutoReload(); - } - }} - onInitialize={initialUrlState => { - if (initialUrlState) { - jumpToTime(initialUrlState.time ? initialUrlState.time : Date.now()); - } - if (initialUrlState && initialUrlState.autoReload) { - startAutoReload(); - } - }} - /> - )} - </WithWaffleTime> -); - -const selectTimeUrlState = createSelector( - waffleTimeSelectors.selectCurrentTime, - waffleTimeSelectors.selectIsAutoReloading, - (time, autoReload) => ({ - time, - autoReload, - }) -); - -const mapToUrlState = (value: any): WaffleTimeUrlState | undefined => - value - ? { - time: mapToTimeUrlState(value.time), - autoReload: mapToAutoReloadUrlState(value.autoReload), - } - : undefined; - -const mapToTimeUrlState = (value: any) => (value && typeof value === 'number' ? value : undefined); - -const mapToAutoReloadUrlState = (value: any) => (typeof value === 'boolean' ? value : undefined); diff --git a/x-pack/plugins/infra/public/containers/waffle/with_waffle_view_state.tsx b/x-pack/plugins/infra/public/containers/waffle/with_waffle_view_state.tsx deleted file mode 100644 index 421c506166d04..0000000000000 --- a/x-pack/plugins/infra/public/containers/waffle/with_waffle_view_state.tsx +++ /dev/null @@ -1,145 +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 { connect } from 'react-redux'; -import { createSelector } from 'reselect'; -import { IIndexPattern } from 'src/plugins/data/public'; -import { - State, - waffleOptionsActions, - waffleOptionsSelectors, - waffleTimeSelectors, - waffleTimeActions, - waffleFilterActions, - waffleFilterSelectors, - initialState, -} from '../../store'; -import { asChildFunctionRenderer } from '../../utils/typed_react'; -import { convertKueryToElasticSearchQuery } from '../../utils/kuery'; - -const selectViewState = createSelector( - waffleOptionsSelectors.selectMetric, - waffleOptionsSelectors.selectView, - waffleOptionsSelectors.selectGroupBy, - waffleOptionsSelectors.selectNodeType, - waffleOptionsSelectors.selectCustomOptions, - waffleOptionsSelectors.selectBoundsOverride, - waffleOptionsSelectors.selectAutoBounds, - waffleTimeSelectors.selectCurrentTime, - waffleTimeSelectors.selectIsAutoReloading, - waffleFilterSelectors.selectWaffleFilterQuery, - waffleOptionsSelectors.selectCustomMetrics, - ( - metric, - view, - groupBy, - nodeType, - customOptions, - boundsOverride, - autoBounds, - time, - autoReload, - filterQuery, - customMetrics - ) => ({ - time, - autoReload, - metric, - groupBy, - nodeType, - view, - customOptions, - boundsOverride, - autoBounds, - filterQuery, - customMetrics, - }) -); - -interface Props { - indexPattern: IIndexPattern; -} - -export const withWaffleViewState = connect( - (state: State) => ({ - viewState: selectViewState(state), - defaultViewState: selectViewState(initialState), - }), - (dispatch, ownProps: Props) => { - return { - onViewChange: (viewState: WaffleViewState) => { - if (viewState.time) { - dispatch(waffleTimeActions.jumpToTime(viewState.time)); - } - if (viewState.autoReload) { - dispatch(waffleTimeActions.startAutoReload()); - } else if (typeof viewState.autoReload !== 'undefined' && !viewState.autoReload) { - dispatch(waffleTimeActions.stopAutoReload()); - } - if (viewState.metric) { - dispatch(waffleOptionsActions.changeMetric(viewState.metric)); - } - if (viewState.groupBy) { - dispatch(waffleOptionsActions.changeGroupBy(viewState.groupBy)); - } - if (viewState.nodeType) { - dispatch(waffleOptionsActions.changeNodeType(viewState.nodeType)); - } - if (viewState.view) { - dispatch(waffleOptionsActions.changeView(viewState.view)); - } - if (viewState.customOptions) { - dispatch(waffleOptionsActions.changeCustomOptions(viewState.customOptions)); - } - if (viewState.customMetrics) { - dispatch(waffleOptionsActions.changeCustomMetrics(viewState.customMetrics)); - } - if (viewState.boundsOverride) { - dispatch(waffleOptionsActions.changeBoundsOverride(viewState.boundsOverride)); - } - if (viewState.autoBounds) { - dispatch(waffleOptionsActions.changeAutoBounds(viewState.autoBounds)); - } - if (viewState.filterQuery) { - dispatch( - waffleFilterActions.applyWaffleFilterQuery({ - query: viewState.filterQuery, - serializedQuery: convertKueryToElasticSearchQuery( - viewState.filterQuery.expression, - ownProps.indexPattern - ), - }) - ); - } else { - dispatch( - waffleFilterActions.applyWaffleFilterQuery({ - query: null, - serializedQuery: null, - }) - ); - } - }, - }; - } -); - -export const WithWaffleViewState = asChildFunctionRenderer(withWaffleViewState); - -/** - * View State - */ -export interface WaffleViewState { - metric?: ReturnType<typeof waffleOptionsSelectors.selectMetric>; - groupBy?: ReturnType<typeof waffleOptionsSelectors.selectGroupBy>; - nodeType?: ReturnType<typeof waffleOptionsSelectors.selectNodeType>; - view?: ReturnType<typeof waffleOptionsSelectors.selectView>; - customOptions?: ReturnType<typeof waffleOptionsSelectors.selectCustomOptions>; - customMetrics?: ReturnType<typeof waffleOptionsSelectors.selectCustomMetrics>; - boundsOverride?: ReturnType<typeof waffleOptionsSelectors.selectBoundsOverride>; - autoBounds?: ReturnType<typeof waffleOptionsSelectors.selectAutoBounds>; - time?: ReturnType<typeof waffleTimeSelectors.selectCurrentTime>; - autoReload?: ReturnType<typeof waffleTimeSelectors.selectIsAutoReloading>; - filterQuery?: ReturnType<typeof waffleFilterSelectors.selectWaffleFilterQuery>; -} diff --git a/x-pack/plugins/infra/public/containers/with_options.tsx b/x-pack/plugins/infra/public/containers/with_options.tsx index 972722890ffef..e18fc85a68d60 100644 --- a/x-pack/plugins/infra/public/containers/with_options.tsx +++ b/x-pack/plugins/infra/public/containers/with_options.tsx @@ -8,7 +8,7 @@ import moment from 'moment'; import React from 'react'; import { euiPaletteColorBlind } from '@elastic/eui'; -import { InfraFormatterType, InfraOptions, InfraWaffleMapLegendMode } from '../lib/lib'; +import { InfraFormatterType, InfraOptions } from '../lib/lib'; import { RendererFunction } from '../utils/typed_react'; const euiVisColorPalette = euiPaletteColorBlind(); @@ -29,7 +29,7 @@ const initialState = { metric: { type: 'cpu' }, groupBy: [], legend: { - type: InfraWaffleMapLegendMode.gradient, + type: 'gradient', rules: [ { value: 0, diff --git a/x-pack/plugins/infra/public/hooks/use_http_request.tsx b/x-pack/plugins/infra/public/hooks/use_http_request.tsx index 50f4a636b48a3..e00abe6380498 100644 --- a/x-pack/plugins/infra/public/hooks/use_http_request.tsx +++ b/x-pack/plugins/infra/public/hooks/use_http_request.tsx @@ -7,28 +7,32 @@ import React, { useMemo, useState } from 'react'; import { IHttpFetchError } from 'src/core/public'; import { i18n } from '@kbn/i18n'; +import { HttpHandler } from 'target/types/core/public/http'; +import { ToastInput } from 'target/types/core/public/notifications/toasts/toasts_api'; import { useTrackedPromise } from '../utils/use_tracked_promise'; import { useKibana } from '../../../../../src/plugins/kibana_react/public'; export function useHTTPRequest<Response>( pathname: string, method: 'GET' | 'POST' | 'PUT' | 'DELETE' | 'HEAD', - body?: string, - decode: (response: any) => Response = response => response + body?: string | null, + decode: (response: any) => Response = response => response, + fetch?: HttpHandler, + toastWarning?: (input: ToastInput) => void ) { const kibana = useKibana(); - const fetch = kibana.services.http?.fetch; - const toasts = kibana.notifications.toasts; + const fetchService = fetch ? fetch : kibana.services.http?.fetch; + const toast = toastWarning ? toastWarning : kibana.notifications.toasts.warning; const [response, setResponse] = useState<Response | null>(null); const [error, setError] = useState<IHttpFetchError | null>(null); const [request, makeRequest] = useTrackedPromise( { cancelPreviousOn: 'resolution', createPromise: () => { - if (!fetch) { + if (!fetchService) { throw new Error('HTTP service is unavailable'); } - return fetch(pathname, { + return fetchService(pathname, { method, body, }); @@ -37,7 +41,7 @@ export function useHTTPRequest<Response>( onReject: (e: unknown) => { const err = e as IHttpFetchError; setError(err); - toasts.warning({ + toast({ toastLifeTimeMs: 3000, title: i18n.translate('xpack.infra.useHTTPRequest.error.title', { defaultMessage: `Error while fetching resource`, @@ -67,7 +71,7 @@ export function useHTTPRequest<Response>( }); }, }, - [pathname, body, method, fetch, toasts] + [pathname, body, method, fetch, toast] ); const loading = useMemo(() => { diff --git a/x-pack/plugins/infra/public/lib/lib.ts b/x-pack/plugins/infra/public/lib/lib.ts index 9f851e185018b..e4de0caf9bb8b 100644 --- a/x-pack/plugins/infra/public/lib/lib.ts +++ b/x-pack/plugins/infra/public/lib/lib.ts @@ -136,18 +136,13 @@ export interface InfraWaffleMapGradientRule { color: string; } -export enum InfraWaffleMapLegendMode { - step = 'step', - gradient = 'gradient', -} - export interface InfraWaffleMapStepLegend { - type: InfraWaffleMapLegendMode.step; + type: 'step'; rules: InfraWaffleMapStepRule[]; } export interface InfraWaffleMapGradientLegend { - type: InfraWaffleMapLegendMode.gradient; + type: 'gradient'; rules: InfraWaffleMapGradientRule[]; } diff --git a/x-pack/plugins/infra/public/pages/infrastructure/index.tsx b/x-pack/plugins/infra/public/pages/infrastructure/index.tsx index 422eb53148fe6..d592ae3480fc9 100644 --- a/x-pack/plugins/infra/public/pages/infrastructure/index.tsx +++ b/x-pack/plugins/infra/public/pages/infrastructure/index.tsx @@ -25,6 +25,9 @@ import { MetricsSettingsPage } from './settings'; import { AppNavigation } from '../../components/navigation/app_navigation'; import { SourceLoadingPage } from '../../components/source_loading_page'; import { useKibana } from '../../../../../../src/plugins/kibana_react/public'; +import { WaffleOptionsProvider } from '../inventory_view/hooks/use_waffle_options'; +import { WaffleTimeProvider } from '../inventory_view/hooks/use_waffle_time'; +import { WaffleFiltersProvider } from '../inventory_view/hooks/use_waffle_filters'; import { AlertDropdown } from '../../components/alerting/metrics/alert_dropdown'; export const InfrastructurePage = ({ match }: RouteComponentProps) => { @@ -32,96 +35,101 @@ export const InfrastructurePage = ({ match }: RouteComponentProps) => { return ( <Source.Provider sourceId="default"> - <ColumnarPage> - <DocumentTitle - title={i18n.translate('xpack.infra.homePage.documentTitle', { - defaultMessage: 'Metrics', - })} - /> - - <HelpCenterContent - feedbackLink="https://discuss.elastic.co/c/metrics" - appName={i18n.translate('xpack.infra.header.infrastructureHelpAppName', { - defaultMessage: 'Metrics', - })} - /> + <WaffleOptionsProvider> + <WaffleTimeProvider> + <WaffleFiltersProvider> + <ColumnarPage> + <DocumentTitle + title={i18n.translate('xpack.infra.homePage.documentTitle', { + defaultMessage: 'Metrics', + })} + /> - <Header - breadcrumbs={[ - { - text: i18n.translate('xpack.infra.header.infrastructureTitle', { - defaultMessage: 'Metrics', - }), - }, - ]} - readOnlyBadge={!uiCapabilities?.infrastructure?.save} - /> + <HelpCenterContent + feedbackLink="https://discuss.elastic.co/c/metrics" + appName={i18n.translate('xpack.infra.header.infrastructureHelpAppName', { + defaultMessage: 'Metrics', + })} + /> - <AppNavigation - aria-label={i18n.translate('xpack.infra.header.infrastructureNavigationTitle', { - defaultMessage: 'Metrics', - })} - > - <EuiFlexGroup gutterSize={'none'} alignItems={'center'}> - <EuiFlexItem> - <RoutedTabs - tabs={[ - { - app: 'metrics', - title: i18n.translate('xpack.infra.homePage.inventoryTabTitle', { - defaultMessage: 'Inventory', - }), - pathname: '/inventory', - }, - { - app: 'metrics', - title: i18n.translate('xpack.infra.homePage.metricsExplorerTabTitle', { - defaultMessage: 'Metrics Explorer', - }), - pathname: '/explorer', - }, + <Header + breadcrumbs={[ { - app: 'metrics', - title: i18n.translate('xpack.infra.homePage.settingsTabTitle', { - defaultMessage: 'Settings', + text: i18n.translate('xpack.infra.header.infrastructureTitle', { + defaultMessage: 'Metrics', }), - pathname: '/settings', }, ]} + readOnlyBadge={!uiCapabilities?.infrastructure?.save} /> - </EuiFlexItem> - <EuiFlexItem grow={false}> - <Route path={'/explorer'} component={AlertDropdown} /> - </EuiFlexItem> - </EuiFlexGroup> - </AppNavigation> + <AppNavigation + aria-label={i18n.translate('xpack.infra.header.infrastructureNavigationTitle', { + defaultMessage: 'Metrics', + })} + > + <EuiFlexGroup gutterSize={'none'} alignItems={'center'}> + <EuiFlexItem> + <RoutedTabs + tabs={[ + { + app: 'metrics', + title: i18n.translate('xpack.infra.homePage.inventoryTabTitle', { + defaultMessage: 'Inventory', + }), + pathname: '/inventory', + }, + { + app: 'metrics', + title: i18n.translate('xpack.infra.homePage.metricsExplorerTabTitle', { + defaultMessage: 'Metrics Explorer', + }), + pathname: '/explorer', + }, + { + app: 'metrics', + title: i18n.translate('xpack.infra.homePage.settingsTabTitle', { + defaultMessage: 'Settings', + }), + pathname: '/settings', + }, + ]} + /> + </EuiFlexItem> + <EuiFlexItem grow={false}> + <Route path={'/explorer'} component={AlertDropdown} /> + </EuiFlexItem> + </EuiFlexGroup> + </AppNavigation> - <Switch> - <Route path={'/inventory'} component={SnapshotPage} /> - <Route - path={'/explorer'} - render={props => ( - <WithSource> - {({ configuration, createDerivedIndexPattern }) => ( - <MetricsExplorerOptionsContainer.Provider> - <WithMetricsExplorerOptionsUrlState /> - {configuration ? ( - <MetricsExplorerPage - derivedIndexPattern={createDerivedIndexPattern('metrics')} - source={configuration} - {...props} - /> - ) : ( - <SourceLoadingPage /> - )} - </MetricsExplorerOptionsContainer.Provider> - )} - </WithSource> - )} - /> - <Route path={'/settings'} component={MetricsSettingsPage} /> - </Switch> - </ColumnarPage> + <Switch> + <Route path={'/inventory'} component={SnapshotPage} /> + <Route + path={'/explorer'} + render={props => ( + <WithSource> + {({ configuration, createDerivedIndexPattern }) => ( + <MetricsExplorerOptionsContainer.Provider> + <WithMetricsExplorerOptionsUrlState /> + {configuration ? ( + <MetricsExplorerPage + derivedIndexPattern={createDerivedIndexPattern('metrics')} + source={configuration} + {...props} + /> + ) : ( + <SourceLoadingPage /> + )} + </MetricsExplorerOptionsContainer.Provider> + )} + </WithSource> + )} + /> + <Route path={'/settings'} component={MetricsSettingsPage} /> + </Switch> + </ColumnarPage> + </WaffleFiltersProvider> + </WaffleTimeProvider> + </WaffleOptionsProvider> </Source.Provider> ); }; diff --git a/x-pack/plugins/infra/public/pages/infrastructure/snapshot/index.tsx b/x-pack/plugins/infra/public/pages/infrastructure/snapshot/index.tsx index dbb8b2d8e2952..48cc56388c0f2 100644 --- a/x-pack/plugins/infra/public/pages/infrastructure/snapshot/index.tsx +++ b/x-pack/plugins/infra/public/pages/infrastructure/snapshot/index.tsx @@ -8,7 +8,6 @@ import { EuiButton, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import React, { useContext } from 'react'; -import { SnapshotPageContent } from './page_content'; import { SnapshotToolbar } from './toolbar'; import { DocumentTitle } from '../../../components/document_title'; @@ -19,17 +18,14 @@ import { SourceErrorPage } from '../../../components/source_error_page'; import { SourceLoadingPage } from '../../../components/source_loading_page'; import { ViewSourceConfigurationButton } from '../../../components/source_configuration'; import { Source } from '../../../containers/source'; -import { WithWaffleFilterUrlState } from '../../../containers/waffle/with_waffle_filters'; -import { WithWaffleOptionsUrlState } from '../../../containers/waffle/with_waffle_options'; -import { WithWaffleTimeUrlState } from '../../../containers/waffle/with_waffle_time'; import { useTrackPageview } from '../../../../../observability/public'; import { useKibana } from '../../../../../../../src/plugins/kibana_react/public'; +import { Layout } from '../../../components/inventory/layout'; import { useLinkProps } from '../../../hooks/use_link_props'; export const SnapshotPage = () => { const uiCapabilities = useKibana().services.application?.capabilities; const { - createDerivedIndexPattern, hasFailedLoadingSource, isLoading, loadSourceFailureMessage, @@ -60,11 +56,8 @@ export const SnapshotPage = () => { <SourceLoadingPage /> ) : metricIndicesExist ? ( <> - <WithWaffleTimeUrlState /> - <WithWaffleFilterUrlState indexPattern={createDerivedIndexPattern('metrics')} /> - <WithWaffleOptionsUrlState /> <SnapshotToolbar /> - <SnapshotPageContent /> + <Layout /> </> ) : hasFailedLoadingSource ? ( <SourceErrorPage errorMessage={loadSourceFailureMessage || ''} retry={loadSource} /> diff --git a/x-pack/plugins/infra/public/pages/infrastructure/snapshot/page_content.tsx b/x-pack/plugins/infra/public/pages/infrastructure/snapshot/page_content.tsx deleted file mode 100644 index 83a4c8d3a497f..0000000000000 --- a/x-pack/plugins/infra/public/pages/infrastructure/snapshot/page_content.tsx +++ /dev/null @@ -1,68 +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 from 'react'; - -import { WithWaffleFilter } from '../../../containers/waffle/with_waffle_filters'; -import { WithWaffleOptions } from '../../../containers/waffle/with_waffle_options'; -import { WithWaffleTime } from '../../../containers/waffle/with_waffle_time'; -import { WithOptions } from '../../../containers/with_options'; -import { WithSource } from '../../../containers/with_source'; -import { Layout } from '../../../components/inventory/layout'; - -export const SnapshotPageContent: React.FC = () => ( - <WithSource> - {({ configuration, createDerivedIndexPattern, sourceId }) => ( - <WithOptions> - {({ wafflemap }) => ( - <WithWaffleFilter indexPattern={createDerivedIndexPattern('metrics')}> - {({ filterQueryAsJson, applyFilterQuery }) => ( - <WithWaffleTime> - {({ currentTime }) => ( - <WithWaffleOptions> - {({ - metric, - groupBy, - nodeType, - view, - changeView, - autoBounds, - boundsOverride, - accountId, - region, - }) => ( - <Layout - currentTime={currentTime} - filterQuery={filterQueryAsJson} - metric={metric} - groupBy={groupBy} - nodeType={nodeType} - sourceId={sourceId} - options={{ - ...wafflemap, - metric, - fields: configuration && configuration.fields, - groupBy, - }} - onDrilldown={applyFilterQuery} - view={view} - onViewChange={changeView} - autoBounds={autoBounds} - boundsOverride={boundsOverride} - accountId={accountId} - region={region} - /> - )} - </WithWaffleOptions> - )} - </WithWaffleTime> - )} - </WithWaffleFilter> - )} - </WithOptions> - )} - </WithSource> -); diff --git a/x-pack/plugins/infra/public/pages/infrastructure/snapshot/toolbar.tsx b/x-pack/plugins/infra/public/pages/infrastructure/snapshot/toolbar.tsx index 3606580e86504..ccdaa5e8dc785 100644 --- a/x-pack/plugins/infra/public/pages/infrastructure/snapshot/toolbar.tsx +++ b/x-pack/plugins/infra/public/pages/infrastructure/snapshot/toolbar.tsx @@ -5,92 +5,24 @@ */ import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; -import { i18n } from '@kbn/i18n'; import React from 'react'; -import { AutocompleteField } from '../../../components/autocomplete_field'; import { Toolbar } from '../../../components/eui/toolbar'; import { WaffleTimeControls } from '../../../components/waffle/waffle_time_controls'; -import { WithWaffleFilter } from '../../../containers/waffle/with_waffle_filters'; -import { WithWaffleTime } from '../../../containers/waffle/with_waffle_time'; -import { WithKueryAutocompletion } from '../../../containers/with_kuery_autocompletion'; -import { WithSource } from '../../../containers/with_source'; -import { WithWaffleOptions } from '../../../containers/waffle/with_waffle_options'; import { WaffleInventorySwitcher } from '../../../components/waffle/waffle_inventory_switcher'; +import { SearchBar } from '../../inventory_view/compontents/search_bar'; export const SnapshotToolbar = () => ( <Toolbar> <EuiFlexGroup alignItems="center" justifyContent="spaceBetween" gutterSize="m"> <EuiFlexItem grow={false}> - <WithWaffleOptions> - {({ - changeMetric, - changeNodeType, - changeGroupBy, - changeAccount, - changeRegion, - changeCustomMetrics, - nodeType, - }) => ( - <WaffleInventorySwitcher - nodeType={nodeType} - changeNodeType={changeNodeType} - changeMetric={changeMetric} - changeGroupBy={changeGroupBy} - changeAccount={changeAccount} - changeRegion={changeRegion} - changeCustomMetrics={changeCustomMetrics} - /> - )} - </WithWaffleOptions> + <WaffleInventorySwitcher /> </EuiFlexItem> <EuiFlexItem> - <WithSource> - {({ createDerivedIndexPattern }) => ( - <WithKueryAutocompletion indexPattern={createDerivedIndexPattern('metrics')}> - {({ isLoadingSuggestions, loadSuggestions, suggestions }) => ( - <WithWaffleFilter indexPattern={createDerivedIndexPattern('metrics')}> - {({ - applyFilterQueryFromKueryExpression, - filterQueryDraft, - isFilterQueryDraftValid, - setFilterQueryDraftFromKueryExpression, - }) => ( - <AutocompleteField - isLoadingSuggestions={isLoadingSuggestions} - isValid={isFilterQueryDraftValid} - loadSuggestions={loadSuggestions} - onChange={setFilterQueryDraftFromKueryExpression} - onSubmit={applyFilterQueryFromKueryExpression} - placeholder={i18n.translate( - 'xpack.infra.homePage.toolbar.kqlSearchFieldPlaceholder', - { - defaultMessage: 'Search for infrastructure data… (e.g. host.name:host-1)', - } - )} - suggestions={suggestions} - value={filterQueryDraft ? filterQueryDraft.expression : ''} - autoFocus={true} - /> - )} - </WithWaffleFilter> - )} - </WithKueryAutocompletion> - )} - </WithSource> + <SearchBar /> </EuiFlexItem> <EuiFlexItem grow={false}> - <WithWaffleTime resetOnUnmount> - {({ currentTime, isAutoReloading, jumpToTime, startAutoReload, stopAutoReload }) => ( - <WaffleTimeControls - currentTime={currentTime} - isLiveStreaming={isAutoReloading} - onChangeTime={jumpToTime} - startLiveStreaming={startAutoReload} - stopLiveStreaming={stopAutoReload} - /> - )} - </WithWaffleTime> + <WaffleTimeControls /> </EuiFlexItem> </EuiFlexGroup> </Toolbar> diff --git a/x-pack/plugins/infra/public/pages/inventory_view/compontents/search_bar.tsx b/x-pack/plugins/infra/public/pages/inventory_view/compontents/search_bar.tsx new file mode 100644 index 0000000000000..f4fde46d434f8 --- /dev/null +++ b/x-pack/plugins/infra/public/pages/inventory_view/compontents/search_bar.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, { useContext } from 'react'; +import { i18n } from '@kbn/i18n'; +import { Source } from '../../../containers/source'; +import { AutocompleteField } from '../../../components/autocomplete_field'; +import { WithKueryAutocompletion } from '../../../containers/with_kuery_autocompletion'; +import { useWaffleFiltersContext } from '../hooks/use_waffle_filters'; + +export const SearchBar = () => { + const { createDerivedIndexPattern } = useContext(Source.Context); + const { + applyFilterQueryFromKueryExpression, + filterQueryDraft, + isFilterQueryDraftValid, + setFilterQueryDraftFromKueryExpression, + } = useWaffleFiltersContext(); + return ( + <WithKueryAutocompletion indexPattern={createDerivedIndexPattern('metrics')}> + {({ isLoadingSuggestions, loadSuggestions, suggestions }) => ( + <AutocompleteField + isLoadingSuggestions={isLoadingSuggestions} + isValid={isFilterQueryDraftValid} + loadSuggestions={loadSuggestions} + onChange={setFilterQueryDraftFromKueryExpression} + onSubmit={applyFilterQueryFromKueryExpression} + placeholder={i18n.translate('xpack.infra.homePage.toolbar.kqlSearchFieldPlaceholder', { + defaultMessage: 'Search for infrastructure data… (e.g. host.name:host-1)', + })} + suggestions={suggestions} + value={filterQueryDraft ? filterQueryDraft : ''} + autoFocus={true} + /> + )} + </WithKueryAutocompletion> + ); +}; diff --git a/x-pack/plugins/infra/public/pages/inventory_view/hooks/use_waffle_filters.ts b/x-pack/plugins/infra/public/pages/inventory_view/hooks/use_waffle_filters.ts new file mode 100644 index 0000000000000..02c079dcaddc4 --- /dev/null +++ b/x-pack/plugins/infra/public/pages/inventory_view/hooks/use_waffle_filters.ts @@ -0,0 +1,93 @@ +/* + * 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 { useState, useMemo, useCallback, useEffect } from 'react'; +import * as rt from 'io-ts'; +import { pipe } from 'fp-ts/lib/pipeable'; +import { fold } from 'fp-ts/lib/Either'; +import { constant, identity } from 'fp-ts/lib/function'; +import createContainter from 'constate'; +import { useUrlState } from '../../../utils/use_url_state'; +import { useSourceContext } from '../../../containers/source'; +import { convertKueryToElasticSearchQuery } from '../../../utils/kuery'; +import { esKuery } from '../../../../../../../src/plugins/data/public'; + +const validateKuery = (expression: string) => { + try { + esKuery.fromKueryExpression(expression); + } catch (err) { + return false; + } + return true; +}; + +export const DEFAULT_WAFFLE_FILTERS_STATE: WaffleFiltersState = { kind: 'kuery', expression: '' }; + +export const useWaffleFilters = () => { + const { createDerivedIndexPattern } = useSourceContext(); + const indexPattern = createDerivedIndexPattern('metrics'); + + const [urlState, setUrlState] = useUrlState<WaffleFiltersState>({ + defaultState: DEFAULT_WAFFLE_FILTERS_STATE, + decodeUrlState, + encodeUrlState, + urlStateKey: 'waffleFilter', + }); + + const [state, setState] = useState<WaffleFiltersState>(urlState); + + useEffect(() => setUrlState(state), [setUrlState, state]); + + const [filterQueryDraft, setFilterQueryDraft] = useState<string>(urlState.expression); + + const filterQueryAsJson = useMemo( + () => convertKueryToElasticSearchQuery(urlState.expression, indexPattern), + [indexPattern, urlState.expression] + ); + + const applyFilterQueryFromKueryExpression = useCallback( + (expression: string) => { + setState(previous => ({ + ...previous, + kind: 'kuery', + expression, + })); + }, + [setState] + ); + + const applyFilterQuery = useCallback((filterQuery: WaffleFiltersState) => { + setState(filterQuery); + setFilterQueryDraft(filterQuery.expression); + }, []); + + const isFilterQueryDraftValid = useMemo(() => validateKuery(filterQueryDraft), [ + filterQueryDraft, + ]); + + return { + filterQuery: urlState, + filterQueryDraft, + filterQueryAsJson, + applyFilterQuery, + setFilterQueryDraftFromKueryExpression: setFilterQueryDraft, + applyFilterQueryFromKueryExpression, + isFilterQueryDraftValid, + setWaffleFiltersState: applyFilterQuery, + }; +}; + +export const WaffleFiltersStateRT = rt.type({ + kind: rt.literal('kuery'), + expression: rt.string, +}); + +export type WaffleFiltersState = rt.TypeOf<typeof WaffleFiltersStateRT>; +const encodeUrlState = WaffleFiltersStateRT.encode; +const decodeUrlState = (value: unknown) => + pipe(WaffleFiltersStateRT.decode(value), fold(constant(undefined), identity)); +export const WaffleFilters = createContainter(useWaffleFilters); +export const [WaffleFiltersProvider, useWaffleFiltersContext] = WaffleFilters; diff --git a/x-pack/plugins/infra/public/pages/inventory_view/hooks/use_waffle_options.ts b/x-pack/plugins/infra/public/pages/inventory_view/hooks/use_waffle_options.ts new file mode 100644 index 0000000000000..2853917d5f683 --- /dev/null +++ b/x-pack/plugins/infra/public/pages/inventory_view/hooks/use_waffle_options.ts @@ -0,0 +1,147 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { useCallback, useState, useEffect } from 'react'; +import * as rt from 'io-ts'; +import { pipe } from 'fp-ts/lib/pipeable'; +import { fold } from 'fp-ts/lib/Either'; +import { constant, identity } from 'fp-ts/lib/function'; +import createContainer from 'constate'; +import { + SnapshotMetricInput, + SnapshotGroupBy, + SnapshotCustomMetricInput, + SnapshotMetricInputRT, + SnapshotGroupByRT, + SnapshotCustomMetricInputRT, +} from '../../../../common/http_api/snapshot_api'; +import { useUrlState } from '../../../utils/use_url_state'; +import { InventoryItemType, ItemTypeRT } from '../../../../common/inventory_models/types'; + +export const DEFAULT_WAFFLE_OPTIONS_STATE: WaffleOptionsState = { + metric: { type: 'cpu' }, + groupBy: [], + nodeType: 'host', + view: 'map', + customOptions: [], + boundsOverride: { max: 1, min: 0 }, + autoBounds: true, + accountId: '', + region: '', + customMetrics: [], +}; + +export const useWaffleOptions = () => { + const [urlState, setUrlState] = useUrlState<WaffleOptionsState>({ + defaultState: DEFAULT_WAFFLE_OPTIONS_STATE, + decodeUrlState, + encodeUrlState, + urlStateKey: 'waffleOptions', + }); + + const [state, setState] = useState<WaffleOptionsState>(urlState); + + useEffect(() => setUrlState(state), [setUrlState, state]); + + const changeMetric = useCallback( + (metric: SnapshotMetricInput) => setState(previous => ({ ...previous, metric })), + [setState] + ); + + const changeGroupBy = useCallback( + (groupBy: SnapshotGroupBy) => setState(previous => ({ ...previous, groupBy })), + [setState] + ); + + const changeNodeType = useCallback( + (nodeType: InventoryItemType) => setState(previous => ({ ...previous, nodeType })), + [setState] + ); + + const changeView = useCallback((view: string) => setState(previous => ({ ...previous, view })), [ + setState, + ]); + + const changeCustomOptions = useCallback( + (customOptions: Array<{ text: string; field: string }>) => + setState(previous => ({ ...previous, customOptions })), + [setState] + ); + + const changeAutoBounds = useCallback( + (autoBounds: boolean) => setState(previous => ({ ...previous, autoBounds })), + [setState] + ); + + const changeBoundsOverride = useCallback( + (boundsOverride: { min: number; max: number }) => + setState(previous => ({ ...previous, boundsOverride })), + [setState] + ); + + const changeAccount = useCallback( + (accountId: string) => setState(previous => ({ ...previous, accountId })), + [setState] + ); + + const changeRegion = useCallback( + (region: string) => setState(previous => ({ ...previous, region })), + [setState] + ); + + const changeCustomMetrics = useCallback( + (customMetrics: SnapshotCustomMetricInput[]) => { + setState(previous => ({ ...previous, customMetrics })); + }, + [setState] + ); + + return { + ...state, + changeMetric, + changeGroupBy, + changeNodeType, + changeView, + changeCustomOptions, + changeAutoBounds, + changeBoundsOverride, + changeAccount, + changeRegion, + changeCustomMetrics, + setWaffleOptionsState: setState, + }; +}; + +export const WaffleOptionsStateRT = rt.type({ + metric: SnapshotMetricInputRT, + groupBy: SnapshotGroupByRT, + nodeType: ItemTypeRT, + view: rt.string, + customOptions: rt.array( + rt.type({ + text: rt.string, + field: rt.string, + }) + ), + boundsOverride: rt.type({ + min: rt.number, + max: rt.number, + }), + autoBounds: rt.boolean, + accountId: rt.string, + region: rt.string, + customMetrics: rt.array(SnapshotCustomMetricInputRT), +}); + +export type WaffleOptionsState = rt.TypeOf<typeof WaffleOptionsStateRT>; +const encodeUrlState = (state: WaffleOptionsState) => { + return WaffleOptionsStateRT.encode(state); +}; +const decodeUrlState = (value: unknown) => + pipe(WaffleOptionsStateRT.decode(value), fold(constant(undefined), identity)); + +export const WaffleOptions = createContainer(useWaffleOptions); +export const [WaffleOptionsProvider, useWaffleOptionsContext] = WaffleOptions; diff --git a/x-pack/plugins/infra/public/pages/inventory_view/hooks/use_waffle_time.ts b/x-pack/plugins/infra/public/pages/inventory_view/hooks/use_waffle_time.ts new file mode 100644 index 0000000000000..051b5e598cb75 --- /dev/null +++ b/x-pack/plugins/infra/public/pages/inventory_view/hooks/use_waffle_time.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 { useCallback, useState, useEffect } from 'react'; +import * as rt from 'io-ts'; +import { pipe } from 'fp-ts/lib/pipeable'; +import { fold } from 'fp-ts/lib/Either'; +import { constant, identity } from 'fp-ts/lib/function'; +import createContainer from 'constate'; +import { useUrlState } from '../../../utils/use_url_state'; + +export const DEFAULT_WAFFLE_TIME_STATE: WaffleTimeState = { + currentTime: Date.now(), + isAutoReloading: false, +}; + +export const useWaffleTime = () => { + const [urlState, setUrlState] = useUrlState<WaffleTimeState>({ + defaultState: DEFAULT_WAFFLE_TIME_STATE, + decodeUrlState, + encodeUrlState, + urlStateKey: 'waffleTime', + }); + + const [state, setState] = useState<WaffleTimeState>(urlState); + + useEffect(() => setUrlState(state), [setUrlState, state]); + + const { currentTime, isAutoReloading } = urlState; + + const startAutoReload = useCallback(() => { + setState(previous => ({ ...previous, isAutoReloading: true })); + }, [setState]); + + const stopAutoReload = useCallback(() => { + setState(previous => ({ ...previous, isAutoReloading: false })); + }, [setState]); + + const jumpToTime = useCallback( + (time: number) => { + setState(previous => ({ ...previous, currentTime: time })); + }, + [setState] + ); + + const currentTimeRange = { + from: currentTime - 1000 * 60 * 5, + interval: '1m', + to: currentTime, + }; + + return { + currentTime, + currentTimeRange, + isAutoReloading, + startAutoReload, + stopAutoReload, + jumpToTime, + setWaffleTimeState: setState, + }; +}; + +export const WaffleTimeStateRT = rt.type({ + currentTime: rt.number, + isAutoReloading: rt.boolean, +}); + +export type WaffleTimeState = rt.TypeOf<typeof WaffleTimeStateRT>; +const encodeUrlState = WaffleTimeStateRT.encode; +const decodeUrlState = (value: unknown) => + pipe(WaffleTimeStateRT.decode(value), fold(constant(undefined), identity)); + +export const WaffleTime = createContainer(useWaffleTime); +export const [WaffleTimeProvider, useWaffleTimeContext] = WaffleTime; diff --git a/x-pack/plugins/infra/public/pages/inventory_view/hooks/use_waffle_view_state.ts b/x-pack/plugins/infra/public/pages/inventory_view/hooks/use_waffle_view_state.ts new file mode 100644 index 0000000000000..869560b2b8709 --- /dev/null +++ b/x-pack/plugins/infra/public/pages/inventory_view/hooks/use_waffle_view_state.ts @@ -0,0 +1,95 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { useCallback } from 'react'; +import { + useWaffleOptionsContext, + DEFAULT_WAFFLE_OPTIONS_STATE, + WaffleOptionsState, +} from './use_waffle_options'; +import { useWaffleTimeContext, DEFAULT_WAFFLE_TIME_STATE } from './use_waffle_time'; +import { + useWaffleFiltersContext, + DEFAULT_WAFFLE_FILTERS_STATE, + WaffleFiltersState, +} from './use_waffle_filters'; + +export const useWaffleViewState = () => { + const { + metric, + groupBy, + nodeType, + view, + customOptions, + customMetrics, + boundsOverride, + autoBounds, + accountId, + region, + setWaffleOptionsState, + } = useWaffleOptionsContext(); + const { currentTime, isAutoReloading, setWaffleTimeState } = useWaffleTimeContext(); + const { filterQuery, setWaffleFiltersState } = useWaffleFiltersContext(); + + const viewState: WaffleViewState = { + metric, + groupBy, + nodeType, + view, + customOptions, + customMetrics, + boundsOverride, + autoBounds, + accountId, + region, + time: currentTime, + autoReload: isAutoReloading, + filterQuery, + }; + + const defaultViewState: WaffleViewState = { + ...DEFAULT_WAFFLE_OPTIONS_STATE, + filterQuery: DEFAULT_WAFFLE_FILTERS_STATE, + time: DEFAULT_WAFFLE_TIME_STATE.currentTime, + autoReload: DEFAULT_WAFFLE_TIME_STATE.isAutoReloading, + }; + + const onViewChange = useCallback( + (newState: WaffleViewState) => { + setWaffleOptionsState({ + metric: newState.metric, + groupBy: newState.groupBy, + nodeType: newState.nodeType, + view: newState.view, + customOptions: newState.customOptions, + customMetrics: newState.customMetrics, + boundsOverride: newState.boundsOverride, + autoBounds: newState.autoBounds, + accountId: newState.accountId, + region: newState.region, + }); + if (newState.time) { + setWaffleTimeState({ + currentTime: newState.time, + isAutoReloading: newState.autoReload, + }); + } + setWaffleFiltersState(newState.filterQuery); + }, + [setWaffleOptionsState, setWaffleTimeState, setWaffleFiltersState] + ); + + return { + viewState, + defaultViewState, + onViewChange, + }; +}; + +export type WaffleViewState = WaffleOptionsState & { + time: number; + autoReload: boolean; + filterQuery: WaffleFiltersState; +}; diff --git a/x-pack/plugins/infra/public/pages/link_to/redirect_to_host_detail_via_ip.tsx b/x-pack/plugins/infra/public/pages/link_to/redirect_to_host_detail_via_ip.tsx index 01b02f1acbbf2..b1dab3bd3f673 100644 --- a/x-pack/plugins/infra/public/pages/link_to/redirect_to_host_detail_via_ip.tsx +++ b/x-pack/plugins/infra/public/pages/link_to/redirect_to_host_detail_via_ip.tsx @@ -8,7 +8,7 @@ import React from 'react'; import { Redirect, RouteComponentProps } from 'react-router-dom'; import { i18n } from '@kbn/i18n'; -import { replaceMetricTimeInQueryString } from '../metrics/containers/with_metrics_time'; +import { replaceMetricTimeInQueryString } from '../metrics/hooks/use_metrics_time'; import { useHostIpToName } from './use_host_ip_to_name'; import { getFromFromLocation, getToFromLocation } from './query_params'; import { LoadingPage } from '../../components/loading_page'; diff --git a/x-pack/plugins/infra/public/pages/link_to/redirect_to_node_detail.tsx b/x-pack/plugins/infra/public/pages/link_to/redirect_to_node_detail.tsx index 9eae632756a3f..72a41f5264244 100644 --- a/x-pack/plugins/infra/public/pages/link_to/redirect_to_node_detail.tsx +++ b/x-pack/plugins/infra/public/pages/link_to/redirect_to_node_detail.tsx @@ -7,7 +7,7 @@ import React from 'react'; import { Redirect, RouteComponentProps } from 'react-router-dom'; -import { replaceMetricTimeInQueryString } from '../metrics/containers/with_metrics_time'; +import { replaceMetricTimeInQueryString } from '../metrics/hooks/use_metrics_time'; import { getFromFromLocation, getToFromLocation } from './query_params'; import { InventoryItemType } from '../../../common/inventory_models/types'; import { LinkDescriptor } from '../../hooks/use_link_props'; diff --git a/x-pack/plugins/infra/public/pages/metrics/components/node_details_page.tsx b/x-pack/plugins/infra/public/pages/metrics/components/node_details_page.tsx index ea91c53faf675..dd2a5f2bdb39e 100644 --- a/x-pack/plugins/infra/public/pages/metrics/components/node_details_page.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/components/node_details_page.tsx @@ -22,7 +22,7 @@ import { MetricsTimeControls } from './time_controls'; import { SideNavContext, NavItem } from '../lib/side_nav_context'; import { PageBody } from './page_body'; import { euiStyled } from '../../../../../observability/public'; -import { MetricsTimeInput } from '../containers/with_metrics_time'; +import { MetricsTimeInput } from '../hooks/use_metrics_time'; import { InfraMetadata } from '../../../../common/http_api/metadata_api'; import { PageError } from './page_error'; import { MetadataContext } from '../../../pages/metrics/containers/metadata_context'; @@ -94,7 +94,7 @@ export const NodeDetailsPage = (props: Props) => { setRefreshInterval={props.setRefreshInterval} onChangeTimeRange={props.setTimeRange} setAutoReload={props.setAutoReload} - onRefresh={props.triggerRefresh} + onRefresh={refetch} /> </MetricsTitleTimeRangeContainer> </EuiPageHeaderSection> diff --git a/x-pack/plugins/infra/public/pages/metrics/components/page_body.tsx b/x-pack/plugins/infra/public/pages/metrics/components/page_body.tsx index 414b9c60adee3..e651d6b92d981 100644 --- a/x-pack/plugins/infra/public/pages/metrics/components/page_body.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/components/page_body.tsx @@ -9,7 +9,7 @@ import { i18n } from '@kbn/i18n'; import { findLayout } from '../../../../common/inventory_models/layouts'; import { InventoryItemType } from '../../../../common/inventory_models/types'; -import { MetricsTimeInput } from '../containers/with_metrics_time'; +import { MetricsTimeInput } from '../hooks/use_metrics_time'; import { InfraLoadingPanel } from '../../../components/loading'; import { NoData } from '../../../components/empty_states'; import { NodeDetailsMetricData } from '../../../../common/http_api/node_details_api'; @@ -19,9 +19,9 @@ interface Props { refetch: () => void; type: InventoryItemType; metrics: NodeDetailsMetricData[]; - onChangeRangeTime?: (time: MetricsTimeInput) => void; - isLiveStreaming?: boolean; - stopLiveStreaming?: () => void; + onChangeRangeTime: (time: MetricsTimeInput) => void; + isLiveStreaming: boolean; + stopLiveStreaming: () => void; } export const PageBody = ({ diff --git a/x-pack/plugins/infra/public/pages/metrics/components/section.tsx b/x-pack/plugins/infra/public/pages/metrics/components/section.tsx index 2f9ed9f54df82..68003737a1f14 100644 --- a/x-pack/plugins/infra/public/pages/metrics/components/section.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/components/section.tsx @@ -41,6 +41,9 @@ export const Section: FunctionComponent<SectionProps> = ({ if (metric === null) { return accumulatedChildren; } + if (!child.props.label) { + return accumulatedChildren; + } return [ ...accumulatedChildren, { diff --git a/x-pack/plugins/infra/public/pages/metrics/components/time_controls.test.tsx b/x-pack/plugins/infra/public/pages/metrics/components/time_controls.test.tsx index 91e25fd8ef585..02ba506e8abe1 100644 --- a/x-pack/plugins/infra/public/pages/metrics/components/time_controls.test.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/components/time_controls.test.tsx @@ -19,7 +19,7 @@ jest.mock('../../../utils/use_kibana_ui_setting', () => ({ import React from 'react'; import { MetricsTimeControls } from './time_controls'; import { mount } from 'enzyme'; -import { MetricsTimeInput } from '../containers/with_metrics_time'; +import { MetricsTimeInput } from '../hooks/use_metrics_time'; describe('MetricsTimeControls', () => { it('should set a valid from and to value for Today', () => { diff --git a/x-pack/plugins/infra/public/pages/metrics/components/time_controls.tsx b/x-pack/plugins/infra/public/pages/metrics/components/time_controls.tsx index b1daaa0320fab..cdbdc9bb7ecdb 100644 --- a/x-pack/plugins/infra/public/pages/metrics/components/time_controls.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/components/time_controls.tsx @@ -7,7 +7,7 @@ import { EuiSuperDatePicker, OnRefreshChangeProps, OnTimeChangeProps } from '@elastic/eui'; import React, { useCallback } from 'react'; import { euiStyled } from '../../../../../observability/public'; -import { MetricsTimeInput } from '../containers/with_metrics_time'; +import { MetricsTimeInput } from '../hooks/use_metrics_time'; import { useKibanaUiSetting } from '../../../utils/use_kibana_ui_setting'; import { mapKibanaQuickRangesToDatePickerRanges } from '../../../utils/map_timepicker_quickranges_to_datepicker_ranges'; @@ -61,8 +61,8 @@ export const MetricsTimeControls = (props: MetricsTimeControlsProps) => { return ( <MetricsTimeControlsContainer> <EuiSuperDatePicker - start={currentTimeRange.from} - end={currentTimeRange.to} + start={currentTimeRange.from.toString()} + end={currentTimeRange.to.toString()} isPaused={!isLiveStreaming} refreshInterval={refreshInterval ? refreshInterval : 0} onTimeChange={handleTimeChange} diff --git a/x-pack/plugins/infra/public/pages/metrics/containers/metrics.gql_query.ts b/x-pack/plugins/infra/public/pages/metrics/containers/metrics.gql_query.ts deleted file mode 100644 index 1241c0d771382..0000000000000 --- a/x-pack/plugins/infra/public/pages/metrics/containers/metrics.gql_query.ts +++ /dev/null @@ -1,38 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import gql from 'graphql-tag'; - -export const metricsQuery = gql` - query MetricsQuery( - $sourceId: ID! - $timerange: InfraTimerangeInput! - $metrics: [InfraMetric!]! - $nodeId: ID! - $cloudId: ID - $nodeType: InfraNodeType! - ) { - source(id: $sourceId) { - id - metrics( - nodeIds: { nodeId: $nodeId, cloudId: $cloudId } - timerange: $timerange - metrics: $metrics - nodeType: $nodeType - ) { - id - series { - id - label - data { - timestamp - value - } - } - } - } - } -`; diff --git a/x-pack/plugins/infra/public/pages/metrics/containers/with_metrics_time.tsx b/x-pack/plugins/infra/public/pages/metrics/containers/with_metrics_time.tsx deleted file mode 100644 index 64d2ddb67139d..0000000000000 --- a/x-pack/plugins/infra/public/pages/metrics/containers/with_metrics_time.tsx +++ /dev/null @@ -1,201 +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 createContainer from 'constate'; -import React, { useContext, useState, useCallback } from 'react'; -import { isNumber } from 'lodash'; -import moment from 'moment'; -import dateMath from '@elastic/datemath'; -import * as rt from 'io-ts'; -import { isRight } from 'fp-ts/lib/Either'; -import { replaceStateKeyInQueryString, UrlStateContainer } from '../../../utils/url_state'; -import { InfraTimerangeInput } from '../../../graphql/types'; - -export interface MetricsTimeInput { - from: string; - to: string; - interval: string; -} - -interface MetricsTimeState { - timeRange: MetricsTimeInput; - parsedTimeRange: InfraTimerangeInput; - setTimeRange: (timeRange: MetricsTimeInput) => void; - refreshInterval: number; - setRefreshInterval: (refreshInterval: number) => void; - isAutoReloading: boolean; - setAutoReload: (isAutoReloading: boolean) => void; - lastRefresh: number; - triggerRefresh: () => void; -} - -const parseRange = (range: MetricsTimeInput) => { - const parsedFrom = dateMath.parse(range.from); - const parsedTo = dateMath.parse(range.to, { roundUp: true }); - return { - ...range, - from: - (parsedFrom && parsedFrom.valueOf()) || - moment() - .subtract(1, 'hour') - .valueOf(), - to: (parsedTo && parsedTo.valueOf()) || moment().valueOf(), - }; -}; - -export const useMetricsTime = () => { - const defaultRange = { - from: 'now-1h', - to: 'now', - interval: '>=1m', - }; - const [isAutoReloading, setAutoReload] = useState(false); - const [refreshInterval, setRefreshInterval] = useState(5000); - const [lastRefresh, setLastRefresh] = useState<number>(moment().valueOf()); - const [timeRange, setTimeRange] = useState(defaultRange); - - const [parsedTimeRange, setParsedTimeRange] = useState(parseRange(defaultRange)); - - const updateTimeRange = useCallback((range: MetricsTimeInput) => { - setTimeRange(range); - setParsedTimeRange(parseRange(range)); - }, []); - - return { - timeRange, - setTimeRange: updateTimeRange, - parsedTimeRange, - refreshInterval, - setRefreshInterval, - isAutoReloading, - setAutoReload, - lastRefresh, - triggerRefresh: useCallback(() => setLastRefresh(moment().valueOf()), [setLastRefresh]), - }; -}; - -export const MetricsTimeContainer = createContainer(useMetricsTime); - -interface WithMetricsTimeProps { - children: (args: MetricsTimeState) => React.ReactElement; -} -export const WithMetricsTime: React.FunctionComponent<WithMetricsTimeProps> = ({ - children, -}: WithMetricsTimeProps) => { - const metricsTimeState = useContext(MetricsTimeContainer.Context); - return children({ ...metricsTimeState }); -}; - -/** - * Url State - */ - -interface MetricsTimeUrlState { - time?: MetricsTimeState['timeRange']; - autoReload?: boolean; - refreshInterval?: number; -} - -export const WithMetricsTimeUrlState = () => ( - <WithMetricsTime> - {({ - timeRange, - setTimeRange, - refreshInterval, - setRefreshInterval, - isAutoReloading, - setAutoReload, - }) => ( - <UrlStateContainer - urlState={{ - time: timeRange, - autoReload: isAutoReloading, - refreshInterval, - }} - urlStateKey="metricTime" - mapToUrlState={mapToUrlState} - onChange={newUrlState => { - if (newUrlState && newUrlState.time) { - setTimeRange(newUrlState.time); - } - if (newUrlState && newUrlState.autoReload) { - setAutoReload(true); - } else if ( - newUrlState && - typeof newUrlState.autoReload !== 'undefined' && - !newUrlState.autoReload - ) { - setAutoReload(false); - } - if (newUrlState && newUrlState.refreshInterval) { - setRefreshInterval(newUrlState.refreshInterval); - } - }} - onInitialize={initialUrlState => { - if (initialUrlState && initialUrlState.time) { - if ( - timeRange.from !== initialUrlState.time.from || - timeRange.to !== initialUrlState.time.to || - timeRange.interval !== initialUrlState.time.interval - ) { - setTimeRange(initialUrlState.time); - } - } - if (initialUrlState && initialUrlState.autoReload) { - setAutoReload(true); - } - if (initialUrlState && initialUrlState.refreshInterval) { - setRefreshInterval(initialUrlState.refreshInterval); - } - }} - /> - )} - </WithMetricsTime> -); - -const mapToUrlState = (value: any): MetricsTimeUrlState | undefined => - value - ? { - time: mapToTimeUrlState(value.time), - autoReload: mapToAutoReloadUrlState(value.autoReload), - refreshInterval: mapToRefreshInterval(value.refreshInterval), - } - : undefined; - -const MetricsTimeRT = rt.type({ - from: rt.union([rt.string, rt.number]), - to: rt.union([rt.string, rt.number]), - interval: rt.string, -}); - -const mapToTimeUrlState = (value: any) => { - const result = MetricsTimeRT.decode(value); - if (isRight(result)) { - const resultValue = result.right; - const to = isNumber(resultValue.to) ? moment(resultValue.to).toISOString() : resultValue.to; - const from = isNumber(resultValue.from) - ? moment(resultValue.from).toISOString() - : resultValue.from; - return { ...resultValue, from, to }; - } - return undefined; -}; - -const mapToAutoReloadUrlState = (value: any) => (typeof value === 'boolean' ? value : undefined); - -const mapToRefreshInterval = (value: any) => (typeof value === 'number' ? value : undefined); - -export const replaceMetricTimeInQueryString = (from: number, to: number) => - Number.isNaN(from) || Number.isNaN(to) - ? (value: string) => value - : replaceStateKeyInQueryString<MetricsTimeUrlState>('metricTime', { - autoReload: false, - time: { - interval: '>=1m', - from: moment(from).toISOString(), - to: moment(to).toISOString(), - }, - }); diff --git a/x-pack/plugins/infra/public/pages/metrics/containers/metrics_time.test.tsx b/x-pack/plugins/infra/public/pages/metrics/hooks/metrics_time.test.tsx similarity index 96% rename from x-pack/plugins/infra/public/pages/metrics/containers/metrics_time.test.tsx rename to x-pack/plugins/infra/public/pages/metrics/hooks/metrics_time.test.tsx index 350fa90810935..17fcc05406470 100644 --- a/x-pack/plugins/infra/public/pages/metrics/containers/metrics_time.test.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/hooks/metrics_time.test.tsx @@ -6,7 +6,7 @@ import { mountHook } from 'test_utils/enzyme_helpers'; -import { useMetricsTime } from './with_metrics_time'; +import { useMetricsTime } from './use_metrics_time'; describe('useMetricsTime hook', () => { describe('timeRange state', () => { diff --git a/x-pack/plugins/infra/public/pages/metrics/hooks/use_metrics_time.ts b/x-pack/plugins/infra/public/pages/metrics/hooks/use_metrics_time.ts new file mode 100644 index 0000000000000..2ed86863535ff --- /dev/null +++ b/x-pack/plugins/infra/public/pages/metrics/hooks/use_metrics_time.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 createContainer from 'constate'; +import { useState, useCallback, useEffect } from 'react'; +import moment from 'moment'; +import dateMath from '@elastic/datemath'; +import * as rt from 'io-ts'; +import { pipe } from 'fp-ts/lib/pipeable'; +import { fold } from 'fp-ts/lib/Either'; +import { constant, identity } from 'fp-ts/lib/function'; +import { useUrlState } from '../../../utils/use_url_state'; +import { replaceStateKeyInQueryString } from '../../../utils/url_state'; + +const parseRange = (range: MetricsTimeInput) => { + const parsedFrom = dateMath.parse(range.from.toString()); + const parsedTo = dateMath.parse(range.to.toString(), { roundUp: true }); + return { + ...range, + from: + (parsedFrom && parsedFrom.valueOf()) || + moment() + .subtract(1, 'hour') + .valueOf(), + to: (parsedTo && parsedTo.valueOf()) || moment().valueOf(), + }; +}; + +const DEFAULT_TIMERANGE: MetricsTimeInput = { + from: 'now-1h', + to: 'now', + interval: '>=1m', +}; + +const DEFAULT_URL_STATE: MetricsTimeUrlState = { + time: DEFAULT_TIMERANGE, + autoReload: false, + refreshInterval: 5000, +}; + +export const useMetricsTime = () => { + const [urlState, setUrlState] = useUrlState<MetricsTimeUrlState>({ + defaultState: DEFAULT_URL_STATE, + decodeUrlState, + encodeUrlState, + urlStateKey: 'metricTime', + }); + + const [isAutoReloading, setAutoReload] = useState(urlState.autoReload || false); + const [refreshInterval, setRefreshInterval] = useState(urlState.refreshInterval || 5000); + const [lastRefresh, setLastRefresh] = useState<number>(moment().valueOf()); + const [timeRange, setTimeRange] = useState(urlState.time || DEFAULT_TIMERANGE); + + useEffect(() => { + const newState = { + time: timeRange, + autoReload: isAutoReloading, + refreshInterval, + }; + return setUrlState(newState); + }, [isAutoReloading, refreshInterval, setUrlState, timeRange]); + + const [parsedTimeRange, setParsedTimeRange] = useState( + parseRange(urlState.time || DEFAULT_TIMERANGE) + ); + + const updateTimeRange = useCallback((range: MetricsTimeInput) => { + setTimeRange(range); + setParsedTimeRange(parseRange(range)); + }, []); + + return { + timeRange, + setTimeRange: updateTimeRange, + parsedTimeRange, + refreshInterval, + setRefreshInterval, + isAutoReloading, + setAutoReload, + lastRefresh, + triggerRefresh: useCallback(() => { + return setLastRefresh(moment().valueOf()); + }, [setLastRefresh]), + }; +}; + +export const MetricsTimeInputRT = rt.type({ + from: rt.union([rt.string, rt.number]), + to: rt.union([rt.string, rt.number]), + interval: rt.string, +}); +export type MetricsTimeInput = rt.TypeOf<typeof MetricsTimeInputRT>; + +export const MetricsTimeUrlStateRT = rt.partial({ + time: MetricsTimeInputRT, + autoReload: rt.boolean, + refreshInterval: rt.number, +}); +export type MetricsTimeUrlState = rt.TypeOf<typeof MetricsTimeUrlStateRT>; + +const encodeUrlState = MetricsTimeUrlStateRT.encode; +const decodeUrlState = (value: unknown) => + pipe(MetricsTimeUrlStateRT.decode(value), fold(constant(undefined), identity)); + +export const replaceMetricTimeInQueryString = (from: number, to: number) => + Number.isNaN(from) || Number.isNaN(to) + ? (value: string) => value + : replaceStateKeyInQueryString<MetricsTimeUrlState>('metricTime', { + autoReload: false, + time: { + interval: '>=1m', + from: moment(from).toISOString(), + to: moment(to).toISOString(), + }, + }); + +export const MetricsTimeContainer = createContainer(useMetricsTime); +export const [MetricsTimeProvider, useMetricsTimeContext] = MetricsTimeContainer; diff --git a/x-pack/plugins/infra/public/pages/metrics/index.tsx b/x-pack/plugins/infra/public/pages/metrics/index.tsx index 52c9825a4d614..531be40d2dc43 100644 --- a/x-pack/plugins/infra/public/pages/metrics/index.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/index.tsx @@ -9,7 +9,6 @@ import { euiStyled, EuiTheme, withTheme } from '../../../../observability/public import { DocumentTitle } from '../../components/document_title'; import { Header } from '../../components/header'; import { ColumnarPage, PageContent } from '../../components/page'; -import { WithMetricsTime, WithMetricsTimeUrlState } from './containers/with_metrics_time'; import { withMetricPageProviders } from './page_providers'; import { useMetadata } from '../../containers/metadata/use_metadata'; import { Source } from '../../containers/source'; @@ -19,6 +18,7 @@ import { NavItem } from './lib/side_nav_context'; import { NodeDetailsPage } from './components/node_details_page'; import { useKibana } from '../../../../../../src/plugins/kibana_react/public'; import { InventoryItemType } from '../../../common/inventory_models/types'; +import { useMetricsTimeContext } from './hooks/use_metrics_time'; import { useLinkProps } from '../../hooks/use_link_props'; const DetailPageContent = euiStyled(PageContent)` @@ -37,19 +37,29 @@ interface Props { } export const MetricDetail = withMetricPageProviders( - withTheme(({ match, theme }: Props) => { + withTheme(({ match }: Props) => { const uiCapabilities = useKibana().services.application?.capabilities; const nodeId = match.params.node; const nodeType = match.params.type as InventoryItemType; const inventoryModel = findInventoryModel(nodeType); const { sourceId } = useContext(Source.Context); + const { + timeRange, + parsedTimeRange, + setTimeRange, + refreshInterval, + setRefreshInterval, + isAutoReloading, + setAutoReload, + triggerRefresh, + } = useMetricsTimeContext(); const { name, filteredRequiredMetrics, loading: metadataLoading, cloudId, metadata, - } = useMetadata(nodeId, nodeType, inventoryModel.requiredMetrics, sourceId); + } = useMetadata(nodeId, nodeType, inventoryModel.requiredMetrics, sourceId, parsedTimeRange); const [sideNav, setSideNav] = useState<NavItem[]>([]); @@ -90,58 +100,41 @@ export const MetricDetail = withMetricPageProviders( } return ( - <WithMetricsTime> - {({ - timeRange, - parsedTimeRange, - setTimeRange, - refreshInterval, - setRefreshInterval, - isAutoReloading, - setAutoReload, - triggerRefresh, - }) => ( - <ColumnarPage> - <Header - breadcrumbs={breadcrumbs} - readOnlyBadge={!uiCapabilities?.infrastructure?.save} - /> - <WithMetricsTimeUrlState /> - <DocumentTitle - title={i18n.translate('xpack.infra.metricDetailPage.documentTitle', { - defaultMessage: 'Infrastructure | Metrics | {name}', - values: { - name, - }, - })} + <ColumnarPage> + <Header breadcrumbs={breadcrumbs} readOnlyBadge={!uiCapabilities?.infrastructure?.save} /> + <DocumentTitle + title={i18n.translate('xpack.infra.metricDetailPage.documentTitle', { + defaultMessage: 'Infrastructure | Metrics | {name}', + values: { + name, + }, + })} + /> + <DetailPageContent data-test-subj="infraMetricsPage"> + {metadata ? ( + <NodeDetailsPage + name={name} + requiredMetrics={filteredRequiredMetrics} + sourceId={sourceId} + timeRange={timeRange} + parsedTimeRange={parsedTimeRange} + nodeType={nodeType} + nodeId={nodeId} + cloudId={cloudId} + metadataLoading={metadataLoading} + isAutoReloading={isAutoReloading} + refreshInterval={refreshInterval} + sideNav={sideNav} + metadata={metadata} + addNavItem={addNavItem} + setRefreshInterval={setRefreshInterval} + setAutoReload={setAutoReload} + triggerRefresh={triggerRefresh} + setTimeRange={setTimeRange} /> - <DetailPageContent data-test-subj="infraMetricsPage"> - {metadata ? ( - <NodeDetailsPage - name={name} - requiredMetrics={filteredRequiredMetrics} - sourceId={sourceId} - timeRange={timeRange} - parsedTimeRange={parsedTimeRange} - nodeType={nodeType} - nodeId={nodeId} - cloudId={cloudId} - metadataLoading={metadataLoading} - isAutoReloading={isAutoReloading} - refreshInterval={refreshInterval} - sideNav={sideNav} - metadata={metadata} - addNavItem={addNavItem} - setRefreshInterval={setRefreshInterval} - setAutoReload={setAutoReload} - triggerRefresh={triggerRefresh} - setTimeRange={setTimeRange} - /> - ) : null} - </DetailPageContent> - </ColumnarPage> - )} - </WithMetricsTime> + ) : null} + </DetailPageContent> + </ColumnarPage> ); }) ); diff --git a/x-pack/plugins/infra/public/pages/metrics/page_providers.tsx b/x-pack/plugins/infra/public/pages/metrics/page_providers.tsx index 0abbd597dd65c..d3f10adec06ed 100644 --- a/x-pack/plugins/infra/public/pages/metrics/page_providers.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/page_providers.tsx @@ -6,15 +6,15 @@ import React from 'react'; -import { MetricsTimeContainer } from './containers/with_metrics_time'; import { Source } from '../../containers/source'; +import { MetricsTimeProvider } from './hooks/use_metrics_time'; export const withMetricPageProviders = <T extends object>(Component: React.ComponentType<T>) => ( props: T ) => ( <Source.Provider sourceId="default"> - <MetricsTimeContainer.Provider> + <MetricsTimeProvider> <Component {...props} /> - </MetricsTimeContainer.Provider> + </MetricsTimeProvider> </Source.Provider> ); diff --git a/x-pack/plugins/infra/public/pages/metrics/types.ts b/x-pack/plugins/infra/public/pages/metrics/types.ts index fd6243292ec07..2cc261df28977 100644 --- a/x-pack/plugins/infra/public/pages/metrics/types.ts +++ b/x-pack/plugins/infra/public/pages/metrics/types.ts @@ -7,7 +7,7 @@ import rt from 'io-ts'; import { EuiTheme } from '../../../../observability/public'; import { InventoryFormatterTypeRT } from '../../../common/inventory_models/types'; -import { MetricsTimeInput } from './containers/with_metrics_time'; +import { MetricsTimeInput } from './hooks/use_metrics_time'; import { NodeDetailsMetricData } from '../../../common/http_api/node_details_api'; export interface LayoutProps { diff --git a/x-pack/plugins/infra/public/store/actions.ts b/x-pack/plugins/infra/public/store/actions.ts deleted file mode 100644 index 8a5d1f6c668d0..0000000000000 --- a/x-pack/plugins/infra/public/store/actions.ts +++ /dev/null @@ -1,7 +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. - */ - -export { waffleFilterActions, waffleTimeActions, waffleOptionsActions } from './local'; diff --git a/x-pack/plugins/infra/public/store/epics.ts b/x-pack/plugins/infra/public/store/epics.ts deleted file mode 100644 index b5e48a4ec6214..0000000000000 --- a/x-pack/plugins/infra/public/store/epics.ts +++ /dev/null @@ -1,11 +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 { combineEpics } from 'redux-observable'; - -import { createLocalEpic } from './local'; - -export const createRootEpic = <State>() => combineEpics(createLocalEpic<State>()); diff --git a/x-pack/plugins/infra/public/store/index.ts b/x-pack/plugins/infra/public/store/index.ts deleted file mode 100644 index 025da41ec40d5..0000000000000 --- a/x-pack/plugins/infra/public/store/index.ts +++ /dev/null @@ -1,11 +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. - */ - -export * from './actions'; -export * from './epics'; -export * from './reducer'; -export * from './selectors'; -export { createStore } from './store'; diff --git a/x-pack/plugins/infra/public/store/local/actions.ts b/x-pack/plugins/infra/public/store/local/actions.ts deleted file mode 100644 index 1c79d5a515cd4..0000000000000 --- a/x-pack/plugins/infra/public/store/local/actions.ts +++ /dev/null @@ -1,9 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -export { waffleFilterActions } from './waffle_filter'; -export { waffleTimeActions } from './waffle_time'; -export { waffleOptionsActions } from './waffle_options'; diff --git a/x-pack/plugins/infra/public/store/local/epic.ts b/x-pack/plugins/infra/public/store/local/epic.ts deleted file mode 100644 index e1a051355576f..0000000000000 --- a/x-pack/plugins/infra/public/store/local/epic.ts +++ /dev/null @@ -1,11 +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 { combineEpics } from 'redux-observable'; - -import { createWaffleTimeEpic } from './waffle_time'; - -export const createLocalEpic = <State>() => combineEpics(createWaffleTimeEpic<State>()); diff --git a/x-pack/plugins/infra/public/store/local/index.ts b/x-pack/plugins/infra/public/store/local/index.ts deleted file mode 100644 index c2843320bfd0c..0000000000000 --- a/x-pack/plugins/infra/public/store/local/index.ts +++ /dev/null @@ -1,10 +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. - */ - -export * from './actions'; -export * from './epic'; -export * from './reducer'; -export * from './selectors'; diff --git a/x-pack/plugins/infra/public/store/local/reducer.ts b/x-pack/plugins/infra/public/store/local/reducer.ts deleted file mode 100644 index 9e194a5d37f49..0000000000000 --- a/x-pack/plugins/infra/public/store/local/reducer.ts +++ /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 { combineReducers } from 'redux'; - -import { initialWaffleFilterState, waffleFilterReducer, WaffleFilterState } from './waffle_filter'; -import { - initialWaffleOptionsState, - waffleOptionsReducer, - WaffleOptionsState, -} from './waffle_options'; -import { initialWaffleTimeState, waffleTimeReducer, WaffleTimeState } from './waffle_time'; - -export interface LocalState { - waffleFilter: WaffleFilterState; - waffleTime: WaffleTimeState; - waffleMetrics: WaffleOptionsState; -} - -export const initialLocalState: LocalState = { - waffleFilter: initialWaffleFilterState, - waffleTime: initialWaffleTimeState, - waffleMetrics: initialWaffleOptionsState, -}; - -export const localReducer = combineReducers<LocalState>({ - waffleFilter: waffleFilterReducer, - waffleTime: waffleTimeReducer, - waffleMetrics: waffleOptionsReducer, -}); diff --git a/x-pack/plugins/infra/public/store/local/selectors.ts b/x-pack/plugins/infra/public/store/local/selectors.ts deleted file mode 100644 index 56ffc53c2bc72..0000000000000 --- a/x-pack/plugins/infra/public/store/local/selectors.ts +++ /dev/null @@ -1,26 +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 { globalizeSelectors } from '../../utils/typed_redux'; -import { LocalState } from './reducer'; -import { waffleFilterSelectors as innerWaffleFilterSelectors } from './waffle_filter'; -import { waffleOptionsSelectors as innerWaffleOptionsSelectors } from './waffle_options'; -import { waffleTimeSelectors as innerWaffleTimeSelectors } from './waffle_time'; - -export const waffleFilterSelectors = globalizeSelectors( - (state: LocalState) => state.waffleFilter, - innerWaffleFilterSelectors -); - -export const waffleTimeSelectors = globalizeSelectors( - (state: LocalState) => state.waffleTime, - innerWaffleTimeSelectors -); - -export const waffleOptionsSelectors = globalizeSelectors( - (state: LocalState) => state.waffleMetrics, - innerWaffleOptionsSelectors -); diff --git a/x-pack/plugins/infra/public/store/local/waffle_filter/actions.ts b/x-pack/plugins/infra/public/store/local/waffle_filter/actions.ts deleted file mode 100644 index a23f9b3108b5b..0000000000000 --- a/x-pack/plugins/infra/public/store/local/waffle_filter/actions.ts +++ /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 actionCreatorFactory from 'typescript-fsa'; - -import { FilterQuery, SerializedFilterQuery } from './reducer'; - -const actionCreator = actionCreatorFactory('x-pack/infra/local/waffle_filter'); - -export const setWaffleFilterQueryDraft = actionCreator<FilterQuery>( - 'SET_WAFFLE_FILTER_QUERY_DRAFT' -); - -export const applyWaffleFilterQuery = actionCreator<SerializedFilterQuery>( - 'APPLY_WAFFLE_FILTER_QUERY' -); diff --git a/x-pack/plugins/infra/public/store/local/waffle_filter/index.ts b/x-pack/plugins/infra/public/store/local/waffle_filter/index.ts deleted file mode 100644 index 558314f2aeda8..0000000000000 --- a/x-pack/plugins/infra/public/store/local/waffle_filter/index.ts +++ /dev/null @@ -1,11 +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 * as waffleFilterActions from './actions'; -import * as waffleFilterSelectors from './selectors'; - -export { waffleFilterActions, waffleFilterSelectors }; -export * from './reducer'; diff --git a/x-pack/plugins/infra/public/store/local/waffle_filter/reducer.ts b/x-pack/plugins/infra/public/store/local/waffle_filter/reducer.ts deleted file mode 100644 index 912ad96357334..0000000000000 --- a/x-pack/plugins/infra/public/store/local/waffle_filter/reducer.ts +++ /dev/null @@ -1,43 +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 { reducerWithInitialState } from 'typescript-fsa-reducers/dist'; - -import { applyWaffleFilterQuery, setWaffleFilterQueryDraft } from './actions'; - -export interface KueryFilterQuery { - kind: 'kuery'; - expression: string; -} - -export type FilterQuery = KueryFilterQuery; - -export interface SerializedFilterQuery { - query: FilterQuery | null; - serializedQuery: string | null; -} - -export interface WaffleFilterState { - filterQuery: SerializedFilterQuery | null; - filterQueryDraft: KueryFilterQuery | null; -} - -export const initialWaffleFilterState: WaffleFilterState = { - filterQuery: null, - filterQueryDraft: null, -}; - -export const waffleFilterReducer = reducerWithInitialState(initialWaffleFilterState) - .case(setWaffleFilterQueryDraft, (state, filterQueryDraft) => ({ - ...state, - filterQueryDraft, - })) - .case(applyWaffleFilterQuery, (state, filterQuery) => ({ - ...state, - filterQuery, - filterQueryDraft: filterQuery.query, - })) - .build(); diff --git a/x-pack/plugins/infra/public/store/local/waffle_filter/selectors.ts b/x-pack/plugins/infra/public/store/local/waffle_filter/selectors.ts deleted file mode 100644 index 047dabd3f0dd3..0000000000000 --- a/x-pack/plugins/infra/public/store/local/waffle_filter/selectors.ts +++ /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 { createSelector } from 'reselect'; - -import { esKuery } from '../../../../../../../src/plugins/data/public'; -import { WaffleFilterState } from './reducer'; - -export const selectWaffleFilterQuery = (state: WaffleFilterState) => - state.filterQuery ? state.filterQuery.query : null; - -export const selectWaffleFilterQueryAsJson = (state: WaffleFilterState) => - state.filterQuery ? state.filterQuery.serializedQuery : null; - -export const selectWaffleFilterQueryDraft = (state: WaffleFilterState) => state.filterQueryDraft; - -export const selectIsWaffleFilterQueryDraftValid = createSelector( - selectWaffleFilterQueryDraft, - filterQueryDraft => { - if (filterQueryDraft && filterQueryDraft.kind === 'kuery') { - try { - esKuery.fromKueryExpression(filterQueryDraft.expression); - } catch (err) { - return false; - } - } - - return true; - } -); diff --git a/x-pack/plugins/infra/public/store/local/waffle_options/actions.ts b/x-pack/plugins/infra/public/store/local/waffle_options/actions.ts deleted file mode 100644 index 88229c31b2056..0000000000000 --- a/x-pack/plugins/infra/public/store/local/waffle_options/actions.ts +++ /dev/null @@ -1,29 +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 actionCreatorFactory from 'typescript-fsa'; -import { - SnapshotGroupBy, - SnapshotMetricInput, - SnapshotCustomMetricInput, -} from '../../../../common/http_api/snapshot_api'; -import { InventoryItemType } from '../../../../common/inventory_models/types'; -import { InfraGroupByOptions, InfraWaffleMapBounds } from '../../../lib/lib'; - -const actionCreator = actionCreatorFactory('x-pack/infra/local/waffle_options'); - -export const changeMetric = actionCreator<SnapshotMetricInput>('CHANGE_METRIC'); -export const changeGroupBy = actionCreator<SnapshotGroupBy>('CHANGE_GROUP_BY'); -export const changeCustomOptions = actionCreator<InfraGroupByOptions[]>('CHANGE_CUSTOM_OPTIONS'); -export const changeNodeType = actionCreator<InventoryItemType>('CHANGE_NODE_TYPE'); -export const changeView = actionCreator<string>('CHANGE_VIEW'); -export const changeBoundsOverride = actionCreator<InfraWaffleMapBounds>('CHANGE_BOUNDS_OVERRIDE'); -export const changeAutoBounds = actionCreator<boolean>('CHANGE_AUTO_BOUNDS'); -export const changeAccount = actionCreator<string>('CHANGE_ACCOUNT'); -export const changeRegion = actionCreator<string>('CHANGE_REGION'); -export const changeCustomMetrics = actionCreator<SnapshotCustomMetricInput[]>( - 'CHANGE_CUSTOM_METRICS' -); diff --git a/x-pack/plugins/infra/public/store/local/waffle_options/index.ts b/x-pack/plugins/infra/public/store/local/waffle_options/index.ts deleted file mode 100644 index 3ecf108eb49d4..0000000000000 --- a/x-pack/plugins/infra/public/store/local/waffle_options/index.ts +++ /dev/null @@ -1,11 +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 * as waffleOptionsActions from './actions'; -import * as waffleOptionsSelectors from './selector'; - -export { waffleOptionsActions, waffleOptionsSelectors }; -export * from './reducer'; diff --git a/x-pack/plugins/infra/public/store/local/waffle_options/reducer.ts b/x-pack/plugins/infra/public/store/local/waffle_options/reducer.ts deleted file mode 100644 index 3789228a7c16b..0000000000000 --- a/x-pack/plugins/infra/public/store/local/waffle_options/reducer.ts +++ /dev/null @@ -1,113 +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 { combineReducers } from 'redux'; -import { reducerWithInitialState } from 'typescript-fsa-reducers'; -import { - SnapshotMetricInput, - SnapshotGroupBy, - SnapshotCustomMetricInput, -} from '../../../../common/http_api/snapshot_api'; -import { InfraGroupByOptions, InfraWaffleMapBounds } from '../../../lib/lib'; -import { - changeAutoBounds, - changeBoundsOverride, - changeCustomOptions, - changeGroupBy, - changeMetric, - changeNodeType, - changeView, - changeAccount, - changeRegion, - changeCustomMetrics, -} from './actions'; -import { InventoryItemType } from '../../../../common/inventory_models/types'; - -export interface WaffleOptionsState { - metric: SnapshotMetricInput; - groupBy: SnapshotGroupBy; - nodeType: InventoryItemType; - view: string; - customOptions: InfraGroupByOptions[]; - boundsOverride: InfraWaffleMapBounds; - autoBounds: boolean; - accountId: string; - region: string; - customMetrics: SnapshotCustomMetricInput[]; -} - -export const initialWaffleOptionsState: WaffleOptionsState = { - metric: { type: 'cpu' }, - groupBy: [], - nodeType: 'host', - view: 'map', - customOptions: [], - boundsOverride: { max: 1, min: 0 }, - autoBounds: true, - accountId: '', - region: '', - customMetrics: [], -}; - -const currentMetricReducer = reducerWithInitialState(initialWaffleOptionsState.metric).case( - changeMetric, - (current, target) => target -); - -const currentCustomOptionsReducer = reducerWithInitialState( - initialWaffleOptionsState.customOptions -).case(changeCustomOptions, (current, target) => target); - -const currentGroupByReducer = reducerWithInitialState(initialWaffleOptionsState.groupBy).case( - changeGroupBy, - (current, target) => target -); - -const currentNodeTypeReducer = reducerWithInitialState(initialWaffleOptionsState.nodeType).case( - changeNodeType, - (current, target) => target -); - -const currentViewReducer = reducerWithInitialState(initialWaffleOptionsState.view).case( - changeView, - (current, target) => target -); - -const currentBoundsOverrideReducer = reducerWithInitialState( - initialWaffleOptionsState.boundsOverride -).case(changeBoundsOverride, (current, target) => target); - -const currentAutoBoundsReducer = reducerWithInitialState(initialWaffleOptionsState.autoBounds).case( - changeAutoBounds, - (current, target) => target -); - -const currentAccountIdReducer = reducerWithInitialState(initialWaffleOptionsState.accountId).case( - changeAccount, - (current, target) => target -); - -const currentRegionReducer = reducerWithInitialState(initialWaffleOptionsState.region).case( - changeRegion, - (current, target) => target -); - -const currentCustomMetricsReducer = reducerWithInitialState( - initialWaffleOptionsState.customMetrics -).case(changeCustomMetrics, (current, target) => target); - -export const waffleOptionsReducer = combineReducers<WaffleOptionsState>({ - metric: currentMetricReducer, - groupBy: currentGroupByReducer, - nodeType: currentNodeTypeReducer, - view: currentViewReducer, - customOptions: currentCustomOptionsReducer, - boundsOverride: currentBoundsOverrideReducer, - autoBounds: currentAutoBoundsReducer, - accountId: currentAccountIdReducer, - region: currentRegionReducer, - customMetrics: currentCustomMetricsReducer, -}); diff --git a/x-pack/plugins/infra/public/store/local/waffle_options/selector.ts b/x-pack/plugins/infra/public/store/local/waffle_options/selector.ts deleted file mode 100644 index 4487af156df97..0000000000000 --- a/x-pack/plugins/infra/public/store/local/waffle_options/selector.ts +++ /dev/null @@ -1,18 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { WaffleOptionsState } from './reducer'; - -export const selectMetric = (state: WaffleOptionsState) => state.metric; -export const selectGroupBy = (state: WaffleOptionsState) => state.groupBy; -export const selectCustomOptions = (state: WaffleOptionsState) => state.customOptions; -export const selectNodeType = (state: WaffleOptionsState) => state.nodeType; -export const selectView = (state: WaffleOptionsState) => state.view; -export const selectBoundsOverride = (state: WaffleOptionsState) => state.boundsOverride; -export const selectAutoBounds = (state: WaffleOptionsState) => state.autoBounds; -export const selectAccountId = (state: WaffleOptionsState) => state.accountId; -export const selectRegion = (state: WaffleOptionsState) => state.region; -export const selectCustomMetrics = (state: WaffleOptionsState) => state.customMetrics; diff --git a/x-pack/plugins/infra/public/store/local/waffle_time/actions.ts b/x-pack/plugins/infra/public/store/local/waffle_time/actions.ts deleted file mode 100644 index fe79f2f536a93..0000000000000 --- a/x-pack/plugins/infra/public/store/local/waffle_time/actions.ts +++ /dev/null @@ -1,15 +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 actionCreatorFactory from 'typescript-fsa'; - -const actionCreator = actionCreatorFactory('x-pack/infra/local/waffle_time'); - -export const jumpToTime = actionCreator<number>('JUMP_TO_TIME'); - -export const startAutoReload = actionCreator('START_AUTO_RELOAD'); - -export const stopAutoReload = actionCreator('STOP_AUTO_RELOAD'); diff --git a/x-pack/plugins/infra/public/store/local/waffle_time/epic.ts b/x-pack/plugins/infra/public/store/local/waffle_time/epic.ts deleted file mode 100644 index 986d6b17a2424..0000000000000 --- a/x-pack/plugins/infra/public/store/local/waffle_time/epic.ts +++ /dev/null @@ -1,38 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { Action } from 'redux'; -import { Epic } from 'redux-observable'; -import { timer } from 'rxjs'; -import { exhaustMap, filter, map, takeUntil, withLatestFrom } from 'rxjs/operators'; - -import { jumpToTime, startAutoReload, stopAutoReload } from './actions'; - -interface WaffleTimeEpicDependencies<State> { - selectWaffleTimeUpdatePolicyInterval: (state: State) => number | null; -} - -export const createWaffleTimeEpic = <State>(): Epic< - Action, - Action, - State, - WaffleTimeEpicDependencies<State> -> => (action$, state$, { selectWaffleTimeUpdatePolicyInterval }) => { - const updateInterval$ = state$.pipe(map(selectWaffleTimeUpdatePolicyInterval), filter(isNotNull)); - - return action$.pipe( - filter(startAutoReload.match), - withLatestFrom(updateInterval$), - exhaustMap(([action, updateInterval]) => - timer(0, updateInterval).pipe( - map(() => jumpToTime(Date.now())), - takeUntil(action$.pipe(filter(stopAutoReload.match))) - ) - ) - ); -}; - -const isNotNull = <T>(value: T | null): value is T => value !== null; diff --git a/x-pack/plugins/infra/public/store/local/waffle_time/index.ts b/x-pack/plugins/infra/public/store/local/waffle_time/index.ts deleted file mode 100644 index 2b99a6d6d5760..0000000000000 --- a/x-pack/plugins/infra/public/store/local/waffle_time/index.ts +++ /dev/null @@ -1,12 +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 * as waffleTimeActions from './actions'; -import * as waffleTimeSelectors from './selectors'; - -export { waffleTimeActions, waffleTimeSelectors }; -export * from './epic'; -export * from './reducer'; diff --git a/x-pack/plugins/infra/public/store/local/waffle_time/reducer.ts b/x-pack/plugins/infra/public/store/local/waffle_time/reducer.ts deleted file mode 100644 index 026e5decf5d37..0000000000000 --- a/x-pack/plugins/infra/public/store/local/waffle_time/reducer.ts +++ /dev/null @@ -1,52 +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 { combineReducers } from 'redux'; -import { reducerWithInitialState } from 'typescript-fsa-reducers/dist'; - -import { jumpToTime, startAutoReload, stopAutoReload } from './actions'; - -interface ManualTimeUpdatePolicy { - policy: 'manual'; -} - -interface IntervalTimeUpdatePolicy { - policy: 'interval'; - interval: number; -} - -type TimeUpdatePolicy = ManualTimeUpdatePolicy | IntervalTimeUpdatePolicy; - -export interface WaffleTimeState { - currentTime: number; - updatePolicy: TimeUpdatePolicy; -} - -export const initialWaffleTimeState: WaffleTimeState = { - currentTime: Date.now(), - updatePolicy: { - policy: 'manual', - }, -}; - -const currentTimeReducer = reducerWithInitialState(initialWaffleTimeState.currentTime).case( - jumpToTime, - (currentTime, targetTime) => targetTime -); - -const updatePolicyReducer = reducerWithInitialState(initialWaffleTimeState.updatePolicy) - .case(startAutoReload, () => ({ - policy: 'interval', - interval: 5000, - })) - .case(stopAutoReload, () => ({ - policy: 'manual', - })); - -export const waffleTimeReducer = combineReducers<WaffleTimeState>({ - currentTime: currentTimeReducer, - updatePolicy: updatePolicyReducer, -}); diff --git a/x-pack/plugins/infra/public/store/local/waffle_time/selectors.ts b/x-pack/plugins/infra/public/store/local/waffle_time/selectors.ts deleted file mode 100644 index 0b6d01bdf5288..0000000000000 --- a/x-pack/plugins/infra/public/store/local/waffle_time/selectors.ts +++ /dev/null @@ -1,23 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { createSelector } from 'reselect'; - -import { WaffleTimeState } from './reducer'; - -export const selectCurrentTime = (state: WaffleTimeState) => state.currentTime; - -export const selectIsAutoReloading = (state: WaffleTimeState) => - state.updatePolicy.policy === 'interval'; - -export const selectTimeUpdatePolicyInterval = (state: WaffleTimeState) => - state.updatePolicy.policy === 'interval' ? state.updatePolicy.interval : null; - -export const selectCurrentTimeRange = createSelector(selectCurrentTime, currentTime => ({ - from: currentTime - 1000 * 60 * 5, - interval: '1m', - to: currentTime, -})); diff --git a/x-pack/plugins/infra/public/store/reducer.ts b/x-pack/plugins/infra/public/store/reducer.ts deleted file mode 100644 index 2536ddbee401b..0000000000000 --- a/x-pack/plugins/infra/public/store/reducer.ts +++ /dev/null @@ -1,21 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { combineReducers } from 'redux'; - -import { initialLocalState, localReducer, LocalState } from './local'; - -export interface State { - local: LocalState; -} - -export const initialState: State = { - local: initialLocalState, -}; - -export const reducer = combineReducers<State>({ - local: localReducer, -}); diff --git a/x-pack/plugins/infra/public/store/selectors.ts b/x-pack/plugins/infra/public/store/selectors.ts deleted file mode 100644 index f4011c232cba4..0000000000000 --- a/x-pack/plugins/infra/public/store/selectors.ts +++ /dev/null @@ -1,22 +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 { globalizeSelectors } from '../utils/typed_redux'; -import { - waffleFilterSelectors as localWaffleFilterSelectors, - waffleOptionsSelectors as localWaffleOptionsSelectors, - waffleTimeSelectors as localWaffleTimeSelectors, -} from './local'; -import { State } from './reducer'; -/** - * local selectors - */ - -const selectLocal = (state: State) => state.local; - -export const waffleFilterSelectors = globalizeSelectors(selectLocal, localWaffleFilterSelectors); -export const waffleTimeSelectors = globalizeSelectors(selectLocal, localWaffleTimeSelectors); -export const waffleOptionsSelectors = globalizeSelectors(selectLocal, localWaffleOptionsSelectors); diff --git a/x-pack/plugins/infra/public/store/store.ts b/x-pack/plugins/infra/public/store/store.ts deleted file mode 100644 index cae0622c5e4a1..0000000000000 --- a/x-pack/plugins/infra/public/store/store.ts +++ /dev/null @@ -1,50 +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 { Action, applyMiddleware, compose, createStore as createBasicStore } from 'redux'; -import { createEpicMiddleware } from 'redux-observable'; -import { Observable } from 'rxjs'; -import { map } from 'rxjs/operators'; - -import { createRootEpic, initialState, reducer, State, waffleTimeSelectors } from '.'; -import { InfraApolloClient, InfraObservableApi } from '../lib/lib'; - -declare global { - interface Window { - __REDUX_DEVTOOLS_EXTENSION_COMPOSE__: typeof compose; - } -} - -export interface StoreDependencies { - apolloClient: Observable<InfraApolloClient>; - observableApi: Observable<InfraObservableApi>; -} - -export function createStore({ apolloClient, observableApi }: StoreDependencies) { - const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose; - - const middlewareDependencies = { - postToApi$: observableApi.pipe(map(({ post }) => post)), - apolloClient$: apolloClient, - selectWaffleTimeUpdatePolicyInterval: waffleTimeSelectors.selectTimeUpdatePolicyInterval, - }; - - const epicMiddleware = createEpicMiddleware<Action, Action, State, typeof middlewareDependencies>( - { - dependencies: middlewareDependencies, - } - ); - - const store = createBasicStore( - reducer, - initialState, - composeEnhancers(applyMiddleware(epicMiddleware)) - ); - - epicMiddleware.run(createRootEpic<State>()); - - return store; -} diff --git a/x-pack/plugins/infra/public/utils/redux_context.tsx b/x-pack/plugins/infra/public/utils/redux_context.tsx deleted file mode 100644 index f249d72a6b56f..0000000000000 --- a/x-pack/plugins/infra/public/utils/redux_context.tsx +++ /dev/null @@ -1,16 +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 { useSelector } from 'react-redux'; -import React, { createContext } from 'react'; -import { State, initialState } from '../store'; - -export const ReduxStateContext = createContext(initialState); - -export const ReduxStateContextProvider = ({ children }: { children: JSX.Element }) => { - const state = useSelector((store: State) => store); - return <ReduxStateContext.Provider value={state}>{children}</ReduxStateContext.Provider>; -}; diff --git a/x-pack/plugins/infra/server/infra_server.ts b/x-pack/plugins/infra/server/infra_server.ts index fb9dd172bf6ed..88b78dfd3e41c 100644 --- a/x-pack/plugins/infra/server/infra_server.ts +++ b/x-pack/plugins/infra/server/infra_server.ts @@ -29,6 +29,7 @@ import { initLogEntriesItemRoute, } from './routes/log_entries'; import { initInventoryMetaRoute } from './routes/inventory_metadata'; +import { initSourceRoute } from './routes/source'; export const initInfraServer = (libs: InfraBackendLibs) => { const schema = makeExecutableSchema({ @@ -48,6 +49,7 @@ export const initInfraServer = (libs: InfraBackendLibs) => { initGetLogEntryRateRoute(libs); initSnapshotRoute(libs); initNodeDetailsRoute(libs); + initSourceRoute(libs); initValidateLogAnalysisIndicesRoute(libs); initLogEntriesRoute(libs); initLogEntriesHighlightsRoute(libs); diff --git a/x-pack/plugins/infra/server/lib/adapters/framework/adapter_types.ts b/x-pack/plugins/infra/server/lib/adapters/framework/adapter_types.ts index 8ddd3935bcc33..038fd457fb6c7 100644 --- a/x-pack/plugins/infra/server/lib/adapters/framework/adapter_types.ts +++ b/x-pack/plugins/infra/server/lib/adapters/framework/adapter_types.ts @@ -20,7 +20,7 @@ export interface InfraServerPluginDeps { home: HomeServerPluginSetup; spaces: SpacesPluginSetup; usageCollection: UsageCollectionSetup; - metrics: VisTypeTimeseriesSetup; + visTypeTimeseries: VisTypeTimeseriesSetup; features: FeaturesPluginSetup; apm: APMPluginContract; alerting: AlertingPluginContract; diff --git a/x-pack/plugins/infra/server/lib/adapters/framework/kibana_framework_adapter.ts b/x-pack/plugins/infra/server/lib/adapters/framework/kibana_framework_adapter.ts index b73acd6703054..eda1fbfa5f4ce 100644 --- a/x-pack/plugins/infra/server/lib/adapters/framework/kibana_framework_adapter.ts +++ b/x-pack/plugins/infra/server/lib/adapters/framework/kibana_framework_adapter.ts @@ -245,7 +245,7 @@ export class KibanaFramework { timerange: { min: number; max: number }, filters: any[] ): Promise<InfraTSVBResponse> { - const { getVisData } = this.plugins.metrics; + const { getVisData } = this.plugins.visTypeTimeseries; if (typeof getVisData !== 'function') { throw new Error('TSVB is not available'); } 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 38cd0cec145f9..000d0823311b3 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 @@ -8,62 +8,77 @@ import { createMetricThresholdExecutor, FIRED_ACTIONS } from './metric_threshold import { Comparator, AlertStates } from './types'; import * as mocks from './test_mocks'; import { AlertExecutorOptions } from '../../../../../alerting/server'; +import { + alertsMock, + AlertServicesMock, + AlertInstanceMock, +} from '../../../../../alerting/server/mocks'; const executor = createMetricThresholdExecutor('test') as (opts: { params: AlertExecutorOptions['params']; services: { callCluster: AlertExecutorOptions['params']['callCluster'] }; }) => Promise<void>; -const alertInstances = new Map(); -const services = { - callCluster(_: string, { body, index }: any) { - if (index === 'alternatebeat-*') return mocks.changedSourceIdResponse; - const metric = body.query.bool.filter[1]?.exists.field; - if (body.aggs.groupings) { - if (body.aggs.groupings.composite.after) { - return mocks.compositeEndResponse; - } - if (metric === 'test.metric.2') { - return mocks.alternateCompositeResponse; - } - return mocks.basicCompositeResponse; +const services: AlertServicesMock = alertsMock.createAlertServices(); +services.callCluster.mockImplementation((_: string, { body, index }: any) => { + if (index === 'alternatebeat-*') return mocks.changedSourceIdResponse; + const metric = body.query.bool.filter[1]?.exists.field; + if (body.aggs.groupings) { + if (body.aggs.groupings.composite.after) { + return mocks.compositeEndResponse; } if (metric === 'test.metric.2') { - return mocks.alternateMetricResponse; + return mocks.alternateCompositeResponse; } - return mocks.basicMetricResponse; - }, - alertInstanceFactory(instanceID: string) { - let state: any; - const actionQueue: any[] = []; - const instance = { - actionQueue: [], - get state() { - return state; - }, - get mostRecentAction() { - return actionQueue.pop(); - }, - }; - alertInstances.set(instanceID, instance); + return mocks.basicCompositeResponse; + } + if (metric === 'test.metric.2') { + return mocks.alternateMetricResponse; + } + return mocks.basicMetricResponse; +}); +services.savedObjectsClient.get.mockImplementation(async (type: string, sourceId: string) => { + if (sourceId === 'alternate') return { - instanceID, - scheduleActions(id: string, action: any) { - actionQueue.push({ id, action }); - }, - replaceState(newState: any) { - state = newState; - }, + id: 'alternate', + attributes: { metricAlias: 'alternatebeat-*' }, + type, + references: [], }; - }, - savedObjectsClient: { - get(_: string, sourceId: string) { - if (sourceId === 'alternate') - return { id: 'alternate', attributes: { metricAlias: 'alternatebeat-*' } }; - return { id: 'default', attributes: { metricAlias: 'metricbeat-*' } }; - }, - }, -}; + return { id: 'default', attributes: { metricAlias: 'metricbeat-*' }, type, references: [] }; +}); + +interface AlertTestInstance { + instance: AlertInstanceMock; + actionQueue: any[]; + state: any; +} +const alertInstances = new Map<string, AlertTestInstance>(); +services.alertInstanceFactory.mockImplementation((instanceID: string) => { + const alertInstance: AlertTestInstance = { + instance: alertsMock.createAlertInstanceFactory(), + actionQueue: [], + state: {}, + }; + alertInstances.set(instanceID, alertInstance); + alertInstance.instance.replaceState.mockImplementation((newState: any) => { + alertInstance.state = newState; + return alertInstance.instance; + }); + alertInstance.instance.scheduleActions.mockImplementation((id: string, action: any) => { + alertInstance.actionQueue.push({ id, action }); + return alertInstance.instance; + }); + return alertInstance.instance; +}); + +function mostRecentAction(id: string) { + return alertInstances.get(id)!.actionQueue.pop(); +} + +function getState(id: string) { + return alertInstances.get(id)!.state; +} const baseCriterion = { aggType: 'avg', @@ -90,65 +105,65 @@ describe('The metric threshold alert type', () => { }); test('alerts as expected with the > comparator', async () => { await execute(Comparator.GT, [0.75]); - expect(alertInstances.get(instanceID).mostRecentAction.id).toBe(FIRED_ACTIONS.id); - expect(alertInstances.get(instanceID).state.alertState).toBe(AlertStates.ALERT); + expect(mostRecentAction(instanceID).id).toBe(FIRED_ACTIONS.id); + expect(getState(instanceID).alertState).toBe(AlertStates.ALERT); await execute(Comparator.GT, [1.5]); - expect(alertInstances.get(instanceID).mostRecentAction).toBe(undefined); - expect(alertInstances.get(instanceID).state.alertState).toBe(AlertStates.OK); + expect(mostRecentAction(instanceID)).toBe(undefined); + expect(getState(instanceID).alertState).toBe(AlertStates.OK); }); test('alerts as expected with the < comparator', async () => { await execute(Comparator.LT, [1.5]); - expect(alertInstances.get(instanceID).mostRecentAction.id).toBe(FIRED_ACTIONS.id); - expect(alertInstances.get(instanceID).state.alertState).toBe(AlertStates.ALERT); + expect(mostRecentAction(instanceID).id).toBe(FIRED_ACTIONS.id); + expect(getState(instanceID).alertState).toBe(AlertStates.ALERT); await execute(Comparator.LT, [0.75]); - expect(alertInstances.get(instanceID).mostRecentAction).toBe(undefined); - expect(alertInstances.get(instanceID).state.alertState).toBe(AlertStates.OK); + expect(mostRecentAction(instanceID)).toBe(undefined); + expect(getState(instanceID).alertState).toBe(AlertStates.OK); }); test('alerts as expected with the >= comparator', async () => { await execute(Comparator.GT_OR_EQ, [0.75]); - expect(alertInstances.get(instanceID).mostRecentAction.id).toBe(FIRED_ACTIONS.id); - expect(alertInstances.get(instanceID).state.alertState).toBe(AlertStates.ALERT); + expect(mostRecentAction(instanceID).id).toBe(FIRED_ACTIONS.id); + expect(getState(instanceID).alertState).toBe(AlertStates.ALERT); await execute(Comparator.GT_OR_EQ, [1.0]); - expect(alertInstances.get(instanceID).mostRecentAction.id).toBe(FIRED_ACTIONS.id); - expect(alertInstances.get(instanceID).state.alertState).toBe(AlertStates.ALERT); + expect(mostRecentAction(instanceID).id).toBe(FIRED_ACTIONS.id); + expect(getState(instanceID).alertState).toBe(AlertStates.ALERT); await execute(Comparator.GT_OR_EQ, [1.5]); - expect(alertInstances.get(instanceID).mostRecentAction).toBe(undefined); - expect(alertInstances.get(instanceID).state.alertState).toBe(AlertStates.OK); + expect(mostRecentAction(instanceID)).toBe(undefined); + expect(getState(instanceID).alertState).toBe(AlertStates.OK); }); test('alerts as expected with the <= comparator', async () => { await execute(Comparator.LT_OR_EQ, [1.5]); - expect(alertInstances.get(instanceID).mostRecentAction.id).toBe(FIRED_ACTIONS.id); - expect(alertInstances.get(instanceID).state.alertState).toBe(AlertStates.ALERT); + expect(mostRecentAction(instanceID).id).toBe(FIRED_ACTIONS.id); + expect(getState(instanceID).alertState).toBe(AlertStates.ALERT); await execute(Comparator.LT_OR_EQ, [1.0]); - expect(alertInstances.get(instanceID).mostRecentAction.id).toBe(FIRED_ACTIONS.id); - expect(alertInstances.get(instanceID).state.alertState).toBe(AlertStates.ALERT); + expect(mostRecentAction(instanceID).id).toBe(FIRED_ACTIONS.id); + expect(getState(instanceID).alertState).toBe(AlertStates.ALERT); await execute(Comparator.LT_OR_EQ, [0.75]); - expect(alertInstances.get(instanceID).mostRecentAction).toBe(undefined); - expect(alertInstances.get(instanceID).state.alertState).toBe(AlertStates.OK); + expect(mostRecentAction(instanceID)).toBe(undefined); + expect(getState(instanceID).alertState).toBe(AlertStates.OK); }); test('alerts as expected with the between comparator', async () => { await execute(Comparator.BETWEEN, [0, 1.5]); - expect(alertInstances.get(instanceID).mostRecentAction.id).toBe(FIRED_ACTIONS.id); - expect(alertInstances.get(instanceID).state.alertState).toBe(AlertStates.ALERT); + expect(mostRecentAction(instanceID).id).toBe(FIRED_ACTIONS.id); + expect(getState(instanceID).alertState).toBe(AlertStates.ALERT); await execute(Comparator.BETWEEN, [0, 0.75]); - expect(alertInstances.get(instanceID).mostRecentAction).toBe(undefined); - expect(alertInstances.get(instanceID).state.alertState).toBe(AlertStates.OK); + expect(mostRecentAction(instanceID)).toBe(undefined); + expect(getState(instanceID).alertState).toBe(AlertStates.OK); }); test('reports expected values to the action context', async () => { await execute(Comparator.GT, [0.75]); - const mostRecentAction = alertInstances.get(instanceID).mostRecentAction; - expect(mostRecentAction.action.group).toBe('*'); - expect(mostRecentAction.action.valueOf.condition0).toBe(1); - expect(mostRecentAction.action.thresholdOf.condition0).toStrictEqual([0.75]); - expect(mostRecentAction.action.metricOf.condition0).toBe('test.metric.1'); + const { action } = mostRecentAction(instanceID); + expect(action.group).toBe('*'); + expect(action.valueOf.condition0).toBe(1); + expect(action.thresholdOf.condition0).toStrictEqual([0.75]); + expect(action.metricOf.condition0).toBe('test.metric.1'); }); test('fetches the index pattern dynamically', async () => { await execute(Comparator.LT, [17], 'alternate'); - expect(alertInstances.get(instanceID).mostRecentAction.id).toBe(FIRED_ACTIONS.id); - expect(alertInstances.get(instanceID).state.alertState).toBe(AlertStates.ALERT); + expect(mostRecentAction(instanceID).id).toBe(FIRED_ACTIONS.id); + expect(getState(instanceID).alertState).toBe(AlertStates.ALERT); await execute(Comparator.LT, [1.5], 'alternate'); - expect(alertInstances.get(instanceID).mostRecentAction).toBe(undefined); - expect(alertInstances.get(instanceID).state.alertState).toBe(AlertStates.OK); + expect(mostRecentAction(instanceID)).toBe(undefined); + expect(getState(instanceID).alertState).toBe(AlertStates.OK); }); }); @@ -171,29 +186,29 @@ describe('The metric threshold alert type', () => { const instanceIdB = 'test-b'; test('sends an alert when all groups pass the threshold', async () => { await execute(Comparator.GT, [0.75]); - expect(alertInstances.get(instanceIdA).mostRecentAction.id).toBe(FIRED_ACTIONS.id); - expect(alertInstances.get(instanceIdA).state.alertState).toBe(AlertStates.ALERT); - expect(alertInstances.get(instanceIdB).mostRecentAction.id).toBe(FIRED_ACTIONS.id); - expect(alertInstances.get(instanceIdB).state.alertState).toBe(AlertStates.ALERT); + expect(mostRecentAction(instanceIdA).id).toBe(FIRED_ACTIONS.id); + expect(getState(instanceIdA).alertState).toBe(AlertStates.ALERT); + expect(mostRecentAction(instanceIdB).id).toBe(FIRED_ACTIONS.id); + expect(getState(instanceIdB).alertState).toBe(AlertStates.ALERT); }); test('sends an alert when only some groups pass the threshold', async () => { await execute(Comparator.LT, [1.5]); - expect(alertInstances.get(instanceIdA).mostRecentAction.id).toBe(FIRED_ACTIONS.id); - expect(alertInstances.get(instanceIdA).state.alertState).toBe(AlertStates.ALERT); - expect(alertInstances.get(instanceIdB).mostRecentAction).toBe(undefined); - expect(alertInstances.get(instanceIdB).state.alertState).toBe(AlertStates.OK); + expect(mostRecentAction(instanceIdA).id).toBe(FIRED_ACTIONS.id); + expect(getState(instanceIdA).alertState).toBe(AlertStates.ALERT); + expect(mostRecentAction(instanceIdB)).toBe(undefined); + expect(getState(instanceIdB).alertState).toBe(AlertStates.OK); }); test('sends no alert when no groups pass the threshold', async () => { await execute(Comparator.GT, [5]); - expect(alertInstances.get(instanceIdA).mostRecentAction).toBe(undefined); - expect(alertInstances.get(instanceIdA).state.alertState).toBe(AlertStates.OK); - expect(alertInstances.get(instanceIdB).mostRecentAction).toBe(undefined); - expect(alertInstances.get(instanceIdB).state.alertState).toBe(AlertStates.OK); + expect(mostRecentAction(instanceIdA)).toBe(undefined); + expect(getState(instanceIdA).alertState).toBe(AlertStates.OK); + expect(mostRecentAction(instanceIdB)).toBe(undefined); + expect(getState(instanceIdB).alertState).toBe(AlertStates.OK); }); test('reports group values to the action context', async () => { await execute(Comparator.GT, [0.75]); - expect(alertInstances.get(instanceIdA).mostRecentAction.action.group).toBe('a'); - expect(alertInstances.get(instanceIdB).mostRecentAction.action.group).toBe('b'); + expect(mostRecentAction(instanceIdA).action.group).toBe('a'); + expect(mostRecentAction(instanceIdB).action.group).toBe('b'); }); }); @@ -226,34 +241,34 @@ describe('The metric threshold alert type', () => { test('sends an alert when all criteria cross the threshold', async () => { const instanceID = 'test-*'; await execute(Comparator.GT_OR_EQ, [1.0], [3.0]); - expect(alertInstances.get(instanceID).mostRecentAction.id).toBe(FIRED_ACTIONS.id); - expect(alertInstances.get(instanceID).state.alertState).toBe(AlertStates.ALERT); + expect(mostRecentAction(instanceID).id).toBe(FIRED_ACTIONS.id); + expect(getState(instanceID).alertState).toBe(AlertStates.ALERT); }); test('sends no alert when some, but not all, criteria cross the threshold', async () => { const instanceID = 'test-*'; await execute(Comparator.LT_OR_EQ, [1.0], [3.0]); - expect(alertInstances.get(instanceID).mostRecentAction).toBe(undefined); - expect(alertInstances.get(instanceID).state.alertState).toBe(AlertStates.OK); + expect(mostRecentAction(instanceID)).toBe(undefined); + expect(getState(instanceID).alertState).toBe(AlertStates.OK); }); test('alerts only on groups that meet all criteria when querying with a groupBy parameter', async () => { const instanceIdA = 'test-a'; const instanceIdB = 'test-b'; await execute(Comparator.GT_OR_EQ, [1.0], [3.0], 'something'); - expect(alertInstances.get(instanceIdA).mostRecentAction.id).toBe(FIRED_ACTIONS.id); - expect(alertInstances.get(instanceIdA).state.alertState).toBe(AlertStates.ALERT); - expect(alertInstances.get(instanceIdB).mostRecentAction).toBe(undefined); - expect(alertInstances.get(instanceIdB).state.alertState).toBe(AlertStates.OK); + expect(mostRecentAction(instanceIdA).id).toBe(FIRED_ACTIONS.id); + expect(getState(instanceIdA).alertState).toBe(AlertStates.ALERT); + expect(mostRecentAction(instanceIdB)).toBe(undefined); + expect(getState(instanceIdB).alertState).toBe(AlertStates.OK); }); test('sends all criteria to the action context', async () => { const instanceID = 'test-*'; await execute(Comparator.GT_OR_EQ, [1.0], [3.0]); - const mostRecentAction = alertInstances.get(instanceID).mostRecentAction; - expect(mostRecentAction.action.valueOf.condition0).toBe(1); - expect(mostRecentAction.action.valueOf.condition1).toBe(3.5); - expect(mostRecentAction.action.thresholdOf.condition0).toStrictEqual([1.0]); - expect(mostRecentAction.action.thresholdOf.condition1).toStrictEqual([3.0]); - expect(mostRecentAction.action.metricOf.condition0).toBe('test.metric.1'); - expect(mostRecentAction.action.metricOf.condition1).toBe('test.metric.2'); + const { action } = mostRecentAction(instanceID); + expect(action.valueOf.condition0).toBe(1); + expect(action.valueOf.condition1).toBe(3.5); + expect(action.thresholdOf.condition0).toStrictEqual([1.0]); + expect(action.thresholdOf.condition1).toStrictEqual([3.0]); + expect(action.metricOf.condition0).toBe('test.metric.1'); + expect(action.metricOf.condition1).toBe('test.metric.2'); }); }); describe('querying with the count aggregator', () => { @@ -275,11 +290,11 @@ describe('The metric threshold alert type', () => { }); test('alerts based on the doc_count value instead of the aggregatedValue', async () => { await execute(Comparator.GT, [2]); - expect(alertInstances.get(instanceID).mostRecentAction.id).toBe(FIRED_ACTIONS.id); - expect(alertInstances.get(instanceID).state.alertState).toBe(AlertStates.ALERT); + expect(mostRecentAction(instanceID).id).toBe(FIRED_ACTIONS.id); + expect(getState(instanceID).alertState).toBe(AlertStates.ALERT); await execute(Comparator.LT, [1.5]); - expect(alertInstances.get(instanceID).mostRecentAction).toBe(undefined); - expect(alertInstances.get(instanceID).state.alertState).toBe(AlertStates.OK); + expect(mostRecentAction(instanceID)).toBe(undefined); + expect(getState(instanceID).alertState).toBe(AlertStates.OK); }); }); }); diff --git a/x-pack/plugins/infra/server/lib/sources/defaults.ts b/x-pack/plugins/infra/server/lib/sources/defaults.ts index b9ead0d169ee6..6b00f59cca1f4 100644 --- a/x-pack/plugins/infra/server/lib/sources/defaults.ts +++ b/x-pack/plugins/infra/server/lib/sources/defaults.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { InfraSourceConfiguration } from './types'; +import { InfraSourceConfiguration } from '../../../common/http_api/source_api'; export const defaultSourceConfiguration: InfraSourceConfiguration = { name: 'Default', diff --git a/x-pack/plugins/infra/server/lib/sources/index.ts b/x-pack/plugins/infra/server/lib/sources/index.ts index 6837f953ea18a..9dcbe02bd064b 100644 --- a/x-pack/plugins/infra/server/lib/sources/index.ts +++ b/x-pack/plugins/infra/server/lib/sources/index.ts @@ -7,4 +7,4 @@ export * from './defaults'; export * from './saved_object_mappings'; export * from './sources'; -export * from './types'; +export * from '../../../common/http_api/source_api'; diff --git a/x-pack/plugins/infra/server/lib/sources/saved_object_mappings.ts b/x-pack/plugins/infra/server/lib/sources/saved_object_mappings.ts index 973a790eeedaf..e5b230373b7ec 100644 --- a/x-pack/plugins/infra/server/lib/sources/saved_object_mappings.ts +++ b/x-pack/plugins/infra/server/lib/sources/saved_object_mappings.ts @@ -5,7 +5,7 @@ */ import { ElasticsearchMappingOf } from '../../utils/typed_elasticsearch_mappings'; -import { InfraSavedSourceConfiguration } from './types'; +import { InfraSavedSourceConfiguration } from '../../../common/http_api/source_api'; export const infraSourceConfigurationSavedObjectType = 'infrastructure-ui-source'; diff --git a/x-pack/plugins/infra/server/lib/sources/sources.ts b/x-pack/plugins/infra/server/lib/sources/sources.ts index c7ff6c9638204..99e062aa49ccf 100644 --- a/x-pack/plugins/infra/server/lib/sources/sources.ts +++ b/x-pack/plugins/infra/server/lib/sources/sources.ts @@ -20,7 +20,8 @@ import { pickSavedSourceConfiguration, SourceConfigurationSavedObjectRuntimeType, StaticSourceConfigurationRuntimeType, -} from './types'; + InfraSource, +} from '../../../common/http_api/source_api'; import { InfraConfig } from '../../../server'; interface Libs { @@ -35,7 +36,10 @@ export class InfraSources { this.libs = libs; } - public async getSourceConfiguration(requestContext: RequestHandlerContext, sourceId: string) { + public async getSourceConfiguration( + requestContext: RequestHandlerContext, + sourceId: string + ): Promise<InfraSource> { const staticDefaultSourceConfiguration = await this.getStaticDefaultSourceConfiguration(); const savedSourceConfiguration = await this.getInternalSourceConfiguration(sourceId) diff --git a/x-pack/plugins/infra/server/lib/sources/types.ts b/x-pack/plugins/infra/server/lib/sources/types.ts deleted file mode 100644 index 1f850635cf35a..0000000000000 --- a/x-pack/plugins/infra/server/lib/sources/types.ts +++ /dev/null @@ -1,149 +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. - */ - -/* eslint-disable @typescript-eslint/no-empty-interface */ - -import * as runtimeTypes from 'io-ts'; -import moment from 'moment'; -import { pipe } from 'fp-ts/lib/pipeable'; -import { chain } from 'fp-ts/lib/Either'; - -export const TimestampFromString = new runtimeTypes.Type<number, string>( - 'TimestampFromString', - (input): input is number => typeof input === 'number', - (input, context) => - pipe( - runtimeTypes.string.validate(input, context), - chain(stringInput => { - const momentValue = moment(stringInput); - return momentValue.isValid() - ? runtimeTypes.success(momentValue.valueOf()) - : runtimeTypes.failure(stringInput, context); - }) - ), - output => new Date(output).toISOString() -); - -/** - * Stored source configuration as read from and written to saved objects - */ - -const SavedSourceConfigurationFieldsRuntimeType = runtimeTypes.partial({ - container: runtimeTypes.string, - host: runtimeTypes.string, - pod: runtimeTypes.string, - tiebreaker: runtimeTypes.string, - timestamp: runtimeTypes.string, -}); - -export const SavedSourceConfigurationTimestampColumnRuntimeType = runtimeTypes.type({ - timestampColumn: runtimeTypes.type({ - id: runtimeTypes.string, - }), -}); - -export const SavedSourceConfigurationMessageColumnRuntimeType = runtimeTypes.type({ - messageColumn: runtimeTypes.type({ - id: runtimeTypes.string, - }), -}); - -export const SavedSourceConfigurationFieldColumnRuntimeType = runtimeTypes.type({ - fieldColumn: runtimeTypes.type({ - id: runtimeTypes.string, - field: runtimeTypes.string, - }), -}); - -export const SavedSourceConfigurationColumnRuntimeType = runtimeTypes.union([ - SavedSourceConfigurationTimestampColumnRuntimeType, - SavedSourceConfigurationMessageColumnRuntimeType, - SavedSourceConfigurationFieldColumnRuntimeType, -]); - -export const SavedSourceConfigurationRuntimeType = runtimeTypes.partial({ - name: runtimeTypes.string, - description: runtimeTypes.string, - metricAlias: runtimeTypes.string, - logAlias: runtimeTypes.string, - fields: SavedSourceConfigurationFieldsRuntimeType, - logColumns: runtimeTypes.array(SavedSourceConfigurationColumnRuntimeType), -}); - -export interface InfraSavedSourceConfiguration - extends runtimeTypes.TypeOf<typeof SavedSourceConfigurationRuntimeType> {} - -export const pickSavedSourceConfiguration = ( - value: InfraSourceConfiguration -): InfraSavedSourceConfiguration => { - const { name, description, metricAlias, logAlias, fields, logColumns } = value; - const { container, host, pod, tiebreaker, timestamp } = fields; - - return { - name, - description, - metricAlias, - logAlias, - fields: { container, host, pod, tiebreaker, timestamp }, - logColumns, - }; -}; - -/** - * Static source configuration as read from the configuration file - */ - -const StaticSourceConfigurationFieldsRuntimeType = runtimeTypes.partial({ - ...SavedSourceConfigurationFieldsRuntimeType.props, - message: runtimeTypes.array(runtimeTypes.string), -}); - -export const StaticSourceConfigurationRuntimeType = runtimeTypes.partial({ - name: runtimeTypes.string, - description: runtimeTypes.string, - metricAlias: runtimeTypes.string, - logAlias: runtimeTypes.string, - fields: StaticSourceConfigurationFieldsRuntimeType, - logColumns: runtimeTypes.array(SavedSourceConfigurationColumnRuntimeType), -}); - -export interface InfraStaticSourceConfiguration - extends runtimeTypes.TypeOf<typeof StaticSourceConfigurationRuntimeType> {} - -/** - * Full source configuration type after all cleanup has been done at the edges - */ - -const SourceConfigurationFieldsRuntimeType = runtimeTypes.type({ - ...StaticSourceConfigurationFieldsRuntimeType.props, -}); - -export const SourceConfigurationRuntimeType = runtimeTypes.type({ - ...SavedSourceConfigurationRuntimeType.props, - fields: SourceConfigurationFieldsRuntimeType, - logColumns: runtimeTypes.array(SavedSourceConfigurationColumnRuntimeType), -}); - -export interface InfraSourceConfiguration - extends runtimeTypes.TypeOf<typeof SourceConfigurationRuntimeType> {} - -/** - * Saved object type with metadata - */ - -export const SourceConfigurationSavedObjectRuntimeType = runtimeTypes.intersection([ - runtimeTypes.type({ - id: runtimeTypes.string, - attributes: SavedSourceConfigurationRuntimeType, - }), - runtimeTypes.partial({ - version: runtimeTypes.string, - updated_at: TimestampFromString, - }), -]); - -export interface SourceConfigurationSavedObject - extends runtimeTypes.TypeOf<typeof SourceConfigurationSavedObjectRuntimeType> {} diff --git a/x-pack/plugins/infra/server/plugin.ts b/x-pack/plugins/infra/server/plugin.ts index 64fc496f3597e..e3804078604cc 100644 --- a/x-pack/plugins/infra/server/plugin.ts +++ b/x-pack/plugins/infra/server/plugin.ts @@ -26,7 +26,7 @@ import { InfraSources } from './lib/sources'; import { InfraServerPluginDeps } from './lib/adapters/framework'; import { METRICS_FEATURE, LOGS_FEATURE } from './features'; import { UsageCollector } from './usage/usage_collector'; -import { InfraStaticSourceConfiguration } from './lib/sources/types'; +import { InfraStaticSourceConfiguration } from '../common/http_api/source_api'; import { registerAlertTypes } from './lib/alerting'; export const config = { diff --git a/x-pack/plugins/infra/server/routes/metadata/index.ts b/x-pack/plugins/infra/server/routes/metadata/index.ts index 03d28110d612a..c45f191b1130d 100644 --- a/x-pack/plugins/infra/server/routes/metadata/index.ts +++ b/x-pack/plugins/infra/server/routes/metadata/index.ts @@ -38,7 +38,7 @@ export const initMetadataRoute = (libs: InfraBackendLibs) => { }, async (requestContext, request, response) => { try { - const { nodeId, nodeType, sourceId } = pipe( + const { nodeId, nodeType, sourceId, timeRange } = pipe( InfraMetadataRequestRT.decode(request.body), fold(throwErrors(Boom.badRequest), identity) ); @@ -52,7 +52,8 @@ export const initMetadataRoute = (libs: InfraBackendLibs) => { requestContext, configuration, nodeId, - nodeType + nodeType, + timeRange ); const metricFeatures = pickFeatureName(metricsMetadata.buckets).map( nameToFeature('metrics') @@ -62,7 +63,13 @@ export const initMetadataRoute = (libs: InfraBackendLibs) => { const cloudInstanceId = get<string>(info, 'cloud.instance.id'); const cloudMetricsMetadata = cloudInstanceId - ? await getCloudMetricsMetadata(framework, requestContext, configuration, cloudInstanceId) + ? await getCloudMetricsMetadata( + framework, + requestContext, + configuration, + cloudInstanceId, + timeRange + ) : { buckets: [] }; const cloudMetricsFeatures = pickFeatureName(cloudMetricsMetadata.buckets).map( nameToFeature('metrics') diff --git a/x-pack/plugins/infra/server/routes/metadata/lib/get_cloud_metric_metadata.ts b/x-pack/plugins/infra/server/routes/metadata/lib/get_cloud_metric_metadata.ts index 75ca3ae3caee2..54a1ca0aaa7e0 100644 --- a/x-pack/plugins/infra/server/routes/metadata/lib/get_cloud_metric_metadata.ts +++ b/x-pack/plugins/infra/server/routes/metadata/lib/get_cloud_metric_metadata.ts @@ -21,7 +21,8 @@ export const getCloudMetricsMetadata = async ( framework: KibanaFramework, requestContext: RequestHandlerContext, sourceConfiguration: InfraSourceConfiguration, - instanceId: string + instanceId: string, + timeRange: { from: number; to: number } ): Promise<InfraCloudMetricsAdapterResponse> => { const metricQuery = { allowNoIndices: true, @@ -30,7 +31,18 @@ export const getCloudMetricsMetadata = async ( body: { query: { bool: { - filter: [{ match: { 'cloud.instance.id': instanceId } }], + filter: [ + { match: { 'cloud.instance.id': instanceId } }, + { + range: { + [sourceConfiguration.fields.timestamp]: { + gte: timeRange.from, + lte: timeRange.to, + format: 'epoch_millis', + }, + }, + }, + ], should: CLOUD_METRICS_MODULES.map(module => ({ match: { 'event.module': module } })), }, }, diff --git a/x-pack/plugins/infra/server/routes/metadata/lib/get_metric_metadata.ts b/x-pack/plugins/infra/server/routes/metadata/lib/get_metric_metadata.ts index 191339565b813..7753d3161039b 100644 --- a/x-pack/plugins/infra/server/routes/metadata/lib/get_metric_metadata.ts +++ b/x-pack/plugins/infra/server/routes/metadata/lib/get_metric_metadata.ts @@ -26,7 +26,8 @@ export const getMetricMetadata = async ( requestContext: RequestHandlerContext, sourceConfiguration: InfraSourceConfiguration, nodeId: string, - nodeType: InventoryItemType + nodeType: InventoryItemType, + timeRange: { from: number; to: number } ): Promise<InfraMetricsAdapterResponse> => { const fields = findInventoryFields(nodeType, sourceConfiguration.fields); const metricQuery = { @@ -41,6 +42,15 @@ export const getMetricMetadata = async ( { match: { [fields.id]: nodeId }, }, + { + range: { + [sourceConfiguration.fields.timestamp]: { + gte: timeRange.from, + lte: timeRange.to, + format: 'epoch_millis', + }, + }, + }, ], }, }, diff --git a/x-pack/plugins/infra/server/routes/source/index.ts b/x-pack/plugins/infra/server/routes/source/index.ts new file mode 100644 index 0000000000000..2f29320d7bb81 --- /dev/null +++ b/x-pack/plugins/infra/server/routes/source/index.ts @@ -0,0 +1,65 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { schema } from '@kbn/config-schema'; +import { SourceResponseRuntimeType } from '../../../common/http_api/source_api'; +import { InfraBackendLibs } from '../../lib/infra_types'; +import { InfraIndexType } from '../../graphql/types'; + +const typeToInfraIndexType = (value: string | undefined) => { + switch (value) { + case 'metrics': + return InfraIndexType.METRICS; + case 'logs': + return InfraIndexType.LOGS; + default: + return InfraIndexType.ANY; + } +}; + +export const initSourceRoute = (libs: InfraBackendLibs) => { + const { framework } = libs; + + framework.registerRoute( + { + method: 'get', + path: '/api/metrics/source/{sourceId}/{type?}', + validate: { + params: schema.object({ + sourceId: schema.string(), + type: schema.string(), + }), + }, + }, + async (requestContext, request, response) => { + try { + const { type, sourceId } = request.params; + + const source = await libs.sources.getSourceConfiguration(requestContext, sourceId); + if (!source) { + return response.notFound(); + } + + const status = { + logIndicesExist: await libs.sourceStatus.hasLogIndices(requestContext, sourceId), + metricIndicesExist: await libs.sourceStatus.hasMetricIndices(requestContext, sourceId), + indexFields: await libs.fields.getFields( + requestContext, + sourceId, + typeToInfraIndexType(type) + ), + }; + + return response.ok({ + body: SourceResponseRuntimeType.encode({ source, status }), + }); + } catch (error) { + return response.internalError({ + body: error.message, + }); + } + } + ); +}; diff --git a/x-pack/plugins/ingest_manager/common/services/datasource_to_agent_datasource.test.ts b/x-pack/plugins/ingest_manager/common/services/datasource_to_agent_datasource.test.ts index b897c03e89f82..3496ea782ee99 100644 --- a/x-pack/plugins/ingest_manager/common/services/datasource_to_agent_datasource.test.ts +++ b/x-pack/plugins/ingest_manager/common/services/datasource_to_agent_datasource.test.ts @@ -38,6 +38,10 @@ describe('Ingest Manager - storedDatasourceToAgentDatasource', () => { fooVar: { value: 'foo-value' }, fooVar2: { value: [1, 2] }, }, + pkg_stream: { + fooKey: 'fooValue1', + fooKey2: ['fooValue2'], + }, }, { id: 'test-logs-bar', @@ -95,7 +99,7 @@ describe('Ingest Manager - storedDatasourceToAgentDatasource', () => { }); }); - it('returns agent datasource config with flattened input and stream configs', () => { + it('returns agent datasource config with flattened input and package stream', () => { expect(storedDatasourceToAgentDatasource({ ...mockDatasource, inputs: [mockInput] })).toEqual({ id: 'mock-datasource', namespace: 'default', @@ -105,34 +109,18 @@ describe('Ingest Manager - storedDatasourceToAgentDatasource', () => { { type: 'test-logs', enabled: true, - inputVar: 'input-value', - inputVar3: { - testField: 'test', - }, streams: [ { id: 'test-logs-foo', enabled: true, dataset: 'foo', - fooVar: 'foo-value', - fooVar2: [1, 2], + fooKey: 'fooValue1', + fooKey2: ['fooValue2'], }, { id: 'test-logs-bar', enabled: true, dataset: 'bar', - barVar: 'bar-value', - barVar2: [1, 2], - barVar3: [ - { - namespace: 'mockNamespace', - anotherProp: 'test', - }, - { - namespace: 'mockNamespace2', - anotherProp: 'test2', - }, - ], }, ], }, @@ -160,17 +148,13 @@ describe('Ingest Manager - storedDatasourceToAgentDatasource', () => { { type: 'test-logs', enabled: true, - inputVar: 'input-value', - inputVar3: { - testField: 'test', - }, streams: [ { id: 'test-logs-foo', enabled: true, dataset: 'foo', - fooVar: 'foo-value', - fooVar2: [1, 2], + fooKey: 'fooValue1', + fooKey2: ['fooValue2'], }, ], }, diff --git a/x-pack/plugins/ingest_manager/common/services/datasource_to_agent_datasource.ts b/x-pack/plugins/ingest_manager/common/services/datasource_to_agent_datasource.ts index 20bbbec8919d6..b509878b7f945 100644 --- a/x-pack/plugins/ingest_manager/common/services/datasource_to_agent_datasource.ts +++ b/x-pack/plugins/ingest_manager/common/services/datasource_to_agent_datasource.ts @@ -3,38 +3,9 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -import { safeLoad } from 'js-yaml'; -import { - Datasource, - NewDatasource, - DatasourceConfigRecord, - DatasourceConfigRecordEntry, - FullAgentConfigDatasource, -} from '../types'; +import { Datasource, NewDatasource, FullAgentConfigDatasource } from '../types'; import { DEFAULT_OUTPUT } from '../constants'; -const configReducer = ( - configResult: DatasourceConfigRecord, - configEntry: [string, DatasourceConfigRecordEntry] -): DatasourceConfigRecord => { - const [configName, { type: configType, value: configValue }] = configEntry; - if (configValue !== undefined && configValue !== '') { - if (configType === 'yaml') { - try { - const yamlValue = safeLoad(configValue); - if (yamlValue) { - configResult[configName] = yamlValue; - } - } catch (e) { - // Silently swallow parsing error - } - } else { - configResult[configName] = configValue; - } - } - return configResult; -}; - export const storedDatasourceToAgentDatasource = ( datasource: Datasource | NewDatasource ): FullAgentConfigDatasource => { @@ -50,14 +21,14 @@ export const storedDatasourceToAgentDatasource = ( .map(input => { const fullInput = { ...input, - ...Object.entries(input.config || {}).reduce(configReducer, {}), streams: input.streams .filter(stream => stream.enabled) .map(stream => { const fullStream = { ...stream, - ...Object.entries(stream.config || {}).reduce(configReducer, {}), + ...stream.pkg_stream, }; + delete fullStream.pkg_stream; delete fullStream.config; return fullStream; }), diff --git a/x-pack/plugins/ingest_manager/common/types/models/datasource.ts b/x-pack/plugins/ingest_manager/common/types/models/datasource.ts index ee4d24ab11777..48243a12120f9 100644 --- a/x-pack/plugins/ingest_manager/common/types/models/datasource.ts +++ b/x-pack/plugins/ingest_manager/common/types/models/datasource.ts @@ -23,6 +23,7 @@ export interface DatasourceInputStream { dataset: string; processors?: string[]; config?: DatasourceConfigRecord; + pkg_stream?: any; } export interface DatasourceInput { 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 5524e7505d74b..53ad0310ea613 100644 --- a/x-pack/plugins/ingest_manager/common/types/models/epm.ts +++ b/x-pack/plugins/ingest_manager/common/types/models/epm.ts @@ -19,7 +19,7 @@ export enum InstallStatus { uninstalling = 'uninstalling', } -export type DetailViewPanelName = 'overview' | 'data-sources'; +export type DetailViewPanelName = 'overview' | 'data-sources' | 'settings'; export type ServiceName = 'kibana' | 'elasticsearch'; export type AssetType = KibanaAssetType | ElasticsearchAssetType | AgentAssetType; @@ -218,6 +218,7 @@ export type PackageInfo = Installable< export interface Installation extends SavedObjectAttributes { installed: AssetReference[]; + es_index_patterns: Record<string, string>; name: string; version: string; } diff --git a/x-pack/plugins/ingest_manager/common/types/rest_spec/datasource.ts b/x-pack/plugins/ingest_manager/common/types/rest_spec/datasource.ts index f630602503f0a..61f1f15d49259 100644 --- a/x-pack/plugins/ingest_manager/common/types/rest_spec/datasource.ts +++ b/x-pack/plugins/ingest_manager/common/types/rest_spec/datasource.ts @@ -4,10 +4,21 @@ * you may not use this file except in compliance with the Elastic License. */ import { Datasource, NewDatasource } from '../models'; -import { ListWithKuery } from './common'; export interface GetDatasourcesRequest { - query: ListWithKuery; + query: { + page: number; + perPage: number; + kuery?: string; + }; +} + +export interface GetDatasourcesResponse { + items: Datasource[]; + total: number; + page: number; + perPage: number; + success: boolean; } export interface GetOneDatasourceRequest { @@ -16,6 +27,11 @@ export interface GetOneDatasourceRequest { }; } +export interface GetOneDatasourceResponse { + item: Datasource; + success: boolean; +} + export interface CreateDatasourceRequest { body: NewDatasource; } @@ -29,6 +45,8 @@ export type UpdateDatasourceRequest = GetOneDatasourceRequest & { body: NewDatasource; }; +export type UpdateDatasourceResponse = CreateDatasourceResponse; + export interface DeleteDatasourcesRequest { body: { datasourceIds: string[]; diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/components/alpha_messaging.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/components/alpha_messaging.tsx new file mode 100644 index 0000000000000..0f3ddee29fa44 --- /dev/null +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/components/alpha_messaging.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 styled from 'styled-components'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { EuiText } from '@elastic/eui'; + +const Message = styled(EuiText).attrs(props => ({ + color: 'subdued', + textAlign: 'center', +}))` + padding: ${props => props.theme.eui.paddingSizes.m}; +`; + +export const AlphaMessaging: React.FC<{}> = () => ( + <Message> + <p> + <small> + <strong> + <FormattedMessage + id="xpack.ingestManager.alphaMessageTitle" + defaultMessage="Alpha release" + /> + </strong> + {' – '} + <FormattedMessage + id="xpack.ingestManager.alphaMessageDescription" + defaultMessage="Ingest Manager is under active development and is not + intended for production purposes." + /> + </small> + </p> + </Message> +); diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/components/enrollment_instructions/shell/index.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/components/enrollment_instructions/shell/index.tsx index e6990927b926e..cb65e31fb74b5 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/components/enrollment_instructions/shell/index.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/components/enrollment_instructions/shell/index.tsx @@ -43,7 +43,7 @@ export const ShellEnrollmentInstructions: React.FunctionComponent<Props> = ({ // apiKey.api_key // } sh -c "$(curl ${kibanaUrl}/api/ingest_manager/fleet/install/${currentPlatform})"`; - const quickInstallInstructions = `./agent enroll ${kibanaUrl} ${apiKey.api_key}`; + const quickInstallInstructions = `./elastic-agent enroll ${kibanaUrl} ${apiKey.api_key}`; return ( <> diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/components/header.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/components/header.tsx index e1f29fdbeb323..1aab6d901a992 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/components/header.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/components/header.tsx @@ -7,14 +7,15 @@ import React, { memo } from 'react'; import styled from 'styled-components'; import { EuiFlexGroup, EuiFlexItem, EuiTabs, EuiTab, EuiSpacer } from '@elastic/eui'; import { Props as EuiTabProps } from '@elastic/eui/src/components/tabs/tab'; +import { EuiFlexItemProps } from '@elastic/eui/src/components/flex/flex_item'; const Container = styled.div` border-bottom: ${props => props.theme.eui.euiBorderThin}; background-color: ${props => props.theme.eui.euiPageBackgroundColor}; `; -const Wrapper = styled.div` - max-width: 1200px; +const Wrapper = styled.div<{ maxWidth?: number }>` + max-width: ${props => props.maxWidth || 1200}px; margin-left: auto; margin-right: auto; padding-top: ${props => props.theme.eui.paddingSizes.xl}; @@ -30,22 +31,36 @@ const Tabs = styled(EuiTabs)` `; export interface HeaderProps { + restrictHeaderWidth?: number; leftColumn?: JSX.Element; rightColumn?: JSX.Element; + rightColumnGrow?: EuiFlexItemProps['grow']; tabs?: EuiTabProps[]; } -const HeaderColumns: React.FC<Omit<HeaderProps, 'tabs'>> = memo(({ leftColumn, rightColumn }) => ( - <EuiFlexGroup alignItems="center"> - {leftColumn ? <EuiFlexItem>{leftColumn}</EuiFlexItem> : null} - {rightColumn ? <EuiFlexItem>{rightColumn}</EuiFlexItem> : null} - </EuiFlexGroup> -)); +const HeaderColumns: React.FC<Omit<HeaderProps, 'tabs'>> = memo( + ({ leftColumn, rightColumn, rightColumnGrow }) => ( + <EuiFlexGroup alignItems="center"> + {leftColumn ? <EuiFlexItem>{leftColumn}</EuiFlexItem> : null} + {rightColumn ? <EuiFlexItem grow={rightColumnGrow}>{rightColumn}</EuiFlexItem> : null} + </EuiFlexGroup> + ) +); -export const Header: React.FC<HeaderProps> = ({ leftColumn, rightColumn, tabs }) => ( +export const Header: React.FC<HeaderProps> = ({ + leftColumn, + rightColumn, + rightColumnGrow, + tabs, + restrictHeaderWidth, +}) => ( <Container> - <Wrapper> - <HeaderColumns leftColumn={leftColumn} rightColumn={rightColumn} /> + <Wrapper maxWidth={restrictHeaderWidth}> + <HeaderColumns + leftColumn={leftColumn} + rightColumn={rightColumn} + rightColumnGrow={rightColumnGrow} + /> <EuiFlexGroup> {tabs ? ( <EuiFlexItem> diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/components/index.ts b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/components/index.ts index 5551bff2c8bde..bdc8f350f7108 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/components/index.ts +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/components/index.ts @@ -6,3 +6,4 @@ export { Loading } from './loading'; export { Error } from './error'; export { Header, HeaderProps } from './header'; +export { AlphaMessaging } from './alpha_messaging'; diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/hooks/use_request/datasource.ts b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/hooks/use_request/datasource.ts index d0072f0355993..0d19ecd0cb735 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/hooks/use_request/datasource.ts +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/hooks/use_request/datasource.ts @@ -3,12 +3,14 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -import { sendRequest } from './use_request'; +import { sendRequest, useRequest } from './use_request'; import { datasourceRouteService } from '../../services'; import { CreateDatasourceRequest, CreateDatasourceResponse } from '../../types'; import { DeleteDatasourcesRequest, DeleteDatasourcesResponse, + GetDatasourcesRequest, + GetDatasourcesResponse, } from '../../../../../common/types/rest_spec'; export const sendCreateDatasource = (body: CreateDatasourceRequest['body']) => { @@ -26,3 +28,11 @@ export const sendDeleteDatasource = (body: DeleteDatasourcesRequest['body']) => body: JSON.stringify(body), }); }; + +export function useGetDatasources(query: GetDatasourcesRequest['query']) { + return useRequest<GetDatasourcesResponse>({ + method: 'get', + path: datasourceRouteService.getListPath(), + query, + }); +} diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/layouts/default.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/layouts/default.tsx index 26f2c85a291a3..f797c509bfca0 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/layouts/default.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/layouts/default.tsx @@ -8,6 +8,7 @@ import styled from 'styled-components'; import { EuiTabs, EuiTab, EuiFlexGroup, EuiFlexItem, EuiIcon } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; import { Section } from '../sections'; +import { AlphaMessaging } from '../components'; import { useLink, useConfig } from '../hooks'; import { EPM_PATH, FLEET_PATH, AGENT_CONFIG_PATH } from '../constants'; @@ -19,6 +20,8 @@ interface Props { const Container = styled.div` min-height: calc(100vh - ${props => props.theme.eui.euiHeaderChildSize}); background: ${props => props.theme.eui.euiColorEmptyShade}; + display: flex; + flex-direction: column; `; const Nav = styled.nav` @@ -80,6 +83,7 @@ export const DefaultLayout: React.FunctionComponent<Props> = ({ section, childre </EuiFlexGroup> </Nav> {children} + <AlphaMessaging /> </Container> ); }; diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/layouts/with_header.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/layouts/with_header.tsx index c77a50d95dca3..bb867718204b2 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/layouts/with_header.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/layouts/with_header.tsx @@ -10,6 +10,8 @@ import { Header, HeaderProps } from '../components'; const Page = styled(EuiPage)` background: ${props => props.theme.eui.euiColorEmptyShade}; + flex: 1; + align-items: flex-start; `; interface Props extends HeaderProps { diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/layouts/without_header.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/layouts/without_header.tsx index cad98c5a0a7e1..9f9fa03942c09 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/layouts/without_header.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/layouts/without_header.tsx @@ -9,6 +9,8 @@ import { EuiPage, EuiPageBody, EuiSpacer } from '@elastic/eui'; const Page = styled(EuiPage)` background: ${props => props.theme.eui.euiColorEmptyShade}; + flex: 1; + align-items: flex-start; `; interface Props { diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_datasource_page/components/confirm_modal.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_datasource_page/components/confirm_modal.tsx new file mode 100644 index 0000000000000..aa7eab8f5be8d --- /dev/null +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_datasource_page/components/confirm_modal.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 from 'react'; +import { EuiCallOut, EuiOverlayMask, EuiConfirmModal, EuiSpacer } from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { i18n } from '@kbn/i18n'; +import { AgentConfig } from '../../../../types'; + +export const ConfirmCreateDatasourceModal: React.FunctionComponent<{ + onConfirm: () => void; + onCancel: () => void; + agentCount: number; + agentConfig: AgentConfig; +}> = ({ onConfirm, onCancel, agentCount, agentConfig }) => { + return ( + <EuiOverlayMask> + <EuiConfirmModal + title={ + <FormattedMessage + id="xpack.ingestManager.createDatasource.confirmModalTitle" + defaultMessage="Save and deploy changes" + /> + } + onCancel={onCancel} + onConfirm={onConfirm} + cancelButtonText={ + <FormattedMessage + id="xpack.ingestManager.deleteApiKeys.confirmModal.cancelButtonLabel" + defaultMessage="Cancel" + /> + } + confirmButtonText={ + <FormattedMessage + id="xpack.ingestManager.createDatasource.confirmModalConfirmButtonLabel" + defaultMessage="Save and deploy changes" + /> + } + buttonColor="primary" + > + <EuiCallOut + iconType="iInCircle" + title={i18n.translate('xpack.ingestManager.createDatasource.confirmModalCalloutTitle', { + defaultMessage: + 'This action will update {agentCount, plural, one {# agent} other {# agents}}', + values: { + agentCount, + }, + })} + > + <FormattedMessage + id="xpack.ingestManager.createDatasource.confirmModalCalloutDescription" + defaultMessage="Fleet has detected that the selected agent configuration, {configName}, is already in use by + some of your agents. As a result of this action, Fleet will deploy updates to all agents + that use this configuration." + values={{ + configName: <b>{agentConfig.name}</b>, + }} + /> + </EuiCallOut> + <EuiSpacer size="l" /> + <FormattedMessage + id="xpack.ingestManager.createDatasource.confirmModalDescription" + defaultMessage="This action can not be undone. Are you sure you wish to continue?" + /> + </EuiConfirmModal> + </EuiOverlayMask> + ); +}; diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_datasource_page/components/index.ts b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_datasource_page/components/index.ts index 3bfca75668911..aa564690a6092 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_datasource_page/components/index.ts +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_datasource_page/components/index.ts @@ -5,4 +5,5 @@ */ export { CreateDatasourcePageLayout } from './layout'; export { DatasourceInputPanel } from './datasource_input_panel'; +export { ConfirmCreateDatasourceModal } from './confirm_modal'; export { DatasourceInputVarField } from './datasource_input_var_field'; diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_datasource_page/components/layout.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_datasource_page/components/layout.tsx index dd242f366e8c0..73a7ba8ec119d 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_datasource_page/components/layout.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_datasource_page/components/layout.tsx @@ -19,108 +19,107 @@ import { WithHeaderLayout } from '../../../../layouts'; import { AgentConfig, PackageInfo } from '../../../../types'; import { PackageIcon } from '../../../../components/package_icon'; import { CreateDatasourceFrom, CreateDatasourceStep } from '../types'; -import { CreateDatasourceStepsNavigation } from './navigation'; export const CreateDatasourcePageLayout: React.FunctionComponent<{ from: CreateDatasourceFrom; basePath: string; cancelUrl: string; maxStep: CreateDatasourceStep | ''; - currentStep: CreateDatasourceStep; agentConfig?: AgentConfig; packageInfo?: PackageInfo; - restrictWidth?: number; -}> = ({ - from, - basePath, - cancelUrl, - maxStep, - currentStep, - agentConfig, - packageInfo, - restrictWidth, - children, -}) => { - return ( - <WithHeaderLayout - restrictWidth={restrictWidth} - leftColumn={ - <EuiFlexGroup direction="column" gutterSize="s" alignItems="flexStart"> - <EuiFlexItem> - <EuiButtonEmpty size="s" iconType="cross" flush="left" href={cancelUrl}> +}> = ({ from, basePath, cancelUrl, maxStep, agentConfig, packageInfo, children }) => { + const leftColumn = ( + <EuiFlexGroup direction="column" gutterSize="s" alignItems="flexStart"> + <EuiFlexItem> + <EuiButtonEmpty size="s" iconType="arrowLeft" flush="left" href={cancelUrl}> + <FormattedMessage + id="xpack.ingestManager.createDatasource.cancelLinkText" + defaultMessage="Cancel" + /> + </EuiButtonEmpty> + </EuiFlexItem> + <EuiFlexItem> + <EuiText> + <h1> + <FormattedMessage + id="xpack.ingestManager.createDatasource.pageTitle" + defaultMessage="Add data source" + /> + </h1> + </EuiText> + </EuiFlexItem> + <EuiFlexItem> + <EuiSpacer size="s" /> + <EuiText color="subdued" size="s"> + {from === 'config' ? ( + <FormattedMessage + id="xpack.ingestManager.createDatasource.pageDescriptionfromConfig" + defaultMessage="Follow the instructions below to add an integration to this agent configuration." + /> + ) : ( + <FormattedMessage + id="xpack.ingestManager.createDatasource.pageDescriptionfromPackage" + defaultMessage="Follow the instructions below to add this integration to an agent configuration." + /> + )} + </EuiText> + </EuiFlexItem> + </EuiFlexGroup> + ); + const rightColumn = ( + <EuiFlexGroup justifyContent="flexEnd" direction={'row'} gutterSize="xl"> + <EuiFlexItem grow={false}> + <EuiSpacer size="s" /> + {agentConfig && from === 'config' ? ( + <EuiDescriptionList style={{ textAlign: 'right' }} textStyle="reverse"> + <EuiDescriptionListTitle> + <FormattedMessage + id="xpack.ingestManager.createDatasource.agentConfigurationNameLabel" + defaultMessage="Configuration" + /> + </EuiDescriptionListTitle> + <EuiDescriptionListDescription> + {agentConfig?.name || '-'} + </EuiDescriptionListDescription> + </EuiDescriptionList> + ) : null} + {packageInfo && from === 'package' ? ( + <EuiDescriptionList style={{ textAlign: 'right' }} textStyle="reverse"> + <EuiDescriptionListTitle> <FormattedMessage - id="xpack.ingestManager.createDatasource.cancelLinkText" - defaultMessage="Cancel" + id="xpack.ingestManager.createDatasource.packageNameLabel" + defaultMessage="Integration" /> - </EuiButtonEmpty> - </EuiFlexItem> - <EuiFlexItem> - <EuiText> - <h1> - <FormattedMessage - id="xpack.ingestManager.createDatasource.pageTitle" - defaultMessage="Create data source" - /> - </h1> - </EuiText> - </EuiFlexItem> - <EuiFlexItem> - <EuiSpacer size="s" /> - <EuiFlexGroup direction={from === 'config' ? 'row' : 'rowReverse'} gutterSize="xl"> - {agentConfig || from === 'config' ? ( + </EuiDescriptionListTitle> + <EuiDescriptionListDescription> + <EuiFlexGroup justifyContent="flexEnd" alignItems="center" gutterSize="s"> <EuiFlexItem grow={false}> - <EuiDescriptionList textStyle="reverse"> - <EuiDescriptionListTitle> - <FormattedMessage - id="xpack.ingestManager.createDatasource.agentConfigurationNameLabel" - defaultMessage="Configuration" - /> - </EuiDescriptionListTitle> - <EuiDescriptionListDescription> - {agentConfig?.name || '-'} - </EuiDescriptionListDescription> - </EuiDescriptionList> + <PackageIcon + packageName={packageInfo?.name || ''} + version={packageInfo?.version || ''} + icons={packageInfo?.icons} + size="m" + /> </EuiFlexItem> - ) : null} - {packageInfo || from === 'package' ? ( <EuiFlexItem grow={false}> - <EuiDescriptionList textStyle="reverse"> - <EuiDescriptionListTitle> - <FormattedMessage - id="xpack.ingestManager.createDatasource.packageNameLabel" - defaultMessage="Integration" - /> - </EuiDescriptionListTitle> - <EuiDescriptionListDescription> - <EuiFlexGroup alignItems="center" gutterSize="s"> - <EuiFlexItem grow={false}> - <PackageIcon - packageName={packageInfo?.name || ''} - version={packageInfo?.version || ''} - icons={packageInfo?.icons} - size="m" - /> - </EuiFlexItem> - <EuiFlexItem grow={false}> - {packageInfo?.title || packageInfo?.name || '-'} - </EuiFlexItem> - </EuiFlexGroup> - </EuiDescriptionListDescription> - </EuiDescriptionList> + {packageInfo?.title || packageInfo?.name || '-'} </EuiFlexItem> - ) : null} - </EuiFlexGroup> - </EuiFlexItem> - </EuiFlexGroup> - } - rightColumn={ - <CreateDatasourceStepsNavigation - from={from} - basePath={basePath} - maxStep={maxStep} - currentStep={currentStep} - /> - } + </EuiFlexGroup> + </EuiDescriptionListDescription> + </EuiDescriptionList> + ) : null} + </EuiFlexItem> + </EuiFlexGroup> + ); + + const maxWidth = 770; + return ( + <WithHeaderLayout + restrictHeaderWidth={maxWidth} + restrictWidth={maxWidth} + leftColumn={leftColumn} + rightColumn={rightColumn} + rightColumnGrow={false} > {children} </WithHeaderLayout> diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_datasource_page/components/navigation.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_datasource_page/components/navigation.tsx deleted file mode 100644 index 7dae981e65c30..0000000000000 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_datasource_page/components/navigation.tsx +++ /dev/null @@ -1,85 +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 from 'react'; -import styled from 'styled-components'; -import { useHistory } from 'react-router-dom'; -import { i18n } from '@kbn/i18n'; -import { EuiStepsHorizontal } from '@elastic/eui'; -import { CreateDatasourceFrom, CreateDatasourceStep } from '../types'; -import { WeightedCreateDatasourceSteps, CREATE_DATASOURCE_STEP_PATHS } from '../constants'; - -const StepsHorizontal = styled(EuiStepsHorizontal)` - background: none; -`; - -export const CreateDatasourceStepsNavigation: React.FunctionComponent<{ - from: CreateDatasourceFrom; - basePath: string; - maxStep: CreateDatasourceStep | ''; - currentStep: CreateDatasourceStep; -}> = ({ from, basePath, maxStep, currentStep }) => { - const history = useHistory(); - - const steps = [ - from === 'config' - ? { - title: i18n.translate('xpack.ingestManager.createDatasource.stepSelectPackageLabel', { - defaultMessage: 'Select integration', - }), - isSelected: currentStep === 'selectPackage', - isComplete: - WeightedCreateDatasourceSteps.indexOf('selectPackage') <= - WeightedCreateDatasourceSteps.indexOf(maxStep), - onClick: () => { - history.push(`${basePath}${CREATE_DATASOURCE_STEP_PATHS.selectPackage}`); - }, - } - : { - title: i18n.translate('xpack.ingestManager.createDatasource.stepSelectConfigLabel', { - defaultMessage: 'Select configuration', - }), - isSelected: currentStep === 'selectConfig', - isComplete: - WeightedCreateDatasourceSteps.indexOf('selectConfig') <= - WeightedCreateDatasourceSteps.indexOf(maxStep), - onClick: () => { - history.push(`${basePath}${CREATE_DATASOURCE_STEP_PATHS.selectConfig}`); - }, - }, - { - title: i18n.translate('xpack.ingestManager.createDatasource.stepConfigureDatasourceLabel', { - defaultMessage: 'Configure data source', - }), - isSelected: currentStep === 'configure', - isComplete: - WeightedCreateDatasourceSteps.indexOf('configure') <= - WeightedCreateDatasourceSteps.indexOf(maxStep), - disabled: - WeightedCreateDatasourceSteps.indexOf(maxStep) < - WeightedCreateDatasourceSteps.indexOf('configure') - 1, - onClick: () => { - history.push(`${basePath}${CREATE_DATASOURCE_STEP_PATHS.configure}`); - }, - }, - { - title: i18n.translate('xpack.ingestManager.createDatasource.stepReviewLabel', { - defaultMessage: 'Review', - }), - isSelected: currentStep === 'review', - isComplete: - WeightedCreateDatasourceSteps.indexOf('review') <= - WeightedCreateDatasourceSteps.indexOf(maxStep), - disabled: - WeightedCreateDatasourceSteps.indexOf(maxStep) < - WeightedCreateDatasourceSteps.indexOf('review') - 1, - onClick: () => { - history.push(`${basePath}${CREATE_DATASOURCE_STEP_PATHS.review}`); - }, - }, - ]; - - return <StepsHorizontal steps={steps} />; -}; diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_datasource_page/constants.ts b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_datasource_page/constants.ts index eea18179560a1..49223a8eb4531 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_datasource_page/constants.ts +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_datasource_page/constants.ts @@ -9,10 +9,3 @@ export const WeightedCreateDatasourceSteps = [ 'configure', 'review', ]; - -export const CREATE_DATASOURCE_STEP_PATHS = { - selectConfig: '/select-config', - selectPackage: '/select-package', - configure: '/configure', - review: '/review', -}; diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_datasource_page/index.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_datasource_page/index.tsx index 461bb750ca6f5..1ad579d591b21 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_datasource_page/index.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_datasource_page/index.tsx @@ -3,45 +3,74 @@ * 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 { - useRouteMatch, - HashRouter as Router, - Switch, - Route, - Redirect, - useHistory, -} from 'react-router-dom'; +import React, { useState, useEffect } from 'react'; +import { useRouteMatch, useHistory } from 'react-router-dom'; +import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; -import { EuiButtonEmpty } from '@elastic/eui'; +import { + EuiButtonEmpty, + EuiButton, + EuiSteps, + EuiBottomBar, + EuiFlexGroup, + EuiFlexItem, + EuiSpacer, +} from '@elastic/eui'; +import { EuiStepProps } from '@elastic/eui/src/components/steps/step'; import { AGENT_CONFIG_DETAILS_PATH } from '../../../constants'; import { AgentConfig, PackageInfo, NewDatasource } from '../../../types'; -import { useLink, sendCreateDatasource } from '../../../hooks'; +import { + useLink, + sendCreateDatasource, + useCore, + useConfig, + sendGetAgentStatus, +} from '../../../hooks'; import { useLinks as useEPMLinks } from '../../epm/hooks'; -import { CreateDatasourcePageLayout } from './components'; +import { CreateDatasourcePageLayout, ConfirmCreateDatasourceModal } from './components'; import { CreateDatasourceFrom, CreateDatasourceStep } from './types'; -import { CREATE_DATASOURCE_STEP_PATHS } from './constants'; -import { DatasourceValidationResults, validateDatasource } from './services'; +import { DatasourceValidationResults, validateDatasource, validationHasErrors } from './services'; import { StepSelectPackage } from './step_select_package'; import { StepSelectConfig } from './step_select_config'; import { StepConfigureDatasource } from './step_configure_datasource'; -import { StepReviewDatasource } from './step_review'; + +import { StepDefineDatasource } from './step_define_datasource'; export const CreateDatasourcePage: React.FunctionComponent = () => { + const { notifications } = useCore(); + const { + fleet: { enabled: isFleetEnabled }, + } = useConfig(); const { params: { configId, pkgkey }, - path: matchPath, url: basePath, } = useRouteMatch(); const history = useHistory(); const from: CreateDatasourceFrom = configId ? 'config' : 'package'; const [maxStep, setMaxStep] = useState<CreateDatasourceStep | ''>(''); - const [isSaving, setIsSaving] = useState<boolean>(false); // Agent config and package info states const [agentConfig, setAgentConfig] = useState<AgentConfig>(); const [packageInfo, setPackageInfo] = useState<PackageInfo>(); + const agentConfigId = agentConfig?.id; + // Retrieve agent count + useEffect(() => { + const getAgentCount = async () => { + if (agentConfigId) { + const { data } = await sendGetAgentStatus({ configId: agentConfigId }); + if (data?.results.total) { + setAgentCount(data.results.total); + } + } + }; + + if (isFleetEnabled && agentConfigId) { + getAgentCount(); + } + }, [agentConfigId, isFleetEnabled]); + const [agentCount, setAgentCount] = useState<number>(0); + // New datasource state const [datasource, setDatasource] = useState<NewDatasource>({ name: '', @@ -60,6 +89,7 @@ export const CreateDatasourcePage: React.FunctionComponent = () => { if (updatedPackageInfo) { setPackageInfo(updatedPackageInfo); } else { + setFormState('INVALID'); setPackageInfo(undefined); setMaxStep(''); } @@ -73,6 +103,7 @@ export const CreateDatasourcePage: React.FunctionComponent = () => { if (updatedAgentConfig) { setAgentConfig(updatedAgentConfig); } else { + setFormState('INVALID'); setAgentConfig(undefined); setMaxStep(''); } @@ -81,6 +112,8 @@ export const CreateDatasourcePage: React.FunctionComponent = () => { console.debug('Agent config updated', updatedAgentConfig); }; + const hasErrors = validationResults ? validationHasErrors(validationResults) : false; + // Update datasource method const updateDatasource = (updatedFields: Partial<NewDatasource>) => { const newDatasource = { @@ -88,9 +121,18 @@ export const CreateDatasourcePage: React.FunctionComponent = () => { ...updatedFields, }; setDatasource(newDatasource); + // eslint-disable-next-line no-console console.debug('Datasource updated', newDatasource); - updateDatasourceValidation(newDatasource); + const newValidationResults = updateDatasourceValidation(newDatasource); + const hasPackage = newDatasource.package; + const hasValidationErrors = newValidationResults + ? validationHasErrors(newValidationResults) + : false; + const hasAgentConfig = newDatasource.config_id && newDatasource.config_id !== ''; + if (hasPackage && hasAgentConfig && !hasValidationErrors) { + setFormState('VALID'); + } }; const updateDatasourceValidation = (newDatasource?: NewDatasource) => { @@ -99,6 +141,8 @@ export const CreateDatasourcePage: React.FunctionComponent = () => { setValidationResults(newValidationResult); // eslint-disable-next-line no-console console.debug('Datasource validation results', newValidationResult); + + return newValidationResult; } }; @@ -112,34 +156,37 @@ export const CreateDatasourcePage: React.FunctionComponent = () => { }); const cancelUrl = from === 'config' ? CONFIG_URL : PACKAGE_URL; - // Redirect to first step - const redirectToFirstStep = - from === 'config' ? ( - <Redirect to={`${basePath}${CREATE_DATASOURCE_STEP_PATHS.selectPackage}`} /> - ) : ( - <Redirect to={`${basePath}${CREATE_DATASOURCE_STEP_PATHS.selectConfig}`} /> - ); - - // Url to first and second steps - const SELECT_PACKAGE_URL = useLink(`${basePath}${CREATE_DATASOURCE_STEP_PATHS.selectPackage}`); - const SELECT_CONFIG_URL = useLink(`${basePath}${CREATE_DATASOURCE_STEP_PATHS.selectConfig}`); - const CONFIGURE_DATASOURCE_URL = useLink(`${basePath}${CREATE_DATASOURCE_STEP_PATHS.configure}`); - const firstStepUrl = from === 'config' ? SELECT_PACKAGE_URL : SELECT_CONFIG_URL; - const secondStepUrl = CONFIGURE_DATASOURCE_URL; - - // Redirect to second step - const redirectToSecondStep = ( - <Redirect to={`${basePath}${CREATE_DATASOURCE_STEP_PATHS.configure}`} /> - ); - // Save datasource + const [formState, setFormState] = useState< + 'VALID' | 'INVALID' | 'CONFIRM' | 'LOADING' | 'SUBMITTED' + >('INVALID'); const saveDatasource = async () => { - setIsSaving(true); + setFormState('LOADING'); const result = await sendCreateDatasource(datasource); - setIsSaving(false); + setFormState('SUBMITTED'); return result; }; + const onSubmit = async () => { + if (formState === 'VALID' && hasErrors) { + setFormState('INVALID'); + return; + } + if (agentCount !== 0 && formState !== 'CONFIRM') { + setFormState('CONFIRM'); + return; + } + const { error } = await saveDatasource(); + if (!error) { + history.push(`${AGENT_CONFIG_DETAILS_PATH}${agentConfig ? agentConfig.id : configId}`); + } else { + notifications.toasts.addError(error, { + title: 'Error', + }); + setFormState('VALID'); + } + }; + const layoutProps = { from, basePath, @@ -147,135 +194,108 @@ export const CreateDatasourcePage: React.FunctionComponent = () => { maxStep, agentConfig, packageInfo, - restrictWidth: 770, }; - return ( - <Router> - <Switch> - {/* Redirect to first step from `/` */} - {from === 'config' ? ( - <Redirect - exact - from={`${matchPath}`} - to={`${matchPath}${CREATE_DATASOURCE_STEP_PATHS.selectPackage}`} + const steps: EuiStepProps[] = [ + from === 'package' + ? { + title: i18n.translate('xpack.ingestManager.createDatasource.stepSelectAgentConfigTitle', { + defaultMessage: 'Select an agent configuration', + }), + children: ( + <StepSelectConfig + pkgkey={pkgkey} + updatePackageInfo={updatePackageInfo} + agentConfig={agentConfig} + updateAgentConfig={updateAgentConfig} + /> + ), + } + : { + title: i18n.translate('xpack.ingestManager.createDatasource.stepSelectPackageTitle', { + defaultMessage: 'Select an integration', + }), + children: ( + <StepSelectPackage + agentConfigId={configId} + updateAgentConfig={updateAgentConfig} + packageInfo={packageInfo} + updatePackageInfo={updatePackageInfo} + /> + ), + }, + { + title: i18n.translate('xpack.ingestManager.createDatasource.stepDefineDatasourceTitle', { + defaultMessage: 'Define your data source', + }), + status: !packageInfo || !agentConfig ? 'disabled' : undefined, + children: + agentConfig && packageInfo ? ( + <StepDefineDatasource + agentConfig={agentConfig} + packageInfo={packageInfo} + datasource={datasource} + updateDatasource={updateDatasource} /> - ) : ( - <Redirect - exact - from={`${matchPath}`} - to={`${matchPath}${CREATE_DATASOURCE_STEP_PATHS.selectConfig}`} + ) : null, + }, + { + title: i18n.translate('xpack.ingestManager.createDatasource.stepConfgiureDatasourceTitle', { + defaultMessage: 'Select the data you want to collect', + }), + status: !packageInfo || !agentConfig ? 'disabled' : undefined, + children: + agentConfig && packageInfo ? ( + <StepConfigureDatasource + agentConfig={agentConfig} + packageInfo={packageInfo} + datasource={datasource} + updateDatasource={updateDatasource} + validationResults={validationResults!} + submitAttempted={formState === 'INVALID'} /> - )} - - {/* First step, either render select package or select config depending on entry */} - {from === 'config' ? ( - <Route path={`${matchPath}${CREATE_DATASOURCE_STEP_PATHS.selectPackage}`}> - <CreateDatasourcePageLayout {...layoutProps} currentStep="selectPackage"> - <StepSelectPackage - agentConfigId={configId} - updateAgentConfig={updateAgentConfig} - packageInfo={packageInfo} - updatePackageInfo={updatePackageInfo} - cancelUrl={cancelUrl} - onNext={() => { - setMaxStep('selectPackage'); - history.push(`${basePath}${CREATE_DATASOURCE_STEP_PATHS.configure}`); - }} - /> - </CreateDatasourcePageLayout> - </Route> - ) : ( - <Route path={`${matchPath}${CREATE_DATASOURCE_STEP_PATHS.selectConfig}`}> - <CreateDatasourcePageLayout {...layoutProps} currentStep="selectConfig"> - <StepSelectConfig - pkgkey={pkgkey} - updatePackageInfo={updatePackageInfo} - agentConfig={agentConfig} - updateAgentConfig={updateAgentConfig} - cancelUrl={cancelUrl} - onNext={() => { - setMaxStep('selectConfig'); - history.push(`${basePath}${CREATE_DATASOURCE_STEP_PATHS.configure}`); - }} - /> - </CreateDatasourcePageLayout> - </Route> - )} - - {/* Second step to configure data source, redirect to first step if agent config */} - {/* or package info isn't defined (i.e. after full page reload) */} - <Route path={`${matchPath}${CREATE_DATASOURCE_STEP_PATHS.configure}`}> - <CreateDatasourcePageLayout {...layoutProps} currentStep="configure"> - {!agentConfig || !packageInfo ? ( - redirectToFirstStep - ) : ( - <StepConfigureDatasource - agentConfig={agentConfig} - packageInfo={packageInfo} - datasource={datasource} - updateDatasource={updateDatasource} - validationResults={validationResults!} - backLink={ - <EuiButtonEmpty href={firstStepUrl} iconType="arrowLeft" iconSide="left"> - {from === 'config' ? ( - <FormattedMessage - id="xpack.ingestManager.createDatasource.changePackageLinkText" - defaultMessage="Change integration" - /> - ) : ( - <FormattedMessage - id="xpack.ingestManager.createDatasource.changeConfigLinkText" - defaultMessage="Change configuration" - /> - )} - </EuiButtonEmpty> - } - cancelUrl={cancelUrl} - onNext={() => { - setMaxStep('configure'); - history.push(`${basePath}${CREATE_DATASOURCE_STEP_PATHS.review}`); - }} + ) : null, + }, + ]; + return ( + <CreateDatasourcePageLayout {...layoutProps}> + {formState === 'CONFIRM' && agentConfig && ( + <ConfirmCreateDatasourceModal + agentCount={agentCount} + agentConfig={agentConfig} + onConfirm={onSubmit} + onCancel={() => setFormState('VALID')} + /> + )} + <EuiSteps steps={steps} /> + <EuiSpacer size="l" /> + <EuiBottomBar css={{ zIndex: 5 }} paddingSize="s"> + <EuiFlexGroup gutterSize="s" justifyContent="flexEnd"> + <EuiFlexItem grow={false}> + <EuiButtonEmpty color="ghost" href={cancelUrl}> + <FormattedMessage + id="xpack.ingestManager.createDatasource.cancelButton" + defaultMessage="Cancel" /> - )} - </CreateDatasourcePageLayout> - </Route> - - {/* Third step to review, redirect to second step if data source name is missing */} - {/* (i.e. after full page reload) */} - <Route path={`${matchPath}${CREATE_DATASOURCE_STEP_PATHS.review}`}> - <CreateDatasourcePageLayout {...layoutProps} currentStep="review"> - {!agentConfig || !datasource.name ? ( - redirectToSecondStep - ) : ( - <StepReviewDatasource - agentConfig={agentConfig} - datasource={datasource} - cancelUrl={cancelUrl} - isSubmitLoading={isSaving} - backLink={ - <EuiButtonEmpty href={secondStepUrl} iconType="arrowLeft" iconSide="left"> - <FormattedMessage - id="xpack.ingestManager.createDatasource.editDatasourceLinkText" - defaultMessage="Edit data source" - /> - </EuiButtonEmpty> - } - onSubmit={async () => { - const { error } = await saveDatasource(); - if (!error) { - history.push( - `${AGENT_CONFIG_DETAILS_PATH}${agentConfig ? agentConfig.id : configId}` - ); - } else { - // TODO: Handle save datasource error - } - }} + </EuiButtonEmpty> + </EuiFlexItem> + <EuiFlexItem grow={false}> + <EuiButton + onClick={onSubmit} + isLoading={formState === 'LOADING'} + disabled={formState !== 'VALID'} + iconType="save" + color="primary" + fill + > + <FormattedMessage + id="xpack.ingestManager.createDatasource.saveButton" + defaultMessage="Save data source" /> - )} - </CreateDatasourcePageLayout> - </Route> - </Switch> - </Router> + </EuiButton> + </EuiFlexItem> + </EuiFlexGroup> + </EuiBottomBar> + </CreateDatasourcePageLayout> ); }; diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_datasource_page/step_configure_datasource.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_datasource_page/step_configure_datasource.tsx index 105d6c66a5704..843891b63ca01 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_datasource_page/step_configure_datasource.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_datasource_page/step_configure_datasource.tsx @@ -3,23 +3,18 @@ * 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, { useEffect, useState, Fragment } from 'react'; -import { i18n } from '@kbn/i18n'; +import React, { useEffect } from 'react'; import { FormattedMessage } from '@kbn/i18n/react'; import { - EuiSteps, EuiPanel, EuiFlexGroup, EuiFlexItem, - EuiFormRow, - EuiButtonEmpty, EuiSpacer, EuiEmptyPrompt, EuiText, - EuiButton, - EuiComboBox, EuiCallOut, } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; import { AgentConfig, PackageInfo, @@ -30,7 +25,7 @@ import { import { Loading } from '../../../components'; import { packageToConfigDatasourceInputs } from '../../../services'; import { DatasourceValidationResults, validationHasErrors } from './services'; -import { DatasourceInputPanel, DatasourceInputVarField } from './components'; +import { DatasourceInputPanel } from './components'; export const StepConfigureDatasource: React.FunctionComponent<{ agentConfig: AgentConfig; @@ -38,24 +33,17 @@ export const StepConfigureDatasource: React.FunctionComponent<{ datasource: NewDatasource; updateDatasource: (fields: Partial<NewDatasource>) => void; validationResults: DatasourceValidationResults; - backLink: JSX.Element; - cancelUrl: string; - onNext: () => void; + submitAttempted: boolean; }> = ({ agentConfig, packageInfo, datasource, updateDatasource, validationResults, - backLink, - cancelUrl, - onNext, + submitAttempted, }) => { // Form show/hide states - const [isShowingAdvancedDefine, setIsShowingAdvancedDefine] = useState<boolean>(false); - // Form submit state - const [submitAttempted, setSubmitAttempted] = useState<boolean>(false); const hasErrors = validationResults ? validationHasErrors(validationResults) : false; // Update datasource's package and config info @@ -95,107 +83,6 @@ export const StepConfigureDatasource: React.FunctionComponent<{ } }, [datasource.package, datasource.config_id, agentConfig, packageInfo, updateDatasource]); - // Step A, define datasource - const renderDefineDatasource = () => ( - <EuiPanel> - <EuiFlexGroup> - <EuiFlexItem grow={1}> - <DatasourceInputVarField - varDef={{ - name: 'name', - title: i18n.translate( - 'xpack.ingestManager.createDatasource.stepConfigure.datasourceNameInputLabel', - { - defaultMessage: 'Data source name', - } - ), - type: 'text', - required: true, - }} - value={datasource.name} - onChange={(newValue: any) => { - updateDatasource({ - name: newValue, - }); - }} - errors={validationResults!.name} - forceShowErrors={submitAttempted} - /> - </EuiFlexItem> - <EuiFlexItem grow={1}> - <DatasourceInputVarField - varDef={{ - name: 'description', - title: i18n.translate( - 'xpack.ingestManager.createDatasource.stepConfigure.datasourceDescriptionInputLabel', - { - defaultMessage: 'Description', - } - ), - type: 'text', - required: false, - }} - value={datasource.description} - onChange={(newValue: any) => { - updateDatasource({ - description: newValue, - }); - }} - errors={validationResults!.description} - forceShowErrors={submitAttempted} - /> - </EuiFlexItem> - </EuiFlexGroup> - <EuiSpacer size="m" /> - <EuiButtonEmpty - flush="left" - size="xs" - iconType={isShowingAdvancedDefine ? 'arrowUp' : 'arrowDown'} - onClick={() => setIsShowingAdvancedDefine(!isShowingAdvancedDefine)} - > - <FormattedMessage - id="xpack.ingestManager.createDatasource.stepConfigure.advancedOptionsToggleLinkText" - defaultMessage="Advanced options" - /> - </EuiButtonEmpty> - {/* Todo: Populate list of existing namespaces */} - {isShowingAdvancedDefine ? ( - <Fragment> - <EuiSpacer size="m" /> - <EuiFlexGroup> - <EuiFlexItem grow={1}> - <EuiFormRow - label={ - <FormattedMessage - id="xpack.ingestManager.createDatasource.stepConfigure.datasourceNamespaceInputLabel" - defaultMessage="Namespace" - /> - } - > - <EuiComboBox - noSuggestions - singleSelection={true} - selectedOptions={datasource.namespace ? [{ label: datasource.namespace }] : []} - onCreateOption={(newNamespace: string) => { - updateDatasource({ - namespace: newNamespace, - }); - }} - onChange={(newNamespaces: Array<{ label: string }>) => { - updateDatasource({ - namespace: newNamespaces.length ? newNamespaces[0].label : '', - }); - }} - /> - </EuiFormRow> - </EuiFlexItem> - <EuiFlexItem grow={1} /> - </EuiFlexGroup> - </Fragment> - ) : null} - </EuiPanel> - ); - // Step B, configure inputs (and their streams) // Assume packages only export one datasource for now const renderConfigureInputs = () => @@ -252,41 +139,10 @@ export const StepConfigureDatasource: React.FunctionComponent<{ return validationResults ? ( <EuiFlexGroup direction="column" gutterSize="none"> - <EuiFlexItem> - <EuiFlexGroup direction="column" gutterSize="none"> - <EuiFlexItem> - <EuiFlexGroup justifyContent="flexEnd"> - <EuiFlexItem grow={false}>{backLink}</EuiFlexItem> - </EuiFlexGroup> - </EuiFlexItem> - <EuiFlexItem> - <EuiSteps - steps={[ - { - title: i18n.translate( - 'xpack.ingestManager.createDatasource.stepConfigure.defineDatasourceTitle', - { - defaultMessage: 'Define your datasource', - } - ), - children: renderDefineDatasource(), - }, - { - title: i18n.translate( - 'xpack.ingestManager.createDatasource.stepConfigure.chooseDataTitle', - { - defaultMessage: 'Choose the data you want to collect', - } - ), - children: renderConfigureInputs(), - }, - ]} - /> - </EuiFlexItem> - </EuiFlexGroup> - </EuiFlexItem> + <EuiFlexItem>{renderConfigureInputs()}</EuiFlexItem> {hasErrors && submitAttempted ? ( <EuiFlexItem> + <EuiSpacer size="m" /> <EuiCallOut title={i18n.translate( 'xpack.ingestManager.createDatasource.stepConfigure.validationErrorTitle', @@ -306,36 +162,6 @@ export const StepConfigureDatasource: React.FunctionComponent<{ <EuiSpacer size="m" /> </EuiFlexItem> ) : null} - <EuiFlexItem> - <EuiFlexGroup justifyContent="flexEnd"> - <EuiFlexItem grow={false}> - <EuiButtonEmpty href={cancelUrl}> - <FormattedMessage - id="xpack.ingestManager.createDatasource.cancelLinkText" - defaultMessage="Cancel" - /> - </EuiButtonEmpty> - </EuiFlexItem> - <EuiFlexItem grow={false}> - <EuiButton - fill - iconType="arrowRight" - iconSide="right" - onClick={() => { - setSubmitAttempted(true); - if (!hasErrors) { - onNext(); - } - }} - > - <FormattedMessage - id="xpack.ingestManager.createDatasource.continueButtonText" - defaultMessage="Continue" - /> - </EuiButton> - </EuiFlexItem> - </EuiFlexGroup> - </EuiFlexItem> </EuiFlexGroup> ) : ( <Loading /> diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_datasource_page/step_define_datasource.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_datasource_page/step_define_datasource.tsx new file mode 100644 index 0000000000000..792389381eaf0 --- /dev/null +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_datasource_page/step_define_datasource.tsx @@ -0,0 +1,165 @@ +/* + * 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, { useEffect, useState, Fragment } from 'react'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { + EuiFlexGrid, + EuiFlexItem, + EuiFormRow, + EuiFieldText, + EuiButtonEmpty, + EuiSpacer, + EuiText, + EuiComboBox, +} from '@elastic/eui'; +import { AgentConfig, PackageInfo, Datasource, NewDatasource } from '../../../types'; +import { packageToConfigDatasourceInputs } from '../../../services'; + +export const StepDefineDatasource: React.FunctionComponent<{ + agentConfig: AgentConfig; + packageInfo: PackageInfo; + datasource: NewDatasource; + updateDatasource: (fields: Partial<NewDatasource>) => void; +}> = ({ agentConfig, packageInfo, datasource, updateDatasource }) => { + // Form show/hide states + const [isShowingAdvancedDefine, setIsShowingAdvancedDefine] = useState<boolean>(false); + + // Update datasource's package and config info + useEffect(() => { + const dsPackage = datasource.package; + const currentPkgKey = dsPackage ? `${dsPackage.name}-${dsPackage.version}` : ''; + const pkgKey = `${packageInfo.name}-${packageInfo.version}`; + + // If package has changed, create shell datasource with input&stream values based on package info + if (currentPkgKey !== pkgKey) { + // Existing datasources on the agent config using the package name, retrieve highest number appended to datasource name + const dsPackageNamePattern = new RegExp(`${packageInfo.name}-(\\d+)`); + const dsWithMatchingNames = (agentConfig.datasources as Datasource[]) + .filter(ds => Boolean(ds.name.match(dsPackageNamePattern))) + .map(ds => parseInt(ds.name.match(dsPackageNamePattern)![1], 10)) + .sort(); + + updateDatasource({ + name: `${packageInfo.name}-${ + dsWithMatchingNames.length ? dsWithMatchingNames[dsWithMatchingNames.length - 1] + 1 : 1 + }`, + package: { + name: packageInfo.name, + title: packageInfo.title, + version: packageInfo.version, + }, + inputs: packageToConfigDatasourceInputs(packageInfo), + }); + } + + // If agent config has changed, update datasource's config ID and namespace + if (datasource.config_id !== agentConfig.id) { + updateDatasource({ + config_id: agentConfig.id, + namespace: agentConfig.namespace, + }); + } + }, [datasource.package, datasource.config_id, agentConfig, packageInfo, updateDatasource]); + + return ( + <> + <EuiFlexGrid columns={2}> + <EuiFlexItem> + <EuiFormRow + label={ + <FormattedMessage + id="xpack.ingestManager.createDatasource.stepConfigure.datasourceNameInputLabel" + defaultMessage="Data source name" + /> + } + > + <EuiFieldText + value={datasource.name} + onChange={e => + updateDatasource({ + name: e.target.value, + }) + } + /> + </EuiFormRow> + </EuiFlexItem> + <EuiFlexItem> + <EuiFormRow + label={ + <FormattedMessage + id="xpack.ingestManager.createDatasource.stepConfigure.datasourceDescriptionInputLabel" + defaultMessage="Description" + /> + } + labelAppend={ + <EuiText size="xs" color="subdued"> + <FormattedMessage + id="xpack.ingestManager.createDatasource.stepConfigure.inputVarFieldOptionalLabel" + defaultMessage="Optional" + /> + </EuiText> + } + > + <EuiFieldText + value={datasource.description} + onChange={e => + updateDatasource({ + description: e.target.value, + }) + } + /> + </EuiFormRow> + </EuiFlexItem> + </EuiFlexGrid> + <EuiSpacer size="m" /> + <EuiButtonEmpty + flush="left" + size="xs" + iconType={isShowingAdvancedDefine ? 'arrowUp' : 'arrowDown'} + onClick={() => setIsShowingAdvancedDefine(!isShowingAdvancedDefine)} + > + <FormattedMessage + id="xpack.ingestManager.createDatasource.stepConfigure.advancedOptionsToggleLinkText" + defaultMessage="Advanced options" + /> + </EuiButtonEmpty> + {/* Todo: Populate list of existing namespaces */} + {isShowingAdvancedDefine ? ( + <Fragment> + <EuiSpacer size="m" /> + <EuiFlexGrid columns={2}> + <EuiFlexItem> + <EuiFormRow + label={ + <FormattedMessage + id="xpack.ingestManager.createDatasource.stepConfigure.datasourceNamespaceInputLabel" + defaultMessage="Namespace" + /> + } + > + <EuiComboBox + noSuggestions + singleSelection={true} + selectedOptions={datasource.namespace ? [{ label: datasource.namespace }] : []} + onCreateOption={(newNamespace: string) => { + updateDatasource({ + namespace: newNamespace, + }); + }} + onChange={(newNamespaces: Array<{ label: string }>) => { + updateDatasource({ + namespace: newNamespaces.length ? newNamespaces[0].label : '', + }); + }} + /> + </EuiFormRow> + </EuiFlexItem> + </EuiFlexGrid> + </Fragment> + ) : null} + </> + ); +}; diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_datasource_page/step_review.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_datasource_page/step_review.tsx deleted file mode 100644 index 20af5954c1436..0000000000000 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_datasource_page/step_review.tsx +++ /dev/null @@ -1,202 +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, useState, useEffect } from 'react'; -import { i18n } from '@kbn/i18n'; -import { FormattedMessage } from '@kbn/i18n/react'; -import { - EuiFlexGroup, - EuiFlexItem, - EuiButtonEmpty, - EuiButton, - EuiTitle, - EuiCallOut, - EuiText, - EuiCheckbox, - EuiTabbedContent, - EuiCodeBlock, - EuiSpacer, -} from '@elastic/eui'; -import { dump } from 'js-yaml'; -import { NewDatasource, AgentConfig } from '../../../types'; -import { useConfig, sendGetAgentStatus } from '../../../hooks'; -import { storedDatasourceToAgentDatasource } from '../../../services'; - -const KEYS_TO_SINK = ['inputs', 'streams']; - -export const StepReviewDatasource: React.FunctionComponent<{ - agentConfig: AgentConfig; - datasource: NewDatasource; - backLink: JSX.Element; - cancelUrl: string; - onSubmit: () => void; - isSubmitLoading: boolean; -}> = ({ agentConfig, datasource, backLink, cancelUrl, onSubmit, isSubmitLoading }) => { - // Agent count info states - const [agentCount, setAgentCount] = useState<number>(0); - const [agentCountChecked, setAgentCountChecked] = useState<boolean>(false); - - // Config information - const { - fleet: { enabled: isFleetEnabled }, - } = useConfig(); - - // Retrieve agent count - useEffect(() => { - const getAgentCount = async () => { - const { data } = await sendGetAgentStatus({ configId: agentConfig.id }); - if (data?.results.total) { - setAgentCount(data.results.total); - } - }; - - if (isFleetEnabled) { - getAgentCount(); - } - }, [agentConfig.id, isFleetEnabled]); - - const showAgentDisclaimer = isFleetEnabled && agentCount; - const fullAgentDatasource = storedDatasourceToAgentDatasource(datasource); - - return ( - <EuiFlexGroup direction="column"> - <EuiFlexItem> - <EuiFlexGroup justifyContent="spaceBetween"> - <EuiFlexItem grow={false}> - <EuiTitle size="s"> - <h3> - <FormattedMessage - id="xpack.ingestManager.createDatasource.stepReview.reviewTitle" - defaultMessage="Review changes" - /> - </h3> - </EuiTitle> - </EuiFlexItem> - <EuiFlexItem grow={false}>{backLink}</EuiFlexItem> - </EuiFlexGroup> - </EuiFlexItem> - - {/* Agents affected warning */} - {showAgentDisclaimer ? ( - <EuiFlexItem> - <EuiCallOut - title={ - <FormattedMessage - id="xpack.ingestManager.createDatasource.stepReview.agentsAffectedCalloutTitle" - defaultMessage="This action will affect {count, plural, one {# agent} other {# agents}}" - values={{ - count: agentCount, - }} - /> - } - > - <EuiText> - <p> - <FormattedMessage - id="xpack.ingestManager.createDatasource.stepReview.agentsAffectedCalloutText" - defaultMessage="Fleet has detected that the selected agent configuration, {configName} is already in use by some your agents. As a result of this action, Fleet will update all agents that are enrolled with this configuration." - values={{ - configName: <strong>{agentConfig.name}</strong>, - }} - /> - </p> - </EuiText> - </EuiCallOut> - </EuiFlexItem> - ) : null} - - {/* Preview and YAML view */} - {/* TODO: Implement preview tab */} - <EuiFlexItem> - <EuiTabbedContent - tabs={[ - { - id: 'yaml', - name: i18n.translate('xpack.ingestManager.agentConfigInfo.yamlTabName', { - defaultMessage: 'YAML', - }), - content: ( - <Fragment> - <EuiSpacer size="s" /> - <EuiCodeBlock language="yaml" isCopyable overflowHeight={450}> - {dump(fullAgentDatasource, { - sortKeys: (a: string, b: string) => { - // Make YAML output prettier by sinking certain fields - if (KEYS_TO_SINK.indexOf(a) > -1) { - return 1; - } - if (KEYS_TO_SINK.indexOf(b) > -1) { - return -1; - } - return 0; - }, - })} - </EuiCodeBlock> - </Fragment> - ), - }, - ]} - /> - </EuiFlexItem> - - {/* Confirm agents affected */} - {showAgentDisclaimer ? ( - <EuiFlexItem> - <EuiFlexGroup direction="column" gutterSize="s"> - <EuiFlexItem> - <EuiTitle size="xs"> - <h4> - <FormattedMessage - id="xpack.ingestManager.createDatasource.stepReview.confirmAgentDisclaimerTitle" - defaultMessage="Confirm your decision" - /> - </h4> - </EuiTitle> - </EuiFlexItem> - <EuiFlexItem> - <EuiCheckbox - id="CreateDatasourceAgentDisclaimer" - label={ - <FormattedMessage - id="xpack.ingestManager.createDatasource.stepReview.confirmAgentDisclaimerText" - defaultMessage="I understand that this action will update all agents that are enrolled with this configuration." - /> - } - checked={agentCountChecked} - onChange={e => setAgentCountChecked(e.target.checked)} - /> - </EuiFlexItem> - </EuiFlexGroup> - </EuiFlexItem> - ) : null} - - <EuiFlexItem> - <EuiFlexGroup justifyContent="flexEnd"> - <EuiFlexItem grow={false}> - <EuiButtonEmpty href={cancelUrl}> - <FormattedMessage - id="xpack.ingestManager.createDatasource.cancelLinkText" - defaultMessage="Cancel" - /> - </EuiButtonEmpty> - </EuiFlexItem> - <EuiFlexItem grow={false}> - <EuiButton - fill - onClick={() => onSubmit()} - isLoading={isSubmitLoading} - disabled={isSubmitLoading || Boolean(showAgentDisclaimer && !agentCountChecked)} - > - <FormattedMessage - id="xpack.ingestManager.createDatasource.addDatasourceButtonText" - defaultMessage="Add datasource to configuration" - /> - </EuiButton> - </EuiFlexItem> - </EuiFlexGroup> - </EuiFlexItem> - </EuiFlexGroup> - ); -}; diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_datasource_page/step_select_config.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_datasource_page/step_select_config.tsx index 2ddfc170069a1..6cbe56e628903 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_datasource_page/step_select_config.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_datasource_page/step_select_config.tsx @@ -6,19 +6,8 @@ import React, { useEffect, useState, Fragment } from 'react'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; -import { - EuiButtonEmpty, - EuiButton, - EuiFlexGroup, - EuiFlexItem, - EuiTitle, - EuiSelectable, - EuiSpacer, - EuiTextColor, -} from '@elastic/eui'; +import { EuiFlexGroup, EuiFlexItem, EuiSelectable, EuiSpacer, EuiTextColor } from '@elastic/eui'; import { Error } from '../../../components'; -import { AGENT_CONFIG_PATH } from '../../../constants'; -import { useCapabilities, useLink } from '../../../hooks'; import { AgentConfig, PackageInfo, GetAgentConfigsResponseItem } from '../../../types'; import { useGetPackageInfoByKey, useGetAgentConfigs, sendGetOneAgentConfig } from '../../../hooks'; @@ -27,20 +16,13 @@ export const StepSelectConfig: React.FunctionComponent<{ updatePackageInfo: (packageInfo: PackageInfo | undefined) => void; agentConfig: AgentConfig | undefined; updateAgentConfig: (config: AgentConfig | undefined) => void; - cancelUrl: string; - onNext: () => void; -}> = ({ pkgkey, updatePackageInfo, agentConfig, updateAgentConfig, cancelUrl, onNext }) => { - const hasWriteCapabilites = useCapabilities().write; +}> = ({ pkgkey, updatePackageInfo, agentConfig, updateAgentConfig }) => { // Selected config state const [selectedConfigId, setSelectedConfigId] = useState<string | undefined>( agentConfig ? agentConfig.id : undefined ); - const [selectedConfigLoading, setSelectedConfigLoading] = useState<boolean>(false); const [selectedConfigError, setSelectedConfigError] = useState<Error>(); - // Todo: replace with create agent config flyout - const CREATE_NEW_CONFIG_URI = useLink(AGENT_CONFIG_PATH); - // Fetch package info const { data: packageInfoData, error: packageInfoError } = useGetPackageInfoByKey(pkgkey); @@ -70,9 +52,7 @@ export const StepSelectConfig: React.FunctionComponent<{ useEffect(() => { const fetchAgentConfigInfo = async () => { if (selectedConfigId) { - setSelectedConfigLoading(true); const { data, error } = await sendGetOneAgentConfig(selectedConfigId); - setSelectedConfigLoading(false); if (error) { setSelectedConfigError(error); updateAgentConfig(undefined); @@ -122,33 +102,6 @@ export const StepSelectConfig: React.FunctionComponent<{ return ( <EuiFlexGroup direction="column"> - <EuiFlexItem> - <EuiFlexGroup justifyContent="spaceBetween" alignItems="center"> - <EuiFlexItem grow={false}> - <EuiTitle size="s"> - <h3> - <FormattedMessage - id="xpack.ingestManager.createDatasource.StepSelectConfig.selectAgentConfigTitle" - defaultMessage="Select an agent configuration" - /> - </h3> - </EuiTitle> - </EuiFlexItem> - <EuiFlexItem grow={false}> - <EuiButtonEmpty - isDisabled={!hasWriteCapabilites} - iconType="plusInCircle" - href={CREATE_NEW_CONFIG_URI} - size="s" - > - <FormattedMessage - id="xpack.ingestManager.createDatasource.StepSelectConfig.createNewConfigButtonText" - defaultMessage="Create new configuration" - /> - </EuiButtonEmpty> - </EuiFlexItem> - </EuiFlexGroup> - </EuiFlexItem> <EuiFlexItem> <EuiSelectable searchable @@ -227,33 +180,6 @@ export const StepSelectConfig: React.FunctionComponent<{ /> </EuiFlexItem> ) : null} - <EuiFlexItem> - <EuiFlexGroup justifyContent="flexEnd"> - <EuiFlexItem grow={false}> - <EuiButtonEmpty href={cancelUrl}> - <FormattedMessage - id="xpack.ingestManager.createDatasource.cancelLinkText" - defaultMessage="Cancel" - /> - </EuiButtonEmpty> - </EuiFlexItem> - <EuiFlexItem grow={false}> - <EuiButton - fill - iconType="arrowRight" - iconSide="right" - isLoading={selectedConfigLoading} - disabled={!selectedConfigId || !!selectedConfigError || selectedConfigLoading} - onClick={() => onNext()} - > - <FormattedMessage - id="xpack.ingestManager.createDatasource.continueButtonText" - defaultMessage="Continue" - /> - </EuiButton> - </EuiFlexItem> - </EuiFlexGroup> - </EuiFlexItem> </EuiFlexGroup> ); }; diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_datasource_page/step_select_package.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_datasource_page/step_select_package.tsx index 496e1d3c0fd7b..8dabb3bc98110 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_datasource_page/step_select_package.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_datasource_page/step_select_package.tsx @@ -6,15 +6,7 @@ import React, { useEffect, useState, Fragment } from 'react'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; -import { - EuiButtonEmpty, - EuiButton, - EuiFlexGroup, - EuiFlexItem, - EuiTitle, - EuiSelectable, - EuiSpacer, -} from '@elastic/eui'; +import { EuiFlexGroup, EuiFlexItem, EuiSelectable, EuiSpacer } from '@elastic/eui'; import { Error } from '../../../components'; import { AgentConfig, PackageInfo } from '../../../types'; import { useGetOneAgentConfig, useGetPackages, sendGetPackageInfoByKey } from '../../../hooks'; @@ -25,14 +17,11 @@ export const StepSelectPackage: React.FunctionComponent<{ updateAgentConfig: (config: AgentConfig | undefined) => void; packageInfo?: PackageInfo; updatePackageInfo: (packageInfo: PackageInfo | undefined) => void; - cancelUrl: string; - onNext: () => void; -}> = ({ agentConfigId, updateAgentConfig, packageInfo, updatePackageInfo, cancelUrl, onNext }) => { +}> = ({ agentConfigId, updateAgentConfig, packageInfo, updatePackageInfo }) => { // Selected package state const [selectedPkgKey, setSelectedPkgKey] = useState<string | undefined>( packageInfo ? `${packageInfo.name}-${packageInfo.version}` : undefined ); - const [selectedPkgLoading, setSelectedPkgLoading] = useState<boolean>(false); const [selectedPkgError, setSelectedPkgError] = useState<Error>(); // Fetch agent config info @@ -57,9 +46,7 @@ export const StepSelectPackage: React.FunctionComponent<{ useEffect(() => { const fetchPackageInfo = async () => { if (selectedPkgKey) { - setSelectedPkgLoading(true); const { data, error } = await sendGetPackageInfoByKey(selectedPkgKey); - setSelectedPkgLoading(false); if (error) { setSelectedPkgError(error); updatePackageInfo(undefined); @@ -109,16 +96,6 @@ export const StepSelectPackage: React.FunctionComponent<{ return ( <EuiFlexGroup direction="column"> - <EuiFlexItem> - <EuiTitle size="s"> - <h3> - <FormattedMessage - id="xpack.ingestManager.createDatasource.stepSelectPackage.selectPackageTitle" - defaultMessage="Select integration" - /> - </h3> - </EuiTitle> - </EuiFlexItem> <EuiFlexItem> <EuiSelectable searchable @@ -186,33 +163,6 @@ export const StepSelectPackage: React.FunctionComponent<{ /> </EuiFlexItem> ) : null} - <EuiFlexItem> - <EuiFlexGroup justifyContent="flexEnd"> - <EuiFlexItem grow={false}> - <EuiButtonEmpty href={cancelUrl}> - <FormattedMessage - id="xpack.ingestManager.createDatasource.cancelLinkText" - defaultMessage="Cancel" - /> - </EuiButtonEmpty> - </EuiFlexItem> - <EuiFlexItem grow={false}> - <EuiButton - fill - iconType="arrowRight" - iconSide="right" - isLoading={selectedPkgLoading} - disabled={!selectedPkgKey || !!selectedPkgError || selectedPkgLoading} - onClick={() => onNext()} - > - <FormattedMessage - id="xpack.ingestManager.createDatasource.continueButtonText" - defaultMessage="Continue" - /> - </EuiButton> - </EuiFlexItem> - </EuiFlexGroup> - </EuiFlexItem> </EuiFlexGroup> ); }; diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/details_page/components/yaml/index.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/details_page/components/yaml/index.tsx index c1cdde730837f..56b109a9bc062 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/details_page/components/yaml/index.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/details_page/components/yaml/index.tsx @@ -25,7 +25,15 @@ import { import { ShellEnrollmentInstructions } from '../../../../../components/enrollment_instructions'; import { Loading } from '../../../../../components'; -const CONFIG_KEYS_ORDER = ['id', 'revision', 'outputs', 'datasources']; +const CONFIG_KEYS_ORDER = [ + 'id', + 'revision', + 'outputs', + 'datasources', + 'enabled', + 'package', + 'input', +]; export const ConfigYamlView = memo<{ config: AgentConfig }>(({ config }) => { const core = useCore(); @@ -47,7 +55,17 @@ export const ConfigYamlView = memo<{ config: AgentConfig }>(({ config }) => { <EuiCodeBlock language="yaml" isCopyable> {dump(fullConfigRequest.data.item, { sortKeys: (keyA: string, keyB: string) => { - return CONFIG_KEYS_ORDER.indexOf(keyA) - CONFIG_KEYS_ORDER.indexOf(keyB); + const indexA = CONFIG_KEYS_ORDER.indexOf(keyA); + const indexB = CONFIG_KEYS_ORDER.indexOf(keyB); + if (indexA >= 0 && indexB < 0) { + return -1; + } + + if (indexA < 0 && indexB >= 0) { + return 1; + } + + return indexA - indexB; }, })} </EuiCodeBlock> diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/hooks/index.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/hooks/index.tsx index 48986481b6061..fbc00fbadcfaa 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/hooks/index.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/hooks/index.tsx @@ -7,7 +7,7 @@ export { useLinks } from './use_links'; export { useLocalSearch, searchIdField } from './use_local_search'; export { PackageInstallProvider, - useDeletePackage, + useUninstallPackage, useGetPackageInstallStatus, useInstallPackage, useSetPackageInstallStatus, diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/hooks/use_package_install.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/hooks/use_package_install.tsx index 537a2616f1786..0c5f45cdc47a7 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/hooks/use_package_install.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/hooks/use_package_install.tsx @@ -6,8 +6,8 @@ import createContainer from 'constate'; import React, { useCallback, useState } from 'react'; +import { FormattedMessage } from '@kbn/i18n/react'; import { NotificationsStart } from 'src/core/public'; -import { useLinks } from '.'; import { toMountPoint } from '../../../../../../../../../src/plugins/kibana_react/public'; import { PackageInfo } from '../../../types'; import { sendInstallPackage, sendRemovePackage } from '../../../hooks'; @@ -25,7 +25,6 @@ type InstallPackageProps = Pick<PackageInfo, 'name' | 'version' | 'title'>; function usePackageInstall({ notifications }: { notifications: NotificationsStart }) { const [packages, setPackage] = useState<PackagesInstall>({}); - const { toDetailView } = useLinks(); const setPackageInstallStatus = useCallback( ({ name, status }: { name: PackageInfo['name']; status: InstallStatus }) => { @@ -46,34 +45,43 @@ function usePackageInstall({ notifications }: { notifications: NotificationsStar if (res.error) { setPackageInstallStatus({ name, status: InstallStatus.notInstalled }); notifications.toasts.addWarning({ - title: `Failed to install ${title} package`, - text: - 'Something went wrong while trying to install this package. Please try again later.', + title: toMountPoint( + <FormattedMessage + id="xpack.ingestManager.integrations.packageInstallErrorTitle" + defaultMessage="Failed to install {title} package" + values={{ title }} + /> + ), + text: toMountPoint( + <FormattedMessage + id="xpack.ingestManager.integrations.packageInstallErrorDescription" + defaultMessage="Something went wrong while trying to install this package. Please try again later." + /> + ), iconType: 'alert', }); } else { setPackageInstallStatus({ name, status: InstallStatus.installed }); - const SuccessMsg = <p>Successfully installed {name}</p>; notifications.toasts.addSuccess({ - title: `Installed ${title} package`, - text: toMountPoint(SuccessMsg), + title: toMountPoint( + <FormattedMessage + id="xpack.ingestManager.integrations.packageInstallSuccessTitle" + defaultMessage="Installed {title}" + values={{ title }} + /> + ), + text: toMountPoint( + <FormattedMessage + id="xpack.ingestManager.integrations.packageInstallSuccessDescription" + defaultMessage="Successfully installed {title}" + values={{ title }} + /> + ), }); - - // TODO: this should probably live somewhere else and use <Redirect />, - // this hook could return the request state and a component could - // use that state. the component should be able to unsubscribe to prevent memory leaks - const packageUrl = toDetailView({ name, version }); - const dataSourcesUrl = toDetailView({ - name, - version, - panel: 'data-sources', - withAppRoot: false, - }); - if (window.location.href.includes(packageUrl)) window.location.hash = dataSourcesUrl; } }, - [notifications.toasts, setPackageInstallStatus, toDetailView] + [notifications.toasts, setPackageInstallStatus] ); const getPackageInstallStatus = useCallback( @@ -83,7 +91,7 @@ function usePackageInstall({ notifications }: { notifications: NotificationsStar [packages] ); - const deletePackage = useCallback( + const uninstallPackage = useCallback( async ({ name, version, title }: Pick<PackageInfo, 'name' | 'version' | 'title'>) => { setPackageInstallStatus({ name, status: InstallStatus.uninstalling }); const pkgkey = `${name}-${version}`; @@ -92,30 +100,43 @@ function usePackageInstall({ notifications }: { notifications: NotificationsStar if (res.error) { setPackageInstallStatus({ name, status: InstallStatus.installed }); notifications.toasts.addWarning({ - title: `Failed to delete ${title} package`, - text: 'Something went wrong while trying to delete this package. Please try again later.', + title: toMountPoint( + <FormattedMessage + id="xpack.ingestManager.integrations.packageUninstallErrorTitle" + defaultMessage="Failed to uninstall {title} package" + values={{ title }} + /> + ), + text: toMountPoint( + <FormattedMessage + id="xpack.ingestManager.integrations.packageUninstallErrorDescription" + defaultMessage="Something went wrong while trying to uninstall this package. Please try again later." + /> + ), iconType: 'alert', }); } else { setPackageInstallStatus({ name, status: InstallStatus.notInstalled }); - const SuccessMsg = <p>Successfully deleted {title}</p>; - notifications.toasts.addSuccess({ - title: `Deleted ${title} package`, - text: toMountPoint(SuccessMsg), - }); - - const packageUrl = toDetailView({ name, version }); - const dataSourcesUrl = toDetailView({ - name, - version, - panel: 'data-sources', + title: toMountPoint( + <FormattedMessage + id="xpack.ingestManager.integrations.packageUninstallSuccessTitle" + defaultMessage="Uninstalled {title}" + values={{ title }} + /> + ), + text: toMountPoint( + <FormattedMessage + id="xpack.ingestManager.integrations.packageUninstallSuccessDescription" + defaultMessage="Successfully uninstalled {title}" + values={{ title }} + /> + ), }); - if (window.location.href.includes(packageUrl)) window.location.href = dataSourcesUrl; } }, - [notifications.toasts, setPackageInstallStatus, toDetailView] + [notifications.toasts, setPackageInstallStatus] ); return { @@ -123,7 +144,7 @@ function usePackageInstall({ notifications }: { notifications: NotificationsStar installPackage, setPackageInstallStatus, getPackageInstallStatus, - deletePackage, + uninstallPackage, }; } @@ -132,11 +153,11 @@ export const [ useInstallPackage, useSetPackageInstallStatus, useGetPackageInstallStatus, - useDeletePackage, + useUninstallPackage, ] = createContainer( usePackageInstall, value => value.installPackage, value => value.setPackageInstallStatus, value => value.getPackageInstallStatus, - value => value.deletePackage + value => value.uninstallPackage ); diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/screens/detail/confirm_package_delete.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/screens/detail/confirm_package_delete.tsx deleted file mode 100644 index 2b3be04ac476b..0000000000000 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/screens/detail/confirm_package_delete.tsx +++ /dev/null @@ -1,32 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ -import { EuiCallOut, EuiConfirmModal, EuiOverlayMask } from '@elastic/eui'; -import React from 'react'; - -interface ConfirmPackageDeleteProps { - onCancel: () => void; - onConfirm: () => void; - packageName: string; - numOfAssets: number; -} -export const ConfirmPackageDelete = (props: ConfirmPackageDeleteProps) => { - const { onCancel, onConfirm, packageName, numOfAssets } = props; - return ( - <EuiOverlayMask> - <EuiConfirmModal - title={`Delete ${packageName}?`} - onCancel={onCancel} - onConfirm={onConfirm} - cancelButtonText="Cancel" - confirmButtonText="Delete package" - defaultFocusedButton="confirm" - buttonColor="danger" - > - <EuiCallOut title={`This package will delete ${numOfAssets} assets.`} color="danger" /> - </EuiConfirmModal> - </EuiOverlayMask> - ); -}; diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/screens/detail/confirm_package_install.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/screens/detail/confirm_package_install.tsx index 137d9cf226b4d..ac30815a941ee 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/screens/detail/confirm_package_install.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/screens/detail/confirm_package_install.tsx @@ -5,6 +5,7 @@ */ import { EuiCallOut, EuiConfirmModal, EuiOverlayMask, EuiSpacer } from '@elastic/eui'; import React from 'react'; +import { FormattedMessage } from '@kbn/i18n/react'; interface ConfirmPackageInstallProps { onCancel: () => void; @@ -17,18 +18,46 @@ export const ConfirmPackageInstall = (props: ConfirmPackageInstallProps) => { return ( <EuiOverlayMask> <EuiConfirmModal - title={`Install ${packageName}?`} + title={ + <FormattedMessage + id="xpack.ingestManager.integrations.settings.confirmInstallModal.installTitle" + defaultMessage="Install {packageName}" + values={{ packageName }} + /> + } onCancel={onCancel} onConfirm={onConfirm} - cancelButtonText="Cancel" - confirmButtonText="Install package" + cancelButtonText={ + <FormattedMessage + id="xpack.ingestManager.integrations.settings.confirmInstallModal.cancelButtonLabel" + defaultMessage="Cancel" + /> + } + confirmButtonText={ + <FormattedMessage + id="xpack.ingestManager.integrations.settings.confirmInstallModal.installButtonLabel" + defaultMessage="Install {packageName}" + values={{ packageName }} + /> + } defaultFocusedButton="confirm" > - <EuiCallOut title={`This package will install ${numOfAssets} assets.`} /> + <EuiCallOut + iconType="iInCircle" + title={ + <FormattedMessage + id="xpack.ingestManager.integrations.settings.confirmInstallModal.installCalloutTitle" + defaultMessage="This action will install {numOfAssets} assets" + values={{ numOfAssets }} + /> + } + /> <EuiSpacer size="l" /> <p> - and will only be accessible to users who have permission to view this Space. Elasticsearch - assets are installed globally and will be accessible to all Kibana users. + <FormattedMessage + id="xpack.ingestManager.integrations.settings.confirmInstallModal.installDescription" + defaultMessage="Kibana assets will be installed in the current Space (Default) and will only be accessible to users who have permission to view this Space. Elasticsearch assets are installed globally and will be accessible to all Kibana users." + /> </p> </EuiConfirmModal> </EuiOverlayMask> diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/screens/detail/confirm_package_uninstall.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/screens/detail/confirm_package_uninstall.tsx new file mode 100644 index 0000000000000..14b9bf77c3a00 --- /dev/null +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/screens/detail/confirm_package_uninstall.tsx @@ -0,0 +1,73 @@ +/* + * 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, EuiConfirmModal, EuiOverlayMask, EuiSpacer } from '@elastic/eui'; +import React from 'react'; +import { FormattedMessage } from '@kbn/i18n/react'; + +interface ConfirmPackageUninstallProps { + onCancel: () => void; + onConfirm: () => void; + packageName: string; + numOfAssets: number; +} +export const ConfirmPackageUninstall = (props: ConfirmPackageUninstallProps) => { + const { onCancel, onConfirm, packageName, numOfAssets } = props; + return ( + <EuiOverlayMask> + <EuiConfirmModal + title={ + <FormattedMessage + id="xpack.ingestManager.integrations.settings.confirmUninstallModal.uninstallTitle" + defaultMessage="Uninstall {packageName}" + values={{ packageName }} + /> + } + onCancel={onCancel} + onConfirm={onConfirm} + cancelButtonText={ + <FormattedMessage + id="xpack.ingestManager.integrations.settings.confirmUninstallModal.cancelButtonLabel" + defaultMessage="Cancel" + /> + } + confirmButtonText={ + <FormattedMessage + id="xpack.ingestManager.integrations.settings.confirmUninstallModal.uninstallButtonLabel" + defaultMessage="Uninstall {packageName}" + values={{ packageName }} + /> + } + defaultFocusedButton="confirm" + buttonColor="danger" + > + <EuiCallOut + color="danger" + title={ + <FormattedMessage + id="xpack.ingestManager.integrations.settings.confirmUninstallModal.uninstallCallout.title" + defaultMessage="This action will remove {numOfAssets} assets" + values={{ numOfAssets }} + /> + } + > + <p> + <FormattedMessage + id="xpack.ingestManager.integrations.settings.confirmUninstallModal.uninstallCallout.description" + defaultMessage="Kibana and Elasticsearch assets that were created by this Integration will be removed. Agents configurations and any data sent by your agents will not be effected." + /> + </p> + </EuiCallOut> + <EuiSpacer size="l" /> + <p> + <FormattedMessage + id="xpack.ingestManager.integrations.settings.confirmUninstallModal.uninstallDescription" + defaultMessage="This action cannot be undone. Are you sure you wish to continue?" + /> + </p> + </EuiConfirmModal> + </EuiOverlayMask> + ); +}; 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 384cbbeed378e..0d4b395895322 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 @@ -15,6 +15,7 @@ import { CenterColumn, LeftColumn, RightColumn } from './layout'; import { OverviewPanel } from './overview_panel'; import { SideNavLinks } from './side_nav_links'; import { DataSourcesPanel } from './data_sources_panel'; +import { SettingsPanel } from './settings_panel'; type ContentProps = PackageInfo & Pick<DetailParams, 'panel'> & { hasIconPanel: boolean }; export function Content(props: ContentProps) { @@ -49,8 +50,10 @@ export function Content(props: ContentProps) { type ContentPanelProps = PackageInfo & Pick<DetailParams, 'panel'>; export function ContentPanel(props: ContentPanelProps) { - const { panel, name, version } = props; + const { panel, name, version, assets, title } = props; switch (panel) { + case 'settings': + return <SettingsPanel name={name} version={version} assets={assets} title={title} />; case 'data-sources': return <DataSourcesPanel name={name} version={version} />; case 'overview': diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/screens/detail/installation_button.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/screens/detail/installation_button.tsx index 8a8afed5570ed..cbbf1ce53c4ea 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/screens/detail/installation_button.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/screens/detail/installation_button.tsx @@ -5,21 +5,21 @@ */ import { EuiButton } from '@elastic/eui'; import React, { Fragment, useCallback, useMemo, useState } from 'react'; +import { FormattedMessage } from '@kbn/i18n/react'; import { PackageInfo, InstallStatus } from '../../../../types'; import { useCapabilities } from '../../../../hooks'; -import { useDeletePackage, useGetPackageInstallStatus, useInstallPackage } from '../../hooks'; -import { ConfirmPackageDelete } from './confirm_package_delete'; +import { useUninstallPackage, useGetPackageInstallStatus, useInstallPackage } from '../../hooks'; +import { ConfirmPackageUninstall } from './confirm_package_uninstall'; import { ConfirmPackageInstall } from './confirm_package_install'; -interface InstallationButtonProps { - package: PackageInfo; -} - +type InstallationButtonProps = Pick<PackageInfo, 'assets' | 'name' | 'title' | 'version'> & { + disabled: boolean; +}; export function InstallationButton(props: InstallationButtonProps) { - const { assets, name, title, version } = props.package; + const { assets, name, title, version, disabled = true } = props; const hasWriteCapabilites = useCapabilities().write; const installPackage = useInstallPackage(); - const deletePackage = useDeletePackage(); + const uninstallPackage = useUninstallPackage(); const getPackageInstallStatus = useGetPackageInstallStatus(); const installationStatus = getPackageInstallStatus(name); @@ -36,11 +36,12 @@ export function InstallationButton(props: InstallationButtonProps) { toggleModal(); }, [installPackage, name, title, toggleModal, version]); - const handleClickDelete = useCallback(() => { - deletePackage({ name, version, title }); + const handleClickUninstall = useCallback(() => { + uninstallPackage({ name, version, title }); toggleModal(); - }, [deletePackage, name, title, toggleModal, version]); + }, [uninstallPackage, name, title, toggleModal, version]); + // counts the number of assets in the package const numOfAssets = useMemo( () => Object.entries(assets).reduce( @@ -56,30 +57,68 @@ export function InstallationButton(props: InstallationButtonProps) { ); const installButton = ( - <EuiButton isLoading={isInstalling} fill={true} onClick={toggleModal}> - {isInstalling ? 'Installing' : 'Install package'} + <EuiButton iconType={'importAction'} isLoading={isInstalling} onClick={toggleModal}> + {isInstalling ? ( + <FormattedMessage + id="xpack.ingestManager.integrations.installPackage.installingPackageButtonLabel" + defaultMessage="Installing {title} assets" + values={{ + title, + }} + /> + ) : ( + <FormattedMessage + id="xpack.ingestManager.integrations.installPackage.installPackageButtonLabel" + defaultMessage="Install {title} assets" + values={{ + title, + }} + /> + )} </EuiButton> ); - const installedButton = ( - <EuiButton isLoading={isRemoving} fill={true} onClick={toggleModal} color="danger"> - {isInstalling ? 'Deleting' : 'Delete package'} + const uninstallButton = ( + <EuiButton + iconType={'trash'} + isLoading={isRemoving} + onClick={toggleModal} + color="danger" + disabled={disabled || isRemoving ? true : false} + > + {isRemoving ? ( + <FormattedMessage + id="xpack.ingestManager.integrations.uninstallPackage.uninstallingPackageButtonLabel" + defaultMessage="Uninstalling {title}" + values={{ + title, + }} + /> + ) : ( + <FormattedMessage + id="xpack.ingestManager.integrations.uninstallPackage.uninstallPackageButtonLabel" + defaultMessage="Uninstall {title}" + values={{ + title, + }} + /> + )} </EuiButton> ); - const deletionModal = ( - <ConfirmPackageDelete + const uninstallModal = ( + <ConfirmPackageUninstall // this is number of which would be installed // deleted includes ingest-pipelines etc so could be larger // not sure how to do this at the moment so using same value numOfAssets={numOfAssets} packageName={title} onCancel={toggleModal} - onConfirm={handleClickDelete} + onConfirm={handleClickUninstall} /> ); - const installationModal = ( + const installModal = ( <ConfirmPackageInstall numOfAssets={numOfAssets} packageName={title} @@ -90,8 +129,8 @@ export function InstallationButton(props: InstallationButtonProps) { return hasWriteCapabilites ? ( <Fragment> - {isInstalled ? installedButton : installButton} - {isModalVisible && (isInstalled ? deletionModal : installationModal)} + {isInstalled || isRemoving ? uninstallButton : installButton} + {isModalVisible && (isInstalled ? uninstallModal : installModal)} </Fragment> ) : null; } 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 new file mode 100644 index 0000000000000..ff7ecf97714b6 --- /dev/null +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/screens/detail/settings_panel.tsx @@ -0,0 +1,118 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { EuiTitle, EuiFlexGroup, EuiFlexItem, EuiText } from '@elastic/eui'; +import { EuiSpacer } from '@elastic/eui'; +import { useGetPackageInstallStatus } from '../../hooks'; +import { InstallStatus, PackageInfo } from '../../../../types'; +import { InstallationButton } from './installation_button'; +import { useGetDatasources } from '../../../../hooks'; + +export const SettingsPanel = ( + props: Pick<PackageInfo, 'assets' | 'name' | 'title' | 'version'> +) => { + const getPackageInstallStatus = useGetPackageInstallStatus(); + const { data: datasourcesData } = useGetDatasources({ + perPage: 0, + page: 1, + kuery: `datasources.package.name:${props.name}`, + }); + const { name, title } = props; + const packageInstallStatus = getPackageInstallStatus(name); + const packageHasDatasources = !!datasourcesData?.total; + + return ( + <EuiText> + <EuiTitle> + <h3> + <FormattedMessage + id="xpack.ingestManager.integrations.settings.packageSettingsTitle" + defaultMessage="Settings" + /> + </h3> + </EuiTitle> + <EuiSpacer size="s" /> + {packageInstallStatus === InstallStatus.notInstalled || + packageInstallStatus === InstallStatus.installing ? ( + <div> + <EuiTitle> + <h4> + <FormattedMessage + id="xpack.ingestManager.integrations.settings.packageInstallTitle" + defaultMessage="Install {title}" + values={{ + title, + }} + /> + </h4> + </EuiTitle> + <EuiSpacer size="s" /> + <p> + <FormattedMessage + id="xpack.ingestManager.integrations.settings.packageInstallDescription" + defaultMessage="Install this integration to setup Kibana and Elasticsearch assets designed for {title} data." + values={{ + title, + }} + /> + </p> + </div> + ) : ( + <div> + <EuiTitle> + <h4> + <FormattedMessage + id="xpack.ingestManager.integrations.settings.packageUninstallTitle" + defaultMessage="Uninstall {title}" + values={{ + title, + }} + /> + </h4> + </EuiTitle> + <EuiSpacer size="s" /> + <p> + <FormattedMessage + id="xpack.ingestManager.integrations.settings.packageUninstallDescription" + defaultMessage="Remove Kibana and Elasticsearch assets that were installed by this Integration." + /> + </p> + </div> + )} + <EuiFlexGroup> + <EuiFlexItem grow={false}> + <p> + <InstallationButton + {...props} + disabled={!datasourcesData ? true : packageHasDatasources} + /> + </p> + </EuiFlexItem> + </EuiFlexGroup> + {packageHasDatasources && ( + <p> + <FormattedMessage + id="xpack.ingestManager.integrations.settings.packageUninstallNoteDescription.packageUninstallNoteDetail" + defaultMessage="{strongNote} {title} cannot be uninstalled because there are active agents that use this integration. To uninstall, remove all {title} data sources from your agent configurations." + values={{ + title, + strongNote: ( + <strong> + <FormattedMessage + id="xpack.ingestManager.integrations.settings.packageUninstallNoteDescription.packageUninstallNoteLabel" + defaultMessage="Note:" + /> + </strong> + ), + }} + /> + </p> + )} + </EuiText> + ); +}; diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/screens/detail/side_nav_links.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/screens/detail/side_nav_links.tsx index 39a6fca2e4318..05729ccfc1af4 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/screens/detail/side_nav_links.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/screens/detail/side_nav_links.tsx @@ -17,6 +17,7 @@ export type NavLinkProps = Pick<PackageInfo, 'name' | 'version'> & { const PanelDisplayNames: Record<DetailViewPanelName, string> = { overview: 'Overview', 'data-sources': 'Data Sources', + settings: 'Settings', }; export function SideNavLinks({ name, version, active }: NavLinkProps) { diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/agent_list_page/components/agent_enrollment_flyout/instructions.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/agent_list_page/components/agent_enrollment_flyout/instructions.tsx index 1bc20c2baf660..a0244c601cd96 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/agent_list_page/components/agent_enrollment_flyout/instructions.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/agent_list_page/components/agent_enrollment_flyout/instructions.tsx @@ -86,7 +86,7 @@ export const EnrollmentInstructions: React.FunctionComponent<Props> = ({ selecte steps={[ { title: i18n.translate('xpack.ingestManager.agentEnrollment.stepSetupAgents', { - defaultMessage: 'Setup Beats agent', + defaultMessage: 'Setup Elastic agent', }), children: ( <ShellEnrollmentInstructions 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 22314b6231d1e..d363c472f2305 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 @@ -364,7 +364,7 @@ export const AgentListPage: React.FunctionComponent<{}> = () => { <h2> <FormattedMessage id="xpack.ingestManager.agentList.noAgentsPrompt" - defaultMessage="No agents installed" + defaultMessage="No agents enrolled" /> </h2> } @@ -373,7 +373,7 @@ export const AgentListPage: React.FunctionComponent<{}> = () => { <EuiButton fill iconType="plusInCircle" onClick={() => setIsEnrollmentFlyoutOpen(true)}> <FormattedMessage id="xpack.ingestManager.agentList.addButton" - defaultMessage="Install new agent" + defaultMessage="Enroll new agent" /> </EuiButton> ) : null diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/enrollment_token_list_page/index.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/enrollment_token_list_page/index.tsx index e4f7202aeee10..7520f88215efe 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/enrollment_token_list_page/index.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/enrollment_token_list_page/index.tsx @@ -72,7 +72,7 @@ const ApiKeyField: React.FunctionComponent<{ apiKeyId: string }> = ({ apiKeyId } {key} </EuiText> ) : ( - <EuiText color="subdued">••••••••••••••••••••••••••</EuiText> + <EuiText color="subdued">•••••••••••••••••••••</EuiText> )} </EuiFlexItem> <EuiFlexItem grow={false}> @@ -151,11 +151,10 @@ export const EnrollmentTokenListPage: React.FunctionComponent<{}> = () => { defaultMessage: 'Name', }), truncateText: true, - width: '300px', textOnly: true, render: (name: string) => { return ( - <EuiText style={NO_WRAP_TRUNCATE_STYLE} title={name}> + <EuiText size="s" style={NO_WRAP_TRUNCATE_STYLE} title={name}> {name} </EuiText> ); @@ -166,7 +165,7 @@ export const EnrollmentTokenListPage: React.FunctionComponent<{}> = () => { name: i18n.translate('xpack.ingestManager.enrollmentTokensList.secretTitle', { defaultMessage: 'Secret', }), - width: '245px', + width: '215px', render: (apiKeyId: string) => { return <ApiKeyField apiKeyId={apiKeyId} />; }, @@ -186,7 +185,7 @@ export const EnrollmentTokenListPage: React.FunctionComponent<{}> = () => { name: i18n.translate('xpack.ingestManager.enrollmentTokensList.createdAtTitle', { defaultMessage: 'Created on', }), - width: '200px', + width: '150px', render: (createdAt: string) => { return createdAt ? ( <FormattedDate year="numeric" month="short" day="2-digit" value={createdAt} /> @@ -198,7 +197,7 @@ export const EnrollmentTokenListPage: React.FunctionComponent<{}> = () => { name: i18n.translate('xpack.ingestManager.enrollmentTokensList.activeTitle', { defaultMessage: 'Active', }), - width: '80px', + width: '70px', render: (active: boolean) => { return ( <EuiText textAlign="center"> @@ -212,7 +211,7 @@ export const EnrollmentTokenListPage: React.FunctionComponent<{}> = () => { name: i18n.translate('xpack.ingestManager.enrollmentTokensList.actionsTitle', { defaultMessage: 'Actions', }), - width: '100px', + width: '70px', render: (_: any, apiKey: EnrollmentAPIKey) => { return ( apiKey.active && ( diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/overview/index.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/overview/index.tsx index ea6b045f504ec..05d150fd9ae23 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/overview/index.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/overview/index.tsx @@ -3,12 +3,71 @@ * 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 { EuiText, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; +import React, { useState } from 'react'; +import styled from 'styled-components'; +import { + EuiButton, + EuiButtonEmpty, + EuiPanel, + EuiText, + EuiTitle, + EuiDescriptionList, + EuiDescriptionListDescription, + EuiDescriptionListTitle, + EuiFlexGrid, + EuiFlexGroup, + EuiFlexItem, +} from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; import { WithHeaderLayout } from '../../layouts'; +import { useLink, useGetAgentConfigs } from '../../hooks'; +import { AgentEnrollmentFlyout } from '../fleet/agent_list_page/components'; +import { EPM_PATH, FLEET_PATH, AGENT_CONFIG_PATH } from '../../constants'; + +const OverviewPanel = styled(EuiPanel).attrs(props => ({ + paddingSize: 'm', +}))` + header { + display: flex; + align-items: center; + justify-content: space-between; + border-bottom: 1px solid ${props => props.theme.eui.euiColorLightShade}; + margin: -${props => props.theme.eui.paddingSizes.m} -${props => props.theme.eui.paddingSizes.m} + ${props => props.theme.eui.paddingSizes.m}; + padding: ${props => props.theme.eui.paddingSizes.s} ${props => props.theme.eui.paddingSizes.m}; + } + + h2 { + padding: ${props => props.theme.eui.paddingSizes.xs} 0; + } +`; + +const OverviewStats = styled(EuiDescriptionList).attrs(props => ({ + compressed: true, + textStyle: 'reverse', + type: 'column', +}))` + & > * { + margin-top: ${props => props.theme.eui.paddingSizes.s} !important; + + &:first-child, + &:nth-child(2) { + margin-top: 0 !important; + } + } +`; export const IngestManagerOverview: React.FunctionComponent = () => { + // Agent enrollment flyout state + const [isEnrollmentFlyoutOpen, setIsEnrollmentFlyoutOpen] = useState<boolean>(false); + + // Agent configs required for enrollment flyout + const agentConfigsRequest = useGetAgentConfigs({ + page: 1, + perPage: 1000, + }); + const agentConfigs = agentConfigsRequest.data ? agentConfigsRequest.data.items : []; + return ( <WithHeaderLayout leftColumn={ @@ -28,13 +87,150 @@ export const IngestManagerOverview: React.FunctionComponent = () => { <p> <FormattedMessage id="xpack.ingestManager.overviewPageSubtitle" - defaultMessage="Lorem ipsum some description about ingest manager." + defaultMessage="Centralized management for Elastic Agents and configurations." /> </p> </EuiText> </EuiFlexItem> </EuiFlexGroup> } - /> + rightColumn={ + <EuiFlexGroup justifyContent="flexEnd"> + <EuiFlexItem grow={false}> + <EuiButton fill iconType="plusInCircle" onClick={() => setIsEnrollmentFlyoutOpen(true)}> + <FormattedMessage + id="xpack.ingestManager.overviewPageEnrollAgentButton" + defaultMessage="Enroll new agent" + /> + </EuiButton> + </EuiFlexItem> + </EuiFlexGroup> + } + > + {isEnrollmentFlyoutOpen && ( + <AgentEnrollmentFlyout + agentConfigs={agentConfigs} + onClose={() => setIsEnrollmentFlyoutOpen(false)} + /> + )} + + <EuiFlexGrid gutterSize="l" columns={2}> + <EuiFlexItem component="section"> + <OverviewPanel> + <header> + <EuiTitle size="xs"> + <h2> + <FormattedMessage + id="xpack.ingestManager.overviewPageIntegrationsPanelTitle" + defaultMessage="Integrations" + /> + </h2> + </EuiTitle> + <EuiButtonEmpty size="xs" flush="right" href={useLink(EPM_PATH)}> + <FormattedMessage + id="xpack.ingestManager.overviewPageIntegrationsPanelAction" + defaultMessage="View integrations" + /> + </EuiButtonEmpty> + </header> + <OverviewStats> + <EuiDescriptionListTitle>Total available</EuiDescriptionListTitle> + <EuiDescriptionListDescription>999</EuiDescriptionListDescription> + <EuiDescriptionListTitle>Installed</EuiDescriptionListTitle> + <EuiDescriptionListDescription>1</EuiDescriptionListDescription> + <EuiDescriptionListTitle>Updated available</EuiDescriptionListTitle> + <EuiDescriptionListDescription>0</EuiDescriptionListDescription> + </OverviewStats> + </OverviewPanel> + </EuiFlexItem> + + <EuiFlexItem component="section"> + <OverviewPanel> + <header> + <EuiTitle size="xs"> + <h2> + <FormattedMessage + id="xpack.ingestManager.overviewPageConfigurationsPanelTitle" + defaultMessage="Configurations" + /> + </h2> + </EuiTitle> + <EuiButtonEmpty size="xs" flush="right" href={useLink(AGENT_CONFIG_PATH)}> + <FormattedMessage + id="xpack.ingestManager.overviewPageConfigurationsPanelAction" + defaultMessage="View configs" + /> + </EuiButtonEmpty> + </header> + <OverviewStats> + <EuiDescriptionListTitle>Total configs</EuiDescriptionListTitle> + <EuiDescriptionListDescription>1</EuiDescriptionListDescription> + <EuiDescriptionListTitle>Data sources</EuiDescriptionListTitle> + <EuiDescriptionListDescription>1</EuiDescriptionListDescription> + </OverviewStats> + </OverviewPanel> + </EuiFlexItem> + + <EuiFlexItem component="section"> + <OverviewPanel> + <header> + <EuiTitle size="xs"> + <h2> + <FormattedMessage + id="xpack.ingestManager.overviewPageFleetPanelTitle" + defaultMessage="Fleet" + /> + </h2> + </EuiTitle> + <EuiButtonEmpty size="xs" flush="right" href={useLink(FLEET_PATH)}> + <FormattedMessage + id="xpack.ingestManager.overviewPageFleetPanelAction" + defaultMessage="View agents" + /> + </EuiButtonEmpty> + </header> + <OverviewStats> + <EuiDescriptionListTitle>Total agents</EuiDescriptionListTitle> + <EuiDescriptionListDescription>0</EuiDescriptionListDescription> + <EuiDescriptionListTitle>Active</EuiDescriptionListTitle> + <EuiDescriptionListDescription>0</EuiDescriptionListDescription> + <EuiDescriptionListTitle>Offline</EuiDescriptionListTitle> + <EuiDescriptionListDescription>0</EuiDescriptionListDescription> + <EuiDescriptionListTitle>Error</EuiDescriptionListTitle> + <EuiDescriptionListDescription>0</EuiDescriptionListDescription> + </OverviewStats> + </OverviewPanel> + </EuiFlexItem> + + <EuiFlexItem component="section"> + <OverviewPanel> + <header> + <EuiTitle size="xs"> + <h2> + <FormattedMessage + id="xpack.ingestManager.overviewPageDataStreamsPanelTitle" + defaultMessage="Data streams" + /> + </h2> + </EuiTitle> + <EuiButtonEmpty size="xs" flush="right"> + <FormattedMessage + id="xpack.ingestManager.overviewPageDataStreamsPanelAction" + defaultMessage="View data streams" + /> + </EuiButtonEmpty> + </header> + <OverviewStats> + <EuiDescriptionListTitle>Data streams</EuiDescriptionListTitle> + <EuiDescriptionListDescription>0</EuiDescriptionListDescription> + <EuiDescriptionListTitle>Name spaces</EuiDescriptionListTitle> + <EuiDescriptionListDescription>0</EuiDescriptionListDescription> + <EuiDescriptionListTitle>Total size</EuiDescriptionListTitle> + <EuiDescriptionListDescription>0 MB</EuiDescriptionListDescription> + </OverviewStats> + </OverviewPanel> + </EuiFlexItem> + </EuiFlexGrid> + </WithHeaderLayout> ); }; diff --git a/x-pack/plugins/ingest_manager/server/index.ts b/x-pack/plugins/ingest_manager/server/index.ts index df7c3d7cf0fbf..7859c44ccfd89 100644 --- a/x-pack/plugins/ingest_manager/server/index.ts +++ b/x-pack/plugins/ingest_manager/server/index.ts @@ -7,6 +7,9 @@ import { schema, TypeOf } from '@kbn/config-schema'; import { PluginInitializerContext } from 'src/core/server'; import { IngestManagerPlugin } from './plugin'; +export { ESIndexPatternService } from './services'; +export { IngestManagerSetupContract } from './plugin'; + export const config = { exposeToBrowser: { epm: true, diff --git a/x-pack/plugins/ingest_manager/server/plugin.ts b/x-pack/plugins/ingest_manager/server/plugin.ts index 45c847fe1f68a..4dd070a7414f0 100644 --- a/x-pack/plugins/ingest_manager/server/plugin.ts +++ b/x-pack/plugins/ingest_manager/server/plugin.ts @@ -11,7 +11,9 @@ import { Plugin, PluginInitializerContext, SavedObjectsServiceStart, -} from 'src/core/server'; + RecursiveReadonly, +} from 'kibana/server'; +import { deepFreeze } from '../../../../src/core/utils'; import { LicensingPluginSetup } from '../../licensing/server'; import { EncryptedSavedObjectsPluginStart } from '../../encrypted_saved_objects/server'; import { SecurityPluginSetup } from '../../security/server'; @@ -38,7 +40,18 @@ import { } from './routes'; import { IngestManagerConfigType } from '../common'; -import { appContextService } from './services'; +import { + appContextService, + ESIndexPatternService, + ESIndexPatternSavedObjectService, +} from './services'; + +/** + * Describes public IngestManager plugin contract returned at the `setup` stage. + */ +export interface IngestManagerSetupContract { + esIndexPatternService: ESIndexPatternService; +} export interface IngestManagerSetupDeps { licensing: LicensingPluginSetup; @@ -63,7 +76,7 @@ const allSavedObjectTypes = [ ENROLLMENT_API_KEYS_SAVED_OBJECT_TYPE, ]; -export class IngestManagerPlugin implements Plugin { +export class IngestManagerPlugin implements Plugin<IngestManagerSetupContract> { private config$: Observable<IngestManagerConfigType>; private security: SecurityPluginSetup | undefined; @@ -71,7 +84,10 @@ export class IngestManagerPlugin implements Plugin { this.config$ = this.initializerContext.config.create<IngestManagerConfigType>(); } - public async setup(core: CoreSetup, deps: IngestManagerSetupDeps) { + public async setup( + core: CoreSetup, + deps: IngestManagerSetupDeps + ): Promise<RecursiveReadonly<IngestManagerSetupContract>> { if (deps.security) { this.security = deps.security; } @@ -130,6 +146,9 @@ export class IngestManagerPlugin implements Plugin { basePath: core.http.basePath, }); } + return deepFreeze({ + esIndexPatternService: new ESIndexPatternSavedObjectService(), + }); } public async start( diff --git a/x-pack/plugins/ingest_manager/server/routes/datasource/handlers.ts b/x-pack/plugins/ingest_manager/server/routes/datasource/handlers.ts index 7ae562cf130ab..56d6053a1451b 100644 --- a/x-pack/plugins/ingest_manager/server/routes/datasource/handlers.ts +++ b/x-pack/plugins/ingest_manager/server/routes/datasource/handlers.ts @@ -4,6 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ import { TypeOf } from '@kbn/config-schema'; +import Boom from 'boom'; import { RequestHandler } from 'src/core/server'; import { appContextService, datasourceService } from '../../services'; import { ensureInstalledPackage } from '../../services/epm/packages'; @@ -75,6 +76,7 @@ export const createDatasourceHandler: RequestHandler< const soClient = context.core.savedObjects.client; const callCluster = context.core.elasticsearch.adminClient.callAsCurrentUser; const user = (await appContextService.getSecurity()?.authc.getCurrentUser(request)) || undefined; + const newData = { ...request.body }; try { // Make sure the datasource package is installed if (request.body.package?.name) { @@ -83,10 +85,18 @@ export const createDatasourceHandler: RequestHandler< pkgName: request.body.package.name, callCluster, }); + + newData.inputs = (await datasourceService.assignPackageStream( + { + pkgName: request.body.package.name, + pkgVersion: request.body.package.version, + }, + request.body.inputs + )) as TypeOf<typeof CreateDatasourceRequestSchema.body>['inputs']; } // Create datasource - const datasource = await datasourceService.create(soClient, request.body, { user }); + const datasource = await datasourceService.create(soClient, newData, { user }); const body: CreateDatasourceResponse = { item: datasource, success: true }; return response.ok({ body, @@ -107,14 +117,33 @@ export const updateDatasourceHandler: RequestHandler< const soClient = context.core.savedObjects.client; const user = (await appContextService.getSecurity()?.authc.getCurrentUser(request)) || undefined; try { - const datasource = await datasourceService.update( + const datasource = await datasourceService.get(soClient, request.params.datasourceId); + + if (!datasource) { + throw Boom.notFound('Datasource not found'); + } + + const newData = { ...request.body }; + const pkg = newData.package || datasource.package; + const inputs = newData.inputs || datasource.inputs; + if (pkg && (newData.inputs || newData.package)) { + newData.inputs = (await datasourceService.assignPackageStream( + { + pkgName: pkg.name, + pkgVersion: pkg.version, + }, + inputs + )) as TypeOf<typeof CreateDatasourceRequestSchema.body>['inputs']; + } + + const updatedDatasource = await datasourceService.update( soClient, request.params.datasourceId, - request.body, + newData, { user } ); return response.ok({ - body: { item: datasource, success: true }, + body: { item: updatedDatasource, success: true }, }); } catch (e) { return response.customError({ diff --git a/x-pack/plugins/ingest_manager/server/saved_objects.ts b/x-pack/plugins/ingest_manager/server/saved_objects.ts index 6800cb4056700..dc0b4695603e4 100644 --- a/x-pack/plugins/ingest_manager/server/saved_objects.ts +++ b/x-pack/plugins/ingest_manager/server/saved_objects.ts @@ -137,6 +137,7 @@ export const savedObjectMappings = { dataset: { type: 'keyword' }, processors: { type: 'keyword' }, config: { type: 'flattened' }, + pkg_stream: { type: 'flattened' }, }, }, }, @@ -149,6 +150,10 @@ export const savedObjectMappings = { name: { type: 'keyword' }, version: { type: 'keyword' }, internal: { type: 'boolean' }, + es_index_patterns: { + dynamic: false, + type: 'object', + }, installed: { type: 'nested', properties: { diff --git a/x-pack/plugins/ingest_manager/server/services/datasource.test.ts b/x-pack/plugins/ingest_manager/server/services/datasource.test.ts new file mode 100644 index 0000000000000..09c59998388d1 --- /dev/null +++ b/x-pack/plugins/ingest_manager/server/services/datasource.test.ts @@ -0,0 +1,137 @@ +/* + * 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 { datasourceService } from './datasource'; + +async function mockedGetAssetsData(_a: any, _b: any, dataset: string) { + if (dataset === 'dataset1') { + return [ + { + buffer: Buffer.from(` +type: log +metricset: ["dataset1"] +paths: +{{#each paths}} +- {{this}} +{{/each}} +`), + }, + ]; + } + return []; +} + +jest.mock('./epm/packages/assets', () => { + return { + getAssetsDataForPackageKey: mockedGetAssetsData, + }; +}); + +describe('Datasource service', () => { + describe('assignPackageStream', () => { + it('should work with cofig variables from the stream', async () => { + const inputs = await datasourceService.assignPackageStream( + { + pkgName: 'package', + pkgVersion: '1.0.0', + }, + [ + { + type: 'log', + enabled: true, + streams: [ + { + id: 'dataset01', + dataset: 'package.dataset1', + enabled: true, + config: { + paths: { + value: ['/var/log/set.log'], + }, + }, + }, + ], + }, + ] + ); + + expect(inputs).toEqual([ + { + type: 'log', + enabled: true, + streams: [ + { + id: 'dataset01', + dataset: 'package.dataset1', + enabled: true, + config: { + paths: { + value: ['/var/log/set.log'], + }, + }, + pkg_stream: { + metricset: ['dataset1'], + paths: ['/var/log/set.log'], + type: 'log', + }, + }, + ], + }, + ]); + }); + + it('should work with config variables at the input level', async () => { + const inputs = await datasourceService.assignPackageStream( + { + pkgName: 'package', + pkgVersion: '1.0.0', + }, + [ + { + type: 'log', + enabled: true, + config: { + paths: { + value: ['/var/log/set.log'], + }, + }, + streams: [ + { + id: 'dataset01', + dataset: 'package.dataset1', + enabled: true, + }, + ], + }, + ] + ); + + expect(inputs).toEqual([ + { + type: 'log', + enabled: true, + config: { + paths: { + value: ['/var/log/set.log'], + }, + }, + streams: [ + { + id: 'dataset01', + dataset: 'package.dataset1', + enabled: true, + pkg_stream: { + metricset: ['dataset1'], + paths: ['/var/log/set.log'], + type: 'log', + }, + }, + ], + }, + ]); + }); + }); +}); diff --git a/x-pack/plugins/ingest_manager/server/services/datasource.ts b/x-pack/plugins/ingest_manager/server/services/datasource.ts index 1b8f2a690b94d..f27252aaa9a84 100644 --- a/x-pack/plugins/ingest_manager/server/services/datasource.ts +++ b/x-pack/plugins/ingest_manager/server/services/datasource.ts @@ -4,16 +4,28 @@ * you may not use this file except in compliance with the Elastic License. */ import { SavedObjectsClientContract } from 'src/core/server'; +import { safeLoad } from 'js-yaml'; import { AuthenticatedUser } from '../../../security/server'; -import { DeleteDatasourcesResponse, packageToConfigDatasource } from '../../common'; +import { + DeleteDatasourcesResponse, + packageToConfigDatasource, + DatasourceInput, + DatasourceInputStream, +} from '../../common'; import { DATASOURCE_SAVED_OBJECT_TYPE } from '../constants'; import { NewDatasource, Datasource, ListWithKuery } from '../types'; import { agentConfigService } from './agent_config'; import { getPackageInfo, getInstallation } from './epm/packages'; import { outputService } from './output'; +import { getAssetsDataForPackageKey } from './epm/packages/assets'; +import { createStream } from './epm/agent/agent'; const SAVED_OBJECT_TYPE = DATASOURCE_SAVED_OBJECT_TYPE; +function getDataset(st: string) { + return st.split('.')[1]; +} + class DatasourceService { public async create( soClient: SavedObjectsClientContract, @@ -187,6 +199,61 @@ class DatasourceService { } } } + + public async assignPackageStream( + pkgInfo: { pkgName: string; pkgVersion: string }, + inputs: DatasourceInput[] + ): Promise<DatasourceInput[]> { + const inputsPromises = inputs.map(input => _assignPackageStreamToInput(pkgInfo, input)); + return Promise.all(inputsPromises); + } +} + +const _isAgentStream = (p: string) => !!p.match(/agent\/stream\/stream\.yml/); + +async function _assignPackageStreamToInput( + pkgInfo: { pkgName: string; pkgVersion: string }, + input: DatasourceInput +) { + const streamsPromises = input.streams.map(stream => + _assignPackageStreamToStream(pkgInfo, input, stream) + ); + + const streams = await Promise.all(streamsPromises); + return { ...input, streams }; +} + +async function _assignPackageStreamToStream( + pkgInfo: { pkgName: string; pkgVersion: string }, + input: DatasourceInput, + stream: DatasourceInputStream +) { + if (!stream.enabled) { + return { ...stream, pkg_stream: undefined }; + } + const dataset = getDataset(stream.dataset); + const assetsData = await getAssetsDataForPackageKey(pkgInfo, _isAgentStream, dataset); + + const [pkgStream] = assetsData; + if (!pkgStream || !pkgStream.buffer) { + throw new Error(`Stream template not found for dataset ${dataset}`); + } + + // Populate template variables from input config and stream config + const data: { [k: string]: string | string[] } = {}; + if (input.config) { + for (const key of Object.keys(input.config)) { + data[key] = input.config[key].value; + } + } + if (stream.config) { + for (const key of Object.keys(stream.config)) { + data[key] = stream.config[key].value; + } + } + const yaml = safeLoad(createStream(data, pkgStream.buffer.toString())); + stream.pkg_stream = yaml; + return { ...stream }; } export const datasourceService = new DatasourceService(); diff --git a/x-pack/plugins/ingest_manager/server/services/epm/agent/agent.test.ts b/x-pack/plugins/ingest_manager/server/services/epm/agent/agent.test.ts index 4f75ba0332418..21de625532f03 100644 --- a/x-pack/plugins/ingest_manager/server/services/epm/agent/agent.test.ts +++ b/x-pack/plugins/ingest_manager/server/services/epm/agent/agent.test.ts @@ -4,29 +4,31 @@ * you may not use this file except in compliance with the Elastic License. */ -import fs from 'fs'; -import * as yaml from 'js-yaml'; -import path from 'path'; -import { createInput } from './agent'; +import { createStream } from './agent'; -test('test converting input and manifest into template', () => { - const manifest = yaml.safeLoad( - fs.readFileSync(path.join(__dirname, 'tests/manifest.yml'), 'utf8') - ); +test('Test creating a stream from template', () => { + const streamTemplate = ` +input: log +paths: +{{#each paths}} + - {{this}} +{{/each}} +exclude_files: [".gz$"] +processors: + - add_locale: ~ + `; + const vars = { + paths: ['/usr/local/var/log/nginx/access.log'], + }; - const inputTemplate = fs.readFileSync(path.join(__dirname, 'tests/input.yml'), 'utf8'); - const output = createInput(manifest.vars, inputTemplate); + const output = createStream(vars, streamTemplate); - // Golden file path - const generatedFile = path.join(__dirname, './tests/input.generated.yaml'); - - // Regenerate the file if `-generate` flag is used - if (process.argv.includes('-generate')) { - fs.writeFileSync(generatedFile, output); - } - - const outputData = fs.readFileSync(generatedFile, 'utf-8'); - - // Check that content file and generated file are equal - expect(outputData).toBe(output); + expect(output).toBe(` +input: log +paths: + - /usr/local/var/log/nginx/access.log +exclude_files: [".gz$"] +processors: + - add_locale: ~ + `); }); diff --git a/x-pack/plugins/ingest_manager/server/services/epm/agent/agent.ts b/x-pack/plugins/ingest_manager/server/services/epm/agent/agent.ts index c7dd3dab38bc1..5d9a6d409aa1a 100644 --- a/x-pack/plugins/ingest_manager/server/services/epm/agent/agent.ts +++ b/x-pack/plugins/ingest_manager/server/services/epm/agent/agent.ts @@ -5,19 +5,12 @@ */ import Handlebars from 'handlebars'; -import { RegistryVarsEntry } from '../../../types'; -/** - * This takes a dataset object as input and merges it with the input template. - * It returns the resolved template as a string. - */ -export function createInput(vars: RegistryVarsEntry[], inputTemplate: string): string { - const view: Record<RegistryVarsEntry['name'], RegistryVarsEntry['default']> = {}; - - for (const v of vars) { - view[v.name] = v.default; - } +interface StreamVars { + [k: string]: string | string[]; +} - const template = Handlebars.compile(inputTemplate); - return template(view); +export function createStream(vars: StreamVars, streamTemplate: string) { + const template = Handlebars.compile(streamTemplate); + return template(vars); } diff --git a/x-pack/plugins/ingest_manager/server/services/epm/agent/tests/input.generated.yaml b/x-pack/plugins/ingest_manager/server/services/epm/agent/tests/input.generated.yaml deleted file mode 100644 index 451ed554ce259..0000000000000 --- a/x-pack/plugins/ingest_manager/server/services/epm/agent/tests/input.generated.yaml +++ /dev/null @@ -1,5 +0,0 @@ -type: log -paths: - - "/var/log/nginx/access.log*" - -tags: nginx diff --git a/x-pack/plugins/ingest_manager/server/services/epm/agent/tests/input.yml b/x-pack/plugins/ingest_manager/server/services/epm/agent/tests/input.yml deleted file mode 100644 index 65a23fc2fa9ad..0000000000000 --- a/x-pack/plugins/ingest_manager/server/services/epm/agent/tests/input.yml +++ /dev/null @@ -1,7 +0,0 @@ -type: log -paths: -{{#each paths}} - - "{{this}}" -{{/each}} - -tags: {{tags}} diff --git a/x-pack/plugins/ingest_manager/server/services/epm/agent/tests/manifest.yml b/x-pack/plugins/ingest_manager/server/services/epm/agent/tests/manifest.yml deleted file mode 100644 index 46a38179fe132..0000000000000 --- a/x-pack/plugins/ingest_manager/server/services/epm/agent/tests/manifest.yml +++ /dev/null @@ -1,20 +0,0 @@ -title: Nginx Acess Logs -release: beta -type: logs -ingest_pipeline: default - -vars: - - name: paths - # Should we define this as array? How will the UI best make sense of it? - type: textarea - default: - - /var/log/nginx/access.log* - # I suggest to use ECS fields for this config options here: https://github.com/elastic/ecs/blob/master/schemas/os.yml - # This would need to be based on a predefined definition on what can be filtered on - os.darwin: - - /usr/local/var/log/nginx/access.log* - os.windows: - - c:/programdata/nginx/logs/*access.log* - - name: tags - default: [nginx] - type: text 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 0e239c24dd9cf..166983fbccc35 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 @@ -47,12 +47,12 @@ exports[`tests loading base.yml: base.yml 1`] = ` "user": { "properties": { "auid": { - "type": "keyword", - "ignore_above": 1024 + "ignore_above": 1024, + "type": "keyword" }, "euid": { - "type": "keyword", - "ignore_above": 1024 + "ignore_above": 1024, + "type": "keyword" } } }, @@ -73,12 +73,12 @@ exports[`tests loading base.yml: base.yml 1`] = ` "nested": { "properties": { "bar": { - "type": "keyword", - "ignore_above": 1024 + "ignore_above": 1024, + "type": "keyword" }, "baz": { - "type": "keyword", - "ignore_above": 1024 + "ignore_above": 1024, + "type": "keyword" } } }, @@ -142,8 +142,8 @@ exports[`tests loading coredns.logs.yml: coredns.logs.yml 1`] = ` "coredns": { "properties": { "id": { - "type": "keyword", - "ignore_above": 1024 + "ignore_above": 1024, + "type": "keyword" }, "query": { "properties": { @@ -151,28 +151,28 @@ exports[`tests loading coredns.logs.yml: coredns.logs.yml 1`] = ` "type": "long" }, "class": { - "type": "keyword", - "ignore_above": 1024 + "ignore_above": 1024, + "type": "keyword" }, "name": { - "type": "keyword", - "ignore_above": 1024 + "ignore_above": 1024, + "type": "keyword" }, "type": { - "type": "keyword", - "ignore_above": 1024 + "ignore_above": 1024, + "type": "keyword" } } }, "response": { "properties": { "code": { - "type": "keyword", - "ignore_above": 1024 + "ignore_above": 1024, + "type": "keyword" }, "flags": { - "type": "keyword", - "ignore_above": 1024 + "ignore_above": 1024, + "type": "keyword" }, "size": { "type": "long" @@ -509,12 +509,12 @@ exports[`tests loading system.yml: system.yml 1`] = ` "diskio": { "properties": { "name": { - "type": "keyword", - "ignore_above": 1024 + "ignore_above": 1024, + "type": "keyword" }, "serial_number": { - "type": "keyword", - "ignore_above": 1024 + "ignore_above": 1024, + "type": "keyword" }, "read": { "properties": { @@ -643,16 +643,16 @@ exports[`tests loading system.yml: system.yml 1`] = ` "type": "long" }, "device_name": { - "type": "keyword", - "ignore_above": 1024 + "ignore_above": 1024, + "type": "keyword" }, "type": { - "type": "keyword", - "ignore_above": 1024 + "ignore_above": 1024, + "type": "keyword" }, "mount_point": { - "type": "keyword", - "ignore_above": 1024 + "ignore_above": 1024, + "type": "keyword" }, "files": { "type": "long" @@ -867,8 +867,8 @@ exports[`tests loading system.yml: system.yml 1`] = ` "network": { "properties": { "name": { - "type": "keyword", - "ignore_above": 1024 + "ignore_above": 1024, + "type": "keyword" }, "out": { "properties": { @@ -946,12 +946,12 @@ exports[`tests loading system.yml: system.yml 1`] = ` "process": { "properties": { "state": { - "type": "keyword", - "ignore_above": 1024 + "ignore_above": 1024, + "type": "keyword" }, "cmdline": { - "type": "keyword", - "ignore_above": 2048 + "ignore_above": 2048, + "type": "keyword" }, "env": { "type": "object" @@ -1040,22 +1040,22 @@ exports[`tests loading system.yml: system.yml 1`] = ` "cgroup": { "properties": { "id": { - "type": "keyword", - "ignore_above": 1024 + "ignore_above": 1024, + "type": "keyword" }, "path": { - "type": "keyword", - "ignore_above": 1024 + "ignore_above": 1024, + "type": "keyword" }, "cpu": { "properties": { "id": { - "type": "keyword", - "ignore_above": 1024 + "ignore_above": 1024, + "type": "keyword" }, "path": { - "type": "keyword", - "ignore_above": 1024 + "ignore_above": 1024, + "type": "keyword" }, "cfs": { "properties": { @@ -1118,12 +1118,12 @@ exports[`tests loading system.yml: system.yml 1`] = ` "cpuacct": { "properties": { "id": { - "type": "keyword", - "ignore_above": 1024 + "ignore_above": 1024, + "type": "keyword" }, "path": { - "type": "keyword", - "ignore_above": 1024 + "ignore_above": 1024, + "type": "keyword" }, "total": { "properties": { @@ -1158,12 +1158,12 @@ exports[`tests loading system.yml: system.yml 1`] = ` "memory": { "properties": { "id": { - "type": "keyword", - "ignore_above": 1024 + "ignore_above": 1024, + "type": "keyword" }, "path": { - "type": "keyword", - "ignore_above": 1024 + "ignore_above": 1024, + "type": "keyword" }, "mem": { "properties": { @@ -1382,12 +1382,12 @@ exports[`tests loading system.yml: system.yml 1`] = ` "blkio": { "properties": { "id": { - "type": "keyword", - "ignore_above": 1024 + "ignore_above": 1024, + "type": "keyword" }, "path": { - "type": "keyword", - "ignore_above": 1024 + "ignore_above": 1024, + "type": "keyword" }, "total": { "properties": { @@ -1436,20 +1436,20 @@ exports[`tests loading system.yml: system.yml 1`] = ` "raid": { "properties": { "name": { - "type": "keyword", - "ignore_above": 1024 + "ignore_above": 1024, + "type": "keyword" }, "status": { - "type": "keyword", - "ignore_above": 1024 + "ignore_above": 1024, + "type": "keyword" }, "level": { - "type": "keyword", - "ignore_above": 1024 + "ignore_above": 1024, + "type": "keyword" }, "sync_action": { - "type": "keyword", - "ignore_above": 1024 + "ignore_above": 1024, + "type": "keyword" }, "disks": { "properties": { @@ -1507,24 +1507,24 @@ exports[`tests loading system.yml: system.yml 1`] = ` "type": "long" }, "host": { - "type": "keyword", - "ignore_above": 1024 + "ignore_above": 1024, + "type": "keyword" }, "etld_plus_one": { - "type": "keyword", - "ignore_above": 1024 + "ignore_above": 1024, + "type": "keyword" }, "host_error": { - "type": "keyword", - "ignore_above": 1024 + "ignore_above": 1024, + "type": "keyword" } } }, "process": { "properties": { "cmdline": { - "type": "keyword", - "ignore_above": 1024 + "ignore_above": 1024, + "type": "keyword" } } }, @@ -1622,42 +1622,42 @@ exports[`tests loading system.yml: system.yml 1`] = ` "users": { "properties": { "id": { - "type": "keyword", - "ignore_above": 1024 + "ignore_above": 1024, + "type": "keyword" }, "seat": { - "type": "keyword", - "ignore_above": 1024 + "ignore_above": 1024, + "type": "keyword" }, "path": { - "type": "keyword", - "ignore_above": 1024 + "ignore_above": 1024, + "type": "keyword" }, "type": { - "type": "keyword", - "ignore_above": 1024 + "ignore_above": 1024, + "type": "keyword" }, "service": { - "type": "keyword", - "ignore_above": 1024 + "ignore_above": 1024, + "type": "keyword" }, "remote": { "type": "boolean" }, "state": { - "type": "keyword", - "ignore_above": 1024 + "ignore_above": 1024, + "type": "keyword" }, "scope": { - "type": "keyword", - "ignore_above": 1024 + "ignore_above": 1024, + "type": "keyword" }, "leader": { "type": "long" }, "remote_host": { - "type": "keyword", - "ignore_above": 1024 + "ignore_above": 1024, + "type": "keyword" } } } diff --git a/x-pack/plugins/ingest_manager/server/services/epm/elasticsearch/template/template.test.ts b/x-pack/plugins/ingest_manager/server/services/epm/elasticsearch/template/template.test.ts index f4e13748641ed..1a73c9581a2de 100644 --- a/x-pack/plugins/ingest_manager/server/services/epm/elasticsearch/template/template.test.ts +++ b/x-pack/plugins/ingest_manager/server/services/epm/elasticsearch/template/template.test.ts @@ -63,3 +63,199 @@ test('tests loading system.yml', () => { expect(template).toMatchSnapshot(path.basename(ymlPath)); }); + +test('tests processing text field with multi fields', () => { + const textWithMultiFieldsLiteralYml = ` +- name: textWithMultiFields + type: text + multi_fields: + - name: raw + type: keyword + - name: indexed + type: text +`; + const textWithMultiFieldsMapping = { + properties: { + textWithMultiFields: { + type: 'text', + fields: { + raw: { + ignore_above: 1024, + type: 'keyword', + }, + indexed: { + type: 'text', + }, + }, + }, + }, + }; + const fields: Field[] = safeLoad(textWithMultiFieldsLiteralYml); + const processedFields = processFields(fields); + const mappings = generateMappings(processedFields); + expect(JSON.stringify(mappings)).toEqual(JSON.stringify(textWithMultiFieldsMapping)); +}); + +test('tests processing keyword field with multi fields', () => { + const keywordWithMultiFieldsLiteralYml = ` +- name: keywordWithMultiFields + type: keyword + multi_fields: + - name: raw + type: keyword + - name: indexed + type: text +`; + + const keywordWithMultiFieldsMapping = { + properties: { + keywordWithMultiFields: { + ignore_above: 1024, + type: 'keyword', + fields: { + raw: { + ignore_above: 1024, + type: 'keyword', + }, + indexed: { + type: 'text', + }, + }, + }, + }, + }; + const fields: Field[] = safeLoad(keywordWithMultiFieldsLiteralYml); + const processedFields = processFields(fields); + const mappings = generateMappings(processedFields); + expect(JSON.stringify(mappings)).toEqual(JSON.stringify(keywordWithMultiFieldsMapping)); +}); + +test('tests processing keyword field with multi fields with analyzed text field', () => { + const keywordWithAnalyzedMultiFieldsLiteralYml = ` + - name: keywordWithAnalyzedMultiField + type: keyword + multi_fields: + - name: analyzed + type: text + analyzer: autocomplete + search_analyzer: standard + `; + + const keywordWithAnalyzedMultiFieldsMapping = { + properties: { + keywordWithAnalyzedMultiField: { + ignore_above: 1024, + type: 'keyword', + fields: { + analyzed: { + analyzer: 'autocomplete', + search_analyzer: 'standard', + type: 'text', + }, + }, + }, + }, + }; + const fields: Field[] = safeLoad(keywordWithAnalyzedMultiFieldsLiteralYml); + const processedFields = processFields(fields); + const mappings = generateMappings(processedFields); + expect(JSON.stringify(mappings)).toEqual(JSON.stringify(keywordWithAnalyzedMultiFieldsMapping)); +}); + +test('tests processing object field with no other attributes', () => { + const objectFieldLiteralYml = ` +- name: objectField + type: object +`; + const objectFieldMapping = { + properties: { + objectField: { + type: 'object', + }, + }, + }; + const fields: Field[] = safeLoad(objectFieldLiteralYml); + const processedFields = processFields(fields); + const mappings = generateMappings(processedFields); + expect(JSON.stringify(mappings)).toEqual(JSON.stringify(objectFieldMapping)); +}); + +test('tests processing object field with enabled set to false', () => { + const objectFieldEnabledFalseLiteralYml = ` +- name: objectField + type: object + enabled: false +`; + const objectFieldEnabledFalseMapping = { + properties: { + objectField: { + type: 'object', + enabled: false, + }, + }, + }; + const fields: Field[] = safeLoad(objectFieldEnabledFalseLiteralYml); + const processedFields = processFields(fields); + const mappings = generateMappings(processedFields); + expect(JSON.stringify(mappings)).toEqual(JSON.stringify(objectFieldEnabledFalseMapping)); +}); + +test('tests processing object field with dynamic set to false', () => { + const objectFieldDynamicFalseLiteralYml = ` +- name: objectField + type: object + dynamic: false +`; + const objectFieldDynamicFalseMapping = { + properties: { + objectField: { + type: 'object', + dynamic: false, + }, + }, + }; + const fields: Field[] = safeLoad(objectFieldDynamicFalseLiteralYml); + const processedFields = processFields(fields); + const mappings = generateMappings(processedFields); + expect(JSON.stringify(mappings)).toEqual(JSON.stringify(objectFieldDynamicFalseMapping)); +}); + +test('tests processing object field with dynamic set to true', () => { + const objectFieldDynamicTrueLiteralYml = ` +- name: objectField + type: object + dynamic: true +`; + const objectFieldDynamicTrueMapping = { + properties: { + objectField: { + type: 'object', + dynamic: true, + }, + }, + }; + const fields: Field[] = safeLoad(objectFieldDynamicTrueLiteralYml); + const processedFields = processFields(fields); + const mappings = generateMappings(processedFields); + expect(JSON.stringify(mappings)).toEqual(JSON.stringify(objectFieldDynamicTrueMapping)); +}); + +test('tests processing object field with dynamic set to strict', () => { + const objectFieldDynamicStrictLiteralYml = ` +- name: objectField + type: object + dynamic: strict +`; + const objectFieldDynamicStrictMapping = { + properties: { + objectField: { + type: 'object', + dynamic: 'strict', + }, + }, + }; + const fields: Field[] = safeLoad(objectFieldDynamicStrictLiteralYml); + const processedFields = processFields(fields); + const mappings = generateMappings(processedFields); + expect(JSON.stringify(mappings)).toEqual(JSON.stringify(objectFieldDynamicStrictMapping)); +}); 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 71c9acc6c10da..22a61d2bdfb7c 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 @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { Field } from '../../fields/field'; +import { Field, Fields } from '../../fields/field'; import { Dataset, IndexTemplate } from '../../../../types'; import { getDatasetAssetBaseName } from '../index'; @@ -15,6 +15,14 @@ interface Mappings { properties: any; } +interface Mapping { + [key: string]: any; +} + +interface MultiFields { + [key: string]: object; +} + const DEFAULT_SCALING_FACTOR = 1000; const DEFAULT_IGNORE_ABOVE = 1024; @@ -67,26 +75,27 @@ export function generateMappings(fields: Field[]): Mappings { fieldProps.scaling_factor = field.scaling_factor || DEFAULT_SCALING_FACTOR; break; case 'text': - fieldProps.type = 'text'; - if (field.analyzer) { - fieldProps.analyzer = field.analyzer; - } - if (field.search_analyzer) { - fieldProps.search_analyzer = field.search_analyzer; + const textMapping = generateTextMapping(field); + fieldProps = { ...fieldProps, ...textMapping, type: 'text' }; + if (field.multi_fields) { + fieldProps.fields = generateMultiFields(field.multi_fields); } break; case 'keyword': - fieldProps.type = 'keyword'; - if (field.ignore_above) { - fieldProps.ignore_above = field.ignore_above; - } else { - fieldProps.ignore_above = DEFAULT_IGNORE_ABOVE; + const keywordMapping = generateKeywordMapping(field); + fieldProps = { ...fieldProps, ...keywordMapping, type: 'keyword' }; + if (field.multi_fields) { + fieldProps.fields = generateMultiFields(field.multi_fields); } break; - // TODO move handling of multi_fields here? case 'object': - // TODO improve fieldProps.type = 'object'; + if (field.hasOwnProperty('enabled')) { + fieldProps.enabled = field.enabled; + } + if (field.hasOwnProperty('dynamic')) { + fieldProps.dynamic = field.dynamic; + } break; case 'array': // this assumes array fields were validated in an earlier step @@ -113,6 +122,45 @@ export function generateMappings(fields: Field[]): Mappings { return { properties: props }; } +function generateMultiFields(fields: Fields): MultiFields { + const multiFields: MultiFields = {}; + if (fields) { + fields.forEach((f: Field) => { + const type = f.type; + switch (type) { + case 'text': + multiFields[f.name] = { ...generateTextMapping(f), type: f.type }; + break; + case 'keyword': + multiFields[f.name] = { ...generateKeywordMapping(f), type: f.type }; + break; + } + }); + } + return multiFields; +} + +function generateKeywordMapping(field: Field): Mapping { + const mapping: Mapping = { + ignore_above: DEFAULT_IGNORE_ABOVE, + }; + if (field.ignore_above) { + mapping.ignore_above = field.ignore_above; + } + return mapping; +} + +function generateTextMapping(field: Field): Mapping { + const mapping: Mapping = {}; + if (field.analyzer) { + mapping.analyzer = field.analyzer; + } + if (field.search_analyzer) { + mapping.search_analyzer = field.search_analyzer; + } + return mapping; +} + function getDefaultProperties(field: Field): Properties { const properties: Properties = {}; @@ -136,6 +184,22 @@ export function generateTemplateName(dataset: Dataset): string { return getDatasetAssetBaseName(dataset); } +/** + * Returns a map of the dataset path fields to elasticsearch index pattern. + * @param datasets an array of Dataset objects + */ +export function generateESIndexPatterns(datasets: Dataset[] | undefined): Record<string, string> { + if (!datasets) { + return {}; + } + + const patterns: Record<string, string> = {}; + for (const dataset of datasets) { + patterns[dataset.path] = generateTemplateName(dataset) + '-*'; + } + return patterns; +} + function getBaseTemplate(type: string, templateName: string, mappings: Mappings): IndexTemplate { return { // We need to decide which order we use for the templates diff --git a/x-pack/plugins/ingest_manager/server/services/epm/fields/field.ts b/x-pack/plugins/ingest_manager/server/services/epm/fields/field.ts index 810896bb50389..9c9843e0454ab 100644 --- a/x-pack/plugins/ingest_manager/server/services/epm/fields/field.ts +++ b/x-pack/plugins/ingest_manager/server/services/epm/fields/field.ts @@ -27,6 +27,7 @@ export interface Field { ignore_above?: number; object_type?: string; scaling_factor?: number; + dynamic?: 'strict' | boolean; // Kibana specific analyzed?: boolean; diff --git a/x-pack/plugins/ingest_manager/server/services/epm/packages/assets.ts b/x-pack/plugins/ingest_manager/server/services/epm/packages/assets.ts index 7026d9eae24c3..50d347c69cc24 100644 --- a/x-pack/plugins/ingest_manager/server/services/epm/packages/assets.ts +++ b/x-pack/plugins/ingest_manager/server/services/epm/packages/assets.ts @@ -71,3 +71,13 @@ export async function getAssetsData( return entries; } + +export async function getAssetsDataForPackageKey( + { pkgName, pkgVersion }: { pkgName: string; pkgVersion: string }, + filter = (path: string): boolean => true, + datasetName?: string +): Promise<Registry.ArchiveEntry[]> { + const registryPkgInfo = await Registry.fetchInfo(pkgName, pkgVersion); + + return getAssetsData(registryPkgInfo, filter, datasetName); +} 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 0e2c2a3d26073..d76584225877c 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 @@ -67,7 +67,7 @@ export async function getPackageInfo(options: { pkgVersion: string; }): Promise<PackageInfo> { const { savedObjectsClient, pkgName, pkgVersion } = options; - const [item, savedObject] = await Promise.all([ + const [item, savedObject, assets] = await Promise.all([ Registry.fetchInfo(pkgName, pkgVersion), getInstallationObject({ savedObjectsClient, pkgName }), Registry.getArchiveInfo(pkgName, pkgVersion), @@ -80,7 +80,7 @@ export async function getPackageInfo(options: { const updated = { ...item, title: item.title || nameAsTitle(item.name), - assets: Registry.groupPathsByService(item?.assets || []), + assets: Registry.groupPathsByService(assets || []), }; return createInstallableFrom(updated, savedObject); } 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 e250b4f176819..0a7642752b3e9 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 @@ -18,6 +18,7 @@ import * as Registry from '../registry'; import { getObject } from './get_objects'; import { getInstallation } from './index'; import { installTemplates } from '../elasticsearch/template/install'; +import { generateESIndexPatterns } from '../elasticsearch/template/template'; import { installPipelines } from '../elasticsearch/ingest_pipeline/install'; import { installILMPolicy } from '../elasticsearch/ilm/install'; @@ -117,17 +118,18 @@ export async function installPackage(options: { installTemplatePromises, ]); - const toSave = res.flat(); + const toSaveAssetRefs: AssetReference[] = res.flat(); + const toSaveESIndexPatterns = generateESIndexPatterns(registryPackageInfo.datasets); // Save those references in the package manager's state saved object - await saveInstallationReferences({ + return await saveInstallationReferences({ savedObjectsClient, pkgkey, pkgName, pkgVersion, internal, - toSave, + toSaveAssetRefs, + toSaveESIndexPatterns, }); - return toSave; } // TODO: make it an exhaustive list @@ -156,25 +158,44 @@ export async function saveInstallationReferences(options: { pkgName: string; pkgVersion: string; internal: boolean; - toSave: AssetReference[]; + toSaveAssetRefs: AssetReference[]; + toSaveESIndexPatterns: Record<string, string>; }) { - const { savedObjectsClient, pkgName, pkgVersion, internal, toSave } = options; + const { + savedObjectsClient, + pkgName, + pkgVersion, + internal, + toSaveAssetRefs, + toSaveESIndexPatterns, + } = options; const installation = await getInstallation({ savedObjectsClient, pkgName }); - const savedRefs = installation?.installed || []; + const savedAssetRefs = installation?.installed || []; + const toInstallESIndexPatterns = Object.assign( + installation?.es_index_patterns || {}, + toSaveESIndexPatterns + ); + const mergeRefsReducer = (current: AssetReference[], pending: AssetReference) => { const hasRef = current.find(c => c.id === pending.id && c.type === pending.type); if (!hasRef) current.push(pending); return current; }; - const toInstall = toSave.reduce(mergeRefsReducer, savedRefs); + const toInstallAssetsRefs = toSaveAssetRefs.reduce(mergeRefsReducer, savedAssetRefs); await savedObjectsClient.create<Installation>( PACKAGES_SAVED_OBJECT_TYPE, - { installed: toInstall, name: pkgName, version: pkgVersion, internal }, + { + installed: toInstallAssetsRefs, + es_index_patterns: toInstallESIndexPatterns, + name: pkgName, + version: pkgVersion, + internal, + }, { id: pkgName, overwrite: true } ); - return toInstall; + return toInstallAssetsRefs; } async function installKibanaSavedObjects({ diff --git a/x-pack/plugins/ingest_manager/server/services/es_index_pattern.ts b/x-pack/plugins/ingest_manager/server/services/es_index_pattern.ts new file mode 100644 index 0000000000000..167e22873979c --- /dev/null +++ b/x-pack/plugins/ingest_manager/server/services/es_index_pattern.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 { SavedObjectsClientContract } from 'kibana/server'; +import { getInstallation } from './epm/packages/get'; + +export interface ESIndexPatternService { + getESIndexPattern( + savedObjectsClient: SavedObjectsClientContract, + pkgName: string, + datasetPath: string + ): Promise<string | undefined>; +} + +export class ESIndexPatternSavedObjectService implements ESIndexPatternService { + public async getESIndexPattern( + savedObjectsClient: SavedObjectsClientContract, + pkgName: string, + datasetPath: string + ): Promise<string | undefined> { + const installation = await getInstallation({ savedObjectsClient, pkgName }); + return installation?.es_index_patterns[datasetPath]; + } +} diff --git a/x-pack/plugins/ingest_manager/server/services/index.ts b/x-pack/plugins/ingest_manager/server/services/index.ts index dd0c898afa425..d64f1b0c2b6fb 100644 --- a/x-pack/plugins/ingest_manager/server/services/index.ts +++ b/x-pack/plugins/ingest_manager/server/services/index.ts @@ -4,6 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ export { appContextService } from './app_context'; +export { ESIndexPatternService, ESIndexPatternSavedObjectService } from './es_index_pattern'; // Saved object services export { datasourceService } from './datasource'; diff --git a/x-pack/plugins/ingest_manager/server/services/setup.ts b/x-pack/plugins/ingest_manager/server/services/setup.ts index bbaf083fb8396..167a24481aba5 100644 --- a/x-pack/plugins/ingest_manager/server/services/setup.ts +++ b/x-pack/plugins/ingest_manager/server/services/setup.ts @@ -123,8 +123,18 @@ async function addPackageToConfig( pkgName: packageToInstall.name, pkgVersion: packageToInstall.version, }); - await datasourceService.create( - soClient, - packageToConfigDatasource(packageInfo, config.id, defaultOutput.id, undefined, config.namespace) + + const newDatasource = packageToConfigDatasource( + packageInfo, + config.id, + defaultOutput.id, + undefined, + config.namespace + ); + newDatasource.inputs = await datasourceService.assignPackageStream( + { pkgName: packageToInstall.name, pkgVersion: packageToInstall.version }, + newDatasource.inputs ); + + await datasourceService.create(soClient, newDatasource); } diff --git a/x-pack/plugins/lens/config.ts b/x-pack/plugins/lens/config.ts new file mode 100644 index 0000000000000..84cf02a7ea541 --- /dev/null +++ b/x-pack/plugins/lens/config.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 { schema, TypeOf } from '@kbn/config-schema'; + +export const configSchema = schema.object({ + enabled: schema.boolean({ defaultValue: true }), +}); + +export type ConfigSchema = TypeOf<typeof configSchema>; diff --git a/x-pack/plugins/lens/kibana.json b/x-pack/plugins/lens/kibana.json index 6abdaad7903be..ce544b31b88ef 100644 --- a/x-pack/plugins/lens/kibana.json +++ b/x-pack/plugins/lens/kibana.json @@ -3,7 +3,15 @@ "version": "8.0.0", "kibanaVersion": "kibana", "server": true, - "ui": false, - "optionalPlugins": ["usageCollection", "taskManager"], + "ui": true, + "requiredPlugins": [ + "data", + "expressions", + "navigation", + "kibanaLegacy", + "uiActions", + "visualizations" + ], + "optionalPlugins": ["embeddable", "usageCollection", "taskManager"], "configPath": ["xpack", "lens"] } diff --git a/x-pack/legacy/plugins/lens/public/_mixins.scss b/x-pack/plugins/lens/public/_mixins.scss similarity index 100% rename from x-pack/legacy/plugins/lens/public/_mixins.scss rename to x-pack/plugins/lens/public/_mixins.scss diff --git a/x-pack/legacy/plugins/lens/public/_variables.scss b/x-pack/plugins/lens/public/_variables.scss similarity index 100% rename from x-pack/legacy/plugins/lens/public/_variables.scss rename to x-pack/plugins/lens/public/_variables.scss diff --git a/x-pack/legacy/plugins/lens/public/app_plugin/_app.scss b/x-pack/plugins/lens/public/app_plugin/_app.scss similarity index 100% rename from x-pack/legacy/plugins/lens/public/app_plugin/_app.scss rename to x-pack/plugins/lens/public/app_plugin/_app.scss diff --git a/x-pack/plugins/lens/public/app_plugin/_index.scss b/x-pack/plugins/lens/public/app_plugin/_index.scss new file mode 100644 index 0000000000000..e72e824224956 --- /dev/null +++ b/x-pack/plugins/lens/public/app_plugin/_index.scss @@ -0,0 +1 @@ +@import 'app'; diff --git a/x-pack/legacy/plugins/lens/public/app_plugin/app.test.tsx b/x-pack/plugins/lens/public/app_plugin/app.test.tsx similarity index 94% rename from x-pack/legacy/plugins/lens/public/app_plugin/app.test.tsx rename to x-pack/plugins/lens/public/app_plugin/app.test.tsx index be72dd4b4edef..41d0e3a7aa9a0 100644 --- a/x-pack/legacy/plugins/lens/public/app_plugin/app.test.tsx +++ b/x-pack/plugins/lens/public/app_plugin/app.test.tsx @@ -9,33 +9,33 @@ import { ReactWrapper } from 'enzyme'; import { act } from 'react-dom/test-utils'; import { App } from './app'; import { EditorFrameInstance } from '../types'; -import { Storage } from '../../../../../../src/plugins/kibana_utils/public'; +import { Storage } from '../../../../../src/plugins/kibana_utils/public'; import { Document, SavedObjectStore } from '../persistence'; import { mount } from 'enzyme'; +import { SavedObjectSaveModal } from '../../../../../src/plugins/saved_objects/public'; import { esFilters, FilterManager, IFieldType, IIndexPattern, -} from '../../../../../../src/plugins/data/public'; -import { dataPluginMock } from '../../../../../../src/plugins/data/public/mocks'; +} from '../../../../../src/plugins/data/public'; +import { dataPluginMock } from '../../../../../src/plugins/data/public/mocks'; const dataStartMock = dataPluginMock.createStartContract(); -import { TopNavMenuData } from '../../../../../../src/plugins/navigation/public'; +import { navigationPluginMock } from '../../../../../src/plugins/navigation/public/mocks'; +import { TopNavMenuData } from '../../../../../src/plugins/navigation/public'; import { coreMock } from 'src/core/public/mocks'; -jest.mock('ui/new_platform'); jest.mock('../persistence'); jest.mock('src/core/public'); -import { npStart } from 'ui/new_platform'; -jest - .spyOn(npStart.plugins.navigation.ui.TopNavMenu.prototype, 'constructor') - .mockImplementation(() => { - return <div className="topNavMenu" />; - }); +const navigationStartMock = navigationPluginMock.createStartContract(); + +jest.spyOn(navigationStartMock.ui.TopNavMenu.prototype, 'constructor').mockImplementation(() => { + return <div className="topNavMenu" />; +}); -const { TopNavMenu } = npStart.plugins.navigation.ui; +const { TopNavMenu } = navigationStartMock.ui; function createMockFrame(): jest.Mocked<EditorFrameInstance> { return { @@ -99,6 +99,7 @@ describe('Lens App', () => { function makeDefaultArgs(): jest.Mocked<{ editorFrame: EditorFrameInstance; data: typeof dataStartMock; + navigation: typeof navigationStartMock; core: typeof core; storage: Storage; docId?: string; @@ -107,6 +108,7 @@ describe('Lens App', () => { addToDashboardMode?: boolean; }> { return ({ + navigation: navigationStartMock, editorFrame: createMockFrame(), core: { ...core, @@ -140,6 +142,7 @@ describe('Lens App', () => { }, redirectTo: jest.fn(id => {}), } as unknown) as jest.Mocked<{ + navigation: typeof navigationStartMock; editorFrame: EditorFrameInstance; data: typeof dataStartMock; core: typeof core; @@ -338,6 +341,7 @@ describe('Lens App', () => { let defaultArgs: jest.Mocked<{ editorFrame: EditorFrameInstance; + navigation: typeof navigationStartMock; data: typeof dataStartMock; core: typeof core; storage: Storage; @@ -647,6 +651,27 @@ describe('Lens App', () => { }, }); }); + + it('does not show the copy button on first save', async () => { + const args = defaultArgs; + args.editorFrame = frame; + + instance = mount(<App {...args} />); + + const onChange = frame.mount.mock.calls[0][1].onChange; + await act(async () => + onChange({ + filterableIndexPatterns: [], + doc: ({ expression: 'valid expression' } as unknown) as Document, + }) + ); + instance.update(); + + await act(async () => getButton(instance).run(instance.getDOMNode())); + instance.update(); + + expect(instance.find(SavedObjectSaveModal).prop('showCopyOnSave')).toEqual(false); + }); }); }); @@ -654,6 +679,7 @@ describe('Lens App', () => { let defaultArgs: jest.Mocked<{ editorFrame: EditorFrameInstance; data: typeof dataStartMock; + navigation: typeof navigationStartMock; core: typeof core; storage: Storage; docId?: string; diff --git a/x-pack/plugins/lens/public/app_plugin/app.tsx b/x-pack/plugins/lens/public/app_plugin/app.tsx new file mode 100644 index 0000000000000..28135dd12a724 --- /dev/null +++ b/x-pack/plugins/lens/public/app_plugin/app.tsx @@ -0,0 +1,419 @@ +/* + * 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 _ from 'lodash'; +import React, { useState, useEffect, useCallback } from 'react'; +import { I18nProvider } from '@kbn/i18n/react'; +import { i18n } from '@kbn/i18n'; +import { Query, DataPublicPluginStart } from 'src/plugins/data/public'; +import { NavigationPublicPluginStart } from 'src/plugins/navigation/public'; +import { AppMountContext, NotificationsStart } from 'kibana/public'; +import { IStorageWrapper } from 'src/plugins/kibana_utils/public'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { KibanaContextProvider } from '../../../../../src/plugins/kibana_react/public'; +import { SavedObjectSaveModal } from '../../../../../src/plugins/saved_objects/public'; +import { Document, SavedObjectStore } from '../persistence'; +import { EditorFrameInstance } from '../types'; +import { NativeRenderer } from '../native_renderer'; +import { trackUiEvent } from '../lens_ui_telemetry'; +import { + esFilters, + Filter, + IndexPattern as IndexPatternInstance, + IndexPatternsContract, + SavedQuery, +} from '../../../../../src/plugins/data/public'; + +interface State { + isLoading: boolean; + isSaveModalVisible: boolean; + indexPatternsForTopNav: IndexPatternInstance[]; + persistedDoc?: Document; + lastKnownDoc?: Document; + + // Properties needed to interface with TopNav + dateRange: { + fromDate: string; + toDate: string; + }; + query: Query; + filters: Filter[]; + savedQuery?: SavedQuery; +} + +export function App({ + editorFrame, + data, + core, + storage, + docId, + docStorage, + redirectTo, + addToDashboardMode, + navigation, +}: { + editorFrame: EditorFrameInstance; + data: DataPublicPluginStart; + navigation: NavigationPublicPluginStart; + core: AppMountContext['core']; + storage: IStorageWrapper; + docId?: string; + docStorage: SavedObjectStore; + redirectTo: (id?: string) => void; + addToDashboardMode?: boolean; +}) { + const language = + storage.get('kibana.userQueryLanguage') || core.uiSettings.get('search:queryLanguage'); + + const [state, setState] = useState<State>(() => { + const currentRange = data.query.timefilter.timefilter.getTime(); + return { + isLoading: !!docId, + isSaveModalVisible: false, + indexPatternsForTopNav: [], + query: { query: '', language }, + dateRange: { + fromDate: currentRange.from, + toDate: currentRange.to, + }, + filters: [], + }; + }); + + const { lastKnownDoc } = state; + + useEffect(() => { + // Clear app-specific filters when navigating to Lens. Necessary because Lens + // can be loaded without a full page refresh + data.query.filterManager.setAppFilters([]); + + const filterSubscription = data.query.filterManager.getUpdates$().subscribe({ + next: () => { + setState(s => ({ ...s, filters: data.query.filterManager.getFilters() })); + trackUiEvent('app_filters_updated'); + }, + }); + + const timeSubscription = data.query.timefilter.timefilter.getTimeUpdate$().subscribe({ + next: () => { + const currentRange = data.query.timefilter.timefilter.getTime(); + setState(s => ({ + ...s, + dateRange: { + fromDate: currentRange.from, + toDate: currentRange.to, + }, + })); + }, + }); + + return () => { + filterSubscription.unsubscribe(); + timeSubscription.unsubscribe(); + }; + }, []); + + // Sync Kibana breadcrumbs any time the saved document's title changes + useEffect(() => { + core.chrome.setBreadcrumbs([ + { + href: core.http.basePath.prepend(`/app/kibana#/visualize`), + text: i18n.translate('xpack.lens.breadcrumbsTitle', { + defaultMessage: 'Visualize', + }), + }, + { + text: state.persistedDoc + ? state.persistedDoc.title + : i18n.translate('xpack.lens.breadcrumbsCreate', { defaultMessage: 'Create' }), + }, + ]); + }, [state.persistedDoc && state.persistedDoc.title]); + + useEffect(() => { + if (docId && (!state.persistedDoc || state.persistedDoc.id !== docId)) { + setState(s => ({ ...s, isLoading: true })); + docStorage + .load(docId) + .then(doc => { + getAllIndexPatterns( + doc.state.datasourceMetaData.filterableIndexPatterns, + data.indexPatterns, + core.notifications + ) + .then(indexPatterns => { + // Don't overwrite any pinned filters + data.query.filterManager.setAppFilters(doc.state.filters); + setState(s => ({ + ...s, + isLoading: false, + persistedDoc: doc, + lastKnownDoc: doc, + query: doc.state.query, + indexPatternsForTopNav: indexPatterns, + })); + }) + .catch(() => { + setState(s => ({ ...s, isLoading: false })); + + redirectTo(); + }); + }) + .catch(() => { + setState(s => ({ ...s, isLoading: false })); + + core.notifications.toasts.addDanger( + i18n.translate('xpack.lens.app.docLoadingError', { + defaultMessage: 'Error loading saved document', + }) + ); + + redirectTo(); + }); + } + }, [docId]); + + const isSaveable = + lastKnownDoc && + lastKnownDoc.expression && + lastKnownDoc.expression.length > 0 && + core.application.capabilities.visualize.save; + + const onError = useCallback( + (e: { message: string }) => + core.notifications.toasts.addDanger({ + title: e.message, + }), + [] + ); + + const { TopNavMenu } = navigation.ui; + + const confirmButton = addToDashboardMode ? ( + <FormattedMessage + id="xpack.lens.app.saveAddToDashboard" + defaultMessage="Save and add to dashboard" + /> + ) : null; + + return ( + <I18nProvider> + <KibanaContextProvider + services={{ + appName: 'lens', + data, + storage, + ...core, + }} + > + <div className="lnsApp"> + <div className="lnsApp__header"> + <TopNavMenu + config={[ + { + label: i18n.translate('xpack.lens.app.save', { + defaultMessage: 'Save', + }), + run: () => { + if (isSaveable && lastKnownDoc) { + setState(s => ({ ...s, isSaveModalVisible: true })); + } + }, + testId: 'lnsApp_saveButton', + disableButton: !isSaveable, + }, + ]} + data-test-subj="lnsApp_topNav" + screenTitle={'lens'} + onQuerySubmit={payload => { + const { dateRange, query } = payload; + + if ( + dateRange.from !== state.dateRange.fromDate || + dateRange.to !== state.dateRange.toDate + ) { + data.query.timefilter.timefilter.setTime(dateRange); + trackUiEvent('app_date_change'); + } else { + trackUiEvent('app_query_change'); + } + + setState(s => ({ + ...s, + dateRange: { + fromDate: dateRange.from, + toDate: dateRange.to, + }, + query: query || s.query, + })); + }} + appName={'lens'} + indexPatterns={state.indexPatternsForTopNav} + showSearchBar={true} + showDatePicker={true} + showQueryBar={true} + showFilterBar={true} + showSaveQuery={core.application.capabilities.visualize.saveQuery as boolean} + savedQuery={state.savedQuery} + onSaved={savedQuery => { + setState(s => ({ ...s, savedQuery })); + }} + onSavedQueryUpdated={savedQuery => { + const savedQueryFilters = savedQuery.attributes.filters || []; + const globalFilters = data.query.filterManager.getGlobalFilters(); + data.query.filterManager.setFilters([...globalFilters, ...savedQueryFilters]); + setState(s => ({ + ...s, + savedQuery: { ...savedQuery }, // Shallow query for reference issues + dateRange: savedQuery.attributes.timefilter + ? { + fromDate: savedQuery.attributes.timefilter.from, + toDate: savedQuery.attributes.timefilter.to, + } + : s.dateRange, + })); + }} + onClearSavedQuery={() => { + data.query.filterManager.setFilters(data.query.filterManager.getGlobalFilters()); + setState(s => ({ + ...s, + savedQuery: undefined, + filters: data.query.filterManager.getGlobalFilters(), + query: { + query: '', + language: + storage.get('kibana.userQueryLanguage') || + core.uiSettings.get('search:queryLanguage'), + }, + })); + }} + query={state.query} + dateRangeFrom={state.dateRange.fromDate} + dateRangeTo={state.dateRange.toDate} + /> + </div> + + {(!state.isLoading || state.persistedDoc) && ( + <NativeRenderer + className="lnsApp__frame" + render={editorFrame.mount} + nativeProps={{ + dateRange: state.dateRange, + query: state.query, + filters: state.filters, + savedQuery: state.savedQuery, + doc: state.persistedDoc, + onError, + onChange: ({ filterableIndexPatterns, doc }) => { + if (!_.isEqual(state.persistedDoc, doc)) { + setState(s => ({ ...s, lastKnownDoc: doc })); + } + + // Update the cached index patterns if the user made a change to any of them + if ( + state.indexPatternsForTopNav.length !== filterableIndexPatterns.length || + filterableIndexPatterns.find( + ({ id }) => + !state.indexPatternsForTopNav.find(indexPattern => indexPattern.id === id) + ) + ) { + getAllIndexPatterns( + filterableIndexPatterns, + data.indexPatterns, + core.notifications + ).then(indexPatterns => { + if (indexPatterns) { + setState(s => ({ ...s, indexPatternsForTopNav: indexPatterns })); + } + }); + } + }, + }} + /> + )} + </div> + {lastKnownDoc && state.isSaveModalVisible && ( + <SavedObjectSaveModal + onSave={props => { + const [pinnedFilters, appFilters] = _.partition( + lastKnownDoc.state?.filters, + esFilters.isFilterPinned + ); + const lastDocWithoutPinned = pinnedFilters?.length + ? { + ...lastKnownDoc, + state: { + ...lastKnownDoc.state, + filters: appFilters, + }, + } + : lastKnownDoc; + + const doc = { + ...lastDocWithoutPinned, + id: props.newCopyOnSave ? undefined : lastKnownDoc.id, + title: props.newTitle, + }; + + docStorage + .save(doc) + .then(({ id }) => { + // Prevents unnecessary network request and disables save button + const newDoc = { ...doc, id }; + setState(s => ({ + ...s, + isSaveModalVisible: false, + persistedDoc: newDoc, + lastKnownDoc: newDoc, + })); + if (docId !== id) { + redirectTo(id); + } + }) + .catch(e => { + // eslint-disable-next-line no-console + console.dir(e); + trackUiEvent('save_failed'); + core.notifications.toasts.addDanger( + i18n.translate('xpack.lens.app.docSavingError', { + defaultMessage: 'Error saving document', + }) + ); + setState(s => ({ ...s, isSaveModalVisible: false })); + }); + }} + onClose={() => setState(s => ({ ...s, isSaveModalVisible: false }))} + title={lastKnownDoc.title || ''} + showCopyOnSave={!!lastKnownDoc.id && !addToDashboardMode} + objectType={i18n.translate('xpack.lens.app.saveModalType', { + defaultMessage: 'Lens visualization', + })} + showDescription={false} + confirmButtonLabel={confirmButton} + /> + )} + </KibanaContextProvider> + </I18nProvider> + ); +} + +export async function getAllIndexPatterns( + ids: Array<{ id: string }>, + indexPatternsService: IndexPatternsContract, + notifications: NotificationsStart +): Promise<IndexPatternInstance[]> { + try { + return await Promise.all(ids.map(({ id }) => indexPatternsService.get(id))); + } catch (e) { + notifications.toasts.addDanger( + i18n.translate('xpack.lens.app.indexPatternLoadingError', { + defaultMessage: 'Error loading index patterns', + }) + ); + + throw new Error(e); + } +} diff --git a/x-pack/legacy/plugins/lens/public/app_plugin/index.ts b/x-pack/plugins/lens/public/app_plugin/index.ts similarity index 100% rename from x-pack/legacy/plugins/lens/public/app_plugin/index.ts rename to x-pack/plugins/lens/public/app_plugin/index.ts diff --git a/x-pack/legacy/plugins/lens/public/assets/chart_area.svg b/x-pack/plugins/lens/public/assets/chart_area.svg similarity index 100% rename from x-pack/legacy/plugins/lens/public/assets/chart_area.svg rename to x-pack/plugins/lens/public/assets/chart_area.svg diff --git a/x-pack/legacy/plugins/lens/public/assets/chart_area_stacked.svg b/x-pack/plugins/lens/public/assets/chart_area_stacked.svg similarity index 100% rename from x-pack/legacy/plugins/lens/public/assets/chart_area_stacked.svg rename to x-pack/plugins/lens/public/assets/chart_area_stacked.svg diff --git a/x-pack/legacy/plugins/lens/public/assets/chart_bar.svg b/x-pack/plugins/lens/public/assets/chart_bar.svg similarity index 100% rename from x-pack/legacy/plugins/lens/public/assets/chart_bar.svg rename to x-pack/plugins/lens/public/assets/chart_bar.svg diff --git a/x-pack/legacy/plugins/lens/public/assets/chart_bar_horizontal.svg b/x-pack/plugins/lens/public/assets/chart_bar_horizontal.svg similarity index 100% rename from x-pack/legacy/plugins/lens/public/assets/chart_bar_horizontal.svg rename to x-pack/plugins/lens/public/assets/chart_bar_horizontal.svg diff --git a/x-pack/legacy/plugins/lens/public/assets/chart_bar_horizontal_stacked.svg b/x-pack/plugins/lens/public/assets/chart_bar_horizontal_stacked.svg similarity index 100% rename from x-pack/legacy/plugins/lens/public/assets/chart_bar_horizontal_stacked.svg rename to x-pack/plugins/lens/public/assets/chart_bar_horizontal_stacked.svg diff --git a/x-pack/legacy/plugins/lens/public/assets/chart_bar_stacked.svg b/x-pack/plugins/lens/public/assets/chart_bar_stacked.svg similarity index 100% rename from x-pack/legacy/plugins/lens/public/assets/chart_bar_stacked.svg rename to x-pack/plugins/lens/public/assets/chart_bar_stacked.svg diff --git a/x-pack/legacy/plugins/lens/public/assets/chart_datatable.svg b/x-pack/plugins/lens/public/assets/chart_datatable.svg similarity index 100% rename from x-pack/legacy/plugins/lens/public/assets/chart_datatable.svg rename to x-pack/plugins/lens/public/assets/chart_datatable.svg diff --git a/x-pack/legacy/plugins/lens/public/assets/chart_line.svg b/x-pack/plugins/lens/public/assets/chart_line.svg similarity index 100% rename from x-pack/legacy/plugins/lens/public/assets/chart_line.svg rename to x-pack/plugins/lens/public/assets/chart_line.svg diff --git a/x-pack/legacy/plugins/lens/public/assets/chart_metric.svg b/x-pack/plugins/lens/public/assets/chart_metric.svg similarity index 100% rename from x-pack/legacy/plugins/lens/public/assets/chart_metric.svg rename to x-pack/plugins/lens/public/assets/chart_metric.svg diff --git a/x-pack/legacy/plugins/lens/public/assets/chart_mixed_xy.svg b/x-pack/plugins/lens/public/assets/chart_mixed_xy.svg similarity index 100% rename from x-pack/legacy/plugins/lens/public/assets/chart_mixed_xy.svg rename to x-pack/plugins/lens/public/assets/chart_mixed_xy.svg diff --git a/x-pack/legacy/plugins/lens/public/assets/lens_app_graphic_dark_2x.png b/x-pack/plugins/lens/public/assets/lens_app_graphic_dark_2x.png similarity index 100% rename from x-pack/legacy/plugins/lens/public/assets/lens_app_graphic_dark_2x.png rename to x-pack/plugins/lens/public/assets/lens_app_graphic_dark_2x.png diff --git a/x-pack/legacy/plugins/lens/public/assets/lens_app_graphic_light_2x.png b/x-pack/plugins/lens/public/assets/lens_app_graphic_light_2x.png similarity index 100% rename from x-pack/legacy/plugins/lens/public/assets/lens_app_graphic_light_2x.png rename to x-pack/plugins/lens/public/assets/lens_app_graphic_light_2x.png diff --git a/x-pack/plugins/lens/public/datatable_visualization/_index.scss b/x-pack/plugins/lens/public/datatable_visualization/_index.scss new file mode 100644 index 0000000000000..532e8106b023f --- /dev/null +++ b/x-pack/plugins/lens/public/datatable_visualization/_index.scss @@ -0,0 +1 @@ +@import 'visualization'; diff --git a/x-pack/legacy/plugins/lens/public/datatable_visualization/_visualization.scss b/x-pack/plugins/lens/public/datatable_visualization/_visualization.scss similarity index 100% rename from x-pack/legacy/plugins/lens/public/datatable_visualization/_visualization.scss rename to x-pack/plugins/lens/public/datatable_visualization/_visualization.scss diff --git a/x-pack/legacy/plugins/lens/public/datatable_visualization/expression.tsx b/x-pack/plugins/lens/public/datatable_visualization/expression.tsx similarity index 98% rename from x-pack/legacy/plugins/lens/public/datatable_visualization/expression.tsx rename to x-pack/plugins/lens/public/datatable_visualization/expression.tsx index 050f294d0b964..772ee13168d02 100644 --- a/x-pack/legacy/plugins/lens/public/datatable_visualization/expression.tsx +++ b/x-pack/plugins/lens/public/datatable_visualization/expression.tsx @@ -13,7 +13,7 @@ import { ExpressionFunctionDefinition, ExpressionRenderDefinition, IInterpreterRenderHandlers, -} from '../../../../../../src/plugins/expressions/public'; +} from '../../../../../src/plugins/expressions/public'; import { VisualizationContainer } from '../visualization_container'; export interface DatatableColumns { @@ -140,6 +140,7 @@ function DatatableComponent(props: DatatableProps & { formatFactory: FormatFacto <EuiBasicTable className="lnsDataTable" data-test-subj="lnsDataTable" + tableLayout="auto" columns={props.args.columns.columnIds .map(field => { const col = firstTable.columns.find(c => c.id === field); diff --git a/x-pack/plugins/lens/public/datatable_visualization/index.ts b/x-pack/plugins/lens/public/datatable_visualization/index.ts new file mode 100644 index 0000000000000..ff036aadfd4cf --- /dev/null +++ b/x-pack/plugins/lens/public/datatable_visualization/index.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 { CoreSetup } from 'kibana/public'; +import { datatableVisualization } from './visualization'; +import { ExpressionsSetup } from '../../../../../src/plugins/expressions/public'; +import { datatable, datatableColumns, getDatatableRenderer } from './expression'; +import { EditorFrameSetup, FormatFactory } from '../types'; + +export interface DatatableVisualizationPluginSetupPlugins { + expressions: ExpressionsSetup; + formatFactory: Promise<FormatFactory>; + editorFrame: EditorFrameSetup; +} + +export class DatatableVisualization { + constructor() {} + + setup( + _core: CoreSetup | null, + { expressions, formatFactory, editorFrame }: DatatableVisualizationPluginSetupPlugins + ) { + expressions.registerFunction(() => datatableColumns); + expressions.registerFunction(() => datatable); + expressions.registerRenderer(() => getDatatableRenderer(formatFactory)); + editorFrame.registerVisualization(datatableVisualization); + } +} diff --git a/x-pack/legacy/plugins/lens/public/datatable_visualization/visualization.test.tsx b/x-pack/plugins/lens/public/datatable_visualization/visualization.test.tsx similarity index 100% rename from x-pack/legacy/plugins/lens/public/datatable_visualization/visualization.test.tsx rename to x-pack/plugins/lens/public/datatable_visualization/visualization.test.tsx diff --git a/x-pack/legacy/plugins/lens/public/datatable_visualization/visualization.tsx b/x-pack/plugins/lens/public/datatable_visualization/visualization.tsx similarity index 100% rename from x-pack/legacy/plugins/lens/public/datatable_visualization/visualization.tsx rename to x-pack/plugins/lens/public/datatable_visualization/visualization.tsx diff --git a/x-pack/legacy/plugins/lens/public/debounced_component/debounced_component.test.tsx b/x-pack/plugins/lens/public/debounced_component/debounced_component.test.tsx similarity index 100% rename from x-pack/legacy/plugins/lens/public/debounced_component/debounced_component.test.tsx rename to x-pack/plugins/lens/public/debounced_component/debounced_component.test.tsx diff --git a/x-pack/legacy/plugins/lens/public/debounced_component/debounced_component.tsx b/x-pack/plugins/lens/public/debounced_component/debounced_component.tsx similarity index 100% rename from x-pack/legacy/plugins/lens/public/debounced_component/debounced_component.tsx rename to x-pack/plugins/lens/public/debounced_component/debounced_component.tsx diff --git a/x-pack/legacy/plugins/lens/public/debounced_component/index.ts b/x-pack/plugins/lens/public/debounced_component/index.ts similarity index 100% rename from x-pack/legacy/plugins/lens/public/debounced_component/index.ts rename to x-pack/plugins/lens/public/debounced_component/index.ts diff --git a/x-pack/legacy/plugins/lens/public/drag_drop/__snapshots__/drag_drop.test.tsx.snap b/x-pack/plugins/lens/public/drag_drop/__snapshots__/drag_drop.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/lens/public/drag_drop/__snapshots__/drag_drop.test.tsx.snap rename to x-pack/plugins/lens/public/drag_drop/__snapshots__/drag_drop.test.tsx.snap diff --git a/x-pack/legacy/plugins/lens/public/drag_drop/_drag_drop.scss b/x-pack/plugins/lens/public/drag_drop/_drag_drop.scss similarity index 100% rename from x-pack/legacy/plugins/lens/public/drag_drop/_drag_drop.scss rename to x-pack/plugins/lens/public/drag_drop/_drag_drop.scss diff --git a/x-pack/plugins/lens/public/drag_drop/_index.scss b/x-pack/plugins/lens/public/drag_drop/_index.scss new file mode 100644 index 0000000000000..ddf9b9aa3e429 --- /dev/null +++ b/x-pack/plugins/lens/public/drag_drop/_index.scss @@ -0,0 +1 @@ +@import 'drag_drop'; diff --git a/x-pack/legacy/plugins/lens/public/drag_drop/drag_drop.test.tsx b/x-pack/plugins/lens/public/drag_drop/drag_drop.test.tsx similarity index 100% rename from x-pack/legacy/plugins/lens/public/drag_drop/drag_drop.test.tsx rename to x-pack/plugins/lens/public/drag_drop/drag_drop.test.tsx diff --git a/x-pack/legacy/plugins/lens/public/drag_drop/drag_drop.tsx b/x-pack/plugins/lens/public/drag_drop/drag_drop.tsx similarity index 100% rename from x-pack/legacy/plugins/lens/public/drag_drop/drag_drop.tsx rename to x-pack/plugins/lens/public/drag_drop/drag_drop.tsx diff --git a/x-pack/legacy/plugins/lens/public/drag_drop/index.ts b/x-pack/plugins/lens/public/drag_drop/index.ts similarity index 100% rename from x-pack/legacy/plugins/lens/public/drag_drop/index.ts rename to x-pack/plugins/lens/public/drag_drop/index.ts diff --git a/x-pack/legacy/plugins/lens/public/drag_drop/providers.test.tsx b/x-pack/plugins/lens/public/drag_drop/providers.test.tsx similarity index 100% rename from x-pack/legacy/plugins/lens/public/drag_drop/providers.test.tsx rename to x-pack/plugins/lens/public/drag_drop/providers.test.tsx diff --git a/x-pack/legacy/plugins/lens/public/drag_drop/providers.tsx b/x-pack/plugins/lens/public/drag_drop/providers.tsx similarity index 100% rename from x-pack/legacy/plugins/lens/public/drag_drop/providers.tsx rename to x-pack/plugins/lens/public/drag_drop/providers.tsx diff --git a/x-pack/legacy/plugins/lens/public/drag_drop/readme.md b/x-pack/plugins/lens/public/drag_drop/readme.md similarity index 100% rename from x-pack/legacy/plugins/lens/public/drag_drop/readme.md rename to x-pack/plugins/lens/public/drag_drop/readme.md diff --git a/x-pack/plugins/lens/public/editor_frame_service/_index.scss b/x-pack/plugins/lens/public/editor_frame_service/_index.scss new file mode 100644 index 0000000000000..199cbe35e25fa --- /dev/null +++ b/x-pack/plugins/lens/public/editor_frame_service/_index.scss @@ -0,0 +1 @@ +@import 'editor_frame/index'; diff --git a/x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/__mocks__/suggestion_helpers.ts b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/__mocks__/suggestion_helpers.ts similarity index 100% rename from x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/__mocks__/suggestion_helpers.ts rename to x-pack/plugins/lens/public/editor_frame_service/editor_frame/__mocks__/suggestion_helpers.ts diff --git a/x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/_chart_switch.scss b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/_chart_switch.scss similarity index 100% rename from x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/_chart_switch.scss rename to x-pack/plugins/lens/public/editor_frame_service/editor_frame/_chart_switch.scss diff --git a/x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/_config_panel_wrapper.scss b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/_config_panel_wrapper.scss similarity index 100% rename from x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/_config_panel_wrapper.scss rename to x-pack/plugins/lens/public/editor_frame_service/editor_frame/_config_panel_wrapper.scss diff --git a/x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/_data_panel_wrapper.scss b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/_data_panel_wrapper.scss similarity index 100% rename from x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/_data_panel_wrapper.scss rename to x-pack/plugins/lens/public/editor_frame_service/editor_frame/_data_panel_wrapper.scss diff --git a/x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/_expression_renderer.scss b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/_expression_renderer.scss similarity index 84% rename from x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/_expression_renderer.scss rename to x-pack/plugins/lens/public/editor_frame_service/editor_frame/_expression_renderer.scss index d43a857e05639..9519544ece575 100644 --- a/x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/_expression_renderer.scss +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/_expression_renderer.scss @@ -1,9 +1,10 @@ .lnsExpressionRenderer { + @include euiScrollBar; position: relative; width: 100%; height: 100%; display: flex; - overflow-x: hidden; + overflow: auto; .lnsExpressionRenderer__component { position: static; // Let the progress indicator position itself against the outer parent diff --git a/x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/_frame_layout.scss b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/_frame_layout.scss similarity index 100% rename from x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/_frame_layout.scss rename to x-pack/plugins/lens/public/editor_frame_service/editor_frame/_frame_layout.scss diff --git a/x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/_suggestion_panel.scss b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/_suggestion_panel.scss similarity index 100% rename from x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/_suggestion_panel.scss rename to x-pack/plugins/lens/public/editor_frame_service/editor_frame/_suggestion_panel.scss diff --git a/x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/_workspace_panel_wrapper.scss b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/_workspace_panel_wrapper.scss similarity index 96% rename from x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/_workspace_panel_wrapper.scss rename to x-pack/plugins/lens/public/editor_frame_service/editor_frame/_workspace_panel_wrapper.scss index 3ef387eca1e8b..4ba19cb4ab05b 100644 --- a/x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/_workspace_panel_wrapper.scss +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/_workspace_panel_wrapper.scss @@ -29,7 +29,7 @@ display: flex; align-items: center; justify-content: center; - overflow-x: hidden; + overflow: hidden; } } } diff --git a/x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/chart_switch.test.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/chart_switch.test.tsx similarity index 100% rename from x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/chart_switch.test.tsx rename to x-pack/plugins/lens/public/editor_frame_service/editor_frame/chart_switch.test.tsx diff --git a/x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/chart_switch.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/chart_switch.tsx similarity index 100% rename from x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/chart_switch.tsx rename to x-pack/plugins/lens/public/editor_frame_service/editor_frame/chart_switch.tsx diff --git a/x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/config_panel_wrapper.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel_wrapper.tsx similarity index 100% rename from x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/config_panel_wrapper.tsx rename to x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel_wrapper.tsx diff --git a/x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/data_panel_wrapper.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/data_panel_wrapper.tsx similarity index 94% rename from x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/data_panel_wrapper.tsx rename to x-pack/plugins/lens/public/editor_frame_service/editor_frame/data_panel_wrapper.tsx index 2a9499077f3c1..cd0aee732793e 100644 --- a/x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/data_panel_wrapper.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/data_panel_wrapper.tsx @@ -7,12 +7,11 @@ import React, { useMemo, memo, useContext, useState } from 'react'; import { i18n } from '@kbn/i18n'; import { EuiPopover, EuiButtonIcon, EuiContextMenuPanel, EuiContextMenuItem } from '@elastic/eui'; -import { DatasourceDataPanelProps, Datasource } from '../../../public'; import { NativeRenderer } from '../../native_renderer'; import { Action } from './state_management'; import { DragContext } from '../../drag_drop'; -import { StateSetter, FramePublicAPI } from '../../types'; -import { Query, Filter } from '../../../../../../../src/plugins/data/public'; +import { StateSetter, FramePublicAPI, DatasourceDataPanelProps, Datasource } from '../../types'; +import { Query, Filter } from '../../../../../../src/plugins/data/public'; interface DataPanelWrapperProps { datasourceState: unknown; diff --git a/x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/editor_frame.test.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/editor_frame.test.tsx similarity index 100% rename from x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/editor_frame.test.tsx rename to x-pack/plugins/lens/public/editor_frame_service/editor_frame/editor_frame.test.tsx diff --git a/x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/editor_frame.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/editor_frame.tsx similarity index 97% rename from x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/editor_frame.tsx rename to x-pack/plugins/lens/public/editor_frame_service/editor_frame/editor_frame.tsx index 082519d9a8feb..b72d9081bbc91 100644 --- a/x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/editor_frame.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/editor_frame.tsx @@ -5,8 +5,8 @@ */ import React, { useEffect, useReducer } from 'react'; -import { CoreSetup, CoreStart } from 'src/core/public'; -import { ReactExpressionRendererType } from '../../../../../../../src/plugins/expressions/public'; +import { CoreSetup, CoreStart } from 'kibana/public'; +import { ReactExpressionRendererType } from '../../../../../../src/plugins/expressions/public'; import { Datasource, DatasourcePublicAPI, @@ -25,7 +25,7 @@ import { RootDragDropProvider } from '../../drag_drop'; import { getSavedObjectFormat } from './save'; import { WorkspacePanelWrapper } from './workspace_panel_wrapper'; import { generateId } from '../../id_generator'; -import { Filter, Query, SavedQuery } from '../../../../../../../src/plugins/data/public'; +import { Filter, Query, SavedQuery } from '../../../../../../src/plugins/data/public'; export interface EditorFrameProps { doc?: Document; diff --git a/x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/expression_helpers.ts b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/expression_helpers.ts similarity index 97% rename from x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/expression_helpers.ts rename to x-pack/plugins/lens/public/editor_frame_service/editor_frame/expression_helpers.ts index d264e6d0da3ad..9d4f8587577a3 100644 --- a/x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/expression_helpers.ts +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/expression_helpers.ts @@ -6,7 +6,7 @@ import { Ast, fromExpression, ExpressionFunctionAST } from '@kbn/interpreter/common'; import { Visualization, Datasource, FramePublicAPI } from '../../types'; -import { Filter, TimeRange, Query } from '../../../../../../../src/plugins/data/public'; +import { Filter, TimeRange, Query } from '../../../../../../src/plugins/data/public'; export function prependDatasourceExpression( visualizationExpression: Ast | string | null, diff --git a/x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/frame_layout.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/frame_layout.tsx similarity index 100% rename from x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/frame_layout.tsx rename to x-pack/plugins/lens/public/editor_frame_service/editor_frame/frame_layout.tsx diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/index.scss b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/index.scss new file mode 100644 index 0000000000000..d4b27c6c98b3c --- /dev/null +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/index.scss @@ -0,0 +1,8 @@ +@import 'chart_switch'; +@import 'config_panel_wrapper'; +@import 'data_panel_wrapper'; +@import 'expression_renderer'; +@import 'frame_layout'; +@import 'suggestion_panel'; +@import 'workspace_panel_wrapper'; + diff --git a/x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/index.ts b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/index.ts similarity index 100% rename from x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/index.ts rename to x-pack/plugins/lens/public/editor_frame_service/editor_frame/index.ts diff --git a/x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/layer_actions.test.ts b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/layer_actions.test.ts similarity index 100% rename from x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/layer_actions.test.ts rename to x-pack/plugins/lens/public/editor_frame_service/editor_frame/layer_actions.test.ts diff --git a/x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/layer_actions.ts b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/layer_actions.ts similarity index 100% rename from x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/layer_actions.ts rename to x-pack/plugins/lens/public/editor_frame_service/editor_frame/layer_actions.ts diff --git a/x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/save.test.ts b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/save.test.ts similarity index 98% rename from x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/save.test.ts rename to x-pack/plugins/lens/public/editor_frame_service/editor_frame/save.test.ts index 60bfbc493f61c..9c7ed265f3539 100644 --- a/x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/save.test.ts +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/save.test.ts @@ -6,7 +6,7 @@ import { getSavedObjectFormat, Props } from './save'; import { createMockDatasource, createMockVisualization } from '../mocks'; -import { esFilters, IIndexPattern, IFieldType } from '../../../../../../../src/plugins/data/public'; +import { esFilters, IIndexPattern, IFieldType } from '../../../../../../src/plugins/data/public'; describe('save editor frame state', () => { const mockVisualization = createMockVisualization(); diff --git a/x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/save.ts b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/save.ts similarity index 100% rename from x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/save.ts rename to x-pack/plugins/lens/public/editor_frame_service/editor_frame/save.ts diff --git a/x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/state_management.test.ts b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/state_management.test.ts similarity index 99% rename from x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/state_management.test.ts rename to x-pack/plugins/lens/public/editor_frame_service/editor_frame/state_management.test.ts index 4aaf2a3ee9e81..1f62929783b63 100644 --- a/x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/state_management.test.ts +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/state_management.test.ts @@ -5,7 +5,7 @@ */ import { getInitialState, reducer } from './state_management'; -import { EditorFrameProps } from '.'; +import { EditorFrameProps } from './index'; import { Datasource, Visualization } from '../../types'; import { createExpressionRendererMock } from '../mocks'; import { coreMock } from 'src/core/public/mocks'; diff --git a/x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/state_management.ts b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/state_management.ts similarity index 99% rename from x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/state_management.ts rename to x-pack/plugins/lens/public/editor_frame_service/editor_frame/state_management.ts index 7d763bcac2cc9..bb6daf5641a64 100644 --- a/x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/state_management.ts +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/state_management.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { EditorFrameProps } from '../editor_frame'; +import { EditorFrameProps } from './index'; import { Document } from '../../persistence/saved_object_store'; export interface PreviewState { diff --git a/x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/suggestion_helpers.test.ts b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/suggestion_helpers.test.ts similarity index 100% rename from x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/suggestion_helpers.test.ts rename to x-pack/plugins/lens/public/editor_frame_service/editor_frame/suggestion_helpers.test.ts diff --git a/x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/suggestion_helpers.ts b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/suggestion_helpers.ts similarity index 100% rename from x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/suggestion_helpers.ts rename to x-pack/plugins/lens/public/editor_frame_service/editor_frame/suggestion_helpers.ts diff --git a/x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/suggestion_panel.test.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/suggestion_panel.test.tsx similarity index 92% rename from x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/suggestion_panel.test.tsx rename to x-pack/plugins/lens/public/editor_frame_service/editor_frame/suggestion_panel.test.tsx index b146f2467c46c..240bdff40b51c 100644 --- a/x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/suggestion_panel.test.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/suggestion_panel.test.tsx @@ -15,7 +15,8 @@ import { createMockFramePublicAPI, } from '../mocks'; import { act } from 'react-dom/test-utils'; -import { ReactExpressionRendererType } from '../../../../../../../src/plugins/expressions/public'; +import { ReactExpressionRendererType } from '../../../../../../src/plugins/expressions/public'; +import { esFilters, IFieldType, IIndexPattern } from '../../../../../../src/plugins/data/public'; import { SuggestionPanel, SuggestionPanelProps } from './suggestion_panel'; import { getSuggestions, Suggestion } from './suggestion_helpers'; import { EuiIcon, EuiPanel, EuiToolTip } from '@elastic/eui'; @@ -243,14 +244,25 @@ describe('suggestion_panel', () => { (mockVisualization.toPreviewExpression as jest.Mock).mockReturnValueOnce('test | expression'); mockDatasource.toExpression.mockReturnValue('datasource_expression'); - mount(<SuggestionPanel {...defaultProps} />); + const indexPattern = ({ id: 'index1' } as unknown) as IIndexPattern; + const field = ({ name: 'myfield' } as unknown) as IFieldType; + + mount( + <SuggestionPanel + {...defaultProps} + frame={{ + ...createMockFramePublicAPI(), + filters: [esFilters.buildExistsFilter(field, indexPattern)], + }} + /> + ); expect(expressionRendererMock).toHaveBeenCalledTimes(1); const passedExpression = (expressionRendererMock as jest.Mock).mock.calls[0][0].expression; expect(passedExpression).toMatchInlineSnapshot(` "kibana - | kibana_context timeRange=\\"{\\\\\\"from\\\\\\":\\\\\\"now-7d\\\\\\",\\\\\\"to\\\\\\":\\\\\\"now\\\\\\"}\\" query=\\"{\\\\\\"query\\\\\\":\\\\\\"\\\\\\",\\\\\\"language\\\\\\":\\\\\\"lucene\\\\\\"}\\" filters=\\"[]\\" + | kibana_context timeRange=\\"{\\\\\\"from\\\\\\":\\\\\\"now-7d\\\\\\",\\\\\\"to\\\\\\":\\\\\\"now\\\\\\"}\\" query=\\"{\\\\\\"query\\\\\\":\\\\\\"\\\\\\",\\\\\\"language\\\\\\":\\\\\\"lucene\\\\\\"}\\" filters=\\"[{\\\\\\"meta\\\\\\":{\\\\\\"index\\\\\\":\\\\\\"index1\\\\\\"},\\\\\\"exists\\\\\\":{\\\\\\"field\\\\\\":\\\\\\"myfield\\\\\\"}}]\\" | lens_merge_tables layerIds=\\"first\\" tables={datasource_expression} | test | expression" diff --git a/x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/suggestion_panel.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/suggestion_panel.tsx similarity index 99% rename from x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/suggestion_panel.tsx rename to x-pack/plugins/lens/public/editor_frame_service/editor_frame/suggestion_panel.tsx index 93f6ea6ea67ac..867214d15578a 100644 --- a/x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/suggestion_panel.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/suggestion_panel.tsx @@ -24,7 +24,7 @@ import classNames from 'classnames'; import { Action, PreviewState } from './state_management'; import { Datasource, Visualization, FramePublicAPI, DatasourcePublicAPI } from '../../types'; import { getSuggestions, switchToSuggestion } from './suggestion_helpers'; -import { ReactExpressionRendererType } from '../../../../../../../src/plugins/expressions/public'; +import { ReactExpressionRendererType } from '../../../../../../src/plugins/expressions/public'; import { prependDatasourceExpression, prependKibanaContext } from './expression_helpers'; import { debouncedComponent } from '../../debounced_component'; import { trackUiEvent, trackSuggestionEvent } from '../../lens_ui_telemetry'; @@ -235,6 +235,7 @@ export function SuggestionPanel({ const expressionContext = { query: frame.query, + filters: frame.filters, timeRange: { from: frame.dateRange.fromDate, to: frame.dateRange.toDate, diff --git a/x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel.test.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel.test.tsx similarity index 99% rename from x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel.test.tsx rename to x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel.test.tsx index 748e5b876da95..33ecee53fa3bc 100644 --- a/x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel.test.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel.test.tsx @@ -6,7 +6,7 @@ import React from 'react'; import { act } from 'react-dom/test-utils'; -import { ReactExpressionRendererProps } from '../../../../../../../src/plugins/expressions/public'; +import { ReactExpressionRendererProps } from '../../../../../../src/plugins/expressions/public'; import { FramePublicAPI, TableSuggestion, Visualization } from '../../types'; import { createMockVisualization, @@ -21,7 +21,7 @@ import { ReactWrapper } from 'enzyme'; import { DragDrop, ChildDragDropProvider } from '../../drag_drop'; import { Ast } from '@kbn/interpreter/common'; import { coreMock } from 'src/core/public/mocks'; -import { esFilters, IFieldType, IIndexPattern } from '../../../../../../../src/plugins/data/public'; +import { esFilters, IFieldType, IIndexPattern } from '../../../../../../src/plugins/data/public'; describe('workspace_panel', () => { let mockVisualization: jest.Mocked<Visualization>; diff --git a/x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel.tsx similarity index 98% rename from x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel.tsx rename to x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel.tsx index c2a5c16e405a2..1f741ca37934f 100644 --- a/x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel.tsx @@ -16,8 +16,8 @@ import { EuiBetaBadge, EuiButtonEmpty, } from '@elastic/eui'; -import { CoreStart, CoreSetup } from 'src/core/public'; -import { ReactExpressionRendererType } from '../../../../../../../src/plugins/expressions/public'; +import { CoreStart, CoreSetup } from 'kibana/public'; +import { ReactExpressionRendererType } from '../../../../../../src/plugins/expressions/public'; import { Action } from './state_management'; import { Datasource, Visualization, FramePublicAPI } from '../../types'; import { DragDrop, DragContext } from '../../drag_drop'; diff --git a/x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel_wrapper.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel_wrapper.tsx similarity index 100% rename from x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel_wrapper.tsx rename to x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel_wrapper.tsx diff --git a/x-pack/legacy/plugins/lens/public/editor_frame_service/embeddable/embeddable.test.tsx b/x-pack/plugins/lens/public/editor_frame_service/embeddable/embeddable.test.tsx similarity index 96% rename from x-pack/legacy/plugins/lens/public/editor_frame_service/embeddable/embeddable.test.tsx rename to x-pack/plugins/lens/public/editor_frame_service/embeddable/embeddable.test.tsx index 55363ebe4d8f3..aeae64514b0fd 100644 --- a/x-pack/legacy/plugins/lens/public/editor_frame_service/embeddable/embeddable.test.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/embeddable/embeddable.test.tsx @@ -9,9 +9,9 @@ import { Embeddable } from './embeddable'; import { ReactExpressionRendererProps } from 'src/plugins/expressions/public'; import { Query, TimeRange, Filter, TimefilterContract } from 'src/plugins/data/public'; import { Document } from '../../persistence'; -import { dataPluginMock } from '../../../../../../../src/plugins/data/public/mocks'; +import { dataPluginMock } from '../../../../../../src/plugins/data/public/mocks'; -jest.mock('../../../../../../../src/plugins/inspector/public/', () => ({ +jest.mock('../../../../../../src/plugins/inspector/public/', () => ({ isAvailable: false, open: false, })); diff --git a/x-pack/legacy/plugins/lens/public/editor_frame_service/embeddable/embeddable.tsx b/x-pack/plugins/lens/public/editor_frame_service/embeddable/embeddable.tsx similarity index 94% rename from x-pack/legacy/plugins/lens/public/editor_frame_service/embeddable/embeddable.tsx rename to x-pack/plugins/lens/public/editor_frame_service/embeddable/embeddable.tsx index c2ab1c72af545..0ef5f6d1a5470 100644 --- a/x-pack/legacy/plugins/lens/public/editor_frame_service/embeddable/embeddable.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/embeddable/embeddable.tsx @@ -16,15 +16,15 @@ import { } from 'src/plugins/data/public'; import { Subscription } from 'rxjs'; -import { ReactExpressionRendererType } from '../../../../../../../src/plugins/expressions/public'; -import { VIS_EVENT_TO_TRIGGER } from '../../../../../../../src/plugins/visualizations/public'; +import { ReactExpressionRendererType } from '../../../../../../src/plugins/expressions/public'; +import { VIS_EVENT_TO_TRIGGER } from '../../../../../../src/plugins/visualizations/public'; import { Embeddable as AbstractEmbeddable, EmbeddableOutput, IContainer, EmbeddableInput, -} from '../../../../../../../src/plugins/embeddable/public'; +} from '../../../../../../src/plugins/embeddable/public'; import { Document, DOC_TYPE } from '../../persistence'; import { ExpressionWrapper } from './expression_wrapper'; diff --git a/x-pack/legacy/plugins/lens/public/editor_frame_service/embeddable/embeddable_factory.ts b/x-pack/plugins/lens/public/editor_frame_service/embeddable/embeddable_factory.ts similarity index 91% rename from x-pack/legacy/plugins/lens/public/editor_frame_service/embeddable/embeddable_factory.ts rename to x-pack/plugins/lens/public/editor_frame_service/embeddable/embeddable_factory.ts index 99a59c756e228..68dbff263f60d 100644 --- a/x-pack/legacy/plugins/lens/public/editor_frame_service/embeddable/embeddable_factory.ts +++ b/x-pack/plugins/lens/public/editor_frame_service/embeddable/embeddable_factory.ts @@ -15,17 +15,17 @@ import { IndexPatternsContract, IndexPattern, TimefilterContract, -} from '../../../../../../../src/plugins/data/public'; -import { ReactExpressionRendererType } from '../../../../../../../src/plugins/expressions/public'; +} from '../../../../../../src/plugins/data/public'; +import { ReactExpressionRendererType } from '../../../../../../src/plugins/expressions/public'; import { EmbeddableFactoryDefinition, ErrorEmbeddable, EmbeddableInput, IContainer, -} from '../../../../../../../src/plugins/embeddable/public'; +} from '../../../../../../src/plugins/embeddable/public'; import { Embeddable } from './embeddable'; import { SavedObjectIndexStore, DOC_TYPE } from '../../persistence'; -import { getEditPath } from '../../../../../../plugins/lens/common'; +import { getEditPath } from '../../../common'; interface StartServices { timefilter: TimefilterContract; diff --git a/x-pack/legacy/plugins/lens/public/editor_frame_service/embeddable/expression_wrapper.tsx b/x-pack/plugins/lens/public/editor_frame_service/embeddable/expression_wrapper.tsx similarity index 100% rename from x-pack/legacy/plugins/lens/public/editor_frame_service/embeddable/expression_wrapper.tsx rename to x-pack/plugins/lens/public/editor_frame_service/embeddable/expression_wrapper.tsx diff --git a/x-pack/legacy/plugins/lens/public/editor_frame_service/format_column.ts b/x-pack/plugins/lens/public/editor_frame_service/format_column.ts similarity index 100% rename from x-pack/legacy/plugins/lens/public/editor_frame_service/format_column.ts rename to x-pack/plugins/lens/public/editor_frame_service/format_column.ts diff --git a/x-pack/legacy/plugins/lens/public/editor_frame_service/index.ts b/x-pack/plugins/lens/public/editor_frame_service/index.ts similarity index 100% rename from x-pack/legacy/plugins/lens/public/editor_frame_service/index.ts rename to x-pack/plugins/lens/public/editor_frame_service/index.ts diff --git a/x-pack/legacy/plugins/lens/public/editor_frame_service/merge_tables.test.ts b/x-pack/plugins/lens/public/editor_frame_service/merge_tables.test.ts similarity index 98% rename from x-pack/legacy/plugins/lens/public/editor_frame_service/merge_tables.test.ts rename to x-pack/plugins/lens/public/editor_frame_service/merge_tables.test.ts index 9368674de31c5..243441f2c8ab3 100644 --- a/x-pack/legacy/plugins/lens/public/editor_frame_service/merge_tables.test.ts +++ b/x-pack/plugins/lens/public/editor_frame_service/merge_tables.test.ts @@ -8,8 +8,6 @@ import moment from 'moment'; import { mergeTables } from './merge_tables'; import { KibanaDatatable } from 'src/plugins/expressions'; -jest.mock('ui/new_platform'); - describe('lens_merge_tables', () => { it('should produce a row with the nested table as defined', () => { const sampleTable1: KibanaDatatable = { diff --git a/x-pack/legacy/plugins/lens/public/editor_frame_service/merge_tables.ts b/x-pack/plugins/lens/public/editor_frame_service/merge_tables.ts similarity index 96% rename from x-pack/legacy/plugins/lens/public/editor_frame_service/merge_tables.ts rename to x-pack/plugins/lens/public/editor_frame_service/merge_tables.ts index c06640fb25de6..7c10ee4a57fad 100644 --- a/x-pack/legacy/plugins/lens/public/editor_frame_service/merge_tables.ts +++ b/x-pack/plugins/lens/public/editor_frame_service/merge_tables.ts @@ -10,7 +10,7 @@ import { ExpressionValueSearchContext, KibanaDatatable, } from 'src/plugins/expressions/public'; -import { search } from '../../../../../../src/plugins/data/public'; +import { search } from '../../../../../src/plugins/data/public'; const { toAbsoluteDates } = search.aggs; import { LensMultiTable } from '../types'; diff --git a/x-pack/legacy/plugins/lens/public/editor_frame_service/mocks.tsx b/x-pack/plugins/lens/public/editor_frame_service/mocks.tsx similarity index 92% rename from x-pack/legacy/plugins/lens/public/editor_frame_service/mocks.tsx rename to x-pack/plugins/lens/public/editor_frame_service/mocks.tsx index 5d2f68a5567eb..50cd1ad8bd53a 100644 --- a/x-pack/legacy/plugins/lens/public/editor_frame_service/mocks.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/mocks.tsx @@ -9,12 +9,12 @@ import { ReactExpressionRendererProps, ExpressionsSetup, ExpressionsStart, -} from '../../../../../../src/plugins/expressions/public'; -import { embeddablePluginMock } from '../../../../../../src/plugins/embeddable/public/mocks'; -import { expressionsPluginMock } from '../../../../../../src/plugins/expressions/public/mocks'; +} from '../../../../../src/plugins/expressions/public'; +import { embeddablePluginMock } from '../../../../../src/plugins/embeddable/public/mocks'; +import { expressionsPluginMock } from '../../../../../src/plugins/expressions/public/mocks'; import { DatasourcePublicAPI, FramePublicAPI, Datasource, Visualization } from '../types'; import { EditorFrameSetupPlugins, EditorFrameStartPlugins } from './service'; -import { dataPluginMock } from '../../../../../../src/plugins/data/public/mocks'; +import { dataPluginMock } from '../../../../../src/plugins/data/public/mocks'; export function createMockVisualization(): jest.Mocked<Visualization> { return { diff --git a/x-pack/legacy/plugins/lens/public/editor_frame_service/service.test.tsx b/x-pack/plugins/lens/public/editor_frame_service/service.test.tsx similarity index 98% rename from x-pack/legacy/plugins/lens/public/editor_frame_service/service.test.tsx rename to x-pack/plugins/lens/public/editor_frame_service/service.test.tsx index 42a1fcc055a1e..fbd65c5044d51 100644 --- a/x-pack/legacy/plugins/lens/public/editor_frame_service/service.test.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/service.test.tsx @@ -14,8 +14,6 @@ import { } from './mocks'; import { CoreSetup } from 'kibana/public'; -jest.mock('ui/new_platform'); - // mock away actual dependencies to prevent all of it being loaded jest.mock('./embeddable/embeddable_factory', () => ({ EmbeddableFactory: class Mock {}, diff --git a/x-pack/legacy/plugins/lens/public/editor_frame_service/service.tsx b/x-pack/plugins/lens/public/editor_frame_service/service.tsx similarity index 91% rename from x-pack/legacy/plugins/lens/public/editor_frame_service/service.tsx rename to x-pack/plugins/lens/public/editor_frame_service/service.tsx index 1375c60060ca8..15fe449d6563b 100644 --- a/x-pack/legacy/plugins/lens/public/editor_frame_service/service.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/service.tsx @@ -7,16 +7,13 @@ import React from 'react'; import { render, unmountComponentAtNode } from 'react-dom'; import { I18nProvider } from '@kbn/i18n/react'; -import { CoreSetup, CoreStart } from 'src/core/public'; -import { - ExpressionsSetup, - ExpressionsStart, -} from '../../../../../../src/plugins/expressions/public'; -import { EmbeddableSetup, EmbeddableStart } from '../../../../../../src/plugins/embeddable/public'; +import { CoreSetup, CoreStart } from 'kibana/public'; +import { ExpressionsSetup, ExpressionsStart } from '../../../../../src/plugins/expressions/public'; +import { EmbeddableSetup, EmbeddableStart } from '../../../../../src/plugins/embeddable/public'; import { DataPublicPluginSetup, DataPublicPluginStart, -} from '../../../../../../src/plugins/data/public'; +} from '../../../../../src/plugins/data/public'; import { Datasource, Visualization, @@ -32,13 +29,13 @@ import { getActiveDatasourceIdFromDoc } from './editor_frame/state_management'; export interface EditorFrameSetupPlugins { data: DataPublicPluginSetup; - embeddable: EmbeddableSetup; + embeddable?: EmbeddableSetup; expressions: ExpressionsSetup; } export interface EditorFrameStartPlugins { data: DataPublicPluginStart; - embeddable: EmbeddableStart; + embeddable?: EmbeddableStart; expressions: ExpressionsStart; } @@ -79,7 +76,9 @@ export class EditorFrameService { }; }; - plugins.embeddable.registerEmbeddableFactory('lens', new EmbeddableFactory(getStartServices)); + if (plugins.embeddable) { + plugins.embeddable.registerEmbeddableFactory('lens', new EmbeddableFactory(getStartServices)); + } return { registerDatasource: datasource => { diff --git a/x-pack/plugins/lens/public/help_menu_util.tsx b/x-pack/plugins/lens/public/help_menu_util.tsx new file mode 100644 index 0000000000000..333a90df4731b --- /dev/null +++ b/x-pack/plugins/lens/public/help_menu_util.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 { ChromeStart, DocLinksStart } from 'kibana/public'; + +export function addHelpMenuToAppChrome(chrome: ChromeStart, docLinks: DocLinksStart) { + chrome.setHelpExtension({ + appName: 'Lens', + links: [ + { + linkType: 'documentation', + href: `${docLinks.ELASTIC_WEBSITE_URL}guide/en/kibana/${docLinks.DOC_LINK_VERSION}/lens.html`, + }, + { + linkType: 'github', + title: '[Lens]', + labels: ['Feature:Lens'], + }, + ], + }); +} diff --git a/x-pack/legacy/plugins/lens/public/helpers/index.ts b/x-pack/plugins/lens/public/helpers/index.ts similarity index 100% rename from x-pack/legacy/plugins/lens/public/helpers/index.ts rename to x-pack/plugins/lens/public/helpers/index.ts diff --git a/x-pack/plugins/lens/public/helpers/url_helper.test.ts b/x-pack/plugins/lens/public/helpers/url_helper.test.ts new file mode 100644 index 0000000000000..37e35ca17e0b3 --- /dev/null +++ b/x-pack/plugins/lens/public/helpers/url_helper.test.ts @@ -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. + */ + +jest.mock('../../../../../src/plugins/dashboard/public', () => ({ + DashboardConstants: { + ADD_EMBEDDABLE_ID: 'addEmbeddableId', + ADD_EMBEDDABLE_TYPE: 'addEmbeddableType', + }, +})); + +import { addEmbeddableToDashboardUrl, getUrlVars } from './url_helper'; + +describe('Dashboard URL Helper', () => { + it('addEmbeddableToDashboardUrl', () => { + const id = '123eb456cd'; + const urlVars = { + x: '1', + y: '2', + z: '3', + }; + const url = + "/pep/app/kibana#/dashboard?_g=(refreshInterval:(pause:!t,value:0),time:(from:now-15m,to:now))&_a=(description:'',filters:!())"; + expect(addEmbeddableToDashboardUrl(url, id, urlVars)).toEqual( + `/pep/app/kibana#/dashboard?_a=%28description%3A%27%27%2Cfilters%3A%21%28%29%29&_g=%28refreshInterval%3A%28pause%3A%21t%2Cvalue%3A0%29%2Ctime%3A%28from%3Anow-15m%2Cto%3Anow%29%29&addEmbeddableId=${id}&addEmbeddableType=lens&x=1&y=2&z=3` + ); + }); + + it('getUrlVars', () => { + let url = + "http://localhost:5601/app/kibana#/dashboard?_g=(refreshInterval:(pause:!t,value:0),time:(from:now-15m,to:now))&_a=(description:'',filters:!()"; + expect(getUrlVars(url)).toEqual({ + _g: '(refreshInterval:(pause:!t,value:0),time:(from:now-15m,to:now))', + _a: "(description:'',filters:!()", + }); + url = 'http://mybusiness.mydomain.com/app/kibana#/dashboard?x=y&y=z'; + expect(getUrlVars(url)).toEqual({ + x: 'y', + y: 'z', + }); + url = 'http://localhost:5601/app/kibana#/dashboard/777182'; + expect(getUrlVars(url)).toEqual({}); + url = + 'http://localhost:5601/app/kibana#/dashboard/777182?title=Some%20Dashboard%20With%20Spaces'; + expect(getUrlVars(url)).toEqual({ title: 'Some Dashboard With Spaces' }); + }); +}); diff --git a/x-pack/plugins/lens/public/helpers/url_helper.ts b/x-pack/plugins/lens/public/helpers/url_helper.ts new file mode 100644 index 0000000000000..0a97ba4b2edf7 --- /dev/null +++ b/x-pack/plugins/lens/public/helpers/url_helper.ts @@ -0,0 +1,45 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { parseUrl, stringify } from 'query-string'; +import { DashboardConstants } from '../../../../../src/plugins/dashboard/public'; + +type UrlVars = Record<string, string>; + +/** + * Return query params from URL + * @param url given url + */ +export function getUrlVars(url: string): Record<string, string> { + const vars: UrlVars = {}; + for (const [, key, value] of url.matchAll(/[?&]+([^=&]+)=([^&]*)/gi)) { + vars[key] = decodeURIComponent(value); + } + return vars; +} + +/** * + * Returns dashboard URL with added embeddableType and embeddableId query params + * eg. + * input: url: /lol/app/kibana#/dashboard?_g=(refreshInterval:(pause:!t,value:0),time:(from:now-15m,to:now)), embeddableId: 12345 + * output: /lol/app/kibana#/dashboard?addEmbeddableType=lens&addEmbeddableId=12345&_g=(refreshInterval:(pause:!t,value:0),time:(from:now-15m,to:now)) + * @param url dasbhoard absolute url + * @param embeddableId id of the saved visualization + * @param urlVars url query params + */ +export function addEmbeddableToDashboardUrl(url: string, embeddableId: string, urlVars: UrlVars) { + const dashboardParsedUrl = parseUrl(url); + const keys = Object.keys(urlVars).sort(); + + keys.forEach(key => { + dashboardParsedUrl.query[key] = urlVars[key]; + }); + dashboardParsedUrl.query[DashboardConstants.ADD_EMBEDDABLE_TYPE] = 'lens'; + dashboardParsedUrl.query[DashboardConstants.ADD_EMBEDDABLE_ID] = embeddableId; + const query = stringify(dashboardParsedUrl.query); + + return `${dashboardParsedUrl.url}?${query}`; +} diff --git a/x-pack/legacy/plugins/lens/public/id_generator/id_generator.test.ts b/x-pack/plugins/lens/public/id_generator/id_generator.test.ts similarity index 100% rename from x-pack/legacy/plugins/lens/public/id_generator/id_generator.test.ts rename to x-pack/plugins/lens/public/id_generator/id_generator.test.ts diff --git a/x-pack/legacy/plugins/lens/public/id_generator/id_generator.ts b/x-pack/plugins/lens/public/id_generator/id_generator.ts similarity index 100% rename from x-pack/legacy/plugins/lens/public/id_generator/id_generator.ts rename to x-pack/plugins/lens/public/id_generator/id_generator.ts diff --git a/x-pack/legacy/plugins/lens/public/id_generator/index.ts b/x-pack/plugins/lens/public/id_generator/index.ts similarity index 100% rename from x-pack/legacy/plugins/lens/public/id_generator/index.ts rename to x-pack/plugins/lens/public/id_generator/index.ts diff --git a/x-pack/plugins/lens/public/index.scss b/x-pack/plugins/lens/public/index.scss new file mode 100644 index 0000000000000..67bbac12be8c3 --- /dev/null +++ b/x-pack/plugins/lens/public/index.scss @@ -0,0 +1,14 @@ +// Import the EUI global scope so we can use EUI constants +@import '@elastic/eui/src/global_styling/variables/index'; +@import '@elastic/eui/src/global_styling/mixins/index'; + +@import 'variables'; +@import 'mixins'; + +@import 'app_plugin/index'; +@import 'datatable_visualization/index'; +@import 'drag_drop/index'; +@import 'editor_frame_service/index'; +@import 'indexpattern_datasource/index'; +@import 'xy_visualization/index'; +@import 'metric_visualization/index'; diff --git a/x-pack/legacy/plugins/lens/public/index.ts b/x-pack/plugins/lens/public/index.ts similarity index 100% rename from x-pack/legacy/plugins/lens/public/index.ts rename to x-pack/plugins/lens/public/index.ts diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_datasource/__mocks__/loader.ts b/x-pack/plugins/lens/public/indexpattern_datasource/__mocks__/loader.ts similarity index 100% rename from x-pack/legacy/plugins/lens/public/indexpattern_datasource/__mocks__/loader.ts rename to x-pack/plugins/lens/public/indexpattern_datasource/__mocks__/loader.ts diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_datasource/__mocks__/state_helpers.ts b/x-pack/plugins/lens/public/indexpattern_datasource/__mocks__/state_helpers.ts similarity index 100% rename from x-pack/legacy/plugins/lens/public/indexpattern_datasource/__mocks__/state_helpers.ts rename to x-pack/plugins/lens/public/indexpattern_datasource/__mocks__/state_helpers.ts diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_datasource/__snapshots__/lens_field_icon.test.tsx.snap b/x-pack/plugins/lens/public/indexpattern_datasource/__snapshots__/lens_field_icon.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/lens/public/indexpattern_datasource/__snapshots__/lens_field_icon.test.tsx.snap rename to x-pack/plugins/lens/public/indexpattern_datasource/__snapshots__/lens_field_icon.test.tsx.snap diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_datasource/_datapanel.scss b/x-pack/plugins/lens/public/indexpattern_datasource/_datapanel.scss similarity index 100% rename from x-pack/legacy/plugins/lens/public/indexpattern_datasource/_datapanel.scss rename to x-pack/plugins/lens/public/indexpattern_datasource/_datapanel.scss diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_datasource/_field_item.scss b/x-pack/plugins/lens/public/indexpattern_datasource/_field_item.scss similarity index 98% rename from x-pack/legacy/plugins/lens/public/indexpattern_datasource/_field_item.scss rename to x-pack/plugins/lens/public/indexpattern_datasource/_field_item.scss index 89f6bbf908419..41919b900c71f 100644 --- a/x-pack/legacy/plugins/lens/public/indexpattern_datasource/_field_item.scss +++ b/x-pack/plugins/lens/public/indexpattern_datasource/_field_item.scss @@ -14,7 +14,7 @@ } .lnsFieldItem--missing { - background: lightOrDarkTheme(transparentize($euiColorMediumShade, 0.9), $euiColorEmptyShade); + background: lightOrDarkTheme(transparentize($euiColorMediumShade, .9), $euiColorEmptyShade); color: $euiColorDarkShade; } diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/_index.scss b/x-pack/plugins/lens/public/indexpattern_datasource/_index.scss new file mode 100644 index 0000000000000..e5d8b408e33e5 --- /dev/null +++ b/x-pack/plugins/lens/public/indexpattern_datasource/_index.scss @@ -0,0 +1,4 @@ +@import 'datapanel'; +@import 'field_item'; + +@import 'dimension_panel/index'; diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_datasource/auto_date.test.ts b/x-pack/plugins/lens/public/indexpattern_datasource/auto_date.test.ts similarity index 96% rename from x-pack/legacy/plugins/lens/public/indexpattern_datasource/auto_date.test.ts rename to x-pack/plugins/lens/public/indexpattern_datasource/auto_date.test.ts index cc1a74a1854ce..5f35ef650a08c 100644 --- a/x-pack/legacy/plugins/lens/public/indexpattern_datasource/auto_date.test.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/auto_date.test.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { dataPluginMock } from '../../../../../../src/plugins/data/public/mocks'; +import { dataPluginMock } from '../../../../../src/plugins/data/public/mocks'; import { getAutoDate } from './auto_date'; describe('auto_date', () => { diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_datasource/auto_date.ts b/x-pack/plugins/lens/public/indexpattern_datasource/auto_date.ts similarity index 92% rename from x-pack/legacy/plugins/lens/public/indexpattern_datasource/auto_date.ts rename to x-pack/plugins/lens/public/indexpattern_datasource/auto_date.ts index 063cbb4d217a7..97a46f4a3e176 100644 --- a/x-pack/legacy/plugins/lens/public/indexpattern_datasource/auto_date.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/auto_date.ts @@ -4,11 +4,11 @@ * you may not use this file except in compliance with the Elastic License. */ -import { DataPublicPluginSetup } from '../../../../../../src/plugins/data/public'; +import { DataPublicPluginSetup } from '../../../../../src/plugins/data/public'; import { ExpressionFunctionDefinition, KibanaContext, -} from '../../../../../../src/plugins/expressions/public'; +} from '../../../../../src/plugins/expressions/public'; interface LensAutoDateProps { aggConfigs: string; diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_datasource/change_indexpattern.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/change_indexpattern.tsx similarity index 100% rename from x-pack/legacy/plugins/lens/public/indexpattern_datasource/change_indexpattern.tsx rename to x-pack/plugins/lens/public/indexpattern_datasource/change_indexpattern.tsx diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_datasource/datapanel.test.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/datapanel.test.tsx similarity index 99% rename from x-pack/legacy/plugins/lens/public/indexpattern_datasource/datapanel.test.tsx rename to x-pack/plugins/lens/public/indexpattern_datasource/datapanel.test.tsx index 3066ac0e11325..c396f0efee42e 100644 --- a/x-pack/legacy/plugins/lens/public/indexpattern_datasource/datapanel.test.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/datapanel.test.tsx @@ -6,6 +6,7 @@ import React, { ChangeEvent } from 'react'; import { createMockedDragDropContext } from './mocks'; +import { dataPluginMock } from '../../../../../src/plugins/data/public/mocks'; import { InnerIndexPatternDataPanel, IndexPatternDataPanel, MemoizedDataPanel } from './datapanel'; import { FieldItem } from './field_item'; import { act } from 'react-dom/test-utils'; @@ -16,8 +17,6 @@ import { ChangeIndexPattern } from './change_indexpattern'; import { EuiProgress } from '@elastic/eui'; import { documentField } from './document_field'; -jest.mock('ui/new_platform'); - const initialState: IndexPatternPrivateState = { indexPatternRefs: [], existingFields: {}, @@ -218,6 +217,7 @@ describe('IndexPattern Data Panel', () => { defaultProps = { indexPatternRefs: [], existingFields: {}, + data: dataPluginMock.createStartContract(), dragDropContext: createMockedDragDropContext(), currentIndexPatternId: '1', indexPatterns: initialState.indexPatterns, diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_datasource/datapanel.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/datapanel.tsx similarity index 98% rename from x-pack/legacy/plugins/lens/public/indexpattern_datasource/datapanel.tsx rename to x-pack/plugins/lens/public/indexpattern_datasource/datapanel.tsx index 7a3c04b67fbc4..79dcdafd916b4 100644 --- a/x-pack/legacy/plugins/lens/public/indexpattern_datasource/datapanel.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/datapanel.tsx @@ -27,6 +27,7 @@ import { } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; +import { DataPublicPluginStart } from 'src/plugins/data/public'; import { DatasourceDataPanelProps, DataType, StateSetter } from '../types'; import { ChildDragDropProvider, DragContextState } from '../drag_drop'; import { FieldItem } from './field_item'; @@ -40,9 +41,10 @@ import { trackUiEvent } from '../lens_ui_telemetry'; import { syncExistingFields } from './loader'; import { fieldExists } from './pure_helpers'; import { Loader } from '../loader'; -import { esQuery, IIndexPattern } from '../../../../../../src/plugins/data/public'; +import { esQuery, IIndexPattern } from '../../../../../src/plugins/data/public'; export type Props = DatasourceDataPanelProps<IndexPatternPrivateState> & { + data: DataPublicPluginStart; changeIndexPattern: ( id: string, state: IndexPatternPrivateState, @@ -78,6 +80,7 @@ export function IndexPatternDataPanel({ state, dragDropContext, core, + data, query, filters, dateRange, @@ -152,6 +155,7 @@ export function IndexPatternDataPanel({ showEmptyFields={state.showEmptyFields} onToggleEmptyFields={onToggleEmptyFields} core={core} + data={data} onChangeIndexPattern={onChangeIndexPattern} existingFields={state.existingFields} /> @@ -177,8 +181,10 @@ export const InnerIndexPatternDataPanel = function InnerIndexPatternDataPanel({ showEmptyFields, onToggleEmptyFields, core, + data, existingFields, }: Pick<DatasourceDataPanelProps, Exclude<keyof DatasourceDataPanelProps, 'state' | 'setState'>> & { + data: DataPublicPluginStart; currentIndexPatternId: string; indexPatternRefs: IndexPatternRef[]; indexPatterns: Record<string, IndexPattern>; @@ -441,6 +447,7 @@ export const InnerIndexPatternDataPanel = function InnerIndexPatternDataPanel({ {specialFields.map(field => ( <FieldItem core={core} + data={data} key={field.name} indexPattern={currentIndexPattern} field={field} @@ -468,6 +475,7 @@ export const InnerIndexPatternDataPanel = function InnerIndexPatternDataPanel({ return ( <FieldItem core={core} + data={data} indexPattern={currentIndexPattern} key={field.name} field={field} diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_datasource/dimension_panel/_field_select.scss b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/_field_select.scss similarity index 100% rename from x-pack/legacy/plugins/lens/public/indexpattern_datasource/dimension_panel/_field_select.scss rename to x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/_field_select.scss diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/_index.scss b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/_index.scss new file mode 100644 index 0000000000000..085a00a2c33c5 --- /dev/null +++ b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/_index.scss @@ -0,0 +1,2 @@ +@import 'field_select'; +@import 'popover'; diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_datasource/dimension_panel/_popover.scss b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/_popover.scss similarity index 100% rename from x-pack/legacy/plugins/lens/public/indexpattern_datasource/dimension_panel/_popover.scss rename to x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/_popover.scss diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_datasource/dimension_panel/bucket_nesting_editor.test.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/bucket_nesting_editor.test.tsx similarity index 100% rename from x-pack/legacy/plugins/lens/public/indexpattern_datasource/dimension_panel/bucket_nesting_editor.test.tsx rename to x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/bucket_nesting_editor.test.tsx diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_datasource/dimension_panel/bucket_nesting_editor.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/bucket_nesting_editor.tsx similarity index 100% rename from x-pack/legacy/plugins/lens/public/indexpattern_datasource/dimension_panel/bucket_nesting_editor.tsx rename to x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/bucket_nesting_editor.tsx diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_panel.test.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_panel.test.tsx similarity index 99% rename from x-pack/legacy/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_panel.test.tsx rename to x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_panel.test.tsx index f4485774bc942..074c40759f8d8 100644 --- a/x-pack/legacy/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_panel.test.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_panel.test.tsx @@ -8,7 +8,7 @@ import { ReactWrapper, ShallowWrapper } from 'enzyme'; import React from 'react'; import { act } from 'react-dom/test-utils'; import { EuiComboBox, EuiSideNav, EuiSideNavItemType, EuiFieldNumber } from '@elastic/eui'; -import { DataPublicPluginStart } from '../../../../../../../src/plugins/data/public'; +import { DataPublicPluginStart } from '../../../../../../src/plugins/data/public'; import { changeColumn } from '../state_helpers'; import { IndexPatternDimensionEditorComponent, @@ -19,18 +19,12 @@ import { import { DragContextState } from '../../drag_drop'; import { createMockedDragDropContext } from '../mocks'; import { mountWithIntl as mount, shallowWithIntl as shallow } from 'test_utils/enzyme_helpers'; -import { - IUiSettingsClient, - SavedObjectsClientContract, - HttpSetup, - CoreSetup, -} from 'src/core/public'; +import { IUiSettingsClient, SavedObjectsClientContract, HttpSetup, CoreSetup } from 'kibana/public'; import { IStorageWrapper } from 'src/plugins/kibana_utils/public'; import { IndexPatternPrivateState } from '../types'; import { documentField } from '../document_field'; import { OperationMetadata } from '../../types'; -jest.mock('ui/new_platform'); jest.mock('../loader'); jest.mock('../state_helpers'); diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_panel.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_panel.tsx similarity index 96% rename from x-pack/legacy/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_panel.tsx rename to x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_panel.tsx index 5d87137db3d39..b3bd08d3bbfbe 100644 --- a/x-pack/legacy/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_panel.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_panel.tsx @@ -8,7 +8,7 @@ import _ from 'lodash'; import React, { memo } from 'react'; import { i18n } from '@kbn/i18n'; import { EuiLink } from '@elastic/eui'; -import { IUiSettingsClient, SavedObjectsClientContract, HttpSetup } from 'src/core/public'; +import { IUiSettingsClient, SavedObjectsClientContract, HttpSetup } from 'kibana/public'; import { IStorageWrapper } from 'src/plugins/kibana_utils/public'; import { DatasourceDimensionTriggerProps, @@ -16,7 +16,7 @@ import { DatasourceDimensionDropProps, DatasourceDimensionDropHandlerProps, } from '../../types'; -import { DataPublicPluginStart } from '../../../../../../../src/plugins/data/public'; +import { DataPublicPluginStart } from '../../../../../../src/plugins/data/public'; import { IndexPatternColumn, OperationType } from '../indexpattern'; import { getAvailableOperationsByMetadata, buildColumn, changeField } from '../operations'; import { PopoverEditor } from './popover_editor'; @@ -24,7 +24,7 @@ import { changeColumn } from '../state_helpers'; import { isDraggedField, hasField } from '../utils'; import { IndexPatternPrivateState, IndexPatternField } from '../types'; import { trackUiEvent } from '../../lens_ui_telemetry'; -import { DateRange } from '../../../../../../plugins/lens/common'; +import { DateRange } from '../../../common'; export type IndexPatternDimensionTriggerProps = DatasourceDimensionTriggerProps< IndexPatternPrivateState @@ -140,12 +140,16 @@ export function onDrop( operationsForNewField && operationsForNewField.includes(selectedColumn.operationType); + if (!operationsForNewField || operationsForNewField.length === 0) { + return false; + } + // If only the field has changed use the onFieldChange method on the operation to get the // new column, otherwise use the regular buildColumn to get a new column. const newColumn = hasFieldChanged ? changeField(selectedColumn, currentIndexPattern, droppedItem.field) : buildColumn({ - op: operationsForNewField ? operationsForNewField[0] : undefined, + op: operationsForNewField[0], columns: props.state.layers[props.layerId].columns, indexPattern: currentIndexPattern, layerId, diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_datasource/dimension_panel/field_select.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/field_select.tsx similarity index 100% rename from x-pack/legacy/plugins/lens/public/indexpattern_datasource/dimension_panel/field_select.tsx rename to x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/field_select.tsx diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_datasource/dimension_panel/format_selector.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/format_selector.tsx similarity index 100% rename from x-pack/legacy/plugins/lens/public/indexpattern_datasource/dimension_panel/format_selector.tsx rename to x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/format_selector.tsx diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_datasource/dimension_panel/index.ts b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/index.ts similarity index 100% rename from x-pack/legacy/plugins/lens/public/indexpattern_datasource/dimension_panel/index.ts rename to x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/index.ts diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_datasource/dimension_panel/popover_editor.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/popover_editor.tsx similarity index 100% rename from x-pack/legacy/plugins/lens/public/indexpattern_datasource/dimension_panel/popover_editor.tsx rename to x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/popover_editor.tsx diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_datasource/document_field.ts b/x-pack/plugins/lens/public/indexpattern_datasource/document_field.ts similarity index 100% rename from x-pack/legacy/plugins/lens/public/indexpattern_datasource/document_field.ts rename to x-pack/plugins/lens/public/indexpattern_datasource/document_field.ts diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_datasource/field_item.test.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/field_item.test.tsx similarity index 95% rename from x-pack/legacy/plugins/lens/public/indexpattern_datasource/field_item.test.tsx rename to x-pack/plugins/lens/public/indexpattern_datasource/field_item.test.tsx index 77f7f8cdab358..6a4a2bd2ba77b 100644 --- a/x-pack/legacy/plugins/lens/public/indexpattern_datasource/field_item.test.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/field_item.test.tsx @@ -10,16 +10,15 @@ import { EuiLoadingSpinner, EuiPopover } from '@elastic/eui'; import { FieldItem, FieldItemProps } from './field_item'; import { coreMock } from 'src/core/public/mocks'; import { mountWithIntl } from 'test_utils/enzyme_helpers'; -import { npStart } from 'ui/new_platform'; -import { DataPublicPluginStart } from '../../../../../../src/plugins/data/public'; +import { DataPublicPluginStart } from '../../../../../src/plugins/data/public'; +import { dataPluginMock } from '../../../../../src/plugins/data/public/mocks'; import { IndexPattern } from './types'; -jest.mock('ui/new_platform'); - describe('IndexPattern Field Item', () => { let defaultProps: FieldItemProps; let indexPattern: IndexPattern; let core: ReturnType<typeof coreMock['createSetup']>; + let data: DataPublicPluginStart; beforeEach(() => { indexPattern = { @@ -61,9 +60,11 @@ describe('IndexPattern Field Item', () => { } as IndexPattern; core = coreMock.createSetup(); + data = dataPluginMock.createStartContract(); core.http.post.mockClear(); defaultProps = { indexPattern, + data, core, highlight: '', dateRange: { @@ -81,7 +82,7 @@ describe('IndexPattern Field Item', () => { exists: true, }; - npStart.plugins.data.fieldFormats = ({ + data.fieldFormats = ({ getDefaultInstance: jest.fn(() => ({ convert: jest.fn((s: unknown) => JSON.stringify(s)), })), diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_datasource/field_item.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/field_item.tsx similarity index 97% rename from x-pack/legacy/plugins/lens/public/indexpattern_datasource/field_item.tsx rename to x-pack/plugins/lens/public/indexpattern_datasource/field_item.tsx index b98f589bc5b98..c4d2a6f8780c6 100644 --- a/x-pack/legacy/plugins/lens/public/indexpattern_datasource/field_item.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/field_item.tsx @@ -20,7 +20,6 @@ import { EuiText, EuiToolTip, } from '@elastic/eui'; -import { npStart } from 'ui/new_platform'; import { EUI_CHARTS_THEME_DARK, EUI_CHARTS_THEME_LIGHT } from '@elastic/eui/dist/eui_charts_theme'; import { Axis, @@ -33,6 +32,7 @@ import { TooltipType, } from '@elastic/charts'; import { i18n } from '@kbn/i18n'; +import { DataPublicPluginStart } from 'src/plugins/data/public'; import { Query, KBN_FIELD_TYPES, @@ -40,17 +40,18 @@ import { Filter, esQuery, IIndexPattern, -} from '../../../../../../src/plugins/data/public'; +} from '../../../../../src/plugins/data/public'; import { DraggedField } from './indexpattern'; import { DragDrop } from '../drag_drop'; import { DatasourceDataPanelProps, DataType } from '../types'; -import { BucketedAggregation, FieldStatsResponse } from '../../../../../plugins/lens/common'; +import { BucketedAggregation, FieldStatsResponse } from '../../common'; import { IndexPattern, IndexPatternField } from './types'; import { LensFieldIcon } from './lens_field_icon'; import { trackUiEvent } from '../lens_ui_telemetry'; export interface FieldItemProps { core: DatasourceDataPanelProps['core']; + data: DataPublicPluginStart; field: IndexPatternField; indexPattern: IndexPattern; highlight?: string; @@ -237,8 +238,16 @@ export function FieldItem(props: FieldItemProps) { } function FieldItemPopoverContents(props: State & FieldItemProps) { - const fieldFormats = npStart.plugins.data.fieldFormats; - const { histogram, topValues, indexPattern, field, dateRange, core, sampledValues } = props; + const { + histogram, + topValues, + indexPattern, + field, + dateRange, + core, + sampledValues, + data: { fieldFormats }, + } = props; const IS_DARK_THEME = core.uiSettings.get('theme:darkMode'); const chartTheme = IS_DARK_THEME ? EUI_CHARTS_THEME_DARK.theme : EUI_CHARTS_THEME_LIGHT.theme; diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/index.ts b/x-pack/plugins/lens/public/indexpattern_datasource/index.ts new file mode 100644 index 0000000000000..fe14f472341af --- /dev/null +++ b/x-pack/plugins/lens/public/indexpattern_datasource/index.ts @@ -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 { CoreSetup } from 'kibana/public'; +import { Storage } from '../../../../../src/plugins/kibana_utils/public'; +import { getIndexPatternDatasource } from './indexpattern'; +import { renameColumns } from './rename_columns'; +import { getAutoDate } from './auto_date'; +import { ExpressionsSetup } from '../../../../../src/plugins/expressions/public'; +import { + DataPublicPluginSetup, + DataPublicPluginStart, +} from '../../../../../src/plugins/data/public'; +import { Datasource, EditorFrameSetup } from '../types'; + +export interface IndexPatternDatasourceSetupPlugins { + expressions: ExpressionsSetup; + data: DataPublicPluginSetup; + editorFrame: EditorFrameSetup; +} + +export interface IndexPatternDatasourceStartPlugins { + data: DataPublicPluginStart; +} + +export class IndexPatternDatasource { + constructor() {} + + setup( + core: CoreSetup<IndexPatternDatasourceStartPlugins>, + { data: dataSetup, expressions, editorFrame }: IndexPatternDatasourceSetupPlugins + ) { + expressions.registerFunction(renameColumns); + expressions.registerFunction(getAutoDate({ data: dataSetup })); + + editorFrame.registerDatasource( + core.getStartServices().then(([coreStart, { data }]) => + getIndexPatternDatasource({ + core: coreStart, + storage: new Storage(localStorage), + data, + }) + ) as Promise<Datasource> + ); + } +} diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_datasource/indexpattern.test.ts b/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.test.ts similarity index 84% rename from x-pack/legacy/plugins/lens/public/indexpattern_datasource/indexpattern.test.ts rename to x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.test.ts index 76e59a170a9e9..e4f3677d0fe88 100644 --- a/x-pack/legacy/plugins/lens/public/indexpattern_datasource/indexpattern.test.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.test.ts @@ -8,13 +8,11 @@ import { IStorageWrapper } from 'src/plugins/kibana_utils/public'; import { getIndexPatternDatasource, IndexPatternColumn, uniqueLabels } from './indexpattern'; import { DatasourcePublicAPI, Operation, Datasource } from '../types'; import { coreMock } from 'src/core/public/mocks'; -import { pluginsMock } from 'ui/new_platform/__mocks__/helpers'; import { IndexPatternPersistedState, IndexPatternPrivateState } from './types'; +import { dataPluginMock } from '../../../../../src/plugins/data/public/mocks'; jest.mock('./loader'); jest.mock('../id_generator'); -// Contains old and new platform data plugins, used for interpreter and filter ratio -jest.mock('ui/new_platform'); const expectedIndexPatterns = { 1: { @@ -140,7 +138,7 @@ describe('IndexPattern Data Source', () => { indexPatternDatasource = getIndexPatternDatasource({ storage: {} as IStorageWrapper, core: coreMock.createStart(), - data: pluginsMock.createStart().data, + data: dataPluginMock.createStartContract(), }); persistedState = { @@ -259,12 +257,54 @@ describe('IndexPattern Data Source', () => { const state = stateFromPersistedState(queryPersistedState); expect(indexPatternDatasource.toExpression(state, 'first')).toMatchInlineSnapshot(` - "esaggs - index=\\"1\\" - metricsAtAllLevels=false - partialRows=false - includeFormatHints=true - aggConfigs={lens_auto_date aggConfigs='[{\\"id\\":\\"col1\\",\\"enabled\\":true,\\"type\\":\\"count\\",\\"schema\\":\\"metric\\",\\"params\\":{}},{\\"id\\":\\"col2\\",\\"enabled\\":true,\\"type\\":\\"date_histogram\\",\\"schema\\":\\"segment\\",\\"params\\":{\\"field\\":\\"timestamp\\",\\"useNormalizedEsInterval\\":true,\\"interval\\":\\"1d\\",\\"drop_partials\\":false,\\"min_doc_count\\":0,\\"extended_bounds\\":{}}}]'} | lens_rename_columns idMap='{\\"col-0-col1\\":{\\"label\\":\\"Count of records\\",\\"dataType\\":\\"number\\",\\"isBucketed\\":false,\\"sourceField\\":\\"Records\\",\\"operationType\\":\\"count\\",\\"id\\":\\"col1\\"},\\"col-1-col2\\":{\\"label\\":\\"Date\\",\\"dataType\\":\\"date\\",\\"isBucketed\\":true,\\"operationType\\":\\"date_histogram\\",\\"sourceField\\":\\"timestamp\\",\\"params\\":{\\"interval\\":\\"1d\\"},\\"id\\":\\"col2\\"}}' " + Object { + "chain": Array [ + Object { + "arguments": Object { + "aggConfigs": Array [ + Object { + "chain": Array [ + Object { + "arguments": Object { + "aggConfigs": Array [ + "[{\\"id\\":\\"col1\\",\\"enabled\\":true,\\"type\\":\\"count\\",\\"schema\\":\\"metric\\",\\"params\\":{}},{\\"id\\":\\"col2\\",\\"enabled\\":true,\\"type\\":\\"date_histogram\\",\\"schema\\":\\"segment\\",\\"params\\":{\\"field\\":\\"timestamp\\",\\"useNormalizedEsInterval\\":true,\\"interval\\":\\"1d\\",\\"drop_partials\\":false,\\"min_doc_count\\":0,\\"extended_bounds\\":{}}}]", + ], + }, + "function": "lens_auto_date", + "type": "function", + }, + ], + "type": "expression", + }, + ], + "includeFormatHints": Array [ + true, + ], + "index": Array [ + "1", + ], + "metricsAtAllLevels": Array [ + false, + ], + "partialRows": Array [ + false, + ], + }, + "function": "esaggs", + "type": "function", + }, + Object { + "arguments": Object { + "idMap": Array [ + "{\\"col-0-col1\\":{\\"label\\":\\"Count of records\\",\\"dataType\\":\\"number\\",\\"isBucketed\\":false,\\"sourceField\\":\\"Records\\",\\"operationType\\":\\"count\\",\\"id\\":\\"col1\\"},\\"col-1-col2\\":{\\"label\\":\\"Date\\",\\"dataType\\":\\"date\\",\\"isBucketed\\":true,\\"operationType\\":\\"date_histogram\\",\\"sourceField\\":\\"timestamp\\",\\"params\\":{\\"interval\\":\\"1d\\"},\\"id\\":\\"col2\\"}}", + ], + }, + "function": "lens_rename_columns", + "type": "function", + }, + ], + "type": "expression", + } `); }); }); diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_datasource/indexpattern.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.tsx similarity index 96% rename from x-pack/legacy/plugins/lens/public/indexpattern_datasource/indexpattern.tsx rename to x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.tsx index 9c2a9c9bf4a09..b8f0460f2a9ab 100644 --- a/x-pack/legacy/plugins/lens/public/indexpattern_datasource/indexpattern.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.tsx @@ -8,7 +8,7 @@ import _ from 'lodash'; import React from 'react'; import { render } from 'react-dom'; import { I18nProvider } from '@kbn/i18n/react'; -import { CoreStart } from 'src/core/public'; +import { CoreStart } from 'kibana/public'; import { i18n } from '@kbn/i18n'; import { IStorageWrapper } from 'src/plugins/kibana_utils/public'; import { @@ -42,10 +42,10 @@ import { IndexPatternPrivateState, IndexPatternPersistedState, } from './types'; -import { KibanaContextProvider } from '../../../../../../src/plugins/kibana_react/public'; -import { Plugin as DataPlugin } from '../../../../../../src/plugins/data/public'; +import { KibanaContextProvider } from '../../../../../src/plugins/kibana_react/public'; +import { DataPublicPluginStart } from '../../../../../src/plugins/data/public'; import { deleteColumn } from './state_helpers'; -import { Datasource, StateSetter } from '..'; +import { Datasource, StateSetter } from '../index'; export { OperationType, IndexPatternColumn } from './operations'; @@ -105,7 +105,7 @@ export function getIndexPatternDatasource({ }: { core: CoreStart; storage: IStorageWrapper; - data: ReturnType<DataPlugin['start']>; + data: DataPublicPluginStart; }) { const savedObjectsClient = core.savedObjects.client; const uiSettings = core.uiSettings; @@ -209,6 +209,7 @@ export function getIndexPatternDatasource({ onError: onIndexPatternLoadError, }); }} + data={data} {...props} /> </I18nProvider>, diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_datasource/indexpattern_suggestions.test.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern_suggestions.test.tsx similarity index 99% rename from x-pack/legacy/plugins/lens/public/indexpattern_datasource/indexpattern_suggestions.test.tsx rename to x-pack/plugins/lens/public/indexpattern_datasource/indexpattern_suggestions.test.tsx index e36622f876acd..2008b326a539c 100644 --- a/x-pack/legacy/plugins/lens/public/indexpattern_datasource/indexpattern_suggestions.test.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern_suggestions.test.tsx @@ -12,7 +12,6 @@ import { getDatasourceSuggestionsFromCurrentState, } from './indexpattern_suggestions'; -jest.mock('ui/new_platform'); jest.mock('./loader'); jest.mock('../id_generator'); @@ -824,10 +823,10 @@ describe('IndexPattern Data Source suggestions', () => { state: expect.objectContaining({ layers: expect.objectContaining({ currentLayer: expect.objectContaining({ - columnOrder: ['cola', 'id1'], + columnOrder: ['cola', 'colb'], columns: { cola: initialState.layers.currentLayer.columns.cola, - id1: expect.objectContaining({ + colb: expect.objectContaining({ operationType: 'avg', sourceField: 'memory', }), diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_datasource/indexpattern_suggestions.ts b/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern_suggestions.ts similarity index 91% rename from x-pack/legacy/plugins/lens/public/indexpattern_datasource/indexpattern_suggestions.ts rename to x-pack/plugins/lens/public/indexpattern_datasource/indexpattern_suggestions.ts index 96127caa67bb4..2b3e976a77ea7 100644 --- a/x-pack/legacy/plugins/lens/public/indexpattern_datasource/indexpattern_suggestions.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern_suggestions.ts @@ -14,9 +14,10 @@ import { getOperationTypesForField, operationDefinitionMap, IndexPatternColumn, + OperationType, } from './operations'; -import { hasField } from './utils'; import { operationDefinitions } from './operations/definitions'; +import { hasField } from './utils'; import { IndexPattern, IndexPatternPrivateState, @@ -141,7 +142,13 @@ function getExistingLayerSuggestionsForField( suggestions.push( buildSuggestion({ state, - updatedLayer: addFieldAsBucketOperation(layer, layerId, indexPattern, field), + updatedLayer: addFieldAsBucketOperation( + layer, + layerId, + indexPattern, + field, + usableAsBucketOperation + ), layerId, changeType: 'extended', }) @@ -176,27 +183,8 @@ function addFieldAsMetricOperation( indexPattern: IndexPattern, field: IndexPatternField ): IndexPatternLayer | undefined { - const operations = getOperationTypesForField(field); - const operationsAlreadyAppliedToThisField = Object.values(layer.columns) - .filter(column => hasField(column) && column.sourceField === field.name) - .map(column => column.operationType); - const operationCandidate = operations.find( - operation => !operationsAlreadyAppliedToThisField.includes(operation) - ); - - if (!operationCandidate) { - return; - } - - const newColumn = buildColumn({ - op: operationCandidate, - columns: layer.columns, - layerId, - indexPattern, - suggestedPriority: undefined, - field, - }); - const newColumnId = generateId(); + const newColumn = getMetricColumn(indexPattern, layerId, field); + const addedColumnId = generateId(); const [, metrics] = separateBucketColumns(layer); @@ -206,20 +194,19 @@ function addFieldAsMetricOperation( indexPatternId: indexPattern.id, columns: { ...layer.columns, - [newColumnId]: newColumn, + [addedColumnId]: newColumn, }, - columnOrder: [...layer.columnOrder, newColumnId], + columnOrder: [...layer.columnOrder, addedColumnId], }; } - // If only one metric, replace instead of add - const newColumns = { ...layer.columns, [newColumnId]: newColumn }; - delete newColumns[metrics[0]]; + // Replacing old column with new column, keeping the old ID + const newColumns = { ...layer.columns, [metrics[0]]: newColumn }; return { indexPatternId: indexPattern.id, columns: newColumns, - columnOrder: [...layer.columnOrder.filter(c => c !== metrics[0]), newColumnId], + columnOrder: layer.columnOrder, // Order is kept by replacing }; } @@ -227,11 +214,11 @@ function addFieldAsBucketOperation( layer: IndexPatternLayer, layerId: string, indexPattern: IndexPattern, - field: IndexPatternField + field: IndexPatternField, + operation: OperationType ): IndexPatternLayer { - const applicableBucketOperation = getBucketOperation(field); const newColumn = buildColumn({ - op: applicableBucketOperation, + op: operation, columns: layer.columns, layerId, indexPattern, @@ -253,7 +240,7 @@ function addFieldAsBucketOperation( let updatedColumnOrder: string[] = []; if (oldDateHistogramId) { - if (applicableBucketOperation === 'terms') { + if (operation === 'terms') { // Insert the new terms bucket above the first date histogram updatedColumnOrder = [ ...buckets.slice(0, oldDateHistogramIndex), @@ -261,7 +248,7 @@ function addFieldAsBucketOperation( ...buckets.slice(oldDateHistogramIndex, buckets.length), ...metrics, ]; - } else if (applicableBucketOperation === 'date_histogram') { + } else if (operation === 'date_histogram') { // Replace date histogram with new date histogram delete updatedColumns[oldDateHistogramId]; updatedColumnOrder = layer.columnOrder.map(columnId => @@ -288,8 +275,9 @@ function getEmptyLayerSuggestionsForField( ): IndexPatternSugestion[] { const indexPattern = state.indexPatterns[indexPatternId]; let newLayer: IndexPatternLayer | undefined; - if (getBucketOperation(field)) { - newLayer = createNewLayerWithBucketAggregation(layerId, indexPattern, field); + const bucketOperation = getBucketOperation(field); + if (bucketOperation) { + newLayer = createNewLayerWithBucketAggregation(layerId, indexPattern, field, bucketOperation); } else if (indexPattern.timeFieldName && getOperationTypesForField(field).length > 0) { newLayer = createNewLayerWithMetricAggregation(layerId, indexPattern, field); } @@ -313,7 +301,8 @@ function getEmptyLayerSuggestionsForField( function createNewLayerWithBucketAggregation( layerId: string, indexPattern: IndexPattern, - field: IndexPatternField + field: IndexPatternField, + operation: OperationType ): IndexPatternLayer { const countColumn = buildColumn({ op: 'count', @@ -330,7 +319,7 @@ function createNewLayerWithBucketAggregation( // let column know about count column const column = buildColumn({ layerId, - op: getBucketOperation(field), + op: operation, indexPattern, columns: { [col2]: countColumn, @@ -356,15 +345,7 @@ function createNewLayerWithMetricAggregation( ): IndexPatternLayer { const dateField = indexPattern.fields.find(f => f.name === indexPattern.timeFieldName)!; - const operations = getOperationTypesForField(field); - const column = buildColumn({ - op: operations[0], - columns: {}, - suggestedPriority: undefined, - field, - indexPattern, - layerId, - }); + const column = getMetricColumn(indexPattern, layerId, field); const dateColumn = buildColumn({ op: 'date_histogram', @@ -501,12 +482,7 @@ function createChangedNestingSuggestion(state: IndexPatternPrivateState, layerId }); } -function createMetricSuggestion( - indexPattern: IndexPattern, - layerId: string, - state: IndexPatternPrivateState, - field: IndexPatternField -) { +function getMetricColumn(indexPattern: IndexPattern, layerId: string, field: IndexPatternField) { const operationDefinitionsMap = _.indexBy(operationDefinitions, 'type'); const [column] = getOperationTypesForField(field) .map(type => @@ -519,6 +495,16 @@ function createMetricSuggestion( }) ) .filter(op => (op.dataType === 'number' || op.dataType === 'document') && !op.isBucketed); + return column; +} + +function createMetricSuggestion( + indexPattern: IndexPattern, + layerId: string, + state: IndexPatternPrivateState, + field: IndexPatternField +) { + const column = getMetricColumn(indexPattern, layerId, field); if (!column) { return; @@ -573,21 +559,26 @@ function createAlternativeMetricSuggestions( return; } const field = indexPattern.fields.find(({ name }) => column.sourceField === name)!; - const alternativeMetricOperations = getOperationTypesForField(field).filter( - operationType => operationType !== column.operationType - ); + const alternativeMetricOperations = getOperationTypesForField(field) + .map(op => + buildColumn({ + op, + columns: layer.columns, + indexPattern, + layerId, + field, + suggestedPriority: undefined, + }) + ) + .filter( + fullOperation => + fullOperation.operationType !== column.operationType && !fullOperation.isBucketed + ); if (alternativeMetricOperations.length === 0) { return; } const newId = generateId(); - const newColumn = buildColumn({ - op: alternativeMetricOperations[0], - columns: layer.columns, - indexPattern, - layerId, - field, - suggestedPriority: undefined, - }); + const newColumn = alternativeMetricOperations[0]; const updatedLayer = { indexPatternId: indexPattern.id, columns: { [newId]: newColumn }, diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_datasource/layerpanel.test.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/layerpanel.test.tsx similarity index 99% rename from x-pack/legacy/plugins/lens/public/indexpattern_datasource/layerpanel.test.tsx rename to x-pack/plugins/lens/public/indexpattern_datasource/layerpanel.test.tsx index 219a6d935e436..4dd29d7925916 100644 --- a/x-pack/legacy/plugins/lens/public/indexpattern_datasource/layerpanel.test.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/layerpanel.test.tsx @@ -12,7 +12,6 @@ import { ShallowWrapper } from 'enzyme'; import { EuiSelectable, EuiSelectableList } from '@elastic/eui'; import { ChangeIndexPattern } from './change_indexpattern'; -jest.mock('ui/new_platform'); jest.mock('./state_helpers'); const initialState: IndexPatternPrivateState = { diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_datasource/layerpanel.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/layerpanel.tsx similarity index 100% rename from x-pack/legacy/plugins/lens/public/indexpattern_datasource/layerpanel.tsx rename to x-pack/plugins/lens/public/indexpattern_datasource/layerpanel.tsx diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_datasource/lens_field_icon.test.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/lens_field_icon.test.tsx similarity index 100% rename from x-pack/legacy/plugins/lens/public/indexpattern_datasource/lens_field_icon.test.tsx rename to x-pack/plugins/lens/public/indexpattern_datasource/lens_field_icon.test.tsx diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_datasource/lens_field_icon.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/lens_field_icon.tsx similarity index 86% rename from x-pack/legacy/plugins/lens/public/indexpattern_datasource/lens_field_icon.tsx rename to x-pack/plugins/lens/public/indexpattern_datasource/lens_field_icon.tsx index 06eda73748cef..bcc83e799d889 100644 --- a/x-pack/legacy/plugins/lens/public/indexpattern_datasource/lens_field_icon.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/lens_field_icon.tsx @@ -5,7 +5,7 @@ */ import React from 'react'; -import { FieldIcon, FieldIconProps } from '../../../../../../src/plugins/kibana_react/public'; +import { FieldIcon, FieldIconProps } from '../../../../../src/plugins/kibana_react/public'; import { DataType } from '../types'; import { normalizeOperationDataType } from './utils'; diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_datasource/loader.test.ts b/x-pack/plugins/lens/public/indexpattern_datasource/loader.test.ts similarity index 99% rename from x-pack/legacy/plugins/lens/public/indexpattern_datasource/loader.test.ts rename to x-pack/plugins/lens/public/indexpattern_datasource/loader.test.ts index ea9c8213ba909..cacf729ba0caf 100644 --- a/x-pack/legacy/plugins/lens/public/indexpattern_datasource/loader.test.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/loader.test.ts @@ -16,8 +16,6 @@ import { import { IndexPatternPersistedState, IndexPatternPrivateState } from './types'; import { documentField } from './document_field'; -// TODO: This should not be necessary -jest.mock('ui/new_platform'); jest.mock('./operations'); const sampleIndexPatterns = { diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_datasource/loader.ts b/x-pack/plugins/lens/public/indexpattern_datasource/loader.ts similarity index 96% rename from x-pack/legacy/plugins/lens/public/indexpattern_datasource/loader.ts rename to x-pack/plugins/lens/public/indexpattern_datasource/loader.ts index f4d5857f4826d..23faab768eba6 100644 --- a/x-pack/legacy/plugins/lens/public/indexpattern_datasource/loader.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/loader.ts @@ -5,8 +5,8 @@ */ import _ from 'lodash'; -import { SavedObjectsClientContract, SavedObjectAttributes, HttpSetup } from 'src/core/public'; -import { SimpleSavedObject } from 'src/core/public'; +import { SavedObjectsClientContract, SavedObjectAttributes, HttpSetup } from 'kibana/public'; +import { SimpleSavedObject } from 'kibana/public'; import { StateSetter } from '../types'; import { IndexPattern, @@ -16,14 +16,14 @@ import { IndexPatternField, } from './types'; import { updateLayerIndexPattern } from './state_helpers'; -import { DateRange, ExistingFields } from '../../../../../plugins/lens/common/types'; -import { BASE_API_URL } from '../../../../../plugins/lens/common'; +import { DateRange, ExistingFields } from '../../common/types'; +import { BASE_API_URL } from '../../common'; import { documentField } from './document_field'; import { indexPatterns as indexPatternsUtils, IFieldType, IndexPatternTypeMeta, -} from '../../../../../../src/plugins/data/public'; +} from '../../../../../src/plugins/data/public'; interface SavedIndexPatternAttributes extends SavedObjectAttributes { title: string; diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_datasource/mocks.ts b/x-pack/plugins/lens/public/indexpattern_datasource/mocks.ts similarity index 100% rename from x-pack/legacy/plugins/lens/public/indexpattern_datasource/mocks.ts rename to x-pack/plugins/lens/public/indexpattern_datasource/mocks.ts diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_datasource/operations/__mocks__/index.ts b/x-pack/plugins/lens/public/indexpattern_datasource/operations/__mocks__/index.ts similarity index 100% rename from x-pack/legacy/plugins/lens/public/indexpattern_datasource/operations/__mocks__/index.ts rename to x-pack/plugins/lens/public/indexpattern_datasource/operations/__mocks__/index.ts diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_datasource/operations/definitions/cardinality.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/cardinality.tsx similarity index 98% rename from x-pack/legacy/plugins/lens/public/indexpattern_datasource/operations/definitions/cardinality.tsx rename to x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/cardinality.tsx index 33325016deaeb..9491ca9ea3787 100644 --- a/x-pack/legacy/plugins/lens/public/indexpattern_datasource/operations/definitions/cardinality.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/cardinality.tsx @@ -5,7 +5,7 @@ */ import { i18n } from '@kbn/i18n'; -import { OperationDefinition } from '.'; +import { OperationDefinition } from './index'; import { FormattedIndexPatternColumn } from './column_types'; const supportedTypes = new Set(['string', 'boolean', 'number', 'ip', 'date']); diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_datasource/operations/definitions/column_types.ts b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/column_types.ts similarity index 100% rename from x-pack/legacy/plugins/lens/public/indexpattern_datasource/operations/definitions/column_types.ts rename to x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/column_types.ts diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_datasource/operations/definitions/count.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/count.tsx similarity index 97% rename from x-pack/legacy/plugins/lens/public/indexpattern_datasource/operations/definitions/count.tsx rename to x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/count.tsx index 1592b1049f666..1dcaf78b58a6c 100644 --- a/x-pack/legacy/plugins/lens/public/indexpattern_datasource/operations/definitions/count.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/count.tsx @@ -5,7 +5,7 @@ */ import { i18n } from '@kbn/i18n'; -import { OperationDefinition } from '.'; +import { OperationDefinition } from './index'; import { FormattedIndexPatternColumn } from './column_types'; import { IndexPatternField } from '../../types'; diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_datasource/operations/definitions/date_histogram.test.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/date_histogram.test.tsx similarity index 99% rename from x-pack/legacy/plugins/lens/public/indexpattern_datasource/operations/definitions/date_histogram.test.tsx rename to x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/date_histogram.test.tsx index dc279fca82d4b..e3b6061248f3b 100644 --- a/x-pack/legacy/plugins/lens/public/indexpattern_datasource/operations/definitions/date_histogram.test.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/date_histogram.test.tsx @@ -6,21 +6,19 @@ import React from 'react'; import { DateHistogramIndexPatternColumn } from './date_histogram'; -import { dateHistogramOperation } from '.'; +import { dateHistogramOperation } from './index'; import { shallow } from 'enzyme'; import { EuiSwitch, EuiSwitchEvent } from '@elastic/eui'; -import { IUiSettingsClient, SavedObjectsClientContract, HttpSetup } from 'src/core/public'; +import { IUiSettingsClient, SavedObjectsClientContract, HttpSetup } from 'kibana/public'; import { coreMock } from 'src/core/public/mocks'; import { IStorageWrapper } from 'src/plugins/kibana_utils/public'; import { dataPluginMock, getCalculateAutoTimeExpression, -} from '../../../../../../../../src/plugins/data/public/mocks'; +} from '../../../../../../../src/plugins/data/public/mocks'; import { createMockedIndexPattern } from '../../mocks'; import { IndexPatternPrivateState } from '../../types'; -jest.mock('ui/new_platform'); - const dataStart = dataPluginMock.createStartContract(); dataStart.search.aggs.calculateAutoTimeExpression = getCalculateAutoTimeExpression({ ...coreMock.createStart().uiSettings, diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_datasource/operations/definitions/date_histogram.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/date_histogram.tsx similarity index 98% rename from x-pack/legacy/plugins/lens/public/indexpattern_datasource/operations/definitions/date_histogram.tsx rename to x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/date_histogram.tsx index 452d5c9140868..6161df1167afe 100644 --- a/x-pack/legacy/plugins/lens/public/indexpattern_datasource/operations/definitions/date_histogram.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/date_histogram.tsx @@ -21,12 +21,9 @@ import { EuiSpacer, } from '@elastic/eui'; import { updateColumnParam } from '../../state_helpers'; -import { OperationDefinition } from '.'; +import { OperationDefinition } from './index'; import { FieldBasedIndexPatternColumn } from './column_types'; -import { - IndexPatternAggRestrictions, - search, -} from '../../../../../../../../src/plugins/data/public'; +import { IndexPatternAggRestrictions, search } from '../../../../../../../src/plugins/data/public'; const { isValidInterval } = search.aggs; const autoInterval = 'auto'; @@ -45,6 +42,7 @@ export const dateHistogramOperation: OperationDefinition<DateHistogramIndexPatte displayName: i18n.translate('xpack.lens.indexPattern.dateHistogram', { defaultMessage: 'Date histogram', }), + priority: 3, // Higher than any metric getPossibleOperationForField: ({ aggregationRestrictions, aggregatable, type }) => { if ( type === 'date' && diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/index.ts b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/index.ts new file mode 100644 index 0000000000000..ef12fca690f0c --- /dev/null +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/index.ts @@ -0,0 +1,198 @@ +/* + * 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 { IUiSettingsClient, SavedObjectsClientContract, HttpSetup } from 'kibana/public'; +import { IStorageWrapper } from 'src/plugins/kibana_utils/public'; +import { termsOperation } from './terms'; +import { cardinalityOperation } from './cardinality'; +import { minOperation, averageOperation, sumOperation, maxOperation } from './metrics'; +import { dateHistogramOperation } from './date_histogram'; +import { countOperation } from './count'; +import { DimensionPriority, StateSetter, OperationMetadata } from '../../../types'; +import { BaseIndexPatternColumn } from './column_types'; +import { IndexPatternPrivateState, IndexPattern, IndexPatternField } from '../../types'; +import { DateRange } from '../../../../common'; +import { DataPublicPluginStart } from '../../../../../../../src/plugins/data/public'; + +// List of all operation definitions registered to this data source. +// If you want to implement a new operation, add it to this array and +// its type will get propagated to everything else +const internalOperationDefinitions = [ + termsOperation, + dateHistogramOperation, + minOperation, + maxOperation, + averageOperation, + cardinalityOperation, + sumOperation, + countOperation, +]; + +export { termsOperation } from './terms'; +export { dateHistogramOperation } from './date_histogram'; +export { minOperation, averageOperation, sumOperation, maxOperation } from './metrics'; +export { countOperation } from './count'; + +/** + * Properties passed to the operation-specific part of the popover editor + */ +export interface ParamEditorProps<C extends BaseIndexPatternColumn> { + currentColumn: C; + state: IndexPatternPrivateState; + setState: StateSetter<IndexPatternPrivateState>; + columnId: string; + layerId: string; + uiSettings: IUiSettingsClient; + storage: IStorageWrapper; + savedObjectsClient: SavedObjectsClientContract; + http: HttpSetup; + dateRange: DateRange; + data: DataPublicPluginStart; +} + +interface BaseOperationDefinitionProps<C extends BaseIndexPatternColumn> { + type: C['operationType']; + /** + * The priority of the operation. If multiple operations are possible in + * a given scenario (e.g. the user dragged a field into the workspace), + * the operation with the highest priority is picked. + */ + priority?: number; + /** + * The name of the operation shown to the user (e.g. in the popover editor). + * Should be i18n-ified. + */ + displayName: string; + /** + * This function is called if another column in the same layer changed or got removed. + * Can be used to update references to other columns (e.g. for sorting). + * Based on the current column and the other updated columns, this function has to + * return an updated column. If not implemented, the `id` function is used instead. + */ + onOtherColumnChanged?: ( + currentColumn: C, + columns: Partial<Record<string, IndexPatternColumn>> + ) => C; + /** + * React component for operation specific settings shown in the popover editor + */ + paramEditor?: React.ComponentType<ParamEditorProps<C>>; + /** + * Function turning a column into an agg config passed to the `esaggs` function + * together with the agg configs returned from other columns. + */ + toEsAggsConfig: (column: C, columnId: string) => unknown; + /** + * Returns true if the `column` can also be used on `newIndexPattern`. + * If this function returns false, the column is removed when switching index pattern + * for a layer + */ + isTransferable: (column: C, newIndexPattern: IndexPattern) => boolean; + /** + * Transfering a column to another index pattern. This can be used to + * adjust operation specific settings such as reacting to aggregation restrictions + * present on the new index pattern. + */ + transfer?: (column: C, newIndexPattern: IndexPattern) => C; +} + +interface BaseBuildColumnArgs { + suggestedPriority: DimensionPriority | undefined; + layerId: string; + columns: Partial<Record<string, IndexPatternColumn>>; + indexPattern: IndexPattern; +} + +interface FieldBasedOperationDefinition<C extends BaseIndexPatternColumn> + extends BaseOperationDefinitionProps<C> { + /** + * Returns the meta data of the operation if applied to the given field. Undefined + * if the field is not applicable to the operation. + */ + getPossibleOperationForField: (field: IndexPatternField) => OperationMetadata | undefined; + /** + * Builds the column object for the given parameters. Should include default p + */ + buildColumn: ( + arg: BaseBuildColumnArgs & { + field: IndexPatternField; + previousColumn?: C; + } + ) => C; + /** + * This method will be called if the user changes the field of an operation. + * You must implement it and return the new column after the field change. + * The most simple implementation will just change the field on the column, and keep + * the rest the same. Some implementations might want to change labels, or their parameters + * when changing the field. + * + * This will only be called for switching the field, not for initially selecting a field. + * + * See {@link OperationDefinition#transfer} for controlling column building when switching an + * index pattern not just a field. + * + * @param oldColumn The column before the user changed the field. + * @param indexPattern The index pattern that field is on. + * @param field The field that the user changed to. + */ + onFieldChange: (oldColumn: C, indexPattern: IndexPattern, field: IndexPatternField) => C; +} + +/** + * Shape of an operation definition. If the type parameter of the definition + * indicates a field based column, `getPossibleOperationForField` has to be + * specified, otherwise `getPossibleOperationForDocument` has to be defined. + */ +export type OperationDefinition<C extends BaseIndexPatternColumn> = FieldBasedOperationDefinition< + C +>; + +// Helper to to infer the column type out of the operation definition. +// This is done to avoid it to have to list out the column types along with +// the operation definition types +type ColumnFromOperationDefinition<D> = D extends OperationDefinition<infer C> ? C : never; + +/** + * A union type of all available column types. If a column is of an unknown type somewhere + * withing the indexpattern data source it should be typed as `IndexPatternColumn` to make + * typeguards possible that consider all available column types. + */ +export type IndexPatternColumn = ColumnFromOperationDefinition< + typeof internalOperationDefinitions[number] +>; + +/** + * A union type of all available operation types. The operation type is a unique id of an operation. + * Each column is assigned to exactly one operation type. + */ +export type OperationType = typeof internalOperationDefinitions[number]['type']; + +/** + * This is an operation definition of an unspecified column out of all possible + * column types. + */ +export type GenericOperationDefinition = FieldBasedOperationDefinition<IndexPatternColumn>; + +/** + * List of all available operation definitions + */ +export const operationDefinitions = internalOperationDefinitions as GenericOperationDefinition[]; + +/** + * Map of all operation visible to consumers (e.g. the dimension panel). + * This simplifies the type of the map and makes it a simple list of unspecified + * operations definitions, because typescript can't infer the type correctly in most + * situations. + * + * If you need a specifically typed version of an operation (e.g. explicitly working with terms), + * you should import the definition directly from this file + * (e.g. `import { termsOperation } from './operations/definitions'`). This map is + * intended to be used in situations where the operation type is not known during compile time. + */ +export const operationDefinitionMap = internalOperationDefinitions.reduce( + (definitionMap, definition) => ({ ...definitionMap, [definition.type]: definition }), + {} +) as Record<OperationType, GenericOperationDefinition>; diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_datasource/operations/definitions/metrics.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/metrics.tsx similarity index 98% rename from x-pack/legacy/plugins/lens/public/indexpattern_datasource/operations/definitions/metrics.tsx rename to x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/metrics.tsx index c2d9478c6ea15..3da635dc13d10 100644 --- a/x-pack/legacy/plugins/lens/public/indexpattern_datasource/operations/definitions/metrics.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/metrics.tsx @@ -5,7 +5,7 @@ */ import { i18n } from '@kbn/i18n'; -import { OperationDefinition } from '.'; +import { OperationDefinition } from './index'; import { FormattedIndexPatternColumn } from './column_types'; type MetricColumn<T> = FormattedIndexPatternColumn & { diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_datasource/operations/definitions/terms.test.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/terms.test.tsx similarity index 98% rename from x-pack/legacy/plugins/lens/public/indexpattern_datasource/operations/definitions/terms.test.tsx rename to x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/terms.test.tsx index 226246714f18d..8f6130e74b5b8 100644 --- a/x-pack/legacy/plugins/lens/public/indexpattern_datasource/operations/definitions/terms.test.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/terms.test.tsx @@ -7,16 +7,14 @@ import React from 'react'; import { shallow } from 'enzyme'; import { EuiRange, EuiSelect } from '@elastic/eui'; -import { IUiSettingsClient, SavedObjectsClientContract, HttpSetup } from 'src/core/public'; +import { IUiSettingsClient, SavedObjectsClientContract, HttpSetup } from 'kibana/public'; import { IStorageWrapper } from 'src/plugins/kibana_utils/public'; -import { dataPluginMock } from '../../../../../../../../src/plugins/data/public/mocks'; +import { dataPluginMock } from '../../../../../../../src/plugins/data/public/mocks'; import { createMockedIndexPattern } from '../../mocks'; import { TermsIndexPatternColumn } from './terms'; -import { termsOperation } from '.'; +import { termsOperation } from './index'; import { IndexPatternPrivateState } from '../../types'; -jest.mock('ui/new_platform'); - const defaultProps = { storage: {} as IStorageWrapper, uiSettings: {} as IUiSettingsClient, @@ -274,7 +272,7 @@ describe('terms', () => { expect(updatedColumn).toBe(initialColumn); }); - it('should switch to alphabetical ordering if the order column is removed', () => { + it('should switch to alphabetical ordering if there are no columns to order by', () => { const termsColumn = termsOperation.onOtherColumnChanged!( { label: 'Top value of category', diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_datasource/operations/definitions/terms.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/terms.tsx similarity index 98% rename from x-pack/legacy/plugins/lens/public/indexpattern_datasource/operations/definitions/terms.tsx rename to x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/terms.tsx index cd0dcc0b7e9ce..7eb10456b2a6e 100644 --- a/x-pack/legacy/plugins/lens/public/indexpattern_datasource/operations/definitions/terms.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/terms.tsx @@ -10,7 +10,7 @@ import { EuiForm, EuiFormRow, EuiRange, EuiSelect } from '@elastic/eui'; import { IndexPatternColumn } from '../../indexpattern'; import { updateColumnParam } from '../../state_helpers'; import { DataType } from '../../../types'; -import { OperationDefinition } from '.'; +import { OperationDefinition } from './index'; import { FieldBasedIndexPatternColumn } from './column_types'; type PropType<C> = C extends React.ComponentType<infer P> ? P : unknown; @@ -53,6 +53,7 @@ export const termsOperation: OperationDefinition<TermsIndexPatternColumn> = { displayName: i18n.translate('xpack.lens.indexPattern.terms', { defaultMessage: 'Top values', }), + priority: 3, // Higher than any metric getPossibleOperationForField: ({ aggregationRestrictions, aggregatable, type }) => { if ( supportedTypes.has(type) && @@ -135,7 +136,7 @@ export const termsOperation: OperationDefinition<TermsIndexPatternColumn> = { } return currentColumn; }, - paramEditor: ({ state, setState, currentColumn, columnId: currentColumnId, layerId }) => { + paramEditor: ({ state, setState, currentColumn, layerId }) => { const SEPARATOR = '$$$'; function toValue(orderBy: TermsIndexPatternColumn['params']['orderBy']) { if (orderBy.type === 'alphabetical') { diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_datasource/operations/index.ts b/x-pack/plugins/lens/public/indexpattern_datasource/operations/index.ts similarity index 100% rename from x-pack/legacy/plugins/lens/public/indexpattern_datasource/operations/index.ts rename to x-pack/plugins/lens/public/indexpattern_datasource/operations/index.ts diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_datasource/operations/operations.test.ts b/x-pack/plugins/lens/public/indexpattern_datasource/operations/operations.test.ts similarity index 88% rename from x-pack/legacy/plugins/lens/public/indexpattern_datasource/operations/operations.test.ts rename to x-pack/plugins/lens/public/indexpattern_datasource/operations/operations.test.ts index 3602491c6eb2c..e5d20839aae3d 100644 --- a/x-pack/legacy/plugins/lens/public/indexpattern_datasource/operations/operations.test.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/operations.test.ts @@ -4,13 +4,11 @@ * you may not use this file except in compliance with the Elastic License. */ -import { getOperationTypesForField, getAvailableOperationsByMetadata, buildColumn } from '.'; -import { AvgIndexPatternColumn, MinIndexPatternColumn } from './definitions/metrics'; -import { CountIndexPatternColumn } from './definitions/count'; +import { getOperationTypesForField, getAvailableOperationsByMetadata, buildColumn } from './index'; +import { AvgIndexPatternColumn } from './definitions/metrics'; import { IndexPatternPrivateState } from '../types'; import { documentField } from '../document_field'; -jest.mock('ui/new_platform'); jest.mock('../loader'); const expectedIndexPatterns = { @@ -198,33 +196,21 @@ describe('getOperationTypesForField', () => { expect(column.operationType).toEqual('avg'); expect(column.sourceField).toEqual(field.name); }); - - it('should pick a suitable field operation if none is passed in', () => { - const field = expectedIndexPatterns[1].fields[1]; - const column = buildColumn({ - layerId: 'first', - indexPattern: expectedIndexPatterns[1], - columns: state.layers.first.columns, - suggestedPriority: 0, - field, - }) as MinIndexPatternColumn; - expect(column.operationType).toEqual('avg'); - expect(column.sourceField).toEqual(field.name); - }); - - it('should pick a suitable document operation if none is passed in', () => { - const column = buildColumn({ - layerId: 'first', - indexPattern: expectedIndexPatterns[1], - columns: state.layers.first.columns, - suggestedPriority: 0, - field: documentField, - }) as CountIndexPatternColumn; - expect(column.operationType).toEqual('count'); - }); }); describe('getAvailableOperationsByMetaData', () => { + it('should put the average operation first', () => { + const numberOperation = getAvailableOperationsByMetadata(expectedIndexPatterns[1]).find( + ({ operationMetaData }) => + !operationMetaData.isBucketed && operationMetaData.dataType === 'number' + )!; + expect(numberOperation.operations[0]).toEqual( + expect.objectContaining({ + operationType: 'avg', + }) + ); + }); + it('should list out all field-operation tuples for different operation meta data', () => { expect(getAvailableOperationsByMetadata(expectedIndexPatterns[1])).toMatchInlineSnapshot(` Array [ @@ -279,17 +265,22 @@ describe('getOperationTypesForField', () => { "operations": Array [ Object { "field": "bytes", - "operationType": "min", + "operationType": "avg", "type": "field", }, Object { "field": "bytes", - "operationType": "max", + "operationType": "sum", "type": "field", }, Object { "field": "bytes", - "operationType": "avg", + "operationType": "min", + "type": "field", + }, + Object { + "field": "bytes", + "operationType": "max", "type": "field", }, Object { @@ -307,11 +298,6 @@ describe('getOperationTypesForField', () => { "operationType": "cardinality", "type": "field", }, - Object { - "field": "bytes", - "operationType": "sum", - "type": "field", - }, ], }, ] diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_datasource/operations/operations.ts b/x-pack/plugins/lens/public/indexpattern_datasource/operations/operations.ts similarity index 89% rename from x-pack/legacy/plugins/lens/public/indexpattern_datasource/operations/operations.ts rename to x-pack/plugins/lens/public/indexpattern_datasource/operations/operations.ts index ce8ea55c445dc..dbcd4eac7fd59 100644 --- a/x-pack/legacy/plugins/lens/public/indexpattern_datasource/operations/operations.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/operations.ts @@ -52,20 +52,22 @@ export function getOperationDisplay() { return display; } +function getSortScoreByPriority(a: GenericOperationDefinition, b: GenericOperationDefinition) { + return (b.priority || Number.NEGATIVE_INFINITY) - (a.priority || Number.NEGATIVE_INFINITY); +} + /** * Returns all `OperationType`s that can build a column using `buildColumn` based on the * passed in field. */ -export function getOperationTypesForField(field: IndexPatternField) { +export function getOperationTypesForField(field: IndexPatternField): OperationType[] { return operationDefinitions .filter( operationDefinition => 'getPossibleOperationForField' in operationDefinition && operationDefinition.getPossibleOperationForField(field) ) - .sort( - (a, b) => (b.priority || Number.NEGATIVE_INFINITY) - (a.priority || Number.NEGATIVE_INFINITY) - ) + .sort(getSortScoreByPriority) .map(({ type }) => type); } @@ -131,7 +133,7 @@ export function getAvailableOperationsByMetadata(indexPattern: IndexPattern) { } }; - operationDefinitions.forEach(operationDefinition => { + operationDefinitions.sort(getSortScoreByPriority).forEach(operationDefinition => { indexPattern.fields.forEach(field => { addToMap( { @@ -156,13 +158,6 @@ function getPossibleOperationForField( : undefined; } -function getDefinition(findFunction: (definition: GenericOperationDefinition) => boolean) { - const candidates = operationDefinitions.filter(findFunction); - return candidates.reduce((a, b) => - (a.priority || Number.NEGATIVE_INFINITY) > (b.priority || Number.NEGATIVE_INFINITY) ? a : b - ); -} - /** * Changes the field of the passed in colum. To do so, this method uses the `onFieldChange` function of * the operation definition of the column. Returns a new column object with the field changed. @@ -204,7 +199,7 @@ export function buildColumn({ suggestedPriority, previousColumn, }: { - op?: OperationType; + op: OperationType; columns: Partial<Record<string, IndexPatternColumn>>; suggestedPriority: DimensionPriority | undefined; layerId: string; @@ -212,15 +207,7 @@ export function buildColumn({ field: IndexPatternField; previousColumn?: IndexPatternColumn; }): IndexPatternColumn { - let operationDefinition: GenericOperationDefinition | undefined; - - if (op) { - operationDefinition = operationDefinitionMap[op]; - } else if (field) { - operationDefinition = getDefinition(definition => - Boolean(getPossibleOperationForField(definition, field)) - ); - } + const operationDefinition = operationDefinitionMap[op]; if (!operationDefinition) { throw new Error('No suitable operation found for given parameters'); diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_datasource/pure_helpers.test.ts b/x-pack/plugins/lens/public/indexpattern_datasource/pure_helpers.test.ts similarity index 100% rename from x-pack/legacy/plugins/lens/public/indexpattern_datasource/pure_helpers.test.ts rename to x-pack/plugins/lens/public/indexpattern_datasource/pure_helpers.test.ts diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_datasource/pure_helpers.ts b/x-pack/plugins/lens/public/indexpattern_datasource/pure_helpers.ts similarity index 100% rename from x-pack/legacy/plugins/lens/public/indexpattern_datasource/pure_helpers.ts rename to x-pack/plugins/lens/public/indexpattern_datasource/pure_helpers.ts diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_datasource/rename_columns.test.ts b/x-pack/plugins/lens/public/indexpattern_datasource/rename_columns.test.ts similarity index 96% rename from x-pack/legacy/plugins/lens/public/indexpattern_datasource/rename_columns.test.ts rename to x-pack/plugins/lens/public/indexpattern_datasource/rename_columns.test.ts index 9da7591305a6c..4bfd6a4f93c75 100644 --- a/x-pack/legacy/plugins/lens/public/indexpattern_datasource/rename_columns.test.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/rename_columns.test.ts @@ -5,8 +5,8 @@ */ import { renameColumns } from './rename_columns'; -import { KibanaDatatable } from '../../../../../../src/plugins/expressions/public'; -import { createMockExecutionContext } from '../../../../../../src/plugins/expressions/common/mocks'; +import { KibanaDatatable } from '../../../../../src/plugins/expressions/public'; +import { createMockExecutionContext } from '../../../../../src/plugins/expressions/common/mocks'; describe('rename_columns', () => { it('should rename columns of a given datatable', () => { diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_datasource/rename_columns.ts b/x-pack/plugins/lens/public/indexpattern_datasource/rename_columns.ts similarity index 100% rename from x-pack/legacy/plugins/lens/public/indexpattern_datasource/rename_columns.ts rename to x-pack/plugins/lens/public/indexpattern_datasource/rename_columns.ts diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_datasource/state_helpers.test.ts b/x-pack/plugins/lens/public/indexpattern_datasource/state_helpers.test.ts similarity index 99% rename from x-pack/legacy/plugins/lens/public/indexpattern_datasource/state_helpers.test.ts rename to x-pack/plugins/lens/public/indexpattern_datasource/state_helpers.test.ts index 0a58853f1ef4f..1e3251a8dedd8 100644 --- a/x-pack/legacy/plugins/lens/public/indexpattern_datasource/state_helpers.test.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/state_helpers.test.ts @@ -17,7 +17,6 @@ import { DateHistogramIndexPatternColumn } from './operations/definitions/date_h import { AvgIndexPatternColumn } from './operations/definitions/metrics'; import { IndexPattern, IndexPatternPrivateState, IndexPatternLayer } from './types'; -jest.mock('ui/new_platform'); jest.mock('./operations'); describe('state_helpers', () => { diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_datasource/state_helpers.ts b/x-pack/plugins/lens/public/indexpattern_datasource/state_helpers.ts similarity index 100% rename from x-pack/legacy/plugins/lens/public/indexpattern_datasource/state_helpers.ts rename to x-pack/plugins/lens/public/indexpattern_datasource/state_helpers.ts diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/to_expression.ts b/x-pack/plugins/lens/public/indexpattern_datasource/to_expression.ts new file mode 100644 index 0000000000000..3ab51b5fa3f2b --- /dev/null +++ b/x-pack/plugins/lens/public/indexpattern_datasource/to_expression.ts @@ -0,0 +1,123 @@ +/* + * 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 _ from 'lodash'; +import { Ast, ExpressionFunctionAST } from '@kbn/interpreter/common'; +import { IndexPatternColumn } from './indexpattern'; +import { operationDefinitionMap } from './operations'; +import { IndexPattern, IndexPatternPrivateState } from './types'; +import { OriginalColumn } from './rename_columns'; + +function getExpressionForLayer( + indexPattern: IndexPattern, + columns: Record<string, IndexPatternColumn>, + columnOrder: string[] +): Ast | null { + if (columnOrder.length === 0) { + return null; + } + + function getEsAggsConfig<C extends IndexPatternColumn>(column: C, columnId: string) { + return operationDefinitionMap[column.operationType].toEsAggsConfig(column, columnId); + } + + const columnEntries = columnOrder.map(colId => [colId, columns[colId]] as const); + + if (columnEntries.length) { + const aggs = columnEntries.map(([colId, col]) => { + return getEsAggsConfig(col, colId); + }); + + const idMap = columnEntries.reduce((currentIdMap, [colId], index) => { + return { + ...currentIdMap, + [`col-${index}-${colId}`]: { + ...columns[colId], + id: colId, + }, + }; + }, {} as Record<string, OriginalColumn>); + + type FormattedColumn = Required<Extract<IndexPatternColumn, { params?: { format: unknown } }>>; + + const columnsWithFormatters = columnEntries.filter( + ([, col]) => col.params && 'format' in col.params && col.params.format + ) as Array<[string, FormattedColumn]>; + const formatterOverrides: ExpressionFunctionAST[] = columnsWithFormatters.map(([id, col]) => { + const format = (col as FormattedColumn).params!.format; + const base: ExpressionFunctionAST = { + type: 'function', + function: 'lens_format_column', + arguments: { + format: [format.id], + columnId: [id], + }, + }; + if (typeof format.params?.decimals === 'number') { + return { + ...base, + arguments: { + ...base.arguments, + decimals: [format.params.decimals], + }, + }; + } + return base; + }); + + return { + type: 'expression', + chain: [ + { + type: 'function', + function: 'esaggs', + arguments: { + index: [indexPattern.id], + metricsAtAllLevels: [false], + partialRows: [false], + includeFormatHints: [true], + aggConfigs: [ + { + type: 'expression', + chain: [ + { + type: 'function', + function: 'lens_auto_date', + arguments: { + aggConfigs: [JSON.stringify(aggs)], + }, + }, + ], + }, + ], + }, + }, + { + type: 'function', + function: 'lens_rename_columns', + arguments: { + idMap: [JSON.stringify(idMap)], + }, + }, + ...formatterOverrides, + ], + }; + } + + return null; +} + +export function toExpression(state: IndexPatternPrivateState, layerId: string) { + if (state.layers[layerId]) { + return getExpressionForLayer( + state.indexPatterns[state.layers[layerId].indexPatternId], + state.layers[layerId].columns, + state.layers[layerId].columnOrder + ); + } + + return null; +} diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/types.ts b/x-pack/plugins/lens/public/indexpattern_datasource/types.ts new file mode 100644 index 0000000000000..563af40ed2720 --- /dev/null +++ b/x-pack/plugins/lens/public/indexpattern_datasource/types.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 { IndexPatternColumn } from './operations'; +import { IndexPatternAggRestrictions } from '../../../../../src/plugins/data/public'; + +export interface IndexPattern { + id: string; + fields: IndexPatternField[]; + title: string; + timeFieldName?: string | null; + fieldFormatMap?: Record< + string, + { + id: string; + params: unknown; + } + >; +} + +export interface IndexPatternField { + name: string; + type: string; + esTypes?: string[]; + aggregatable: boolean; + scripted?: boolean; + searchable: boolean; + aggregationRestrictions?: Partial<IndexPatternAggRestrictions>; +} + +export interface IndexPatternLayer { + columnOrder: string[]; + columns: Record<string, IndexPatternColumn>; + // Each layer is tied to the index pattern that created it + indexPatternId: string; +} + +export interface IndexPatternPersistedState { + currentIndexPatternId: string; + layers: Record<string, IndexPatternLayer>; +} + +export type IndexPatternPrivateState = IndexPatternPersistedState & { + indexPatternRefs: IndexPatternRef[]; + indexPatterns: Record<string, IndexPattern>; + + /** + * indexPatternId -> fieldName -> boolean + */ + existingFields: Record<string, Record<string, boolean>>; + showEmptyFields: boolean; +}; + +export interface IndexPatternRef { + id: string; + title: string; +} diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_datasource/utils.ts b/x-pack/plugins/lens/public/indexpattern_datasource/utils.ts similarity index 100% rename from x-pack/legacy/plugins/lens/public/indexpattern_datasource/utils.ts rename to x-pack/plugins/lens/public/indexpattern_datasource/utils.ts diff --git a/x-pack/legacy/plugins/lens/public/lens_ui_telemetry/factory.test.ts b/x-pack/plugins/lens/public/lens_ui_telemetry/factory.test.ts similarity index 100% rename from x-pack/legacy/plugins/lens/public/lens_ui_telemetry/factory.test.ts rename to x-pack/plugins/lens/public/lens_ui_telemetry/factory.test.ts diff --git a/x-pack/plugins/lens/public/lens_ui_telemetry/factory.ts b/x-pack/plugins/lens/public/lens_ui_telemetry/factory.ts new file mode 100644 index 0000000000000..10b052c66efed --- /dev/null +++ b/x-pack/plugins/lens/public/lens_ui_telemetry/factory.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 moment from 'moment'; +import { HttpSetup } from 'kibana/public'; + +import { IStorageWrapper } from 'src/plugins/kibana_utils/public'; +import { BASE_API_URL } from '../../common'; + +const STORAGE_KEY = 'lens-ui-telemetry'; + +let reportManager: LensReportManager; + +export function setReportManager(newManager: LensReportManager) { + if (reportManager) { + reportManager.stop(); + } + reportManager = newManager; +} + +export function stopReportManager() { + if (reportManager) { + reportManager.stop(); + } +} + +export function trackUiEvent(name: string) { + if (reportManager) { + reportManager.trackEvent(name); + } +} + +export function trackSuggestionEvent(name: string) { + if (reportManager) { + reportManager.trackSuggestionEvent(name); + } +} + +export class LensReportManager { + private events: Record<string, Record<string, number>> = {}; + private suggestionEvents: Record<string, Record<string, number>> = {}; + + private storage: IStorageWrapper; + private http: HttpSetup; + private timer: ReturnType<typeof setInterval>; + + constructor({ storage, http }: { storage: IStorageWrapper; http: HttpSetup }) { + this.storage = storage; + this.http = http; + + this.readFromStorage(); + + this.timer = setInterval(() => { + this.postToServer(); + }, 10000); + } + + public trackEvent(name: string) { + this.readFromStorage(); + this.trackTo(this.events, name); + } + + public trackSuggestionEvent(name: string) { + this.readFromStorage(); + this.trackTo(this.suggestionEvents, name); + } + + public stop() { + if (this.timer) { + clearInterval(this.timer); + } + } + + private readFromStorage() { + const data = this.storage.get(STORAGE_KEY); + if (data && typeof data.events === 'object' && typeof data.suggestionEvents === 'object') { + this.events = data.events; + this.suggestionEvents = data.suggestionEvents; + } + } + + private async postToServer() { + this.readFromStorage(); + if (Object.keys(this.events).length || Object.keys(this.suggestionEvents).length) { + try { + await this.http.post(`${BASE_API_URL}/telemetry`, { + body: JSON.stringify({ + events: this.events, + suggestionEvents: this.suggestionEvents, + }), + }); + this.events = {}; + this.suggestionEvents = {}; + this.write(); + } catch (e) { + // Silent error because events will be reported during the next timer + } + } + } + + private trackTo(target: Record<string, Record<string, number>>, name: string) { + const date = moment() + .utc() + .format('YYYY-MM-DD'); + if (!target[date]) { + target[date] = { + [name]: 1, + }; + } else if (!target[date][name]) { + target[date][name] = 1; + } else { + target[date][name] += 1; + } + + this.write(); + } + + private write() { + this.storage.set(STORAGE_KEY, { events: this.events, suggestionEvents: this.suggestionEvents }); + } +} diff --git a/x-pack/legacy/plugins/lens/public/lens_ui_telemetry/index.ts b/x-pack/plugins/lens/public/lens_ui_telemetry/index.ts similarity index 100% rename from x-pack/legacy/plugins/lens/public/lens_ui_telemetry/index.ts rename to x-pack/plugins/lens/public/lens_ui_telemetry/index.ts diff --git a/x-pack/legacy/plugins/lens/public/loader.test.tsx b/x-pack/plugins/lens/public/loader.test.tsx similarity index 100% rename from x-pack/legacy/plugins/lens/public/loader.test.tsx rename to x-pack/plugins/lens/public/loader.test.tsx diff --git a/x-pack/legacy/plugins/lens/public/loader.tsx b/x-pack/plugins/lens/public/loader.tsx similarity index 100% rename from x-pack/legacy/plugins/lens/public/loader.tsx rename to x-pack/plugins/lens/public/loader.tsx diff --git a/x-pack/legacy/plugins/lens/public/metric_visualization/auto_scale.test.tsx b/x-pack/plugins/lens/public/metric_visualization/auto_scale.test.tsx similarity index 100% rename from x-pack/legacy/plugins/lens/public/metric_visualization/auto_scale.test.tsx rename to x-pack/plugins/lens/public/metric_visualization/auto_scale.test.tsx diff --git a/x-pack/legacy/plugins/lens/public/metric_visualization/auto_scale.tsx b/x-pack/plugins/lens/public/metric_visualization/auto_scale.tsx similarity index 100% rename from x-pack/legacy/plugins/lens/public/metric_visualization/auto_scale.tsx rename to x-pack/plugins/lens/public/metric_visualization/auto_scale.tsx diff --git a/x-pack/legacy/plugins/lens/public/metric_visualization/index.scss b/x-pack/plugins/lens/public/metric_visualization/index.scss similarity index 100% rename from x-pack/legacy/plugins/lens/public/metric_visualization/index.scss rename to x-pack/plugins/lens/public/metric_visualization/index.scss diff --git a/x-pack/plugins/lens/public/metric_visualization/index.ts b/x-pack/plugins/lens/public/metric_visualization/index.ts new file mode 100644 index 0000000000000..2960da52191e4 --- /dev/null +++ b/x-pack/plugins/lens/public/metric_visualization/index.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 { CoreSetup } from 'kibana/public'; +import { metricVisualization } from './metric_visualization'; +import { ExpressionsSetup } from '../../../../../src/plugins/expressions/public'; +import { metricChart, getMetricChartRenderer } from './metric_expression'; +import { EditorFrameSetup, FormatFactory } from '../types'; + +export interface MetricVisualizationPluginSetupPlugins { + expressions: ExpressionsSetup; + formatFactory: Promise<FormatFactory>; + editorFrame: EditorFrameSetup; +} + +export class MetricVisualization { + constructor() {} + + setup( + _core: CoreSetup | null, + { expressions, formatFactory, editorFrame }: MetricVisualizationPluginSetupPlugins + ) { + expressions.registerFunction(() => metricChart); + + expressions.registerRenderer(() => getMetricChartRenderer(formatFactory)); + + editorFrame.registerVisualization(metricVisualization); + } +} diff --git a/x-pack/legacy/plugins/lens/public/metric_visualization/metric_expression.test.tsx b/x-pack/plugins/lens/public/metric_visualization/metric_expression.test.tsx similarity index 94% rename from x-pack/legacy/plugins/lens/public/metric_visualization/metric_expression.test.tsx rename to x-pack/plugins/lens/public/metric_visualization/metric_expression.test.tsx index 3da38d486aecd..2406e7cd42ebc 100644 --- a/x-pack/legacy/plugins/lens/public/metric_visualization/metric_expression.test.tsx +++ b/x-pack/plugins/lens/public/metric_visualization/metric_expression.test.tsx @@ -9,8 +9,8 @@ import { LensMultiTable } from '../types'; import React from 'react'; import { shallow } from 'enzyme'; import { MetricConfig } from './types'; -import { createMockExecutionContext } from '../../../../../../src/plugins/expressions/common/mocks'; -import { IFieldFormat } from '../../../../../../src/plugins/data/public'; +import { createMockExecutionContext } from '../../../../../src/plugins/expressions/common/mocks'; +import { IFieldFormat } from '../../../../../src/plugins/data/public'; function sampleArgs() { const data: LensMultiTable = { diff --git a/x-pack/legacy/plugins/lens/public/metric_visualization/metric_expression.tsx b/x-pack/plugins/lens/public/metric_visualization/metric_expression.tsx similarity index 98% rename from x-pack/legacy/plugins/lens/public/metric_visualization/metric_expression.tsx rename to x-pack/plugins/lens/public/metric_visualization/metric_expression.tsx index a80552e57a9e0..3484837f65b43 100644 --- a/x-pack/legacy/plugins/lens/public/metric_visualization/metric_expression.tsx +++ b/x-pack/plugins/lens/public/metric_visualization/metric_expression.tsx @@ -10,7 +10,7 @@ import { ExpressionFunctionDefinition, ExpressionRenderDefinition, IInterpreterRenderHandlers, -} from '../../../../../../src/plugins/expressions/public'; +} from '../../../../../src/plugins/expressions/public'; import { MetricConfig } from './types'; import { FormatFactory, LensMultiTable } from '../types'; import { AutoScale } from './auto_scale'; diff --git a/x-pack/legacy/plugins/lens/public/metric_visualization/metric_suggestions.test.ts b/x-pack/plugins/lens/public/metric_visualization/metric_suggestions.test.ts similarity index 98% rename from x-pack/legacy/plugins/lens/public/metric_visualization/metric_suggestions.test.ts rename to x-pack/plugins/lens/public/metric_visualization/metric_suggestions.test.ts index c9bfadbefaf5f..ef93f0b5bf064 100644 --- a/x-pack/legacy/plugins/lens/public/metric_visualization/metric_suggestions.test.ts +++ b/x-pack/plugins/lens/public/metric_visualization/metric_suggestions.test.ts @@ -5,7 +5,7 @@ */ import { getSuggestions } from './metric_suggestions'; -import { TableSuggestionColumn, TableSuggestion } from '..'; +import { TableSuggestionColumn, TableSuggestion } from '../index'; describe('metric_suggestions', () => { function numCol(columnId: string): TableSuggestionColumn { diff --git a/x-pack/legacy/plugins/lens/public/metric_visualization/metric_suggestions.ts b/x-pack/plugins/lens/public/metric_visualization/metric_suggestions.ts similarity index 100% rename from x-pack/legacy/plugins/lens/public/metric_visualization/metric_suggestions.ts rename to x-pack/plugins/lens/public/metric_visualization/metric_suggestions.ts diff --git a/x-pack/legacy/plugins/lens/public/metric_visualization/metric_visualization.test.ts b/x-pack/plugins/lens/public/metric_visualization/metric_visualization.test.ts similarity index 100% rename from x-pack/legacy/plugins/lens/public/metric_visualization/metric_visualization.test.ts rename to x-pack/plugins/lens/public/metric_visualization/metric_visualization.test.ts diff --git a/x-pack/legacy/plugins/lens/public/metric_visualization/metric_visualization.tsx b/x-pack/plugins/lens/public/metric_visualization/metric_visualization.tsx similarity index 100% rename from x-pack/legacy/plugins/lens/public/metric_visualization/metric_visualization.tsx rename to x-pack/plugins/lens/public/metric_visualization/metric_visualization.tsx diff --git a/x-pack/legacy/plugins/lens/public/metric_visualization/types.ts b/x-pack/plugins/lens/public/metric_visualization/types.ts similarity index 100% rename from x-pack/legacy/plugins/lens/public/metric_visualization/types.ts rename to x-pack/plugins/lens/public/metric_visualization/types.ts diff --git a/x-pack/legacy/plugins/lens/public/native_renderer/index.ts b/x-pack/plugins/lens/public/native_renderer/index.ts similarity index 100% rename from x-pack/legacy/plugins/lens/public/native_renderer/index.ts rename to x-pack/plugins/lens/public/native_renderer/index.ts diff --git a/x-pack/legacy/plugins/lens/public/native_renderer/native_renderer.test.tsx b/x-pack/plugins/lens/public/native_renderer/native_renderer.test.tsx similarity index 100% rename from x-pack/legacy/plugins/lens/public/native_renderer/native_renderer.test.tsx rename to x-pack/plugins/lens/public/native_renderer/native_renderer.test.tsx diff --git a/x-pack/legacy/plugins/lens/public/native_renderer/native_renderer.tsx b/x-pack/plugins/lens/public/native_renderer/native_renderer.tsx similarity index 100% rename from x-pack/legacy/plugins/lens/public/native_renderer/native_renderer.tsx rename to x-pack/plugins/lens/public/native_renderer/native_renderer.tsx diff --git a/x-pack/legacy/plugins/lens/public/persistence/index.ts b/x-pack/plugins/lens/public/persistence/index.ts similarity index 100% rename from x-pack/legacy/plugins/lens/public/persistence/index.ts rename to x-pack/plugins/lens/public/persistence/index.ts diff --git a/x-pack/legacy/plugins/lens/public/persistence/saved_object_store.test.ts b/x-pack/plugins/lens/public/persistence/saved_object_store.test.ts similarity index 100% rename from x-pack/legacy/plugins/lens/public/persistence/saved_object_store.test.ts rename to x-pack/plugins/lens/public/persistence/saved_object_store.test.ts diff --git a/x-pack/legacy/plugins/lens/public/persistence/saved_object_store.ts b/x-pack/plugins/lens/public/persistence/saved_object_store.ts similarity index 94% rename from x-pack/legacy/plugins/lens/public/persistence/saved_object_store.ts rename to x-pack/plugins/lens/public/persistence/saved_object_store.ts index ac0b3322b400e..015f4b9b825f4 100644 --- a/x-pack/legacy/plugins/lens/public/persistence/saved_object_store.ts +++ b/x-pack/plugins/lens/public/persistence/saved_object_store.ts @@ -5,8 +5,8 @@ */ // eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { SavedObjectAttributes } from 'src/core/server'; -import { Query, Filter } from '../../../../../../src/plugins/data/public'; +import { SavedObjectAttributes } from 'kibana/server'; +import { Query, Filter } from '../../../../../src/plugins/data/public'; export interface Document { id?: string; diff --git a/x-pack/plugins/lens/public/plugin.tsx b/x-pack/plugins/lens/public/plugin.tsx new file mode 100644 index 0000000000000..8d760eb0df501 --- /dev/null +++ b/x-pack/plugins/lens/public/plugin.tsx @@ -0,0 +1,208 @@ +/* + * 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 { FormattedMessage, I18nProvider } from '@kbn/i18n/react'; +import { HashRouter, Route, RouteComponentProps, Switch } from 'react-router-dom'; +import { render, unmountComponentAtNode } from 'react-dom'; +import rison, { RisonObject, RisonValue } from 'rison-node'; +import { isObject } from 'lodash'; + +import { AppMountParameters, CoreSetup, CoreStart } from 'kibana/public'; +import { DataPublicPluginSetup, DataPublicPluginStart } from 'src/plugins/data/public'; +import { EmbeddableSetup, EmbeddableStart } from 'src/plugins/embeddable/public'; +import { ExpressionsSetup, ExpressionsStart } from 'src/plugins/expressions/public'; +import { VisualizationsSetup } from 'src/plugins/visualizations/public'; +import { NavigationPublicPluginStart } from 'src/plugins/navigation/public'; +import { KibanaLegacySetup } from 'src/plugins/kibana_legacy/public'; +import { DashboardConstants } from '../../../../src/plugins/dashboard/public'; +import { Storage } from '../../../../src/plugins/kibana_utils/public'; +import { EditorFrameService } from './editor_frame_service'; +import { IndexPatternDatasource } from './indexpattern_datasource'; +import { addHelpMenuToAppChrome } from './help_menu_util'; +import { SavedObjectIndexStore } from './persistence'; +import { XyVisualization } from './xy_visualization'; +import { MetricVisualization } from './metric_visualization'; +import { DatatableVisualization } from './datatable_visualization'; +import { App } from './app_plugin'; +import { + LensReportManager, + setReportManager, + stopReportManager, + trackUiEvent, +} from './lens_ui_telemetry'; + +import { UiActionsStart } from '../../../../src/plugins/ui_actions/public'; +import { NOT_INTERNATIONALIZED_PRODUCT_NAME } from '../common'; +import { addEmbeddableToDashboardUrl, getUrlVars } from './helpers'; +import { EditorFrameStart } from './types'; +import { getLensAliasConfig } from './vis_type_alias'; + +import './index.scss'; + +export interface LensPluginSetupDependencies { + kibanaLegacy: KibanaLegacySetup; + expressions: ExpressionsSetup; + data: DataPublicPluginSetup; + embeddable?: EmbeddableSetup; + visualizations: VisualizationsSetup; +} + +export interface LensPluginStartDependencies { + data: DataPublicPluginStart; + embeddable: EmbeddableStart; + expressions: ExpressionsStart; + navigation: NavigationPublicPluginStart; + uiActions: UiActionsStart; +} + +export const isRisonObject = (value: RisonValue): value is RisonObject => { + return isObject(value); +}; +export class LensPlugin { + private datatableVisualization: DatatableVisualization; + private editorFrameService: EditorFrameService; + private createEditorFrame: EditorFrameStart['createInstance'] | null = null; + private indexpatternDatasource: IndexPatternDatasource; + private xyVisualization: XyVisualization; + private metricVisualization: MetricVisualization; + + constructor() { + this.datatableVisualization = new DatatableVisualization(); + this.editorFrameService = new EditorFrameService(); + this.indexpatternDatasource = new IndexPatternDatasource(); + this.xyVisualization = new XyVisualization(); + this.metricVisualization = new MetricVisualization(); + } + + setup( + core: CoreSetup<LensPluginStartDependencies, void>, + { kibanaLegacy, expressions, data, embeddable, visualizations }: LensPluginSetupDependencies + ) { + const editorFrameSetupInterface = this.editorFrameService.setup(core, { + data, + embeddable, + expressions, + }); + const dependencies = { + expressions, + data, + editorFrame: editorFrameSetupInterface, + formatFactory: core + .getStartServices() + .then(([_, { data: dataStart }]) => dataStart.fieldFormats.deserialize), + }; + this.indexpatternDatasource.setup(core, dependencies); + this.xyVisualization.setup(core, dependencies); + this.datatableVisualization.setup(core, dependencies); + this.metricVisualization.setup(core, dependencies); + + visualizations.registerAlias(getLensAliasConfig()); + + kibanaLegacy.registerLegacyApp({ + id: 'lens', + title: NOT_INTERNATIONALIZED_PRODUCT_NAME, + mount: async (params: AppMountParameters) => { + const [coreStart, startDependencies] = await core.getStartServices(); + const { data: dataStart, navigation } = startDependencies; + const savedObjectsClient = coreStart.savedObjects.client; + addHelpMenuToAppChrome(coreStart.chrome, coreStart.docLinks); + + const instance = await this.createEditorFrame!(); + + setReportManager( + new LensReportManager({ + storage: new Storage(localStorage), + http: core.http, + }) + ); + const updateUrlTime = (urlVars: Record<string, string>): void => { + const decoded = rison.decode(urlVars._g); + if (!isRisonObject(decoded)) { + return; + } + // @ts-ignore + decoded.time = dataStart.query.timefilter.timefilter.getTime(); + urlVars._g = rison.encode(decoded); + }; + const redirectTo = ( + routeProps: RouteComponentProps<{ id?: string }>, + addToDashboardMode: boolean, + id?: string + ) => { + if (!id) { + routeProps.history.push('/lens'); + } else if (!addToDashboardMode) { + routeProps.history.push(`/lens/edit/${id}`); + } else if (addToDashboardMode && id) { + routeProps.history.push(`/lens/edit/${id}`); + const lastDashboardLink = coreStart.chrome.navLinks.get('kibana:dashboard'); + if (!lastDashboardLink || !lastDashboardLink.url) { + throw new Error('Cannot get last dashboard url'); + } + const urlVars = getUrlVars(lastDashboardLink.url); + updateUrlTime(urlVars); // we need to pass in timerange in query params directly + const dashboardUrl = addEmbeddableToDashboardUrl(lastDashboardLink.url, id, urlVars); + window.history.pushState({}, '', dashboardUrl); + } + }; + + const renderEditor = (routeProps: RouteComponentProps<{ id?: string }>) => { + trackUiEvent('loaded'); + const addToDashboardMode = + !!routeProps.location.search && + routeProps.location.search.includes( + DashboardConstants.ADD_VISUALIZATION_TO_DASHBOARD_MODE_PARAM + ); + return ( + <App + core={coreStart} + data={dataStart} + navigation={navigation} + editorFrame={instance} + storage={new Storage(localStorage)} + docId={routeProps.match.params.id} + docStorage={new SavedObjectIndexStore(savedObjectsClient)} + redirectTo={id => redirectTo(routeProps, addToDashboardMode, id)} + addToDashboardMode={addToDashboardMode} + /> + ); + }; + + function NotFound() { + trackUiEvent('loaded_404'); + return <FormattedMessage id="xpack.lens.app404" defaultMessage="404 Not Found" />; + } + + render( + <I18nProvider> + <HashRouter> + <Switch> + <Route exact path="/lens/edit/:id" render={renderEditor} /> + <Route exact path="/lens" render={renderEditor} /> + <Route path="/lens" component={NotFound} /> + </Switch> + </HashRouter> + </I18nProvider>, + params.element + ); + return () => { + instance.unmount(); + unmountComponentAtNode(params.element); + }; + }, + }); + } + + start(core: CoreStart, startDependencies: LensPluginStartDependencies) { + this.createEditorFrame = this.editorFrameService.start(core, startDependencies).createInstance; + this.xyVisualization.start(core, startDependencies); + } + + stop() { + stopReportManager(); + } +} diff --git a/x-pack/plugins/lens/public/types.ts b/x-pack/plugins/lens/public/types.ts new file mode 100644 index 0000000000000..0b432c0c70727 --- /dev/null +++ b/x-pack/plugins/lens/public/types.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 { Ast } from '@kbn/interpreter/common'; +import { IconType } from '@elastic/eui/src/components/icon/icon'; +import { CoreSetup } from 'kibana/public'; +import { KibanaDatatable, SerializedFieldFormat } from '../../../../src/plugins/expressions/public'; +import { DragContextState } from './drag_drop'; +import { Document } from './persistence'; +import { DateRange } from '../common'; +import { Query, Filter, SavedQuery, IFieldFormat } from '../../../../src/plugins/data/public'; + +export type ErrorCallback = (e: { message: string }) => void; + +export type FormatFactory = (mapping?: SerializedFieldFormat) => IFieldFormat; + +export interface PublicAPIProps<T> { + state: T; + layerId: string; + dateRange: DateRange; +} + +export interface EditorFrameProps { + onError: ErrorCallback; + doc?: Document; + dateRange: DateRange; + query: Query; + filters: Filter[]; + savedQuery?: SavedQuery; + + // Frame loader (app or embeddable) is expected to call this when it loads and updates + // This should be replaced with a top-down state + onChange: (newState: { + filterableIndexPatterns: DatasourceMetaData['filterableIndexPatterns']; + doc: Document; + }) => void; +} +export interface EditorFrameInstance { + mount: (element: Element, props: EditorFrameProps) => void; + unmount: () => void; +} + +export interface EditorFrameSetup { + // generic type on the API functions to pull the "unknown vs. specific type" error into the implementation + registerDatasource: <T, P>(datasource: Datasource<T, P> | Promise<Datasource<T, P>>) => void; + registerVisualization: <T, P>( + visualization: Visualization<T, P> | Promise<Visualization<T, P>> + ) => void; +} + +export interface EditorFrameStart { + createInstance: () => Promise<EditorFrameInstance>; +} + +// Hints the default nesting to the data source. 0 is the highest priority +export type DimensionPriority = 0 | 1 | 2; + +export interface TableSuggestionColumn { + columnId: string; + operation: Operation; +} + +/** + * A possible table a datasource can create. This object is passed to the visualization + * which tries to build a meaningful visualization given the shape of the table. If this + * is possible, the visualization returns a `VisualizationSuggestion` object + */ +export interface TableSuggestion { + /** + * Flag indicating whether the table will include more than one column. + * This is not the case for example for a single metric aggregation + * */ + isMultiRow: boolean; + /** + * The columns of the table. Each column has to be mapped to a dimension in a chart. If a visualization + * can't use all columns of a suggestion, it should not return a `VisualizationSuggestion` based on it + * because there would be unreferenced columns + */ + columns: TableSuggestionColumn[]; + /** + * The layer this table will replace. This is only relevant if the visualization this suggestion is passed + * is currently active and has multiple layers configured. If this suggestion is applied, the table of this + * layer will be replaced by the columns specified in this suggestion + */ + layerId: string; + /** + * A label describing the table. This can be used to provide a title for the `VisualizationSuggestion`, + * but the visualization can also decide to overwrite it. + */ + label?: string; + /** + * The change type indicates what was changed in this table compared to the currently active table of this layer. + */ + changeType: TableChangeType; +} + +/** + * Indicates what was changed in this table compared to the currently active table of this layer. + * * `initial` means the layer associated with this table does not exist in the current configuration + * * `unchanged` means the table is the same in the currently active configuration + * * `reduced` means the table is a reduced version of the currently active table (some columns dropped, but not all of them) + * * `extended` means the table is an extended version of the currently active table (added one or multiple additional columns) + * * `layers` means the change is a change to the layer structure, not to the table + */ +export type TableChangeType = 'initial' | 'unchanged' | 'reduced' | 'extended' | 'layers'; + +export interface DatasourceSuggestion<T = unknown> { + state: T; + table: TableSuggestion; + keptLayerIds: string[]; +} + +export interface DatasourceMetaData { + filterableIndexPatterns: Array<{ id: string; title: string }>; +} + +export type StateSetter<T> = (newState: T | ((prevState: T) => T)) => void; + +/** + * Interface for the datasource registry + */ +export interface Datasource<T = unknown, P = unknown> { + id: string; + + // For initializing, either from an empty state or from persisted state + // Because this will be called at runtime, state might have a type of `any` and + // datasources should validate their arguments + initialize: (state?: P) => Promise<T>; + + // Given the current state, which parts should be saved? + getPersistableState: (state: T) => P; + + insertLayer: (state: T, newLayerId: string) => T; + removeLayer: (state: T, layerId: string) => T; + clearLayer: (state: T, layerId: string) => T; + getLayers: (state: T) => string[]; + removeColumn: (props: { prevState: T; layerId: string; columnId: string }) => T; + + renderDataPanel: (domElement: Element, props: DatasourceDataPanelProps<T>) => void; + renderDimensionTrigger: (domElement: Element, props: DatasourceDimensionTriggerProps<T>) => void; + renderDimensionEditor: (domElement: Element, props: DatasourceDimensionEditorProps<T>) => void; + renderLayerPanel: (domElement: Element, props: DatasourceLayerPanelProps<T>) => void; + canHandleDrop: (props: DatasourceDimensionDropProps<T>) => boolean; + onDrop: (props: DatasourceDimensionDropHandlerProps<T>) => boolean; + + toExpression: (state: T, layerId: string) => Ast | string | null; + + getMetaData: (state: T) => DatasourceMetaData; + + getDatasourceSuggestionsForField: (state: T, field: unknown) => Array<DatasourceSuggestion<T>>; + getDatasourceSuggestionsFromCurrentState: (state: T) => Array<DatasourceSuggestion<T>>; + + getPublicAPI: (props: PublicAPIProps<T>) => DatasourcePublicAPI; +} + +/** + * This is an API provided to visualizations by the frame, which calls the publicAPI on the datasource + */ +export interface DatasourcePublicAPI { + datasourceId: string; + getTableSpec: () => Array<{ columnId: string }>; + getOperationForColumnId: (columnId: string) => Operation | null; +} + +export interface DatasourceDataPanelProps<T = unknown> { + state: T; + dragDropContext: DragContextState; + setState: StateSetter<T>; + core: Pick<CoreSetup, 'http' | 'notifications' | 'uiSettings'>; + query: Query; + dateRange: DateRange; + filters: Filter[]; +} + +interface SharedDimensionProps { + /** Visualizations can restrict operations based on their own rules. + * For example, limiting to only bucketed or only numeric operations. + */ + filterOperations: (operation: OperationMetadata) => boolean; + + /** Visualizations can hint at the role this dimension would play, which + * affects the default ordering of the query + */ + suggestedPriority?: DimensionPriority; + + /** Some dimension editors will allow users to change the operation grouping + * from the panel, and this lets the visualization hint that it doesn't want + * users to have that level of control + */ + hideGrouping?: boolean; +} + +export type DatasourceDimensionProps<T> = SharedDimensionProps & { + layerId: string; + columnId: string; + onRemove?: (accessor: string) => void; + state: T; +}; + +// The only way a visualization has to restrict the query building +export type DatasourceDimensionEditorProps<T = unknown> = DatasourceDimensionProps<T> & { + setState: StateSetter<T>; + core: Pick<CoreSetup, 'http' | 'notifications' | 'uiSettings'>; + dateRange: DateRange; +}; + +export type DatasourceDimensionTriggerProps<T> = DatasourceDimensionProps<T> & { + dragDropContext: DragContextState; + togglePopover: () => void; +}; + +export interface DatasourceLayerPanelProps<T> { + layerId: string; + state: T; + setState: StateSetter<T>; +} + +export type DatasourceDimensionDropProps<T> = SharedDimensionProps & { + layerId: string; + columnId: string; + state: T; + setState: StateSetter<T>; + dragDropContext: DragContextState; +}; + +export type DatasourceDimensionDropHandlerProps<T> = DatasourceDimensionDropProps<T> & { + droppedItem: unknown; +}; + +export type DataType = 'document' | 'string' | 'number' | 'date' | 'boolean' | 'ip'; + +// An operation represents a column in a table, not any information +// about how the column was created such as whether it is a sum or average. +// Visualizations are able to filter based on the output, not based on the +// underlying data +export interface Operation extends OperationMetadata { + // User-facing label for the operation + label: string; +} + +export interface OperationMetadata { + // The output of this operation will have this data type + dataType: DataType; + // A bucketed operation is grouped by duplicate values, otherwise each row is + // treated as unique + isBucketed: boolean; + scale?: 'ordinal' | 'interval' | 'ratio'; + // Extra meta-information like cardinality, color + // TODO currently it's not possible to differentiate between a field from a raw + // document and an aggregated metric which might be handy in some cases. Once we + // introduce a raw document datasource, this should be considered here. +} + +export interface LensMultiTable { + type: 'lens_multitable'; + tables: Record<string, KibanaDatatable>; + dateRange?: { + fromDate: Date; + toDate: Date; + }; +} + +export interface VisualizationConfigProps<T = unknown> { + layerId: string; + frame: FramePublicAPI; + state: T; +} + +export type VisualizationLayerWidgetProps<T = unknown> = VisualizationConfigProps<T> & { + setState: (newState: T) => void; +}; + +type VisualizationDimensionGroupConfig = SharedDimensionProps & { + groupLabel: string; + + /** ID is passed back to visualization. For example, `x` */ + groupId: string; + accessors: string[]; + supportsMoreColumns: boolean; + /** If required, a warning will appear if accessors are empty */ + required?: boolean; + dataTestSubj?: string; +}; + +interface VisualizationDimensionChangeProps<T> { + layerId: string; + columnId: string; + prevState: T; +} + +/** + * Object passed to `getSuggestions` of a visualization. + * It contains a possible table the current datasource could + * provide and the state of the visualization if it is currently active. + * + * If the current datasource suggests multiple tables, `getSuggestions` + * is called multiple times with separate `SuggestionRequest` objects. + */ +export interface SuggestionRequest<T = unknown> { + /** + * A table configuration the datasource could provide. + */ + table: TableSuggestion; + /** + * State is only passed if the visualization is active. + */ + state?: T; + /** + * The visualization needs to know which table is being suggested + */ + keptLayerIds: string[]; +} + +/** + * A possible configuration of a given visualization. It is based on a `TableSuggestion`. + * Suggestion might be shown in the UI to be chosen by the user directly, but they are + * also applied directly under some circumstances (dragging in the first field from the data + * panel or switching to another visualization in the chart switcher). + */ +export interface VisualizationSuggestion<T = unknown> { + /** + * The score of a suggestion should indicate how valuable the suggestion is. It is used + * to rank multiple suggestions of multiple visualizations. The number should be between 0 and 1 + */ + score: number; + /** + * Flag indicating whether this suggestion should not be advertised to the user. It is still + * considered in scenarios where the available suggestion with the highest suggestion is applied + * directly. + */ + hide?: boolean; + /** + * Descriptive title of the suggestion. Should be as short as possible. This title is shown if + * the suggestion is advertised to the user and will also show either the `previewExpression` or + * the `previewIcon` + */ + title: string; + /** + * The new state of the visualization if this suggestion is applied. + */ + state: T; + /** + * An EUI icon type shown instead of the preview expression. + */ + previewIcon: IconType; +} + +export interface FramePublicAPI { + datasourceLayers: Record<string, DatasourcePublicAPI>; + + dateRange: DateRange; + query: Query; + filters: Filter[]; + + // Adds a new layer. This has a side effect of updating the datasource state + addNewLayer: () => string; + removeLayers: (layerIds: string[]) => void; +} + +export interface VisualizationType { + id: string; + icon?: IconType; + largeIcon?: IconType; + label: string; +} + +export interface Visualization<T = unknown, P = unknown> { + id: string; + + visualizationTypes: VisualizationType[]; + + getLayerIds: (state: T) => string[]; + clearLayer: (state: T, layerId: string) => T; + removeLayer?: (state: T, layerId: string) => T; + appendLayer?: (state: T, layerId: string) => T; + + // Layer context menu is used by visualizations for styling the entire layer + // For example, the XY visualization uses this to have multiple chart types + getLayerContextMenuIcon?: (opts: { state: T; layerId: string }) => IconType | undefined; + renderLayerContextMenu?: (domElement: Element, props: VisualizationLayerWidgetProps<T>) => void; + + getConfiguration: ( + props: VisualizationConfigProps<T> + ) => { groups: VisualizationDimensionGroupConfig[] }; + + getDescription: ( + state: T + ) => { + icon?: IconType; + label: string; + }; + + switchVisualizationType?: (visualizationTypeId: string, state: T) => T; + + // For initializing from saved object + initialize: (frame: FramePublicAPI, state?: P) => T; + + getPersistableState: (state: T) => P; + + // Actions triggered by the frame which tell the datasource that a dimension is being changed + setDimension: ( + props: VisualizationDimensionChangeProps<T> & { + groupId: string; + } + ) => T; + removeDimension: (props: VisualizationDimensionChangeProps<T>) => T; + + toExpression: (state: T, frame: FramePublicAPI) => Ast | string | null; + + /** + * Epression to render a preview version of the chart in very constraint space. + * If there is no expression provided, the preview icon is used. + */ + toPreviewExpression?: (state: T, frame: FramePublicAPI) => Ast | string | null; + + // The frame will call this function on all visualizations when the table changes, or when + // rendering additional ways of using the data + getSuggestions: (context: SuggestionRequest<T>) => Array<VisualizationSuggestion<T>>; +} diff --git a/x-pack/legacy/plugins/lens/public/vis_type_alias.ts b/x-pack/plugins/lens/public/vis_type_alias.ts similarity index 95% rename from x-pack/legacy/plugins/lens/public/vis_type_alias.ts rename to x-pack/plugins/lens/public/vis_type_alias.ts index 123b994e6ccce..807504ee2b9c2 100644 --- a/x-pack/legacy/plugins/lens/public/vis_type_alias.ts +++ b/x-pack/plugins/lens/public/vis_type_alias.ts @@ -6,7 +6,7 @@ import { i18n } from '@kbn/i18n'; import { VisTypeAlias } from 'src/plugins/visualizations/public'; -import { getBasePath, getEditPath } from '../../../../plugins/lens/common'; +import { getBasePath, getEditPath } from '../common'; export const getLensAliasConfig = (): VisTypeAlias => ({ aliasUrl: getBasePath(), diff --git a/x-pack/legacy/plugins/lens/public/visualization_container.test.tsx b/x-pack/plugins/lens/public/visualization_container.test.tsx similarity index 100% rename from x-pack/legacy/plugins/lens/public/visualization_container.test.tsx rename to x-pack/plugins/lens/public/visualization_container.test.tsx diff --git a/x-pack/legacy/plugins/lens/public/visualization_container.tsx b/x-pack/plugins/lens/public/visualization_container.tsx similarity index 100% rename from x-pack/legacy/plugins/lens/public/visualization_container.tsx rename to x-pack/plugins/lens/public/visualization_container.tsx diff --git a/x-pack/legacy/plugins/lens/public/xy_visualization/__snapshots__/to_expression.test.ts.snap b/x-pack/plugins/lens/public/xy_visualization/__snapshots__/to_expression.test.ts.snap similarity index 100% rename from x-pack/legacy/plugins/lens/public/xy_visualization/__snapshots__/to_expression.test.ts.snap rename to x-pack/plugins/lens/public/xy_visualization/__snapshots__/to_expression.test.ts.snap diff --git a/x-pack/legacy/plugins/lens/public/xy_visualization/__snapshots__/xy_expression.test.tsx.snap b/x-pack/plugins/lens/public/xy_visualization/__snapshots__/xy_expression.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/lens/public/xy_visualization/__snapshots__/xy_expression.test.tsx.snap rename to x-pack/plugins/lens/public/xy_visualization/__snapshots__/xy_expression.test.tsx.snap diff --git a/x-pack/plugins/lens/public/xy_visualization/_index.scss b/x-pack/plugins/lens/public/xy_visualization/_index.scss new file mode 100644 index 0000000000000..110a9589a6fb4 --- /dev/null +++ b/x-pack/plugins/lens/public/xy_visualization/_index.scss @@ -0,0 +1 @@ +@import 'xy_expression'; diff --git a/x-pack/legacy/plugins/lens/public/xy_visualization/_xy_expression.scss b/x-pack/plugins/lens/public/xy_visualization/_xy_expression.scss similarity index 100% rename from x-pack/legacy/plugins/lens/public/xy_visualization/_xy_expression.scss rename to x-pack/plugins/lens/public/xy_visualization/_xy_expression.scss diff --git a/x-pack/plugins/lens/public/xy_visualization/index.ts b/x-pack/plugins/lens/public/xy_visualization/index.ts new file mode 100644 index 0000000000000..5dfae097be834 --- /dev/null +++ b/x-pack/plugins/lens/public/xy_visualization/index.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 { EUI_CHARTS_THEME_DARK, EUI_CHARTS_THEME_LIGHT } from '@elastic/eui/dist/eui_charts_theme'; +import { CoreSetup, IUiSettingsClient, CoreStart } from 'kibana/public'; +import moment from 'moment-timezone'; +import { ExpressionsSetup } from '../../../../../src/plugins/expressions/public'; +import { xyVisualization } from './xy_visualization'; +import { xyChart, getXyChartRenderer } from './xy_expression'; +import { legendConfig, xConfig, layerConfig } from './types'; +import { EditorFrameSetup, FormatFactory } from '../types'; +import { UiActionsStart } from '../../../../../src/plugins/ui_actions/public'; +import { setExecuteTriggerActions } from './services'; + +export interface XyVisualizationPluginSetupPlugins { + expressions: ExpressionsSetup; + formatFactory: Promise<FormatFactory>; + editorFrame: EditorFrameSetup; +} + +interface XyVisualizationPluginStartPlugins { + uiActions: UiActionsStart; +} + +function getTimeZone(uiSettings: IUiSettingsClient) { + const configuredTimeZone = uiSettings.get('dateFormat:tz'); + if (configuredTimeZone === 'Browser') { + return moment.tz.guess(); + } + + return configuredTimeZone; +} + +export class XyVisualization { + constructor() {} + + setup( + core: CoreSetup, + { expressions, formatFactory, editorFrame }: XyVisualizationPluginSetupPlugins + ) { + expressions.registerFunction(() => legendConfig); + expressions.registerFunction(() => xConfig); + expressions.registerFunction(() => layerConfig); + expressions.registerFunction(() => xyChart); + + expressions.registerRenderer( + getXyChartRenderer({ + formatFactory, + chartTheme: core.uiSettings.get<boolean>('theme:darkMode') + ? EUI_CHARTS_THEME_DARK.theme + : EUI_CHARTS_THEME_LIGHT.theme, + timeZone: getTimeZone(core.uiSettings), + }) + ); + + editorFrame.registerVisualization(xyVisualization); + } + start(core: CoreStart, { uiActions }: XyVisualizationPluginStartPlugins) { + setExecuteTriggerActions(uiActions.executeTriggerActions); + } +} diff --git a/x-pack/plugins/lens/public/xy_visualization/services.ts b/x-pack/plugins/lens/public/xy_visualization/services.ts new file mode 100644 index 0000000000000..51289fe0c63e7 --- /dev/null +++ b/x-pack/plugins/lens/public/xy_visualization/services.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 { createGetterSetter } from '../../../../../src/plugins/kibana_utils/public'; +import { UiActionsStart } from '../../../../../src/plugins/ui_actions/public'; + +export const [getExecuteTriggerActions, setExecuteTriggerActions] = createGetterSetter< + UiActionsStart['executeTriggerActions'] +>('executeTriggerActions'); diff --git a/x-pack/legacy/plugins/lens/public/xy_visualization/state_helpers.ts b/x-pack/plugins/lens/public/xy_visualization/state_helpers.ts similarity index 100% rename from x-pack/legacy/plugins/lens/public/xy_visualization/state_helpers.ts rename to x-pack/plugins/lens/public/xy_visualization/state_helpers.ts diff --git a/x-pack/legacy/plugins/lens/public/xy_visualization/to_expression.test.ts b/x-pack/plugins/lens/public/xy_visualization/to_expression.test.ts similarity index 100% rename from x-pack/legacy/plugins/lens/public/xy_visualization/to_expression.test.ts rename to x-pack/plugins/lens/public/xy_visualization/to_expression.test.ts diff --git a/x-pack/legacy/plugins/lens/public/xy_visualization/to_expression.ts b/x-pack/plugins/lens/public/xy_visualization/to_expression.ts similarity index 100% rename from x-pack/legacy/plugins/lens/public/xy_visualization/to_expression.ts rename to x-pack/plugins/lens/public/xy_visualization/to_expression.ts diff --git a/x-pack/plugins/lens/public/xy_visualization/types.ts b/x-pack/plugins/lens/public/xy_visualization/types.ts new file mode 100644 index 0000000000000..7a5837d382c7b --- /dev/null +++ b/x-pack/plugins/lens/public/xy_visualization/types.ts @@ -0,0 +1,282 @@ +/* + * 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 { Position } from '@elastic/charts'; +import { i18n } from '@kbn/i18n'; +import { ArgumentType, ExpressionFunctionDefinition } from 'src/plugins/expressions/common'; +import chartAreaSVG from '../assets/chart_area.svg'; +import chartAreaStackedSVG from '../assets/chart_area_stacked.svg'; +import chartBarSVG from '../assets/chart_bar.svg'; +import chartBarStackedSVG from '../assets/chart_bar_stacked.svg'; +import chartBarHorizontalSVG from '../assets/chart_bar_horizontal.svg'; +import chartBarHorizontalStackedSVG from '../assets/chart_bar_horizontal_stacked.svg'; +import chartLineSVG from '../assets/chart_line.svg'; + +import { VisualizationType } from '../index'; + +export interface LegendConfig { + isVisible: boolean; + position: Position; +} + +type LegendConfigResult = LegendConfig & { type: 'lens_xy_legendConfig' }; + +export const legendConfig: ExpressionFunctionDefinition< + 'lens_xy_legendConfig', + null, + LegendConfig, + LegendConfigResult +> = { + name: 'lens_xy_legendConfig', + aliases: [], + type: 'lens_xy_legendConfig', + help: `Configure the xy chart's legend`, + inputTypes: ['null'], + args: { + isVisible: { + types: ['boolean'], + help: i18n.translate('xpack.lens.xyChart.isVisible.help', { + defaultMessage: 'Specifies whether or not the legend is visible.', + }), + }, + position: { + types: ['string'], + options: [Position.Top, Position.Right, Position.Bottom, Position.Left], + help: i18n.translate('xpack.lens.xyChart.position.help', { + defaultMessage: 'Specifies the legend position.', + }), + }, + }, + fn: function fn(input: unknown, args: LegendConfig) { + return { + type: 'lens_xy_legendConfig', + ...args, + }; + }, +}; + +interface AxisConfig { + title: string; + hide?: boolean; +} + +const axisConfig: { [key in keyof AxisConfig]: ArgumentType<AxisConfig[key]> } = { + title: { + types: ['string'], + help: i18n.translate('xpack.lens.xyChart.title.help', { + defaultMessage: 'The axis title', + }), + }, + hide: { + types: ['boolean'], + default: false, + help: 'Show / hide axis', + }, +}; + +export interface YState extends AxisConfig { + accessors: string[]; +} + +export interface XConfig extends AxisConfig { + accessor: string; +} + +type XConfigResult = XConfig & { type: 'lens_xy_xConfig' }; + +export const xConfig: ExpressionFunctionDefinition< + 'lens_xy_xConfig', + null, + XConfig, + XConfigResult +> = { + name: 'lens_xy_xConfig', + aliases: [], + type: 'lens_xy_xConfig', + help: `Configure the xy chart's x axis`, + inputTypes: ['null'], + args: { + ...axisConfig, + accessor: { + types: ['string'], + help: 'The column to display on the x axis.', + }, + }, + fn: function fn(input: unknown, args: XConfig) { + return { + type: 'lens_xy_xConfig', + ...args, + }; + }, +}; + +type LayerConfigResult = LayerArgs & { type: 'lens_xy_layer' }; + +export const layerConfig: ExpressionFunctionDefinition< + 'lens_xy_layer', + null, + LayerArgs, + LayerConfigResult +> = { + name: 'lens_xy_layer', + aliases: [], + type: 'lens_xy_layer', + help: `Configure a layer in the xy chart`, + inputTypes: ['null'], + args: { + ...axisConfig, + layerId: { + types: ['string'], + help: '', + }, + xAccessor: { + types: ['string'], + help: '', + }, + seriesType: { + types: ['string'], + options: ['bar', 'line', 'area', 'bar_stacked', 'area_stacked'], + help: 'The type of chart to display.', + }, + xScaleType: { + options: ['ordinal', 'linear', 'time'], + help: 'The scale type of the x axis', + default: 'ordinal', + }, + isHistogram: { + types: ['boolean'], + default: false, + help: 'Whether to layout the chart as a histogram', + }, + yScaleType: { + options: ['log', 'sqrt', 'linear', 'time'], + help: 'The scale type of the y axes', + default: 'linear', + }, + splitAccessor: { + types: ['string'], + help: 'The column to split by', + multi: false, + }, + accessors: { + types: ['string'], + help: 'The columns to display on the y axis.', + multi: true, + }, + columnToLabel: { + types: ['string'], + help: 'JSON key-value pairs of column ID to label', + }, + }, + fn: function fn(input: unknown, args: LayerArgs) { + return { + type: 'lens_xy_layer', + ...args, + }; + }, +}; + +export type SeriesType = + | 'bar' + | 'bar_horizontal' + | 'line' + | 'area' + | 'bar_stacked' + | 'bar_horizontal_stacked' + | 'area_stacked'; + +export interface LayerConfig { + hide?: boolean; + layerId: string; + xAccessor?: string; + accessors: string[]; + seriesType: SeriesType; + splitAccessor?: string; +} + +export type LayerArgs = LayerConfig & { + columnToLabel?: string; // Actually a JSON key-value pair + yScaleType: 'time' | 'linear' | 'log' | 'sqrt'; + xScaleType: 'time' | 'linear' | 'ordinal'; + isHistogram: boolean; +}; + +// Arguments to XY chart expression, with computed properties +export interface XYArgs { + xTitle: string; + yTitle: string; + legend: LegendConfig & { type: 'lens_xy_legendConfig' }; + layers: LayerArgs[]; +} + +// Persisted parts of the state +export interface XYState { + preferredSeriesType: SeriesType; + legend: LegendConfig; + layers: LayerConfig[]; +} + +export type State = XYState; +export type PersistableState = XYState; + +export const visualizationTypes: VisualizationType[] = [ + { + id: 'bar', + icon: 'visBarVertical', + largeIcon: chartBarSVG, + label: i18n.translate('xpack.lens.xyVisualization.barLabel', { + defaultMessage: 'Bar', + }), + }, + { + id: 'bar_horizontal', + icon: 'visBarHorizontal', + largeIcon: chartBarHorizontalSVG, + label: i18n.translate('xpack.lens.xyVisualization.barHorizontalLabel', { + defaultMessage: 'Horizontal bar', + }), + }, + { + id: 'bar_stacked', + icon: 'visBarVerticalStacked', + largeIcon: chartBarStackedSVG, + label: i18n.translate('xpack.lens.xyVisualization.stackedBarLabel', { + defaultMessage: 'Stacked bar', + }), + }, + { + id: 'bar_horizontal_stacked', + icon: 'visBarHorizontalStacked', + largeIcon: chartBarHorizontalStackedSVG, + label: i18n.translate('xpack.lens.xyVisualization.stackedBarHorizontalLabel', { + defaultMessage: 'Stacked horizontal bar', + }), + }, + { + id: 'line', + icon: 'visLine', + largeIcon: chartLineSVG, + label: i18n.translate('xpack.lens.xyVisualization.lineLabel', { + defaultMessage: 'Line', + }), + }, + { + id: 'area', + icon: 'visArea', + largeIcon: chartAreaSVG, + label: i18n.translate('xpack.lens.xyVisualization.areaLabel', { + defaultMessage: 'Area', + }), + }, + { + id: 'area_stacked', + icon: 'visAreaStacked', + largeIcon: chartAreaStackedSVG, + label: i18n.translate('xpack.lens.xyVisualization.stackedAreaLabel', { + defaultMessage: 'Stacked area', + }), + }, +]; diff --git a/x-pack/legacy/plugins/lens/public/xy_visualization/xy_config_panel.test.tsx b/x-pack/plugins/lens/public/xy_visualization/xy_config_panel.test.tsx similarity index 100% rename from x-pack/legacy/plugins/lens/public/xy_visualization/xy_config_panel.test.tsx rename to x-pack/plugins/lens/public/xy_visualization/xy_config_panel.test.tsx diff --git a/x-pack/legacy/plugins/lens/public/xy_visualization/xy_config_panel.tsx b/x-pack/plugins/lens/public/xy_visualization/xy_config_panel.tsx similarity index 100% rename from x-pack/legacy/plugins/lens/public/xy_visualization/xy_config_panel.tsx rename to x-pack/plugins/lens/public/xy_visualization/xy_config_panel.tsx diff --git a/x-pack/plugins/lens/public/xy_visualization/xy_expression.test.tsx b/x-pack/plugins/lens/public/xy_visualization/xy_expression.test.tsx new file mode 100644 index 0000000000000..80d33d1b95b61 --- /dev/null +++ b/x-pack/plugins/lens/public/xy_visualization/xy_expression.test.tsx @@ -0,0 +1,984 @@ +/* + * 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 { + AreaSeries, + Axis, + BarSeries, + Position, + LineSeries, + Settings, + ScaleType, + GeometryValue, + XYChartSeriesIdentifier, + SeriesNameFn, +} from '@elastic/charts'; +import { xyChart, XYChart } from './xy_expression'; +import { LensMultiTable } from '../types'; +import { KibanaDatatable, KibanaDatatableRow } from '../../../../../src/plugins/expressions/public'; +import React from 'react'; +import { shallow } from 'enzyme'; +import { XYArgs, LegendConfig, legendConfig, layerConfig, LayerArgs } from './types'; +import { createMockExecutionContext } from '../../../../../src/plugins/expressions/common/mocks'; +import { mountWithIntl } from 'test_utils/enzyme_helpers'; + +const executeTriggerActions = jest.fn(); + +const createSampleDatatableWithRows = (rows: KibanaDatatableRow[]): KibanaDatatable => ({ + type: 'kibana_datatable', + columns: [ + { + id: 'a', + name: 'a', + formatHint: { id: 'number', params: { pattern: '0,0.000' } }, + }, + { id: 'b', name: 'b', formatHint: { id: 'number', params: { pattern: '000,0' } } }, + { + id: 'c', + name: 'c', + formatHint: { id: 'string' }, + meta: { type: 'date-histogram', aggConfigParams: { interval: '10s' } }, + }, + { id: 'd', name: 'ColD', formatHint: { id: 'string' } }, + ], + rows, +}); + +const sampleLayer: LayerArgs = { + layerId: 'first', + seriesType: 'line', + xAccessor: 'c', + accessors: ['a', 'b'], + splitAccessor: 'd', + columnToLabel: '{"a": "Label A", "b": "Label B", "d": "Label D"}', + xScaleType: 'ordinal', + yScaleType: 'linear', + isHistogram: false, +}; + +const createArgsWithLayers = (layers: LayerArgs[] = [sampleLayer]): XYArgs => ({ + xTitle: '', + yTitle: '', + legend: { + type: 'lens_xy_legendConfig', + isVisible: false, + position: Position.Top, + }, + layers, +}); + +function sampleArgs() { + const data: LensMultiTable = { + type: 'lens_multitable', + tables: { + first: createSampleDatatableWithRows([ + { a: 1, b: 2, c: 'I', d: 'Foo' }, + { a: 1, b: 5, c: 'J', d: 'Bar' }, + ]), + }, + }; + + const args: XYArgs = createArgsWithLayers(); + + return { data, args }; +} + +describe('xy_expression', () => { + describe('configs', () => { + test('legendConfig produces the correct arguments', () => { + const args: LegendConfig = { + isVisible: true, + position: Position.Left, + }; + + const result = legendConfig.fn(null, args, createMockExecutionContext()); + + expect(result).toEqual({ + type: 'lens_xy_legendConfig', + ...args, + }); + }); + + test('layerConfig produces the correct arguments', () => { + const args: LayerArgs = { + layerId: 'first', + seriesType: 'line', + xAccessor: 'c', + accessors: ['a', 'b'], + splitAccessor: 'd', + xScaleType: 'linear', + yScaleType: 'linear', + isHistogram: false, + }; + + const result = layerConfig.fn(null, args, createMockExecutionContext()); + + expect(result).toEqual({ + type: 'lens_xy_layer', + ...args, + }); + }); + }); + + describe('xyChart', () => { + test('it renders with the specified data and args', () => { + const { data, args } = sampleArgs(); + const result = xyChart.fn(data, args, createMockExecutionContext()); + + expect(result).toEqual({ + type: 'render', + as: 'lens_xy_chart_renderer', + value: { data, args }, + }); + }); + }); + + describe('XYChart component', () => { + let getFormatSpy: jest.Mock; + let convertSpy: jest.Mock; + + beforeEach(() => { + convertSpy = jest.fn(x => x); + getFormatSpy = jest.fn(); + getFormatSpy.mockReturnValue({ convert: convertSpy }); + }); + + test('it renders line', () => { + const { data, args } = sampleArgs(); + + const component = shallow( + <XYChart + data={data} + args={{ ...args, layers: [{ ...args.layers[0], seriesType: 'line' }] }} + formatFactory={getFormatSpy} + timeZone="UTC" + chartTheme={{}} + executeTriggerActions={executeTriggerActions} + /> + ); + expect(component).toMatchSnapshot(); + expect(component.find(LineSeries)).toHaveLength(1); + }); + + describe('date range', () => { + const timeSampleLayer: LayerArgs = { + layerId: 'first', + seriesType: 'line', + xAccessor: 'c', + accessors: ['a', 'b'], + splitAccessor: 'd', + columnToLabel: '{"a": "Label A", "b": "Label B", "d": "Label D"}', + xScaleType: 'time', + yScaleType: 'linear', + isHistogram: false, + }; + const multiLayerArgs = createArgsWithLayers([ + timeSampleLayer, + { + ...timeSampleLayer, + layerId: 'second', + seriesType: 'bar', + xScaleType: 'time', + }, + ]); + test('it uses the full date range', () => { + const { data, args } = sampleArgs(); + + const component = shallow( + <XYChart + data={{ + ...data, + dateRange: { + fromDate: new Date('2019-01-02T05:00:00.000Z'), + toDate: new Date('2019-01-03T05:00:00.000Z'), + }, + }} + args={{ + ...args, + layers: [{ ...args.layers[0], seriesType: 'line', xScaleType: 'time' }], + }} + formatFactory={getFormatSpy} + timeZone="UTC" + chartTheme={{}} + executeTriggerActions={executeTriggerActions} + /> + ); + expect(component.find(Settings).prop('xDomain')).toMatchInlineSnapshot(` + Object { + "max": 1546491600000, + "min": 1546405200000, + "minInterval": undefined, + } + `); + }); + + test('it generates correct xDomain for a layer with single value and a layer with no data (1-0) ', () => { + const data: LensMultiTable = { + type: 'lens_multitable', + tables: { + first: createSampleDatatableWithRows([{ a: 1, b: 2, c: 'I', d: 'Foo' }]), + second: createSampleDatatableWithRows([]), + }, + }; + + const component = shallow( + <XYChart + data={{ + ...data, + dateRange: { + fromDate: new Date('2019-01-02T05:00:00.000Z'), + toDate: new Date('2019-01-03T05:00:00.000Z'), + }, + }} + args={multiLayerArgs} + formatFactory={getFormatSpy} + timeZone="UTC" + chartTheme={{}} + executeTriggerActions={executeTriggerActions} + /> + ); + + expect(component.find(Settings).prop('xDomain')).toMatchInlineSnapshot(` + Object { + "max": 1546491600000, + "min": 1546405200000, + "minInterval": 10000, + } + `); + }); + + test('it generates correct xDomain for two layers with single value(1-1)', () => { + const data: LensMultiTable = { + type: 'lens_multitable', + tables: { + first: createSampleDatatableWithRows([{ a: 1, b: 2, c: 'I', d: 'Foo' }]), + second: createSampleDatatableWithRows([{ a: 10, b: 5, c: 'J', d: 'Bar' }]), + }, + }; + const component = shallow( + <XYChart + data={{ + ...data, + dateRange: { + fromDate: new Date('2019-01-02T05:00:00.000Z'), + toDate: new Date('2019-01-03T05:00:00.000Z'), + }, + }} + args={multiLayerArgs} + formatFactory={getFormatSpy} + timeZone="UTC" + chartTheme={{}} + executeTriggerActions={executeTriggerActions} + /> + ); + + expect(component.find(Settings).prop('xDomain')).toMatchInlineSnapshot(` + Object { + "max": 1546491600000, + "min": 1546405200000, + "minInterval": 10000, + } + `); + }); + test('it generates correct xDomain for a layer with single value and layer with multiple value data (1-n)', () => { + const data: LensMultiTable = { + type: 'lens_multitable', + tables: { + first: createSampleDatatableWithRows([{ a: 1, b: 2, c: 'I', d: 'Foo' }]), + second: createSampleDatatableWithRows([ + { a: 10, b: 5, c: 'J', d: 'Bar' }, + { a: 8, b: 5, c: 'K', d: 'Buzz' }, + ]), + }, + }; + const component = shallow( + <XYChart + data={{ + ...data, + dateRange: { + fromDate: new Date('2019-01-02T05:00:00.000Z'), + toDate: new Date('2019-01-03T05:00:00.000Z'), + }, + }} + args={multiLayerArgs} + formatFactory={getFormatSpy} + timeZone="UTC" + chartTheme={{}} + executeTriggerActions={executeTriggerActions} + /> + ); + + expect(component.find(Settings).prop('xDomain')).toMatchInlineSnapshot(` + Object { + "max": 1546491600000, + "min": 1546405200000, + "minInterval": undefined, + } + `); + }); + + test('it generates correct xDomain for 2 layers with multiple value data (n-n)', () => { + const data: LensMultiTable = { + type: 'lens_multitable', + tables: { + first: createSampleDatatableWithRows([ + { a: 1, b: 2, c: 'I', d: 'Foo' }, + { a: 8, b: 5, c: 'K', d: 'Buzz' }, + { a: 9, b: 7, c: 'L', d: 'Bar' }, + { a: 10, b: 2, c: 'G', d: 'Bear' }, + ]), + second: createSampleDatatableWithRows([ + { a: 10, b: 5, c: 'J', d: 'Bar' }, + { a: 8, b: 4, c: 'K', d: 'Fi' }, + { a: 1, b: 8, c: 'O', d: 'Pi' }, + ]), + }, + }; + const component = shallow( + <XYChart + data={{ + ...data, + dateRange: { + fromDate: new Date('2019-01-02T05:00:00.000Z'), + toDate: new Date('2019-01-03T05:00:00.000Z'), + }, + }} + args={multiLayerArgs} + formatFactory={getFormatSpy} + timeZone="UTC" + chartTheme={{}} + executeTriggerActions={executeTriggerActions} + /> + ); + + expect(component.find(Settings).prop('xDomain')).toMatchInlineSnapshot(` + Object { + "max": 1546491600000, + "min": 1546405200000, + "minInterval": undefined, + } + `); + }); + }); + + test('it does not use date range if the x is not a time scale', () => { + const { data, args } = sampleArgs(); + + const component = shallow( + <XYChart + data={{ + ...data, + dateRange: { + fromDate: new Date('2019-01-02T05:00:00.000Z'), + toDate: new Date('2019-01-03T05:00:00.000Z'), + }, + }} + args={{ + ...args, + layers: [{ ...args.layers[0], seriesType: 'line', xScaleType: 'linear' }], + }} + formatFactory={getFormatSpy} + timeZone="UTC" + chartTheme={{}} + executeTriggerActions={executeTriggerActions} + /> + ); + expect(component.find(Settings).prop('xDomain')).toBeUndefined(); + }); + + test('it renders bar', () => { + const { data, args } = sampleArgs(); + const component = shallow( + <XYChart + data={data} + args={{ ...args, layers: [{ ...args.layers[0], seriesType: 'bar' }] }} + formatFactory={getFormatSpy} + timeZone="UTC" + chartTheme={{}} + executeTriggerActions={executeTriggerActions} + /> + ); + expect(component).toMatchSnapshot(); + expect(component.find(BarSeries)).toHaveLength(1); + }); + + test('it renders area', () => { + const { data, args } = sampleArgs(); + const component = shallow( + <XYChart + data={data} + args={{ ...args, layers: [{ ...args.layers[0], seriesType: 'area' }] }} + formatFactory={getFormatSpy} + timeZone="UTC" + chartTheme={{}} + executeTriggerActions={executeTriggerActions} + /> + ); + expect(component).toMatchSnapshot(); + expect(component.find(AreaSeries)).toHaveLength(1); + }); + + test('it renders horizontal bar', () => { + const { data, args } = sampleArgs(); + const component = shallow( + <XYChart + data={data} + args={{ ...args, layers: [{ ...args.layers[0], seriesType: 'bar_horizontal' }] }} + formatFactory={getFormatSpy} + timeZone="UTC" + chartTheme={{}} + executeTriggerActions={executeTriggerActions} + /> + ); + expect(component).toMatchSnapshot(); + expect(component.find(BarSeries)).toHaveLength(1); + expect(component.find(Settings).prop('rotation')).toEqual(90); + }); + + test('onElementClick returns correct context data', () => { + const geometry: GeometryValue = { x: 5, y: 1, accessor: 'y1' }; + const series = { + key: 'spec{d}yAccessor{d}splitAccessors{b-2}', + specId: 'd', + yAccessor: 'd', + splitAccessors: {}, + seriesKeys: [2, 'd'], + }; + + const { args, data } = sampleArgs(); + + const wrapper = mountWithIntl( + <XYChart + data={data} + args={{ + ...args, + layers: [ + { + layerId: 'first', + isHistogram: true, + seriesType: 'bar_stacked', + xAccessor: 'b', + yScaleType: 'linear', + xScaleType: 'time', + splitAccessor: 'b', + accessors: ['d'], + columnToLabel: '{"a": "Label A", "b": "Label B", "d": "Label D"}', + }, + ], + }} + formatFactory={getFormatSpy} + timeZone="UTC" + chartTheme={{}} + executeTriggerActions={executeTriggerActions} + /> + ); + + wrapper + .find(Settings) + .first() + .prop('onElementClick')!([[geometry, series as XYChartSeriesIdentifier]]); + + expect(executeTriggerActions).toHaveBeenCalledWith('VALUE_CLICK_TRIGGER', { + data: { + data: [ + { + column: 1, + row: 1, + table: data.tables.first, + value: 5, + }, + { + column: 1, + row: 0, + table: data.tables.first, + value: 2, + }, + ], + }, + }); + }); + + test('it renders stacked bar', () => { + const { data, args } = sampleArgs(); + const component = shallow( + <XYChart + data={data} + args={{ ...args, layers: [{ ...args.layers[0], seriesType: 'bar_stacked' }] }} + formatFactory={getFormatSpy} + timeZone="UTC" + chartTheme={{}} + executeTriggerActions={executeTriggerActions} + /> + ); + expect(component).toMatchSnapshot(); + expect(component.find(BarSeries)).toHaveLength(1); + expect(component.find(BarSeries).prop('stackAccessors')).toHaveLength(1); + }); + + test('it renders stacked area', () => { + const { data, args } = sampleArgs(); + const component = shallow( + <XYChart + data={data} + args={{ ...args, layers: [{ ...args.layers[0], seriesType: 'area_stacked' }] }} + formatFactory={getFormatSpy} + timeZone="UTC" + chartTheme={{}} + executeTriggerActions={executeTriggerActions} + /> + ); + expect(component).toMatchSnapshot(); + expect(component.find(AreaSeries)).toHaveLength(1); + expect(component.find(AreaSeries).prop('stackAccessors')).toHaveLength(1); + }); + + test('it renders stacked horizontal bar', () => { + const { data, args } = sampleArgs(); + const component = shallow( + <XYChart + data={data} + args={{ + ...args, + layers: [{ ...args.layers[0], seriesType: 'bar_horizontal_stacked' }], + }} + formatFactory={getFormatSpy} + timeZone="UTC" + chartTheme={{}} + executeTriggerActions={executeTriggerActions} + /> + ); + expect(component).toMatchSnapshot(); + expect(component.find(BarSeries)).toHaveLength(1); + expect(component.find(BarSeries).prop('stackAccessors')).toHaveLength(1); + expect(component.find(Settings).prop('rotation')).toEqual(90); + }); + + test('it passes time zone to the series', () => { + const { data, args } = sampleArgs(); + const component = shallow( + <XYChart + data={data} + args={args} + formatFactory={getFormatSpy} + timeZone="CEST" + chartTheme={{}} + executeTriggerActions={executeTriggerActions} + /> + ); + expect(component.find(LineSeries).prop('timeZone')).toEqual('CEST'); + }); + + test('it applies histogram mode to the series for single series', () => { + const { data, args } = sampleArgs(); + const firstLayer: LayerArgs = { ...args.layers[0], seriesType: 'bar', isHistogram: true }; + delete firstLayer.splitAccessor; + const component = shallow( + <XYChart + data={data} + args={{ ...args, layers: [firstLayer] }} + formatFactory={getFormatSpy} + timeZone="UTC" + chartTheme={{}} + executeTriggerActions={executeTriggerActions} + /> + ); + expect(component.find(BarSeries).prop('enableHistogramMode')).toEqual(true); + }); + + test('it applies histogram mode to the series for stacked series', () => { + const { data, args } = sampleArgs(); + const component = shallow( + <XYChart + data={data} + args={{ + ...args, + layers: [ + { + ...args.layers[0], + seriesType: 'bar_stacked', + isHistogram: true, + }, + ], + }} + formatFactory={getFormatSpy} + timeZone="UTC" + chartTheme={{}} + executeTriggerActions={executeTriggerActions} + /> + ); + expect(component.find(BarSeries).prop('enableHistogramMode')).toEqual(true); + }); + + test('it does not apply histogram mode for splitted series', () => { + const { data, args } = sampleArgs(); + const component = shallow( + <XYChart + data={data} + args={{ + ...args, + layers: [{ ...args.layers[0], seriesType: 'bar', isHistogram: true }], + }} + formatFactory={getFormatSpy} + timeZone="UTC" + chartTheme={{}} + executeTriggerActions={executeTriggerActions} + /> + ); + expect(component.find(BarSeries).prop('enableHistogramMode')).toEqual(false); + }); + + describe('provides correct series naming', () => { + const dataWithoutFormats: LensMultiTable = { + type: 'lens_multitable', + tables: { + first: { + type: 'kibana_datatable', + columns: [ + { id: 'a', name: 'a' }, + { id: 'b', name: 'b' }, + { id: 'c', name: 'c' }, + { id: 'd', name: 'd' }, + ], + rows: [ + { a: 1, b: 2, c: 'I', d: 'Row 1' }, + { a: 1, b: 5, c: 'J', d: 'Row 2' }, + ], + }, + }, + }; + const dataWithFormats: LensMultiTable = { + type: 'lens_multitable', + tables: { + first: { + type: 'kibana_datatable', + columns: [ + { id: 'a', name: 'a' }, + { id: 'b', name: 'b' }, + { id: 'c', name: 'c' }, + { id: 'd', name: 'd', formatHint: { id: 'custom' } }, + ], + rows: [ + { a: 1, b: 2, c: 'I', d: 'Row 1' }, + { a: 1, b: 5, c: 'J', d: 'Row 2' }, + ], + }, + }, + }; + + const nameFnArgs = { + seriesKeys: [], + key: '', + specId: 'a', + yAccessor: '', + splitAccessors: new Map(), + }; + + const getRenderedComponent = (data: LensMultiTable, args: XYArgs) => { + return shallow( + <XYChart + data={data} + args={args} + formatFactory={getFormatSpy} + timeZone="UTC" + chartTheme={{}} + executeTriggerActions={executeTriggerActions} + /> + ); + }; + + test('simplest xy chart without human-readable name', () => { + const args = createArgsWithLayers(); + const newArgs = { + ...args, + layers: [ + { + ...args.layers[0], + accessors: ['a'], + splitAccessor: undefined, + columnToLabel: '', + }, + ], + }; + + const component = getRenderedComponent(dataWithoutFormats, newArgs); + const nameFn = component.find(LineSeries).prop('name') as SeriesNameFn; + + // In this case, the ID is used as the name. This shouldn't happen in practice + expect(nameFn({ ...nameFnArgs, seriesKeys: ['a'] }, false)).toEqual(''); + expect(nameFn({ ...nameFnArgs, seriesKeys: ['nonsense'] }, false)).toEqual(''); + }); + + test('simplest xy chart with empty name', () => { + const args = createArgsWithLayers(); + const newArgs = { + ...args, + layers: [ + { + ...args.layers[0], + accessors: ['a'], + splitAccessor: undefined, + columnToLabel: '{"a":""}', + }, + ], + }; + + const component = getRenderedComponent(dataWithoutFormats, newArgs); + const nameFn = component.find(LineSeries).prop('name') as SeriesNameFn; + + // In this case, the ID is used as the name. This shouldn't happen in practice + expect(nameFn({ ...nameFnArgs, seriesKeys: ['a'] }, false)).toEqual(''); + expect(nameFn({ ...nameFnArgs, seriesKeys: ['nonsense'] }, false)).toEqual(''); + }); + + test('simplest xy chart with human-readable name', () => { + const args = createArgsWithLayers(); + const newArgs = { + ...args, + layers: [ + { + ...args.layers[0], + accessors: ['a'], + splitAccessor: undefined, + columnToLabel: '{"a":"Column A"}', + }, + ], + }; + + const component = getRenderedComponent(dataWithoutFormats, newArgs); + const nameFn = component.find(LineSeries).prop('name') as SeriesNameFn; + + expect(nameFn({ ...nameFnArgs, seriesKeys: ['a'] }, false)).toEqual('Column A'); + }); + + test('multiple y accessors', () => { + const args = createArgsWithLayers(); + const newArgs = { + ...args, + layers: [ + { + ...args.layers[0], + accessors: ['a', 'b'], + splitAccessor: undefined, + columnToLabel: '{"a": "Label A"}', + }, + ], + }; + + const component = getRenderedComponent(dataWithoutFormats, newArgs); + const nameFn = component.find(LineSeries).prop('name') as SeriesNameFn; + + // This accessor has a human-readable name + expect(nameFn({ ...nameFnArgs, seriesKeys: ['a'] }, false)).toEqual('Label A'); + // This accessor does not + expect(nameFn({ ...nameFnArgs, seriesKeys: ['b'] }, false)).toEqual(''); + expect(nameFn({ ...nameFnArgs, seriesKeys: ['nonsense'] }, false)).toEqual(''); + }); + + test('split series without formatting and single y accessor', () => { + const args = createArgsWithLayers(); + const newArgs = { + ...args, + layers: [ + { + ...args.layers[0], + accessors: ['a'], + splitAccessor: 'd', + columnToLabel: '{"a": "Label A"}', + }, + ], + }; + + const component = getRenderedComponent(dataWithoutFormats, newArgs); + const nameFn = component.find(LineSeries).prop('name') as SeriesNameFn; + + expect(nameFn({ ...nameFnArgs, seriesKeys: ['split1', 'a'] }, false)).toEqual('split1'); + }); + + test('split series with formatting and single y accessor', () => { + const args = createArgsWithLayers(); + const newArgs = { + ...args, + layers: [ + { + ...args.layers[0], + accessors: ['a'], + splitAccessor: 'd', + columnToLabel: '{"a": "Label A"}', + }, + ], + }; + + const component = getRenderedComponent(dataWithFormats, newArgs); + const nameFn = component.find(LineSeries).prop('name') as SeriesNameFn; + + convertSpy.mockReturnValueOnce('formatted'); + expect(nameFn({ ...nameFnArgs, seriesKeys: ['split1', 'a'] }, false)).toEqual('formatted'); + expect(getFormatSpy).toHaveBeenCalledWith({ id: 'custom' }); + }); + + test('split series without formatting with multiple y accessors', () => { + const args = createArgsWithLayers(); + const newArgs = { + ...args, + layers: [ + { + ...args.layers[0], + accessors: ['a', 'b'], + splitAccessor: 'd', + columnToLabel: '{"a": "Label A","b": "Label B"}', + }, + ], + }; + + const component = getRenderedComponent(dataWithoutFormats, newArgs); + const nameFn = component.find(LineSeries).prop('name') as SeriesNameFn; + + expect(nameFn({ ...nameFnArgs, seriesKeys: ['split1', 'b'] }, false)).toEqual( + 'split1 - Label B' + ); + }); + + test('split series with formatting with multiple y accessors', () => { + const args = createArgsWithLayers(); + const newArgs = { + ...args, + layers: [ + { + ...args.layers[0], + accessors: ['a', 'b'], + splitAccessor: 'd', + columnToLabel: '{"a": "Label A","b": "Label B"}', + }, + ], + }; + + const component = getRenderedComponent(dataWithFormats, newArgs); + const nameFn = component.find(LineSeries).prop('name') as SeriesNameFn; + + convertSpy.mockReturnValueOnce('formatted1').mockReturnValueOnce('formatted2'); + expect(nameFn({ ...nameFnArgs, seriesKeys: ['split1', 'a'] }, false)).toEqual( + 'formatted1 - Label A' + ); + expect(nameFn({ ...nameFnArgs, seriesKeys: ['split1', 'b'] }, false)).toEqual( + 'formatted2 - Label B' + ); + }); + }); + + test('it set the scale of the x axis according to the args prop', () => { + const { data, args } = sampleArgs(); + + const component = shallow( + <XYChart + data={data} + args={{ ...args, layers: [{ ...args.layers[0], xScaleType: 'ordinal' }] }} + formatFactory={getFormatSpy} + timeZone="UTC" + chartTheme={{}} + executeTriggerActions={executeTriggerActions} + /> + ); + expect(component.find(LineSeries).prop('xScaleType')).toEqual(ScaleType.Ordinal); + }); + + test('it set the scale of the y axis according to the args prop', () => { + const { data, args } = sampleArgs(); + + const component = shallow( + <XYChart + data={data} + args={{ ...args, layers: [{ ...args.layers[0], yScaleType: 'sqrt' }] }} + formatFactory={getFormatSpy} + timeZone="UTC" + chartTheme={{}} + executeTriggerActions={executeTriggerActions} + /> + ); + expect(component.find(LineSeries).prop('yScaleType')).toEqual(ScaleType.Sqrt); + }); + + test('it gets the formatter for the x axis', () => { + const { data, args } = sampleArgs(); + + shallow( + <XYChart + data={{ ...data }} + args={{ ...args }} + formatFactory={getFormatSpy} + timeZone="UTC" + chartTheme={{}} + executeTriggerActions={executeTriggerActions} + /> + ); + + expect(getFormatSpy).toHaveBeenCalledWith({ id: 'string' }); + }); + + test('it gets a default formatter for y if there are multiple y accessors', () => { + const { data, args } = sampleArgs(); + + shallow( + <XYChart + data={{ ...data }} + args={{ ...args }} + formatFactory={getFormatSpy} + timeZone="UTC" + chartTheme={{}} + executeTriggerActions={executeTriggerActions} + /> + ); + + expect(getFormatSpy).toHaveBeenCalledWith({ id: 'number' }); + }); + + test('it gets the formatter for the y axis if there is only one accessor', () => { + const { data, args } = sampleArgs(); + + shallow( + <XYChart + data={{ ...data }} + args={{ ...args, layers: [{ ...args.layers[0], accessors: ['a'] }] }} + formatFactory={getFormatSpy} + chartTheme={{}} + timeZone="UTC" + executeTriggerActions={executeTriggerActions} + /> + ); + expect(getFormatSpy).toHaveBeenCalledWith({ + id: 'number', + params: { pattern: '0,0.000' }, + }); + }); + + test('it should pass the formatter function to the axis', () => { + const { data, args } = sampleArgs(); + + const instance = shallow( + <XYChart + data={{ ...data }} + args={{ ...args }} + formatFactory={getFormatSpy} + timeZone="UTC" + chartTheme={{}} + executeTriggerActions={executeTriggerActions} + /> + ); + + const tickFormatter = instance + .find(Axis) + .first() + .prop('tickFormat'); + + if (!tickFormatter) { + throw new Error('tickFormatter prop not found'); + } + + tickFormatter('I'); + + expect(convertSpy).toHaveBeenCalledWith('I'); + }); + }); +}); diff --git a/x-pack/legacy/plugins/lens/public/xy_visualization/xy_expression.tsx b/x-pack/plugins/lens/public/xy_visualization/xy_expression.tsx similarity index 88% rename from x-pack/legacy/plugins/lens/public/xy_visualization/xy_expression.tsx rename to x-pack/plugins/lens/public/xy_visualization/xy_expression.tsx index f5798688badc5..f12a0e5b907c7 100644 --- a/x-pack/legacy/plugins/lens/public/xy_visualization/xy_expression.tsx +++ b/x-pack/plugins/lens/public/xy_visualization/xy_expression.tsx @@ -28,14 +28,14 @@ import { import { EuiIcon, EuiText, IconType, EuiSpacer } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; import { i18n } from '@kbn/i18n'; -import { EmbeddableVisTriggerContext } from '../../../../../../src/plugins/embeddable/public'; -import { VIS_EVENT_TO_TRIGGER } from '../../../../../../src/plugins/visualizations/public'; +import { EmbeddableVisTriggerContext } from '../../../../../src/plugins/embeddable/public'; +import { VIS_EVENT_TO_TRIGGER } from '../../../../../src/plugins/visualizations/public'; import { LensMultiTable, FormatFactory } from '../types'; import { XYArgs, SeriesType, visualizationTypes } from './types'; import { VisualizationContainer } from '../visualization_container'; import { isHorizontalChart } from './state_helpers'; -import { UiActionsStart } from '../../../../../../src/plugins/ui_actions/public'; -import { parseInterval } from '../../../../../../src/plugins/data/common'; +import { UiActionsStart } from '../../../../../src/plugins/ui_actions/public'; +import { parseInterval } from '../../../../../src/plugins/data/common'; import { getExecuteTriggerActions } from './services'; type InferPropType<T> = T extends React.FunctionComponent<infer P> ? P : T; @@ -361,12 +361,31 @@ export function XYChart({ enableHistogramMode: isHistogram && (seriesType.includes('stacked') || !splitAccessor), timeZone, name(d) { + const splitHint = table.columns.find(col => col.id === splitAccessor)?.formatHint; + + // For multiple y series, the name of the operation is used on each, either: + // * Key - Y name + // * Formatted value - Y name if (accessors.length > 1) { return d.seriesKeys - .map((key: string | number) => columnToLabelMap[key] || key) + .map((key: string | number, i) => { + if (i === 0 && splitHint) { + return formatFactory(splitHint).convert(key); + } + return splitAccessor && i === 0 ? key : columnToLabelMap[key] ?? ''; + }) .join(' - '); } - return columnToLabelMap[d.seriesKeys[0]] ?? d.seriesKeys[0]; + + // For formatted split series, format the key + // This handles splitting by dates, for example + if (splitHint) { + return formatFactory(splitHint).convert(d.seriesKeys[0]); + } + // This handles both split and single-y cases: + // * If split series without formatting, show the value literally + // * If single Y, the seriesKey will be the acccessor, so we show the human-readable name + return splitAccessor ? d.seriesKeys[0] : columnToLabelMap[d.seriesKeys[0]] ?? ''; }, }; diff --git a/x-pack/legacy/plugins/lens/public/xy_visualization/xy_suggestions.test.ts b/x-pack/plugins/lens/public/xy_visualization/xy_suggestions.test.ts similarity index 100% rename from x-pack/legacy/plugins/lens/public/xy_visualization/xy_suggestions.test.ts rename to x-pack/plugins/lens/public/xy_visualization/xy_suggestions.test.ts diff --git a/x-pack/legacy/plugins/lens/public/xy_visualization/xy_suggestions.ts b/x-pack/plugins/lens/public/xy_visualization/xy_suggestions.ts similarity index 100% rename from x-pack/legacy/plugins/lens/public/xy_visualization/xy_suggestions.ts rename to x-pack/plugins/lens/public/xy_visualization/xy_suggestions.ts diff --git a/x-pack/legacy/plugins/lens/public/xy_visualization/xy_visualization.test.ts b/x-pack/plugins/lens/public/xy_visualization/xy_visualization.test.ts similarity index 100% rename from x-pack/legacy/plugins/lens/public/xy_visualization/xy_visualization.test.ts rename to x-pack/plugins/lens/public/xy_visualization/xy_visualization.test.ts diff --git a/x-pack/legacy/plugins/lens/public/xy_visualization/xy_visualization.tsx b/x-pack/plugins/lens/public/xy_visualization/xy_visualization.tsx similarity index 100% rename from x-pack/legacy/plugins/lens/public/xy_visualization/xy_visualization.tsx rename to x-pack/plugins/lens/public/xy_visualization/xy_visualization.tsx diff --git a/x-pack/legacy/plugins/lens/readme.md b/x-pack/plugins/lens/readme.md similarity index 100% rename from x-pack/legacy/plugins/lens/readme.md rename to x-pack/plugins/lens/readme.md diff --git a/x-pack/plugins/lens/server/index.ts b/x-pack/plugins/lens/server/index.ts index 3b9e94986d247..8aeeeab4539b6 100644 --- a/x-pack/plugins/lens/server/index.ts +++ b/x-pack/plugins/lens/server/index.ts @@ -4,10 +4,16 @@ * you may not use this file except in compliance with the Elastic License. */ -import { PluginInitializerContext } from 'kibana/server'; +import { PluginInitializerContext, PluginConfigDescriptor } from 'kibana/server'; import { LensServerPlugin } from './plugin'; export * from './plugin'; +import { configSchema, ConfigSchema } from '../config'; + +export const config: PluginConfigDescriptor<ConfigSchema> = { + schema: configSchema, +}; + export const plugin = (initializerContext: PluginInitializerContext) => new LensServerPlugin(initializerContext); diff --git a/x-pack/plugins/lens/server/migrations.test.ts b/x-pack/plugins/lens/server/migrations.test.ts new file mode 100644 index 0000000000000..e80308cc9acdb --- /dev/null +++ b/x-pack/plugins/lens/server/migrations.test.ts @@ -0,0 +1,161 @@ +/* + * 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 { migrations } from './migrations'; +import { SavedObjectMigrationContext } from 'src/core/server'; + +describe('Lens migrations', () => { + describe('7.7.0 missing dimensions in XY', () => { + const context = {} as SavedObjectMigrationContext; + + const example = { + type: 'lens', + attributes: { + expression: + 'kibana\n| kibana_context query="{\\"language\\":\\"kuery\\",\\"query\\":\\"\\"}" \n| lens_merge_tables layerIds="c61a8afb-a185-4fae-a064-fb3846f6c451" \n tables={esaggs index="logstash-*" metricsAtAllLevels=false partialRows=false includeFormatHints=true aggConfigs="[{\\"id\\":\\"2cd09808-3915-49f4-b3b0-82767eba23f7\\",\\"enabled\\":true,\\"type\\":\\"max\\",\\"schema\\":\\"metric\\",\\"params\\":{\\"field\\":\\"bytes\\"}}]" | lens_rename_columns idMap="{\\"col-0-2cd09808-3915-49f4-b3b0-82767eba23f7\\":\\"2cd09808-3915-49f4-b3b0-82767eba23f7\\"}"}\n| lens_metric_chart title="Maximum of bytes" accessor="2cd09808-3915-49f4-b3b0-82767eba23f7"', + state: { + datasourceMetaData: { + filterableIndexPatterns: [ + { + id: 'logstash-*', + title: 'logstash-*', + }, + ], + }, + datasourceStates: { + indexpattern: { + currentIndexPatternId: 'logstash-*', + layers: { + 'c61a8afb-a185-4fae-a064-fb3846f6c451': { + columnOrder: ['2cd09808-3915-49f4-b3b0-82767eba23f7'], + columns: { + '2cd09808-3915-49f4-b3b0-82767eba23f7': { + dataType: 'number', + isBucketed: false, + label: 'Maximum of bytes', + operationType: 'max', + scale: 'ratio', + sourceField: 'bytes', + }, + 'd3e62a7a-c259-4fff-a2fc-eebf20b7008a': { + dataType: 'number', + isBucketed: false, + label: 'Minimum of bytes', + operationType: 'min', + scale: 'ratio', + sourceField: 'bytes', + }, + 'd6e40cea-6299-43b4-9c9d-b4ee305a2ce8': { + dataType: 'date', + isBucketed: true, + label: 'Date Histogram of @timestamp', + operationType: 'date_histogram', + params: { + interval: 'auto', + }, + scale: 'interval', + sourceField: '@timestamp', + }, + }, + indexPatternId: 'logstash-*', + }, + }, + }, + }, + filters: [], + query: { + language: 'kuery', + query: '', + }, + visualization: { + accessor: '2cd09808-3915-49f4-b3b0-82767eba23f7', + isHorizontal: false, + layerId: 'c61a8afb-a185-4fae-a064-fb3846f6c451', + layers: [ + { + accessors: [ + 'd3e62a7a-c259-4fff-a2fc-eebf20b7008a', + '26ef70a9-c837-444c-886e-6bd905ee7335', + ], + layerId: 'c61a8afb-a185-4fae-a064-fb3846f6c451', + seriesType: 'area', + splitAccessor: '54cd64ed-2a44-4591-af84-b2624504569a', + xAccessor: 'd6e40cea-6299-43b4-9c9d-b4ee305a2ce8', + }, + ], + legend: { + isVisible: true, + position: 'right', + }, + preferredSeriesType: 'area', + }, + }, + title: 'Artistpreviouslyknownaslens', + visualizationType: 'lnsXY', + }, + }; + + it('should not change anything by XY visualizations', () => { + const target = { + ...example, + attributes: { + ...example.attributes, + visualizationType: 'lnsMetric', + }, + }; + const result = migrations['7.7.0'](target, context); + expect(result).toEqual(target); + }); + + it('should handle missing layers', () => { + const result = migrations['7.7.0']( + { + ...example, + attributes: { + ...example.attributes, + state: { + ...example.attributes.state, + datasourceStates: { + indexpattern: { + layers: [], + }, + }, + }, + }, + }, + context + ); + + expect(result.attributes.state.visualization.layers).toEqual([ + { + layerId: 'c61a8afb-a185-4fae-a064-fb3846f6c451', + seriesType: 'area', + // Removed split accessor + splitAccessor: undefined, + xAccessor: undefined, + // Removed a yAcccessor + accessors: [], + }, + ]); + }); + + it('should remove only missing accessors', () => { + const result = migrations['7.7.0'](example, context); + + expect(result.attributes.state.visualization.layers).toEqual([ + { + layerId: 'c61a8afb-a185-4fae-a064-fb3846f6c451', + seriesType: 'area', + xAccessor: 'd6e40cea-6299-43b4-9c9d-b4ee305a2ce8', + // Removed split accessor + splitAccessor: undefined, + // Removed a yAcccessor + accessors: ['d3e62a7a-c259-4fff-a2fc-eebf20b7008a'], + }, + ]); + }); + }); +}); diff --git a/x-pack/plugins/lens/server/migrations.ts b/x-pack/plugins/lens/server/migrations.ts new file mode 100644 index 0000000000000..3d238723b7438 --- /dev/null +++ b/x-pack/plugins/lens/server/migrations.ts @@ -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 { cloneDeep } from 'lodash'; +import { SavedObjectMigrationFn } from 'src/core/server'; + +interface XYLayerPre77 { + layerId: string; + xAccessor: string; + splitAccessor: string; + accessors: string[]; +} + +export const migrations: Record<string, SavedObjectMigrationFn> = { + '7.7.0': doc => { + const newDoc = cloneDeep(doc); + if (newDoc.attributes?.visualizationType === 'lnsXY') { + const datasourceState = newDoc.attributes.state?.datasourceStates?.indexpattern; + const datasourceLayers = datasourceState?.layers ?? {}; + const xyState = newDoc.attributes.state?.visualization; + newDoc.attributes.state.visualization.layers = xyState.layers.map((layer: XYLayerPre77) => { + const layerId = layer.layerId; + const datasource = datasourceLayers[layerId]; + return { + ...layer, + xAccessor: datasource?.columns[layer.xAccessor] ? layer.xAccessor : undefined, + splitAccessor: datasource?.columns[layer.splitAccessor] ? layer.splitAccessor : undefined, + accessors: layer.accessors.filter(accessor => !!datasource?.columns[accessor]), + }; + }) as typeof xyState.layers; + } + return newDoc; + }, +}; diff --git a/x-pack/plugins/lens/server/routes/existing_fields.test.ts b/x-pack/plugins/lens/server/routes/existing_fields.test.ts index 9bd11b6863d93..ed1f85ab902f8 100644 --- a/x-pack/plugins/lens/server/routes/existing_fields.test.ts +++ b/x-pack/plugins/lens/server/routes/existing_fields.test.ts @@ -57,6 +57,24 @@ describe('existingFields', () => { expect(result).toEqual(['geo.coordinates']); }); + it('should handle objects with dotted fields', () => { + const result = existingFields( + [indexPattern({ 'geo.country_name': 'US' })], + [field('geo.country_name')] + ); + + expect(result).toEqual(['geo.country_name']); + }); + + it('should handle arrays with dotted fields on both sides', () => { + const result = existingFields( + [indexPattern({ 'process.cpu': [{ 'user.pct': 50 }] })], + [field('process.cpu.user.pct')] + ); + + expect(result).toEqual(['process.cpu.user.pct']); + }); + it('should be false if it hits a positive leaf before the end of the path', () => { const result = existingFields( [indexPattern({ geo: { coordinates: 32 } })], diff --git a/x-pack/plugins/lens/server/routes/existing_fields.ts b/x-pack/plugins/lens/server/routes/existing_fields.ts index b1964a9150982..3e91d17950061 100644 --- a/x-pack/plugins/lens/server/routes/existing_fields.ts +++ b/x-pack/plugins/lens/server/routes/existing_fields.ts @@ -258,6 +258,8 @@ async function fetchIndexPatternStats({ return result.hits.hits; } +// Recursive function to determine if the _source of a document +// contains a known path. function exists(obj: unknown, path: string[], i = 0): boolean { if (obj == null) { return false; @@ -272,6 +274,22 @@ function exists(obj: unknown, path: string[], i = 0): boolean { } if (typeof obj === 'object') { + // Because Elasticsearch flattens paths, dots in the field name are allowed + // as JSON keys. For example, { 'a.b': 10 } + const partialKeyMatches = Object.getOwnPropertyNames(obj) + .map(key => key.split('.')) + .filter(keyPaths => keyPaths.every((key, keyIndex) => key === path[keyIndex + i])); + + if (partialKeyMatches.length) { + return partialKeyMatches.every(keyPaths => { + return exists( + (obj as Record<string, unknown>)[keyPaths.join('.')], + path, + i + keyPaths.length + ); + }); + } + return exists((obj as Record<string, unknown>)[path[i]], path, i + 1); } diff --git a/x-pack/plugins/lens/server/saved_objects.ts b/x-pack/plugins/lens/server/saved_objects.ts index 42dc750878f45..ac80eb098e780 100644 --- a/x-pack/plugins/lens/server/saved_objects.ts +++ b/x-pack/plugins/lens/server/saved_objects.ts @@ -6,12 +6,13 @@ import { CoreSetup } from 'kibana/server'; import { getEditPath } from '../common'; +import { migrations } from './migrations'; export function setupSavedObjects(core: CoreSetup) { core.savedObjects.registerType({ name: 'lens', hidden: false, - namespaceAgnostic: false, + namespaceType: 'single', management: { icon: 'lensApp', defaultSearchField: 'title', @@ -22,6 +23,7 @@ export function setupSavedObjects(core: CoreSetup) { uiCapabilitiesPath: 'visualize.show', }), }, + migrations, mappings: { properties: { title: { @@ -44,7 +46,7 @@ export function setupSavedObjects(core: CoreSetup) { core.savedObjects.registerType({ name: 'lens-ui-telemetry', hidden: false, - namespaceAgnostic: false, + namespaceType: 'single', mappings: { properties: { name: { diff --git a/x-pack/plugins/lens/server/usage/task.ts b/x-pack/plugins/lens/server/usage/task.ts index e2462be149745..e1a0bf20f7b48 100644 --- a/x-pack/plugins/lens/server/usage/task.ts +++ b/x-pack/plugins/lens/server/usage/task.ts @@ -184,7 +184,10 @@ export function telemetryTaskRunner( ) { return ({ taskInstance }: RunContext) => { const { state } = taskInstance; - const callCluster = core.elasticsearch.adminClient.callAsInternalUser; + const callCluster = async (...args: Parameters<APICaller>) => { + const [coreStart] = await core.getStartServices(); + return coreStart.elasticsearch.legacy.client.callAsInternalUser(...args); + }; return { async run() { @@ -207,6 +210,7 @@ export function telemetryTaskRunner( }) .catch(errMsg => logger.warn(`Error executing lens telemetry task: ${errMsg}`)); }, + async cancel() {}, }; }; } diff --git a/x-pack/plugins/licensing/common/licensing.mock.ts b/x-pack/plugins/licensing/common/licensing.mock.ts index bf8b85e3e981b..4a6b27255587a 100644 --- a/x-pack/plugins/licensing/common/licensing.mock.ts +++ b/x-pack/plugins/licensing/common/licensing.mock.ts @@ -53,6 +53,7 @@ const createLicenseMock = () => { }; mock.check.mockReturnValue({ state: 'valid' }); mock.hasAtLeast.mockReturnValue(true); + mock.getFeature.mockReturnValue({ isAvailable: true, isEnabled: true }); return mock; }; export const licenseMock = { diff --git a/x-pack/plugins/licensing/server/index.ts b/x-pack/plugins/licensing/server/index.ts index 0e14ead7c6c57..76e65afc595c4 100644 --- a/x-pack/plugins/licensing/server/index.ts +++ b/x-pack/plugins/licensing/server/index.ts @@ -12,3 +12,4 @@ export const plugin = (context: PluginInitializerContext) => new LicensingPlugin export * from '../common/types'; export * from './types'; export { config } from './licensing_config'; +export { CheckLicense, wrapRouteWithLicenseCheck } from './wrap_route_with_license_check'; diff --git a/x-pack/plugins/licensing/server/wrap_route_with_license_check.test.ts b/x-pack/plugins/licensing/server/wrap_route_with_license_check.test.ts new file mode 100644 index 0000000000000..7abdd3f6190ce --- /dev/null +++ b/x-pack/plugins/licensing/server/wrap_route_with_license_check.test.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 { httpServerMock } from 'src/core/server/mocks'; + +import { wrapRouteWithLicenseCheck, CheckLicense } from './wrap_route_with_license_check'; + +const context = { + licensing: { + license: {}, + }, +} as any; +const request = httpServerMock.createKibanaRequest(); + +describe('wrapRouteWithLicenseCheck', () => { + it('calls route handler if checkLicense returns "valid": true', async () => { + const checkLicense: CheckLicense = () => ({ valid: true, message: null }); + const routeHandler = jest.fn(); + const wrapper = wrapRouteWithLicenseCheck(checkLicense, routeHandler); + const response = httpServerMock.createResponseFactory(); + + await wrapper(context, request, response); + + expect(routeHandler).toHaveBeenCalledTimes(1); + expect(routeHandler).toHaveBeenCalledWith(context, request, response); + }); + + it('does not call route handler if checkLicense returns "valid": false', async () => { + const checkLicense: CheckLicense = () => ({ valid: false, message: 'reason' }); + const routeHandler = jest.fn(); + const wrapper = wrapRouteWithLicenseCheck(checkLicense, routeHandler); + const response = httpServerMock.createResponseFactory(); + + await wrapper(context, request, response); + + expect(routeHandler).toHaveBeenCalledTimes(0); + expect(response.forbidden).toHaveBeenCalledTimes(1); + expect(response.forbidden).toHaveBeenCalledWith({ body: 'reason' }); + }); + + it('allows an exception to bubble up if handler throws', async () => { + const checkLicense: CheckLicense = () => ({ valid: true, message: null }); + const routeHandler = () => { + throw new Error('reason'); + }; + const wrapper = wrapRouteWithLicenseCheck(checkLicense, routeHandler); + const response = httpServerMock.createResponseFactory(); + + await expect(wrapper(context, request, response)).rejects.toThrowErrorMatchingInlineSnapshot( + `"reason"` + ); + }); + + it('allows an exception to bubble up if "checkLicense" throws', async () => { + const checkLicense: CheckLicense = () => { + throw new Error('reason'); + }; + const routeHandler = jest.fn(); + const wrapper = wrapRouteWithLicenseCheck(checkLicense, routeHandler); + const response = httpServerMock.createResponseFactory(); + + await expect(wrapper(context, request, response)).rejects.toThrowErrorMatchingInlineSnapshot( + `"reason"` + ); + + expect(routeHandler).toHaveBeenCalledTimes(0); + }); +}); diff --git a/x-pack/plugins/licensing/server/wrap_route_with_license_check.ts b/x-pack/plugins/licensing/server/wrap_route_with_license_check.ts new file mode 100644 index 0000000000000..e0cac8d9db208 --- /dev/null +++ b/x-pack/plugins/licensing/server/wrap_route_with_license_check.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. + */ + +import { + RequestHandler, + RequestHandlerContext, + KibanaRequest, + RouteMethod, + KibanaResponseFactory, +} from 'src/core/server'; + +import { ILicense } from '../common/types'; + +export type CheckLicense = ( + license: ILicense +) => { valid: false; message: string } | { valid: true; message: null }; + +export function wrapRouteWithLicenseCheck<P, Q, B>( + checkLicense: CheckLicense, + handler: RequestHandler<P, Q, B> +): RequestHandler<P, Q, B> { + return async ( + context: RequestHandlerContext, + request: KibanaRequest<P, Q, B, RouteMethod>, + response: KibanaResponseFactory + ) => { + const licenseCheckResult = checkLicense(context.licensing.license); + + if (licenseCheckResult.valid) { + return handler(context, request, response); + } else { + return response.forbidden({ + body: licenseCheckResult.message, + }); + } + }; +} diff --git a/x-pack/legacy/plugins/logstash/common/constants/es_scroll_settings.js b/x-pack/plugins/logstash/common/constants/es_scroll_settings.ts similarity index 100% rename from x-pack/legacy/plugins/logstash/common/constants/es_scroll_settings.js rename to x-pack/plugins/logstash/common/constants/es_scroll_settings.ts diff --git a/x-pack/legacy/plugins/logstash/common/constants/index.js b/x-pack/plugins/logstash/common/constants/index.ts similarity index 100% rename from x-pack/legacy/plugins/logstash/common/constants/index.js rename to x-pack/plugins/logstash/common/constants/index.ts diff --git a/x-pack/legacy/plugins/logstash/common/constants/index_names.js b/x-pack/plugins/logstash/common/constants/index_names.ts similarity index 100% rename from x-pack/legacy/plugins/logstash/common/constants/index_names.js rename to x-pack/plugins/logstash/common/constants/index_names.ts diff --git a/x-pack/legacy/plugins/logstash/common/constants/monitoring.js b/x-pack/plugins/logstash/common/constants/monitoring.ts similarity index 100% rename from x-pack/legacy/plugins/logstash/common/constants/monitoring.js rename to x-pack/plugins/logstash/common/constants/monitoring.ts diff --git a/x-pack/legacy/plugins/logstash/common/constants/pagination.js b/x-pack/plugins/logstash/common/constants/pagination.ts similarity index 100% rename from x-pack/legacy/plugins/logstash/common/constants/pagination.js rename to x-pack/plugins/logstash/common/constants/pagination.ts diff --git a/x-pack/legacy/plugins/logstash/common/constants/pipeline.js b/x-pack/plugins/logstash/common/constants/pipeline.ts similarity index 100% rename from x-pack/legacy/plugins/logstash/common/constants/pipeline.js rename to x-pack/plugins/logstash/common/constants/pipeline.ts diff --git a/x-pack/legacy/plugins/logstash/common/constants/plugin.js b/x-pack/plugins/logstash/common/constants/plugin.ts similarity index 100% rename from x-pack/legacy/plugins/logstash/common/constants/plugin.js rename to x-pack/plugins/logstash/common/constants/plugin.ts diff --git a/x-pack/legacy/plugins/logstash/common/constants/routes.js b/x-pack/plugins/logstash/common/constants/routes.ts similarity index 100% rename from x-pack/legacy/plugins/logstash/common/constants/routes.js rename to x-pack/plugins/logstash/common/constants/routes.ts diff --git a/x-pack/legacy/plugins/logstash/common/constants/tooltips.js b/x-pack/plugins/logstash/common/constants/tooltips.ts similarity index 100% rename from x-pack/legacy/plugins/logstash/common/constants/tooltips.js rename to x-pack/plugins/logstash/common/constants/tooltips.ts diff --git a/x-pack/plugins/logstash/kibana.json b/x-pack/plugins/logstash/kibana.json new file mode 100644 index 0000000000000..bcc926535d3c2 --- /dev/null +++ b/x-pack/plugins/logstash/kibana.json @@ -0,0 +1,12 @@ +{ + "id": "logstash", + "version": "0.0.1", + "kibanaVersion": "kibana", + "configPath": ["xpack", "logstash"], + "requiredPlugins": [ + "licensing" + ], + "optionalPlugins": ["security"], + "server": true, + "ui": false +} diff --git a/x-pack/plugins/logstash/server/index.ts b/x-pack/plugins/logstash/server/index.ts new file mode 100644 index 0000000000000..cc65184a1f3a0 --- /dev/null +++ b/x-pack/plugins/logstash/server/index.ts @@ -0,0 +1,16 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { schema } from '@kbn/config-schema'; +import { PluginInitializerContext, PluginConfigDescriptor } from 'src/core/server'; +import { LogstashPlugin } from './plugin'; + +export const plugin = (context: PluginInitializerContext) => new LogstashPlugin(context); + +export const config: PluginConfigDescriptor = { + schema: schema.object({ + enabled: schema.boolean({ defaultValue: true }), + }), +}; diff --git a/x-pack/plugins/logstash/server/lib/check_license/check_license.test.ts b/x-pack/plugins/logstash/server/lib/check_license/check_license.test.ts new file mode 100755 index 0000000000000..324e0a22ff378 --- /dev/null +++ b/x-pack/plugins/logstash/server/lib/check_license/check_license.test.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 { licensingMock } from '../../../../licensing/server/mocks'; +import { checkLicense } from './check_license'; + +describe('check_license', function() { + describe('returns "valid": false & message when', () => { + it('license information is not available', () => { + const license = licensingMock.createLicenseMock(); + license.isAvailable = false; + + const { valid, message } = checkLicense(license); + + expect(valid).toBe(false); + expect(message).toStrictEqual(expect.any(String)); + }); + + it('license level is not enough', () => { + const license = licensingMock.createLicenseMock(); + license.hasAtLeast.mockReturnValue(false); + + const { valid, message } = checkLicense(license); + + expect(valid).toBe(false); + expect(message).toStrictEqual(expect.any(String)); + }); + + it('license is expired', () => { + const license = licensingMock.createLicenseMock(); + license.isActive = false; + + const { valid, message } = checkLicense(license); + + expect(valid).toBe(false); + expect(message).toStrictEqual(expect.any(String)); + }); + + it('elasticsearch security is disabled', () => { + const license = licensingMock.createLicenseMock(); + license.getFeature.mockReturnValue({ isEnabled: false, isAvailable: false }); + + const { valid, message } = checkLicense(license); + + expect(valid).toBe(false); + expect(message).toStrictEqual(expect.any(String)); + }); + }); + + it('returns "valid": true without message otherwise', () => { + const license = licensingMock.createLicenseMock(); + + const { valid, message } = checkLicense(license); + + expect(valid).toBe(true); + expect(message).toBe(null); + }); +}); diff --git a/x-pack/plugins/logstash/server/lib/check_license/check_license.ts b/x-pack/plugins/logstash/server/lib/check_license/check_license.ts new file mode 100644 index 0000000000000..4eef2eb9b0681 --- /dev/null +++ b/x-pack/plugins/logstash/server/lib/check_license/check_license.ts @@ -0,0 +1,65 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { i18n } from '@kbn/i18n'; + +import { CheckLicense } from '../../../../licensing/server'; + +export const checkLicense: CheckLicense = license => { + if (!license.isAvailable) { + return { + valid: false, + message: i18n.translate( + 'xpack.logstash.managementSection.notPossibleToManagePipelinesMessage', + { + defaultMessage: + 'You cannot manage Logstash pipelines because license information is not available at this time.', + } + ), + }; + } + + if (!license.hasAtLeast('standard')) { + return { + valid: false, + message: i18n.translate('xpack.logstash.managementSection.licenseDoesNotSupportDescription', { + defaultMessage: + 'Your {licenseType} license does not support Logstash pipeline management features. Please upgrade your license.', + values: { licenseType: license.type }, + }), + }; + } + + if (!license.isActive) { + return { + valid: false, + message: i18n.translate( + 'xpack.logstash.managementSection.pipelineCrudOperationsNotAllowedDescription', + { + defaultMessage: + 'You cannot edit, create, or delete your Logstash pipelines because your {licenseType} license has expired.', + values: { licenseType: license.type }, + } + ), + }; + } + + if (!license.getFeature('security').isEnabled) { + return { + valid: false, + message: i18n.translate('xpack.logstash.managementSection.enableSecurityDescription', { + defaultMessage: + 'Security must be enabled in order to use Logstash pipeline management features.' + + ' Please set xpack.security.enabled: true in your elasticsearch.yml.', + }), + }; + } + + return { + valid: true, + message: null, + }; +}; diff --git a/x-pack/legacy/plugins/index_lifecycle_management/server/lib/check_license/index.ts b/x-pack/plugins/logstash/server/lib/check_license/index.ts similarity index 100% rename from x-pack/legacy/plugins/index_lifecycle_management/server/lib/check_license/index.ts rename to x-pack/plugins/logstash/server/lib/check_license/index.ts diff --git a/x-pack/plugins/logstash/server/lib/fetch_all_from_scroll/fetch_all_from_scroll.test.ts b/x-pack/plugins/logstash/server/lib/fetch_all_from_scroll/fetch_all_from_scroll.test.ts new file mode 100755 index 0000000000000..8cd6b70d47570 --- /dev/null +++ b/x-pack/plugins/logstash/server/lib/fetch_all_from_scroll/fetch_all_from_scroll.test.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 { fetchAllFromScroll } from './fetch_all_from_scroll'; + +describe('fetch_all_from_scroll', () => { + let stubCallWithRequest: jest.Mock; + + beforeEach(() => { + stubCallWithRequest = jest.fn(); + stubCallWithRequest + .mockResolvedValueOnce({ + hits: { + hits: ['newhit'], + }, + _scroll_id: 'newScrollId', + }) + .mockResolvedValueOnce({ + hits: { + hits: [], + }, + }); + }); + + describe('#fetchAllFromScroll', () => { + describe('when the passed-in response has no hits', () => { + const mockResponse = { + hits: { + hits: [], + }, + }; + + it('should return an empty array of hits', async () => { + const hits = await fetchAllFromScroll(mockResponse as any, stubCallWithRequest); + expect(hits).toEqual([]); + }); + + it('should not call callWithRequest', async () => { + await fetchAllFromScroll(mockResponse as any, stubCallWithRequest); + expect(stubCallWithRequest).toHaveBeenCalledTimes(0); + }); + }); + + describe('when the passed-in response has some hits', () => { + const mockResponse = { + hits: { + hits: ['foo', 'bar'], + }, + _scroll_id: 'originalScrollId', + }; + + it('should return the hits from the response', async () => { + const hits = await fetchAllFromScroll(mockResponse as any, stubCallWithRequest); + expect(hits).toEqual(['foo', 'bar', 'newhit']); + }); + + it('should call callWithRequest', async () => { + await fetchAllFromScroll(mockResponse as any, stubCallWithRequest); + expect(stubCallWithRequest).toHaveBeenCalledTimes(2); + + const firstCallWithRequestCallArgs = stubCallWithRequest.mock.calls[0]; + expect(firstCallWithRequestCallArgs[1].body.scroll_id).toBe('originalScrollId'); + + const secondCallWithRequestCallArgs = stubCallWithRequest.mock.calls[1]; + expect(secondCallWithRequestCallArgs[1].body.scroll_id).toBe('newScrollId'); + }); + }); + }); +}); diff --git a/x-pack/plugins/logstash/server/lib/fetch_all_from_scroll/fetch_all_from_scroll.ts b/x-pack/plugins/logstash/server/lib/fetch_all_from_scroll/fetch_all_from_scroll.ts new file mode 100755 index 0000000000000..060cf188a4c60 --- /dev/null +++ b/x-pack/plugins/logstash/server/lib/fetch_all_from_scroll/fetch_all_from_scroll.ts @@ -0,0 +1,34 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { APICaller } from 'src/core/server'; +import { SearchResponse } from 'elasticsearch'; + +import { ES_SCROLL_SETTINGS } from '../../../common/constants'; +import { Hits } from '../../types'; + +export async function fetchAllFromScroll( + response: SearchResponse<any>, + callWithRequest: APICaller, + hits: Hits = [] +): Promise<Hits> { + const newHits = response.hits?.hits || []; + const scrollId = response._scroll_id; + + if (newHits.length > 0) { + hits.push(...newHits); + + const innerResponse = await callWithRequest('scroll', { + body: { + scroll: ES_SCROLL_SETTINGS.KEEPALIVE, + scroll_id: scrollId, + }, + }); + + return await fetchAllFromScroll(innerResponse, callWithRequest, hits); + } + + return hits; +} diff --git a/x-pack/legacy/plugins/logstash/server/lib/fetch_all_from_scroll/index.js b/x-pack/plugins/logstash/server/lib/fetch_all_from_scroll/index.ts similarity index 100% rename from x-pack/legacy/plugins/logstash/server/lib/fetch_all_from_scroll/index.js rename to x-pack/plugins/logstash/server/lib/fetch_all_from_scroll/index.ts diff --git a/x-pack/plugins/logstash/server/models/cluster/cluster.test.ts b/x-pack/plugins/logstash/server/models/cluster/cluster.test.ts new file mode 100755 index 0000000000000..63f9f1e58f6ec --- /dev/null +++ b/x-pack/plugins/logstash/server/models/cluster/cluster.test.ts @@ -0,0 +1,22 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { Cluster } from './cluster'; + +describe('cluster', () => { + describe('Cluster', () => { + describe('fromUpstreamJSON factory method', () => { + const upstreamJSON = { + cluster_uuid: 'S-S4NNZDRV-g9c-JrIhx6A', + }; + + it('returns correct Cluster instance', () => { + const cluster = Cluster.fromUpstreamJSON(upstreamJSON); + expect(cluster.uuid).toEqual(upstreamJSON.cluster_uuid); + }); + }); + }); +}); diff --git a/x-pack/plugins/logstash/server/models/cluster/cluster.ts b/x-pack/plugins/logstash/server/models/cluster/cluster.ts new file mode 100755 index 0000000000000..54f03605e14d6 --- /dev/null +++ b/x-pack/plugins/logstash/server/models/cluster/cluster.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 { get } from 'lodash'; + +/** + * This model deals with a cluster object from ES and converts it to Kibana downstream + */ +export class Cluster { + public readonly uuid: string; + constructor({ uuid }: { uuid: string }) { + this.uuid = uuid; + } + + public get downstreamJSON() { + const json = { + uuid: this.uuid, + }; + + return json; + } + + // generate Pipeline object from elasticsearch response + static fromUpstreamJSON(upstreamCluster: Record<string, string>) { + const uuid = get<string>(upstreamCluster, 'cluster_uuid'); + return new Cluster({ uuid }); + } +} diff --git a/x-pack/legacy/plugins/logstash/server/models/cluster/index.js b/x-pack/plugins/logstash/server/models/cluster/index.ts similarity index 100% rename from x-pack/legacy/plugins/logstash/server/models/cluster/index.js rename to x-pack/plugins/logstash/server/models/cluster/index.ts diff --git a/x-pack/legacy/plugins/logstash/server/models/pipeline/index.js b/x-pack/plugins/logstash/server/models/pipeline/index.ts similarity index 100% rename from x-pack/legacy/plugins/logstash/server/models/pipeline/index.js rename to x-pack/plugins/logstash/server/models/pipeline/index.ts diff --git a/x-pack/plugins/logstash/server/models/pipeline/pipeline.test.ts b/x-pack/plugins/logstash/server/models/pipeline/pipeline.test.ts new file mode 100755 index 0000000000000..82ce0d72e2052 --- /dev/null +++ b/x-pack/plugins/logstash/server/models/pipeline/pipeline.test.ts @@ -0,0 +1,75 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { Pipeline } from './pipeline'; + +describe('pipeline', () => { + describe('Pipeline', () => { + describe('fromUpstreamJSON factory method', () => { + const upstreamJSON = { + _id: 'apache', + _source: { + description: 'this is an apache pipeline', + pipeline_metadata: { + version: 1, + type: 'logstash_pipeline', + }, + username: 'elastic', + pipeline: 'input {} filter { grok {} }\n output {}', + }, + }; + + it('returns correct Pipeline instance', () => { + const pipeline = Pipeline.fromUpstreamJSON(upstreamJSON); + expect(pipeline.id).toBe(upstreamJSON._id); + expect(pipeline.description).toBe(upstreamJSON._source.description); + expect(pipeline.username).toBe(upstreamJSON._source.username); + expect(pipeline.pipeline).toBe(upstreamJSON._source.pipeline); + }); + + it('throws if pipeline argument does not contain an id property', () => { + const badJSON = { + // no _id + _source: upstreamJSON._source, + }; + const testFromUpstreamJsonError = () => { + return Pipeline.fromUpstreamJSON(badJSON); + }; + expect(testFromUpstreamJsonError).toThrowError( + /upstreamPipeline argument must contain an id property/i + ); + }); + }); + + describe('upstreamJSON getter method', () => { + it('returns the upstream JSON', () => { + const downstreamJSON = { + id: 'apache', + description: 'this is an apache pipeline', + username: 'elastic', + pipeline: 'input {} filter { grok {} }\n output {}', + }; + const pipeline = new Pipeline(downstreamJSON); + const expectedUpstreamJSON = { + description: 'this is an apache pipeline', + pipeline_metadata: { + type: 'logstash_pipeline', + version: 1, + }, + username: 'elastic', + pipeline: 'input {} filter { grok {} }\n output {}', + }; + // can't do an object level comparison because modified field is always `now` + expect(pipeline.upstreamJSON.last_modified).toStrictEqual(expect.any(String)); + expect(pipeline.upstreamJSON.description).toBe(expectedUpstreamJSON.description); + expect(pipeline.upstreamJSON.pipeline_metadata).toEqual( + expectedUpstreamJSON.pipeline_metadata + ); + expect(pipeline.upstreamJSON.pipeline).toBe(expectedUpstreamJSON.pipeline); + }); + }); + }); +}); diff --git a/x-pack/plugins/logstash/server/models/pipeline/pipeline.ts b/x-pack/plugins/logstash/server/models/pipeline/pipeline.ts new file mode 100755 index 0000000000000..3f2debeebeb46 --- /dev/null +++ b/x-pack/plugins/logstash/server/models/pipeline/pipeline.ts @@ -0,0 +1,116 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import moment from 'moment'; +import { badRequest } from 'boom'; +import { get } from 'lodash'; +import { i18n } from '@kbn/i18n'; + +interface PipelineOptions { + id: string; + description: string; + pipeline: string; + username?: string; + settings?: Record<string, any>; +} + +interface DownstreamPipeline { + description: string; + pipeline: string; + settings?: Record<string, any>; +} +/** + * This model deals with a pipeline object from ES and converts it to Kibana downstream + */ +export class Pipeline { + public readonly id: string; + public readonly description: string; + public readonly username?: string; + public readonly pipeline: string; + private readonly settings: Record<string, any>; + + constructor(options: PipelineOptions) { + this.id = options.id; + this.description = options.description; + this.username = options.username; + this.pipeline = options.pipeline; + this.settings = options.settings || {}; + } + + public get downstreamJSON() { + const json = { + id: this.id, + description: this.description, + username: this.username, + pipeline: this.pipeline, + settings: this.settings, + }; + + return json; + } + + /** + * Returns the JSON schema for the pipeline doc that Elasticsearch expects + * For now, we hard code pipeline_metadata since we don't use it yet + * pipeline_metadata.version is the version of the Logstash config stored in + * pipeline field. + * pipeline_metadata.type is the Logstash config type (future: LIR, json, etc) + * @return {[JSON]} [Elasticsearch JSON] + */ + public get upstreamJSON() { + return { + description: this.description, + last_modified: moment().toISOString(), + pipeline_metadata: { + version: 1, + type: 'logstash_pipeline', + }, + username: this.username, + pipeline: this.pipeline, + pipeline_settings: this.settings, + }; + } + + // generate Pipeline object from kibana response + static fromDownstreamJSON( + downstreamPipeline: DownstreamPipeline, + pipelineId: string, + username?: string + ) { + const opts = { + id: pipelineId, + description: downstreamPipeline.description, + username, + pipeline: downstreamPipeline.pipeline, + settings: downstreamPipeline.settings, + }; + + return new Pipeline(opts); + } + + // generate Pipeline object from elasticsearch response + static fromUpstreamJSON(upstreamPipeline: Record<string, any>) { + if (!upstreamPipeline._id) { + throw badRequest( + i18n.translate( + 'xpack.logstash.upstreamPipelineArgumentMustContainAnIdPropertyErrorMessage', + { + defaultMessage: 'upstreamPipeline argument must contain an id property', + } + ) + ); + } + const id = get<string>(upstreamPipeline, '_id'); + const description = get<string>(upstreamPipeline, '_source.description'); + const username = get<string>(upstreamPipeline, '_source.username'); + const pipeline = get<string>(upstreamPipeline, '_source.pipeline'); + const settings = get<Record<string, any>>(upstreamPipeline, '_source.pipeline_settings'); + + const opts: PipelineOptions = { id, description, username, pipeline, settings }; + + return new Pipeline(opts); + } +} diff --git a/x-pack/legacy/plugins/logstash/server/models/pipeline_list_item/index.js b/x-pack/plugins/logstash/server/models/pipeline_list_item/index.ts similarity index 100% rename from x-pack/legacy/plugins/logstash/server/models/pipeline_list_item/index.js rename to x-pack/plugins/logstash/server/models/pipeline_list_item/index.ts diff --git a/x-pack/plugins/logstash/server/models/pipeline_list_item/pipeline_list_item.test.ts b/x-pack/plugins/logstash/server/models/pipeline_list_item/pipeline_list_item.test.ts new file mode 100755 index 0000000000000..c557e84443b02 --- /dev/null +++ b/x-pack/plugins/logstash/server/models/pipeline_list_item/pipeline_list_item.test.ts @@ -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. + */ + +import { PipelineListItem } from './pipeline_list_item'; + +describe('pipeline_list_item', () => { + describe('PipelineListItem', () => { + const upstreamJSON = { + _id: 'apache', + _source: { + description: 'this is an apache pipeline', + last_modified: '2017-05-14T02:50:51.250Z', + pipeline_metadata: { + type: 'logstash_pipeline', + version: 1, + }, + username: 'elastic', + pipeline: 'input {} filter { grok {} }\n output {}', + }, + _index: 'index', + _type: 'type', + _score: 100, + }; + + describe('fromUpstreamJSON factory method', () => { + it('returns correct PipelineListItem instance', () => { + const pipelineListItem = PipelineListItem.fromUpstreamJSON(upstreamJSON); + expect(pipelineListItem.id).toBe(upstreamJSON._id); + expect(pipelineListItem.description).toBe(upstreamJSON._source.description); + expect(pipelineListItem.username).toBe(upstreamJSON._source.username); + expect(pipelineListItem.last_modified).toBe(upstreamJSON._source.last_modified); + }); + }); + + describe('downstreamJSON getter method', () => { + it('returns the downstreamJSON JSON', () => { + const pipelineListItem = PipelineListItem.fromUpstreamJSON(upstreamJSON); + const expectedDownstreamJSON = { + id: 'apache', + description: 'this is an apache pipeline', + username: 'elastic', + last_modified: '2017-05-14T02:50:51.250Z', + }; + expect(pipelineListItem.downstreamJSON).toEqual(expectedDownstreamJSON); + }); + }); + }); +}); diff --git a/x-pack/plugins/logstash/server/models/pipeline_list_item/pipeline_list_item.ts b/x-pack/plugins/logstash/server/models/pipeline_list_item/pipeline_list_item.ts new file mode 100755 index 0000000000000..98c91fca1fcca --- /dev/null +++ b/x-pack/plugins/logstash/server/models/pipeline_list_item/pipeline_list_item.ts @@ -0,0 +1,47 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { get } from 'lodash'; +import { Hit, PipelineListItemOptions } from '../../types'; + +export class PipelineListItem { + public readonly id: string; + public readonly description: string; + public readonly last_modified: string; + public readonly username: string; + constructor(options: PipelineListItemOptions) { + this.id = options.id; + this.description = options.description; + this.last_modified = options.last_modified; + this.username = options.username; + } + + public get downstreamJSON() { + const json = { + id: this.id, + description: this.description, + last_modified: this.last_modified, + username: this.username, + }; + + return json; + } + + /** + * Takes the json GET response from ES and constructs a pipeline model to be used + * in Kibana downstream + */ + static fromUpstreamJSON(pipeline: Hit) { + const opts = { + id: pipeline._id, + description: get<string>(pipeline, '_source.description'), + last_modified: get<string>(pipeline, '_source.last_modified'), + username: get<string>(pipeline, '_source.username'), + }; + + return new PipelineListItem(opts); + } +} diff --git a/x-pack/plugins/logstash/server/plugin.ts b/x-pack/plugins/logstash/server/plugin.ts new file mode 100644 index 0000000000000..c048dd13bfb0c --- /dev/null +++ b/x-pack/plugins/logstash/server/plugin.ts @@ -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. + */ +import { + CoreSetup, + CoreStart, + ICustomClusterClient, + Logger, + Plugin, + PluginInitializerContext, +} from 'src/core/server'; +import { LicensingPluginSetup } from '../../licensing/server'; +import { SecurityPluginSetup } from '../../security/server'; + +import { registerRoutes } from './routes'; + +interface SetupDeps { + licensing: LicensingPluginSetup; + security?: SecurityPluginSetup; +} + +export class LogstashPlugin implements Plugin { + private readonly logger: Logger; + private esClient?: ICustomClusterClient; + private coreSetup?: CoreSetup; + constructor(context: PluginInitializerContext) { + this.logger = context.logger.get(); + } + + setup(core: CoreSetup, deps: SetupDeps) { + this.logger.debug('Setting up Logstash plugin'); + + this.coreSetup = core; + registerRoutes(core.http.createRouter(), deps.security); + } + + start(core: CoreStart) { + const esClient = core.elasticsearch.legacy.createClient('logstash'); + + this.coreSetup!.http.registerRouteHandlerContext('logstash', async (context, request) => { + return { esClient: esClient.asScoped(request) }; + }); + } + stop() { + if (this.esClient) { + this.esClient.close(); + } + } +} diff --git a/x-pack/plugins/logstash/server/routes/cluster/index.ts b/x-pack/plugins/logstash/server/routes/cluster/index.ts new file mode 100644 index 0000000000000..c43c409cf5a17 --- /dev/null +++ b/x-pack/plugins/logstash/server/routes/cluster/index.ts @@ -0,0 +1,6 @@ +/* + * 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 { registerClusterLoadRoute } from './load'; diff --git a/x-pack/plugins/logstash/server/routes/cluster/load.ts b/x-pack/plugins/logstash/server/routes/cluster/load.ts new file mode 100644 index 0000000000000..18fe21f3da675 --- /dev/null +++ b/x-pack/plugins/logstash/server/routes/cluster/load.ts @@ -0,0 +1,34 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { IRouter } from 'src/core/server'; +import { wrapRouteWithLicenseCheck } from '../../../../licensing/server'; +import { Cluster } from '../../models/cluster'; +import { checkLicense } from '../../lib/check_license'; + +export function registerClusterLoadRoute(router: IRouter) { + router.get( + { + path: '/api/logstash/cluster', + validate: false, + }, + wrapRouteWithLicenseCheck(checkLicense, async (context, request, response) => { + try { + const client = context.logstash!.esClient; + const info = await client.callAsCurrentUser('info'); + return response.ok({ + body: { + cluster: Cluster.fromUpstreamJSON(info).downstreamJSON, + }, + }); + } catch (err) { + if (err.status === 403) { + return response.ok(); + } + return response.internalError(); + } + }) + ); +} diff --git a/x-pack/plugins/logstash/server/routes/index.ts b/x-pack/plugins/logstash/server/routes/index.ts new file mode 100644 index 0000000000000..0c7183b409055 --- /dev/null +++ b/x-pack/plugins/logstash/server/routes/index.ts @@ -0,0 +1,28 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { IRouter } from 'src/core/server'; +import { SecurityPluginSetup } from '../../../security/server'; +import { registerClusterLoadRoute } from './cluster'; +import { + registerPipelineDeleteRoute, + registerPipelineLoadRoute, + registerPipelineSaveRoute, +} from './pipeline'; +import { registerPipelinesListRoute, registerPipelinesDeleteRoute } from './pipelines'; +import { registerUpgradeRoute } from './upgrade'; + +export function registerRoutes(router: IRouter, security?: SecurityPluginSetup) { + registerClusterLoadRoute(router); + + registerPipelineDeleteRoute(router); + registerPipelineLoadRoute(router); + registerPipelineSaveRoute(router, security); + + registerPipelinesListRoute(router); + registerPipelinesDeleteRoute(router); + + registerUpgradeRoute(router); +} diff --git a/x-pack/plugins/logstash/server/routes/pipeline/delete.ts b/x-pack/plugins/logstash/server/routes/pipeline/delete.ts new file mode 100644 index 0000000000000..4aeae3e0ae2d5 --- /dev/null +++ b/x-pack/plugins/logstash/server/routes/pipeline/delete.ts @@ -0,0 +1,38 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { schema } from '@kbn/config-schema'; +import { IRouter } from 'src/core/server'; +import { INDEX_NAMES } from '../../../common/constants'; +import { wrapRouteWithLicenseCheck } from '../../../../licensing/server'; + +import { checkLicense } from '../../lib/check_license'; + +export function registerPipelineDeleteRoute(router: IRouter) { + router.delete( + { + path: '/api/logstash/pipeline/{id}', + validate: { + params: schema.object({ + id: schema.string(), + }), + }, + }, + wrapRouteWithLicenseCheck( + checkLicense, + router.handleLegacyErrors(async (context, request, response) => { + const client = context.logstash!.esClient; + + await client.callAsCurrentUser('delete', { + index: INDEX_NAMES.PIPELINES, + id: request.params.id, + refresh: 'wait_for', + }); + + return response.noContent(); + }) + ) + ); +} diff --git a/x-pack/plugins/logstash/server/routes/pipeline/index.ts b/x-pack/plugins/logstash/server/routes/pipeline/index.ts new file mode 100644 index 0000000000000..e7db6e18ddaf3 --- /dev/null +++ b/x-pack/plugins/logstash/server/routes/pipeline/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 { registerPipelineDeleteRoute } from './delete'; +export { registerPipelineLoadRoute } from './load'; +export { registerPipelineSaveRoute } from './save'; diff --git a/x-pack/plugins/logstash/server/routes/pipeline/load.ts b/x-pack/plugins/logstash/server/routes/pipeline/load.ts new file mode 100644 index 0000000000000..fec9097114d26 --- /dev/null +++ b/x-pack/plugins/logstash/server/routes/pipeline/load.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 { schema } from '@kbn/config-schema'; +import { IRouter } from 'src/core/server'; + +import { INDEX_NAMES } from '../../../common/constants'; +import { Pipeline } from '../../models/pipeline'; +import { wrapRouteWithLicenseCheck } from '../../../../licensing/server'; +import { checkLicense } from '../../lib/check_license'; + +export function registerPipelineLoadRoute(router: IRouter) { + router.get( + { + path: '/api/logstash/pipeline/{id}', + validate: { + params: schema.object({ + id: schema.string(), + }), + }, + }, + wrapRouteWithLicenseCheck( + checkLicense, + router.handleLegacyErrors(async (context, request, response) => { + const client = context.logstash!.esClient; + + const result = await client.callAsCurrentUser('get', { + index: INDEX_NAMES.PIPELINES, + id: request.params.id, + _source: ['description', 'username', 'pipeline', 'pipeline_settings'], + ignore: [404], + }); + + if (!result.found) { + return response.notFound(); + } + + return response.ok({ + body: Pipeline.fromUpstreamJSON(result).downstreamJSON, + }); + }) + ) + ); +} diff --git a/x-pack/plugins/logstash/server/routes/pipeline/save.ts b/x-pack/plugins/logstash/server/routes/pipeline/save.ts new file mode 100644 index 0000000000000..556c281944a85 --- /dev/null +++ b/x-pack/plugins/logstash/server/routes/pipeline/save.ts @@ -0,0 +1,69 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { schema } from '@kbn/config-schema'; +import { i18n } from '@kbn/i18n'; +import { IRouter } from 'src/core/server'; + +import { INDEX_NAMES } from '../../../common/constants'; +import { Pipeline } from '../../models/pipeline'; +import { wrapRouteWithLicenseCheck } from '../../../../licensing/server'; +import { SecurityPluginSetup } from '../../../../security/server'; +import { checkLicense } from '../../lib/check_license'; + +export function registerPipelineSaveRoute(router: IRouter, security?: SecurityPluginSetup) { + router.put( + { + path: '/api/logstash/pipeline/{id}', + validate: { + params: schema.object({ + id: schema.string(), + }), + body: schema.object({ + id: schema.string(), + description: schema.string(), + pipeline: schema.string(), + username: schema.string(), + settings: schema.maybe(schema.object({}, { unknowns: 'allow' })), + }), + }, + }, + wrapRouteWithLicenseCheck( + checkLicense, + router.handleLegacyErrors(async (context, request, response) => { + try { + let username: string | undefined; + if (security) { + const user = await security.authc.getCurrentUser(request); + username = user?.username; + } + + const client = context.logstash!.esClient; + const pipeline = Pipeline.fromDownstreamJSON(request.body, request.params.id, username); + + await client.callAsCurrentUser('index', { + index: INDEX_NAMES.PIPELINES, + id: pipeline.id, + body: pipeline.upstreamJSON, + refresh: 'wait_for', + }); + + return response.noContent(); + } catch (err) { + const statusCode = err.statusCode; + // handles the permissions issue of Elasticsearch + if (statusCode === 403) { + return response.forbidden({ + body: i18n.translate('xpack.logstash.insufficientUserPermissionsDescription', { + defaultMessage: 'Insufficient user permissions for managing Logstash pipelines', + }), + }); + } + throw err; + } + }) + ) + ); +} diff --git a/x-pack/plugins/logstash/server/routes/pipelines/delete.ts b/x-pack/plugins/logstash/server/routes/pipelines/delete.ts new file mode 100644 index 0000000000000..ac3097ac0424b --- /dev/null +++ b/x-pack/plugins/logstash/server/routes/pipelines/delete.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 { schema } from '@kbn/config-schema'; +import { APICaller, IRouter } from 'src/core/server'; +import { wrapRouteWithLicenseCheck } from '../../../../licensing/server'; + +import { INDEX_NAMES } from '../../../common/constants'; +import { checkLicense } from '../../lib/check_license'; + +async function deletePipelines(callWithRequest: APICaller, pipelineIds: string[]) { + const deletePromises = pipelineIds.map(pipelineId => { + return callWithRequest('delete', { + index: INDEX_NAMES.PIPELINES, + id: pipelineId, + refresh: 'wait_for', + }) + .then(success => ({ success })) + .catch(error => ({ error })); + }); + + const results = await Promise.all(deletePromises); + const successes = results.filter(result => Reflect.has(result, 'success')); + const errors = results.filter(result => Reflect.has(result, 'error')); + + return { + numSuccesses: successes.length, + numErrors: errors.length, + }; +} + +export function registerPipelinesDeleteRoute(router: IRouter) { + router.post( + { + path: '/api/logstash/pipelines/delete', + validate: { + body: schema.object({ + pipelineIds: schema.arrayOf(schema.string()), + }), + }, + }, + wrapRouteWithLicenseCheck( + checkLicense, + router.handleLegacyErrors(async (context, request, response) => { + const client = context.logstash!.esClient; + const results = await deletePipelines(client.callAsCurrentUser, request.body.pipelineIds); + + return response.ok({ body: { results } }); + }) + ) + ); +} diff --git a/x-pack/plugins/logstash/server/routes/pipelines/index.ts b/x-pack/plugins/logstash/server/routes/pipelines/index.ts new file mode 100644 index 0000000000000..36681502cfc70 --- /dev/null +++ b/x-pack/plugins/logstash/server/routes/pipelines/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 { registerPipelinesListRoute } from './list'; +export { registerPipelinesDeleteRoute } from './delete'; diff --git a/x-pack/plugins/logstash/server/routes/pipelines/list.ts b/x-pack/plugins/logstash/server/routes/pipelines/list.ts new file mode 100644 index 0000000000000..bc477a25a7988 --- /dev/null +++ b/x-pack/plugins/logstash/server/routes/pipelines/list.ts @@ -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 { i18n } from '@kbn/i18n'; +import { SearchResponse } from 'elasticsearch'; +import { APICaller, IRouter } from 'src/core/server'; +import { wrapRouteWithLicenseCheck } from '../../../../licensing/server'; + +import { INDEX_NAMES, ES_SCROLL_SETTINGS } from '../../../common/constants'; +import { PipelineListItem } from '../../models/pipeline_list_item'; +import { fetchAllFromScroll } from '../../lib/fetch_all_from_scroll'; +import { checkLicense } from '../../lib/check_license'; + +async function fetchPipelines(callWithRequest: APICaller) { + const params = { + index: INDEX_NAMES.PIPELINES, + scroll: ES_SCROLL_SETTINGS.KEEPALIVE, + body: { + size: ES_SCROLL_SETTINGS.PAGE_SIZE, + }, + ignore: [404], + }; + + const response = await callWithRequest<SearchResponse<any>>('search', params); + return fetchAllFromScroll(response, callWithRequest); +} + +export function registerPipelinesListRoute(router: IRouter) { + router.get( + { + path: '/api/logstash/pipelines', + validate: false, + }, + wrapRouteWithLicenseCheck( + checkLicense, + router.handleLegacyErrors(async (context, request, response) => { + try { + const client = context.logstash!.esClient; + const pipelinesHits = await fetchPipelines(client.callAsCurrentUser); + + const pipelines = pipelinesHits.map(pipeline => { + return PipelineListItem.fromUpstreamJSON(pipeline).downstreamJSON; + }); + + return response.ok({ body: { pipelines } }); + } catch (err) { + const statusCode = err.statusCode; + // handles the permissions issue of Elasticsearch + if (statusCode === 403) { + return response.forbidden({ + body: i18n.translate('xpack.logstash.insufficientUserPermissionsDescription', { + defaultMessage: 'Insufficient user permissions for managing Logstash pipelines', + }), + }); + } + throw err; + } + }) + ) + ); +} diff --git a/x-pack/plugins/logstash/server/routes/upgrade/index.ts b/x-pack/plugins/logstash/server/routes/upgrade/index.ts new file mode 100644 index 0000000000000..3a5b0868b446b --- /dev/null +++ b/x-pack/plugins/logstash/server/routes/upgrade/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 { registerUpgradeRoute } from './upgrade'; diff --git a/x-pack/plugins/logstash/server/routes/upgrade/upgrade.ts b/x-pack/plugins/logstash/server/routes/upgrade/upgrade.ts new file mode 100644 index 0000000000000..2bd2c0f89e190 --- /dev/null +++ b/x-pack/plugins/logstash/server/routes/upgrade/upgrade.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 { IRouter } from 'src/core/server'; +import { wrapRouteWithLicenseCheck } from '../../../../licensing/server'; + +import { INDEX_NAMES } from '../../../common/constants'; +import { checkLicense } from '../../lib/check_license'; + +export function registerUpgradeRoute(router: IRouter) { + router.post( + { + path: '/api/logstash/upgrade', + validate: false, + }, + wrapRouteWithLicenseCheck( + checkLicense, + router.handleLegacyErrors(async (context, request, response) => { + const client = context.logstash!.esClient; + + const doesIndexExist = await client.callAsCurrentUser('indices.exists', { + index: INDEX_NAMES.PIPELINES, + }); + + // If index doesn't exist yet, there is no mapping to upgrade + if (doesIndexExist) { + await client.callAsCurrentUser('indices.putMapping', { + index: INDEX_NAMES.PIPELINES, + body: { + properties: { + pipeline_settings: { + dynamic: false, + type: 'object', + }, + }, + }, + }); + } + + return response.ok({ body: { is_upgraded: true } }); + }) + ) + ); +} diff --git a/x-pack/plugins/logstash/server/types.ts b/x-pack/plugins/logstash/server/types.ts new file mode 100644 index 0000000000000..2b266b2f27708 --- /dev/null +++ b/x-pack/plugins/logstash/server/types.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 { SearchResponse } from 'elasticsearch'; +import { IScopedClusterClient } from 'src/core/server'; + +type UnwrapArray<T> = T extends Array<infer U> ? U : never; + +export type Hits = SearchResponse<any>['hits']['hits']; +export type Hit = UnwrapArray<Hits>; + +export interface PipelineListItemOptions { + id: string; + description: string; + last_modified: string; + username: string; +} + +declare module 'src/core/server' { + interface RequestHandlerContext { + logstash?: { + esClient: IScopedClusterClient; + }; + } +} diff --git a/x-pack/plugins/maps/common/constants.ts b/x-pack/plugins/maps/common/constants.ts index f3997f741a1bf..a4006732224ce 100644 --- a/x-pack/plugins/maps/common/constants.ts +++ b/x-pack/plugins/maps/common/constants.ts @@ -1,9 +1,3 @@ -/* - * 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. - */ - /* * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one * or more contributor license agreements. Licensed under the Elastic License; @@ -46,9 +40,10 @@ export function createMapPath(id: string) { export enum LAYER_TYPE { TILE = 'TILE', VECTOR = 'VECTOR', - VECTOR_TILE = 'VECTOR_TILE', + VECTOR_TILE = 'VECTOR_TILE', // for static display of mvt vector tiles with a mapbox stylesheet. Does not support any ad-hoc configurations. Used for consuming EMS vector tiles. HEATMAP = 'HEATMAP', BLENDED_VECTOR = 'BLENDED_VECTOR', + TILED_VECTOR = 'TILED_VECTOR', // similar to a regular vector-layer, but it consumes the data as .mvt tilea iso GeoJson. It supports similar ad-hoc configurations like a regular vector layer (E.g. using IVectorStyle), although there is some loss of functionality e.g. does not support term joining } export enum SORT_ORDER { @@ -56,20 +51,25 @@ export enum SORT_ORDER { DESC = 'desc', } -export const EMS_TMS = 'EMS_TMS'; -export const EMS_FILE = 'EMS_FILE'; -export const ES_GEO_GRID = 'ES_GEO_GRID'; -export const ES_SEARCH = 'ES_SEARCH'; -export const ES_PEW_PEW = 'ES_PEW_PEW'; -export const EMS_XYZ = 'EMS_XYZ'; // identifies a custom TMS source. Name is a little unfortunate. -export const WMS = 'WMS'; -export const KIBANA_TILEMAP = 'KIBANA_TILEMAP'; -export const REGIONMAP_FILE = 'REGIONMAP_FILE'; +export enum SOURCE_TYPES { + EMS_TMS = 'EMS_TMS', + EMS_FILE = 'EMS_FILE', + ES_GEO_GRID = 'ES_GEO_GRID', + ES_SEARCH = 'ES_SEARCH', + ES_PEW_PEW = 'ES_PEW_PEW', + EMS_XYZ = 'EMS_XYZ', // identifies a custom TMS source. Name is a little unfortunate. + WMS = 'WMS', + KIBANA_TILEMAP = 'KIBANA_TILEMAP', + REGIONMAP_FILE = 'REGIONMAP_FILE', + GEOJSON_FILE = 'GEOJSON_FILE', + MVT_SINGLE_LAYER = 'MVT_SINGLE_LAYER', +} export enum FIELD_ORIGIN { SOURCE = 'source', JOIN = 'join', } +export const JOIN_FIELD_NAME_PREFIX = '__kbnjoin__'; export const SOURCE_DATA_ID_ORIGIN = 'source'; export const META_ID_ORIGIN_SUFFIX = 'meta'; @@ -77,8 +77,6 @@ export const SOURCE_META_ID_ORIGIN = `${SOURCE_DATA_ID_ORIGIN}_${META_ID_ORIGIN_ export const FORMATTERS_ID_ORIGIN_SUFFIX = 'formatters'; export const SOURCE_FORMATTERS_ID_ORIGIN = `${SOURCE_DATA_ID_ORIGIN}_${FORMATTERS_ID_ORIGIN_SUFFIX}`; -export const GEOJSON_FILE = 'GEOJSON_FILE'; - export const MIN_ZOOM = 0; export const MAX_ZOOM = 24; @@ -129,6 +127,7 @@ export enum DRAW_TYPE { POLYGON = 'POLYGON', } +export const AGG_DELIMITER = '_of_'; export enum AGG_TYPE { AVG = 'avg', COUNT = 'count', diff --git a/x-pack/plugins/maps/common/descriptor_types/data_request_descriptor_types.d.ts b/x-pack/plugins/maps/common/descriptor_types/data_request_descriptor_types.d.ts index ceba2fe56db12..e94dc6694b38d 100644 --- a/x-pack/plugins/maps/common/descriptor_types/data_request_descriptor_types.d.ts +++ b/x-pack/plugins/maps/common/descriptor_types/data_request_descriptor_types.d.ts @@ -31,12 +31,12 @@ type ESGeoGridSourceSyncMeta = { requestType: RENDER_AS; }; -export type VectorSourceSyncMeta = ESSearchSourceSyncMeta | ESGeoGridSourceSyncMeta; +export type VectorSourceSyncMeta = ESSearchSourceSyncMeta | ESGeoGridSourceSyncMeta | null; export type VectorSourceRequestMeta = MapFilters & { applyGlobalQuery: boolean; fieldNames: string[]; - geogridPrecision: number; + geogridPrecision?: number; sourceQuery: MapQuery; sourceMeta: VectorSourceSyncMeta; }; @@ -65,7 +65,7 @@ export type DataMeta = Partial<VectorSourceRequestMeta> & export type DataRequestDescriptor = { dataId: string; - dataMetaAtStart?: DataMeta; + dataMetaAtStart?: DataMeta | null; dataRequestToken?: symbol; data?: object; dataMeta?: DataMeta; diff --git a/x-pack/plugins/maps/common/descriptor_types/descriptor_types.d.ts b/x-pack/plugins/maps/common/descriptor_types/descriptor_types.d.ts index fb49e1aaebe1c..f8175b0ed3f10 100644 --- a/x-pack/plugins/maps/common/descriptor_types/descriptor_types.d.ts +++ b/x-pack/plugins/maps/common/descriptor_types/descriptor_types.d.ts @@ -9,6 +9,11 @@ import { AGG_TYPE, GRID_RESOLUTION, RENDER_AS, SORT_ORDER, SCALING_TYPES } from import { VectorStyleDescriptor } from './style_property_descriptor_types'; import { DataRequestDescriptor } from './data_request_descriptor_types'; +export type AttributionDescriptor = { + attributionText?: string; + attributionUrl?: string; +}; + export type AbstractSourceDescriptor = { id?: string; type: string; @@ -84,10 +89,21 @@ export type WMSSourceDescriptor = { attributionUrl: string; }; -export type XYZTMSSourceDescriptor = { - id: string; - type: string; +export type XYZTMSSourceDescriptor = AbstractSourceDescriptor & + AttributionDescriptor & { + urlTemplate: string; + }; + +export type TiledSingleLayerVectorSourceDescriptor = AbstractSourceDescriptor & { urlTemplate: string; + layerName: string; + + // These are the min/max zoom levels of the availability of the a particular layerName in the tileset at urlTemplate. + // These are _not_ the visible zoom-range of the data on a map. + // Tiled data can be displayed at higher levels of zoom than that they are stored in the tileset. + // e.g. EMS basemap data from level 14 is at most detailed resolution and can be displayed at higher levels + minSourceZoom: number; + maxSourceZoom: number; }; export type JoinDescriptor = { @@ -95,6 +111,18 @@ export type JoinDescriptor = { right: ESTermSourceDescriptor; }; +export type SourceDescriptor = + | XYZTMSSourceDescriptor + | WMSSourceDescriptor + | KibanaTilemapSourceDescriptor + | KibanaRegionmapSourceDescriptor + | ESTermSourceDescriptor + | ESSearchSourceDescriptor + | ESGeoGridSourceDescriptor + | EMSFileSourceDescriptor + | ESPewPewSourceDescriptor + | TiledSingleLayerVectorSourceDescriptor; + export type LayerDescriptor = { __dataRequests?: DataRequestDescriptor[]; __isInErrorState?: boolean; @@ -104,7 +132,7 @@ export type LayerDescriptor = { label?: string; minZoom?: number; maxZoom?: number; - sourceDescriptor: AbstractSourceDescriptor; + sourceDescriptor: SourceDescriptor; type?: string; visible?: boolean; }; diff --git a/x-pack/plugins/maps/common/get_join_key.ts b/x-pack/plugins/maps/common/get_join_key.ts new file mode 100644 index 0000000000000..f1ee95126b9a9 --- /dev/null +++ b/x-pack/plugins/maps/common/get_join_key.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 { AGG_DELIMITER, AGG_TYPE, JOIN_FIELD_NAME_PREFIX } from './constants'; + +// function in common since its needed by migration +export function getJoinAggKey({ + aggType, + aggFieldName, + rightSourceId, +}: { + aggType: AGG_TYPE; + aggFieldName?: string; + rightSourceId: string; +}) { + const metricKey = + aggType !== AGG_TYPE.COUNT ? `${aggType}${AGG_DELIMITER}${aggFieldName}` : aggType; + return `${JOIN_FIELD_NAME_PREFIX}${metricKey}__${rightSourceId}`; +} diff --git a/x-pack/plugins/maps/public/elasticsearch_geo_utils.js b/x-pack/plugins/maps/public/elasticsearch_geo_utils.js index 79467e26ec3fa..417c5d84f8916 100644 --- a/x-pack/plugins/maps/public/elasticsearch_geo_utils.js +++ b/x-pack/plugins/maps/public/elasticsearch_geo_utils.js @@ -64,7 +64,7 @@ function ensureGeometryType(type, expectedTypes) { * @param {string} geoFieldType Geometry field type ["geo_point", "geo_shape"] * @returns {number} */ -export function hitsToGeoJson(hits, flattenHit, geoFieldName, geoFieldType) { +export function hitsToGeoJson(hits, flattenHit, geoFieldName, geoFieldType, epochMillisFields) { const features = []; const tmpGeometriesAccumulator = []; @@ -80,6 +80,16 @@ export function hitsToGeoJson(hits, flattenHit, geoFieldName, geoFieldType) { geoShapeToGeometry(properties[geoFieldName], tmpGeometriesAccumulator); } + // There is a bug in Elasticsearch API where epoch_millis are returned as a string instead of a number + // https://github.com/elastic/elasticsearch/issues/50622 + // Convert these field values to integers. + for (let i = 0; i < epochMillisFields.length; i++) { + const fieldName = epochMillisFields[i]; + if (typeof properties[fieldName] === 'string') { + properties[fieldName] = parseInt(properties[fieldName]); + } + } + // don't include geometry field value in properties delete properties[geoFieldName]; @@ -231,28 +241,16 @@ function createGeoBoundBoxFilter(geometry, geoFieldName, filterProps = {}) { }; } -function createGeoPolygonFilter(polygonCoordinates, geoFieldName, filterProps = {}) { - return { - geo_polygon: { - ignore_unmapped: true, - [geoFieldName]: { - points: polygonCoordinates[POLYGON_COORDINATES_EXTERIOR_INDEX].map(coordinatePair => { - return { - lon: coordinatePair[LON_INDEX], - lat: coordinatePair[LAT_INDEX], - }; - }), - }, - }, - ...filterProps, - }; -} - export function createExtentFilter(mapExtent, geoFieldName, geoFieldType) { ensureGeoField(geoFieldType); const safePolygon = convertMapExtentToPolygon(mapExtent); + // Extent filters are used to dynamically filter data for the current map view port. + // Continue to use geo_bounding_box queries for extent filters + // 1) geo_bounding_box queries are faster than polygon queries + // 2) geo_shape benefits of pre-indexed shapes and + // compatability across multi-indices with geo_point and geo_shape do not apply to this use case. if (geoFieldType === ES_GEO_FIELD_TYPE.GEO_POINT) { return createGeoBoundBoxFilter(safePolygon, geoFieldName); } @@ -267,15 +265,7 @@ export function createExtentFilter(mapExtent, geoFieldName, geoFieldType) { }; } -export function createSpatialFilterWithBoundingBox(options) { - return createGeometryFilterWithMeta({ ...options, isBoundingBox: true }); -} - -export function createSpatialFilterWithGeometry(options) { - return createGeometryFilterWithMeta(options); -} - -function createGeometryFilterWithMeta({ +export function createSpatialFilterWithGeometry({ preIndexedShape, geometry, geometryLabel, @@ -283,16 +273,16 @@ function createGeometryFilterWithMeta({ geoFieldName, geoFieldType, relation = ES_SPATIAL_RELATIONS.INTERSECTS, - isBoundingBox = false, }) { ensureGeoField(geoFieldType); - const relationLabel = - geoFieldType === ES_GEO_FIELD_TYPE.GEO_POINT - ? i18n.translate('xpack.maps.es_geo_utils.shapeFilter.geoPointRelationLabel', { - defaultMessage: 'in', - }) - : getEsSpatialRelationLabel(relation); + const isGeoPoint = geoFieldType === ES_GEO_FIELD_TYPE.GEO_POINT; + + const relationLabel = isGeoPoint + ? i18n.translate('xpack.maps.es_geo_utils.shapeFilter.geoPointRelationLabel', { + defaultMessage: 'in', + }) + : getEsSpatialRelationLabel(relation); const meta = { type: SPATIAL_FILTER_TYPE, negate: false, @@ -301,47 +291,24 @@ function createGeometryFilterWithMeta({ alias: `${geoFieldName} ${relationLabel} ${geometryLabel}`, }; - if (geoFieldType === ES_GEO_FIELD_TYPE.GEO_SHAPE) { - const shapeQuery = { - relation, - }; - - if (preIndexedShape) { - shapeQuery.indexed_shape = preIndexedShape; - } else { - shapeQuery.shape = geometry; - } - - return { - meta, - geo_shape: { - ignore_unmapped: true, - [geoFieldName]: shapeQuery, - }, - }; - } - - // geo_points supports limited geometry types - ensureGeometryType(geometry.type, [GEO_JSON_TYPE.POLYGON, GEO_JSON_TYPE.MULTI_POLYGON]); - - if (geometry.type === GEO_JSON_TYPE.MULTI_POLYGON) { - return { - meta, - query: { - bool: { - should: geometry.coordinates.map(polygonCoordinates => { - return createGeoPolygonFilter(polygonCoordinates, geoFieldName); - }), - }, - }, - }; - } + const shapeQuery = { + // geo_shape query with geo_point field only supports intersects relation + relation: isGeoPoint ? ES_SPATIAL_RELATIONS.INTERSECTS : relation, + }; - if (isBoundingBox) { - return createGeoBoundBoxFilter(geometry, geoFieldName, { meta }); + if (preIndexedShape) { + shapeQuery.indexed_shape = preIndexedShape; + } else { + shapeQuery.shape = geometry; } - return createGeoPolygonFilter(geometry.coordinates, geoFieldName, { meta }); + return { + meta, + geo_shape: { + ignore_unmapped: true, + [geoFieldName]: shapeQuery, + }, + }; } export function createDistanceFilterWithMeta({ diff --git a/x-pack/plugins/maps/public/elasticsearch_geo_utils.test.js b/x-pack/plugins/maps/public/elasticsearch_geo_utils.test.js index 5db7556be4639..fc02e19173843 100644 --- a/x-pack/plugins/maps/public/elasticsearch_geo_utils.test.js +++ b/x-pack/plugins/maps/public/elasticsearch_geo_utils.test.js @@ -66,7 +66,7 @@ describe('hitsToGeoJson', () => { }, }, ]; - const geojson = hitsToGeoJson(hits, flattenHitMock, geoFieldName, 'geo_point'); + const geojson = hitsToGeoJson(hits, flattenHitMock, geoFieldName, 'geo_point', []); expect(geojson.type).toBe('FeatureCollection'); expect(geojson.features.length).toBe(2); expect(geojson.features[0]).toEqual({ @@ -94,7 +94,7 @@ describe('hitsToGeoJson', () => { _source: {}, }, ]; - const geojson = hitsToGeoJson(hits, flattenHitMock, geoFieldName, 'geo_point'); + const geojson = hitsToGeoJson(hits, flattenHitMock, geoFieldName, 'geo_point', []); expect(geojson.type).toBe('FeatureCollection'); expect(geojson.features.length).toBe(1); }); @@ -111,7 +111,7 @@ describe('hitsToGeoJson', () => { }, }, ]; - const geojson = hitsToGeoJson(hits, flattenHitMock, geoFieldName, 'geo_point'); + const geojson = hitsToGeoJson(hits, flattenHitMock, geoFieldName, 'geo_point', []); expect(geojson.features.length).toBe(1); const feature = geojson.features[0]; expect(feature.properties.myField).toBe(8); @@ -128,7 +128,7 @@ describe('hitsToGeoJson', () => { }, }, ]; - const geojson = hitsToGeoJson(hits, flattenHitMock, geoFieldName, 'geo_point'); + const geojson = hitsToGeoJson(hits, flattenHitMock, geoFieldName, 'geo_point', []); expect(geojson.type).toBe('FeatureCollection'); expect(geojson.features.length).toBe(2); expect(geojson.features[0]).toEqual({ @@ -159,6 +159,23 @@ describe('hitsToGeoJson', () => { }); }); + it('Should convert epoch_millis value from string to integer', () => { + const hits = [ + { + _id: 'doc1', + _index: 'index1', + _source: { + [geoFieldName]: '20,100', + myDateField: '1587156257081', + }, + }, + ]; + const geojson = hitsToGeoJson(hits, flattenHitMock, geoFieldName, 'geo_point', ['myDateField']); + expect(geojson.type).toBe('FeatureCollection'); + expect(geojson.features.length).toBe(1); + expect(geojson.features[0].properties.myDateField).toBe(1587156257081); + }); + describe('dot in geoFieldName', () => { const indexPatternMock = { fields: { @@ -184,7 +201,7 @@ describe('hitsToGeoJson', () => { }, }, ]; - const geojson = hitsToGeoJson(hits, indexPatternFlattenHit, 'my.location', 'geo_point'); + const geojson = hitsToGeoJson(hits, indexPatternFlattenHit, 'my.location', 'geo_point', []); expect(geojson.features[0].geometry).toEqual({ coordinates: [100, 20], type: 'Point', @@ -199,7 +216,7 @@ describe('hitsToGeoJson', () => { }, }, ]; - const geojson = hitsToGeoJson(hits, indexPatternFlattenHit, 'my.location', 'geo_point'); + const geojson = hitsToGeoJson(hits, indexPatternFlattenHit, 'my.location', 'geo_point', []); expect(geojson.features[0].geometry).toEqual({ coordinates: [100, 20], type: 'Point', diff --git a/x-pack/plugins/maps/public/kibana_services.d.ts b/x-pack/plugins/maps/public/kibana_services.d.ts new file mode 100644 index 0000000000000..867557c296292 --- /dev/null +++ b/x-pack/plugins/maps/public/kibana_services.d.ts @@ -0,0 +1,55 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { IIndexPattern } from 'src/plugins/data/public'; + +export function getLicenseId(): any; +export function getInspector(): any; +export function getFileUploadComponent(): any; +export function getIndexPatternSelectComponent(): any; +export function getHttp(): any; +export function getTimeFilter(): any; +export function getInjectedVarFunc(): any; +export function getToasts(): any; +export function getIndexPatternService(): { + get: (id: string) => IIndexPattern | undefined; +}; +export function getAutocompleteService(): any; +export function getSavedObjectsClient(): any; +export function getMapsCapabilities(): any; +export function getVisualizations(): any; +export function getDocLinks(): any; +export function getCoreChrome(): any; +export function getUiSettings(): any; +export function getCoreOverlays(): any; +export function getData(): any; +export function getUiActions(): any; +export function getCore(): any; +export function getNavigation(): any; +export function getCoreI18n(): any; + +export function setLicenseId(args: unknown): void; +export function setInspector(args: unknown): void; +export function setFileUpload(args: unknown): void; +export function setIndexPatternSelect(args: unknown): void; +export function setHttp(args: unknown): void; +export function setTimeFilter(args: unknown): void; +export function setInjectedVarFunc(args: unknown): void; +export function setToasts(args: unknown): void; +export function setIndexPatternService(args: unknown): void; +export function setAutocompleteService(args: unknown): void; +export function setSavedObjectsClient(args: unknown): void; +export function setMapsCapabilities(args: unknown): void; +export function setVisualizations(args: unknown): void; +export function setDocLinks(args: unknown): void; +export function setCoreChrome(args: unknown): void; +export function setUiSettings(args: unknown): void; +export function setCoreOverlays(args: unknown): void; +export function setData(args: unknown): void; +export function setUiActions(args: unknown): void; +export function setCore(args: unknown): void; +export function setNavigation(args: unknown): void; +export function setCoreI18n(args: unknown): void; diff --git a/x-pack/plugins/maps/public/kibana_services.js b/x-pack/plugins/maps/public/kibana_services.js index d2ddecfdf915b..dcbd54a09381f 100644 --- a/x-pack/plugins/maps/public/kibana_services.js +++ b/x-pack/plugins/maps/public/kibana_services.js @@ -38,7 +38,9 @@ export const getFileUploadComponent = () => { }; let getInjectedVar; -export const setInjectedVarFunc = getInjectedVarFunc => (getInjectedVar = getInjectedVarFunc); +export const setInjectedVarFunc = getInjectedVarFunc => { + getInjectedVar = getInjectedVarFunc; +}; export const getInjectedVarFunc = () => getInjectedVar; let uiSettings; @@ -89,3 +91,49 @@ export async function fetchSearchSourceAndRecordWithInspector({ return resp; } + +let savedObjectsClient; +export const setSavedObjectsClient = coreSavedObjectsClient => + (savedObjectsClient = coreSavedObjectsClient); +export const getSavedObjectsClient = () => savedObjectsClient; + +let chrome; +export const setCoreChrome = coreChrome => (chrome = coreChrome); +export const getCoreChrome = () => chrome; + +let mapsCapabilities; +export const setMapsCapabilities = coreAppMapsCapabilities => + (mapsCapabilities = coreAppMapsCapabilities); +export const getMapsCapabilities = () => mapsCapabilities; + +let visualizations; +export const setVisualizations = visPlugin => (visualizations = visPlugin); +export const getVisualizations = () => visualizations; + +let docLinks; +export const setDocLinks = coreDocLinks => (docLinks = coreDocLinks); +export const getDocLinks = () => docLinks; + +let overlays; +export const setCoreOverlays = coreOverlays => (overlays = coreOverlays); +export const getCoreOverlays = () => overlays; + +let data; +export const setData = dataPlugin => (data = dataPlugin); +export const getData = () => data; + +let uiActions; +export const setUiActions = pluginUiActions => (uiActions = pluginUiActions); +export const getUiActions = () => uiActions; + +let core; +export const setCore = kibanaCore => (core = kibanaCore); +export const getCore = () => core; + +let navigation; +export const setNavigation = pluginNavigation => (navigation = pluginNavigation); +export const getNavigation = () => navigation; + +let coreI18n; +export const setCoreI18n = kibanaCoreI18n => (coreI18n = kibanaCoreI18n); +export const getCoreI18n = () => coreI18n; diff --git a/x-pack/plugins/maps/public/layers/blended_vector_layer.ts b/x-pack/plugins/maps/public/layers/blended_vector_layer.ts index f5526ad703dd2..9a9ea2968ceeb 100644 --- a/x-pack/plugins/maps/public/layers/blended_vector_layer.ts +++ b/x-pack/plugins/maps/public/layers/blended_vector_layer.ts @@ -11,9 +11,9 @@ import { getDefaultDynamicProperties } from './styles/vector/vector_style_defaul import { IDynamicStyleProperty } from './styles/vector/properties/dynamic_style_property'; import { IStyleProperty } from './styles/vector/properties/style_property'; import { + SOURCE_TYPES, COUNT_PROP_LABEL, COUNT_PROP_NAME, - ES_GEO_GRID, LAYER_TYPE, AGG_TYPE, RENDER_AS, @@ -34,6 +34,7 @@ import { VectorStyleDescriptor, SizeDynamicOptions, DynamicStylePropertyOptions, + VectorLayerDescriptor, } from '../../common/descriptor_types'; const ACTIVE_COUNT_DATA_ID = 'ACTIVE_COUNT_DATA_ID'; @@ -147,7 +148,10 @@ function getClusterStyleDescriptor( export class BlendedVectorLayer extends VectorLayer implements IVectorLayer { static type = LAYER_TYPE.BLENDED_VECTOR; - static createDescriptor(options: VectorLayerArguments, mapColors: string[]) { + static createDescriptor( + options: VectorLayerDescriptor, + mapColors: string[] + ): VectorLayerDescriptor { const layerDescriptor = VectorLayer.createDescriptor(options, mapColors); layerDescriptor.type = BlendedVectorLayer.type; return layerDescriptor; @@ -176,7 +180,11 @@ export class BlendedVectorLayer extends VectorLayer implements IVectorLayer { const sourceDataRequest = this.getSourceDataRequest(); if (sourceDataRequest) { const requestMeta = sourceDataRequest.getMeta(); - if (requestMeta && requestMeta.sourceType && requestMeta.sourceType === ES_GEO_GRID) { + if ( + requestMeta && + requestMeta.sourceType && + requestMeta.sourceType === SOURCE_TYPES.ES_GEO_GRID + ) { isClustered = true; } } diff --git a/x-pack/plugins/maps/public/layers/joins/inner_join.test.js b/x-pack/plugins/maps/public/layers/joins/inner_join.test.js index 65c37860ffa18..f197a67becfae 100644 --- a/x-pack/plugins/maps/public/layers/joins/inner_join.test.js +++ b/x-pack/plugins/maps/public/layers/joins/inner_join.test.js @@ -35,7 +35,7 @@ const leftJoin = new InnerJoin( }, mockSource ); -const COUNT_PROPERTY_NAME = '__kbnjoin__count_groupby_kibana_sample_data_logs.geo.dest'; +const COUNT_PROPERTY_NAME = '__kbnjoin__count__d3625663-5b34-4d50-a784-0d743f676a0c'; describe('joinPropertiesToFeature', () => { it('Should add join property to features in feature collection', () => { diff --git a/x-pack/plugins/maps/public/layers/layer.d.ts b/x-pack/plugins/maps/public/layers/layer.d.ts index de59642ede8ab..e8fc5d473626c 100644 --- a/x-pack/plugins/maps/public/layers/layer.d.ts +++ b/x-pack/plugins/maps/public/layers/layer.d.ts @@ -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 { LayerDescriptor, MapExtent, MapFilters } from '../../common/descriptor_types'; +import { LayerDescriptor, MapExtent, MapFilters, MapQuery } from '../../common/descriptor_types'; import { ISource } from './sources/source'; import { DataRequest } from './util/data_request'; import { SyncContext } from '../actions/map_actions'; @@ -17,6 +17,11 @@ export interface ILayer { getSource(): ISource; getSourceForEditing(): ISource; syncData(syncContext: SyncContext): Promise<void>; + isVisible(): boolean; + showAtZoomLevel(zoomLevel: number): boolean; + getMinZoom(): number; + getMaxZoom(): number; + getMinSourceZoom(): number; } export interface ILayerArguments { @@ -25,6 +30,7 @@ export interface ILayerArguments { } export class AbstractLayer implements ILayer { + static createDescriptor(options: Partial<LayerDescriptor>, mapColors?: string[]): LayerDescriptor; constructor(layerArguments: ILayerArguments); getBounds(mapFilters: MapFilters): Promise<MapExtent>; getDataRequest(id: string): DataRequest | undefined; @@ -34,4 +40,12 @@ export class AbstractLayer implements ILayer { getSource(): ISource; getSourceForEditing(): ISource; syncData(syncContext: SyncContext): Promise<void>; + isVisible(): boolean; + showAtZoomLevel(zoomLevel: number): boolean; + getMinZoom(): number; + getMaxZoom(): number; + getMinSourceZoom(): number; + getQuery(): MapQuery; + _removeStaleMbSourcesAndLayers(mbMap: unknown): void; + _requiresPrevSourceCleanup(mbMap: unknown): boolean; } diff --git a/x-pack/plugins/maps/public/layers/layer.js b/x-pack/plugins/maps/public/layers/layer.js index 26bce872b3c2c..19dcbaf1dfcfd 100644 --- a/x-pack/plugins/maps/public/layers/layer.js +++ b/x-pack/plugins/maps/public/layers/layer.js @@ -141,7 +141,8 @@ export class AbstractLayer { defaultMessage: `Layer is hidden.`, }); } else if (!this.showAtZoomLevel(zoomLevel)) { - const { minZoom, maxZoom } = this.getZoomConfig(); + const minZoom = this.getMinZoom(); + const maxZoom = this.getMaxZoom(); icon = <EuiIcon size="m" type="expand" />; tooltipContent = i18n.translate('xpack.maps.layer.zoomFeedbackTooltip', { defaultMessage: `Layer is visible between zoom levels {minZoom} and {maxZoom}.`, @@ -203,7 +204,7 @@ export class AbstractLayer { } showAtZoomLevel(zoom) { - return zoom >= this._descriptor.minZoom && zoom <= this._descriptor.maxZoom; + return zoom >= this.getMinZoom() && zoom <= this.getMaxZoom(); } getMinZoom() { @@ -214,6 +215,30 @@ export class AbstractLayer { return this._descriptor.maxZoom; } + getMinSourceZoom() { + return this._source.getMinZoom(); + } + + _requiresPrevSourceCleanup() { + return false; + } + + _removeStaleMbSourcesAndLayers(mbMap) { + if (this._requiresPrevSourceCleanup(mbMap)) { + const mbStyle = mbMap.getStyle(); + mbStyle.layers.forEach(mbLayer => { + if (this.ownsMbLayerId(mbLayer.id)) { + mbMap.removeLayer(mbLayer.id); + } + }); + Object.keys(mbStyle.sources).some(mbSourceId => { + if (this.ownsMbSourceId(mbSourceId)) { + mbMap.removeSource(mbSourceId); + } + }); + } + } + getAlpha() { return this._descriptor.alpha; } @@ -222,13 +247,6 @@ export class AbstractLayer { return this._descriptor.query; } - getZoomConfig() { - return { - minZoom: this._descriptor.minZoom, - maxZoom: this._descriptor.maxZoom, - }; - } - getCurrentStyle() { return this._style; } diff --git a/x-pack/plugins/maps/public/layers/layer_wizard_registry.ts b/x-pack/plugins/maps/public/layers/layer_wizard_registry.ts index 3ef4701269994..cb87aeaa9da3f 100644 --- a/x-pack/plugins/maps/public/layers/layer_wizard_registry.ts +++ b/x-pack/plugins/maps/public/layers/layer_wizard_registry.ts @@ -5,17 +5,21 @@ */ /* eslint-disable @typescript-eslint/consistent-type-definitions */ -type LayerWizard = { +import { ReactElement } from 'react'; +import { ISource } from './sources/source'; + +export type PreviewSourceHandler = (source: ISource | null) => void; + +export type RenderWizardArguments = { + onPreviewSource: PreviewSourceHandler; + inspectorAdapters: object; +}; + +export type LayerWizard = { description: string; icon: string; isIndexingSource?: boolean; - renderWizard({ - onPreviewSource, - inspectorAdapters, - }: { - onPreviewSource: () => void; - inspectorAdapters: unknown; - }): unknown; + renderWizard(renderWizardArguments: RenderWizardArguments): ReactElement<any>; title: string; }; diff --git a/x-pack/plugins/maps/public/layers/load_layer_wizards.js b/x-pack/plugins/maps/public/layers/load_layer_wizards.js deleted file mode 100644 index d0169165eaa35..0000000000000 --- a/x-pack/plugins/maps/public/layers/load_layer_wizards.js +++ /dev/null @@ -1,30 +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 { registerLayerWizard } from './layer_wizard_registry'; -import { uploadLayerWizardConfig } from './sources/client_file_source'; -import { esDocumentsLayerWizardConfig } from './sources/es_search_source'; -import { clustersLayerWizardConfig, heatmapLayerWizardConfig } from './sources/es_geo_grid_source'; -import { point2PointLayerWizardConfig } from './sources/es_pew_pew_source/es_pew_pew_source'; -import { emsBoundariesLayerWizardConfig } from './sources/ems_file_source'; -import { emsBaseMapLayerWizardConfig } from './sources/ems_tms_source'; -import { kibanaRegionMapLayerWizardConfig } from './sources/kibana_regionmap_source'; -import { kibanaBasemapLayerWizardConfig } from './sources/kibana_tilemap_source'; -import { tmsLayerWizardConfig } from './sources/xyz_tms_source'; -import { wmsLayerWizardConfig } from './sources/wms_source'; - -// Registration order determines display order -registerLayerWizard(uploadLayerWizardConfig); -registerLayerWizard(esDocumentsLayerWizardConfig); -registerLayerWizard(clustersLayerWizardConfig); -registerLayerWizard(heatmapLayerWizardConfig); -registerLayerWizard(point2PointLayerWizardConfig); -registerLayerWizard(emsBoundariesLayerWizardConfig); -registerLayerWizard(emsBaseMapLayerWizardConfig); -registerLayerWizard(kibanaRegionMapLayerWizardConfig); -registerLayerWizard(kibanaBasemapLayerWizardConfig); -registerLayerWizard(tmsLayerWizardConfig); -registerLayerWizard(wmsLayerWizardConfig); diff --git a/x-pack/plugins/maps/public/layers/load_layer_wizards.ts b/x-pack/plugins/maps/public/layers/load_layer_wizards.ts new file mode 100644 index 0000000000000..49d128257fe20 --- /dev/null +++ b/x-pack/plugins/maps/public/layers/load_layer_wizards.ts @@ -0,0 +1,66 @@ +/* + * 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 { registerLayerWizard } from './layer_wizard_registry'; +// @ts-ignore +import { uploadLayerWizardConfig } from './sources/client_file_source'; +// @ts-ignore +import { esDocumentsLayerWizardConfig } from './sources/es_search_source'; +// @ts-ignore +import { clustersLayerWizardConfig, heatmapLayerWizardConfig } from './sources/es_geo_grid_source'; +// @ts-ignore +import { point2PointLayerWizardConfig } from './sources/es_pew_pew_source/es_pew_pew_source'; +// @ts-ignore +import { emsBoundariesLayerWizardConfig } from './sources/ems_file_source'; +// @ts-ignore +import { emsBaseMapLayerWizardConfig } from './sources/ems_tms_source'; +// @ts-ignore +import { kibanaRegionMapLayerWizardConfig } from './sources/kibana_regionmap_source'; +// @ts-ignore +import { kibanaBasemapLayerWizardConfig } from './sources/kibana_tilemap_source'; +import { tmsLayerWizardConfig } from './sources/xyz_tms_source'; +// @ts-ignore +import { wmsLayerWizardConfig } from './sources/wms_source'; +import { mvtVectorSourceWizardConfig } from './sources/mvt_single_layer_vector_source'; +// @ts-ignore +import { getInjectedVarFunc } from '../kibana_services'; + +// Registration order determines display order +let registered = false; +export function registerLayerWizards() { + if (registered) { + return; + } + // @ts-ignore + registerLayerWizard(uploadLayerWizardConfig); + // @ts-ignore + registerLayerWizard(esDocumentsLayerWizardConfig); + // @ts-ignore + registerLayerWizard(clustersLayerWizardConfig); + // @ts-ignore + registerLayerWizard(heatmapLayerWizardConfig); + // @ts-ignore + registerLayerWizard(point2PointLayerWizardConfig); + // @ts-ignore + registerLayerWizard(emsBoundariesLayerWizardConfig); + // @ts-ignore + registerLayerWizard(emsBaseMapLayerWizardConfig); + // @ts-ignore + registerLayerWizard(kibanaRegionMapLayerWizardConfig); + // @ts-ignore + registerLayerWizard(kibanaBasemapLayerWizardConfig); + registerLayerWizard(tmsLayerWizardConfig); + // @ts-ignore + registerLayerWizard(wmsLayerWizardConfig); + + const getInjectedVar = getInjectedVarFunc(); + if (getInjectedVar && getInjectedVar('enableVectorTiles', false)) { + // eslint-disable-next-line no-console + console.warn('Vector tiles are an experimental feature and should not be used in production.'); + registerLayerWizard(mvtVectorSourceWizardConfig); + } + registered = true; +} diff --git a/x-pack/plugins/maps/public/layers/sources/client_file_source/geojson_file_source.js b/x-pack/plugins/maps/public/layers/sources/client_file_source/geojson_file_source.js index 1003f8329da22..137513ad7c612 100644 --- a/x-pack/plugins/maps/public/layers/sources/client_file_source/geojson_file_source.js +++ b/x-pack/plugins/maps/public/layers/sources/client_file_source/geojson_file_source.js @@ -8,18 +8,18 @@ import { AbstractVectorSource } from '../vector_source'; import React from 'react'; import { ES_GEO_FIELD_TYPE, - GEOJSON_FILE, + SOURCE_TYPES, DEFAULT_MAX_RESULT_WINDOW, + SCALING_TYPES, } from '../../../../common/constants'; import { ClientFileCreateSourceEditor } from './create_client_file_source_editor'; import { ESSearchSource } from '../es_search_source'; import uuid from 'uuid/v4'; -import _ from 'lodash'; import { i18n } from '@kbn/i18n'; import { registerSource } from '../source_registry'; export class GeojsonFileSource extends AbstractVectorSource { - static type = GEOJSON_FILE; + static type = SOURCE_TYPES.GEOJSON_FILE; static isIndexingSource = true; @@ -91,23 +91,22 @@ const viewIndexedData = ( importErrorHandler(indexResponses); return; } - const { fields, id } = indexPatternResp; - const geoFieldArr = fields.filter(field => - Object.values(ES_GEO_FIELD_TYPE).includes(field.type) - ); - const geoField = _.get(geoFieldArr, '[0].name'); - const indexPatternId = id; + const { fields, id: indexPatternId } = indexPatternResp; + const geoField = fields.find(field => Object.values(ES_GEO_FIELD_TYPE).includes(field.type)); if (!indexPatternId || !geoField) { addAndViewSource(null); } else { - // Only turn on bounds filter for large doc counts - const filterByMapBounds = indexDataResp.docCount > DEFAULT_MAX_RESULT_WINDOW; const source = new ESSearchSource( { id: uuid(), indexPatternId, - geoField, - filterByMapBounds, + geoField: geoField.name, + // Only turn on bounds filter for large doc counts + filterByMapBounds: indexDataResp.docCount > DEFAULT_MAX_RESULT_WINDOW, + scalingType: + geoField.type === ES_GEO_FIELD_TYPE.GEO_POINT + ? SCALING_TYPES.CLUSTERS + : SCALING_TYPES.LIMIT, }, inspectorAdapters ); @@ -131,7 +130,7 @@ const previewGeojsonFile = (onPreviewSource, inspectorAdapters) => { registerSource({ ConstructorFunction: GeojsonFileSource, - type: GEOJSON_FILE, + type: SOURCE_TYPES.GEOJSON_FILE, }); export const uploadLayerWizardConfig = { diff --git a/x-pack/plugins/maps/public/layers/sources/ems_file_source/ems_file_source.js b/x-pack/plugins/maps/public/layers/sources/ems_file_source/ems_file_source.js index d3ccc0cb55821..e8af17b911939 100644 --- a/x-pack/plugins/maps/public/layers/sources/ems_file_source/ems_file_source.js +++ b/x-pack/plugins/maps/public/layers/sources/ems_file_source/ems_file_source.js @@ -7,7 +7,7 @@ import { AbstractVectorSource } from '../vector_source'; import { VECTOR_SHAPE_TYPES } from '../vector_feature_types'; import React from 'react'; -import { EMS_FILE, FIELD_ORIGIN } from '../../../../common/constants'; +import { SOURCE_TYPES, FIELD_ORIGIN } from '../../../../common/constants'; import { getEMSClient } from '../../../meta'; import { EMSFileCreateSourceEditor } from './create_source_editor'; import { i18n } from '@kbn/i18n'; @@ -21,7 +21,7 @@ const sourceTitle = i18n.translate('xpack.maps.source.emsFileTitle', { }); export class EMSFileSource extends AbstractVectorSource { - static type = EMS_FILE; + static type = SOURCE_TYPES.EMS_FILE; static createDescriptor({ id, tooltipProperties = [] }) { return { @@ -159,7 +159,7 @@ export class EMSFileSource extends AbstractVectorSource { registerSource({ ConstructorFunction: EMSFileSource, - type: EMS_FILE, + type: SOURCE_TYPES.EMS_FILE, }); export const emsBoundariesLayerWizardConfig = { diff --git a/x-pack/plugins/maps/public/layers/sources/ems_tms_source/ems_tms_source.js b/x-pack/plugins/maps/public/layers/sources/ems_tms_source/ems_tms_source.js index 1da3680dfdc86..79121c4cdb31f 100644 --- a/x-pack/plugins/maps/public/layers/sources/ems_tms_source/ems_tms_source.js +++ b/x-pack/plugins/maps/public/layers/sources/ems_tms_source/ems_tms_source.js @@ -14,7 +14,7 @@ import { TileServiceSelect } from './tile_service_select'; import { UpdateSourceEditor } from './update_source_editor'; import { i18n } from '@kbn/i18n'; import { getDataSourceLabel } from '../../../../common/i18n_getters'; -import { EMS_TMS } from '../../../../common/constants'; +import { SOURCE_TYPES } from '../../../../common/constants'; import { getInjectedVarFunc, getUiSettings } from '../../../kibana_services'; import { registerSource } from '../source_registry'; @@ -23,7 +23,7 @@ const sourceTitle = i18n.translate('xpack.maps.source.emsTileTitle', { }); export class EMSTMSSource extends AbstractTMSSource { - static type = EMS_TMS; + static type = SOURCE_TYPES.EMS_TMS; static createDescriptor(sourceConfig) { return { @@ -148,7 +148,7 @@ export class EMSTMSSource extends AbstractTMSSource { registerSource({ ConstructorFunction: EMSTMSSource, - type: EMS_TMS, + type: SOURCE_TYPES.EMS_TMS, }); export const emsBaseMapLayerWizardConfig = { diff --git a/x-pack/plugins/maps/public/layers/sources/es_agg_source.d.ts b/x-pack/plugins/maps/public/layers/sources/es_agg_source.d.ts deleted file mode 100644 index 99ee1ec652b54..0000000000000 --- a/x-pack/plugins/maps/public/layers/sources/es_agg_source.d.ts +++ /dev/null @@ -1,25 +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 { IESSource } from './es_source'; -import { AbstractESSource } from './es_source'; -import { AGG_TYPE } from '../../../common/constants'; -import { IESAggField } from '../fields/es_agg_field'; -import { AbstractESAggSourceDescriptor } from '../../../common/descriptor_types'; - -export interface IESAggSource extends IESSource { - getAggKey(aggType: AGG_TYPE, fieldName: string): string; - getAggLabel(aggType: AGG_TYPE, fieldName: string): string; - getMetricFields(): IESAggField[]; -} - -export class AbstractESAggSource extends AbstractESSource implements IESAggSource { - constructor(sourceDescriptor: AbstractESAggSourceDescriptor, inspectorAdapters: object); - - getAggKey(aggType: AGG_TYPE, fieldName: string): string; - getAggLabel(aggType: AGG_TYPE, fieldName: string): string; - getMetricFields(): IESAggField[]; -} diff --git a/x-pack/plugins/maps/public/layers/sources/es_agg_source/es_agg_source.d.ts b/x-pack/plugins/maps/public/layers/sources/es_agg_source/es_agg_source.d.ts new file mode 100644 index 0000000000000..a93f9121d1e62 --- /dev/null +++ b/x-pack/plugins/maps/public/layers/sources/es_agg_source/es_agg_source.d.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 { IESSource } from '../es_source'; +import { AbstractESSource } from '../es_source'; +import { AGG_TYPE } from '../../../../common/constants'; +import { IESAggField } from '../../fields/es_agg_field'; +import { AbstractESAggSourceDescriptor } from '../../../../common/descriptor_types'; + +export interface IESAggSource extends IESSource { + getAggKey(aggType: AGG_TYPE, fieldName: string): string; + getAggLabel(aggType: AGG_TYPE, fieldName: string): string; + getMetricFields(): IESAggField[]; +} + +export class AbstractESAggSource extends AbstractESSource implements IESAggSource { + constructor(sourceDescriptor: AbstractESAggSourceDescriptor, inspectorAdapters: object); + + getAggKey(aggType: AGG_TYPE, fieldName: string): string; + getAggLabel(aggType: AGG_TYPE, fieldName: string): string; + getMetricFields(): IESAggField[]; +} diff --git a/x-pack/plugins/maps/public/layers/sources/es_agg_source.js b/x-pack/plugins/maps/public/layers/sources/es_agg_source/es_agg_source.js similarity index 94% rename from x-pack/plugins/maps/public/layers/sources/es_agg_source.js rename to x-pack/plugins/maps/public/layers/sources/es_agg_source/es_agg_source.js index 9f4b89cadc777..58c56fe32f766 100644 --- a/x-pack/plugins/maps/public/layers/sources/es_agg_source.js +++ b/x-pack/plugins/maps/public/layers/sources/es_agg_source/es_agg_source.js @@ -5,17 +5,15 @@ */ import { i18n } from '@kbn/i18n'; -import { AbstractESSource } from './es_source'; -import { esAggFieldsFactory } from '../fields/es_agg_field'; - +import { AbstractESSource } from '../es_source'; +import { esAggFieldsFactory } from '../../fields/es_agg_field'; import { + AGG_DELIMITER, AGG_TYPE, COUNT_PROP_LABEL, COUNT_PROP_NAME, FIELD_ORIGIN, -} from '../../../common/constants'; - -export const AGG_DELIMITER = '_of_'; +} from '../../../../common/constants'; export class AbstractESAggSource extends AbstractESSource { constructor(descriptor, inspectorAdapters) { diff --git a/x-pack/plugins/maps/public/layers/sources/es_agg_source.test.ts b/x-pack/plugins/maps/public/layers/sources/es_agg_source/es_agg_source.test.ts similarity index 89% rename from x-pack/plugins/maps/public/layers/sources/es_agg_source.test.ts rename to x-pack/plugins/maps/public/layers/sources/es_agg_source/es_agg_source.test.ts index 848091586eb9c..87abbedfdf50e 100644 --- a/x-pack/plugins/maps/public/layers/sources/es_agg_source.test.ts +++ b/x-pack/plugins/maps/public/layers/sources/es_agg_source/es_agg_source.test.ts @@ -4,12 +4,12 @@ * you may not use this file except in compliance with the Elastic License. */ -import { AbstractESAggSource } from './es_agg_source'; -import { IField } from '../fields/field'; -import { IESAggField } from '../fields/es_agg_field'; +import { AbstractESAggSource } from '../es_agg_source'; +import { IField } from '../../fields/field'; +import { IESAggField } from '../../fields/es_agg_field'; import _ from 'lodash'; -import { AGG_TYPE } from '../../../common/constants'; -import { AggDescriptor } from '../../../common/descriptor_types'; +import { AGG_TYPE } from '../../../../common/constants'; +import { AggDescriptor } from '../../../../common/descriptor_types'; jest.mock('ui/new_platform'); diff --git a/x-pack/plugins/maps/public/layers/sources/es_agg_source/index.ts b/x-pack/plugins/maps/public/layers/sources/es_agg_source/index.ts new file mode 100644 index 0000000000000..cbf4eceefd432 --- /dev/null +++ b/x-pack/plugins/maps/public/layers/sources/es_agg_source/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 * from './es_agg_source'; diff --git a/x-pack/plugins/maps/public/layers/sources/es_geo_grid_source/es_geo_grid_source.d.ts b/x-pack/plugins/maps/public/layers/sources/es_geo_grid_source/es_geo_grid_source.d.ts index 3f596cea1ae39..96347c444dd5b 100644 --- a/x-pack/plugins/maps/public/layers/sources/es_geo_grid_source/es_geo_grid_source.d.ts +++ b/x-pack/plugins/maps/public/layers/sources/es_geo_grid_source/es_geo_grid_source.d.ts @@ -23,6 +23,7 @@ export class ESGeoGridSource extends AbstractESAggSource { constructor(sourceDescriptor: ESGeoGridSourceDescriptor, inspectorAdapters: unknown); + getFieldNames(): string[]; getGridResolution(): GRID_RESOLUTION; getGeoGridPrecision(zoom: number): number; } diff --git a/x-pack/plugins/maps/public/layers/sources/es_geo_grid_source/es_geo_grid_source.js b/x-pack/plugins/maps/public/layers/sources/es_geo_grid_source/es_geo_grid_source.js index 04f944396ab35..b9ef13e520bf8 100644 --- a/x-pack/plugins/maps/public/layers/sources/es_geo_grid_source/es_geo_grid_source.js +++ b/x-pack/plugins/maps/public/layers/sources/es_geo_grid_source/es_geo_grid_source.js @@ -17,8 +17,8 @@ import { COLOR_GRADIENTS } from '../../styles/color_utils'; import { CreateSourceEditor } from './create_source_editor'; import { UpdateSourceEditor } from './update_source_editor'; import { + SOURCE_TYPES, DEFAULT_MAX_BUCKETS_LIMIT, - ES_GEO_GRID, COUNT_PROP_NAME, COLOR_MAP_TYPE, RENDER_AS, @@ -45,7 +45,7 @@ const heatmapTitle = i18n.translate('xpack.maps.source.esGridHeatmapTitle', { }); export class ESGeoGridSource extends AbstractESAggSource { - static type = ES_GEO_GRID; + static type = SOURCE_TYPES.ES_GEO_GRID; static createDescriptor({ indexPatternId, geoField, requestType, resolution }) { return { @@ -311,7 +311,7 @@ export class ESGeoGridSource extends AbstractESAggSource { }, meta: { areResultsTrimmed: false, - sourceType: ES_GEO_GRID, + sourceType: SOURCE_TYPES.ES_GEO_GRID, }, }; } @@ -420,7 +420,7 @@ export class ESGeoGridSource extends AbstractESAggSource { registerSource({ ConstructorFunction: ESGeoGridSource, - type: ES_GEO_GRID, + type: SOURCE_TYPES.ES_GEO_GRID, }); export const clustersLayerWizardConfig = { diff --git a/x-pack/plugins/maps/public/layers/sources/es_geo_grid_source/es_geo_grid_source.test.ts b/x-pack/plugins/maps/public/layers/sources/es_geo_grid_source/es_geo_grid_source.test.ts index 727435c3cbfef..e35bb998ce7db 100644 --- a/x-pack/plugins/maps/public/layers/sources/es_geo_grid_source/es_geo_grid_source.test.ts +++ b/x-pack/plugins/maps/public/layers/sources/es_geo_grid_source/es_geo_grid_source.test.ts @@ -7,7 +7,7 @@ jest.mock('../../../kibana_services', () => {}); jest.mock('ui/new_platform'); import { ESGeoGridSource } from './es_geo_grid_source'; -import { ES_GEO_GRID, GRID_RESOLUTION, RENDER_AS } from '../../../../common/constants'; +import { GRID_RESOLUTION, RENDER_AS, SOURCE_TYPES } from '../../../../common/constants'; describe('ESGeoGridSource', () => { const geogridSource = new ESGeoGridSource( @@ -17,7 +17,7 @@ describe('ESGeoGridSource', () => { geoField: 'bar', metrics: [], resolution: GRID_RESOLUTION.COARSE, - type: ES_GEO_GRID, + type: SOURCE_TYPES.ES_GEO_GRID, requestType: RENDER_AS.HEATMAP, }, {} diff --git a/x-pack/plugins/maps/public/layers/sources/es_pew_pew_source/es_pew_pew_source.js b/x-pack/plugins/maps/public/layers/sources/es_pew_pew_source/es_pew_pew_source.js index ea3a2d2fe634d..57e5afb99404b 100644 --- a/x-pack/plugins/maps/public/layers/sources/es_pew_pew_source/es_pew_pew_source.js +++ b/x-pack/plugins/maps/public/layers/sources/es_pew_pew_source/es_pew_pew_source.js @@ -16,7 +16,7 @@ import { getDefaultDynamicProperties } from '../../styles/vector/vector_style_de import { i18n } from '@kbn/i18n'; import { FIELD_ORIGIN, - ES_PEW_PEW, + SOURCE_TYPES, COUNT_PROP_NAME, VECTOR_STYLES, } from '../../../../common/constants'; @@ -35,7 +35,7 @@ const sourceTitle = i18n.translate('xpack.maps.source.pewPewTitle', { }); export class ESPewPewSource extends AbstractESAggSource { - static type = ES_PEW_PEW; + static type = SOURCE_TYPES.ES_PEW_PEW; static createDescriptor({ indexPatternId, sourceGeoField, destGeoField }) { return { @@ -232,7 +232,7 @@ export class ESPewPewSource extends AbstractESAggSource { registerSource({ ConstructorFunction: ESPewPewSource, - type: ES_PEW_PEW, + type: SOURCE_TYPES.ES_PEW_PEW, }); export const point2PointLayerWizardConfig = { diff --git a/x-pack/plugins/maps/public/layers/sources/es_search_source/es_search_source.d.ts b/x-pack/plugins/maps/public/layers/sources/es_search_source/es_search_source.d.ts index 0a4e48a195ec6..c904280a38c85 100644 --- a/x-pack/plugins/maps/public/layers/sources/es_search_source/es_search_source.d.ts +++ b/x-pack/plugins/maps/public/layers/sources/es_search_source/es_search_source.d.ts @@ -9,4 +9,5 @@ import { ESSearchSourceDescriptor } from '../../../../common/descriptor_types'; export class ESSearchSource extends AbstractESSource { constructor(sourceDescriptor: Partial<ESSearchSourceDescriptor>, inspectorAdapters: unknown); + getFieldNames(): string[]; } diff --git a/x-pack/plugins/maps/public/layers/sources/es_search_source/es_search_source.js b/x-pack/plugins/maps/public/layers/sources/es_search_source/es_search_source.js index ce9932bd15cea..34fed37933e13 100644 --- a/x-pack/plugins/maps/public/layers/sources/es_search_source/es_search_source.js +++ b/x-pack/plugins/maps/public/layers/sources/es_search_source/es_search_source.js @@ -17,7 +17,7 @@ import { hitsToGeoJson } from '../../../elasticsearch_geo_utils'; import { CreateSourceEditor } from './create_source_editor'; import { UpdateSourceEditor } from './update_source_editor'; import { - ES_SEARCH, + SOURCE_TYPES, ES_GEO_FIELD_TYPE, DEFAULT_MAX_BUCKETS_LIMIT, SORT_ORDER, @@ -69,7 +69,7 @@ function getDocValueAndSourceFields(indexPattern, fieldNames) { } export class ESSearchSource extends AbstractESSource { - static type = ES_SEARCH; + static type = SOURCE_TYPES.ES_SEARCH; constructor(descriptor, inspectorAdapters) { super( @@ -387,11 +387,21 @@ export class ESSearchSource extends AbstractESSource { }); return properties; }; + const epochMillisFields = searchFilters.fieldNames.filter(fieldName => { + const field = getField(indexPattern, fieldName); + return field.readFromDocValues && field.type === 'date'; + }); let featureCollection; try { const geoField = await this._getGeoField(); - featureCollection = hitsToGeoJson(hits, flattenHit, geoField.name, geoField.type); + featureCollection = hitsToGeoJson( + hits, + flattenHit, + geoField.name, + geoField.type, + epochMillisFields + ); } catch (error) { throw new Error( i18n.translate('xpack.maps.source.esSearch.convertToGeoJsonErrorMsg', { @@ -404,7 +414,7 @@ export class ESSearchSource extends AbstractESSource { return { data: featureCollection, - meta: { ...meta, sourceType: ES_SEARCH }, + meta: { ...meta, sourceType: SOURCE_TYPES.ES_SEARCH }, }; } @@ -570,7 +580,7 @@ export class ESSearchSource extends AbstractESSource { registerSource({ ConstructorFunction: ESSearchSource, - type: ES_SEARCH, + type: SOURCE_TYPES.ES_SEARCH, }); export const esDocumentsLayerWizardConfig = { diff --git a/x-pack/plugins/maps/public/layers/sources/es_search_source/es_search_source.test.ts b/x-pack/plugins/maps/public/layers/sources/es_search_source/es_search_source.test.ts index 2197e24aedb59..66cc2ddd85404 100644 --- a/x-pack/plugins/maps/public/layers/sources/es_search_source/es_search_source.test.ts +++ b/x-pack/plugins/maps/public/layers/sources/es_search_source/es_search_source.test.ts @@ -8,11 +8,11 @@ jest.mock('../../../kibana_services'); import { ESSearchSource } from './es_search_source'; import { VectorLayer } from '../../vector_layer'; -import { ES_SEARCH, SCALING_TYPES } from '../../../../common/constants'; +import { SCALING_TYPES, SOURCE_TYPES } from '../../../../common/constants'; import { ESSearchSourceDescriptor } from '../../../../common/descriptor_types'; const descriptor: ESSearchSourceDescriptor = { - type: ES_SEARCH, + type: SOURCE_TYPES.ES_SEARCH, id: '1234', indexPatternId: 'myIndexPattern', geoField: 'myLocation', diff --git a/x-pack/plugins/maps/public/layers/sources/es_source.d.ts b/x-pack/plugins/maps/public/layers/sources/es_source/es_source.d.ts similarity index 78% rename from x-pack/plugins/maps/public/layers/sources/es_source.d.ts rename to x-pack/plugins/maps/public/layers/sources/es_source/es_source.d.ts index 65851d0e7bd38..092dc3bf0d5a8 100644 --- a/x-pack/plugins/maps/public/layers/sources/es_source.d.ts +++ b/x-pack/plugins/maps/public/layers/sources/es_source/es_source.d.ts @@ -4,10 +4,10 @@ * you may not use this file except in compliance with the Elastic License. */ -import { AbstractVectorSource } from './vector_source'; -import { IVectorSource } from './vector_source'; -import { IndexPattern, SearchSource } from '../../../../../../src/plugins/data/public'; -import { VectorSourceRequestMeta } from '../../../common/descriptor_types'; +import { AbstractVectorSource } from '../vector_source'; +import { IVectorSource } from '../vector_source'; +import { IndexPattern, SearchSource } from '../../../../../../../src/plugins/data/public'; +import { VectorSourceRequestMeta } from '../../../../common/descriptor_types'; export interface IESSource extends IVectorSource { getId(): string; diff --git a/x-pack/plugins/maps/public/layers/sources/es_source.js b/x-pack/plugins/maps/public/layers/sources/es_source/es_source.js similarity index 95% rename from x-pack/plugins/maps/public/layers/sources/es_source.js rename to x-pack/plugins/maps/public/layers/sources/es_source/es_source.js index d90a802a38344..3402e367cbd73 100644 --- a/x-pack/plugins/maps/public/layers/sources/es_source.js +++ b/x-pack/plugins/maps/public/layers/sources/es_source/es_source.js @@ -4,23 +4,23 @@ * you may not use this file except in compliance with the Elastic License. */ -import { AbstractVectorSource } from './vector_source'; +import { AbstractVectorSource } from '../vector_source'; import { getAutocompleteService, fetchSearchSourceAndRecordWithInspector, getIndexPatternService, SearchSource, getTimeFilter, -} from '../../kibana_services'; -import { createExtentFilter } from '../../elasticsearch_geo_utils'; +} from '../../../kibana_services'; +import { createExtentFilter } from '../../../elasticsearch_geo_utils'; import _ from 'lodash'; import { i18n } from '@kbn/i18n'; import uuid from 'uuid/v4'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { copyPersistentState } from '../../reducers/util'; -import { ES_GEO_FIELD_TYPE } from '../../../common/constants'; -import { DataRequestAbortError } from '../util/data_request'; -import { expandToTileBoundaries } from './es_geo_grid_source/geo_tile_utils'; +import { copyPersistentState } from '../../../reducers/util'; +import { ES_GEO_FIELD_TYPE } from '../../../../common/constants'; +import { DataRequestAbortError } from '../../util/data_request'; +import { expandToTileBoundaries } from '../es_geo_grid_source/geo_tile_utils'; export class AbstractESSource extends AbstractVectorSource { constructor(descriptor, inspectorAdapters) { diff --git a/x-pack/plugins/maps/public/layers/sources/es_source/index.ts b/x-pack/plugins/maps/public/layers/sources/es_source/index.ts new file mode 100644 index 0000000000000..227a4dd634457 --- /dev/null +++ b/x-pack/plugins/maps/public/layers/sources/es_source/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 * from './es_source'; diff --git a/x-pack/plugins/maps/public/layers/sources/es_term_source.d.ts b/x-pack/plugins/maps/public/layers/sources/es_term_source/es_term_source.d.ts similarity index 77% rename from x-pack/plugins/maps/public/layers/sources/es_term_source.d.ts rename to x-pack/plugins/maps/public/layers/sources/es_term_source/es_term_source.d.ts index 44cdc851b4fc7..701bd5e2c8b5e 100644 --- a/x-pack/plugins/maps/public/layers/sources/es_term_source.d.ts +++ b/x-pack/plugins/maps/public/layers/sources/es_term_source/es_term_source.d.ts @@ -4,8 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ -import { IField } from '../fields/field'; -import { IESAggSource } from './es_agg_source'; +import { IField } from '../../fields/field'; +import { IESAggSource } from '../es_agg_source'; export interface IESTermSource extends IESAggSource { getTermField(): IField; diff --git a/x-pack/plugins/maps/public/layers/sources/es_term_source.js b/x-pack/plugins/maps/public/layers/sources/es_term_source/es_term_source.js similarity index 86% rename from x-pack/plugins/maps/public/layers/sources/es_term_source.js rename to x-pack/plugins/maps/public/layers/sources/es_term_source/es_term_source.js index 3ce0fb58aba19..cb07bb0e7d2ed 100644 --- a/x-pack/plugins/maps/public/layers/sources/es_term_source.js +++ b/x-pack/plugins/maps/public/layers/sources/es_term_source/es_term_source.js @@ -7,15 +7,14 @@ import _ from 'lodash'; import { i18n } from '@kbn/i18n'; -import { DEFAULT_MAX_BUCKETS_LIMIT, FIELD_ORIGIN, AGG_TYPE } from '../../../common/constants'; -import { ESDocField } from '../fields/es_doc_field'; -import { AbstractESAggSource, AGG_DELIMITER } from './es_agg_source'; -import { getField, addFieldToDSL, extractPropertiesFromBucket } from '../util/es_agg_utils'; +import { AGG_TYPE, DEFAULT_MAX_BUCKETS_LIMIT, FIELD_ORIGIN } from '../../../../common/constants'; +import { getJoinAggKey } from '../../../../common/get_join_key'; +import { ESDocField } from '../../fields/es_doc_field'; +import { AbstractESAggSource } from '../es_agg_source'; +import { getField, addFieldToDSL, extractPropertiesFromBucket } from '../../util/es_agg_utils'; const TERMS_AGG_NAME = 'join'; -const FIELD_NAME_PREFIX = '__kbnjoin__'; -const GROUP_BY_DELIMITER = '_groupby_'; const TERMS_BUCKET_KEYS_TO_IGNORE = ['key', 'doc_count']; export function extractPropertiesMap(rawEsData, countPropertyName) { @@ -64,11 +63,11 @@ export class ESTermSource extends AbstractESAggSource { } getAggKey(aggType, fieldName) { - const metricKey = - aggType !== AGG_TYPE.COUNT ? `${aggType}${AGG_DELIMITER}${fieldName}` : aggType; - return `${FIELD_NAME_PREFIX}${metricKey}${GROUP_BY_DELIMITER}${ - this._descriptor.indexPatternTitle - }.${this._termField.getName()}`; + return getJoinAggKey({ + aggType, + aggFieldName: fieldName, + rightSourceId: this._descriptor.id, + }); } getAggLabel(aggType, fieldName) { diff --git a/x-pack/plugins/maps/public/layers/sources/es_term_source.test.js b/x-pack/plugins/maps/public/layers/sources/es_term_source/es_term_source.test.js similarity index 83% rename from x-pack/plugins/maps/public/layers/sources/es_term_source.test.js rename to x-pack/plugins/maps/public/layers/sources/es_term_source/es_term_source.test.js index 14ffd068df465..14eb39180a6b8 100644 --- a/x-pack/plugins/maps/public/layers/sources/es_term_source.test.js +++ b/x-pack/plugins/maps/public/layers/sources/es_term_source/es_term_source.test.js @@ -7,7 +7,7 @@ import { ESTermSource, extractPropertiesMap } from './es_term_source'; jest.mock('ui/new_platform'); -jest.mock('../vector_layer', () => {}); +jest.mock('../../vector_layer', () => {}); const indexPatternTitle = 'myIndex'; const termFieldName = 'myTermField'; @@ -32,33 +32,32 @@ const metricExamples = [ describe('getMetricFields', () => { it('should override name and label of count metric', async () => { const source = new ESTermSource({ + id: '1234', indexPatternTitle: indexPatternTitle, term: termFieldName, }); const metrics = source.getMetricFields(); - expect(metrics[0].getName()).toEqual('__kbnjoin__count_groupby_myIndex.myTermField'); + expect(metrics[0].getName()).toEqual('__kbnjoin__count__1234'); expect(await metrics[0].getLabel()).toEqual('Count of myIndex'); }); it('should override name and label of sum metric', async () => { const source = new ESTermSource({ + id: '1234', indexPatternTitle: indexPatternTitle, term: termFieldName, metrics: metricExamples, }); const metrics = source.getMetricFields(); - expect(metrics[0].getName()).toEqual( - '__kbnjoin__sum_of_myFieldGettingSummed_groupby_myIndex.myTermField' - ); + expect(metrics[0].getName()).toEqual('__kbnjoin__sum_of_myFieldGettingSummed__1234'); expect(await metrics[0].getLabel()).toEqual('my custom label'); - expect(metrics[1].getName()).toEqual('__kbnjoin__count_groupby_myIndex.myTermField'); + expect(metrics[1].getName()).toEqual('__kbnjoin__count__1234'); expect(await metrics[1].getLabel()).toEqual('Count of myIndex'); }); }); describe('extractPropertiesMap', () => { - const minPropName = - '__kbnjoin__min_of_avlAirTemp_groupby_kibana_sample_data_ky_avl.kytcCountyNmbr'; + const minPropName = '__kbnjoin__min_of_avlAirTemp__1234'; const responseWithNumberTypes = { aggregations: { join: { @@ -81,7 +80,7 @@ describe('extractPropertiesMap', () => { }, }, }; - const countPropName = '__kbnjoin__count_groupby_kibana_sample_data_ky_avl.kytcCountyNmbr'; + const countPropName = '__kbnjoin__count__1234'; let propertiesMap; beforeAll(() => { diff --git a/x-pack/plugins/maps/public/layers/sources/es_term_source/index.ts b/x-pack/plugins/maps/public/layers/sources/es_term_source/index.ts new file mode 100644 index 0000000000000..6fad8384c690d --- /dev/null +++ b/x-pack/plugins/maps/public/layers/sources/es_term_source/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 * from './es_term_source'; diff --git a/x-pack/plugins/maps/public/layers/sources/kibana_regionmap_source/kibana_regionmap_source.js b/x-pack/plugins/maps/public/layers/sources/kibana_regionmap_source/kibana_regionmap_source.js index 7f4bcfa41f7c4..be333f8ee85a4 100644 --- a/x-pack/plugins/maps/public/layers/sources/kibana_regionmap_source/kibana_regionmap_source.js +++ b/x-pack/plugins/maps/public/layers/sources/kibana_regionmap_source/kibana_regionmap_source.js @@ -10,7 +10,7 @@ import { CreateSourceEditor } from './create_source_editor'; import { getKibanaRegionList } from '../../../meta'; import { i18n } from '@kbn/i18n'; import { getDataSourceLabel } from '../../../../common/i18n_getters'; -import { FIELD_ORIGIN, REGIONMAP_FILE } from '../../../../common/constants'; +import { FIELD_ORIGIN, SOURCE_TYPES } from '../../../../common/constants'; import { KibanaRegionField } from '../../fields/kibana_region_field'; import { registerSource } from '../source_registry'; @@ -19,7 +19,7 @@ const sourceTitle = i18n.translate('xpack.maps.source.kbnRegionMapTitle', { }); export class KibanaRegionmapSource extends AbstractVectorSource { - static type = REGIONMAP_FILE; + static type = SOURCE_TYPES.REGIONMAP_FILE; static createDescriptor({ name }) { return { @@ -99,7 +99,7 @@ export class KibanaRegionmapSource extends AbstractVectorSource { registerSource({ ConstructorFunction: KibanaRegionmapSource, - type: REGIONMAP_FILE, + type: SOURCE_TYPES.REGIONMAP_FILE, }); export const kibanaRegionMapLayerWizardConfig = { diff --git a/x-pack/plugins/maps/public/layers/sources/kibana_tilemap_source/kibana_tilemap_source.js b/x-pack/plugins/maps/public/layers/sources/kibana_tilemap_source/kibana_tilemap_source.js index b21bb6bdbbad4..bbb653eff32e2 100644 --- a/x-pack/plugins/maps/public/layers/sources/kibana_tilemap_source/kibana_tilemap_source.js +++ b/x-pack/plugins/maps/public/layers/sources/kibana_tilemap_source/kibana_tilemap_source.js @@ -11,7 +11,7 @@ import { getKibanaTileMap } from '../../../meta'; import { i18n } from '@kbn/i18n'; import { getDataSourceLabel } from '../../../../common/i18n_getters'; import _ from 'lodash'; -import { KIBANA_TILEMAP } from '../../../../common/constants'; +import { SOURCE_TYPES } from '../../../../common/constants'; import { registerSource } from '../source_registry'; const sourceTitle = i18n.translate('xpack.maps.source.kbnTMSTitle', { @@ -19,7 +19,7 @@ const sourceTitle = i18n.translate('xpack.maps.source.kbnTMSTitle', { }); export class KibanaTilemapSource extends AbstractTMSSource { - static type = KIBANA_TILEMAP; + static type = SOURCE_TYPES.KIBANA_TILEMAP; static createDescriptor() { return { @@ -86,7 +86,7 @@ export class KibanaTilemapSource extends AbstractTMSSource { registerSource({ ConstructorFunction: KibanaTilemapSource, - type: KIBANA_TILEMAP, + type: SOURCE_TYPES.KIBANA_TILEMAP, }); export const kibanaBasemapLayerWizardConfig = { diff --git a/x-pack/plugins/maps/public/layers/sources/mvt_single_layer_vector_source/index.ts b/x-pack/plugins/maps/public/layers/sources/mvt_single_layer_vector_source/index.ts new file mode 100644 index 0000000000000..89b7e76a7e359 --- /dev/null +++ b/x-pack/plugins/maps/public/layers/sources/mvt_single_layer_vector_source/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 * from './mvt_single_layer_vector_source'; +export * from './layer_wizard'; diff --git a/x-pack/plugins/maps/public/layers/sources/mvt_single_layer_vector_source/layer_wizard.tsx b/x-pack/plugins/maps/public/layers/sources/mvt_single_layer_vector_source/layer_wizard.tsx new file mode 100644 index 0000000000000..dfdea1489d50c --- /dev/null +++ b/x-pack/plugins/maps/public/layers/sources/mvt_single_layer_vector_source/layer_wizard.tsx @@ -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 { i18n } from '@kbn/i18n'; +import React from 'react'; +import { + MVTSingleLayerVectorSourceEditor, + MVTSingleLayerVectorSourceConfig, +} from './mvt_single_layer_vector_source_editor'; +import { MVTSingleLayerVectorSource, sourceTitle } from './mvt_single_layer_vector_source'; +import { LayerWizard, RenderWizardArguments } from '../../layer_wizard_registry'; +import { SOURCE_TYPES } from '../../../../common/constants'; + +export const mvtVectorSourceWizardConfig: LayerWizard = { + description: i18n.translate('xpack.maps.source.mvtVectorSourceWizard', { + defaultMessage: 'Vector source wizard', + }), + icon: 'grid', + renderWizard: ({ onPreviewSource, inspectorAdapters }: RenderWizardArguments) => { + const onSourceConfigChange = ({ + urlTemplate, + layerName, + minSourceZoom, + maxSourceZoom, + }: MVTSingleLayerVectorSourceConfig) => { + const sourceDescriptor = MVTSingleLayerVectorSource.createDescriptor({ + urlTemplate, + layerName, + minSourceZoom, + maxSourceZoom, + type: SOURCE_TYPES.MVT_SINGLE_LAYER, + }); + const source = new MVTSingleLayerVectorSource(sourceDescriptor, inspectorAdapters); + onPreviewSource(source); + }; + return <MVTSingleLayerVectorSourceEditor onSourceConfigChange={onSourceConfigChange} />; + }, + title: sourceTitle, +}; diff --git a/x-pack/plugins/maps/public/layers/sources/mvt_single_layer_vector_source/mvt_single_layer_vector_source.ts b/x-pack/plugins/maps/public/layers/sources/mvt_single_layer_vector_source/mvt_single_layer_vector_source.ts new file mode 100644 index 0000000000000..0bfda6be72203 --- /dev/null +++ b/x-pack/plugins/maps/public/layers/sources/mvt_single_layer_vector_source/mvt_single_layer_vector_source.ts @@ -0,0 +1,175 @@ +/* + * 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 uuid from 'uuid/v4'; +import { AbstractSource, ImmutableSourceProperty } from '../source'; +import { TiledVectorLayer } from '../../tiled_vector_layer'; +import { GeoJsonWithMeta, ITiledSingleLayerVectorSource } from '../vector_source'; +import { MAX_ZOOM, MIN_ZOOM, SOURCE_TYPES } from '../../../../common/constants'; +import { VECTOR_SHAPE_TYPES } from '../vector_feature_types'; +import { IField } from '../../fields/field'; +import { registerSource } from '../source_registry'; +import { getDataSourceLabel, getUrlLabel } from '../../../../common/i18n_getters'; +import { + LayerDescriptor, + MapExtent, + TiledSingleLayerVectorSourceDescriptor, + VectorSourceRequestMeta, + VectorSourceSyncMeta, +} from '../../../../common/descriptor_types'; +import { VectorLayerArguments } from '../../vector_layer'; + +export const sourceTitle = i18n.translate( + 'xpack.maps.source.MVTSingleLayerVectorSource.sourceTitle', + { + defaultMessage: 'Vector Tile Layer', + } +); + +export class MVTSingleLayerVectorSource extends AbstractSource + implements ITiledSingleLayerVectorSource { + static createDescriptor({ + urlTemplate, + layerName, + minSourceZoom, + maxSourceZoom, + }: TiledSingleLayerVectorSourceDescriptor) { + return { + type: SOURCE_TYPES.MVT_SINGLE_LAYER, + id: uuid(), + urlTemplate, + layerName, + minSourceZoom: Math.max(MIN_ZOOM, minSourceZoom), + maxSourceZoom: Math.min(MAX_ZOOM, maxSourceZoom), + }; + } + + readonly _descriptor: TiledSingleLayerVectorSourceDescriptor; + + constructor( + sourceDescriptor: TiledSingleLayerVectorSourceDescriptor, + inspectorAdapters?: object + ) { + super(sourceDescriptor, inspectorAdapters); + this._descriptor = sourceDescriptor; + } + + renderSourceSettingsEditor() { + return null; + } + + getFieldNames(): string[] { + return []; + } + + createDefaultLayer(options: LayerDescriptor): TiledVectorLayer { + const layerDescriptor = { + sourceDescriptor: this._descriptor, + ...options, + }; + const normalizedLayerDescriptor = TiledVectorLayer.createDescriptor(layerDescriptor, []); + const vectorLayerArguments: VectorLayerArguments = { + layerDescriptor: normalizedLayerDescriptor, + source: this, + }; + return new TiledVectorLayer(vectorLayerArguments); + } + + getGeoJsonWithMeta( + layerName: 'string', + searchFilters: unknown[], + registerCancelCallback: (callback: () => void) => void + ): Promise<GeoJsonWithMeta> { + // todo: remove this method + // This is a consequence of ITiledSingleLayerVectorSource extending IVectorSource. + throw new Error('Does not implement getGeoJsonWithMeta'); + } + + async getFields(): Promise<IField[]> { + return []; + } + + async getImmutableProperties(): Promise<ImmutableSourceProperty[]> { + return [ + { label: getDataSourceLabel(), value: sourceTitle }, + { label: getUrlLabel(), value: this._descriptor.urlTemplate }, + { + label: i18n.translate('xpack.maps.source.MVTSingleLayerVectorSource.layerNameMessage', { + defaultMessage: 'Layer name', + }), + value: this._descriptor.layerName, + }, + { + label: i18n.translate('xpack.maps.source.MVTSingleLayerVectorSource.minZoomMessage', { + defaultMessage: 'Min zoom', + }), + value: this._descriptor.minSourceZoom.toString(), + }, + { + label: i18n.translate('xpack.maps.source.MVTSingleLayerVectorSource.maxZoomMessage', { + defaultMessage: 'Max zoom', + }), + value: this._descriptor.maxSourceZoom.toString(), + }, + ]; + } + + async getDisplayName(): Promise<string> { + return this._descriptor.layerName; + } + + async getUrlTemplateWithMeta() { + return { + urlTemplate: this._descriptor.urlTemplate, + layerName: this._descriptor.layerName, + minSourceZoom: this._descriptor.minSourceZoom, + maxSourceZoom: this._descriptor.maxSourceZoom, + }; + } + + async getSupportedShapeTypes(): Promise<VECTOR_SHAPE_TYPES[]> { + return [VECTOR_SHAPE_TYPES.POINT, VECTOR_SHAPE_TYPES.LINE, VECTOR_SHAPE_TYPES.POLYGON]; + } + + canFormatFeatureProperties() { + return false; + } + + getMinZoom() { + return this._descriptor.minSourceZoom; + } + + getMaxZoom() { + return this._descriptor.maxSourceZoom; + } + + getBoundsForFilters(searchFilters: VectorSourceRequestMeta): MapExtent { + return { + maxLat: 90, + maxLon: 180, + minLat: -90, + minLon: -180, + }; + } + + getFieldByName(fieldName: string): IField | null { + return null; + } + + getSyncMeta(): VectorSourceSyncMeta { + return null; + } + + getApplyGlobalQuery(): boolean { + return false; + } +} + +registerSource({ + ConstructorFunction: MVTSingleLayerVectorSource, + type: SOURCE_TYPES.MVT_SINGLE_LAYER, +}); diff --git a/x-pack/plugins/maps/public/layers/sources/mvt_single_layer_vector_source/mvt_single_layer_vector_source_editor.tsx b/x-pack/plugins/maps/public/layers/sources/mvt_single_layer_vector_source/mvt_single_layer_vector_source_editor.tsx new file mode 100644 index 0000000000000..7a4b8d43811da --- /dev/null +++ b/x-pack/plugins/maps/public/layers/sources/mvt_single_layer_vector_source/mvt_single_layer_vector_source_editor.tsx @@ -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. + */ +/* eslint-disable @typescript-eslint/consistent-type-definitions */ + +import React, { Fragment, Component, ChangeEvent } from 'react'; +import _ from 'lodash'; +import { EuiFieldText, EuiFormRow } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { MAX_ZOOM, MIN_ZOOM } from '../../../../common/constants'; +import { ValidatedDualRange, Value } from '../../../../../../../src/plugins/kibana_react/public'; + +export type MVTSingleLayerVectorSourceConfig = { + urlTemplate: string; + layerName: string; + minSourceZoom: number; + maxSourceZoom: number; +}; + +export interface Props { + onSourceConfigChange: (sourceConfig: MVTSingleLayerVectorSourceConfig) => void; +} + +interface State { + urlTemplate: string; + layerName: string; + minSourceZoom: number; + maxSourceZoom: number; +} + +export class MVTSingleLayerVectorSourceEditor extends Component<Props, State> { + state = { + urlTemplate: '', + layerName: '', + minSourceZoom: MIN_ZOOM, + maxSourceZoom: MAX_ZOOM, + }; + + _sourceConfigChange = _.debounce(() => { + const canPreview = + this.state.urlTemplate.indexOf('{x}') >= 0 && + this.state.urlTemplate.indexOf('{y}') >= 0 && + this.state.urlTemplate.indexOf('{z}') >= 0; + + if (canPreview && this.state.layerName) { + this.props.onSourceConfigChange({ + urlTemplate: this.state.urlTemplate, + layerName: this.state.layerName, + minSourceZoom: this.state.minSourceZoom, + maxSourceZoom: this.state.maxSourceZoom, + }); + } + }, 200); + + _handleUrlTemplateChange = (e: ChangeEvent<HTMLInputElement>) => { + const url = e.target.value; + this.setState( + { + urlTemplate: url, + }, + () => this._sourceConfigChange() + ); + }; + + _handleLayerNameInputChange = (e: ChangeEvent<HTMLInputElement>) => { + const layerName = e.target.value; + this.setState( + { + layerName, + }, + () => this._sourceConfigChange() + ); + }; + + _handleZoomRangeChange = (e: Value) => { + const minSourceZoom = parseInt(e[0] as string, 10); + const maxSourceZoom = parseInt(e[1] as string, 10); + + if (this.state.minSourceZoom !== minSourceZoom || this.state.maxSourceZoom !== maxSourceZoom) { + this.setState({ minSourceZoom, maxSourceZoom }, () => this._sourceConfigChange()); + } + }; + + render() { + return ( + <Fragment> + <EuiFormRow + label={i18n.translate('xpack.maps.source.MVTSingleLayerVectorSourceEditor.urlMessage', { + defaultMessage: 'Url', + })} + > + <EuiFieldText value={this.state.urlTemplate} onChange={this._handleUrlTemplateChange} /> + </EuiFormRow> + <EuiFormRow + label={i18n.translate( + 'xpack.maps.source.MVTSingleLayerVectorSourceEditor.layerNameMessage', + { + defaultMessage: 'Layer name', + } + )} + > + <EuiFieldText value={this.state.layerName} onChange={this._handleLayerNameInputChange} /> + </EuiFormRow> + <ValidatedDualRange + label="" + formRowDisplay="columnCompressed" + min={MIN_ZOOM} + max={MAX_ZOOM} + value={[this.state.minSourceZoom, this.state.maxSourceZoom]} + showInput="inputWithPopover" + showRange + showLabels + onChange={this._handleZoomRangeChange} + allowEmptyRange={false} + compressed + prepend={i18n.translate( + 'xpack.maps.source.MVTSingleLayerVectorSourceEditor.dataZoomRangeMessage', + { + defaultMessage: 'Zoom levels', + } + )} + /> + </Fragment> + ); + } +} diff --git a/x-pack/plugins/maps/public/layers/sources/source.d.ts b/x-pack/plugins/maps/public/layers/sources/source.d.ts index e1706ad7b7d77..5a01da02adaae 100644 --- a/x-pack/plugins/maps/public/layers/sources/source.d.ts +++ b/x-pack/plugins/maps/public/layers/sources/source.d.ts @@ -3,12 +3,23 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ +/* eslint-disable @typescript-eslint/consistent-type-definitions */ -import { AbstractSourceDescriptor } from '../../../common/descriptor_types'; +import { AbstractSourceDescriptor, LayerDescriptor } from '../../../common/descriptor_types'; import { ILayer } from '../layer'; +export type ImmutableSourceProperty = { + label: string; + value: string; +}; + +export type Attribution = { + url: string; + label: string; +}; + export interface ISource { - createDefaultLayer(): ILayer; + createDefaultLayer(options?: LayerDescriptor): ILayer; destroy(): void; getDisplayName(): Promise<string>; getInspectorAdapters(): object; @@ -18,13 +29,18 @@ export interface ISource { isQueryAware(): boolean; isRefreshTimerAware(): Promise<boolean>; isTimeAware(): Promise<boolean>; + getImmutableProperties(): Promise<ImmutableSourceProperty[]>; + getAttributions(): Promise<Attribution[]>; + getMinZoom(): number; + getMaxZoom(): number; } export class AbstractSource implements ISource { - constructor(sourceDescriptor: AbstractSourceDescriptor, inspectorAdapters: object); + readonly _descriptor: AbstractSourceDescriptor; + constructor(sourceDescriptor: AbstractSourceDescriptor, inspectorAdapters?: object); destroy(): void; - createDefaultLayer(): ILayer; + createDefaultLayer(options?: LayerDescriptor, mapColors?: string[]): ILayer; getDisplayName(): Promise<string>; getInspectorAdapters(): object; isFieldAware(): boolean; @@ -33,4 +49,8 @@ export class AbstractSource implements ISource { isQueryAware(): boolean; isRefreshTimerAware(): Promise<boolean>; isTimeAware(): Promise<boolean>; + getImmutableProperties(): Promise<ImmutableSourceProperty[]>; + getAttributions(): Promise<Attribution[]>; + getMinZoom(): number; + getMaxZoom(): number; } diff --git a/x-pack/plugins/maps/public/layers/sources/source.js b/x-pack/plugins/maps/public/layers/sources/source.js index 3029a5c091202..555b8999d6284 100644 --- a/x-pack/plugins/maps/public/layers/sources/source.js +++ b/x-pack/plugins/maps/public/layers/sources/source.js @@ -6,6 +6,7 @@ // eslint-disable-next-line @kbn/eslint/no-restricted-paths import { copyPersistentState } from '../../reducers/util'; +import { MIN_ZOOM, MAX_ZOOM } from '../../../common/constants'; export class AbstractSource { static isIndexingSource = false; @@ -79,7 +80,7 @@ export class AbstractSource { return false; } - isQueryAware() { + async isTimeAware() { return false; } @@ -107,6 +108,14 @@ export class AbstractSource { return []; } + isFilterByMapBounds() { + return false; + } + + isQueryAware() { + return false; + } + getGeoGridPrecision() { return 0; } @@ -140,4 +149,12 @@ export class AbstractSource { async getValueSuggestions(/* field, query */) { return []; } + + getMinZoom() { + return MIN_ZOOM; + } + + getMaxZoom() { + return MAX_ZOOM; + } } diff --git a/x-pack/plugins/maps/public/layers/sources/source_registry.ts b/x-pack/plugins/maps/public/layers/sources/source_registry.ts index 518cab68b601b..3b334d45092ad 100644 --- a/x-pack/plugins/maps/public/layers/sources/source_registry.ts +++ b/x-pack/plugins/maps/public/layers/sources/source_registry.ts @@ -5,13 +5,12 @@ */ /* eslint-disable @typescript-eslint/consistent-type-definitions */ -import { AbstractSourceDescriptor } from '../../../common/descriptor_types'; import { ISource } from './source'; type SourceRegistryEntry = { ConstructorFunction: new ( - sourceDescriptor: AbstractSourceDescriptor, - inspectorAdapters: unknown + sourceDescriptor: any, // this is the source-descriptor that corresponds specifically to the particular ISource instance + inspectorAdapters?: object ) => ISource; type: string; }; diff --git a/x-pack/plugins/maps/public/layers/sources/tms_source/index.ts b/x-pack/plugins/maps/public/layers/sources/tms_source/index.ts new file mode 100644 index 0000000000000..f6bb1c6bc34ec --- /dev/null +++ b/x-pack/plugins/maps/public/layers/sources/tms_source/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 * from './tms_source'; diff --git a/x-pack/plugins/maps/public/layers/sources/tms_source.d.ts b/x-pack/plugins/maps/public/layers/sources/tms_source/tms_source.d.ts similarity index 80% rename from x-pack/plugins/maps/public/layers/sources/tms_source.d.ts rename to x-pack/plugins/maps/public/layers/sources/tms_source/tms_source.d.ts index 90b6f28e050fd..b31138f4cdb86 100644 --- a/x-pack/plugins/maps/public/layers/sources/tms_source.d.ts +++ b/x-pack/plugins/maps/public/layers/sources/tms_source/tms_source.d.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { AbstractSource, ISource } from './source'; +import { AbstractSource, Attribution, ISource } from '../source'; export interface ITMSSource extends ISource { getUrlTemplate(): Promise<string>; @@ -12,4 +12,5 @@ export interface ITMSSource extends ISource { export class AbstractTMSSource extends AbstractSource implements ITMSSource { getUrlTemplate(): Promise<string>; + getAttributions(): Promise<Attribution[]>; } diff --git a/x-pack/plugins/maps/public/layers/sources/tms_source.js b/x-pack/plugins/maps/public/layers/sources/tms_source/tms_source.js similarity index 94% rename from x-pack/plugins/maps/public/layers/sources/tms_source.js rename to x-pack/plugins/maps/public/layers/sources/tms_source/tms_source.js index f2ec9f2a29378..13b8da11633bc 100644 --- a/x-pack/plugins/maps/public/layers/sources/tms_source.js +++ b/x-pack/plugins/maps/public/layers/sources/tms_source/tms_source.js @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { AbstractSource } from './source'; +import { AbstractSource } from '../source'; export class AbstractTMSSource extends AbstractSource { async getUrlTemplate() { diff --git a/x-pack/plugins/maps/public/layers/sources/vector_feature_types.js b/x-pack/plugins/maps/public/layers/sources/vector_feature_types.js deleted file mode 100644 index cc5f30389c4f3..0000000000000 --- a/x-pack/plugins/maps/public/layers/sources/vector_feature_types.js +++ /dev/null @@ -1,11 +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. - */ - -export const VECTOR_SHAPE_TYPES = { - POINT: 'POINT', - LINE: 'LINE', - POLYGON: 'POLYGON', -}; diff --git a/x-pack/plugins/maps/public/layers/sources/vector_feature_types.ts b/x-pack/plugins/maps/public/layers/sources/vector_feature_types.ts new file mode 100644 index 0000000000000..9f03357e17dad --- /dev/null +++ b/x-pack/plugins/maps/public/layers/sources/vector_feature_types.ts @@ -0,0 +1,11 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export enum VECTOR_SHAPE_TYPES { + POINT = 'POINT', + LINE = 'LINE', + POLYGON = 'POLYGON', +} diff --git a/x-pack/plugins/maps/public/layers/sources/vector_source.d.ts b/x-pack/plugins/maps/public/layers/sources/vector_source.d.ts deleted file mode 100644 index d597e64277186..0000000000000 --- a/x-pack/plugins/maps/public/layers/sources/vector_source.d.ts +++ /dev/null @@ -1,49 +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. - */ -/* eslint-disable @typescript-eslint/consistent-type-definitions */ - -import { FeatureCollection } from 'geojson'; -import { AbstractSource, ISource } from './source'; -import { IField } from '../fields/field'; -import { - ESSearchSourceResponseMeta, - MapExtent, - VectorSourceRequestMeta, - VectorSourceSyncMeta, -} from '../../../common/descriptor_types'; - -export type GeoJsonFetchMeta = ESSearchSourceResponseMeta; - -export type GeoJsonWithMeta = { - data: FeatureCollection; - meta?: GeoJsonFetchMeta; -}; - -export interface IVectorSource extends ISource { - getBoundsForFilters(searchFilters: VectorSourceRequestMeta): MapExtent; - getGeoJsonWithMeta( - layerName: 'string', - searchFilters: unknown[], - registerCancelCallback: (callback: () => void) => void - ): Promise<GeoJsonWithMeta>; - - getFields(): Promise<IField[]>; - getFieldByName(fieldName: string): IField; - getSyncMeta(): VectorSourceSyncMeta; -} - -export class AbstractVectorSource extends AbstractSource implements IVectorSource { - getBoundsForFilters(searchFilters: VectorSourceRequestMeta): MapExtent; - getGeoJsonWithMeta( - layerName: 'string', - searchFilters: unknown[], - registerCancelCallback: (callback: () => void) => void - ): Promise<GeoJsonWithMeta>; - - getFields(): Promise<IField[]>; - getFieldByName(fieldName: string): IField; - getSyncMeta(): VectorSourceSyncMeta; -} diff --git a/x-pack/plugins/maps/public/layers/sources/vector_source/index.ts b/x-pack/plugins/maps/public/layers/sources/vector_source/index.ts new file mode 100644 index 0000000000000..62e801d027634 --- /dev/null +++ b/x-pack/plugins/maps/public/layers/sources/vector_source/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 * from './vector_source'; diff --git a/x-pack/plugins/maps/public/layers/sources/vector_source/vector_source.d.ts b/x-pack/plugins/maps/public/layers/sources/vector_source/vector_source.d.ts new file mode 100644 index 0000000000000..804915dd73052 --- /dev/null +++ b/x-pack/plugins/maps/public/layers/sources/vector_source/vector_source.d.ts @@ -0,0 +1,67 @@ +/* + * 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. + */ +/* eslint-disable @typescript-eslint/consistent-type-definitions */ + +import { FeatureCollection } from 'geojson'; +import { AbstractSource, ISource } from '../source'; +import { IField } from '../../fields/field'; +import { + ESSearchSourceResponseMeta, + MapExtent, + VectorSourceRequestMeta, + VectorSourceSyncMeta, +} from '../../../../common/descriptor_types'; +import { VECTOR_SHAPE_TYPES } from '../vector_feature_types'; + +export type GeoJsonFetchMeta = ESSearchSourceResponseMeta; + +export type GeoJsonWithMeta = { + data: FeatureCollection; + meta?: GeoJsonFetchMeta; +}; + +export interface IVectorSource extends ISource { + getBoundsForFilters(searchFilters: VectorSourceRequestMeta): MapExtent; + getGeoJsonWithMeta( + layerName: 'string', + searchFilters: unknown[], + registerCancelCallback: (callback: () => void) => void + ): Promise<GeoJsonWithMeta>; + + getFields(): Promise<IField[]>; + getFieldByName(fieldName: string): IField | null; + getSyncMeta(): VectorSourceSyncMeta; + getFieldNames(): string[]; + getApplyGlobalQuery(): boolean; +} + +export class AbstractVectorSource extends AbstractSource implements IVectorSource { + getBoundsForFilters(searchFilters: VectorSourceRequestMeta): MapExtent; + getGeoJsonWithMeta( + layerName: 'string', + searchFilters: unknown[], + registerCancelCallback: (callback: () => void) => void + ): Promise<GeoJsonWithMeta>; + + getFields(): Promise<IField[]>; + getFieldByName(fieldName: string): IField | null; + getSyncMeta(): VectorSourceSyncMeta; + getSupportedShapeTypes(): Promise<VECTOR_SHAPE_TYPES[]>; + canFormatFeatureProperties(): boolean; + getApplyGlobalQuery(): boolean; + getFieldNames(): string[]; +} + +export interface ITiledSingleLayerVectorSource extends IVectorSource { + getUrlTemplateWithMeta(): Promise<{ + layerName: string; + urlTemplate: string; + minSourceZoom: number; + maxSourceZoom: number; + }>; + getMinZoom(): number; + getMaxZoom(): number; +} diff --git a/x-pack/plugins/maps/public/layers/sources/vector_source.js b/x-pack/plugins/maps/public/layers/sources/vector_source/vector_source.js similarity index 92% rename from x-pack/plugins/maps/public/layers/sources/vector_source.js rename to x-pack/plugins/maps/public/layers/sources/vector_source/vector_source.js index 7f97b1b21d189..509584cbc415a 100644 --- a/x-pack/plugins/maps/public/layers/sources/vector_source.js +++ b/x-pack/plugins/maps/public/layers/sources/vector_source/vector_source.js @@ -4,14 +4,14 @@ * you may not use this file except in compliance with the Elastic License. */ -import { VectorLayer } from '../vector_layer'; -import { TooltipProperty } from '../tooltips/tooltip_property'; -import { VectorStyle } from '../styles/vector/vector_style'; -import { AbstractSource } from './source'; +import { VectorLayer } from '../../vector_layer'; +import { TooltipProperty } from '../../tooltips/tooltip_property'; +import { VectorStyle } from '../../styles/vector/vector_style'; +import { AbstractSource } from './../source'; import * as topojson from 'topojson-client'; import _ from 'lodash'; import { i18n } from '@kbn/i18n'; -import { VECTOR_SHAPE_TYPES } from './vector_feature_types'; +import { VECTOR_SHAPE_TYPES } from './../vector_feature_types'; export class AbstractVectorSource extends AbstractSource { static async getGeoJson({ format, featureCollectionPath, fetchUrl }) { @@ -60,6 +60,10 @@ export class AbstractVectorSource extends AbstractSource { throw new Error(`Should implemement ${this.constructor.type} ${this}`); } + getFieldNames() { + return []; + } + /** * Retrieves a field. This may be an existing instance. * @param fieldName diff --git a/x-pack/plugins/maps/public/layers/sources/wms_source/wms_source.js b/x-pack/plugins/maps/public/layers/sources/wms_source/wms_source.js index 749560a2bb4b1..33f764784124e 100644 --- a/x-pack/plugins/maps/public/layers/sources/wms_source/wms_source.js +++ b/x-pack/plugins/maps/public/layers/sources/wms_source/wms_source.js @@ -12,7 +12,7 @@ import { WMSCreateSourceEditor } from './wms_create_source_editor'; import { i18n } from '@kbn/i18n'; import { getDataSourceLabel, getUrlLabel } from '../../../../common/i18n_getters'; import { WmsClient } from './wms_client'; -import { WMS } from '../../../../common/constants'; +import { SOURCE_TYPES } from '../../../../common/constants'; import { registerSource } from '../source_registry'; const sourceTitle = i18n.translate('xpack.maps.source.wmsTitle', { @@ -20,7 +20,7 @@ const sourceTitle = i18n.translate('xpack.maps.source.wmsTitle', { }); export class WMSSource extends AbstractTMSSource { - static type = WMS; + static type = SOURCE_TYPES.WMS; static createDescriptor({ serviceUrl, layers, styles, attributionText, attributionUrl }) { return { @@ -92,7 +92,7 @@ export class WMSSource extends AbstractTMSSource { registerSource({ ConstructorFunction: WMSSource, - type: WMS, + type: SOURCE_TYPES.WMS, }); export const wmsLayerWizardConfig = { diff --git a/x-pack/plugins/maps/public/layers/sources/xyz_tms_source.d.ts b/x-pack/plugins/maps/public/layers/sources/xyz_tms_source.d.ts deleted file mode 100644 index 579c9debeab3e..0000000000000 --- a/x-pack/plugins/maps/public/layers/sources/xyz_tms_source.d.ts +++ /dev/null @@ -1,11 +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 { AbstractTMSSource } from './tms_source'; -import { XYZTMSSourceDescriptor } from '../../../common/descriptor_types'; - -export class XYZTMSSource extends AbstractTMSSource { - constructor(sourceDescriptor: XYZTMSSourceDescriptor, inspectorAdapters: unknown); -} diff --git a/x-pack/plugins/maps/public/layers/sources/xyz_tms_source.js b/x-pack/plugins/maps/public/layers/sources/xyz_tms_source.js deleted file mode 100644 index d53fbffd21512..0000000000000 --- a/x-pack/plugins/maps/public/layers/sources/xyz_tms_source.js +++ /dev/null @@ -1,187 +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 { EuiFieldText, EuiFormRow } from '@elastic/eui'; - -import { AbstractTMSSource } from './tms_source'; -import { TileLayer } from '../tile_layer'; -import { i18n } from '@kbn/i18n'; -import { getDataSourceLabel, getUrlLabel } from '../../../common/i18n_getters'; -import _ from 'lodash'; -import { EMS_XYZ } from '../../../common/constants'; -import { registerSource } from './source_registry'; - -const sourceTitle = i18n.translate('xpack.maps.source.ems_xyzTitle', { - defaultMessage: 'Tile Map Service', -}); - -export class XYZTMSSource extends AbstractTMSSource { - static type = EMS_XYZ; - - static createDescriptor({ urlTemplate, attributionText, attributionUrl }) { - return { - type: XYZTMSSource.type, - urlTemplate, - attributionText, - attributionUrl, - }; - } - - async getImmutableProperties() { - return [ - { label: getDataSourceLabel(), value: sourceTitle }, - { label: getUrlLabel(), value: this._descriptor.urlTemplate }, - ]; - } - - _createDefaultLayerDescriptor(options) { - return TileLayer.createDescriptor({ - sourceDescriptor: this._descriptor, - ...options, - }); - } - - createDefaultLayer(options) { - return new TileLayer({ - layerDescriptor: this._createDefaultLayerDescriptor(options), - source: this, - }); - } - - async getDisplayName() { - return this._descriptor.urlTemplate; - } - - getAttributions() { - const { attributionText, attributionUrl } = this._descriptor; - const attributionComplete = !!attributionText && !!attributionUrl; - - return attributionComplete - ? [ - { - url: attributionUrl, - label: attributionText, - }, - ] - : []; - } - - getUrlTemplate() { - return this._descriptor.urlTemplate; - } -} - -class XYZTMSEditor extends React.Component { - state = { - tmsInput: '', - tmsCanPreview: false, - attributionText: '', - attributionUrl: '', - }; - - _sourceConfigChange = _.debounce(updatedSourceConfig => { - if (this.state.tmsCanPreview) { - this.props.onSourceConfigChange(updatedSourceConfig); - } - }, 2000); - - _handleTMSInputChange(e) { - const url = e.target.value; - - const canPreview = - url.indexOf('{x}') >= 0 && url.indexOf('{y}') >= 0 && url.indexOf('{z}') >= 0; - this.setState( - { - tmsInput: url, - tmsCanPreview: canPreview, - }, - () => this._sourceConfigChange({ urlTemplate: url }) - ); - } - - _handleTMSAttributionChange(attributionUpdate) { - this.setState(attributionUpdate, () => { - const { attributionText, attributionUrl, tmsInput } = this.state; - - if (tmsInput && attributionText && attributionUrl) { - this._sourceConfigChange({ - urlTemplate: tmsInput, - attributionText, - attributionUrl, - }); - } - }); - } - - render() { - const { attributionText, attributionUrl } = this.state; - - return ( - <Fragment> - <EuiFormRow label="Url"> - <EuiFieldText - placeholder={'https://a.tile.openstreetmap.org/{z}/{x}/{y}.png'} - onChange={e => this._handleTMSInputChange(e)} - /> - </EuiFormRow> - <EuiFormRow - label="Attribution text" - isInvalid={attributionUrl !== '' && attributionText === ''} - error={[ - i18n.translate('xpack.maps.xyztmssource.attributionText', { - defaultMessage: 'Attribution url must have accompanying text', - }), - ]} - > - <EuiFieldText - placeholder={'© OpenStreetMap contributors'} - onChange={({ target }) => - this._handleTMSAttributionChange({ attributionText: target.value }) - } - /> - </EuiFormRow> - <EuiFormRow - label="Attribution link" - isInvalid={attributionText !== '' && attributionUrl === ''} - error={[ - i18n.translate('xpack.maps.xyztmssource.attributionLink', { - defaultMessage: 'Attribution text must have an accompanying link', - }), - ]} - > - <EuiFieldText - placeholder={'https://www.openstreetmap.org/copyright'} - onChange={({ target }) => - this._handleTMSAttributionChange({ attributionUrl: target.value }) - } - /> - </EuiFormRow> - </Fragment> - ); - } -} - -registerSource({ - ConstructorFunction: XYZTMSSource, - type: EMS_XYZ, -}); - -export const tmsLayerWizardConfig = { - description: i18n.translate('xpack.maps.source.ems_xyzDescription', { - defaultMessage: 'Tile map service configured in interface', - }), - icon: 'grid', - renderWizard: ({ onPreviewSource, inspectorAdapters }) => { - const onSourceConfigChange = sourceConfig => { - const sourceDescriptor = XYZTMSSource.createDescriptor(sourceConfig); - const source = new XYZTMSSource(sourceDescriptor, inspectorAdapters); - onPreviewSource(source); - }; - return <XYZTMSEditor onSourceConfigChange={onSourceConfigChange} />; - }, - title: sourceTitle, -}; diff --git a/x-pack/plugins/maps/public/layers/sources/xyz_tms_source.test.ts b/x-pack/plugins/maps/public/layers/sources/xyz_tms_source.test.ts deleted file mode 100644 index e5ab5e57122ba..0000000000000 --- a/x-pack/plugins/maps/public/layers/sources/xyz_tms_source.test.ts +++ /dev/null @@ -1,30 +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 { XYZTMSSource } from './xyz_tms_source'; -import { ILayer } from '../layer'; -import { TileLayer } from '../tile_layer'; -import { EMS_XYZ } from '../../../common/constants'; -import { XYZTMSSourceDescriptor } from '../../../common/descriptor_types'; - -const descriptor: XYZTMSSourceDescriptor = { - type: EMS_XYZ, - urlTemplate: 'https://example.com/{x}/{y}/{z}.png', - id: 'foobar', -}; -describe('xyz Tilemap Source', () => { - it('should create a tile-layer', () => { - const source = new XYZTMSSource(descriptor, null); - const layer: ILayer = source.createDefaultLayer(); - expect(layer instanceof TileLayer).toEqual(true); - }); - - it('should echo url template for url template', async () => { - const source = new XYZTMSSource(descriptor, null); - const template = await source.getUrlTemplate(); - expect(template).toEqual(descriptor.urlTemplate); - }); -}); diff --git a/x-pack/plugins/maps/public/layers/sources/xyz_tms_source/index.ts b/x-pack/plugins/maps/public/layers/sources/xyz_tms_source/index.ts new file mode 100644 index 0000000000000..c1116ad47a375 --- /dev/null +++ b/x-pack/plugins/maps/public/layers/sources/xyz_tms_source/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 * from './xyz_tms_source'; +export * from './layer_wizard'; diff --git a/x-pack/plugins/maps/public/layers/sources/xyz_tms_source/layer_wizard.tsx b/x-pack/plugins/maps/public/layers/sources/xyz_tms_source/layer_wizard.tsx new file mode 100644 index 0000000000000..8b1ed588c8dd1 --- /dev/null +++ b/x-pack/plugins/maps/public/layers/sources/xyz_tms_source/layer_wizard.tsx @@ -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 { i18n } from '@kbn/i18n'; +import React from 'react'; +import { XYZTMSEditor, XYZTMSSourceConfig } from './xyz_tms_editor'; +import { XYZTMSSource, sourceTitle } from './xyz_tms_source'; +import { LayerWizard, RenderWizardArguments } from '../../layer_wizard_registry'; + +export const tmsLayerWizardConfig: LayerWizard = { + description: i18n.translate('xpack.maps.source.ems_xyzDescription', { + defaultMessage: 'Tile map service configured in interface', + }), + icon: 'grid', + renderWizard: ({ onPreviewSource }: RenderWizardArguments) => { + const onSourceConfigChange = (sourceConfig: XYZTMSSourceConfig) => { + const sourceDescriptor = XYZTMSSource.createDescriptor(sourceConfig); + const source = new XYZTMSSource(sourceDescriptor); + onPreviewSource(source); + }; + return <XYZTMSEditor onSourceConfigChange={onSourceConfigChange} />; + }, + title: sourceTitle, +}; diff --git a/x-pack/plugins/maps/public/layers/sources/xyz_tms_source/xyz_tms_editor.tsx b/x-pack/plugins/maps/public/layers/sources/xyz_tms_source/xyz_tms_editor.tsx new file mode 100644 index 0000000000000..0ee0fbb23bcff --- /dev/null +++ b/x-pack/plugins/maps/public/layers/sources/xyz_tms_source/xyz_tms_editor.tsx @@ -0,0 +1,122 @@ +/* + * 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. + */ +/* eslint-disable @typescript-eslint/consistent-type-definitions */ + +import React, { Fragment, Component, ChangeEvent } from 'react'; +import _ from 'lodash'; +import { EuiFormRow, EuiFieldText } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { AttributionDescriptor } from '../../../../common/descriptor_types'; + +export type XYZTMSSourceConfig = AttributionDescriptor & { + urlTemplate: string; +}; + +export interface Props { + onSourceConfigChange: (sourceConfig: XYZTMSSourceConfig) => void; +} + +interface State { + tmsInput: string; + tmsCanPreview: boolean; + attributionText: string; + attributionUrl: string; +} + +export class XYZTMSEditor extends Component<Props, State> { + state = { + tmsInput: '', + tmsCanPreview: false, + attributionText: '', + attributionUrl: '', + }; + + _sourceConfigChange = _.debounce((updatedSourceConfig: XYZTMSSourceConfig) => { + if (this.state.tmsCanPreview) { + this.props.onSourceConfigChange(updatedSourceConfig); + } + }, 2000); + + _handleTMSInputChange(e: ChangeEvent<HTMLInputElement>) { + const url = e.target.value; + + const canPreview = + url.indexOf('{x}') >= 0 && url.indexOf('{y}') >= 0 && url.indexOf('{z}') >= 0; + this.setState( + { + tmsInput: url, + tmsCanPreview: canPreview, + }, + () => this._sourceConfigChange({ urlTemplate: url }) + ); + } + + _handleTMSAttributionChange(attributionUpdate: AttributionDescriptor) { + this.setState( + { + attributionUrl: attributionUpdate.attributionUrl || '', + attributionText: attributionUpdate.attributionText || '', + }, + () => { + const { attributionText, attributionUrl, tmsInput } = this.state; + + if (tmsInput && attributionText && attributionUrl) { + this._sourceConfigChange({ + urlTemplate: tmsInput, + attributionText, + attributionUrl, + }); + } + } + ); + } + + render() { + const { attributionText, attributionUrl } = this.state; + return ( + <Fragment> + <EuiFormRow label="Url"> + <EuiFieldText + placeholder={'https://a.tile.openstreetmap.org/{z}/{x}/{y}.png'} + onChange={e => this._handleTMSInputChange(e)} + /> + </EuiFormRow> + <EuiFormRow + label="Attribution text" + isInvalid={attributionUrl !== '' && attributionText === ''} + error={[ + i18n.translate('xpack.maps.xyztmssource.attributionText', { + defaultMessage: 'Attribution url must have accompanying text', + }), + ]} + > + <EuiFieldText + placeholder={'© OpenStreetMap contributors'} + onChange={({ target }: ChangeEvent<HTMLInputElement>) => + this._handleTMSAttributionChange({ attributionText: target.value }) + } + /> + </EuiFormRow> + <EuiFormRow + label="Attribution link" + isInvalid={attributionText !== '' && attributionUrl === ''} + error={[ + i18n.translate('xpack.maps.xyztmssource.attributionLink', { + defaultMessage: 'Attribution text must have an accompanying link', + }), + ]} + > + <EuiFieldText + placeholder={'https://www.openstreetmap.org/copyright'} + onChange={({ target }: ChangeEvent<HTMLInputElement>) => + this._handleTMSAttributionChange({ attributionUrl: target.value }) + } + /> + </EuiFormRow> + </Fragment> + ); + } +} diff --git a/x-pack/plugins/maps/public/layers/sources/xyz_tms_source/xyz_tms_source.test.ts b/x-pack/plugins/maps/public/layers/sources/xyz_tms_source/xyz_tms_source.test.ts new file mode 100644 index 0000000000000..4031a18bff7cb --- /dev/null +++ b/x-pack/plugins/maps/public/layers/sources/xyz_tms_source/xyz_tms_source.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 { XYZTMSSource } from './xyz_tms_source'; +import { ILayer } from '../../layer'; +import { TileLayer } from '../../tile_layer'; +import { SOURCE_TYPES } from '../../../../common/constants'; +import { XYZTMSSourceDescriptor } from '../../../../common/descriptor_types'; + +const descriptor: XYZTMSSourceDescriptor = { + type: SOURCE_TYPES.EMS_XYZ, + urlTemplate: 'https://example.com/{x}/{y}/{z}.png', + id: 'foobar', +}; +describe('xyz Tilemap Source', () => { + it('should create a tile-layer', () => { + const source = new XYZTMSSource(descriptor); + const layer: ILayer = source.createDefaultLayer(); + expect(layer instanceof TileLayer).toEqual(true); + }); + + it('should echo url template for url template', async () => { + const source = new XYZTMSSource(descriptor); + const template = await source.getUrlTemplate(); + expect(template).toEqual(descriptor.urlTemplate); + }); +}); diff --git a/x-pack/plugins/maps/public/layers/sources/xyz_tms_source/xyz_tms_source.ts b/x-pack/plugins/maps/public/layers/sources/xyz_tms_source/xyz_tms_source.ts new file mode 100644 index 0000000000000..8b64480f92961 --- /dev/null +++ b/x-pack/plugins/maps/public/layers/sources/xyz_tms_source/xyz_tms_source.ts @@ -0,0 +1,87 @@ +/* + * 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 { TileLayer } from '../../tile_layer'; +import { getDataSourceLabel, getUrlLabel } from '../../../../common/i18n_getters'; +import { SOURCE_TYPES } from '../../../../common/constants'; +import { registerSource } from '../source_registry'; +import { AbstractTMSSource } from '../tms_source'; +import { LayerDescriptor, XYZTMSSourceDescriptor } from '../../../../common/descriptor_types'; +import { Attribution, ImmutableSourceProperty } from '../source'; +import { XYZTMSSourceConfig } from './xyz_tms_editor'; + +export const sourceTitle = i18n.translate('xpack.maps.source.ems_xyzTitle', { + defaultMessage: 'Tile Map Service', +}); + +export class XYZTMSSource extends AbstractTMSSource { + static type = SOURCE_TYPES.EMS_XYZ; + + readonly _descriptor: XYZTMSSourceDescriptor; + + static createDescriptor({ + urlTemplate, + attributionText, + attributionUrl, + }: XYZTMSSourceConfig): XYZTMSSourceDescriptor { + return { + type: XYZTMSSource.type, + urlTemplate, + attributionText, + attributionUrl, + }; + } + + constructor(sourceDescriptor: XYZTMSSourceDescriptor) { + super(sourceDescriptor); + this._descriptor = sourceDescriptor; + } + + async getImmutableProperties(): Promise<ImmutableSourceProperty[]> { + return [ + { label: getDataSourceLabel(), value: sourceTitle }, + { label: getUrlLabel(), value: this._descriptor.urlTemplate }, + ]; + } + + createDefaultLayer(options?: LayerDescriptor): TileLayer { + const layerDescriptor: LayerDescriptor = TileLayer.createDescriptor({ + sourceDescriptor: this._descriptor, + ...options, + }); + return new TileLayer({ + layerDescriptor, + source: this, + }); + } + + async getDisplayName(): Promise<string> { + return this._descriptor.urlTemplate; + } + + async getAttributions(): Promise<Attribution[]> { + const { attributionText, attributionUrl } = this._descriptor; + const attributionComplete = !!attributionText && !!attributionUrl; + return attributionComplete + ? [ + { + url: attributionUrl as string, + label: attributionText as string, + }, + ] + : []; + } + + async getUrlTemplate(): Promise<string> { + return this._descriptor.urlTemplate; + } +} + +registerSource({ + ConstructorFunction: XYZTMSSource, + type: SOURCE_TYPES.EMS_XYZ, +}); diff --git a/x-pack/plugins/maps/public/layers/styles/vector/components/vector_style_editor.js b/x-pack/plugins/maps/public/layers/styles/vector/components/vector_style_editor.js index 6cece5efb3a5d..c46dc2cb4b73e 100644 --- a/x-pack/plugins/maps/public/layers/styles/vector/components/vector_style_editor.js +++ b/x-pack/plugins/maps/public/layers/styles/vector/components/vector_style_editor.js @@ -37,7 +37,7 @@ export class VectorStyleEditor extends Component { defaultDynamicProperties: getDefaultDynamicProperties(), defaultStaticProperties: getDefaultStaticProperties(), supportedFeatures: undefined, - selectedFeatureType: undefined, + selectedFeature: null, }; componentWillUnmount() { @@ -91,18 +91,20 @@ export class VectorStyleEditor extends Component { return; } - let selectedFeature = VECTOR_SHAPE_TYPES.POLYGON; - if (this.props.isPointsOnly) { - selectedFeature = VECTOR_SHAPE_TYPES.POINT; - } else if (this.props.isLinesOnly) { - selectedFeature = VECTOR_SHAPE_TYPES.LINE; + if (!_.isEqual(supportedFeatures, this.state.supportedFeatures)) { + this.setState({ supportedFeatures }); } - if ( - !_.isEqual(supportedFeatures, this.state.supportedFeatures) || - selectedFeature !== this.state.selectedFeature - ) { - this.setState({ supportedFeatures, selectedFeature }); + if (this.state.selectedFeature === null) { + let selectedFeature = VECTOR_SHAPE_TYPES.POLYGON; + if (this.props.isPointsOnly) { + selectedFeature = VECTOR_SHAPE_TYPES.POINT; + } else if (this.props.isLinesOnly) { + selectedFeature = VECTOR_SHAPE_TYPES.LINE; + } + this.setState({ + selectedFeature: selectedFeature, + }); } } diff --git a/x-pack/plugins/maps/public/layers/styles/vector/properties/dynamic_style_property.js b/x-pack/plugins/maps/public/layers/styles/vector/properties/dynamic_style_property.js index ea521f8749d80..8cef78f9a8f21 100644 --- a/x-pack/plugins/maps/public/layers/styles/vector/properties/dynamic_style_property.js +++ b/x-pack/plugins/maps/public/layers/styles/vector/properties/dynamic_style_property.js @@ -279,6 +279,10 @@ export class DynamicStyleProperty extends AbstractStyleProperty { } getNumericalMbFeatureStateValue(value) { + if (typeof value === 'number') { + return value; + } + const valueAsFloat = parseFloat(value); return isNaN(valueAsFloat) ? null : valueAsFloat; } diff --git a/x-pack/plugins/maps/public/layers/styles/vector/vector_style.d.ts b/x-pack/plugins/maps/public/layers/styles/vector/vector_style.d.ts index 77ea44ac26bf9..e010d5ac7d7a3 100644 --- a/x-pack/plugins/maps/public/layers/styles/vector/vector_style.d.ts +++ b/x-pack/plugins/maps/public/layers/styles/vector/vector_style.d.ts @@ -7,17 +7,23 @@ import { IStyleProperty } from './properties/style_property'; import { IDynamicStyleProperty } from './properties/dynamic_style_property'; import { IVectorLayer } from '../../vector_layer'; import { IVectorSource } from '../../sources/vector_source'; -import { VectorStyleDescriptor } from '../../../../common/descriptor_types'; +import { + VectorStyleDescriptor, + VectorStylePropertiesDescriptor, +} from '../../../../common/descriptor_types'; export interface IVectorStyle { getAllStyleProperties(): IStyleProperty[]; getDescriptor(): VectorStyleDescriptor; getDynamicPropertiesArray(): IDynamicStyleProperty[]; + getSourceFieldNames(): string[]; } export class VectorStyle implements IVectorStyle { + static createDescriptor(properties: VectorStylePropertiesDescriptor): VectorStyleDescriptor; + static createDefaultStyleProperties(mapColors: string[]): VectorStylePropertiesDescriptor; constructor(descriptor: VectorStyleDescriptor, source: IVectorSource, layer: IVectorLayer); - + getSourceFieldNames(): string[]; getAllStyleProperties(): IStyleProperty[]; getDescriptor(): VectorStyleDescriptor; getDynamicPropertiesArray(): IDynamicStyleProperty[]; diff --git a/x-pack/plugins/maps/public/layers/tile_layer.js b/x-pack/plugins/maps/public/layers/tile_layer.js index aa2619e96f834..2ac60e12d137a 100644 --- a/x-pack/plugins/maps/public/layers/tile_layer.js +++ b/x-pack/plugins/maps/public/layers/tile_layer.js @@ -11,8 +11,8 @@ import { SOURCE_DATA_ID_ORIGIN, LAYER_TYPE } from '../../common/constants'; export class TileLayer extends AbstractLayer { static type = LAYER_TYPE.TILE; - static createDescriptor(options) { - const tileLayerDescriptor = super.createDescriptor(options); + static createDescriptor(options, mapColors) { + const tileLayerDescriptor = super.createDescriptor(options, mapColors); tileLayerDescriptor.type = TileLayer.type; tileLayerDescriptor.alpha = _.get(options, 'alpha', 1); return tileLayerDescriptor; diff --git a/x-pack/plugins/maps/public/layers/tile_layer.test.ts b/x-pack/plugins/maps/public/layers/tile_layer.test.ts index 1cb99dcbc1a70..f8c2fd9db60fa 100644 --- a/x-pack/plugins/maps/public/layers/tile_layer.test.ts +++ b/x-pack/plugins/maps/public/layers/tile_layer.test.ts @@ -5,19 +5,19 @@ */ import { TileLayer } from './tile_layer'; -import { EMS_XYZ } from '../../common/constants'; +import { SOURCE_TYPES } from '../../common/constants'; import { XYZTMSSourceDescriptor } from '../../common/descriptor_types'; import { ITMSSource, AbstractTMSSource } from './sources/tms_source'; import { ILayer } from './layer'; const sourceDescriptor: XYZTMSSourceDescriptor = { - type: EMS_XYZ, + type: SOURCE_TYPES.EMS_XYZ, urlTemplate: 'https://example.com/{x}/{y}/{z}.png', id: 'foobar', }; class MockTileSource extends AbstractTMSSource implements ITMSSource { - private readonly _descriptor: XYZTMSSourceDescriptor; + readonly _descriptor: XYZTMSSourceDescriptor; constructor(descriptor: XYZTMSSourceDescriptor) { super(descriptor, {}); this._descriptor = descriptor; diff --git a/x-pack/plugins/maps/public/layers/tiled_vector_layer.tsx b/x-pack/plugins/maps/public/layers/tiled_vector_layer.tsx new file mode 100644 index 0000000000000..c47cae5641e56 --- /dev/null +++ b/x-pack/plugins/maps/public/layers/tiled_vector_layer.tsx @@ -0,0 +1,170 @@ +/* + * 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 { EuiIcon } from '@elastic/eui'; +import { VectorStyle } from './styles/vector/vector_style'; +import { SOURCE_DATA_ID_ORIGIN, LAYER_TYPE } from '../../common/constants'; +import { VectorLayer, VectorLayerArguments } from './vector_layer'; +import { canSkipSourceUpdate } from './util/can_skip_fetch'; +import { ITiledSingleLayerVectorSource } from './sources/vector_source'; +import { SyncContext } from '../actions/map_actions'; +import { ISource } from './sources/source'; +import { VectorLayerDescriptor, VectorSourceRequestMeta } from '../../common/descriptor_types'; +import { MVTSingleLayerVectorSourceConfig } from './sources/mvt_single_layer_vector_source/mvt_single_layer_vector_source_editor'; + +export class TiledVectorLayer extends VectorLayer { + static type = LAYER_TYPE.TILED_VECTOR; + + static createDescriptor( + descriptor: VectorLayerDescriptor, + mapColors: string[] + ): VectorLayerDescriptor { + const layerDescriptor = super.createDescriptor(descriptor, mapColors); + layerDescriptor.type = TiledVectorLayer.type; + + if (!layerDescriptor.style) { + const styleProperties = VectorStyle.createDefaultStyleProperties(mapColors); + layerDescriptor.style = VectorStyle.createDescriptor(styleProperties); + } + + return layerDescriptor; + } + + readonly _source: ITiledSingleLayerVectorSource; // downcast to the more specific type + + constructor({ layerDescriptor, source }: VectorLayerArguments) { + super({ layerDescriptor, source }); + this._source = source as ITiledSingleLayerVectorSource; + } + + getCustomIconAndTooltipContent() { + return { + icon: <EuiIcon size="m" type={this.getLayerTypeIconName()} />, + }; + } + + async _syncMVTUrlTemplate({ startLoading, stopLoading, onLoadError, dataFilters }: SyncContext) { + const requestToken: symbol = Symbol(`layer-${this.getId()}-${SOURCE_DATA_ID_ORIGIN}`); + const searchFilters: VectorSourceRequestMeta = this._getSearchFilters( + dataFilters, + this.getSource(), + this._style + ); + const prevDataRequest = this.getSourceDataRequest(); + + const canSkip = await canSkipSourceUpdate({ + source: this._source as ISource, + prevDataRequest, + nextMeta: searchFilters, + }); + if (canSkip) { + return null; + } + + startLoading(SOURCE_DATA_ID_ORIGIN, requestToken, searchFilters); + try { + const templateWithMeta = await this._source.getUrlTemplateWithMeta(); + stopLoading(SOURCE_DATA_ID_ORIGIN, requestToken, templateWithMeta, {}); + } catch (error) { + onLoadError(SOURCE_DATA_ID_ORIGIN, requestToken, error.message); + } + } + + async syncData(syncContext: SyncContext) { + if (!this.isVisible() || !this.showAtZoomLevel(syncContext.dataFilters.zoom)) { + return; + } + + await this._syncSourceStyleMeta(syncContext, this._source, this._style); + await this._syncSourceFormatters(syncContext, this._source, this._style); + await this._syncMVTUrlTemplate(syncContext); + } + + _syncSourceBindingWithMb(mbMap: unknown) { + // @ts-ignore + const mbSource = mbMap.getSource(this.getId()); + if (!mbSource) { + const sourceDataRequest = this.getSourceDataRequest(); + if (!sourceDataRequest) { + // this is possible if the layer was invisible at startup. + // the actions will not perform any data=syncing as an optimization when a layer is invisible + // when turning the layer back into visible, it's possible the url has not been resovled yet. + return; + } + + const sourceMeta: MVTSingleLayerVectorSourceConfig | null = sourceDataRequest.getData() as MVTSingleLayerVectorSourceConfig; + if (!sourceMeta) { + return; + } + + const sourceId = this.getId(); + + // @ts-ignore + mbMap.addSource(sourceId, { + type: 'vector', + tiles: [sourceMeta.urlTemplate], + minzoom: sourceMeta.minSourceZoom, + maxzoom: sourceMeta.maxSourceZoom, + }); + } + } + + _syncStylePropertiesWithMb(mbMap: unknown) { + // @ts-ignore + const mbSource = mbMap.getSource(this.getId()); + if (!mbSource) { + return; + } + + const sourceDataRequest = this.getSourceDataRequest(); + if (!sourceDataRequest) { + return; + } + const sourceMeta: MVTSingleLayerVectorSourceConfig = sourceDataRequest.getData() as MVTSingleLayerVectorSourceConfig; + + this._setMbPointsProperties(mbMap, sourceMeta.layerName); + this._setMbLinePolygonProperties(mbMap, sourceMeta.layerName); + } + + _requiresPrevSourceCleanup(mbMap: unknown): boolean { + // @ts-ignore + const mbTileSource = mbMap.getSource(this.getId()); + if (!mbTileSource) { + return false; + } + const dataRequest = this.getSourceDataRequest(); + if (!dataRequest) { + return false; + } + const tiledSourceMeta: MVTSingleLayerVectorSourceConfig | null = dataRequest.getData() as MVTSingleLayerVectorSourceConfig; + if ( + mbTileSource.tiles[0] === tiledSourceMeta.urlTemplate && + mbTileSource.minzoom === tiledSourceMeta.minSourceZoom && + mbTileSource.maxzoom === tiledSourceMeta.maxSourceZoom + ) { + // TileURL and zoom-range captures all the state. If this does not change, no updates are required. + return false; + } + + return true; + } + + syncLayerWithMB(mbMap: unknown) { + this._removeStaleMbSourcesAndLayers(mbMap); + this._syncSourceBindingWithMb(mbMap); + this._syncStylePropertiesWithMb(mbMap); + } + + getJoins() { + return []; + } + + getMinZoom() { + // higher resolution vector tiles cannot be displayed at lower-res + return Math.max(this._source.getMinZoom(), super.getMinZoom()); + } +} diff --git a/x-pack/plugins/maps/public/layers/util/data_request.ts b/x-pack/plugins/maps/public/layers/util/data_request.ts index eeef5c49c6ef8..44b7b2ffb6ae7 100644 --- a/x-pack/plugins/maps/public/layers/util/data_request.ts +++ b/x-pack/plugins/maps/public/layers/util/data_request.ts @@ -26,9 +26,13 @@ export class DataRequest { } getMeta(): DataMeta { - return this.hasData() - ? _.get(this._descriptor, 'dataMeta', {}) - : _.get(this._descriptor, 'dataMetaAtStart', {}); + if (this._descriptor.dataMetaAtStart) { + return this._descriptor.dataMetaAtStart; + } else if (this._descriptor.dataMeta) { + return this._descriptor.dataMeta; + } else { + return {}; + } } hasData(): boolean { diff --git a/x-pack/plugins/maps/public/layers/vector_layer.d.ts b/x-pack/plugins/maps/public/layers/vector_layer.d.ts index 70fd9927b7732..3d5b8054ff3fd 100644 --- a/x-pack/plugins/maps/public/layers/vector_layer.d.ts +++ b/x-pack/plugins/maps/public/layers/vector_layer.d.ts @@ -9,6 +9,7 @@ import { AbstractLayer } from './layer'; import { IVectorSource } from './sources/vector_source'; import { MapFilters, + LayerDescriptor, VectorLayerDescriptor, VectorSourceRequestMeta, } from '../../common/descriptor_types'; @@ -20,7 +21,7 @@ import { SyncContext } from '../actions/map_actions'; type VectorLayerArguments = { source: IVectorSource; - joins: IJoin[]; + joins?: IJoin[]; layerDescriptor: VectorLayerDescriptor; }; @@ -28,26 +29,43 @@ export interface IVectorLayer extends ILayer { getFields(): Promise<IField[]>; getStyleEditorFields(): Promise<IField[]>; getValidJoins(): IJoin[]; + getSource(): IVectorSource; } export class VectorLayer extends AbstractLayer implements IVectorLayer { static createDescriptor( - options: VectorLayerArguments, - mapColors: string[] + options: Partial<LayerDescriptor>, + mapColors?: string[] ): VectorLayerDescriptor; protected readonly _source: IVectorSource; protected readonly _style: IVectorStyle; constructor(options: VectorLayerArguments); - + getLayerTypeIconName(): string; getFields(): Promise<IField[]>; getStyleEditorFields(): Promise<IField[]>; getValidJoins(): IJoin[]; + _syncSourceStyleMeta( + syncContext: SyncContext, + source: IVectorSource, + style: IVectorStyle + ): Promise<void>; + _syncSourceFormatters( + syncContext: SyncContext, + source: IVectorSource, + style: IVectorStyle + ): Promise<void>; + syncLayerWithMB(mbMap: unknown): void; _getSearchFilters( dataFilters: MapFilters, source: IVectorSource, style: IVectorStyle ): VectorSourceRequestMeta; _syncData(syncContext: SyncContext, source: IVectorSource, style: IVectorStyle): Promise<void>; + ownsMbSourceId(sourceId: string): boolean; + ownsMbLayerId(sourceId: string): boolean; + _setMbPointsProperties(mbMap: unknown, mvtSourceLayer?: string): void; + _setMbLinePolygonProperties(mbMap: unknown, mvtSourceLayer?: string): void; + getSource(): IVectorSource; } diff --git a/x-pack/plugins/maps/public/layers/vector_layer.js b/x-pack/plugins/maps/public/layers/vector_layer.js index d606420909281..c5947a63587ea 100644 --- a/x-pack/plugins/maps/public/layers/vector_layer.js +++ b/x-pack/plugins/maps/public/layers/vector_layer.js @@ -641,7 +641,7 @@ export class VectorLayer extends AbstractLayer { } } - _setMbPointsProperties(mbMap) { + _setMbPointsProperties(mbMap, mvtSourceLayer) { const pointLayerId = this._getMbPointLayerId(); const symbolLayerId = this._getMbSymbolLayerId(); const pointLayer = mbMap.getLayer(pointLayerId); @@ -658,7 +658,7 @@ export class VectorLayer extends AbstractLayer { if (symbolLayer) { mbMap.setLayoutProperty(symbolLayerId, 'visibility', 'none'); } - this._setMbCircleProperties(mbMap); + this._setMbCircleProperties(mbMap, mvtSourceLayer); } else { markerLayerId = symbolLayerId; textLayerId = symbolLayerId; @@ -666,7 +666,7 @@ export class VectorLayer extends AbstractLayer { mbMap.setLayoutProperty(pointLayerId, 'visibility', 'none'); mbMap.setLayoutProperty(this._getMbTextLayerId(), 'visibility', 'none'); } - this._setMbSymbolProperties(mbMap); + this._setMbSymbolProperties(mbMap, mvtSourceLayer); } this.syncVisibilityWithMb(mbMap, markerLayerId); @@ -677,27 +677,36 @@ export class VectorLayer extends AbstractLayer { } } - _setMbCircleProperties(mbMap) { + _setMbCircleProperties(mbMap, mvtSourceLayer) { const sourceId = this.getId(); const pointLayerId = this._getMbPointLayerId(); const pointLayer = mbMap.getLayer(pointLayerId); if (!pointLayer) { - mbMap.addLayer({ + const mbLayer = { id: pointLayerId, type: 'circle', source: sourceId, paint: {}, - }); + }; + + if (mvtSourceLayer) { + mbLayer['source-layer'] = mvtSourceLayer; + } + mbMap.addLayer(mbLayer); } const textLayerId = this._getMbTextLayerId(); const textLayer = mbMap.getLayer(textLayerId); if (!textLayer) { - mbMap.addLayer({ + const mbLayer = { id: textLayerId, type: 'symbol', source: sourceId, - }); + }; + if (mvtSourceLayer) { + mbLayer['source-layer'] = mvtSourceLayer; + } + mbMap.addLayer(mbLayer); } const filterExpr = getPointFilterExpression(this._hasJoins()); @@ -719,17 +728,21 @@ export class VectorLayer extends AbstractLayer { }); } - _setMbSymbolProperties(mbMap) { + _setMbSymbolProperties(mbMap, mvtSourceLayer) { const sourceId = this.getId(); const symbolLayerId = this._getMbSymbolLayerId(); const symbolLayer = mbMap.getLayer(symbolLayerId); if (!symbolLayer) { - mbMap.addLayer({ + const mbLayer = { id: symbolLayerId, type: 'symbol', source: sourceId, - }); + }; + if (mvtSourceLayer) { + mbLayer['source-layer'] = mvtSourceLayer; + } + mbMap.addLayer(mbLayer); } const filterExpr = getPointFilterExpression(this._hasJoins()); @@ -750,26 +763,34 @@ export class VectorLayer extends AbstractLayer { }); } - _setMbLinePolygonProperties(mbMap) { + _setMbLinePolygonProperties(mbMap, mvtSourceLayer) { const sourceId = this.getId(); const fillLayerId = this._getMbPolygonLayerId(); const lineLayerId = this._getMbLineLayerId(); const hasJoins = this._hasJoins(); if (!mbMap.getLayer(fillLayerId)) { - mbMap.addLayer({ + const mbLayer = { id: fillLayerId, type: 'fill', source: sourceId, paint: {}, - }); + }; + if (mvtSourceLayer) { + mbLayer['source-layer'] = mvtSourceLayer; + } + mbMap.addLayer(mbLayer); } if (!mbMap.getLayer(lineLayerId)) { - mbMap.addLayer({ + const mbLayer = { id: lineLayerId, type: 'line', source: sourceId, paint: {}, - }); + }; + if (mvtSourceLayer) { + mbLayer['source-layer'] = mvtSourceLayer; + } + mbMap.addLayer(mbLayer); } this.getCurrentStyle().setMBPaintProperties({ alpha: this.getAlpha(), diff --git a/x-pack/plugins/maps/public/layers/vector_tile_layer.js b/x-pack/plugins/maps/public/layers/vector_tile_layer.js index 44987fd3e78f0..c620ec6c56dc3 100644 --- a/x-pack/plugins/maps/public/layers/vector_tile_layer.js +++ b/x-pack/plugins/maps/public/layers/vector_tile_layer.js @@ -161,19 +161,7 @@ export class VectorTileLayer extends TileLayer { return; } - if (this._requiresPrevSourceCleanup(mbMap)) { - const mbStyle = mbMap.getStyle(); - mbStyle.layers.forEach(mbLayer => { - if (this.ownsMbLayerId(mbLayer.id)) { - mbMap.removeLayer(mbLayer.id); - } - }); - Object.keys(mbStyle.sources).some(mbSourceId => { - if (this.ownsMbSourceId(mbSourceId)) { - mbMap.removeSource(mbSourceId); - } - }); - } + this._removeStaleMbSourcesAndLayers(mbMap); let initialBootstrapCompleted = false; const sourceIds = Object.keys(vectorStyle.sources); diff --git a/x-pack/plugins/maps/public/plugin.ts b/x-pack/plugins/maps/public/plugin.ts index 9437c2512ded4..d3b9626dc8366 100644 --- a/x-pack/plugins/maps/public/plugin.ts +++ b/x-pack/plugins/maps/public/plugin.ts @@ -10,6 +10,12 @@ import { Setup as InspectorSetupContract } from 'src/plugins/inspector/public'; import { MapView } from './inspector/views/map_view'; import { setAutocompleteService, + setCore, + setCoreChrome, + setCoreI18n, + setCoreOverlays, + setData, + setDocLinks, setFileUpload, setHttp, setIndexPatternSelect, @@ -17,11 +23,17 @@ import { setInjectedVarFunc, setInspector, setLicenseId, + setMapsCapabilities, + setNavigation, + setSavedObjectsClient, setTimeFilter, setToasts, + setUiActions, setUiSettings, + setVisualizations, // @ts-ignore } from './kibana_services'; +import { registerLayerWizards } from './layers/load_layer_wizards'; export interface MapsPluginSetupDependencies { inspector: InspectorSetupContract; @@ -31,25 +43,37 @@ export interface MapsPluginStartDependencies {} export const bindSetupCoreAndPlugins = (core: CoreSetup, plugins: any) => { const { licensing } = plugins; - const { injectedMetadata, http } = core; + const { injectedMetadata, uiSettings, http, notifications } = core; if (licensing) { licensing.license$.subscribe(({ uid }: { uid: string }) => setLicenseId(uid)); } setInjectedVarFunc(injectedMetadata.getInjectedVar); setHttp(http); - setUiSettings(core.uiSettings); - setInjectedVarFunc(core.injectedMetadata.getInjectedVar); - setToasts(core.notifications.toasts); + setToasts(notifications.toasts); + setInjectedVarFunc(injectedMetadata.getInjectedVar); + setVisualizations(plugins.visualizations); + setUiSettings(uiSettings); }; export const bindStartCoreAndPlugins = (core: CoreStart, plugins: any) => { - const { file_upload, data, inspector } = plugins; + const { fileUpload, data, inspector } = plugins; setInspector(inspector); - setFileUpload(file_upload); + setFileUpload(fileUpload); setIndexPatternSelect(data.ui.IndexPatternSelect); setTimeFilter(data.query.timefilter.timefilter); setIndexPatternService(data.indexPatterns); setAutocompleteService(data.autocomplete); + setCore(core); + setSavedObjectsClient(core.savedObjects.client); + setCoreChrome(core.chrome); + setCoreOverlays(core.overlays); + setMapsCapabilities(core.application.capabilities.maps); + setDocLinks(core.docLinks); + setData(plugins.data); + setUiActions(plugins.uiActions); + setNavigation(plugins.navigation); + setCoreI18n(core.i18n); + registerLayerWizards(); }; /** diff --git a/x-pack/plugins/maps/public/reducers/map.js b/x-pack/plugins/maps/public/reducers/map.js index 1e20df89c8fad..251a2304538ed 100644 --- a/x-pack/plugins/maps/public/reducers/map.js +++ b/x-pack/plugins/maps/public/reducers/map.js @@ -57,8 +57,13 @@ const updateLayerInList = (state, layerId, attribute, newValue) => { if (!layerId) { return state; } + const { layerList } = state; const layerIdx = getLayerIndex(layerList, layerId); + if (layerIdx === -1) { + return state; + } + const updatedLayer = { ...layerList[layerIdx], // Update layer w/ new value. If no value provided, toggle boolean value @@ -74,7 +79,7 @@ const updateLayerInList = (state, layerId, attribute, newValue) => { return { ...state, layerList: updatedList }; }; -const updateLayerSourceDescriptorProp = (state, layerId, propName, value, newLayerType) => { +const updateLayerSourceDescriptorProp = (state, layerId, propName, value) => { const { layerList } = state; const layerIdx = getLayerIndex(layerList, layerId); const updatedLayer = { @@ -84,9 +89,6 @@ const updateLayerSourceDescriptorProp = (state, layerId, propName, value, newLay [propName]: value, }, }; - if (newLayerType) { - updatedLayer.type = newLayerType; - } const updatedList = [ ...layerList.slice(0, layerIdx), updatedLayer, @@ -261,13 +263,7 @@ export function map(state = INITIAL_STATE, action) { case UPDATE_LAYER_PROP: return updateLayerInList(state, action.id, action.propName, action.newValue); case UPDATE_SOURCE_PROP: - return updateLayerSourceDescriptorProp( - state, - action.layerId, - action.propName, - action.value, - action.newLayerType - ); + return updateLayerSourceDescriptorProp(state, action.layerId, action.propName, action.value); case SET_JOINS: const layerDescriptor = state.layerList.find( descriptor => descriptor.id === action.layer.getId() diff --git a/x-pack/legacy/plugins/tilemap/README.md b/x-pack/plugins/maps_legacy_licensing/README.md similarity index 100% rename from x-pack/legacy/plugins/tilemap/README.md rename to x-pack/plugins/maps_legacy_licensing/README.md diff --git a/x-pack/plugins/maps_legacy_licensing/kibana.json b/x-pack/plugins/maps_legacy_licensing/kibana.json new file mode 100644 index 0000000000000..e98c33d21ec40 --- /dev/null +++ b/x-pack/plugins/maps_legacy_licensing/kibana.json @@ -0,0 +1,8 @@ +{ + "id": "mapsLegacyLicensing", + "version": "8.0.0", + "kibanaVersion": "kibana", + "server": false, + "ui": true, + "requiredPlugins": ["licensing", "mapsLegacy"] +} diff --git a/x-pack/plugins/maps_legacy_licensing/public/index.ts b/x-pack/plugins/maps_legacy_licensing/public/index.ts new file mode 100644 index 0000000000000..edda6118f92dd --- /dev/null +++ b/x-pack/plugins/maps_legacy_licensing/public/index.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 { MapsLegacyLicensing } from './plugin'; + +export function plugin() { + return new MapsLegacyLicensing(); +} diff --git a/x-pack/plugins/maps_legacy_licensing/public/plugin.ts b/x-pack/plugins/maps_legacy_licensing/public/plugin.ts new file mode 100644 index 0000000000000..69c25efd96e75 --- /dev/null +++ b/x-pack/plugins/maps_legacy_licensing/public/plugin.ts @@ -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 { CoreSetup, CoreStart, Plugin } from 'kibana/public'; +import { LicensingPluginSetup, ILicense } from '../../licensing/public'; + +/** + * These are the interfaces with your public contracts. You should export these + * for other plugins to use in _their_ `SetupDeps`/`StartDeps` interfaces. + * @public + */ + +// eslint-disable-next-line @typescript-eslint/no-empty-interface +export interface MapsLegacyLicensingSetupDependencies { + licensing: LicensingPluginSetup; + mapsLegacy: any; +} +// eslint-disable-next-line @typescript-eslint/no-empty-interface +export interface MapsLegacyLicensingStartDependencies {} + +export type MapsLegacyLicensingSetup = ReturnType<MapsLegacyLicensing['setup']>; +export type MapsLegacyLicensingStart = ReturnType<MapsLegacyLicensing['start']>; + +export class MapsLegacyLicensing + implements Plugin<MapsLegacyLicensingSetup, MapsLegacyLicensingStart> { + public setup(core: CoreSetup, plugins: MapsLegacyLicensingSetupDependencies) { + const { + licensing, + mapsLegacy: { serviceSettings }, + } = plugins; + if (licensing) { + licensing.license$.subscribe((license: ILicense) => { + const { uid, isActive } = license; + if (isActive && license.hasAtLeast('basic')) { + serviceSettings.setQueryParams({ license: uid }); + serviceSettings.disableZoomMessage(); + } else { + serviceSettings.setQueryParams({ license: undefined }); + serviceSettings.enableZoomMessage(); + } + }); + } + } + + public start(core: CoreStart, plugins: MapsLegacyLicensingStartDependencies) {} +} diff --git a/x-pack/plugins/ml/common/constants/file_datavisualizer.ts b/x-pack/plugins/ml/common/constants/file_datavisualizer.ts index 81d51bfa25816..7e18b36fd7bab 100644 --- a/x-pack/plugins/ml/common/constants/file_datavisualizer.ts +++ b/x-pack/plugins/ml/common/constants/file_datavisualizer.ts @@ -4,8 +4,10 @@ * you may not use this file except in compliance with the Elastic License. */ -export const MAX_BYTES = 104857600; -export const ABSOLUTE_MAX_BYTES = MAX_BYTES * 5; +export const MAX_FILE_SIZE = '100MB'; +export const MAX_FILE_SIZE_BYTES = 104857600; // 100MB + +export const ABSOLUTE_MAX_FILE_SIZE_BYTES = 1073741274; // 1GB export const FILE_SIZE_DISPLAY_FORMAT = '0,0.[0] b'; // Value to use in the Elasticsearch index mapping meta data to identify the diff --git a/x-pack/plugins/ml/common/types/file_datavisualizer.ts b/x-pack/plugins/ml/common/types/file_datavisualizer.ts index f771547b97811..c997a4e24f868 100644 --- a/x-pack/plugins/ml/common/types/file_datavisualizer.ts +++ b/x-pack/plugins/ml/common/types/file_datavisualizer.ts @@ -67,13 +67,15 @@ export interface ImportResponse { export interface ImportFailure { item: number; reason: string; - doc: Doc; + doc: ImportDoc; } export interface Doc { message: string; } +export type ImportDoc = Doc | string; + export interface Settings { pipeline?: string; index: string; diff --git a/x-pack/plugins/ml/common/types/ml_config.ts b/x-pack/plugins/ml/common/types/ml_config.ts index 8fd9fd22bad8a..f2ddadccb2170 100644 --- a/x-pack/plugins/ml/common/types/ml_config.ts +++ b/x-pack/plugins/ml/common/types/ml_config.ts @@ -5,11 +5,11 @@ */ import { schema, TypeOf } from '@kbn/config-schema'; -import { MAX_BYTES } from '../constants/file_datavisualizer'; +import { MAX_FILE_SIZE } from '../constants/file_datavisualizer'; export const configSchema = schema.object({ file_data_visualizer: schema.object({ - max_file_size_bytes: schema.number({ defaultValue: MAX_BYTES }), + max_file_size: schema.string({ defaultValue: MAX_FILE_SIZE }), }), }); diff --git a/x-pack/plugins/ml/common/util/__tests__/anomaly_utils.js b/x-pack/plugins/ml/common/util/__tests__/anomaly_utils.js deleted file mode 100644 index 515304d222c8c..0000000000000 --- a/x-pack/plugins/ml/common/util/__tests__/anomaly_utils.js +++ /dev/null @@ -1,443 +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 expect from '@kbn/expect'; -import { - getSeverity, - getSeverityWithLow, - getSeverityColor, - getMultiBucketImpactLabel, - getEntityFieldName, - getEntityFieldValue, - getEntityFieldList, - showActualForFunction, - showTypicalForFunction, - isRuleSupported, - aggregationTypeTransform, -} from '../anomaly_utils'; - -describe('ML - anomaly utils', () => { - const partitionEntityRecord = { - job_id: 'farequote', - result_type: 'record', - probability: 0.012818, - record_score: 0.0162059, - bucket_span: 300, - detector_index: 0, - timestamp: 1455047400000, - partition_field_name: 'airline', - partition_field_value: 'AAL', - function: 'mean', - function_description: 'mean', - field_name: 'responsetime', - }; - - const byEntityRecord = { - job_id: 'farequote', - result_type: 'record', - probability: 0.012818, - record_score: 0.0162059, - bucket_span: 300, - detector_index: 0, - timestamp: 1455047400000, - by_field_name: 'airline', - by_field_value: 'JZA', - function: 'mean', - function_description: 'mean', - field_name: 'responsetime', - }; - - const overEntityRecord = { - job_id: 'gallery', - result_type: 'record', - probability: 2.81806e-9, - record_score: 59.055, - bucket_span: 3600, - detector_index: 4, - timestamp: 1420552800000, - function: 'sum', - function_description: 'sum', - field_name: 'bytes', - by_field_name: 'method', - over_field_name: 'clientip', - over_field_value: '37.157.32.164', - }; - - const noEntityRecord = { - job_id: 'farequote_no_by', - result_type: 'record', - probability: 0.0191711, - record_score: 4.38431, - initial_record_score: 19.654, - bucket_span: 300, - detector_index: 0, - timestamp: 1454890500000, - function: 'mean', - function_description: 'mean', - field_name: 'responsetime', - }; - - const metricNoEntityRecord = { - job_id: 'farequote_metric', - result_type: 'record', - probability: 0.030133495093182184, - record_score: 0.024881740359975164, - initial_record_score: 0.024881740359975164, - bucket_span: 900, - detector_index: 0, - is_interim: false, - timestamp: 1486845000000, - function: 'metric', - function_description: 'mean', - typical: [545.7764658569108], - actual: [758.8220213274412], - field_name: 'responsetime', - influencers: [ - { - influencer_field_name: 'airline', - influencer_field_values: ['NKS'], - }, - ], - airline: ['NKS'], - }; - - const rareEntityRecord = { - job_id: 'gallery', - result_type: 'record', - probability: 0.02277014211908481, - record_score: 4.545378107075983, - initial_record_score: 4.545378107075983, - bucket_span: 3600, - detector_index: 0, - is_interim: false, - timestamp: 1495879200000, - by_field_name: 'status', - function: 'rare', - function_description: 'rare', - over_field_name: 'clientip', - over_field_value: '173.252.74.112', - causes: [ - { - probability: 0.02277014211908481, - by_field_name: 'status', - by_field_value: '206', - function: 'rare', - function_description: 'rare', - typical: [0.00014832458182211878], - actual: [1], - over_field_name: 'clientip', - over_field_value: '173.252.74.112', - }, - ], - influencers: [ - { - influencer_field_name: 'uri', - influencer_field_values: [ - '/wp-content/uploads/2013/06/dune_house_oil_on_canvas_24x20-298x298.jpg', - '/wp-content/uploads/2013/10/Case-dAste-1-11-298x298.png', - ], - }, - { - influencer_field_name: 'status', - influencer_field_values: ['206'], - }, - { - influencer_field_name: 'clientip', - influencer_field_values: ['173.252.74.112'], - }, - ], - clientip: ['173.252.74.112'], - uri: [ - '/wp-content/uploads/2013/06/dune_house_oil_on_canvas_24x20-298x298.jpg', - '/wp-content/uploads/2013/10/Case-dAste-1-11-298x298.png', - ], - status: ['206'], - }; - - describe('getSeverity', () => { - it('returns warning for 0 <= score < 25', () => { - expect(getSeverity(0).id).to.be('warning'); - expect(getSeverity(0.001).id).to.be('warning'); - expect(getSeverity(24.99).id).to.be('warning'); - }); - - it('returns minor for 25 <= score < 50', () => { - expect(getSeverity(25).id).to.be('minor'); - expect(getSeverity(49.99).id).to.be('minor'); - }); - - it('returns minor for 50 <= score < 75', () => { - expect(getSeverity(50).id).to.be('major'); - expect(getSeverity(74.99).id).to.be('major'); - }); - - it('returns critical for score >= 75', () => { - expect(getSeverity(75).id).to.be('critical'); - expect(getSeverity(100).id).to.be('critical'); - expect(getSeverity(1000).id).to.be('critical'); - }); - - it('returns unknown for scores less than 0 or string input', () => { - expect(getSeverity(-10).id).to.be('unknown'); - expect(getSeverity('value').id).to.be('unknown'); - }); - }); - - describe('getSeverityWithLow', () => { - it('returns low for 0 <= score < 3', () => { - expect(getSeverityWithLow(0).id).to.be('low'); - expect(getSeverityWithLow(0.001).id).to.be('low'); - expect(getSeverityWithLow(2.99).id).to.be('low'); - }); - - it('returns warning for 3 <= score < 25', () => { - expect(getSeverityWithLow(3).id).to.be('warning'); - expect(getSeverityWithLow(24.99).id).to.be('warning'); - }); - - it('returns minor for 25 <= score < 50', () => { - expect(getSeverityWithLow(25).id).to.be('minor'); - expect(getSeverityWithLow(49.99).id).to.be('minor'); - }); - - it('returns minor for 50 <= score < 75', () => { - expect(getSeverityWithLow(50).id).to.be('major'); - expect(getSeverityWithLow(74.99).id).to.be('major'); - }); - - it('returns critical for score >= 75', () => { - expect(getSeverityWithLow(75).id).to.be('critical'); - expect(getSeverityWithLow(100).id).to.be('critical'); - expect(getSeverityWithLow(1000).id).to.be('critical'); - }); - - it('returns unknown for scores less than 0 or string input', () => { - expect(getSeverityWithLow(-10).id).to.be('unknown'); - expect(getSeverityWithLow('value').id).to.be('unknown'); - }); - }); - - describe('getSeverityColor', () => { - it('returns correct hex code for low for 0 <= score < 3', () => { - expect(getSeverityColor(0)).to.be('#d2e9f7'); - expect(getSeverityColor(0.001)).to.be('#d2e9f7'); - expect(getSeverityColor(2.99)).to.be('#d2e9f7'); - }); - - it('returns correct hex code for warning for 3 <= score < 25', () => { - expect(getSeverityColor(3)).to.be('#8bc8fb'); - expect(getSeverityColor(24.99)).to.be('#8bc8fb'); - }); - - it('returns correct hex code for minor for 25 <= score < 50', () => { - expect(getSeverityColor(25)).to.be('#fdec25'); - expect(getSeverityColor(49.99)).to.be('#fdec25'); - }); - - it('returns correct hex code for major for 50 <= score < 75', () => { - expect(getSeverityColor(50)).to.be('#fba740'); - expect(getSeverityColor(74.99)).to.be('#fba740'); - }); - - it('returns correct hex code for critical for score >= 75', () => { - expect(getSeverityColor(75)).to.be('#fe5050'); - expect(getSeverityColor(100)).to.be('#fe5050'); - expect(getSeverityColor(1000)).to.be('#fe5050'); - }); - - it('returns correct hex code for unknown for scores less than 0 or string input', () => { - expect(getSeverityColor(-10)).to.be('#ffffff'); - expect(getSeverityColor('value')).to.be('#ffffff'); - }); - }); - - describe('getMultiBucketImpactLabel', () => { - it('returns high for 3 <= score <= 5', () => { - expect(getMultiBucketImpactLabel(3)).to.be('high'); - expect(getMultiBucketImpactLabel(5)).to.be('high'); - }); - - it('returns medium for 2 <= score < 3', () => { - expect(getMultiBucketImpactLabel(2)).to.be('medium'); - expect(getMultiBucketImpactLabel(2.99)).to.be('medium'); - }); - - it('returns low for 1 <= score < 2', () => { - expect(getMultiBucketImpactLabel(1)).to.be('low'); - expect(getMultiBucketImpactLabel(1.99)).to.be('low'); - }); - - it('returns none for -5 <= score < 1', () => { - expect(getMultiBucketImpactLabel(-5)).to.be('none'); - expect(getMultiBucketImpactLabel(0.99)).to.be('none'); - }); - - it('returns expected label when impact outside normal bounds', () => { - expect(getMultiBucketImpactLabel(10)).to.be('high'); - expect(getMultiBucketImpactLabel(-10)).to.be('none'); - }); - }); - - describe('getEntityFieldName', () => { - it('returns the by field name', () => { - expect(getEntityFieldName(byEntityRecord)).to.be('airline'); - }); - - it('returns the partition field name', () => { - expect(getEntityFieldName(partitionEntityRecord)).to.be('airline'); - }); - - it('returns the over field name', () => { - expect(getEntityFieldName(overEntityRecord)).to.be('clientip'); - }); - - it('returns undefined if no by, over or partition fields', () => { - expect(getEntityFieldName(noEntityRecord)).to.be(undefined); - }); - }); - - describe('getEntityFieldValue', () => { - it('returns the by field value', () => { - expect(getEntityFieldValue(byEntityRecord)).to.be('JZA'); - }); - - it('returns the partition field value', () => { - expect(getEntityFieldValue(partitionEntityRecord)).to.be('AAL'); - }); - - it('returns the over field value', () => { - expect(getEntityFieldValue(overEntityRecord)).to.be('37.157.32.164'); - }); - - it('returns undefined if no by, over or partition fields', () => { - expect(getEntityFieldValue(noEntityRecord)).to.be(undefined); - }); - }); - - describe('getEntityFieldList', () => { - it('returns an empty list for a record with no by, over or partition fields', () => { - expect(getEntityFieldList(noEntityRecord)).to.be.empty(); - }); - - it('returns correct list for a record with a by field', () => { - expect(getEntityFieldList(byEntityRecord)).to.eql([ - { - fieldName: 'airline', - fieldValue: 'JZA', - fieldType: 'by', - }, - ]); - }); - - it('returns correct list for a record with a partition field', () => { - expect(getEntityFieldList(partitionEntityRecord)).to.eql([ - { - fieldName: 'airline', - fieldValue: 'AAL', - fieldType: 'partition', - }, - ]); - }); - - it('returns correct list for a record with an over field', () => { - expect(getEntityFieldList(overEntityRecord)).to.eql([ - { - fieldName: 'clientip', - fieldValue: '37.157.32.164', - fieldType: 'over', - }, - ]); - }); - - it('returns correct list for a record with a by and over field', () => { - expect(getEntityFieldList(rareEntityRecord)).to.eql([ - { - fieldName: 'clientip', - fieldValue: '173.252.74.112', - fieldType: 'over', - }, - ]); - }); - }); - - describe('showActualForFunction', () => { - it('returns true for expected function descriptions', () => { - expect(showActualForFunction('count')).to.be(true); - expect(showActualForFunction('distinct_count')).to.be(true); - expect(showActualForFunction('lat_long')).to.be(true); - expect(showActualForFunction('mean')).to.be(true); - expect(showActualForFunction('max')).to.be(true); - expect(showActualForFunction('min')).to.be(true); - expect(showActualForFunction('sum')).to.be(true); - expect(showActualForFunction('median')).to.be(true); - expect(showActualForFunction('varp')).to.be(true); - expect(showActualForFunction('info_content')).to.be(true); - expect(showActualForFunction('time')).to.be(true); - }); - - it('returns false for expected function descriptions', () => { - expect(showActualForFunction('rare')).to.be(false); - }); - }); - - describe('showTypicalForFunction', () => { - it('returns true for expected function descriptions', () => { - expect(showTypicalForFunction('count')).to.be(true); - expect(showTypicalForFunction('distinct_count')).to.be(true); - expect(showTypicalForFunction('lat_long')).to.be(true); - expect(showTypicalForFunction('mean')).to.be(true); - expect(showTypicalForFunction('max')).to.be(true); - expect(showTypicalForFunction('min')).to.be(true); - expect(showTypicalForFunction('sum')).to.be(true); - expect(showTypicalForFunction('median')).to.be(true); - expect(showTypicalForFunction('varp')).to.be(true); - expect(showTypicalForFunction('info_content')).to.be(true); - expect(showTypicalForFunction('time')).to.be(true); - }); - - it('returns false for expected function descriptions', () => { - expect(showTypicalForFunction('rare')).to.be(false); - }); - }); - - describe('isRuleSupported', () => { - it('returns true for anomalies supporting rules', () => { - expect(isRuleSupported(partitionEntityRecord)).to.be(true); - expect(isRuleSupported(byEntityRecord)).to.be(true); - expect(isRuleSupported(overEntityRecord)).to.be(true); - expect(isRuleSupported(rareEntityRecord)).to.be(true); - expect(isRuleSupported(noEntityRecord)).to.be(true); - }); - - it('returns false for anomaly not supporting rules', () => { - expect(isRuleSupported(metricNoEntityRecord)).to.be(false); - }); - }); - - describe('aggregationTypeTransform', () => { - it('returns correct ES aggregation type for ML function description', () => { - expect(aggregationTypeTransform.toES('count')).to.be('count'); - expect(aggregationTypeTransform.toES('distinct_count')).to.be('cardinality'); - expect(aggregationTypeTransform.toES('distinct_count')).to.not.be('distinct_count'); - expect(aggregationTypeTransform.toES('mean')).to.be('avg'); - expect(aggregationTypeTransform.toES('mean')).to.not.be('mean'); - expect(aggregationTypeTransform.toES('max')).to.be('max'); - expect(aggregationTypeTransform.toES('min')).to.be('min'); - expect(aggregationTypeTransform.toES('sum')).to.be('sum'); - }); - - it('returns correct ML function description for ES aggregation type', () => { - expect(aggregationTypeTransform.toML('count')).to.be('count'); - expect(aggregationTypeTransform.toML('cardinality')).to.be('distinct_count'); - expect(aggregationTypeTransform.toML('cardinality')).to.not.be('cardinality'); - expect(aggregationTypeTransform.toML('avg')).to.be('mean'); - expect(aggregationTypeTransform.toML('avg')).to.not.be('avg'); - expect(aggregationTypeTransform.toML('max')).to.be('max'); - expect(aggregationTypeTransform.toML('min')).to.be('min'); - expect(aggregationTypeTransform.toML('sum')).to.be('sum'); - }); - }); -}); diff --git a/x-pack/plugins/ml/common/util/__tests__/job_utils.js b/x-pack/plugins/ml/common/util/__tests__/job_utils.js deleted file mode 100644 index 60270fd438846..0000000000000 --- a/x-pack/plugins/ml/common/util/__tests__/job_utils.js +++ /dev/null @@ -1,590 +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 expect from '@kbn/expect'; -import { - calculateDatafeedFrequencyDefaultSeconds, - isTimeSeriesViewJob, - isTimeSeriesViewDetector, - isSourceDataChartableForDetector, - isModelPlotChartableForDetector, - getPartitioningFieldNames, - isModelPlotEnabled, - isJobVersionGte, - mlFunctionToESAggregation, - isJobIdValid, - ML_MEDIAN_PERCENTS, - prefixDatafeedId, - getSafeAggregationName, - getLatestDataOrBucketTimestamp, -} from '../job_utils'; - -describe('ML - job utils', () => { - describe('calculateDatafeedFrequencyDefaultSeconds', () => { - it('returns correct frequency for 119', () => { - const result = calculateDatafeedFrequencyDefaultSeconds(119); - expect(result).to.be(60); - }); - it('returns correct frequency for 120', () => { - const result = calculateDatafeedFrequencyDefaultSeconds(120); - expect(result).to.be(60); - }); - it('returns correct frequency for 300', () => { - const result = calculateDatafeedFrequencyDefaultSeconds(300); - expect(result).to.be(150); - }); - it('returns correct frequency for 601', () => { - const result = calculateDatafeedFrequencyDefaultSeconds(601); - expect(result).to.be(300); - }); - it('returns correct frequency for 43200', () => { - const result = calculateDatafeedFrequencyDefaultSeconds(43200); - expect(result).to.be(600); - }); - it('returns correct frequency for 43201', () => { - const result = calculateDatafeedFrequencyDefaultSeconds(43201); - expect(result).to.be(3600); - }); - }); - - describe('isTimeSeriesViewJob', () => { - it('returns true when job has a single detector with a metric function', () => { - const job = { - analysis_config: { - detectors: [ - { - function: 'high_count', - partition_field_name: 'status', - detector_description: 'High count status code', - }, - ], - }, - }; - - expect(isTimeSeriesViewJob(job)).to.be(true); - }); - - it('returns true when job has at least one detector with a metric function', () => { - const job = { - analysis_config: { - detectors: [ - { - function: 'high_count', - partition_field_name: 'status', - detector_description: 'High count status code', - }, - { - function: 'freq_rare', - by_field_name: 'uri', - over_field_name: 'clientip', - detector_description: 'Freq rare URI', - }, - ], - }, - }; - - expect(isTimeSeriesViewJob(job)).to.be(true); - }); - - it('returns false when job does not have at least one detector with a metric function', () => { - const job = { - analysis_config: { - detectors: [ - { - function: 'varp', - by_field_name: 'responsetime', - detector_description: 'Varp responsetime', - }, - { - function: 'freq_rare', - by_field_name: 'uri', - over_field_name: 'clientip', - detector_description: 'Freq rare URI', - }, - ], - }, - }; - - expect(isTimeSeriesViewJob(job)).to.be(false); - }); - - it('returns false when job has a single count by category detector', () => { - const job = { - analysis_config: { - detectors: [ - { - function: 'count', - by_field_name: 'mlcategory', - detector_description: 'Count by category', - }, - ], - }, - }; - - expect(isTimeSeriesViewJob(job)).to.be(false); - }); - }); - - describe('isTimeSeriesViewDetector', () => { - const job = { - analysis_config: { - detectors: [ - { - function: 'sum', - field_name: 'bytes', - partition_field_name: 'clientip', - detector_description: 'High bytes client IP', - }, - { - function: 'freq_rare', - by_field_name: 'uri', - over_field_name: 'clientip', - detector_description: 'Freq rare URI', - }, - { - function: 'count', - by_field_name: 'mlcategory', - detector_description: 'Count by category', - }, - { function: 'count', by_field_name: 'hrd', detector_description: 'count by hrd' }, - { function: 'mean', field_name: 'NetworkDiff', detector_description: 'avg NetworkDiff' }, - ], - }, - datafeed_config: { - script_fields: { - hrd: { - script: { - inline: 'return domainSplit(doc["query"].value, params).get(1);', - lang: 'painless', - }, - }, - NetworkDiff: { - script: { - source: 'doc["NetworkOut"].value - doc["NetworkIn"].value', - lang: 'painless', - }, - }, - }, - }, - }; - - it('returns true for a detector with a metric function', () => { - expect(isTimeSeriesViewDetector(job, 0)).to.be(true); - }); - - it('returns false for a detector with a non-metric function', () => { - expect(isTimeSeriesViewDetector(job, 1)).to.be(false); - }); - - it('returns false for a detector using count on an mlcategory field', () => { - expect(isTimeSeriesViewDetector(job, 2)).to.be(false); - }); - - it('returns false for a detector using a script field as a by field', () => { - expect(isTimeSeriesViewDetector(job, 3)).to.be(false); - }); - - it('returns false for a detector using a script field as a metric field_name', () => { - expect(isTimeSeriesViewDetector(job, 4)).to.be(false); - }); - }); - - describe('isSourceDataChartableForDetector', () => { - const job = { - analysis_config: { - detectors: [ - { function: 'count' }, // 0 - { function: 'low_count' }, // 1 - { function: 'high_count' }, // 2 - { function: 'non_zero_count' }, // 3 - { function: 'low_non_zero_count' }, // 4 - { function: 'high_non_zero_count' }, // 5 - { function: 'distinct_count' }, // 6 - { function: 'low_distinct_count' }, // 7 - { function: 'high_distinct_count' }, // 8 - { function: 'metric' }, // 9 - { function: 'mean' }, // 10 - { function: 'low_mean' }, // 11 - { function: 'high_mean' }, // 12 - { function: 'median' }, // 13 - { function: 'low_median' }, // 14 - { function: 'high_median' }, // 15 - { function: 'min' }, // 16 - { function: 'max' }, // 17 - { function: 'sum' }, // 18 - { function: 'low_sum' }, // 19 - { function: 'high_sum' }, // 20 - { function: 'non_null_sum' }, // 21 - { function: 'low_non_null_sum' }, // 22 - { function: 'high_non_null_sum' }, // 23 - { function: 'rare' }, // 24 - { function: 'count', by_field_name: 'mlcategory' }, // 25 - { function: 'count', by_field_name: 'hrd' }, // 26 - { function: 'freq_rare' }, // 27 - { function: 'info_content' }, // 28 - { function: 'low_info_content' }, // 29 - { function: 'high_info_content' }, // 30 - { function: 'varp' }, // 31 - { function: 'low_varp' }, // 32 - { function: 'high_varp' }, // 33 - { function: 'time_of_day' }, // 34 - { function: 'time_of_week' }, // 35 - { function: 'lat_long' }, // 36 - { function: 'mean', field_name: 'NetworkDiff' }, //37 - ], - }, - datafeed_config: { - script_fields: { - hrd: { - script: { - inline: 'return domainSplit(doc["query"].value, params).get(1);', - lang: 'painless', - }, - }, - NetworkDiff: { - script: { - source: 'doc["NetworkOut"].value - doc["NetworkIn"].value', - lang: 'painless', - }, - }, - }, - }, - }; - - it('returns true for expected detectors', () => { - expect(isSourceDataChartableForDetector(job, 0)).to.be(true); - expect(isSourceDataChartableForDetector(job, 1)).to.be(true); - expect(isSourceDataChartableForDetector(job, 2)).to.be(true); - expect(isSourceDataChartableForDetector(job, 3)).to.be(true); - expect(isSourceDataChartableForDetector(job, 4)).to.be(true); - expect(isSourceDataChartableForDetector(job, 5)).to.be(true); - expect(isSourceDataChartableForDetector(job, 6)).to.be(true); - expect(isSourceDataChartableForDetector(job, 7)).to.be(true); - expect(isSourceDataChartableForDetector(job, 8)).to.be(true); - expect(isSourceDataChartableForDetector(job, 9)).to.be(true); - expect(isSourceDataChartableForDetector(job, 10)).to.be(true); - expect(isSourceDataChartableForDetector(job, 11)).to.be(true); - expect(isSourceDataChartableForDetector(job, 12)).to.be(true); - expect(isSourceDataChartableForDetector(job, 13)).to.be(true); - expect(isSourceDataChartableForDetector(job, 14)).to.be(true); - expect(isSourceDataChartableForDetector(job, 15)).to.be(true); - expect(isSourceDataChartableForDetector(job, 16)).to.be(true); - expect(isSourceDataChartableForDetector(job, 17)).to.be(true); - expect(isSourceDataChartableForDetector(job, 18)).to.be(true); - expect(isSourceDataChartableForDetector(job, 19)).to.be(true); - expect(isSourceDataChartableForDetector(job, 20)).to.be(true); - expect(isSourceDataChartableForDetector(job, 21)).to.be(true); - expect(isSourceDataChartableForDetector(job, 22)).to.be(true); - expect(isSourceDataChartableForDetector(job, 23)).to.be(true); - expect(isSourceDataChartableForDetector(job, 24)).to.be(true); - }); - - it('returns false for expected detectors', () => { - expect(isSourceDataChartableForDetector(job, 25)).to.be(false); - expect(isSourceDataChartableForDetector(job, 26)).to.be(false); - expect(isSourceDataChartableForDetector(job, 27)).to.be(false); - expect(isSourceDataChartableForDetector(job, 28)).to.be(false); - expect(isSourceDataChartableForDetector(job, 29)).to.be(false); - expect(isSourceDataChartableForDetector(job, 30)).to.be(false); - expect(isSourceDataChartableForDetector(job, 31)).to.be(false); - expect(isSourceDataChartableForDetector(job, 32)).to.be(false); - expect(isSourceDataChartableForDetector(job, 33)).to.be(false); - expect(isSourceDataChartableForDetector(job, 34)).to.be(false); - expect(isSourceDataChartableForDetector(job, 35)).to.be(false); - expect(isSourceDataChartableForDetector(job, 36)).to.be(false); - expect(isSourceDataChartableForDetector(job, 37)).to.be(false); - }); - }); - - describe('isModelPlotChartableForDetector', () => { - const job1 = { - analysis_config: { - detectors: [{ function: 'count' }], - }, - }; - - const job2 = { - analysis_config: { - detectors: [{ function: 'count' }, { function: 'info_content' }], - }, - model_plot_config: { - enabled: true, - }, - }; - - it('returns false when model plot is not enabled', () => { - expect(isModelPlotChartableForDetector(job1, 0)).to.be(false); - }); - - it('returns true for count detector when model plot is enabled', () => { - expect(isModelPlotChartableForDetector(job2, 0)).to.be(true); - }); - - it('returns true for info_content detector when model plot is enabled', () => { - expect(isModelPlotChartableForDetector(job2, 1)).to.be(true); - }); - }); - - describe('getPartitioningFieldNames', () => { - const job = { - analysis_config: { - detectors: [ - { - function: 'count', - detector_description: 'count', - }, - { - function: 'count', - partition_field_name: 'clientip', - detector_description: 'Count by clientip', - }, - { - function: 'freq_rare', - by_field_name: 'uri', - over_field_name: 'clientip', - detector_description: 'Freq rare URI', - }, - { - function: 'sum', - field_name: 'bytes', - by_field_name: 'uri', - over_field_name: 'clientip', - partition_field_name: 'method', - detector_description: 'sum bytes', - }, - ], - }, - }; - - it('returns empty array for a detector with no partitioning fields', () => { - const resp = getPartitioningFieldNames(job, 0); - expect(resp).to.be.an('array'); - expect(resp).to.be.empty(); - }); - - it('returns expected array for a detector with a partition field', () => { - const resp = getPartitioningFieldNames(job, 1); - expect(resp).to.be.an('array'); - expect(resp).to.have.length(1); - expect(resp).to.contain('clientip'); - }); - - it('returns expected array for a detector with by and over fields', () => { - const resp = getPartitioningFieldNames(job, 2); - expect(resp).to.be.an('array'); - expect(resp).to.have.length(2); - expect(resp).to.contain('uri'); - expect(resp).to.contain('clientip'); - }); - - it('returns expected array for a detector with partition, by and over fields', () => { - const resp = getPartitioningFieldNames(job, 3); - expect(resp).to.be.an('array'); - expect(resp).to.have.length(3); - expect(resp).to.contain('uri'); - expect(resp).to.contain('clientip'); - expect(resp).to.contain('method'); - }); - }); - - describe('isModelPlotEnabled', () => { - it('returns true for a job in which model plot has been enabled', () => { - const job = { - analysis_config: { - detectors: [ - { - function: 'high_count', - partition_field_name: 'status', - detector_description: 'High count status code', - }, - ], - }, - model_plot_config: { - enabled: true, - }, - }; - - expect(isModelPlotEnabled(job, 0)).to.be(true); - }); - - it('returns expected values for a job in which model plot has been enabled with terms', () => { - const job = { - analysis_config: { - detectors: [ - { - function: 'max', - field_name: 'responsetime', - partition_field_name: 'country', - by_field_name: 'airline', - }, - ], - }, - model_plot_config: { - enabled: true, - terms: 'US,AAL', - }, - }; - - expect( - isModelPlotEnabled(job, 0, [ - { fieldName: 'country', fieldValue: 'US' }, - { fieldName: 'airline', fieldValue: 'AAL' }, - ]) - ).to.be(true); - expect(isModelPlotEnabled(job, 0, [{ fieldName: 'country', fieldValue: 'US' }])).to.be(false); - expect( - isModelPlotEnabled(job, 0, [ - { fieldName: 'country', fieldValue: 'GB' }, - { fieldName: 'airline', fieldValue: 'AAL' }, - ]) - ).to.be(false); - expect( - isModelPlotEnabled(job, 0, [ - { fieldName: 'country', fieldValue: 'JP' }, - { fieldName: 'airline', fieldValue: 'JAL' }, - ]) - ).to.be(false); - }); - - it('returns true for jobs in which model plot has not been enabled', () => { - const job1 = { - analysis_config: { - detectors: [ - { - function: 'high_count', - partition_field_name: 'status', - detector_description: 'High count status code', - }, - ], - }, - model_plot_config: { - enabled: false, - }, - }; - const job2 = {}; - - expect(isModelPlotEnabled(job1, 0)).to.be(false); - expect(isModelPlotEnabled(job2, 0)).to.be(false); - }); - }); - - describe('isJobVersionGte', () => { - const job = { - job_version: '6.1.1', - }; - - it('returns true for later job version', () => { - expect(isJobVersionGte(job, '6.1.0')).to.be(true); - }); - it('returns true for equal job version', () => { - expect(isJobVersionGte(job, '6.1.1')).to.be(true); - }); - it('returns false for earlier job version', () => { - expect(isJobVersionGte(job, '6.1.2')).to.be(false); - }); - }); - - describe('mlFunctionToESAggregation', () => { - it('returns correct ES aggregation type for ML function', () => { - expect(mlFunctionToESAggregation('count')).to.be('count'); - expect(mlFunctionToESAggregation('low_count')).to.be('count'); - expect(mlFunctionToESAggregation('high_count')).to.be('count'); - expect(mlFunctionToESAggregation('non_zero_count')).to.be('count'); - expect(mlFunctionToESAggregation('low_non_zero_count')).to.be('count'); - expect(mlFunctionToESAggregation('high_non_zero_count')).to.be('count'); - expect(mlFunctionToESAggregation('distinct_count')).to.be('cardinality'); - expect(mlFunctionToESAggregation('low_distinct_count')).to.be('cardinality'); - expect(mlFunctionToESAggregation('high_distinct_count')).to.be('cardinality'); - expect(mlFunctionToESAggregation('metric')).to.be('avg'); - expect(mlFunctionToESAggregation('mean')).to.be('avg'); - expect(mlFunctionToESAggregation('low_mean')).to.be('avg'); - expect(mlFunctionToESAggregation('high_mean')).to.be('avg'); - expect(mlFunctionToESAggregation('min')).to.be('min'); - expect(mlFunctionToESAggregation('max')).to.be('max'); - expect(mlFunctionToESAggregation('sum')).to.be('sum'); - expect(mlFunctionToESAggregation('low_sum')).to.be('sum'); - expect(mlFunctionToESAggregation('high_sum')).to.be('sum'); - expect(mlFunctionToESAggregation('non_null_sum')).to.be('sum'); - expect(mlFunctionToESAggregation('low_non_null_sum')).to.be('sum'); - expect(mlFunctionToESAggregation('high_non_null_sum')).to.be('sum'); - expect(mlFunctionToESAggregation('rare')).to.be('count'); - expect(mlFunctionToESAggregation('freq_rare')).to.be(null); - expect(mlFunctionToESAggregation('info_content')).to.be(null); - expect(mlFunctionToESAggregation('low_info_content')).to.be(null); - expect(mlFunctionToESAggregation('high_info_content')).to.be(null); - expect(mlFunctionToESAggregation('median')).to.be('percentiles'); - expect(mlFunctionToESAggregation('low_median')).to.be('percentiles'); - expect(mlFunctionToESAggregation('high_median')).to.be('percentiles'); - expect(mlFunctionToESAggregation('varp')).to.be(null); - expect(mlFunctionToESAggregation('low_varp')).to.be(null); - expect(mlFunctionToESAggregation('high_varp')).to.be(null); - expect(mlFunctionToESAggregation('time_of_day')).to.be(null); - expect(mlFunctionToESAggregation('time_of_week')).to.be(null); - expect(mlFunctionToESAggregation('lat_long')).to.be(null); - }); - }); - - describe('isJobIdValid', () => { - it('returns true for job id: "good_job-name"', () => { - expect(isJobIdValid('good_job-name')).to.be(true); - }); - it('returns false for job id: "_bad_job-name"', () => { - expect(isJobIdValid('_bad_job-name')).to.be(false); - }); - it('returns false for job id: "bad_job-name_"', () => { - expect(isJobIdValid('bad_job-name_')).to.be(false); - }); - it('returns false for job id: "-bad_job-name"', () => { - expect(isJobIdValid('-bad_job-name')).to.be(false); - }); - it('returns false for job id: "bad_job-name-"', () => { - expect(isJobIdValid('bad_job-name-')).to.be(false); - }); - it('returns false for job id: "bad&job-name"', () => { - expect(isJobIdValid('bad&job-name')).to.be(false); - }); - }); - - describe('ML_MEDIAN_PERCENTS', () => { - it("is '50.0'", () => { - expect(ML_MEDIAN_PERCENTS).to.be('50.0'); - }); - }); - - describe('prefixDatafeedId', () => { - it('returns datafeed-prefix-job from datafeed-job"', () => { - expect(prefixDatafeedId('datafeed-job', 'prefix-')).to.be('datafeed-prefix-job'); - }); - - it('returns datafeed-prefix-job from job"', () => { - expect(prefixDatafeedId('job', 'prefix-')).to.be('datafeed-prefix-job'); - }); - }); - - describe('getSafeAggregationName', () => { - it('"foo" should be "foo"', () => { - expect(getSafeAggregationName('foo', 0)).to.be('foo'); - }); - it('"foo.bar" should be "foo.bar"', () => { - expect(getSafeAggregationName('foo.bar', 0)).to.be('foo.bar'); - }); - it('"foo&bar" should be "field_0"', () => { - expect(getSafeAggregationName('foo&bar', 0)).to.be('field_0'); - }); - }); - - describe('getLatestDataOrBucketTimestamp', () => { - it('returns expected value when no gap in data at end of bucket processing', () => { - expect(getLatestDataOrBucketTimestamp(1549929594000, 1549928700000)).to.be(1549929594000); - }); - it('returns expected value when there is a gap in data at end of bucket processing', () => { - expect(getLatestDataOrBucketTimestamp(1549929594000, 1562256600000)).to.be(1562256600000); - }); - it('returns expected value when job has not run', () => { - expect(getLatestDataOrBucketTimestamp(undefined, undefined)).to.be(undefined); - }); - }); -}); diff --git a/x-pack/plugins/ml/common/util/anomaly_utils.d.ts b/x-pack/plugins/ml/common/util/anomaly_utils.d.ts deleted file mode 100644 index adeb6dc7dd5b9..0000000000000 --- a/x-pack/plugins/ml/common/util/anomaly_utils.d.ts +++ /dev/null @@ -1,11 +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 { ANOMALY_SEVERITY } from '../constants/anomalies'; - -export function getSeverity(normalizedScore: number): string; -export function getSeverityType(normalizedScore: number): ANOMALY_SEVERITY; -export function getSeverityColor(normalizedScore: number): string; diff --git a/x-pack/plugins/ml/common/util/anomaly_utils.js b/x-pack/plugins/ml/common/util/anomaly_utils.js deleted file mode 100644 index 16c27b6af869d..0000000000000 --- a/x-pack/plugins/ml/common/util/anomaly_utils.js +++ /dev/null @@ -1,332 +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. - */ - -/* - * Contains functions for operations commonly performed on anomaly data - * to extract information for display in dashboards. - */ - -import { i18n } from '@kbn/i18n'; -import { CONDITIONS_NOT_SUPPORTED_FUNCTIONS } from '../constants/detector_rule'; -import { MULTI_BUCKET_IMPACT } from '../constants/multi_bucket_impact'; -import { ANOMALY_SEVERITY, ANOMALY_THRESHOLD } from '../constants/anomalies'; - -// List of function descriptions for which actual values from record level results should be displayed. -const DISPLAY_ACTUAL_FUNCTIONS = [ - 'count', - 'distinct_count', - 'lat_long', - 'mean', - 'max', - 'min', - 'sum', - 'median', - 'varp', - 'info_content', - 'time', -]; - -// List of function descriptions for which typical values from record level results should be displayed. -const DISPLAY_TYPICAL_FUNCTIONS = [ - 'count', - 'distinct_count', - 'lat_long', - 'mean', - 'max', - 'min', - 'sum', - 'median', - 'varp', - 'info_content', - 'time', -]; - -let severityTypes; - -function getSeverityTypes() { - if (severityTypes) { - return severityTypes; - } - - return (severityTypes = { - critical: { - id: ANOMALY_SEVERITY.CRITICAL, - label: i18n.translate('xpack.ml.anomalyUtils.severity.criticalLabel', { - defaultMessage: 'critical', - }), - }, - major: { - id: ANOMALY_SEVERITY.MAJOR, - label: i18n.translate('xpack.ml.anomalyUtils.severity.majorLabel', { - defaultMessage: 'major', - }), - }, - minor: { - id: ANOMALY_SEVERITY.MINOR, - label: i18n.translate('xpack.ml.anomalyUtils.severity.minorLabel', { - defaultMessage: 'minor', - }), - }, - warning: { - id: ANOMALY_SEVERITY.WARNING, - label: i18n.translate('xpack.ml.anomalyUtils.severity.warningLabel', { - defaultMessage: 'warning', - }), - }, - unknown: { - id: ANOMALY_SEVERITY.UNKNOWN, - label: i18n.translate('xpack.ml.anomalyUtils.severity.unknownLabel', { - defaultMessage: 'unknown', - }), - }, - low: { - id: ANOMALY_SEVERITY.LOW, - label: i18n.translate('xpack.ml.anomalyUtils.severityWithLow.lowLabel', { - defaultMessage: 'low', - }), - }, - }); -} - -// Returns a severity label (one of critical, major, minor, warning or unknown) -// for the supplied normalized anomaly score (a value between 0 and 100). -export function getSeverity(normalizedScore) { - const severityTypesList = getSeverityTypes(); - - if (normalizedScore >= ANOMALY_THRESHOLD.CRITICAL) { - return severityTypesList.critical; - } else if (normalizedScore >= ANOMALY_THRESHOLD.MAJOR) { - return severityTypesList.major; - } else if (normalizedScore >= ANOMALY_THRESHOLD.MINOR) { - return severityTypesList.minor; - } else if (normalizedScore >= ANOMALY_THRESHOLD.LOW) { - return severityTypesList.warning; - } else { - return severityTypesList.unknown; - } -} - -export function getSeverityType(normalizedScore) { - if (normalizedScore >= 75) { - return ANOMALY_SEVERITY.CRITICAL; - } else if (normalizedScore >= 50) { - return ANOMALY_SEVERITY.MAJOR; - } else if (normalizedScore >= 25) { - return ANOMALY_SEVERITY.MINOR; - } else if (normalizedScore >= 3) { - return ANOMALY_SEVERITY.WARNING; - } else if (normalizedScore >= 0) { - return ANOMALY_SEVERITY.LOW; - } else { - return ANOMALY_SEVERITY.UNKNOWN; - } -} - -// Returns a severity label (one of critical, major, minor, warning, low or unknown) -// for the supplied normalized anomaly score (a value between 0 and 100), where scores -// less than 3 are assigned a severity of 'low'. -export function getSeverityWithLow(normalizedScore) { - const severityTypesList = getSeverityTypes(); - - if (normalizedScore >= ANOMALY_THRESHOLD.CRITICAL) { - return severityTypesList.critical; - } else if (normalizedScore >= ANOMALY_THRESHOLD.MAJOR) { - return severityTypesList.major; - } else if (normalizedScore >= ANOMALY_THRESHOLD.MINOR) { - return severityTypesList.minor; - } else if (normalizedScore >= ANOMALY_THRESHOLD.WARNING) { - return severityTypesList.warning; - } else if (normalizedScore >= ANOMALY_THRESHOLD.LOW) { - return severityTypesList.low; - } else { - return severityTypesList.unknown; - } -} - -// Returns a severity RGB color (one of critical, major, minor, warning, low_warning or unknown) -// for the supplied normalized anomaly score (a value between 0 and 100). -export function getSeverityColor(normalizedScore) { - if (normalizedScore >= ANOMALY_THRESHOLD.CRITICAL) { - return '#fe5050'; - } else if (normalizedScore >= ANOMALY_THRESHOLD.MAJOR) { - return '#fba740'; - } else if (normalizedScore >= ANOMALY_THRESHOLD.MINOR) { - return '#fdec25'; - } else if (normalizedScore >= ANOMALY_THRESHOLD.WARNING) { - return '#8bc8fb'; - } else if (normalizedScore >= ANOMALY_THRESHOLD.LOW) { - return '#d2e9f7'; - } else { - return '#ffffff'; - } -} - -// Returns a label to use for the multi-bucket impact of an anomaly -// according to the value of the multi_bucket_impact field of a record, -// which ranges from -5 to +5. -export function getMultiBucketImpactLabel(multiBucketImpact) { - if (multiBucketImpact >= MULTI_BUCKET_IMPACT.HIGH) { - return i18n.translate('xpack.ml.anomalyUtils.multiBucketImpact.highLabel', { - defaultMessage: 'high', - }); - } else if (multiBucketImpact >= MULTI_BUCKET_IMPACT.MEDIUM) { - return i18n.translate('xpack.ml.anomalyUtils.multiBucketImpact.mediumLabel', { - defaultMessage: 'medium', - }); - } else if (multiBucketImpact >= MULTI_BUCKET_IMPACT.LOW) { - return i18n.translate('xpack.ml.anomalyUtils.multiBucketImpact.lowLabel', { - defaultMessage: 'low', - }); - } else { - return i18n.translate('xpack.ml.anomalyUtils.multiBucketImpact.noneLabel', { - defaultMessage: 'none', - }); - } -} - -// Returns the name of the field to use as the entity name from the source record -// obtained from Elasticsearch. The function looks first for a by_field, then over_field, -// then partition_field, returning undefined if none of these fields are present. -export function getEntityFieldName(record) { - // Analyses with by and over fields, will have a top-level by_field_name, but - // the by_field_value(s) will be in the nested causes array. - if (record.by_field_name !== undefined && record.by_field_value !== undefined) { - return record.by_field_name; - } - - if (record.over_field_name !== undefined) { - return record.over_field_name; - } - - if (record.partition_field_name !== undefined) { - return record.partition_field_name; - } - - return undefined; -} - -// Returns the value of the field to use as the entity value from the source record -// obtained from Elasticsearch. The function looks first for a by_field, then over_field, -// then partition_field, returning undefined if none of these fields are present. -export function getEntityFieldValue(record) { - if (record.by_field_value !== undefined) { - return record.by_field_value; - } - - if (record.over_field_value !== undefined) { - return record.over_field_value; - } - - if (record.partition_field_value !== undefined) { - return record.partition_field_value; - } - - return undefined; -} - -// Returns the list of partitioning entity fields for the source record as a list -// of objects in the form { fieldName: airline, fieldValue: AAL, fieldType: partition } -export function getEntityFieldList(record) { - const entityFields = []; - if (record.partition_field_name !== undefined) { - entityFields.push({ - fieldName: record.partition_field_name, - fieldValue: record.partition_field_value, - fieldType: 'partition', - }); - } - - if (record.over_field_name !== undefined) { - entityFields.push({ - fieldName: record.over_field_name, - fieldValue: record.over_field_value, - fieldType: 'over', - }); - } - - // For jobs with by and over fields, don't add the 'by' field as this - // field will only be added to the top-level fields for record type results - // if it also an influencer over the bucket. - if (record.by_field_name !== undefined && record.over_field_name === undefined) { - entityFields.push({ - fieldName: record.by_field_name, - fieldValue: record.by_field_value, - fieldType: 'by', - }); - } - - return entityFields; -} - -// Returns whether actual values should be displayed for a record with the specified function description. -// Note that the 'function' field in a record contains what the user entered e.g. 'high_count', -// whereas the 'function_description' field holds a ML-built display hint for function e.g. 'count'. -export function showActualForFunction(functionDescription) { - return DISPLAY_ACTUAL_FUNCTIONS.indexOf(functionDescription) > -1; -} - -// Returns whether typical values should be displayed for a record with the specified function description. -// Note that the 'function' field in a record contains what the user entered e.g. 'high_count', -// whereas the 'function_description' field holds a ML-built display hint for function e.g. 'count'. -export function showTypicalForFunction(functionDescription) { - return DISPLAY_TYPICAL_FUNCTIONS.indexOf(functionDescription) > -1; -} - -// Returns whether a rule can be configured against the specified anomaly. -export function isRuleSupported(record) { - // A rule can be configured with a numeric condition if the function supports it, - // and/or with scope if there is a partitioning fields. - return ( - CONDITIONS_NOT_SUPPORTED_FUNCTIONS.indexOf(record.function) === -1 || - getEntityFieldName(record) !== undefined - ); -} - -// Two functions for converting aggregation type names. -// ML and ES use different names for the same function. -// Possible values for ML aggregation type are (defined in lib/model/CAnomalyDetector.cc): -// count -// distinct_count -// rare -// info_content -// mean -// median -// min -// max -// varp -// sum -// lat_long -// time -// The input to toES and the output from toML correspond to the value of the -// function_description field of anomaly records. -export const aggregationTypeTransform = { - toES: function(oldAggType) { - let newAggType = oldAggType; - - if (newAggType === 'mean') { - newAggType = 'avg'; - } else if (newAggType === 'distinct_count') { - newAggType = 'cardinality'; - } else if (newAggType === 'median') { - newAggType = 'percentiles'; - } - - return newAggType; - }, - toML: function(oldAggType) { - let newAggType = oldAggType; - - if (newAggType === 'avg') { - newAggType = 'mean'; - } else if (newAggType === 'cardinality') { - newAggType = 'distinct_count'; - } else if (newAggType === 'percentiles') { - newAggType = 'median'; - } - - return newAggType; - }, -}; diff --git a/x-pack/plugins/ml/common/util/anomaly_utils.test.ts b/x-pack/plugins/ml/common/util/anomaly_utils.test.ts new file mode 100644 index 0000000000000..1343e4611c215 --- /dev/null +++ b/x-pack/plugins/ml/common/util/anomaly_utils.test.ts @@ -0,0 +1,444 @@ +/* + * 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 { AnomalyRecordDoc } from '../types/anomalies'; + +import { + aggregationTypeTransform, + getEntityFieldList, + getEntityFieldName, + getEntityFieldValue, + getMultiBucketImpactLabel, + getSeverity, + getSeverityWithLow, + getSeverityColor, + isRuleSupported, + showActualForFunction, + showTypicalForFunction, +} from './anomaly_utils'; + +describe('ML - anomaly utils', () => { + const partitionEntityRecord: AnomalyRecordDoc = { + job_id: 'farequote', + result_type: 'record', + probability: 0.012818, + record_score: 0.0162059, + initial_record_score: 0.0162059, + bucket_span: 300, + detector_index: 0, + is_interim: false, + timestamp: 1455047400000, + partition_field_name: 'airline', + partition_field_value: 'AAL', + function: 'mean', + function_description: 'mean', + field_name: 'responsetime', + }; + + const byEntityRecord: AnomalyRecordDoc = { + job_id: 'farequote', + result_type: 'record', + probability: 0.012818, + record_score: 0.0162059, + initial_record_score: 0.0162059, + bucket_span: 300, + detector_index: 0, + is_interim: false, + timestamp: 1455047400000, + by_field_name: 'airline', + by_field_value: 'JZA', + function: 'mean', + function_description: 'mean', + field_name: 'responsetime', + }; + + const overEntityRecord: AnomalyRecordDoc = { + job_id: 'gallery', + result_type: 'record', + probability: 2.81806e-9, + record_score: 59.055, + initial_record_score: 59.055, + bucket_span: 3600, + detector_index: 4, + is_interim: false, + timestamp: 1420552800000, + function: 'sum', + function_description: 'sum', + field_name: 'bytes', + by_field_name: 'method', + over_field_name: 'clientip', + over_field_value: '37.157.32.164', + }; + + const noEntityRecord: AnomalyRecordDoc = { + job_id: 'farequote_no_by', + result_type: 'record', + probability: 0.0191711, + record_score: 4.38431, + initial_record_score: 19.654, + bucket_span: 300, + detector_index: 0, + is_interim: false, + timestamp: 1454890500000, + function: 'mean', + function_description: 'mean', + field_name: 'responsetime', + }; + + const metricNoEntityRecord: AnomalyRecordDoc = { + job_id: 'farequote_metric', + result_type: 'record', + probability: 0.030133495093182184, + record_score: 0.024881740359975164, + initial_record_score: 0.024881740359975164, + bucket_span: 900, + detector_index: 0, + is_interim: false, + timestamp: 1486845000000, + function: 'metric', + function_description: 'mean', + typical: [545.7764658569108], + actual: [758.8220213274412], + field_name: 'responsetime', + influencers: [ + { + influencer_field_name: 'airline', + influencer_field_values: ['NKS'], + }, + ], + airline: ['NKS'], + }; + + const rareEntityRecord: AnomalyRecordDoc = { + job_id: 'gallery', + result_type: 'record', + probability: 0.02277014211908481, + record_score: 4.545378107075983, + initial_record_score: 4.545378107075983, + bucket_span: 3600, + detector_index: 0, + is_interim: false, + timestamp: 1495879200000, + by_field_name: 'status', + function: 'rare', + function_description: 'rare', + over_field_name: 'clientip', + over_field_value: '173.252.74.112', + causes: [ + { + probability: 0.02277014211908481, + by_field_name: 'status', + by_field_value: '206', + function: 'rare', + function_description: 'rare', + typical: [0.00014832458182211878], + actual: [1], + over_field_name: 'clientip', + over_field_value: '173.252.74.112', + }, + ], + influencers: [ + { + influencer_field_name: 'uri', + influencer_field_values: [ + '/wp-content/uploads/2013/06/dune_house_oil_on_canvas_24x20-298x298.jpg', + '/wp-content/uploads/2013/10/Case-dAste-1-11-298x298.png', + ], + }, + { + influencer_field_name: 'status', + influencer_field_values: ['206'], + }, + { + influencer_field_name: 'clientip', + influencer_field_values: ['173.252.74.112'], + }, + ], + clientip: ['173.252.74.112'], + uri: [ + '/wp-content/uploads/2013/06/dune_house_oil_on_canvas_24x20-298x298.jpg', + '/wp-content/uploads/2013/10/Case-dAste-1-11-298x298.png', + ], + status: ['206'], + }; + + describe('getSeverity', () => { + test('returns warning for 0 <= score < 25', () => { + expect(getSeverity(0).id).toBe('warning'); + expect(getSeverity(0.001).id).toBe('warning'); + expect(getSeverity(24.99).id).toBe('warning'); + }); + + test('returns minor for 25 <= score < 50', () => { + expect(getSeverity(25).id).toBe('minor'); + expect(getSeverity(49.99).id).toBe('minor'); + }); + + test('returns minor for 50 <= score < 75', () => { + expect(getSeverity(50).id).toBe('major'); + expect(getSeverity(74.99).id).toBe('major'); + }); + + test('returns critical for score >= 75', () => { + expect(getSeverity(75).id).toBe('critical'); + expect(getSeverity(100).id).toBe('critical'); + expect(getSeverity(1000).id).toBe('critical'); + }); + + test('returns unknown for scores less than 0', () => { + expect(getSeverity(-10).id).toBe('unknown'); + }); + }); + + describe('getSeverityWithLow', () => { + test('returns low for 0 <= score < 3', () => { + expect(getSeverityWithLow(0).id).toBe('low'); + expect(getSeverityWithLow(0.001).id).toBe('low'); + expect(getSeverityWithLow(2.99).id).toBe('low'); + }); + + test('returns warning for 3 <= score < 25', () => { + expect(getSeverityWithLow(3).id).toBe('warning'); + expect(getSeverityWithLow(24.99).id).toBe('warning'); + }); + + test('returns minor for 25 <= score < 50', () => { + expect(getSeverityWithLow(25).id).toBe('minor'); + expect(getSeverityWithLow(49.99).id).toBe('minor'); + }); + + test('returns minor for 50 <= score < 75', () => { + expect(getSeverityWithLow(50).id).toBe('major'); + expect(getSeverityWithLow(74.99).id).toBe('major'); + }); + + test('returns critical for score >= 75', () => { + expect(getSeverityWithLow(75).id).toBe('critical'); + expect(getSeverityWithLow(100).id).toBe('critical'); + expect(getSeverityWithLow(1000).id).toBe('critical'); + }); + + test('returns unknown for scores less than 0 ', () => { + expect(getSeverityWithLow(-10).id).toBe('unknown'); + }); + }); + + describe('getSeverityColor', () => { + test('returns correct hex code for low for 0 <= score < 3', () => { + expect(getSeverityColor(0)).toBe('#d2e9f7'); + expect(getSeverityColor(0.001)).toBe('#d2e9f7'); + expect(getSeverityColor(2.99)).toBe('#d2e9f7'); + }); + + test('returns correct hex code for warning for 3 <= score < 25', () => { + expect(getSeverityColor(3)).toBe('#8bc8fb'); + expect(getSeverityColor(24.99)).toBe('#8bc8fb'); + }); + + test('returns correct hex code for minor for 25 <= score < 50', () => { + expect(getSeverityColor(25)).toBe('#fdec25'); + expect(getSeverityColor(49.99)).toBe('#fdec25'); + }); + + test('returns correct hex code for major for 50 <= score < 75', () => { + expect(getSeverityColor(50)).toBe('#fba740'); + expect(getSeverityColor(74.99)).toBe('#fba740'); + }); + + test('returns correct hex code for critical for score >= 75', () => { + expect(getSeverityColor(75)).toBe('#fe5050'); + expect(getSeverityColor(100)).toBe('#fe5050'); + expect(getSeverityColor(1000)).toBe('#fe5050'); + }); + + test('returns correct hex code for unknown for scores less than 0', () => { + expect(getSeverityColor(-10)).toBe('#ffffff'); + }); + }); + + describe('getMultiBucketImpactLabel', () => { + test('returns high for 3 <= score <= 5', () => { + expect(getMultiBucketImpactLabel(3)).toBe('high'); + expect(getMultiBucketImpactLabel(5)).toBe('high'); + }); + + test('returns medium for 2 <= score < 3', () => { + expect(getMultiBucketImpactLabel(2)).toBe('medium'); + expect(getMultiBucketImpactLabel(2.99)).toBe('medium'); + }); + + test('returns low for 1 <= score < 2', () => { + expect(getMultiBucketImpactLabel(1)).toBe('low'); + expect(getMultiBucketImpactLabel(1.99)).toBe('low'); + }); + + test('returns none for -5 <= score < 1', () => { + expect(getMultiBucketImpactLabel(-5)).toBe('none'); + expect(getMultiBucketImpactLabel(0.99)).toBe('none'); + }); + + test('returns expected label when impact outside normal bounds', () => { + expect(getMultiBucketImpactLabel(10)).toBe('high'); + expect(getMultiBucketImpactLabel(-10)).toBe('none'); + }); + }); + + describe('getEntityFieldName', () => { + it('returns the by field name', () => { + expect(getEntityFieldName(byEntityRecord)).toBe('airline'); + }); + + it('returns the partition field name', () => { + expect(getEntityFieldName(partitionEntityRecord)).toBe('airline'); + }); + + it('returns the over field name', () => { + expect(getEntityFieldName(overEntityRecord)).toBe('clientip'); + }); + + it('returns undefined if no by, over or partition fields', () => { + expect(getEntityFieldName(noEntityRecord)).toBe(undefined); + }); + }); + + describe('getEntityFieldValue', () => { + test('returns the by field value', () => { + expect(getEntityFieldValue(byEntityRecord)).toBe('JZA'); + }); + + test('returns the partition field value', () => { + expect(getEntityFieldValue(partitionEntityRecord)).toBe('AAL'); + }); + + test('returns the over field value', () => { + expect(getEntityFieldValue(overEntityRecord)).toBe('37.157.32.164'); + }); + + test('returns undefined if no by, over or partition fields', () => { + expect(getEntityFieldValue(noEntityRecord)).toBe(undefined); + }); + }); + + describe('getEntityFieldList', () => { + test('returns an empty list for a record with no by, over or partition fields', () => { + expect(getEntityFieldList(noEntityRecord)).toHaveLength(0); + }); + + test('returns correct list for a record with a by field', () => { + expect(getEntityFieldList(byEntityRecord)).toEqual([ + { + fieldName: 'airline', + fieldValue: 'JZA', + fieldType: 'by', + }, + ]); + }); + + test('returns correct list for a record with a partition field', () => { + expect(getEntityFieldList(partitionEntityRecord)).toEqual([ + { + fieldName: 'airline', + fieldValue: 'AAL', + fieldType: 'partition', + }, + ]); + }); + + test('returns correct list for a record with an over field', () => { + expect(getEntityFieldList(overEntityRecord)).toEqual([ + { + fieldName: 'clientip', + fieldValue: '37.157.32.164', + fieldType: 'over', + }, + ]); + }); + + test('returns correct list for a record with a by and over field', () => { + expect(getEntityFieldList(rareEntityRecord)).toEqual([ + { + fieldName: 'clientip', + fieldValue: '173.252.74.112', + fieldType: 'over', + }, + ]); + }); + }); + + describe('showActualForFunction', () => { + test('returns true for expected function descriptions', () => { + expect(showActualForFunction('count')).toBe(true); + expect(showActualForFunction('distinct_count')).toBe(true); + expect(showActualForFunction('lat_long')).toBe(true); + expect(showActualForFunction('mean')).toBe(true); + expect(showActualForFunction('max')).toBe(true); + expect(showActualForFunction('min')).toBe(true); + expect(showActualForFunction('sum')).toBe(true); + expect(showActualForFunction('median')).toBe(true); + expect(showActualForFunction('varp')).toBe(true); + expect(showActualForFunction('info_content')).toBe(true); + expect(showActualForFunction('time')).toBe(true); + }); + + test('returns false for expected function descriptions', () => { + expect(showActualForFunction('rare')).toBe(false); + }); + }); + + describe('showTypicalForFunction', () => { + test('returns true for expected function descriptions', () => { + expect(showTypicalForFunction('count')).toBe(true); + expect(showTypicalForFunction('distinct_count')).toBe(true); + expect(showTypicalForFunction('lat_long')).toBe(true); + expect(showTypicalForFunction('mean')).toBe(true); + expect(showTypicalForFunction('max')).toBe(true); + expect(showTypicalForFunction('min')).toBe(true); + expect(showTypicalForFunction('sum')).toBe(true); + expect(showTypicalForFunction('median')).toBe(true); + expect(showTypicalForFunction('varp')).toBe(true); + expect(showTypicalForFunction('info_content')).toBe(true); + expect(showTypicalForFunction('time')).toBe(true); + }); + + test('returns false for expected function descriptions', () => { + expect(showTypicalForFunction('rare')).toBe(false); + }); + }); + + describe('isRuleSupported', () => { + test('returns true for anomalies supporting rules', () => { + expect(isRuleSupported(partitionEntityRecord)).toBe(true); + expect(isRuleSupported(byEntityRecord)).toBe(true); + expect(isRuleSupported(overEntityRecord)).toBe(true); + expect(isRuleSupported(rareEntityRecord)).toBe(true); + expect(isRuleSupported(noEntityRecord)).toBe(true); + }); + + it('returns false for anomaly not supporting rules', () => { + expect(isRuleSupported(metricNoEntityRecord)).toBe(false); + }); + }); + + describe('aggregationTypeTransform', () => { + test('returns correct ES aggregation type for ML function description', () => { + expect(aggregationTypeTransform.toES('count')).toBe('count'); + expect(aggregationTypeTransform.toES('distinct_count')).toBe('cardinality'); + expect(aggregationTypeTransform.toES('mean')).toBe('avg'); + expect(aggregationTypeTransform.toES('max')).toBe('max'); + expect(aggregationTypeTransform.toES('min')).toBe('min'); + expect(aggregationTypeTransform.toES('sum')).toBe('sum'); + }); + + test('returns correct ML function description for ES aggregation type', () => { + expect(aggregationTypeTransform.toML('count')).toBe('count'); + expect(aggregationTypeTransform.toML('cardinality')).toBe('distinct_count'); + expect(aggregationTypeTransform.toML('avg')).toBe('mean'); + expect(aggregationTypeTransform.toML('max')).toBe('max'); + expect(aggregationTypeTransform.toML('min')).toBe('min'); + expect(aggregationTypeTransform.toML('sum')).toBe('sum'); + }); + }); +}); diff --git a/x-pack/plugins/ml/common/util/anomaly_utils.ts b/x-pack/plugins/ml/common/util/anomaly_utils.ts new file mode 100644 index 0000000000000..36b91f5580b39 --- /dev/null +++ b/x-pack/plugins/ml/common/util/anomaly_utils.ts @@ -0,0 +1,350 @@ +/* + * 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. + */ + +/* + * Contains functions for operations commonly performed on anomaly data + * to extract information for display in dashboards. + */ + +import { i18n } from '@kbn/i18n'; +import { CONDITIONS_NOT_SUPPORTED_FUNCTIONS } from '../constants/detector_rule'; +import { MULTI_BUCKET_IMPACT } from '../constants/multi_bucket_impact'; +import { ANOMALY_SEVERITY, ANOMALY_THRESHOLD } from '../constants/anomalies'; +import { AnomalyRecordDoc } from '../types/anomalies'; + +export interface SeverityType { + id: ANOMALY_SEVERITY; + label: string; +} + +export enum ENTITY_FIELD_TYPE { + BY = 'by', + OVER = 'over', + PARTITON = 'partition', +} + +export interface EntityField { + fieldName: string; + fieldValue: string | number | undefined; + fieldType: ENTITY_FIELD_TYPE; +} + +// List of function descriptions for which actual values from record level results should be displayed. +const DISPLAY_ACTUAL_FUNCTIONS = [ + 'count', + 'distinct_count', + 'lat_long', + 'mean', + 'max', + 'min', + 'sum', + 'median', + 'varp', + 'info_content', + 'time', +]; + +// List of function descriptions for which typical values from record level results should be displayed. +const DISPLAY_TYPICAL_FUNCTIONS = [ + 'count', + 'distinct_count', + 'lat_long', + 'mean', + 'max', + 'min', + 'sum', + 'median', + 'varp', + 'info_content', + 'time', +]; + +let severityTypes: Record<string, SeverityType>; + +function getSeverityTypes() { + if (severityTypes) { + return severityTypes; + } + + return (severityTypes = { + critical: { + id: ANOMALY_SEVERITY.CRITICAL, + label: i18n.translate('xpack.ml.anomalyUtils.severity.criticalLabel', { + defaultMessage: 'critical', + }), + }, + major: { + id: ANOMALY_SEVERITY.MAJOR, + label: i18n.translate('xpack.ml.anomalyUtils.severity.majorLabel', { + defaultMessage: 'major', + }), + }, + minor: { + id: ANOMALY_SEVERITY.MINOR, + label: i18n.translate('xpack.ml.anomalyUtils.severity.minorLabel', { + defaultMessage: 'minor', + }), + }, + warning: { + id: ANOMALY_SEVERITY.WARNING, + label: i18n.translate('xpack.ml.anomalyUtils.severity.warningLabel', { + defaultMessage: 'warning', + }), + }, + unknown: { + id: ANOMALY_SEVERITY.UNKNOWN, + label: i18n.translate('xpack.ml.anomalyUtils.severity.unknownLabel', { + defaultMessage: 'unknown', + }), + }, + low: { + id: ANOMALY_SEVERITY.LOW, + label: i18n.translate('xpack.ml.anomalyUtils.severityWithLow.lowLabel', { + defaultMessage: 'low', + }), + }, + }); +} + +// Returns a severity label (one of critical, major, minor, warning or unknown) +// for the supplied normalized anomaly score (a value between 0 and 100). +export function getSeverity(normalizedScore: number): SeverityType { + const severityTypesList = getSeverityTypes(); + + if (normalizedScore >= ANOMALY_THRESHOLD.CRITICAL) { + return severityTypesList.critical; + } else if (normalizedScore >= ANOMALY_THRESHOLD.MAJOR) { + return severityTypesList.major; + } else if (normalizedScore >= ANOMALY_THRESHOLD.MINOR) { + return severityTypesList.minor; + } else if (normalizedScore >= ANOMALY_THRESHOLD.LOW) { + return severityTypesList.warning; + } else { + return severityTypesList.unknown; + } +} + +export function getSeverityType(normalizedScore: number): ANOMALY_SEVERITY { + if (normalizedScore >= 75) { + return ANOMALY_SEVERITY.CRITICAL; + } else if (normalizedScore >= 50) { + return ANOMALY_SEVERITY.MAJOR; + } else if (normalizedScore >= 25) { + return ANOMALY_SEVERITY.MINOR; + } else if (normalizedScore >= 3) { + return ANOMALY_SEVERITY.WARNING; + } else if (normalizedScore >= 0) { + return ANOMALY_SEVERITY.LOW; + } else { + return ANOMALY_SEVERITY.UNKNOWN; + } +} + +// Returns a severity label (one of critical, major, minor, warning, low or unknown) +// for the supplied normalized anomaly score (a value between 0 and 100), where scores +// less than 3 are assigned a severity of 'low'. +export function getSeverityWithLow(normalizedScore: number): SeverityType { + const severityTypesList = getSeverityTypes(); + + if (normalizedScore >= ANOMALY_THRESHOLD.CRITICAL) { + return severityTypesList.critical; + } else if (normalizedScore >= ANOMALY_THRESHOLD.MAJOR) { + return severityTypesList.major; + } else if (normalizedScore >= ANOMALY_THRESHOLD.MINOR) { + return severityTypesList.minor; + } else if (normalizedScore >= ANOMALY_THRESHOLD.WARNING) { + return severityTypesList.warning; + } else if (normalizedScore >= ANOMALY_THRESHOLD.LOW) { + return severityTypesList.low; + } else { + return severityTypesList.unknown; + } +} + +// Returns a severity RGB color (one of critical, major, minor, warning, low_warning or unknown) +// for the supplied normalized anomaly score (a value between 0 and 100). +export function getSeverityColor(normalizedScore: number): string { + if (normalizedScore >= ANOMALY_THRESHOLD.CRITICAL) { + return '#fe5050'; + } else if (normalizedScore >= ANOMALY_THRESHOLD.MAJOR) { + return '#fba740'; + } else if (normalizedScore >= ANOMALY_THRESHOLD.MINOR) { + return '#fdec25'; + } else if (normalizedScore >= ANOMALY_THRESHOLD.WARNING) { + return '#8bc8fb'; + } else if (normalizedScore >= ANOMALY_THRESHOLD.LOW) { + return '#d2e9f7'; + } else { + return '#ffffff'; + } +} + +// Returns a label to use for the multi-bucket impact of an anomaly +// according to the value of the multi_bucket_impact field of a record, +// which ranges from -5 to +5. +export function getMultiBucketImpactLabel(multiBucketImpact: number): string { + if (multiBucketImpact >= MULTI_BUCKET_IMPACT.HIGH) { + return i18n.translate('xpack.ml.anomalyUtils.multiBucketImpact.highLabel', { + defaultMessage: 'high', + }); + } else if (multiBucketImpact >= MULTI_BUCKET_IMPACT.MEDIUM) { + return i18n.translate('xpack.ml.anomalyUtils.multiBucketImpact.mediumLabel', { + defaultMessage: 'medium', + }); + } else if (multiBucketImpact >= MULTI_BUCKET_IMPACT.LOW) { + return i18n.translate('xpack.ml.anomalyUtils.multiBucketImpact.lowLabel', { + defaultMessage: 'low', + }); + } else { + return i18n.translate('xpack.ml.anomalyUtils.multiBucketImpact.noneLabel', { + defaultMessage: 'none', + }); + } +} + +// Returns the name of the field to use as the entity name from the source record +// obtained from Elasticsearch. The function looks first for a by_field, then over_field, +// then partition_field, returning undefined if none of these fields are present. +export function getEntityFieldName(record: AnomalyRecordDoc): string | undefined { + // Analyses with by and over fields, will have a top-level by_field_name, but + // the by_field_value(s) will be in the nested causes array. + if (record.by_field_name !== undefined && record.by_field_value !== undefined) { + return record.by_field_name; + } + + if (record.over_field_name !== undefined) { + return record.over_field_name; + } + + if (record.partition_field_name !== undefined) { + return record.partition_field_name; + } + + return undefined; +} + +// Returns the value of the field to use as the entity value from the source record +// obtained from Elasticsearch. The function looks first for a by_field, then over_field, +// then partition_field, returning undefined if none of these fields are present. +export function getEntityFieldValue(record: AnomalyRecordDoc): string | number | undefined { + if (record.by_field_value !== undefined) { + return record.by_field_value; + } + + if (record.over_field_value !== undefined) { + return record.over_field_value; + } + + if (record.partition_field_value !== undefined) { + return record.partition_field_value; + } + + return undefined; +} + +// Returns the list of partitioning entity fields for the source record as a list +// of objects in the form { fieldName: airline, fieldValue: AAL, fieldType: partition } +export function getEntityFieldList(record: AnomalyRecordDoc): EntityField[] { + const entityFields: EntityField[] = []; + if (record.partition_field_name !== undefined) { + entityFields.push({ + fieldName: record.partition_field_name, + fieldValue: record.partition_field_value, + fieldType: ENTITY_FIELD_TYPE.PARTITON, + }); + } + + if (record.over_field_name !== undefined) { + entityFields.push({ + fieldName: record.over_field_name, + fieldValue: record.over_field_value, + fieldType: ENTITY_FIELD_TYPE.OVER, + }); + } + + // For jobs with by and over fields, don't add the 'by' field as this + // field will only be added to the top-level fields for record type results + // if it also an influencer over the bucket. + if (record.by_field_name !== undefined && record.over_field_name === undefined) { + entityFields.push({ + fieldName: record.by_field_name, + fieldValue: record.by_field_value, + fieldType: ENTITY_FIELD_TYPE.BY, + }); + } + + return entityFields; +} + +// Returns whether actual values should be displayed for a record with the specified function description. +// Note that the 'function' field in a record contains what the user entered e.g. 'high_count', +// whereas the 'function_description' field holds a ML-built display hint for function e.g. 'count'. +export function showActualForFunction(functionDescription: string): boolean { + return DISPLAY_ACTUAL_FUNCTIONS.indexOf(functionDescription) > -1; +} + +// Returns whether typical values should be displayed for a record with the specified function description. +// Note that the 'function' field in a record contains what the user entered e.g. 'high_count', +// whereas the 'function_description' field holds a ML-built display hint for function e.g. 'count'. +export function showTypicalForFunction(functionDescription: string): boolean { + return DISPLAY_TYPICAL_FUNCTIONS.indexOf(functionDescription) > -1; +} + +// Returns whether a rule can be configured against the specified anomaly. +export function isRuleSupported(record: AnomalyRecordDoc): boolean { + // A rule can be configured with a numeric condition if the function supports it, + // and/or with scope if there is a partitioning fields. + return ( + CONDITIONS_NOT_SUPPORTED_FUNCTIONS.indexOf(record.function) === -1 || + getEntityFieldName(record) !== undefined + ); +} + +// Two functions for converting aggregation type names. +// ML and ES use different names for the same function. +// Possible values for ML aggregation type are (defined in lib/model/CAnomalyDetector.cc): +// count +// distinct_count +// rare +// info_content +// mean +// median +// min +// max +// varp +// sum +// lat_long +// time +// The input to toES and the output from toML correspond to the value of the +// function_description field of anomaly records. +export const aggregationTypeTransform = { + toES(oldAggType: string): string { + let newAggType = oldAggType; + + if (newAggType === 'mean') { + newAggType = 'avg'; + } else if (newAggType === 'distinct_count') { + newAggType = 'cardinality'; + } else if (newAggType === 'median') { + newAggType = 'percentiles'; + } + + return newAggType; + }, + toML(oldAggType: string): string { + let newAggType = oldAggType; + + if (newAggType === 'avg') { + newAggType = 'mean'; + } else if (newAggType === 'cardinality') { + newAggType = 'distinct_count'; + } else if (newAggType === 'percentiles') { + newAggType = 'median'; + } + + return newAggType; + }, +}; diff --git a/x-pack/plugins/ml/common/util/job_utils.test.js b/x-pack/plugins/ml/common/util/job_utils.test.js new file mode 100644 index 0000000000000..a5df160bdf5ca --- /dev/null +++ b/x-pack/plugins/ml/common/util/job_utils.test.js @@ -0,0 +1,579 @@ +/* + * 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 { + calculateDatafeedFrequencyDefaultSeconds, + isTimeSeriesViewJob, + isTimeSeriesViewDetector, + isSourceDataChartableForDetector, + isModelPlotChartableForDetector, + getPartitioningFieldNames, + isModelPlotEnabled, + isJobVersionGte, + mlFunctionToESAggregation, + isJobIdValid, + ML_MEDIAN_PERCENTS, + prefixDatafeedId, + getSafeAggregationName, + getLatestDataOrBucketTimestamp, +} from './job_utils'; + +describe('ML - job utils', () => { + describe('calculateDatafeedFrequencyDefaultSeconds', () => { + test('returns correct frequency for 119', () => { + const result = calculateDatafeedFrequencyDefaultSeconds(119); + expect(result).toBe(60); + }); + test('returns correct frequency for 120', () => { + const result = calculateDatafeedFrequencyDefaultSeconds(120); + expect(result).toBe(60); + }); + test('returns correct frequency for 300', () => { + const result = calculateDatafeedFrequencyDefaultSeconds(300); + expect(result).toBe(150); + }); + test('returns correct frequency for 601', () => { + const result = calculateDatafeedFrequencyDefaultSeconds(601); + expect(result).toBe(300); + }); + test('returns correct frequency for 43200', () => { + const result = calculateDatafeedFrequencyDefaultSeconds(43200); + expect(result).toBe(600); + }); + test('returns correct frequency for 43201', () => { + const result = calculateDatafeedFrequencyDefaultSeconds(43201); + expect(result).toBe(3600); + }); + }); + + describe('isTimeSeriesViewJob', () => { + test('returns true when job has a single detector with a metric function', () => { + const job = { + analysis_config: { + detectors: [ + { + function: 'high_count', + partition_field_name: 'status', + detector_description: 'High count status code', + }, + ], + }, + }; + + expect(isTimeSeriesViewJob(job)).toBe(true); + }); + + test('returns true when job has at least one detector with a metric function', () => { + const job = { + analysis_config: { + detectors: [ + { + function: 'high_count', + partition_field_name: 'status', + detector_description: 'High count status code', + }, + { + function: 'freq_rare', + by_field_name: 'uri', + over_field_name: 'clientip', + detector_description: 'Freq rare URI', + }, + ], + }, + }; + + expect(isTimeSeriesViewJob(job)).toBe(true); + }); + + test('returns false when job does not have at least one detector with a metric function', () => { + const job = { + analysis_config: { + detectors: [ + { + function: 'varp', + by_field_name: 'responsetime', + detector_description: 'Varp responsetime', + }, + { + function: 'freq_rare', + by_field_name: 'uri', + over_field_name: 'clientip', + detector_description: 'Freq rare URI', + }, + ], + }, + }; + + expect(isTimeSeriesViewJob(job)).toBe(false); + }); + + test('returns false when job has a single count by category detector', () => { + const job = { + analysis_config: { + detectors: [ + { + function: 'count', + by_field_name: 'mlcategory', + detector_description: 'Count by category', + }, + ], + }, + }; + + expect(isTimeSeriesViewJob(job)).toBe(false); + }); + }); + + describe('isTimeSeriesViewDetector', () => { + const job = { + analysis_config: { + detectors: [ + { + function: 'sum', + field_name: 'bytes', + partition_field_name: 'clientip', + detector_description: 'High bytes client IP', + }, + { + function: 'freq_rare', + by_field_name: 'uri', + over_field_name: 'clientip', + detector_description: 'Freq rare URI', + }, + { + function: 'count', + by_field_name: 'mlcategory', + detector_description: 'Count by category', + }, + { function: 'count', by_field_name: 'hrd', detector_description: 'count by hrd' }, + { function: 'mean', field_name: 'NetworkDiff', detector_description: 'avg NetworkDiff' }, + ], + }, + datafeed_config: { + script_fields: { + hrd: { + script: { + inline: 'return domainSplit(doc["query"].value, params).get(1);', + lang: 'painless', + }, + }, + NetworkDiff: { + script: { + source: 'doc["NetworkOut"].value - doc["NetworkIn"].value', + lang: 'painless', + }, + }, + }, + }, + }; + + test('returns true for a detector with a metric function', () => { + expect(isTimeSeriesViewDetector(job, 0)).toBe(true); + }); + + test('returns false for a detector with a non-metric function', () => { + expect(isTimeSeriesViewDetector(job, 1)).toBe(false); + }); + + test('returns false for a detector using count on an mlcategory field', () => { + expect(isTimeSeriesViewDetector(job, 2)).toBe(false); + }); + + test('returns false for a detector using a script field as a by field', () => { + expect(isTimeSeriesViewDetector(job, 3)).toBe(false); + }); + + test('returns false for a detector using a script field as a metric field_name', () => { + expect(isTimeSeriesViewDetector(job, 4)).toBe(false); + }); + }); + + describe('isSourceDataChartableForDetector', () => { + const job = { + analysis_config: { + detectors: [ + { function: 'count' }, // 0 + { function: 'low_count' }, // 1 + { function: 'high_count' }, // 2 + { function: 'non_zero_count' }, // 3 + { function: 'low_non_zero_count' }, // 4 + { function: 'high_non_zero_count' }, // 5 + { function: 'distinct_count' }, // 6 + { function: 'low_distinct_count' }, // 7 + { function: 'high_distinct_count' }, // 8 + { function: 'metric' }, // 9 + { function: 'mean' }, // 10 + { function: 'low_mean' }, // 11 + { function: 'high_mean' }, // 12 + { function: 'median' }, // 13 + { function: 'low_median' }, // 14 + { function: 'high_median' }, // 15 + { function: 'min' }, // 16 + { function: 'max' }, // 17 + { function: 'sum' }, // 18 + { function: 'low_sum' }, // 19 + { function: 'high_sum' }, // 20 + { function: 'non_null_sum' }, // 21 + { function: 'low_non_null_sum' }, // 22 + { function: 'high_non_null_sum' }, // 23 + { function: 'rare' }, // 24 + { function: 'count', by_field_name: 'mlcategory' }, // 25 + { function: 'count', by_field_name: 'hrd' }, // 26 + { function: 'freq_rare' }, // 27 + { function: 'info_content' }, // 28 + { function: 'low_info_content' }, // 29 + { function: 'high_info_content' }, // 30 + { function: 'varp' }, // 31 + { function: 'low_varp' }, // 32 + { function: 'high_varp' }, // 33 + { function: 'time_of_day' }, // 34 + { function: 'time_of_week' }, // 35 + { function: 'lat_long' }, // 36 + { function: 'mean', field_name: 'NetworkDiff' }, // 37 + ], + }, + datafeed_config: { + script_fields: { + hrd: { + script: { + inline: 'return domainSplit(doc["query"].value, params).get(1);', + lang: 'painless', + }, + }, + NetworkDiff: { + script: { + source: 'doc["NetworkOut"].value - doc["NetworkIn"].value', + lang: 'painless', + }, + }, + }, + }, + }; + + test('returns true for expected detectors', () => { + expect(isSourceDataChartableForDetector(job, 0)).toBe(true); + expect(isSourceDataChartableForDetector(job, 1)).toBe(true); + expect(isSourceDataChartableForDetector(job, 2)).toBe(true); + expect(isSourceDataChartableForDetector(job, 3)).toBe(true); + expect(isSourceDataChartableForDetector(job, 4)).toBe(true); + expect(isSourceDataChartableForDetector(job, 5)).toBe(true); + expect(isSourceDataChartableForDetector(job, 6)).toBe(true); + expect(isSourceDataChartableForDetector(job, 7)).toBe(true); + expect(isSourceDataChartableForDetector(job, 8)).toBe(true); + expect(isSourceDataChartableForDetector(job, 9)).toBe(true); + expect(isSourceDataChartableForDetector(job, 10)).toBe(true); + expect(isSourceDataChartableForDetector(job, 11)).toBe(true); + expect(isSourceDataChartableForDetector(job, 12)).toBe(true); + expect(isSourceDataChartableForDetector(job, 13)).toBe(true); + expect(isSourceDataChartableForDetector(job, 14)).toBe(true); + expect(isSourceDataChartableForDetector(job, 15)).toBe(true); + expect(isSourceDataChartableForDetector(job, 16)).toBe(true); + expect(isSourceDataChartableForDetector(job, 17)).toBe(true); + expect(isSourceDataChartableForDetector(job, 18)).toBe(true); + expect(isSourceDataChartableForDetector(job, 19)).toBe(true); + expect(isSourceDataChartableForDetector(job, 20)).toBe(true); + expect(isSourceDataChartableForDetector(job, 21)).toBe(true); + expect(isSourceDataChartableForDetector(job, 22)).toBe(true); + expect(isSourceDataChartableForDetector(job, 23)).toBe(true); + expect(isSourceDataChartableForDetector(job, 24)).toBe(true); + }); + + test('returns false for expected detectors', () => { + expect(isSourceDataChartableForDetector(job, 25)).toBe(false); + expect(isSourceDataChartableForDetector(job, 26)).toBe(false); + expect(isSourceDataChartableForDetector(job, 27)).toBe(false); + expect(isSourceDataChartableForDetector(job, 28)).toBe(false); + expect(isSourceDataChartableForDetector(job, 29)).toBe(false); + expect(isSourceDataChartableForDetector(job, 30)).toBe(false); + expect(isSourceDataChartableForDetector(job, 31)).toBe(false); + expect(isSourceDataChartableForDetector(job, 32)).toBe(false); + expect(isSourceDataChartableForDetector(job, 33)).toBe(false); + expect(isSourceDataChartableForDetector(job, 34)).toBe(false); + expect(isSourceDataChartableForDetector(job, 35)).toBe(false); + expect(isSourceDataChartableForDetector(job, 36)).toBe(false); + expect(isSourceDataChartableForDetector(job, 37)).toBe(false); + }); + }); + + describe('isModelPlotChartableForDetector', () => { + const job1 = { + analysis_config: { + detectors: [{ function: 'count' }], + }, + }; + + const job2 = { + analysis_config: { + detectors: [{ function: 'count' }, { function: 'info_content' }], + }, + model_plot_config: { + enabled: true, + }, + }; + + test('returns false when model plot is not enabled', () => { + expect(isModelPlotChartableForDetector(job1, 0)).toBe(false); + }); + + test('returns true for count detector when model plot is enabled', () => { + expect(isModelPlotChartableForDetector(job2, 0)).toBe(true); + }); + + test('returns true for info_content detector when model plot is enabled', () => { + expect(isModelPlotChartableForDetector(job2, 1)).toBe(true); + }); + }); + + describe('getPartitioningFieldNames', () => { + const job = { + analysis_config: { + detectors: [ + { + function: 'count', + detector_description: 'count', + }, + { + function: 'count', + partition_field_name: 'clientip', + detector_description: 'Count by clientip', + }, + { + function: 'freq_rare', + by_field_name: 'uri', + over_field_name: 'clientip', + detector_description: 'Freq rare URI', + }, + { + function: 'sum', + field_name: 'bytes', + by_field_name: 'uri', + over_field_name: 'clientip', + partition_field_name: 'method', + detector_description: 'sum bytes', + }, + ], + }, + }; + + test('returns empty array for a detector with no partitioning fields', () => { + const resp = getPartitioningFieldNames(job, 0); + expect(resp).toEqual([]); + }); + + test('returns expected array for a detector with a partition field', () => { + const resp = getPartitioningFieldNames(job, 1); + expect(resp).toEqual(['clientip']); + }); + + test('returns expected array for a detector with by and over fields', () => { + const resp = getPartitioningFieldNames(job, 2); + expect(resp).toEqual(['uri', 'clientip']); + }); + + test('returns expected array for a detector with partition, by and over fields', () => { + const resp = getPartitioningFieldNames(job, 3); + expect(resp).toEqual(['method', 'uri', 'clientip']); + }); + }); + + describe('isModelPlotEnabled', () => { + test('returns true for a job in which model plot has been enabled', () => { + const job = { + analysis_config: { + detectors: [ + { + function: 'high_count', + partition_field_name: 'status', + detector_description: 'High count status code', + }, + ], + }, + model_plot_config: { + enabled: true, + }, + }; + + expect(isModelPlotEnabled(job, 0)).toBe(true); + }); + + test('returns expected values for a job in which model plot has been enabled with terms', () => { + const job = { + analysis_config: { + detectors: [ + { + function: 'max', + field_name: 'responsetime', + partition_field_name: 'country', + by_field_name: 'airline', + }, + ], + }, + model_plot_config: { + enabled: true, + terms: 'US,AAL', + }, + }; + + expect( + isModelPlotEnabled(job, 0, [ + { fieldName: 'country', fieldValue: 'US' }, + { fieldName: 'airline', fieldValue: 'AAL' }, + ]) + ).toBe(true); + expect(isModelPlotEnabled(job, 0, [{ fieldName: 'country', fieldValue: 'US' }])).toBe(false); + expect( + isModelPlotEnabled(job, 0, [ + { fieldName: 'country', fieldValue: 'GB' }, + { fieldName: 'airline', fieldValue: 'AAL' }, + ]) + ).toBe(false); + expect( + isModelPlotEnabled(job, 0, [ + { fieldName: 'country', fieldValue: 'JP' }, + { fieldName: 'airline', fieldValue: 'JAL' }, + ]) + ).toBe(false); + }); + + test('returns true for jobs in which model plot has not been enabled', () => { + const job1 = { + analysis_config: { + detectors: [ + { + function: 'high_count', + partition_field_name: 'status', + detector_description: 'High count status code', + }, + ], + }, + model_plot_config: { + enabled: false, + }, + }; + const job2 = {}; + + expect(isModelPlotEnabled(job1, 0)).toBe(false); + expect(isModelPlotEnabled(job2, 0)).toBe(false); + }); + }); + + describe('isJobVersionGte', () => { + const job = { + job_version: '6.1.1', + }; + + test('returns true for later job version', () => { + expect(isJobVersionGte(job, '6.1.0')).toBe(true); + }); + test('returns true for equal job version', () => { + expect(isJobVersionGte(job, '6.1.1')).toBe(true); + }); + test('returns false for earlier job version', () => { + expect(isJobVersionGte(job, '6.1.2')).toBe(false); + }); + }); + + describe('mlFunctionToESAggregation', () => { + test('returns correct ES aggregation type for ML function', () => { + expect(mlFunctionToESAggregation('count')).toBe('count'); + expect(mlFunctionToESAggregation('low_count')).toBe('count'); + expect(mlFunctionToESAggregation('high_count')).toBe('count'); + expect(mlFunctionToESAggregation('non_zero_count')).toBe('count'); + expect(mlFunctionToESAggregation('low_non_zero_count')).toBe('count'); + expect(mlFunctionToESAggregation('high_non_zero_count')).toBe('count'); + expect(mlFunctionToESAggregation('distinct_count')).toBe('cardinality'); + expect(mlFunctionToESAggregation('low_distinct_count')).toBe('cardinality'); + expect(mlFunctionToESAggregation('high_distinct_count')).toBe('cardinality'); + expect(mlFunctionToESAggregation('metric')).toBe('avg'); + expect(mlFunctionToESAggregation('mean')).toBe('avg'); + expect(mlFunctionToESAggregation('low_mean')).toBe('avg'); + expect(mlFunctionToESAggregation('high_mean')).toBe('avg'); + expect(mlFunctionToESAggregation('min')).toBe('min'); + expect(mlFunctionToESAggregation('max')).toBe('max'); + expect(mlFunctionToESAggregation('sum')).toBe('sum'); + expect(mlFunctionToESAggregation('low_sum')).toBe('sum'); + expect(mlFunctionToESAggregation('high_sum')).toBe('sum'); + expect(mlFunctionToESAggregation('non_null_sum')).toBe('sum'); + expect(mlFunctionToESAggregation('low_non_null_sum')).toBe('sum'); + expect(mlFunctionToESAggregation('high_non_null_sum')).toBe('sum'); + expect(mlFunctionToESAggregation('rare')).toBe('count'); + expect(mlFunctionToESAggregation('freq_rare')).toBe(null); + expect(mlFunctionToESAggregation('info_content')).toBe(null); + expect(mlFunctionToESAggregation('low_info_content')).toBe(null); + expect(mlFunctionToESAggregation('high_info_content')).toBe(null); + expect(mlFunctionToESAggregation('median')).toBe('percentiles'); + expect(mlFunctionToESAggregation('low_median')).toBe('percentiles'); + expect(mlFunctionToESAggregation('high_median')).toBe('percentiles'); + expect(mlFunctionToESAggregation('varp')).toBe(null); + expect(mlFunctionToESAggregation('low_varp')).toBe(null); + expect(mlFunctionToESAggregation('high_varp')).toBe(null); + expect(mlFunctionToESAggregation('time_of_day')).toBe(null); + expect(mlFunctionToESAggregation('time_of_week')).toBe(null); + expect(mlFunctionToESAggregation('lat_long')).toBe(null); + }); + }); + + describe('isJobIdValid', () => { + test('returns true for job id: "good_job-name"', () => { + expect(isJobIdValid('good_job-name')).toBe(true); + }); + test('returns false for job id: "_bad_job-name"', () => { + expect(isJobIdValid('_bad_job-name')).toBe(false); + }); + test('returns false for job id: "bad_job-name_"', () => { + expect(isJobIdValid('bad_job-name_')).toBe(false); + }); + test('returns false for job id: "-bad_job-name"', () => { + expect(isJobIdValid('-bad_job-name')).toBe(false); + }); + test('returns false for job id: "bad_job-name-"', () => { + expect(isJobIdValid('bad_job-name-')).toBe(false); + }); + test('returns false for job id: "bad&job-name"', () => { + expect(isJobIdValid('bad&job-name')).toBe(false); + }); + }); + + describe('ML_MEDIAN_PERCENTS', () => { + test("is '50.0'", () => { + expect(ML_MEDIAN_PERCENTS).toBe('50.0'); + }); + }); + + describe('prefixDatafeedId', () => { + test('returns datafeed-prefix-job from datafeed-job"', () => { + expect(prefixDatafeedId('datafeed-job', 'prefix-')).toBe('datafeed-prefix-job'); + }); + + test('returns datafeed-prefix-job from job"', () => { + expect(prefixDatafeedId('job', 'prefix-')).toBe('datafeed-prefix-job'); + }); + }); + + describe('getSafeAggregationName', () => { + test('"foo" should be "foo"', () => { + expect(getSafeAggregationName('foo', 0)).toBe('foo'); + }); + test('"foo.bar" should be "foo.bar"', () => { + expect(getSafeAggregationName('foo.bar', 0)).toBe('foo.bar'); + }); + test('"foo&bar" should be "field_0"', () => { + expect(getSafeAggregationName('foo&bar', 0)).toBe('field_0'); + }); + }); + + describe('getLatestDataOrBucketTimestamp', () => { + test('returns expected value when no gap in data at end of bucket processing', () => { + expect(getLatestDataOrBucketTimestamp(1549929594000, 1549928700000)).toBe(1549929594000); + }); + test('returns expected value when there is a gap in data at end of bucket processing', () => { + expect(getLatestDataOrBucketTimestamp(1549929594000, 1562256600000)).toBe(1562256600000); + }); + test('returns expected value when job has not run', () => { + expect(getLatestDataOrBucketTimestamp(undefined, undefined)).toBe(undefined); + }); + }); +}); diff --git a/x-pack/plugins/ml/package.json b/x-pack/plugins/ml/package.json new file mode 100644 index 0000000000000..739dd806fcbb9 --- /dev/null +++ b/x-pack/plugins/ml/package.json @@ -0,0 +1,15 @@ +{ + "author": "Elastic", + "name": "ml", + "version": "0.0.0", + "private": true, + "license": "Elastic-License", + "scripts": { + "build:apiDocScripts": "cd server/routes/apidoc_scripts && tsc", + "apiDocs": "yarn build:apiDocScripts && cd ./server/routes/ && apidoc --parse-workers apischema=./apidoc_scripts/target/schema_worker.js --parse-parsers apischema=./apidoc_scripts/target/schema_parser.js --parse-filters apiversion=./apidoc_scripts/target/version_filter.js -i . -o ../routes_doc && apidoc-markdown -p ../routes_doc -o ../routes_doc/ML_API.md" + }, + "devDependencies": { + "apidoc": "^0.20.1", + "apidoc-markdown": "^5.0.0" + } +} diff --git a/x-pack/plugins/ml/public/application/components/custom_hooks/index.ts b/x-pack/plugins/ml/public/application/components/custom_hooks/index.ts index dfd74d8970cb4..ffead802bd6f9 100644 --- a/x-pack/plugins/ml/public/application/components/custom_hooks/index.ts +++ b/x-pack/plugins/ml/public/application/components/custom_hooks/index.ts @@ -5,4 +5,3 @@ */ export { usePartialState } from './use_partial_state'; -export { useXJsonMode, xJsonMode } from './use_x_json_mode'; diff --git a/x-pack/plugins/ml/public/application/components/custom_hooks/use_x_json_mode.ts b/x-pack/plugins/ml/public/application/components/custom_hooks/use_x_json_mode.ts deleted file mode 100644 index c979632db54d6..0000000000000 --- a/x-pack/plugins/ml/public/application/components/custom_hooks/use_x_json_mode.ts +++ /dev/null @@ -1,26 +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 { useState } from 'react'; -import { - collapseLiteralStrings, - expandLiteralStrings, - XJsonMode, -} from '../../../../shared_imports'; - -// @ts-ignore -export const xJsonMode = new XJsonMode(); - -export const useXJsonMode = (json: string) => { - const [xJson, setXJson] = useState(expandLiteralStrings(json)); - - return { - xJson, - setXJson, - xJsonMode, - convertToJson: collapseLiteralStrings, - }; -}; diff --git a/x-pack/plugins/ml/public/application/components/rule_editor/__tests__/utils.js b/x-pack/plugins/ml/public/application/components/rule_editor/__tests__/utils.js deleted file mode 100644 index b5f9bdeaa12aa..0000000000000 --- a/x-pack/plugins/ml/public/application/components/rule_editor/__tests__/utils.js +++ /dev/null @@ -1,126 +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 expect from '@kbn/expect'; -import { isValidRule, buildRuleDescription, getAppliesToValueFromAnomaly } from '../utils'; -import { - ACTION, - APPLIES_TO, - OPERATOR, - FILTER_TYPE, -} from '../../../../../common/constants/detector_rule'; - -describe('ML - rule editor utils', () => { - const ruleWithCondition = { - actions: [ACTION.SKIP_RESULT], - conditions: [ - { - applies_to: APPLIES_TO.ACTUAL, - operator: OPERATOR.GREATER_THAN, - value: 10, - }, - ], - }; - - const ruleWithScope = { - actions: [ACTION.SKIP_RESULT], - scope: { - instance: { - filter_id: 'test_aws_instances', - filter_type: FILTER_TYPE.INCLUDE, - enabled: true, - }, - }, - }; - - const ruleWithConditionAndScope = { - actions: [ACTION.SKIP_RESULT], - conditions: [ - { - applies_to: APPLIES_TO.TYPICAL, - operator: OPERATOR.LESS_THAN, - value: 100, - }, - ], - scope: { - instance: { - filter_id: 'test_aws_instances', - filter_type: FILTER_TYPE.EXCLUDE, - enabled: true, - }, - }, - }; - - describe('isValidRule', () => { - it('returns true for a rule with an action and a condition', () => { - expect(isValidRule(ruleWithCondition)).to.be(true); - }); - - it('returns true for a rule with an action and scope', () => { - expect(isValidRule(ruleWithScope)).to.be(true); - }); - - it('returns true for a rule with an action, scope and condition', () => { - expect(isValidRule(ruleWithConditionAndScope)).to.be(true); - }); - - it('returns false for a rule with no action', () => { - const ruleWithNoAction = { - actions: [], - conditions: [ - { - applies_to: APPLIES_TO.TYPICAL, - operator: OPERATOR.LESS_THAN, - value: 100, - }, - ], - }; - - expect(isValidRule(ruleWithNoAction)).to.be(false); - }); - - it('returns false for a rule with no scope or conditions', () => { - const ruleWithNoScopeOrCondition = { - actions: [ACTION.SKIP_RESULT], - }; - - expect(isValidRule(ruleWithNoScopeOrCondition)).to.be(false); - }); - }); - - describe('buildRuleDescription', () => { - it('returns expected rule descriptions', () => { - expect(buildRuleDescription(ruleWithCondition)).to.be( - 'skip result when actual is greater than 10' - ); - expect(buildRuleDescription(ruleWithScope)).to.be( - 'skip result when instance is in test_aws_instances' - ); - expect(buildRuleDescription(ruleWithConditionAndScope)).to.be( - 'skip result when typical is less than 100 AND instance is not in test_aws_instances' - ); - }); - }); - - describe('getAppliesToValueFromAnomaly', () => { - const anomaly = { - actual: [210], - typical: [1.23], - }; - - it('returns expected actual value from an anomaly', () => { - expect(getAppliesToValueFromAnomaly(anomaly, APPLIES_TO.ACTUAL)).to.be(210); - }); - - it('returns expected typical value from an anomaly', () => { - expect(getAppliesToValueFromAnomaly(anomaly, APPLIES_TO.TYPICAL)).to.be(1.23); - }); - - it('returns expected diff from typical value from an anomaly', () => { - expect(getAppliesToValueFromAnomaly(anomaly, APPLIES_TO.DIFF_FROM_TYPICAL)).to.be(208.77); - }); - }); -}); diff --git a/x-pack/plugins/ml/public/application/components/rule_editor/utils.test.js b/x-pack/plugins/ml/public/application/components/rule_editor/utils.test.js new file mode 100644 index 0000000000000..18e382f8fe5e8 --- /dev/null +++ b/x-pack/plugins/ml/public/application/components/rule_editor/utils.test.js @@ -0,0 +1,125 @@ +/* + * 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 { isValidRule, buildRuleDescription, getAppliesToValueFromAnomaly } from './utils'; +import { + ACTION, + APPLIES_TO, + OPERATOR, + FILTER_TYPE, +} from '../../../../common/constants/detector_rule'; + +describe('ML - rule editor utils', () => { + const ruleWithCondition = { + actions: [ACTION.SKIP_RESULT], + conditions: [ + { + applies_to: APPLIES_TO.ACTUAL, + operator: OPERATOR.GREATER_THAN, + value: 10, + }, + ], + }; + + const ruleWithScope = { + actions: [ACTION.SKIP_RESULT], + scope: { + instance: { + filter_id: 'test_aws_instances', + filter_type: FILTER_TYPE.INCLUDE, + enabled: true, + }, + }, + }; + + const ruleWithConditionAndScope = { + actions: [ACTION.SKIP_RESULT], + conditions: [ + { + applies_to: APPLIES_TO.TYPICAL, + operator: OPERATOR.LESS_THAN, + value: 100, + }, + ], + scope: { + instance: { + filter_id: 'test_aws_instances', + filter_type: FILTER_TYPE.EXCLUDE, + enabled: true, + }, + }, + }; + + describe('isValidRule', () => { + test('returns true for a rule with an action and a condition', () => { + expect(isValidRule(ruleWithCondition)).toBe(true); + }); + + test('returns true for a rule with an action and scope', () => { + expect(isValidRule(ruleWithScope)).toBe(true); + }); + + test('returns true for a rule with an action, scope and condition', () => { + expect(isValidRule(ruleWithConditionAndScope)).toBe(true); + }); + + test('returns false for a rule with no action', () => { + const ruleWithNoAction = { + actions: [], + conditions: [ + { + applies_to: APPLIES_TO.TYPICAL, + operator: OPERATOR.LESS_THAN, + value: 100, + }, + ], + }; + + expect(isValidRule(ruleWithNoAction)).toBe(false); + }); + + test('returns false for a rule with no scope or conditions', () => { + const ruleWithNoScopeOrCondition = { + actions: [ACTION.SKIP_RESULT], + }; + + expect(isValidRule(ruleWithNoScopeOrCondition)).toBe(false); + }); + }); + + describe('buildRuleDescription', () => { + test('returns expected rule descriptions', () => { + expect(buildRuleDescription(ruleWithCondition)).toBe( + 'skip result when actual is greater than 10' + ); + expect(buildRuleDescription(ruleWithScope)).toBe( + 'skip result when instance is in test_aws_instances' + ); + expect(buildRuleDescription(ruleWithConditionAndScope)).toBe( + 'skip result when typical is less than 100 AND instance is not in test_aws_instances' + ); + }); + }); + + describe('getAppliesToValueFromAnomaly', () => { + const anomaly = { + actual: [210], + typical: [1.23], + }; + + test('returns expected actual value from an anomaly', () => { + expect(getAppliesToValueFromAnomaly(anomaly, APPLIES_TO.ACTUAL)).toBe(210); + }); + + test('returns expected typical value from an anomaly', () => { + expect(getAppliesToValueFromAnomaly(anomaly, APPLIES_TO.TYPICAL)).toBe(1.23); + }); + + test('returns expected diff from typical value from an anomaly', () => { + expect(getAppliesToValueFromAnomaly(anomaly, APPLIES_TO.DIFF_FROM_TYPICAL)).toBe(208.77); + }); + }); +}); 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 511ebb7e1647a..fb3b2b3519947 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 @@ -13,7 +13,6 @@ import { ml } from '../../services/ml_api_service'; import { Dictionary } from '../../../../common/types/common'; import { getErrorMessage } from '../../../../common/util/errors'; import { SavedSearchQuery } from '../../contexts/ml'; -import { SortDirection } from '../../components/ml_in_memory_table'; export type IndexName = string; export type IndexPattern = string; @@ -54,12 +53,8 @@ export interface ClassificationAnalysis { } export interface LoadExploreDataArg { - field: string; - direction: SortDirection; + filterByIsTraining?: boolean; searchQuery: SavedSearchQuery; - requiresKeyword?: boolean; - pageIndex?: number; - pageSize?: number; } export const SEARCH_SIZE = 1000; @@ -272,6 +267,11 @@ export const isResultsSearchBoolQuery = (arg: any): arg is ResultsSearchBoolQuer return keys.length === 1 && keys[0] === 'bool'; }; +export const isQueryStringQuery = (arg: any): arg is QueryStringQuery => { + const keys = Object.keys(arg); + return keys.length === 1 && keys[0] === 'query_string'; +}; + export const isRegressionEvaluateResponse = (arg: any): arg is RegressionEvaluateResponse => { const keys = Object.keys(arg); return ( @@ -396,6 +396,10 @@ interface ResultsSearchTermQuery { term: Dictionary<any>; } +interface QueryStringQuery { + query_string: Dictionary<any>; +} + export type ResultsSearchQuery = ResultsSearchBoolQuery | ResultsSearchTermQuery | SavedSearchQuery; export function getEvalQueryBody({ @@ -405,20 +409,44 @@ export function getEvalQueryBody({ ignoreDefaultQuery, }: { resultsField: string; - isTraining: boolean; + isTraining?: boolean; searchQuery?: ResultsSearchQuery; ignoreDefaultQuery?: boolean; }) { - let query: ResultsSearchQuery = { + let query: any; + + const trainingQuery: ResultsSearchQuery = { term: { [`${resultsField}.is_training`]: { value: isTraining } }, }; - if (searchQuery !== undefined && ignoreDefaultQuery === true) { - query = searchQuery; - } else if (searchQuery !== undefined && isResultsSearchBoolQuery(searchQuery)) { - const searchQueryClone = cloneDeep(searchQuery); - searchQueryClone.bool.must.push(query); + const searchQueryClone = cloneDeep(searchQuery); + + if (isResultsSearchBoolQuery(searchQueryClone)) { + if (searchQueryClone.bool.must === undefined) { + searchQueryClone.bool.must = []; + } + + if (isTraining !== undefined) { + searchQueryClone.bool.must.push(trainingQuery); + } + query = searchQueryClone; + } else if (isQueryStringQuery(searchQueryClone)) { + query = { + bool: { + must: [searchQueryClone], + }, + }; + if (isTraining !== undefined) { + query.bool.must.push(trainingQuery); + } + } else { + // Not a bool or string query so we need to create it so can add the trainingQuery + query = { + bool: { + must: isTraining !== undefined ? [trainingQuery] : [], + }, + }; } return query; } @@ -434,7 +462,7 @@ interface EvaluateMetrics { } interface LoadEvalDataConfig { - isTraining: boolean; + isTraining?: boolean; index: string; dependentVariable: string; resultsField: string; @@ -513,7 +541,7 @@ interface TrackTotalHitsSearchResponse { interface LoadDocsCountConfig { ignoreDefaultQuery?: boolean; - isTraining: boolean; + isTraining?: boolean; searchQuery: SavedSearchQuery; resultsField: string; destIndex: string; diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/common/fields.ts b/x-pack/plugins/ml/public/application/data_frame_analytics/common/fields.ts index 92d8731959895..f165669bdd674 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/common/fields.ts +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/common/fields.ts @@ -246,8 +246,15 @@ export const getDefaultFieldsFromJobCaps = ( fields: Field[], jobConfig: DataFrameAnalyticsConfig, needsDestIndexFields: boolean -): { selectedFields: Field[]; docFields: Field[]; depVarType?: ES_FIELD_TYPES } => { - const fieldsObj = { selectedFields: [], docFields: [] }; +): { + selectedFields: Field[]; + docFields: Field[]; + depVarType?: ES_FIELD_TYPES; +} => { + const fieldsObj = { + selectedFields: [], + docFields: [], + }; if (fields.length === 0) { return fieldsObj; } @@ -267,38 +274,37 @@ export const getDefaultFieldsFromJobCaps = ( const featureImportanceFields = []; if ((numTopFeatureImportanceValues ?? 0) > 0) { - featureImportanceFields.push( - ...fields.map(d => ({ - id: `${resultsField}.feature_importance.${d.id}`, - name: `${resultsField}.feature_importance.${d.name}`, - type: KBN_FIELD_TYPES.NUMBER, - })) - ); + featureImportanceFields.push({ + id: `${resultsField}.feature_importance`, + name: `${resultsField}.feature_importance`, + type: KBN_FIELD_TYPES.NUMBER, + }); } + let allFields: any = []; // Only need to add these fields if we didn't use dest index pattern to get the fields - const allFields: any = - needsDestIndexFields === true - ? [ - { - id: `${resultsField}.is_training`, - name: `${resultsField}.is_training`, - type: ES_FIELD_TYPES.BOOLEAN, - }, - { id: predictedField, name: predictedField, type }, - ...featureImportanceFields, - ] - : []; - - allFields.push(...fields); + if (needsDestIndexFields === true) { + allFields.push( + { + id: `${resultsField}.is_training`, + name: `${resultsField}.is_training`, + type: ES_FIELD_TYPES.BOOLEAN, + }, + { id: predictedField, name: predictedField, type } + ); + } + + allFields.push(...fields, ...featureImportanceFields); allFields.sort(({ name: a }: { name: string }, { name: b }: { name: string }) => sortRegressionResultsFields(a, b, jobConfig) ); + // Remove feature_importance fields provided by dest index since feature_importance is an array the path is not valid + if (needsDestIndexFields === false) { + allFields = allFields.filter((field: any) => !field.name.includes('.feature_importance.')); + } let selectedFields = allFields.filter( - (field: any) => - field.name === predictedField || - (!field.name.includes('.keyword') && !field.name.includes('.feature_importance.')) + (field: any) => field.name === predictedField || !field.name.includes('.keyword') ); if (selectedFields.length > DEFAULT_REGRESSION_COLUMNS) { diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/classification_exploration/classification_exploration_data_grid.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/classification_exploration/classification_exploration_data_grid.tsx new file mode 100644 index 0000000000000..424fc002795ca --- /dev/null +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/classification_exploration/classification_exploration_data_grid.tsx @@ -0,0 +1,135 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { Dispatch, FC, SetStateAction, useCallback, useMemo } from 'react'; + +import { i18n } from '@kbn/i18n'; + +import { EuiDataGrid, EuiDataGridPaginationProps, EuiDataGridSorting } from '@elastic/eui'; + +import { euiDataGridStyle, euiDataGridToolbarSettings } from '../../../../common'; + +import { mlFieldFormatService } from '../../../../../services/field_format_service'; + +import { IndexPattern } from '../../../../../../../../../../src/plugins/data/public'; + +const PAGE_SIZE_OPTIONS = [5, 10, 25, 50]; + +type Pagination = Pick<EuiDataGridPaginationProps, 'pageIndex' | 'pageSize'>; +type TableItem = Record<string, any>; + +interface ExplorationDataGridProps { + colorRange?: (d: number) => string; + columns: any[]; + indexPattern: IndexPattern; + pagination: Pagination; + resultsField: string; + rowCount: number; + selectedFields: string[]; + setPagination: Dispatch<SetStateAction<Pagination>>; + setSelectedFields: Dispatch<SetStateAction<string[]>>; + setSortingColumns: Dispatch<SetStateAction<EuiDataGridSorting['columns']>>; + sortingColumns: EuiDataGridSorting['columns']; + tableItems: TableItem[]; +} + +export const ClassificationExplorationDataGrid: FC<ExplorationDataGridProps> = ({ + columns, + indexPattern, + pagination, + resultsField, + rowCount, + selectedFields, + setPagination, + setSelectedFields, + setSortingColumns, + sortingColumns, + tableItems, +}) => { + const renderCellValue = useMemo(() => { + return ({ rowIndex, columnId }: { rowIndex: number; columnId: string; setCellProps: any }) => { + const adjustedRowIndex = rowIndex - pagination.pageIndex * pagination.pageSize; + + const fullItem = tableItems[adjustedRowIndex]; + + if (fullItem === undefined) { + return null; + } + + let format: any; + + if (indexPattern !== undefined) { + format = mlFieldFormatService.getFieldFormatFromIndexPattern(indexPattern, columnId, ''); + } + + const cellValue = + fullItem.hasOwnProperty(columnId) && fullItem[columnId] !== undefined + ? fullItem[columnId] + : null; + + if (format !== undefined) { + return format.convert(cellValue, 'text'); + } + + if (typeof cellValue === 'string' || cellValue === null) { + return cellValue; + } + + if (typeof cellValue === 'boolean') { + return cellValue ? 'true' : 'false'; + } + + if (typeof cellValue === 'object' && cellValue !== null) { + return JSON.stringify(cellValue); + } + + return cellValue; + }; + }, [resultsField, rowCount, tableItems, pagination.pageIndex, pagination.pageSize]); + + const onChangeItemsPerPage = useCallback( + pageSize => { + setPagination(p => { + const pageIndex = Math.floor((p.pageSize * p.pageIndex) / pageSize); + return { pageIndex, pageSize }; + }); + }, + [setPagination] + ); + + const onChangePage = useCallback(pageIndex => setPagination(p => ({ ...p, pageIndex })), [ + setPagination, + ]); + + const onSort = useCallback(sc => setSortingColumns(sc), [setSortingColumns]); + + return ( + <EuiDataGrid + aria-label={i18n.translate( + 'xpack.ml.dataframe.analytics.classificationExploration.dataGridAriaLabel', + { + defaultMessage: 'Classification results table', + } + )} + columns={columns} + columnVisibility={{ + visibleColumns: selectedFields, + setVisibleColumns: setSelectedFields, + }} + gridStyle={euiDataGridStyle} + rowCount={rowCount} + renderCellValue={renderCellValue} + sorting={{ columns: sortingColumns, onSort }} + toolbarVisibility={euiDataGridToolbarSettings} + pagination={{ + ...pagination, + pageSizeOptions: PAGE_SIZE_OPTIONS, + onChangeItemsPerPage, + onChangePage, + }} + /> + ); +}; diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/classification_exploration/evaluate_panel.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/classification_exploration/evaluate_panel.tsx index 91dae49ba5c49..af90547606f82 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/classification_exploration/evaluate_panel.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/classification_exploration/evaluate_panel.tsx @@ -117,13 +117,7 @@ export const EvaluatePanel: FC<Props> = ({ jobConfig, jobStatus, searchQuery }) const resultsField = jobConfig.dest.results_field; let requiresKeyword = false; - const loadData = async ({ - isTrainingClause, - ignoreDefaultQuery = true, - }: { - isTrainingClause: { query: string; operator: string }; - ignoreDefaultQuery?: boolean; - }) => { + const loadData = async ({ isTraining }: { isTraining: boolean | undefined }) => { setIsLoading(true); try { @@ -134,19 +128,18 @@ export const EvaluatePanel: FC<Props> = ({ jobConfig, jobStatus, searchQuery }) } const evalData = await loadEvalData({ - isTraining: false, + isTraining, index, dependentVariable, resultsField, predictionFieldName, searchQuery, - ignoreDefaultQuery, jobType: ANALYSIS_CONFIG_TYPE.CLASSIFICATION, requiresKeyword, }); const docsCountResp = await loadDocsCount({ - isTraining: false, + isTraining, searchQuery, resultsField, destIndex: jobConfig.dest.index, @@ -225,29 +218,46 @@ export const EvaluatePanel: FC<Props> = ({ jobConfig, jobStatus, searchQuery }) }, [confusionMatrixData]); useEffect(() => { - const hasIsTrainingClause = - isResultsSearchBoolQuery(searchQuery) && - searchQuery.bool.must.filter( - (clause: any) => clause.match && clause.match[`${resultsField}.is_training`] !== undefined - ); - const isTrainingClause = - hasIsTrainingClause && - hasIsTrainingClause[0] && - hasIsTrainingClause[0].match[`${resultsField}.is_training`]; + let isTraining: boolean | undefined; + const query = + isResultsSearchBoolQuery(searchQuery) && (searchQuery.bool.should || searchQuery.bool.filter); - const noTrainingQuery = isTrainingClause === false || isTrainingClause === undefined; + if (query !== undefined && query !== false) { + for (let i = 0; i < query.length; i++) { + const clause = query[i]; - if (noTrainingQuery) { + if (clause.match && clause.match[`${resultsField}.is_training`] !== undefined) { + isTraining = clause.match[`${resultsField}.is_training`]; + break; + } else if ( + clause.bool && + (clause.bool.should !== undefined || clause.bool.filter !== undefined) + ) { + const innerQuery = clause.bool.should || clause.bool.filter; + if (innerQuery !== undefined) { + for (let j = 0; j < innerQuery.length; j++) { + const innerClause = innerQuery[j]; + if ( + innerClause.match && + innerClause.match[`${resultsField}.is_training`] !== undefined + ) { + isTraining = innerClause.match[`${resultsField}.is_training`]; + break; + } + } + } + } + } + } + if (isTraining === undefined) { setDataSubsetTitle(SUBSET_TITLE.ENTIRE); } else { setDataSubsetTitle( - isTrainingClause && isTrainingClause.query === 'true' - ? SUBSET_TITLE.TRAINING - : SUBSET_TITLE.TESTING + isTraining && isTraining === true ? SUBSET_TITLE.TRAINING : SUBSET_TITLE.TESTING ); } - loadData({ isTrainingClause }); + loadData({ isTraining }); }, [JSON.stringify(searchQuery)]); const renderCellValue = ({ diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/classification_exploration/results_table.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/classification_exploration/results_table.tsx index 9758dd969b443..bf63dfe68fe9e 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/classification_exploration/results_table.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/classification_exploration/results_table.tsx @@ -4,71 +4,39 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { Fragment, FC, useEffect, useState } from 'react'; -import moment from 'moment-timezone'; - +import React, { Fragment, FC, useEffect } from 'react'; import { i18n } from '@kbn/i18n'; import { - EuiBadge, - EuiButtonIcon, EuiCallOut, - EuiCheckbox, EuiFlexGroup, EuiFlexItem, EuiFormRow, EuiPanel, - EuiPopover, - EuiPopoverTitle, EuiProgress, EuiSpacer, EuiText, - EuiToolTip, - Query, } from '@elastic/eui'; -import { Query as QueryType } from '../../../analytics_management/components/analytics_list/common'; -import { ES_FIELD_TYPES } from '../../../../../../../../../../src/plugins/data/public'; -import { mlFieldFormatService } from '../../../../../services/field_format_service'; import { IndexPattern } from '../../../../../../../../../../src/plugins/data/public'; - -import { - ColumnType, - mlInMemoryTableBasicFactory, - OnTableChangeArg, - SortingPropType, - SORT_DIRECTION, -} from '../../../../../components/ml_in_memory_table'; - -import { formatHumanReadableDateTimeSeconds } from '../../../../../util/date_utils'; -import { Field } from '../../../../../../../common/types/fields'; -import { SavedSearchQuery } from '../../../../../contexts/ml'; import { BASIC_NUMERICAL_TYPES, EXTENDED_NUMERICAL_TYPES, - isKeywordAndTextType, sortRegressionResultsFields, } from '../../../../common/fields'; import { - toggleSelectedField, - EsDoc, DataFrameAnalyticsConfig, - EsFieldName, MAX_COLUMNS, - getPredictedFieldName, INDEX_STATUS, SEARCH_SIZE, defaultSearchQuery, - getDependentVar, } from '../../../../common'; import { getTaskStateBadge } from '../../../analytics_management/components/analytics_list/columns'; import { DATA_FRAME_TASK_STATE } from '../../../analytics_management/components/analytics_list/common'; -import { useExploreData, TableItem } from './use_explore_data'; +import { useExploreData } from './use_explore_data'; // TableItem import { ExplorationTitle } from './classification_exploration'; - -const PAGE_SIZE_OPTIONS = [5, 10, 25, 50]; - -const MlInMemoryTableBasic = mlInMemoryTableBasicFactory<TableItem>(); +import { ClassificationExplorationDataGrid } from './classification_exploration_data_grid'; +import { ExplorationQueryBar } from '../exploration_query_bar'; const showingDocs = i18n.translate( 'xpack.ml.dataframe.analytics.classificationExploration.documentsShownHelpText', @@ -94,307 +62,65 @@ interface Props { export const ResultsTable: FC<Props> = React.memo( ({ indexPattern, jobConfig, jobStatus, setEvaluateSearchQuery }) => { - const [pageIndex, setPageIndex] = useState(0); - const [pageSize, setPageSize] = useState(25); - const [selectedFields, setSelectedFields] = useState([] as Field[]); - const [docFields, setDocFields] = useState([] as Field[]); - const [depVarType, setDepVarType] = useState<ES_FIELD_TYPES | undefined>(undefined); - const [isColumnsPopoverVisible, setColumnsPopoverVisible] = useState(false); - const [searchQuery, setSearchQuery] = useState<SavedSearchQuery>(defaultSearchQuery); - const [searchError, setSearchError] = useState<any>(undefined); - const [searchString, setSearchString] = useState<string | undefined>(undefined); - - const predictedFieldName = getPredictedFieldName( - jobConfig.dest.results_field, - jobConfig.analysis - ); - - const dependentVariable = getDependentVar(jobConfig.analysis); - - function toggleColumnsPopover() { - setColumnsPopoverVisible(!isColumnsPopoverVisible); - } - - function closeColumnsPopover() { - setColumnsPopoverVisible(false); - } - - function toggleColumn(column: EsFieldName) { - if (tableItems.length > 0 && jobConfig !== undefined) { - // spread to a new array otherwise the component wouldn't re-render - setSelectedFields([ - ...toggleSelectedField(selectedFields, column, jobConfig.dest.results_field, depVarType), - ]); - } - } - const needsDestIndexFields = indexPattern && indexPattern.title === jobConfig.source.index[0]; - + const resultsField = jobConfig.dest.results_field; const { errorMessage, - loadExploreData, - sortField, - sortDirection, - status, - tableItems, - } = useExploreData( - jobConfig, - needsDestIndexFields, + fieldTypes, + pagination, + searchQuery, selectedFields, + rowCount, + setPagination, + setSearchQuery, setSelectedFields, - setDocFields, - setDepVarType - ); + setSortingColumns, + sortingColumns, + status, + tableFields, + tableItems, + } = useExploreData(jobConfig, needsDestIndexFields); - const columns: Array<ColumnType<TableItem>> = selectedFields - .sort(({ name: a }, { name: b }) => sortRegressionResultsFields(a, b, jobConfig)) - .map(field => { - const { type } = field; - let format: any; + useEffect(() => { + setEvaluateSearchQuery(searchQuery); + }, [JSON.stringify(searchQuery)]); - if (indexPattern !== undefined) { - format = mlFieldFormatService.getFieldFormatFromIndexPattern(indexPattern, field.id, ''); - } + const columns = tableFields + .sort((a: any, b: any) => sortRegressionResultsFields(a, b, jobConfig)) + .map((field: any) => { + // Built-in values are ['boolean', 'currency', 'datetime', 'numeric', 'json'] + // To fall back to the default string schema it needs to be undefined. + let schema; + let isSortable = true; + const type = fieldTypes[field]; const isNumber = type !== undefined && (BASIC_NUMERICAL_TYPES.has(type) || EXTENDED_NUMERICAL_TYPES.has(type)); - const column: ColumnType<TableItem> = { - field: field.name, - name: field.name, - sortable: true, - truncateText: true, - }; - - const render = (d: any, fullItem: EsDoc) => { - if (format !== undefined) { - d = format.convert(d, 'text'); - return d; - } - - if (Array.isArray(d) && d.every(item => typeof item === 'string')) { - // If the cells data is an array of strings, return as a comma separated list. - // The list will get limited to 5 items with `…` at the end if there's more in the original array. - return `${d.slice(0, 5).join(', ')}${d.length > 5 ? ', …' : ''}`; - } else if (Array.isArray(d)) { - // If the cells data is an array of e.g. objects, display a 'array' badge with a - // tooltip that explains that this type of field is not supported in this table. - return ( - <EuiToolTip - content={i18n.translate( - 'xpack.ml.dataframe.analytics.classificationExploration.indexArrayToolTipContent', - { - defaultMessage: - 'The full content of this array based column cannot be displayed.', - } - )} - > - <EuiBadge> - {i18n.translate( - 'xpack.ml.dataframe.analytics.classificationExploration.indexArrayBadgeContent', - { - defaultMessage: 'array', - } - )} - </EuiBadge> - </EuiToolTip> - ); - } - - return d; - }; - if (isNumber) { - column.dataType = 'number'; - column.render = render; - } else if (typeof type !== 'undefined') { - switch (type) { - case ES_FIELD_TYPES.BOOLEAN: - column.dataType = ES_FIELD_TYPES.BOOLEAN; - column.render = d => (d ? 'true' : 'false'); - break; - case ES_FIELD_TYPES.DATE: - column.align = 'right'; - if (format !== undefined) { - column.render = render; - } else { - column.render = (d: any) => { - if (d !== undefined) { - return formatHumanReadableDateTimeSeconds(moment(d).unix() * 1000); - } - return d; - }; - } - break; - default: - column.render = render; - break; - } - } else { - column.render = render; + schema = 'numeric'; } - return column; - }); - - const docFieldsCount = docFields.length; - - useEffect(() => { - if ( - jobConfig !== undefined && - columns.length > 0 && - selectedFields.length > 0 && - sortField !== undefined && - sortDirection !== undefined && - selectedFields.some(field => field.name === sortField) - ) { - let field = sortField; - // If sorting by predictedField use dependentVar type - if (predictedFieldName === sortField) { - field = dependentVariable; + switch (type) { + case 'date': + schema = 'datetime'; + break; + case 'geo_point': + schema = 'json'; + break; + case 'boolean': + schema = 'boolean'; + break; } - const requiresKeyword = isKeywordAndTextType(field); - - loadExploreData({ - field: sortField, - direction: sortDirection, - searchQuery, - requiresKeyword, - }); - } - }, [JSON.stringify(searchQuery)]); - - useEffect(() => { - // By default set sorting to descending on the prediction field (`<dependent_varible or prediction_field_name>_prediction`). - // if that's not available sort ascending on the first column. Check if the current sorting field is still available. - if ( - jobConfig !== undefined && - columns.length > 0 && - selectedFields.length > 0 && - !selectedFields.some(field => field.name === sortField) - ) { - const predictedFieldSelected = selectedFields.some( - field => field.name === predictedFieldName - ); - - // CHECK IF keyword suffix is needed (if predicted field is selected we have to check the dependent variable type) - let sortByField = predictedFieldSelected ? dependentVariable : selectedFields[0].name; - - const requiresKeyword = isKeywordAndTextType(sortByField); - - sortByField = predictedFieldSelected ? predictedFieldName : sortByField; - - const direction = predictedFieldSelected ? SORT_DIRECTION.DESC : SORT_DIRECTION.ASC; - loadExploreData({ field: sortByField, direction, searchQuery, requiresKeyword }); - } - }, [ - jobConfig, - columns.length, - selectedFields.length, - sortField, - sortDirection, - tableItems.length, - ]); - - let sorting: SortingPropType = false; - let onTableChange; - - if (columns.length > 0 && sortField !== '' && sortField !== undefined) { - sorting = { - sort: { - field: sortField, - direction: sortDirection, - }, - }; - - onTableChange = ({ - page = { index: 0, size: 10 }, - sort = { field: sortField, direction: sortDirection }, - }: OnTableChangeArg) => { - const { index, size } = page; - setPageIndex(index); - setPageSize(size); - if (sort.field !== sortField || sort.direction !== sortDirection) { - let field = sort.field; - // If sorting by predictedField use depVar for type check - if (predictedFieldName === sort.field) { - field = dependentVariable; - } - - loadExploreData({ - ...sort, - searchQuery, - requiresKeyword: isKeywordAndTextType(field), - }); + if (field === `${resultsField}.feature_importance`) { + isSortable = false; } - }; - } - const pagination = { - initialPageIndex: pageIndex, - initialPageSize: pageSize, - totalItemCount: tableItems.length, - pageSizeOptions: PAGE_SIZE_OPTIONS, - hidePerPageOptions: false, - }; - - const onQueryChange = ({ query, error }: { query: QueryType; error: any }) => { - if (error) { - setSearchError(error.message); - } else { - try { - const esQueryDsl = Query.toESQuery(query); - setSearchQuery(esQueryDsl); - setSearchString(query.text); - setSearchError(undefined); - // set query for use in evaluate panel - setEvaluateSearchQuery(esQueryDsl); - } catch (e) { - setSearchError(e.toString()); - } - } - }; + return { id: field, schema, isSortable }; + }); - const search = { - onChange: onQueryChange, - defaultQuery: searchString, - box: { - incremental: false, - placeholder: i18n.translate( - 'xpack.ml.dataframe.analytics.regressionExploration.searchBoxPlaceholder', - { - defaultMessage: 'E.g. avg>0.5', - } - ), - }, - filters: [ - { - type: 'field_value_toggle_group', - field: `${jobConfig.dest.results_field}.is_training`, - items: [ - { - value: false, - name: i18n.translate( - 'xpack.ml.dataframe.analytics.regressionExploration.isTestingLabel', - { - defaultMessage: 'Testing', - } - ), - }, - { - value: true, - name: i18n.translate( - 'xpack.ml.dataframe.analytics.regressionExploration.isTrainingLabel', - { - defaultMessage: 'Training', - } - ), - }, - ], - }, - ], - }; + const docFieldsCount = tableFields.length; if (jobConfig === undefined) { return null; @@ -426,11 +152,6 @@ export const ResultsTable: FC<Props> = React.memo( ); } - const tableError = - status === INDEX_STATUS.ERROR && errorMessage.includes('parsing_exception') - ? errorMessage - : searchError; - return ( <EuiPanel grow={false} @@ -456,7 +177,7 @@ export const ResultsTable: FC<Props> = React.memo( {docFieldsCount > MAX_COLUMNS && ( <EuiText size="s"> {i18n.translate( - 'xpack.ml.dataframe.analytics.regressionExploration.fieldSelection', + 'xpack.ml.dataframe.analytics.classificationExploration.fieldSelection', { defaultMessage: '{selectedFieldsLength, number} of {docFieldsCount, number} {docFieldsCount, plural, one {field} other {fields}} selected', @@ -466,52 +187,6 @@ export const ResultsTable: FC<Props> = React.memo( </EuiText> )} </EuiFlexItem> - <EuiFlexItem grow={false}> - <EuiText size="s"> - <EuiPopover - id="popover" - button={ - <EuiButtonIcon - iconType="gear" - onClick={toggleColumnsPopover} - aria-label={i18n.translate( - 'xpack.ml.dataframe.analytics.regressionExploration.selectColumnsAriaLabel', - { - defaultMessage: 'Select columns', - } - )} - /> - } - isOpen={isColumnsPopoverVisible} - closePopover={closeColumnsPopover} - ownFocus - > - <EuiPopoverTitle> - {i18n.translate( - 'xpack.ml.dataframe.analytics.regressionExploration.selectFieldsPopoverTitle', - { - defaultMessage: 'Select fields', - } - )} - </EuiPopoverTitle> - <div style={{ maxHeight: '400px', overflowY: 'scroll' }}> - {docFields.map(({ name }) => ( - <EuiCheckbox - key={name} - id={name} - label={name} - checked={selectedFields.some(field => field.name === name)} - onChange={() => toggleColumn(name)} - disabled={ - selectedFields.some(field => field.name === name) && - selectedFields.length === 1 - } - /> - ))} - </div> - </EuiPopover> - </EuiText> - </EuiFlexItem> </EuiFlexGroup> </EuiFlexItem> </EuiFlexGroup> @@ -520,28 +195,39 @@ export const ResultsTable: FC<Props> = React.memo( <EuiProgress size="xs" color="accent" max={1} value={0} /> )} {(columns.length > 0 || searchQuery !== defaultSearchQuery) && ( - <Fragment> - <EuiFormRow - helpText={tableItems.length === SEARCH_SIZE ? showingFirstDocs : showingDocs} - > - <Fragment /> - </EuiFormRow> - <EuiSpacer /> - <MlInMemoryTableBasic - allowNeutralSort={false} - columns={columns} - compressed - hasActions={false} - isSelectable={false} - items={tableItems} - onTableChange={onTableChange} - pagination={pagination} - responsive={false} - search={search} - error={tableError} - sorting={sorting} - /> - </Fragment> + <EuiFlexGroup direction="column"> + <EuiFlexItem grow={false}> + <EuiSpacer size="s" /> + <EuiFlexGroup justifyContent="spaceBetween"> + <EuiFlexItem> + <ExplorationQueryBar + indexPattern={indexPattern} + setSearchQuery={setSearchQuery} + /> + </EuiFlexItem> + </EuiFlexGroup> + <EuiFormRow + helpText={tableItems.length === SEARCH_SIZE ? showingFirstDocs : showingDocs} + > + <Fragment /> + </EuiFormRow> + </EuiFlexItem> + <EuiFlexItem grow={false}> + <ClassificationExplorationDataGrid + columns={columns} + indexPattern={indexPattern} + pagination={pagination} + resultsField={jobConfig.dest.results_field} + rowCount={rowCount} + selectedFields={selectedFields} + setPagination={setPagination} + setSelectedFields={setSelectedFields} + setSortingColumns={setSortingColumns} + sortingColumns={sortingColumns} + tableItems={tableItems} + /> + </EuiFlexItem> + </EuiFlexGroup> )} </EuiPanel> ); diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/classification_exploration/use_explore_data.ts b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/classification_exploration/use_explore_data.ts index 6038def592e5c..c8809ca5e471b 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/classification_exploration/use_explore_data.ts +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/classification_exploration/use_explore_data.ts @@ -3,87 +3,124 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -/* - * 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, { useEffect, useState } from 'react'; +import { useEffect, useState, Dispatch, SetStateAction } from 'react'; +import { EuiDataGridPaginationProps, EuiDataGridSorting } from '@elastic/eui'; import { SearchResponse } from 'elasticsearch'; import { cloneDeep } from 'lodash'; -import { SortDirection, SORT_DIRECTION } from '../../../../../components/ml_in_memory_table'; +import { SORT_DIRECTION } from '../../../../../components/ml_in_memory_table'; import { ml } from '../../../../../services/ml_api_service'; import { getNestedProperty } from '../../../../../util/object_utils'; import { newJobCapsService } from '../../../../../services/new_job_capabilities_service'; -import { Field } from '../../../../../../../common/types/fields'; +import { isKeywordAndTextType } from '../../../../common/fields'; +import { Dictionary } from '../../../../../../../common/types/common'; import { ES_FIELD_TYPES } from '../../../../../../../../../../src/plugins/data/public'; import { - LoadExploreDataArg, defaultSearchQuery, ResultsSearchQuery, isResultsSearchBoolQuery, + LoadExploreDataArg, } from '../../../../common/analytics'; import { getDefaultFieldsFromJobCaps, + getDependentVar, getFlattenedFields, + getPredictedFieldName, DataFrameAnalyticsConfig, EsFieldName, INDEX_STATUS, - SEARCH_SIZE, - SearchQuery, } from '../../../../common'; +import { SavedSearchQuery } from '../../../../../contexts/ml'; export type TableItem = Record<string, any>; +type Pagination = Pick<EuiDataGridPaginationProps, 'pageIndex' | 'pageSize'>; export interface UseExploreDataReturnType { errorMessage: string; - loadExploreData: (arg: LoadExploreDataArg) => void; - sortField: EsFieldName; - sortDirection: SortDirection; + fieldTypes: { [key: string]: ES_FIELD_TYPES }; + pagination: Pagination; + rowCount: number; + searchQuery: SavedSearchQuery; + selectedFields: EsFieldName[]; + setFilterByIsTraining: Dispatch<SetStateAction<undefined | boolean>>; + setPagination: Dispatch<SetStateAction<Pagination>>; + setSearchQuery: Dispatch<SetStateAction<SavedSearchQuery>>; + setSelectedFields: Dispatch<SetStateAction<EsFieldName[]>>; + setSortingColumns: Dispatch<SetStateAction<EuiDataGridSorting['columns']>>; + sortingColumns: EuiDataGridSorting['columns']; status: INDEX_STATUS; + tableFields: string[]; tableItems: TableItem[]; } +type EsSorting = Dictionary<{ + order: 'asc' | 'desc'; +}>; + +// The types specified in `@types/elasticsearch` are out of date and still have `total: number`. +interface SearchResponse7 extends SearchResponse<any> { + hits: SearchResponse<any>['hits'] & { + total: { + value: number; + relation: string; + }; + }; +} + export const useExploreData = ( - jobConfig: DataFrameAnalyticsConfig | undefined, - needsDestIndexFields: boolean, - selectedFields: Field[], - setSelectedFields: React.Dispatch<React.SetStateAction<Field[]>>, - setDocFields: React.Dispatch<React.SetStateAction<Field[]>>, - setDepVarType: React.Dispatch<React.SetStateAction<ES_FIELD_TYPES | undefined>> + jobConfig: DataFrameAnalyticsConfig, + needsDestIndexFields: boolean ): UseExploreDataReturnType => { const [errorMessage, setErrorMessage] = useState(''); const [status, setStatus] = useState(INDEX_STATUS.UNUSED); + + const [selectedFields, setSelectedFields] = useState([] as EsFieldName[]); + const [tableFields, setTableFields] = useState<string[]>([]); const [tableItems, setTableItems] = useState<TableItem[]>([]); - const [sortField, setSortField] = useState<string>(''); - const [sortDirection, setSortDirection] = useState<SortDirection>(SORT_DIRECTION.ASC); + const [fieldTypes, setFieldTypes] = useState<{ [key: string]: ES_FIELD_TYPES }>({}); + const [rowCount, setRowCount] = useState(0); + + const [pagination, setPagination] = useState({ pageIndex: 0, pageSize: 25 }); + const [searchQuery, setSearchQuery] = useState<SavedSearchQuery>(defaultSearchQuery); + const [filterByIsTraining, setFilterByIsTraining] = useState<undefined | boolean>(undefined); + const [sortingColumns, setSortingColumns] = useState<EuiDataGridSorting['columns']>([]); + + const predictedFieldName = getPredictedFieldName( + jobConfig.dest.results_field, + jobConfig.analysis + ); + const dependentVariable = getDependentVar(jobConfig.analysis); const getDefaultSelectedFields = () => { const { fields } = newJobCapsService; - if (selectedFields.length === 0 && jobConfig !== undefined) { - const { - selectedFields: defaultSelected, - docFields, - depVarType, - } = getDefaultFieldsFromJobCaps(fields, jobConfig, needsDestIndexFields); - - setDepVarType(depVarType); - setSelectedFields(defaultSelected); - setDocFields(docFields); + const { selectedFields: defaultSelected, docFields } = getDefaultFieldsFromJobCaps( + fields, + jobConfig, + needsDestIndexFields + ); + + const types: { [key: string]: ES_FIELD_TYPES } = {}; + const allFields: string[] = []; + + docFields.forEach(field => { + types[field.id] = field.type; + allFields.push(field.id); + }); + + setFieldTypes(types); + setSelectedFields(defaultSelected.map(field => field.id)); + setTableFields(allFields); } }; const loadExploreData = async ({ - field, - direction, - searchQuery, - requiresKeyword, + filterByIsTraining: isTraining, + searchQuery: incomingQuery, }: LoadExploreDataArg) => { if (jobConfig !== undefined) { setErrorMessage(''); @@ -91,15 +128,33 @@ export const useExploreData = ( try { const resultsField = jobConfig.dest.results_field; - const searchQueryClone: ResultsSearchQuery = cloneDeep(searchQuery); + const searchQueryClone: ResultsSearchQuery = cloneDeep(incomingQuery); let query: ResultsSearchQuery; + const { pageIndex, pageSize } = pagination; + // If filterByIsTraining is defined - add that in to the final query + const trainingQuery = + isTraining !== undefined + ? { + term: { [`${resultsField}.is_training`]: { value: isTraining } }, + } + : undefined; - if (JSON.stringify(searchQuery) === JSON.stringify(defaultSearchQuery)) { - query = { + if (JSON.stringify(incomingQuery) === JSON.stringify(defaultSearchQuery)) { + const existsQuery = { exists: { field: resultsField, }, }; + + query = { + bool: { + must: [existsQuery], + }, + }; + + if (trainingQuery !== undefined && isResultsSearchBoolQuery(query)) { + query.bool.must.push(trainingQuery); + } } else if (isResultsSearchBoolQuery(searchQueryClone)) { if (searchQueryClone.bool.must === undefined) { searchQueryClone.bool.must = []; @@ -111,33 +166,37 @@ export const useExploreData = ( }, }); + if (trainingQuery !== undefined) { + searchQueryClone.bool.must.push(trainingQuery); + } + query = searchQueryClone; } else { query = searchQueryClone; } - const body: SearchQuery = { - query, - }; + const sort: EsSorting = sortingColumns + .map(column => { + const { id } = column; + column.id = isKeywordAndTextType(id) ? `${id}.keyword` : id; + return column; + }) + .reduce((s, column) => { + s[column.id] = { order: column.direction }; + return s; + }, {} as EsSorting); - if (field !== undefined) { - body.sort = [ - { - [`${field}${requiresKeyword ? '.keyword' : ''}`]: { - order: direction, - }, - }, - ]; - } - - const resp: SearchResponse<any> = await ml.esSearch({ + const resp: SearchResponse7 = await ml.esSearch({ index: jobConfig.dest.index, - size: SEARCH_SIZE, - body, + body: { + query, + from: pageIndex * pageSize, + size: pageSize, + ...(Object.keys(sort).length > 0 ? { sort } : {}), + }, }); - setSortField(field); - setSortDirection(direction); + setRowCount(resp.hits.total.value); const docs = resp.hits.hits; @@ -189,10 +248,45 @@ export const useExploreData = ( }; useEffect(() => { - if (jobConfig !== undefined) { - getDefaultSelectedFields(); - } + getDefaultSelectedFields(); + }, [jobConfig && jobConfig.id]); + + // By default set sorting to descending on the prediction field (`<dependent_varible or prediction_field_name>_prediction`). + useEffect(() => { + const sortByField = isKeywordAndTextType(dependentVariable) + ? `${predictedFieldName}.keyword` + : predictedFieldName; + const direction = SORT_DIRECTION.DESC; + + setSortingColumns([{ id: sortByField, direction }]); }, [jobConfig && jobConfig.id]); - return { errorMessage, loadExploreData, sortField, sortDirection, status, tableItems }; + useEffect(() => { + loadExploreData({ filterByIsTraining, searchQuery }); + }, [ + filterByIsTraining, + jobConfig && jobConfig.id, + pagination, + searchQuery, + selectedFields, + sortingColumns, + ]); + + return { + errorMessage, + fieldTypes, + pagination, + searchQuery, + selectedFields, + rowCount, + setFilterByIsTraining, + setPagination, + setSelectedFields, + setSortingColumns, + setSearchQuery, + sortingColumns, + status, + tableItems, + tableFields, + }; }; diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/regression_exploration/evaluate_panel.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/regression_exploration/evaluate_panel.tsx index 9f235ae6c45c0..6ef6666be5ec6 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/regression_exploration/evaluate_panel.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/regression_exploration/evaluate_panel.tsx @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { FC, Fragment, useEffect, useState } from 'react'; +import React, { FC, useEffect, useState } from 'react'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; import { @@ -17,7 +17,7 @@ import { EuiTitle, } from '@elastic/eui'; import { useMlKibana } from '../../../../../contexts/kibana'; -import { ErrorCallout } from '../error_callout'; +import { SavedSearchQuery } from '../../../../../contexts/ml'; import { getValuesFromResponse, getDependentVar, @@ -33,14 +33,13 @@ import { EvaluateStat } from './evaluate_stat'; import { isResultsSearchBoolQuery, isRegressionEvaluateResponse, - ResultsSearchQuery, ANALYSIS_CONFIG_TYPE, } from '../../../../common/analytics'; interface Props { jobConfig: DataFrameAnalyticsConfig; jobStatus?: DATA_FRAME_TASK_STATE; - searchQuery: ResultsSearchQuery; + searchQuery: SavedSearchQuery; } const defaultEval: Eval = { meanSquaredError: '', rSquared: '', error: null }; @@ -54,6 +53,7 @@ export const EvaluatePanel: FC<Props> = ({ jobConfig, jobStatus, searchQuery }) const [generalizationEval, setGeneralizationEval] = useState<Eval>(defaultEval); const [isLoadingTraining, setIsLoadingTraining] = useState<boolean>(false); const [isLoadingGeneralization, setIsLoadingGeneralization] = useState<boolean>(false); + const [isTrainingFilter, setIsTrainingFilter] = useState<boolean | undefined>(undefined); const [trainingDocsCount, setTrainingDocsCount] = useState<null | number>(null); const [generalizationDocsCount, setGeneralizationDocsCount] = useState<null | number>(null); @@ -92,8 +92,8 @@ export const EvaluatePanel: FC<Props> = ({ jobConfig, jobStatus, searchQuery }) } else { setIsLoadingGeneralization(false); setGeneralizationEval({ - meanSquaredError: '', - rSquared: '', + meanSquaredError: '--', + rSquared: '--', error: genErrorEval.error, }); } @@ -128,108 +128,78 @@ export const EvaluatePanel: FC<Props> = ({ jobConfig, jobStatus, searchQuery }) } else { setIsLoadingTraining(false); setTrainingEval({ - meanSquaredError: '', - rSquared: '', + meanSquaredError: '--', + rSquared: '--', error: trainingErrorEval.error, }); } }; - const loadData = async ({ - isTrainingClause, - }: { - isTrainingClause?: { query: string; operator: string }; - }) => { - // searchBar query is filtering for testing data - if (isTrainingClause !== undefined && isTrainingClause.query === 'false') { - loadGeneralizationData(); - - const docsCountResp = await loadDocsCount({ - isTraining: false, - searchQuery, - resultsField, - destIndex: jobConfig.dest.index, - }); - - if (docsCountResp.success === true) { - setGeneralizationDocsCount(docsCountResp.docsCount); - } else { - setGeneralizationDocsCount(null); - } - - setTrainingDocsCount(0); - setTrainingEval({ - meanSquaredError: '--', - rSquared: '--', - error: null, - }); - } else if (isTrainingClause !== undefined && isTrainingClause.query === 'true') { - // searchBar query is filtering for training data - loadTrainingData(); - - const docsCountResp = await loadDocsCount({ - isTraining: true, - searchQuery, - resultsField, - destIndex: jobConfig.dest.index, - }); - - if (docsCountResp.success === true) { - setTrainingDocsCount(docsCountResp.docsCount); - } else { - setTrainingDocsCount(null); - } - - setGeneralizationDocsCount(0); - setGeneralizationEval({ - meanSquaredError: '--', - rSquared: '--', - error: null, - }); + const loadData = async () => { + loadGeneralizationData(false); + const genDocsCountResp = await loadDocsCount({ + ignoreDefaultQuery: false, + isTraining: false, + searchQuery, + resultsField, + destIndex: jobConfig.dest.index, + }); + if (genDocsCountResp.success === true) { + setGeneralizationDocsCount(genDocsCountResp.docsCount); } else { - // No is_training clause/filter from search bar so load both - loadGeneralizationData(false); - const genDocsCountResp = await loadDocsCount({ - ignoreDefaultQuery: false, - isTraining: false, - searchQuery, - resultsField, - destIndex: jobConfig.dest.index, - }); - if (genDocsCountResp.success === true) { - setGeneralizationDocsCount(genDocsCountResp.docsCount); - } else { - setGeneralizationDocsCount(null); - } + setGeneralizationDocsCount(null); + } - loadTrainingData(false); - const trainDocsCountResp = await loadDocsCount({ - ignoreDefaultQuery: false, - isTraining: true, - searchQuery, - resultsField, - destIndex: jobConfig.dest.index, - }); - if (trainDocsCountResp.success === true) { - setTrainingDocsCount(trainDocsCountResp.docsCount); - } else { - setTrainingDocsCount(null); - } + loadTrainingData(false); + const trainDocsCountResp = await loadDocsCount({ + ignoreDefaultQuery: false, + isTraining: true, + searchQuery, + resultsField, + destIndex: jobConfig.dest.index, + }); + if (trainDocsCountResp.success === true) { + setTrainingDocsCount(trainDocsCountResp.docsCount); + } else { + setTrainingDocsCount(null); } }; useEffect(() => { - const hasIsTrainingClause = - isResultsSearchBoolQuery(searchQuery) && - searchQuery.bool.must.filter( - (clause: any) => clause.match && clause.match[`${resultsField}.is_training`] !== undefined - ); - const isTrainingClause = - hasIsTrainingClause && - hasIsTrainingClause[0] && - hasIsTrainingClause[0].match[`${resultsField}.is_training`]; + let isTraining: boolean | undefined; + const query = + isResultsSearchBoolQuery(searchQuery) && (searchQuery.bool.should || searchQuery.bool.filter); + + if (query !== undefined && query !== false) { + for (let i = 0; i < query.length; i++) { + const clause = query[i]; - loadData({ isTrainingClause }); + if (clause.match && clause.match[`${resultsField}.is_training`] !== undefined) { + isTraining = clause.match[`${resultsField}.is_training`]; + break; + } else if ( + clause.bool && + (clause.bool.should !== undefined || clause.bool.filter !== undefined) + ) { + const innerQuery = clause.bool.should || clause.bool.filter; + if (innerQuery !== undefined) { + for (let j = 0; j < innerQuery.length; j++) { + const innerClause = innerQuery[j]; + if ( + innerClause.match && + innerClause.match[`${resultsField}.is_training`] !== undefined + ) { + isTraining = innerClause.match[`${resultsField}.is_training`]; + break; + } + } + } + } + } + } + + setIsTrainingFilter(isTraining); + loadData(); }, [JSON.stringify(searchQuery)]); return ( @@ -293,13 +263,18 @@ export const EvaluatePanel: FC<Props> = ({ jobConfig, jobStatus, searchQuery }) defaultMessage="{docsCount, plural, one {# doc} other {# docs}} evaluated" values={{ docsCount: generalizationDocsCount }} /> + {isTrainingFilter === true && generalizationDocsCount === 0 && ( + <FormattedMessage + id="xpack.ml.dataframe.analytics.regressionExploration.generalizationFilterText" + defaultMessage=". Filtering for training data." + /> + )} </EuiText> )} <EuiSpacer /> - <EuiFlexGroup> - {generalizationEval.error !== null && <ErrorCallout error={generalizationEval.error} />} - {generalizationEval.error === null && ( - <Fragment> + <EuiFlexGroup direction="column" gutterSize="none"> + <EuiFlexItem> + <EuiFlexGroup> <EuiFlexItem> <EvaluateStat dataTestSubj={'mlDFAnalyticsRegressionGenMSEstat'} @@ -316,7 +291,14 @@ export const EvaluatePanel: FC<Props> = ({ jobConfig, jobStatus, searchQuery }) isMSE={false} /> </EuiFlexItem> - </Fragment> + </EuiFlexGroup> + </EuiFlexItem> + {generalizationEval.error !== null && ( + <EuiFlexItem grow={false}> + <EuiText size="xs" color="danger"> + {generalizationEval.error} + </EuiText> + </EuiFlexItem> )} </EuiFlexGroup> </EuiFlexItem> @@ -338,13 +320,18 @@ export const EvaluatePanel: FC<Props> = ({ jobConfig, jobStatus, searchQuery }) defaultMessage="{docsCount, plural, one {# doc} other {# docs}} evaluated" values={{ docsCount: trainingDocsCount }} /> + {isTrainingFilter === false && trainingDocsCount === 0 && ( + <FormattedMessage + id="xpack.ml.dataframe.analytics.regressionExploration.trainingFilterText" + defaultMessage=". Filtering for testing data." + /> + )} </EuiText> )} <EuiSpacer /> - <EuiFlexGroup> - {trainingEval.error !== null && <ErrorCallout error={trainingEval.error} />} - {trainingEval.error === null && ( - <Fragment> + <EuiFlexGroup direction="column" gutterSize="none"> + <EuiFlexItem> + <EuiFlexGroup> <EuiFlexItem> <EvaluateStat dataTestSubj={'mlDFAnalyticsRegressionTrainingMSEstat'} @@ -361,7 +348,14 @@ export const EvaluatePanel: FC<Props> = ({ jobConfig, jobStatus, searchQuery }) isMSE={false} /> </EuiFlexItem> - </Fragment> + </EuiFlexGroup> + </EuiFlexItem> + {trainingEval.error !== null && ( + <EuiFlexItem grow={false}> + <EuiText size="xs" color="danger"> + {trainingEval.error} + </EuiText> + </EuiFlexItem> )} </EuiFlexGroup> </EuiFlexItem> diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/regression_exploration/regression_exploration_data_grid.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/regression_exploration/regression_exploration_data_grid.tsx new file mode 100644 index 0000000000000..0fcb1ed600719 --- /dev/null +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/regression_exploration/regression_exploration_data_grid.tsx @@ -0,0 +1,135 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { Dispatch, FC, SetStateAction, useCallback, useMemo } from 'react'; + +import { i18n } from '@kbn/i18n'; + +import { EuiDataGrid, EuiDataGridPaginationProps, EuiDataGridSorting } from '@elastic/eui'; + +import { euiDataGridStyle, euiDataGridToolbarSettings } from '../../../../common'; + +import { mlFieldFormatService } from '../../../../../services/field_format_service'; + +import { IndexPattern } from '../../../../../../../../../../src/plugins/data/public'; + +const PAGE_SIZE_OPTIONS = [5, 10, 25, 50]; + +type Pagination = Pick<EuiDataGridPaginationProps, 'pageIndex' | 'pageSize'>; +type TableItem = Record<string, any>; + +interface ExplorationDataGridProps { + colorRange?: (d: number) => string; + columns: any[]; + indexPattern: IndexPattern; + pagination: Pagination; + resultsField: string; + rowCount: number; + selectedFields: string[]; + setPagination: Dispatch<SetStateAction<Pagination>>; + setSelectedFields: Dispatch<SetStateAction<string[]>>; + setSortingColumns: Dispatch<SetStateAction<EuiDataGridSorting['columns']>>; + sortingColumns: EuiDataGridSorting['columns']; + tableItems: TableItem[]; +} + +export const RegressionExplorationDataGrid: FC<ExplorationDataGridProps> = ({ + columns, + indexPattern, + pagination, + resultsField, + rowCount, + selectedFields, + setPagination, + setSelectedFields, + setSortingColumns, + sortingColumns, + tableItems, +}) => { + const renderCellValue = useMemo(() => { + return ({ rowIndex, columnId }: { rowIndex: number; columnId: string; setCellProps: any }) => { + const adjustedRowIndex = rowIndex - pagination.pageIndex * pagination.pageSize; + + const fullItem = tableItems[adjustedRowIndex]; + + if (fullItem === undefined) { + return null; + } + + let format: any; + + if (indexPattern !== undefined) { + format = mlFieldFormatService.getFieldFormatFromIndexPattern(indexPattern, columnId, ''); + } + + const cellValue = + fullItem.hasOwnProperty(columnId) && fullItem[columnId] !== undefined + ? fullItem[columnId] + : null; + + if (format !== undefined) { + return format.convert(cellValue, 'text'); + } + + if (typeof cellValue === 'string' || cellValue === null) { + return cellValue; + } + + if (typeof cellValue === 'boolean') { + return cellValue ? 'true' : 'false'; + } + + if (typeof cellValue === 'object' && cellValue !== null) { + return JSON.stringify(cellValue); + } + + return cellValue; + }; + }, [resultsField, rowCount, tableItems, pagination.pageIndex, pagination.pageSize]); + + const onChangeItemsPerPage = useCallback( + pageSize => { + setPagination(p => { + const pageIndex = Math.floor((p.pageSize * p.pageIndex) / pageSize); + return { pageIndex, pageSize }; + }); + }, + [setPagination] + ); + + const onChangePage = useCallback(pageIndex => setPagination(p => ({ ...p, pageIndex })), [ + setPagination, + ]); + + const onSort = useCallback(sc => setSortingColumns(sc), [setSortingColumns]); + + return ( + <EuiDataGrid + aria-label={i18n.translate( + 'xpack.ml.dataframe.analytics.regressionExploration.dataGridAriaLabel', + { + defaultMessage: 'Regression results table', + } + )} + columns={columns} + columnVisibility={{ + visibleColumns: selectedFields, + setVisibleColumns: setSelectedFields, + }} + gridStyle={euiDataGridStyle} + rowCount={rowCount} + renderCellValue={renderCellValue} + sorting={{ columns: sortingColumns, onSort }} + toolbarVisibility={euiDataGridToolbarSettings} + pagination={{ + ...pagination, + pageSizeOptions: PAGE_SIZE_OPTIONS, + onChangeItemsPerPage, + onChangePage, + }} + /> + ); +}; diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/regression_exploration/results_table.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/regression_exploration/results_table.tsx index a35be5400f46b..43fa50b2e4df5 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/regression_exploration/results_table.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/regression_exploration/results_table.tsx @@ -4,72 +4,41 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { Fragment, FC, useEffect, useState } from 'react'; -import moment from 'moment-timezone'; +import React, { Fragment, FC, useEffect } from 'react'; import { i18n } from '@kbn/i18n'; import { - EuiBadge, - EuiButtonIcon, EuiCallOut, - EuiCheckbox, EuiFlexGroup, EuiFlexItem, EuiFormRow, EuiPanel, - EuiPopover, - EuiPopoverTitle, EuiProgress, EuiSpacer, EuiText, - EuiToolTip, - Query, } from '@elastic/eui'; -import { Query as QueryType } from '../../../analytics_management/components/analytics_list/common'; -import { ES_FIELD_TYPES } from '../../../../../../../../../../src/plugins/data/public'; -import { mlFieldFormatService } from '../../../../../services/field_format_service'; - -import { - ColumnType, - mlInMemoryTableBasicFactory, - OnTableChangeArg, - SortingPropType, - SORT_DIRECTION, -} from '../../../../../components/ml_in_memory_table'; - -import { formatHumanReadableDateTimeSeconds } from '../../../../../util/date_utils'; -import { Field } from '../../../../../../../common/types/fields'; -import { SavedSearchQuery } from '../../../../../contexts/ml'; import { BASIC_NUMERICAL_TYPES, EXTENDED_NUMERICAL_TYPES, - toggleSelectedField, - isKeywordAndTextType, sortRegressionResultsFields, } from '../../../../common/fields'; import { DataFrameAnalyticsConfig, - EsFieldName, - EsDoc, MAX_COLUMNS, - getPredictedFieldName, INDEX_STATUS, SEARCH_SIZE, defaultSearchQuery, - getDependentVar, } from '../../../../common'; import { getTaskStateBadge } from '../../../analytics_management/components/analytics_list/columns'; import { DATA_FRAME_TASK_STATE } from '../../../analytics_management/components/analytics_list/common'; import { IndexPattern } from '../../../../../../../../../../src/plugins/data/public'; -import { useExploreData, TableItem } from './use_explore_data'; +import { useExploreData } from './use_explore_data'; import { ExplorationTitle } from './regression_exploration'; - -const PAGE_SIZE_OPTIONS = [5, 10, 25, 50]; - -const MlInMemoryTableBasic = mlInMemoryTableBasicFactory<TableItem>(); +import { RegressionExplorationDataGrid } from './regression_exploration_data_grid'; +import { ExplorationQueryBar } from '../exploration_query_bar'; const showingDocs = i18n.translate( 'xpack.ml.dataframe.analytics.regressionExploration.documentsShownHelpText', @@ -95,308 +64,65 @@ interface Props { export const ResultsTable: FC<Props> = React.memo( ({ indexPattern, jobConfig, jobStatus, setEvaluateSearchQuery }) => { - const [pageIndex, setPageIndex] = useState(0); - const [pageSize, setPageSize] = useState(25); - const [selectedFields, setSelectedFields] = useState([] as Field[]); - const [docFields, setDocFields] = useState([] as Field[]); - const [depVarType, setDepVarType] = useState<ES_FIELD_TYPES | undefined>(undefined); - const [isColumnsPopoverVisible, setColumnsPopoverVisible] = useState(false); - const [searchQuery, setSearchQuery] = useState<SavedSearchQuery>(defaultSearchQuery); - const [searchError, setSearchError] = useState<any>(undefined); - const [searchString, setSearchString] = useState<string | undefined>(undefined); - - const predictedFieldName = getPredictedFieldName( - jobConfig.dest.results_field, - jobConfig.analysis - ); - - const dependentVariable = getDependentVar(jobConfig.analysis); - - function toggleColumnsPopover() { - setColumnsPopoverVisible(!isColumnsPopoverVisible); - } - - function closeColumnsPopover() { - setColumnsPopoverVisible(false); - } - - function toggleColumn(column: EsFieldName) { - if (tableItems.length > 0 && jobConfig !== undefined) { - // spread to a new array otherwise the component wouldn't re-render - setSelectedFields([ - ...toggleSelectedField(selectedFields, column, jobConfig.dest.results_field, depVarType), - ]); - } - } - const needsDestIndexFields = indexPattern && indexPattern.title === jobConfig.source.index[0]; - + const resultsField = jobConfig.dest.results_field; const { errorMessage, - loadExploreData, - sortField, - sortDirection, - status, - tableItems, - } = useExploreData( - jobConfig, - needsDestIndexFields, + fieldTypes, + pagination, + searchQuery, selectedFields, + rowCount, + setPagination, + setSearchQuery, setSelectedFields, - setDocFields, - setDepVarType - ); - - const columns: Array<ColumnType<TableItem>> = selectedFields - .sort(({ name: a }, { name: b }) => sortRegressionResultsFields(a, b, jobConfig)) - .map(field => { - const { type } = field; - let format: any; + setSortingColumns, + sortingColumns, + status, + tableFields, + tableItems, + } = useExploreData(jobConfig, needsDestIndexFields); - if (indexPattern !== undefined) { - format = mlFieldFormatService.getFieldFormatFromIndexPattern(indexPattern, field.id, ''); - } + useEffect(() => { + setEvaluateSearchQuery(searchQuery); + }, [JSON.stringify(searchQuery)]); + const columns = tableFields + .sort((a: any, b: any) => sortRegressionResultsFields(a, b, jobConfig)) + .map((field: any) => { + // Built-in values are ['boolean', 'currency', 'datetime', 'numeric', 'json'] + // To fall back to the default string schema it needs to be undefined. + let schema; + let isSortable = true; + const type = fieldTypes[field]; const isNumber = type !== undefined && (BASIC_NUMERICAL_TYPES.has(type) || EXTENDED_NUMERICAL_TYPES.has(type)); - const column: ColumnType<TableItem> = { - field: field.name, - name: field.name, - sortable: true, - truncateText: true, - }; - - const render = (d: any, fullItem: EsDoc) => { - if (format !== undefined) { - d = format.convert(d, 'text'); - return d; - } - - if (Array.isArray(d) && d.every(item => typeof item === 'string')) { - // If the cells data is an array of strings, return as a comma separated list. - // The list will get limited to 5 items with `…` at the end if there's more in the original array. - return `${d.slice(0, 5).join(', ')}${d.length > 5 ? ', …' : ''}`; - } else if (Array.isArray(d)) { - // If the cells data is an array of e.g. objects, display a 'array' badge with a - // tooltip that explains that this type of field is not supported in this table. - return ( - <EuiToolTip - content={i18n.translate( - 'xpack.ml.dataframe.analytics.regressionExploration.indexArrayToolTipContent', - { - defaultMessage: - 'The full content of this array based column cannot be displayed.', - } - )} - > - <EuiBadge> - {i18n.translate( - 'xpack.ml.dataframe.analytics.regressionExploration.indexArrayBadgeContent', - { - defaultMessage: 'array', - } - )} - </EuiBadge> - </EuiToolTip> - ); - } - - return d; - }; - if (isNumber) { - column.dataType = 'number'; - column.render = render; - } else if (typeof type !== 'undefined') { - switch (type) { - case ES_FIELD_TYPES.BOOLEAN: - column.dataType = ES_FIELD_TYPES.BOOLEAN; - column.render = d => (d ? 'true' : 'false'); - break; - case ES_FIELD_TYPES.DATE: - column.align = 'right'; - if (format !== undefined) { - column.render = render; - } else { - column.render = (d: any) => { - if (d !== undefined) { - return formatHumanReadableDateTimeSeconds(moment(d).unix() * 1000); - } - return d; - }; - } - break; - default: - column.render = render; - break; - } - } else { - column.render = render; + schema = 'numeric'; } - return column; - }); - - const docFieldsCount = docFields.length; - - useEffect(() => { - if ( - jobConfig !== undefined && - columns.length > 0 && - selectedFields.length > 0 && - sortField !== undefined && - sortDirection !== undefined && - selectedFields.some(field => field.name === sortField) - ) { - let field = sortField; - // If sorting by predictedField use dependentVar type - if (predictedFieldName === sortField) { - field = dependentVariable; + switch (type) { + case 'date': + schema = 'datetime'; + break; + case 'geo_point': + schema = 'json'; + break; + case 'boolean': + schema = 'boolean'; + break; } - const requiresKeyword = isKeywordAndTextType(field); - - loadExploreData({ - field: sortField, - direction: sortDirection, - searchQuery, - requiresKeyword, - }); - } - }, [JSON.stringify(searchQuery)]); - - useEffect(() => { - // By default set sorting to descending on the prediction field (`<dependent_varible or prediction_field_name>_prediction`). - // if that's not available sort ascending on the first column. Check if the current sorting field is still available. - if ( - jobConfig !== undefined && - columns.length > 0 && - selectedFields.length > 0 && - !selectedFields.some(field => field.name === sortField) - ) { - const predictedFieldSelected = selectedFields.some( - field => field.name === predictedFieldName - ); - - // CHECK IF keyword suffix is needed (if predicted field is selected we have to check the dependent variable type) - let sortByField = predictedFieldSelected ? dependentVariable : selectedFields[0].name; - - const requiresKeyword = isKeywordAndTextType(sortByField); - - sortByField = predictedFieldSelected ? predictedFieldName : sortByField; - - const direction = predictedFieldSelected ? SORT_DIRECTION.DESC : SORT_DIRECTION.ASC; - loadExploreData({ field: sortByField, direction, searchQuery, requiresKeyword }); - } - }, [ - jobConfig, - columns.length, - selectedFields.length, - sortField, - sortDirection, - tableItems.length, - ]); - - let sorting: SortingPropType = false; - let onTableChange; - - if (columns.length > 0 && sortField !== '' && sortField !== undefined) { - sorting = { - sort: { - field: sortField, - direction: sortDirection, - }, - }; - - onTableChange = ({ - page = { index: 0, size: 10 }, - sort = { field: sortField, direction: sortDirection }, - }: OnTableChangeArg) => { - const { index, size } = page; - setPageIndex(index); - setPageSize(size); - - if (sort.field !== sortField || sort.direction !== sortDirection) { - let field = sort.field; - // If sorting by predictedField use depVar for type check - if (predictedFieldName === sort.field) { - field = dependentVariable; - } - loadExploreData({ - ...sort, - searchQuery, - requiresKeyword: isKeywordAndTextType(field), - }); + if (field === `${resultsField}.feature_importance`) { + isSortable = false; } - }; - } - - const pagination = { - initialPageIndex: pageIndex, - initialPageSize: pageSize, - totalItemCount: tableItems.length, - pageSizeOptions: PAGE_SIZE_OPTIONS, - hidePerPageOptions: false, - }; - const onQueryChange = ({ query, error }: { query: QueryType; error: any }) => { - if (error) { - setSearchError(error.message); - } else { - try { - const esQueryDsl = Query.toESQuery(query); - setSearchQuery(esQueryDsl); - setSearchString(query.text); - setSearchError(undefined); - // set query for use in evaluate panel - setEvaluateSearchQuery(esQueryDsl); - } catch (e) { - setSearchError(e.toString()); - } - } - }; + return { id: field, schema, isSortable }; + }); - const search = { - onChange: onQueryChange, - defaultQuery: searchString, - box: { - incremental: false, - placeholder: i18n.translate( - 'xpack.ml.dataframe.analytics.regressionExploration.searchBoxPlaceholder', - { - defaultMessage: 'E.g. avg>0.5', - } - ), - }, - filters: [ - { - type: 'field_value_toggle_group', - field: `${jobConfig.dest.results_field}.is_training`, - items: [ - { - value: false, - name: i18n.translate( - 'xpack.ml.dataframe.analytics.regressionExploration.isTestingLabel', - { - defaultMessage: 'Testing', - } - ), - }, - { - value: true, - name: i18n.translate( - 'xpack.ml.dataframe.analytics.regressionExploration.isTrainingLabel', - { - defaultMessage: 'Training', - } - ), - }, - ], - }, - ], - }; + const docFieldsCount = tableFields.length; if (jobConfig === undefined) { return null; @@ -428,11 +154,6 @@ export const ResultsTable: FC<Props> = React.memo( ); } - const tableError = - status === INDEX_STATUS.ERROR && errorMessage.includes('parsing_exception') - ? errorMessage - : searchError; - return ( <EuiPanel grow={false} data-test-subj="mlDFAnalyticsRegressionExplorationTablePanel"> <EuiFlexGroup alignItems="center" justifyContent="spaceBetween" responsive={false}> @@ -464,52 +185,6 @@ export const ResultsTable: FC<Props> = React.memo( </EuiText> )} </EuiFlexItem> - <EuiFlexItem grow={false}> - <EuiText size="s"> - <EuiPopover - id="popover" - button={ - <EuiButtonIcon - iconType="gear" - onClick={toggleColumnsPopover} - aria-label={i18n.translate( - 'xpack.ml.dataframe.analytics.regressionExploration.selectColumnsAriaLabel', - { - defaultMessage: 'Select columns', - } - )} - /> - } - isOpen={isColumnsPopoverVisible} - closePopover={closeColumnsPopover} - ownFocus - > - <EuiPopoverTitle> - {i18n.translate( - 'xpack.ml.dataframe.analytics.regressionExploration.selectFieldsPopoverTitle', - { - defaultMessage: 'Select fields', - } - )} - </EuiPopoverTitle> - <div style={{ maxHeight: '400px', overflowY: 'scroll' }}> - {docFields.map(({ name }) => ( - <EuiCheckbox - id={name} - key={name} - label={name} - checked={selectedFields.some(field => field.name === name)} - onChange={() => toggleColumn(name)} - disabled={ - selectedFields.some(field => field.name === name) && - selectedFields.length === 1 - } - /> - ))} - </div> - </EuiPopover> - </EuiText> - </EuiFlexItem> </EuiFlexGroup> </EuiFlexItem> </EuiFlexGroup> @@ -518,29 +193,39 @@ export const ResultsTable: FC<Props> = React.memo( <EuiProgress size="xs" color="accent" max={1} value={0} /> )} {(columns.length > 0 || searchQuery !== defaultSearchQuery) && ( - <Fragment> - <EuiFormRow - helpText={tableItems.length === SEARCH_SIZE ? showingFirstDocs : showingDocs} - > - <Fragment /> - </EuiFormRow> - - <EuiSpacer /> - <MlInMemoryTableBasic - allowNeutralSort={false} - columns={columns} - compressed - hasActions={false} - isSelectable={false} - items={tableItems} - onTableChange={onTableChange} - pagination={pagination} - responsive={false} - search={search} - error={tableError} - sorting={sorting} - /> - </Fragment> + <EuiFlexGroup direction="column"> + <EuiFlexItem grow={false}> + <EuiSpacer size="s" /> + <EuiFlexGroup justifyContent="spaceBetween"> + <EuiFlexItem> + <ExplorationQueryBar + indexPattern={indexPattern} + setSearchQuery={setSearchQuery} + /> + </EuiFlexItem> + </EuiFlexGroup> + <EuiFormRow + helpText={tableItems.length === SEARCH_SIZE ? showingFirstDocs : showingDocs} + > + <Fragment /> + </EuiFormRow> + </EuiFlexItem> + <EuiFlexItem grow={false}> + <RegressionExplorationDataGrid + columns={columns} + indexPattern={indexPattern} + pagination={pagination} + resultsField={jobConfig.dest.results_field} + rowCount={rowCount} + selectedFields={selectedFields} + setPagination={setPagination} + setSelectedFields={setSelectedFields} + setSortingColumns={setSortingColumns} + sortingColumns={sortingColumns} + tableItems={tableItems} + /> + </EuiFlexItem> + </EuiFlexGroup> )} </EuiPanel> ); diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/regression_exploration/use_explore_data.ts b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/regression_exploration/use_explore_data.ts index e158e952c1c18..978aafd10de11 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/regression_exploration/use_explore_data.ts +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/regression_exploration/use_explore_data.ts @@ -4,27 +4,29 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { useEffect, useState } from 'react'; +import { useEffect, useState, Dispatch, SetStateAction } from 'react'; +import { EuiDataGridPaginationProps, EuiDataGridSorting } from '@elastic/eui'; import { SearchResponse } from 'elasticsearch'; import { cloneDeep } from 'lodash'; -import { SortDirection, SORT_DIRECTION } from '../../../../../components/ml_in_memory_table'; - import { ml } from '../../../../../services/ml_api_service'; import { getNestedProperty } from '../../../../../util/object_utils'; import { newJobCapsService } from '../../../../../services/new_job_capabilities_service'; +import { SORT_DIRECTION } from '../../../../../components/ml_in_memory_table'; +import { SavedSearchQuery } from '../../../../../contexts/ml'; import { getDefaultFieldsFromJobCaps, + getDependentVar, getFlattenedFields, + getPredictedFieldName, DataFrameAnalyticsConfig, EsFieldName, INDEX_STATUS, - SEARCH_SIZE, - SearchQuery, } from '../../../../common'; -import { Field } from '../../../../../../../common/types/fields'; +import { Dictionary } from '../../../../../../../common/types/common'; +import { isKeywordAndTextType } from '../../../../common/fields'; import { ES_FIELD_TYPES } from '../../../../../../../../../../src/plugins/data/public'; import { LoadExploreDataArg, @@ -34,51 +36,90 @@ import { } from '../../../../common/analytics'; export type TableItem = Record<string, any>; +type Pagination = Pick<EuiDataGridPaginationProps, 'pageIndex' | 'pageSize'>; export interface UseExploreDataReturnType { errorMessage: string; - loadExploreData: (arg: LoadExploreDataArg) => void; - sortField: EsFieldName; - sortDirection: SortDirection; + fieldTypes: { [key: string]: ES_FIELD_TYPES }; + pagination: Pagination; + rowCount: number; + searchQuery: SavedSearchQuery; + selectedFields: EsFieldName[]; + setFilterByIsTraining: Dispatch<SetStateAction<undefined | boolean>>; + setPagination: Dispatch<SetStateAction<Pagination>>; + setSearchQuery: Dispatch<SetStateAction<SavedSearchQuery>>; + setSelectedFields: Dispatch<SetStateAction<EsFieldName[]>>; + setSortingColumns: Dispatch<SetStateAction<EuiDataGridSorting['columns']>>; + sortingColumns: EuiDataGridSorting['columns']; status: INDEX_STATUS; + tableFields: string[]; tableItems: TableItem[]; } +type EsSorting = Dictionary<{ + order: 'asc' | 'desc'; +}>; + +// The types specified in `@types/elasticsearch` are out of date and still have `total: number`. +interface SearchResponse7 extends SearchResponse<any> { + hits: SearchResponse<any>['hits'] & { + total: { + value: number; + relation: string; + }; + }; +} + export const useExploreData = ( - jobConfig: DataFrameAnalyticsConfig | undefined, - needsDestIndexFields: boolean, - selectedFields: Field[], - setSelectedFields: React.Dispatch<React.SetStateAction<Field[]>>, - setDocFields: React.Dispatch<React.SetStateAction<Field[]>>, - setDepVarType: React.Dispatch<React.SetStateAction<ES_FIELD_TYPES | undefined>> + jobConfig: DataFrameAnalyticsConfig, + needsDestIndexFields: boolean ): UseExploreDataReturnType => { const [errorMessage, setErrorMessage] = useState(''); const [status, setStatus] = useState(INDEX_STATUS.UNUSED); + + const [selectedFields, setSelectedFields] = useState([] as EsFieldName[]); + const [tableFields, setTableFields] = useState<string[]>([]); const [tableItems, setTableItems] = useState<TableItem[]>([]); - const [sortField, setSortField] = useState<string>(''); - const [sortDirection, setSortDirection] = useState<SortDirection>(SORT_DIRECTION.ASC); + const [fieldTypes, setFieldTypes] = useState<{ [key: string]: ES_FIELD_TYPES }>({}); + const [rowCount, setRowCount] = useState(0); + + const [pagination, setPagination] = useState({ pageIndex: 0, pageSize: 25 }); + const [searchQuery, setSearchQuery] = useState<SavedSearchQuery>(defaultSearchQuery); + const [filterByIsTraining, setFilterByIsTraining] = useState<undefined | boolean>(undefined); + const [sortingColumns, setSortingColumns] = useState<EuiDataGridSorting['columns']>([]); + + const predictedFieldName = getPredictedFieldName( + jobConfig.dest.results_field, + jobConfig.analysis + ); + const dependentVariable = getDependentVar(jobConfig.analysis); const getDefaultSelectedFields = () => { const { fields } = newJobCapsService; - if (selectedFields.length === 0 && jobConfig !== undefined) { - const { - selectedFields: defaultSelected, - docFields, - depVarType, - } = getDefaultFieldsFromJobCaps(fields, jobConfig, needsDestIndexFields); - - setDepVarType(depVarType); - setSelectedFields(defaultSelected); - setDocFields(docFields); + const { selectedFields: defaultSelected, docFields } = getDefaultFieldsFromJobCaps( + fields, + jobConfig, + needsDestIndexFields + ); + + const types: { [key: string]: ES_FIELD_TYPES } = {}; + const allFields: string[] = []; + + docFields.forEach(field => { + types[field.id] = field.type; + allFields.push(field.id); + }); + + setFieldTypes(types); + setSelectedFields(defaultSelected.map(field => field.id)); + setTableFields(allFields); } }; const loadExploreData = async ({ - field, - direction, - searchQuery, - requiresKeyword, + filterByIsTraining: isTraining, + searchQuery: incomingQuery, }: LoadExploreDataArg) => { if (jobConfig !== undefined) { setErrorMessage(''); @@ -86,15 +127,33 @@ export const useExploreData = ( try { const resultsField = jobConfig.dest.results_field; - const searchQueryClone: ResultsSearchQuery = cloneDeep(searchQuery); + const searchQueryClone: ResultsSearchQuery = cloneDeep(incomingQuery); let query: ResultsSearchQuery; + const { pageIndex, pageSize } = pagination; + // If filterByIsTraining is defined - add that in to the final query + const trainingQuery = + isTraining !== undefined + ? { + term: { [`${resultsField}.is_training`]: { value: isTraining } }, + } + : undefined; - if (JSON.stringify(searchQuery) === JSON.stringify(defaultSearchQuery)) { - query = { + if (JSON.stringify(incomingQuery) === JSON.stringify(defaultSearchQuery)) { + const existsQuery = { exists: { field: resultsField, }, }; + + query = { + bool: { + must: [existsQuery], + }, + }; + + if (trainingQuery !== undefined && isResultsSearchBoolQuery(query)) { + query.bool.must.push(trainingQuery); + } } else if (isResultsSearchBoolQuery(searchQueryClone)) { if (searchQueryClone.bool.must === undefined) { searchQueryClone.bool.must = []; @@ -106,32 +165,37 @@ export const useExploreData = ( }, }); + if (trainingQuery !== undefined) { + searchQueryClone.bool.must.push(trainingQuery); + } + query = searchQueryClone; } else { query = searchQueryClone; } - const body: SearchQuery = { - query, - }; - - if (field !== undefined) { - body.sort = [ - { - [`${field}${requiresKeyword ? '.keyword' : ''}`]: { - order: direction, - }, - }, - ]; - } - const resp: SearchResponse<any> = await ml.esSearch({ + const sort: EsSorting = sortingColumns + .map(column => { + const { id } = column; + column.id = isKeywordAndTextType(id) ? `${id}.keyword` : id; + return column; + }) + .reduce((s, column) => { + s[column.id] = { order: column.direction }; + return s; + }, {} as EsSorting); + + const resp: SearchResponse7 = await ml.esSearch({ index: jobConfig.dest.index, - size: SEARCH_SIZE, - body, + body: { + query, + from: pageIndex * pageSize, + size: pageSize, + ...(Object.keys(sort).length > 0 ? { sort } : {}), + }, }); - setSortField(field); - setSortDirection(direction); + setRowCount(resp.hits.total.value); const docs = resp.hits.hits; @@ -183,10 +247,45 @@ export const useExploreData = ( }; useEffect(() => { - if (jobConfig !== undefined) { - getDefaultSelectedFields(); - } + getDefaultSelectedFields(); }, [jobConfig && jobConfig.id]); - return { errorMessage, loadExploreData, sortField, sortDirection, status, tableItems }; + // By default set sorting to descending on the prediction field (`<dependent_varible or prediction_field_name>_prediction`). + useEffect(() => { + const sortByField = isKeywordAndTextType(dependentVariable) + ? `${predictedFieldName}.keyword` + : predictedFieldName; + const direction = SORT_DIRECTION.DESC; + + setSortingColumns([{ id: sortByField, direction }]); + }, [jobConfig && jobConfig.id]); + + useEffect(() => { + loadExploreData({ filterByIsTraining, searchQuery }); + }, [ + filterByIsTraining, + jobConfig && jobConfig.id, + pagination, + searchQuery, + selectedFields, + sortingColumns, + ]); + + return { + errorMessage, + fieldTypes, + pagination, + searchQuery, + selectedFields, + rowCount, + setFilterByIsTraining, + setPagination, + setSelectedFields, + setSortingColumns, + setSearchQuery, + sortingColumns, + status, + tableItems, + tableFields, + }; }; diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/create_analytics_advanced_editor/create_analytics_advanced_editor.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/create_analytics_advanced_editor/create_analytics_advanced_editor.tsx index a3e5da5e2d039..cef03cc0d0c76 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/create_analytics_advanced_editor/create_analytics_advanced_editor.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/create_analytics_advanced_editor/create_analytics_advanced_editor.tsx @@ -18,8 +18,11 @@ import { import { i18n } from '@kbn/i18n'; +import { XJsonMode } from '../../../../../../../shared_imports'; + +const xJsonMode = new XJsonMode(); + import { CreateAnalyticsFormProps } from '../../hooks/use_create_analytics_form'; -import { xJsonMode } from '../../../../../components/custom_hooks'; export const CreateAnalyticsAdvancedEditor: FC<CreateAnalyticsFormProps> = ({ actions, state }) => { const { setAdvancedEditorRawString, setFormState } = actions; 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 ded6e50947035..d55eb14a20e29 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 @@ -11,7 +11,7 @@ import numeral from '@elastic/numeral'; import { isEmpty } from 'lodash'; import { isValidIndexName } from '../../../../../../../common/util/es_utils'; -import { collapseLiteralStrings } from '../../../../../../../../../../src/plugins/es_ui_shared/console_lang/lib/json_xjson_translation_tools'; +import { collapseLiteralStrings } from '../../../../../../../../../../src/plugins/es_ui_shared/public'; import { Action, ACTION } from './actions'; import { getInitialState, getJobConfigFromFormState, State } from './state'; diff --git a/x-pack/plugins/ml/public/application/datavisualizer/file_based/components/file_datavisualizer_view/file_datavisualizer_view.js b/x-pack/plugins/ml/public/application/datavisualizer/file_based/components/file_datavisualizer_view/file_datavisualizer_view.js index d1b615a878b2b..c73ab4b9e11c7 100644 --- a/x-pack/plugins/ml/public/application/datavisualizer/file_based/components/file_datavisualizer_view/file_datavisualizer_view.js +++ b/x-pack/plugins/ml/public/application/datavisualizer/file_based/components/file_datavisualizer_view/file_datavisualizer_view.js @@ -24,14 +24,11 @@ import { readFile, createUrlOverrides, processResults, - reduceData, hasImportPermission, } from '../utils'; import { MODE } from './constants'; -const UPLOAD_SIZE_MB = 5; - export class FileDataVisualizerView extends Component { constructor(props) { super(props); @@ -40,6 +37,7 @@ export class FileDataVisualizerView extends Component { files: {}, fileName: '', fileContents: '', + data: [], fileSize: 0, fileTooLarge: false, fileCouldNotBeRead: false, @@ -79,6 +77,7 @@ export class FileDataVisualizerView extends Component { loaded: false, fileName: '', fileContents: '', + data: [], fileSize: 0, fileTooLarge: false, fileCouldNotBeRead: false, @@ -97,15 +96,15 @@ export class FileDataVisualizerView extends Component { async loadFile(file) { if (file.size <= this.maxFileUploadBytes) { try { - const fileContents = await readFile(file); - const data = fileContents.data; + const { data, fileContents } = await readFile(file); this.setState({ - fileContents: data, + data, + fileContents, fileName: file.name, fileSize: file.size, }); - await this.loadSettings(data); + await this.analyzeFile(fileContents); } catch (error) { this.setState({ loaded: false, @@ -124,14 +123,9 @@ export class FileDataVisualizerView extends Component { } } - async loadSettings(data, overrides, isRetry = false) { + async analyzeFile(fileContents, overrides, isRetry = false) { try { - // reduce the amount of data being sent to the endpoint - // 5MB should be enough to contain 1000 lines - const lessData = reduceData(data, UPLOAD_SIZE_MB); - console.log('overrides', overrides); - const { analyzeFile } = ml.fileDatavisualizer; - const resp = await analyzeFile(lessData, overrides); + const resp = await ml.fileDatavisualizer.analyzeFile(fileContents, overrides); const serverSettings = processResults(resp); const serverOverrides = resp.overrides; @@ -198,7 +192,7 @@ export class FileDataVisualizerView extends Component { loading: true, loaded: false, }); - this.loadSettings(data, this.previousOverrides, true); + this.analyzeFile(fileContents, this.previousOverrides, true); } } } @@ -240,7 +234,7 @@ export class FileDataVisualizerView extends Component { }, () => { const formattedOverrides = createUrlOverrides(overrides, this.originalSettings); - this.loadSettings(this.state.fileContents, formattedOverrides); + this.analyzeFile(this.state.fileContents, formattedOverrides); } ); }; @@ -261,6 +255,7 @@ export class FileDataVisualizerView extends Component { results, explanation, fileContents, + data, fileName, fileSize, fileTooLarge, @@ -339,6 +334,7 @@ export class FileDataVisualizerView extends Component { results={results} fileName={fileName} fileContents={fileContents} + data={data} indexPatterns={this.props.indexPatterns} kibanaConfig={this.props.kibanaConfig} showBottomBar={this.showBottomBar} diff --git a/x-pack/plugins/ml/public/application/datavisualizer/file_based/components/import_view/import_view.js b/x-pack/plugins/ml/public/application/datavisualizer/file_based/components/import_view/import_view.js index 4c9579bfd4b46..2bf7bbeb641d0 100644 --- a/x-pack/plugins/ml/public/application/datavisualizer/file_based/components/import_view/import_view.js +++ b/x-pack/plugins/ml/public/application/datavisualizer/file_based/components/import_view/import_view.js @@ -94,7 +94,7 @@ export class ImportView extends Component { // TODO - sort this function out. it's a mess async import() { - const { fileContents, results, indexPatterns, kibanaConfig, showBottomBar } = this.props; + const { data, results, indexPatterns, kibanaConfig, showBottomBar } = this.props; const { format } = results; let { timeFieldName } = this.state; @@ -217,7 +217,7 @@ export class ImportView extends Component { if (success) { const importer = importerFactory(format, results, indexCreationSettings); if (importer !== undefined) { - const readResp = importer.read(fileContents, this.setReadProgress); + const readResp = importer.read(data, this.setReadProgress); success = readResp.success; this.setState({ readStatus: success ? IMPORT_STATUS.COMPLETE : IMPORT_STATUS.FAILED, diff --git a/x-pack/plugins/ml/public/application/datavisualizer/file_based/components/import_view/importer/importer.ts b/x-pack/plugins/ml/public/application/datavisualizer/file_based/components/import_view/importer/importer.ts index c97f1c147c454..718587ad15ad5 100644 --- a/x-pack/plugins/ml/public/application/datavisualizer/file_based/components/import_view/importer/importer.ts +++ b/x-pack/plugins/ml/public/application/datavisualizer/file_based/components/import_view/importer/importer.ts @@ -9,7 +9,7 @@ import moment from 'moment'; import { i18n } from '@kbn/i18n'; import { ml } from '../../../../../services/ml_api_service'; import { - Doc, + ImportDoc, ImportFailure, ImportResponse, Mappings, @@ -20,6 +20,7 @@ import { const CHUNK_SIZE = 5000; const MAX_CHUNK_CHAR_COUNT = 1000000; const IMPORT_RETRIES = 5; +const STRING_CHUNKS_MB = 100; export interface ImportConfig { settings: Settings; @@ -34,12 +35,19 @@ export interface ImportResults { error?: any; } -export class Importer { +export interface CreateDocsResponse { + success: boolean; + remainder: number; + docs: ImportDoc[]; + error?: any; +} + +export abstract class Importer { private _settings: Settings; private _mappings: Mappings; private _pipeline: IngestPipeline; - protected _docArray: Doc[] = []; + protected _docArray: ImportDoc[] = []; constructor({ settings, mappings, pipeline }: ImportConfig) { this._settings = settings; @@ -47,7 +55,33 @@ export class Importer { this._pipeline = pipeline; } - async initializeImport(index: string) { + public read(data: ArrayBuffer) { + const decoder = new TextDecoder(); + const size = STRING_CHUNKS_MB * Math.pow(2, 20); + + // chop the data up into 100MB chunks for processing. + // if the chop produces a partial line at the end, a character "remainder" count + // is returned which is used to roll the next chunk back that many chars so + // it is included in the next chunk. + const parts = Math.ceil(data.byteLength / size); + let remainder = 0; + for (let i = 0; i < parts; i++) { + const byteArray = decoder.decode(data.slice(i * size - remainder, (i + 1) * size)); + const { success, docs, remainder: tempRemainder } = this._createDocs(byteArray); + if (success) { + this._docArray = this._docArray.concat(docs); + remainder = tempRemainder; + } else { + return { success: false }; + } + } + + return { success: true }; + } + + protected abstract _createDocs(t: string): CreateDocsResponse; + + public async initializeImport(index: string) { const settings = this._settings; const mappings = this._mappings; const pipeline = this._pipeline; @@ -75,7 +109,7 @@ export class Importer { return createIndexResp; } - async import( + public async import( id: string, index: string, pipelineId: string, @@ -201,8 +235,8 @@ function updatePipelineTimezone(ingestPipeline: IngestPipeline) { } } -function createDocumentChunks(docArray: Doc[]) { - const chunks: Doc[][] = []; +function createDocumentChunks(docArray: ImportDoc[]) { + const chunks: ImportDoc[][] = []; // chop docArray into 5000 doc chunks const tempChunks = chunk(docArray, CHUNK_SIZE); diff --git a/x-pack/plugins/ml/public/application/datavisualizer/file_based/components/import_view/importer/message_importer.ts b/x-pack/plugins/ml/public/application/datavisualizer/file_based/components/import_view/importer/message_importer.ts index 7ccc5a8d673f4..65be24d9e7be4 100644 --- a/x-pack/plugins/ml/public/application/datavisualizer/file_based/components/import_view/importer/message_importer.ts +++ b/x-pack/plugins/ml/public/application/datavisualizer/file_based/components/import_view/importer/message_importer.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { Importer, ImportConfig } from './importer'; +import { Importer, ImportConfig, CreateDocsResponse } from './importer'; import { Doc, FindFileStructureResponse, @@ -33,54 +33,54 @@ export class MessageImporter extends Importer { // multiline_start_pattern regex // if it does, it is a legitimate end of line and can be pushed into the list, // if not, it must be a newline char inside a field value, so keep looking. - read(text: string) { + protected _createDocs(text: string): CreateDocsResponse { + let remainder = 0; try { - const data: Doc[] = []; + const docs: Doc[] = []; let message = ''; let line = ''; for (let i = 0; i < text.length; i++) { const char = text[i]; if (char === '\n') { - message = this.processLine(data, message, line); + message = this._processLine(docs, message, line); line = ''; } else { line += char; } } - // the last line may have been missing a newline ending - if (line !== '') { - message = this.processLine(data, message, line); - } + remainder = line.length; - // add the last message to the list if not already done + // // add the last message to the list if not already done if (message !== '') { - this.addMessage(data, message); + this._addMessage(docs, message); } // remove first line if it is blank - if (data[0] && data[0].message === '') { - data.shift(); + if (docs[0] && docs[0].message === '') { + docs.shift(); } - this._docArray = data; - return { success: true, + docs, + remainder, }; } catch (error) { return { success: false, + docs: [], + remainder, error, }; } } - processLine(data: Doc[], message: string, line: string) { + private _processLine(data: Doc[], message: string, line: string) { if (this._excludeLinesRegex === null || line.match(this._excludeLinesRegex) === null) { if (this._multilineStartRegex === null || line.match(this._multilineStartRegex) !== null) { - this.addMessage(data, message); + this._addMessage(data, message); message = ''; } else if (data.length === 0) { // discard everything before the first line that is considered the first line of a message @@ -95,7 +95,7 @@ export class MessageImporter extends Importer { return message; } - addMessage(data: Doc[], message: string) { + private _addMessage(data: Doc[], message: string) { // if the message ended \r\n (Windows line endings) // then omit the \r as well as the \n for consistency message = message.replace(/\r$/, ''); diff --git a/x-pack/plugins/ml/public/application/datavisualizer/file_based/components/import_view/importer/ndjson_importer.ts b/x-pack/plugins/ml/public/application/datavisualizer/file_based/components/import_view/importer/ndjson_importer.ts index 7f5f37abc5246..17c9de8ef4558 100644 --- a/x-pack/plugins/ml/public/application/datavisualizer/file_based/components/import_view/importer/ndjson_importer.ts +++ b/x-pack/plugins/ml/public/application/datavisualizer/file_based/components/import_view/importer/ndjson_importer.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { Importer, ImportConfig } from './importer'; +import { Importer, ImportConfig, CreateDocsResponse } from './importer'; import { FindFileStructureResponse } from '../../../../../../../common/types/file_datavisualizer'; export class NdjsonImporter extends Importer { @@ -12,27 +12,42 @@ export class NdjsonImporter extends Importer { super(settings); } - read(json: string) { + protected _createDocs(json: string): CreateDocsResponse { + let remainder = 0; try { const splitJson = json.split(/}\s*\n/); + const incompleteLastLine = json.match(/}\s*\n?$/) === null; - const ndjson: any[] = []; - for (let i = 0; i < splitJson.length; i++) { - if (splitJson[i] !== '') { - // note the extra } at the end of the line, adding back - // the one that was eaten in the split - ndjson.push(`${splitJson[i]}}`); + const docs: string[] = []; + if (splitJson.length) { + for (let i = 0; i < splitJson.length - 1; i++) { + if (splitJson[i] !== '') { + // note the extra } at the end of the line, adding back + // the one that was eaten in the split + docs.push(`${splitJson[i]}}`); + } } - } - this._docArray = ndjson; + const lastDoc = splitJson[splitJson.length - 1]; + if (lastDoc) { + if (incompleteLastLine === true) { + remainder = lastDoc.length; + } else { + docs.push(`${lastDoc}}`); + } + } + } return { success: true, + docs, + remainder, }; } catch (error) { return { success: false, + docs: [], + remainder, error, }; } diff --git a/x-pack/plugins/ml/public/application/datavisualizer/file_based/components/utils/index.ts b/x-pack/plugins/ml/public/application/datavisualizer/file_based/components/utils/index.ts index 0f0036a7c4616..492a797f7a2f2 100644 --- a/x-pack/plugins/ml/public/application/datavisualizer/file_based/components/utils/index.ts +++ b/x-pack/plugins/ml/public/application/datavisualizer/file_based/components/utils/index.ts @@ -9,7 +9,6 @@ export { hasImportPermission, processResults, readFile, - reduceData, getMaxBytes, getMaxBytesFormatted, } from './utils'; diff --git a/x-pack/plugins/ml/public/application/datavisualizer/file_based/components/utils/utils.ts b/x-pack/plugins/ml/public/application/datavisualizer/file_based/components/utils/utils.ts index 0d2016b71ed83..7b6464570e55c 100644 --- a/x-pack/plugins/ml/public/application/datavisualizer/file_based/components/utils/utils.ts +++ b/x-pack/plugins/ml/public/application/datavisualizer/file_based/components/utils/utils.ts @@ -9,12 +9,14 @@ import numeral from '@elastic/numeral'; import { ml } from '../../../../services/ml_api_service'; import { AnalysisResult, InputOverrides } from '../../../../../../common/types/file_datavisualizer'; import { - ABSOLUTE_MAX_BYTES, + MAX_FILE_SIZE_BYTES, + ABSOLUTE_MAX_FILE_SIZE_BYTES, FILE_SIZE_DISPLAY_FORMAT, } from '../../../../../../common/constants/file_datavisualizer'; import { getMlConfig } from '../../../../util/dependency_cache'; const DEFAULT_LINES_TO_SAMPLE = 1000; +const UPLOAD_SIZE_MB = 5; const overrideDefaults = { timestampFormat: undefined, @@ -34,15 +36,22 @@ export function readFile(file: File) { return new Promise((resolve, reject) => { if (file && file.size) { const reader = new FileReader(); - reader.readAsText(file); + reader.readAsArrayBuffer(file); reader.onload = (() => { return () => { + const decoder = new TextDecoder(); const data = reader.result; - if (data === '') { + if (data === null || typeof data === 'string') { + return reject(); + } + const size = UPLOAD_SIZE_MB * Math.pow(2, 20); + const fileContents = decoder.decode(data.slice(0, size)); + + if (fileContents === '') { reject(); } else { - resolve({ data }); + resolve({ fileContents, data }); } }; })(); @@ -52,17 +61,14 @@ export function readFile(file: File) { }); } -export function reduceData(data: string, mb: number) { - // assuming ascii characters in the file where 1 char is 1 byte - // TODO - change this when other non UTF-8 formats are - // supported for the read data - const size = mb * Math.pow(2, 20); - return data.length >= size ? data.slice(0, size) : data; -} - export function getMaxBytes() { - const maxBytes = getMlConfig().file_data_visualizer.max_file_size_bytes; - return maxBytes < ABSOLUTE_MAX_BYTES ? maxBytes : ABSOLUTE_MAX_BYTES; + const maxFileSize = getMlConfig().file_data_visualizer.max_file_size; + // @ts-ignore + const maxBytes = numeral(maxFileSize.toUpperCase()).value(); + if (maxBytes < MAX_FILE_SIZE_BYTES) { + return MAX_FILE_SIZE_BYTES; + } + return maxBytes < ABSOLUTE_MAX_FILE_SIZE_BYTES ? maxBytes : ABSOLUTE_MAX_FILE_SIZE_BYTES; } export function getMaxBytesFormatted() { diff --git a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/ml_job_editor/ml_job_editor.tsx b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/ml_job_editor/ml_job_editor.tsx index 0633c62f754e0..c10212ee31564 100644 --- a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/ml_job_editor/ml_job_editor.tsx +++ b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/ml_job_editor/ml_job_editor.tsx @@ -7,10 +7,9 @@ import React, { FC } from 'react'; import { EuiCodeEditor, EuiCodeEditorProps } from '@elastic/eui'; -import { expandLiteralStrings } from '../../../../../../shared_imports'; -import { xJsonMode } from '../../../../components/custom_hooks'; +import { expandLiteralStrings, XJsonMode } from '../../../../../../shared_imports'; -export const ML_EDITOR_MODE = { TEXT: 'text', JSON: 'json', XJSON: xJsonMode }; +export const ML_EDITOR_MODE = { TEXT: 'text', JSON: 'json', XJSON: new XJsonMode() }; interface MlJobEditorProps { value: string; diff --git a/x-pack/plugins/ml/public/application/util/__tests__/calc_auto_interval.js b/x-pack/plugins/ml/public/application/util/__tests__/calc_auto_interval.js deleted file mode 100644 index 0553cec5cd7d4..0000000000000 --- a/x-pack/plugins/ml/public/application/util/__tests__/calc_auto_interval.js +++ /dev/null @@ -1,140 +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 expect from '@kbn/expect'; -import moment from 'moment'; - -import { timeBucketsCalcAutoIntervalProvider } from '../calc_auto_interval'; - -describe('ML - calc auto intervals', () => { - const calcAuto = timeBucketsCalcAutoIntervalProvider(); - - describe('near interval', () => { - it('returns 0ms buckets for undefined / 0 bars', () => { - const interval = calcAuto.near(0, undefined); - expect(interval.asMilliseconds()).to.be(0); - }); - - it('returns 1000ms buckets for 60s / 100 bars', () => { - const interval = calcAuto.near(100, moment.duration(60, 's')); - expect(interval.asMilliseconds()).to.be(1000); - }); - - it('returns 5m buckets for 8h / 100 bars', () => { - const interval = calcAuto.near(100, moment.duration(8, 'h')); - expect(interval.asMinutes()).to.be(5); - }); - - it('returns 15m buckets for 1d / 100 bars', () => { - const interval = calcAuto.near(100, moment.duration(1, 'd')); - expect(interval.asMinutes()).to.be(15); - }); - - it('returns 1h buckets for 20d / 500 bars', () => { - const interval = calcAuto.near(500, moment.duration(20, 'd')); - expect(interval.asHours()).to.be(1); - }); - - it('returns 6h buckets for 100d / 500 bars', () => { - const interval = calcAuto.near(500, moment.duration(100, 'd')); - expect(interval.asHours()).to.be(6); - }); - - it('returns 24h buckets for 1y / 500 bars', () => { - const interval = calcAuto.near(500, moment.duration(1, 'y')); - expect(interval.asHours()).to.be(24); - }); - - it('returns 12h buckets for 1y / 1000 bars', () => { - const interval = calcAuto.near(1000, moment.duration(1, 'y')); - expect(interval.asHours()).to.be(12); - }); - }); - - describe('lessThan interval', () => { - it('returns 0ms buckets for undefined / 0 bars', () => { - const interval = calcAuto.lessThan(0, undefined); - expect(interval.asMilliseconds()).to.be(0); - }); - - it('returns 500ms buckets for 60s / 100 bars', () => { - const interval = calcAuto.lessThan(100, moment.duration(60, 's')); - expect(interval.asMilliseconds()).to.be(500); - }); - - it('returns 5m buckets for 8h / 100 bars', () => { - const interval = calcAuto.lessThan(100, moment.duration(8, 'h')); - expect(interval.asMinutes()).to.be(5); - }); - - it('returns 30m buckets for 1d / 100 bars', () => { - const interval = calcAuto.lessThan(100, moment.duration(1, 'd')); - expect(interval.asMinutes()).to.be(30); - }); - - it('returns 1h buckets for 20d / 500 bars', () => { - const interval = calcAuto.lessThan(500, moment.duration(20, 'd')); - expect(interval.asHours()).to.be(1); - }); - - it('returns 6h buckets for 100d / 500 bars', () => { - const interval = calcAuto.lessThan(500, moment.duration(100, 'd')); - expect(interval.asHours()).to.be(6); - }); - - it('returns 24h buckets for 1y / 500 bars', () => { - const interval = calcAuto.lessThan(500, moment.duration(1, 'y')); - expect(interval.asHours()).to.be(24); - }); - - it('returns 12h buckets for 1y / 1000 bars', () => { - const interval = calcAuto.lessThan(1000, moment.duration(1, 'y')); - expect(interval.asHours()).to.be(12); - }); - }); - - describe('atLeast interval', () => { - it('returns 0ms buckets for undefined / 0 bars', () => { - const interval = calcAuto.atLeast(0, undefined); - expect(interval.asMilliseconds()).to.be(0); - }); - - it('returns 100ms buckets for 60s / 100 bars', () => { - const interval = calcAuto.atLeast(100, moment.duration(60, 's')); - expect(interval.asMilliseconds()).to.be(100); - }); - - it('returns 1m buckets for 8h / 100 bars', () => { - const interval = calcAuto.atLeast(100, moment.duration(8, 'h')); - expect(interval.asMinutes()).to.be(1); - }); - - it('returns 10m buckets for 1d / 100 bars', () => { - const interval = calcAuto.atLeast(100, moment.duration(1, 'd')); - expect(interval.asMinutes()).to.be(10); - }); - - it('returns 30m buckets for 20d / 500 bars', () => { - const interval = calcAuto.atLeast(500, moment.duration(20, 'd')); - expect(interval.asMinutes()).to.be(30); - }); - - it('returns 4h buckets for 100d / 500 bars', () => { - const interval = calcAuto.atLeast(500, moment.duration(100, 'd')); - expect(interval.asHours()).to.be(4); - }); - - it('returns 12h buckets for 1y / 500 bars', () => { - const interval = calcAuto.atLeast(500, moment.duration(1, 'y')); - expect(interval.asHours()).to.be(12); - }); - - it('returns 8h buckets for 1y / 1000 bars', () => { - const interval = calcAuto.atLeast(1000, moment.duration(1, 'y')); - expect(interval.asHours()).to.be(8); - }); - }); -}); diff --git a/x-pack/plugins/ml/public/application/util/__tests__/chart_utils.js b/x-pack/plugins/ml/public/application/util/__tests__/chart_utils.js deleted file mode 100644 index 89df5946abe76..0000000000000 --- a/x-pack/plugins/ml/public/application/util/__tests__/chart_utils.js +++ /dev/null @@ -1,297 +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 $ from 'jquery'; -import d3 from 'd3'; -import expect from '@kbn/expect'; -import { - chartLimits, - filterAxisLabels, - getChartType, - numTicks, - showMultiBucketAnomalyMarker, - showMultiBucketAnomalyTooltip, -} from '../chart_utils'; -import { MULTI_BUCKET_IMPACT } from '../../../../common/constants/multi_bucket_impact'; -import { CHART_TYPE } from '../../explorer/explorer_constants'; - -describe('ML - chart utils', () => { - describe('chartLimits', () => { - it('returns NaN when called without data', () => { - const limits = chartLimits(); - expect(limits.min).to.be.NaN; - expect(limits.max).to.be.NaN; - }); - - it('returns {max: 625736376, min: 201039318} for some test data', () => { - const data = [ - { - date: new Date('2017-02-23T08:00:00.000Z'), - value: 228243469, - anomalyScore: 63.32916, - numberOfCauses: 1, - actual: [228243469], - typical: [133107.7703441773], - }, - { date: new Date('2017-02-23T09:00:00.000Z'), value: null }, - { date: new Date('2017-02-23T10:00:00.000Z'), value: null }, - { date: new Date('2017-02-23T11:00:00.000Z'), value: null }, - { - date: new Date('2017-02-23T12:00:00.000Z'), - value: 625736376, - anomalyScore: 97.32085, - numberOfCauses: 1, - actual: [625736376], - typical: [132830.424736973], - }, - { - date: new Date('2017-02-23T13:00:00.000Z'), - value: 201039318, - anomalyScore: 59.83488, - numberOfCauses: 1, - actual: [201039318], - typical: [132739.5267403542], - }, - ]; - - const limits = chartLimits(data); - - // {max: 625736376, min: 201039318} - expect(limits.min).to.be(201039318); - expect(limits.max).to.be(625736376); - }); - - it("adds 5% padding when min/max are the same, e.g. when there's only one data point", () => { - const data = [ - { - date: new Date('2017-02-23T08:00:00.000Z'), - value: 100, - anomalyScore: 50, - numberOfCauses: 1, - actual: [100], - typical: [100], - }, - ]; - - const limits = chartLimits(data); - expect(limits.min).to.be(95); - expect(limits.max).to.be(105); - }); - - it('returns minimum of 0 when data includes an anomaly for missing data', () => { - const data = [ - { date: new Date('2017-02-23T09:00:00.000Z'), value: 22.2 }, - { date: new Date('2017-02-23T10:00:00.000Z'), value: 23.3 }, - { date: new Date('2017-02-23T11:00:00.000Z'), value: 24.4 }, - { - date: new Date('2017-02-23T12:00:00.000Z'), - value: null, - anomalyScore: 97.32085, - actual: [0], - typical: [22.2], - }, - { date: new Date('2017-02-23T13:00:00.000Z'), value: 21.3 }, - { date: new Date('2017-02-23T14:00:00.000Z'), value: 21.2 }, - { date: new Date('2017-02-23T15:00:00.000Z'), value: 21.1 }, - ]; - - const limits = chartLimits(data); - expect(limits.min).to.be(0); - expect(limits.max).to.be(24.4); - }); - }); - - describe('filterAxisLabels', () => { - it('throws an error when called without arguments', () => { - expect(() => filterAxisLabels()).to.throwError(); - }); - - it('filters axis labels', () => { - // this provides a dummy structure of axis labels. - // the first one should always be filtered because it overflows on the - // left side of the axis. the last one should be filtered based on the - // given width parameter when doing the test calls. - $('body').append(` - <svg id="filterAxisLabels"> - <g class="x axis"> - <g class="tick" transform="translate(5,0)"> - <text dy=".71em" y="10" x="0" style="text-anchor: middle;">06:00</text> - </g> - <g class="tick" transform="translate(187.24137931034485,0)"> - <text dy=".71em" y="10" x="0" style="text-anchor: middle;">12:00</text> - </g> - <g class="tick" transform="translate(486.82758620689657,0)"> - <text dy=".71em" y="10" x="0" style="text-anchor: middle;">18:00</text> - </g> - <g class="tick" transform="translate(786.4137931034483,0)"> - <text dy=".71em" y="10" x="0" style="text-anchor: middle;">00:00</text> - </g> - </g> - </svg> - `); - - const selector = '#filterAxisLabels .x.axis'; - - // given this width, the last tick should not be removed - filterAxisLabels(d3.selectAll(selector), 1000); - expect(d3.selectAll(selector + ' .tick text').size()).to.be(3); - - // given this width, the last tick should be removed - filterAxisLabels(d3.selectAll(selector), 790); - expect(d3.selectAll(selector + ' .tick text').size()).to.be(2); - - // clean up - $('#filterAxisLabels').remove(); - }); - }); - - describe('getChartType', () => { - const singleMetricConfig = { - metricFunction: 'avg', - functionDescription: 'mean', - fieldName: 'responsetime', - entityFields: [], - }; - - const multiMetricConfig = { - metricFunction: 'avg', - functionDescription: 'mean', - fieldName: 'responsetime', - entityFields: [ - { - fieldName: 'airline', - fieldValue: 'AAL', - fieldType: 'partition', - }, - ], - }; - - const populationConfig = { - metricFunction: 'avg', - functionDescription: 'mean', - fieldName: 'http.response.body.bytes', - entityFields: [ - { - fieldName: 'source.ip', - fieldValue: '10.11.12.13', - fieldType: 'over', - }, - ], - }; - - const rareConfig = { - metricFunction: 'count', - functionDescription: 'rare', - entityFields: [ - { - fieldName: 'http.response.status_code', - fieldValue: '404', - fieldType: 'by', - }, - ], - }; - - const varpModelPlotConfig = { - metricFunction: null, - functionDescription: 'varp', - fieldName: 'NetworkOut', - entityFields: [ - { - fieldName: 'instance', - fieldValue: 'i-ef74d410', - fieldType: 'over', - }, - ], - }; - - const overScriptFieldModelPlotConfig = { - metricFunction: 'count', - functionDescription: 'count', - fieldName: 'highest_registered_domain', - entityFields: [ - { - fieldName: 'highest_registered_domain', - fieldValue: 'elastic.co', - fieldType: 'over', - }, - ], - datafeedConfig: { - script_fields: { - highest_registered_domain: { - script: { - source: "return domainSplit(doc['query'].value, params).get(1);", - lang: 'painless', - }, - ignore_failure: false, - }, - }, - }, - }; - - it('returns single metric chart type as expected for configs', () => { - expect(getChartType(singleMetricConfig)).to.be(CHART_TYPE.SINGLE_METRIC); - expect(getChartType(multiMetricConfig)).to.be(CHART_TYPE.SINGLE_METRIC); - expect(getChartType(varpModelPlotConfig)).to.be(CHART_TYPE.SINGLE_METRIC); - expect(getChartType(overScriptFieldModelPlotConfig)).to.be(CHART_TYPE.SINGLE_METRIC); - }); - - it('returns event distribution chart type as expected for configs', () => { - expect(getChartType(rareConfig)).to.be(CHART_TYPE.EVENT_DISTRIBUTION); - }); - - it('returns population distribution chart type as expected for configs', () => { - expect(getChartType(populationConfig)).to.be(CHART_TYPE.POPULATION_DISTRIBUTION); - }); - }); - - describe('numTicks', () => { - it('returns 10 for 1000', () => { - expect(numTicks(1000)).to.be(10); - }); - }); - - describe('showMultiBucketAnomalyMarker', () => { - it('returns true for points with multiBucketImpact at or above medium impact', () => { - expect(showMultiBucketAnomalyMarker({ multiBucketImpact: MULTI_BUCKET_IMPACT.HIGH })).to.be( - true - ); - expect(showMultiBucketAnomalyMarker({ multiBucketImpact: MULTI_BUCKET_IMPACT.MEDIUM })).to.be( - true - ); - }); - - it('returns false for points with multiBucketImpact missing or below medium impact', () => { - expect(showMultiBucketAnomalyMarker({})).to.be(false); - expect(showMultiBucketAnomalyMarker({ multiBucketImpact: MULTI_BUCKET_IMPACT.LOW })).to.be( - false - ); - expect(showMultiBucketAnomalyMarker({ multiBucketImpact: MULTI_BUCKET_IMPACT.NONE })).to.be( - false - ); - }); - }); - - describe('showMultiBucketAnomalyTooltip', () => { - it('returns true for points with multiBucketImpact at or above low impact', () => { - expect(showMultiBucketAnomalyTooltip({ multiBucketImpact: MULTI_BUCKET_IMPACT.HIGH })).to.be( - true - ); - expect( - showMultiBucketAnomalyTooltip({ multiBucketImpact: MULTI_BUCKET_IMPACT.MEDIUM }) - ).to.be(true); - expect(showMultiBucketAnomalyTooltip({ multiBucketImpact: MULTI_BUCKET_IMPACT.LOW })).to.be( - true - ); - }); - - it('returns false for points with multiBucketImpact missing or below medium impact', () => { - expect(showMultiBucketAnomalyTooltip({})).to.be(false); - expect(showMultiBucketAnomalyTooltip({ multiBucketImpact: MULTI_BUCKET_IMPACT.NONE })).to.be( - false - ); - }); - }); -}); diff --git a/x-pack/plugins/ml/public/application/util/__tests__/string_utils.js b/x-pack/plugins/ml/public/application/util/__tests__/string_utils.js deleted file mode 100644 index 702e9dfd96205..0000000000000 --- a/x-pack/plugins/ml/public/application/util/__tests__/string_utils.js +++ /dev/null @@ -1,229 +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 expect from '@kbn/expect'; -import { - replaceStringTokens, - detectorToString, - sortByKey, - guessTimeFormat, - toLocaleString, - mlEscape, - escapeForElasticsearchQuery, -} from '../string_utils'; - -describe('ML - string utils', () => { - describe('replaceStringTokens', () => { - const testRecord = { - job_id: 'test_job', - result_type: 'record', - probability: 0.0191711, - record_score: 4.3, - bucket_span: 300, - detector_index: 0, - timestamp: 1454890500000, - function: 'mean', - function_description: 'mean', - field_name: 'responsetime', - user: "Des O'Connor", - testfield1: 'test$tring=[+-?]', - testfield2: '{<()>}', - testfield3: 'host=\\\\test@uk.dev', - }; - - it('returns correct values without URI encoding', () => { - const result = replaceStringTokens('user=$user$,time=$timestamp$', testRecord, false); - expect(result).to.be("user=Des O'Connor,time=1454890500000"); - }); - - it('returns correct values for missing token without URI encoding', () => { - const result = replaceStringTokens('user=$username$,time=$timestamp$', testRecord, false); - expect(result).to.be('user=$username$,time=1454890500000'); - }); - - it('returns correct values with URI encoding', () => { - const testString1 = 'https://www.google.co.uk/webhp#q=$testfield1$'; - const testString2 = 'https://www.google.co.uk/webhp#q=$testfield2$'; - const testString3 = 'https://www.google.co.uk/webhp#q=$testfield3$'; - const testString4 = 'https://www.google.co.uk/webhp#q=$user$'; - - const result1 = replaceStringTokens(testString1, testRecord, true); - const result2 = replaceStringTokens(testString2, testRecord, true); - const result3 = replaceStringTokens(testString3, testRecord, true); - const result4 = replaceStringTokens(testString4, testRecord, true); - - expect(result1).to.be('https://www.google.co.uk/webhp#q=test%24tring%3D%5B%2B-%3F%5D'); - expect(result2).to.be('https://www.google.co.uk/webhp#q=%7B%3C()%3E%7D'); - expect(result3).to.be('https://www.google.co.uk/webhp#q=host%3D%5C%5Ctest%40uk.dev'); - expect(result4).to.be("https://www.google.co.uk/webhp#q=Des%20O'Connor"); - }); - - it('returns correct values for missing token with URI encoding', () => { - const testString = 'https://www.google.co.uk/webhp#q=$username$&time=$timestamp$'; - const result = replaceStringTokens(testString, testRecord, true); - expect(result).to.be('https://www.google.co.uk/webhp#q=$username$&time=1454890500000'); - }); - }); - - describe('detectorToString', () => { - it('returns the correct descriptions for detectors', () => { - const detector1 = { - function: 'count', - }; - - const detector2 = { - function: 'count', - by_field_name: 'airline', - use_null: false, - }; - - const detector3 = { - function: 'mean', - field_name: 'CPUUtilization', - partition_field_name: 'region', - by_field_name: 'host', - over_field_name: 'user', - exclude_frequent: 'all', - }; - - expect(detectorToString(detector1)).to.be('count'); - expect(detectorToString(detector2)).to.be('count by airline use_null=false'); - expect(detectorToString(detector3)).to.be( - 'mean(CPUUtilization) by host over user partition_field_name=region exclude_frequent=all' - ); - }); - }); - - describe('sortByKey', () => { - const obj = { - zebra: 'stripes', - giraffe: 'neck', - elephant: 'trunk', - }; - - const valueComparator = function(value) { - return value; - }; - - it('returns correct ordering with default comparator', () => { - const result = sortByKey(obj, false); - const keys = Object.keys(result); - expect(keys[0]).to.be('elephant'); - expect(keys[1]).to.be('giraffe'); - expect(keys[2]).to.be('zebra'); - }); - - it('returns correct ordering with default comparator and order reversed', () => { - const result = sortByKey(obj, true); - const keys = Object.keys(result); - expect(keys[0]).to.be('zebra'); - expect(keys[1]).to.be('giraffe'); - expect(keys[2]).to.be('elephant'); - }); - - it('returns correct ordering with comparator', () => { - const result = sortByKey(obj, false, valueComparator); - const keys = Object.keys(result); - expect(keys[0]).to.be('giraffe'); - expect(keys[1]).to.be('zebra'); - expect(keys[2]).to.be('elephant'); - }); - - it('returns correct ordering with comparator and order reversed', () => { - const result = sortByKey(obj, true, valueComparator); - const keys = Object.keys(result); - expect(keys[0]).to.be('elephant'); - expect(keys[1]).to.be('zebra'); - expect(keys[2]).to.be('giraffe'); - }); - }); - - describe('guessTimeFormat', () => { - it('returns correct format for various dates', () => { - expect(guessTimeFormat('2017-03-24T00:00')).to.be("yyyy-MM-dd'T'HH:mm"); - expect(guessTimeFormat('2017-03-24 00:00')).to.be('yyyy-MM-dd HH:mm'); - expect(guessTimeFormat('2017-03-24 00:00:00')).to.be('yyyy-MM-dd HH:mm:ss'); - expect(guessTimeFormat('2017-03-24 00:00:00Z')).to.be('yyyy-MM-dd HH:mm:ssX'); - expect(guessTimeFormat('2017-03-24 00:00:00.000')).to.be('yyyy-MM-dd HH:mm:ss.SSS'); - expect(guessTimeFormat('2017-03-24 00:00:00:000')).to.be('yyyy-MM-dd HH:mm:ss:SSS'); - expect(guessTimeFormat('2017-03-24 00:00:00.000+00:00:00')).to.be( - 'yyyy-MM-dd HH:mm:ss.SSSXXXXX' - ); - expect(guessTimeFormat('2017-03-24 00:00:00.000+00:00')).to.be('yyyy-MM-dd HH:mm:ss.SSSXXX'); - expect(guessTimeFormat('2017-03-24 00:00:00.000+000000')).to.be( - 'yyyy-MM-dd HH:mm:ss.SSSXXXX' - ); - expect(guessTimeFormat('2017-03-24 00:00:00.000+0000')).to.be('yyyy-MM-dd HH:mm:ss.SSSZ'); - expect(guessTimeFormat('2017-03-24 00:00:00.000+00')).to.be('yyyy-MM-dd HH:mm:ss.SSSX'); - expect(guessTimeFormat('2017-03-24 00:00:00.000Z')).to.be('yyyy-MM-dd HH:mm:ss.SSSX'); - expect(guessTimeFormat('2017-03-24 00:00:00.000 GMT')).to.be('yyyy-MM-dd HH:mm:ss.SSS zzz'); - expect(guessTimeFormat('2017-03-24 00:00:00 GMT')).to.be('yyyy-MM-dd HH:mm:ss zzz'); - expect(guessTimeFormat('2017 03 24 00:00:00.000')).to.be('yyyy MM dd HH:mm:ss.SSS'); - expect(guessTimeFormat('2017.03.24 00:00:00.000')).to.be('yyyy.MM.dd HH:mm:ss.SSS'); - expect(guessTimeFormat('2017/03/24 00:00:00.000')).to.be('yyyy/MM/dd HH:mm:ss.SSS'); - expect(guessTimeFormat('24/03/2017 00:00:00.000')).to.be('dd/MM/yyyy HH:mm:ss.SSS'); - expect(guessTimeFormat('03 24 2017 00:00:00.000')).to.be('MM dd yyyy HH:mm:ss.SSS'); - expect(guessTimeFormat('03/24/2017 00:00:00.000')).to.be('MM/dd/yyyy HH:mm:ss.SSS'); - expect(guessTimeFormat('2017 Mar 24 00:00:00.000')).to.be('yyyy MMM dd HH:mm:ss.SSS'); - expect(guessTimeFormat('Mar 24 2017 00:00:00.000')).to.be('MMM dd yyyy HH:mm:ss.SSS'); - expect(guessTimeFormat('24 Mar 2017 00:00:00.000')).to.be('dd MMM yyyy HH:mm:ss.SSS'); - expect(guessTimeFormat('1490313600')).to.be('epoch'); - expect(guessTimeFormat('1490313600000')).to.be('epoch_ms'); - }); - }); - - describe('toLocaleString', () => { - it('returns correct comma placement for large numbers', () => { - expect(toLocaleString(1)).to.be('1'); - expect(toLocaleString(10)).to.be('10'); - expect(toLocaleString(100)).to.be('100'); - expect(toLocaleString(1000)).to.be('1,000'); - expect(toLocaleString(10000)).to.be('10,000'); - expect(toLocaleString(100000)).to.be('100,000'); - expect(toLocaleString(1000000)).to.be('1,000,000'); - expect(toLocaleString(10000000)).to.be('10,000,000'); - expect(toLocaleString(100000000)).to.be('100,000,000'); - expect(toLocaleString(1000000000)).to.be('1,000,000,000'); - }); - }); - - describe('mlEscape', () => { - it('returns correct escaping of characters', () => { - expect(mlEscape('foo&bar')).to.be('foo&bar'); - expect(mlEscape('foo<bar')).to.be('foo<bar'); - expect(mlEscape('foo>bar')).to.be('foo>bar'); - expect(mlEscape('foo"bar')).to.be('foo"bar'); - expect(mlEscape("foo'bar")).to.be('foo'bar'); - expect(mlEscape('foo/bar')).to.be('foo/bar'); - }); - }); - - describe('escapeForElasticsearchQuery', () => { - it('returns correct escaping of reserved elasticsearch characters', () => { - expect(escapeForElasticsearchQuery('foo+bar')).to.be('foo\\+bar'); - expect(escapeForElasticsearchQuery('foo-bar')).to.be('foo\\-bar'); - expect(escapeForElasticsearchQuery('foo=bar')).to.be('foo\\=bar'); - expect(escapeForElasticsearchQuery('foo&&bar')).to.be('foo\\&\\&bar'); - expect(escapeForElasticsearchQuery('foo||bar')).to.be('foo\\|\\|bar'); - expect(escapeForElasticsearchQuery('foo>bar')).to.be('foo\\>bar'); - expect(escapeForElasticsearchQuery('foo<bar')).to.be('foo\\<bar'); - expect(escapeForElasticsearchQuery('foo!bar')).to.be('foo\\!bar'); - expect(escapeForElasticsearchQuery('foo(bar')).to.be('foo\\(bar'); - expect(escapeForElasticsearchQuery('foo)bar')).to.be('foo\\)bar'); - expect(escapeForElasticsearchQuery('foo{bar')).to.be('foo\\{bar'); - expect(escapeForElasticsearchQuery('foo[bar')).to.be('foo\\[bar'); - expect(escapeForElasticsearchQuery('foo]bar')).to.be('foo\\]bar'); - expect(escapeForElasticsearchQuery('foo^bar')).to.be('foo\\^bar'); - expect(escapeForElasticsearchQuery('foo"bar')).to.be('foo\\"bar'); - expect(escapeForElasticsearchQuery('foo~bar')).to.be('foo\\~bar'); - expect(escapeForElasticsearchQuery('foo*bar')).to.be('foo\\*bar'); - expect(escapeForElasticsearchQuery('foo?bar')).to.be('foo\\?bar'); - expect(escapeForElasticsearchQuery('foo:bar')).to.be('foo\\:bar'); - expect(escapeForElasticsearchQuery('foo\\bar')).to.be('foo\\\\bar'); - expect(escapeForElasticsearchQuery('foo/bar')).to.be('foo\\/bar'); - }); - }); -}); diff --git a/x-pack/plugins/ml/public/application/util/calc_auto_interval.test.js b/x-pack/plugins/ml/public/application/util/calc_auto_interval.test.js new file mode 100644 index 0000000000000..c36557a0e6951 --- /dev/null +++ b/x-pack/plugins/ml/public/application/util/calc_auto_interval.test.js @@ -0,0 +1,139 @@ +/* + * 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 moment from 'moment'; + +import { timeBucketsCalcAutoIntervalProvider } from './calc_auto_interval'; + +describe('ML - calc auto intervals', () => { + const calcAuto = timeBucketsCalcAutoIntervalProvider(); + + describe('near interval', () => { + test('returns 0ms buckets for undefined / 0 bars', () => { + const interval = calcAuto.near(0, undefined); + expect(interval.asMilliseconds()).toBe(0); + }); + + test('returns 1000ms buckets for 60s / 100 bars', () => { + const interval = calcAuto.near(100, moment.duration(60, 's')); + expect(interval.asMilliseconds()).toBe(1000); + }); + + test('returns 5m buckets for 8h / 100 bars', () => { + const interval = calcAuto.near(100, moment.duration(8, 'h')); + expect(interval.asMinutes()).toBe(5); + }); + + test('returns 15m buckets for 1d / 100 bars', () => { + const interval = calcAuto.near(100, moment.duration(1, 'd')); + expect(interval.asMinutes()).toBe(15); + }); + + test('returns 1h buckets for 20d / 500 bars', () => { + const interval = calcAuto.near(500, moment.duration(20, 'd')); + expect(interval.asHours()).toBe(1); + }); + + test('returns 6h buckets for 100d / 500 bars', () => { + const interval = calcAuto.near(500, moment.duration(100, 'd')); + expect(interval.asHours()).toBe(6); + }); + + test('returns 24h buckets for 1y / 500 bars', () => { + const interval = calcAuto.near(500, moment.duration(1, 'y')); + expect(interval.asHours()).toBe(24); + }); + + test('returns 12h buckets for 1y / 1000 bars', () => { + const interval = calcAuto.near(1000, moment.duration(1, 'y')); + expect(interval.asHours()).toBe(12); + }); + }); + + describe('lessThan interval', () => { + test('returns 0ms buckets for undefined / 0 bars', () => { + const interval = calcAuto.lessThan(0, undefined); + expect(interval.asMilliseconds()).toBe(0); + }); + + test('returns 500ms buckets for 60s / 100 bars', () => { + const interval = calcAuto.lessThan(100, moment.duration(60, 's')); + expect(interval.asMilliseconds()).toBe(500); + }); + + test('returns 5m buckets for 8h / 100 bars', () => { + const interval = calcAuto.lessThan(100, moment.duration(8, 'h')); + expect(interval.asMinutes()).toBe(5); + }); + + test('returns 30m buckets for 1d / 100 bars', () => { + const interval = calcAuto.lessThan(100, moment.duration(1, 'd')); + expect(interval.asMinutes()).toBe(30); + }); + + test('returns 1h buckets for 20d / 500 bars', () => { + const interval = calcAuto.lessThan(500, moment.duration(20, 'd')); + expect(interval.asHours()).toBe(1); + }); + + test('returns 6h buckets for 100d / 500 bars', () => { + const interval = calcAuto.lessThan(500, moment.duration(100, 'd')); + expect(interval.asHours()).toBe(6); + }); + + test('returns 24h buckets for 1y / 500 bars', () => { + const interval = calcAuto.lessThan(500, moment.duration(1, 'y')); + expect(interval.asHours()).toBe(24); + }); + + test('returns 12h buckets for 1y / 1000 bars', () => { + const interval = calcAuto.lessThan(1000, moment.duration(1, 'y')); + expect(interval.asHours()).toBe(12); + }); + }); + + describe('atLeast interval', () => { + test('returns 0ms buckets for undefined / 0 bars', () => { + const interval = calcAuto.atLeast(0, undefined); + expect(interval.asMilliseconds()).toBe(0); + }); + + test('returns 100ms buckets for 60s / 100 bars', () => { + const interval = calcAuto.atLeast(100, moment.duration(60, 's')); + expect(interval.asMilliseconds()).toBe(100); + }); + + test('returns 1m buckets for 8h / 100 bars', () => { + const interval = calcAuto.atLeast(100, moment.duration(8, 'h')); + expect(interval.asMinutes()).toBe(1); + }); + + test('returns 10m buckets for 1d / 100 bars', () => { + const interval = calcAuto.atLeast(100, moment.duration(1, 'd')); + expect(interval.asMinutes()).toBe(10); + }); + + test('returns 30m buckets for 20d / 500 bars', () => { + const interval = calcAuto.atLeast(500, moment.duration(20, 'd')); + expect(interval.asMinutes()).toBe(30); + }); + + test('returns 4h buckets for 100d / 500 bars', () => { + const interval = calcAuto.atLeast(500, moment.duration(100, 'd')); + expect(interval.asHours()).toBe(4); + }); + + test('returns 12h buckets for 1y / 500 bars', () => { + const interval = calcAuto.atLeast(500, moment.duration(1, 'y')); + expect(interval.asHours()).toBe(12); + }); + + test('returns 8h buckets for 1y / 1000 bars', () => { + const interval = calcAuto.atLeast(1000, moment.duration(1, 'y')); + expect(interval.asHours()).toBe(8); + }); + }); +}); diff --git a/x-pack/plugins/ml/public/application/util/chart_utils.test.js b/x-pack/plugins/ml/public/application/util/chart_utils.test.js index 4b33cb131be7f..57aea3c0ab5aa 100644 --- a/x-pack/plugins/ml/public/application/util/chart_utils.test.js +++ b/x-pack/plugins/ml/public/application/util/chart_utils.test.js @@ -29,246 +29,488 @@ const timefilter = getTimefilter(); import d3 from 'd3'; import moment from 'moment'; -import { mount } from 'enzyme'; import React from 'react'; +import { render } from '@testing-library/react'; import { + chartLimits, + getChartType, getExploreSeriesLink, getTickValues, - isLabelLengthAboveThreshold, getXTransform, + isLabelLengthAboveThreshold, + numTicks, removeLabelOverlap, + showMultiBucketAnomalyMarker, + showMultiBucketAnomalyTooltip, } from './chart_utils'; +import { MULTI_BUCKET_IMPACT } from '../../../common/constants/multi_bucket_impact'; +import { CHART_TYPE } from '../explorer/explorer_constants'; + timefilter.setTime({ from: moment(seriesConfig.selectedEarliest).toISOString(), to: moment(seriesConfig.selectedLatest).toISOString(), }); -describe('getExploreSeriesLink', () => { - test('get timeseriesexplorer link', () => { - const link = getExploreSeriesLink(seriesConfig); - const expectedLink = - `#/timeseriesexplorer?_g=(ml:(jobIds:!(population-03)),` + - `refreshInterval:(display:Off,pause:!f,value:0),time:(from:'2017-02-23T00:00:00.000Z',mode:absolute,` + - `to:'2017-02-23T23:59:59.999Z'))&_a=(mlTimeSeriesExplorer%3A(detectorIndex%3A0%2Centities%3A` + - `(nginx.access.remote_ip%3A'72.57.0.53')%2Czoom%3A(from%3A'2017-02-19T20%3A00%3A00.000Z'%2Cto%3A'2017-02-27T04%3A00%3A00.000Z'))` + - `%2Cquery%3A(query_string%3A(analyze_wildcard%3A!t%2Cquery%3A'*')))`; - - expect(link).toBe(expectedLink); - }); -}); +describe('ML - chart utils', () => { + describe('chartLimits', () => { + test('returns NaN when called without data', () => { + const limits = chartLimits(); + expect(limits.min).toBeNaN(); + expect(limits.max).toBeNaN(); + }); -describe('getTickValues', () => { - test('farequote sample data', () => { - const tickValues = getTickValues(1486656000000, 14400000, 1486606500000, 1486719900000); - - expect(tickValues).toEqual([ - 1486612800000, - 1486627200000, - 1486641600000, - 1486656000000, - 1486670400000, - 1486684800000, - 1486699200000, - 1486713600000, - ]); - }); + test('returns {max: 625736376, min: 201039318} for some test data', () => { + const data = [ + { + date: new Date('2017-02-23T08:00:00.000Z'), + value: 228243469, + anomalyScore: 63.32916, + numberOfCauses: 1, + actual: [228243469], + typical: [133107.7703441773], + }, + { date: new Date('2017-02-23T09:00:00.000Z'), value: null }, + { date: new Date('2017-02-23T10:00:00.000Z'), value: null }, + { date: new Date('2017-02-23T11:00:00.000Z'), value: null }, + { + date: new Date('2017-02-23T12:00:00.000Z'), + value: 625736376, + anomalyScore: 97.32085, + numberOfCauses: 1, + actual: [625736376], + typical: [132830.424736973], + }, + { + date: new Date('2017-02-23T13:00:00.000Z'), + value: 201039318, + anomalyScore: 59.83488, + numberOfCauses: 1, + actual: [201039318], + typical: [132739.5267403542], + }, + ]; + + const limits = chartLimits(data); + + // {max: 625736376, min: 201039318} + expect(limits.min).toBe(201039318); + expect(limits.max).toBe(625736376); + }); - test('filebeat sample data', () => { - const tickValues = getTickValues(1486080000000, 14400000, 1485860400000, 1486314000000); - expect(tickValues).toEqual([ - 1485864000000, - 1485878400000, - 1485892800000, - 1485907200000, - 1485921600000, - 1485936000000, - 1485950400000, - 1485964800000, - 1485979200000, - 1485993600000, - 1486008000000, - 1486022400000, - 1486036800000, - 1486051200000, - 1486065600000, - 1486080000000, - 1486094400000, - 1486108800000, - 1486123200000, - 1486137600000, - 1486152000000, - 1486166400000, - 1486180800000, - 1486195200000, - 1486209600000, - 1486224000000, - 1486238400000, - 1486252800000, - 1486267200000, - 1486281600000, - 1486296000000, - 1486310400000, - ]); + test("adds 5% padding when min/max are the same, e.g. when there's only one data point", () => { + const data = [ + { + date: new Date('2017-02-23T08:00:00.000Z'), + value: 100, + anomalyScore: 50, + numberOfCauses: 1, + actual: [100], + typical: [100], + }, + ]; + + const limits = chartLimits(data); + expect(limits.min).toBe(95); + expect(limits.max).toBe(105); + }); + + test('returns minimum of 0 when data includes an anomaly for missing data', () => { + const data = [ + { date: new Date('2017-02-23T09:00:00.000Z'), value: 22.2 }, + { date: new Date('2017-02-23T10:00:00.000Z'), value: 23.3 }, + { date: new Date('2017-02-23T11:00:00.000Z'), value: 24.4 }, + { + date: new Date('2017-02-23T12:00:00.000Z'), + value: null, + anomalyScore: 97.32085, + actual: [0], + typical: [22.2], + }, + { date: new Date('2017-02-23T13:00:00.000Z'), value: 21.3 }, + { date: new Date('2017-02-23T14:00:00.000Z'), value: 21.2 }, + { date: new Date('2017-02-23T15:00:00.000Z'), value: 21.1 }, + ]; + + const limits = chartLimits(data); + expect(limits.min).toBe(0); + expect(limits.max).toBe(24.4); + }); }); - test('gallery sample data', () => { - const tickValues = getTickValues(1518652800000, 604800000, 1518274800000, 1519635600000); - expect(tickValues).toEqual([1518652800000, 1519257600000]); + describe('getChartType', () => { + const singleMetricConfig = { + metricFunction: 'avg', + functionDescription: 'mean', + fieldName: 'responsetime', + entityFields: [], + }; + + const multiMetricConfig = { + metricFunction: 'avg', + functionDescription: 'mean', + fieldName: 'responsetime', + entityFields: [ + { + fieldName: 'airline', + fieldValue: 'AAL', + fieldType: 'partition', + }, + ], + }; + + const populationConfig = { + metricFunction: 'avg', + functionDescription: 'mean', + fieldName: 'http.response.body.bytes', + entityFields: [ + { + fieldName: 'source.ip', + fieldValue: '10.11.12.13', + fieldType: 'over', + }, + ], + }; + + const rareConfig = { + metricFunction: 'count', + functionDescription: 'rare', + entityFields: [ + { + fieldName: 'http.response.status_code', + fieldValue: '404', + fieldType: 'by', + }, + ], + }; + + const varpModelPlotConfig = { + metricFunction: null, + functionDescription: 'varp', + fieldName: 'NetworkOut', + entityFields: [ + { + fieldName: 'instance', + fieldValue: 'i-ef74d410', + fieldType: 'over', + }, + ], + }; + + const overScriptFieldModelPlotConfig = { + metricFunction: 'count', + functionDescription: 'count', + fieldName: 'highest_registered_domain', + entityFields: [ + { + fieldName: 'highest_registered_domain', + fieldValue: 'elastic.co', + fieldType: 'over', + }, + ], + datafeedConfig: { + script_fields: { + highest_registered_domain: { + script: { + source: "return domainSplit(doc['query'].value, params).get(1);", + lang: 'painless', + }, + ignore_failure: false, + }, + }, + }, + }; + + test('returns single metric chart type as expected for configs', () => { + expect(getChartType(singleMetricConfig)).toBe(CHART_TYPE.SINGLE_METRIC); + expect(getChartType(multiMetricConfig)).toBe(CHART_TYPE.SINGLE_METRIC); + expect(getChartType(varpModelPlotConfig)).toBe(CHART_TYPE.SINGLE_METRIC); + expect(getChartType(overScriptFieldModelPlotConfig)).toBe(CHART_TYPE.SINGLE_METRIC); + }); + + test('returns event distribution chart type as expected for configs', () => { + expect(getChartType(rareConfig)).toBe(CHART_TYPE.EVENT_DISTRIBUTION); + }); + + test('returns population distribution chart type as expected for configs', () => { + expect(getChartType(populationConfig)).toBe(CHART_TYPE.POPULATION_DISTRIBUTION); + }); }); - test('invalid tickIntervals trigger an error', () => { - expect(() => { - getTickValues(1518652800000, 0, 1518274800000, 1519635600000); - }).toThrow(); - expect(() => { - getTickValues(1518652800000, -604800000, 1518274800000, 1519635600000); - }).toThrow(); + describe('getExploreSeriesLink', () => { + test('get timeseriesexplorer link', () => { + const link = getExploreSeriesLink(seriesConfig); + const expectedLink = + `#/timeseriesexplorer?_g=(ml:(jobIds:!(population-03)),` + + `refreshInterval:(display:Off,pause:!f,value:0),time:(from:'2017-02-23T00:00:00.000Z',mode:absolute,` + + `to:'2017-02-23T23:59:59.999Z'))&_a=(mlTimeSeriesExplorer%3A(detectorIndex%3A0%2Centities%3A` + + `(nginx.access.remote_ip%3A'72.57.0.53')%2Czoom%3A(from%3A'2017-02-19T20%3A00%3A00.000Z'%2Cto%3A'2017-02-27T04%3A00%3A00.000Z'))` + + `%2Cquery%3A(query_string%3A(analyze_wildcard%3A!t%2Cquery%3A'*')))`; + + expect(link).toBe(expectedLink); + }); }); -}); -describe('isLabelLengthAboveThreshold', () => { - test('short label', () => { - const isLongLabel = isLabelLengthAboveThreshold({ - detectorLabel: 'count', - entityFields: seriesConfig.entityFields, + describe('numTicks', () => { + test('returns 10 for 1000', () => { + expect(numTicks(1000)).toBe(10); }); - expect(isLongLabel).toBeFalsy(); }); - test('long label', () => { - const isLongLabel = isLabelLengthAboveThreshold(seriesConfig); - expect(isLongLabel).toBeTruthy(); + describe('showMultiBucketAnomalyMarker', () => { + test('returns true for points with multiBucketImpact at or above medium impact', () => { + expect(showMultiBucketAnomalyMarker({ multiBucketImpact: MULTI_BUCKET_IMPACT.HIGH })).toBe( + true + ); + expect(showMultiBucketAnomalyMarker({ multiBucketImpact: MULTI_BUCKET_IMPACT.MEDIUM })).toBe( + true + ); + }); + + test('returns false for points with multiBucketImpact missing or below medium impact', () => { + expect(showMultiBucketAnomalyMarker({})).toBe(false); + expect(showMultiBucketAnomalyMarker({ multiBucketImpact: MULTI_BUCKET_IMPACT.LOW })).toBe( + false + ); + expect(showMultiBucketAnomalyMarker({ multiBucketImpact: MULTI_BUCKET_IMPACT.NONE })).toBe( + false + ); + }); }); -}); -describe('getXTransform', () => { - const expectedXTransform = 0.007167499999999999; + describe('showMultiBucketAnomalyTooltip', () => { + test('returns true for points with multiBucketImpact at or above low impact', () => { + expect(showMultiBucketAnomalyTooltip({ multiBucketImpact: MULTI_BUCKET_IMPACT.HIGH })).toBe( + true + ); + expect(showMultiBucketAnomalyTooltip({ multiBucketImpact: MULTI_BUCKET_IMPACT.MEDIUM })).toBe( + true + ); + expect(showMultiBucketAnomalyTooltip({ multiBucketImpact: MULTI_BUCKET_IMPACT.LOW })).toBe( + true + ); + }); - test('Chrome/Safari/Firefox String variant.', () => { - const transformStr = 'translate(0.007167499999999999,0)'; - const xTransform = getXTransform(transformStr); - expect(xTransform).toEqual(expectedXTransform); + test('returns false for points with multiBucketImpact missing or below medium impact', () => { + expect(showMultiBucketAnomalyTooltip({})).toBe(false); + expect(showMultiBucketAnomalyTooltip({ multiBucketImpact: MULTI_BUCKET_IMPACT.NONE })).toBe( + false + ); + }); }); - test('IE11 String variant.', () => { - const transformStr = 'translate(0.007167499999999999)'; - const xTransform = getXTransform(transformStr); - expect(xTransform).toEqual(expectedXTransform); + describe('getTickValues', () => { + test('farequote sample data', () => { + const tickValues = getTickValues(1486656000000, 14400000, 1486606500000, 1486719900000); + + expect(tickValues).toEqual([ + 1486612800000, + 1486627200000, + 1486641600000, + 1486656000000, + 1486670400000, + 1486684800000, + 1486699200000, + 1486713600000, + ]); + }); + + test('filebeat sample data', () => { + const tickValues = getTickValues(1486080000000, 14400000, 1485860400000, 1486314000000); + expect(tickValues).toEqual([ + 1485864000000, + 1485878400000, + 1485892800000, + 1485907200000, + 1485921600000, + 1485936000000, + 1485950400000, + 1485964800000, + 1485979200000, + 1485993600000, + 1486008000000, + 1486022400000, + 1486036800000, + 1486051200000, + 1486065600000, + 1486080000000, + 1486094400000, + 1486108800000, + 1486123200000, + 1486137600000, + 1486152000000, + 1486166400000, + 1486180800000, + 1486195200000, + 1486209600000, + 1486224000000, + 1486238400000, + 1486252800000, + 1486267200000, + 1486281600000, + 1486296000000, + 1486310400000, + ]); + }); + + test('gallery sample data', () => { + const tickValues = getTickValues(1518652800000, 604800000, 1518274800000, 1519635600000); + expect(tickValues).toEqual([1518652800000, 1519257600000]); + }); + + test('invalid tickIntervals trigger an error', () => { + expect(() => { + getTickValues(1518652800000, 0, 1518274800000, 1519635600000); + }).toThrow(); + expect(() => { + getTickValues(1518652800000, -604800000, 1518274800000, 1519635600000); + }).toThrow(); + }); }); - test('Invalid String.', () => { - const transformStr = 'translate()'; - const xTransform = getXTransform(transformStr); - expect(xTransform).toEqual(NaN); + describe('isLabelLengthAboveThreshold', () => { + test('short label', () => { + const isLongLabel = isLabelLengthAboveThreshold({ + detectorLabel: 'count', + entityFields: seriesConfig.entityFields, + }); + expect(isLongLabel).toBeFalsy(); + }); + + test('long label', () => { + const isLongLabel = isLabelLengthAboveThreshold(seriesConfig); + expect(isLongLabel).toBeTruthy(); + }); }); -}); -describe('removeLabelOverlap', () => { - const originalGetBBox = SVGElement.prototype.getBBox; - - // This resembles how ExplorerChart renders its x axis. - // We set up this boilerplate so we can then run removeLabelOverlap() - // on some "real" structure. - function axisSetup({ interval, plotEarliest, plotLatest, startTimeMs, xAxisTickFormat }) { - const wrapper = mount(<div className="content-wrapper" />); - const node = wrapper.getDOMNode(); - - const chartHeight = 170; - const margin = { top: 10, right: 0, bottom: 30, left: 60 }; - const svgWidth = 500; - const svgHeight = chartHeight + margin.top + margin.bottom; - const vizWidth = 500; - - const chartElement = d3.select(node); - - const lineChartXScale = d3.time - .scale() - .range([0, vizWidth]) - .domain([plotEarliest, plotLatest]); - - const xAxis = d3.svg - .axis() - .scale(lineChartXScale) - .orient('bottom') - .innerTickSize(-chartHeight) - .outerTickSize(0) - .tickPadding(10) - .tickFormat(d => moment(d).format(xAxisTickFormat)); - - const tickValues = getTickValues(startTimeMs, interval, plotEarliest, plotLatest); - xAxis.tickValues(tickValues); - - const svg = chartElement - .append('svg') - .attr('width', svgWidth) - .attr('height', svgHeight); - - const axes = svg.append('g'); - - const gAxis = axes - .append('g') - .attr('class', 'x axis') - .attr('transform', 'translate(0,' + chartHeight + ')') - .call(xAxis); - - return { - gAxis, - node, - vizWidth, - }; - } + describe('getXTransform', () => { + const expectedXTransform = 0.007167499999999999; - test('farequote sample data', () => { - const mockedGetBBox = { width: 27.21875 }; - SVGElement.prototype.getBBox = () => mockedGetBBox; + test('Chrome/Safari/Firefox String variant.', () => { + const transformStr = 'translate(0.007167499999999999,0)'; + const xTransform = getXTransform(transformStr); + expect(xTransform).toEqual(expectedXTransform); + }); - const startTimeMs = 1486656000000; - const interval = 14400000; + test('IE11 String variant.', () => { + const transformStr = 'translate(0.007167499999999999)'; + const xTransform = getXTransform(transformStr); + expect(xTransform).toEqual(expectedXTransform); + }); - const { gAxis, node, vizWidth } = axisSetup({ - interval, - plotEarliest: 1486606500000, - plotLatest: 1486719900000, - startTimeMs, - xAxisTickFormat: 'HH:mm', + test('Invalid String.', () => { + const transformStr = 'translate()'; + const xTransform = getXTransform(transformStr); + expect(xTransform).toEqual(NaN); }); + }); - expect(node.getElementsByTagName('text')).toHaveLength(8); + describe('removeLabelOverlap', () => { + const originalGetBBox = SVGElement.prototype.getBBox; + + // This resembles how ExplorerChart renders its x axis. + // We set up this boilerplate so we can then run removeLabelOverlap() + // on some "real" structure. + function axisSetup({ interval, plotEarliest, plotLatest, startTimeMs, xAxisTickFormat }) { + const { container } = render(<div className="content-wrapper" />); + const node = container.querySelector('.content-wrapper'); + + const chartHeight = 170; + const margin = { top: 10, right: 0, bottom: 30, left: 60 }; + const svgWidth = 500; + const svgHeight = chartHeight + margin.top + margin.bottom; + const vizWidth = 500; + + const chartElement = d3.select(node); + + const lineChartXScale = d3.time + .scale() + .range([0, vizWidth]) + .domain([plotEarliest, plotLatest]); + + const xAxis = d3.svg + .axis() + .scale(lineChartXScale) + .orient('bottom') + .innerTickSize(-chartHeight) + .outerTickSize(0) + .tickPadding(10) + .tickFormat(d => moment(d).format(xAxisTickFormat)); + + const tickValues = getTickValues(startTimeMs, interval, plotEarliest, plotLatest); + xAxis.tickValues(tickValues); + + const svg = chartElement + .append('svg') + .attr('width', svgWidth) + .attr('height', svgHeight); + + const axes = svg.append('g'); + + const gAxis = axes + .append('g') + .attr('class', 'x axis') + .attr('transform', 'translate(0,' + chartHeight + ')') + .call(xAxis); - removeLabelOverlap(gAxis, startTimeMs, interval, vizWidth); + return { + gAxis, + node, + vizWidth, + }; + } - // at the vizWidth of 500, the most left and right tick label - // will get removed because it overflows the chart area - expect(node.getElementsByTagName('text')).toHaveLength(6); + test('farequote sample data', () => { + const mockedGetBBox = { width: 27.21875 }; + SVGElement.prototype.getBBox = () => mockedGetBBox; - SVGElement.prototype.getBBox = originalGetBBox; - }); + const startTimeMs = 1486656000000; + const interval = 14400000; + + const { gAxis, node, vizWidth } = axisSetup({ + interval, + plotEarliest: 1486606500000, + plotLatest: 1486719900000, + startTimeMs, + xAxisTickFormat: 'HH:mm', + }); - test('filebeat sample data', () => { - const mockedGetBBox = { width: 85.640625 }; - SVGElement.prototype.getBBox = () => mockedGetBBox; + expect(node.getElementsByTagName('text')).toHaveLength(8); - const startTimeMs = 1486080000000; - const interval = 14400000; + removeLabelOverlap(gAxis, startTimeMs, interval, vizWidth); - const { gAxis, node, vizWidth } = axisSetup({ - interval, - plotEarliest: 1485860400000, - plotLatest: 1486314000000, - startTimeMs, - xAxisTickFormat: 'YYYY-MM-DD HH:mm', + // at the vizWidth of 500, the most left and right tick label + // will get removed because it overflows the chart area + expect(node.getElementsByTagName('text')).toHaveLength(6); + + SVGElement.prototype.getBBox = originalGetBBox; }); - expect(node.getElementsByTagName('text')).toHaveLength(32); + test('filebeat sample data', () => { + const mockedGetBBox = { width: 85.640625 }; + SVGElement.prototype.getBBox = () => mockedGetBBox; + + const startTimeMs = 1486080000000; + const interval = 14400000; - removeLabelOverlap(gAxis, startTimeMs, interval, vizWidth); + const { gAxis, node, vizWidth } = axisSetup({ + interval, + plotEarliest: 1485860400000, + plotLatest: 1486314000000, + startTimeMs, + xAxisTickFormat: 'YYYY-MM-DD HH:mm', + }); - // In this case labels get reduced significantly because of the wider - // labels (full dates + time) and the narrow interval. - expect(node.getElementsByTagName('text')).toHaveLength(3); + expect(node.getElementsByTagName('text')).toHaveLength(32); - SVGElement.prototype.getBBox = originalGetBBox; + removeLabelOverlap(gAxis, startTimeMs, interval, vizWidth); + + // In this case labels get reduced significantly because of the wider + // labels (full dates + time) and the narrow interval. + expect(node.getElementsByTagName('text')).toHaveLength(3); + + SVGElement.prototype.getBBox = originalGetBBox; + }); }); }); diff --git a/x-pack/plugins/ml/public/application/util/string_utils.d.ts b/x-pack/plugins/ml/public/application/util/string_utils.d.ts index b5063907e1fdf..531e44e3e78c1 100644 --- a/x-pack/plugins/ml/public/application/util/string_utils.d.ts +++ b/x-pack/plugins/ml/public/application/util/string_utils.d.ts @@ -14,4 +14,8 @@ export function replaceStringTokens( export function detectorToString(dtr: any): string; +export function sortByKey(list: any, reverse: boolean, comparator?: any): any; + export function toLocaleString(x: number): string; + +export function mlEscape(str: string): string; diff --git a/x-pack/plugins/ml/public/application/util/string_utils.js b/x-pack/plugins/ml/public/application/util/string_utils.js index 172d334099b3d..66835984df5e5 100644 --- a/x-pack/plugins/ml/public/application/util/string_utils.js +++ b/x-pack/plugins/ml/public/application/util/string_utils.js @@ -99,211 +99,6 @@ export function sortByKey(list, reverse, comparator) { ); } -// guess the time format for a given time string -export function guessTimeFormat(time) { - let format = ''; - let matched = false; - if (isNaN(time)) { - let match; - - // match date format - if (!matched) { - let reg = ''; - - reg += '('; // 1 ( date - - reg += '('; // 2 ( yyyy-MM-dd - reg += '(\\d{4})'; // 3 yyyy - reg += '([-/.\\s])'; // 4 - or . or \s - reg += '('; // 5 ( month - reg += '([01]\\d)'; // 6 MM - reg += '|'; // or - reg += '(\\w{3})'; // 7 MMM - reg += ')'; // ) end month - reg += '([-/.\\s])'; // 8 - or . or \s - reg += '([0-3]\\d)'; // 9 dd 0-3 and 0-9 - reg += ')'; // ) end yyyy-MM-dd - - reg += '|'; // or - - reg += '('; // 10 ( d[d]-MM[M]-yyyy or MM[M]-d[d]-yyyy - - reg += '('; // 11 ( day or month - reg += '(\\d{1,2})'; // 12 d or M or dd or MM - reg += '|'; // or - reg += '(\\w{3})'; // 13 MMM - reg += ')'; // ) end day or month - - reg += '([-/.\\s])'; // 14 - or . or \s - - reg += '('; // 15 ( day or month - reg += '(\\d{1,2})'; // 12 d or M or dd or MM - reg += '|'; // or - reg += '(\\w{3})'; // 17 MMM - reg += ')'; // ) end day or month - - reg += '([-/.\\s])'; // 18 - or . or \s - reg += '(\\d{4})'; // 19 yyyy - reg += ')'; // ) end d[d]-MM[M]-yyyy or MM[M]-d[d]-yyyy - - reg += ')'; // ) end date - - reg += '([T\\s])?'; // 20 T or space - - reg += '([0-2]\\d)'; // 21 HH 0-2 and 0-9 - reg += '([:.])'; // 22 :. - reg += '([0-5]\\d)'; // 23 mm 0-5 and 0-9 - reg += '('; // 24 ( optional secs - reg += '([:.])'; // 25 :. - reg += '([0-5]\\d)'; // 26 ss 0-5 and 0-9 - reg += ')?'; // ) end optional secs - reg += '('; // 27 ( optional millisecs - reg += '([:.])'; // 28 :. - reg += '(\\d{3})'; // 29 3 * 0-9 - reg += ')?'; // ) end optional millisecs - reg += '('; // 30 ( optional timezone matches - reg += '([+-]\\d{2}[:.]\\d{2}[:.]\\d{2})'; // 31 +- 0-9 0-9 :. 0-9 0-9 :. 0-9 0-9 e.g. +00:00:00 - reg += '|'; // or - reg += '([+-]\\d{2}[:.]\\d{2})'; // 32 +- 0-9 0-9 :. 0-9 0-9 e.g. +00:00 - reg += '|'; // or - reg += '([+-]\\d{6})'; // 33 +- 6 * 0-9 e.g. +000000 - reg += '|'; // or - reg += '([+-]\\d{4})'; // 34 +- 4 * 0-9 e.g. +0000 - reg += '|'; // or - reg += '(Z)'; // 35 Z - reg += '|'; // or - reg += '([+-]\\d{2})'; // 36 +- 0-9 0-9 e.g. +00 - reg += '|'; // or - reg += '('; // 37 ( string timezone - reg += '(\\s)'; // 38 optional space - reg += '(\\w{1,4})'; // 39 1-4 letters e.g UTC - reg += ')'; // ) end string timezone - reg += ')?'; // ) end optional timezone - - console.log('guessTimeFormat: time format regex: ' + reg); - - match = time.match(new RegExp(reg)); - // console.log(match); - if (match) { - // add the standard data and time - if (match[2] !== undefined) { - // match yyyy-[MM MMM]-dd - format += 'yyyy'; - format += match[4]; - if (match[6] !== undefined) { - format += 'MM'; - } else if (match[7] !== undefined) { - format += 'MMM'; - } - format += match[8]; - format += 'dd'; - } else if (match[10] !== undefined) { - // match dd-MM[M]-yyyy or MM[M]-dd-yyyy - - if (match[13] !== undefined) { - // found a word as the first part - // e.g., Jan 01 2000 - format += 'MMM'; - format += match[14]; - format += 'dd'; - } else if (match[17] !== undefined) { - // found a word as the second part - // e.g., 01 Jan 2000 - format += 'dd'; - format += match[14]; - format += 'MMM'; - } else { - // check to see if the first number is greater than 12 - // e.g., 24/03/1981 - // this is a guess, but is only thing we can do - // with one line from the data set - if (match[12] !== undefined && +match[12] > 12) { - format += 'dd'; - format += match[14]; - format += 'MM'; - } else { - // default to US format. - format += 'MM'; - format += match[14]; - format += 'dd'; - } - } - - format += match[18]; - format += 'yyyy'; - } - - // optional T or space splitter - // wrap T in single quotes - format += match[20] === 'T' ? "'" + match[20] + "'" : match[20]; - format += 'HH'; - format += match[22]; - format += 'mm'; - - // add optional secs - if (match[24] !== undefined) { - format += match[25]; - format += 'ss'; - } - - // add optional millisecs - if (match[27] !== undefined) { - // .000 - format += match[28]; - format += 'SSS'; - } - - // add optional time zone - if (match[31] !== undefined) { - // +00:00:00 - format += 'XXXXX'; - } else if (match[32] !== undefined) { - // +00:00 - format += 'XXX'; - } else if (match[33] !== undefined) { - // +000000 - format += 'XXXX'; - } else if (match[34] !== undefined) { - // +0000 - format += 'Z'; - } else if (match[35] !== undefined || match[36] !== undefined) { - // Z or +00 - format += 'X'; - } else if (match[37] !== undefined) { - // UTC - if (match[38] !== undefined) { - // add optional space char - format += match[38]; - } - // add time zone name, up to 4 chars - for (let i = 0; i < match[39].length; i++) { - format += 'z'; - } - } - matched = true; - } - } - } else { - // time field is a number, so probably epoch or epoch_ms - if (time > 10000000000) { - // probably milliseconds - format = 'epoch_ms'; - } else { - // probably seconds - format = 'epoch'; - } - matched = true; - } - - if (matched) { - console.log('guessTimeFormat: guessed time format: ', format); - } else { - console.log('guessTimeFormat: time format could not be guessed from: ' + time); - } - - return format; -} - // add commas to large numbers // Number.toLocaleString is not supported on safari export function toLocaleString(x) { diff --git a/x-pack/plugins/ml/public/application/util/string_utils.test.ts b/x-pack/plugins/ml/public/application/util/string_utils.test.ts new file mode 100644 index 0000000000000..d940fce2ee1d5 --- /dev/null +++ b/x-pack/plugins/ml/public/application/util/string_utils.test.ts @@ -0,0 +1,193 @@ +/* + * 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 { + replaceStringTokens, + detectorToString, + sortByKey, + toLocaleString, + mlEscape, + escapeForElasticsearchQuery, +} from './string_utils'; + +describe('ML - string utils', () => { + describe('replaceStringTokens', () => { + const testRecord = { + job_id: 'test_job', + result_type: 'record', + probability: 0.0191711, + record_score: 4.3, + bucket_span: 300, + detector_index: 0, + timestamp: 1454890500000, + function: 'mean', + function_description: 'mean', + field_name: 'responsetime', + user: "Des O'Connor", + testfield1: 'test$tring=[+-?]', + testfield2: '{<()>}', + testfield3: 'host=\\\\test@uk.dev', + }; + + test('returns correct values without URI encoding', () => { + const result = replaceStringTokens('user=$user$,time=$timestamp$', testRecord, false); + expect(result).toBe("user=Des O'Connor,time=1454890500000"); + }); + + test('returns correct values for missing token without URI encoding', () => { + const result = replaceStringTokens('user=$username$,time=$timestamp$', testRecord, false); + expect(result).toBe('user=$username$,time=1454890500000'); + }); + + test('returns correct values with URI encoding', () => { + const testString1 = 'https://www.google.co.uk/webhp#q=$testfield1$'; + const testString2 = 'https://www.google.co.uk/webhp#q=$testfield2$'; + const testString3 = 'https://www.google.co.uk/webhp#q=$testfield3$'; + const testString4 = 'https://www.google.co.uk/webhp#q=$user$'; + + const result1 = replaceStringTokens(testString1, testRecord, true); + const result2 = replaceStringTokens(testString2, testRecord, true); + const result3 = replaceStringTokens(testString3, testRecord, true); + const result4 = replaceStringTokens(testString4, testRecord, true); + + expect(result1).toBe('https://www.google.co.uk/webhp#q=test%24tring%3D%5B%2B-%3F%5D'); + expect(result2).toBe('https://www.google.co.uk/webhp#q=%7B%3C()%3E%7D'); + expect(result3).toBe('https://www.google.co.uk/webhp#q=host%3D%5C%5Ctest%40uk.dev'); + expect(result4).toBe("https://www.google.co.uk/webhp#q=Des%20O'Connor"); + }); + + test('returns correct values for missing token with URI encoding', () => { + const testString = 'https://www.google.co.uk/webhp#q=$username$&time=$timestamp$'; + const result = replaceStringTokens(testString, testRecord, true); + expect(result).toBe('https://www.google.co.uk/webhp#q=$username$&time=1454890500000'); + }); + }); + + describe('detectorToString', () => { + test('returns the correct descriptions for detectors', () => { + const detector1 = { + function: 'count', + }; + + const detector2 = { + function: 'count', + by_field_name: 'airline', + use_null: false, + }; + + const detector3 = { + function: 'mean', + field_name: 'CPUUtilization', + partition_field_name: 'region', + by_field_name: 'host', + over_field_name: 'user', + exclude_frequent: 'all', + }; + + expect(detectorToString(detector1)).toBe('count'); + expect(detectorToString(detector2)).toBe('count by airline use_null=false'); + expect(detectorToString(detector3)).toBe( + 'mean(CPUUtilization) by host over user partition_field_name=region exclude_frequent=all' + ); + }); + }); + + describe('sortByKey', () => { + const obj = { + zebra: 'stripes', + giraffe: 'neck', + elephant: 'trunk', + }; + + const valueComparator = function(value: string) { + return value; + }; + + test('returns correct ordering with default comparator', () => { + const result = sortByKey(obj, false); + const keys = Object.keys(result); + expect(keys[0]).toBe('elephant'); + expect(keys[1]).toBe('giraffe'); + expect(keys[2]).toBe('zebra'); + }); + + test('returns correct ordering with default comparator and order reversed', () => { + const result = sortByKey(obj, true); + const keys = Object.keys(result); + expect(keys[0]).toBe('zebra'); + expect(keys[1]).toBe('giraffe'); + expect(keys[2]).toBe('elephant'); + }); + + test('returns correct ordering with comparator', () => { + const result = sortByKey(obj, false, valueComparator); + const keys = Object.keys(result); + expect(keys[0]).toBe('giraffe'); + expect(keys[1]).toBe('zebra'); + expect(keys[2]).toBe('elephant'); + }); + + test('returns correct ordering with comparator and order reversed', () => { + const result = sortByKey(obj, true, valueComparator); + const keys = Object.keys(result); + expect(keys[0]).toBe('elephant'); + expect(keys[1]).toBe('zebra'); + expect(keys[2]).toBe('giraffe'); + }); + }); + + describe('toLocaleString', () => { + test('returns correct comma placement for large numbers', () => { + expect(toLocaleString(1)).toBe('1'); + expect(toLocaleString(10)).toBe('10'); + expect(toLocaleString(100)).toBe('100'); + expect(toLocaleString(1000)).toBe('1,000'); + expect(toLocaleString(10000)).toBe('10,000'); + expect(toLocaleString(100000)).toBe('100,000'); + expect(toLocaleString(1000000)).toBe('1,000,000'); + expect(toLocaleString(10000000)).toBe('10,000,000'); + expect(toLocaleString(100000000)).toBe('100,000,000'); + expect(toLocaleString(1000000000)).toBe('1,000,000,000'); + }); + }); + + describe('mlEscape', () => { + test('returns correct escaping of characters', () => { + expect(mlEscape('foo&bar')).toBe('foo&bar'); + expect(mlEscape('foo<bar')).toBe('foo<bar'); + expect(mlEscape('foo>bar')).toBe('foo>bar'); + expect(mlEscape('foo"bar')).toBe('foo"bar'); + expect(mlEscape("foo'bar")).toBe('foo'bar'); + expect(mlEscape('foo/bar')).toBe('foo/bar'); + }); + }); + + describe('escapeForElasticsearchQuery', () => { + test('returns correct escaping of reserved elasticsearch characters', () => { + expect(escapeForElasticsearchQuery('foo+bar')).toBe('foo\\+bar'); + expect(escapeForElasticsearchQuery('foo-bar')).toBe('foo\\-bar'); + expect(escapeForElasticsearchQuery('foo=bar')).toBe('foo\\=bar'); + expect(escapeForElasticsearchQuery('foo&&bar')).toBe('foo\\&\\&bar'); + expect(escapeForElasticsearchQuery('foo||bar')).toBe('foo\\|\\|bar'); + expect(escapeForElasticsearchQuery('foo>bar')).toBe('foo\\>bar'); + expect(escapeForElasticsearchQuery('foo<bar')).toBe('foo\\<bar'); + expect(escapeForElasticsearchQuery('foo!bar')).toBe('foo\\!bar'); + expect(escapeForElasticsearchQuery('foo(bar')).toBe('foo\\(bar'); + expect(escapeForElasticsearchQuery('foo)bar')).toBe('foo\\)bar'); + expect(escapeForElasticsearchQuery('foo{bar')).toBe('foo\\{bar'); + expect(escapeForElasticsearchQuery('foo[bar')).toBe('foo\\[bar'); + expect(escapeForElasticsearchQuery('foo]bar')).toBe('foo\\]bar'); + expect(escapeForElasticsearchQuery('foo^bar')).toBe('foo\\^bar'); + expect(escapeForElasticsearchQuery('foo"bar')).toBe('foo\\"bar'); + expect(escapeForElasticsearchQuery('foo~bar')).toBe('foo\\~bar'); + expect(escapeForElasticsearchQuery('foo*bar')).toBe('foo\\*bar'); + expect(escapeForElasticsearchQuery('foo?bar')).toBe('foo\\?bar'); + expect(escapeForElasticsearchQuery('foo:bar')).toBe('foo\\:bar'); + expect(escapeForElasticsearchQuery('foo\\bar')).toBe('foo\\\\bar'); + expect(escapeForElasticsearchQuery('foo/bar')).toBe('foo\\/bar'); + }); + }); +}); diff --git a/x-pack/plugins/ml/server/lib/__tests__/query_utils.js b/x-pack/plugins/ml/server/lib/__tests__/query_utils.js deleted file mode 100644 index 05292abb36b25..0000000000000 --- a/x-pack/plugins/ml/server/lib/__tests__/query_utils.js +++ /dev/null @@ -1,88 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import expect from '@kbn/expect'; -import { - buildBaseFilterCriteria, - buildSamplerAggregation, - getSamplerAggregationsResponsePath, -} from '../query_utils'; - -describe('ML - query utils', () => { - describe('buildBaseFilterCriteria', () => { - const earliestMs = 1483228800000; // 1 Jan 2017 00:00:00 - const latestMs = 1485907199000; // 31 Jan 2017 23:59:59 - const query = { - query_string: { - query: 'region:sa-east-1', - analyze_wildcard: true, - default_field: '*', - }, - }; - - it('returns correct criteria for time range', () => { - expect(buildBaseFilterCriteria('timestamp', earliestMs, latestMs)).to.eql([ - { - range: { - timestamp: { - gte: earliestMs, - lte: latestMs, - format: 'epoch_millis', - }, - }, - }, - ]); - }); - - it('returns correct criteria for time range and query', () => { - expect(buildBaseFilterCriteria('timestamp', earliestMs, latestMs, query)).to.eql([ - { - range: { - timestamp: { - gte: earliestMs, - lte: latestMs, - format: 'epoch_millis', - }, - }, - }, - query, - ]); - }); - }); - - describe('buildSamplerAggregation', () => { - const testAggs = { - bytes_stats: { - stats: { field: 'bytes' }, - }, - }; - - it('returns wrapped sampler aggregation for sampler shard size of 1000', () => { - expect(buildSamplerAggregation(testAggs, 1000)).to.eql({ - sample: { - sampler: { - shard_size: 1000, - }, - aggs: testAggs, - }, - }); - }); - - it('returns un-sampled aggregation as-is for sampler shard size of 0', () => { - expect(buildSamplerAggregation(testAggs, 0)).to.eql(testAggs); - }); - }); - - describe('getSamplerAggregationsResponsePath', () => { - it('returns correct path for sampler shard size of 1000', () => { - expect(getSamplerAggregationsResponsePath(1000)).to.eql(['sample']); - }); - - it('returns correct path for sampler shard size of 0', () => { - expect(getSamplerAggregationsResponsePath(0)).to.eql([]); - }); - }); -}); diff --git a/x-pack/plugins/ml/server/lib/query_utils.test.ts b/x-pack/plugins/ml/server/lib/query_utils.test.ts new file mode 100644 index 0000000000000..c2f5e814da332 --- /dev/null +++ b/x-pack/plugins/ml/server/lib/query_utils.test.ts @@ -0,0 +1,87 @@ +/* + * 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 { + buildBaseFilterCriteria, + buildSamplerAggregation, + getSamplerAggregationsResponsePath, +} from './query_utils'; + +describe('ML - query utils', () => { + describe('buildBaseFilterCriteria', () => { + const earliestMs = 1483228800000; // 1 Jan 2017 00:00:00 + const latestMs = 1485907199000; // 31 Jan 2017 23:59:59 + const query = { + query_string: { + query: 'region:sa-east-1', + analyze_wildcard: true, + default_field: '*', + }, + }; + + test('returns correct criteria for time range', () => { + expect(buildBaseFilterCriteria('timestamp', earliestMs, latestMs)).toEqual([ + { + range: { + timestamp: { + gte: earliestMs, + lte: latestMs, + format: 'epoch_millis', + }, + }, + }, + ]); + }); + + test('returns correct criteria for time range and query', () => { + expect(buildBaseFilterCriteria('timestamp', earliestMs, latestMs, query)).toEqual([ + { + range: { + timestamp: { + gte: earliestMs, + lte: latestMs, + format: 'epoch_millis', + }, + }, + }, + query, + ]); + }); + }); + + describe('buildSamplerAggregation', () => { + const testAggs = { + bytes_stats: { + stats: { field: 'bytes' }, + }, + }; + + test('returns wrapped sampler aggregation for sampler shard size of 1000', () => { + expect(buildSamplerAggregation(testAggs, 1000)).toEqual({ + sample: { + sampler: { + shard_size: 1000, + }, + aggs: testAggs, + }, + }); + }); + + test('returns un-sampled aggregation as-is for sampler shard size of 0', () => { + expect(buildSamplerAggregation(testAggs, 0)).toEqual(testAggs); + }); + }); + + describe('getSamplerAggregationsResponsePath', () => { + test('returns correct path for sampler shard size of 1000', () => { + expect(getSamplerAggregationsResponsePath(1000)).toEqual(['sample']); + }); + + test('returns correct path for sampler shard size of 0', () => { + expect(getSamplerAggregationsResponsePath(0)).toEqual([]); + }); + }); +}); diff --git a/x-pack/plugins/ml/server/lib/telemetry/mappings.ts b/x-pack/plugins/ml/server/lib/telemetry/mappings.ts index 87e2243328422..5aaf9f8c79dc0 100644 --- a/x-pack/plugins/ml/server/lib/telemetry/mappings.ts +++ b/x-pack/plugins/ml/server/lib/telemetry/mappings.ts @@ -10,7 +10,7 @@ import { TELEMETRY_DOC_ID } from './telemetry'; export const mlTelemetryMappingsType: SavedObjectsType = { name: TELEMETRY_DOC_ID, hidden: false, - namespaceAgnostic: true, + namespaceType: 'agnostic', mappings: { properties: { file_data_visualizer: { diff --git a/x-pack/plugins/ml/server/models/job_service/jobs.ts b/x-pack/plugins/ml/server/models/job_service/jobs.ts index edcabcac93c2a..6024ecf4925e6 100644 --- a/x-pack/plugins/ml/server/models/job_service/jobs.ts +++ b/x-pack/plugins/ml/server/models/job_service/jobs.ts @@ -16,6 +16,7 @@ import { DatafeedWithStats, CombinedJobWithStats, } from '../../../common/types/anomaly_detection_jobs'; +import { GLOBAL_CALENDAR } from '../../../common/constants/calendars'; import { datafeedsProvider, MlDatafeedsResponse, MlDatafeedsStatsResponse } from './datafeeds'; import { jobAuditMessagesProvider } from '../job_audit_messages'; import { resultsServiceProvider } from '../results_service'; @@ -227,6 +228,8 @@ export function jobsProvider(callAsCurrentUser: APICaller) { const groups: { [jobId: string]: string[] } = {}; const datafeeds: { [id: string]: DatafeedWithStats } = {}; const calendarsByJobId: { [jobId: string]: string[] } = {}; + const globalCalendars: string[] = []; + const requests: [ Promise<MlJobsResponse>, Promise<MlJobsStatsResponse>, @@ -298,7 +301,9 @@ export function jobsProvider(callAsCurrentUser: APICaller) { if (calendarResults) { calendarResults.forEach(cal => { cal.job_ids.forEach(id => { - if (groups[id]) { + if (id === GLOBAL_CALENDAR) { + globalCalendars.push(cal.calendar_id); + } else if (groups[id]) { groups[id].forEach(jId => { if (calendarsByJobId[jId] !== undefined) { calendarsByJobId[jId].push(cal.calendar_id); @@ -325,8 +330,12 @@ export function jobsProvider(callAsCurrentUser: APICaller) { jobResults.jobs.forEach(job => { const tempJob = job as CombinedJobWithStats; - if (calendarsByJobId[tempJob.job_id].length) { - tempJob.calendars = calendarsByJobId[tempJob.job_id]; + const calendars: string[] = [ + ...(calendarsByJobId[tempJob.job_id] || []), + ...(globalCalendars || []), + ]; + if (calendars.length) { + tempJob.calendars = calendars; } if (jobStatsResults && jobStatsResults.jobs) { diff --git a/x-pack/plugins/ml/server/routes/README.md b/x-pack/plugins/ml/server/routes/README.md index 1d08335af3d2e..70af73c37dadd 100644 --- a/x-pack/plugins/ml/server/routes/README.md +++ b/x-pack/plugins/ml/server/routes/README.md @@ -6,11 +6,14 @@ Each route handler requires [apiDoc](https://github.com/apidoc/apidoc) annotatio to generate documentation. The [apidoc-markdown](https://github.com/rigwild/apidoc-markdown) package is also required in order to generate the markdown. -For now the process is pretty manual. You need to make sure the packages mentioned above are installed globally -to execute the following command from the directory in which this README file is located. +There are custom parser and worker (`x-pack/plugins/ml/server/routes/apidoc_scripts`) to process api schemas for each documentation entry. It's written with typescript so make sure all the scripts in the folder are compiled before executing `apidoc` command. + +Make sure you have run `yarn kbn bootstrap` to get all requires dev dependencies. Then execute the following command from the ml plugin folder: ``` -apidoc -i . -o ../routes_doc && apidoc-markdown -p ../routes_doc -o ../routes_doc/ML_API.md +yarn run apiDocs ``` +It compiles all the required scripts and generates the documentation both in HTML and Markdown formats. + It will create a new directory `routes_doc` (next to the `routes` folder) which contains the documentation in HTML format -as well as `ML_API.md` file. \ No newline at end of file +as well as `ML_API.md` file. diff --git a/x-pack/plugins/ml/server/routes/annotations.ts b/x-pack/plugins/ml/server/routes/annotations.ts index 56c0b639e2c85..d5abebda00caa 100644 --- a/x-pack/plugins/ml/server/routes/annotations.ts +++ b/x-pack/plugins/ml/server/routes/annotations.ts @@ -5,10 +5,8 @@ */ import Boom from 'boom'; -import _ from 'lodash'; import { i18n } from '@kbn/i18n'; -import { schema } from '@kbn/config-schema'; import { SecurityPluginSetup } from '../../../security/server'; import { isAnnotationsFeatureAvailable } from '../lib/check_annotations'; import { annotationServiceProvider } from '../models/annotation_service'; @@ -45,10 +43,7 @@ export function annotationRoutes( * @apiName GetAnnotations * @apiDescription Gets annotations. * - * @apiParam {String[]} jobIds List of job IDs - * @apiParam {String} earliestMs - * @apiParam {Number} latestMs - * @apiParam {Number} maxAnnotations Max limit of annotations returned + * @apiSchema (body) getAnnotationsSchema * * @apiSuccess {Boolean} success * @apiSuccess {Object} annotations @@ -57,7 +52,7 @@ export function annotationRoutes( { path: '/api/ml/annotations', validate: { - body: schema.object(getAnnotationsSchema), + body: getAnnotationsSchema, }, }, mlLicense.fullLicenseAPIGuard(async (context, request, response) => { @@ -83,14 +78,13 @@ export function annotationRoutes( * @apiName IndexAnnotations * @apiDescription Index the annotation. * - * @apiParam {Object} annotation - * @apiParam {String} username + * @apiSchema (body) indexAnnotationSchema */ router.put( { path: '/api/ml/annotations/index', validate: { - body: schema.object(indexAnnotationSchema), + body: indexAnnotationSchema, }, }, mlLicense.fullLicenseAPIGuard(async (context, request, response) => { @@ -124,17 +118,17 @@ export function annotationRoutes( /** * @apiGroup Annotations * - * @api {delete} /api/ml/annotations/index Deletes annotation + * @api {delete} /api/ml/annotations/delete/:annotationId Deletes annotation * @apiName DeleteAnnotation * @apiDescription Deletes specified annotation * - * @apiParam {String} annotationId + * @apiSchema (params) deleteAnnotationSchema */ router.delete( { path: '/api/ml/annotations/delete/{annotationId}', validate: { - params: schema.object(deleteAnnotationSchema), + params: deleteAnnotationSchema, }, }, mlLicense.fullLicenseAPIGuard(async (context, request, response) => { diff --git a/x-pack/plugins/ml/server/routes/anomaly_detectors.ts b/x-pack/plugins/ml/server/routes/anomaly_detectors.ts index d03e76072c315..a675eb58dc792 100644 --- a/x-pack/plugins/ml/server/routes/anomaly_detectors.ts +++ b/x-pack/plugins/ml/server/routes/anomaly_detectors.ts @@ -10,6 +10,13 @@ import { RouteInitialization } from '../types'; import { anomalyDetectionJobSchema, anomalyDetectionUpdateJobSchema, + jobIdSchema, + getRecordsSchema, + getBucketsSchema, + getOverallBucketsSchema, + getCategoriesSchema, + forecastAnomalyDetector, + getBucketParamsSchema, } from './schemas/anomaly_detectors_schema'; /** @@ -50,15 +57,13 @@ export function jobRoutes({ router, mlLicense }: RouteInitialization) { * @apiName GetAnomalyDetectorsById * @apiDescription Returns the anomaly detection job. * - * @apiParam {String} jobId Job ID. + * @apiSchema (params) jobIdSchema */ router.get( { path: '/api/ml/anomaly_detectors/{jobId}', validate: { - params: schema.object({ - jobId: schema.string(), - }), + params: jobIdSchema, }, }, mlLicense.fullLicenseAPIGuard(async (context, request, response) => { @@ -108,15 +113,13 @@ export function jobRoutes({ router, mlLicense }: RouteInitialization) { * @apiName GetAnomalyDetectorsStatsById * @apiDescription Returns anomaly detection job statistics. * - * @apiParam {String} jobId Job ID. + * @apiSchema (params) jobIdSchema */ router.get( { path: '/api/ml/anomaly_detectors/{jobId}/_stats', validate: { - params: schema.object({ - jobId: schema.string(), - }), + params: jobIdSchema, }, }, mlLicense.fullLicenseAPIGuard(async (context, request, response) => { @@ -139,15 +142,14 @@ export function jobRoutes({ router, mlLicense }: RouteInitialization) { * @apiName CreateAnomalyDetectors * @apiDescription Creates an anomaly detection job. * - * @apiParam {String} jobId Job ID. + * @apiSchema (params) jobIdSchema + * @apiSchema (body) anomalyDetectionJobSchema */ router.put( { path: '/api/ml/anomaly_detectors/{jobId}', validate: { - params: schema.object({ - jobId: schema.string(), - }), + params: jobIdSchema, body: schema.object(anomalyDetectionJobSchema), }, }, @@ -174,16 +176,15 @@ export function jobRoutes({ router, mlLicense }: RouteInitialization) { * @apiName UpdateAnomalyDetectors * @apiDescription Updates certain properties of an anomaly detection job. * - * @apiParam {String} jobId Job ID. + * @apiSchema (params) jobIdSchema + * @apiSchema (body) anomalyDetectionUpdateJobSchema */ router.post( { path: '/api/ml/anomaly_detectors/{jobId}/_update', validate: { - params: schema.object({ - jobId: schema.string(), - }), - body: schema.object({ ...anomalyDetectionUpdateJobSchema }), + params: jobIdSchema, + body: anomalyDetectionUpdateJobSchema, }, }, mlLicense.fullLicenseAPIGuard(async (context, request, response) => { @@ -209,15 +210,13 @@ export function jobRoutes({ router, mlLicense }: RouteInitialization) { * @apiName OpenAnomalyDetectorsJob * @apiDescription Opens an anomaly detection job. * - * @apiParam {String} jobId Job ID. + * @apiSchema (params) jobIdSchema */ router.post( { path: '/api/ml/anomaly_detectors/{jobId}/_open', validate: { - params: schema.object({ - jobId: schema.string(), - }), + params: jobIdSchema, }, }, mlLicense.fullLicenseAPIGuard(async (context, request, response) => { @@ -242,15 +241,13 @@ export function jobRoutes({ router, mlLicense }: RouteInitialization) { * @apiName CloseAnomalyDetectorsJob * @apiDescription Closes an anomaly detection job. * - * @apiParam {String} jobId Job ID. + * @apiSchema (params) jobIdSchema */ router.post( { path: '/api/ml/anomaly_detectors/{jobId}/_close', validate: { - params: schema.object({ - jobId: schema.string(), - }), + params: jobIdSchema, }, }, mlLicense.fullLicenseAPIGuard(async (context, request, response) => { @@ -279,15 +276,13 @@ export function jobRoutes({ router, mlLicense }: RouteInitialization) { * @apiName DeleteAnomalyDetectorsJob * @apiDescription Deletes specified anomaly detection job. * - * @apiParam {String} jobId Job ID. + * @apiSchema (params) jobIdSchema */ router.delete( { path: '/api/ml/anomaly_detectors/{jobId}', validate: { - params: schema.object({ - jobId: schema.string(), - }), + params: jobIdSchema, }, }, mlLicense.fullLicenseAPIGuard(async (context, request, response) => { @@ -315,8 +310,6 @@ export function jobRoutes({ router, mlLicense }: RouteInitialization) { * @api {post} /api/ml/anomaly_detectors/_validate/detector Validate detector * @apiName ValidateAnomalyDetector * @apiDescription Validates specified detector. - * - * @apiParam {String} jobId Job ID. */ router.post( { @@ -346,16 +339,15 @@ export function jobRoutes({ router, mlLicense }: RouteInitialization) { * @apiName ForecastAnomalyDetector * @apiDescription Creates a forecast for the specified anomaly detection job, predicting the future behavior of a time series by using its historical behavior. * - * @apiParam {String} jobId Job ID. + * @apiSchema (params) jobIdSchema + * @apiSchema (body) forecastAnomalyDetector */ router.post( { path: '/api/ml/anomaly_detectors/{jobId}/_forecast', validate: { - params: schema.object({ - jobId: schema.string(), - }), - body: schema.object({ duration: schema.any() }), + params: jobIdSchema, + body: forecastAnomalyDetector, }, }, mlLicense.fullLicenseAPIGuard(async (context, request, response) => { @@ -382,7 +374,8 @@ export function jobRoutes({ router, mlLicense }: RouteInitialization) { * @apiName GetRecords * @apiDescription Retrieves anomaly records for a job. * - * @apiParam {String} jobId Job ID. + * @apiSchema (params) jobIdSchema + * @apiSchema (body) getRecordsSchema * * @apiSuccess {Number} count * @apiSuccess {Object[]} records @@ -391,23 +384,8 @@ export function jobRoutes({ router, mlLicense }: RouteInitialization) { { path: '/api/ml/anomaly_detectors/{jobId}/results/records', validate: { - params: schema.object({ - jobId: schema.string(), - }), - body: schema.object({ - desc: schema.maybe(schema.boolean()), - end: schema.maybe(schema.string()), - exclude_interim: schema.maybe(schema.boolean()), - page: schema.maybe( - schema.object({ - from: schema.maybe(schema.number()), - size: schema.maybe(schema.number()), - }) - ), - record_score: schema.maybe(schema.number()), - sort: schema.maybe(schema.string()), - start: schema.maybe(schema.string()), - }), + params: jobIdSchema, + body: getRecordsSchema, }, }, mlLicense.fullLicenseAPIGuard(async (context, request, response) => { @@ -432,8 +410,8 @@ export function jobRoutes({ router, mlLicense }: RouteInitialization) { * @apiName GetBuckets * @apiDescription The get buckets API presents a chronological view of the records, grouped by bucket. * - * @apiParam {String} jobId Job ID. - * @apiParam {String} timestamp. + * @apiSchema (params) getBucketParamsSchema + * @apiSchema (body) getBucketsSchema * * @apiSuccess {Number} count * @apiSuccess {Object[]} buckets @@ -442,25 +420,8 @@ export function jobRoutes({ router, mlLicense }: RouteInitialization) { { path: '/api/ml/anomaly_detectors/{jobId}/results/buckets/{timestamp?}', validate: { - params: schema.object({ - jobId: schema.string(), - timestamp: schema.maybe(schema.string()), - }), - body: schema.object({ - anomaly_score: schema.maybe(schema.number()), - desc: schema.maybe(schema.boolean()), - end: schema.maybe(schema.string()), - exclude_interim: schema.maybe(schema.boolean()), - expand: schema.maybe(schema.boolean()), - page: schema.maybe( - schema.object({ - from: schema.maybe(schema.number()), - size: schema.maybe(schema.number()), - }) - ), - sort: schema.maybe(schema.string()), - start: schema.maybe(schema.string()), - }), + params: getBucketParamsSchema, + body: getBucketsSchema, }, }, mlLicense.fullLicenseAPIGuard(async (context, request, response) => { @@ -486,7 +447,8 @@ export function jobRoutes({ router, mlLicense }: RouteInitialization) { * @apiName GetOverallBuckets * @apiDescription Retrieves overall bucket results that summarize the bucket results of multiple anomaly detection jobs. * - * @apiParam {String} jobId Job ID. + * @apiSchema (params) jobIdSchema + * @apiSchema (body) getOverallBucketsSchema * * @apiSuccess {Number} count * @apiSuccess {Object[]} overall_buckets @@ -495,15 +457,8 @@ export function jobRoutes({ router, mlLicense }: RouteInitialization) { { path: '/api/ml/anomaly_detectors/{jobId}/results/overall_buckets', validate: { - params: schema.object({ - jobId: schema.string(), - }), - body: schema.object({ - topN: schema.number(), - bucketSpan: schema.string(), - start: schema.number(), - end: schema.number(), - }), + params: jobIdSchema, + body: getOverallBucketsSchema, }, }, mlLicense.fullLicenseAPIGuard(async (context, request, response) => { @@ -531,17 +486,13 @@ export function jobRoutes({ router, mlLicense }: RouteInitialization) { * @apiName GetCategories * @apiDescription Returns the categories results for the specified job ID and category ID. * - * @apiParam {String} jobId Job ID. - * @apiParam {String} categoryId Category ID. + * @apiSchema (params) getCategoriesSchema */ router.get( { path: '/api/ml/anomaly_detectors/{jobId}/results/categories/{categoryId}', validate: { - params: schema.object({ - categoryId: schema.string(), - jobId: schema.string(), - }), + params: getCategoriesSchema, }, }, mlLicense.fullLicenseAPIGuard(async (context, request, response) => { diff --git a/x-pack/plugins/ml/server/routes/apidoc.json b/x-pack/plugins/ml/server/routes/apidoc.json index c5aa3e4d792fd..4848de6db7049 100644 --- a/x-pack/plugins/ml/server/routes/apidoc.json +++ b/x-pack/plugins/ml/server/routes/apidoc.json @@ -1,6 +1,6 @@ { "name": "ml_kibana_api", - "version": "0.1.0", + "version": "7.8.0", "description": "ML Kibana API", "title": "ML Kibana API", "order": [ @@ -9,61 +9,65 @@ "GetDataFrameAnalyticsById", "GetDataFrameAnalyticsStats", "GetDataFrameAnalyticsStatsById", - "UpdateDataFrameAnalytics", "EvaluateDataFrameAnalytics", "ExplainDataFrameAnalytics", - "DeleteDataFrameAnalytics", "StartDataFrameAnalyticsJob", "StopsDataFrameAnalyticsJob", "GetDataFrameAnalyticsMessages", + "UpdateDataFrameAnalytics", + "DeleteDataFrameAnalytics", + "DataVisualizer", "GetOverallStats", "GetStatsForFields", + "AnomalyDetectors", + "CreateAnomalyDetectors", + "OpenAnomalyDetectorsJob", "GetAnomalyDetectors", "GetAnomalyDetectorsById", "GetAnomalyDetectorsStats", "GetAnomalyDetectorsStatsById", - "CreateAnomalyDetectors", - "UpdateAnomalyDetectors", - "OpenAnomalyDetectorsJob", "CloseAnomalyDetectorsJob", - "DeleteAnomalyDetectorsJob", "ValidateAnomalyDetector", "ForecastAnomalyDetector", "GetRecords", "GetBuckets", "GetOverallBuckets", "GetCategories", + "UpdateAnomalyDetectors", + "DeleteAnomalyDetectorsJob", + "FileDataVisualizer", "AnalyzeFile", "ImportFile", + "ResultsService", "GetAnomaliesTableData", "GetCategoryDefinition", "GetMaxAnomalyScore", "GetCategoryExamples", "GetPartitionFieldsValues", + "DataRecognizer", "RecognizeIndex", "GetModule", "SetupModule", "CheckExistingModuleJobs", + "Annotations", "GetAnnotations", "IndexAnnotations", "DeleteAnnotation", + "JobService", "ForceStartDatafeeds", "StopDatafeeds", - "DeleteJobs", "CloseJobs", "JobsSummary", "JobsWithTimeRange", "CreateFullJobsList", "GetAllGroups", - "UpdateGroups", - "DeletingJobTasks", "JobsExist", "NewJobCaps", "NewJobLineChart", @@ -72,42 +76,60 @@ "GetLookBackProgress", "ValidateCategoryExamples", "TopCategories", + "UpdateGroups", + "DeletingJobTasks", + "DeleteJobs", + + "Calendars", + "PutCalendars", + "GetCalendars", + "GetCalendarById", + "UpdateCalendarById", + "DeleteCalendarById", + "Filters", + "CreateFilter", "GetFilters", "GetFilterById", - "CreateFilter", + "GetFiltersStats", "UpdateFilter", "DeleteFilter", - "GetFiltersStats", + "Indices", "FieldCaps", + "SystemRoutes", "HasPrivileges", "MlCapabilities", "MlNodeCount", "MlInfo", "MlEsSearch", + "JobAuditMessages", "GetJobAuditMessages", "GetAllJobAuditMessages", + "JobValidation", "EstimateBucketSpan", "CalculateModelMemoryLimit", "ValidateCardinality", "ValidateJob", + "NotificationSettings", "GetNotificationSettings", + "DatafeedService", + "CreateDatafeed", + "PreviewDatafeed", "GetDatafeeds", "GetDatafeed", "GetDatafeedsStats", "GetDatafeedStats", - "CreateDatafeed", "UpdateDatafeed", - "DeleteDatafeed", "StartDatafeed", "StopDatafeed", - "PreviewDatafeed", + "DeleteDatafeed", + "FieldsService", "GetCardinalityOfFields", "GetTimeFieldRange" diff --git a/x-pack/plugins/ml/server/routes/apidoc_scripts/schema_extractor.ts b/x-pack/plugins/ml/server/routes/apidoc_scripts/schema_extractor.ts new file mode 100644 index 0000000000000..01adcb462689e --- /dev/null +++ b/x-pack/plugins/ml/server/routes/apidoc_scripts/schema_extractor.ts @@ -0,0 +1,168 @@ +/* + * 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 ts from 'typescript'; + +export interface DocEntry { + name: string; + documentation?: string; + type: string; + optional?: boolean; + nested?: DocEntry[]; +} + +/** Generate documentation for all schema definitions in a set of .ts files */ +export function extractDocumentation( + fileNames: string[], + options: ts.CompilerOptions = { + target: ts.ScriptTarget.ES2015, + module: ts.ModuleKind.CommonJS, + } +): Map<string, DocEntry[]> { + // Build a program using the set of root file names in fileNames + const program = ts.createProgram(fileNames, options); + + // Get the checker, we will use it to find more about properties + const checker: ts.TypeChecker = program.getTypeChecker(); + + // Result map + const result = new Map<string, DocEntry[]>(); + + // Visit every sourceFile in the program + for (const sourceFile of program.getSourceFiles()) { + if (!sourceFile.isDeclarationFile) { + // Walk the tree to search for schemas + ts.forEachChild(sourceFile, visit); + } + } + + return result; + + /** visit nodes finding exported schemas */ + function visit(node: ts.Node) { + if (isNodeExported(node) && ts.isVariableDeclaration(node)) { + const schemaName = node.name.getText(); + const schemaType = checker.getTypeAtLocation(node); + result.set(schemaName, extractDocEntries(schemaType!)); + } + + if (node.getChildCount() > 0) { + ts.forEachChild(node, visit); + } + } + + /** + * Extracts doc entries for the schema definition + * @param schemaType + */ + function extractDocEntries(schemaType: ts.Type): DocEntry[] { + const collection: DocEntry[] = []; + + const members = getTypeMembers(schemaType); + + if (!members) { + return collection; + } + + members.forEach(member => { + collection.push(serializeProperty(member)); + }); + + return collection; + } + + /** + * Resolves members of the type + * @param type + */ + function getTypeMembers(type: ts.Type): ts.Symbol[] | undefined { + const argsOfType = checker.getTypeArguments((type as unknown) as ts.TypeReference); + + let members = type.getProperties(); + + if (argsOfType && argsOfType.length > 0) { + members = argsOfType[0].getProperties(); + } + + return members; + } + + function resolveTypeArgument(type: ts.Type): ts.SymbolTable | string { + // required to extract members + type.getProperty('type'); + + // @ts-ignores + let members = type.members; + + const typeArguments = checker.getTypeArguments((type as unknown) as ts.TypeReference); + + if (type.aliasTypeArguments) { + // @ts-ignores + members = type.aliasTypeArguments[0].members; + } + + if (typeArguments.length > 0) { + members = resolveTypeArgument(typeArguments[0]); + } + + if (members === undefined) { + members = checker.typeToString(type); + } + + return members; + } + + function serializeProperty(symbol: ts.Symbol): DocEntry { + // @ts-ignore + const typeOfSymbol = symbol.type; + const typeArguments = checker.getTypeArguments((typeOfSymbol as unknown) as ts.TypeReference); + + let resultType: ts.Type = typeOfSymbol; + + let members; + if (typeArguments.length > 0) { + members = resolveTypeArgument(typeArguments[0]); + resultType = typeArguments[0]; + } + + let typeAsString = checker.typeToString(resultType); + + const nestedEntries: DocEntry[] = []; + if (members && typeof members !== 'string' && members.size > 0) { + // we hit an object or collection + typeAsString = + resultType.symbol.name === 'Array' || typeOfSymbol.symbol.name === 'Array' + ? `${symbol.getName()}[]` + : symbol.getName(); + + members.forEach(member => { + nestedEntries.push(serializeProperty(member)); + }); + } + + return { + name: symbol.getName(), + documentation: getCommentString(symbol), + type: typeAsString, + ...(nestedEntries.length > 0 ? { nested: nestedEntries } : {}), + }; + } + + function getCommentString(symbol: ts.Symbol): string { + return ts.displayPartsToString(symbol.getDocumentationComment(checker)).replace(/\n/g, ' '); + } + + /** + * True if this is visible outside this file, false otherwise + */ + function isNodeExported(node: ts.Node): boolean { + return ( + // eslint-disable-next-line no-bitwise + (ts.getCombinedModifierFlags(node as ts.Declaration) & ts.ModifierFlags.Export) !== 0 || + (!!node.parent && node.parent.kind === ts.SyntaxKind.SourceFile) + ); + } +} diff --git a/x-pack/plugins/ml/server/routes/apidoc_scripts/schema_parser.ts b/x-pack/plugins/ml/server/routes/apidoc_scripts/schema_parser.ts new file mode 100644 index 0000000000000..eabe7dcd7bd8f --- /dev/null +++ b/x-pack/plugins/ml/server/routes/apidoc_scripts/schema_parser.ts @@ -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. + */ + +function parse(content?: string) { + const schema = typeof content === 'string' && content.trim(); + + if (!schema) { + return null; + } + + const result = schema.match(/\((\w+)\)\s+(\w+)/); + + if (result === null || result.length < 3) { + throw new Error( + 'Invalid schema definition. Required format is `@apiSchema (<GROUP_NAME>) <SCHEMA_NAME>`' + ); + } + + const group = result[1]; + + return { + group, + name: result[2], + }; +} + +/** + * Exports + */ +module.exports = { + parse, + path: 'local.schemas', + method: 'push', +}; diff --git a/x-pack/plugins/ml/server/routes/apidoc_scripts/schema_worker.ts b/x-pack/plugins/ml/server/routes/apidoc_scripts/schema_worker.ts new file mode 100644 index 0000000000000..7514e482783b3 --- /dev/null +++ b/x-pack/plugins/ml/server/routes/apidoc_scripts/schema_worker.ts @@ -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 * as fs from 'fs'; +import * as path from 'path'; +import { DocEntry, extractDocumentation } from './schema_extractor'; +import { ApiParameter, Block } from './types'; + +export function postProcess(parsedFiles: any[]): void { + const schemasDirPath = `${__dirname}${path.sep}..${path.sep}..${path.sep}schemas${path.sep}`; + const schemaFiles = fs + .readdirSync(schemasDirPath) + .map(filename => path.resolve(schemasDirPath + filename)); + + const schemaDocs = extractDocumentation(schemaFiles); + + parsedFiles.forEach(parsedFile => { + parsedFile.forEach((block: Block) => { + const { + local: { schemas }, + } = block; + if (!schemas || schemas.length === 0) return; + + for (const schema of schemas) { + const { name: schemaName, group: paramsGroup } = schema; + const schemaFields = schemaDocs.get(schemaName); + + if (!schemaFields) return; + + updateBlockParameters(schemaFields, block, paramsGroup); + } + }); + }); +} + +/** + * Extracts schema's doc entries to apidoc parameters + * @param docEntries + * @param block + * @param paramsGroup + */ +function updateBlockParameters(docEntries: DocEntry[], block: Block, paramsGroup: string): void { + if (!block.local.parameter) { + block.local.parameter = { + fields: {}, + }; + } + + if (!block.local.parameter.fields![paramsGroup]) { + block.local.parameter.fields![paramsGroup] = []; + } + const collection = block.local.parameter.fields![paramsGroup] as ApiParameter[]; + + for (const field of docEntries) { + collection.push({ + group: paramsGroup, + type: escapeSpecial(field.type), + size: undefined, + allowedValues: undefined, + optional: !!field.optional, + field: field.name, + defaultValue: undefined, + description: field.documentation, + }); + + if (field.nested) { + updateBlockParameters(field.nested, block, field.name); + } + } +} + +/** + * Escape special character to make sure the markdown table isn't broken + */ +function escapeSpecial(str: string): string { + return str.replace(/\|/g, '\\|'); +} diff --git a/x-pack/plugins/ml/server/routes/apidoc_scripts/tsconfig.json b/x-pack/plugins/ml/server/routes/apidoc_scripts/tsconfig.json new file mode 100644 index 0000000000000..e3108b8c759f4 --- /dev/null +++ b/x-pack/plugins/ml/server/routes/apidoc_scripts/tsconfig.json @@ -0,0 +1,14 @@ +{ + "extends": "../../../tsconfig.json", + "compilerOptions": { + "outDir": "./target", + "target": "es6", + "moduleResolution": "node" + }, + "include": [ + "schema_worker.ts", + "schema_parser.ts", + "schema_extractor.ts", + "version_filter.ts" + ] +} diff --git a/x-pack/plugins/ml/server/routes/apidoc_scripts/types.ts b/x-pack/plugins/ml/server/routes/apidoc_scripts/types.ts new file mode 100644 index 0000000000000..08a443905ee05 --- /dev/null +++ b/x-pack/plugins/ml/server/routes/apidoc_scripts/types.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. + */ + +export interface ApiParameter { + group: string; + type: any; + size: undefined; + allowedValues: undefined; + optional: boolean; + field: string; + defaultValue: undefined; + description?: string; +} + +interface Local { + group: string; + type: string; + url: string; + title: string; + name: string; + description: string; + parameter: { + fields?: { + [key: string]: ApiParameter[] | undefined; + }; + }; + success: { fields: ObjectConstructor[] }; + version: string; + filename: string; + schemas?: Array<{ + name: string; + group: string; + }>; +} + +export interface Block { + global: any; + local: Local; +} diff --git a/x-pack/plugins/ml/server/routes/apidoc_scripts/version_filter.ts b/x-pack/plugins/ml/server/routes/apidoc_scripts/version_filter.ts new file mode 100644 index 0000000000000..8cbe38d667b2c --- /dev/null +++ b/x-pack/plugins/ml/server/routes/apidoc_scripts/version_filter.ts @@ -0,0 +1,21 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { Block } from './types'; + +const API_VERSION = '7.8.0'; + +/** + * Post Filter parsed results. + * Updates api version of the endpoints. + */ +export function postFilter(parsedFiles: any[]) { + parsedFiles.forEach(parsedFile => { + parsedFile.forEach((block: Block) => { + block.local.version = API_VERSION; + }); + }); +} diff --git a/x-pack/plugins/ml/server/routes/calendars.ts b/x-pack/plugins/ml/server/routes/calendars.ts index 34950c6ed79f7..a17601f74ae93 100644 --- a/x-pack/plugins/ml/server/routes/calendars.ts +++ b/x-pack/plugins/ml/server/routes/calendars.ts @@ -5,10 +5,9 @@ */ import { RequestHandlerContext } from 'kibana/server'; -import { schema } from '@kbn/config-schema'; import { wrapError } from '../client/error_wrapper'; import { RouteInitialization } from '../types'; -import { calendarSchema } from './schemas/calendars_schema'; +import { calendarSchema, calendarIdSchema, calendarIdsSchema } from './schemas/calendars_schema'; import { CalendarManager, Calendar, FormCalendar } from '../models/calendar'; function getAllCalendars(context: RequestHandlerContext) { @@ -42,7 +41,13 @@ function getCalendarsByIds(context: RequestHandlerContext, calendarIds: string) } export function calendars({ router, mlLicense }: RouteInitialization) { - // Gets calendars - size limit has been explicitly set to 1000 + /** + * @apiGroup Calendars + * + * @api {get} /api/ml/calendars Gets calendars + * @apiName GetCalendars + * @apiDescription Gets calendars - size limit has been explicitly set to 1000 + */ router.get( { path: '/api/ml/calendars', @@ -61,11 +66,20 @@ export function calendars({ router, mlLicense }: RouteInitialization) { }) ); + /** + * @apiGroup Calendars + * + * @api {get} /api/ml/calendars/:calendarIds Gets a calendar + * @apiName GetCalendarById + * @apiDescription Gets calendar by id + * + * @apiSchema (params) calendarIdsSchema + */ router.get( { path: '/api/ml/calendars/{calendarIds}', validate: { - params: schema.object({ calendarIds: schema.string() }), + params: calendarIdsSchema, }, }, mlLicense.fullLicenseAPIGuard(async (context, request, response) => { @@ -88,11 +102,20 @@ export function calendars({ router, mlLicense }: RouteInitialization) { }) ); + /** + * @apiGroup Calendars + * + * @api {put} /api/ml/calendars Creates a calendar + * @apiName PutCalendars + * @apiDescription Creates a calendar + * + * @apiSchema (body) calendarSchema + */ router.put( { path: '/api/ml/calendars', validate: { - body: schema.object({ ...calendarSchema }), + body: calendarSchema, }, }, mlLicense.fullLicenseAPIGuard(async (context, request, response) => { @@ -109,12 +132,22 @@ export function calendars({ router, mlLicense }: RouteInitialization) { }) ); + /** + * @apiGroup Calendars + * + * @api {put} /api/ml/calendars/:calendarId Updates a calendar + * @apiName UpdateCalendarById + * @apiDescription Updates a calendar + * + * @apiSchema (params) calendarIdSchema + * @apiSchema (body) calendarSchema + */ router.put( { path: '/api/ml/calendars/{calendarId}', validate: { - params: schema.object({ calendarId: schema.string() }), - body: schema.object({ ...calendarSchema }), + params: calendarIdSchema, + body: calendarSchema, }, }, mlLicense.fullLicenseAPIGuard(async (context, request, response) => { @@ -132,11 +165,20 @@ export function calendars({ router, mlLicense }: RouteInitialization) { }) ); + /** + * @apiGroup Calendars + * + * @api {delete} /api/ml/calendars/:calendarId Deletes a calendar + * @apiName DeleteCalendarById + * @apiDescription Deletes a calendar + * + * @apiSchema (params) calendarIdSchema + */ router.delete( { path: '/api/ml/calendars/{calendarId}', validate: { - params: schema.object({ calendarId: schema.string() }), + params: calendarIdSchema, }, }, mlLicense.fullLicenseAPIGuard(async (context, request, response) => { diff --git a/x-pack/plugins/ml/server/routes/data_frame_analytics.ts b/x-pack/plugins/ml/server/routes/data_frame_analytics.ts index 7ed1aa02b24ab..dd9e0ea66aa9d 100644 --- a/x-pack/plugins/ml/server/routes/data_frame_analytics.ts +++ b/x-pack/plugins/ml/server/routes/data_frame_analytics.ts @@ -4,7 +4,6 @@ * you may not use this file except in compliance with the Elastic License. */ -import { schema } from '@kbn/config-schema'; import { wrapError } from '../client/error_wrapper'; import { analyticsAuditMessagesProvider } from '../models/data_frame_analytics/analytics_audit_messages'; import { RouteInitialization } from '../types'; @@ -12,6 +11,8 @@ import { dataAnalyticsJobConfigSchema, dataAnalyticsEvaluateSchema, dataAnalyticsExplainSchema, + analyticsIdSchema, + stopsDataFrameAnalyticsJobQuerySchema, } from './schemas/data_analytics_schema'; /** @@ -31,9 +32,7 @@ export function dataFrameAnalyticsRoutes({ router, mlLicense }: RouteInitializat router.get( { path: '/api/ml/data_frame/analytics', - validate: { - params: schema.object({ analyticsId: schema.maybe(schema.string()) }), - }, + validate: false, }, mlLicense.fullLicenseAPIGuard(async (context, request, response) => { try { @@ -54,13 +53,13 @@ export function dataFrameAnalyticsRoutes({ router, mlLicense }: RouteInitializat * @apiName GetDataFrameAnalyticsById * @apiDescription Returns the data frame analytics job. * - * @apiParam {String} analyticsId Analytics ID. + * @apiSchema (params) analyticsIdSchema */ router.get( { path: '/api/ml/data_frame/analytics/{analyticsId}', validate: { - params: schema.object({ analyticsId: schema.string() }), + params: analyticsIdSchema, }, }, mlLicense.fullLicenseAPIGuard(async (context, request, response) => { @@ -111,13 +110,13 @@ export function dataFrameAnalyticsRoutes({ router, mlLicense }: RouteInitializat * @apiName GetDataFrameAnalyticsStatsById * @apiDescription Returns data frame analytics job statistics. * - * @apiParam {String} analyticsId Analytics ID. + * @apiSchema (params) analyticsIdSchema */ router.get( { path: '/api/ml/data_frame/analytics/{analyticsId}/_stats', validate: { - params: schema.object({ analyticsId: schema.string() }), + params: analyticsIdSchema, }, }, mlLicense.fullLicenseAPIGuard(async (context, request, response) => { @@ -146,16 +145,15 @@ export function dataFrameAnalyticsRoutes({ router, mlLicense }: RouteInitializat * @apiDescription This API creates a data frame analytics job that performs an analysis * on the source index and stores the outcome in a destination index. * - * @apiParam {String} analyticsId Analytics ID. + * @apiSchema (params) analyticsIdSchema + * @apiSchema (body) dataAnalyticsJobConfigSchema */ router.put( { path: '/api/ml/data_frame/analytics/{analyticsId}', validate: { - params: schema.object({ - analyticsId: schema.string(), - }), - body: schema.object(dataAnalyticsJobConfigSchema), + params: analyticsIdSchema, + body: dataAnalyticsJobConfigSchema, }, }, mlLicense.fullLicenseAPIGuard(async (context, request, response) => { @@ -183,12 +181,14 @@ export function dataFrameAnalyticsRoutes({ router, mlLicense }: RouteInitializat * @api {post} /api/ml/data_frame/_evaluate Evaluate the data frame analytics for an annotated index * @apiName EvaluateDataFrameAnalytics * @apiDescription Evaluates the data frame analytics for an annotated index. + * + * @apiSchema (body) dataAnalyticsEvaluateSchema */ router.post( { path: '/api/ml/data_frame/_evaluate', validate: { - body: schema.object({ ...dataAnalyticsEvaluateSchema }), + body: dataAnalyticsEvaluateSchema, }, }, mlLicense.fullLicenseAPIGuard(async (context, request, response) => { @@ -216,19 +216,13 @@ export function dataFrameAnalyticsRoutes({ router, mlLicense }: RouteInitializat * @apiDescription This API provides explanations for a data frame analytics config * that either exists already or one that has not been created yet. * - * @apiParam {String} [description] - * @apiParam {Object} [dest] - * @apiParam {Object} source - * @apiParam {String} source.index - * @apiParam {Object} analysis - * @apiParam {Object} [analyzed_fields] - * @apiParam {String} [model_memory_limit] + * @apiSchema (body) dataAnalyticsExplainSchema */ router.post( { path: '/api/ml/data_frame/analytics/_explain', validate: { - body: schema.object({ ...dataAnalyticsExplainSchema }), + body: dataAnalyticsExplainSchema, }, }, mlLicense.fullLicenseAPIGuard(async (context, request, response) => { @@ -255,15 +249,13 @@ export function dataFrameAnalyticsRoutes({ router, mlLicense }: RouteInitializat * @apiName DeleteDataFrameAnalytics * @apiDescription Deletes specified data frame analytics job. * - * @apiParam {String} analyticsId Analytics ID. + * @apiSchema (params) analyticsIdSchema */ router.delete( { path: '/api/ml/data_frame/analytics/{analyticsId}', validate: { - params: schema.object({ - analyticsId: schema.string(), - }), + params: analyticsIdSchema, }, }, mlLicense.fullLicenseAPIGuard(async (context, request, response) => { @@ -291,15 +283,13 @@ export function dataFrameAnalyticsRoutes({ router, mlLicense }: RouteInitializat * @apiName StartDataFrameAnalyticsJob * @apiDescription Starts a data frame analytics job. * - * @apiParam {String} analyticsId Analytics ID. + * @apiSchema (params) analyticsIdSchema */ router.post( { path: '/api/ml/data_frame/analytics/{analyticsId}/_start', validate: { - params: schema.object({ - analyticsId: schema.string(), - }), + params: analyticsIdSchema, }, }, mlLicense.fullLicenseAPIGuard(async (context, request, response) => { @@ -324,16 +314,15 @@ export function dataFrameAnalyticsRoutes({ router, mlLicense }: RouteInitializat * @apiName StopsDataFrameAnalyticsJob * @apiDescription Stops a data frame analytics job. * - * @apiParam {String} analyticsId Analytics ID. + * @apiSchema (params) analyticsIdSchema + * @apiSchema (query) stopsDataFrameAnalyticsJobQuerySchema */ router.post( { path: '/api/ml/data_frame/analytics/{analyticsId}/_stop', validate: { - params: schema.object({ - analyticsId: schema.string(), - force: schema.maybe(schema.boolean()), - }), + params: analyticsIdSchema, + query: stopsDataFrameAnalyticsJobQuerySchema, }, }, mlLicense.fullLicenseAPIGuard(async (context, request, response) => { @@ -367,13 +356,13 @@ export function dataFrameAnalyticsRoutes({ router, mlLicense }: RouteInitializat * @apiName GetDataFrameAnalyticsMessages * @apiDescription Returns the list of audit messages for data frame analytics jobs. * - * @apiParam {String} analyticsId Analytics ID. + * @apiSchema (params) analyticsIdSchema */ router.get( { path: '/api/ml/data_frame/analytics/{analyticsId}/messages', validate: { - params: schema.object({ analyticsId: schema.string() }), + params: analyticsIdSchema, }, }, mlLicense.fullLicenseAPIGuard(async (context, request, response) => { diff --git a/x-pack/plugins/ml/server/routes/data_visualizer.ts b/x-pack/plugins/ml/server/routes/data_visualizer.ts index b37c80b815e1a..a4c0d5553a4b2 100644 --- a/x-pack/plugins/ml/server/routes/data_visualizer.ts +++ b/x-pack/plugins/ml/server/routes/data_visualizer.ts @@ -11,6 +11,7 @@ import { Field } from '../models/data_visualizer/data_visualizer'; import { dataVisualizerFieldStatsSchema, dataVisualizerOverallStatsSchema, + indexPatternTitleSchema, } from './schemas/data_visualizer_schema'; import { RouteInitialization } from '../types'; @@ -75,12 +76,16 @@ export function dataVisualizerRoutes({ router, mlLicense }: RouteInitialization) * @apiName GetStatsForFields * @apiDescription Returns fields stats of the index pattern. * - * @apiParam {String} indexPatternTitle Index pattern title. + * @apiSchema (params) indexPatternTitleSchema + * @apiSchema (body) dataVisualizerFieldStatsSchema */ router.post( { path: '/api/ml/data_visualizer/get_field_stats/{indexPatternTitle}', - validate: dataVisualizerFieldStatsSchema, + validate: { + params: indexPatternTitleSchema, + body: dataVisualizerFieldStatsSchema, + }, }, mlLicense.basicLicenseAPIGuard(async (context, request, response) => { try { @@ -127,12 +132,16 @@ export function dataVisualizerRoutes({ router, mlLicense }: RouteInitialization) * @apiName GetOverallStats * @apiDescription Returns overall stats of the index pattern. * - * @apiParam {String} indexPatternTitle Index pattern title. + * @apiSchema (params) indexPatternTitleSchema + * @apiSchema (body) dataVisualizerOverallStatsSchema */ router.post( { path: '/api/ml/data_visualizer/get_overall_stats/{indexPatternTitle}', - validate: dataVisualizerOverallStatsSchema, + validate: { + params: indexPatternTitleSchema, + body: dataVisualizerOverallStatsSchema, + }, }, mlLicense.basicLicenseAPIGuard(async (context, request, response) => { try { diff --git a/x-pack/plugins/ml/server/routes/datafeeds.ts b/x-pack/plugins/ml/server/routes/datafeeds.ts index c1ee839340996..ec667e1d305f5 100644 --- a/x-pack/plugins/ml/server/routes/datafeeds.ts +++ b/x-pack/plugins/ml/server/routes/datafeeds.ts @@ -4,10 +4,14 @@ * you may not use this file except in compliance with the Elastic License. */ -import { schema } from '@kbn/config-schema'; import { wrapError } from '../client/error_wrapper'; import { RouteInitialization } from '../types'; -import { startDatafeedSchema, datafeedConfigSchema } from './schemas/datafeeds_schema'; +import { + startDatafeedSchema, + datafeedConfigSchema, + datafeedIdSchema, + deleteDatafeedQuerySchema, +} from './schemas/datafeeds_schema'; /** * Routes for datafeed service @@ -44,12 +48,14 @@ export function dataFeedRoutes({ router, mlLicense }: RouteInitialization) { * @api {get} /api/ml/datafeeds/:datafeedId Get datafeed for given datafeed id * @apiName GetDatafeed * @apiDescription Retrieves configuration information for datafeed + * + * @apiSchema (params) datafeedIdSchema */ router.get( { path: '/api/ml/datafeeds/{datafeedId}', validate: { - params: schema.object({ datafeedId: schema.string() }), + params: datafeedIdSchema, }, }, mlLicense.fullLicenseAPIGuard(async (context, request, response) => { @@ -97,12 +103,14 @@ export function dataFeedRoutes({ router, mlLicense }: RouteInitialization) { * @api {get} /api/ml/datafeeds/:datafeedId/_stats Get datafeed stats for given datafeed id * @apiName GetDatafeedStats * @apiDescription Retrieves usage information for datafeed + * + * @apiSchema (params) datafeedIdSchema */ router.get( { path: '/api/ml/datafeeds/{datafeedId}/_stats', validate: { - params: schema.object({ datafeedId: schema.string() }), + params: datafeedIdSchema, }, }, mlLicense.fullLicenseAPIGuard(async (context, request, response) => { @@ -127,12 +135,15 @@ export function dataFeedRoutes({ router, mlLicense }: RouteInitialization) { * @api {put} /api/ml/datafeeds/:datafeedId Creates datafeed * @apiName CreateDatafeed * @apiDescription Instantiates a datafeed + * + * @apiSchema (params) datafeedIdSchema + * @apiSchema (body) datafeedConfigSchema */ router.put( { path: '/api/ml/datafeeds/{datafeedId}', validate: { - params: schema.object({ datafeedId: schema.string() }), + params: datafeedIdSchema, body: datafeedConfigSchema, }, }, @@ -159,12 +170,15 @@ export function dataFeedRoutes({ router, mlLicense }: RouteInitialization) { * @api {post} /api/ml/datafeeds/:datafeedId/_update Updates datafeed for given datafeed id * @apiName UpdateDatafeed * @apiDescription Updates certain properties of a datafeed + * + * @apiSchema (params) datafeedIdSchema + * @apiSchema (body) datafeedConfigSchema */ router.post( { path: '/api/ml/datafeeds/{datafeedId}/_update', validate: { - params: schema.object({ datafeedId: schema.string() }), + params: datafeedIdSchema, body: datafeedConfigSchema, }, }, @@ -191,13 +205,16 @@ export function dataFeedRoutes({ router, mlLicense }: RouteInitialization) { * @api {delete} /api/ml/datafeeds/:datafeedId Deletes datafeed * @apiName DeleteDatafeed * @apiDescription Deletes an existing datafeed + * + * @apiSchema (params) datafeedIdSchema + * @apiSchema (query) deleteDatafeedQuerySchema */ router.delete( { path: '/api/ml/datafeeds/{datafeedId}', validate: { - params: schema.object({ datafeedId: schema.string() }), - query: schema.maybe(schema.object({ force: schema.maybe(schema.any()) })), + params: datafeedIdSchema, + query: deleteDatafeedQuerySchema, }, }, mlLicense.fullLicenseAPIGuard(async (context, request, response) => { @@ -227,12 +244,15 @@ export function dataFeedRoutes({ router, mlLicense }: RouteInitialization) { * @api {post} /api/ml/datafeeds/:datafeedId/_start Starts datafeed for given datafeed id(s) * @apiName StartDatafeed * @apiDescription Starts one or more datafeeds + * + * @apiSchema (params) datafeedIdSchema + * @apiSchema (body) startDatafeedSchema */ router.post( { path: '/api/ml/datafeeds/{datafeedId}/_start', validate: { - params: schema.object({ datafeedId: schema.string() }), + params: datafeedIdSchema, body: startDatafeedSchema, }, }, @@ -262,12 +282,14 @@ export function dataFeedRoutes({ router, mlLicense }: RouteInitialization) { * @api {post} /api/ml/datafeeds/:datafeedId/_stop Stops datafeed for given datafeed id(s) * @apiName StopDatafeed * @apiDescription Stops one or more datafeeds + * + * @apiSchema (params) datafeedIdSchema */ router.post( { path: '/api/ml/datafeeds/{datafeedId}/_stop', validate: { - params: schema.object({ datafeedId: schema.string() }), + params: datafeedIdSchema, }, }, mlLicense.fullLicenseAPIGuard(async (context, request, response) => { @@ -293,12 +315,14 @@ export function dataFeedRoutes({ router, mlLicense }: RouteInitialization) { * @api {get} /api/ml/datafeeds/:datafeedId/_preview Preview datafeed for given datafeed id * @apiName PreviewDatafeed * @apiDescription Previews a datafeed + * + * @apiSchema (params) datafeedIdSchema */ router.get( { path: '/api/ml/datafeeds/{datafeedId}/_preview', validate: { - params: schema.object({ datafeedId: schema.string() }), + params: datafeedIdSchema, }, }, mlLicense.fullLicenseAPIGuard(async (context, request, response) => { diff --git a/x-pack/plugins/ml/server/routes/fields_service.ts b/x-pack/plugins/ml/server/routes/fields_service.ts index db7613b163457..9a5f47409c8a0 100644 --- a/x-pack/plugins/ml/server/routes/fields_service.ts +++ b/x-pack/plugins/ml/server/routes/fields_service.ts @@ -35,6 +35,8 @@ export function fieldsService({ router, mlLicense }: RouteInitialization) { * @api {post} /api/ml/fields_service/field_cardinality Get cardinality of fields * @apiName GetCardinalityOfFields * @apiDescription Returns the cardinality of one or more fields. Returns an Object whose keys are the names of the fields, with values equal to the cardinality of the field + * + * @apiSchema (body) getCardinalityOfFieldsSchema */ router.post( { @@ -63,6 +65,8 @@ export function fieldsService({ router, mlLicense }: RouteInitialization) { * @api {post} /api/ml/fields_service/time_field_range Get time field range * @apiName GetTimeFieldRange * @apiDescription Returns the timefield range for the given index + * + * @apiSchema (body) getTimeFieldRangeSchema */ router.post( { diff --git a/x-pack/plugins/ml/server/routes/file_data_visualizer.ts b/x-pack/plugins/ml/server/routes/file_data_visualizer.ts index b915d13aa9720..3f3fc3f547b6a 100644 --- a/x-pack/plugins/ml/server/routes/file_data_visualizer.ts +++ b/x-pack/plugins/ml/server/routes/file_data_visualizer.ts @@ -6,7 +6,7 @@ import { schema } from '@kbn/config-schema'; import { RequestHandlerContext } from 'kibana/server'; -import { MAX_BYTES } from '../../common/constants/file_datavisualizer'; +import { MAX_FILE_SIZE_BYTES } from '../../common/constants/file_datavisualizer'; import { InputOverrides, Settings, @@ -22,6 +22,11 @@ import { import { RouteInitialization } from '../types'; import { updateTelemetry } from '../lib/telemetry'; +import { + analyzeFileQuerySchema, + importFileBodySchema, + importFileQuerySchema, +} from './schemas/file_data_visualizer_schema'; function analyzeFiles(context: RequestHandlerContext, data: InputData, overrides: InputOverrides) { const { analyzeFile } = fileDataVisualizerProvider(context.ml!.mlClient.callAsCurrentUser); @@ -51,35 +56,20 @@ export function fileDataVisualizerRoutes({ router, mlLicense }: RouteInitializat * @api {post} /api/ml/file_data_visualizer/analyze_file Analyze file data * @apiName AnalyzeFile * @apiDescription Performs analysis of the file data. + * + * @apiSchema (query) analyzeFileQuerySchema */ router.post( { path: '/api/ml/file_data_visualizer/analyze_file', validate: { body: schema.any(), - query: schema.maybe( - schema.object({ - charset: schema.maybe(schema.string()), - column_names: schema.maybe(schema.string()), - delimiter: schema.maybe(schema.string()), - explain: schema.maybe(schema.string()), - format: schema.maybe(schema.string()), - grok_pattern: schema.maybe(schema.string()), - has_header_row: schema.maybe(schema.string()), - line_merge_size_limit: schema.maybe(schema.string()), - lines_to_sample: schema.maybe(schema.string()), - quote: schema.maybe(schema.string()), - should_trim_fields: schema.maybe(schema.string()), - timeout: schema.maybe(schema.string()), - timestamp_field: schema.maybe(schema.string()), - timestamp_format: schema.maybe(schema.string()), - }) - ), + query: analyzeFileQuerySchema, }, options: { body: { accepts: ['text/*', 'application/json'], - maxBytes: MAX_BYTES, + maxBytes: MAX_FILE_SIZE_BYTES, }, }, }, @@ -99,29 +89,21 @@ export function fileDataVisualizerRoutes({ router, mlLicense }: RouteInitializat * @api {post} /api/ml/file_data_visualizer/import Import file data * @apiName ImportFile * @apiDescription Imports file data into elasticsearch index. + * + * @apiSchema (query) importFileQuerySchema + * @apiSchema (body) importFileBodySchema */ router.post( { path: '/api/ml/file_data_visualizer/import', validate: { - query: schema.object({ - id: schema.maybe(schema.string()), - }), - body: schema.object({ - index: schema.maybe(schema.string()), - data: schema.arrayOf(schema.any()), - settings: schema.maybe(schema.any()), - mappings: schema.any(), - ingestPipeline: schema.object({ - id: schema.maybe(schema.string()), - pipeline: schema.maybe(schema.any()), - }), - }), + query: importFileQuerySchema, + body: importFileBodySchema, }, options: { body: { accepts: ['application/json'], - maxBytes: MAX_BYTES, + maxBytes: MAX_FILE_SIZE_BYTES, }, }, }, diff --git a/x-pack/plugins/ml/server/routes/filters.ts b/x-pack/plugins/ml/server/routes/filters.ts index e827ed96b12af..738c25070358d 100644 --- a/x-pack/plugins/ml/server/routes/filters.ts +++ b/x-pack/plugins/ml/server/routes/filters.ts @@ -5,10 +5,9 @@ */ import { RequestHandlerContext } from 'kibana/server'; -import { schema } from '@kbn/config-schema'; import { wrapError } from '../client/error_wrapper'; import { RouteInitialization } from '../types'; -import { createFilterSchema, updateFilterSchema } from './schemas/filters_schema'; +import { createFilterSchema, filterIdSchema, updateFilterSchema } from './schemas/filters_schema'; import { FilterManager, FormFilter } from '../models/filter'; // TODO - add function for returning a list of just the filter IDs. @@ -79,6 +78,8 @@ export function filtersRoutes({ router, mlLicense }: RouteInitialization) { * @apiName GetFilterById * @apiDescription Retrieves the filter with the specified ID. * + * @apiSchema (params) filterIdSchema + * * @apiSuccess {Boolean} success * @apiSuccess {Object} filter the filter with the specified ID */ @@ -86,7 +87,7 @@ export function filtersRoutes({ router, mlLicense }: RouteInitialization) { { path: '/api/ml/filters/{filterId}', validate: { - params: schema.object({ filterId: schema.string() }), + params: filterIdSchema, }, }, mlLicense.fullLicenseAPIGuard(async (context, request, response) => { @@ -108,6 +109,8 @@ export function filtersRoutes({ router, mlLicense }: RouteInitialization) { * @apiName CreateFilter * @apiDescription Instantiates a filter, for use by custom rules in anomaly detection. * + * @apiSchema (body) createFilterSchema + * * @apiSuccess {Boolean} success * @apiSuccess {Object} filter created filter */ @@ -115,7 +118,7 @@ export function filtersRoutes({ router, mlLicense }: RouteInitialization) { { path: '/api/ml/filters', validate: { - body: schema.object(createFilterSchema), + body: createFilterSchema, }, }, mlLicense.fullLicenseAPIGuard(async (context, request, response) => { @@ -139,6 +142,9 @@ export function filtersRoutes({ router, mlLicense }: RouteInitialization) { * @apiName UpdateFilter * @apiDescription Updates the description of a filter, adds items or removes items. * + * @apiSchema (params) filterIdSchema + * @apiSchema (body) updateFilterSchema + * * @apiSuccess {Boolean} success * @apiSuccess {Object} filter updated filter */ @@ -146,8 +152,8 @@ export function filtersRoutes({ router, mlLicense }: RouteInitialization) { { path: '/api/ml/filters/{filterId}', validate: { - params: schema.object({ filterId: schema.string() }), - body: schema.object(updateFilterSchema), + params: filterIdSchema, + body: updateFilterSchema, }, }, mlLicense.fullLicenseAPIGuard(async (context, request, response) => { @@ -172,13 +178,13 @@ export function filtersRoutes({ router, mlLicense }: RouteInitialization) { * @apiName DeleteFilter * @apiDescription Deletes the filter with the specified ID. * - * @apiParam {String} filterId the ID of the filter to delete + * @apiSchema (params) filterIdSchema */ router.delete( { path: '/api/ml/filters/{filterId}', validate: { - params: schema.object({ filterId: schema.string() }), + params: filterIdSchema, }, }, mlLicense.fullLicenseAPIGuard(async (context, request, response) => { diff --git a/x-pack/plugins/ml/server/routes/indices.ts b/x-pack/plugins/ml/server/routes/indices.ts index fe66cc8b01396..e434936beba63 100644 --- a/x-pack/plugins/ml/server/routes/indices.ts +++ b/x-pack/plugins/ml/server/routes/indices.ts @@ -4,9 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ -import { schema } from '@kbn/config-schema'; import { wrapError } from '../client/error_wrapper'; import { RouteInitialization } from '../types'; +import { indicesSchema } from './schemas/indices_schema'; /** * Indices routes. @@ -15,18 +15,17 @@ export function indicesRoutes({ router, mlLicense }: RouteInitialization) { /** * @apiGroup Indices * - * @api {post} /api/ml/indices/field_caps + * @api {post} /api/ml/indices/field_caps Field caps * @apiName FieldCaps * @apiDescription Retrieves the capabilities of fields among multiple indices. + * + * @apiSchema (body) indicesSchema */ router.post( { path: '/api/ml/indices/field_caps', validate: { - body: schema.object({ - index: schema.maybe(schema.string()), - fields: schema.maybe(schema.arrayOf(schema.string())), - }), + body: indicesSchema, }, }, mlLicense.fullLicenseAPIGuard(async (context, request, response) => { diff --git a/x-pack/plugins/ml/server/routes/job_audit_messages.ts b/x-pack/plugins/ml/server/routes/job_audit_messages.ts index 5c6d8023cc172..71499748691f6 100644 --- a/x-pack/plugins/ml/server/routes/job_audit_messages.ts +++ b/x-pack/plugins/ml/server/routes/job_audit_messages.ts @@ -4,10 +4,10 @@ * you may not use this file except in compliance with the Elastic License. */ -import { schema } from '@kbn/config-schema'; import { wrapError } from '../client/error_wrapper'; import { RouteInitialization } from '../types'; import { jobAuditMessagesProvider } from '../models/job_audit_messages'; +import { jobAuditMessagesQuerySchema, jobIdSchema } from './schemas/job_audit_messages_schema'; /** * Routes for job audit message routes @@ -19,13 +19,16 @@ export function jobAuditMessagesRoutes({ router, mlLicense }: RouteInitializatio * @api {get} /api/ml/job_audit_messages/messages/:jobId Get audit messages * @apiName GetJobAuditMessages * @apiDescription Returns audit messages for specified job ID + * + * @apiSchema (params) jobIdSchema + * @apiSchema (query) jobAuditMessagesQuerySchema */ router.get( { path: '/api/ml/job_audit_messages/messages/{jobId}', validate: { - params: schema.object({ jobId: schema.maybe(schema.string()) }), - query: schema.maybe(schema.object({ from: schema.maybe(schema.any()) })), + params: jobIdSchema, + query: jobAuditMessagesQuerySchema, }, }, mlLicense.fullLicenseAPIGuard(async (context, request, response) => { @@ -52,13 +55,14 @@ export function jobAuditMessagesRoutes({ router, mlLicense }: RouteInitializatio * @api {get} /api/ml/job_audit_messages/messages Get all audit messages * @apiName GetAllJobAuditMessages * @apiDescription Returns all audit messages + * + * @apiSchema (query) jobAuditMessagesQuerySchema */ router.get( { path: '/api/ml/job_audit_messages/messages', validate: { - params: schema.object({ jobId: schema.maybe(schema.string()) }), - query: schema.maybe(schema.object({ from: schema.maybe(schema.any()) })), + query: jobAuditMessagesQuerySchema, }, }, mlLicense.fullLicenseAPIGuard(async (context, request, response) => { diff --git a/x-pack/plugins/ml/server/routes/job_service.ts b/x-pack/plugins/ml/server/routes/job_service.ts index 718f9e81603b1..493974cbafe36 100644 --- a/x-pack/plugins/ml/server/routes/job_service.ts +++ b/x-pack/plugins/ml/server/routes/job_service.ts @@ -53,12 +53,14 @@ export function jobServiceRoutes({ router, mlLicense }: RouteInitialization) { * @api {post} /api/ml/jobs/force_start_datafeeds Start datafeeds * @apiName ForceStartDatafeeds * @apiDescription Starts one or more datafeeds + * + * @apiSchema (body) forceStartDatafeedSchema */ router.post( { path: '/api/ml/jobs/force_start_datafeeds', validate: { - body: schema.object(forceStartDatafeedSchema), + body: forceStartDatafeedSchema, }, }, mlLicense.fullLicenseAPIGuard(async (context, request, response) => { @@ -82,12 +84,14 @@ export function jobServiceRoutes({ router, mlLicense }: RouteInitialization) { * @api {post} /api/ml/jobs/stop_datafeeds Stop datafeeds * @apiName StopDatafeeds * @apiDescription Stops one or more datafeeds + * + * @apiSchema (body) datafeedIdsSchema */ router.post( { path: '/api/ml/jobs/stop_datafeeds', validate: { - body: schema.object(datafeedIdsSchema), + body: datafeedIdsSchema, }, }, mlLicense.fullLicenseAPIGuard(async (context, request, response) => { @@ -111,12 +115,14 @@ export function jobServiceRoutes({ router, mlLicense }: RouteInitialization) { * @api {post} /api/ml/jobs/delete_jobs Delete jobs * @apiName DeleteJobs * @apiDescription Deletes an existing anomaly detection job + * + * @apiSchema (body) jobIdsSchema */ router.post( { path: '/api/ml/jobs/delete_jobs', validate: { - body: schema.object(jobIdsSchema), + body: jobIdsSchema, }, }, mlLicense.fullLicenseAPIGuard(async (context, request, response) => { @@ -140,12 +146,14 @@ export function jobServiceRoutes({ router, mlLicense }: RouteInitialization) { * @api {post} /api/ml/jobs/close_jobs Close jobs * @apiName CloseJobs * @apiDescription Closes one or more anomaly detection jobs + * + * @apiSchema (body) jobIdsSchema */ router.post( { path: '/api/ml/jobs/close_jobs', validate: { - body: schema.object(jobIdsSchema), + body: jobIdsSchema, }, }, mlLicense.fullLicenseAPIGuard(async (context, request, response) => { @@ -169,12 +177,14 @@ export function jobServiceRoutes({ router, mlLicense }: RouteInitialization) { * @api {post} /api/ml/jobs/jobs_summary Jobs summary * @apiName JobsSummary * @apiDescription Creates a summary jobs list. Jobs include job stats, datafeed stats, and calendars. + * + * @apiSchema (body) jobIdsSchema */ router.post( { path: '/api/ml/jobs/jobs_summary', validate: { - body: schema.object(jobIdsSchema), + body: jobIdsSchema, }, }, mlLicense.fullLicenseAPIGuard(async (context, request, response) => { @@ -198,6 +208,8 @@ export function jobServiceRoutes({ router, mlLicense }: RouteInitialization) { * @api {post} /api/ml/jobs/jobs_with_time_range Jobs with time range * @apiName JobsWithTimeRange * @apiDescription Creates a list of jobs with data about the job's time range + * + * @apiSchema (body) jobsWithTimerangeSchema */ router.post( { @@ -226,12 +238,14 @@ export function jobServiceRoutes({ router, mlLicense }: RouteInitialization) { * @api {post} /api/ml/jobs/jobs Create jobs list * @apiName CreateFullJobsList * @apiDescription Creates a list of jobs + * + * @apiSchema (body) jobIdsSchema */ router.post( { path: '/api/ml/jobs/jobs', validate: { - body: schema.object(jobIdsSchema), + body: jobIdsSchema, }, }, mlLicense.fullLicenseAPIGuard(async (context, request, response) => { @@ -281,6 +295,8 @@ export function jobServiceRoutes({ router, mlLicense }: RouteInitialization) { * @api {post} /api/ml/jobs/update_groups Update job groups * @apiName UpdateGroups * @apiDescription Updates 'groups' property of an anomaly detection job + * + * @apiSchema (body) updateGroupsSchema */ router.post( { @@ -336,12 +352,14 @@ export function jobServiceRoutes({ router, mlLicense }: RouteInitialization) { * @api {post} /api/ml/jobs/jobs_exist Check if jobs exist * @apiName JobsExist * @apiDescription Checks if each of the jobs in the specified list of IDs exist + * + * @apiSchema (body) jobIdsSchema */ router.post( { path: '/api/ml/jobs/jobs_exist', validate: { - body: schema.object(jobIdsSchema), + body: jobIdsSchema, }, }, mlLicense.fullLicenseAPIGuard(async (context, request, response) => { @@ -397,6 +415,8 @@ export function jobServiceRoutes({ router, mlLicense }: RouteInitialization) { * @api {post} /api/ml/jobs/new_job_line_chart Get job line chart data * @apiName NewJobLineChart * @apiDescription Returns line chart data for anomaly detection job + * + * @apiSchema (body) chartSchema */ router.post( { @@ -447,6 +467,8 @@ export function jobServiceRoutes({ router, mlLicense }: RouteInitialization) { * @api {post} /api/ml/jobs/new_job_population_chart Get population job chart data * @apiName NewJobPopulationChart * @apiDescription Returns population job chart data + * + * @apiSchema (body) chartSchema */ router.post( { @@ -523,6 +545,8 @@ export function jobServiceRoutes({ router, mlLicense }: RouteInitialization) { * @api {post} /api/ml/jobs/look_back_progress Get lookback progress * @apiName GetLookBackProgress * @apiDescription Returns current progress of anomaly detection job + * + * @apiSchema (body) lookBackProgressSchema */ router.post( { @@ -552,6 +576,8 @@ export function jobServiceRoutes({ router, mlLicense }: RouteInitialization) { * @api {post} /api/ml/jobs/categorization_field_examples Get categorization field examples * @apiName ValidateCategoryExamples * @apiDescription Validates category examples + * + * @apiSchema (body) categorizationFieldExamplesSchema */ router.post( { @@ -611,6 +637,8 @@ export function jobServiceRoutes({ router, mlLicense }: RouteInitialization) { * @api {post} /api/ml/jobs/top_categories Get top categories * @apiName TopCategories * @apiDescription Returns list of top categories + * + * @apiSchema (body) topCategoriesSchema */ router.post( { diff --git a/x-pack/plugins/ml/server/routes/job_validation.ts b/x-pack/plugins/ml/server/routes/job_validation.ts index 75d9cdf375049..dd2bd9deadf43 100644 --- a/x-pack/plugins/ml/server/routes/job_validation.ts +++ b/x-pack/plugins/ml/server/routes/job_validation.ts @@ -6,7 +6,7 @@ import Boom from 'boom'; import { RequestHandlerContext } from 'kibana/server'; -import { schema, TypeOf } from '@kbn/config-schema'; +import { TypeOf } from '@kbn/config-schema'; import { AnalysisConfig } from '../../common/types/anomaly_detection_jobs'; import { wrapError } from '../client/error_wrapper'; import { RouteInitialization } from '../types'; @@ -48,6 +48,8 @@ export function jobValidationRoutes({ router, mlLicense }: RouteInitialization, * @api {post} /api/ml/validate/estimate_bucket_span Estimate bucket span * @apiName EstimateBucketSpan * @apiDescription Estimates minimum viable bucket span based on the characteristics of a pre-viewed subset of the data + * + * @apiSchema (body) estimateBucketSpanSchema */ router.post( { @@ -94,6 +96,8 @@ export function jobValidationRoutes({ router, mlLicense }: RouteInitialization, * @apiName CalculateModelMemoryLimit * @apiDescription Calls _estimate_model_memory endpoint to retrieve model memory estimation. * + * @apiSchema (body) modelMemoryLimitSchema + * * @apiSuccess {String} modelMemoryLimit */ router.post( @@ -122,12 +126,14 @@ export function jobValidationRoutes({ router, mlLicense }: RouteInitialization, * @api {post} /api/ml/validate/cardinality Validate cardinality * @apiName ValidateCardinality * @apiDescription Validates cardinality for the given job configuration + * + * @apiSchema (body) validateCardinalitySchema */ router.post( { path: '/api/ml/validate/cardinality', validate: { - body: schema.object(validateCardinalitySchema), + body: validateCardinalitySchema, }, }, mlLicense.fullLicenseAPIGuard(async (context, request, response) => { @@ -152,6 +158,8 @@ export function jobValidationRoutes({ router, mlLicense }: RouteInitialization, * @api {post} /api/ml/validate/job Validates job * @apiName ValidateJob * @apiDescription Validates the given job configuration + * + * @apiSchema (body) validateJobSchema */ router.post( { diff --git a/x-pack/plugins/ml/server/routes/modules.ts b/x-pack/plugins/ml/server/routes/modules.ts index 358cd0ac2871c..2d462b6dc207a 100644 --- a/x-pack/plugins/ml/server/routes/modules.ts +++ b/x-pack/plugins/ml/server/routes/modules.ts @@ -152,7 +152,7 @@ export function dataRecognizer({ router, mlLicense }: RouteInitialization) { * @apiName SetupModule * @apiDescription Created module items. * - * @apiParam {String} moduleId Module id + * @apiSchema (body) setupModuleBodySchema */ router.post( { diff --git a/x-pack/plugins/ml/server/routes/results_service.ts b/x-pack/plugins/ml/server/routes/results_service.ts index 9849410eaf0d4..89c267340fe52 100644 --- a/x-pack/plugins/ml/server/routes/results_service.ts +++ b/x-pack/plugins/ml/server/routes/results_service.ts @@ -5,7 +5,6 @@ */ import { RequestHandlerContext } from 'kibana/server'; -import { schema } from '@kbn/config-schema'; import { wrapError } from '../client/error_wrapper'; import { RouteInitialization } from '../types'; import { @@ -80,12 +79,14 @@ export function resultsServiceRoutes({ router, mlLicense }: RouteInitialization) * @api {post} /api/ml/results/anomalies_table_data Prepare anomalies records for table display * @apiName GetAnomaliesTableData * @apiDescription Retrieves anomaly records for an anomaly detection job and formats them for anomalies table display + * + * @apiSchema (body) anomaliesTableDataSchema */ router.post( { path: '/api/ml/results/anomalies_table_data', validate: { - body: schema.object(anomaliesTableDataSchema), + body: anomaliesTableDataSchema, }, }, mlLicense.fullLicenseAPIGuard(async (context, request, response) => { @@ -107,12 +108,14 @@ export function resultsServiceRoutes({ router, mlLicense }: RouteInitialization) * @api {post} /api/ml/results/category_definition Returns category definition * @apiName GetCategoryDefinition * @apiDescription Returns the definition of the category with the specified ID and job ID + * + * @apiSchema (body) categoryDefinitionSchema */ router.post( { path: '/api/ml/results/category_definition', validate: { - body: schema.object(categoryDefinitionSchema), + body: categoryDefinitionSchema, }, }, mlLicense.fullLicenseAPIGuard(async (context, request, response) => { @@ -134,12 +137,14 @@ export function resultsServiceRoutes({ router, mlLicense }: RouteInitialization) * @api {post} /api/ml/results/max_anomaly_score Returns the maximum anomaly_score * @apiName GetMaxAnomalyScore * @apiDescription Returns the maximum anomaly score of the bucket results for the request job ID(s) and time range + * + * @apiSchema (body) maxAnomalyScoreSchema */ router.post( { path: '/api/ml/results/max_anomaly_score', validate: { - body: schema.object(maxAnomalyScoreSchema), + body: maxAnomalyScoreSchema, }, }, mlLicense.fullLicenseAPIGuard(async (context, request, response) => { @@ -161,12 +166,14 @@ export function resultsServiceRoutes({ router, mlLicense }: RouteInitialization) * @api {post} /api/ml/results/category_examples Returns category examples * @apiName GetCategoryExamples * @apiDescription Returns examples for the categories with the specified IDs from the job with the supplied ID + * + * @apiSchema (body) categoryExamplesSchema */ router.post( { path: '/api/ml/results/category_examples', validate: { - body: schema.object(categoryExamplesSchema), + body: categoryExamplesSchema, }, }, mlLicense.fullLicenseAPIGuard(async (context, request, response) => { @@ -188,12 +195,14 @@ export function resultsServiceRoutes({ router, mlLicense }: RouteInitialization) * @api {post} /api/ml/results/partition_fields_values Returns partition fields values * @apiName GetPartitionFieldsValues * @apiDescription Returns the partition fields with values that match the provided criteria for the specified job ID. + * + * @apiSchema (body) partitionFieldValuesSchema */ router.post( { path: '/api/ml/results/partition_fields_values', validate: { - body: schema.object(partitionFieldValuesSchema), + body: partitionFieldValuesSchema, }, }, mlLicense.fullLicenseAPIGuard(async (context, request, response) => { diff --git a/x-pack/plugins/ml/server/routes/schemas/annotations_schema.ts b/x-pack/plugins/ml/server/routes/schemas/annotations_schema.ts index 7d3d6aabb129c..fade2093ac842 100644 --- a/x-pack/plugins/ml/server/routes/schemas/annotations_schema.ts +++ b/x-pack/plugins/ml/server/routes/schemas/annotations_schema.ts @@ -6,7 +6,7 @@ import { schema } from '@kbn/config-schema'; -export const indexAnnotationSchema = { +export const indexAnnotationSchema = schema.object({ timestamp: schema.number(), end_timestamp: schema.number(), annotation: schema.string(), @@ -16,15 +16,16 @@ export const indexAnnotationSchema = { create_username: schema.maybe(schema.string()), modified_time: schema.maybe(schema.number()), modified_username: schema.maybe(schema.string()), + /** Document id */ _id: schema.maybe(schema.string()), key: schema.maybe(schema.string()), -}; +}); -export const getAnnotationsSchema = { +export const getAnnotationsSchema = schema.object({ jobIds: schema.arrayOf(schema.string()), earliestMs: schema.oneOf([schema.nullable(schema.number()), schema.maybe(schema.number())]), latestMs: schema.oneOf([schema.nullable(schema.number()), schema.maybe(schema.number())]), maxAnnotations: schema.number(), -}; +}); -export const deleteAnnotationSchema = { annotationId: schema.string() }; +export const deleteAnnotationSchema = schema.object({ annotationId: schema.string() }); diff --git a/x-pack/plugins/ml/server/routes/schemas/anomaly_detectors_schema.ts b/x-pack/plugins/ml/server/routes/schemas/anomaly_detectors_schema.ts index 22c3d94dfb29e..ab1305d9bc354 100644 --- a/x-pack/plugins/ml/server/routes/schemas/anomaly_detectors_schema.ts +++ b/x-pack/plugins/ml/server/routes/schemas/anomaly_detectors_schema.ts @@ -26,6 +26,7 @@ const detectorSchema = schema.object({ over_field_name: schema.maybe(schema.string()), partition_field_name: schema.maybe(schema.string()), detector_description: schema.maybe(schema.string()), + /** Custom rules */ custom_rules: customRulesSchema, }); @@ -37,20 +38,24 @@ const customUrlSchema = { const customSettingsSchema = schema.object( { + /** Indicates the creator entity */ created_by: schema.maybe(schema.string()), - custom_urls: schema.maybe(schema.arrayOf(schema.maybe(schema.object({ ...customUrlSchema })))), + custom_urls: schema.maybe(schema.arrayOf(schema.maybe(schema.object(customUrlSchema)))), }, { unknowns: 'allow' } // Create / Update job API allows other fields to be added to custom_settings. ); -export const anomalyDetectionUpdateJobSchema = { +export const anomalyDetectionUpdateJobSchema = schema.object({ description: schema.maybe(schema.string()), detectors: schema.maybe( schema.arrayOf( schema.maybe( schema.object({ + /** Detector index */ detector_index: schema.number(), + /** Description */ description: schema.maybe(schema.string()), + /** Custom rules */ custom_rules: customRulesSchema, }) ) @@ -64,7 +69,7 @@ export const anomalyDetectionUpdateJobSchema = { }) ), groups: schema.maybe(schema.arrayOf(schema.maybe(schema.string()))), -}; +}); export const analysisConfigSchema = schema.object({ bucket_span: schema.maybe(schema.string()), @@ -78,6 +83,7 @@ export const anomalyDetectionJobSchema = { analysis_config: analysisConfigSchema, analysis_limits: schema.maybe( schema.object({ + /** Limit of categorization examples */ categorization_examples_limit: schema.maybe(schema.number()), model_memory_limit: schema.maybe(schema.string()), }) @@ -88,6 +94,7 @@ export const anomalyDetectionJobSchema = { allow_lazy_open: schema.maybe(schema.any()), data_counts: schema.maybe(schema.any()), data_description: schema.object({ + /** Format */ format: schema.maybe(schema.string()), time_field: schema.string(), time_format: schema.maybe(schema.string()), @@ -110,3 +117,63 @@ export const anomalyDetectionJobSchema = { results_retention_days: schema.maybe(schema.number()), state: schema.maybe(schema.string()), }; + +export const jobIdSchema = schema.object({ + /** Job id */ + jobId: schema.string(), +}); + +export const getRecordsSchema = schema.object({ + desc: schema.maybe(schema.boolean()), + end: schema.maybe(schema.string()), + exclude_interim: schema.maybe(schema.boolean()), + page: schema.maybe( + schema.object({ + from: schema.maybe(schema.number()), + size: schema.maybe(schema.number()), + }) + ), + record_score: schema.maybe(schema.number()), + sort: schema.maybe(schema.string()), + start: schema.maybe(schema.string()), +}); + +export const getBucketsSchema = schema.object({ + anomaly_score: schema.maybe(schema.number()), + desc: schema.maybe(schema.boolean()), + end: schema.maybe(schema.string()), + exclude_interim: schema.maybe(schema.boolean()), + expand: schema.maybe(schema.boolean()), + /** Page definition */ + page: schema.maybe( + schema.object({ + /** Page offset */ + from: schema.maybe(schema.number()), + /** Size of the page */ + size: schema.maybe(schema.number()), + }) + ), + sort: schema.maybe(schema.string()), + start: schema.maybe(schema.string()), +}); + +export const getBucketParamsSchema = schema.object({ + jobId: schema.string(), + timestamp: schema.maybe(schema.string()), +}); + +export const getOverallBucketsSchema = schema.object({ + topN: schema.number(), + bucketSpan: schema.string(), + start: schema.number(), + end: schema.number(), +}); + +export const getCategoriesSchema = schema.object({ + /** Category id */ + categoryId: schema.string(), + /** Job id */ + jobId: schema.string(), +}); + +export const forecastAnomalyDetector = schema.object({ duration: schema.any() }); diff --git a/x-pack/plugins/ml/server/routes/schemas/calendars_schema.ts b/x-pack/plugins/ml/server/routes/schemas/calendars_schema.ts index f5e59d983a9aa..6d8f94311816d 100644 --- a/x-pack/plugins/ml/server/routes/schemas/calendars_schema.ts +++ b/x-pack/plugins/ml/server/routes/schemas/calendars_schema.ts @@ -6,7 +6,7 @@ import { schema } from '@kbn/config-schema'; -export const calendarSchema = { +export const calendarSchema = schema.object({ calendar_id: schema.maybe(schema.string()), calendarId: schema.string(), job_ids: schema.arrayOf(schema.maybe(schema.string())), @@ -22,4 +22,11 @@ export const calendarSchema = { }) ) ), -}; +}); + +export const calendarIdSchema = schema.object({ calendarId: schema.string() }); + +export const calendarIdsSchema = schema.object({ + /** Comma-separated list of calendar IDs */ + calendarIds: schema.string(), +}); diff --git a/x-pack/plugins/ml/server/routes/schemas/data_analytics_schema.ts b/x-pack/plugins/ml/server/routes/schemas/data_analytics_schema.ts index 21454fa884b82..f1d4947a7abc5 100644 --- a/x-pack/plugins/ml/server/routes/schemas/data_analytics_schema.ts +++ b/x-pack/plugins/ml/server/routes/schemas/data_analytics_schema.ts @@ -6,7 +6,7 @@ import { schema } from '@kbn/config-schema'; -export const dataAnalyticsJobConfigSchema = { +export const dataAnalyticsJobConfigSchema = schema.object({ description: schema.maybe(schema.string()), dest: schema.object({ index: schema.string(), @@ -17,7 +17,9 @@ export const dataAnalyticsJobConfigSchema = { query: schema.maybe(schema.any()), _source: schema.maybe( schema.object({ + /** Fields to include in results */ includes: schema.maybe(schema.arrayOf(schema.maybe(schema.string()))), + /** Fields to exclude from results */ excludes: schema.maybe(schema.arrayOf(schema.maybe(schema.string()))), }) ), @@ -26,9 +28,9 @@ export const dataAnalyticsJobConfigSchema = { analysis: schema.any(), analyzed_fields: schema.any(), model_memory_limit: schema.string(), -}; +}); -export const dataAnalyticsEvaluateSchema = { +export const dataAnalyticsEvaluateSchema = schema.object({ index: schema.string(), query: schema.maybe(schema.any()), evaluation: schema.maybe( @@ -37,15 +39,27 @@ export const dataAnalyticsEvaluateSchema = { classification: schema.maybe(schema.any()), }) ), -}; +}); -export const dataAnalyticsExplainSchema = { +export const dataAnalyticsExplainSchema = schema.object({ description: schema.maybe(schema.string()), dest: schema.maybe(schema.any()), + /** Source */ source: schema.object({ index: schema.string(), }), analysis: schema.any(), analyzed_fields: schema.maybe(schema.any()), model_memory_limit: schema.maybe(schema.string()), -}; +}); + +export const analyticsIdSchema = schema.object({ + /** + * Analytics ID + */ + analyticsId: schema.string(), +}); + +export const stopsDataFrameAnalyticsJobQuerySchema = schema.object({ + force: schema.maybe(schema.boolean()), +}); diff --git a/x-pack/plugins/ml/server/routes/schemas/data_visualizer_schema.ts b/x-pack/plugins/ml/server/routes/schemas/data_visualizer_schema.ts index 0c10b2d5b4f16..1a1d02f991b55 100644 --- a/x-pack/plugins/ml/server/routes/schemas/data_visualizer_schema.ts +++ b/x-pack/plugins/ml/server/routes/schemas/data_visualizer_schema.ts @@ -6,33 +6,27 @@ import { schema } from '@kbn/config-schema'; -export const dataVisualizerFieldStatsSchema = { - params: schema.object({ - indexPatternTitle: schema.string(), - }), - body: schema.object({ - query: schema.any(), - fields: schema.arrayOf(schema.any()), - samplerShardSize: schema.number(), - timeFieldName: schema.maybe(schema.string()), - earliest: schema.maybe(schema.number()), - latest: schema.maybe(schema.number()), - interval: schema.maybe(schema.string()), - maxExamples: schema.number(), - }), -}; +export const indexPatternTitleSchema = schema.object({ + indexPatternTitle: schema.string(), +}); -export const dataVisualizerOverallStatsSchema = { - params: schema.object({ - indexPatternTitle: schema.string(), - }), - body: schema.object({ - query: schema.any(), - aggregatableFields: schema.arrayOf(schema.string()), - nonAggregatableFields: schema.arrayOf(schema.string()), - samplerShardSize: schema.number(), - timeFieldName: schema.maybe(schema.string()), - earliest: schema.maybe(schema.number()), - latest: schema.maybe(schema.number()), - }), -}; +export const dataVisualizerFieldStatsSchema = schema.object({ + query: schema.any(), + fields: schema.arrayOf(schema.any()), + samplerShardSize: schema.number(), + timeFieldName: schema.maybe(schema.string()), + earliest: schema.maybe(schema.number()), + latest: schema.maybe(schema.number()), + interval: schema.maybe(schema.string()), + maxExamples: schema.number(), +}); + +export const dataVisualizerOverallStatsSchema = schema.object({ + query: schema.any(), + aggregatableFields: schema.arrayOf(schema.string()), + nonAggregatableFields: schema.arrayOf(schema.string()), + samplerShardSize: schema.number(), + timeFieldName: schema.maybe(schema.string()), + earliest: schema.maybe(schema.number()), + latest: schema.maybe(schema.number()), +}); diff --git a/x-pack/plugins/ml/server/routes/schemas/datafeeds_schema.ts b/x-pack/plugins/ml/server/routes/schemas/datafeeds_schema.ts index 466e70197e3d1..2cfb9d7d275d5 100644 --- a/x-pack/plugins/ml/server/routes/schemas/datafeeds_schema.ts +++ b/x-pack/plugins/ml/server/routes/schemas/datafeeds_schema.ts @@ -42,3 +42,9 @@ export const datafeedConfigSchema = schema.object({ }) ), }); + +export const datafeedIdSchema = schema.object({ datafeedId: schema.string() }); + +export const deleteDatafeedQuerySchema = schema.maybe( + schema.object({ force: schema.maybe(schema.any()) }) +); diff --git a/x-pack/plugins/ml/server/routes/schemas/file_data_visualizer_schema.ts b/x-pack/plugins/ml/server/routes/schemas/file_data_visualizer_schema.ts new file mode 100644 index 0000000000000..9a80cf795cabf --- /dev/null +++ b/x-pack/plugins/ml/server/routes/schemas/file_data_visualizer_schema.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 { schema } from '@kbn/config-schema'; + +export const analyzeFileQuerySchema = schema.maybe( + schema.object({ + charset: schema.maybe(schema.string()), + column_names: schema.maybe(schema.string()), + delimiter: schema.maybe(schema.string()), + explain: schema.maybe(schema.string()), + format: schema.maybe(schema.string()), + grok_pattern: schema.maybe(schema.string()), + has_header_row: schema.maybe(schema.string()), + line_merge_size_limit: schema.maybe(schema.string()), + lines_to_sample: schema.maybe(schema.string()), + quote: schema.maybe(schema.string()), + should_trim_fields: schema.maybe(schema.string()), + timeout: schema.maybe(schema.string()), + timestamp_field: schema.maybe(schema.string()), + timestamp_format: schema.maybe(schema.string()), + }) +); + +export const importFileQuerySchema = schema.object({ + id: schema.maybe(schema.string()), +}); + +export const importFileBodySchema = schema.object({ + index: schema.maybe(schema.string()), + data: schema.arrayOf(schema.any()), + settings: schema.maybe(schema.any()), + /** Mappings */ + mappings: schema.any(), + /** Ingest pipeline definition */ + ingestPipeline: schema.object({ + id: schema.maybe(schema.string()), + pipeline: schema.maybe(schema.any()), + }), +}); diff --git a/x-pack/plugins/ml/server/routes/schemas/filters_schema.ts b/x-pack/plugins/ml/server/routes/schemas/filters_schema.ts index dffee56565c73..d33d34c7096ce 100644 --- a/x-pack/plugins/ml/server/routes/schemas/filters_schema.ts +++ b/x-pack/plugins/ml/server/routes/schemas/filters_schema.ts @@ -6,14 +6,21 @@ import { schema } from '@kbn/config-schema'; -export const createFilterSchema = { +export const createFilterSchema = schema.object({ filterId: schema.string(), description: schema.maybe(schema.string()), items: schema.arrayOf(schema.string()), -}; +}); -export const updateFilterSchema = { +export const updateFilterSchema = schema.object({ description: schema.maybe(schema.string()), addItems: schema.maybe(schema.arrayOf(schema.string())), removeItems: schema.maybe(schema.arrayOf(schema.string())), -}; +}); + +export const filterIdSchema = schema.object({ + /** + * ID of the filter + */ + filterId: schema.string(), +}); diff --git a/x-pack/plugins/ml/server/routes/schemas/indices_schema.ts b/x-pack/plugins/ml/server/routes/schemas/indices_schema.ts new file mode 100644 index 0000000000000..f1b06392292f0 --- /dev/null +++ b/x-pack/plugins/ml/server/routes/schemas/indices_schema.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 { schema } from '@kbn/config-schema'; + +export const indicesSchema = schema.object({ + index: schema.maybe(schema.string()), + fields: schema.maybe(schema.arrayOf(schema.string())), +}); diff --git a/x-pack/plugins/ml/server/routes/schemas/job_audit_messages_schema.ts b/x-pack/plugins/ml/server/routes/schemas/job_audit_messages_schema.ts new file mode 100644 index 0000000000000..b94a004384eb1 --- /dev/null +++ b/x-pack/plugins/ml/server/routes/schemas/job_audit_messages_schema.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 { schema } from '@kbn/config-schema'; + +export const jobIdSchema = schema.object({ jobId: schema.maybe(schema.string()) }); + +export const jobAuditMessagesQuerySchema = schema.maybe( + schema.object({ from: schema.maybe(schema.any()) }) +); diff --git a/x-pack/plugins/ml/server/routes/schemas/job_service_schema.ts b/x-pack/plugins/ml/server/routes/schemas/job_service_schema.ts index deb62678a777c..d2036b8a7c0fa 100644 --- a/x-pack/plugins/ml/server/routes/schemas/job_service_schema.ts +++ b/x-pack/plugins/ml/server/routes/schemas/job_service_schema.ts @@ -29,21 +29,25 @@ export const chartSchema = { splitFieldValue: schema.maybe(schema.nullable(schema.string())), }; -export const datafeedIdsSchema = { datafeedIds: schema.arrayOf(schema.maybe(schema.string())) }; +export const datafeedIdsSchema = schema.object({ + datafeedIds: schema.arrayOf(schema.maybe(schema.string())), +}); -export const forceStartDatafeedSchema = { +export const forceStartDatafeedSchema = schema.object({ datafeedIds: schema.arrayOf(schema.maybe(schema.string())), start: schema.maybe(schema.number()), end: schema.maybe(schema.number()), -}; +}); -export const jobIdsSchema = { +export const jobIdsSchema = schema.object({ jobIds: schema.maybe( schema.oneOf([schema.string(), schema.arrayOf(schema.maybe(schema.string()))]) ), -}; +}); -export const jobsWithTimerangeSchema = { dateFormatTz: schema.maybe(schema.string()) }; +export const jobsWithTimerangeSchema = { + dateFormatTz: schema.maybe(schema.string()), +}; export const lookBackProgressSchema = { jobId: schema.string(), diff --git a/x-pack/plugins/ml/server/routes/schemas/job_validation_schema.ts b/x-pack/plugins/ml/server/routes/schemas/job_validation_schema.ts index 3ded6e770eed5..f12c85962a28d 100644 --- a/x-pack/plugins/ml/server/routes/schemas/job_validation_schema.ts +++ b/x-pack/plugins/ml/server/routes/schemas/job_validation_schema.ts @@ -37,7 +37,7 @@ export const validateJobSchema = schema.object({ job: schema.object(anomalyDetectionJobSchema), }); -export const validateCardinalitySchema = { +export const validateCardinalitySchema = schema.object({ ...anomalyDetectionJobSchema, datafeed_config: datafeedConfigSchema, -}; +}); diff --git a/x-pack/plugins/ml/server/routes/schemas/results_service_schema.ts b/x-pack/plugins/ml/server/routes/schemas/results_service_schema.ts index 32d829db7f81b..f7317e534b33b 100644 --- a/x-pack/plugins/ml/server/routes/schemas/results_service_schema.ts +++ b/x-pack/plugins/ml/server/routes/schemas/results_service_schema.ts @@ -12,7 +12,7 @@ const criteriaFieldSchema = schema.object({ fieldValue: schema.any(), }); -export const anomaliesTableDataSchema = { +export const anomaliesTableDataSchema = schema.object({ jobIds: schema.arrayOf(schema.string()), criteriaFields: schema.arrayOf(criteriaFieldSchema), influencers: schema.arrayOf( @@ -26,29 +26,29 @@ export const anomaliesTableDataSchema = { maxRecords: schema.number(), maxExamples: schema.maybe(schema.number()), influencersFilterQuery: schema.maybe(schema.any()), -}; +}); -export const categoryDefinitionSchema = { +export const categoryDefinitionSchema = schema.object({ jobId: schema.maybe(schema.string()), categoryId: schema.string(), -}; +}); -export const maxAnomalyScoreSchema = { +export const maxAnomalyScoreSchema = schema.object({ jobIds: schema.arrayOf(schema.string()), earliestMs: schema.maybe(schema.number()), latestMs: schema.maybe(schema.number()), -}; +}); -export const categoryExamplesSchema = { +export const categoryExamplesSchema = schema.object({ jobId: schema.string(), categoryIds: schema.arrayOf(schema.string()), maxExamples: schema.number(), -}; +}); -export const partitionFieldValuesSchema = { +export const partitionFieldValuesSchema = schema.object({ jobId: schema.string(), searchTerm: schema.maybe(schema.any()), criteriaFields: schema.arrayOf(criteriaFieldSchema), earliestMs: schema.number(), latestMs: schema.number(), -}; +}); diff --git a/x-pack/plugins/ml/shared_imports.ts b/x-pack/plugins/ml/shared_imports.ts index 94ce8c82f1d95..a82ed5387818d 100644 --- a/x-pack/plugins/ml/shared_imports.ts +++ b/x-pack/plugins/ml/shared_imports.ts @@ -4,9 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ -export { XJsonMode } from '../../../src/plugins/es_ui_shared/console_lang/ace/modes/x_json'; - export { + XJsonMode, collapseLiteralStrings, expandLiteralStrings, -} from '../../../src/plugins/es_ui_shared/console_lang/lib'; +} from '../../../src/plugins/es_ui_shared/public'; diff --git a/x-pack/plugins/monitoring/server/alerts/cluster_state.test.ts b/x-pack/plugins/monitoring/server/alerts/cluster_state.test.ts index 6a9ca88437347..bcc1a8abe5cb0 100644 --- a/x-pack/plugins/monitoring/server/alerts/cluster_state.test.ts +++ b/x-pack/plugins/monitoring/server/alerts/cluster_state.test.ts @@ -4,14 +4,13 @@ * you may not use this file except in compliance with the Elastic License. */ import { Logger } from 'src/core/server'; -import { savedObjectsClientMock } from 'src/core/server/mocks'; import { getClusterState } from './cluster_state'; -import { AlertServices } from '../../../alerting/server'; import { ALERT_TYPE_CLUSTER_STATE } from '../../common/constants'; import { AlertCommonParams, AlertCommonState, AlertClusterStatePerClusterState } from './types'; import { getPreparedAlert } from '../lib/alerts/get_prepared_alert'; import { executeActions } from '../lib/alerts/cluster_state.lib'; import { AlertClusterStateState } from './enums'; +import { alertsMock, AlertServicesMock } from '../../../alerting/server/mocks'; jest.mock('../lib/alerts/cluster_state.lib', () => ({ executeActions: jest.fn(), @@ -26,18 +25,8 @@ jest.mock('../lib/alerts/get_prepared_alert', () => ({ }), })); -interface MockServices { - callCluster: jest.Mock; - alertInstanceFactory: jest.Mock; - savedObjectsClient: jest.Mock; -} - describe('getClusterState', () => { - const services: MockServices | AlertServices = { - callCluster: jest.fn(), - alertInstanceFactory: jest.fn(), - savedObjectsClient: savedObjectsClientMock.create(), - }; + const services: AlertServicesMock = alertsMock.createAlertServices(); const params: AlertCommonParams = { dateFormat: 'YYYY', @@ -107,7 +96,7 @@ describe('getClusterState', () => { it('should alert if green -> yellow', async () => { const result = await setupAlert(AlertClusterStateState.Green, AlertClusterStateState.Yellow); expect(executeActions).toHaveBeenCalledWith( - undefined, + services.alertInstanceFactory(ALERT_TYPE_CLUSTER_STATE), cluster, AlertClusterStateState.Yellow, emailAddress @@ -121,7 +110,7 @@ describe('getClusterState', () => { it('should alert if yellow -> green', async () => { const result = await setupAlert(AlertClusterStateState.Yellow, AlertClusterStateState.Green); expect(executeActions).toHaveBeenCalledWith( - undefined, + services.alertInstanceFactory(ALERT_TYPE_CLUSTER_STATE), cluster, AlertClusterStateState.Green, emailAddress, @@ -135,7 +124,7 @@ describe('getClusterState', () => { it('should alert if green -> red', async () => { const result = await setupAlert(AlertClusterStateState.Green, AlertClusterStateState.Red); expect(executeActions).toHaveBeenCalledWith( - undefined, + services.alertInstanceFactory(ALERT_TYPE_CLUSTER_STATE), cluster, AlertClusterStateState.Red, emailAddress @@ -149,7 +138,7 @@ describe('getClusterState', () => { it('should alert if red -> green', async () => { const result = await setupAlert(AlertClusterStateState.Red, AlertClusterStateState.Green); expect(executeActions).toHaveBeenCalledWith( - undefined, + services.alertInstanceFactory(ALERT_TYPE_CLUSTER_STATE), cluster, AlertClusterStateState.Green, emailAddress, diff --git a/x-pack/plugins/monitoring/server/alerts/license_expiration.test.ts b/x-pack/plugins/monitoring/server/alerts/license_expiration.test.ts index 92047e300bc1f..f9d2ec3e1d48e 100644 --- a/x-pack/plugins/monitoring/server/alerts/license_expiration.test.ts +++ b/x-pack/plugins/monitoring/server/alerts/license_expiration.test.ts @@ -8,8 +8,6 @@ import moment from 'moment-timezone'; import { getLicenseExpiration } from './license_expiration'; import { ALERT_TYPE_LICENSE_EXPIRATION } from '../../common/constants'; import { Logger } from 'src/core/server'; -import { AlertServices } from '../../../alerting/server'; -import { savedObjectsClientMock } from 'src/core/server/mocks'; import { AlertCommonParams, AlertCommonState, @@ -18,6 +16,7 @@ import { } from './types'; import { executeActions } from '../lib/alerts/license_expiration.lib'; import { PreparedAlert, getPreparedAlert } from '../lib/alerts/get_prepared_alert'; +import { alertsMock, AlertServicesMock } from '../../../alerting/server/mocks'; jest.mock('../lib/alerts/license_expiration.lib', () => ({ executeActions: jest.fn(), @@ -32,18 +31,8 @@ jest.mock('../lib/alerts/get_prepared_alert', () => ({ }), })); -interface MockServices { - callCluster: jest.Mock; - alertInstanceFactory: jest.Mock; - savedObjectsClient: jest.Mock; -} - describe('getLicenseExpiration', () => { - const services: MockServices | AlertServices = { - callCluster: jest.fn(), - alertInstanceFactory: jest.fn(), - savedObjectsClient: savedObjectsClientMock.create(), - }; + const services: AlertServicesMock = alertsMock.createAlertServices(); const params: AlertCommonParams = { dateFormat: 'YYYY', @@ -106,6 +95,7 @@ describe('getLicenseExpiration', () => { } afterEach(() => { + jest.clearAllMocks(); (executeActions as jest.Mock).mockClear(); (getPreparedAlert as jest.Mock).mockClear(); }); @@ -135,7 +125,7 @@ describe('getLicenseExpiration', () => { const newState = result[clusterUuid] as AlertLicensePerClusterState; expect(newState.expiredCheckDateMS > 0).toBe(true); expect(executeActions).toHaveBeenCalledWith( - undefined, + services.alertInstanceFactory(ALERT_TYPE_LICENSE_EXPIRATION), cluster, moment.utc(expiryDateMS), dateFormat, @@ -157,7 +147,7 @@ describe('getLicenseExpiration', () => { const newState = result[clusterUuid] as AlertLicensePerClusterState; expect(newState.expiredCheckDateMS).toBe(0); expect(executeActions).toHaveBeenCalledWith( - undefined, + services.alertInstanceFactory(ALERT_TYPE_LICENSE_EXPIRATION), cluster, moment.utc(expiryDateMS), dateFormat, @@ -196,7 +186,7 @@ describe('getLicenseExpiration', () => { const newState = result[clusterUuid] as AlertLicensePerClusterState; expect(newState.expiredCheckDateMS > 0).toBe(true); expect(executeActions).toHaveBeenCalledWith( - undefined, + services.alertInstanceFactory(ALERT_TYPE_LICENSE_EXPIRATION), cluster, moment.utc(expiryDateMS), dateFormat, diff --git a/x-pack/plugins/monitoring/server/lib/errors/known_errors.js b/x-pack/plugins/monitoring/server/lib/errors/known_errors.js index 6f02d0b0b26c0..17bcdd0414adf 100644 --- a/x-pack/plugins/monitoring/server/lib/errors/known_errors.js +++ b/x-pack/plugins/monitoring/server/lib/errors/known_errors.js @@ -47,5 +47,7 @@ export function isKnownError(err) { export function handleKnownError(err) { err.message = err.message + ': ' + (err.description || mapTypeMessage[err.constructor.name]); - return boomify(err, { statusCode: KNOWN_ERROR_STATUS_CODE }); + let statusCode = err.statusCode || err.status; + statusCode = statusCode !== 500 ? statusCode : KNOWN_ERROR_STATUS_CODE; + return boomify(err, { statusCode }); } diff --git a/x-pack/plugins/monitoring/server/plugin.ts b/x-pack/plugins/monitoring/server/plugin.ts index 0fa1a5bf144ac..a45e80ac71d65 100644 --- a/x-pack/plugins/monitoring/server/plugin.ts +++ b/x-pack/plugins/monitoring/server/plugin.ts @@ -10,12 +10,6 @@ import { i18n } from '@kbn/i18n'; import { has, get } from 'lodash'; import { UsageCollectionSetup } from 'src/plugins/usage_collection/server'; import { TelemetryCollectionManagerPluginSetup } from 'src/plugins/telemetry_collection_manager/server'; -import { - LOGGING_TAG, - KIBANA_MONITORING_LOGGING_TAG, - KIBANA_ALERTING_ENABLED, - KIBANA_STATS_TYPE_MONITORING, -} from '../common/constants'; import { Logger, PluginInitializerContext, @@ -27,7 +21,15 @@ import { CoreStart, IRouter, IClusterClient, -} from '../../../../src/core/server'; + CustomHttpResponseOptions, + ResponseError, +} from 'kibana/server'; +import { + LOGGING_TAG, + KIBANA_MONITORING_LOGGING_TAG, + KIBANA_ALERTING_ENABLED, + KIBANA_STATS_TYPE_MONITORING, +} from '../common/constants'; import { MonitoringConfig } from './config'; // @ts-ignore import { requireUIRoutes } from './routes'; @@ -92,6 +94,16 @@ interface IBulkUploader { // This is used to test the version of kibana const snapshotRegex = /-snapshot/i; +const wrapError = (error: any): CustomHttpResponseOptions<ResponseError> => { + const options = { statusCode: error.statusCode ?? 500 }; + const boom = Boom.isBoom(error) ? error : Boom.boomify(error, options); + return { + body: boom, + headers: boom.output.headers, + statusCode: boom.output.statusCode, + }; +}; + export class Plugin { private readonly initializerContext: PluginInitializerContext; private readonly log: Logger; @@ -369,12 +381,16 @@ export class Plugin { }, }, }; - - const result = await options.handler(legacyRequest); - if (Boom.isBoom(result)) { - return res.customError({ statusCode: result.output.statusCode, body: result }); + try { + const result = await options.handler(legacyRequest); + return res.ok({ body: result }); + } catch (err) { + const statusCode: number = err.output?.statusCode || err.statusCode || err.status; + if (Boom.isBoom(err) || statusCode !== 500) { + return res.customError({ statusCode, body: err }); + } + return res.internalError(wrapError(err)); } - return res.ok({ body: result }); }; const validate: any = get(options, 'config.validate', false); diff --git a/x-pack/plugins/monitoring/server/routes/api/v1/cluster/cluster.js b/x-pack/plugins/monitoring/server/routes/api/v1/cluster/cluster.js index 8d6fe04cdb7bd..240cb84539dbf 100644 --- a/x-pack/plugins/monitoring/server/routes/api/v1/cluster/cluster.js +++ b/x-pack/plugins/monitoring/server/routes/api/v1/cluster/cluster.js @@ -46,9 +46,13 @@ export function clusterRoute(server) { codePaths: req.payload.codePaths, }; - return getClustersFromRequest(req, indexPatterns, options).catch(err => - handleError(err, req) - ); + let clusters = []; + try { + clusters = await getClustersFromRequest(req, indexPatterns, options); + } catch (err) { + throw handleError(err, req); + } + return clusters; }, }); } diff --git a/x-pack/plugins/monitoring/server/telemetry_collection/get_all_stats.test.ts b/x-pack/plugins/monitoring/server/telemetry_collection/get_all_stats.test.ts index 1a9f2a4da32c2..f0ad6399c6c72 100644 --- a/x-pack/plugins/monitoring/server/telemetry_collection/get_all_stats.test.ts +++ b/x-pack/plugins/monitoring/server/telemetry_collection/get_all_stats.test.ts @@ -182,7 +182,6 @@ describe('get_all_stats', () => { }, { logger: coreMock.createPluginInitializerContext().logger.get('test'), - isDev: true, version: 'version', maxBucketSize: 1, } @@ -208,7 +207,6 @@ describe('get_all_stats', () => { }, { logger: coreMock.createPluginInitializerContext().logger.get('test'), - isDev: true, version: 'version', maxBucketSize: 1, } diff --git a/x-pack/plugins/oss_telemetry/server/lib/tasks/index.ts b/x-pack/plugins/oss_telemetry/server/lib/tasks/index.ts index be08338807dd0..415aeb2791d9e 100644 --- a/x-pack/plugins/oss_telemetry/server/lib/tasks/index.ts +++ b/x-pack/plugins/oss_telemetry/server/lib/tasks/index.ts @@ -17,12 +17,12 @@ import { export function registerTasks({ taskManager, logger, - elasticsearch, + getStartServices, config, }: { taskManager?: TaskManagerSetupContract; logger: Logger; - elasticsearch: CoreSetup['elasticsearch']; + getStartServices: CoreSetup['getStartServices']; config: Observable<{ kibana: { index: string } }>; }) { if (!taskManager) { @@ -30,13 +30,18 @@ export function registerTasks({ return; } + const esClientPromise = getStartServices().then( + ([{ elasticsearch }]) => elasticsearch.legacy.client + ); + taskManager.registerTaskDefinitions({ [VIS_TELEMETRY_TASK]: { title: 'X-Pack telemetry calculator for Visualizations', type: VIS_TELEMETRY_TASK, createTaskRunner({ taskInstance }: { taskInstance: TaskInstance }) { return { - run: visualizationsTaskRunner(taskInstance, config, elasticsearch), + run: visualizationsTaskRunner(taskInstance, config, esClientPromise), + cancel: async () => {}, }; }, }, diff --git a/x-pack/plugins/oss_telemetry/server/lib/tasks/visualizations/task_runner.test.ts b/x-pack/plugins/oss_telemetry/server/lib/tasks/visualizations/task_runner.test.ts index dff5e24db3c6e..6a47983a6f4d9 100644 --- a/x-pack/plugins/oss_telemetry/server/lib/tasks/visualizations/task_runner.test.ts +++ b/x-pack/plugins/oss_telemetry/server/lib/tasks/visualizations/task_runner.test.ts @@ -4,7 +4,6 @@ * you may not use this file except in compliance with the Elastic License. */ -import moment from 'moment'; import { getMockCallWithInternal, getMockConfig, @@ -13,6 +12,7 @@ import { } from '../../../test_utils'; import { visualizationsTaskRunner } from './task_runner'; import { TaskInstance } from '../../../../../task_manager/server'; +import { getNextMidnight } from '../../get_next_midnight'; describe('visualizationsTaskRunner', () => { let mockTaskInstance: TaskInstance; @@ -41,12 +41,6 @@ describe('visualizationsTaskRunner', () => { }); test('Summarizes visualization response data', async () => { - const getNextMidnight = () => - moment() - .add(1, 'days') - .startOf('day') - .toDate(); - const runner = visualizationsTaskRunner(mockTaskInstance, getMockConfig(), getMockEs()); const result = await runner(); diff --git a/x-pack/plugins/oss_telemetry/server/lib/tasks/visualizations/task_runner.ts b/x-pack/plugins/oss_telemetry/server/lib/tasks/visualizations/task_runner.ts index 556dc465e562d..f60c44e548f3f 100644 --- a/x-pack/plugins/oss_telemetry/server/lib/tasks/visualizations/task_runner.ts +++ b/x-pack/plugins/oss_telemetry/server/lib/tasks/visualizations/task_runner.ts @@ -6,7 +6,7 @@ import { Observable } from 'rxjs'; import _, { countBy, groupBy, mapValues } from 'lodash'; -import { APICaller, CoreSetup } from 'kibana/server'; +import { APICaller, IClusterClient } from 'src/core/server'; import { getNextMidnight } from '../../get_next_midnight'; import { TaskInstance } from '../../../../../task_manager/server'; import { ESSearchHit } from '../../../../../apm/typings/elasticsearch'; @@ -73,17 +73,15 @@ async function getStats(callCluster: APICaller, index: string) { export function visualizationsTaskRunner( taskInstance: TaskInstance, config: Observable<{ kibana: { index: string } }>, - es: CoreSetup['elasticsearch'] + esClientPromise: Promise<IClusterClient> ) { - const { callAsInternalUser: callCluster } = es.createClient('data'); - return async () => { let stats; let error; try { const index = (await config.toPromise()).kibana.index; - stats = await getStats(callCluster, index); + stats = await getStats((await esClientPromise).callAsInternalUser, index); } catch (err) { if (err.constructor === Error) { error = err.message; diff --git a/x-pack/plugins/oss_telemetry/server/plugin.ts b/x-pack/plugins/oss_telemetry/server/plugin.ts index 430fca2d39837..6a447da66952a 100644 --- a/x-pack/plugins/oss_telemetry/server/plugin.ts +++ b/x-pack/plugins/oss_telemetry/server/plugin.ts @@ -35,7 +35,7 @@ export class OssTelemetryPlugin implements Plugin { registerTasks({ taskManager: deps.taskManager, logger: this.logger, - elasticsearch: core.elasticsearch, + getStartServices: core.getStartServices, config: this.config, }); registerCollectors( diff --git a/x-pack/plugins/oss_telemetry/server/test_utils/index.ts b/x-pack/plugins/oss_telemetry/server/test_utils/index.ts index fc8c98c164623..4d703db3dcc64 100644 --- a/x-pack/plugins/oss_telemetry/server/test_utils/index.ts +++ b/x-pack/plugins/oss_telemetry/server/test_utils/index.ts @@ -4,9 +4,10 @@ * you may not use this file except in compliance with the Elastic License. */ -import { APICaller, CoreSetup } from 'kibana/server'; +import { APICaller } from 'kibana/server'; import { of } from 'rxjs'; +import { elasticsearchServiceMock } from '../../../../../src/core/server/mocks'; import { ConcreteTaskInstance, TaskStatus, @@ -43,10 +44,11 @@ const defaultMockSavedObjects = [ const defaultMockTaskDocs = [getMockTaskInstance()]; -export const getMockEs = (mockCallWithInternal: APICaller = getMockCallWithInternal()) => - (({ - createClient: () => ({ callAsInternalUser: mockCallWithInternal }), - } as unknown) as CoreSetup['elasticsearch']); +export const getMockEs = async (mockCallWithInternal: APICaller = getMockCallWithInternal()) => { + const client = elasticsearchServiceMock.createClusterClient(); + (client.callAsInternalUser as any) = mockCallWithInternal; + return client; +}; export const getMockCallWithInternal = (hits: unknown[] = defaultMockSavedObjects): APICaller => { return ((() => { diff --git a/x-pack/plugins/remote_clusters/public/application/services/ui_metric.ts b/x-pack/plugins/remote_clusters/public/application/services/ui_metric.ts index 91354155cacb0..4fc3c438e76d6 100644 --- a/x-pack/plugins/remote_clusters/public/application/services/ui_metric.ts +++ b/x-pack/plugins/remote_clusters/public/application/services/ui_metric.ts @@ -3,17 +3,24 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ - +import { UiStatsMetricType, METRIC_TYPE } from '@kbn/analytics'; import { UsageCollectionSetup } from 'src/plugins/usage_collection/public'; -import { UiStatsMetricType } from '@kbn/analytics'; import { UIM_APP_NAME } from '../constants'; -export let trackUiMetric: (metricType: UiStatsMetricType, eventName: string) => void; -export let METRIC_TYPE: UsageCollectionSetup['METRIC_TYPE']; +export { METRIC_TYPE }; + +export let usageCollection: UsageCollectionSetup | undefined; + +export function init(_usageCollection: UsageCollectionSetup): void { + usageCollection = _usageCollection; +} -export function init(usageCollection: UsageCollectionSetup): void { - trackUiMetric = usageCollection.reportUiStats.bind(usageCollection, UIM_APP_NAME); - METRIC_TYPE = usageCollection.METRIC_TYPE; +export function trackUiMetric(metricType: UiStatsMetricType, name: string) { + if (!usageCollection) { + return; + } + const { reportUiStats } = usageCollection; + reportUiStats(UIM_APP_NAME, metricType, name); } /** diff --git a/x-pack/plugins/remote_clusters/server/plugin.ts b/x-pack/plugins/remote_clusters/server/plugin.ts index b86f16228878a..fca4a5dbc5f94 100644 --- a/x-pack/plugins/remote_clusters/server/plugin.ts +++ b/x-pack/plugins/remote_clusters/server/plugin.ts @@ -29,15 +29,9 @@ export class RemoteClustersServerPlugin implements Plugin<void, void, any, any> this.licenseStatus = { valid: false }; } - async setup( - { http, elasticsearch: elasticsearchService }: CoreSetup, - { licensing, cloud }: Dependencies - ) { - const elasticsearch = await elasticsearchService.adminClient; + async setup({ http }: CoreSetup, { licensing, cloud }: Dependencies) { const router = http.createRouter(); const routeDependencies: RouteDependencies = { - elasticsearch, - elasticsearchService, router, getLicenseStatus: () => this.licenseStatus, config: { diff --git a/x-pack/plugins/remote_clusters/server/types.ts b/x-pack/plugins/remote_clusters/server/types.ts index 85678cba92f19..23f4ed158c2d4 100644 --- a/x-pack/plugins/remote_clusters/server/types.ts +++ b/x-pack/plugins/remote_clusters/server/types.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { IRouter, ElasticsearchServiceSetup, IClusterClient } from 'kibana/server'; +import { IRouter } from 'kibana/server'; import { LicensingPluginSetup } from '../../licensing/server'; import { CloudSetup } from '../../cloud/server'; @@ -16,8 +16,6 @@ export interface Dependencies { export interface RouteDependencies { router: IRouter; getLicenseStatus: () => LicenseStatus; - elasticsearchService: ElasticsearchServiceSetup; - elasticsearch: IClusterClient; config: { isCloudEnabled: boolean; }; diff --git a/x-pack/plugins/reporting/config.ts b/x-pack/plugins/reporting/config.ts deleted file mode 100644 index f1d6b1a8f248f..0000000000000 --- a/x-pack/plugins/reporting/config.ts +++ /dev/null @@ -1,10 +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. - */ - -export const reportingPollConfig = { - jobCompletionNotifier: { interval: 10000, intervalErrorMultiplier: 5 }, - jobsRefresh: { interval: 5000, intervalErrorMultiplier: 5 }, -}; diff --git a/x-pack/plugins/reporting/constants.ts b/x-pack/plugins/reporting/constants.ts index 8f47a0a6b2ac1..5756d29face12 100644 --- a/x-pack/plugins/reporting/constants.ts +++ b/x-pack/plugins/reporting/constants.ts @@ -24,6 +24,7 @@ export const REPORTING_MANAGEMENT_HOME = '/app/kibana#/management/kibana/reporti // Statuses export const JOB_STATUS_FAILED = 'failed'; export const JOB_STATUS_COMPLETED = 'completed'; +export const JOB_STATUS_WARNINGS = 'completed_with_warnings'; export enum JobStatuses { PENDING = 'pending', @@ -31,6 +32,7 @@ export enum JobStatuses { COMPLETED = 'completed', FAILED = 'failed', CANCELLED = 'cancelled', + WARNINGS = 'completed_with_warnings', } // Types diff --git a/x-pack/plugins/reporting/index.d.ts b/x-pack/plugins/reporting/index.d.ts index 7c1a2ebd7d9de..26d661e29bd94 100644 --- a/x-pack/plugins/reporting/index.d.ts +++ b/x-pack/plugins/reporting/index.d.ts @@ -14,7 +14,12 @@ import { } from '../../../src/core/public'; export type JobId = string; -export type JobStatus = 'completed' | 'pending' | 'processing' | 'failed'; +export type JobStatus = + | 'completed' + | 'completed_with_warnings' + | 'pending' + | 'processing' + | 'failed'; export type HttpService = HttpSetup; export type NotificationsService = NotificationsStart; diff --git a/x-pack/plugins/reporting/kibana.json b/x-pack/plugins/reporting/kibana.json index 3da2d2a094706..d068711b87c9d 100644 --- a/x-pack/plugins/reporting/kibana.json +++ b/x-pack/plugins/reporting/kibana.json @@ -2,6 +2,9 @@ "id": "reporting", "version": "8.0.0", "kibanaVersion": "kibana", + "optionalPlugins": [ + "usageCollection" + ], "configPath": ["xpack", "reporting"], "requiredPlugins": [ "home", @@ -12,6 +15,6 @@ "share", "kibanaLegacy" ], - "server": false, + "server": true, "ui": true } diff --git a/x-pack/plugins/reporting/public/components/buttons/report_download_button.tsx b/x-pack/plugins/reporting/public/components/buttons/report_download_button.tsx index b0674c149609d..6c13264ebcb1f 100644 --- a/x-pack/plugins/reporting/public/components/buttons/report_download_button.tsx +++ b/x-pack/plugins/reporting/public/components/buttons/report_download_button.tsx @@ -14,7 +14,7 @@ type Props = { record: ListingJob } & ListingProps; export const ReportDownloadButton: FunctionComponent<Props> = (props: Props) => { const { record, apiClient, intl } = props; - if (record.status !== JobStatuses.COMPLETED) { + if (record.status !== JobStatuses.COMPLETED && record.status !== JobStatuses.WARNINGS) { return null; } diff --git a/x-pack/plugins/reporting/public/components/report_listing.test.tsx b/x-pack/plugins/reporting/public/components/report_listing.test.tsx index 9b541261a690b..380a3b3295b9f 100644 --- a/x-pack/plugins/reporting/public/components/report_listing.test.tsx +++ b/x-pack/plugins/reporting/public/components/report_listing.test.tsx @@ -17,17 +17,16 @@ import { ReportListing } from './report_listing'; const reportingAPIClient = { list: () => Promise.resolve([ - { _index: '.reporting-2019.08.18', _id: 'jzoik8dh1q2i89fb5f19znm6', _source: { payload: { layout: { id: 'preserve_layout', dimensions: { width: 1635, height: 792 } }, type: 'dashboard', title: 'Names', }, max_attempts: 3, browser_type: 'chromium', created_at: '2019-08-23T19:34:24.869Z', jobtype: 'printable_pdf', created_by: 'elastic', attempts: 0, status: 'pending', }, }, // prettier-ignore - { _index: '.reporting-2019.08.18', _id: 'jzoik7tn1q2i89fb5f60e5ve', _score: null, _source: { payload: { layout: { id: 'preserve_layout', dimensions: { width: 1635, height: 792 } }, type: 'dashboard', title: 'Names', }, max_attempts: 3, browser_type: 'chromium', created_at: '2019-08-23T19:34:24.155Z', jobtype: 'printable_pdf', created_by: 'elastic', attempts: 0, status: 'pending', }, }, // prettier-ignore - { _index: '.reporting-2019.08.18', _id: 'jzoik5tb1q2i89fb5fckchny', _score: null, _source: { payload: { layout: { id: 'png', dimensions: { width: 1898, height: 876 } }, title: 'cool dashboard', type: 'dashboard', }, max_attempts: 3, browser_type: 'chromium', created_at: '2019-08-23T19:34:21.551Z', jobtype: 'PNG', created_by: 'elastic', attempts: 0, status: 'pending', }, }, // prettier-ignore - { _index: '.reporting-2019.08.18', _id: 'jzoik5a11q2i89fb5f130t2m', _score: null, _source: { payload: { layout: { id: 'png', dimensions: { width: 1898, height: 876 } }, title: 'cool dashboard', type: 'dashboard', }, max_attempts: 3, browser_type: 'chromium', created_at: '2019-08-23T19:34:20.857Z', jobtype: 'PNG', created_by: 'elastic', attempts: 0, status: 'pending', }, }, // prettier-ignore - { _index: '.reporting-2019.08.18', _id: 'jzoik3ka1q2i89fb5fdx93g7', _score: null, _source: { payload: { layout: { id: 'preserve_layout', dimensions: { width: 1898, height: 876 } }, type: 'dashboard', title: 'cool dashboard', }, max_attempts: 3, browser_type: 'chromium', created_at: '2019-08-23T19:34:18.634Z', jobtype: 'printable_pdf', created_by: 'elastic', attempts: 0, status: 'pending', }, }, // prettier-ignore - { _index: '.reporting-2019.08.18', _id: 'jzoik2vt1q2i89fb5ffw723n', _score: null, _source: { payload: { layout: { id: 'preserve_layout', dimensions: { width: 1898, height: 876 } }, type: 'dashboard', title: 'cool dashboard', }, max_attempts: 3, browser_type: 'chromium', created_at: '2019-08-23T19:34:17.753Z', jobtype: 'printable_pdf', created_by: 'elastic', attempts: 0, status: 'pending', }, }, // prettier-ignore - { _index: '.reporting-2019.08.18', _id: 'jzoik1851q2i89fb5fdge6e7', _score: null, _source: { payload: { layout: { id: 'preserve_layout', dimensions: { width: 1080, height: 720 } }, type: 'canvas workpad', title: 'My Canvas Workpad - Dark', }, max_attempts: 3, browser_type: 'chromium', created_at: '2019-08-23T19:34:15.605Z', jobtype: 'printable_pdf', created_by: 'elastic', attempts: 0, status: 'pending', }, }, // prettier-ignore - { _index: '.reporting-2019.08.18', _id: 'jzoijyre1q2i89fb5fa7xzvi', _score: null, _source: { payload: { type: 'dashboard', title: 'tests-panels', }, max_attempts: 3, browser_type: 'chromium', created_at: '2019-08-23T19:34:12.410Z', jobtype: 'printable_pdf', created_by: 'elastic', attempts: 0, status: 'pending', }, }, // prettier-ignore - { _index: '.reporting-2019.08.18', _id: 'jzoijv5h1q2i89fb5ffklnhx', _score: null, _source: { payload: { type: 'dashboard', title: 'tests-panels', }, max_attempts: 3, browser_type: 'chromium', created_at: '2019-08-23T19:34:07.733Z', jobtype: 'printable_pdf', created_by: 'elastic', attempts: 0, status: 'pending', }, }, // prettier-ignore - { _index: '.reporting-2019.08.18', _id: 'jznhgk7r1bx789fb5f6hxok7', _score: null, _source: { kibana_name: 'spicy.local', browser_type: 'chromium', created_at: '2019-08-23T02:15:47.799Z', jobtype: 'printable_pdf', created_by: 'elastic', kibana_id: 'ca75e26c-2b7d-464f-aef0-babb67c735a0', output: { content_type: 'application/pdf', size: 877114 }, completed_at: '2019-08-23T02:15:57.707Z', payload: { type: 'dashboard (legacy)', title: 'tests-panels', }, max_attempts: 3, started_at: '2019-08-23T02:15:48.794Z', attempts: 1, status: 'completed', }, }, // prettier-ignore - ]), + { _id: 'k90e51pk1ieucbae0c3t8wo2', _index: '.reporting-2020.04.12', _score: null, _source: { attempts: 0, browser_type: 'chromium', created_at: '2020-04-14T21:01:13.064Z', created_by: 'elastic', jobtype: 'printable_pdf', max_attempts: 1, meta: { layout: 'preserve_layout', objectType: 'canvas workpad', }, payload: { basePath: '/kbn', browserTimezone: 'America/Phoenix', forceNow: '2020-04-14T21:01:13.062Z', layout: { dimensions: { height: 720, width: 1080, }, id: 'preserve_layout', }, objectType: 'canvas workpad', relativeUrls: [ '/s/hsyjklk/app/canvas#/export/workpad/pdf/workpad-53d38306-eda4-410f-b5e7-efbeca1a8c63/page/1', ], title: 'My Canvas Workpad', }, priority: 10, process_expiration: '1970-01-01T00:00:00.000Z', status: 'pending', timeout: 300000, }, sort: [1586898073064], }, // prettier-ignore + { _id: 'k90e51pk1ieucbae0c3t8wo1', _index: '.reporting-2020.04.12', _score: null, _source: { attempts: 1, browser_type: 'chromium', created_at: '2020-04-14T21:01:13.064Z', created_by: 'elastic', jobtype: 'printable_pdf', kibana_id: '5b2de169-2785-441b-ae8c-186a1936b17d', kibana_name: 'spicy.local', max_attempts: 1, meta: { layout: 'preserve_layout', objectType: 'canvas workpad', }, payload: { basePath: '/kbn', browserTimezone: 'America/Phoenix', forceNow: '2020-04-14T21:01:13.062Z', layout: { dimensions: { height: 720, width: 1080, }, id: 'preserve_layout', }, objectType: 'canvas workpad', relativeUrls: [ '/s/hsyjklk/app/canvas#/export/workpad/pdf/workpad-53d38306-eda4-410f-b5e7-efbeca1a8c63/page/1', ], title: 'My Canvas Workpad', }, priority: 10, process_expiration: '2020-04-14T21:06:14.526Z', started_at: '2020-04-14T21:01:14.526Z', status: 'processing', timeout: 300000, }, sort: [1586898073064], }, + { _id: 'k90cmthd1gv8cbae0c2le8bo', _index: '.reporting-2020.04.12', _score: null, _source: { attempts: 1, browser_type: 'chromium', completed_at: '2020-04-14T20:19:14.748Z', created_at: '2020-04-14T20:19:02.977Z', created_by: 'elastic', jobtype: 'printable_pdf', kibana_id: '5b2de169-2785-441b-ae8c-186a1936b17d', kibana_name: 'spicy.local', max_attempts: 1, meta: { layout: 'preserve_layout', objectType: 'canvas workpad', }, output: { content_type: 'application/pdf', size: 80262, }, payload: { basePath: '/kbn', browserTimezone: 'America/Phoenix', forceNow: '2020-04-14T20:19:02.976Z', layout: { dimensions: { height: 720, width: 1080, }, id: 'preserve_layout', }, objectType: 'canvas workpad', relativeUrls: [ '/s/hsyjklk/app/canvas#/export/workpad/pdf/workpad-53d38306-eda4-410f-b5e7-efbeca1a8c63/page/1', ], title: 'My Canvas Workpad', }, priority: 10, process_expiration: '2020-04-14T20:24:04.073Z', started_at: '2020-04-14T20:19:04.073Z', status: 'completed', timeout: 300000, }, sort: [1586895542977], }, + { _id: 'k906958e1d4wcbae0c9hip1a', _index: '.reporting-2020.04.12', _score: null, _source: { attempts: 1, browser_type: 'chromium', completed_at: '2020-04-14T17:21:08.223Z', created_at: '2020-04-14T17:20:27.326Z', created_by: 'elastic', jobtype: 'printable_pdf', kibana_id: '5b2de169-2785-441b-ae8c-186a1936b17d', kibana_name: 'spicy.local', max_attempts: 1, meta: { layout: 'preserve_layout', objectType: 'canvas workpad', }, output: { content_type: 'application/pdf', size: 49468, warnings: [ 'An error occurred when trying to read the page for visualization panel info. You may need to increase \'xpack.reporting.capture.timeouts.waitForElements\'. TimeoutError: waiting for selector "[data-shared-item],[data-shared-items-count]" failed: timeout 30000ms exceeded', ], }, payload: { basePath: '/kbn', browserTimezone: 'America/Phoenix', forceNow: '2020-04-14T17:20:27.326Z', layout: { dimensions: { height: 720, width: 1080, }, id: 'preserve_layout', }, objectType: 'canvas workpad', relativeUrls: [ '/s/hsyjklk/app/canvas#/export/workpad/pdf/workpad-53d38306-eda4-410f-b5e8-efbeca1a8c63/page/1', ], title: 'My Canvas Workpad', }, priority: 10, process_expiration: '2020-04-14T17:25:29.444Z', started_at: '2020-04-14T17:20:29.444Z', status: 'completed_with_warnings', timeout: 300000, }, sort: [1586884827326], }, + { _id: 'k9067y2a1d4wcbae0cad38n0', _index: '.reporting-2020.04.12', _score: null, _source: { attempts: 1, browser_type: 'chromium', completed_at: '2020-04-14T17:19:53.244Z', created_at: '2020-04-14T17:19:31.379Z', created_by: 'elastic', jobtype: 'printable_pdf', kibana_id: '5b2de169-2785-441b-ae8c-186a1936b17d', kibana_name: 'spicy.local', max_attempts: 1, meta: { layout: 'preserve_layout', objectType: 'canvas workpad', }, output: { content_type: 'application/pdf', size: 80262, }, payload: { basePath: '/kbn', browserTimezone: 'America/Phoenix', forceNow: '2020-04-14T17:19:31.378Z', layout: { dimensions: { height: 720, width: 1080, }, id: 'preserve_layout', }, objectType: 'canvas workpad', relativeUrls: [ '/s/hsyjklk/app/canvas#/export/workpad/pdf/workpad-53d38306-eda4-410f-b5e7-efbeca1a8c63/page/1', ], title: 'My Canvas Workpad', }, priority: 10, process_expiration: '2020-04-14T17:24:39.883Z', started_at: '2020-04-14T17:19:39.883Z', status: 'completed', timeout: 300000, }, sort: [1586884771379], }, + { _id: 'k9067s1m1d4wcbae0cdnvcms', _index: '.reporting-2020.04.12', _score: null, _source: { attempts: 1, browser_type: 'chromium', completed_at: '2020-04-14T17:19:36.822Z', created_at: '2020-04-14T17:19:23.578Z', created_by: 'elastic', jobtype: 'printable_pdf', kibana_id: '5b2de169-2785-441b-ae8c-186a1936b17d', kibana_name: 'spicy.local', max_attempts: 1, meta: { layout: 'preserve_layout', objectType: 'canvas workpad', }, output: { content_type: 'application/pdf', size: 80262, }, payload: { basePath: '/kbn', browserTimezone: 'America/Phoenix', forceNow: '2020-04-14T17:19:23.578Z', layout: { dimensions: { height: 720, width: 1080, }, id: 'preserve_layout', }, objectType: 'canvas workpad', relativeUrls: [ '/s/hsyjklk/app/canvas#/export/workpad/pdf/workpad-53d38306-eda4-410f-b5e7-efbeca1a8c63/page/1', ], title: 'My Canvas Workpad', }, priority: 10, process_expiration: '2020-04-14T17:24:25.247Z', started_at: '2020-04-14T17:19:25.247Z', status: 'completed', timeout: 300000, }, sort: [1586884763578], }, + { _id: 'k9065q3s1d4wcbae0c00fxlh', _index: '.reporting-2020.04.12', _score: null, _source: { attempts: 1, browser_type: 'chromium', completed_at: '2020-04-14T17:18:03.910Z', created_at: '2020-04-14T17:17:47.752Z', created_by: 'elastic', jobtype: 'printable_pdf', kibana_id: '5b2de169-2785-441b-ae8c-186a1936b17d', kibana_name: 'spicy.local', max_attempts: 1, meta: { layout: 'preserve_layout', objectType: 'canvas workpad', }, output: { content_type: 'application/pdf', size: 80262, }, payload: { basePath: '/kbn', browserTimezone: 'America/Phoenix', forceNow: '2020-04-14T17:17:47.750Z', layout: { dimensions: { height: 720, width: 1080, }, id: 'preserve_layout', }, objectType: 'canvas workpad', relativeUrls: [ '/s/hsyjklk/app/canvas#/export/workpad/pdf/workpad-53d38306-eda4-410f-b5e7-efbeca1a8c63/page/1', ], title: 'My Canvas Workpad', }, priority: 10, process_expiration: '2020-04-14T17:22:50.379Z', started_at: '2020-04-14T17:17:50.379Z', status: 'completed', timeout: 300000, }, sort: [1586884667752], }, + { _id: 'k905zdw11d34cbae0c3y6tzh', _index: '.reporting-2020.04.12', _score: null, _source: { attempts: 1, browser_type: 'chromium', completed_at: '2020-04-14T17:13:03.719Z', created_at: '2020-04-14T17:12:51.985Z', created_by: 'elastic', jobtype: 'printable_pdf', kibana_id: '5b2de169-2785-441b-ae8c-186a1936b17d', kibana_name: 'spicy.local', max_attempts: 1, meta: { layout: 'preserve_layout', objectType: 'canvas workpad', }, output: { content_type: 'application/pdf', size: 80262, }, payload: { basePath: '/kbn', browserTimezone: 'America/Phoenix', forceNow: '2020-04-14T17:12:51.984Z', layout: { dimensions: { height: 720, width: 1080, }, id: 'preserve_layout', }, objectType: 'canvas workpad', relativeUrls: [ '/s/hsyjklk/app/canvas#/export/workpad/pdf/workpad-53d38306-eda4-410f-b5e7-efbeca1a8c63/page/1', ], title: 'My Canvas Workpad', }, priority: 10, process_expiration: '2020-04-14T17:17:52.431Z', started_at: '2020-04-14T17:12:52.431Z', status: 'completed', timeout: 300000, }, sort: [1586884371985], }, + { _id: 'k8t4ylcb07mi9d006214ifyg', _index: '.reporting-2020.04.05', _score: null, _source: { attempts: 1, browser_type: 'chromium', completed_at: '2020-04-09T19:10:10.049Z', created_at: '2020-04-09T19:09:52.139Z', created_by: 'elastic', jobtype: 'PNG', kibana_id: 'f2e59b4e-f79b-4a48-8a7d-6d50a3c1d914', kibana_name: 'spicy.local', max_attempts: 1, meta: { layout: 'png', objectType: 'visualization', }, output: { content_type: 'image/png', }, payload: { basePath: '/kbn', browserTimezone: 'America/Phoenix', forceNow: '2020-04-09T19:09:52.137Z', layout: { dimensions: { height: 1575, width: 1423, }, id: 'png', }, objectType: 'visualization', relativeUrl: "/s/hsyjklk/app/kibana#/visualize/edit/94d1fe40-7a94-11ea-b373-0749f92ad295?_a=(filters:!(),linked:!f,query:(language:kuery,query:''),uiState:(),vis:(aggs:!((enabled:!t,id:'1',params:(),schema:metric,type:count)),params:(addLegend:!f,addTooltip:!t,metric:(colorSchema:'Green%20to%20Red',colorsRange:!((from:0,to:10000)),invertColors:!f,labels:(show:!t),metricColorMode:None,percentageMode:!f,style:(bgColor:!f,bgFill:%23000,fontSize:60,labelColor:!f,subText:''),useRanges:!f),type:metric),title:count,type:metric))&_g=(filters:!(),refreshInterval:(pause:!t,value:0),time:(from:now-15y,to:now))&indexPattern=d81752b0-7434-11ea-be36-1f978cda44d4&type=metric", title: 'count', }, priority: 10, process_expiration: '2020-04-09T19:14:54.570Z', started_at: '2020-04-09T19:09:54.570Z', status: 'completed', timeout: 300000, }, sort: [1586459392139], }, + ]), // prettier-ignore total: () => Promise.resolve(18), } as any; diff --git a/x-pack/plugins/reporting/public/components/report_listing.tsx b/x-pack/plugins/reporting/public/components/report_listing.tsx index 93dd293876f82..885e9577471a0 100644 --- a/x-pack/plugins/reporting/public/components/report_listing.tsx +++ b/x-pack/plugins/reporting/public/components/report_listing.tsx @@ -87,6 +87,12 @@ const jobStatusLabelsMap = new Map<JobStatuses, string>([ defaultMessage: 'Completed', }), ], + [ + JobStatuses.WARNINGS, + i18n.translate('xpack.reporting.jobStatuses.warningText', { + defaultMessage: 'Completed with warnings', + }), + ], [ JobStatuses.FAILED, i18n.translate('xpack.reporting.jobStatuses.failedText', { @@ -410,7 +416,11 @@ class ReportListingUi extends Component<Props, State> { statusTimestamp = this.formatDate(record.started_at); } else if ( record.completed_at && - (status === JobStatuses.COMPLETED || status === JobStatuses.FAILED) + ([ + JobStatuses.COMPLETED, + JobStatuses.FAILED, + JobStatuses.WARNINGS, + ] as string[]).includes(status) ) { statusTimestamp = this.formatDate(record.completed_at); } diff --git a/x-pack/plugins/reporting/public/lib/stream_handler.ts b/x-pack/plugins/reporting/public/lib/stream_handler.ts index 1aae30f6fdfb0..3c121f1712685 100644 --- a/x-pack/plugins/reporting/public/lib/stream_handler.ts +++ b/x-pack/plugins/reporting/public/lib/stream_handler.ts @@ -11,6 +11,7 @@ import { JOB_COMPLETION_NOTIFICATIONS_SESSION_KEY, JOB_STATUS_COMPLETED, JOB_STATUS_FAILED, + JOB_STATUS_WARNINGS, } from '../../constants'; import { @@ -112,7 +113,7 @@ export class ReportingNotifierStreamHandler { _source: { status: jobStatus }, } = job; if (storedJobs.includes(jobId)) { - if (jobStatus === JOB_STATUS_COMPLETED) { + if (jobStatus === JOB_STATUS_COMPLETED || jobStatus === JOB_STATUS_WARNINGS) { completedJobs.push(summarizeJob(job)); } else if (jobStatus === JOB_STATUS_FAILED) { failedJobs.push(summarizeJob(job)); diff --git a/x-pack/plugins/reporting/server/config/create_config.test.ts b/x-pack/plugins/reporting/server/config/create_config.test.ts new file mode 100644 index 0000000000000..3107866be6496 --- /dev/null +++ b/x-pack/plugins/reporting/server/config/create_config.test.ts @@ -0,0 +1,165 @@ +/* + * 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 Rx from 'rxjs'; +import { CoreSetup, Logger, PluginInitializerContext } from 'src/core/server'; +import { ConfigType as ReportingConfigType } from './schema'; +import { createConfig$ } from './create_config'; + +interface KibanaServer { + host?: string; + port?: number; + protocol?: string; +} + +const makeMockInitContext = (config: { + capture?: Partial<ReportingConfigType['capture']>; + encryptionKey?: string; + kibanaServer: Partial<ReportingConfigType['kibanaServer']>; +}): PluginInitializerContext => + ({ + config: { + create: () => + Rx.of({ + ...config, + capture: config.capture || { browser: { chromium: { disableSandbox: false } } }, + kibanaServer: config.kibanaServer || {}, + }), + }, + } as PluginInitializerContext); + +const makeMockCoreSetup = (serverInfo: KibanaServer): CoreSetup => + ({ http: { getServerInfo: () => serverInfo } } as any); + +describe('Reporting server createConfig$', () => { + let mockCoreSetup: CoreSetup; + let mockInitContext: PluginInitializerContext; + let mockLogger: Logger; + + beforeEach(() => { + mockCoreSetup = makeMockCoreSetup({ host: 'kibanaHost', port: 5601, protocol: 'http' }); + mockInitContext = makeMockInitContext({ + kibanaServer: {}, + }); + mockLogger = ({ warn: jest.fn(), debug: jest.fn() } as unknown) as Logger; + }); + + afterEach(() => { + jest.resetAllMocks(); + }); + + it('creates random encryption key and default config using host, protocol, and port from server info', async () => { + const result = await createConfig$(mockCoreSetup, mockInitContext, mockLogger).toPromise(); + + expect(result.encryptionKey).toMatch(/\S{32,}/); // random 32 characters + expect(result.kibanaServer).toMatchInlineSnapshot(` + Object { + "hostname": "kibanaHost", + "port": 5601, + "protocol": "http", + } + `); + expect((mockLogger.warn as any).mock.calls.length).toBe(1); + expect((mockLogger.warn as any).mock.calls[0]).toMatchObject([ + 'Generating a random key for xpack.reporting.encryptionKey. To prevent sessions from being invalidated on restart, please set xpack.reporting.encryptionKey in kibana.yml', + ]); + }); + + it('uses the user-provided encryption key', async () => { + mockInitContext = makeMockInitContext({ + encryptionKey: 'iiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiii', + kibanaServer: {}, + }); + const result = await createConfig$(mockCoreSetup, mockInitContext, mockLogger).toPromise(); + + expect(result.encryptionKey).toMatch('iiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiii'); + expect((mockLogger.warn as any).mock.calls.length).toBe(0); + }); + + it('uses the user-provided encryption key, reporting kibanaServer settings to override server info', async () => { + mockInitContext = makeMockInitContext({ + encryptionKey: 'iiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiii', + kibanaServer: { + hostname: 'reportingHost', + port: 5677, + protocol: 'httpsa', + }, + }); + const result = await createConfig$(mockCoreSetup, mockInitContext, mockLogger).toPromise(); + + expect(result).toMatchInlineSnapshot(` + Object { + "capture": Object { + "browser": Object { + "chromium": Object { + "disableSandbox": false, + }, + }, + }, + "encryptionKey": "iiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiii", + "kibanaServer": Object { + "hostname": "reportingHost", + "port": 5677, + "protocol": "httpsa", + }, + } + `); + expect((mockLogger.warn as any).mock.calls.length).toBe(0); + }); + + it('show warning when kibanaServer.hostName === "0"', async () => { + mockInitContext = makeMockInitContext({ + encryptionKey: 'aaaaaaaaaaaaabbbbbbbbbbbbaaaaaaaaa', + kibanaServer: { hostname: '0' }, + }); + const result = await createConfig$(mockCoreSetup, mockInitContext, mockLogger).toPromise(); + + expect(result.kibanaServer).toMatchInlineSnapshot(` + Object { + "hostname": "0.0.0.0", + "port": 5601, + "protocol": "http", + } + `); + expect((mockLogger.warn as any).mock.calls.length).toBe(1); + expect((mockLogger.warn as any).mock.calls[0]).toMatchObject([ + `Found 'server.host: \"0\"' in Kibana configuration. This is incompatible with Reporting. To enable Reporting to work, 'xpack.reporting.kibanaServer.hostname: 0.0.0.0' is being automatically ` + + `to the configuration. You can change the setting to 'server.host: 0.0.0.0' or add 'xpack.reporting.kibanaServer.hostname: 0.0.0.0' in kibana.yml to prevent this message.`, + ]); + }); + + it('uses user-provided disableSandbox: false', async () => { + mockInitContext = makeMockInitContext({ + encryptionKey: '888888888888888888888888888888888', + capture: { browser: { chromium: { disableSandbox: false } } }, + } as ReportingConfigType); + const result = await createConfig$(mockCoreSetup, mockInitContext, mockLogger).toPromise(); + + expect(result.capture.browser.chromium).toMatchObject({ disableSandbox: false }); + expect((mockLogger.warn as any).mock.calls.length).toBe(0); + }); + + it('uses user-provided disableSandbox: true', async () => { + mockInitContext = makeMockInitContext({ + encryptionKey: '888888888888888888888888888888888', + capture: { browser: { chromium: { disableSandbox: true } } }, + } as ReportingConfigType); + const result = await createConfig$(mockCoreSetup, mockInitContext, mockLogger).toPromise(); + + expect(result.capture.browser.chromium).toMatchObject({ disableSandbox: true }); + expect((mockLogger.warn as any).mock.calls.length).toBe(0); + }); + + it('provides a default for disableSandbox', async () => { + mockInitContext = makeMockInitContext({ + encryptionKey: '888888888888888888888888888888888', + } as ReportingConfigType); + const result = await createConfig$(mockCoreSetup, mockInitContext, mockLogger).toPromise(); + + expect(result.capture.browser.chromium).toMatchObject({ disableSandbox: expect.any(Boolean) }); + expect((mockLogger.warn as any).mock.calls.length).toBe(0); + }); +}); diff --git a/x-pack/plugins/reporting/server/config/create_config.ts b/x-pack/plugins/reporting/server/config/create_config.ts new file mode 100644 index 0000000000000..1e6e8bbde5d27 --- /dev/null +++ b/x-pack/plugins/reporting/server/config/create_config.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 { i18n } from '@kbn/i18n/'; +import { TypeOf } from '@kbn/config-schema'; +import crypto from 'crypto'; +import { capitalize } from 'lodash'; +import { map, mergeMap } from 'rxjs/operators'; +import { CoreSetup, Logger, PluginInitializerContext } from 'src/core/server'; +import { getDefaultChromiumSandboxDisabled } from './default_chromium_sandbox_disabled'; +import { ConfigSchema } from './schema'; + +/* + * Set up dynamic config defaults + * - xpack.capture.browser.chromium.disableSandbox + * - xpack.kibanaServer + * - xpack.reporting.encryptionKey + */ +export function createConfig$(core: CoreSetup, context: PluginInitializerContext, logger: Logger) { + return context.config.create<TypeOf<typeof ConfigSchema>>().pipe( + map(config => { + // encryption key + let encryptionKey = config.encryptionKey; + if (encryptionKey === undefined) { + logger.warn( + i18n.translate('xpack.reporting.serverConfig.randomEncryptionKey', { + defaultMessage: + 'Generating a random key for xpack.reporting.encryptionKey. To prevent sessions from being invalidated on ' + + 'restart, please set xpack.reporting.encryptionKey in kibana.yml', + }) + ); + encryptionKey = crypto.randomBytes(16).toString('hex'); + } + const { kibanaServer: reportingServer } = config; + const serverInfo = core.http.getServerInfo(); + // kibanaServer.hostname, default to server.host, don't allow "0" + let kibanaServerHostname = reportingServer.hostname + ? reportingServer.hostname + : serverInfo.host; + if (kibanaServerHostname === '0') { + logger.warn( + i18n.translate('xpack.reporting.serverConfig.invalidServerHostname', { + defaultMessage: + `Found 'server.host: "0"' in Kibana configuration. This is incompatible with Reporting. ` + + `To enable Reporting to work, '{configKey}: 0.0.0.0' is being automatically to the configuration. ` + + `You can change the setting to 'server.host: 0.0.0.0' or add '{configKey}: 0.0.0.0' in kibana.yml to prevent this message.`, + values: { configKey: 'xpack.reporting.kibanaServer.hostname' }, + }) + ); + kibanaServerHostname = '0.0.0.0'; + } + // kibanaServer.port, default to server.port + const kibanaServerPort = reportingServer.port + ? reportingServer.port + : serverInfo.port; // prettier-ignore + // kibanaServer.protocol, default to server.protocol + const kibanaServerProtocol = reportingServer.protocol + ? reportingServer.protocol + : serverInfo.protocol; + return { + ...config, + encryptionKey, + kibanaServer: { + hostname: kibanaServerHostname, + port: kibanaServerPort, + protocol: kibanaServerProtocol, + }, + }; + }), + mergeMap(async config => { + if (config.capture.browser.chromium.disableSandbox != null) { + // disableSandbox was set by user + return config; + } + + // disableSandbox was not set by user, apply default for OS + const { os, disableSandbox } = await getDefaultChromiumSandboxDisabled(); + const osName = [os.os, os.dist, os.release] + .filter(Boolean) + .map(capitalize) + .join(' '); + + logger.debug( + i18n.translate('xpack.reporting.serverConfig.osDetected', { + defaultMessage: `Running on OS: '{osName}'`, + values: { osName }, + }) + ); + + if (disableSandbox === true) { + logger.warn( + i18n.translate('xpack.reporting.serverConfig.autoSet.sandboxDisabled', { + defaultMessage: `Chromium sandbox provides an additional layer of protection, but is not supported for {osName} OS. Automatically setting '{configKey}: true'.`, + values: { + configKey: 'xpack.reporting.capture.browser.chromium.disableSandbox', + osName, + }, + }) + ); + } else { + logger.info( + i18n.translate('xpack.reporting.serverConfig.autoSet.sandboxEnabled', { + defaultMessage: `Chromium sandbox provides an additional layer of protection, and is supported for {osName} OS. Automatically enabling Chromium sandbox.`, + values: { osName }, + }) + ); + } + + return { + ...config, + capture: { + ...config.capture, + browser: { + ...config.capture.browser, + chromium: { ...config.capture.browser.chromium, disableSandbox }, + }, + }, + }; + }) + ); +} diff --git a/x-pack/plugins/reporting/server/config/default_chromium_sandbox_disabled.test.ts b/x-pack/plugins/reporting/server/config/default_chromium_sandbox_disabled.test.ts new file mode 100644 index 0000000000000..307c96bb34909 --- /dev/null +++ b/x-pack/plugins/reporting/server/config/default_chromium_sandbox_disabled.test.ts @@ -0,0 +1,38 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +jest.mock('getos', () => { + return jest.fn(); +}); + +import { getDefaultChromiumSandboxDisabled } from './default_chromium_sandbox_disabled'; +import getos from 'getos'; + +interface TestObject { + os: string; + dist?: string; + release?: string; +} + +function defaultTest(os: TestObject, expectedDefault: boolean) { + test(`${expectedDefault ? 'disabled' : 'enabled'} on ${JSON.stringify(os)}`, async () => { + (getos as jest.Mock).mockImplementation(cb => cb(null, os)); + const actualDefault = await getDefaultChromiumSandboxDisabled(); + expect(actualDefault.disableSandbox).toBe(expectedDefault); + }); +} + +defaultTest({ os: 'win32' }, false); +defaultTest({ os: 'darwin' }, false); +defaultTest({ os: 'linux', dist: 'Centos', release: '7.0' }, true); +defaultTest({ os: 'linux', dist: 'Red Hat Linux', release: '7.0' }, true); +defaultTest({ os: 'linux', dist: 'Ubuntu Linux', release: '14.04' }, false); +defaultTest({ os: 'linux', dist: 'Ubuntu Linux', release: '16.04' }, false); +defaultTest({ os: 'linux', dist: 'SUSE Linux', release: '11' }, false); +defaultTest({ os: 'linux', dist: 'SUSE Linux', release: '12' }, false); +defaultTest({ os: 'linux', dist: 'SUSE Linux', release: '42.0' }, false); +defaultTest({ os: 'linux', dist: 'Debian', release: '8' }, true); +defaultTest({ os: 'linux', dist: 'Debian', release: '9' }, true); diff --git a/x-pack/legacy/plugins/reporting/server/browsers/default_chromium_sandbox_disabled.ts b/x-pack/plugins/reporting/server/config/default_chromium_sandbox_disabled.ts similarity index 80% rename from x-pack/legacy/plugins/reporting/server/browsers/default_chromium_sandbox_disabled.ts rename to x-pack/plugins/reporting/server/config/default_chromium_sandbox_disabled.ts index a21a4b33722ff..525041d5c772b 100644 --- a/x-pack/legacy/plugins/reporting/server/browsers/default_chromium_sandbox_disabled.ts +++ b/x-pack/plugins/reporting/server/config/default_chromium_sandbox_disabled.ts @@ -31,12 +31,17 @@ const distroSupportsUnprivilegedUsernamespaces = (distro: string) => { return true; }; -export async function getDefaultChromiumSandboxDisabled() { +interface OsSummary { + disableSandbox: boolean; + os: { os: string; dist?: string; release?: string }; +} + +export async function getDefaultChromiumSandboxDisabled(): Promise<OsSummary> { const os = await getos(); if (os.os === 'linux' && !distroSupportsUnprivilegedUsernamespaces(os.dist)) { - return true; + return { os, disableSandbox: true }; } else { - return false; + return { os, disableSandbox: false }; } } diff --git a/x-pack/plugins/reporting/server/config/index.ts b/x-pack/plugins/reporting/server/config/index.ts new file mode 100644 index 0000000000000..f0a0a093aa8c0 --- /dev/null +++ b/x-pack/plugins/reporting/server/config/index.ts @@ -0,0 +1,23 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { PluginConfigDescriptor } from 'kibana/server'; +import { ConfigSchema, ConfigType } from './schema'; + +export { createConfig$ } from './create_config'; + +export const config: PluginConfigDescriptor<ConfigType> = { + schema: ConfigSchema, + deprecations: ({ unused }) => [ + unused('capture.browser.chromium.maxScreenshotDimension'), + unused('capture.concurrency'), + unused('capture.settleTime'), + unused('capture.timeout'), + unused('kibanaApp'), + ], +}; + +export { ConfigSchema, ConfigType }; diff --git a/x-pack/plugins/reporting/server/config/schema.test.ts b/x-pack/plugins/reporting/server/config/schema.test.ts new file mode 100644 index 0000000000000..41285c2bfa133 --- /dev/null +++ b/x-pack/plugins/reporting/server/config/schema.test.ts @@ -0,0 +1,134 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { ConfigSchema } from './schema'; + +describe('Reporting Config Schema', () => { + it(`context {"dev":false,"dist":false} produces correct config`, () => { + expect(ConfigSchema.validate({}, { dev: false, dist: false })).toMatchObject({ + capture: { + browser: { + autoDownload: true, + chromium: { proxy: { enabled: false } }, + type: 'chromium', + }, + loadDelay: 3000, + maxAttempts: 1, + networkPolicy: { + enabled: true, + rules: [ + { allow: true, host: undefined, protocol: 'http:' }, + { allow: true, host: undefined, protocol: 'https:' }, + { allow: true, host: undefined, protocol: 'ws:' }, + { allow: true, host: undefined, protocol: 'wss:' }, + { allow: true, host: undefined, protocol: 'data:' }, + { allow: false, host: undefined, protocol: undefined }, + ], + }, + viewport: { height: 1200, width: 1950 }, + zoom: 2, + }, + csv: { + checkForFormulas: true, + enablePanelActionDownload: true, + maxSizeBytes: 10485760, + scroll: { duration: '30s', size: 500 }, + }, + encryptionKey: 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa', + index: '.reporting', + kibanaServer: {}, + poll: { + jobCompletionNotifier: { interval: 10000, intervalErrorMultiplier: 5 }, + jobsRefresh: { interval: 5000, intervalErrorMultiplier: 5 }, + }, + queue: { + indexInterval: 'week', + pollEnabled: true, + pollInterval: 3000, + pollIntervalErrorMultiplier: 10, + timeout: 120000, + }, + roles: { allow: ['reporting_user'] }, + }); + }); + + it(`context {"dev":false,"dist":true} produces correct config`, () => { + expect(ConfigSchema.validate({}, { dev: false, dist: true })).toMatchObject({ + capture: { + browser: { + autoDownload: false, + chromium: { + inspect: false, + proxy: { enabled: false }, + }, + type: 'chromium', + }, + loadDelay: 3000, + maxAttempts: 3, + networkPolicy: { + enabled: true, + rules: [ + { allow: true, host: undefined, protocol: 'http:' }, + { allow: true, host: undefined, protocol: 'https:' }, + { allow: true, host: undefined, protocol: 'ws:' }, + { allow: true, host: undefined, protocol: 'wss:' }, + { allow: true, host: undefined, protocol: 'data:' }, + { allow: false, host: undefined, protocol: undefined }, + ], + }, + viewport: { height: 1200, width: 1950 }, + zoom: 2, + }, + csv: { + checkForFormulas: true, + enablePanelActionDownload: true, + maxSizeBytes: 10485760, + scroll: { duration: '30s', size: 500 }, + }, + index: '.reporting', + kibanaServer: {}, + poll: { + jobCompletionNotifier: { interval: 10000, intervalErrorMultiplier: 5 }, + jobsRefresh: { interval: 5000, intervalErrorMultiplier: 5 }, + }, + queue: { + indexInterval: 'week', + pollEnabled: true, + pollInterval: 3000, + pollIntervalErrorMultiplier: 10, + timeout: 120000, + }, + roles: { allow: ['reporting_user'] }, + }); + }); + + it(`allows optional settings`, () => { + // encryption key + expect( + ConfigSchema.validate({ encryptionKey: 'qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq' }) + .encryptionKey + ).toBe('qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq'); + + // disableSandbox + expect( + ConfigSchema.validate({ capture: { browser: { chromium: { disableSandbox: true } } } }) + .capture.browser.chromium + ).toMatchObject({ disableSandbox: true, proxy: { enabled: false } }); + + // kibanaServer + expect( + ConfigSchema.validate({ kibanaServer: { hostname: 'Frodo' } }).kibanaServer + ).toMatchObject({ hostname: 'Frodo' }); + }); + + it(`logs the proper validation messages`, () => { + // kibanaServer + const throwValidationErr = () => ConfigSchema.validate({ kibanaServer: { hostname: '0' } }); + expect(throwValidationErr).toThrowError( + `[kibanaServer.hostname]: must not be "0" for the headless browser to correctly resolve the host` + ); + }); +}); diff --git a/x-pack/plugins/reporting/server/config/schema.ts b/x-pack/plugins/reporting/server/config/schema.ts new file mode 100644 index 0000000000000..402fddcb5e014 --- /dev/null +++ b/x-pack/plugins/reporting/server/config/schema.ts @@ -0,0 +1,175 @@ +/* + * 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, TypeOf } from '@kbn/config-schema'; +import moment from 'moment'; + +const KibanaServerSchema = schema.object({ + hostname: schema.maybe( + schema.string({ + validate(value) { + if (value === '0') { + return 'must not be "0" for the headless browser to correctly resolve the host'; + } + }, + hostname: true, + }) + ), + port: schema.maybe(schema.number()), + protocol: schema.maybe( + schema.string({ + validate(value) { + if (!/^https?$/.test(value)) { + return 'must be "http" or "https"'; + } + }, + }) + ), +}); // default values are all dynamic in createConfig$ + +const QueueSchema = schema.object({ + indexInterval: schema.string({ defaultValue: 'week' }), + pollEnabled: schema.boolean({ defaultValue: true }), + pollInterval: schema.number({ defaultValue: 3000 }), + pollIntervalErrorMultiplier: schema.number({ defaultValue: 10 }), + timeout: schema.number({ defaultValue: moment.duration(2, 'm').asMilliseconds() }), +}); + +const RulesSchema = schema.object({ + allow: schema.boolean(), + host: schema.maybe(schema.string()), + protocol: schema.maybe(schema.string()), +}); + +const CaptureSchema = schema.object({ + timeouts: schema.object({ + openUrl: schema.number({ defaultValue: 30000 }), + waitForElements: schema.number({ defaultValue: 30000 }), + renderComplete: schema.number({ defaultValue: 30000 }), + }), + networkPolicy: schema.object({ + enabled: schema.boolean({ defaultValue: true }), + rules: schema.arrayOf(RulesSchema, { + defaultValue: [ + { host: undefined, allow: true, protocol: 'http:' }, + { host: undefined, allow: true, protocol: 'https:' }, + { host: undefined, allow: true, protocol: 'ws:' }, + { host: undefined, allow: true, protocol: 'wss:' }, + { host: undefined, allow: true, protocol: 'data:' }, + { host: undefined, allow: false, protocol: undefined }, // Default action is to deny! + ], + }), + }), + zoom: schema.number({ defaultValue: 2 }), + viewport: schema.object({ + width: schema.number({ defaultValue: 1950 }), + height: schema.number({ defaultValue: 1200 }), + }), + loadDelay: schema.number({ + defaultValue: moment.duration(3, 's').asMilliseconds(), + }), // TODO: use schema.duration + browser: schema.object({ + autoDownload: schema.conditional( + schema.contextRef('dist'), + true, + schema.boolean({ defaultValue: false }), + schema.boolean({ defaultValue: true }) + ), + chromium: schema.object({ + inspect: schema.conditional( + schema.contextRef('dist'), + true, + schema.boolean({ defaultValue: false }), + schema.maybe(schema.never()) + ), + disableSandbox: schema.maybe(schema.boolean()), // default value is dynamic in createConfig$ + proxy: schema.object({ + enabled: schema.boolean({ defaultValue: false }), + server: schema.conditional( + schema.siblingRef('enabled'), + true, + schema.uri({ scheme: ['http', 'https'] }), + schema.maybe(schema.never()) + ), + bypass: schema.conditional( + schema.siblingRef('enabled'), + true, + schema.arrayOf(schema.string({ hostname: true })), + schema.maybe(schema.never()) + ), + }), + }), + type: schema.string({ defaultValue: 'chromium' }), + }), + maxAttempts: schema.conditional( + schema.contextRef('dist'), + true, + schema.number({ defaultValue: 3 }), + schema.number({ defaultValue: 1 }) + ), +}); + +const CsvSchema = schema.object({ + checkForFormulas: schema.boolean({ defaultValue: true }), + escapeFormulaValues: schema.boolean({ defaultValue: false }), + enablePanelActionDownload: schema.boolean({ defaultValue: true }), + maxSizeBytes: schema.number({ + defaultValue: 1024 * 1024 * 10, // 10MB + }), // TODO: use schema.byteSize + useByteOrderMarkEncoding: schema.boolean({ defaultValue: false }), + scroll: schema.object({ + duration: schema.string({ + defaultValue: '30s', + validate(value) { + if (!/^[0-9]+(d|h|m|s|ms|micros|nanos)$/.test(value)) { + return 'must be a duration string'; + } + }, + }), + size: schema.number({ defaultValue: 500 }), + }), +}); + +const EncryptionKeySchema = schema.conditional( + schema.contextRef('dist'), + true, + schema.maybe(schema.string({ minLength: 32 })), // default value is dynamic in createConfig$ + schema.string({ minLength: 32, defaultValue: 'a'.repeat(32) }) +); + +const RolesSchema = schema.object({ + allow: schema.arrayOf(schema.string(), { defaultValue: ['reporting_user'] }), +}); + +const IndexSchema = schema.string({ defaultValue: '.reporting' }); + +const PollSchema = schema.object({ + jobCompletionNotifier: schema.object({ + interval: schema.number({ + defaultValue: moment.duration(10, 's').asMilliseconds(), + }), // TODO: use schema.duration + intervalErrorMultiplier: schema.number({ defaultValue: 5 }), + }), + jobsRefresh: schema.object({ + interval: schema.number({ + defaultValue: moment.duration(5, 's').asMilliseconds(), + }), // TODO: use schema.duration + intervalErrorMultiplier: schema.number({ defaultValue: 5 }), + }), +}); + +export const ConfigSchema = schema.object({ + kibanaServer: KibanaServerSchema, + queue: QueueSchema, + capture: CaptureSchema, + csv: CsvSchema, + encryptionKey: EncryptionKeySchema, + roles: RolesSchema, + index: IndexSchema, + poll: PollSchema, +}); + +export type ConfigType = TypeOf<typeof ConfigSchema>; diff --git a/x-pack/plugins/reporting/server/index.ts b/x-pack/plugins/reporting/server/index.ts new file mode 100644 index 0000000000000..2b1844cf2e10e --- /dev/null +++ b/x-pack/plugins/reporting/server/index.ts @@ -0,0 +1,14 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { PluginInitializerContext } from 'src/core/server'; +import { ReportingPlugin } from './plugin'; + +export { config, ConfigSchema } from './config'; +export { ConfigType, PluginsSetup } from './plugin'; + +export const plugin = (initializerContext: PluginInitializerContext) => + new ReportingPlugin(initializerContext); diff --git a/x-pack/plugins/reporting/server/plugin.ts b/x-pack/plugins/reporting/server/plugin.ts new file mode 100644 index 0000000000000..905ed2b237c86 --- /dev/null +++ b/x-pack/plugins/reporting/server/plugin.ts @@ -0,0 +1,38 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { Observable } from 'rxjs'; +import { first } from 'rxjs/operators'; +import { CoreSetup, Logger, Plugin, PluginInitializerContext } from 'src/core/server'; +import { ConfigType, createConfig$ } from './config'; + +export interface PluginsSetup { + /** @deprecated */ + __legacy: { + config$: Observable<ConfigType>; + }; +} + +export class ReportingPlugin implements Plugin<PluginsSetup> { + private readonly log: Logger; + + constructor(private readonly initializerContext: PluginInitializerContext) { + this.log = this.initializerContext.logger.get(); + } + + public async setup(core: CoreSetup): Promise<PluginsSetup> { + return { + __legacy: { + config$: createConfig$(core, this.initializerContext, this.log).pipe(first()), + }, + }; + } + + public start() {} + public stop() {} +} + +export { ConfigType }; diff --git a/x-pack/plugins/rollup/common/index.ts b/x-pack/plugins/rollup/common/index.ts new file mode 100644 index 0000000000000..aeffa3dc3959f --- /dev/null +++ b/x-pack/plugins/rollup/common/index.ts @@ -0,0 +1,27 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export const CONFIG_ROLLUPS = 'rollups:enableIndexPatterns'; + +export const API_BASE_PATH = '/api/rollup'; + +export { + UIM_APP_NAME, + UIM_APP_LOAD, + UIM_JOB_CREATE, + UIM_JOB_DELETE, + UIM_JOB_DELETE_MANY, + UIM_JOB_START, + UIM_JOB_START_MANY, + UIM_JOB_STOP, + UIM_JOB_STOP_MANY, + UIM_SHOW_DETAILS_CLICK, + UIM_DETAIL_PANEL_SUMMARY_TAB_CLICK, + UIM_DETAIL_PANEL_TERMS_TAB_CLICK, + UIM_DETAIL_PANEL_HISTOGRAM_TAB_CLICK, + UIM_DETAIL_PANEL_METRICS_TAB_CLICK, + UIM_DETAIL_PANEL_JSON_TAB_CLICK, +} from './ui_metric'; diff --git a/x-pack/legacy/plugins/rollup/common/ui_metric.ts b/x-pack/plugins/rollup/common/ui_metric.ts similarity index 100% rename from x-pack/legacy/plugins/rollup/common/ui_metric.ts rename to x-pack/plugins/rollup/common/ui_metric.ts diff --git a/x-pack/legacy/plugins/rollup/fixtures/index.js b/x-pack/plugins/rollup/fixtures/index.js similarity index 100% rename from x-pack/legacy/plugins/rollup/fixtures/index.js rename to x-pack/plugins/rollup/fixtures/index.js diff --git a/x-pack/legacy/plugins/rollup/fixtures/job.js b/x-pack/plugins/rollup/fixtures/job.js similarity index 95% rename from x-pack/legacy/plugins/rollup/fixtures/job.js rename to x-pack/plugins/rollup/fixtures/job.js index 3889cc6087d83..310244a5031e7 100644 --- a/x-pack/legacy/plugins/rollup/fixtures/job.js +++ b/x-pack/plugins/rollup/fixtures/job.js @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { getRandomString } from '../../../../test_utils'; +import { getRandomString } from '../../../test_utils'; const initialValues = { dateHistogramField: 'timestamp', diff --git a/x-pack/plugins/rollup/kibana.json b/x-pack/plugins/rollup/kibana.json index 6ab2fc8907c0d..8f832f6c6a345 100644 --- a/x-pack/plugins/rollup/kibana.json +++ b/x-pack/plugins/rollup/kibana.json @@ -2,5 +2,8 @@ "id": "rollup", "version": "8.0.0", "kibanaVersion": "kibana", - "server": true + "server": true, + "ui": true, + "optionalPlugins": ["home", "indexManagement", "indexPatternManagement", "usageCollection"], + "requiredPlugins": ["management", "data"] } diff --git a/x-pack/legacy/plugins/rollup/public/application.tsx b/x-pack/plugins/rollup/public/application.tsx similarity index 87% rename from x-pack/legacy/plugins/rollup/public/application.tsx rename to x-pack/plugins/rollup/public/application.tsx index df17d37bc3465..1bdf940d746b2 100644 --- a/x-pack/legacy/plugins/rollup/public/application.tsx +++ b/x-pack/plugins/rollup/public/application.tsx @@ -5,15 +5,17 @@ */ import React from 'react'; +import { ChromeBreadcrumb, CoreSetup } from 'kibana/public'; import { render, unmountComponentAtNode } from 'react-dom'; import { Provider } from 'react-redux'; -import { KibanaContextProvider } from '../../../../../src/plugins/kibana_react/public'; -import { ChromeBreadcrumb, CoreSetup } from '../../../../../src/core/public'; +import { KibanaContextProvider } from '../../../../src/plugins/kibana_react/public'; // @ts-ignore import { rollupJobsStore } from './crud_app/store'; // @ts-ignore import { App } from './crud_app/app'; +import './index.scss'; + /** * This module will be loaded asynchronously to reduce the bundle size of your plugin's main bundle. */ diff --git a/x-pack/legacy/plugins/rollup/public/crud_app/_crud_app.scss b/x-pack/plugins/rollup/public/crud_app/_crud_app.scss similarity index 100% rename from x-pack/legacy/plugins/rollup/public/crud_app/_crud_app.scss rename to x-pack/plugins/rollup/public/crud_app/_crud_app.scss diff --git a/x-pack/legacy/plugins/rollup/public/crud_app/app.js b/x-pack/plugins/rollup/public/crud_app/app.js similarity index 93% rename from x-pack/legacy/plugins/rollup/public/crud_app/app.js rename to x-pack/plugins/rollup/public/crud_app/app.js index da35c8a56f2d2..0ef3253eeb94e 100644 --- a/x-pack/legacy/plugins/rollup/public/crud_app/app.js +++ b/x-pack/plugins/rollup/public/crud_app/app.js @@ -10,7 +10,8 @@ import { HashRouter, Switch, Route, Redirect, withRouter } from 'react-router-do import { UIM_APP_LOAD } from '../../common'; import { CRUD_APP_BASE_PATH } from './constants'; -import { registerRouter, setUserHasLeftApp, trackUiMetric, METRIC_TYPE } from './services'; +import { registerRouter, setUserHasLeftApp, METRIC_TYPE } from './services'; +import { trackUiMetric } from '../kibana_services'; import { JobList, JobCreate } from './sections'; class ShareRouterComponent extends Component { diff --git a/x-pack/legacy/plugins/rollup/public/crud_app/constants/index.js b/x-pack/plugins/rollup/public/crud_app/constants/index.js similarity index 100% rename from x-pack/legacy/plugins/rollup/public/crud_app/constants/index.js rename to x-pack/plugins/rollup/public/crud_app/constants/index.js diff --git a/x-pack/legacy/plugins/rollup/public/crud_app/constants/metrics_config.js b/x-pack/plugins/rollup/public/crud_app/constants/metrics_config.js similarity index 100% rename from x-pack/legacy/plugins/rollup/public/crud_app/constants/metrics_config.js rename to x-pack/plugins/rollup/public/crud_app/constants/metrics_config.js diff --git a/x-pack/legacy/plugins/rollup/public/crud_app/constants/paths.js b/x-pack/plugins/rollup/public/crud_app/constants/paths.js similarity index 100% rename from x-pack/legacy/plugins/rollup/public/crud_app/constants/paths.js rename to x-pack/plugins/rollup/public/crud_app/constants/paths.js diff --git a/x-pack/legacy/plugins/rollup/public/crud_app/sections/components/field_list/field_list.js b/x-pack/plugins/rollup/public/crud_app/sections/components/field_list/field_list.js similarity index 100% rename from x-pack/legacy/plugins/rollup/public/crud_app/sections/components/field_list/field_list.js rename to x-pack/plugins/rollup/public/crud_app/sections/components/field_list/field_list.js diff --git a/x-pack/legacy/plugins/rollup/public/crud_app/sections/components/field_list/index.js b/x-pack/plugins/rollup/public/crud_app/sections/components/field_list/index.js similarity index 100% rename from x-pack/legacy/plugins/rollup/public/crud_app/sections/components/field_list/index.js rename to x-pack/plugins/rollup/public/crud_app/sections/components/field_list/index.js diff --git a/x-pack/legacy/plugins/rollup/public/crud_app/sections/components/index.js b/x-pack/plugins/rollup/public/crud_app/sections/components/index.js similarity index 100% rename from x-pack/legacy/plugins/rollup/public/crud_app/sections/components/index.js rename to x-pack/plugins/rollup/public/crud_app/sections/components/index.js diff --git a/x-pack/legacy/plugins/rollup/public/crud_app/sections/components/job_action_menu/confirm_delete_modal/confirm_delete_modal.js b/x-pack/plugins/rollup/public/crud_app/sections/components/job_action_menu/confirm_delete_modal/confirm_delete_modal.js similarity index 100% rename from x-pack/legacy/plugins/rollup/public/crud_app/sections/components/job_action_menu/confirm_delete_modal/confirm_delete_modal.js rename to x-pack/plugins/rollup/public/crud_app/sections/components/job_action_menu/confirm_delete_modal/confirm_delete_modal.js diff --git a/x-pack/legacy/plugins/rollup/public/crud_app/sections/components/job_action_menu/confirm_delete_modal/index.js b/x-pack/plugins/rollup/public/crud_app/sections/components/job_action_menu/confirm_delete_modal/index.js similarity index 100% rename from x-pack/legacy/plugins/rollup/public/crud_app/sections/components/job_action_menu/confirm_delete_modal/index.js rename to x-pack/plugins/rollup/public/crud_app/sections/components/job_action_menu/confirm_delete_modal/index.js diff --git a/x-pack/legacy/plugins/rollup/public/crud_app/sections/components/job_action_menu/index.js b/x-pack/plugins/rollup/public/crud_app/sections/components/job_action_menu/index.js similarity index 100% rename from x-pack/legacy/plugins/rollup/public/crud_app/sections/components/job_action_menu/index.js rename to x-pack/plugins/rollup/public/crud_app/sections/components/job_action_menu/index.js diff --git a/x-pack/legacy/plugins/rollup/public/crud_app/sections/components/job_action_menu/job_action_menu.container.js b/x-pack/plugins/rollup/public/crud_app/sections/components/job_action_menu/job_action_menu.container.js similarity index 100% rename from x-pack/legacy/plugins/rollup/public/crud_app/sections/components/job_action_menu/job_action_menu.container.js rename to x-pack/plugins/rollup/public/crud_app/sections/components/job_action_menu/job_action_menu.container.js diff --git a/x-pack/legacy/plugins/rollup/public/crud_app/sections/components/job_action_menu/job_action_menu.js b/x-pack/plugins/rollup/public/crud_app/sections/components/job_action_menu/job_action_menu.js similarity index 100% rename from x-pack/legacy/plugins/rollup/public/crud_app/sections/components/job_action_menu/job_action_menu.js rename to x-pack/plugins/rollup/public/crud_app/sections/components/job_action_menu/job_action_menu.js diff --git a/x-pack/legacy/plugins/rollup/public/crud_app/sections/components/job_details/index.js b/x-pack/plugins/rollup/public/crud_app/sections/components/job_details/index.js similarity index 100% rename from x-pack/legacy/plugins/rollup/public/crud_app/sections/components/job_details/index.js rename to x-pack/plugins/rollup/public/crud_app/sections/components/job_details/index.js diff --git a/x-pack/legacy/plugins/rollup/public/crud_app/sections/components/job_details/job_details.js b/x-pack/plugins/rollup/public/crud_app/sections/components/job_details/job_details.js similarity index 100% rename from x-pack/legacy/plugins/rollup/public/crud_app/sections/components/job_details/job_details.js rename to x-pack/plugins/rollup/public/crud_app/sections/components/job_details/job_details.js diff --git a/x-pack/legacy/plugins/rollup/public/crud_app/sections/components/job_details/tabs/index.js b/x-pack/plugins/rollup/public/crud_app/sections/components/job_details/tabs/index.js similarity index 100% rename from x-pack/legacy/plugins/rollup/public/crud_app/sections/components/job_details/tabs/index.js rename to x-pack/plugins/rollup/public/crud_app/sections/components/job_details/tabs/index.js diff --git a/x-pack/legacy/plugins/rollup/public/crud_app/sections/components/job_details/tabs/tab_histogram.js b/x-pack/plugins/rollup/public/crud_app/sections/components/job_details/tabs/tab_histogram.js similarity index 100% rename from x-pack/legacy/plugins/rollup/public/crud_app/sections/components/job_details/tabs/tab_histogram.js rename to x-pack/plugins/rollup/public/crud_app/sections/components/job_details/tabs/tab_histogram.js diff --git a/x-pack/legacy/plugins/rollup/public/crud_app/sections/components/job_details/tabs/tab_json.js b/x-pack/plugins/rollup/public/crud_app/sections/components/job_details/tabs/tab_json.js similarity index 100% rename from x-pack/legacy/plugins/rollup/public/crud_app/sections/components/job_details/tabs/tab_json.js rename to x-pack/plugins/rollup/public/crud_app/sections/components/job_details/tabs/tab_json.js diff --git a/x-pack/legacy/plugins/rollup/public/crud_app/sections/components/job_details/tabs/tab_metrics.js b/x-pack/plugins/rollup/public/crud_app/sections/components/job_details/tabs/tab_metrics.js similarity index 100% rename from x-pack/legacy/plugins/rollup/public/crud_app/sections/components/job_details/tabs/tab_metrics.js rename to x-pack/plugins/rollup/public/crud_app/sections/components/job_details/tabs/tab_metrics.js diff --git a/x-pack/legacy/plugins/rollup/public/crud_app/sections/components/job_details/tabs/tab_request.js b/x-pack/plugins/rollup/public/crud_app/sections/components/job_details/tabs/tab_request.js similarity index 100% rename from x-pack/legacy/plugins/rollup/public/crud_app/sections/components/job_details/tabs/tab_request.js rename to x-pack/plugins/rollup/public/crud_app/sections/components/job_details/tabs/tab_request.js diff --git a/x-pack/legacy/plugins/rollup/public/crud_app/sections/components/job_details/tabs/tab_summary.js b/x-pack/plugins/rollup/public/crud_app/sections/components/job_details/tabs/tab_summary.js similarity index 100% rename from x-pack/legacy/plugins/rollup/public/crud_app/sections/components/job_details/tabs/tab_summary.js rename to x-pack/plugins/rollup/public/crud_app/sections/components/job_details/tabs/tab_summary.js diff --git a/x-pack/legacy/plugins/rollup/public/crud_app/sections/components/job_details/tabs/tab_terms.js b/x-pack/plugins/rollup/public/crud_app/sections/components/job_details/tabs/tab_terms.js similarity index 100% rename from x-pack/legacy/plugins/rollup/public/crud_app/sections/components/job_details/tabs/tab_terms.js rename to x-pack/plugins/rollup/public/crud_app/sections/components/job_details/tabs/tab_terms.js diff --git a/x-pack/legacy/plugins/rollup/public/crud_app/sections/components/job_status/index.js b/x-pack/plugins/rollup/public/crud_app/sections/components/job_status/index.js similarity index 100% rename from x-pack/legacy/plugins/rollup/public/crud_app/sections/components/job_status/index.js rename to x-pack/plugins/rollup/public/crud_app/sections/components/job_status/index.js diff --git a/x-pack/legacy/plugins/rollup/public/crud_app/sections/components/job_status/job_status.js b/x-pack/plugins/rollup/public/crud_app/sections/components/job_status/job_status.js similarity index 100% rename from x-pack/legacy/plugins/rollup/public/crud_app/sections/components/job_status/job_status.js rename to x-pack/plugins/rollup/public/crud_app/sections/components/job_status/job_status.js diff --git a/x-pack/legacy/plugins/rollup/public/crud_app/sections/index.js b/x-pack/plugins/rollup/public/crud_app/sections/index.js similarity index 100% rename from x-pack/legacy/plugins/rollup/public/crud_app/sections/index.js rename to x-pack/plugins/rollup/public/crud_app/sections/index.js diff --git a/x-pack/legacy/plugins/rollup/public/crud_app/sections/job_create/index.js b/x-pack/plugins/rollup/public/crud_app/sections/job_create/index.js similarity index 100% rename from x-pack/legacy/plugins/rollup/public/crud_app/sections/job_create/index.js rename to x-pack/plugins/rollup/public/crud_app/sections/job_create/index.js diff --git a/x-pack/legacy/plugins/rollup/public/crud_app/sections/job_create/job_create.container.js b/x-pack/plugins/rollup/public/crud_app/sections/job_create/job_create.container.js similarity index 100% rename from x-pack/legacy/plugins/rollup/public/crud_app/sections/job_create/job_create.container.js rename to x-pack/plugins/rollup/public/crud_app/sections/job_create/job_create.container.js diff --git a/x-pack/legacy/plugins/rollup/public/crud_app/sections/job_create/job_create.js b/x-pack/plugins/rollup/public/crud_app/sections/job_create/job_create.js similarity index 99% rename from x-pack/legacy/plugins/rollup/public/crud_app/sections/job_create/job_create.js rename to x-pack/plugins/rollup/public/crud_app/sections/job_create/job_create.js index 5379778c77e2f..4458054f30dd1 100644 --- a/x-pack/legacy/plugins/rollup/public/crud_app/sections/job_create/job_create.js +++ b/x-pack/plugins/rollup/public/crud_app/sections/job_create/job_create.js @@ -14,7 +14,7 @@ import first from 'lodash/array/first'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; -import { withKibana } from '../../../../../../../../src/plugins/kibana_react/public/'; +import { withKibana } from '../../../../../../../src/plugins/kibana_react/public'; import { EuiCallOut, diff --git a/x-pack/legacy/plugins/rollup/public/crud_app/sections/job_create/navigation/index.js b/x-pack/plugins/rollup/public/crud_app/sections/job_create/navigation/index.js similarity index 100% rename from x-pack/legacy/plugins/rollup/public/crud_app/sections/job_create/navigation/index.js rename to x-pack/plugins/rollup/public/crud_app/sections/job_create/navigation/index.js diff --git a/x-pack/legacy/plugins/rollup/public/crud_app/sections/job_create/navigation/navigation.js b/x-pack/plugins/rollup/public/crud_app/sections/job_create/navigation/navigation.js similarity index 100% rename from x-pack/legacy/plugins/rollup/public/crud_app/sections/job_create/navigation/navigation.js rename to x-pack/plugins/rollup/public/crud_app/sections/job_create/navigation/navigation.js diff --git a/x-pack/legacy/plugins/rollup/public/crud_app/sections/job_create/steps/components/field_chooser.js b/x-pack/plugins/rollup/public/crud_app/sections/job_create/steps/components/field_chooser.js similarity index 100% rename from x-pack/legacy/plugins/rollup/public/crud_app/sections/job_create/steps/components/field_chooser.js rename to x-pack/plugins/rollup/public/crud_app/sections/job_create/steps/components/field_chooser.js diff --git a/x-pack/legacy/plugins/rollup/public/crud_app/sections/job_create/steps/components/index.js b/x-pack/plugins/rollup/public/crud_app/sections/job_create/steps/components/index.js similarity index 100% rename from x-pack/legacy/plugins/rollup/public/crud_app/sections/job_create/steps/components/index.js rename to x-pack/plugins/rollup/public/crud_app/sections/job_create/steps/components/index.js diff --git a/x-pack/legacy/plugins/rollup/public/crud_app/sections/job_create/steps/components/step_error.js b/x-pack/plugins/rollup/public/crud_app/sections/job_create/steps/components/step_error.js similarity index 100% rename from x-pack/legacy/plugins/rollup/public/crud_app/sections/job_create/steps/components/step_error.js rename to x-pack/plugins/rollup/public/crud_app/sections/job_create/steps/components/step_error.js diff --git a/x-pack/legacy/plugins/rollup/public/crud_app/sections/job_create/steps/index.js b/x-pack/plugins/rollup/public/crud_app/sections/job_create/steps/index.js similarity index 100% rename from x-pack/legacy/plugins/rollup/public/crud_app/sections/job_create/steps/index.js rename to x-pack/plugins/rollup/public/crud_app/sections/job_create/steps/index.js diff --git a/x-pack/legacy/plugins/rollup/public/crud_app/sections/job_create/steps/step_date_histogram.js b/x-pack/plugins/rollup/public/crud_app/sections/job_create/steps/step_date_histogram.js similarity index 99% rename from x-pack/legacy/plugins/rollup/public/crud_app/sections/job_create/steps/step_date_histogram.js rename to x-pack/plugins/rollup/public/crud_app/sections/job_create/steps/step_date_histogram.js index ba65d082c0b4b..c327e51a6b7ee 100644 --- a/x-pack/legacy/plugins/rollup/public/crud_app/sections/job_create/steps/step_date_histogram.js +++ b/x-pack/plugins/rollup/public/crud_app/sections/job_create/steps/step_date_histogram.js @@ -24,7 +24,7 @@ import { EuiTitle, } from '@elastic/eui'; -import { search } from '../../../../../../../../../src/plugins/data/public'; +import { search } from '../../../../../../../../src/plugins/data/public'; const { parseEsInterval } = search.aggs; import { getDateHistogramDetailsUrl, getDateHistogramAggregationUrl } from '../../../services'; diff --git a/x-pack/legacy/plugins/rollup/public/crud_app/sections/job_create/steps/step_histogram.js b/x-pack/plugins/rollup/public/crud_app/sections/job_create/steps/step_histogram.js similarity index 100% rename from x-pack/legacy/plugins/rollup/public/crud_app/sections/job_create/steps/step_histogram.js rename to x-pack/plugins/rollup/public/crud_app/sections/job_create/steps/step_histogram.js diff --git a/x-pack/legacy/plugins/rollup/public/crud_app/sections/job_create/steps/step_logistics.js b/x-pack/plugins/rollup/public/crud_app/sections/job_create/steps/step_logistics.js similarity index 99% rename from x-pack/legacy/plugins/rollup/public/crud_app/sections/job_create/steps/step_logistics.js rename to x-pack/plugins/rollup/public/crud_app/sections/job_create/steps/step_logistics.js index 5462a46bf59b9..0fd76e572b2e5 100644 --- a/x-pack/legacy/plugins/rollup/public/crud_app/sections/job_create/steps/step_logistics.js +++ b/x-pack/plugins/rollup/public/crud_app/sections/job_create/steps/step_logistics.js @@ -24,8 +24,8 @@ import { EuiTitle, } from '@elastic/eui'; -import { CronEditor } from '../../../../../../../../../src/plugins/es_ui_shared/public'; -import { indexPatterns } from '../../../../../../../../../src/plugins/data/public'; +import { CronEditor } from '../../../../../../../../src/plugins/es_ui_shared/public'; +import { indexPatterns } from '../../../../../../../../src/plugins/data/public'; import { indices } from '../../../../shared_imports'; import { getLogisticalDetailsUrl, getCronUrl } from '../../../services'; diff --git a/x-pack/legacy/plugins/rollup/public/crud_app/sections/job_create/steps/step_metrics.js b/x-pack/plugins/rollup/public/crud_app/sections/job_create/steps/step_metrics.js similarity index 100% rename from x-pack/legacy/plugins/rollup/public/crud_app/sections/job_create/steps/step_metrics.js rename to x-pack/plugins/rollup/public/crud_app/sections/job_create/steps/step_metrics.js diff --git a/x-pack/legacy/plugins/rollup/public/crud_app/sections/job_create/steps/step_review.js b/x-pack/plugins/rollup/public/crud_app/sections/job_create/steps/step_review.js similarity index 100% rename from x-pack/legacy/plugins/rollup/public/crud_app/sections/job_create/steps/step_review.js rename to x-pack/plugins/rollup/public/crud_app/sections/job_create/steps/step_review.js diff --git a/x-pack/legacy/plugins/rollup/public/crud_app/sections/job_create/steps/step_terms.js b/x-pack/plugins/rollup/public/crud_app/sections/job_create/steps/step_terms.js similarity index 100% rename from x-pack/legacy/plugins/rollup/public/crud_app/sections/job_create/steps/step_terms.js rename to x-pack/plugins/rollup/public/crud_app/sections/job_create/steps/step_terms.js diff --git a/x-pack/plugins/rollup/public/crud_app/sections/job_create/steps_config/index.js b/x-pack/plugins/rollup/public/crud_app/sections/job_create/steps_config/index.js new file mode 100644 index 0000000000000..4a55c4679c3d8 --- /dev/null +++ b/x-pack/plugins/rollup/public/crud_app/sections/job_create/steps_config/index.js @@ -0,0 +1,188 @@ +/* + * 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 cloneDeep from 'lodash/lang/cloneDeep'; +import get from 'lodash/object/get'; +import pick from 'lodash/object/pick'; + +import { WEEK } from '../../../../../../../../src/plugins/es_ui_shared/public'; + +import { validateId } from './validate_id'; +import { validateIndexPattern } from './validate_index_pattern'; +import { validateRollupIndex } from './validate_rollup_index'; +import { validateRollupCron } from './validate_rollup_cron'; +import { validateRollupPageSize } from './validate_rollup_page_size'; +import { validateRollupDelay } from './validate_rollup_delay'; +import { validateDateHistogramField } from './validate_date_histogram_field'; +import { validateDateHistogramInterval } from './validate_date_histogram_interval'; +import { validateHistogramInterval } from './validate_histogram_interval'; +import { validateMetrics } from './validate_metrics'; + +export const STEP_LOGISTICS = 'STEP_LOGISTICS'; +export const STEP_DATE_HISTOGRAM = 'STEP_DATE_HISTOGRAM'; +export const STEP_TERMS = 'STEP_TERMS'; +export const STEP_HISTOGRAM = 'STEP_HISTOGRAM'; +export const STEP_METRICS = 'STEP_METRICS'; +export const STEP_REVIEW = 'STEP_REVIEW'; + +export const stepIds = [ + STEP_LOGISTICS, + STEP_DATE_HISTOGRAM, + STEP_TERMS, + STEP_HISTOGRAM, + STEP_METRICS, + STEP_REVIEW, +]; + +/** + * Map a specific wizard step to two functions: + * 1. getDefaultFields: (overrides) => object + * 2. fieldValidations + * + * See x-pack/plugins/rollup/public/crud_app/services/jobs.js for more information on override's shape + */ +export const stepIdToStepConfigMap = { + [STEP_LOGISTICS]: { + getDefaultFields: (overrides = {}) => { + // We don't display the simple editor if there are overrides for the rollup's cron + const isAdvancedCronVisible = !!overrides.rollupCron; + + // The best page size boils down to how much memory the user has, e.g. how many buckets should + // be accumulated at one time. 1000 is probably a safe size without being too small. + const rollupPageSize = get(overrides, ['json', 'config', 'page_size'], 1000); + const clonedRollupId = overrides.id || undefined; + const id = overrides.id ? `${overrides.id}-copy` : ''; + + const defaults = { + indexPattern: '', + rollupIndex: '', + // Every week on Saturday, at 00:00:00 + rollupCron: '0 0 0 ? * 7', + simpleRollupCron: '0 0 0 ? * 7', + rollupPageSize, + // Though the API doesn't require a delay, in many real-world cases, servers will go down for + // a few hours as they're being restarted. A delay of 1d would allow them that period to reboot + // and the "expense" is pretty negligible in most cases: 1 day of extra non-rolled-up data. + rollupDelay: '1d', + cronFrequency: WEEK, + fieldToPreferredValueMap: {}, + }; + + return { + ...defaults, + ...pick(overrides, Object.keys(defaults)), + id, + isAdvancedCronVisible, + rollupPageSize, + clonedRollupId, + }; + }, + fieldsValidator: fields => { + const { + id, + indexPattern, + rollupIndex, + rollupCron, + rollupPageSize, + rollupDelay, + clonedRollupId, + } = fields; + return { + id: validateId(id, clonedRollupId), + indexPattern: validateIndexPattern(indexPattern, rollupIndex), + rollupIndex: validateRollupIndex(rollupIndex, indexPattern), + rollupCron: validateRollupCron(rollupCron), + rollupPageSize: validateRollupPageSize(rollupPageSize), + rollupDelay: validateRollupDelay(rollupDelay), + }; + }, + }, + [STEP_DATE_HISTOGRAM]: { + getDefaultFields: (overrides = {}) => { + const defaults = { + dateHistogramField: null, + dateHistogramInterval: null, + dateHistogramTimeZone: 'UTC', + }; + + return { + ...defaults, + ...pick(overrides, Object.keys(defaults)), + }; + }, + fieldsValidator: fields => { + const { dateHistogramField, dateHistogramInterval } = fields; + + return { + dateHistogramField: validateDateHistogramField(dateHistogramField), + dateHistogramInterval: validateDateHistogramInterval(dateHistogramInterval), + }; + }, + }, + [STEP_TERMS]: { + getDefaultFields: (overrides = {}) => { + return { + terms: [], + ...pick(overrides, ['terms']), + }; + }, + }, + [STEP_HISTOGRAM]: { + getDefaultFields: overrides => { + return { + histogram: [], + histogramInterval: undefined, + ...pick(overrides, ['histogram', 'histogramInterval']), + }; + }, + fieldsValidator: fields => { + const { histogram, histogramInterval } = fields; + + return { + histogramInterval: validateHistogramInterval(histogram, histogramInterval), + }; + }, + }, + [STEP_METRICS]: { + getDefaultFields: (overrides = {}) => { + return { + metrics: [], + ...pick(overrides, ['metrics']), + }; + }, + fieldsValidator: fields => { + const { metrics } = fields; + + return { + metrics: validateMetrics(metrics), + }; + }, + }, + [STEP_REVIEW]: { + getDefaultFields: () => ({}), + }, +}; + +export function getAffectedStepsFields(fields, stepsFields) { + const { indexPattern } = fields; + + const affectedStepsFields = cloneDeep(stepsFields); + + // A new index pattern means we have to clear all of the fields which depend upon it. + if (indexPattern) { + affectedStepsFields[STEP_DATE_HISTOGRAM].dateHistogramField = undefined; + affectedStepsFields[STEP_TERMS].terms = []; + affectedStepsFields[STEP_HISTOGRAM].histogram = []; + affectedStepsFields[STEP_METRICS].metrics = []; + } + + return affectedStepsFields; +} + +export function hasErrors(fieldErrors) { + const errorValues = Object.values(fieldErrors); + return errorValues.some(error => error !== undefined); +} diff --git a/x-pack/legacy/plugins/rollup/public/crud_app/sections/job_create/steps_config/validate_date_histogram_field.js b/x-pack/plugins/rollup/public/crud_app/sections/job_create/steps_config/validate_date_histogram_field.js similarity index 100% rename from x-pack/legacy/plugins/rollup/public/crud_app/sections/job_create/steps_config/validate_date_histogram_field.js rename to x-pack/plugins/rollup/public/crud_app/sections/job_create/steps_config/validate_date_histogram_field.js diff --git a/x-pack/legacy/plugins/rollup/public/crud_app/sections/job_create/steps_config/validate_date_histogram_interval.js b/x-pack/plugins/rollup/public/crud_app/sections/job_create/steps_config/validate_date_histogram_interval.js similarity index 95% rename from x-pack/legacy/plugins/rollup/public/crud_app/sections/job_create/steps_config/validate_date_histogram_interval.js rename to x-pack/plugins/rollup/public/crud_app/sections/job_create/steps_config/validate_date_histogram_interval.js index b6c824bc8c553..819c814e373d5 100644 --- a/x-pack/legacy/plugins/rollup/public/crud_app/sections/job_create/steps_config/validate_date_histogram_interval.js +++ b/x-pack/plugins/rollup/public/crud_app/sections/job_create/steps_config/validate_date_histogram_interval.js @@ -6,7 +6,7 @@ import React from 'react'; import { FormattedMessage } from '@kbn/i18n/react'; -import { search } from '../../../../../../../../../src/plugins/data/public'; +import { search } from '../../../../../../../../src/plugins/data/public'; const { InvalidEsIntervalFormatError, InvalidEsCalendarIntervalError, diff --git a/x-pack/legacy/plugins/rollup/public/crud_app/sections/job_create/steps_config/validate_histogram_interval.js b/x-pack/plugins/rollup/public/crud_app/sections/job_create/steps_config/validate_histogram_interval.js similarity index 100% rename from x-pack/legacy/plugins/rollup/public/crud_app/sections/job_create/steps_config/validate_histogram_interval.js rename to x-pack/plugins/rollup/public/crud_app/sections/job_create/steps_config/validate_histogram_interval.js diff --git a/x-pack/legacy/plugins/rollup/public/crud_app/sections/job_create/steps_config/validate_id.js b/x-pack/plugins/rollup/public/crud_app/sections/job_create/steps_config/validate_id.js similarity index 100% rename from x-pack/legacy/plugins/rollup/public/crud_app/sections/job_create/steps_config/validate_id.js rename to x-pack/plugins/rollup/public/crud_app/sections/job_create/steps_config/validate_id.js diff --git a/x-pack/legacy/plugins/rollup/public/crud_app/sections/job_create/steps_config/validate_index_pattern.js b/x-pack/plugins/rollup/public/crud_app/sections/job_create/steps_config/validate_index_pattern.js similarity index 95% rename from x-pack/legacy/plugins/rollup/public/crud_app/sections/job_create/steps_config/validate_index_pattern.js rename to x-pack/plugins/rollup/public/crud_app/sections/job_create/steps_config/validate_index_pattern.js index 206cc325813c0..534fcaa744744 100644 --- a/x-pack/legacy/plugins/rollup/public/crud_app/sections/job_create/steps_config/validate_index_pattern.js +++ b/x-pack/plugins/rollup/public/crud_app/sections/job_create/steps_config/validate_index_pattern.js @@ -7,7 +7,7 @@ import React from 'react'; import { FormattedMessage } from '@kbn/i18n/react'; -import { indexPatterns } from '../../../../../../../../../src/plugins/data/public'; +import { indexPatterns } from '../../../../../../../../src/plugins/data/public'; export function validateIndexPattern(indexPattern, rollupIndex) { if (!indexPattern || !indexPattern.trim()) { return [ diff --git a/x-pack/legacy/plugins/rollup/public/crud_app/sections/job_create/steps_config/validate_metrics.js b/x-pack/plugins/rollup/public/crud_app/sections/job_create/steps_config/validate_metrics.js similarity index 100% rename from x-pack/legacy/plugins/rollup/public/crud_app/sections/job_create/steps_config/validate_metrics.js rename to x-pack/plugins/rollup/public/crud_app/sections/job_create/steps_config/validate_metrics.js diff --git a/x-pack/legacy/plugins/rollup/public/crud_app/sections/job_create/steps_config/validate_rollup_cron.js b/x-pack/plugins/rollup/public/crud_app/sections/job_create/steps_config/validate_rollup_cron.js similarity index 100% rename from x-pack/legacy/plugins/rollup/public/crud_app/sections/job_create/steps_config/validate_rollup_cron.js rename to x-pack/plugins/rollup/public/crud_app/sections/job_create/steps_config/validate_rollup_cron.js diff --git a/x-pack/legacy/plugins/rollup/public/crud_app/sections/job_create/steps_config/validate_rollup_delay.js b/x-pack/plugins/rollup/public/crud_app/sections/job_create/steps_config/validate_rollup_delay.js similarity index 95% rename from x-pack/legacy/plugins/rollup/public/crud_app/sections/job_create/steps_config/validate_rollup_delay.js rename to x-pack/plugins/rollup/public/crud_app/sections/job_create/steps_config/validate_rollup_delay.js index 37c2ca9a1d775..66bfd43bc3d1b 100644 --- a/x-pack/legacy/plugins/rollup/public/crud_app/sections/job_create/steps_config/validate_rollup_delay.js +++ b/x-pack/plugins/rollup/public/crud_app/sections/job_create/steps_config/validate_rollup_delay.js @@ -6,7 +6,7 @@ import React from 'react'; import { FormattedMessage } from '@kbn/i18n/react'; -import { search } from '../../../../../../../../../src/plugins/data/public'; +import { search } from '../../../../../../../../src/plugins/data/public'; const { InvalidEsIntervalFormatError, InvalidEsCalendarIntervalError, diff --git a/x-pack/legacy/plugins/rollup/public/crud_app/sections/job_create/steps_config/validate_rollup_index.js b/x-pack/plugins/rollup/public/crud_app/sections/job_create/steps_config/validate_rollup_index.js similarity index 100% rename from x-pack/legacy/plugins/rollup/public/crud_app/sections/job_create/steps_config/validate_rollup_index.js rename to x-pack/plugins/rollup/public/crud_app/sections/job_create/steps_config/validate_rollup_index.js diff --git a/x-pack/legacy/plugins/rollup/public/crud_app/sections/job_create/steps_config/validate_rollup_page_size.js b/x-pack/plugins/rollup/public/crud_app/sections/job_create/steps_config/validate_rollup_page_size.js similarity index 100% rename from x-pack/legacy/plugins/rollup/public/crud_app/sections/job_create/steps_config/validate_rollup_page_size.js rename to x-pack/plugins/rollup/public/crud_app/sections/job_create/steps_config/validate_rollup_page_size.js diff --git a/x-pack/legacy/plugins/rollup/public/crud_app/sections/job_list/detail_panel/detail_panel.container.js b/x-pack/plugins/rollup/public/crud_app/sections/job_list/detail_panel/detail_panel.container.js similarity index 100% rename from x-pack/legacy/plugins/rollup/public/crud_app/sections/job_list/detail_panel/detail_panel.container.js rename to x-pack/plugins/rollup/public/crud_app/sections/job_list/detail_panel/detail_panel.container.js diff --git a/x-pack/legacy/plugins/rollup/public/crud_app/sections/job_list/detail_panel/detail_panel.js b/x-pack/plugins/rollup/public/crud_app/sections/job_list/detail_panel/detail_panel.js similarity index 98% rename from x-pack/legacy/plugins/rollup/public/crud_app/sections/job_list/detail_panel/detail_panel.js rename to x-pack/plugins/rollup/public/crud_app/sections/job_list/detail_panel/detail_panel.js index f774b1d7f63b7..3f168a66feed8 100644 --- a/x-pack/legacy/plugins/rollup/public/crud_app/sections/job_list/detail_panel/detail_panel.js +++ b/x-pack/plugins/rollup/public/crud_app/sections/job_list/detail_panel/detail_panel.js @@ -34,7 +34,8 @@ import { UIM_DETAIL_PANEL_METRICS_TAB_CLICK, UIM_DETAIL_PANEL_JSON_TAB_CLICK, } from '../../../../../common'; -import { trackUiMetric, METRIC_TYPE } from '../../../services'; +import { METRIC_TYPE } from '../../../services'; +import { trackUiMetric } from '../../../../kibana_services'; import { JobActionMenu, diff --git a/x-pack/legacy/plugins/rollup/public/crud_app/sections/job_list/detail_panel/detail_panel.test.js b/x-pack/plugins/rollup/public/crud_app/sections/job_list/detail_panel/detail_panel.test.js similarity index 98% rename from x-pack/legacy/plugins/rollup/public/crud_app/sections/job_list/detail_panel/detail_panel.test.js rename to x-pack/plugins/rollup/public/crud_app/sections/job_list/detail_panel/detail_panel.test.js index 9ac8e6075e4cf..cea5b3c3e96e5 100644 --- a/x-pack/legacy/plugins/rollup/public/crud_app/sections/job_list/detail_panel/detail_panel.test.js +++ b/x-pack/plugins/rollup/public/crud_app/sections/job_list/detail_panel/detail_panel.test.js @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { registerTestBed } from '../../../../../../../../test_utils'; +import { registerTestBed } from '../../../../../../../test_utils'; import { getJob } from '../../../../../fixtures'; import { rollupJobsStore } from '../../../store'; import { DetailPanel } from './detail_panel'; @@ -17,9 +17,8 @@ import { tabToHumanizedMap, } from '../../components'; -jest.mock('ui/new_platform'); -jest.mock('../../../services', () => { - const services = require.requireActual('../../../services'); +jest.mock('../../../../kibana_services', () => { + const services = require.requireActual('../../../../kibana_services'); return { ...services, trackUiMetric: jest.fn(), diff --git a/x-pack/legacy/plugins/rollup/public/crud_app/sections/job_list/detail_panel/index.js b/x-pack/plugins/rollup/public/crud_app/sections/job_list/detail_panel/index.js similarity index 100% rename from x-pack/legacy/plugins/rollup/public/crud_app/sections/job_list/detail_panel/index.js rename to x-pack/plugins/rollup/public/crud_app/sections/job_list/detail_panel/index.js diff --git a/x-pack/legacy/plugins/rollup/public/crud_app/sections/job_list/index.js b/x-pack/plugins/rollup/public/crud_app/sections/job_list/index.js similarity index 100% rename from x-pack/legacy/plugins/rollup/public/crud_app/sections/job_list/index.js rename to x-pack/plugins/rollup/public/crud_app/sections/job_list/index.js diff --git a/x-pack/legacy/plugins/rollup/public/crud_app/sections/job_list/job_list.container.js b/x-pack/plugins/rollup/public/crud_app/sections/job_list/job_list.container.js similarity index 100% rename from x-pack/legacy/plugins/rollup/public/crud_app/sections/job_list/job_list.container.js rename to x-pack/plugins/rollup/public/crud_app/sections/job_list/job_list.container.js diff --git a/x-pack/legacy/plugins/rollup/public/crud_app/sections/job_list/job_list.js b/x-pack/plugins/rollup/public/crud_app/sections/job_list/job_list.js similarity index 98% rename from x-pack/legacy/plugins/rollup/public/crud_app/sections/job_list/job_list.js rename to x-pack/plugins/rollup/public/crud_app/sections/job_list/job_list.js index 98329a687217a..011becded148c 100644 --- a/x-pack/legacy/plugins/rollup/public/crud_app/sections/job_list/job_list.js +++ b/x-pack/plugins/rollup/public/crud_app/sections/job_list/job_list.js @@ -25,7 +25,7 @@ import { EuiCallOut, } from '@elastic/eui'; -import { withKibana } from '../../../../../../../../src/plugins/kibana_react/public/'; +import { withKibana } from '../../../../../../../src/plugins/kibana_react/public'; import { CRUD_APP_BASE_PATH } from '../../constants'; import { getRouterLinkProps, extractQueryParams, listBreadcrumb } from '../../services'; diff --git a/x-pack/plugins/rollup/public/crud_app/sections/job_list/job_list.test.js b/x-pack/plugins/rollup/public/crud_app/sections/job_list/job_list.test.js new file mode 100644 index 0000000000000..8c3bffd223ca9 --- /dev/null +++ b/x-pack/plugins/rollup/public/crud_app/sections/job_list/job_list.test.js @@ -0,0 +1,94 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { registerTestBed } from '../../../../../../test_utils'; +import { rollupJobsStore } from '../../store'; +import { JobList } from './job_list'; + +import { KibanaContextProvider } from '../../../../../../../src/plugins/kibana_react/public'; +import { coreMock } from '../../../../../../../src/core/public/mocks'; +const startMock = coreMock.createStart(); + +jest.mock('../../services', () => { + const services = require.requireActual('../../services'); + return { + ...services, + getRouterLinkProps: link => ({ href: link }), + }; +}); + +const defaultProps = { + history: { location: {} }, + loadJobs: () => {}, + refreshJobs: () => {}, + openDetailPanel: () => {}, + hasJobs: false, + isLoading: false, +}; + +const services = { + setBreadcrumbs: startMock.chrome.setBreadcrumbs, +}; +const Component = props => ( + <KibanaContextProvider services={services}> + <JobList {...props} /> + </KibanaContextProvider> +); + +const initTestBed = registerTestBed(Component, { defaultProps, store: rollupJobsStore }); + +describe('<JobList />', () => { + it('should render empty prompt when loading is complete and there are no jobs', () => { + const { exists } = initTestBed(); + + expect(exists('jobListEmptyPrompt')).toBeTruthy(); + }); + + it('should display a loading message when loading the jobs', () => { + const { component, exists } = initTestBed({ isLoading: true }); + + expect(exists('jobListLoading')).toBeTruthy(); + expect(component.find('JobTable').length).toBeFalsy(); + }); + + it('should display the <JobTable /> when there are jobs', () => { + const { component, exists } = initTestBed({ hasJobs: true }); + + expect(exists('jobListLoading')).toBeFalsy(); + expect(component.find('JobTable').length).toBeTruthy(); + }); + + describe('when there is an API error', () => { + const { exists, find } = initTestBed({ + jobLoadError: { + status: 400, + body: { statusCode: 400, error: 'Houston we got a problem.' }, + }, + }); + + it('should display a callout with the status and the message', () => { + expect(exists('jobListError')).toBeTruthy(); + expect( + find('jobListError') + .find('EuiText') + .text() + ).toEqual('400 Houston we got a problem.'); + }); + }); + + describe('when the user does not have the permission to access it', () => { + const { exists } = initTestBed({ jobLoadError: { status: 403 } }); + + it('should render a callout message', () => { + expect(exists('jobListNoPermission')).toBeTruthy(); + }); + + it('should display the page header', () => { + expect(exists('jobListPageHeader')).toBeTruthy(); + }); + }); +}); diff --git a/x-pack/legacy/plugins/rollup/public/crud_app/sections/job_list/job_table/index.js b/x-pack/plugins/rollup/public/crud_app/sections/job_list/job_table/index.js similarity index 100% rename from x-pack/legacy/plugins/rollup/public/crud_app/sections/job_list/job_table/index.js rename to x-pack/plugins/rollup/public/crud_app/sections/job_list/job_table/index.js diff --git a/x-pack/legacy/plugins/rollup/public/crud_app/sections/job_list/job_table/job_table.container.js b/x-pack/plugins/rollup/public/crud_app/sections/job_list/job_table/job_table.container.js similarity index 100% rename from x-pack/legacy/plugins/rollup/public/crud_app/sections/job_list/job_table/job_table.container.js rename to x-pack/plugins/rollup/public/crud_app/sections/job_list/job_table/job_table.container.js diff --git a/x-pack/legacy/plugins/rollup/public/crud_app/sections/job_list/job_table/job_table.js b/x-pack/plugins/rollup/public/crud_app/sections/job_list/job_table/job_table.js similarity index 99% rename from x-pack/legacy/plugins/rollup/public/crud_app/sections/job_list/job_table/job_table.js rename to x-pack/plugins/rollup/public/crud_app/sections/job_list/job_table/job_table.js index 4dbe396ab8410..f47992e1b501a 100644 --- a/x-pack/legacy/plugins/rollup/public/crud_app/sections/job_list/job_table/job_table.js +++ b/x-pack/plugins/rollup/public/crud_app/sections/job_list/job_table/job_table.js @@ -30,7 +30,8 @@ import { } from '@elastic/eui'; import { UIM_SHOW_DETAILS_CLICK } from '../../../../../common'; -import { trackUiMetric, METRIC_TYPE } from '../../../services'; +import { METRIC_TYPE } from '../../../services'; +import { trackUiMetric } from '../../../../kibana_services'; import { JobActionMenu, JobStatus } from '../../components'; const COLUMNS = [ diff --git a/x-pack/legacy/plugins/rollup/public/crud_app/sections/job_list/job_table/job_table.test.js b/x-pack/plugins/rollup/public/crud_app/sections/job_list/job_table/job_table.test.js similarity index 96% rename from x-pack/legacy/plugins/rollup/public/crud_app/sections/job_list/job_table/job_table.test.js rename to x-pack/plugins/rollup/public/crud_app/sections/job_list/job_table/job_table.test.js index c688343d5f768..c6a3d3a8f69ad 100644 --- a/x-pack/legacy/plugins/rollup/public/crud_app/sections/job_list/job_table/job_table.test.js +++ b/x-pack/plugins/rollup/public/crud_app/sections/job_list/job_table/job_table.test.js @@ -6,14 +6,13 @@ import { Pager } from '@elastic/eui'; -import { registerTestBed } from '../../../../../../../../test_utils'; +import { registerTestBed } from '../../../../../../../test_utils'; import { getJobs, jobCount } from '../../../../../fixtures'; import { rollupJobsStore } from '../../../store'; import { JobTable } from './job_table'; -jest.mock('ui/new_platform'); -jest.mock('../../../services', () => { - const services = require.requireActual('../../../services'); +jest.mock('../../../../kibana_services', () => { + const services = require.requireActual('../../../../kibana_services'); return { ...services, trackUiMetric: jest.fn(), diff --git a/x-pack/legacy/plugins/rollup/public/crud_app/services/api.js b/x-pack/plugins/rollup/public/crud_app/services/api.js similarity index 100% rename from x-pack/legacy/plugins/rollup/public/crud_app/services/api.js rename to x-pack/plugins/rollup/public/crud_app/services/api.js diff --git a/x-pack/legacy/plugins/rollup/public/crud_app/services/api_errors.ts b/x-pack/plugins/rollup/public/crud_app/services/api_errors.ts similarity index 100% rename from x-pack/legacy/plugins/rollup/public/crud_app/services/api_errors.ts rename to x-pack/plugins/rollup/public/crud_app/services/api_errors.ts diff --git a/x-pack/legacy/plugins/rollup/public/crud_app/services/breadcrumbs.js b/x-pack/plugins/rollup/public/crud_app/services/breadcrumbs.js similarity index 100% rename from x-pack/legacy/plugins/rollup/public/crud_app/services/breadcrumbs.js rename to x-pack/plugins/rollup/public/crud_app/services/breadcrumbs.js diff --git a/x-pack/legacy/plugins/rollup/public/crud_app/services/documentation_links.js b/x-pack/plugins/rollup/public/crud_app/services/documentation_links.js similarity index 100% rename from x-pack/legacy/plugins/rollup/public/crud_app/services/documentation_links.js rename to x-pack/plugins/rollup/public/crud_app/services/documentation_links.js diff --git a/x-pack/legacy/plugins/rollup/public/crud_app/services/filter_items.js b/x-pack/plugins/rollup/public/crud_app/services/filter_items.js similarity index 100% rename from x-pack/legacy/plugins/rollup/public/crud_app/services/filter_items.js rename to x-pack/plugins/rollup/public/crud_app/services/filter_items.js diff --git a/x-pack/legacy/plugins/rollup/public/crud_app/services/flatten_panel_tree.js b/x-pack/plugins/rollup/public/crud_app/services/flatten_panel_tree.js similarity index 100% rename from x-pack/legacy/plugins/rollup/public/crud_app/services/flatten_panel_tree.js rename to x-pack/plugins/rollup/public/crud_app/services/flatten_panel_tree.js diff --git a/x-pack/legacy/plugins/rollup/public/crud_app/services/format_fields.js b/x-pack/plugins/rollup/public/crud_app/services/format_fields.js similarity index 100% rename from x-pack/legacy/plugins/rollup/public/crud_app/services/format_fields.js rename to x-pack/plugins/rollup/public/crud_app/services/format_fields.js diff --git a/x-pack/legacy/plugins/rollup/public/crud_app/services/http_provider.ts b/x-pack/plugins/rollup/public/crud_app/services/http_provider.ts similarity index 91% rename from x-pack/legacy/plugins/rollup/public/crud_app/services/http_provider.ts rename to x-pack/plugins/rollup/public/crud_app/services/http_provider.ts index dd84328084d05..93898610b844e 100644 --- a/x-pack/legacy/plugins/rollup/public/crud_app/services/http_provider.ts +++ b/x-pack/plugins/rollup/public/crud_app/services/http_provider.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { HttpStart } from 'src/core/public'; +import { HttpStart } from 'kibana/public'; let _http: HttpStart | null = null; diff --git a/x-pack/plugins/rollup/public/crud_app/services/index.js b/x-pack/plugins/rollup/public/crud_app/services/index.js new file mode 100644 index 0000000000000..0b45b1bdb6b5f --- /dev/null +++ b/x-pack/plugins/rollup/public/crud_app/services/index.js @@ -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. + */ + +export { createJob, deleteJobs, loadJobs, startJobs, stopJobs, validateIndexPattern } from './api'; + +export { showApiError, showApiWarning } from './api_errors'; + +export { listBreadcrumb, createBreadcrumb } from './breadcrumbs'; + +export { + setEsBaseAndXPackBase, + getLogisticalDetailsUrl, + getDateHistogramDetailsUrl, + getDateHistogramAggregationUrl, + getTermsDetailsUrl, + getHistogramDetailsUrl, + getMetricsDetailsUrl, + getCronUrl, +} from './documentation_links'; + +export { filterItems } from './filter_items'; + +export { flattenPanelTree } from './flatten_panel_tree'; + +export { formatFields } from './format_fields'; + +export { setHttp, getHttp } from './http_provider'; + +export { serializeJob, deserializeJob, deserializeJobs } from './jobs'; + +export { createNoticeableDelay } from './noticeable_delay'; + +export { extractQueryParams } from './query_params'; + +export { + setUserHasLeftApp, + getUserHasLeftApp, + registerRouter, + getRouter, + getRouterLinkProps, +} from './routing'; + +export { sortTable } from './sort_table'; + +export { retypeMetrics } from './retype_metrics'; + +export { METRIC_TYPE } from './track_ui_metric'; diff --git a/x-pack/legacy/plugins/rollup/public/crud_app/services/jobs.js b/x-pack/plugins/rollup/public/crud_app/services/jobs.js similarity index 100% rename from x-pack/legacy/plugins/rollup/public/crud_app/services/jobs.js rename to x-pack/plugins/rollup/public/crud_app/services/jobs.js diff --git a/x-pack/legacy/plugins/rollup/public/crud_app/services/noticeable_delay.js b/x-pack/plugins/rollup/public/crud_app/services/noticeable_delay.js similarity index 100% rename from x-pack/legacy/plugins/rollup/public/crud_app/services/noticeable_delay.js rename to x-pack/plugins/rollup/public/crud_app/services/noticeable_delay.js diff --git a/x-pack/legacy/plugins/rollup/public/crud_app/services/query_params.js b/x-pack/plugins/rollup/public/crud_app/services/query_params.js similarity index 100% rename from x-pack/legacy/plugins/rollup/public/crud_app/services/query_params.js rename to x-pack/plugins/rollup/public/crud_app/services/query_params.js diff --git a/x-pack/legacy/plugins/rollup/public/crud_app/services/retype_metrics.js b/x-pack/plugins/rollup/public/crud_app/services/retype_metrics.js similarity index 100% rename from x-pack/legacy/plugins/rollup/public/crud_app/services/retype_metrics.js rename to x-pack/plugins/rollup/public/crud_app/services/retype_metrics.js diff --git a/x-pack/legacy/plugins/rollup/public/crud_app/services/routing.js b/x-pack/plugins/rollup/public/crud_app/services/routing.js similarity index 100% rename from x-pack/legacy/plugins/rollup/public/crud_app/services/routing.js rename to x-pack/plugins/rollup/public/crud_app/services/routing.js diff --git a/x-pack/legacy/plugins/rollup/public/crud_app/services/sort_table.js b/x-pack/plugins/rollup/public/crud_app/services/sort_table.js similarity index 100% rename from x-pack/legacy/plugins/rollup/public/crud_app/services/sort_table.js rename to x-pack/plugins/rollup/public/crud_app/services/sort_table.js diff --git a/x-pack/plugins/rollup/public/crud_app/services/track_ui_metric.ts b/x-pack/plugins/rollup/public/crud_app/services/track_ui_metric.ts new file mode 100644 index 0000000000000..aa1cc2dfea323 --- /dev/null +++ b/x-pack/plugins/rollup/public/crud_app/services/track_ui_metric.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 { METRIC_TYPE } from '@kbn/analytics'; +import { trackUiMetric } from '../../kibana_services'; + +export { METRIC_TYPE }; + +/** + * Transparently return provided request Promise, while allowing us to track + * a successful completion of the request. + */ +export function trackUserRequest<TResponse>(request: Promise<TResponse>, actionType: string) { + // Only track successful actions. + return request.then(response => { + trackUiMetric(METRIC_TYPE.LOADED, actionType); + // We return the response immediately without waiting for the tracking request to resolve, + // to avoid adding additional latency. + return response; + }); +} diff --git a/x-pack/legacy/plugins/rollup/public/crud_app/store/action_types.js b/x-pack/plugins/rollup/public/crud_app/store/action_types.js similarity index 100% rename from x-pack/legacy/plugins/rollup/public/crud_app/store/action_types.js rename to x-pack/plugins/rollup/public/crud_app/store/action_types.js diff --git a/x-pack/legacy/plugins/rollup/public/crud_app/store/actions/change_job_status.js b/x-pack/plugins/rollup/public/crud_app/store/actions/change_job_status.js similarity index 100% rename from x-pack/legacy/plugins/rollup/public/crud_app/store/actions/change_job_status.js rename to x-pack/plugins/rollup/public/crud_app/store/actions/change_job_status.js diff --git a/x-pack/legacy/plugins/rollup/public/crud_app/store/actions/clone_job.js b/x-pack/plugins/rollup/public/crud_app/store/actions/clone_job.js similarity index 100% rename from x-pack/legacy/plugins/rollup/public/crud_app/store/actions/clone_job.js rename to x-pack/plugins/rollup/public/crud_app/store/actions/clone_job.js diff --git a/x-pack/legacy/plugins/rollup/public/crud_app/store/actions/create_job.js b/x-pack/plugins/rollup/public/crud_app/store/actions/create_job.js similarity index 100% rename from x-pack/legacy/plugins/rollup/public/crud_app/store/actions/create_job.js rename to x-pack/plugins/rollup/public/crud_app/store/actions/create_job.js diff --git a/x-pack/legacy/plugins/rollup/public/crud_app/store/actions/delete_jobs.js b/x-pack/plugins/rollup/public/crud_app/store/actions/delete_jobs.js similarity index 100% rename from x-pack/legacy/plugins/rollup/public/crud_app/store/actions/delete_jobs.js rename to x-pack/plugins/rollup/public/crud_app/store/actions/delete_jobs.js diff --git a/x-pack/legacy/plugins/rollup/public/crud_app/store/actions/detail_panel.js b/x-pack/plugins/rollup/public/crud_app/store/actions/detail_panel.js similarity index 100% rename from x-pack/legacy/plugins/rollup/public/crud_app/store/actions/detail_panel.js rename to x-pack/plugins/rollup/public/crud_app/store/actions/detail_panel.js diff --git a/x-pack/legacy/plugins/rollup/public/crud_app/store/actions/index.js b/x-pack/plugins/rollup/public/crud_app/store/actions/index.js similarity index 100% rename from x-pack/legacy/plugins/rollup/public/crud_app/store/actions/index.js rename to x-pack/plugins/rollup/public/crud_app/store/actions/index.js diff --git a/x-pack/legacy/plugins/rollup/public/crud_app/store/actions/load_jobs.js b/x-pack/plugins/rollup/public/crud_app/store/actions/load_jobs.js similarity index 100% rename from x-pack/legacy/plugins/rollup/public/crud_app/store/actions/load_jobs.js rename to x-pack/plugins/rollup/public/crud_app/store/actions/load_jobs.js diff --git a/x-pack/legacy/plugins/rollup/public/crud_app/store/actions/refresh_jobs.js b/x-pack/plugins/rollup/public/crud_app/store/actions/refresh_jobs.js similarity index 100% rename from x-pack/legacy/plugins/rollup/public/crud_app/store/actions/refresh_jobs.js rename to x-pack/plugins/rollup/public/crud_app/store/actions/refresh_jobs.js diff --git a/x-pack/legacy/plugins/rollup/public/crud_app/store/actions/table_state.js b/x-pack/plugins/rollup/public/crud_app/store/actions/table_state.js similarity index 100% rename from x-pack/legacy/plugins/rollup/public/crud_app/store/actions/table_state.js rename to x-pack/plugins/rollup/public/crud_app/store/actions/table_state.js diff --git a/x-pack/legacy/plugins/rollup/public/crud_app/store/index.js b/x-pack/plugins/rollup/public/crud_app/store/index.js similarity index 100% rename from x-pack/legacy/plugins/rollup/public/crud_app/store/index.js rename to x-pack/plugins/rollup/public/crud_app/store/index.js diff --git a/x-pack/legacy/plugins/rollup/public/crud_app/store/middleware/clone_job.js b/x-pack/plugins/rollup/public/crud_app/store/middleware/clone_job.js similarity index 100% rename from x-pack/legacy/plugins/rollup/public/crud_app/store/middleware/clone_job.js rename to x-pack/plugins/rollup/public/crud_app/store/middleware/clone_job.js diff --git a/x-pack/legacy/plugins/rollup/public/crud_app/store/middleware/detail_panel.js b/x-pack/plugins/rollup/public/crud_app/store/middleware/detail_panel.js similarity index 100% rename from x-pack/legacy/plugins/rollup/public/crud_app/store/middleware/detail_panel.js rename to x-pack/plugins/rollup/public/crud_app/store/middleware/detail_panel.js diff --git a/x-pack/legacy/plugins/rollup/public/crud_app/store/middleware/index.js b/x-pack/plugins/rollup/public/crud_app/store/middleware/index.js similarity index 100% rename from x-pack/legacy/plugins/rollup/public/crud_app/store/middleware/index.js rename to x-pack/plugins/rollup/public/crud_app/store/middleware/index.js diff --git a/x-pack/legacy/plugins/rollup/public/crud_app/store/reducers/clone_job.js b/x-pack/plugins/rollup/public/crud_app/store/reducers/clone_job.js similarity index 100% rename from x-pack/legacy/plugins/rollup/public/crud_app/store/reducers/clone_job.js rename to x-pack/plugins/rollup/public/crud_app/store/reducers/clone_job.js diff --git a/x-pack/legacy/plugins/rollup/public/crud_app/store/reducers/create_job.js b/x-pack/plugins/rollup/public/crud_app/store/reducers/create_job.js similarity index 100% rename from x-pack/legacy/plugins/rollup/public/crud_app/store/reducers/create_job.js rename to x-pack/plugins/rollup/public/crud_app/store/reducers/create_job.js diff --git a/x-pack/legacy/plugins/rollup/public/crud_app/store/reducers/detail_panel.js b/x-pack/plugins/rollup/public/crud_app/store/reducers/detail_panel.js similarity index 100% rename from x-pack/legacy/plugins/rollup/public/crud_app/store/reducers/detail_panel.js rename to x-pack/plugins/rollup/public/crud_app/store/reducers/detail_panel.js diff --git a/x-pack/legacy/plugins/rollup/public/crud_app/store/reducers/index.js b/x-pack/plugins/rollup/public/crud_app/store/reducers/index.js similarity index 100% rename from x-pack/legacy/plugins/rollup/public/crud_app/store/reducers/index.js rename to x-pack/plugins/rollup/public/crud_app/store/reducers/index.js diff --git a/x-pack/legacy/plugins/rollup/public/crud_app/store/reducers/jobs.js b/x-pack/plugins/rollup/public/crud_app/store/reducers/jobs.js similarity index 100% rename from x-pack/legacy/plugins/rollup/public/crud_app/store/reducers/jobs.js rename to x-pack/plugins/rollup/public/crud_app/store/reducers/jobs.js diff --git a/x-pack/legacy/plugins/rollup/public/crud_app/store/reducers/table_state.js b/x-pack/plugins/rollup/public/crud_app/store/reducers/table_state.js similarity index 100% rename from x-pack/legacy/plugins/rollup/public/crud_app/store/reducers/table_state.js rename to x-pack/plugins/rollup/public/crud_app/store/reducers/table_state.js diff --git a/x-pack/legacy/plugins/rollup/public/crud_app/store/reducers/update_job.js b/x-pack/plugins/rollup/public/crud_app/store/reducers/update_job.js similarity index 100% rename from x-pack/legacy/plugins/rollup/public/crud_app/store/reducers/update_job.js rename to x-pack/plugins/rollup/public/crud_app/store/reducers/update_job.js diff --git a/x-pack/legacy/plugins/rollup/public/crud_app/store/selectors/index.js b/x-pack/plugins/rollup/public/crud_app/store/selectors/index.js similarity index 100% rename from x-pack/legacy/plugins/rollup/public/crud_app/store/selectors/index.js rename to x-pack/plugins/rollup/public/crud_app/store/selectors/index.js diff --git a/x-pack/legacy/plugins/rollup/public/crud_app/store/store.js b/x-pack/plugins/rollup/public/crud_app/store/store.js similarity index 100% rename from x-pack/legacy/plugins/rollup/public/crud_app/store/store.js rename to x-pack/plugins/rollup/public/crud_app/store/store.js diff --git a/x-pack/legacy/plugins/rollup/public/extend_index_management/index.ts b/x-pack/plugins/rollup/public/extend_index_management/index.ts similarity index 100% rename from x-pack/legacy/plugins/rollup/public/extend_index_management/index.ts rename to x-pack/plugins/rollup/public/extend_index_management/index.ts diff --git a/x-pack/plugins/rollup/public/index.scss b/x-pack/plugins/rollup/public/index.scss new file mode 100644 index 0000000000000..cbbedcebb043e --- /dev/null +++ b/x-pack/plugins/rollup/public/index.scss @@ -0,0 +1,10 @@ +// Index management plugin styles + +// Prefix all styles with "rollup" to avoid conflicts. +// Examples +// rollupChart +// rollupChart__legend +// rollupChart__legend--small +// rollupChart__legend-isLoading + +@import 'crud_app/_crud_app'; diff --git a/x-pack/plugins/rollup/public/index.ts b/x-pack/plugins/rollup/public/index.ts new file mode 100644 index 0000000000000..4c965fc38f4e7 --- /dev/null +++ b/x-pack/plugins/rollup/public/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. + */ + +import { RollupPlugin } from './plugin'; + +export const plugin = () => new RollupPlugin(); diff --git a/x-pack/legacy/plugins/rollup/public/index_pattern_creation/components/rollup_prompt/index.js b/x-pack/plugins/rollup/public/index_pattern_creation/components/rollup_prompt/index.js similarity index 100% rename from x-pack/legacy/plugins/rollup/public/index_pattern_creation/components/rollup_prompt/index.js rename to x-pack/plugins/rollup/public/index_pattern_creation/components/rollup_prompt/index.js diff --git a/x-pack/legacy/plugins/rollup/public/index_pattern_creation/components/rollup_prompt/rollup_prompt.js b/x-pack/plugins/rollup/public/index_pattern_creation/components/rollup_prompt/rollup_prompt.js similarity index 100% rename from x-pack/legacy/plugins/rollup/public/index_pattern_creation/components/rollup_prompt/rollup_prompt.js rename to x-pack/plugins/rollup/public/index_pattern_creation/components/rollup_prompt/rollup_prompt.js diff --git a/x-pack/legacy/plugins/rollup/public/index_pattern_creation/rollup_index_pattern_creation_config.js b/x-pack/plugins/rollup/public/index_pattern_creation/rollup_index_pattern_creation_config.js similarity index 97% rename from x-pack/legacy/plugins/rollup/public/index_pattern_creation/rollup_index_pattern_creation_config.js rename to x-pack/plugins/rollup/public/index_pattern_creation/rollup_index_pattern_creation_config.js index f4de2a3098127..c29fba44285be 100644 --- a/x-pack/legacy/plugins/rollup/public/index_pattern_creation/rollup_index_pattern_creation_config.js +++ b/x-pack/plugins/rollup/public/index_pattern_creation/rollup_index_pattern_creation_config.js @@ -8,7 +8,7 @@ import React from 'react'; import { i18n } from '@kbn/i18n'; import { RollupPrompt } from './components/rollup_prompt'; -import { IndexPatternCreationConfig } from '../../../../../../src/plugins/index_pattern_management/public'; +import { IndexPatternCreationConfig } from '../../../../../src/plugins/index_pattern_management/public'; const rollupIndexPatternTypeName = i18n.translate( 'xpack.rollupJobs.editRollupIndexPattern.createIndex.defaultTypeName', diff --git a/x-pack/legacy/plugins/rollup/public/index_pattern_list/rollup_index_pattern_list_config.js b/x-pack/plugins/rollup/public/index_pattern_list/rollup_index_pattern_list_config.js similarity index 93% rename from x-pack/legacy/plugins/rollup/public/index_pattern_list/rollup_index_pattern_list_config.js rename to x-pack/plugins/rollup/public/index_pattern_list/rollup_index_pattern_list_config.js index 809a76d1868b2..e61287d303643 100644 --- a/x-pack/legacy/plugins/rollup/public/index_pattern_list/rollup_index_pattern_list_config.js +++ b/x-pack/plugins/rollup/public/index_pattern_list/rollup_index_pattern_list_config.js @@ -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 { IndexPatternListConfig } from '../../../../../../src/plugins/index_pattern_management/public'; +import { IndexPatternListConfig } from '../../../../../src/plugins/index_pattern_management/public'; function isRollup(indexPattern) { return ( diff --git a/x-pack/plugins/rollup/public/kibana_services.ts b/x-pack/plugins/rollup/public/kibana_services.ts new file mode 100644 index 0000000000000..edbf69568f5e5 --- /dev/null +++ b/x-pack/plugins/rollup/public/kibana_services.ts @@ -0,0 +1,47 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { NotificationsStart, FatalErrorsSetup } from 'kibana/public'; +import { UiStatsMetricType } from '@kbn/analytics'; +import { createGetterSetter } from '../../../../src/plugins/kibana_utils/common'; + +let notifications: NotificationsStart | null = null; +let fatalErrors: FatalErrorsSetup | null = null; + +export function getNotifications() { + if (!notifications) { + throw new Error('Rollup notifications is not defined'); + } + return notifications; +} +export function setNotifications(newNotifications: NotificationsStart) { + notifications = newNotifications; +} + +export function getFatalErrors() { + if (!fatalErrors) { + throw new Error('Rollup fatalErrors is not defined'); + } + return fatalErrors; +} +export function setFatalErrors(newFatalErrors: FatalErrorsSetup) { + fatalErrors = newFatalErrors; +} + +export const [getUiStatsReporter, setUiStatsReporter] = createGetterSetter< + (type: UiStatsMetricType, eventNames: string | string[], count?: number) => void +>('uiMetric'); + +// default value if usageCollection is not available +setUiStatsReporter(() => {}); + +export function trackUiMetric( + type: UiStatsMetricType, + eventNames: string | string[], + count?: number +) { + getUiStatsReporter()(type, eventNames, count); +} diff --git a/x-pack/plugins/rollup/public/plugin.ts b/x-pack/plugins/rollup/public/plugin.ts new file mode 100644 index 0000000000000..fd1b90fbc9855 --- /dev/null +++ b/x-pack/plugins/rollup/public/plugin.ts @@ -0,0 +1,123 @@ +/* + * 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 { CoreSetup, CoreStart, Plugin } from 'kibana/public'; +import { rollupBadgeExtension, rollupToggleExtension } from './extend_index_management'; +// @ts-ignore +import { RollupIndexPatternCreationConfig } from './index_pattern_creation/rollup_index_pattern_creation_config'; +// @ts-ignore +import { RollupIndexPatternListConfig } from './index_pattern_list/rollup_index_pattern_list_config'; +// @ts-ignore +import { initAggTypeFilter } from './visualize/agg_type_filter'; +// @ts-ignore +import { initAggTypeFieldFilter } from './visualize/agg_type_field_filter'; +import { CONFIG_ROLLUPS, UIM_APP_NAME } from '../common'; +import { + FeatureCatalogueCategory, + HomePublicPluginSetup, +} from '../../../../src/plugins/home/public'; +// @ts-ignore +import { CRUD_APP_BASE_PATH } from './crud_app/constants'; +import { ManagementSetup } from '../../../../src/plugins/management/public'; +import { IndexManagementPluginSetup } from '../../index_management/public'; +import { IndexPatternManagementSetup } from '../../../../src/plugins/index_pattern_management/public'; +import { DataPublicPluginStart, search } from '../../../../src/plugins/data/public'; +// @ts-ignore +import { setEsBaseAndXPackBase, setHttp } from './crud_app/services/index'; +import { setNotifications, setFatalErrors, setUiStatsReporter } from './kibana_services'; +import { UsageCollectionSetup } from '../../../../src/plugins/usage_collection/public'; + +export interface RollupPluginSetupDependencies { + home?: HomePublicPluginSetup; + management: ManagementSetup; + indexManagement?: IndexManagementPluginSetup; + indexPatternManagement: IndexPatternManagementSetup; + usageCollection?: UsageCollectionSetup; +} + +export interface RollupPluginStartDependencies { + data: DataPublicPluginStart; +} + +export class RollupPlugin implements Plugin { + setup( + core: CoreSetup, + { + home, + management, + indexManagement, + indexPatternManagement, + usageCollection, + }: RollupPluginSetupDependencies + ) { + setFatalErrors(core.fatalErrors); + if (usageCollection) { + setUiStatsReporter(usageCollection.reportUiStats.bind(usageCollection, UIM_APP_NAME)); + } + + if (indexManagement) { + indexManagement.extensionsService.addBadge(rollupBadgeExtension); + indexManagement.extensionsService.addToggle(rollupToggleExtension); + } + + const isRollupIndexPatternsEnabled = core.uiSettings.get(CONFIG_ROLLUPS); + + if (isRollupIndexPatternsEnabled) { + indexPatternManagement.creation.addCreationConfig(RollupIndexPatternCreationConfig); + indexPatternManagement.list.addListConfig(RollupIndexPatternListConfig); + } + + if (home) { + home.featureCatalogue.register({ + id: 'rollup_jobs', + title: 'Rollups', + description: i18n.translate('xpack.rollupJobs.featureCatalogueDescription', { + defaultMessage: + 'Summarize and store historical data in a smaller index for future analysis.', + }), + icon: 'indexRollupApp', + path: `#${CRUD_APP_BASE_PATH}/job_list`, + showOnHomePage: true, + category: FeatureCatalogueCategory.ADMIN, + }); + } + + const esSection = management.sections.getSection('elasticsearch'); + if (esSection) { + esSection.registerApp({ + id: 'rollup_jobs', + title: i18n.translate('xpack.rollupJobs.appTitle', { defaultMessage: 'Rollup Jobs' }), + order: 3, + async mount(params) { + params.setBreadcrumbs([ + { + text: i18n.translate('xpack.rollupJobs.breadcrumbsTitle', { + defaultMessage: 'Rollup Jobs', + }), + }, + ]); + const { renderApp } = await import('./application'); + + return renderApp(core, params); + }, + }); + } + } + + start(core: CoreStart, plugins: RollupPluginStartDependencies) { + setHttp(core.http); + setNotifications(core.notifications); + setEsBaseAndXPackBase(core.docLinks.ELASTIC_WEBSITE_URL, core.docLinks.DOC_LINK_VERSION); + + const isRollupIndexPatternsEnabled = core.uiSettings.get(CONFIG_ROLLUPS); + + if (isRollupIndexPatternsEnabled) { + initAggTypeFilter(search.aggs.aggTypeFilters); + initAggTypeFieldFilter(plugins.data.search.__LEGACY.aggTypeFieldFilters); + } + } +} diff --git a/x-pack/legacy/plugins/rollup/public/shared_imports.ts b/x-pack/plugins/rollup/public/shared_imports.ts similarity index 76% rename from x-pack/legacy/plugins/rollup/public/shared_imports.ts rename to x-pack/plugins/rollup/public/shared_imports.ts index 6bf74da6db6fe..1ac25a1a0e5f8 100644 --- a/x-pack/legacy/plugins/rollup/public/shared_imports.ts +++ b/x-pack/plugins/rollup/public/shared_imports.ts @@ -4,4 +4,4 @@ * you may not use this file except in compliance with the Elastic License. */ -export { indices } from '../../../../../src/plugins/es_ui_shared/public'; +export { indices } from '../../../../src/plugins/es_ui_shared/public'; diff --git a/x-pack/legacy/plugins/rollup/__jest__/client_integration/helpers/constants.js b/x-pack/plugins/rollup/public/test/client_integration/helpers/constants.js similarity index 100% rename from x-pack/legacy/plugins/rollup/__jest__/client_integration/helpers/constants.js rename to x-pack/plugins/rollup/public/test/client_integration/helpers/constants.js diff --git a/x-pack/legacy/plugins/rollup/__jest__/client_integration/helpers/index.js b/x-pack/plugins/rollup/public/test/client_integration/helpers/index.js similarity index 100% rename from x-pack/legacy/plugins/rollup/__jest__/client_integration/helpers/index.js rename to x-pack/plugins/rollup/public/test/client_integration/helpers/index.js diff --git a/x-pack/legacy/plugins/rollup/__jest__/client_integration/helpers/job_clone.helpers.js b/x-pack/plugins/rollup/public/test/client_integration/helpers/job_clone.helpers.js similarity index 83% rename from x-pack/legacy/plugins/rollup/__jest__/client_integration/helpers/job_clone.helpers.js rename to x-pack/plugins/rollup/public/test/client_integration/helpers/job_clone.helpers.js index a8376bb31b23f..fa9929a207615 100644 --- a/x-pack/legacy/plugins/rollup/__jest__/client_integration/helpers/job_clone.helpers.js +++ b/x-pack/plugins/rollup/public/test/client_integration/helpers/job_clone.helpers.js @@ -5,10 +5,10 @@ */ import { registerTestBed } from '../../../../../../test_utils'; -import { createRollupJobsStore } from '../../../public/crud_app/store'; -import { JobCreate } from '../../../public/crud_app/sections'; +import { createRollupJobsStore } from '../../../crud_app/store'; +import { JobCreate } from '../../../crud_app/sections'; import { JOB_TO_CLONE } from './constants'; -import { deserializeJob } from '../../../public/crud_app/services'; +import { deserializeJob } from '../../../crud_app/services'; import { wrapComponent } from './setup_context'; diff --git a/x-pack/legacy/plugins/rollup/__jest__/client_integration/helpers/job_create.helpers.js b/x-pack/plugins/rollup/public/test/client_integration/helpers/job_create.helpers.js similarity index 96% rename from x-pack/legacy/plugins/rollup/__jest__/client_integration/helpers/job_create.helpers.js rename to x-pack/plugins/rollup/public/test/client_integration/helpers/job_create.helpers.js index 2395fd014dd1e..7ddcfa9eb83ff 100644 --- a/x-pack/legacy/plugins/rollup/__jest__/client_integration/helpers/job_create.helpers.js +++ b/x-pack/plugins/rollup/public/test/client_integration/helpers/job_create.helpers.js @@ -5,8 +5,8 @@ */ import { registerTestBed } from '../../../../../../test_utils'; -import { rollupJobsStore } from '../../../public/crud_app/store'; -import { JobCreate } from '../../../public/crud_app/sections'; +import { rollupJobsStore } from '../../../crud_app/store'; +import { JobCreate } from '../../../crud_app/sections'; import { JOB_TO_CREATE } from './constants'; diff --git a/x-pack/plugins/rollup/public/test/client_integration/helpers/job_list.helpers.js b/x-pack/plugins/rollup/public/test/client_integration/helpers/job_list.helpers.js new file mode 100644 index 0000000000000..fda1ec8cfed22 --- /dev/null +++ b/x-pack/plugins/rollup/public/test/client_integration/helpers/job_list.helpers.js @@ -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 { registerTestBed } from '../../../../../../test_utils'; +import { registerRouter } from '../../../crud_app/services'; +import { createRollupJobsStore } from '../../../crud_app/store'; +import { JobList } from '../../../crud_app/sections/job_list'; + +import { wrapComponent } from './setup_context'; + +const testBedConfig = { + store: createRollupJobsStore, + memoryRouter: { + onRouter: router => { + // register our react memory router + registerRouter(router); + }, + }, +}; + +export const setup = registerTestBed(wrapComponent(JobList), testBedConfig); diff --git a/x-pack/legacy/plugins/rollup/__jest__/client_integration/helpers/setup_context.tsx b/x-pack/plugins/rollup/public/test/client_integration/helpers/setup_context.tsx similarity index 100% rename from x-pack/legacy/plugins/rollup/__jest__/client_integration/helpers/setup_context.tsx rename to x-pack/plugins/rollup/public/test/client_integration/helpers/setup_context.tsx diff --git a/x-pack/legacy/plugins/rollup/__jest__/client_integration/helpers/setup_environment.ts b/x-pack/plugins/rollup/public/test/client_integration/helpers/setup_environment.ts similarity index 100% rename from x-pack/legacy/plugins/rollup/__jest__/client_integration/helpers/setup_environment.ts rename to x-pack/plugins/rollup/public/test/client_integration/helpers/setup_environment.ts diff --git a/x-pack/legacy/plugins/rollup/__jest__/client_integration/job_create_clone.test.js b/x-pack/plugins/rollup/public/test/client_integration/job_create_clone.test.js similarity index 93% rename from x-pack/legacy/plugins/rollup/__jest__/client_integration/job_create_clone.test.js rename to x-pack/plugins/rollup/public/test/client_integration/job_create_clone.test.js index b7c98ed179c7a..7b61a03dcde45 100644 --- a/x-pack/legacy/plugins/rollup/__jest__/client_integration/job_create_clone.test.js +++ b/x-pack/plugins/rollup/public/test/client_integration/job_create_clone.test.js @@ -4,11 +4,10 @@ * you may not use this file except in compliance with the Elastic License. */ -import { setHttp } from '../../public/crud_app/services'; +import { setHttp } from '../../crud_app/services'; import { mockHttpRequest, pageHelpers, nextTick } from './helpers'; import { JOB_TO_CLONE, JOB_CLONE_INDEX_PATTERN_CHECK } from './helpers/constants'; - -jest.mock('ui/new_platform'); +import { coreMock } from '../../../../../../src/core/public/mocks'; jest.mock('lodash/function/debounce', () => fn => fn); @@ -23,23 +22,23 @@ describe('Cloning a rollup job through create job wizard', () => { let form; let table; let actions; - let npStart; + let startMock; beforeAll(() => { - npStart = require('ui/new_platform').npStart; // eslint-disable-line - setHttp(npStart.core.http); + startMock = coreMock.createStart(); + setHttp(startMock.http); }); beforeEach(() => { - mockHttpRequest(npStart.core.http, { indxPatternVldtResp: JOB_CLONE_INDEX_PATTERN_CHECK }); + mockHttpRequest(startMock.http, { indxPatternVldtResp: JOB_CLONE_INDEX_PATTERN_CHECK }); ({ exists, find, form, actions, table } = setup()); }); afterEach(() => { - npStart.core.http.get.mockClear(); - npStart.core.http.post.mockClear(); - npStart.core.http.put.mockClear(); + startMock.http.get.mockClear(); + startMock.http.post.mockClear(); + startMock.http.put.mockClear(); }); it('should have fields correctly pre-populated', async () => { diff --git a/x-pack/legacy/plugins/rollup/__jest__/client_integration/job_create_date_histogram.test.js b/x-pack/plugins/rollup/public/test/client_integration/job_create_date_histogram.test.js similarity index 88% rename from x-pack/legacy/plugins/rollup/__jest__/client_integration/job_create_date_histogram.test.js rename to x-pack/plugins/rollup/public/test/client_integration/job_create_date_histogram.test.js index b8ec7d9f85d00..095609ac2b2d7 100644 --- a/x-pack/legacy/plugins/rollup/__jest__/client_integration/job_create_date_histogram.test.js +++ b/x-pack/plugins/rollup/public/test/client_integration/job_create_date_histogram.test.js @@ -6,10 +6,9 @@ import moment from 'moment-timezone'; -import { setHttp } from '../../public/crud_app/services'; +import { setHttp } from '../../crud_app/services'; import { mockHttpRequest, pageHelpers } from './helpers'; - -jest.mock('ui/new_platform'); +import { coreMock } from '../../../../../../src/core/public/mocks'; jest.mock('lodash/function/debounce', () => fn => fn); @@ -22,23 +21,23 @@ describe('Create Rollup Job, step 2: Date histogram', () => { let goToStep; let form; let getEuiStepsHorizontalActive; - let npStart; + let startMock; beforeAll(() => { - npStart = require('ui/new_platform').npStart; // eslint-disable-line - setHttp(npStart.core.http); + startMock = coreMock.createStart(); + setHttp(startMock.http); }); beforeEach(() => { // Set "default" mock responses by not providing any arguments - mockHttpRequest(npStart.core.http); + mockHttpRequest(startMock.http); ({ find, exists, actions, form, getEuiStepsHorizontalActive, goToStep } = setup()); }); afterEach(() => { - npStart.core.http.get.mockClear(); - npStart.core.http.post.mockClear(); - npStart.core.http.put.mockClear(); + startMock.http.get.mockClear(); + startMock.http.post.mockClear(); + startMock.http.put.mockClear(); }); describe('layout', () => { @@ -73,7 +72,7 @@ describe('Create Rollup Job, step 2: Date histogram', () => { describe('Date field select', () => { it('should set the options value from the index pattern', async () => { const dateFields = ['field1', 'field2', 'field3']; - mockHttpRequest(npStart.core.http, { indxPatternVldtResp: { dateFields } }); + mockHttpRequest(startMock.http, { indxPatternVldtResp: { dateFields } }); await goToStep(2); @@ -85,7 +84,7 @@ describe('Create Rollup Job, step 2: Date histogram', () => { it('should sort the options in ascending order', async () => { const dateFields = ['field3', 'field2', 'field1']; - mockHttpRequest(npStart.core.http, { indxPatternVldtResp: { dateFields } }); + mockHttpRequest(startMock.http, { indxPatternVldtResp: { dateFields } }); await goToStep(2); diff --git a/x-pack/legacy/plugins/rollup/__jest__/client_integration/job_create_histogram.test.js b/x-pack/plugins/rollup/public/test/client_integration/job_create_histogram.test.js similarity index 91% rename from x-pack/legacy/plugins/rollup/__jest__/client_integration/job_create_histogram.test.js rename to x-pack/plugins/rollup/public/test/client_integration/job_create_histogram.test.js index c4b5d753f1a26..141aa06843556 100644 --- a/x-pack/legacy/plugins/rollup/__jest__/client_integration/job_create_histogram.test.js +++ b/x-pack/plugins/rollup/public/test/client_integration/job_create_histogram.test.js @@ -4,10 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ -import { setHttp } from '../../public/crud_app/services'; +import { setHttp } from '../../crud_app/services'; import { mockHttpRequest, pageHelpers } from './helpers'; - -jest.mock('ui/new_platform'); +import { coreMock } from '../../../../../../src/core/public/mocks'; jest.mock('lodash/function/debounce', () => fn => fn); @@ -21,24 +20,24 @@ describe('Create Rollup Job, step 4: Histogram', () => { let goToStep; let table; let form; - let npStart; + let startMock; beforeAll(() => { - npStart = require('ui/new_platform').npStart; // eslint-disable-line - setHttp(npStart.core.http); + startMock = coreMock.createStart(); + setHttp(startMock.http); }); beforeEach(() => { // Set "default" mock responses by not providing any arguments - mockHttpRequest(npStart.core.http); + mockHttpRequest(startMock.http); ({ find, exists, actions, getEuiStepsHorizontalActive, goToStep, table, form } = setup()); }); afterEach(() => { - npStart.core.http.get.mockClear(); - npStart.core.http.post.mockClear(); - npStart.core.http.put.mockClear(); + startMock.http.get.mockClear(); + startMock.http.post.mockClear(); + startMock.http.put.mockClear(); }); const numericFields = ['a-numericField', 'b-numericField']; @@ -111,7 +110,7 @@ describe('Create Rollup Job, step 4: Histogram', () => { describe('when no histogram fields are availalbe', () => { it('should indicate it to the user', async () => { - mockHttpRequest(npStart.core.http, { indxPatternVldtResp: { numericFields: [] } }); + mockHttpRequest(startMock.http, { indxPatternVldtResp: { numericFields: [] } }); await goToStepAndOpenFieldChooser(); const { tableCellsValues } = table.getMetaData('rollupJobHistogramFieldChooser-table'); @@ -122,7 +121,7 @@ describe('Create Rollup Job, step 4: Histogram', () => { describe('when histogram fields are available', () => { beforeEach(async () => { - mockHttpRequest(npStart.core.http, { indxPatternVldtResp: { numericFields } }); + mockHttpRequest(startMock.http, { indxPatternVldtResp: { numericFields } }); await goToStepAndOpenFieldChooser(); }); @@ -156,7 +155,7 @@ describe('Create Rollup Job, step 4: Histogram', () => { it('should have a delete button on each row to remove an histogram field', async () => { // First let's add a term to the list - mockHttpRequest(npStart.core.http, { indxPatternVldtResp: { numericFields } }); + mockHttpRequest(startMock.http, { indxPatternVldtResp: { numericFields } }); await goToStepAndOpenFieldChooser(); const { rows: fieldChooserRows } = table.getMetaData('rollupJobHistogramFieldChooser-table'); fieldChooserRows[0].reactWrapper.simulate('click'); @@ -183,7 +182,7 @@ describe('Create Rollup Job, step 4: Histogram', () => { }; beforeEach(async () => { - mockHttpRequest(npStart.core.http, { indxPatternVldtResp: { numericFields } }); + mockHttpRequest(startMock.http, { indxPatternVldtResp: { numericFields } }); await goToStep(4); addHistogramFieldToList(); }); diff --git a/x-pack/legacy/plugins/rollup/__jest__/client_integration/job_create_logistics.test.js b/x-pack/plugins/rollup/public/test/client_integration/job_create_logistics.test.js similarity index 96% rename from x-pack/legacy/plugins/rollup/__jest__/client_integration/job_create_logistics.test.js rename to x-pack/plugins/rollup/public/test/client_integration/job_create_logistics.test.js index ebc03e40cc2b9..7b89244bb52a9 100644 --- a/x-pack/legacy/plugins/rollup/__jest__/client_integration/job_create_logistics.test.js +++ b/x-pack/plugins/rollup/public/test/client_integration/job_create_logistics.test.js @@ -13,10 +13,9 @@ import { YEAR, } from '../../../../../../src/plugins/es_ui_shared/public'; import { indexPatterns } from '../../../../../../src/plugins/data/public'; -import { setHttp } from '../../public/crud_app/services'; +import { setHttp } from '../../crud_app/services'; import { mockHttpRequest, pageHelpers } from './helpers'; - -jest.mock('ui/new_platform'); +import { coreMock } from '../../../../../../src/core/public/mocks'; jest.mock('lodash/function/debounce', () => fn => fn); @@ -28,24 +27,24 @@ describe('Create Rollup Job, step 1: Logistics', () => { let actions; let form; let getEuiStepsHorizontalActive; - let npStart; + let startMock; beforeAll(() => { - npStart = require('ui/new_platform').npStart; // eslint-disable-line - setHttp(npStart.core.http); + startMock = coreMock.createStart(); + setHttp(startMock.http); }); beforeEach(() => { // Set "default" mock responses by not providing any arguments - mockHttpRequest(npStart.core.http); + mockHttpRequest(startMock.http); ({ find, exists, actions, form, getEuiStepsHorizontalActive } = setup()); }); afterEach(() => { - npStart.core.http.get.mockClear(); - npStart.core.http.post.mockClear(); - npStart.core.http.put.mockClear(); + startMock.http.get.mockClear(); + startMock.http.post.mockClear(); + startMock.http.put.mockClear(); }); it('should have the horizontal step active on "Logistics"', () => { @@ -97,14 +96,14 @@ describe('Create Rollup Job, step 1: Logistics', () => { }); it('should not allow an unknown index pattern', async () => { - mockHttpRequest(npStart.core.http, { indxPatternVldtResp: { doesMatchIndices: false } }); + mockHttpRequest(startMock.http, { indxPatternVldtResp: { doesMatchIndices: false } }); await form.setInputValue('rollupIndexPattern', 'unknown', true); actions.clickNextStep(); expect(form.getErrorsMessages()).toContain("Index pattern doesn't match any indices."); }); it('should not allow an index pattern without time fields', async () => { - mockHttpRequest(npStart.core.http, { indxPatternVldtResp: { dateFields: [] } }); + mockHttpRequest(startMock.http, { indxPatternVldtResp: { dateFields: [] } }); await form.setInputValue('rollupIndexPattern', 'abc', true); actions.clickNextStep(); expect(form.getErrorsMessages()).toContain( @@ -113,7 +112,7 @@ describe('Create Rollup Job, step 1: Logistics', () => { }); it('should not allow an index pattern that matches a rollup index', async () => { - mockHttpRequest(npStart.core.http, { + mockHttpRequest(startMock.http, { indxPatternVldtResp: { doesMatchRollupIndices: true }, }); await form.setInputValue('rollupIndexPattern', 'abc', true); diff --git a/x-pack/legacy/plugins/rollup/__jest__/client_integration/job_create_metrics.test.js b/x-pack/plugins/rollup/public/test/client_integration/job_create_metrics.test.js similarity index 95% rename from x-pack/legacy/plugins/rollup/__jest__/client_integration/job_create_metrics.test.js rename to x-pack/plugins/rollup/public/test/client_integration/job_create_metrics.test.js index a72dc8b25c083..1e9dd88648da6 100644 --- a/x-pack/legacy/plugins/rollup/__jest__/client_integration/job_create_metrics.test.js +++ b/x-pack/plugins/rollup/public/test/client_integration/job_create_metrics.test.js @@ -4,10 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ -import { setHttp } from '../../public/crud_app/services'; +import { setHttp } from '../../crud_app/services'; import { mockHttpRequest, pageHelpers } from './helpers'; - -jest.mock('ui/new_platform'); +import { coreMock } from '../../../../../../src/core/public/mocks'; jest.mock('lodash/function/debounce', () => fn => fn); @@ -21,24 +20,24 @@ describe('Create Rollup Job, step 5: Metrics', () => { let goToStep; let table; let metrics; - let npStart; + let startMock; beforeAll(() => { - npStart = require('ui/new_platform').npStart; // eslint-disable-line - setHttp(npStart.core.http); + startMock = coreMock.createStart(); + setHttp(startMock.http); }); beforeEach(() => { // Set "default" mock responses by not providing any arguments - mockHttpRequest(npStart.core.http); + mockHttpRequest(startMock.http); ({ find, exists, actions, getEuiStepsHorizontalActive, goToStep, table, metrics } = setup()); }); afterEach(() => { - npStart.core.http.get.mockClear(); - npStart.core.http.post.mockClear(); - npStart.core.http.put.mockClear(); + startMock.http.get.mockClear(); + startMock.http.post.mockClear(); + startMock.http.put.mockClear(); }); const numericFields = ['a-numericField', 'c-numericField']; @@ -112,7 +111,7 @@ describe('Create Rollup Job, step 5: Metrics', () => { describe('table', () => { beforeEach(async () => { - mockHttpRequest(npStart.core.http, { indxPatternVldtResp: { numericFields, dateFields } }); + mockHttpRequest(startMock.http, { indxPatternVldtResp: { numericFields, dateFields } }); await goToStepAndOpenFieldChooser(); }); @@ -169,7 +168,7 @@ describe('Create Rollup Job, step 5: Metrics', () => { describe('when fields are added', () => { beforeEach(async () => { - mockHttpRequest(npStart.core.http, { indxPatternVldtResp: { numericFields, dateFields } }); + mockHttpRequest(startMock.http, { indxPatternVldtResp: { numericFields, dateFields } }); await goToStepAndOpenFieldChooser(); }); @@ -260,7 +259,7 @@ describe('Create Rollup Job, step 5: Metrics', () => { let getFieldListTableRows; beforeEach(async () => { - mockHttpRequest(npStart.core.http, { indxPatternVldtResp: { numericFields, dateFields } }); + mockHttpRequest(startMock.http, { indxPatternVldtResp: { numericFields, dateFields } }); await goToStep(5); await addFieldToList('numeric'); diff --git a/x-pack/plugins/rollup/public/test/client_integration/job_create_review.test.js b/x-pack/plugins/rollup/public/test/client_integration/job_create_review.test.js new file mode 100644 index 0000000000000..d625b8f11208f --- /dev/null +++ b/x-pack/plugins/rollup/public/test/client_integration/job_create_review.test.js @@ -0,0 +1,182 @@ +/* + * 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 { pageHelpers, mockHttpRequest } from './helpers'; +import { first } from 'lodash'; +import { setHttp } from '../../crud_app/services'; +import { JOBS } from './helpers/constants'; +import { coreMock } from '../../../../../../src/core/public/mocks'; + +jest.mock('lodash/function/debounce', () => fn => fn); + +jest.mock('../../kibana_services', () => { + const services = require.requireActual('../../kibana_services'); + return { + ...services, + getUiStatsReporter: jest.fn(() => () => {}), + }; +}); + +const { setup } = pageHelpers.jobCreate; + +describe('Create Rollup Job, step 6: Review', () => { + let find; + let exists; + let actions; + let getEuiStepsHorizontalActive; + let goToStep; + let table; + let form; + let startMock; + + beforeAll(() => { + startMock = coreMock.createStart(); + setHttp(startMock.http); + }); + + beforeEach(() => { + // Set "default" mock responses by not providing any arguments + mockHttpRequest(startMock.http); + ({ find, exists, actions, getEuiStepsHorizontalActive, goToStep, table, form } = setup()); + }); + + afterEach(() => { + startMock.http.get.mockClear(); + startMock.http.post.mockClear(); + startMock.http.put.mockClear(); + }); + + describe('layout', () => { + beforeEach(async () => { + await goToStep(6); + }); + + it('should have the horizontal step active on "Review"', () => { + expect(getEuiStepsHorizontalActive()).toContain('Review'); + }); + + it('should have the title set to "Review"', () => { + expect(exists('rollupJobCreateReviewTitle')).toBe(true); + }); + + it('should have the "next" and "save" button visible', () => { + expect(exists('rollupJobBackButton')).toBe(true); + expect(exists('rollupJobNextButton')).toBe(false); + expect(exists('rollupJobSaveButton')).toBe(true); + }); + + it('should go to the "Metrics" step when clicking the back button', async () => { + actions.clickPreviousStep(); + expect(getEuiStepsHorizontalActive()).toContain('Metrics'); + }); + }); + + describe('tabs', () => { + const getTabsText = () => find('stepReviewTab').map(tab => tab.text()); + const selectFirstField = step => { + find('rollupJobShowFieldChooserButton').simulate('click'); + + // Select the first term field + table + .getMetaData(`rollupJob${step}FieldChooser-table`) + .rows[0].reactWrapper.simulate('click'); + }; + + it('should have a "Summary" & "Request" tabs to review the Job', async () => { + await goToStep(6); + expect(getTabsText()).toEqual(['Summary', 'Request']); + }); + + it('should have a "Summary", "Terms" & "Request" tab if a term aggregation was added', async () => { + mockHttpRequest(startMock.http, { indxPatternVldtResp: { numericFields: ['my-field'] } }); + await goToStep(3); + selectFirstField('Terms'); + + actions.clickNextStep(); // go to step 4 + actions.clickNextStep(); // go to step 5 + actions.clickNextStep(); // go to review + + expect(getTabsText()).toEqual(['Summary', 'Terms', 'Request']); + }); + + it('should have a "Summary", "Histogram" & "Request" tab if a histogram field was added', async () => { + mockHttpRequest(startMock.http, { indxPatternVldtResp: { numericFields: ['a-field'] } }); + await goToStep(4); + selectFirstField('Histogram'); + form.setInputValue('rollupJobCreateHistogramInterval', 3); // set an interval + + actions.clickNextStep(); // go to step 5 + actions.clickNextStep(); // go to review + + expect(getTabsText()).toEqual(['Summary', 'Histogram', 'Request']); + }); + + it('should have a "Summary", "Metrics" & "Request" tab if a histogram field was added', async () => { + mockHttpRequest(startMock.http, { + indxPatternVldtResp: { + numericFields: ['a-field'], + dateFields: ['b-field'], + }, + }); + await goToStep(5); + selectFirstField('Metrics'); + form.selectCheckBox('rollupJobMetricsCheckbox-avg'); // select a metric + + actions.clickNextStep(); // go to review + + expect(getTabsText()).toEqual(['Summary', 'Metrics', 'Request']); + }); + }); + + describe('save()', () => { + const jobCreateApiPath = '/api/rollup/create'; + const jobStartApiPath = '/api/rollup/start'; + + describe('without starting job after creation', () => { + it('should call the "create" Api server endpoint', async () => { + mockHttpRequest(startMock.http, { + createdJob: first(JOBS.jobs), + }); + + await goToStep(6); + + expect(startMock.http.put).not.toHaveBeenCalledWith(jobCreateApiPath); // make sure it hasn't been called + expect(startMock.http.get).not.toHaveBeenCalledWith(jobStartApiPath); // make sure it hasn't been called + + actions.clickSave(); + // Given the following anti-jitter sleep x-pack/plugins/rollup/public/crud_app/store/actions/create_job.js + // we add a longer sleep here :( + await new Promise(res => setTimeout(res, 750)); + + expect(startMock.http.put).toHaveBeenCalledWith(jobCreateApiPath, expect.anything()); // It has been called! + expect(startMock.http.get).not.toHaveBeenCalledWith(jobStartApiPath); // It has still not been called! + }); + }); + + describe('with starting job after creation', () => { + it('should call the "create" and "start" Api server endpoints', async () => { + mockHttpRequest(startMock.http, { + createdJob: first(JOBS.jobs), + }); + + await goToStep(6); + + find('rollupJobToggleJobStartAfterCreation').simulate('change', { + target: { checked: true }, + }); + + expect(startMock.http.post).not.toHaveBeenCalledWith(jobStartApiPath); // make sure it hasn't been called + + actions.clickSave(); + // Given the following anti-jitter sleep x-pack/plugins/rollup/public/crud_app/store/actions/create_job.js + // we add a longer sleep here :( + await new Promise(res => setTimeout(res, 750)); + + expect(startMock.http.post).toHaveBeenCalledWith(jobStartApiPath, expect.anything()); // It has been called! + }); + }); + }); +}); diff --git a/x-pack/legacy/plugins/rollup/__jest__/client_integration/job_create_terms.test.js b/x-pack/plugins/rollup/public/test/client_integration/job_create_terms.test.js similarity index 93% rename from x-pack/legacy/plugins/rollup/__jest__/client_integration/job_create_terms.test.js rename to x-pack/plugins/rollup/public/test/client_integration/job_create_terms.test.js index f111a7df2c250..61993f3092840 100644 --- a/x-pack/legacy/plugins/rollup/__jest__/client_integration/job_create_terms.test.js +++ b/x-pack/plugins/rollup/public/test/client_integration/job_create_terms.test.js @@ -4,10 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ -import { setHttp } from '../../public/crud_app/services'; +import { setHttp } from '../../crud_app/services'; import { pageHelpers, mockHttpRequest } from './helpers'; - -jest.mock('ui/new_platform'); +import { coreMock } from '../../../../../../src/core/public/mocks'; jest.mock('lodash/function/debounce', () => fn => fn); @@ -20,22 +19,22 @@ describe('Create Rollup Job, step 3: Terms', () => { let getEuiStepsHorizontalActive; let goToStep; let table; - let npStart; + let startMock; beforeAll(() => { - npStart = require('ui/new_platform').npStart; // eslint-disable-line - setHttp(npStart.core.http); + startMock = coreMock.createStart(); + setHttp(startMock.http); }); beforeEach(() => { // Set "default" mock responses by not providing any arguments - mockHttpRequest(npStart.core.http); + mockHttpRequest(startMock.http); ({ find, exists, actions, getEuiStepsHorizontalActive, goToStep, table } = setup()); }); afterEach(() => { - npStart.core.http.get.mockClear(); + startMock.http.get.mockClear(); }); const numericFields = ['a-numericField', 'c-numericField']; @@ -109,7 +108,7 @@ describe('Create Rollup Job, step 3: Terms', () => { describe('when no terms are available', () => { it('should indicate it to the user', async () => { - mockHttpRequest(npStart.core.http, { + mockHttpRequest(startMock.http, { indxPatternVldtResp: { numericFields: [], keywordFields: [], @@ -125,7 +124,7 @@ describe('Create Rollup Job, step 3: Terms', () => { describe('when terms are available', () => { beforeEach(async () => { - mockHttpRequest(npStart.core.http, { + mockHttpRequest(startMock.http, { indxPatternVldtResp: { numericFields, keywordFields, @@ -171,7 +170,7 @@ describe('Create Rollup Job, step 3: Terms', () => { it('should have a delete button on each row to remove a term', async () => { // First let's add a term to the list - mockHttpRequest(npStart.core.http, { + mockHttpRequest(startMock.http, { indxPatternVldtResp: { numericFields, keywordFields, diff --git a/x-pack/plugins/rollup/public/test/client_integration/job_list.test.js b/x-pack/plugins/rollup/public/test/client_integration/job_list.test.js new file mode 100644 index 0000000000000..c6988236d6b7c --- /dev/null +++ b/x-pack/plugins/rollup/public/test/client_integration/job_list.test.js @@ -0,0 +1,94 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { getRouter, setHttp } from '../../crud_app/services'; +import { mockHttpRequest, pageHelpers, nextTick } from './helpers'; +import { JOBS } from './helpers/constants'; +import { coreMock } from '../../../../../../src/core/public/mocks'; + +jest.mock('../../crud_app/services', () => { + const services = require.requireActual('../../crud_app/services'); + return { + ...services, + getRouterLinkProps: link => ({ href: link }), + }; +}); + +jest.mock('../../kibana_services', () => { + const services = require.requireActual('../../kibana_services'); + return { + ...services, + getUiStatsReporter: jest.fn(() => () => {}), + }; +}); + +const { setup } = pageHelpers.jobList; + +describe('<JobList />', () => { + describe('detail panel', () => { + let component; + let table; + let exists; + let startMock; + + beforeAll(() => { + startMock = coreMock.createStart(); + setHttp(startMock.http); + }); + + beforeEach(async () => { + mockHttpRequest(startMock.http, { jobs: JOBS }); + + ({ component, exists, table } = setup()); + + await nextTick(); // We need to wait next tick for the mock server response to comes in + component.update(); + }); + + afterEach(() => { + startMock.http.get.mockClear(); + }); + + test('should open the detail panel when clicking on a job in the table', () => { + const { rows } = table.getMetaData('rollupJobsListTable'); + const button = rows[0].columns[1].reactWrapper.find('button'); + + expect(exists('rollupJobDetailFlyout')).toBe(false); // make sure it is not shown + + button.simulate('click'); + + expect(exists('rollupJobDetailFlyout')).toBe(true); + }); + + test('should add the Job id to the route query params when opening the detail panel', () => { + const { rows } = table.getMetaData('rollupJobsListTable'); + const button = rows[0].columns[1].reactWrapper.find('button'); + + expect(getRouter().history.location.search).toEqual(''); + + button.simulate('click'); + + const { + jobs: [ + { + config: { id: jobId }, + }, + ], + } = JOBS; + expect(getRouter().history.location.search).toEqual(`?job=${jobId}`); + }); + + test('should open the detail panel whenever a job id is added to the query params', () => { + expect(exists('rollupJobDetailFlyout')).toBe(false); // make sure it is not shown + + getRouter().history.replace({ search: `?job=bar` }); + + component.update(); + + expect(exists('rollupJobDetailFlyout')).toBe(true); + }); + }); +}); diff --git a/x-pack/plugins/rollup/public/test/client_integration/job_list_clone.test.js b/x-pack/plugins/rollup/public/test/client_integration/job_list_clone.test.js new file mode 100644 index 0000000000000..bdf57a555cdad --- /dev/null +++ b/x-pack/plugins/rollup/public/test/client_integration/job_list_clone.test.js @@ -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 { mockHttpRequest, pageHelpers, nextTick } from './helpers'; +import { JOB_TO_CLONE, JOB_CLONE_INDEX_PATTERN_CHECK } from './helpers/constants'; +import { getRouter } from '../../crud_app/services/routing'; +import { setHttp } from '../../crud_app/services'; +import { CRUD_APP_BASE_PATH } from '../../crud_app/constants'; +import { coreMock } from '../../../../../../src/core/public/mocks'; + +jest.mock('lodash/function/debounce', () => fn => fn); + +jest.mock('../../kibana_services', () => { + const services = require.requireActual('../../kibana_services'); + return { + ...services, + getUiStatsReporter: jest.fn(() => () => {}), + }; +}); + +const { setup } = pageHelpers.jobList; + +describe('Smoke test cloning an existing rollup job from job list', () => { + let table; + let find; + let component; + let exists; + let startMock; + + beforeAll(() => { + startMock = coreMock.createStart(); + setHttp(startMock.http); + }); + + beforeEach(async () => { + mockHttpRequest(startMock.http, { + jobs: JOB_TO_CLONE, + indxPatternVldtResp: JOB_CLONE_INDEX_PATTERN_CHECK, + }); + + ({ find, exists, table, component } = setup()); + + await nextTick(); // We need to wait next tick for the mock server response to comes in + component.update(); + }); + + afterEach(() => { + startMock.http.get.mockClear(); + }); + + it('should navigate to create view with default values set', async () => { + const router = getRouter(); + const { rows } = table.getMetaData('rollupJobsListTable'); + const button = rows[0].columns[1].reactWrapper.find('button'); + + expect(exists('rollupJobDetailFlyout')).toBe(false); // make sure it is not shown + + button.simulate('click'); + + expect(exists('rollupJobDetailFlyout')).toBe(true); + expect(exists('jobActionMenuButton')).toBe(true); + + find('jobActionMenuButton').simulate('click'); + + expect(router.history.location.pathname).not.toBe(`${CRUD_APP_BASE_PATH}/create`); + find('jobCloneActionContextMenu').simulate('click'); + expect(router.history.location.pathname).toBe(`${CRUD_APP_BASE_PATH}/create`); + }); +}); diff --git a/x-pack/legacy/plugins/rollup/public/visualize/agg_type_field_filter.js b/x-pack/plugins/rollup/public/visualize/agg_type_field_filter.js similarity index 100% rename from x-pack/legacy/plugins/rollup/public/visualize/agg_type_field_filter.js rename to x-pack/plugins/rollup/public/visualize/agg_type_field_filter.js diff --git a/x-pack/legacy/plugins/rollup/public/visualize/agg_type_filter.js b/x-pack/plugins/rollup/public/visualize/agg_type_filter.js similarity index 100% rename from x-pack/legacy/plugins/rollup/public/visualize/agg_type_filter.js rename to x-pack/plugins/rollup/public/visualize/agg_type_filter.js diff --git a/x-pack/plugins/rollup/server/plugin.ts b/x-pack/plugins/rollup/server/plugin.ts index fa05b8d1307d6..ea6d197e22029 100644 --- a/x-pack/plugins/rollup/server/plugin.ts +++ b/x-pack/plugins/rollup/server/plugin.ts @@ -4,7 +4,10 @@ * you may not use this file except in compliance with the Elastic License. */ -import { Plugin, PluginInitializerContext } from 'src/core/server'; +import { CoreSetup, Plugin, PluginInitializerContext } from 'src/core/server'; +import { i18n } from '@kbn/i18n'; +import { schema } from '@kbn/config-schema'; +import { CONFIG_ROLLUPS } from '../common'; export class RollupPlugin implements Plugin<RollupSetup> { private readonly initContext: PluginInitializerContext; @@ -13,7 +16,23 @@ export class RollupPlugin implements Plugin<RollupSetup> { this.initContext = initContext; } - public setup() { + public setup(core: CoreSetup) { + core.uiSettings.register({ + [CONFIG_ROLLUPS]: { + name: i18n.translate('xpack.rollupJobs.rollupIndexPatternsTitle', { + defaultMessage: 'Enable rollup index patterns', + }), + value: true, + description: i18n.translate('xpack.rollupJobs.rollupIndexPatternsDescription', { + defaultMessage: `Enable the creation of index patterns which capture rollup indices, + which in turn enable visualizations based on rollup data. Refresh + the page to apply the changes.`, + }), + category: ['rollups'], + schema: schema.boolean(), + }, + }); + return { __legacy: { config: this.initContext.config, diff --git a/x-pack/plugins/searchprofiler/public/application/editor/editor.test.tsx b/x-pack/plugins/searchprofiler/public/application/editor/editor.test.tsx index ad56b3957eb74..e56fc79156666 100644 --- a/x-pack/plugins/searchprofiler/public/application/editor/editor.test.tsx +++ b/x-pack/plugins/searchprofiler/public/application/editor/editor.test.tsx @@ -6,8 +6,6 @@ import 'brace'; import 'brace/mode/json'; -import '../../../../es_ui_shared/console_lang/mocks'; - import { registerTestBed } from '../../../../../test_utils'; import { Editor, Props } from '.'; diff --git a/x-pack/plugins/searchprofiler/public/application/editor/init_editor.ts b/x-pack/plugins/searchprofiler/public/application/editor/init_editor.ts index 6f19ce12eb639..3ad92531e4367 100644 --- a/x-pack/plugins/searchprofiler/public/application/editor/init_editor.ts +++ b/x-pack/plugins/searchprofiler/public/application/editor/init_editor.ts @@ -5,7 +5,7 @@ */ import ace from 'brace'; -import { installXJsonMode } from '../../../../es_ui_shared/console_lang'; +import { installXJsonMode } from '../../../../../../src/plugins/es_ui_shared/public'; export function initializeEditor({ el, diff --git a/x-pack/plugins/searchprofiler/public/application/utils/check_for_json_errors.ts b/x-pack/plugins/searchprofiler/public/application/utils/check_for_json_errors.ts index 99687de0f1440..58a62c4636c25 100644 --- a/x-pack/plugins/searchprofiler/public/application/utils/check_for_json_errors.ts +++ b/x-pack/plugins/searchprofiler/public/application/utils/check_for_json_errors.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { collapseLiteralStrings } from '../../../../../../src/plugins/es_ui_shared/console_lang/lib'; +import { collapseLiteralStrings } from '../../../../../../src/plugins/es_ui_shared/public'; export function checkForParseErrors(json: string) { const sanitizedJson = collapseLiteralStrings(json); diff --git a/x-pack/plugins/security/common/licensing/license_service.test.ts b/x-pack/plugins/security/common/licensing/license_service.test.ts index dfc94042ef930..5bdfa7d4886aa 100644 --- a/x-pack/plugins/security/common/licensing/license_service.test.ts +++ b/x-pack/plugins/security/common/licensing/license_service.test.ts @@ -78,14 +78,14 @@ describe('license features', function() { expect(subscriptionHandler.mock.calls[1]).toMatchInlineSnapshot(` Array [ Object { - "allowLogin": false, - "allowRbac": false, - "allowRoleDocumentLevelSecurity": false, - "allowRoleFieldLevelSecurity": false, - "allowSubFeaturePrivileges": false, - "showLinks": false, - "showLogin": false, - "showRoleMappingsManagement": false, + "allowLogin": true, + "allowRbac": true, + "allowRoleDocumentLevelSecurity": true, + "allowRoleFieldLevelSecurity": true, + "allowSubFeaturePrivileges": true, + "showLinks": true, + "showLogin": true, + "showRoleMappingsManagement": true, }, ] `); diff --git a/x-pack/plugins/security/server/audit/audit_logger.test.ts b/x-pack/plugins/security/server/audit/audit_logger.test.ts index 01cde02b7dfdd..f7ee210a21a74 100644 --- a/x-pack/plugins/security/server/audit/audit_logger.test.ts +++ b/x-pack/plugins/security/server/audit/audit_logger.test.ts @@ -18,25 +18,46 @@ describe(`#savedObjectsAuthorizationFailure`, () => { const username = 'foo-user'; const action = 'foo-action'; const types = ['foo-type-1', 'foo-type-2']; - const missing = [`saved_object:${types[0]}/foo-action`, `saved_object:${types[1]}/foo-action`]; + const spaceIds = ['foo-space', 'bar-space']; + const missing = [ + { + spaceId: 'foo-space', + privilege: `saved_object:${types[0]}/${action}`, + }, + { + spaceId: 'foo-space', + privilege: `saved_object:${types[1]}/${action}`, + }, + ]; const args = { foo: 'bar', baz: 'quz', }; - securityAuditLogger.savedObjectsAuthorizationFailure(username, action, types, missing, args); + securityAuditLogger.savedObjectsAuthorizationFailure( + username, + action, + types, + spaceIds, + missing, + args + ); expect(auditLogger.log).toHaveBeenCalledWith( 'saved_objects_authorization_failure', - expect.stringContaining(`${username} unauthorized to ${action}`), + expect.any(String), { username, action, types, + spaceIds, missing, args, } ); + expect(auditLogger.log.mock.calls[0][1]).toMatchInlineSnapshot( + `"foo-user unauthorized to [foo-action] [foo-type-1,foo-type-2] in [foo-space,bar-space]: missing [(foo-space)saved_object:foo-type-1/foo-action,(foo-space)saved_object:foo-type-2/foo-action]"` + ); }); }); @@ -47,22 +68,27 @@ describe(`#savedObjectsAuthorizationSuccess`, () => { const username = 'foo-user'; const action = 'foo-action'; const types = ['foo-type-1', 'foo-type-2']; + const spaceIds = ['foo-space', 'bar-space']; const args = { foo: 'bar', baz: 'quz', }; - securityAuditLogger.savedObjectsAuthorizationSuccess(username, action, types, args); + securityAuditLogger.savedObjectsAuthorizationSuccess(username, action, types, spaceIds, args); expect(auditLogger.log).toHaveBeenCalledWith( 'saved_objects_authorization_success', - expect.stringContaining(`${username} authorized to ${action}`), + expect.any(String), { username, action, types, + spaceIds, args, } ); + expect(auditLogger.log.mock.calls[0][1]).toMatchInlineSnapshot( + `"foo-user authorized to [foo-action] [foo-type-1,foo-type-2] in [foo-space,bar-space]"` + ); }); }); diff --git a/x-pack/plugins/security/server/audit/audit_logger.ts b/x-pack/plugins/security/server/audit/audit_logger.ts index df8df35f97b49..40b525b5d2188 100644 --- a/x-pack/plugins/security/server/audit/audit_logger.ts +++ b/x-pack/plugins/security/server/audit/audit_logger.ts @@ -13,16 +13,23 @@ export class SecurityAuditLogger { username: string, action: string, types: string[], - missing: string[], + spaceIds: string[], + missing: Array<{ spaceId?: string; privilege: string }>, args?: Record<string, unknown> ) { + const typesString = types.join(','); + const spacesString = spaceIds.length ? ` in [${spaceIds.join(',')}]` : ''; + const missingString = missing + .map(({ spaceId, privilege }) => `${spaceId ? `(${spaceId})` : ''}${privilege}`) + .join(','); this.getAuditLogger().log( 'saved_objects_authorization_failure', - `${username} unauthorized to ${action} ${types.join(',')}, missing ${missing.join(',')}`, + `${username} unauthorized to [${action}] [${typesString}]${spacesString}: missing [${missingString}]`, { username, action, types, + spaceIds, missing, args, } @@ -33,15 +40,19 @@ export class SecurityAuditLogger { username: string, action: string, types: string[], + spaceIds: string[], args?: Record<string, unknown> ) { + const typesString = types.join(','); + const spacesString = spaceIds.length ? ` in [${spaceIds.join(',')}]` : ''; this.getAuditLogger().log( 'saved_objects_authorization_success', - `${username} authorized to ${action} ${types.join(',')}`, + `${username} authorized to [${action}] [${typesString}]${spacesString}`, { username, action, types, + spaceIds, args, } ); diff --git a/x-pack/plugins/security/server/authorization/__snapshots__/check_privileges.test.ts.snap b/x-pack/plugins/security/server/authorization/__snapshots__/check_privileges.test.ts.snap deleted file mode 100644 index 1212c2cd6a5cb..0000000000000 --- a/x-pack/plugins/security/server/authorization/__snapshots__/check_privileges.test.ts.snap +++ /dev/null @@ -1,27 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`#atSpace throws error when checking for login and user has login but doesn't have version 1`] = `[Error: Multiple versions of Kibana are running against the same Elasticsearch cluster, unable to authorize user.]`; - -exports[`#atSpace with a malformed Elasticsearch response throws a validation error when an extra privilege is present in the response 1`] = `[Error: Invalid response received from Elasticsearch has_privilege endpoint. ValidationError: child "application" fails because [child "kibana-our_application" fails because [child "space:space_1" fails because ["saved_object:bar-type/get" is not allowed]]]]`; - -exports[`#atSpace with a malformed Elasticsearch response throws a validation error when privileges are missing in the response 1`] = `[Error: Invalid response received from Elasticsearch has_privilege endpoint. ValidationError: child "application" fails because [child "kibana-our_application" fails because [child "space:space_1" fails because [child "saved_object:foo-type/get" fails because ["saved_object:foo-type/get" is required]]]]]`; - -exports[`#atSpaces throws error when Elasticsearch returns malformed response 1`] = `[Error: Invalid response received from Elasticsearch has_privilege endpoint. ValidationError: child "application" fails because [child "kibana-our_application" fails because [child "space:space_1" fails because [child "mock-action:version" fails because ["mock-action:version" is required]]]]]`; - -exports[`#atSpaces throws error when checking for login and user has login but doesn't have version 1`] = `[Error: Multiple versions of Kibana are running against the same Elasticsearch cluster, unable to authorize user.]`; - -exports[`#atSpaces with a malformed Elasticsearch response throws a validation error when an a space is missing in the response 1`] = `[Error: Invalid response received from Elasticsearch has_privilege endpoint. ValidationError: child "application" fails because [child "kibana-our_application" fails because [child "space:space_2" fails because ["space:space_2" is required]]]]`; - -exports[`#atSpaces with a malformed Elasticsearch response throws a validation error when an extra privilege is present in the response 1`] = `[Error: Invalid response received from Elasticsearch has_privilege endpoint. ValidationError: child "application" fails because [child "kibana-our_application" fails because [child "space:space_2" fails because ["space:space_2" is required]]]]`; - -exports[`#atSpaces with a malformed Elasticsearch response throws a validation error when an extra space is present in the response 1`] = `[Error: Invalid response received from Elasticsearch has_privilege endpoint. ValidationError: child "application" fails because [child "kibana-our_application" fails because ["space:space_3" is not allowed]]]`; - -exports[`#atSpaces with a malformed Elasticsearch response throws a validation error when privileges are missing in the response 1`] = `[Error: Invalid response received from Elasticsearch has_privilege endpoint. ValidationError: child "application" fails because [child "kibana-our_application" fails because [child "space:space_2" fails because ["space:space_2" is required]]]]`; - -exports[`#globally throws error when Elasticsearch returns malformed response 1`] = `[Error: Invalid response received from Elasticsearch has_privilege endpoint. ValidationError: child "application" fails because [child "kibana-our_application" fails because [child "*" fails because [child "mock-action:version" fails because ["mock-action:version" is required]]]]]`; - -exports[`#globally throws error when checking for login and user has login but doesn't have version 1`] = `[Error: Multiple versions of Kibana are running against the same Elasticsearch cluster, unable to authorize user.]`; - -exports[`#globally with a malformed Elasticsearch response throws a validation error when an extra privilege is present in the response 1`] = `[Error: Invalid response received from Elasticsearch has_privilege endpoint. ValidationError: child "application" fails because [child "kibana-our_application" fails because [child "*" fails because ["saved_object:bar-type/get" is not allowed]]]]`; - -exports[`#globally with a malformed Elasticsearch response throws a validation error when privileges are missing in the response 1`] = `[Error: Invalid response received from Elasticsearch has_privilege endpoint. ValidationError: child "application" fails because [child "kibana-our_application" fails because [child "*" fails because [child "saved_object:foo-type/get" fails because ["saved_object:foo-type/get" is required]]]]]`; diff --git a/x-pack/plugins/security/server/authorization/api_authorization.ts b/x-pack/plugins/security/server/authorization/api_authorization.ts index cc672fbc69e06..88b3f2c6f7155 100644 --- a/x-pack/plugins/security/server/authorization/api_authorization.ts +++ b/x-pack/plugins/security/server/authorization/api_authorization.ts @@ -24,7 +24,6 @@ export function initAPIAuthorization( // if there are no tags starting with "access:", just continue if (actionTags.length === 0) { - logger.debug('API endpoint is not marked with "access:" tags, skipping.'); return toolkit.next(); } @@ -34,11 +33,11 @@ export function initAPIAuthorization( // we've actually authorized the request if (checkPrivilegesResponse.hasAllRequested) { - logger.debug(`authorized for "${request.url.path}"`); + logger.debug(`User authorized for "${request.url.path}"`); return toolkit.next(); } - logger.debug(`not authorized for "${request.url.path}"`); + logger.warn(`User not authorized for "${request.url.path}": responding with 404`); return response.notFound(); }); } diff --git a/x-pack/plugins/security/server/authorization/check_privileges.test.ts b/x-pack/plugins/security/server/authorization/check_privileges.test.ts index 8c1241937892e..a64c5d509ca11 100644 --- a/x-pack/plugins/security/server/authorization/check_privileges.test.ts +++ b/x-pack/plugins/security/server/authorization/check_privileges.test.ts @@ -31,123 +31,121 @@ const createMockClusterClient = (response: any) => { }; describe('#atSpace', () => { - const checkPrivilegesAtSpaceTest = ( - description: string, - options: { - spaceId: string; - privilegeOrPrivileges: string | string[]; - esHasPrivilegesResponse: HasPrivilegesResponse; - expectedResult?: any; - expectErrorThrown?: any; + const checkPrivilegesAtSpaceTest = async (options: { + spaceId: string; + privilegeOrPrivileges: string | string[]; + esHasPrivilegesResponse: HasPrivilegesResponse; + }) => { + const { mockClusterClient, mockScopedClusterClient } = createMockClusterClient( + options.esHasPrivilegesResponse + ); + const checkPrivilegesWithRequest = checkPrivilegesWithRequestFactory( + mockActions, + mockClusterClient, + application + ); + const request = httpServerMock.createKibanaRequest(); + const checkPrivileges = checkPrivilegesWithRequest(request); + + let actualResult; + let errorThrown = null; + try { + actualResult = await checkPrivileges.atSpace(options.spaceId, options.privilegeOrPrivileges); + } catch (err) { + errorThrown = err; } - ) => { - test(description, async () => { - const { mockClusterClient, mockScopedClusterClient } = createMockClusterClient( - options.esHasPrivilegesResponse - ); - const checkPrivilegesWithRequest = checkPrivilegesWithRequestFactory( - mockActions, - mockClusterClient, - application - ); - const request = httpServerMock.createKibanaRequest(); - const checkPrivileges = checkPrivilegesWithRequest(request); - - let actualResult; - let errorThrown = null; - try { - actualResult = await checkPrivileges.atSpace( - options.spaceId, - options.privilegeOrPrivileges - ); - } catch (err) { - errorThrown = err; - } - expect(mockClusterClient.asScoped).toHaveBeenCalledWith(request); - expect(mockScopedClusterClient.callAsCurrentUser).toHaveBeenCalledWith( - 'shield.hasPrivileges', - { - body: { - applications: [ - { - application, - resources: [`space:${options.spaceId}`], - privileges: uniq([ - mockActions.version, - mockActions.login, - ...(Array.isArray(options.privilegeOrPrivileges) - ? options.privilegeOrPrivileges - : [options.privilegeOrPrivileges]), - ]), - }, - ], + expect(mockClusterClient.asScoped).toHaveBeenCalledWith(request); + expect(mockScopedClusterClient.callAsCurrentUser).toHaveBeenCalledWith('shield.hasPrivileges', { + body: { + applications: [ + { + application, + resources: [`space:${options.spaceId}`], + privileges: uniq([ + mockActions.version, + mockActions.login, + ...(Array.isArray(options.privilegeOrPrivileges) + ? options.privilegeOrPrivileges + : [options.privilegeOrPrivileges]), + ]), }, - } - ); - - if (options.expectedResult) { - expect(errorThrown).toBeNull(); - expect(actualResult).toEqual(options.expectedResult); - } - - if (options.expectErrorThrown) { - expect(errorThrown).toMatchSnapshot(); - } + ], + }, }); + + if (errorThrown) { + return errorThrown; + } + return actualResult; }; - checkPrivilegesAtSpaceTest('successful when checking for login and user has login', { - spaceId: 'space_1', - privilegeOrPrivileges: mockActions.login, - esHasPrivilegesResponse: { - has_all_requested: true, - username: 'foo-username', - application: { - [application]: { - 'space:space_1': { - [mockActions.login]: true, - [mockActions.version]: true, + test('successful when checking for login and user has login', async () => { + const result = await checkPrivilegesAtSpaceTest({ + spaceId: 'space_1', + privilegeOrPrivileges: mockActions.login, + esHasPrivilegesResponse: { + has_all_requested: true, + username: 'foo-username', + application: { + [application]: { + 'space:space_1': { + [mockActions.login]: true, + [mockActions.version]: true, + }, }, }, }, - }, - expectedResult: { - hasAllRequested: true, - username: 'foo-username', - privileges: { - [mockActions.login]: true, - }, - }, + }); + expect(result).toMatchInlineSnapshot(` + Object { + "hasAllRequested": true, + "privileges": Array [ + Object { + "authorized": true, + "privilege": "mock-action:login", + "resource": "space_1", + }, + ], + "username": "foo-username", + } + `); }); - checkPrivilegesAtSpaceTest(`failure when checking for login and user doesn't have login`, { - spaceId: 'space_1', - privilegeOrPrivileges: mockActions.login, - esHasPrivilegesResponse: { - has_all_requested: false, - username: 'foo-username', - application: { - [application]: { - 'space:space_1': { - [mockActions.login]: false, - [mockActions.version]: true, + test(`failure when checking for login and user doesn't have login`, async () => { + const result = await checkPrivilegesAtSpaceTest({ + spaceId: 'space_1', + privilegeOrPrivileges: mockActions.login, + esHasPrivilegesResponse: { + has_all_requested: false, + username: 'foo-username', + application: { + [application]: { + 'space:space_1': { + [mockActions.login]: false, + [mockActions.version]: true, + }, }, }, }, - }, - expectedResult: { - hasAllRequested: false, - username: 'foo-username', - privileges: { - [mockActions.login]: false, - }, - }, + }); + expect(result).toMatchInlineSnapshot(` + Object { + "hasAllRequested": false, + "privileges": Array [ + Object { + "authorized": false, + "privilege": "mock-action:login", + "resource": "space_1", + }, + ], + "username": "foo-username", + } + `); }); - checkPrivilegesAtSpaceTest( - `throws error when checking for login and user has login but doesn't have version`, - { + test(`throws error when checking for login and user has login but doesn't have version`, async () => { + const result = await checkPrivilegesAtSpaceTest({ spaceId: 'space_1', privilegeOrPrivileges: mockActions.login, esHasPrivilegesResponse: { @@ -162,74 +160,99 @@ describe('#atSpace', () => { }, }, }, - expectErrorThrown: true, - } - ); - - checkPrivilegesAtSpaceTest(`successful when checking for two actions and the user has both`, { - spaceId: 'space_1', - privilegeOrPrivileges: [ - `saved_object:${savedObjectTypes[0]}/get`, - `saved_object:${savedObjectTypes[1]}/get`, - ], - esHasPrivilegesResponse: { - has_all_requested: true, - username: 'foo-username', - application: { - [application]: { - 'space:space_1': { - [mockActions.login]: true, - [mockActions.version]: true, - [`saved_object:${savedObjectTypes[0]}/get`]: true, - [`saved_object:${savedObjectTypes[1]}/get`]: true, + }); + expect(result).toMatchInlineSnapshot( + `[Error: Multiple versions of Kibana are running against the same Elasticsearch cluster, unable to authorize user.]` + ); + }); + + test(`successful when checking for two actions and the user has both`, async () => { + const result = await checkPrivilegesAtSpaceTest({ + spaceId: 'space_1', + privilegeOrPrivileges: [ + `saved_object:${savedObjectTypes[0]}/get`, + `saved_object:${savedObjectTypes[1]}/get`, + ], + esHasPrivilegesResponse: { + has_all_requested: true, + username: 'foo-username', + application: { + [application]: { + 'space:space_1': { + [mockActions.login]: true, + [mockActions.version]: true, + [`saved_object:${savedObjectTypes[0]}/get`]: true, + [`saved_object:${savedObjectTypes[1]}/get`]: true, + }, }, }, }, - }, - expectedResult: { - hasAllRequested: true, - username: 'foo-username', - privileges: { - [`saved_object:${savedObjectTypes[0]}/get`]: true, - [`saved_object:${savedObjectTypes[1]}/get`]: true, - }, - }, + }); + expect(result).toMatchInlineSnapshot(` + Object { + "hasAllRequested": true, + "privileges": Array [ + Object { + "authorized": true, + "privilege": "saved_object:foo-type/get", + "resource": "space_1", + }, + Object { + "authorized": true, + "privilege": "saved_object:bar-type/get", + "resource": "space_1", + }, + ], + "username": "foo-username", + } + `); }); - checkPrivilegesAtSpaceTest(`failure when checking for two actions and the user has only one`, { - spaceId: 'space_1', - privilegeOrPrivileges: [ - `saved_object:${savedObjectTypes[0]}/get`, - `saved_object:${savedObjectTypes[1]}/get`, - ], - esHasPrivilegesResponse: { - has_all_requested: false, - username: 'foo-username', - application: { - [application]: { - 'space:space_1': { - [mockActions.login]: true, - [mockActions.version]: true, - [`saved_object:${savedObjectTypes[0]}/get`]: false, - [`saved_object:${savedObjectTypes[1]}/get`]: true, + test(`failure when checking for two actions and the user has only one`, async () => { + const result = await checkPrivilegesAtSpaceTest({ + spaceId: 'space_1', + privilegeOrPrivileges: [ + `saved_object:${savedObjectTypes[0]}/get`, + `saved_object:${savedObjectTypes[1]}/get`, + ], + esHasPrivilegesResponse: { + has_all_requested: false, + username: 'foo-username', + application: { + [application]: { + 'space:space_1': { + [mockActions.login]: true, + [mockActions.version]: true, + [`saved_object:${savedObjectTypes[0]}/get`]: false, + [`saved_object:${savedObjectTypes[1]}/get`]: true, + }, }, }, }, - }, - expectedResult: { - hasAllRequested: false, - username: 'foo-username', - privileges: { - [`saved_object:${savedObjectTypes[0]}/get`]: false, - [`saved_object:${savedObjectTypes[1]}/get`]: true, - }, - }, + }); + expect(result).toMatchInlineSnapshot(` + Object { + "hasAllRequested": false, + "privileges": Array [ + Object { + "authorized": false, + "privilege": "saved_object:foo-type/get", + "resource": "space_1", + }, + Object { + "authorized": true, + "privilege": "saved_object:bar-type/get", + "resource": "space_1", + }, + ], + "username": "foo-username", + } + `); }); describe('with a malformed Elasticsearch response', () => { - checkPrivilegesAtSpaceTest( - `throws a validation error when an extra privilege is present in the response`, - { + test(`throws a validation error when an extra privilege is present in the response`, async () => { + const result = await checkPrivilegesAtSpaceTest({ spaceId: 'space_1', privilegeOrPrivileges: [`saved_object:${savedObjectTypes[0]}/get`], esHasPrivilegesResponse: { @@ -246,13 +269,14 @@ describe('#atSpace', () => { }, }, }, - expectErrorThrown: true, - } - ); + }); + expect(result).toMatchInlineSnapshot( + `[Error: Invalid response received from Elasticsearch has_privilege endpoint. ValidationError: child "application" fails because [child "kibana-our_application" fails because [child "space:space_1" fails because ["saved_object:bar-type/get" is not allowed]]]]` + ); + }); - checkPrivilegesAtSpaceTest( - `throws a validation error when privileges are missing in the response`, - { + test(`throws a validation error when privileges are missing in the response`, async () => { + const result = await checkPrivilegesAtSpaceTest({ spaceId: 'space_1', privilegeOrPrivileges: [`saved_object:${savedObjectTypes[0]}/get`], esHasPrivilegesResponse: { @@ -267,82 +291,69 @@ describe('#atSpace', () => { }, }, }, - expectErrorThrown: true, - } - ); + }); + expect(result).toMatchInlineSnapshot( + `[Error: Invalid response received from Elasticsearch has_privilege endpoint. ValidationError: child "application" fails because [child "kibana-our_application" fails because [child "space:space_1" fails because [child "saved_object:foo-type/get" fails because ["saved_object:foo-type/get" is required]]]]]` + ); + }); }); }); describe('#atSpaces', () => { - const checkPrivilegesAtSpacesTest = ( - description: string, - options: { - spaceIds: string[]; - privilegeOrPrivileges: string | string[]; - esHasPrivilegesResponse: HasPrivilegesResponse; - expectedResult?: any; - expectErrorThrown?: any; - } - ) => { - test(description, async () => { - const { mockClusterClient, mockScopedClusterClient } = createMockClusterClient( - options.esHasPrivilegesResponse - ); - const checkPrivilegesWithRequest = checkPrivilegesWithRequestFactory( - mockActions, - mockClusterClient, - application - ); - const request = httpServerMock.createKibanaRequest(); - const checkPrivileges = checkPrivilegesWithRequest(request); - - let actualResult; - let errorThrown = null; - try { - actualResult = await checkPrivileges.atSpaces( - options.spaceIds, - options.privilegeOrPrivileges - ); - } catch (err) { - errorThrown = err; - } + const checkPrivilegesAtSpacesTest = async (options: { + spaceIds: string[]; + privilegeOrPrivileges: string | string[]; + esHasPrivilegesResponse: HasPrivilegesResponse; + }) => { + const { mockClusterClient, mockScopedClusterClient } = createMockClusterClient( + options.esHasPrivilegesResponse + ); + const checkPrivilegesWithRequest = checkPrivilegesWithRequestFactory( + mockActions, + mockClusterClient, + application + ); + const request = httpServerMock.createKibanaRequest(); + const checkPrivileges = checkPrivilegesWithRequest(request); - expect(mockClusterClient.asScoped).toHaveBeenCalledWith(request); - expect(mockScopedClusterClient.callAsCurrentUser).toHaveBeenCalledWith( - 'shield.hasPrivileges', - { - body: { - applications: [ - { - application, - resources: options.spaceIds.map(spaceId => `space:${spaceId}`), - privileges: uniq([ - mockActions.version, - mockActions.login, - ...(Array.isArray(options.privilegeOrPrivileges) - ? options.privilegeOrPrivileges - : [options.privilegeOrPrivileges]), - ]), - }, - ], - }, - } + let actualResult; + let errorThrown = null; + try { + actualResult = await checkPrivileges.atSpaces( + options.spaceIds, + options.privilegeOrPrivileges ); + } catch (err) { + errorThrown = err; + } - if (options.expectedResult) { - expect(errorThrown).toBeNull(); - expect(actualResult).toEqual(options.expectedResult); - } - - if (options.expectErrorThrown) { - expect(errorThrown).toMatchSnapshot(); - } + expect(mockClusterClient.asScoped).toHaveBeenCalledWith(request); + expect(mockScopedClusterClient.callAsCurrentUser).toHaveBeenCalledWith('shield.hasPrivileges', { + body: { + applications: [ + { + application, + resources: options.spaceIds.map(spaceId => `space:${spaceId}`), + privileges: uniq([ + mockActions.version, + mockActions.login, + ...(Array.isArray(options.privilegeOrPrivileges) + ? options.privilegeOrPrivileges + : [options.privilegeOrPrivileges]), + ]), + }, + ], + }, }); + + if (errorThrown) { + return errorThrown; + } + return actualResult; }; - checkPrivilegesAtSpacesTest( - 'successful when checking for login and user has login at both spaces', - { + test('successful when checking for login and user has login at both spaces', async () => { + const result = await checkPrivilegesAtSpacesTest({ spaceIds: ['space_1', 'space_2'], privilegeOrPrivileges: mockActions.login, esHasPrivilegesResponse: { @@ -361,24 +372,29 @@ describe('#atSpaces', () => { }, }, }, - expectedResult: { - hasAllRequested: true, - username: 'foo-username', - spacePrivileges: { - space_1: { - [mockActions.login]: true, + }); + expect(result).toMatchInlineSnapshot(` + Object { + "hasAllRequested": true, + "privileges": Array [ + Object { + "authorized": true, + "privilege": "mock-action:login", + "resource": "space_1", }, - space_2: { - [mockActions.login]: true, + Object { + "authorized": true, + "privilege": "mock-action:login", + "resource": "space_2", }, - }, - }, - } - ); + ], + "username": "foo-username", + } + `); + }); - checkPrivilegesAtSpacesTest( - 'failure when checking for login and user has login at only one space', - { + test('failure when checking for login and user has login at only one space', async () => { + const result = await checkPrivilegesAtSpacesTest({ spaceIds: ['space_1', 'space_2'], privilegeOrPrivileges: mockActions.login, esHasPrivilegesResponse: { @@ -397,24 +413,29 @@ describe('#atSpaces', () => { }, }, }, - expectedResult: { - hasAllRequested: false, - username: 'foo-username', - spacePrivileges: { - space_1: { - [mockActions.login]: true, + }); + expect(result).toMatchInlineSnapshot(` + Object { + "hasAllRequested": false, + "privileges": Array [ + Object { + "authorized": true, + "privilege": "mock-action:login", + "resource": "space_1", }, - space_2: { - [mockActions.login]: false, + Object { + "authorized": false, + "privilege": "mock-action:login", + "resource": "space_2", }, - }, - }, - } - ); + ], + "username": "foo-username", + } + `); + }); - checkPrivilegesAtSpacesTest( - `throws error when checking for login and user has login but doesn't have version`, - { + test(`throws error when checking for login and user has login but doesn't have version`, async () => { + const result = await checkPrivilegesAtSpacesTest({ spaceIds: ['space_1', 'space_2'], privilegeOrPrivileges: mockActions.login, esHasPrivilegesResponse: { @@ -433,38 +454,43 @@ describe('#atSpaces', () => { }, }, }, - expectErrorThrown: true, - } - ); - - checkPrivilegesAtSpacesTest(`throws error when Elasticsearch returns malformed response`, { - spaceIds: ['space_1', 'space_2'], - privilegeOrPrivileges: [ - `saved_object:${savedObjectTypes[0]}/get`, - `saved_object:${savedObjectTypes[1]}/get`, - ], - esHasPrivilegesResponse: { - has_all_requested: true, - username: 'foo-username', - application: { - [application]: { - 'space:space_1': { - [`saved_object:${savedObjectTypes[0]}/get`]: true, - [`saved_object:${savedObjectTypes[1]}/get`]: true, - }, - 'space:space_2': { - [`saved_object:${savedObjectTypes[0]}/get`]: true, - [`saved_object:${savedObjectTypes[1]}/get`]: true, + }); + expect(result).toMatchInlineSnapshot( + `[Error: Multiple versions of Kibana are running against the same Elasticsearch cluster, unable to authorize user.]` + ); + }); + + test(`throws error when Elasticsearch returns malformed response`, async () => { + const result = await checkPrivilegesAtSpacesTest({ + spaceIds: ['space_1', 'space_2'], + privilegeOrPrivileges: [ + `saved_object:${savedObjectTypes[0]}/get`, + `saved_object:${savedObjectTypes[1]}/get`, + ], + esHasPrivilegesResponse: { + has_all_requested: true, + username: 'foo-username', + application: { + [application]: { + 'space:space_1': { + [`saved_object:${savedObjectTypes[0]}/get`]: true, + [`saved_object:${savedObjectTypes[1]}/get`]: true, + }, + 'space:space_2': { + [`saved_object:${savedObjectTypes[0]}/get`]: true, + [`saved_object:${savedObjectTypes[1]}/get`]: true, + }, }, }, }, - }, - expectErrorThrown: true, + }); + expect(result).toMatchInlineSnapshot( + `[Error: Invalid response received from Elasticsearch has_privilege endpoint. ValidationError: child "application" fails because [child "kibana-our_application" fails because [child "space:space_1" fails because [child "mock-action:version" fails because ["mock-action:version" is required]]]]]` + ); }); - checkPrivilegesAtSpacesTest( - `successful when checking for two actions at two spaces and user has it all`, - { + test(`successful when checking for two actions at two spaces and user has it all`, async () => { + const result = await checkPrivilegesAtSpacesTest({ spaceIds: ['space_1', 'space_2'], privilegeOrPrivileges: [ `saved_object:${savedObjectTypes[0]}/get`, @@ -490,26 +516,39 @@ describe('#atSpaces', () => { }, }, }, - expectedResult: { - hasAllRequested: true, - username: 'foo-username', - spacePrivileges: { - space_1: { - [`saved_object:${savedObjectTypes[0]}/get`]: true, - [`saved_object:${savedObjectTypes[1]}/get`]: true, + }); + expect(result).toMatchInlineSnapshot(` + Object { + "hasAllRequested": true, + "privileges": Array [ + Object { + "authorized": true, + "privilege": "saved_object:foo-type/get", + "resource": "space_1", }, - space_2: { - [`saved_object:${savedObjectTypes[0]}/get`]: true, - [`saved_object:${savedObjectTypes[1]}/get`]: true, + Object { + "authorized": true, + "privilege": "saved_object:bar-type/get", + "resource": "space_1", }, - }, - }, - } - ); + Object { + "authorized": true, + "privilege": "saved_object:foo-type/get", + "resource": "space_2", + }, + Object { + "authorized": true, + "privilege": "saved_object:bar-type/get", + "resource": "space_2", + }, + ], + "username": "foo-username", + } + `); + }); - checkPrivilegesAtSpacesTest( - `failure when checking for two actions at two spaces and user has one action at one space`, - { + test(`failure when checking for two actions at two spaces and user has one action at one space`, async () => { + const result = await checkPrivilegesAtSpacesTest({ spaceIds: ['space_1', 'space_2'], privilegeOrPrivileges: [ `saved_object:${savedObjectTypes[0]}/get`, @@ -535,26 +574,39 @@ describe('#atSpaces', () => { }, }, }, - expectedResult: { - hasAllRequested: false, - username: 'foo-username', - spacePrivileges: { - space_1: { - [`saved_object:${savedObjectTypes[0]}/get`]: true, - [`saved_object:${savedObjectTypes[1]}/get`]: false, + }); + expect(result).toMatchInlineSnapshot(` + Object { + "hasAllRequested": false, + "privileges": Array [ + Object { + "authorized": true, + "privilege": "saved_object:foo-type/get", + "resource": "space_1", }, - space_2: { - [`saved_object:${savedObjectTypes[0]}/get`]: false, - [`saved_object:${savedObjectTypes[1]}/get`]: false, + Object { + "authorized": false, + "privilege": "saved_object:bar-type/get", + "resource": "space_1", }, - }, - }, - } - ); + Object { + "authorized": false, + "privilege": "saved_object:foo-type/get", + "resource": "space_2", + }, + Object { + "authorized": false, + "privilege": "saved_object:bar-type/get", + "resource": "space_2", + }, + ], + "username": "foo-username", + } + `); + }); - checkPrivilegesAtSpacesTest( - `failure when checking for two actions at two spaces and user has two actions at one space`, - { + test(`failure when checking for two actions at two spaces and user has two actions at one space`, async () => { + const result = await checkPrivilegesAtSpacesTest({ spaceIds: ['space_1', 'space_2'], privilegeOrPrivileges: [ `saved_object:${savedObjectTypes[0]}/get`, @@ -580,26 +632,39 @@ describe('#atSpaces', () => { }, }, }, - expectedResult: { - hasAllRequested: false, - username: 'foo-username', - spacePrivileges: { - space_1: { - [`saved_object:${savedObjectTypes[0]}/get`]: true, - [`saved_object:${savedObjectTypes[1]}/get`]: true, + }); + expect(result).toMatchInlineSnapshot(` + Object { + "hasAllRequested": false, + "privileges": Array [ + Object { + "authorized": true, + "privilege": "saved_object:foo-type/get", + "resource": "space_1", }, - space_2: { - [`saved_object:${savedObjectTypes[0]}/get`]: false, - [`saved_object:${savedObjectTypes[1]}/get`]: false, + Object { + "authorized": true, + "privilege": "saved_object:bar-type/get", + "resource": "space_1", }, - }, - }, - } - ); + Object { + "authorized": false, + "privilege": "saved_object:foo-type/get", + "resource": "space_2", + }, + Object { + "authorized": false, + "privilege": "saved_object:bar-type/get", + "resource": "space_2", + }, + ], + "username": "foo-username", + } + `); + }); - checkPrivilegesAtSpacesTest( - `failure when checking for two actions at two spaces and user has two actions at one space & one action at the other`, - { + test(`failure when checking for two actions at two spaces and user has two actions at one space & one action at the other`, async () => { + const result = await checkPrivilegesAtSpacesTest({ spaceIds: ['space_1', 'space_2'], privilegeOrPrivileges: [ `saved_object:${savedObjectTypes[0]}/get`, @@ -625,27 +690,40 @@ describe('#atSpaces', () => { }, }, }, - expectedResult: { - hasAllRequested: false, - username: 'foo-username', - spacePrivileges: { - space_1: { - [`saved_object:${savedObjectTypes[0]}/get`]: true, - [`saved_object:${savedObjectTypes[1]}/get`]: true, + }); + expect(result).toMatchInlineSnapshot(` + Object { + "hasAllRequested": false, + "privileges": Array [ + Object { + "authorized": true, + "privilege": "saved_object:foo-type/get", + "resource": "space_1", }, - space_2: { - [`saved_object:${savedObjectTypes[0]}/get`]: true, - [`saved_object:${savedObjectTypes[1]}/get`]: false, + Object { + "authorized": true, + "privilege": "saved_object:bar-type/get", + "resource": "space_1", }, - }, - }, - } - ); + Object { + "authorized": true, + "privilege": "saved_object:foo-type/get", + "resource": "space_2", + }, + Object { + "authorized": false, + "privilege": "saved_object:bar-type/get", + "resource": "space_2", + }, + ], + "username": "foo-username", + } + `); + }); describe('with a malformed Elasticsearch response', () => { - checkPrivilegesAtSpacesTest( - `throws a validation error when an extra privilege is present in the response`, - { + test(`throws a validation error when an extra privilege is present in the response`, async () => { + const result = await checkPrivilegesAtSpacesTest({ spaceIds: ['space_1', 'space_2'], privilegeOrPrivileges: [`saved_object:${savedObjectTypes[0]}/get`], esHasPrivilegesResponse: { @@ -668,13 +746,14 @@ describe('#atSpaces', () => { }, }, }, - expectErrorThrown: true, - } - ); + }); + expect(result).toMatchInlineSnapshot( + `[Error: Invalid response received from Elasticsearch has_privilege endpoint. ValidationError: child "application" fails because [child "kibana-our_application" fails because [child "space:space_2" fails because ["space:space_2" is required]]]]` + ); + }); - checkPrivilegesAtSpacesTest( - `throws a validation error when privileges are missing in the response`, - { + test(`throws a validation error when privileges are missing in the response`, async () => { + const result = await checkPrivilegesAtSpacesTest({ spaceIds: ['space_1', 'space_2'], privilegeOrPrivileges: [`saved_object:${savedObjectTypes[0]}/get`], esHasPrivilegesResponse: { @@ -695,13 +774,14 @@ describe('#atSpaces', () => { }, }, }, - expectErrorThrown: true, - } - ); + }); + expect(result).toMatchInlineSnapshot( + `[Error: Invalid response received from Elasticsearch has_privilege endpoint. ValidationError: child "application" fails because [child "kibana-our_application" fails because [child "space:space_2" fails because ["space:space_2" is required]]]]` + ); + }); - checkPrivilegesAtSpacesTest( - `throws a validation error when an extra space is present in the response`, - { + test(`throws a validation error when an extra space is present in the response`, async () => { + const result = await checkPrivilegesAtSpacesTest({ spaceIds: ['space_1', 'space_2'], privilegeOrPrivileges: [`saved_object:${savedObjectTypes[0]}/get`], esHasPrivilegesResponse: { @@ -727,13 +807,14 @@ describe('#atSpaces', () => { }, }, }, - expectErrorThrown: true, - } - ); + }); + expect(result).toMatchInlineSnapshot( + `[Error: Invalid response received from Elasticsearch has_privilege endpoint. ValidationError: child "application" fails because [child "kibana-our_application" fails because ["space:space_3" is not allowed]]]` + ); + }); - checkPrivilegesAtSpacesTest( - `throws a validation error when an a space is missing in the response`, - { + test(`throws a validation error when an a space is missing in the response`, async () => { + const result = await checkPrivilegesAtSpacesTest({ spaceIds: ['space_1', 'space_2'], privilegeOrPrivileges: [`saved_object:${savedObjectTypes[0]}/get`], esHasPrivilegesResponse: { @@ -749,124 +830,127 @@ describe('#atSpaces', () => { }, }, }, - expectErrorThrown: true, - } - ); + }); + expect(result).toMatchInlineSnapshot( + `[Error: Invalid response received from Elasticsearch has_privilege endpoint. ValidationError: child "application" fails because [child "kibana-our_application" fails because [child "space:space_2" fails because ["space:space_2" is required]]]]` + ); + }); }); }); describe('#globally', () => { - const checkPrivilegesGloballyTest = ( - description: string, - options: { - privilegeOrPrivileges: string | string[]; - esHasPrivilegesResponse: HasPrivilegesResponse; - expectedResult?: any; - expectErrorThrown?: any; + const checkPrivilegesGloballyTest = async (options: { + privilegeOrPrivileges: string | string[]; + esHasPrivilegesResponse: HasPrivilegesResponse; + }) => { + const { mockClusterClient, mockScopedClusterClient } = createMockClusterClient( + options.esHasPrivilegesResponse + ); + const checkPrivilegesWithRequest = checkPrivilegesWithRequestFactory( + mockActions, + mockClusterClient, + application + ); + const request = httpServerMock.createKibanaRequest(); + const checkPrivileges = checkPrivilegesWithRequest(request); + + let actualResult; + let errorThrown = null; + try { + actualResult = await checkPrivileges.globally(options.privilegeOrPrivileges); + } catch (err) { + errorThrown = err; } - ) => { - test(description, async () => { - const { mockClusterClient, mockScopedClusterClient } = createMockClusterClient( - options.esHasPrivilegesResponse - ); - const checkPrivilegesWithRequest = checkPrivilegesWithRequestFactory( - mockActions, - mockClusterClient, - application - ); - const request = httpServerMock.createKibanaRequest(); - const checkPrivileges = checkPrivilegesWithRequest(request); - - let actualResult; - let errorThrown = null; - try { - actualResult = await checkPrivileges.globally(options.privilegeOrPrivileges); - } catch (err) { - errorThrown = err; - } - expect(mockClusterClient.asScoped).toHaveBeenCalledWith(request); - expect(mockScopedClusterClient.callAsCurrentUser).toHaveBeenCalledWith( - 'shield.hasPrivileges', - { - body: { - applications: [ - { - application, - resources: [GLOBAL_RESOURCE], - privileges: uniq([ - mockActions.version, - mockActions.login, - ...(Array.isArray(options.privilegeOrPrivileges) - ? options.privilegeOrPrivileges - : [options.privilegeOrPrivileges]), - ]), - }, - ], + expect(mockClusterClient.asScoped).toHaveBeenCalledWith(request); + expect(mockScopedClusterClient.callAsCurrentUser).toHaveBeenCalledWith('shield.hasPrivileges', { + body: { + applications: [ + { + application, + resources: [GLOBAL_RESOURCE], + privileges: uniq([ + mockActions.version, + mockActions.login, + ...(Array.isArray(options.privilegeOrPrivileges) + ? options.privilegeOrPrivileges + : [options.privilegeOrPrivileges]), + ]), }, - } - ); - - if (options.expectedResult) { - expect(errorThrown).toBeNull(); - expect(actualResult).toEqual(options.expectedResult); - } - - if (options.expectErrorThrown) { - expect(errorThrown).toMatchSnapshot(); - } + ], + }, }); + + if (errorThrown) { + return errorThrown; + } + return actualResult; }; - checkPrivilegesGloballyTest('successful when checking for login and user has login', { - privilegeOrPrivileges: mockActions.login, - esHasPrivilegesResponse: { - has_all_requested: true, - username: 'foo-username', - application: { - [application]: { - [GLOBAL_RESOURCE]: { - [mockActions.login]: true, - [mockActions.version]: true, + test('successful when checking for login and user has login', async () => { + const result = await checkPrivilegesGloballyTest({ + privilegeOrPrivileges: mockActions.login, + esHasPrivilegesResponse: { + has_all_requested: true, + username: 'foo-username', + application: { + [application]: { + [GLOBAL_RESOURCE]: { + [mockActions.login]: true, + [mockActions.version]: true, + }, }, }, }, - }, - expectedResult: { - hasAllRequested: true, - username: 'foo-username', - privileges: { - [mockActions.login]: true, - }, - }, + }); + expect(result).toMatchInlineSnapshot(` + Object { + "hasAllRequested": true, + "privileges": Array [ + Object { + "authorized": true, + "privilege": "mock-action:login", + "resource": undefined, + }, + ], + "username": "foo-username", + } + `); }); - checkPrivilegesGloballyTest(`failure when checking for login and user doesn't have login`, { - privilegeOrPrivileges: mockActions.login, - esHasPrivilegesResponse: { - has_all_requested: false, - username: 'foo-username', - application: { - [application]: { - [GLOBAL_RESOURCE]: { - [mockActions.login]: false, - [mockActions.version]: true, + test(`failure when checking for login and user doesn't have login`, async () => { + const result = await checkPrivilegesGloballyTest({ + privilegeOrPrivileges: mockActions.login, + esHasPrivilegesResponse: { + has_all_requested: false, + username: 'foo-username', + application: { + [application]: { + [GLOBAL_RESOURCE]: { + [mockActions.login]: false, + [mockActions.version]: true, + }, }, }, }, - }, - expectedResult: { - hasAllRequested: false, - username: 'foo-username', - privileges: { - [mockActions.login]: false, - }, - }, + }); + expect(result).toMatchInlineSnapshot(` + Object { + "hasAllRequested": false, + "privileges": Array [ + Object { + "authorized": false, + "privilege": "mock-action:login", + "resource": undefined, + }, + ], + "username": "foo-username", + } + `); }); - checkPrivilegesGloballyTest( - `throws error when checking for login and user has login but doesn't have version`, - { + test(`throws error when checking for login and user has login but doesn't have version`, async () => { + const result = await checkPrivilegesGloballyTest({ privilegeOrPrivileges: mockActions.login, esHasPrivilegesResponse: { has_all_requested: false, @@ -880,92 +964,121 @@ describe('#globally', () => { }, }, }, - expectErrorThrown: true, - } - ); - - checkPrivilegesGloballyTest(`throws error when Elasticsearch returns malformed response`, { - privilegeOrPrivileges: [ - `saved_object:${savedObjectTypes[0]}/get`, - `saved_object:${savedObjectTypes[1]}/get`, - ], - esHasPrivilegesResponse: { - has_all_requested: false, - username: 'foo-username', - application: { - [application]: { - [GLOBAL_RESOURCE]: { - [`saved_object:${savedObjectTypes[0]}/get`]: false, - [`saved_object:${savedObjectTypes[1]}/get`]: true, + }); + expect(result).toMatchInlineSnapshot( + `[Error: Multiple versions of Kibana are running against the same Elasticsearch cluster, unable to authorize user.]` + ); + }); + + test(`throws error when Elasticsearch returns malformed response`, async () => { + const result = await checkPrivilegesGloballyTest({ + privilegeOrPrivileges: [ + `saved_object:${savedObjectTypes[0]}/get`, + `saved_object:${savedObjectTypes[1]}/get`, + ], + esHasPrivilegesResponse: { + has_all_requested: false, + username: 'foo-username', + application: { + [application]: { + [GLOBAL_RESOURCE]: { + [`saved_object:${savedObjectTypes[0]}/get`]: false, + [`saved_object:${savedObjectTypes[1]}/get`]: true, + }, }, }, }, - }, - expectErrorThrown: true, + }); + expect(result).toMatchInlineSnapshot( + `[Error: Invalid response received from Elasticsearch has_privilege endpoint. ValidationError: child "application" fails because [child "kibana-our_application" fails because [child "*" fails because [child "mock-action:version" fails because ["mock-action:version" is required]]]]]` + ); }); - checkPrivilegesGloballyTest(`successful when checking for two actions and the user has both`, { - privilegeOrPrivileges: [ - `saved_object:${savedObjectTypes[0]}/get`, - `saved_object:${savedObjectTypes[1]}/get`, - ], - esHasPrivilegesResponse: { - has_all_requested: true, - username: 'foo-username', - application: { - [application]: { - [GLOBAL_RESOURCE]: { - [mockActions.login]: true, - [mockActions.version]: true, - [`saved_object:${savedObjectTypes[0]}/get`]: true, - [`saved_object:${savedObjectTypes[1]}/get`]: true, + test(`successful when checking for two actions and the user has both`, async () => { + const result = await checkPrivilegesGloballyTest({ + privilegeOrPrivileges: [ + `saved_object:${savedObjectTypes[0]}/get`, + `saved_object:${savedObjectTypes[1]}/get`, + ], + esHasPrivilegesResponse: { + has_all_requested: true, + username: 'foo-username', + application: { + [application]: { + [GLOBAL_RESOURCE]: { + [mockActions.login]: true, + [mockActions.version]: true, + [`saved_object:${savedObjectTypes[0]}/get`]: true, + [`saved_object:${savedObjectTypes[1]}/get`]: true, + }, }, }, }, - }, - expectedResult: { - hasAllRequested: true, - username: 'foo-username', - privileges: { - [`saved_object:${savedObjectTypes[0]}/get`]: true, - [`saved_object:${savedObjectTypes[1]}/get`]: true, - }, - }, + }); + expect(result).toMatchInlineSnapshot(` + Object { + "hasAllRequested": true, + "privileges": Array [ + Object { + "authorized": true, + "privilege": "saved_object:foo-type/get", + "resource": undefined, + }, + Object { + "authorized": true, + "privilege": "saved_object:bar-type/get", + "resource": undefined, + }, + ], + "username": "foo-username", + } + `); }); - checkPrivilegesGloballyTest(`failure when checking for two actions and the user has only one`, { - privilegeOrPrivileges: [ - `saved_object:${savedObjectTypes[0]}/get`, - `saved_object:${savedObjectTypes[1]}/get`, - ], - esHasPrivilegesResponse: { - has_all_requested: false, - username: 'foo-username', - application: { - [application]: { - [GLOBAL_RESOURCE]: { - [mockActions.login]: true, - [mockActions.version]: true, - [`saved_object:${savedObjectTypes[0]}/get`]: false, - [`saved_object:${savedObjectTypes[1]}/get`]: true, + test(`failure when checking for two actions and the user has only one`, async () => { + const result = await checkPrivilegesGloballyTest({ + privilegeOrPrivileges: [ + `saved_object:${savedObjectTypes[0]}/get`, + `saved_object:${savedObjectTypes[1]}/get`, + ], + esHasPrivilegesResponse: { + has_all_requested: false, + username: 'foo-username', + application: { + [application]: { + [GLOBAL_RESOURCE]: { + [mockActions.login]: true, + [mockActions.version]: true, + [`saved_object:${savedObjectTypes[0]}/get`]: false, + [`saved_object:${savedObjectTypes[1]}/get`]: true, + }, }, }, }, - }, - expectedResult: { - hasAllRequested: false, - username: 'foo-username', - privileges: { - [`saved_object:${savedObjectTypes[0]}/get`]: false, - [`saved_object:${savedObjectTypes[1]}/get`]: true, - }, - }, + }); + expect(result).toMatchInlineSnapshot(` + Object { + "hasAllRequested": false, + "privileges": Array [ + Object { + "authorized": false, + "privilege": "saved_object:foo-type/get", + "resource": undefined, + }, + Object { + "authorized": true, + "privilege": "saved_object:bar-type/get", + "resource": undefined, + }, + ], + "username": "foo-username", + } + `); }); describe('with a malformed Elasticsearch response', () => { - checkPrivilegesGloballyTest( - `throws a validation error when an extra privilege is present in the response`, - { + test(`throws a validation error when an extra privilege is present in the response`, async () => { + const result = await checkPrivilegesGloballyTest({ privilegeOrPrivileges: [`saved_object:${savedObjectTypes[0]}/get`], esHasPrivilegesResponse: { has_all_requested: false, @@ -981,13 +1094,14 @@ describe('#globally', () => { }, }, }, - expectErrorThrown: true, - } - ); + }); + expect(result).toMatchInlineSnapshot( + `[Error: Invalid response received from Elasticsearch has_privilege endpoint. ValidationError: child "application" fails because [child "kibana-our_application" fails because [child "*" fails because ["saved_object:bar-type/get" is not allowed]]]]` + ); + }); - checkPrivilegesGloballyTest( - `throws a validation error when privileges are missing in the response`, - { + test(`throws a validation error when privileges are missing in the response`, async () => { + const result = await checkPrivilegesGloballyTest({ privilegeOrPrivileges: [`saved_object:${savedObjectTypes[0]}/get`], esHasPrivilegesResponse: { has_all_requested: false, @@ -1001,8 +1115,10 @@ describe('#globally', () => { }, }, }, - expectErrorThrown: true, - } - ); + }); + expect(result).toMatchInlineSnapshot( + `[Error: Invalid response received from Elasticsearch has_privilege endpoint. ValidationError: child "application" fails because [child "kibana-our_application" fails because [child "*" fails because [child "saved_object:foo-type/get" fails because ["saved_object:foo-type/get" is required]]]]]` + ); + }); }); }); diff --git a/x-pack/plugins/security/server/authorization/check_privileges.ts b/x-pack/plugins/security/server/authorization/check_privileges.ts index 3ef7a8f29a0bf..177a49d6defe9 100644 --- a/x-pack/plugins/security/server/authorization/check_privileges.ts +++ b/x-pack/plugins/security/server/authorization/check_privileges.ts @@ -16,32 +16,17 @@ interface CheckPrivilegesActions { version: string; } -interface CheckPrivilegesAtResourcesResponse { +export interface CheckPrivilegesResponse { hasAllRequested: boolean; username: string; - resourcePrivileges: { - [resource: string]: { - [privilege: string]: boolean; - }; - }; -} - -export interface CheckPrivilegesAtResourceResponse { - hasAllRequested: boolean; - username: string; - privileges: { - [privilege: string]: boolean; - }; -} - -export interface CheckPrivilegesAtSpacesResponse { - hasAllRequested: boolean; - username: string; - spacePrivileges: { - [spaceId: string]: { - [privilege: string]: boolean; - }; - }; + privileges: Array<{ + /** + * If this attribute is undefined, this element is a privilege for the global resource. + */ + resource?: string; + privilege: string; + authorized: boolean; + }>; } export type CheckPrivilegesWithRequest = (request: KibanaRequest) => CheckPrivileges; @@ -50,12 +35,12 @@ export interface CheckPrivileges { atSpace( spaceId: string, privilegeOrPrivileges: string | string[] - ): Promise<CheckPrivilegesAtResourceResponse>; + ): Promise<CheckPrivilegesResponse>; atSpaces( spaceIds: string[], privilegeOrPrivileges: string | string[] - ): Promise<CheckPrivilegesAtSpacesResponse>; - globally(privilegeOrPrivileges: string | string[]): Promise<CheckPrivilegesAtResourceResponse>; + ): Promise<CheckPrivilegesResponse>; + globally(privilegeOrPrivileges: string | string[]): Promise<CheckPrivilegesResponse>; } export function checkPrivilegesWithRequestFactory( @@ -75,7 +60,7 @@ export function checkPrivilegesWithRequestFactory( const checkPrivilegesAtResources = async ( resources: string[], privilegeOrPrivileges: string | string[] - ): Promise<CheckPrivilegesAtResourcesResponse> => { + ): Promise<CheckPrivilegesResponse> => { const privileges = Array.isArray(privilegeOrPrivileges) ? privilegeOrPrivileges : [privilegeOrPrivileges]; @@ -106,55 +91,43 @@ export function checkPrivilegesWithRequestFactory( ); } + // we need to filter out the non requested privileges from the response + const resourcePrivileges = transform(applicationPrivilegesResponse, (result, value, key) => { + result[key!] = pick(value, privileges); + }) as HasPrivilegesResponseApplication; + const privilegeArray = Object.entries(resourcePrivileges) + .map(([key, val]) => { + // we need to turn the resource responses back into the space ids + const resource = + key !== GLOBAL_RESOURCE ? ResourceSerializer.deserializeSpaceResource(key!) : undefined; + return Object.entries(val).map(([privilege, authorized]) => ({ + resource, + privilege, + authorized, + })); + }) + .flat(); + return { hasAllRequested: hasPrivilegesResponse.has_all_requested, username: hasPrivilegesResponse.username, - // we need to filter out the non requested privileges from the response - resourcePrivileges: transform(applicationPrivilegesResponse, (result, value, key) => { - result[key!] = pick(value, privileges); - }), - }; - }; - - const checkPrivilegesAtResource = async ( - resource: string, - privilegeOrPrivileges: string | string[] - ) => { - const { hasAllRequested, username, resourcePrivileges } = await checkPrivilegesAtResources( - [resource], - privilegeOrPrivileges - ); - return { - hasAllRequested, - username, - privileges: resourcePrivileges[resource], + privileges: privilegeArray, }; }; return { async atSpace(spaceId: string, privilegeOrPrivileges: string | string[]) { const spaceResource = ResourceSerializer.serializeSpaceResource(spaceId); - return await checkPrivilegesAtResource(spaceResource, privilegeOrPrivileges); + return await checkPrivilegesAtResources([spaceResource], privilegeOrPrivileges); }, async atSpaces(spaceIds: string[], privilegeOrPrivileges: string | string[]) { const spaceResources = spaceIds.map(spaceId => ResourceSerializer.serializeSpaceResource(spaceId) ); - const { hasAllRequested, username, resourcePrivileges } = await checkPrivilegesAtResources( - spaceResources, - privilegeOrPrivileges - ); - return { - hasAllRequested, - username, - // we need to turn the resource responses back into the space ids - spacePrivileges: transform(resourcePrivileges, (result, value, key) => { - result[ResourceSerializer.deserializeSpaceResource(key!)] = value; - }), - }; + return await checkPrivilegesAtResources(spaceResources, privilegeOrPrivileges); }, async globally(privilegeOrPrivileges: string | string[]) { - return await checkPrivilegesAtResource(GLOBAL_RESOURCE, privilegeOrPrivileges); + return await checkPrivilegesAtResources([GLOBAL_RESOURCE], privilegeOrPrivileges); }, }; }; diff --git a/x-pack/plugins/security/server/authorization/check_privileges_dynamically.ts b/x-pack/plugins/security/server/authorization/check_privileges_dynamically.ts index 0377dd06eb669..6014bad739e77 100644 --- a/x-pack/plugins/security/server/authorization/check_privileges_dynamically.ts +++ b/x-pack/plugins/security/server/authorization/check_privileges_dynamically.ts @@ -6,11 +6,11 @@ import { KibanaRequest } from '../../../../../src/core/server'; import { SpacesService } from '../plugin'; -import { CheckPrivilegesAtResourceResponse, CheckPrivilegesWithRequest } from './check_privileges'; +import { CheckPrivilegesResponse, CheckPrivilegesWithRequest } from './check_privileges'; export type CheckPrivilegesDynamically = ( privilegeOrPrivileges: string | string[] -) => Promise<CheckPrivilegesAtResourceResponse>; +) => Promise<CheckPrivilegesResponse>; export type CheckPrivilegesDynamicallyWithRequest = ( request: KibanaRequest diff --git a/x-pack/plugins/security/server/authorization/check_saved_objects_privileges.test.ts b/x-pack/plugins/security/server/authorization/check_saved_objects_privileges.test.ts index 4618e8e6641fc..43b3824500579 100644 --- a/x-pack/plugins/security/server/authorization/check_saved_objects_privileges.test.ts +++ b/x-pack/plugins/security/server/authorization/check_saved_objects_privileges.test.ts @@ -7,57 +7,113 @@ import { checkSavedObjectsPrivilegesWithRequestFactory } from './check_saved_objects_privileges'; import { httpServerMock } from '../../../../../src/core/server/mocks'; +import { CheckPrivileges, CheckPrivilegesWithRequest } from './check_privileges'; +import { SpacesService } from '../plugin'; -test(`checkPrivileges.atSpace when spaces is enabled`, async () => { - const expectedResult = Symbol(); - const spaceId = 'foo-space'; - const mockCheckPrivileges = { - atSpace: jest.fn().mockReturnValue(expectedResult), - }; - const mockCheckPrivilegesWithRequest = jest.fn().mockReturnValue(mockCheckPrivileges); - const request = httpServerMock.createKibanaRequest(); - const privilegeOrPrivileges = ['foo', 'bar']; - const mockSpacesService = { - getSpaceId: jest.fn(), - namespaceToSpaceId: jest.fn().mockReturnValue(spaceId), - }; +let mockCheckPrivileges: jest.Mocked<CheckPrivileges>; +let mockCheckPrivilegesWithRequest: jest.Mocked<CheckPrivilegesWithRequest>; +let mockSpacesService: jest.Mocked<SpacesService> | undefined; +const request = httpServerMock.createKibanaRequest(); - const checkSavedObjectsPrivileges = checkSavedObjectsPrivilegesWithRequestFactory( +const createFactory = () => + checkSavedObjectsPrivilegesWithRequestFactory( mockCheckPrivilegesWithRequest, () => mockSpacesService )(request); - const namespace = 'foo'; - - const result = await checkSavedObjectsPrivileges(privilegeOrPrivileges, namespace); +beforeEach(() => { + mockCheckPrivileges = { + atSpace: jest.fn(), + atSpaces: jest.fn(), + globally: jest.fn(), + }; + mockCheckPrivilegesWithRequest = jest.fn().mockReturnValue(mockCheckPrivileges); - expect(result).toBe(expectedResult); - expect(mockCheckPrivilegesWithRequest).toHaveBeenCalledWith(request); - expect(mockCheckPrivileges.atSpace).toHaveBeenCalledWith(spaceId, privilegeOrPrivileges); - expect(mockSpacesService.namespaceToSpaceId).toBeCalledWith(namespace); + mockSpacesService = { + getSpaceId: jest.fn(), + namespaceToSpaceId: jest.fn().mockImplementation((namespace: string) => `${namespace}-id`), + }; }); -test(`checkPrivileges.globally when spaces is disabled`, async () => { - const expectedResult = Symbol(); - const mockCheckPrivileges = { - globally: jest.fn().mockReturnValue(expectedResult), - }; - const mockCheckPrivilegesWithRequest = jest.fn().mockReturnValue(mockCheckPrivileges); +describe('#checkSavedObjectsPrivileges', () => { + const actions = ['foo', 'bar']; + const namespace1 = 'baz'; + const namespace2 = 'qux'; - const request = httpServerMock.createKibanaRequest(); + describe('when checking multiple namespaces', () => { + const namespaces = [namespace1, namespace2]; - const privilegeOrPrivileges = ['foo', 'bar']; + test(`throws an error when Spaces is disabled`, async () => { + mockSpacesService = undefined; + const checkSavedObjectsPrivileges = createFactory(); - const checkSavedObjectsPrivileges = checkSavedObjectsPrivilegesWithRequestFactory( - mockCheckPrivilegesWithRequest, - () => undefined - )(request); + await expect( + checkSavedObjectsPrivileges(actions, namespaces) + ).rejects.toThrowErrorMatchingInlineSnapshot( + `"Can't check saved object privileges for multiple namespaces if Spaces is disabled"` + ); + }); + + test(`throws an error when using an empty namespaces array`, async () => { + const checkSavedObjectsPrivileges = createFactory(); + + await expect( + checkSavedObjectsPrivileges(actions, []) + ).rejects.toThrowErrorMatchingInlineSnapshot( + `"Can't check saved object privileges for 0 namespaces"` + ); + }); + + test(`uses checkPrivileges.atSpaces when spaces is enabled`, async () => { + const expectedResult = Symbol(); + mockCheckPrivileges.atSpaces.mockReturnValue(expectedResult as any); + const checkSavedObjectsPrivileges = createFactory(); + + const result = await checkSavedObjectsPrivileges(actions, namespaces); + + expect(result).toBe(expectedResult); + expect(mockSpacesService!.namespaceToSpaceId).toHaveBeenCalledTimes(2); + expect(mockSpacesService!.namespaceToSpaceId).toHaveBeenNthCalledWith(1, namespace1); + expect(mockSpacesService!.namespaceToSpaceId).toHaveBeenNthCalledWith(2, namespace2); + expect(mockCheckPrivilegesWithRequest).toHaveBeenCalledTimes(1); + expect(mockCheckPrivilegesWithRequest).toHaveBeenCalledWith(request); + expect(mockCheckPrivileges.atSpaces).toHaveBeenCalledTimes(1); + const spaceIds = mockSpacesService!.namespaceToSpaceId.mock.results.map(x => x.value); + expect(mockCheckPrivileges.atSpaces).toHaveBeenCalledWith(spaceIds, actions); + }); + }); + + describe('when checking a single namespace', () => { + test(`uses checkPrivileges.atSpace when Spaces is enabled`, async () => { + const expectedResult = Symbol(); + mockCheckPrivileges.atSpace.mockReturnValue(expectedResult as any); + const checkSavedObjectsPrivileges = createFactory(); + + const result = await checkSavedObjectsPrivileges(actions, namespace1); + + expect(result).toBe(expectedResult); + expect(mockSpacesService!.namespaceToSpaceId).toHaveBeenCalledTimes(1); + expect(mockSpacesService!.namespaceToSpaceId).toHaveBeenCalledWith(namespace1); + expect(mockCheckPrivilegesWithRequest).toHaveBeenCalledTimes(1); + expect(mockCheckPrivilegesWithRequest).toHaveBeenCalledWith(request); + expect(mockCheckPrivileges.atSpace).toHaveBeenCalledTimes(1); + const spaceId = mockSpacesService!.namespaceToSpaceId.mock.results[0].value; + expect(mockCheckPrivileges.atSpace).toHaveBeenCalledWith(spaceId, actions); + }); - const namespace = 'foo'; + test(`uses checkPrivileges.globally when Spaces is disabled`, async () => { + const expectedResult = Symbol(); + mockCheckPrivileges.globally.mockReturnValue(expectedResult as any); + mockSpacesService = undefined; + const checkSavedObjectsPrivileges = createFactory(); - const result = await checkSavedObjectsPrivileges(privilegeOrPrivileges, namespace); + const result = await checkSavedObjectsPrivileges(actions, namespace1); - expect(result).toBe(expectedResult); - expect(mockCheckPrivilegesWithRequest).toHaveBeenCalledWith(request); - expect(mockCheckPrivileges.globally).toHaveBeenCalledWith(privilegeOrPrivileges); + expect(result).toBe(expectedResult); + expect(mockCheckPrivilegesWithRequest).toHaveBeenCalledTimes(1); + expect(mockCheckPrivilegesWithRequest).toHaveBeenCalledWith(request); + expect(mockCheckPrivileges.globally).toHaveBeenCalledTimes(1); + expect(mockCheckPrivileges.globally).toHaveBeenCalledWith(actions); + }); + }); }); diff --git a/x-pack/plugins/security/server/authorization/check_saved_objects_privileges.ts b/x-pack/plugins/security/server/authorization/check_saved_objects_privileges.ts index 02958fe265efa..43140143a1773 100644 --- a/x-pack/plugins/security/server/authorization/check_saved_objects_privileges.ts +++ b/x-pack/plugins/security/server/authorization/check_saved_objects_privileges.ts @@ -6,32 +6,44 @@ import { KibanaRequest } from '../../../../../src/core/server'; import { SpacesService } from '../plugin'; -import { CheckPrivilegesAtResourceResponse, CheckPrivilegesWithRequest } from './check_privileges'; +import { CheckPrivilegesWithRequest, CheckPrivilegesResponse } from './check_privileges'; export type CheckSavedObjectsPrivilegesWithRequest = ( request: KibanaRequest ) => CheckSavedObjectsPrivileges; + export type CheckSavedObjectsPrivileges = ( actions: string | string[], - namespace?: string -) => Promise<CheckPrivilegesAtResourceResponse>; + namespaceOrNamespaces?: string | string[] +) => Promise<CheckPrivilegesResponse>; export const checkSavedObjectsPrivilegesWithRequestFactory = ( checkPrivilegesWithRequest: CheckPrivilegesWithRequest, getSpacesService: () => SpacesService | undefined ): CheckSavedObjectsPrivilegesWithRequest => { - return function checkSavedObjectsPrivilegesWithRequest(request: KibanaRequest) { + return function checkSavedObjectsPrivilegesWithRequest( + request: KibanaRequest + ): CheckSavedObjectsPrivileges { return async function checkSavedObjectsPrivileges( actions: string | string[], - namespace?: string + namespaceOrNamespaces?: string | string[] ) { const spacesService = getSpacesService(); - return spacesService - ? await checkPrivilegesWithRequest(request).atSpace( - spacesService.namespaceToSpaceId(namespace), - actions - ) - : await checkPrivilegesWithRequest(request).globally(actions); + if (Array.isArray(namespaceOrNamespaces)) { + if (spacesService === undefined) { + throw new Error( + `Can't check saved object privileges for multiple namespaces if Spaces is disabled` + ); + } else if (!namespaceOrNamespaces.length) { + throw new Error(`Can't check saved object privileges for 0 namespaces`); + } + const spaceIds = namespaceOrNamespaces.map(x => spacesService.namespaceToSpaceId(x)); + return await checkPrivilegesWithRequest(request).atSpaces(spaceIds, actions); + } else if (spacesService) { + const spaceId = spacesService.namespaceToSpaceId(namespaceOrNamespaces); + return await checkPrivilegesWithRequest(request).atSpace(spaceId, actions); + } + return await checkPrivilegesWithRequest(request).globally(actions); }; }; }; diff --git a/x-pack/plugins/security/server/authorization/disable_ui_capabilities.test.ts b/x-pack/plugins/security/server/authorization/disable_ui_capabilities.test.ts index 912ae60e12065..ea97a1b3b590c 100644 --- a/x-pack/plugins/security/server/authorization/disable_ui_capabilities.test.ts +++ b/x-pack/plugins/security/server/authorization/disable_ui_capabilities.test.ts @@ -11,7 +11,9 @@ import { httpServerMock, loggingServiceMock } from '../../../../../src/core/serv import { authorizationMock } from './index.mock'; import { Feature } from '../../../features/server'; -type MockAuthzOptions = { rejectCheckPrivileges: any } | { resolveCheckPrivileges: any }; +type MockAuthzOptions = + | { rejectCheckPrivileges: any } + | { resolveCheckPrivileges: { privileges: Array<{ privilege: string; authorized: boolean }> } }; const actions = new Actions('1.0.0-zeta1'); const mockRequest = httpServerMock.createKibanaRequest(); @@ -26,7 +28,8 @@ const createMockAuthz = (options: MockAuthzOptions) => { throw options.rejectCheckPrivileges; } - expect(checkActions).toEqual(Object.keys(options.resolveCheckPrivileges.privileges)); + const expected = options.resolveCheckPrivileges.privileges.map(x => x.privilege); + expect(checkActions).toEqual(expected); return options.resolveCheckPrivileges; }); }); @@ -226,17 +229,17 @@ describe('usingPrivileges', () => { test(`disables ui capabilities when they don't have privileges`, async () => { const mockAuthz = createMockAuthz({ resolveCheckPrivileges: { - privileges: { - [actions.ui.get('navLinks', 'foo')]: true, - [actions.ui.get('navLinks', 'bar')]: false, - [actions.ui.get('navLinks', 'quz')]: false, - [actions.ui.get('management', 'kibana', 'indices')]: true, - [actions.ui.get('management', 'kibana', 'settings')]: false, - [actions.ui.get('fooFeature', 'foo')]: true, - [actions.ui.get('fooFeature', 'bar')]: false, - [actions.ui.get('barFeature', 'foo')]: true, - [actions.ui.get('barFeature', 'bar')]: false, - }, + privileges: [ + { privilege: actions.ui.get('navLinks', 'foo'), authorized: true }, + { privilege: actions.ui.get('navLinks', 'bar'), authorized: false }, + { privilege: actions.ui.get('navLinks', 'quz'), authorized: false }, + { privilege: actions.ui.get('management', 'kibana', 'indices'), authorized: true }, + { privilege: actions.ui.get('management', 'kibana', 'settings'), authorized: false }, + { privilege: actions.ui.get('fooFeature', 'foo'), authorized: true }, + { privilege: actions.ui.get('fooFeature', 'bar'), authorized: false }, + { privilege: actions.ui.get('barFeature', 'foo'), authorized: true }, + { privilege: actions.ui.get('barFeature', 'bar'), authorized: false }, + ], }, }); @@ -314,15 +317,15 @@ describe('usingPrivileges', () => { test(`doesn't re-enable disabled uiCapabilities`, async () => { const mockAuthz = createMockAuthz({ resolveCheckPrivileges: { - privileges: { - [actions.ui.get('navLinks', 'foo')]: true, - [actions.ui.get('navLinks', 'bar')]: true, - [actions.ui.get('management', 'kibana', 'indices')]: true, - [actions.ui.get('fooFeature', 'foo')]: true, - [actions.ui.get('fooFeature', 'bar')]: true, - [actions.ui.get('barFeature', 'foo')]: true, - [actions.ui.get('barFeature', 'bar')]: true, - }, + privileges: [ + { privilege: actions.ui.get('navLinks', 'foo'), authorized: true }, + { privilege: actions.ui.get('navLinks', 'bar'), authorized: true }, + { privilege: actions.ui.get('management', 'kibana', 'indices'), authorized: true }, + { privilege: actions.ui.get('fooFeature', 'foo'), authorized: true }, + { privilege: actions.ui.get('fooFeature', 'bar'), authorized: true }, + { privilege: actions.ui.get('barFeature', 'foo'), authorized: true }, + { privilege: actions.ui.get('barFeature', 'bar'), authorized: true }, + ], }, }); diff --git a/x-pack/plugins/security/server/authorization/disable_ui_capabilities.ts b/x-pack/plugins/security/server/authorization/disable_ui_capabilities.ts index be26f52fbf756..f0f1a42ad0bd5 100644 --- a/x-pack/plugins/security/server/authorization/disable_ui_capabilities.ts +++ b/x-pack/plugins/security/server/authorization/disable_ui_capabilities.ts @@ -9,7 +9,7 @@ import { UICapabilities } from 'ui/capabilities'; import { KibanaRequest, Logger } from '../../../../../src/core/server'; import { Feature } from '../../../features/server'; -import { CheckPrivilegesAtResourceResponse } from './check_privileges'; +import { CheckPrivilegesResponse } from './check_privileges'; import { Authorization } from './index'; export function disableUICapabilitiesFactory( @@ -77,7 +77,7 @@ export function disableUICapabilitiesFactory( [] ); - let checkPrivilegesResponse: CheckPrivilegesAtResourceResponse; + let checkPrivilegesResponse: CheckPrivilegesResponse; try { const checkPrivilegesDynamically = authz.checkPrivilegesDynamicallyWithRequest(request); checkPrivilegesResponse = await checkPrivilegesDynamically(uiActions); @@ -105,7 +105,9 @@ export function disableUICapabilitiesFactory( } const action = authz.actions.ui.get(featureId, ...uiCapabilityParts); - return checkPrivilegesResponse.privileges[action] === true; + return checkPrivilegesResponse.privileges.some( + x => x.privilege === action && x.authorized === true + ); }; return mapValues(uiCapabilities, (featureUICapabilities, featureId) => { diff --git a/x-pack/plugins/security/server/plugin.ts b/x-pack/plugins/security/server/plugin.ts index 032d231fe798f..9dd4aaafa3494 100644 --- a/x-pack/plugins/security/server/plugin.ts +++ b/x-pack/plugins/security/server/plugin.ts @@ -149,6 +149,7 @@ export class Plugin { auditLogger: new SecurityAuditLogger(() => this.getLegacyAPI().auditLogger), authz, savedObjects: core.savedObjects, + getSpacesService: this.getSpacesService, }); core.capabilities.registerSwitcher(authz.disableUnauthorizedCapabilities); @@ -156,12 +157,12 @@ export class Plugin { defineRoutes({ router: core.http.createRouter(), basePath: core.http.basePath, + httpResources: core.http.resources, logger: this.initializerContext.logger.get('routes'), clusterClient: this.clusterClient, config, authc, authz, - csp: core.http.csp, license, }); diff --git a/x-pack/plugins/security/server/routes/authentication/index.ts b/x-pack/plugins/security/server/routes/authentication/index.ts index 7e9eb75bbf753..d09f65525f44e 100644 --- a/x-pack/plugins/security/server/routes/authentication/index.ts +++ b/x-pack/plugins/security/server/routes/authentication/index.ts @@ -11,17 +11,6 @@ import { defineCommonRoutes } from './common'; import { defineOIDCRoutes } from './oidc'; import { RouteDefinitionParams } from '..'; -export function createCustomResourceResponse(body: string, contentType: string, cspHeader: string) { - return { - body, - headers: { - 'content-type': contentType, - 'content-security-policy': cspHeader, - }, - statusCode: 200, - }; -} - export function defineAuthenticationRoutes(params: RouteDefinitionParams) { defineSessionRoutes(params); defineCommonRoutes(params); diff --git a/x-pack/plugins/security/server/routes/authentication/oidc.ts b/x-pack/plugins/security/server/routes/authentication/oidc.ts index d325a453af9d1..5d8a7ae7bdfea 100644 --- a/x-pack/plugins/security/server/routes/authentication/oidc.ts +++ b/x-pack/plugins/security/server/routes/authentication/oidc.ts @@ -8,7 +8,6 @@ import { schema } from '@kbn/config-schema'; import { i18n } from '@kbn/i18n'; import { KibanaRequest, KibanaResponseFactory } from '../../../../../../src/core/server'; import { OIDCLogin } from '../../authentication'; -import { createCustomResourceResponse } from '.'; import { createLicensedRouteHandler } from '../licensed_route_handler'; import { wrapIntoCustomErrorResponse } from '../../errors'; import { @@ -20,7 +19,13 @@ import { RouteDefinitionParams } from '..'; /** * Defines routes required for SAML authentication. */ -export function defineOIDCRoutes({ router, logger, authc, csp, basePath }: RouteDefinitionParams) { +export function defineOIDCRoutes({ + router, + httpResources, + logger, + authc, + basePath, +}: RouteDefinitionParams) { // Generate two identical routes with new and deprecated URL and issue a warning if route with deprecated URL is ever used. for (const path of ['/api/security/oidc/implicit', '/api/security/v1/oidc/implicit']) { /** @@ -28,7 +33,7 @@ export function defineOIDCRoutes({ router, logger, authc, csp, basePath }: Route * is used, so that we can extract authentication response from URL fragment and send it to * the `/api/security/oidc/callback` route. */ - router.get( + httpResources.register( { path, validate: false, @@ -42,18 +47,14 @@ export function defineOIDCRoutes({ router, logger, authc, csp, basePath }: Route { tags: ['deprecation'] } ); } - return response.custom( - createCustomResourceResponse( - ` - <!DOCTYPE html> - <title>Kibana OpenID Connect Login - - - `, - 'text/html', - csp.header - ) - ); + return response.renderHtml({ + body: ` + + Kibana OpenID Connect Login + + + `, + }); } ); } @@ -63,7 +64,7 @@ export function defineOIDCRoutes({ router, logger, authc, csp, basePath }: Route * that extracts fragment part from the URL and send it to the `/api/security/oidc/callback` route. * We need this separate endpoint because of default CSP policy that forbids inline scripts. */ - router.get( + httpResources.register( { path: '/internal/security/oidc/implicit.js', validate: false, @@ -71,17 +72,13 @@ export function defineOIDCRoutes({ router, logger, authc, csp, basePath }: Route }, (context, request, response) => { const serverBasePath = basePath.serverBasePath; - return response.custom( - createCustomResourceResponse( - ` + return response.renderJs({ + body: ` window.location.replace( '${serverBasePath}/api/security/oidc/callback?authenticationResponseURI=' + encodeURIComponent(window.location.href) ); `, - 'text/javascript', - csp.header - ) - ); + }); } ); @@ -155,7 +152,9 @@ export function defineOIDCRoutes({ router, logger, authc, csp, basePath }: Route } if (!loginAttempt) { - return response.badRequest({ body: 'Unrecognized login attempt.' }); + return response.badRequest({ + body: 'Unrecognized login attempt.', + }); } return performOIDCLogin(request, response, loginAttempt); diff --git a/x-pack/plugins/security/server/routes/authentication/saml.ts b/x-pack/plugins/security/server/routes/authentication/saml.ts index 8f08f250a1c75..30e1f6f336bdd 100644 --- a/x-pack/plugins/security/server/routes/authentication/saml.ts +++ b/x-pack/plugins/security/server/routes/authentication/saml.ts @@ -7,14 +7,19 @@ import { schema } from '@kbn/config-schema'; import { SAMLLogin } from '../../authentication'; import { SAMLAuthenticationProvider } from '../../authentication/providers'; -import { createCustomResourceResponse } from '.'; import { RouteDefinitionParams } from '..'; /** * Defines routes required for SAML authentication. */ -export function defineSAMLRoutes({ router, logger, authc, csp, basePath }: RouteDefinitionParams) { - router.get( +export function defineSAMLRoutes({ + router, + httpResources, + logger, + authc, + basePath, +}: RouteDefinitionParams) { + httpResources.register( { path: '/internal/security/saml/capture-url-fragment', validate: false, @@ -22,39 +27,30 @@ export function defineSAMLRoutes({ router, logger, authc, csp, basePath }: Route }, (context, request, response) => { // We're also preventing `favicon.ico` request since it can cause new SAML handshake. - return response.custom( - createCustomResourceResponse( - ` + return response.renderHtml({ + body: ` Kibana SAML Login `, - 'text/html', - csp.header - ) - ); + }); } ); - - router.get( + httpResources.register( { path: '/internal/security/saml/capture-url-fragment.js', validate: false, options: { authRequired: false }, }, (context, request, response) => { - return response.custom( - createCustomResourceResponse( - ` + return response.renderJs({ + body: ` window.location.replace( '${basePath.serverBasePath}/internal/security/saml/start?redirectURLFragment=' + encodeURIComponent(window.location.hash) ); `, - 'text/javascript', - csp.header - ) - ); + }); } ); diff --git a/x-pack/plugins/security/server/routes/index.mock.ts b/x-pack/plugins/security/server/routes/index.mock.ts index aaefdad6c221a..b0c74b98ee19b 100644 --- a/x-pack/plugins/security/server/routes/index.mock.ts +++ b/x-pack/plugins/security/server/routes/index.mock.ts @@ -8,6 +8,7 @@ import { elasticsearchServiceMock, httpServiceMock, loggingServiceMock, + httpResourcesMock, } from '../../../../../src/core/server/mocks'; import { authenticationMock } from '../authentication/index.mock'; import { authorizationMock } from '../authorization/index.mock'; @@ -27,5 +28,6 @@ export const routeDefinitionParamsMock = { authc: authenticationMock.create(), authz: authorizationMock.create(), license: licenseMock.create(), + httpResources: httpResourcesMock.createRegistrar(), }), }; diff --git a/x-pack/plugins/security/server/routes/index.ts b/x-pack/plugins/security/server/routes/index.ts index a372fcf092707..e43072b95c906 100644 --- a/x-pack/plugins/security/server/routes/index.ts +++ b/x-pack/plugins/security/server/routes/index.ts @@ -4,7 +4,13 @@ * you may not use this file except in compliance with the Elastic License. */ -import { CoreSetup, IClusterClient, IRouter, Logger } from '../../../../../src/core/server'; +import { + CoreSetup, + HttpResources, + IClusterClient, + IRouter, + Logger, +} from '../../../../../src/core/server'; import { SecurityLicense } from '../../common/licensing'; import { Authentication } from '../authentication'; import { Authorization } from '../authorization'; @@ -24,7 +30,7 @@ import { defineViewRoutes } from './views'; export interface RouteDefinitionParams { router: IRouter; basePath: CoreSetup['http']['basePath']; - csp: CoreSetup['http']['csp']; + httpResources: HttpResources; logger: Logger; clusterClient: IClusterClient; config: ConfigType; diff --git a/x-pack/plugins/security/server/routes/views/account_management.ts b/x-pack/plugins/security/server/routes/views/account_management.ts index 3c84483d8f494..696a5e12b64c1 100644 --- a/x-pack/plugins/security/server/routes/views/account_management.ts +++ b/x-pack/plugins/security/server/routes/views/account_management.ts @@ -9,11 +9,8 @@ import { RouteDefinitionParams } from '..'; /** * Defines routes required for the Account Management view. */ -export function defineAccountManagementRoutes({ router, csp }: RouteDefinitionParams) { - router.get({ path: '/security/account', validate: false }, async (context, request, response) => { - return response.ok({ - body: await context.core.rendering.render({ includeUserSettings: true }), - headers: { 'content-security-policy': csp.header }, - }); - }); +export function defineAccountManagementRoutes({ httpResources }: RouteDefinitionParams) { + httpResources.register({ path: '/security/account', validate: false }, (context, req, res) => + res.renderCoreApp() + ); } diff --git a/x-pack/plugins/security/server/routes/views/index.test.ts b/x-pack/plugins/security/server/routes/views/index.test.ts index 80f7f62a5ff43..a8e7e905b119a 100644 --- a/x-pack/plugins/security/server/routes/views/index.test.ts +++ b/x-pack/plugins/security/server/routes/views/index.test.ts @@ -17,7 +17,8 @@ describe('View routes', () => { defineViewRoutes(routeParamsMock); - expect(routeParamsMock.router.get.mock.calls.map(([{ path }]) => path)).toMatchInlineSnapshot(` + expect(routeParamsMock.httpResources.register.mock.calls.map(([{ path }]) => path)) + .toMatchInlineSnapshot(` Array [ "/security/account", "/security/logged_out", @@ -25,6 +26,9 @@ describe('View routes', () => { "/security/overwritten_session", ] `); + expect(routeParamsMock.router.get.mock.calls.map(([{ path }]) => path)).toMatchInlineSnapshot( + `Array []` + ); }); it('registers Login routes if `basic` provider is enabled', () => { @@ -35,16 +39,21 @@ describe('View routes', () => { defineViewRoutes(routeParamsMock); - expect(routeParamsMock.router.get.mock.calls.map(([{ path }]) => path)).toMatchInlineSnapshot(` + expect(routeParamsMock.httpResources.register.mock.calls.map(([{ path }]) => path)) + .toMatchInlineSnapshot(` Array [ "/login", - "/internal/security/login_state", "/security/account", "/security/logged_out", "/logout", "/security/overwritten_session", ] `); + expect(routeParamsMock.router.get.mock.calls.map(([{ path }]) => path)).toMatchInlineSnapshot(` + Array [ + "/internal/security/login_state", + ] + `); }); it('registers Login routes if `token` provider is enabled', () => { @@ -55,16 +64,21 @@ describe('View routes', () => { defineViewRoutes(routeParamsMock); - expect(routeParamsMock.router.get.mock.calls.map(([{ path }]) => path)).toMatchInlineSnapshot(` + expect(routeParamsMock.httpResources.register.mock.calls.map(([{ path }]) => path)) + .toMatchInlineSnapshot(` Array [ "/login", - "/internal/security/login_state", "/security/account", "/security/logged_out", "/logout", "/security/overwritten_session", ] `); + expect(routeParamsMock.router.get.mock.calls.map(([{ path }]) => path)).toMatchInlineSnapshot(` + Array [ + "/internal/security/login_state", + ] + `); }); it('registers Login routes if Login Selector is enabled even if both `token` and `basic` providers are not enabled', () => { @@ -75,15 +89,20 @@ describe('View routes', () => { defineViewRoutes(routeParamsMock); - expect(routeParamsMock.router.get.mock.calls.map(([{ path }]) => path)).toMatchInlineSnapshot(` + expect(routeParamsMock.httpResources.register.mock.calls.map(([{ path }]) => path)) + .toMatchInlineSnapshot(` Array [ "/login", - "/internal/security/login_state", "/security/account", "/security/logged_out", "/logout", "/security/overwritten_session", ] `); + expect(routeParamsMock.router.get.mock.calls.map(([{ path }]) => path)).toMatchInlineSnapshot(` + Array [ + "/internal/security/login_state", + ] + `); }); }); diff --git a/x-pack/plugins/security/server/routes/views/logged_out.test.ts b/x-pack/plugins/security/server/routes/views/logged_out.test.ts index 822802b62d874..3ff05d242d9dd 100644 --- a/x-pack/plugins/security/server/routes/views/logged_out.test.ts +++ b/x-pack/plugins/security/server/routes/views/logged_out.test.ts @@ -4,20 +4,16 @@ * you may not use this file except in compliance with the Elastic License. */ -import { - RequestHandler, - RouteConfig, - kibanaResponseFactory, -} from '../../../../../../src/core/server'; +import { HttpResourcesRequestHandler, RouteConfig } from '../../../../../../src/core/server'; import { Authentication } from '../../authentication'; import { defineLoggedOutRoutes } from './logged_out'; -import { coreMock, httpServerMock } from '../../../../../../src/core/server/mocks'; +import { httpServerMock, httpResourcesMock } from '../../../../../../src/core/server/mocks'; import { routeDefinitionParamsMock } from '../index.mock'; describe('LoggedOut view routes', () => { let authc: jest.Mocked; - let routeHandler: RequestHandler; + let routeHandler: HttpResourcesRequestHandler; let routeConfig: RouteConfig; beforeEach(() => { const routeParamsMock = routeDefinitionParamsMock.create(); @@ -28,7 +24,7 @@ describe('LoggedOut view routes', () => { const [ loggedOutRouteConfig, loggedOutRouteHandler, - ] = routeParamsMock.router.get.mock.calls.find( + ] = routeParamsMock.httpResources.register.mock.calls.find( ([{ path }]) => path === '/security/logged_out' )!; @@ -51,9 +47,11 @@ describe('LoggedOut view routes', () => { const request = httpServerMock.createKibanaRequest(); - await expect(routeHandler({} as any, request, kibanaResponseFactory)).resolves.toEqual({ - options: { headers: { location: '/mock-server-basepath/' } }, - status: 302, + const responseFactory = httpResourcesMock.createResponseFactory(); + await routeHandler({} as any, request, responseFactory); + + expect(responseFactory.redirected).toHaveBeenCalledWith({ + headers: { location: '/mock-server-basepath/' }, }); expect(authc.getSessionInfo).toHaveBeenCalledWith(request); @@ -63,21 +61,10 @@ describe('LoggedOut view routes', () => { authc.getSessionInfo.mockResolvedValue(null); const request = httpServerMock.createKibanaRequest(); - const contextMock = coreMock.createRequestHandlerContext(); - - await expect( - routeHandler({ core: contextMock } as any, request, kibanaResponseFactory) - ).resolves.toEqual({ - options: { - headers: { - 'content-security-policy': - "script-src 'unsafe-eval' 'self'; worker-src blob: 'self'; style-src 'unsafe-inline' 'self'", - }, - }, - status: 200, - }); + const responseFactory = httpResourcesMock.createResponseFactory(); + await routeHandler({} as any, request, responseFactory); expect(authc.getSessionInfo).toHaveBeenCalledWith(request); - expect(contextMock.rendering.render).toHaveBeenCalledWith({ includeUserSettings: false }); + expect(responseFactory.renderAnonymousCoreApp).toHaveBeenCalledWith(); }); }); diff --git a/x-pack/plugins/security/server/routes/views/logged_out.ts b/x-pack/plugins/security/server/routes/views/logged_out.ts index 2f69d8c35f03e..43c2f01b1b53d 100644 --- a/x-pack/plugins/security/server/routes/views/logged_out.ts +++ b/x-pack/plugins/security/server/routes/views/logged_out.ts @@ -16,13 +16,12 @@ import { RouteDefinitionParams } from '..'; * Defines routes required for the Logged Out view. */ export function defineLoggedOutRoutes({ - router, logger, authc, - csp, + httpResources, basePath, }: RouteDefinitionParams) { - router.get( + httpResources.register( { path: '/security/logged_out', validate: false, @@ -39,10 +38,7 @@ export function defineLoggedOutRoutes({ }); } - return response.ok({ - body: await context.core.rendering.render({ includeUserSettings: false }), - headers: { 'content-security-policy': csp.header }, - }); + return response.renderAnonymousCoreApp(); } ); } diff --git a/x-pack/plugins/security/server/routes/views/login.test.ts b/x-pack/plugins/security/server/routes/views/login.test.ts index 7751f9a952c09..d43319efbdfb9 100644 --- a/x-pack/plugins/security/server/routes/views/login.test.ts +++ b/x-pack/plugins/security/server/routes/views/login.test.ts @@ -7,26 +7,34 @@ import { URL } from 'url'; import { Type } from '@kbn/config-schema'; import { + HttpResources, + HttpResourcesRequestHandler, + IRouter, RequestHandler, - RouteConfig, kibanaResponseFactory, - IRouter, + RouteConfig, } from '../../../../../../src/core/server'; import { SecurityLicense } from '../../../common/licensing'; import { LoginState } from '../../../common/login_state'; import { ConfigType } from '../../config'; import { defineLoginRoutes } from './login'; -import { coreMock, httpServerMock } from '../../../../../../src/core/server/mocks'; +import { + coreMock, + httpServerMock, + httpResourcesMock, +} from '../../../../../../src/core/server/mocks'; import { routeDefinitionParamsMock } from '../index.mock'; describe('Login view routes', () => { + let httpResources: jest.Mocked; let router: jest.Mocked; let license: jest.Mocked; let config: ConfigType; beforeEach(() => { const routeParamsMock = routeDefinitionParamsMock.create(); router = routeParamsMock.router; + httpResources = routeParamsMock.httpResources; license = routeParamsMock.license; config = routeParamsMock.config; @@ -34,10 +42,10 @@ describe('Login view routes', () => { }); describe('View route', () => { - let routeHandler: RequestHandler; + let routeHandler: HttpResourcesRequestHandler; let routeConfig: RouteConfig; beforeEach(() => { - const [loginRouteConfig, loginRouteHandler] = router.get.mock.calls.find( + const [loginRouteConfig, loginRouteHandler] = httpResources.register.mock.calls.find( ([{ path }]) => path === '/login' )!; @@ -96,9 +104,11 @@ describe('Login view routes', () => { 'https://kibana.co' ); license.getFeatures.mockReturnValue({ showLogin: true } as any); - await expect(routeHandler({} as any, request, kibanaResponseFactory)).resolves.toEqual({ - options: { headers: { location: `${expectedLocation}` } }, - status: 302, + const responseFactory = httpResourcesMock.createResponseFactory(); + + await routeHandler({} as any, request, responseFactory); + expect(responseFactory.redirected).toHaveBeenCalledWith({ + headers: { location: `${expectedLocation}` }, }); // Redirect if `showLogin` is `false` even if user is not authenticated. @@ -108,9 +118,12 @@ describe('Login view routes', () => { 'https://kibana.co' ); license.getFeatures.mockReturnValue({ showLogin: false } as any); - await expect(routeHandler({} as any, request, kibanaResponseFactory)).resolves.toEqual({ - options: { headers: { location: `${expectedLocation}` } }, - status: 302, + responseFactory.redirected.mockClear(); + + await routeHandler({} as any, request, responseFactory); + + expect(responseFactory.redirected).toHaveBeenCalledWith({ + headers: { location: `${expectedLocation}` }, }); } }); @@ -121,19 +134,9 @@ describe('Login view routes', () => { const request = httpServerMock.createKibanaRequest({ auth: { isAuthenticated: false } }); const contextMock = coreMock.createRequestHandlerContext(); - await expect( - routeHandler({ core: contextMock } as any, request, kibanaResponseFactory) - ).resolves.toEqual({ - options: { - headers: { - 'content-security-policy': - "script-src 'unsafe-eval' 'self'; worker-src blob: 'self'; style-src 'unsafe-inline' 'self'", - }, - }, - status: 200, - }); - - expect(contextMock.rendering.render).toHaveBeenCalledWith({ includeUserSettings: false }); + const responseFactory = httpResourcesMock.createResponseFactory(); + await routeHandler({ core: contextMock } as any, request, responseFactory); + expect(responseFactory.renderAnonymousCoreApp).toHaveBeenCalledWith(); }); }); diff --git a/x-pack/plugins/security/server/routes/views/login.ts b/x-pack/plugins/security/server/routes/views/login.ts index 4cabd4337971c..4d6747de713f7 100644 --- a/x-pack/plugins/security/server/routes/views/login.ts +++ b/x-pack/plugins/security/server/routes/views/login.ts @@ -16,11 +16,11 @@ export function defineLoginRoutes({ config, router, logger, - csp, + httpResources, basePath, license, }: RouteDefinitionParams) { - router.get( + httpResources.register( { path: '/login', validate: { @@ -45,10 +45,7 @@ export function defineLoginRoutes({ }); } - return response.ok({ - body: await context.core.rendering.render({ includeUserSettings: false }), - headers: { 'content-security-policy': csp.header }, - }); + return response.renderAnonymousCoreApp(); } ); diff --git a/x-pack/plugins/security/server/routes/views/logout.ts b/x-pack/plugins/security/server/routes/views/logout.ts index 8fa8e689a1c38..370cb069096a3 100644 --- a/x-pack/plugins/security/server/routes/views/logout.ts +++ b/x-pack/plugins/security/server/routes/views/logout.ts @@ -9,18 +9,9 @@ import { RouteDefinitionParams } from '..'; /** * Defines routes required for the Logout out view. */ -export function defineLogoutRoutes({ router, csp }: RouteDefinitionParams) { - router.get( - { - path: '/logout', - validate: false, - options: { authRequired: false }, - }, - async (context, request, response) => { - return response.ok({ - body: await context.core.rendering.render({ includeUserSettings: false }), - headers: { 'content-security-policy': csp.header }, - }); - } +export function defineLogoutRoutes({ httpResources }: RouteDefinitionParams) { + httpResources.register( + { path: '/logout', validate: false, options: { authRequired: false } }, + (context, request, response) => response.renderAnonymousCoreApp() ); } diff --git a/x-pack/plugins/security/server/routes/views/overwritten_session.ts b/x-pack/plugins/security/server/routes/views/overwritten_session.ts index c21ab1c207362..ee4988cb122cc 100644 --- a/x-pack/plugins/security/server/routes/views/overwritten_session.ts +++ b/x-pack/plugins/security/server/routes/views/overwritten_session.ts @@ -9,14 +9,9 @@ import { RouteDefinitionParams } from '..'; /** * Defines routes required for the Overwritten Session view. */ -export function defineOverwrittenSessionRoutes({ router, csp }: RouteDefinitionParams) { - router.get( +export function defineOverwrittenSessionRoutes({ httpResources }: RouteDefinitionParams) { + httpResources.register( { path: '/security/overwritten_session', validate: false }, - async (context, request, response) => { - return response.ok({ - body: await context.core.rendering.render({ includeUserSettings: true }), - headers: { 'content-security-policy': csp.header }, - }); - } + (context, req, res) => res.renderCoreApp() ); } diff --git a/x-pack/plugins/security/server/saved_objects/index.ts b/x-pack/plugins/security/server/saved_objects/index.ts index 5954729562847..7dac745fcf84b 100644 --- a/x-pack/plugins/security/server/saved_objects/index.ts +++ b/x-pack/plugins/security/server/saved_objects/index.ts @@ -13,14 +13,21 @@ import { import { SecureSavedObjectsClientWrapper } from './secure_saved_objects_client_wrapper'; import { Authorization } from '../authorization'; import { SecurityAuditLogger } from '../audit'; +import { SpacesService } from '../plugin'; interface SetupSavedObjectsParams { auditLogger: SecurityAuditLogger; authz: Pick; savedObjects: CoreSetup['savedObjects']; + getSpacesService(): SpacesService | undefined; } -export function setupSavedObjects({ auditLogger, authz, savedObjects }: SetupSavedObjectsParams) { +export function setupSavedObjects({ + auditLogger, + authz, + savedObjects, + getSpacesService, +}: SetupSavedObjectsParams) { const getKibanaRequest = (request: KibanaRequest | LegacyRequest) => request instanceof KibanaRequest ? request : KibanaRequest.from(request); @@ -44,6 +51,7 @@ export function setupSavedObjects({ auditLogger, authz, savedObjects }: SetupSav kibanaRequest ), errors: SavedObjectsClient.errors, + getSpacesService, }) : client; }); diff --git a/x-pack/plugins/security/server/saved_objects/secure_saved_objects_client_wrapper.test.ts b/x-pack/plugins/security/server/saved_objects/secure_saved_objects_client_wrapper.test.ts index 3c04508e3a74a..3c4034e07f995 100644 --- a/x-pack/plugins/security/server/saved_objects/secure_saved_objects_client_wrapper.test.ts +++ b/x-pack/plugins/security/server/saved_objects/secure_saved_objects_client_wrapper.test.ts @@ -9,6 +9,11 @@ import { Actions } from '../authorization'; import { securityAuditLoggerMock } from '../audit/index.mock'; import { savedObjectsClientMock } from '../../../../../src/core/server/mocks'; import { SavedObjectsClientContract } from 'kibana/server'; +import { SavedObjectActions } from '../authorization/actions/saved_object'; + +let clientOpts: ReturnType; +let client: SecureSavedObjectsClientWrapper; +const USERNAME = Symbol(); const createSecureSavedObjectsClientWrapperOptions = () => { const actions = new Actions('some-version'); @@ -22,810 +27,677 @@ const createSecureSavedObjectsClientWrapperOptions = () => { const errors = ({ decorateForbiddenError: jest.fn().mockReturnValue(forbiddenError), decorateGeneralError: jest.fn().mockReturnValue(generalError), + isNotFoundError: jest.fn().mockReturnValue(false), } as unknown) as jest.Mocked; + const getSpacesService = jest.fn().mockReturnValue(true); return { actions, baseClient: savedObjectsClientMock.create(), checkSavedObjectsPrivilegesAsCurrentUser: jest.fn(), errors, + getSpacesService, auditLogger: securityAuditLoggerMock.create(), forbiddenError, generalError, }; }; -describe('#errors', () => { - test(`assigns errors from constructor to .errors`, () => { - const options = createSecureSavedObjectsClientWrapperOptions(); - const client = new SecureSavedObjectsClientWrapper(options); +const expectGeneralError = async (fn: Function, args: Record) => { + // mock the checkPrivileges.globally rejection + const rejection = new Error('An actual error would happen here'); + clientOpts.checkSavedObjectsPrivilegesAsCurrentUser.mockRejectedValue(rejection); + + await expect(fn.bind(client)(...Object.values(args))).rejects.toThrowError( + clientOpts.generalError + ); + expect(clientOpts.errors.decorateGeneralError).toHaveBeenCalledTimes(1); + expect(clientOpts.auditLogger.savedObjectsAuthorizationFailure).not.toHaveBeenCalled(); + expect(clientOpts.auditLogger.savedObjectsAuthorizationSuccess).not.toHaveBeenCalled(); +}; + +/** + * Fails the first authorization check, passes any others + * Requires that function args are passed in as key/value pairs + * The argument properties must be in the correct order to be spread properly + */ +const expectForbiddenError = async (fn: Function, args: Record) => { + clientOpts.checkSavedObjectsPrivilegesAsCurrentUser.mockImplementation( + getMockCheckPrivilegesFailure + ); + + await expect(fn.bind(client)(...Object.values(args))).rejects.toThrowError( + clientOpts.forbiddenError + ); + const getCalls = (clientOpts.actions.savedObject.get as jest.MockedFunction< + SavedObjectActions['get'] + >).mock.calls; + const actions = clientOpts.checkSavedObjectsPrivilegesAsCurrentUser.mock.calls[0][0]; + const spaceId = args.options?.namespace || 'default'; + + const ACTION = getCalls[0][1]; + const types = getCalls.map(x => x[0]); + const missing = [{ spaceId, privilege: actions[0] }]; // if there was more than one type, only the first type was unauthorized + const spaceIds = [spaceId]; + + expect(clientOpts.errors.decorateForbiddenError).toHaveBeenCalledTimes(1); + expect(clientOpts.auditLogger.savedObjectsAuthorizationFailure).toHaveBeenCalledTimes(1); + expect(clientOpts.auditLogger.savedObjectsAuthorizationFailure).toHaveBeenCalledWith( + USERNAME, + ACTION, + types, + spaceIds, + missing, + args + ); + expect(clientOpts.auditLogger.savedObjectsAuthorizationSuccess).not.toHaveBeenCalled(); +}; + +const expectSuccess = async (fn: Function, args: Record) => { + const result = await fn.bind(client)(...Object.values(args)); + const getCalls = (clientOpts.actions.savedObject.get as jest.MockedFunction< + SavedObjectActions['get'] + >).mock.calls; + const ACTION = getCalls[0][1]; + const types = getCalls.map(x => x[0]); + const spaceIds = [args.options?.namespace || 'default']; + + expect(clientOpts.auditLogger.savedObjectsAuthorizationFailure).not.toHaveBeenCalled(); + expect(clientOpts.auditLogger.savedObjectsAuthorizationSuccess).toHaveBeenCalledTimes(1); + expect(clientOpts.auditLogger.savedObjectsAuthorizationSuccess).toHaveBeenCalledWith( + USERNAME, + ACTION, + types, + spaceIds, + args + ); + return result; +}; + +const expectPrivilegeCheck = async (fn: Function, args: Record) => { + clientOpts.checkSavedObjectsPrivilegesAsCurrentUser.mockImplementation( + getMockCheckPrivilegesFailure + ); + + await expect(fn.bind(client)(...Object.values(args))).rejects.toThrow(); // test is simpler with error case + const getResults = (clientOpts.actions.savedObject.get as jest.MockedFunction< + SavedObjectActions['get'] + >).mock.results; + const actions = getResults.map(x => x.value); + + expect(clientOpts.checkSavedObjectsPrivilegesAsCurrentUser).toHaveBeenCalledTimes(1); + expect(clientOpts.checkSavedObjectsPrivilegesAsCurrentUser).toHaveBeenCalledWith( + actions, + args.options?.namespace + ); +}; + +const expectObjectNamespaceFiltering = async (fn: Function, args: Record) => { + clientOpts.checkSavedObjectsPrivilegesAsCurrentUser.mockImplementationOnce( + getMockCheckPrivilegesSuccess // privilege check for authorization + ); + clientOpts.checkSavedObjectsPrivilegesAsCurrentUser.mockImplementation( + getMockCheckPrivilegesFailure // privilege check for namespace filtering + ); + + const authorizedNamespace = args.options.namespace || 'default'; + const namespaces = ['some-other-namespace', authorizedNamespace]; + const returnValue = { namespaces, foo: 'bar' }; + // we don't know which base client method will be called; mock them all + clientOpts.baseClient.create.mockReturnValue(returnValue as any); + clientOpts.baseClient.get.mockReturnValue(returnValue as any); + clientOpts.baseClient.update.mockReturnValue(returnValue as any); + + const result = await fn.bind(client)(...Object.values(args)); + expect(result).toEqual(expect.objectContaining({ namespaces: [authorizedNamespace, '?'] })); + + expect(clientOpts.checkSavedObjectsPrivilegesAsCurrentUser).toHaveBeenCalledTimes(2); + expect(clientOpts.checkSavedObjectsPrivilegesAsCurrentUser).toHaveBeenLastCalledWith( + 'login:', + namespaces + ); +}; + +const expectObjectsNamespaceFiltering = async (fn: Function, args: Record) => { + clientOpts.checkSavedObjectsPrivilegesAsCurrentUser.mockImplementationOnce( + getMockCheckPrivilegesSuccess // privilege check for authorization + ); + clientOpts.checkSavedObjectsPrivilegesAsCurrentUser.mockImplementation( + getMockCheckPrivilegesFailure // privilege check for namespace filtering + ); + + const authorizedNamespace = args.options.namespace || 'default'; + const returnValue = { + saved_objects: [ + { namespaces: ['foo'] }, + { namespaces: [authorizedNamespace] }, + { namespaces: ['foo', authorizedNamespace] }, + ], + }; + + // we don't know which base client method will be called; mock them all + clientOpts.baseClient.bulkCreate.mockReturnValue(returnValue as any); + clientOpts.baseClient.bulkGet.mockReturnValue(returnValue as any); + clientOpts.baseClient.bulkUpdate.mockReturnValue(returnValue as any); + clientOpts.baseClient.find.mockReturnValue(returnValue as any); + + const result = await fn.bind(client)(...Object.values(args)); + expect(result).toEqual( + expect.objectContaining({ + saved_objects: [ + { namespaces: ['?'] }, + { namespaces: [authorizedNamespace] }, + { namespaces: [authorizedNamespace, '?'] }, + ], + }) + ); + + expect(clientOpts.checkSavedObjectsPrivilegesAsCurrentUser).toHaveBeenCalledTimes(2); + expect(clientOpts.checkSavedObjectsPrivilegesAsCurrentUser).toHaveBeenLastCalledWith('login:', [ + 'foo', + authorizedNamespace, + ]); +}; + +function getMockCheckPrivilegesSuccess(actions: string | string[], namespaces?: string | string[]) { + const _namespaces = Array.isArray(namespaces) ? namespaces : [namespaces || 'default']; + const _actions = Array.isArray(actions) ? actions : [actions]; + return { + hasAllRequested: true, + username: USERNAME, + privileges: _namespaces + .map(resource => + _actions.map(action => ({ + resource, + privilege: action, + authorized: true, + })) + ) + .flat(), + }; +} + +/** + * Fails the authorization check for the first privilege, and passes any others + * This check may be for an action for two different types in the same namespace + * Or, it may be for an action for the same type in two different namespaces + * Either way, the first privilege check returned is false, and any others return true + */ +function getMockCheckPrivilegesFailure(actions: string | string[], namespaces?: string | string[]) { + const _namespaces = Array.isArray(namespaces) ? namespaces : [namespaces || 'default']; + const _actions = Array.isArray(actions) ? actions : [actions]; + return { + hasAllRequested: false, + username: USERNAME, + privileges: _namespaces + .map((resource, idxa) => + _actions.map((action, idxb) => ({ + resource, + privilege: action, + authorized: idxa > 0 || idxb > 0, + })) + ) + .flat(), + }; +} + +/** + * Before each test, create the Client with its Options + */ +beforeEach(() => { + clientOpts = createSecureSavedObjectsClientWrapperOptions(); + client = new SecureSavedObjectsClientWrapper(clientOpts); + + // succeed privilege checks by default + clientOpts.checkSavedObjectsPrivilegesAsCurrentUser.mockImplementation( + getMockCheckPrivilegesSuccess + ); +}); + +describe('#addToNamespaces', () => { + const type = 'foo'; + const id = `${type}-id`; + const newNs1 = 'foo-namespace'; + const newNs2 = 'bar-namespace'; + const namespaces = [newNs1, newNs2]; + const currentNs = 'default'; + const privilege1 = `mock-saved_object:${type}/create`; + const privilege2 = `mock-saved_object:${type}/update`; + + test(`throws decorated GeneralError when hasPrivileges rejects promise`, async () => { + await expectGeneralError(client.addToNamespaces, { type, id, namespaces }); + }); + + test(`throws decorated ForbiddenError when unauthorized to create in new space`, async () => { + clientOpts.checkSavedObjectsPrivilegesAsCurrentUser.mockImplementation( + getMockCheckPrivilegesFailure + ); + + await expect(client.addToNamespaces(type, id, namespaces)).rejects.toThrowError( + clientOpts.forbiddenError + ); + + expect(clientOpts.errors.decorateForbiddenError).toHaveBeenCalledTimes(1); + expect(clientOpts.auditLogger.savedObjectsAuthorizationFailure).toHaveBeenCalledTimes(1); + expect(clientOpts.auditLogger.savedObjectsAuthorizationFailure).toHaveBeenCalledWith( + USERNAME, + 'addToNamespacesCreate', + [type], + namespaces.sort(), + [{ privilege: privilege1, spaceId: newNs1 }], + { id, type, namespaces, options: {} } + ); + expect(clientOpts.auditLogger.savedObjectsAuthorizationSuccess).not.toHaveBeenCalled(); + }); + + test(`throws decorated ForbiddenError when unauthorized to update in current space`, async () => { + clientOpts.checkSavedObjectsPrivilegesAsCurrentUser.mockImplementationOnce( + getMockCheckPrivilegesSuccess // create + ); + clientOpts.checkSavedObjectsPrivilegesAsCurrentUser.mockImplementation( + getMockCheckPrivilegesFailure // update + ); + + await expect(client.addToNamespaces(type, id, namespaces)).rejects.toThrowError( + clientOpts.forbiddenError + ); + + expect(clientOpts.errors.decorateForbiddenError).toHaveBeenCalledTimes(1); + expect(clientOpts.auditLogger.savedObjectsAuthorizationFailure).toHaveBeenCalledTimes(1); + expect( + clientOpts.auditLogger.savedObjectsAuthorizationFailure + ).toHaveBeenLastCalledWith( + USERNAME, + 'addToNamespacesUpdate', + [type], + [currentNs], + [{ privilege: privilege2, spaceId: currentNs }], + { id, type, namespaces, options: {} } + ); + expect(clientOpts.auditLogger.savedObjectsAuthorizationSuccess).toHaveBeenCalledTimes(1); + }); + + test(`returns result of baseClient.addToNamespaces when authorized`, async () => { + const apiCallReturnValue = Symbol(); + clientOpts.baseClient.addToNamespaces.mockReturnValue(apiCallReturnValue as any); + + const result = await client.addToNamespaces(type, id, namespaces); + expect(result).toBe(apiCallReturnValue); + + expect(clientOpts.auditLogger.savedObjectsAuthorizationFailure).not.toHaveBeenCalled(); + expect(clientOpts.auditLogger.savedObjectsAuthorizationSuccess).toHaveBeenCalledTimes(2); + expect(clientOpts.auditLogger.savedObjectsAuthorizationSuccess).toHaveBeenNthCalledWith( + 1, + USERNAME, + 'addToNamespacesCreate', // action for privilege check is 'create', but auditAction is 'addToNamespacesCreate' + [type], + namespaces.sort(), + { type, id, namespaces, options: {} } + ); + expect(clientOpts.auditLogger.savedObjectsAuthorizationSuccess).toHaveBeenNthCalledWith( + 2, + USERNAME, + 'addToNamespacesUpdate', // action for privilege check is 'update', but auditAction is 'addToNamespacesUpdate' + [type], + [currentNs], + { type, id, namespaces, options: {} } + ); + }); + + test(`checks privileges for user, actions, and namespace`, async () => { + clientOpts.checkSavedObjectsPrivilegesAsCurrentUser.mockImplementationOnce( + getMockCheckPrivilegesSuccess // create + ); + clientOpts.checkSavedObjectsPrivilegesAsCurrentUser.mockImplementation( + getMockCheckPrivilegesFailure // update + ); + + await expect(client.addToNamespaces(type, id, namespaces)).rejects.toThrow(); // test is simpler with error case + + expect(clientOpts.checkSavedObjectsPrivilegesAsCurrentUser).toHaveBeenCalledTimes(2); + expect(clientOpts.checkSavedObjectsPrivilegesAsCurrentUser).toHaveBeenNthCalledWith( + 1, + [privilege1], + namespaces + ); + expect(clientOpts.checkSavedObjectsPrivilegesAsCurrentUser).toHaveBeenNthCalledWith( + 2, + [privilege2], + undefined // default namespace + ); + }); +}); + +describe('#bulkCreate', () => { + const attributes = { some: 'attr' }; + const obj1 = Object.freeze({ type: 'foo', otherThing: 'sup', attributes }); + const obj2 = Object.freeze({ type: 'bar', otherThing: 'everyone', attributes }); + const options = Object.freeze({ namespace: 'some-ns' }); + + test(`throws decorated GeneralError when hasPrivileges rejects promise`, async () => { + const objects = [obj1]; + await expectGeneralError(client.bulkCreate, { objects }); + }); + + test(`throws decorated ForbiddenError when unauthorized`, async () => { + const objects = [obj1, obj2]; + await expectForbiddenError(client.bulkCreate, { objects, options }); + }); + + test(`returns result of baseClient.bulkCreate when authorized`, async () => { + const apiCallReturnValue = { saved_objects: [], foo: 'bar' }; + clientOpts.baseClient.bulkCreate.mockReturnValue(apiCallReturnValue as any); + + const objects = [obj1, obj2]; + const result = await expectSuccess(client.bulkCreate, { objects, options }); + expect(result).toEqual(apiCallReturnValue); + }); + + test(`checks privileges for user, actions, and namespace`, async () => { + const objects = [obj1, obj2]; + await expectPrivilegeCheck(client.bulkCreate, { objects, options }); + }); + + test(`filters namespaces that the user doesn't have access to`, async () => { + const objects = [obj1, obj2]; + await expectObjectsNamespaceFiltering(client.bulkCreate, { objects, options }); + }); +}); + +describe('#bulkGet', () => { + const obj1 = Object.freeze({ type: 'foo', id: 'foo-id' }); + const obj2 = Object.freeze({ type: 'bar', id: 'bar-id' }); + const options = Object.freeze({ namespace: 'some-ns' }); + + test(`throws decorated GeneralError when hasPrivileges rejects promise`, async () => { + const objects = [obj1]; + await expectGeneralError(client.bulkGet, { objects }); + }); + + test(`throws decorated ForbiddenError when unauthorized`, async () => { + const objects = [obj1, obj2]; + await expectForbiddenError(client.bulkGet, { objects, options }); + }); + + test(`returns result of baseClient.bulkGet when authorized`, async () => { + const apiCallReturnValue = { saved_objects: [], foo: 'bar' }; + clientOpts.baseClient.bulkGet.mockReturnValue(apiCallReturnValue as any); + + const objects = [obj1, obj2]; + const result = await expectSuccess(client.bulkGet, { objects, options }); + expect(result).toEqual(apiCallReturnValue); + }); + + test(`checks privileges for user, actions, and namespace`, async () => { + const objects = [obj1, obj2]; + await expectPrivilegeCheck(client.bulkGet, { objects, options }); + }); + + test(`filters namespaces that the user doesn't have access to`, async () => { + const objects = [obj1, obj2]; + await expectObjectsNamespaceFiltering(client.bulkGet, { objects, options }); + }); +}); + +describe('#bulkUpdate', () => { + const obj1 = Object.freeze({ type: 'foo', id: 'foo-id', attributes: { some: 'attr' } }); + const obj2 = Object.freeze({ type: 'bar', id: 'bar-id', attributes: { other: 'attr' } }); + const options = Object.freeze({ namespace: 'some-ns' }); + + test(`throws decorated GeneralError when hasPrivileges rejects promise`, async () => { + const objects = [obj1]; + await expectGeneralError(client.bulkUpdate, { objects }); + }); + + test(`throws decorated ForbiddenError when unauthorized`, async () => { + const objects = [obj1, obj2]; + await expectForbiddenError(client.bulkUpdate, { objects, options }); + }); + + test(`returns result of baseClient.bulkUpdate when authorized`, async () => { + const apiCallReturnValue = { saved_objects: [], foo: 'bar' }; + clientOpts.baseClient.bulkUpdate.mockReturnValue(apiCallReturnValue as any); + + const objects = [obj1, obj2]; + const result = await expectSuccess(client.bulkUpdate, { objects, options }); + expect(result).toEqual(apiCallReturnValue); + }); + + test(`checks privileges for user, actions, and namespace`, async () => { + const objects = [obj1, obj2]; + await expectPrivilegeCheck(client.bulkUpdate, { objects, options }); + }); + + test(`filters namespaces that the user doesn't have access to`, async () => { + const objects = [obj1, obj2]; + await expectObjectsNamespaceFiltering(client.bulkUpdate, { objects, options }); + }); +}); + +describe('#create', () => { + const type = 'foo'; + const attributes = { some_attr: 's' }; + const options = Object.freeze({ namespace: 'some-ns' }); + + test(`throws decorated GeneralError when checkPrivileges.globally rejects promise`, async () => { + await expectGeneralError(client.create, { type }); + }); + + test(`throws decorated ForbiddenError when unauthorized`, async () => { + await expectForbiddenError(client.create, { type, attributes, options }); + }); + + test(`returns result of baseClient.create when authorized`, async () => { + const apiCallReturnValue = Symbol(); + clientOpts.baseClient.create.mockResolvedValue(apiCallReturnValue as any); + + const result = await expectSuccess(client.create, { type, attributes, options }); + expect(result).toBe(apiCallReturnValue); + }); - expect(client.errors).toBe(options.errors); + test(`checks privileges for user, actions, and namespace`, async () => { + await expectPrivilegeCheck(client.create, { type, attributes, options }); + }); + + test(`filters namespaces that the user doesn't have access to`, async () => { + await expectObjectNamespaceFiltering(client.create, { type, attributes, options }); }); }); -describe(`spaces disabled`, () => { - describe('#create', () => { - test(`throws decorated GeneralError when checkPrivileges.globally rejects promise`, async () => { - const type = 'foo'; - const options = createSecureSavedObjectsClientWrapperOptions(); - options.checkSavedObjectsPrivilegesAsCurrentUser.mockRejectedValue( - new Error('An actual error would happen here') - ); - const client = new SecureSavedObjectsClientWrapper(options); - - await expect(client.create(type)).rejects.toThrowError(options.generalError); - expect(options.checkSavedObjectsPrivilegesAsCurrentUser).toHaveBeenCalledWith( - [options.actions.savedObject.get(type, 'create')], - undefined - ); - expect(options.errors.decorateGeneralError).toHaveBeenCalledTimes(1); - expect(options.auditLogger.savedObjectsAuthorizationFailure).not.toHaveBeenCalled(); - expect(options.auditLogger.savedObjectsAuthorizationSuccess).not.toHaveBeenCalled(); - }); - - test(`throws decorated ForbiddenError when unauthorized`, async () => { - const type = 'foo'; - const username = Symbol(); - const options = createSecureSavedObjectsClientWrapperOptions(); - options.checkSavedObjectsPrivilegesAsCurrentUser.mockResolvedValue({ - hasAllRequested: false, - username, - privileges: { [options.actions.savedObject.get(type, 'create')]: false }, - }); - - const client = new SecureSavedObjectsClientWrapper(options); - - const attributes = { some_attr: 's' }; - const apiCallOptions = Object.freeze({ namespace: 'some-ns' }); - await expect(client.create(type, attributes, apiCallOptions)).rejects.toThrowError( - options.forbiddenError - ); - - expect(options.checkSavedObjectsPrivilegesAsCurrentUser).toHaveBeenCalledWith( - [options.actions.savedObject.get(type, 'create')], - apiCallOptions.namespace - ); - expect(options.errors.decorateForbiddenError).toHaveBeenCalledTimes(1); - expect(options.auditLogger.savedObjectsAuthorizationFailure).toHaveBeenCalledWith( - username, - 'create', - [type], - [options.actions.savedObject.get(type, 'create')], - { type, attributes, options: apiCallOptions } - ); - expect(options.auditLogger.savedObjectsAuthorizationSuccess).not.toHaveBeenCalled(); - }); - - test(`returns result of baseClient.create when authorized`, async () => { - const type = 'foo'; - const username = Symbol(); - const options = createSecureSavedObjectsClientWrapperOptions(); - options.checkSavedObjectsPrivilegesAsCurrentUser.mockResolvedValue({ - hasAllRequested: true, - username, - privileges: { [options.actions.savedObject.get(type, 'create')]: true }, - }); - - const apiCallReturnValue = Symbol(); - options.baseClient.create.mockReturnValue(apiCallReturnValue as any); - - const client = new SecureSavedObjectsClientWrapper(options); - - const attributes = { some_attr: 's' }; - const apiCallOptions = Object.freeze({ namespace: 'some-ns' }); - await expect(client.create(type, attributes, apiCallOptions)).resolves.toBe( - apiCallReturnValue - ); - - expect(options.checkSavedObjectsPrivilegesAsCurrentUser).toHaveBeenCalledWith( - [options.actions.savedObject.get(type, 'create')], - apiCallOptions.namespace - ); - expect(options.baseClient.create).toHaveBeenCalledWith(type, attributes, apiCallOptions); - expect(options.auditLogger.savedObjectsAuthorizationFailure).not.toHaveBeenCalled(); - expect(options.auditLogger.savedObjectsAuthorizationSuccess).toHaveBeenCalledWith( - username, - 'create', - [type], - { type, attributes, options: apiCallOptions } - ); - }); - }); - - describe('#bulkCreate', () => { - test(`throws decorated GeneralError when hasPrivileges rejects promise`, async () => { - const type = 'foo'; - const options = createSecureSavedObjectsClientWrapperOptions(); - options.checkSavedObjectsPrivilegesAsCurrentUser.mockRejectedValue( - new Error('An actual error would happen here') - ); - const client = new SecureSavedObjectsClientWrapper(options); - - const apiCallOptions = Object.freeze({ namespace: 'some-ns' }); - await expect( - client.bulkCreate([{ type, attributes: {} }], apiCallOptions) - ).rejects.toThrowError(options.generalError); - expect(options.checkSavedObjectsPrivilegesAsCurrentUser).toHaveBeenCalledWith( - [options.actions.savedObject.get(type, 'bulk_create')], - apiCallOptions.namespace - ); - expect(options.errors.decorateGeneralError).toHaveBeenCalledTimes(1); - expect(options.auditLogger.savedObjectsAuthorizationFailure).not.toHaveBeenCalled(); - expect(options.auditLogger.savedObjectsAuthorizationSuccess).not.toHaveBeenCalled(); - }); - - test(`throws decorated ForbiddenError when unauthorized`, async () => { - const type1 = 'foo'; - const type2 = 'bar'; - const username = Symbol(); - const options = createSecureSavedObjectsClientWrapperOptions(); - options.checkSavedObjectsPrivilegesAsCurrentUser.mockResolvedValue({ - hasAllRequested: false, - username, - privileges: { - [options.actions.savedObject.get(type1, 'bulk_create')]: false, - [options.actions.savedObject.get(type2, 'bulk_create')]: true, - }, - }); - - const client = new SecureSavedObjectsClientWrapper(options); - - const objects = [ - { type: type1, attributes: {} }, - { type: type2, attributes: {} }, - ]; - const apiCallOptions = Object.freeze({ namespace: 'some-ns' }); - await expect(client.bulkCreate(objects, apiCallOptions)).rejects.toThrowError( - options.forbiddenError - ); - - expect(options.checkSavedObjectsPrivilegesAsCurrentUser).toHaveBeenCalledWith( - [ - options.actions.savedObject.get(type1, 'bulk_create'), - options.actions.savedObject.get(type2, 'bulk_create'), - ], - apiCallOptions.namespace - ); - expect(options.errors.decorateForbiddenError).toHaveBeenCalledTimes(1); - expect(options.auditLogger.savedObjectsAuthorizationFailure).toHaveBeenCalledWith( - username, - 'bulk_create', - [type1, type2], - [options.actions.savedObject.get(type1, 'bulk_create')], - { objects, options: apiCallOptions } - ); - expect(options.auditLogger.savedObjectsAuthorizationSuccess).not.toHaveBeenCalled(); - }); - - test(`returns result of baseClient.bulkCreate when authorized`, async () => { - const type1 = 'foo'; - const type2 = 'bar'; - const username = Symbol(); - const options = createSecureSavedObjectsClientWrapperOptions(); - options.checkSavedObjectsPrivilegesAsCurrentUser.mockResolvedValue({ - hasAllRequested: true, - username, - privileges: { - [options.actions.savedObject.get(type1, 'bulk_create')]: true, - [options.actions.savedObject.get(type2, 'bulk_create')]: true, - }, - }); - - const apiCallReturnValue = Symbol(); - options.baseClient.bulkCreate.mockReturnValue(apiCallReturnValue as any); - - const client = new SecureSavedObjectsClientWrapper(options); - - const objects = [ - { type: type1, otherThing: 'sup', attributes: {} }, - { type: type2, otherThing: 'everyone', attributes: {} }, - ]; - const apiCallOptions = Object.freeze({ namespace: 'some-ns' }); - await expect(client.bulkCreate(objects, apiCallOptions)).resolves.toBe(apiCallReturnValue); - - expect(options.checkSavedObjectsPrivilegesAsCurrentUser).toHaveBeenCalledWith( - [ - options.actions.savedObject.get(type1, 'bulk_create'), - options.actions.savedObject.get(type2, 'bulk_create'), - ], - apiCallOptions.namespace - ); - expect(options.baseClient.bulkCreate).toHaveBeenCalledWith(objects, apiCallOptions); - expect(options.auditLogger.savedObjectsAuthorizationFailure).not.toHaveBeenCalled(); - expect(options.auditLogger.savedObjectsAuthorizationSuccess).toHaveBeenCalledWith( - username, - 'bulk_create', - [type1, type2], - { objects, options: apiCallOptions } - ); - }); - }); - - describe('#delete', () => { - test(`throws decorated GeneralError when hasPrivileges rejects promise`, async () => { - const type = 'foo'; - const options = createSecureSavedObjectsClientWrapperOptions(); - options.checkSavedObjectsPrivilegesAsCurrentUser.mockRejectedValue( - new Error('An actual error would happen here') - ); - const client = new SecureSavedObjectsClientWrapper(options); - - await expect(client.delete(type, 'bar')).rejects.toThrowError(options.generalError); - expect(options.checkSavedObjectsPrivilegesAsCurrentUser).toHaveBeenCalledWith( - [options.actions.savedObject.get(type, 'delete')], - undefined - ); - expect(options.errors.decorateGeneralError).toHaveBeenCalledTimes(1); - expect(options.auditLogger.savedObjectsAuthorizationFailure).not.toHaveBeenCalled(); - expect(options.auditLogger.savedObjectsAuthorizationSuccess).not.toHaveBeenCalled(); - }); - - test(`throws decorated ForbiddenError when unauthorized`, async () => { - const type = 'foo'; - const id = 'bar'; - const username = Symbol(); - const options = createSecureSavedObjectsClientWrapperOptions(); - options.checkSavedObjectsPrivilegesAsCurrentUser.mockResolvedValue({ - hasAllRequested: false, - username, - privileges: { - [options.actions.savedObject.get(type, 'delete')]: false, - }, - }); - - const client = new SecureSavedObjectsClientWrapper(options); - - const apiCallOptions = Object.freeze({ namespace: 'some-ns' }); - await expect(client.delete(type, id, apiCallOptions)).rejects.toThrowError( - options.forbiddenError - ); - - expect(options.checkSavedObjectsPrivilegesAsCurrentUser).toHaveBeenCalledWith( - [options.actions.savedObject.get(type, 'delete')], - apiCallOptions.namespace - ); - expect(options.errors.decorateForbiddenError).toHaveBeenCalledTimes(1); - expect(options.auditLogger.savedObjectsAuthorizationFailure).toHaveBeenCalledWith( - username, - 'delete', - [type], - [options.actions.savedObject.get(type, 'delete')], - { type, id, options: apiCallOptions } - ); - expect(options.auditLogger.savedObjectsAuthorizationSuccess).not.toHaveBeenCalled(); - }); - - test(`returns result of internalRepository.delete when authorized`, async () => { - const type = 'foo'; - const id = 'bar'; - const username = Symbol(); - const options = createSecureSavedObjectsClientWrapperOptions(); - options.checkSavedObjectsPrivilegesAsCurrentUser.mockResolvedValue({ - hasAllRequested: true, - username, - privileges: { [options.actions.savedObject.get(type, 'delete')]: true }, - }); - - const apiCallReturnValue = Symbol(); - options.baseClient.delete.mockReturnValue(apiCallReturnValue as any); - - const client = new SecureSavedObjectsClientWrapper(options); - - const apiCallOptions = Object.freeze({ namespace: 'some-ns' }); - await expect(client.delete(type, id, apiCallOptions)).resolves.toBe(apiCallReturnValue); - - expect(options.checkSavedObjectsPrivilegesAsCurrentUser).toHaveBeenCalledWith( - [options.actions.savedObject.get(type, 'delete')], - apiCallOptions.namespace - ); - expect(options.baseClient.delete).toHaveBeenCalledWith(type, id, apiCallOptions); - expect(options.auditLogger.savedObjectsAuthorizationFailure).not.toHaveBeenCalled(); - expect(options.auditLogger.savedObjectsAuthorizationSuccess).toHaveBeenCalledWith( - username, - 'delete', - [type], - { type, id, options: apiCallOptions } - ); - }); - }); - - describe('#find', () => { - test(`throws decorated GeneralError when hasPrivileges rejects promise`, async () => { - const type = 'foo'; - const options = createSecureSavedObjectsClientWrapperOptions(); - options.checkSavedObjectsPrivilegesAsCurrentUser.mockRejectedValue( - new Error('An actual error would happen here') - ); - const client = new SecureSavedObjectsClientWrapper(options); - - await expect(client.find({ type })).rejects.toThrowError(options.generalError); - expect(options.checkSavedObjectsPrivilegesAsCurrentUser).toHaveBeenCalledWith( - [options.actions.savedObject.get(type, 'find')], - undefined - ); - expect(options.errors.decorateGeneralError).toHaveBeenCalledTimes(1); - expect(options.auditLogger.savedObjectsAuthorizationFailure).not.toHaveBeenCalled(); - expect(options.auditLogger.savedObjectsAuthorizationSuccess).not.toHaveBeenCalled(); - }); - - test(`throws decorated ForbiddenError when type's singular and unauthorized`, async () => { - const type = 'foo'; - const username = Symbol(); - const options = createSecureSavedObjectsClientWrapperOptions(); - options.checkSavedObjectsPrivilegesAsCurrentUser.mockResolvedValue({ - hasAllRequested: false, - username, - privileges: { [options.actions.savedObject.get(type, 'find')]: false }, - }); - - const client = new SecureSavedObjectsClientWrapper(options); - - const apiCallOptions = Object.freeze({ type, namespace: 'some-ns' }); - await expect(client.find(apiCallOptions)).rejects.toThrowError(options.forbiddenError); - - expect(options.checkSavedObjectsPrivilegesAsCurrentUser).toHaveBeenCalledWith( - [options.actions.savedObject.get(type, 'find')], - apiCallOptions.namespace - ); - expect(options.errors.decorateForbiddenError).toHaveBeenCalledTimes(1); - expect(options.auditLogger.savedObjectsAuthorizationFailure).toHaveBeenCalledWith( - username, - 'find', - [type], - [options.actions.savedObject.get(type, 'find')], - { options: apiCallOptions } - ); - expect(options.auditLogger.savedObjectsAuthorizationSuccess).not.toHaveBeenCalled(); - }); - - test(`throws decorated ForbiddenError when type's an array and unauthorized`, async () => { - const type1 = 'foo'; - const type2 = 'bar'; - const username = Symbol(); - const options = createSecureSavedObjectsClientWrapperOptions(); - options.checkSavedObjectsPrivilegesAsCurrentUser.mockResolvedValue({ - hasAllRequested: false, - username, - privileges: { - [options.actions.savedObject.get(type1, 'find')]: false, - [options.actions.savedObject.get(type2, 'find')]: true, - }, - }); - - const client = new SecureSavedObjectsClientWrapper(options); - - const apiCallOptions = Object.freeze({ type: [type1, type2], namespace: 'some-ns' }); - await expect(client.find(apiCallOptions)).rejects.toThrowError(options.forbiddenError); - - expect(options.checkSavedObjectsPrivilegesAsCurrentUser).toHaveBeenCalledWith( - [ - options.actions.savedObject.get(type1, 'find'), - options.actions.savedObject.get(type2, 'find'), - ], - apiCallOptions.namespace - ); - expect(options.errors.decorateForbiddenError).toHaveBeenCalledTimes(1); - expect(options.auditLogger.savedObjectsAuthorizationFailure).toHaveBeenCalledWith( - username, - 'find', - [type1, type2], - [options.actions.savedObject.get(type1, 'find')], - { options: apiCallOptions } - ); - expect(options.auditLogger.savedObjectsAuthorizationSuccess).not.toHaveBeenCalled(); - }); - - test(`returns result of baseClient.find when authorized`, async () => { - const type = 'foo'; - const username = Symbol(); - const options = createSecureSavedObjectsClientWrapperOptions(); - options.checkSavedObjectsPrivilegesAsCurrentUser.mockResolvedValue({ - hasAllRequested: true, - username, - privileges: { [options.actions.savedObject.get(type, 'find')]: true }, - }); - - const apiCallReturnValue = Symbol(); - options.baseClient.find.mockReturnValue(apiCallReturnValue as any); - - const client = new SecureSavedObjectsClientWrapper(options); - - const apiCallOptions = Object.freeze({ type, namespace: 'some-ns' }); - await expect(client.find(apiCallOptions)).resolves.toBe(apiCallReturnValue); - - expect(options.checkSavedObjectsPrivilegesAsCurrentUser).toHaveBeenCalledWith( - [options.actions.savedObject.get(type, 'find')], - apiCallOptions.namespace - ); - expect(options.baseClient.find).toHaveBeenCalledWith(apiCallOptions); - expect(options.auditLogger.savedObjectsAuthorizationFailure).not.toHaveBeenCalled(); - expect(options.auditLogger.savedObjectsAuthorizationSuccess).toHaveBeenCalledWith( - username, - 'find', - [type], - { options: apiCallOptions } - ); - }); - }); - - describe('#bulkGet', () => { - test(`throws decorated GeneralError when hasPrivileges rejects promise`, async () => { - const type = 'foo'; - const options = createSecureSavedObjectsClientWrapperOptions(); - options.checkSavedObjectsPrivilegesAsCurrentUser.mockRejectedValue( - new Error('An actual error would happen here') - ); - const client = new SecureSavedObjectsClientWrapper(options); - - await expect(client.bulkGet([{ id: 'bar', type }])).rejects.toThrowError( - options.generalError - ); - expect(options.checkSavedObjectsPrivilegesAsCurrentUser).toHaveBeenCalledWith( - [options.actions.savedObject.get(type, 'bulk_get')], - undefined - ); - expect(options.errors.decorateGeneralError).toHaveBeenCalledTimes(1); - expect(options.auditLogger.savedObjectsAuthorizationFailure).not.toHaveBeenCalled(); - expect(options.auditLogger.savedObjectsAuthorizationSuccess).not.toHaveBeenCalled(); - }); - - test(`throws decorated ForbiddenError when unauthorized`, async () => { - const type1 = 'foo'; - const type2 = 'bar'; - const username = Symbol(); - const options = createSecureSavedObjectsClientWrapperOptions(); - options.checkSavedObjectsPrivilegesAsCurrentUser.mockResolvedValue({ - hasAllRequested: false, - username, - privileges: { - [options.actions.savedObject.get(type1, 'bulk_get')]: false, - [options.actions.savedObject.get(type2, 'bulk_get')]: true, - }, - }); - - const client = new SecureSavedObjectsClientWrapper(options); - - const objects = [ - { type: type1, id: `bar-${type1}` }, - { type: type2, id: `bar-${type2}` }, - ]; - const apiCallOptions = Object.freeze({ namespace: 'some-ns' }); - await expect(client.bulkGet(objects, apiCallOptions)).rejects.toThrowError( - options.forbiddenError - ); - - expect(options.checkSavedObjectsPrivilegesAsCurrentUser).toHaveBeenCalledWith( - [ - options.actions.savedObject.get(type1, 'bulk_get'), - options.actions.savedObject.get(type2, 'bulk_get'), - ], - apiCallOptions.namespace - ); - expect(options.errors.decorateForbiddenError).toHaveBeenCalledTimes(1); - expect(options.auditLogger.savedObjectsAuthorizationFailure).toHaveBeenCalledWith( - username, - 'bulk_get', - [type1, type2], - [options.actions.savedObject.get(type1, 'bulk_get')], - { objects, options: apiCallOptions } - ); - expect(options.auditLogger.savedObjectsAuthorizationSuccess).not.toHaveBeenCalled(); - }); - - test(`returns result of baseClient.bulkGet when authorized`, async () => { - const type1 = 'foo'; - const type2 = 'bar'; - const username = Symbol(); - const options = createSecureSavedObjectsClientWrapperOptions(); - options.checkSavedObjectsPrivilegesAsCurrentUser.mockResolvedValue({ - hasAllRequested: true, - username, - privileges: { - [options.actions.savedObject.get(type1, 'bulk_get')]: true, - [options.actions.savedObject.get(type2, 'bulk_get')]: true, - }, - }); - - const apiCallReturnValue = Symbol(); - options.baseClient.bulkGet.mockReturnValue(apiCallReturnValue as any); - - const client = new SecureSavedObjectsClientWrapper(options); - - const objects = [ - { type: type1, id: `id-${type1}` }, - { type: type2, id: `id-${type2}` }, - ]; - const apiCallOptions = Object.freeze({ namespace: 'some-ns' }); - await expect(client.bulkGet(objects, apiCallOptions)).resolves.toBe(apiCallReturnValue); - - expect(options.checkSavedObjectsPrivilegesAsCurrentUser).toHaveBeenCalledWith( - [ - options.actions.savedObject.get(type1, 'bulk_get'), - options.actions.savedObject.get(type2, 'bulk_get'), - ], - apiCallOptions.namespace - ); - expect(options.baseClient.bulkGet).toHaveBeenCalledWith(objects, apiCallOptions); - expect(options.auditLogger.savedObjectsAuthorizationFailure).not.toHaveBeenCalled(); - expect(options.auditLogger.savedObjectsAuthorizationSuccess).toHaveBeenCalledWith( - username, - 'bulk_get', - [type1, type2], - { objects, options: apiCallOptions } - ); - }); - }); - - describe('#get', () => { - test(`throws decorated GeneralError when hasPrivileges rejects promise`, async () => { - const type = 'foo'; - const options = createSecureSavedObjectsClientWrapperOptions(); - options.checkSavedObjectsPrivilegesAsCurrentUser.mockRejectedValue( - new Error('An actual error would happen here') - ); - const client = new SecureSavedObjectsClientWrapper(options); - - await expect(client.get(type, 'bar')).rejects.toThrowError(options.generalError); - expect(options.checkSavedObjectsPrivilegesAsCurrentUser).toHaveBeenCalledWith( - [options.actions.savedObject.get(type, 'get')], - undefined - ); - expect(options.errors.decorateGeneralError).toHaveBeenCalledTimes(1); - expect(options.auditLogger.savedObjectsAuthorizationFailure).not.toHaveBeenCalled(); - expect(options.auditLogger.savedObjectsAuthorizationSuccess).not.toHaveBeenCalled(); - }); - - test(`throws decorated ForbiddenError when unauthorized`, async () => { - const type = 'foo'; - const id = 'bar'; - const username = Symbol(); - const options = createSecureSavedObjectsClientWrapperOptions(); - options.checkSavedObjectsPrivilegesAsCurrentUser.mockResolvedValue({ - hasAllRequested: false, - username, - privileges: { - [options.actions.savedObject.get(type, 'get')]: false, - }, - }); - - const client = new SecureSavedObjectsClientWrapper(options); - - const apiCallOptions = Object.freeze({ namespace: 'some-ns' }); - await expect(client.get(type, id, apiCallOptions)).rejects.toThrowError( - options.forbiddenError - ); - - expect(options.checkSavedObjectsPrivilegesAsCurrentUser).toHaveBeenCalledWith( - [options.actions.savedObject.get(type, 'get')], - apiCallOptions.namespace - ); - expect(options.errors.decorateForbiddenError).toHaveBeenCalledTimes(1); - expect(options.auditLogger.savedObjectsAuthorizationFailure).toHaveBeenCalledWith( - username, - 'get', - [type], - [options.actions.savedObject.get(type, 'get')], - { type, id, options: apiCallOptions } - ); - expect(options.auditLogger.savedObjectsAuthorizationSuccess).not.toHaveBeenCalled(); - }); - - test(`returns result of baseClient.get when authorized`, async () => { - const type = 'foo'; - const id = 'bar'; - const username = Symbol(); - const options = createSecureSavedObjectsClientWrapperOptions(); - options.checkSavedObjectsPrivilegesAsCurrentUser.mockResolvedValue({ - hasAllRequested: true, - username, - privileges: { [options.actions.savedObject.get(type, 'get')]: true }, - }); - - const apiCallReturnValue = Symbol(); - options.baseClient.get.mockReturnValue(apiCallReturnValue as any); - - const client = new SecureSavedObjectsClientWrapper(options); - - const apiCallOptions = Object.freeze({ namespace: 'some-ns' }); - await expect(client.get(type, id, apiCallOptions)).resolves.toBe(apiCallReturnValue); - - expect(options.checkSavedObjectsPrivilegesAsCurrentUser).toHaveBeenCalledWith( - [options.actions.savedObject.get(type, 'get')], - apiCallOptions.namespace - ); - expect(options.baseClient.get).toHaveBeenCalledWith(type, id, apiCallOptions); - expect(options.auditLogger.savedObjectsAuthorizationFailure).not.toHaveBeenCalled(); - expect(options.auditLogger.savedObjectsAuthorizationSuccess).toHaveBeenCalledWith( - username, - 'get', - [type], - { type, id, options: apiCallOptions } - ); - }); - }); - - describe('#update', () => { - test(`throws decorated GeneralError when hasPrivileges rejects promise`, async () => { - const type = 'foo'; - const options = createSecureSavedObjectsClientWrapperOptions(); - options.checkSavedObjectsPrivilegesAsCurrentUser.mockRejectedValue( - new Error('An actual error would happen here') - ); - const client = new SecureSavedObjectsClientWrapper(options); - - await expect(client.update(type, 'bar', {})).rejects.toThrowError(options.generalError); - expect(options.checkSavedObjectsPrivilegesAsCurrentUser).toHaveBeenCalledWith( - [options.actions.savedObject.get(type, 'update')], - undefined - ); - expect(options.errors.decorateGeneralError).toHaveBeenCalledTimes(1); - expect(options.auditLogger.savedObjectsAuthorizationFailure).not.toHaveBeenCalled(); - expect(options.auditLogger.savedObjectsAuthorizationSuccess).not.toHaveBeenCalled(); - }); - - test(`throws decorated ForbiddenError when unauthorized`, async () => { - const type = 'foo'; - const id = 'bar'; - const username = Symbol(); - const options = createSecureSavedObjectsClientWrapperOptions(); - options.checkSavedObjectsPrivilegesAsCurrentUser.mockResolvedValue({ - hasAllRequested: false, - username, - privileges: { - [options.actions.savedObject.get(type, 'update')]: false, - }, - }); - - const client = new SecureSavedObjectsClientWrapper(options); - - const attributes = { some: 'attr' }; - const apiCallOptions = Object.freeze({ namespace: 'some-ns' }); - await expect(client.update(type, id, attributes, apiCallOptions)).rejects.toThrowError( - options.forbiddenError - ); - - expect(options.checkSavedObjectsPrivilegesAsCurrentUser).toHaveBeenCalledWith( - [options.actions.savedObject.get(type, 'update')], - apiCallOptions.namespace - ); - expect(options.errors.decorateForbiddenError).toHaveBeenCalledTimes(1); - expect(options.auditLogger.savedObjectsAuthorizationFailure).toHaveBeenCalledWith( - username, - 'update', - [type], - [options.actions.savedObject.get(type, 'update')], - { type, id, attributes, options: apiCallOptions } - ); - expect(options.auditLogger.savedObjectsAuthorizationSuccess).not.toHaveBeenCalled(); - }); - - test(`returns result of baseClient.update when authorized`, async () => { - const type = 'foo'; - const id = 'bar'; - const username = Symbol(); - const options = createSecureSavedObjectsClientWrapperOptions(); - options.checkSavedObjectsPrivilegesAsCurrentUser.mockResolvedValue({ - hasAllRequested: true, - username, - privileges: { [options.actions.savedObject.get(type, 'update')]: true }, - }); - - const apiCallReturnValue = Symbol(); - options.baseClient.update.mockReturnValue(apiCallReturnValue as any); - - const client = new SecureSavedObjectsClientWrapper(options); - - const attributes = { some: 'attr' }; - const apiCallOptions = Object.freeze({ namespace: 'some-ns' }); - await expect(client.update(type, id, attributes, apiCallOptions)).resolves.toBe( - apiCallReturnValue - ); - - expect(options.checkSavedObjectsPrivilegesAsCurrentUser).toHaveBeenCalledWith( - [options.actions.savedObject.get(type, 'update')], - apiCallOptions.namespace - ); - expect(options.baseClient.update).toHaveBeenCalledWith(type, id, attributes, apiCallOptions); - expect(options.auditLogger.savedObjectsAuthorizationFailure).not.toHaveBeenCalled(); - expect(options.auditLogger.savedObjectsAuthorizationSuccess).toHaveBeenCalledWith( - username, - 'update', - [type], - { type, id, attributes, options: apiCallOptions } - ); - }); - }); - - describe('#bulkUpdate', () => { - test(`throws decorated GeneralError when hasPrivileges rejects promise`, async () => { - const type = 'foo'; - const options = createSecureSavedObjectsClientWrapperOptions(); - options.checkSavedObjectsPrivilegesAsCurrentUser.mockRejectedValue( - new Error('An actual error would happen here') - ); - const client = new SecureSavedObjectsClientWrapper(options); - - await expect(client.bulkUpdate([{ id: 'bar', type, attributes: {} }])).rejects.toThrowError( - options.generalError - ); - expect(options.checkSavedObjectsPrivilegesAsCurrentUser).toHaveBeenCalledWith( - [options.actions.savedObject.get(type, 'bulk_update')], - undefined - ); - expect(options.errors.decorateGeneralError).toHaveBeenCalledTimes(1); - expect(options.auditLogger.savedObjectsAuthorizationFailure).not.toHaveBeenCalled(); - expect(options.auditLogger.savedObjectsAuthorizationSuccess).not.toHaveBeenCalled(); - }); - - test(`throws decorated ForbiddenError when unauthorized`, async () => { - const type = 'foo'; - const username = Symbol(); - const options = createSecureSavedObjectsClientWrapperOptions(); - options.checkSavedObjectsPrivilegesAsCurrentUser.mockResolvedValue({ - hasAllRequested: false, - username, - privileges: { - [options.actions.savedObject.get(type, 'bulk_update')]: false, - }, - }); - - const client = new SecureSavedObjectsClientWrapper(options); - - const objects = [{ type, id: `bar-${type}`, attributes: {} }]; - const apiCallOptions = Object.freeze({ namespace: 'some-ns' }); - await expect(client.bulkUpdate(objects, apiCallOptions)).rejects.toThrowError( - options.forbiddenError - ); - - expect(options.checkSavedObjectsPrivilegesAsCurrentUser).toHaveBeenCalledWith( - [options.actions.savedObject.get(type, 'bulk_update')], - apiCallOptions.namespace - ); - expect(options.errors.decorateForbiddenError).toHaveBeenCalledTimes(1); - expect(options.auditLogger.savedObjectsAuthorizationFailure).toHaveBeenCalledWith( - username, - 'bulk_update', - [type], - [options.actions.savedObject.get(type, 'bulk_update')], - { objects, options: apiCallOptions } - ); - expect(options.auditLogger.savedObjectsAuthorizationSuccess).not.toHaveBeenCalled(); - }); - - test(`returns result of baseClient.bulkUpdate when authorized`, async () => { - const type = 'foo'; - const username = Symbol(); - const options = createSecureSavedObjectsClientWrapperOptions(); - options.checkSavedObjectsPrivilegesAsCurrentUser.mockResolvedValue({ - hasAllRequested: true, - username, - privileges: { - [options.actions.savedObject.get(type, 'bulk_update')]: true, - }, - }); - - const apiCallReturnValue = Symbol(); - options.baseClient.bulkUpdate.mockReturnValue(apiCallReturnValue as any); - - const client = new SecureSavedObjectsClientWrapper(options); - - const objects = [{ type, id: `id-${type}`, attributes: {} }]; - const apiCallOptions = Object.freeze({ namespace: 'some-ns' }); - await expect(client.bulkUpdate(objects, apiCallOptions)).resolves.toBe(apiCallReturnValue); - - expect(options.checkSavedObjectsPrivilegesAsCurrentUser).toHaveBeenCalledWith( - [options.actions.savedObject.get(type, 'bulk_update')], - apiCallOptions.namespace - ); - expect(options.baseClient.bulkUpdate).toHaveBeenCalledWith(objects, apiCallOptions); - expect(options.auditLogger.savedObjectsAuthorizationFailure).not.toHaveBeenCalled(); - expect(options.auditLogger.savedObjectsAuthorizationSuccess).toHaveBeenCalledWith( - username, - 'bulk_update', - [type], - { objects, options: apiCallOptions } - ); - }); +describe('#delete', () => { + const type = 'foo'; + const id = `${type}-id`; + const options = Object.freeze({ namespace: 'some-ns' }); + + test(`throws decorated GeneralError when hasPrivileges rejects promise`, async () => { + await expectGeneralError(client.delete, { type, id }); + }); + + test(`throws decorated ForbiddenError when unauthorized`, async () => { + await expectForbiddenError(client.delete, { type, id, options }); + }); + + test(`returns result of internalRepository.delete when authorized`, async () => { + const apiCallReturnValue = Symbol(); + clientOpts.baseClient.delete.mockReturnValue(apiCallReturnValue as any); + + const result = await expectSuccess(client.delete, { type, id, options }); + expect(result).toBe(apiCallReturnValue); + }); + + test(`checks privileges for user, actions, and namespace`, async () => { + await expectPrivilegeCheck(client.delete, { type, id, options }); + }); +}); + +describe('#find', () => { + const type1 = 'foo'; + const type2 = 'bar'; + + test(`throws decorated GeneralError when hasPrivileges rejects promise`, async () => { + await expectGeneralError(client.find, { type: type1 }); + }); + + test(`throws decorated ForbiddenError when type's singular and unauthorized`, async () => { + const options = Object.freeze({ type: type1, namespace: 'some-ns' }); + await expectForbiddenError(client.find, { options }); + }); + + test(`throws decorated ForbiddenError when type's an array and unauthorized`, async () => { + const options = Object.freeze({ type: [type1, type2], namespace: 'some-ns' }); + await expectForbiddenError(client.find, { options }); + }); + + test(`returns result of baseClient.find when authorized`, async () => { + const apiCallReturnValue = { saved_objects: [], foo: 'bar' }; + clientOpts.baseClient.find.mockReturnValue(apiCallReturnValue as any); + + const options = Object.freeze({ type: type1, namespace: 'some-ns' }); + const result = await expectSuccess(client.find, { options }); + expect(result).toEqual(apiCallReturnValue); + }); + + test(`checks privileges for user, actions, and namespace`, async () => { + const options = Object.freeze({ type: [type1, type2], namespace: 'some-ns' }); + await expectPrivilegeCheck(client.find, { options }); + }); + + test(`filters namespaces that the user doesn't have access to`, async () => { + const options = Object.freeze({ type: [type1, type2], namespace: 'some-ns' }); + await expectObjectsNamespaceFiltering(client.find, { options }); + }); +}); + +describe('#get', () => { + const type = 'foo'; + const id = `${type}-id`; + const options = Object.freeze({ namespace: 'some-ns' }); + + test(`throws decorated GeneralError when hasPrivileges rejects promise`, async () => { + await expectGeneralError(client.get, { type, id }); + }); + + test(`throws decorated ForbiddenError when unauthorized`, async () => { + await expectForbiddenError(client.get, { type, id, options }); + }); + + test(`returns result of baseClient.get when authorized`, async () => { + const apiCallReturnValue = Symbol(); + clientOpts.baseClient.get.mockReturnValue(apiCallReturnValue as any); + + const result = await expectSuccess(client.get, { type, id, options }); + expect(result).toBe(apiCallReturnValue); + }); + + test(`checks privileges for user, actions, and namespace`, async () => { + await expectPrivilegeCheck(client.get, { type, id, options }); + }); + + test(`filters namespaces that the user doesn't have access to`, async () => { + await expectObjectNamespaceFiltering(client.get, { type, id, options }); + }); +}); + +describe('#deleteFromNamespaces', () => { + const type = 'foo'; + const id = `${type}-id`; + const namespace1 = 'foo-namespace'; + const namespace2 = 'bar-namespace'; + const namespaces = [namespace1, namespace2]; + const privilege = `mock-saved_object:${type}/delete`; + + test(`throws decorated GeneralError when hasPrivileges rejects promise`, async () => { + await expectGeneralError(client.deleteFromNamespaces, { type, id, namespaces }); + }); + + test(`throws decorated ForbiddenError when unauthorized`, async () => { + clientOpts.checkSavedObjectsPrivilegesAsCurrentUser.mockImplementation( + getMockCheckPrivilegesFailure + ); + + await expect(client.deleteFromNamespaces(type, id, namespaces)).rejects.toThrowError( + clientOpts.forbiddenError + ); + + expect(clientOpts.errors.decorateForbiddenError).toHaveBeenCalledTimes(1); + expect(clientOpts.auditLogger.savedObjectsAuthorizationFailure).toHaveBeenCalledTimes(1); + expect(clientOpts.auditLogger.savedObjectsAuthorizationFailure).toHaveBeenCalledWith( + USERNAME, + 'deleteFromNamespaces', // action for privilege check is 'delete', but auditAction is 'deleteFromNamespaces' + [type], + namespaces.sort(), + [{ privilege, spaceId: namespace1 }], + { type, id, namespaces, options: {} } + ); + expect(clientOpts.auditLogger.savedObjectsAuthorizationSuccess).not.toHaveBeenCalled(); + }); + + test(`returns result of baseClient.deleteFromNamespaces when authorized`, async () => { + const apiCallReturnValue = Symbol(); + clientOpts.baseClient.deleteFromNamespaces.mockReturnValue(apiCallReturnValue as any); + + const result = await client.deleteFromNamespaces(type, id, namespaces); + expect(result).toBe(apiCallReturnValue); + + expect(clientOpts.auditLogger.savedObjectsAuthorizationFailure).not.toHaveBeenCalled(); + expect(clientOpts.auditLogger.savedObjectsAuthorizationSuccess).toHaveBeenCalledTimes(1); + expect(clientOpts.auditLogger.savedObjectsAuthorizationSuccess).toHaveBeenCalledWith( + USERNAME, + 'deleteFromNamespaces', // action for privilege check is 'delete', but auditAction is 'deleteFromNamespaces' + [type], + namespaces.sort(), + { type, id, namespaces, options: {} } + ); + }); + + test(`checks privileges for user, actions, and namespace`, async () => { + clientOpts.checkSavedObjectsPrivilegesAsCurrentUser.mockImplementation( + getMockCheckPrivilegesFailure + ); + + await expect(client.deleteFromNamespaces(type, id, namespaces)).rejects.toThrow(); // test is simpler with error case + + expect(clientOpts.checkSavedObjectsPrivilegesAsCurrentUser).toHaveBeenCalledTimes(1); + expect(clientOpts.checkSavedObjectsPrivilegesAsCurrentUser).toHaveBeenCalledWith( + [privilege], + namespaces + ); + }); +}); + +describe('#update', () => { + const type = 'foo'; + const id = `${type}-id`; + const attributes = { some: 'attr' }; + const options = Object.freeze({ namespace: 'some-ns' }); + + test(`throws decorated GeneralError when hasPrivileges rejects promise`, async () => { + await expectGeneralError(client.update, { type, id, attributes }); + }); + + test(`throws decorated ForbiddenError when unauthorized`, async () => { + await expectForbiddenError(client.update, { type, id, attributes, options }); + }); + + test(`returns result of baseClient.update when authorized`, async () => { + const apiCallReturnValue = Symbol(); + clientOpts.baseClient.update.mockReturnValue(apiCallReturnValue as any); + + const result = await expectSuccess(client.update, { type, id, attributes, options }); + expect(result).toBe(apiCallReturnValue); + }); + + test(`checks privileges for user, actions, and namespace`, async () => { + await expectPrivilegeCheck(client.update, { type, id, attributes, options }); + }); + + test(`filters namespaces that the user doesn't have access to`, async () => { + await expectObjectNamespaceFiltering(client.update, { type, id, attributes, options }); + }); +}); + +describe('other', () => { + test(`assigns errors from constructor to .errors`, () => { + expect(client.errors).toBe(clientOpts.errors); }); }); diff --git a/x-pack/plugins/security/server/saved_objects/secure_saved_objects_client_wrapper.ts b/x-pack/plugins/security/server/saved_objects/secure_saved_objects_client_wrapper.ts index 2209e7fb66fcb..29503d475be73 100644 --- a/x-pack/plugins/security/server/saved_objects/secure_saved_objects_client_wrapper.ts +++ b/x-pack/plugins/security/server/saved_objects/secure_saved_objects_client_wrapper.ts @@ -13,9 +13,13 @@ import { SavedObjectsCreateOptions, SavedObjectsFindOptions, SavedObjectsUpdateOptions, + SavedObjectsAddToNamespacesOptions, + SavedObjectsDeleteFromNamespacesOptions, } from '../../../../../src/core/server'; import { SecurityAuditLogger } from '../audit'; import { Actions, CheckSavedObjectsPrivileges } from '../authorization'; +import { CheckPrivilegesResponse } from '../authorization/check_privileges'; +import { SpacesService } from '../plugin'; interface SecureSavedObjectsClientWrapperOptions { actions: Actions; @@ -23,6 +27,19 @@ interface SecureSavedObjectsClientWrapperOptions { baseClient: SavedObjectsClientContract; errors: SavedObjectsClientContract['errors']; checkSavedObjectsPrivilegesAsCurrentUser: CheckSavedObjectsPrivileges; + getSpacesService(): SpacesService | undefined; +} + +interface SavedObjectNamespaces { + namespaces?: string[]; +} + +interface SavedObjectsNamespaces { + saved_objects: SavedObjectNamespaces[]; +} + +function uniq(arr: T[]): T[] { + return Array.from(new Set(arr)); } export class SecureSavedObjectsClientWrapper implements SavedObjectsClientContract { @@ -30,19 +47,23 @@ export class SecureSavedObjectsClientWrapper implements SavedObjectsClientContra private readonly auditLogger: PublicMethodsOf; private readonly baseClient: SavedObjectsClientContract; private readonly checkSavedObjectsPrivilegesAsCurrentUser: CheckSavedObjectsPrivileges; + private getSpacesService: () => SpacesService | undefined; public readonly errors: SavedObjectsClientContract['errors']; + constructor({ actions, auditLogger, baseClient, checkSavedObjectsPrivilegesAsCurrentUser, errors, + getSpacesService, }: SecureSavedObjectsClientWrapperOptions) { this.errors = errors; this.actions = actions; this.auditLogger = auditLogger; this.baseClient = baseClient; this.checkSavedObjectsPrivilegesAsCurrentUser = checkSavedObjectsPrivilegesAsCurrentUser; + this.getSpacesService = getSpacesService; } public async create( @@ -52,7 +73,8 @@ export class SecureSavedObjectsClientWrapper implements SavedObjectsClientContra ) { await this.ensureAuthorized(type, 'create', options.namespace, { type, attributes, options }); - return await this.baseClient.create(type, attributes, options); + const savedObject = await this.baseClient.create(type, attributes, options); + return await this.redactSavedObjectNamespaces(savedObject); } public async bulkCreate( @@ -66,7 +88,8 @@ export class SecureSavedObjectsClientWrapper implements SavedObjectsClientContra { objects, options } ); - return await this.baseClient.bulkCreate(objects, options); + const response = await this.baseClient.bulkCreate(objects, options); + return await this.redactSavedObjectsNamespaces(response); } public async delete(type: string, id: string, options: SavedObjectsBaseOptions = {}) { @@ -78,7 +101,8 @@ export class SecureSavedObjectsClientWrapper implements SavedObjectsClientContra public async find(options: SavedObjectsFindOptions) { await this.ensureAuthorized(options.type, 'find', options.namespace, { options }); - return this.baseClient.find(options); + const response = await this.baseClient.find(options); + return await this.redactSavedObjectsNamespaces(response); } public async bulkGet( @@ -90,13 +114,15 @@ export class SecureSavedObjectsClientWrapper implements SavedObjectsClientContra options, }); - return await this.baseClient.bulkGet(objects, options); + const response = await this.baseClient.bulkGet(objects, options); + return await this.redactSavedObjectsNamespaces(response); } public async get(type: string, id: string, options: SavedObjectsBaseOptions = {}) { await this.ensureAuthorized(type, 'get', options.namespace, { type, id, options }); - return await this.baseClient.get(type, id, options); + const savedObject = await this.baseClient.get(type, id, options); + return await this.redactSavedObjectNamespaces(savedObject); } public async update( @@ -105,14 +131,44 @@ export class SecureSavedObjectsClientWrapper implements SavedObjectsClientContra attributes: Partial, options: SavedObjectsUpdateOptions = {} ) { - await this.ensureAuthorized(type, 'update', options.namespace, { - type, - id, - attributes, - options, - }); + const args = { type, id, attributes, options }; + await this.ensureAuthorized(type, 'update', options.namespace, args); - return await this.baseClient.update(type, id, attributes, options); + const savedObject = await this.baseClient.update(type, id, attributes, options); + return await this.redactSavedObjectNamespaces(savedObject); + } + + public async addToNamespaces( + type: string, + id: string, + namespaces: string[], + options: SavedObjectsAddToNamespacesOptions = {} + ) { + const args = { type, id, namespaces, options }; + const { namespace } = options; + // To share an object, the user must have the "create" permission in each of the destination namespaces. + await this.ensureAuthorized(type, 'create', namespaces, args, 'addToNamespacesCreate'); + + // To share an object, the user must also have the "update" permission in one or more of the source namespaces. Because the + // `addToNamespaces` operation is scoped to the current namespace, we can just check if the user has the "update" permission in the + // current namespace. If the user has permission, but the saved object doesn't exist in this namespace, the base client operation will + // result in a 404 error. + await this.ensureAuthorized(type, 'update', namespace, args, 'addToNamespacesUpdate'); + + return await this.baseClient.addToNamespaces(type, id, namespaces, options); + } + + public async deleteFromNamespaces( + type: string, + id: string, + namespaces: string[], + options: SavedObjectsDeleteFromNamespacesOptions = {} + ) { + const args = { type, id, namespaces, options }; + // To un-share an object, the user must have the "delete" permission in each of the target namespaces. + await this.ensureAuthorized(type, 'delete', namespaces, args, 'deleteFromNamespaces'); + + return await this.baseClient.deleteFromNamespaces(type, id, namespaces, options); } public async bulkUpdate( @@ -126,12 +182,16 @@ export class SecureSavedObjectsClientWrapper implements SavedObjectsClientContra { objects, options } ); - return await this.baseClient.bulkUpdate(objects, options); + const response = await this.baseClient.bulkUpdate(objects, options); + return await this.redactSavedObjectsNamespaces(response); } - private async checkPrivileges(actions: string | string[], namespace?: string) { + private async checkPrivileges( + actions: string | string[], + namespaceOrNamespaces?: string | string[] + ) { try { - return await this.checkSavedObjectsPrivilegesAsCurrentUser(actions, namespace); + return await this.checkSavedObjectsPrivilegesAsCurrentUser(actions, namespaceOrNamespaces); } catch (error) { throw this.errors.decorateGeneralError(error, error.body && error.body.reason); } @@ -140,43 +200,133 @@ export class SecureSavedObjectsClientWrapper implements SavedObjectsClientContra private async ensureAuthorized( typeOrTypes: string | string[], action: string, - namespace?: string, - args?: Record + namespaceOrNamespaces?: string | string[], + args?: Record, + auditAction: string = action, + requiresAll = true ) { const types = Array.isArray(typeOrTypes) ? typeOrTypes : [typeOrTypes]; const actionsToTypesMap = new Map( types.map(type => [this.actions.savedObject.get(type, action), type]) ); const actions = Array.from(actionsToTypesMap.keys()); - const { hasAllRequested, username, privileges } = await this.checkPrivileges( - actions, - namespace - ); + const result = await this.checkPrivileges(actions, namespaceOrNamespaces); + + const { hasAllRequested, username, privileges } = result; + const spaceIds = uniq( + privileges.map(({ resource }) => resource).filter(x => x !== undefined) + ).sort() as string[]; - if (hasAllRequested) { - this.auditLogger.savedObjectsAuthorizationSuccess(username, action, types, args); + const isAuthorized = + (requiresAll && hasAllRequested) || + (!requiresAll && privileges.some(({ authorized }) => authorized)); + if (isAuthorized) { + this.auditLogger.savedObjectsAuthorizationSuccess( + username, + auditAction, + types, + spaceIds, + args + ); } else { const missingPrivileges = this.getMissingPrivileges(privileges); this.auditLogger.savedObjectsAuthorizationFailure( username, - action, + auditAction, types, + spaceIds, missingPrivileges, args ); - const msg = `Unable to ${action} ${missingPrivileges - .map(privilege => actionsToTypesMap.get(privilege)) - .sort() - .join(',')}`; + const targetTypes = uniq( + missingPrivileges.map(({ privilege }) => actionsToTypesMap.get(privilege)).sort() + ).join(','); + const msg = `Unable to ${action} ${targetTypes}`; throw this.errors.decorateForbiddenError(new Error(msg)); } } - private getMissingPrivileges(privileges: Record) { - return Object.keys(privileges).filter(privilege => !privileges[privilege]); + private getMissingPrivileges(privileges: CheckPrivilegesResponse['privileges']) { + return privileges + .filter(({ authorized }) => !authorized) + .map(({ resource, privilege }) => ({ spaceId: resource, privilege })); } private getUniqueObjectTypes(objects: Array<{ type: string }>) { - return [...new Set(objects.map(o => o.type))]; + return uniq(objects.map(o => o.type)); + } + + private async getNamespacesPrivilegeMap(namespaces: string[]) { + const action = this.actions.login; + const checkPrivilegesResult = await this.checkPrivileges(action, namespaces); + // check if the user can log into each namespace + const map = checkPrivilegesResult.privileges.reduce( + (acc: Record, { resource, authorized }) => { + // there should never be a case where more than one privilege is returned for a given space + // if there is, fail-safe (authorized + unauthorized = unauthorized) + if (resource && (!authorized || !acc.hasOwnProperty(resource))) { + acc[resource] = authorized; + } + return acc; + }, + {} + ); + return map; + } + + private redactAndSortNamespaces(spaceIds: string[], privilegeMap: Record) { + const comparator = (a: string, b: string) => { + const _a = a.toLowerCase(); + const _b = b.toLowerCase(); + if (_a === '?') { + return 1; + } else if (_a < _b) { + return -1; + } else if (_a > _b) { + return 1; + } + return 0; + }; + return spaceIds.map(spaceId => (privilegeMap[spaceId] ? spaceId : '?')).sort(comparator); + } + + private async redactSavedObjectNamespaces( + savedObject: T + ): Promise { + if (this.getSpacesService() === undefined || savedObject.namespaces == null) { + return savedObject; + } + + const privilegeMap = await this.getNamespacesPrivilegeMap(savedObject.namespaces); + + return { + ...savedObject, + namespaces: this.redactAndSortNamespaces(savedObject.namespaces, privilegeMap), + }; + } + + private async redactSavedObjectsNamespaces( + response: T + ): Promise { + if (this.getSpacesService() === undefined) { + return response; + } + const { saved_objects: savedObjects } = response; + const namespaces = uniq(savedObjects.flatMap(savedObject => savedObject.namespaces || [])); + if (namespaces.length === 0) { + return response; + } + + const privilegeMap = await this.getNamespacesPrivilegeMap(namespaces); + + return { + ...response, + saved_objects: savedObjects.map(savedObject => ({ + ...savedObject, + namespaces: + savedObject.namespaces && + this.redactAndSortNamespaces(savedObject.namespaces, privilegeMap), + })), + }; } } diff --git a/x-pack/plugins/siem/.gitattributes b/x-pack/plugins/siem/.gitattributes new file mode 100644 index 0000000000000..96ab5dadbda10 --- /dev/null +++ b/x-pack/plugins/siem/.gitattributes @@ -0,0 +1,4 @@ +# Auto-collapse generated files in GitHub +# https://help.github.com/en/articles/customizing-how-changed-files-appear-on-github +x-pack/plugins/siem/server/graphql/types.ts linguist-generated=true + diff --git a/x-pack/plugins/siem/common/constants.ts b/x-pack/plugins/siem/common/constants.ts new file mode 100644 index 0000000000000..edde5c6b8fa0d --- /dev/null +++ b/x-pack/plugins/siem/common/constants.ts @@ -0,0 +1,140 @@ +/* + * 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 APP_ID = 'siem'; +export const APP_NAME = 'SIEM'; +export const DEFAULT_BYTES_FORMAT = 'format:bytes:defaultPattern'; +export const DEFAULT_DATE_FORMAT = 'dateFormat'; +export const DEFAULT_DATE_FORMAT_TZ = 'dateFormat:tz'; +export const DEFAULT_DARK_MODE = 'theme:darkMode'; +export const DEFAULT_INDEX_KEY = 'siem:defaultIndex'; +export const DEFAULT_NUMBER_FORMAT = 'format:number:defaultPattern'; +export const DEFAULT_TIME_RANGE = 'timepicker:timeDefaults'; +export const DEFAULT_REFRESH_RATE_INTERVAL = 'timepicker:refreshIntervalDefaults'; +export const DEFAULT_SIEM_TIME_RANGE = 'siem:timeDefaults'; +export const DEFAULT_SIEM_REFRESH_INTERVAL = 'siem:refreshIntervalDefaults'; +export const DEFAULT_SIGNALS_INDEX = '.siem-signals'; +export const DEFAULT_MAX_SIGNALS = 100; +export const DEFAULT_SEARCH_AFTER_PAGE_SIZE = 100; +export const DEFAULT_ANOMALY_SCORE = 'siem:defaultAnomalyScore'; +export const DEFAULT_MAX_TABLE_QUERY_SIZE = 10000; +export const DEFAULT_SCALE_DATE_FORMAT = 'dateFormat:scaled'; +export const DEFAULT_FROM = 'now-24h'; +export const DEFAULT_TO = 'now'; +export const DEFAULT_INTERVAL_PAUSE = true; +export const DEFAULT_INTERVAL_TYPE = 'manual'; +export const DEFAULT_INTERVAL_VALUE = 300000; // ms +export const DEFAULT_TIMEPICKER_QUICK_RANGES = 'timepicker:quickRanges'; + +/** The comma-delimited list of Elasticsearch indices from which the SIEM app collects events */ +export const DEFAULT_INDEX_PATTERN = [ + 'apm-*-transaction*', + 'auditbeat-*', + 'endgame-*', + 'filebeat-*', + 'packetbeat-*', + 'winlogbeat-*', +]; + +/** This Kibana Advanced Setting enables the `Security news` feed widget */ +export const ENABLE_NEWS_FEED_SETTING = 'siem:enableNewsFeed'; + +/** This Kibana Advanced Setting specifies the URL of the News feed widget */ +export const NEWS_FEED_URL_SETTING = 'siem:newsFeedUrl'; + +/** The default value for News feed widget */ +export const NEWS_FEED_URL_SETTING_DEFAULT = 'https://feeds.elastic.co/security-solution'; + +/** This Kibana Advanced Setting specifies the URLs of `IP Reputation Links`*/ +export const IP_REPUTATION_LINKS_SETTING = 'siem:ipReputationLinks'; + +/** The default value for `IP Reputation Links` */ +export const IP_REPUTATION_LINKS_SETTING_DEFAULT = `[ + { "name": "virustotal.com", "url_template": "https://www.virustotal.com/gui/search/{{ip}}" }, + { "name": "talosIntelligence.com", "url_template": "https://talosintelligence.com/reputation_center/lookup?search={{ip}}" } +]`; + +/** + * Id for the signals alerting type + */ +export const SIGNALS_ID = `${APP_ID}.signals`; + +/** + * Id for the notifications alerting type + */ +export const NOTIFICATIONS_ID = `${APP_ID}.notifications`; + +/** + * Special internal structure for tags for signals. This is used + * to filter out tags that have internal structures within them. + */ +export const INTERNAL_IDENTIFIER = '__internal'; +export const INTERNAL_RULE_ID_KEY = `${INTERNAL_IDENTIFIER}_rule_id`; +export const INTERNAL_RULE_ALERT_ID_KEY = `${INTERNAL_IDENTIFIER}_rule_alert_id`; +export const INTERNAL_IMMUTABLE_KEY = `${INTERNAL_IDENTIFIER}_immutable`; + +/** + * Detection engine routes + */ +export const DETECTION_ENGINE_URL = '/api/detection_engine'; +export const DETECTION_ENGINE_RULES_URL = `${DETECTION_ENGINE_URL}/rules`; +export const DETECTION_ENGINE_PREPACKAGED_URL = `${DETECTION_ENGINE_RULES_URL}/prepackaged`; +export const DETECTION_ENGINE_PRIVILEGES_URL = `${DETECTION_ENGINE_URL}/privileges`; +export const DETECTION_ENGINE_INDEX_URL = `${DETECTION_ENGINE_URL}/index`; +export const DETECTION_ENGINE_TAGS_URL = `${DETECTION_ENGINE_URL}/tags`; +export const DETECTION_ENGINE_RULES_STATUS_URL = `${DETECTION_ENGINE_RULES_URL}/_find_statuses`; +export const DETECTION_ENGINE_PREPACKAGED_RULES_STATUS_URL = `${DETECTION_ENGINE_RULES_URL}/prepackaged/_status`; + +export const TIMELINE_URL = '/api/timeline'; +export const TIMELINE_EXPORT_URL = `${TIMELINE_URL}/_export`; +export const TIMELINE_IMPORT_URL = `${TIMELINE_URL}/_import`; + +/** + * Default signals index key for kibana.dev.yml + */ +export const SIGNALS_INDEX_KEY = 'signalsIndex'; +export const DETECTION_ENGINE_SIGNALS_URL = `${DETECTION_ENGINE_URL}/signals`; +export const DETECTION_ENGINE_SIGNALS_STATUS_URL = `${DETECTION_ENGINE_SIGNALS_URL}/status`; +export const DETECTION_ENGINE_QUERY_SIGNALS_URL = `${DETECTION_ENGINE_SIGNALS_URL}/search`; + +/** + * Common naming convention for an unauthenticated user + */ +export const UNAUTHENTICATED_USER = 'Unauthenticated'; + +/* + Licensing requirements + */ +export const MINIMUM_ML_LICENSE = 'platinum'; + +/* + Rule notifications options +*/ +export const NOTIFICATION_SUPPORTED_ACTION_TYPES_IDS = [ + '.email', + '.slack', + '.pagerduty', + '.webhook', +]; +export const NOTIFICATION_THROTTLE_NO_ACTIONS = 'no_actions'; +export const NOTIFICATION_THROTTLE_RULE = 'rule'; + +/** + * Histograms for fields named in this list should be displayed with an + * "All others" bucket, to count events that don't specify a value for + * the field being counted + */ +export const showAllOthersBucket: string[] = [ + 'destination.ip', + 'event.action', + 'event.category', + 'event.dataset', + 'event.module', + 'signal.rule.threat.tactic.name', + 'source.ip', + 'destination.ip', + 'user.name', +]; diff --git a/x-pack/legacy/plugins/siem/default_index_pattern.ts b/x-pack/plugins/siem/common/default_index_pattern.ts similarity index 100% rename from x-pack/legacy/plugins/siem/default_index_pattern.ts rename to x-pack/plugins/siem/common/default_index_pattern.ts diff --git a/x-pack/legacy/plugins/siem/common/detection_engine/ml_helpers.test.ts b/x-pack/plugins/siem/common/detection_engine/ml_helpers.test.ts similarity index 100% rename from x-pack/legacy/plugins/siem/common/detection_engine/ml_helpers.test.ts rename to x-pack/plugins/siem/common/detection_engine/ml_helpers.test.ts diff --git a/x-pack/legacy/plugins/siem/common/detection_engine/ml_helpers.ts b/x-pack/plugins/siem/common/detection_engine/ml_helpers.ts similarity index 100% rename from x-pack/legacy/plugins/siem/common/detection_engine/ml_helpers.ts rename to x-pack/plugins/siem/common/detection_engine/ml_helpers.ts diff --git a/x-pack/legacy/plugins/siem/common/detection_engine/transform_actions.test.ts b/x-pack/plugins/siem/common/detection_engine/transform_actions.test.ts similarity index 100% rename from x-pack/legacy/plugins/siem/common/detection_engine/transform_actions.test.ts rename to x-pack/plugins/siem/common/detection_engine/transform_actions.test.ts diff --git a/x-pack/legacy/plugins/siem/common/detection_engine/transform_actions.ts b/x-pack/plugins/siem/common/detection_engine/transform_actions.ts similarity index 90% rename from x-pack/legacy/plugins/siem/common/detection_engine/transform_actions.ts rename to x-pack/plugins/siem/common/detection_engine/transform_actions.ts index aeb4d53933022..4ce3823575833 100644 --- a/x-pack/legacy/plugins/siem/common/detection_engine/transform_actions.ts +++ b/x-pack/plugins/siem/common/detection_engine/transform_actions.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { AlertAction } from '../../../../../plugins/alerting/common'; +import { AlertAction } from '../../../alerting/common'; import { RuleAlertAction } from './types'; export const transformRuleToAlertAction = ({ diff --git a/x-pack/plugins/siem/common/detection_engine/types.ts b/x-pack/plugins/siem/common/detection_engine/types.ts new file mode 100644 index 0000000000000..5a91cfd4809c6 --- /dev/null +++ b/x-pack/plugins/siem/common/detection_engine/types.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. + */ +import * as t from 'io-ts'; + +import { AlertAction } from '../../../alerting/common'; + +export type RuleAlertAction = Omit & { + action_type_id: string; +}; + +export const RuleTypeSchema = t.keyof({ + query: null, + saved_query: null, + machine_learning: null, +}); +export type RuleType = t.TypeOf; diff --git a/x-pack/legacy/plugins/siem/common/graphql/root/index.ts b/x-pack/plugins/siem/common/graphql/root/index.ts similarity index 100% rename from x-pack/legacy/plugins/siem/common/graphql/root/index.ts rename to x-pack/plugins/siem/common/graphql/root/index.ts diff --git a/x-pack/legacy/plugins/siem/common/graphql/root/schema.gql.ts b/x-pack/plugins/siem/common/graphql/root/schema.gql.ts similarity index 100% rename from x-pack/legacy/plugins/siem/common/graphql/root/schema.gql.ts rename to x-pack/plugins/siem/common/graphql/root/schema.gql.ts diff --git a/x-pack/legacy/plugins/siem/common/graphql/shared/index.ts b/x-pack/plugins/siem/common/graphql/shared/index.ts similarity index 100% rename from x-pack/legacy/plugins/siem/common/graphql/shared/index.ts rename to x-pack/plugins/siem/common/graphql/shared/index.ts diff --git a/x-pack/legacy/plugins/siem/common/graphql/shared/schema.gql.ts b/x-pack/plugins/siem/common/graphql/shared/schema.gql.ts similarity index 100% rename from x-pack/legacy/plugins/siem/common/graphql/shared/schema.gql.ts rename to x-pack/plugins/siem/common/graphql/shared/schema.gql.ts diff --git a/x-pack/legacy/plugins/siem/common/typed_json.ts b/x-pack/plugins/siem/common/typed_json.ts similarity index 91% rename from x-pack/legacy/plugins/siem/common/typed_json.ts rename to x-pack/plugins/siem/common/typed_json.ts index dcd26d176d746..62e7319e091cb 100644 --- a/x-pack/legacy/plugins/siem/common/typed_json.ts +++ b/x-pack/plugins/siem/common/typed_json.ts @@ -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 { JsonObject } from '../../../../../src/plugins/kibana_utils/public'; +import { JsonObject } from '../../../../src/plugins/kibana_utils/public'; export type ESQuery = ESRangeQuery | ESQueryStringQuery | ESMatchQuery | ESTermQuery | JsonObject; diff --git a/x-pack/legacy/plugins/siem/common/utility_types.ts b/x-pack/plugins/siem/common/utility_types.ts similarity index 100% rename from x-pack/legacy/plugins/siem/common/utility_types.ts rename to x-pack/plugins/siem/common/utility_types.ts diff --git a/x-pack/legacy/plugins/siem/cypress/.eslintrc.json b/x-pack/plugins/siem/cypress/.eslintrc.json similarity index 100% rename from x-pack/legacy/plugins/siem/cypress/.eslintrc.json rename to x-pack/plugins/siem/cypress/.eslintrc.json diff --git a/x-pack/legacy/plugins/siem/cypress/.gitignore b/x-pack/plugins/siem/cypress/.gitignore similarity index 100% rename from x-pack/legacy/plugins/siem/cypress/.gitignore rename to x-pack/plugins/siem/cypress/.gitignore diff --git a/x-pack/plugins/siem/cypress/README.md b/x-pack/plugins/siem/cypress/README.md new file mode 100644 index 0000000000000..d84c66fec1c3a --- /dev/null +++ b/x-pack/plugins/siem/cypress/README.md @@ -0,0 +1,355 @@ +# Cypress Tests + +The `siem/cypress` directory contains end to end tests, (plus a few tests +that rely on mocked API calls), that execute via [Cypress](https://www.cypress.io/). + +Cypress tests may be run against: + +- A local Kibana instance, interactively or via the command line. Credentials +are specified via `kibana.dev.yml` or environment variables. +- A remote Elastic Cloud instance (override `baseUrl`), interactively or via +the command line. Again, credentials are specified via `kibana.dev.yml` or +environment variables. +- As part of CI (override `baseUrl` and pass credentials via the +`CYPRESS_ELASTICSEARCH_USERNAME` and `CYPRESS_ELASTICSEARCH_PASSWORD` +environment variables), via command line. + +At present, Cypress tests are only executed manually. They are **not** yet +integrated in the Kibana CI infrastructure, and therefore do **not** run +automatically when you submit a PR. + +## Smoke Tests + +Smoke Tests are located in `siem/cypress/integration/smoke_tests` + +## Structure + +### Tasks + +_Tasks_ are functions that my be re-used across tests. Inside the _tasks_ folder there are some other folders that represents +the page to which we will perform the actions. For each folder we are going to create a file for each one of the sections that + has the page. + +i.e. +- tasks + - hosts + - events.ts + +### Screens + +In _screens_ folder we are going to find all the elements we want to interact in our tests. Inside _screens_ fonder there +are some other folders that represents the page that contains the elements the tests are going to interact with. For each +folder we are going to create a file for each one of the sections that the page has. + +i.e. +- tasks + - hosts + - events.ts + +## Mock Data + +We prefer not to mock API responses in most of our smoke tests, but sometimes +it's necessary because a test must assert that a specific value is rendered, +and it's not possible to derive that value based on the data in the +environment where tests are running. + +Mocked responses API from the server are located in `siem/cypress/fixtures`. + +## Speeding up test execution time + +Loading the web page takes a big amount of time, in order to minimize that impact, the following points should be +taken into consideration until another solution is implemented: + +- Don't refresh the page for every test to clean the state of it. +- Instead, group the tests that are similar in different contexts. +- For every context login only once, clean the state between tests if needed without re-loading the page. +- All tests in a spec file must be order-independent. + - If you need to reload the page to make the tests order-independent, consider to create a new context. + +Remember that minimizing the number of times the web page is loaded, we minimize as well the execution time. + +## Authentication + +When running tests, there are two ways to specify the credentials used to +authenticate with Kibana: + +- Via `kibana.dev.yml` (recommended for developers) +- Via the `CYPRESS_ELASTICSEARCH_USERNAME` and `CYPRESS_ELASTICSEARCH_PASSWORD` +environment variables (recommended for CI), or when testing a remote Kibana +instance, e.g. in Elastic Cloud. + +Note: Tests that use the `login()` test helper function for authentication will +automatically use the `CYPRESS_ELASTICSEARCH_USERNAME` and `CYPRESS_ELASTICSEARCH_PASSWORD` +environment variables when they are defined, and fall back to the values in +`config/kibana.dev.yml` when they are unset. + +### Content Security Policy (CSP) Settings + +Your local or cloud Kibana server must have the `csp.strict: false` setting +configured in `kibana.dev.yml`, or `kibana.yml`, as shown in the example below: + +```yaml +csp.strict: false +``` + +The above setting is required to prevent the _Please upgrade +your browser_ / _This Kibana installation has strict security requirements +enabled that your current browser does not meet._ warning that's displayed for +unsupported user agents, like the one reported by Cypress when running tests. + +### Example `kibana.dev.yml` + +If you're a developer running tests interactively or on the command line, the +easiset way to specify the credentials used for authentication is to update + `kibana.dev.yml` per the following example: + +```yaml +csp.strict: false +elasticsearch: + username: 'elastic' + password: '' + hosts: ['https://:9200'] +``` + +## Running (Headless) Tests on the Command Line as a Jenkins execution (The preferred way) + +To run (headless) tests as a Jenkins execution. + +1. First bootstrap kibana changes from the Kibana root directory: + +```sh +yarn kbn bootstrap +``` + +2. Launch Cypress command line test runner: + +```sh +cd x-pack/plugins/siem +yarn cypress:run-as-ci +``` + +Note that with this type of execution you don't need to have running a kibana and elasticsearch instance. This is because + the command, as it would happen in the CI, will launch the instances. The elasticsearch instance will be fed data + found in: `x-pack/test/siem_cypress/es_archives` + +As in this case we want to mimic a CI execution we want to execute the tests with the same set of data, this is why +in this case does not make sense to override Cypress environment variables. + +### Test data + +As mentioned above, when running the tests as Jenkins the tests are populated with data ("archives") found in: `x-pack/test/siem_cypress/es_archives`. + +By default, each test is populated with some base data: an empty kibana index and a set of auditbeat data (the `empty_kibana` and `auditbeat` archives, respectively). This is usually enough to cover most of the scenarios that we are testing. + +#### Running tests with additional archives + +When the base data is insufficient, one can specify additional archives. Use `esArchiverLoad` to load the necessary archive, and `esArchiverUnload` to remove the archive from elasticsearch: + +```typescript +import { esArchiverLoad, esArchiverUnload } from '../tasks/es_archiver'; + +describe('This are going to be a set of tests', () => { + before(() => { + esArchiverLoad('name_of_the_data_set_you_want_to_load'); + }); + + after(() => { + esArchiverUnload('name_of_the_data_set_you_want_to_unload'); + }); + + it('Going to test something awesome', () => { + hereGoesYourAwesomeTestcode + }); +}); + +``` + +Note that loading and unloading data take a significant amount of time, so try to minimize their use. + +### Current archives + +The current archives can be found in `x-pack/test/siem_cypress/es_archives/`. + +- auditbeat + - Auditbeat data generated in Sep, 2019 with the following hosts present: + - suricata-iowa + - siem-kibana + - siem-es + - jessie +- closed_signals + - Set of data with 108 closed signals linked to "Signals test" custom rule. +- custome_rules + - Set if data with just 4 custom activated rules. +- empty_kibana + - Empty kibana board. +- prebuilt_rules_loaded + - Elastic prebuilt loaded rules and deactivated. +- signals + - Set of data with 108 opened signals linked to "Signals test" custom rule. + +### How to generate a new archive + +We are using es_archiver in order to manage the data that our Cypress tests needs. + +1. Setup if possible a clean instance of kibana and elasticsearch (if not, possible please try to clean the data that you are going to generate). +2. With the kibana and elasticsearch instance up and running, create the data that you need for your test. +3. When you are sure that you have all the data you need run the following command from: `x-pack/plugins/siem` + +```sh +node ../../../scripts/es_archiver save --dir ../../test/siem_cypress/es_archives --config ../../../test/functional/config.js --es-url http://:@: +``` + +Example: +```sh +node ../../../scripts/es_archiver save custom_rules ".kibana",".siem-signal*" --dir ../../test/siem_cypress/es_archives --config ../../../test/functional/config.js --es-url http://elastic:changeme@localhost:9220 +``` + +Note that the command is going to create the folder if does not exist in the directory with the imported data. + + +## Running Tests Interactively + +Use the Cypress interactive test runner to develop and debug specific tests +by adding a `.only` to the test you're developing, or click on a specific +spec in the interactive test runner to run just the tests in that spec. + +To run and debug tests in interactively via the Cypress test runner: + +1. Disable CSP on the local or remote Kibana instance, as described in the +_Content Security Policy (CSP) Settings_ section above. + +2. To specify the credentials required for authentication, configure +`config/kibana.dev.yml`, as described in the _Server and Authentication +Requirements_ section above, or specify them via environment variables +as described later in this section. + +3. Start a local instance of the Kibana development server (only if testing against a +local host): + +```sh +yarn start --no-base-path +``` + +4. Launch the Cypress interactive test runner via one of the following options: + +- To run tests interactively against the default (local) host specified by +`baseUrl`, as configured in `plugins/siem/cypress.json`: + +```sh +cd x-pack/plugins/siem +yarn cypress:open +``` + +- To (optionally) run tests interactively against a different host, pass the +`CYPRESS_baseUrl` environment variable on the command line when launching the +test runner, as shown in the following example: + +```sh +cd x-pack/plugins/siem +CYPRESS_baseUrl=http://localhost:5601 yarn cypress:open +``` + +- To (optionally) override username and password via environment variables when +running tests interactively: + +```sh +cd x-pack/plugins/siem +CYPRESS_baseUrl=http://localhost:5601 CYPRESS_ELASTICSEARCH_USERNAME=elastic CYPRESS_ELASTICSEARCH_PASSWORD= yarn cypress:open +``` + +5. Click the `Run all specs` button in the Cypress test runner (after adding +a `.only` to an `it` or `describe` block). + +## Running (Headless) Tests on the Command Line + +To run (headless) tests on the command line: + +1. Disable CSP on the local or remote Kibana instance, as described in the +_Content Security Policy (CSP) Settings_ section above. + +2. To specify the credentials required for authentication, configure +`config/kibana.dev.yml`, as described in the _Server and Authentication +Requirements_ section above, or specify them via environment variables +as described later in this section. + +3. Start a local instance of the Kibana development server (only if testing against a +local host): + +```sh +yarn start --no-base-path +``` + +4. Launch the Cypress command line test runner via one of the following options: + +- To run tests on the command line against the default (local) host specified by +`baseUrl`, as configured in `plugins/siem/cypress.json`: + +```sh +cd x-pack/plugins/siem +yarn cypress:run +``` + +- To (optionally) run tests on the command line against a different host, pass +`CYPRESS_baseUrl` as an environment variable on the command line, as shown in +the following example: + +```sh +cd x-pack/plugins/siem +CYPRESS_baseUrl=http://localhost:5601 yarn cypress:run +``` + +- To (optionally) override username and password via environment variables when +running via the command line: + +```sh +cd x-pack/plugins/siem +CYPRESS_baseUrl=http://localhost:5601 CYPRESS_ELASTICSEARCH_USERNAME=elastic CYPRESS_ELASTICSEARCH_PASSWORD= yarn cypress:run +``` + +## Reporting + +When Cypress tests are run on the command line via `yarn cypress:run`, +reporting artifacts are generated under the `target` directory in the root +of the Kibana, as detailed for each artifact type in the sections below. + +### HTML Reports + +An HTML report (e.g. for email notifications) is output to: + +``` +target/kibana-siem/cypress/results/output.html +``` + +### Screenshots + +Screenshots of failed tests are output to: + +``` +target/kibana-siem/cypress/screenshots +``` + +### `junit` Reports + +The Kibana CI process reports `junit` test results from the `target/junit` directory. + +Cypress `junit` reports are generated in `target/kibana-siem/cypress/results` +and copied to the `target/junit` directory. + +### Videos (optional) + +Videos are disabled by default, but can optionally be enabled by setting the +`CYPRESS_video=true` environment variable: + +``` +CYPRESS_video=true yarn cypress:run +``` + +Videos are (optionally) output to: + +``` +target/kibana-siem/cypress/videos +``` + +## Linting + +Optional linting rules for Cypress and linting setup can be found [here](https://github.com/cypress-io/eslint-plugin-cypress#usage) diff --git a/x-pack/plugins/siem/cypress/cypress.json b/x-pack/plugins/siem/cypress/cypress.json new file mode 100644 index 0000000000000..7a4efba8c2d64 --- /dev/null +++ b/x-pack/plugins/siem/cypress/cypress.json @@ -0,0 +1,8 @@ +{ + "baseUrl": "http://localhost:5601", + "defaultCommandTimeout": 120000, + "screenshotsFolder": "../../../target/kibana-siem/cypress/screenshots", + "trashAssetsBeforeRuns": false, + "video": false, + "videosFolder": "../../../target/kibana-siem/cypress/videos" +} diff --git a/x-pack/legacy/plugins/siem/cypress/fixtures/overview.json b/x-pack/plugins/siem/cypress/fixtures/overview.json similarity index 100% rename from x-pack/legacy/plugins/siem/cypress/fixtures/overview.json rename to x-pack/plugins/siem/cypress/fixtures/overview.json diff --git a/x-pack/legacy/plugins/siem/cypress/integration/detections.spec.ts b/x-pack/plugins/siem/cypress/integration/detections.spec.ts similarity index 100% rename from x-pack/legacy/plugins/siem/cypress/integration/detections.spec.ts rename to x-pack/plugins/siem/cypress/integration/detections.spec.ts diff --git a/x-pack/legacy/plugins/siem/cypress/integration/detections_timeline.spec.ts b/x-pack/plugins/siem/cypress/integration/detections_timeline.spec.ts similarity index 100% rename from x-pack/legacy/plugins/siem/cypress/integration/detections_timeline.spec.ts rename to x-pack/plugins/siem/cypress/integration/detections_timeline.spec.ts diff --git a/x-pack/legacy/plugins/siem/cypress/integration/events_viewer.spec.ts b/x-pack/plugins/siem/cypress/integration/events_viewer.spec.ts similarity index 100% rename from x-pack/legacy/plugins/siem/cypress/integration/events_viewer.spec.ts rename to x-pack/plugins/siem/cypress/integration/events_viewer.spec.ts diff --git a/x-pack/legacy/plugins/siem/cypress/integration/fields_browser.spec.ts b/x-pack/plugins/siem/cypress/integration/fields_browser.spec.ts similarity index 100% rename from x-pack/legacy/plugins/siem/cypress/integration/fields_browser.spec.ts rename to x-pack/plugins/siem/cypress/integration/fields_browser.spec.ts diff --git a/x-pack/legacy/plugins/siem/cypress/integration/inspect.spec.ts b/x-pack/plugins/siem/cypress/integration/inspect.spec.ts similarity index 100% rename from x-pack/legacy/plugins/siem/cypress/integration/inspect.spec.ts rename to x-pack/plugins/siem/cypress/integration/inspect.spec.ts diff --git a/x-pack/legacy/plugins/siem/cypress/integration/ml_conditional_links.spec.ts b/x-pack/plugins/siem/cypress/integration/ml_conditional_links.spec.ts similarity index 100% rename from x-pack/legacy/plugins/siem/cypress/integration/ml_conditional_links.spec.ts rename to x-pack/plugins/siem/cypress/integration/ml_conditional_links.spec.ts diff --git a/x-pack/legacy/plugins/siem/cypress/integration/navigation.spec.ts b/x-pack/plugins/siem/cypress/integration/navigation.spec.ts similarity index 100% rename from x-pack/legacy/plugins/siem/cypress/integration/navigation.spec.ts rename to x-pack/plugins/siem/cypress/integration/navigation.spec.ts diff --git a/x-pack/legacy/plugins/siem/cypress/integration/overview.spec.ts b/x-pack/plugins/siem/cypress/integration/overview.spec.ts similarity index 100% rename from x-pack/legacy/plugins/siem/cypress/integration/overview.spec.ts rename to x-pack/plugins/siem/cypress/integration/overview.spec.ts diff --git a/x-pack/legacy/plugins/siem/cypress/integration/pagination.spec.ts b/x-pack/plugins/siem/cypress/integration/pagination.spec.ts similarity index 100% rename from x-pack/legacy/plugins/siem/cypress/integration/pagination.spec.ts rename to x-pack/plugins/siem/cypress/integration/pagination.spec.ts diff --git a/x-pack/plugins/siem/cypress/integration/signal_detection_rules.spec.ts b/x-pack/plugins/siem/cypress/integration/signal_detection_rules.spec.ts new file mode 100644 index 0000000000000..ce6a49b675ef1 --- /dev/null +++ b/x-pack/plugins/siem/cypress/integration/signal_detection_rules.spec.ts @@ -0,0 +1,83 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { + FIFTH_RULE, + FIRST_RULE, + RULE_NAME, + RULE_SWITCH, + SECOND_RULE, + SEVENTH_RULE, +} from '../screens/signal_detection_rules'; + +import { + goToManageSignalDetectionRules, + waitForSignalsPanelToBeLoaded, + waitForSignalsIndexToBeCreated, +} from '../tasks/detections'; +import { esArchiverLoad, esArchiverUnload } from '../tasks/es_archiver'; +import { loginAndWaitForPageWithoutDateRange } from '../tasks/login'; +import { + activateRule, + sortByActivatedRules, + waitForLoadElasticPrebuiltDetectionRulesTableToBeLoaded, + waitForRuleToBeActivated, +} from '../tasks/signal_detection_rules'; + +import { DETECTIONS } from '../urls/navigation'; + +describe('Signal detection rules', () => { + before(() => { + esArchiverLoad('prebuilt_rules_loaded'); + }); + + after(() => { + esArchiverUnload('prebuilt_rules_loaded'); + }); + + it('Sorts by activated rules', () => { + loginAndWaitForPageWithoutDateRange(DETECTIONS); + waitForSignalsPanelToBeLoaded(); + waitForSignalsIndexToBeCreated(); + goToManageSignalDetectionRules(); + waitForLoadElasticPrebuiltDetectionRulesTableToBeLoaded(); + cy.get(RULE_NAME) + .eq(FIFTH_RULE) + .invoke('text') + .then(fifthRuleName => { + activateRule(FIFTH_RULE); + waitForRuleToBeActivated(); + cy.get(RULE_NAME) + .eq(SEVENTH_RULE) + .invoke('text') + .then(seventhRuleName => { + activateRule(SEVENTH_RULE); + waitForRuleToBeActivated(); + sortByActivatedRules(); + + cy.get(RULE_NAME) + .eq(FIRST_RULE) + .invoke('text') + .then(firstRuleName => { + cy.get(RULE_NAME) + .eq(SECOND_RULE) + .invoke('text') + .then(secondRuleName => { + const expectedRulesNames = `${firstRuleName} ${secondRuleName}`; + cy.wrap(expectedRulesNames).should('include', fifthRuleName); + cy.wrap(expectedRulesNames).should('include', seventhRuleName); + }); + }); + + cy.get(RULE_SWITCH) + .eq(FIRST_RULE) + .should('have.attr', 'role', 'switch'); + cy.get(RULE_SWITCH) + .eq(SECOND_RULE) + .should('have.attr', 'role', 'switch'); + }); + }); + }); +}); diff --git a/x-pack/legacy/plugins/siem/cypress/integration/signal_detection_rules_custom.spec.ts b/x-pack/plugins/siem/cypress/integration/signal_detection_rules_custom.spec.ts similarity index 100% rename from x-pack/legacy/plugins/siem/cypress/integration/signal_detection_rules_custom.spec.ts rename to x-pack/plugins/siem/cypress/integration/signal_detection_rules_custom.spec.ts diff --git a/x-pack/legacy/plugins/siem/cypress/integration/signal_detection_rules_ml.spec.ts b/x-pack/plugins/siem/cypress/integration/signal_detection_rules_ml.spec.ts similarity index 100% rename from x-pack/legacy/plugins/siem/cypress/integration/signal_detection_rules_ml.spec.ts rename to x-pack/plugins/siem/cypress/integration/signal_detection_rules_ml.spec.ts diff --git a/x-pack/legacy/plugins/siem/cypress/integration/signal_detection_rules_prebuilt.spec.ts b/x-pack/plugins/siem/cypress/integration/signal_detection_rules_prebuilt.spec.ts similarity index 100% rename from x-pack/legacy/plugins/siem/cypress/integration/signal_detection_rules_prebuilt.spec.ts rename to x-pack/plugins/siem/cypress/integration/signal_detection_rules_prebuilt.spec.ts diff --git a/x-pack/legacy/plugins/siem/cypress/integration/timeline_data_providers.spec.ts b/x-pack/plugins/siem/cypress/integration/timeline_data_providers.spec.ts similarity index 100% rename from x-pack/legacy/plugins/siem/cypress/integration/timeline_data_providers.spec.ts rename to x-pack/plugins/siem/cypress/integration/timeline_data_providers.spec.ts diff --git a/x-pack/legacy/plugins/siem/cypress/integration/timeline_flyout_button.spec.ts b/x-pack/plugins/siem/cypress/integration/timeline_flyout_button.spec.ts similarity index 100% rename from x-pack/legacy/plugins/siem/cypress/integration/timeline_flyout_button.spec.ts rename to x-pack/plugins/siem/cypress/integration/timeline_flyout_button.spec.ts diff --git a/x-pack/legacy/plugins/siem/cypress/integration/timeline_search_or_filter.spec.ts b/x-pack/plugins/siem/cypress/integration/timeline_search_or_filter.spec.ts similarity index 100% rename from x-pack/legacy/plugins/siem/cypress/integration/timeline_search_or_filter.spec.ts rename to x-pack/plugins/siem/cypress/integration/timeline_search_or_filter.spec.ts diff --git a/x-pack/legacy/plugins/siem/cypress/integration/timeline_toggle_column.spec.ts b/x-pack/plugins/siem/cypress/integration/timeline_toggle_column.spec.ts similarity index 100% rename from x-pack/legacy/plugins/siem/cypress/integration/timeline_toggle_column.spec.ts rename to x-pack/plugins/siem/cypress/integration/timeline_toggle_column.spec.ts diff --git a/x-pack/legacy/plugins/siem/cypress/integration/url_state.spec.ts b/x-pack/plugins/siem/cypress/integration/url_state.spec.ts similarity index 100% rename from x-pack/legacy/plugins/siem/cypress/integration/url_state.spec.ts rename to x-pack/plugins/siem/cypress/integration/url_state.spec.ts diff --git a/x-pack/legacy/plugins/siem/cypress/objects/rule.ts b/x-pack/plugins/siem/cypress/objects/rule.ts similarity index 100% rename from x-pack/legacy/plugins/siem/cypress/objects/rule.ts rename to x-pack/plugins/siem/cypress/objects/rule.ts diff --git a/x-pack/legacy/plugins/siem/cypress/objects/timeline.ts b/x-pack/plugins/siem/cypress/objects/timeline.ts similarity index 100% rename from x-pack/legacy/plugins/siem/cypress/objects/timeline.ts rename to x-pack/plugins/siem/cypress/objects/timeline.ts diff --git a/x-pack/legacy/plugins/siem/cypress/plugins/index.js b/x-pack/plugins/siem/cypress/plugins/index.js similarity index 100% rename from x-pack/legacy/plugins/siem/cypress/plugins/index.js rename to x-pack/plugins/siem/cypress/plugins/index.js diff --git a/x-pack/plugins/siem/cypress/reporter_config.json b/x-pack/plugins/siem/cypress/reporter_config.json new file mode 100644 index 0000000000000..e7e08eeae1dab --- /dev/null +++ b/x-pack/plugins/siem/cypress/reporter_config.json @@ -0,0 +1,10 @@ +{ + "reporterEnabled": "mochawesome, mocha-junit-reporter", + "reporterOptions": { + "html": false, + "json": true, + "mochaFile": "../../../target/kibana-siem/cypress/results/TEST-siem-cypress-[hash].xml", + "overwrite": false, + "reportDir": "../../../target/kibana-siem/cypress/results" + } +} diff --git a/x-pack/legacy/plugins/siem/cypress/screens/create_new_rule.ts b/x-pack/plugins/siem/cypress/screens/create_new_rule.ts similarity index 100% rename from x-pack/legacy/plugins/siem/cypress/screens/create_new_rule.ts rename to x-pack/plugins/siem/cypress/screens/create_new_rule.ts diff --git a/x-pack/legacy/plugins/siem/cypress/screens/date_picker.ts b/x-pack/plugins/siem/cypress/screens/date_picker.ts similarity index 100% rename from x-pack/legacy/plugins/siem/cypress/screens/date_picker.ts rename to x-pack/plugins/siem/cypress/screens/date_picker.ts diff --git a/x-pack/legacy/plugins/siem/cypress/screens/detections.ts b/x-pack/plugins/siem/cypress/screens/detections.ts similarity index 100% rename from x-pack/legacy/plugins/siem/cypress/screens/detections.ts rename to x-pack/plugins/siem/cypress/screens/detections.ts diff --git a/x-pack/legacy/plugins/siem/cypress/screens/fields_browser.ts b/x-pack/plugins/siem/cypress/screens/fields_browser.ts similarity index 100% rename from x-pack/legacy/plugins/siem/cypress/screens/fields_browser.ts rename to x-pack/plugins/siem/cypress/screens/fields_browser.ts diff --git a/x-pack/legacy/plugins/siem/cypress/screens/hosts/all_hosts.ts b/x-pack/plugins/siem/cypress/screens/hosts/all_hosts.ts similarity index 100% rename from x-pack/legacy/plugins/siem/cypress/screens/hosts/all_hosts.ts rename to x-pack/plugins/siem/cypress/screens/hosts/all_hosts.ts diff --git a/x-pack/legacy/plugins/siem/cypress/screens/hosts/authentications.ts b/x-pack/plugins/siem/cypress/screens/hosts/authentications.ts similarity index 100% rename from x-pack/legacy/plugins/siem/cypress/screens/hosts/authentications.ts rename to x-pack/plugins/siem/cypress/screens/hosts/authentications.ts diff --git a/x-pack/legacy/plugins/siem/cypress/screens/hosts/events.ts b/x-pack/plugins/siem/cypress/screens/hosts/events.ts similarity index 100% rename from x-pack/legacy/plugins/siem/cypress/screens/hosts/events.ts rename to x-pack/plugins/siem/cypress/screens/hosts/events.ts diff --git a/x-pack/legacy/plugins/siem/cypress/screens/hosts/main.ts b/x-pack/plugins/siem/cypress/screens/hosts/main.ts similarity index 100% rename from x-pack/legacy/plugins/siem/cypress/screens/hosts/main.ts rename to x-pack/plugins/siem/cypress/screens/hosts/main.ts diff --git a/x-pack/legacy/plugins/siem/cypress/screens/hosts/uncommon_processes.ts b/x-pack/plugins/siem/cypress/screens/hosts/uncommon_processes.ts similarity index 100% rename from x-pack/legacy/plugins/siem/cypress/screens/hosts/uncommon_processes.ts rename to x-pack/plugins/siem/cypress/screens/hosts/uncommon_processes.ts diff --git a/x-pack/legacy/plugins/siem/cypress/screens/inspect.ts b/x-pack/plugins/siem/cypress/screens/inspect.ts similarity index 100% rename from x-pack/legacy/plugins/siem/cypress/screens/inspect.ts rename to x-pack/plugins/siem/cypress/screens/inspect.ts diff --git a/x-pack/legacy/plugins/siem/cypress/screens/network/flows.ts b/x-pack/plugins/siem/cypress/screens/network/flows.ts similarity index 100% rename from x-pack/legacy/plugins/siem/cypress/screens/network/flows.ts rename to x-pack/plugins/siem/cypress/screens/network/flows.ts diff --git a/x-pack/legacy/plugins/siem/cypress/screens/overview.ts b/x-pack/plugins/siem/cypress/screens/overview.ts similarity index 100% rename from x-pack/legacy/plugins/siem/cypress/screens/overview.ts rename to x-pack/plugins/siem/cypress/screens/overview.ts diff --git a/x-pack/legacy/plugins/siem/cypress/screens/pagination.ts b/x-pack/plugins/siem/cypress/screens/pagination.ts similarity index 100% rename from x-pack/legacy/plugins/siem/cypress/screens/pagination.ts rename to x-pack/plugins/siem/cypress/screens/pagination.ts diff --git a/x-pack/legacy/plugins/siem/cypress/screens/rule_details.ts b/x-pack/plugins/siem/cypress/screens/rule_details.ts similarity index 100% rename from x-pack/legacy/plugins/siem/cypress/screens/rule_details.ts rename to x-pack/plugins/siem/cypress/screens/rule_details.ts diff --git a/x-pack/legacy/plugins/siem/cypress/screens/siem_header.ts b/x-pack/plugins/siem/cypress/screens/siem_header.ts similarity index 100% rename from x-pack/legacy/plugins/siem/cypress/screens/siem_header.ts rename to x-pack/plugins/siem/cypress/screens/siem_header.ts diff --git a/x-pack/legacy/plugins/siem/cypress/screens/siem_main.ts b/x-pack/plugins/siem/cypress/screens/siem_main.ts similarity index 100% rename from x-pack/legacy/plugins/siem/cypress/screens/siem_main.ts rename to x-pack/plugins/siem/cypress/screens/siem_main.ts diff --git a/x-pack/legacy/plugins/siem/cypress/screens/signal_detection_rules.ts b/x-pack/plugins/siem/cypress/screens/signal_detection_rules.ts similarity index 100% rename from x-pack/legacy/plugins/siem/cypress/screens/signal_detection_rules.ts rename to x-pack/plugins/siem/cypress/screens/signal_detection_rules.ts diff --git a/x-pack/legacy/plugins/siem/cypress/screens/timeline.ts b/x-pack/plugins/siem/cypress/screens/timeline.ts similarity index 100% rename from x-pack/legacy/plugins/siem/cypress/screens/timeline.ts rename to x-pack/plugins/siem/cypress/screens/timeline.ts diff --git a/x-pack/legacy/plugins/siem/cypress/support/commands.js b/x-pack/plugins/siem/cypress/support/commands.js similarity index 100% rename from x-pack/legacy/plugins/siem/cypress/support/commands.js rename to x-pack/plugins/siem/cypress/support/commands.js diff --git a/x-pack/legacy/plugins/siem/cypress/support/index.d.ts b/x-pack/plugins/siem/cypress/support/index.d.ts similarity index 100% rename from x-pack/legacy/plugins/siem/cypress/support/index.d.ts rename to x-pack/plugins/siem/cypress/support/index.d.ts diff --git a/x-pack/legacy/plugins/siem/cypress/support/index.js b/x-pack/plugins/siem/cypress/support/index.js similarity index 100% rename from x-pack/legacy/plugins/siem/cypress/support/index.js rename to x-pack/plugins/siem/cypress/support/index.js diff --git a/x-pack/legacy/plugins/siem/cypress/tasks/common.ts b/x-pack/plugins/siem/cypress/tasks/common.ts similarity index 100% rename from x-pack/legacy/plugins/siem/cypress/tasks/common.ts rename to x-pack/plugins/siem/cypress/tasks/common.ts diff --git a/x-pack/legacy/plugins/siem/cypress/tasks/create_new_rule.ts b/x-pack/plugins/siem/cypress/tasks/create_new_rule.ts similarity index 100% rename from x-pack/legacy/plugins/siem/cypress/tasks/create_new_rule.ts rename to x-pack/plugins/siem/cypress/tasks/create_new_rule.ts diff --git a/x-pack/legacy/plugins/siem/cypress/tasks/date_picker.ts b/x-pack/plugins/siem/cypress/tasks/date_picker.ts similarity index 100% rename from x-pack/legacy/plugins/siem/cypress/tasks/date_picker.ts rename to x-pack/plugins/siem/cypress/tasks/date_picker.ts diff --git a/x-pack/legacy/plugins/siem/cypress/tasks/detections.ts b/x-pack/plugins/siem/cypress/tasks/detections.ts similarity index 100% rename from x-pack/legacy/plugins/siem/cypress/tasks/detections.ts rename to x-pack/plugins/siem/cypress/tasks/detections.ts diff --git a/x-pack/plugins/siem/cypress/tasks/es_archiver.ts b/x-pack/plugins/siem/cypress/tasks/es_archiver.ts new file mode 100644 index 0000000000000..8a4ab8c819457 --- /dev/null +++ b/x-pack/plugins/siem/cypress/tasks/es_archiver.ts @@ -0,0 +1,45 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export const esArchiverLoadEmptyKibana = () => { + cy.exec( + `node ../../../scripts/es_archiver empty_kibana load empty--dir ../../test/siem_cypress/es_archives --config ../../../test/functional/config.js --es-url ${Cypress.env( + 'ELASTICSEARCH_URL' + )} --kibana-url ${Cypress.config().baseUrl}` + ); +}; + +export const esArchiverLoad = (folder: string) => { + cy.exec( + `node ../../../scripts/es_archiver load ${folder} --dir ../../test/siem_cypress/es_archives --config ../../../test/functional/config.js --es-url ${Cypress.env( + 'ELASTICSEARCH_URL' + )} --kibana-url ${Cypress.config().baseUrl}` + ); +}; + +export const esArchiverUnload = (folder: string) => { + cy.exec( + `node ../../../scripts/es_archiver unload ${folder} --dir ../../test/siem_cypress/es_archives --config ../../../test/functional/config.js --es-url ${Cypress.env( + 'ELASTICSEARCH_URL' + )} --kibana-url ${Cypress.config().baseUrl}` + ); +}; + +export const esArchiverUnloadEmptyKibana = () => { + cy.exec( + `node ../../../scripts/es_archiver unload empty_kibana empty--dir ../../test/siem_cypress/es_archives --config ../../../test/functional/config.js --es-url ${Cypress.env( + 'ELASTICSEARCH_URL' + )} --kibana-url ${Cypress.config().baseUrl}` + ); +}; + +export const esArchiverResetKibana = () => { + cy.exec( + `node ../../../scripts/es_archiver empty-kibana-index --config ../../../test/functional/config.js --es-url ${Cypress.env( + 'ELASTICSEARCH_URL' + )} --kibana-url ${Cypress.config().baseUrl}` + ); +}; diff --git a/x-pack/legacy/plugins/siem/cypress/tasks/fields_browser.ts b/x-pack/plugins/siem/cypress/tasks/fields_browser.ts similarity index 100% rename from x-pack/legacy/plugins/siem/cypress/tasks/fields_browser.ts rename to x-pack/plugins/siem/cypress/tasks/fields_browser.ts diff --git a/x-pack/legacy/plugins/siem/cypress/tasks/hosts/all_hosts.ts b/x-pack/plugins/siem/cypress/tasks/hosts/all_hosts.ts similarity index 100% rename from x-pack/legacy/plugins/siem/cypress/tasks/hosts/all_hosts.ts rename to x-pack/plugins/siem/cypress/tasks/hosts/all_hosts.ts diff --git a/x-pack/legacy/plugins/siem/cypress/tasks/hosts/authentications.ts b/x-pack/plugins/siem/cypress/tasks/hosts/authentications.ts similarity index 100% rename from x-pack/legacy/plugins/siem/cypress/tasks/hosts/authentications.ts rename to x-pack/plugins/siem/cypress/tasks/hosts/authentications.ts diff --git a/x-pack/legacy/plugins/siem/cypress/tasks/hosts/events.ts b/x-pack/plugins/siem/cypress/tasks/hosts/events.ts similarity index 100% rename from x-pack/legacy/plugins/siem/cypress/tasks/hosts/events.ts rename to x-pack/plugins/siem/cypress/tasks/hosts/events.ts diff --git a/x-pack/legacy/plugins/siem/cypress/tasks/hosts/main.ts b/x-pack/plugins/siem/cypress/tasks/hosts/main.ts similarity index 100% rename from x-pack/legacy/plugins/siem/cypress/tasks/hosts/main.ts rename to x-pack/plugins/siem/cypress/tasks/hosts/main.ts diff --git a/x-pack/legacy/plugins/siem/cypress/tasks/hosts/uncommon_processes.ts b/x-pack/plugins/siem/cypress/tasks/hosts/uncommon_processes.ts similarity index 100% rename from x-pack/legacy/plugins/siem/cypress/tasks/hosts/uncommon_processes.ts rename to x-pack/plugins/siem/cypress/tasks/hosts/uncommon_processes.ts diff --git a/x-pack/legacy/plugins/siem/cypress/tasks/inspect.ts b/x-pack/plugins/siem/cypress/tasks/inspect.ts similarity index 100% rename from x-pack/legacy/plugins/siem/cypress/tasks/inspect.ts rename to x-pack/plugins/siem/cypress/tasks/inspect.ts diff --git a/x-pack/legacy/plugins/siem/cypress/tasks/login.ts b/x-pack/plugins/siem/cypress/tasks/login.ts similarity index 98% rename from x-pack/legacy/plugins/siem/cypress/tasks/login.ts rename to x-pack/plugins/siem/cypress/tasks/login.ts index c7788b080d06e..1bbf41d05db00 100644 --- a/x-pack/legacy/plugins/siem/cypress/tasks/login.ts +++ b/x-pack/plugins/siem/cypress/tasks/login.ts @@ -10,7 +10,7 @@ import * as yaml from 'js-yaml'; * Credentials in the `kibana.dev.yml` config file will be used to authenticate * with Kibana when credentials are not provided via environment variables */ -const KIBANA_DEV_YML_PATH = '../../../../config/kibana.dev.yml'; +const KIBANA_DEV_YML_PATH = '../../../config/kibana.dev.yml'; /** * The configuration path in `kibana.dev.yml` to the username to be used when diff --git a/x-pack/legacy/plugins/siem/cypress/tasks/network/flows.ts b/x-pack/plugins/siem/cypress/tasks/network/flows.ts similarity index 100% rename from x-pack/legacy/plugins/siem/cypress/tasks/network/flows.ts rename to x-pack/plugins/siem/cypress/tasks/network/flows.ts diff --git a/x-pack/legacy/plugins/siem/cypress/tasks/overview.ts b/x-pack/plugins/siem/cypress/tasks/overview.ts similarity index 100% rename from x-pack/legacy/plugins/siem/cypress/tasks/overview.ts rename to x-pack/plugins/siem/cypress/tasks/overview.ts diff --git a/x-pack/legacy/plugins/siem/cypress/tasks/pagination.ts b/x-pack/plugins/siem/cypress/tasks/pagination.ts similarity index 100% rename from x-pack/legacy/plugins/siem/cypress/tasks/pagination.ts rename to x-pack/plugins/siem/cypress/tasks/pagination.ts diff --git a/x-pack/legacy/plugins/siem/cypress/tasks/siem_header.ts b/x-pack/plugins/siem/cypress/tasks/siem_header.ts similarity index 100% rename from x-pack/legacy/plugins/siem/cypress/tasks/siem_header.ts rename to x-pack/plugins/siem/cypress/tasks/siem_header.ts diff --git a/x-pack/legacy/plugins/siem/cypress/tasks/siem_main.ts b/x-pack/plugins/siem/cypress/tasks/siem_main.ts similarity index 100% rename from x-pack/legacy/plugins/siem/cypress/tasks/siem_main.ts rename to x-pack/plugins/siem/cypress/tasks/siem_main.ts diff --git a/x-pack/legacy/plugins/siem/cypress/tasks/signal_detection_rules.ts b/x-pack/plugins/siem/cypress/tasks/signal_detection_rules.ts similarity index 100% rename from x-pack/legacy/plugins/siem/cypress/tasks/signal_detection_rules.ts rename to x-pack/plugins/siem/cypress/tasks/signal_detection_rules.ts diff --git a/x-pack/legacy/plugins/siem/cypress/tasks/timeline.ts b/x-pack/plugins/siem/cypress/tasks/timeline.ts similarity index 100% rename from x-pack/legacy/plugins/siem/cypress/tasks/timeline.ts rename to x-pack/plugins/siem/cypress/tasks/timeline.ts diff --git a/x-pack/legacy/plugins/siem/cypress/tsconfig.json b/x-pack/plugins/siem/cypress/tsconfig.json similarity index 100% rename from x-pack/legacy/plugins/siem/cypress/tsconfig.json rename to x-pack/plugins/siem/cypress/tsconfig.json diff --git a/x-pack/legacy/plugins/siem/cypress/urls/ml_conditional_links.ts b/x-pack/plugins/siem/cypress/urls/ml_conditional_links.ts similarity index 100% rename from x-pack/legacy/plugins/siem/cypress/urls/ml_conditional_links.ts rename to x-pack/plugins/siem/cypress/urls/ml_conditional_links.ts diff --git a/x-pack/legacy/plugins/siem/cypress/urls/navigation.ts b/x-pack/plugins/siem/cypress/urls/navigation.ts similarity index 100% rename from x-pack/legacy/plugins/siem/cypress/urls/navigation.ts rename to x-pack/plugins/siem/cypress/urls/navigation.ts diff --git a/x-pack/legacy/plugins/siem/cypress/urls/state.ts b/x-pack/plugins/siem/cypress/urls/state.ts similarity index 100% rename from x-pack/legacy/plugins/siem/cypress/urls/state.ts rename to x-pack/plugins/siem/cypress/urls/state.ts diff --git a/x-pack/plugins/siem/kibana.json b/x-pack/plugins/siem/kibana.json index 2bc33b87a1b43..1eb1a7dbde876 100644 --- a/x-pack/plugins/siem/kibana.json +++ b/x-pack/plugins/siem/kibana.json @@ -3,6 +3,8 @@ "version": "8.0.0", "kibanaVersion": "kibana", "configPath": ["xpack", "siem"], + "requiredPlugins": ["actions", "alerting", "features", "licensing"], + "optionalPlugins": ["encryptedSavedObjects", "ml", "security", "spaces"], "server": true, "ui": false } diff --git a/x-pack/plugins/siem/package.json b/x-pack/plugins/siem/package.json new file mode 100644 index 0000000000000..1fcef46243628 --- /dev/null +++ b/x-pack/plugins/siem/package.json @@ -0,0 +1,20 @@ +{ + "author": "Elastic", + "name": "siem", + "version": "8.0.0", + "private": true, + "license": "Elastic-License", + "scripts": { + "extract-mitre-attacks": "node scripts/extract_tactics_techniques_mitre.js & node ../../../scripts/eslint ../../legacy/plugins/siem/public/pages/detection_engine/mitre/mitre_tactics_techniques.ts --fix", + "build-graphql-types": "node scripts/generate_types_from_graphql.js", + "cypress:open": "cypress open --config-file ./cypress/cypress.json", + "cypress:run": "cypress run --spec ./cypress/integration/**/*.spec.ts --config-file ./cypress/cypress.json --reporter ../../node_modules/cypress-multi-reporters --reporter-options configFile=./cypress/reporter_config.json; status=$?; ../../node_modules/.bin/mochawesome-merge --reportDir ../../../target/kibana-siem/cypress/results > ../../../target/kibana-siem/cypress/results/output.json; ../../../node_modules/.bin/marge ../../../target/kibana-siem/cypress/results/output.json --reportDir ../../../target/kibana-siem/cypress/results; mkdir -p ../../../target/junit && cp ../../../target/kibana-siem/cypress/results/*.xml ../../../target/junit/ && exit $status;", + "cypress:run-as-ci": "node ../../../scripts/functional_tests --config ../../test/siem_cypress/config.ts" + }, + "devDependencies": { + "@types/lodash": "^4.14.110" + }, + "dependencies": { + "lodash": "^4.17.15" + } +} diff --git a/x-pack/plugins/siem/scripts/check_circular_deps.js b/x-pack/plugins/siem/scripts/check_circular_deps.js new file mode 100644 index 0000000000000..4ba7020d13465 --- /dev/null +++ b/x-pack/plugins/siem/scripts/check_circular_deps.js @@ -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. + */ + +require('../../../../src/setup_node_env'); +require('./check_circular_deps/run_check_circular_deps_cli'); diff --git a/x-pack/legacy/plugins/siem/dev_tools/circular_deps/run_check_circular_deps_cli.js b/x-pack/plugins/siem/scripts/check_circular_deps/run_check_circular_deps_cli.js similarity index 88% rename from x-pack/legacy/plugins/siem/dev_tools/circular_deps/run_check_circular_deps_cli.js rename to x-pack/plugins/siem/scripts/check_circular_deps/run_check_circular_deps_cli.js index f3a97f5b9c9b6..0b5e5d6cf13b5 100644 --- a/x-pack/legacy/plugins/siem/dev_tools/circular_deps/run_check_circular_deps_cli.js +++ b/x-pack/plugins/siem/scripts/check_circular_deps/run_check_circular_deps_cli.js @@ -11,10 +11,13 @@ import madge from 'madge'; /* eslint-disable-next-line import/no-extraneous-dependencies */ import { run, createFailError } from '@kbn/dev-utils'; +const legacyPluginPath = '../../../../legacy/plugins/siem'; +const pluginPath = '../..'; + run( async ({ log }) => { const result = await madge( - [resolve(__dirname, '../../public'), resolve(__dirname, '../../common')], + [resolve(__dirname, legacyPluginPath, 'public'), resolve(__dirname, pluginPath, 'common')], { fileExtensions: ['ts', 'js', 'tsx'], excludeRegExp: [ diff --git a/x-pack/legacy/plugins/siem/scripts/combined_schema.ts b/x-pack/plugins/siem/scripts/combined_schema.ts similarity index 91% rename from x-pack/legacy/plugins/siem/scripts/combined_schema.ts rename to x-pack/plugins/siem/scripts/combined_schema.ts index 625eb3a4a4755..48215548650fe 100644 --- a/x-pack/legacy/plugins/siem/scripts/combined_schema.ts +++ b/x-pack/plugins/siem/scripts/combined_schema.ts @@ -6,6 +6,7 @@ import { buildSchemaFromTypeDefinitions } from 'graphql-tools'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths import { schemas as serverSchemas } from '../server/graphql'; export const schemas = [...serverSchemas]; diff --git a/x-pack/legacy/plugins/siem/scripts/convert_saved_search_to_rules.js b/x-pack/plugins/siem/scripts/convert_saved_search_to_rules.js similarity index 99% rename from x-pack/legacy/plugins/siem/scripts/convert_saved_search_to_rules.js rename to x-pack/plugins/siem/scripts/convert_saved_search_to_rules.js index 233d4dd7de721..65da56dd09bca 100644 --- a/x-pack/legacy/plugins/siem/scripts/convert_saved_search_to_rules.js +++ b/x-pack/plugins/siem/scripts/convert_saved_search_to_rules.js @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -require('../../../../../src/setup_node_env'); +require('../../../../src/setup_node_env'); const fs = require('fs'); const path = require('path'); diff --git a/x-pack/legacy/plugins/siem/scripts/extract_tactics_techniques_mitre.js b/x-pack/plugins/siem/scripts/extract_tactics_techniques_mitre.js similarity index 95% rename from x-pack/legacy/plugins/siem/scripts/extract_tactics_techniques_mitre.js rename to x-pack/plugins/siem/scripts/extract_tactics_techniques_mitre.js index 6cb2a40049631..478463b1a8064 100644 --- a/x-pack/legacy/plugins/siem/scripts/extract_tactics_techniques_mitre.js +++ b/x-pack/plugins/siem/scripts/extract_tactics_techniques_mitre.js @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -require('../../../../../src/setup_node_env'); +require('../../../../src/setup_node_env'); const fs = require('fs'); // eslint-disable-next-line import/no-extraneous-dependencies @@ -12,7 +12,13 @@ const fetch = require('node-fetch'); const { camelCase } = require('lodash'); const { resolve } = require('path'); -const OUTPUT_DIRECTORY = resolve('public', 'pages', 'detection_engine', 'mitre'); +const OUTPUT_DIRECTORY = resolve( + '../../legacy/plugins/siem', + 'public', + 'pages', + 'detection_engine', + 'mitre' +); const MITRE_ENTREPRISE_ATTACK_URL = 'https://raw.githubusercontent.com/mitre/cti/master/enterprise-attack/enterprise-attack.json'; diff --git a/x-pack/legacy/plugins/siem/scripts/generate_types_from_graphql.js b/x-pack/plugins/siem/scripts/generate_types_from_graphql.js similarity index 91% rename from x-pack/legacy/plugins/siem/scripts/generate_types_from_graphql.js rename to x-pack/plugins/siem/scripts/generate_types_from_graphql.js index 36674fec73e09..bded8832aba5a 100644 --- a/x-pack/legacy/plugins/siem/scripts/generate_types_from_graphql.js +++ b/x-pack/plugins/siem/scripts/generate_types_from_graphql.js @@ -4,18 +4,25 @@ * you may not use this file except in compliance with the Elastic License. */ -require('../../../../../src/setup_node_env'); +require('../../../../src/setup_node_env'); const { join, resolve } = require('path'); // eslint-disable-next-line import/no-extraneous-dependencies, import/no-unresolved const { generate } = require('graphql-code-generator'); +const legacyPluginPath = '../../legacy/plugins/siem'; + const GRAPHQL_GLOBS = [ - join('public', 'containers', '**', '*.gql_query.ts{,x}'), + join(legacyPluginPath, 'public', 'containers', '**', '*.gql_query.ts{,x}'), join('common', 'graphql', '**', '*.gql_query.ts{,x}'), ]; -const OUTPUT_INTROSPECTION_PATH = resolve('public', 'graphql', 'introspection.json'); -const OUTPUT_CLIENT_TYPES_PATH = resolve('public', 'graphql', 'types.ts'); +const OUTPUT_INTROSPECTION_PATH = resolve( + legacyPluginPath, + 'public', + 'graphql', + 'introspection.json' +); +const OUTPUT_CLIENT_TYPES_PATH = resolve(legacyPluginPath, 'public', 'graphql', 'types.ts'); const OUTPUT_SERVER_TYPES_PATH = resolve('server', 'graphql', 'types.ts'); const SCHEMA_PATH = resolve(__dirname, 'combined_schema.ts'); diff --git a/x-pack/legacy/plugins/siem/scripts/loop_cypress_tests.js b/x-pack/plugins/siem/scripts/loop_cypress_tests.js similarity index 100% rename from x-pack/legacy/plugins/siem/scripts/loop_cypress_tests.js rename to x-pack/plugins/siem/scripts/loop_cypress_tests.js diff --git a/x-pack/legacy/plugins/siem/scripts/optimize_tsconfig.js b/x-pack/plugins/siem/scripts/optimize_tsconfig.js similarity index 100% rename from x-pack/legacy/plugins/siem/scripts/optimize_tsconfig.js rename to x-pack/plugins/siem/scripts/optimize_tsconfig.js diff --git a/x-pack/plugins/siem/scripts/optimize_tsconfig/README.md b/x-pack/plugins/siem/scripts/optimize_tsconfig/README.md new file mode 100644 index 0000000000000..2b402367c1db3 --- /dev/null +++ b/x-pack/plugins/siem/scripts/optimize_tsconfig/README.md @@ -0,0 +1,16 @@ +Hard forked from here: +x-pack/legacy/plugins/apm/scripts/optimize-tsconfig.js + + +#### Optimizing TypeScript + +Kibana and X-Pack are very large TypeScript projects, and it comes at a cost. Editor responsiveness is not great, and the CLI type check for X-Pack takes about a minute. To get faster feedback, we create a smaller SIEM TypeScript project that only type checks the SIEM project and the files it uses. This optimization consists of creating a `tsconfig.json` in SIEM that includes the Kibana/X-Pack typings, and editing the Kibana/X-Pack configurations to not include any files, or removing the configurations altogether. The script configures git to ignore any changes in these files, and has an undo script as well. + +To run the optimization: + +`$ node x-pack/plugins/siem/scripts/optimize_tsconfig` + +To undo the optimization: + +`$ node x-pack/plugins/siem/scripts/unoptimize_tsconfig` + diff --git a/x-pack/legacy/plugins/siem/scripts/optimize_tsconfig/optimize.js b/x-pack/plugins/siem/scripts/optimize_tsconfig/optimize.js similarity index 100% rename from x-pack/legacy/plugins/siem/scripts/optimize_tsconfig/optimize.js rename to x-pack/plugins/siem/scripts/optimize_tsconfig/optimize.js diff --git a/x-pack/legacy/plugins/siem/scripts/optimize_tsconfig/paths.js b/x-pack/plugins/siem/scripts/optimize_tsconfig/paths.js similarity index 90% rename from x-pack/legacy/plugins/siem/scripts/optimize_tsconfig/paths.js rename to x-pack/plugins/siem/scripts/optimize_tsconfig/paths.js index ca26203e17d2e..c75e16f74b932 100644 --- a/x-pack/legacy/plugins/siem/scripts/optimize_tsconfig/paths.js +++ b/x-pack/plugins/siem/scripts/optimize_tsconfig/paths.js @@ -5,7 +5,7 @@ */ const path = require('path'); -const xpackRoot = path.resolve(__dirname, '../../../../..'); +const xpackRoot = path.resolve(__dirname, '../../../..'); const kibanaRoot = path.resolve(xpackRoot, '..'); const tsconfigTpl = path.resolve(__dirname, './tsconfig.json'); diff --git a/x-pack/plugins/siem/scripts/optimize_tsconfig/tsconfig.json b/x-pack/plugins/siem/scripts/optimize_tsconfig/tsconfig.json new file mode 100644 index 0000000000000..42d26c4c27ed6 --- /dev/null +++ b/x-pack/plugins/siem/scripts/optimize_tsconfig/tsconfig.json @@ -0,0 +1,16 @@ +{ + "include": [ + "typings/**/*", + "plugins/siem/**/*", + "legacy/plugins/siem/**/*", + "plugins/apm/typings/numeral.d.ts", + "legacy/plugins/canvas/types/webpack.d.ts", + "plugins/triggers_actions_ui/**/*" + ], + "exclude": [ + "test/**/*", + "**/__fixtures__/**/*", + "plugins/siem/cypress/**/*", + "**/typespec_tests.ts" + ] +} diff --git a/x-pack/legacy/plugins/siem/scripts/optimize_tsconfig/unoptimize.js b/x-pack/plugins/siem/scripts/optimize_tsconfig/unoptimize.js similarity index 100% rename from x-pack/legacy/plugins/siem/scripts/optimize_tsconfig/unoptimize.js rename to x-pack/plugins/siem/scripts/optimize_tsconfig/unoptimize.js diff --git a/x-pack/legacy/plugins/siem/scripts/storybook.js b/x-pack/plugins/siem/scripts/storybook.js similarity index 100% rename from x-pack/legacy/plugins/siem/scripts/storybook.js rename to x-pack/plugins/siem/scripts/storybook.js diff --git a/x-pack/legacy/plugins/siem/scripts/unoptimize_tsconfig.js b/x-pack/plugins/siem/scripts/unoptimize_tsconfig.js similarity index 100% rename from x-pack/legacy/plugins/siem/scripts/unoptimize_tsconfig.js rename to x-pack/plugins/siem/scripts/unoptimize_tsconfig.js diff --git a/x-pack/legacy/plugins/siem/server/client/client.test.ts b/x-pack/plugins/siem/server/client/client.test.ts similarity index 79% rename from x-pack/legacy/plugins/siem/server/client/client.test.ts rename to x-pack/plugins/siem/server/client/client.test.ts index bfe7b97f43003..94ff2149b8c64 100644 --- a/x-pack/legacy/plugins/siem/server/client/client.test.ts +++ b/x-pack/plugins/siem/server/client/client.test.ts @@ -4,17 +4,17 @@ * you may not use this file except in compliance with the Elastic License. */ -import { SiemClient } from './client'; +import { SIGNALS_INDEX_KEY } from '../../common/constants'; import { createMockConfig } from '../lib/detection_engine/routes/__mocks__'; +import { SiemClient } from './client'; describe('SiemClient', () => { describe('#signalsIndex', () => { it('returns the index scoped to the specified spaceId', () => { - let mockConfig = createMockConfig(); - mockConfig = () => ({ - get: jest.fn(() => 'mockSignalsIndex'), - has: jest.fn(), - }); + const mockConfig = { + ...createMockConfig(), + [SIGNALS_INDEX_KEY]: 'mockSignalsIndex', + }; const spaceId = 'fooSpace'; const client = new SiemClient(spaceId, mockConfig); diff --git a/x-pack/plugins/siem/server/client/client.ts b/x-pack/plugins/siem/server/client/client.ts new file mode 100644 index 0000000000000..6cb0d4cfade77 --- /dev/null +++ b/x-pack/plugins/siem/server/client/client.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 { ConfigType } from '..'; + +export class SiemClient { + public readonly signalsIndex: string; + + constructor(private spaceId: string, private config: ConfigType) { + const configuredSignalsIndex = this.config.signalsIndex; + + this.signalsIndex = `${configuredSignalsIndex}-${this.spaceId}`; + } +} diff --git a/x-pack/legacy/plugins/siem/server/client/factory.test.ts b/x-pack/plugins/siem/server/client/factory.test.ts similarity index 82% rename from x-pack/legacy/plugins/siem/server/client/factory.test.ts rename to x-pack/plugins/siem/server/client/factory.test.ts index c166b6b838be2..f0cddc5f09747 100644 --- a/x-pack/legacy/plugins/siem/server/client/factory.test.ts +++ b/x-pack/plugins/siem/server/client/factory.test.ts @@ -4,7 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ -import { httpServerMock } from '../../../../../../src/core/server/mocks'; +import { httpServerMock } from '../../../../../src/core/server/mocks'; +import { createMockConfig } from '../lib/detection_engine/routes/__mocks__'; import { SiemClientFactory } from './factory'; import { SiemClient } from './client'; @@ -16,7 +17,7 @@ describe('SiemClientFactory', () => { it('constructs a client with the current spaceId', () => { const factory = new SiemClientFactory(); const mockRequest = httpServerMock.createKibanaRequest(); - factory.setup({ getSpaceId: () => 'mockSpace', config: jest.fn() }); + factory.setup({ getSpaceId: () => 'mockSpace', config: createMockConfig() }); factory.create(mockRequest); expect(mockClient).toHaveBeenCalledWith('mockSpace', expect.anything()); @@ -25,7 +26,7 @@ describe('SiemClientFactory', () => { it('constructs a client with the default spaceId if spaces are disabled', () => { const factory = new SiemClientFactory(); const mockRequest = httpServerMock.createKibanaRequest(); - factory.setup({ getSpaceId: undefined, config: jest.fn() }); + factory.setup({ getSpaceId: undefined, config: createMockConfig() }); factory.create(mockRequest); expect(mockClient).toHaveBeenCalledWith('default', expect.anything()); diff --git a/x-pack/plugins/siem/server/client/factory.ts b/x-pack/plugins/siem/server/client/factory.ts new file mode 100644 index 0000000000000..d3d6b84e5b090 --- /dev/null +++ b/x-pack/plugins/siem/server/client/factory.ts @@ -0,0 +1,35 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { KibanaRequest } from '../../../../../src/core/server'; +import { SiemClient } from './client'; +import { ConfigType } from '..'; + +interface SetupDependencies { + getSpaceId?: (request: KibanaRequest) => string | undefined; + config: ConfigType; +} + +export class SiemClientFactory { + private getSpaceId?: SetupDependencies['getSpaceId']; + private config?: SetupDependencies['config']; + + public setup({ getSpaceId, config }: SetupDependencies) { + this.getSpaceId = getSpaceId; + this.config = config; + } + + public create(request: KibanaRequest): SiemClient { + if (this.config == null) { + throw new Error( + 'Cannot create SiemClient as config is not present. Did you forget to call setup()?' + ); + } + + const spaceId = this.getSpaceId?.(request) ?? 'default'; + return new SiemClient(spaceId, this.config); + } +} diff --git a/x-pack/legacy/plugins/siem/server/client/index.ts b/x-pack/plugins/siem/server/client/index.ts similarity index 100% rename from x-pack/legacy/plugins/siem/server/client/index.ts rename to x-pack/plugins/siem/server/client/index.ts diff --git a/x-pack/plugins/siem/server/config.ts b/x-pack/plugins/siem/server/config.ts index 224043c0c6fe5..4b0e8d34ef1a0 100644 --- a/x-pack/plugins/siem/server/config.ts +++ b/x-pack/plugins/siem/server/config.ts @@ -7,13 +7,14 @@ import { Observable } from 'rxjs'; import { schema, TypeOf } from '@kbn/config-schema'; import { PluginInitializerContext } from '../../../../src/core/server'; -import { - SIGNALS_INDEX_KEY, - DEFAULT_SIGNALS_INDEX, -} from '../../../legacy/plugins/siem/common/constants'; +import { SIGNALS_INDEX_KEY, DEFAULT_SIGNALS_INDEX } from '../common/constants'; export const configSchema = schema.object({ enabled: schema.boolean({ defaultValue: true }), + maxRuleImportExportSize: schema.number({ defaultValue: 10000 }), + maxRuleImportPayloadBytes: schema.number({ defaultValue: 10485760 }), + maxTimelineImportExportSize: schema.number({ defaultValue: 10000 }), + maxTimelineImportPayloadBytes: schema.number({ defaultValue: 10485760 }), [SIGNALS_INDEX_KEY]: schema.string({ defaultValue: DEFAULT_SIGNALS_INDEX }), }); diff --git a/x-pack/legacy/plugins/siem/server/graphql/authentications/index.ts b/x-pack/plugins/siem/server/graphql/authentications/index.ts similarity index 100% rename from x-pack/legacy/plugins/siem/server/graphql/authentications/index.ts rename to x-pack/plugins/siem/server/graphql/authentications/index.ts diff --git a/x-pack/legacy/plugins/siem/server/graphql/authentications/resolvers.ts b/x-pack/plugins/siem/server/graphql/authentications/resolvers.ts similarity index 100% rename from x-pack/legacy/plugins/siem/server/graphql/authentications/resolvers.ts rename to x-pack/plugins/siem/server/graphql/authentications/resolvers.ts diff --git a/x-pack/legacy/plugins/siem/server/graphql/authentications/schema.gql.ts b/x-pack/plugins/siem/server/graphql/authentications/schema.gql.ts similarity index 100% rename from x-pack/legacy/plugins/siem/server/graphql/authentications/schema.gql.ts rename to x-pack/plugins/siem/server/graphql/authentications/schema.gql.ts diff --git a/x-pack/legacy/plugins/siem/server/graphql/ecs/index.ts b/x-pack/plugins/siem/server/graphql/ecs/index.ts similarity index 100% rename from x-pack/legacy/plugins/siem/server/graphql/ecs/index.ts rename to x-pack/plugins/siem/server/graphql/ecs/index.ts diff --git a/x-pack/legacy/plugins/siem/server/graphql/ecs/resolvers.ts b/x-pack/plugins/siem/server/graphql/ecs/resolvers.ts similarity index 100% rename from x-pack/legacy/plugins/siem/server/graphql/ecs/resolvers.ts rename to x-pack/plugins/siem/server/graphql/ecs/resolvers.ts diff --git a/x-pack/legacy/plugins/siem/server/graphql/ecs/schema.gql.ts b/x-pack/plugins/siem/server/graphql/ecs/schema.gql.ts similarity index 100% rename from x-pack/legacy/plugins/siem/server/graphql/ecs/schema.gql.ts rename to x-pack/plugins/siem/server/graphql/ecs/schema.gql.ts diff --git a/x-pack/legacy/plugins/siem/server/graphql/events/index.ts b/x-pack/plugins/siem/server/graphql/events/index.ts similarity index 100% rename from x-pack/legacy/plugins/siem/server/graphql/events/index.ts rename to x-pack/plugins/siem/server/graphql/events/index.ts diff --git a/x-pack/legacy/plugins/siem/server/graphql/events/resolvers.ts b/x-pack/plugins/siem/server/graphql/events/resolvers.ts similarity index 100% rename from x-pack/legacy/plugins/siem/server/graphql/events/resolvers.ts rename to x-pack/plugins/siem/server/graphql/events/resolvers.ts diff --git a/x-pack/legacy/plugins/siem/server/graphql/events/schema.gql.ts b/x-pack/plugins/siem/server/graphql/events/schema.gql.ts similarity index 100% rename from x-pack/legacy/plugins/siem/server/graphql/events/schema.gql.ts rename to x-pack/plugins/siem/server/graphql/events/schema.gql.ts diff --git a/x-pack/legacy/plugins/siem/server/graphql/hosts/index.ts b/x-pack/plugins/siem/server/graphql/hosts/index.ts similarity index 100% rename from x-pack/legacy/plugins/siem/server/graphql/hosts/index.ts rename to x-pack/plugins/siem/server/graphql/hosts/index.ts diff --git a/x-pack/legacy/plugins/siem/server/graphql/hosts/resolvers.ts b/x-pack/plugins/siem/server/graphql/hosts/resolvers.ts similarity index 100% rename from x-pack/legacy/plugins/siem/server/graphql/hosts/resolvers.ts rename to x-pack/plugins/siem/server/graphql/hosts/resolvers.ts diff --git a/x-pack/legacy/plugins/siem/server/graphql/hosts/schema.gql.ts b/x-pack/plugins/siem/server/graphql/hosts/schema.gql.ts similarity index 100% rename from x-pack/legacy/plugins/siem/server/graphql/hosts/schema.gql.ts rename to x-pack/plugins/siem/server/graphql/hosts/schema.gql.ts diff --git a/x-pack/legacy/plugins/siem/server/graphql/index.ts b/x-pack/plugins/siem/server/graphql/index.ts similarity index 100% rename from x-pack/legacy/plugins/siem/server/graphql/index.ts rename to x-pack/plugins/siem/server/graphql/index.ts diff --git a/x-pack/legacy/plugins/siem/server/graphql/ip_details/index.ts b/x-pack/plugins/siem/server/graphql/ip_details/index.ts similarity index 100% rename from x-pack/legacy/plugins/siem/server/graphql/ip_details/index.ts rename to x-pack/plugins/siem/server/graphql/ip_details/index.ts diff --git a/x-pack/legacy/plugins/siem/server/graphql/ip_details/resolvers.ts b/x-pack/plugins/siem/server/graphql/ip_details/resolvers.ts similarity index 100% rename from x-pack/legacy/plugins/siem/server/graphql/ip_details/resolvers.ts rename to x-pack/plugins/siem/server/graphql/ip_details/resolvers.ts diff --git a/x-pack/legacy/plugins/siem/server/graphql/ip_details/schema.gql.ts b/x-pack/plugins/siem/server/graphql/ip_details/schema.gql.ts similarity index 100% rename from x-pack/legacy/plugins/siem/server/graphql/ip_details/schema.gql.ts rename to x-pack/plugins/siem/server/graphql/ip_details/schema.gql.ts diff --git a/x-pack/legacy/plugins/siem/server/graphql/kpi_hosts/index.ts b/x-pack/plugins/siem/server/graphql/kpi_hosts/index.ts similarity index 100% rename from x-pack/legacy/plugins/siem/server/graphql/kpi_hosts/index.ts rename to x-pack/plugins/siem/server/graphql/kpi_hosts/index.ts diff --git a/x-pack/legacy/plugins/siem/server/graphql/kpi_hosts/resolvers.ts b/x-pack/plugins/siem/server/graphql/kpi_hosts/resolvers.ts similarity index 100% rename from x-pack/legacy/plugins/siem/server/graphql/kpi_hosts/resolvers.ts rename to x-pack/plugins/siem/server/graphql/kpi_hosts/resolvers.ts diff --git a/x-pack/legacy/plugins/siem/server/graphql/kpi_hosts/schema.gql.ts b/x-pack/plugins/siem/server/graphql/kpi_hosts/schema.gql.ts similarity index 100% rename from x-pack/legacy/plugins/siem/server/graphql/kpi_hosts/schema.gql.ts rename to x-pack/plugins/siem/server/graphql/kpi_hosts/schema.gql.ts diff --git a/x-pack/legacy/plugins/siem/server/graphql/kpi_network/index.ts b/x-pack/plugins/siem/server/graphql/kpi_network/index.ts similarity index 100% rename from x-pack/legacy/plugins/siem/server/graphql/kpi_network/index.ts rename to x-pack/plugins/siem/server/graphql/kpi_network/index.ts diff --git a/x-pack/legacy/plugins/siem/server/graphql/kpi_network/resolvers.ts b/x-pack/plugins/siem/server/graphql/kpi_network/resolvers.ts similarity index 100% rename from x-pack/legacy/plugins/siem/server/graphql/kpi_network/resolvers.ts rename to x-pack/plugins/siem/server/graphql/kpi_network/resolvers.ts diff --git a/x-pack/legacy/plugins/siem/server/graphql/kpi_network/schema.gql.ts b/x-pack/plugins/siem/server/graphql/kpi_network/schema.gql.ts similarity index 100% rename from x-pack/legacy/plugins/siem/server/graphql/kpi_network/schema.gql.ts rename to x-pack/plugins/siem/server/graphql/kpi_network/schema.gql.ts diff --git a/x-pack/legacy/plugins/siem/server/graphql/matrix_histogram/index.ts b/x-pack/plugins/siem/server/graphql/matrix_histogram/index.ts similarity index 100% rename from x-pack/legacy/plugins/siem/server/graphql/matrix_histogram/index.ts rename to x-pack/plugins/siem/server/graphql/matrix_histogram/index.ts diff --git a/x-pack/legacy/plugins/siem/server/graphql/matrix_histogram/resolvers.ts b/x-pack/plugins/siem/server/graphql/matrix_histogram/resolvers.ts similarity index 100% rename from x-pack/legacy/plugins/siem/server/graphql/matrix_histogram/resolvers.ts rename to x-pack/plugins/siem/server/graphql/matrix_histogram/resolvers.ts diff --git a/x-pack/legacy/plugins/siem/server/graphql/matrix_histogram/schema.gql.ts b/x-pack/plugins/siem/server/graphql/matrix_histogram/schema.gql.ts similarity index 100% rename from x-pack/legacy/plugins/siem/server/graphql/matrix_histogram/schema.gql.ts rename to x-pack/plugins/siem/server/graphql/matrix_histogram/schema.gql.ts diff --git a/x-pack/legacy/plugins/siem/server/graphql/network/index.ts b/x-pack/plugins/siem/server/graphql/network/index.ts similarity index 100% rename from x-pack/legacy/plugins/siem/server/graphql/network/index.ts rename to x-pack/plugins/siem/server/graphql/network/index.ts diff --git a/x-pack/legacy/plugins/siem/server/graphql/network/resolvers.ts b/x-pack/plugins/siem/server/graphql/network/resolvers.ts similarity index 100% rename from x-pack/legacy/plugins/siem/server/graphql/network/resolvers.ts rename to x-pack/plugins/siem/server/graphql/network/resolvers.ts diff --git a/x-pack/legacy/plugins/siem/server/graphql/network/schema.gql.ts b/x-pack/plugins/siem/server/graphql/network/schema.gql.ts similarity index 100% rename from x-pack/legacy/plugins/siem/server/graphql/network/schema.gql.ts rename to x-pack/plugins/siem/server/graphql/network/schema.gql.ts diff --git a/x-pack/legacy/plugins/siem/server/graphql/note/index.ts b/x-pack/plugins/siem/server/graphql/note/index.ts similarity index 100% rename from x-pack/legacy/plugins/siem/server/graphql/note/index.ts rename to x-pack/plugins/siem/server/graphql/note/index.ts diff --git a/x-pack/legacy/plugins/siem/server/graphql/note/resolvers.ts b/x-pack/plugins/siem/server/graphql/note/resolvers.ts similarity index 100% rename from x-pack/legacy/plugins/siem/server/graphql/note/resolvers.ts rename to x-pack/plugins/siem/server/graphql/note/resolvers.ts diff --git a/x-pack/legacy/plugins/siem/server/graphql/note/schema.gql.ts b/x-pack/plugins/siem/server/graphql/note/schema.gql.ts similarity index 100% rename from x-pack/legacy/plugins/siem/server/graphql/note/schema.gql.ts rename to x-pack/plugins/siem/server/graphql/note/schema.gql.ts diff --git a/x-pack/legacy/plugins/siem/server/graphql/overview/index.ts b/x-pack/plugins/siem/server/graphql/overview/index.ts similarity index 100% rename from x-pack/legacy/plugins/siem/server/graphql/overview/index.ts rename to x-pack/plugins/siem/server/graphql/overview/index.ts diff --git a/x-pack/legacy/plugins/siem/server/graphql/overview/resolvers.ts b/x-pack/plugins/siem/server/graphql/overview/resolvers.ts similarity index 100% rename from x-pack/legacy/plugins/siem/server/graphql/overview/resolvers.ts rename to x-pack/plugins/siem/server/graphql/overview/resolvers.ts diff --git a/x-pack/legacy/plugins/siem/server/graphql/overview/schema.gql.ts b/x-pack/plugins/siem/server/graphql/overview/schema.gql.ts similarity index 100% rename from x-pack/legacy/plugins/siem/server/graphql/overview/schema.gql.ts rename to x-pack/plugins/siem/server/graphql/overview/schema.gql.ts diff --git a/x-pack/legacy/plugins/siem/server/graphql/pinned_event/index.ts b/x-pack/plugins/siem/server/graphql/pinned_event/index.ts similarity index 100% rename from x-pack/legacy/plugins/siem/server/graphql/pinned_event/index.ts rename to x-pack/plugins/siem/server/graphql/pinned_event/index.ts diff --git a/x-pack/legacy/plugins/siem/server/graphql/pinned_event/resolvers.ts b/x-pack/plugins/siem/server/graphql/pinned_event/resolvers.ts similarity index 100% rename from x-pack/legacy/plugins/siem/server/graphql/pinned_event/resolvers.ts rename to x-pack/plugins/siem/server/graphql/pinned_event/resolvers.ts diff --git a/x-pack/legacy/plugins/siem/server/graphql/pinned_event/schema.gql.ts b/x-pack/plugins/siem/server/graphql/pinned_event/schema.gql.ts similarity index 100% rename from x-pack/legacy/plugins/siem/server/graphql/pinned_event/schema.gql.ts rename to x-pack/plugins/siem/server/graphql/pinned_event/schema.gql.ts diff --git a/x-pack/legacy/plugins/siem/server/graphql/scalar_date/index.ts b/x-pack/plugins/siem/server/graphql/scalar_date/index.ts similarity index 100% rename from x-pack/legacy/plugins/siem/server/graphql/scalar_date/index.ts rename to x-pack/plugins/siem/server/graphql/scalar_date/index.ts diff --git a/x-pack/legacy/plugins/siem/server/graphql/scalar_date/resolvers.test.ts b/x-pack/plugins/siem/server/graphql/scalar_date/resolvers.test.ts similarity index 100% rename from x-pack/legacy/plugins/siem/server/graphql/scalar_date/resolvers.test.ts rename to x-pack/plugins/siem/server/graphql/scalar_date/resolvers.test.ts diff --git a/x-pack/legacy/plugins/siem/server/graphql/scalar_date/resolvers.ts b/x-pack/plugins/siem/server/graphql/scalar_date/resolvers.ts similarity index 100% rename from x-pack/legacy/plugins/siem/server/graphql/scalar_date/resolvers.ts rename to x-pack/plugins/siem/server/graphql/scalar_date/resolvers.ts diff --git a/x-pack/legacy/plugins/siem/server/graphql/scalar_date/schema.gql.ts b/x-pack/plugins/siem/server/graphql/scalar_date/schema.gql.ts similarity index 100% rename from x-pack/legacy/plugins/siem/server/graphql/scalar_date/schema.gql.ts rename to x-pack/plugins/siem/server/graphql/scalar_date/schema.gql.ts diff --git a/x-pack/legacy/plugins/siem/server/graphql/scalar_to_any/index.ts b/x-pack/plugins/siem/server/graphql/scalar_to_any/index.ts similarity index 100% rename from x-pack/legacy/plugins/siem/server/graphql/scalar_to_any/index.ts rename to x-pack/plugins/siem/server/graphql/scalar_to_any/index.ts diff --git a/x-pack/legacy/plugins/siem/server/graphql/scalar_to_any/resolvers.ts b/x-pack/plugins/siem/server/graphql/scalar_to_any/resolvers.ts similarity index 100% rename from x-pack/legacy/plugins/siem/server/graphql/scalar_to_any/resolvers.ts rename to x-pack/plugins/siem/server/graphql/scalar_to_any/resolvers.ts diff --git a/x-pack/legacy/plugins/siem/server/graphql/scalar_to_any/schema.gql.ts b/x-pack/plugins/siem/server/graphql/scalar_to_any/schema.gql.ts similarity index 100% rename from x-pack/legacy/plugins/siem/server/graphql/scalar_to_any/schema.gql.ts rename to x-pack/plugins/siem/server/graphql/scalar_to_any/schema.gql.ts diff --git a/x-pack/legacy/plugins/siem/server/graphql/scalar_to_boolean_array/index.ts b/x-pack/plugins/siem/server/graphql/scalar_to_boolean_array/index.ts similarity index 100% rename from x-pack/legacy/plugins/siem/server/graphql/scalar_to_boolean_array/index.ts rename to x-pack/plugins/siem/server/graphql/scalar_to_boolean_array/index.ts diff --git a/x-pack/legacy/plugins/siem/server/graphql/scalar_to_boolean_array/resolvers.test.ts b/x-pack/plugins/siem/server/graphql/scalar_to_boolean_array/resolvers.test.ts similarity index 100% rename from x-pack/legacy/plugins/siem/server/graphql/scalar_to_boolean_array/resolvers.test.ts rename to x-pack/plugins/siem/server/graphql/scalar_to_boolean_array/resolvers.test.ts diff --git a/x-pack/legacy/plugins/siem/server/graphql/scalar_to_boolean_array/resolvers.ts b/x-pack/plugins/siem/server/graphql/scalar_to_boolean_array/resolvers.ts similarity index 100% rename from x-pack/legacy/plugins/siem/server/graphql/scalar_to_boolean_array/resolvers.ts rename to x-pack/plugins/siem/server/graphql/scalar_to_boolean_array/resolvers.ts diff --git a/x-pack/legacy/plugins/siem/server/graphql/scalar_to_boolean_array/schema.gql.ts b/x-pack/plugins/siem/server/graphql/scalar_to_boolean_array/schema.gql.ts similarity index 100% rename from x-pack/legacy/plugins/siem/server/graphql/scalar_to_boolean_array/schema.gql.ts rename to x-pack/plugins/siem/server/graphql/scalar_to_boolean_array/schema.gql.ts diff --git a/x-pack/legacy/plugins/siem/server/graphql/scalar_to_date_array/index.ts b/x-pack/plugins/siem/server/graphql/scalar_to_date_array/index.ts similarity index 100% rename from x-pack/legacy/plugins/siem/server/graphql/scalar_to_date_array/index.ts rename to x-pack/plugins/siem/server/graphql/scalar_to_date_array/index.ts diff --git a/x-pack/legacy/plugins/siem/server/graphql/scalar_to_date_array/resolvers.test.ts b/x-pack/plugins/siem/server/graphql/scalar_to_date_array/resolvers.test.ts similarity index 100% rename from x-pack/legacy/plugins/siem/server/graphql/scalar_to_date_array/resolvers.test.ts rename to x-pack/plugins/siem/server/graphql/scalar_to_date_array/resolvers.test.ts diff --git a/x-pack/legacy/plugins/siem/server/graphql/scalar_to_date_array/resolvers.ts b/x-pack/plugins/siem/server/graphql/scalar_to_date_array/resolvers.ts similarity index 100% rename from x-pack/legacy/plugins/siem/server/graphql/scalar_to_date_array/resolvers.ts rename to x-pack/plugins/siem/server/graphql/scalar_to_date_array/resolvers.ts diff --git a/x-pack/legacy/plugins/siem/server/graphql/scalar_to_date_array/schema.gql.ts b/x-pack/plugins/siem/server/graphql/scalar_to_date_array/schema.gql.ts similarity index 100% rename from x-pack/legacy/plugins/siem/server/graphql/scalar_to_date_array/schema.gql.ts rename to x-pack/plugins/siem/server/graphql/scalar_to_date_array/schema.gql.ts diff --git a/x-pack/legacy/plugins/siem/server/graphql/scalar_to_number_array/index.ts b/x-pack/plugins/siem/server/graphql/scalar_to_number_array/index.ts similarity index 100% rename from x-pack/legacy/plugins/siem/server/graphql/scalar_to_number_array/index.ts rename to x-pack/plugins/siem/server/graphql/scalar_to_number_array/index.ts diff --git a/x-pack/legacy/plugins/siem/server/graphql/scalar_to_number_array/resolvers.test.ts b/x-pack/plugins/siem/server/graphql/scalar_to_number_array/resolvers.test.ts similarity index 100% rename from x-pack/legacy/plugins/siem/server/graphql/scalar_to_number_array/resolvers.test.ts rename to x-pack/plugins/siem/server/graphql/scalar_to_number_array/resolvers.test.ts diff --git a/x-pack/legacy/plugins/siem/server/graphql/scalar_to_number_array/resolvers.ts b/x-pack/plugins/siem/server/graphql/scalar_to_number_array/resolvers.ts similarity index 100% rename from x-pack/legacy/plugins/siem/server/graphql/scalar_to_number_array/resolvers.ts rename to x-pack/plugins/siem/server/graphql/scalar_to_number_array/resolvers.ts diff --git a/x-pack/legacy/plugins/siem/server/graphql/scalar_to_number_array/schema.gql.ts b/x-pack/plugins/siem/server/graphql/scalar_to_number_array/schema.gql.ts similarity index 100% rename from x-pack/legacy/plugins/siem/server/graphql/scalar_to_number_array/schema.gql.ts rename to x-pack/plugins/siem/server/graphql/scalar_to_number_array/schema.gql.ts diff --git a/x-pack/legacy/plugins/siem/server/graphql/source_status/index.ts b/x-pack/plugins/siem/server/graphql/source_status/index.ts similarity index 100% rename from x-pack/legacy/plugins/siem/server/graphql/source_status/index.ts rename to x-pack/plugins/siem/server/graphql/source_status/index.ts diff --git a/x-pack/legacy/plugins/siem/server/graphql/source_status/resolvers.ts b/x-pack/plugins/siem/server/graphql/source_status/resolvers.ts similarity index 100% rename from x-pack/legacy/plugins/siem/server/graphql/source_status/resolvers.ts rename to x-pack/plugins/siem/server/graphql/source_status/resolvers.ts diff --git a/x-pack/legacy/plugins/siem/server/graphql/source_status/schema.gql.ts b/x-pack/plugins/siem/server/graphql/source_status/schema.gql.ts similarity index 100% rename from x-pack/legacy/plugins/siem/server/graphql/source_status/schema.gql.ts rename to x-pack/plugins/siem/server/graphql/source_status/schema.gql.ts diff --git a/x-pack/legacy/plugins/siem/server/graphql/sources/index.ts b/x-pack/plugins/siem/server/graphql/sources/index.ts similarity index 100% rename from x-pack/legacy/plugins/siem/server/graphql/sources/index.ts rename to x-pack/plugins/siem/server/graphql/sources/index.ts diff --git a/x-pack/legacy/plugins/siem/server/graphql/sources/resolvers.ts b/x-pack/plugins/siem/server/graphql/sources/resolvers.ts similarity index 100% rename from x-pack/legacy/plugins/siem/server/graphql/sources/resolvers.ts rename to x-pack/plugins/siem/server/graphql/sources/resolvers.ts diff --git a/x-pack/legacy/plugins/siem/server/graphql/sources/schema.gql.ts b/x-pack/plugins/siem/server/graphql/sources/schema.gql.ts similarity index 100% rename from x-pack/legacy/plugins/siem/server/graphql/sources/schema.gql.ts rename to x-pack/plugins/siem/server/graphql/sources/schema.gql.ts diff --git a/x-pack/legacy/plugins/siem/server/graphql/timeline/index.ts b/x-pack/plugins/siem/server/graphql/timeline/index.ts similarity index 100% rename from x-pack/legacy/plugins/siem/server/graphql/timeline/index.ts rename to x-pack/plugins/siem/server/graphql/timeline/index.ts diff --git a/x-pack/legacy/plugins/siem/server/graphql/timeline/resolvers.ts b/x-pack/plugins/siem/server/graphql/timeline/resolvers.ts similarity index 100% rename from x-pack/legacy/plugins/siem/server/graphql/timeline/resolvers.ts rename to x-pack/plugins/siem/server/graphql/timeline/resolvers.ts diff --git a/x-pack/legacy/plugins/siem/server/graphql/timeline/schema.gql.ts b/x-pack/plugins/siem/server/graphql/timeline/schema.gql.ts similarity index 100% rename from x-pack/legacy/plugins/siem/server/graphql/timeline/schema.gql.ts rename to x-pack/plugins/siem/server/graphql/timeline/schema.gql.ts diff --git a/x-pack/legacy/plugins/siem/server/graphql/tls/index.ts b/x-pack/plugins/siem/server/graphql/tls/index.ts similarity index 100% rename from x-pack/legacy/plugins/siem/server/graphql/tls/index.ts rename to x-pack/plugins/siem/server/graphql/tls/index.ts diff --git a/x-pack/legacy/plugins/siem/server/graphql/tls/resolvers.ts b/x-pack/plugins/siem/server/graphql/tls/resolvers.ts similarity index 100% rename from x-pack/legacy/plugins/siem/server/graphql/tls/resolvers.ts rename to x-pack/plugins/siem/server/graphql/tls/resolvers.ts diff --git a/x-pack/legacy/plugins/siem/server/graphql/tls/schema.gql.ts b/x-pack/plugins/siem/server/graphql/tls/schema.gql.ts similarity index 100% rename from x-pack/legacy/plugins/siem/server/graphql/tls/schema.gql.ts rename to x-pack/plugins/siem/server/graphql/tls/schema.gql.ts diff --git a/x-pack/legacy/plugins/siem/server/graphql/types.ts b/x-pack/plugins/siem/server/graphql/types.ts similarity index 100% rename from x-pack/legacy/plugins/siem/server/graphql/types.ts rename to x-pack/plugins/siem/server/graphql/types.ts diff --git a/x-pack/legacy/plugins/siem/server/graphql/uncommon_processes/index.ts b/x-pack/plugins/siem/server/graphql/uncommon_processes/index.ts similarity index 100% rename from x-pack/legacy/plugins/siem/server/graphql/uncommon_processes/index.ts rename to x-pack/plugins/siem/server/graphql/uncommon_processes/index.ts diff --git a/x-pack/legacy/plugins/siem/server/graphql/uncommon_processes/resolvers.ts b/x-pack/plugins/siem/server/graphql/uncommon_processes/resolvers.ts similarity index 100% rename from x-pack/legacy/plugins/siem/server/graphql/uncommon_processes/resolvers.ts rename to x-pack/plugins/siem/server/graphql/uncommon_processes/resolvers.ts diff --git a/x-pack/legacy/plugins/siem/server/graphql/uncommon_processes/schema.gql.ts b/x-pack/plugins/siem/server/graphql/uncommon_processes/schema.gql.ts similarity index 100% rename from x-pack/legacy/plugins/siem/server/graphql/uncommon_processes/schema.gql.ts rename to x-pack/plugins/siem/server/graphql/uncommon_processes/schema.gql.ts diff --git a/x-pack/legacy/plugins/siem/server/graphql/who_am_i/index.ts b/x-pack/plugins/siem/server/graphql/who_am_i/index.ts similarity index 100% rename from x-pack/legacy/plugins/siem/server/graphql/who_am_i/index.ts rename to x-pack/plugins/siem/server/graphql/who_am_i/index.ts diff --git a/x-pack/legacy/plugins/siem/server/graphql/who_am_i/resolvers.ts b/x-pack/plugins/siem/server/graphql/who_am_i/resolvers.ts similarity index 100% rename from x-pack/legacy/plugins/siem/server/graphql/who_am_i/resolvers.ts rename to x-pack/plugins/siem/server/graphql/who_am_i/resolvers.ts diff --git a/x-pack/legacy/plugins/siem/server/graphql/who_am_i/schema.gql.ts b/x-pack/plugins/siem/server/graphql/who_am_i/schema.gql.ts similarity index 100% rename from x-pack/legacy/plugins/siem/server/graphql/who_am_i/schema.gql.ts rename to x-pack/plugins/siem/server/graphql/who_am_i/schema.gql.ts diff --git a/x-pack/legacy/plugins/siem/server/init_server.ts b/x-pack/plugins/siem/server/init_server.ts similarity index 100% rename from x-pack/legacy/plugins/siem/server/init_server.ts rename to x-pack/plugins/siem/server/init_server.ts diff --git a/x-pack/legacy/plugins/siem/server/lib/authentications/elasticsearch_adapter.test.ts b/x-pack/plugins/siem/server/lib/authentications/elasticsearch_adapter.test.ts similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/authentications/elasticsearch_adapter.test.ts rename to x-pack/plugins/siem/server/lib/authentications/elasticsearch_adapter.test.ts diff --git a/x-pack/legacy/plugins/siem/server/lib/authentications/elasticsearch_adapter.ts b/x-pack/plugins/siem/server/lib/authentications/elasticsearch_adapter.ts similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/authentications/elasticsearch_adapter.ts rename to x-pack/plugins/siem/server/lib/authentications/elasticsearch_adapter.ts diff --git a/x-pack/legacy/plugins/siem/server/lib/authentications/index.ts b/x-pack/plugins/siem/server/lib/authentications/index.ts similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/authentications/index.ts rename to x-pack/plugins/siem/server/lib/authentications/index.ts diff --git a/x-pack/legacy/plugins/siem/server/lib/authentications/query.dsl.ts b/x-pack/plugins/siem/server/lib/authentications/query.dsl.ts similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/authentications/query.dsl.ts rename to x-pack/plugins/siem/server/lib/authentications/query.dsl.ts diff --git a/x-pack/legacy/plugins/siem/server/lib/authentications/types.ts b/x-pack/plugins/siem/server/lib/authentications/types.ts similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/authentications/types.ts rename to x-pack/plugins/siem/server/lib/authentications/types.ts diff --git a/x-pack/legacy/plugins/siem/server/lib/compose/kibana.ts b/x-pack/plugins/siem/server/lib/compose/kibana.ts similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/compose/kibana.ts rename to x-pack/plugins/siem/server/lib/compose/kibana.ts diff --git a/x-pack/legacy/plugins/siem/server/lib/configuration/adapter_types.ts b/x-pack/plugins/siem/server/lib/configuration/adapter_types.ts similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/configuration/adapter_types.ts rename to x-pack/plugins/siem/server/lib/configuration/adapter_types.ts diff --git a/x-pack/legacy/plugins/siem/server/lib/configuration/index.ts b/x-pack/plugins/siem/server/lib/configuration/index.ts similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/configuration/index.ts rename to x-pack/plugins/siem/server/lib/configuration/index.ts diff --git a/x-pack/legacy/plugins/siem/server/lib/configuration/inmemory_configuration_adapter.ts b/x-pack/plugins/siem/server/lib/configuration/inmemory_configuration_adapter.ts similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/configuration/inmemory_configuration_adapter.ts rename to x-pack/plugins/siem/server/lib/configuration/inmemory_configuration_adapter.ts diff --git a/x-pack/plugins/siem/server/lib/detection_engine/README.md b/x-pack/plugins/siem/server/lib/detection_engine/README.md new file mode 100644 index 0000000000000..610e82fd5f6ee --- /dev/null +++ b/x-pack/plugins/siem/server/lib/detection_engine/README.md @@ -0,0 +1,167 @@ +README.md for developers working on the backend detection engine on how to get started +using the CURL scripts in the scripts folder. + +The scripts rely on CURL and jq: + +- [CURL](https://curl.haxx.se) +- [jq](https://stedolan.github.io/jq/) + +Install curl and jq + +```sh +brew update +brew install curl +brew install jq +``` + +Open `$HOME/.zshrc` or `${HOME}.bashrc` depending on your SHELL output from `echo $SHELL` +and add these environment variables: + +```sh +export ELASTICSEARCH_USERNAME=${user} +export ELASTICSEARCH_PASSWORD=${password} +export ELASTICSEARCH_URL=https://${ip}:9200 +export KIBANA_URL=http://localhost:5601 +export TASK_MANAGER_INDEX=.kibana-task-manager-${your user id} +export KIBANA_INDEX=.kibana-${your user id} +``` + +source `$HOME/.zshrc` or `${HOME}.bashrc` to ensure variables are set: + +```sh +source ~/.zshrc +``` + +Open your `kibana.dev.yml` file and add these lines: + +```sh +xpack.siem.signalsIndex: .siem-signals-${your user id} +``` + +Restart Kibana and ensure that you are using `--no-base-path` as changing the base path is a feature but will +get in the way of the CURL scripts written as is. You should see alerting and actions starting up like so afterwards + +```sh +server log [22:05:22.277] [info][status][plugin:alerting@8.0.0] Status changed from uninitialized to green - Ready +server log [22:05:22.270] [info][status][plugin:actions@8.0.0] Status changed from uninitialized to green - Ready +``` + +Go to the scripts folder `cd kibana/x-pack/plugins/siem/server/lib/detection_engine/scripts` and run: + +```sh +./hard_reset.sh +./post_rule.sh +``` + +which will: + +- Delete any existing actions you have +- Delete any existing alerts you have +- Delete any existing alert tasks you have +- Delete any existing signal mapping, policies, and template, you might have previously had. +- Add the latest signal index and its mappings using your settings from `kibana.dev.yml` environment variable of `xpack.siem.signalsIndex`. +- Posts the sample rule from `./rules/queries/query_with_rule_id.json` +- The sample rule checks for root or admin every 5 minutes and reports that as a signal if it is a positive hit + +Now you can run + +```sh +./find_rules.sh +``` + +You should see the new rules created like so: + +```sh +{ + "page": 1, + "perPage": 20, + "total": 1, + "data": [ + { + "created_by": "elastic", + "description": "Detecting root and admin users", + "enabled": true, + "false_positives": [], + "from": "now-6m", + "id": "a556065c-0656-4ba1-ad64-a77ca9d2013b", + "immutable": false, + "index": [ + "auditbeat-*", + "filebeat-*", + "packetbeat-*", + "winlogbeat-*" + ], + "interval": "5m", + "rule_id": "rule-1", + "language": "kuery", + "output_index": ".siem-signals-some-name", + "max_signals": 100, + "risk_score": 1, + "name": "Detect Root/Admin Users", + "query": "user.name: root or user.name: admin", + "references": [ + "http://www.example.com", + "https://ww.example.com" + ], + "severity": "high", + "updated_by": "elastic", + "tags": [], + "to": "now", + "type": "query" + } + ] +} +``` + +Every 5 minutes if you get positive hits you will see messages on info like so: + +```sh +server log [09:54:59.013] [info][plugins][siem] Total signals found from signal rule "id: a556065c-0656-4ba1-ad64-a77ca9d2013b", "ruleId: rule-1": 10000 +``` + +Rules are [space aware](https://www.elastic.co/guide/en/kibana/master/xpack-spaces.html) and default +to the "default" (empty) URL space if you do not export the variable of `SPACE_URL`. Example, if you want to +post rules to `test-space` you set `SPACE_URL` to be: + +```sh +export SPACE_URL=/s/test-space +``` + +The `${SPACE_URL}` is in front of all the APIs to correctly create, modify, delete, and update +them from within the defined space. If this variable is not defined the default which is the url of an +empty string will be used. + +Add the `.siem-signals-${your user id}` to your advanced SIEM settings to see any signals +created which should update once every 5 minutes at this point. + +Also add the `.siem-signals-${your user id}` as a kibana index for Maps to be able to see the +signals + +Optionally you can add these debug statements to your `kibana.dev.yml` to see more information when running the detection +engine + +```sh +logging.verbose: true +logging.events: + { + log: ['siem', 'info', 'warning', 'error', 'fatal'], + request: ['info', 'warning', 'error', 'fatal'], + error: '*', + ops: __no-ops__, + } +``` + +See these two README.md's pages for more references on the alerting and actions API: +https://github.com/elastic/kibana/blob/master/x-pack/plugins/alerting/README.md +https://github.com/elastic/kibana/tree/master/x-pack/plugins/actions + +### Signals API + +To update the status of a signal or group of signals, the following scripts provide an example of how to +go about doing so. +`cd x-pack/plugins/siem/server/lib/detection_engine/scripts` +`./signals/put_signal_doc.sh` will post a sample signal doc into the signals index to play with +`./signals/set_status_with_id.sh closed` will update the status of the sample signal to closed +`./signals/set_status_with_id.sh open` will update the status of the sample signal to open +`./signals/set_status_with_query.sh closed` will update the status of the signals in the result of the query to closed. +`./signals/set_status_with_query.sh open` will update the status of the signals in the result of the query to open. diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/errors/bad_request_error.ts b/x-pack/plugins/siem/server/lib/detection_engine/errors/bad_request_error.ts similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/errors/bad_request_error.ts rename to x-pack/plugins/siem/server/lib/detection_engine/errors/bad_request_error.ts diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/feature_flags.test.ts b/x-pack/plugins/siem/server/lib/detection_engine/feature_flags.test.ts similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/feature_flags.test.ts rename to x-pack/plugins/siem/server/lib/detection_engine/feature_flags.test.ts diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/feature_flags.ts b/x-pack/plugins/siem/server/lib/detection_engine/feature_flags.ts similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/feature_flags.ts rename to x-pack/plugins/siem/server/lib/detection_engine/feature_flags.ts diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/index/create_bootstrap_index.ts b/x-pack/plugins/siem/server/lib/detection_engine/index/create_bootstrap_index.ts similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/index/create_bootstrap_index.ts rename to x-pack/plugins/siem/server/lib/detection_engine/index/create_bootstrap_index.ts diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/index/delete_all_index.ts b/x-pack/plugins/siem/server/lib/detection_engine/index/delete_all_index.ts similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/index/delete_all_index.ts rename to x-pack/plugins/siem/server/lib/detection_engine/index/delete_all_index.ts diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/index/delete_policy.ts b/x-pack/plugins/siem/server/lib/detection_engine/index/delete_policy.ts similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/index/delete_policy.ts rename to x-pack/plugins/siem/server/lib/detection_engine/index/delete_policy.ts diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/index/delete_template.ts b/x-pack/plugins/siem/server/lib/detection_engine/index/delete_template.ts similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/index/delete_template.ts rename to x-pack/plugins/siem/server/lib/detection_engine/index/delete_template.ts diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/index/get_index_exists.test.ts b/x-pack/plugins/siem/server/lib/detection_engine/index/get_index_exists.test.ts similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/index/get_index_exists.test.ts rename to x-pack/plugins/siem/server/lib/detection_engine/index/get_index_exists.test.ts diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/index/get_index_exists.ts b/x-pack/plugins/siem/server/lib/detection_engine/index/get_index_exists.ts similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/index/get_index_exists.ts rename to x-pack/plugins/siem/server/lib/detection_engine/index/get_index_exists.ts diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/index/get_policy_exists.ts b/x-pack/plugins/siem/server/lib/detection_engine/index/get_policy_exists.ts similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/index/get_policy_exists.ts rename to x-pack/plugins/siem/server/lib/detection_engine/index/get_policy_exists.ts diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/index/get_template_exists.ts b/x-pack/plugins/siem/server/lib/detection_engine/index/get_template_exists.ts similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/index/get_template_exists.ts rename to x-pack/plugins/siem/server/lib/detection_engine/index/get_template_exists.ts diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/index/read_index.ts b/x-pack/plugins/siem/server/lib/detection_engine/index/read_index.ts similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/index/read_index.ts rename to x-pack/plugins/siem/server/lib/detection_engine/index/read_index.ts diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/index/set_policy.ts b/x-pack/plugins/siem/server/lib/detection_engine/index/set_policy.ts similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/index/set_policy.ts rename to x-pack/plugins/siem/server/lib/detection_engine/index/set_policy.ts diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/index/set_template.ts b/x-pack/plugins/siem/server/lib/detection_engine/index/set_template.ts similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/index/set_template.ts rename to x-pack/plugins/siem/server/lib/detection_engine/index/set_template.ts diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/notifications/add_tags.test.ts b/x-pack/plugins/siem/server/lib/detection_engine/notifications/add_tags.test.ts similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/notifications/add_tags.test.ts rename to x-pack/plugins/siem/server/lib/detection_engine/notifications/add_tags.test.ts diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/notifications/add_tags.ts b/x-pack/plugins/siem/server/lib/detection_engine/notifications/add_tags.ts similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/notifications/add_tags.ts rename to x-pack/plugins/siem/server/lib/detection_engine/notifications/add_tags.ts diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/notifications/build_signals_query.test.ts b/x-pack/plugins/siem/server/lib/detection_engine/notifications/build_signals_query.test.ts similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/notifications/build_signals_query.test.ts rename to x-pack/plugins/siem/server/lib/detection_engine/notifications/build_signals_query.test.ts diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/notifications/build_signals_query.ts b/x-pack/plugins/siem/server/lib/detection_engine/notifications/build_signals_query.ts similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/notifications/build_signals_query.ts rename to x-pack/plugins/siem/server/lib/detection_engine/notifications/build_signals_query.ts diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/notifications/create_notifications.test.ts b/x-pack/plugins/siem/server/lib/detection_engine/notifications/create_notifications.test.ts similarity index 95% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/notifications/create_notifications.test.ts rename to x-pack/plugins/siem/server/lib/detection_engine/notifications/create_notifications.test.ts index 3878f5dae8889..e0414f842ceb3 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/notifications/create_notifications.test.ts +++ b/x-pack/plugins/siem/server/lib/detection_engine/notifications/create_notifications.test.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { alertsClientMock } from '../../../../../../../plugins/alerting/server/mocks'; +import { alertsClientMock } from '../../../../../alerting/server/mocks'; import { createNotifications } from './create_notifications'; describe('createNotifications', () => { diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/notifications/create_notifications.ts b/x-pack/plugins/siem/server/lib/detection_engine/notifications/create_notifications.ts similarity index 93% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/notifications/create_notifications.ts rename to x-pack/plugins/siem/server/lib/detection_engine/notifications/create_notifications.ts index ccd7576255d83..35a737177ad49 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/notifications/create_notifications.ts +++ b/x-pack/plugins/siem/server/lib/detection_engine/notifications/create_notifications.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { Alert } from '../../../../../../../plugins/alerting/common'; +import { Alert } from '../../../../../alerting/common'; import { APP_ID, NOTIFICATIONS_ID } from '../../../../common/constants'; import { CreateNotificationParams } from './types'; import { addTags } from './add_tags'; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/notifications/delete_notifications.test.ts b/x-pack/plugins/siem/server/lib/detection_engine/notifications/delete_notifications.test.ts similarity index 97% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/notifications/delete_notifications.test.ts rename to x-pack/plugins/siem/server/lib/detection_engine/notifications/delete_notifications.test.ts index 7e5c0eaf6286e..089822f486aeb 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/notifications/delete_notifications.test.ts +++ b/x-pack/plugins/siem/server/lib/detection_engine/notifications/delete_notifications.test.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { alertsClientMock } from '../../../../../../../plugins/alerting/server/mocks'; +import { alertsClientMock } from '../../../../../alerting/server/mocks'; import { deleteNotifications } from './delete_notifications'; import { readNotifications } from './read_notifications'; jest.mock('./read_notifications'); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/notifications/delete_notifications.ts b/x-pack/plugins/siem/server/lib/detection_engine/notifications/delete_notifications.ts similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/notifications/delete_notifications.ts rename to x-pack/plugins/siem/server/lib/detection_engine/notifications/delete_notifications.ts diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/notifications/find_notifications.test.ts b/x-pack/plugins/siem/server/lib/detection_engine/notifications/find_notifications.test.ts similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/notifications/find_notifications.test.ts rename to x-pack/plugins/siem/server/lib/detection_engine/notifications/find_notifications.test.ts diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/notifications/find_notifications.ts b/x-pack/plugins/siem/server/lib/detection_engine/notifications/find_notifications.ts similarity index 92% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/notifications/find_notifications.ts rename to x-pack/plugins/siem/server/lib/detection_engine/notifications/find_notifications.ts index fcdeda608fe4e..b47ea348bd4d6 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/notifications/find_notifications.ts +++ b/x-pack/plugins/siem/server/lib/detection_engine/notifications/find_notifications.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { FindResult } from '../../../../../../../plugins/alerting/server'; +import { FindResult } from '../../../../../alerting/server'; import { NOTIFICATIONS_ID } from '../../../../common/constants'; import { FindNotificationParams } from './types'; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/notifications/get_signals_count.ts b/x-pack/plugins/siem/server/lib/detection_engine/notifications/get_signals_count.ts similarity index 92% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/notifications/get_signals_count.ts rename to x-pack/plugins/siem/server/lib/detection_engine/notifications/get_signals_count.ts index 7ff6a4e5164bd..69f37da1e225b 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/notifications/get_signals_count.ts +++ b/x-pack/plugins/siem/server/lib/detection_engine/notifications/get_signals_count.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { AlertServices } from '../../../../../../../plugins/alerting/server'; +import { AlertServices } from '../../../../../alerting/server'; import { buildSignalsSearchQuery } from './build_signals_query'; interface GetSignalsCount { diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/notifications/read_notifications.test.ts b/x-pack/plugins/siem/server/lib/detection_engine/notifications/read_notifications.test.ts similarity index 98% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/notifications/read_notifications.test.ts rename to x-pack/plugins/siem/server/lib/detection_engine/notifications/read_notifications.test.ts index 834ad2460959c..961aac15c484d 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/notifications/read_notifications.test.ts +++ b/x-pack/plugins/siem/server/lib/detection_engine/notifications/read_notifications.test.ts @@ -5,7 +5,7 @@ */ import { readNotifications } from './read_notifications'; -import { alertsClientMock } from '../../../../../../../plugins/alerting/server/mocks'; +import { alertsClientMock } from '../../../../../alerting/server/mocks'; import { getNotificationResult, getFindNotificationsResultWithSingleHit, diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/notifications/read_notifications.ts b/x-pack/plugins/siem/server/lib/detection_engine/notifications/read_notifications.ts similarity index 94% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/notifications/read_notifications.ts rename to x-pack/plugins/siem/server/lib/detection_engine/notifications/read_notifications.ts index 87bdd6f3f40e1..c585c474556a1 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/notifications/read_notifications.ts +++ b/x-pack/plugins/siem/server/lib/detection_engine/notifications/read_notifications.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { SanitizedAlert } from '../../../../../../../plugins/alerting/common'; +import { SanitizedAlert } from '../../../../../alerting/common'; import { ReadNotificationParams, isAlertType } from './types'; import { findNotifications } from './find_notifications'; import { INTERNAL_RULE_ALERT_ID_KEY } from '../../../../common/constants'; diff --git a/x-pack/plugins/siem/server/lib/detection_engine/notifications/rules_notification_alert_type.test.ts b/x-pack/plugins/siem/server/lib/detection_engine/notifications/rules_notification_alert_type.test.ts new file mode 100644 index 0000000000000..6244a4cc64e68 --- /dev/null +++ b/x-pack/plugins/siem/server/lib/detection_engine/notifications/rules_notification_alert_type.test.ts @@ -0,0 +1,129 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { loggerMock } from 'src/core/server/logging/logger.mock'; +import { getResult } from '../routes/__mocks__/request_responses'; +import { rulesNotificationAlertType } from './rules_notification_alert_type'; +import { buildSignalsSearchQuery } from './build_signals_query'; +import { alertsMock, AlertServicesMock } from '../../../../../../plugins/alerting/server/mocks'; +import { NotificationExecutorOptions } from './types'; +jest.mock('./build_signals_query'); + +describe('rules_notification_alert_type', () => { + let payload: NotificationExecutorOptions; + let alert: ReturnType; + let logger: ReturnType; + let alertServices: AlertServicesMock; + + beforeEach(() => { + alertServices = alertsMock.createAlertServices(); + logger = loggerMock.create(); + + payload = { + alertId: '1111', + services: alertServices, + params: { ruleAlertId: '2222' }, + state: {}, + spaceId: '', + name: 'name', + tags: [], + startedAt: new Date('2019-12-14T16:40:33.400Z'), + previousStartedAt: new Date('2019-12-13T16:40:33.400Z'), + createdBy: 'elastic', + updatedBy: 'elastic', + }; + + alert = rulesNotificationAlertType({ + logger, + }); + }); + + describe('executor', () => { + it('throws an error if rule alert was not found', async () => { + alertServices.savedObjectsClient.get.mockResolvedValue({ + id: 'id', + attributes: {}, + type: 'type', + references: [], + }); + await alert.executor(payload); + expect(logger.error).toHaveBeenCalledWith( + `Saved object for alert ${payload.params.ruleAlertId} was not found` + ); + }); + + it('should call buildSignalsSearchQuery with proper params', async () => { + const ruleAlert = getResult(); + alertServices.savedObjectsClient.get.mockResolvedValue({ + id: 'id', + type: 'type', + references: [], + attributes: ruleAlert, + }); + alertServices.callCluster.mockResolvedValue({ + count: 0, + }); + + await alert.executor(payload); + + expect(buildSignalsSearchQuery).toHaveBeenCalledWith( + expect.objectContaining({ + from: '1576255233400', + index: '.siem-signals', + ruleId: 'rule-1', + to: '1576341633400', + }) + ); + }); + + it('should not call alertInstanceFactory if signalsCount was 0', async () => { + const ruleAlert = getResult(); + alertServices.savedObjectsClient.get.mockResolvedValue({ + id: 'id', + type: 'type', + references: [], + attributes: ruleAlert, + }); + alertServices.callCluster.mockResolvedValue({ + count: 0, + }); + + await alert.executor(payload); + + expect(alertServices.alertInstanceFactory).not.toHaveBeenCalled(); + }); + + it('should call scheduleActions if signalsCount was greater than 0', async () => { + const ruleAlert = getResult(); + alertServices.savedObjectsClient.get.mockResolvedValue({ + id: 'id', + type: 'type', + references: [], + attributes: ruleAlert, + }); + alertServices.callCluster.mockResolvedValue({ + count: 10, + }); + + await alert.executor(payload); + + expect(alertServices.alertInstanceFactory).toHaveBeenCalled(); + + const [{ value: alertInstanceMock }] = alertServices.alertInstanceFactory.mock.results; + expect(alertInstanceMock.replaceState).toHaveBeenCalledWith( + expect.objectContaining({ signals_count: 10 }) + ); + expect(alertInstanceMock.scheduleActions).toHaveBeenCalledWith( + 'default', + expect.objectContaining({ + rule: expect.objectContaining({ + name: ruleAlert.name, + }), + }) + ); + }); + }); +}); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/notifications/rules_notification_alert_type.ts b/x-pack/plugins/siem/server/lib/detection_engine/notifications/rules_notification_alert_type.ts similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/notifications/rules_notification_alert_type.ts rename to x-pack/plugins/siem/server/lib/detection_engine/notifications/rules_notification_alert_type.ts diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/notifications/schedule_notification_actions.ts b/x-pack/plugins/siem/server/lib/detection_engine/notifications/schedule_notification_actions.ts similarity index 92% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/notifications/schedule_notification_actions.ts rename to x-pack/plugins/siem/server/lib/detection_engine/notifications/schedule_notification_actions.ts index 9f145af79ca90..a0bd5e092c6ea 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/notifications/schedule_notification_actions.ts +++ b/x-pack/plugins/siem/server/lib/detection_engine/notifications/schedule_notification_actions.ts @@ -5,7 +5,7 @@ */ import { mapKeys, snakeCase } from 'lodash/fp'; -import { AlertInstance } from '../../../../../../../plugins/alerting/server'; +import { AlertInstance } from '../../../../../alerting/server'; import { RuleTypeParams } from '../types'; export type NotificationRuleTypeParams = RuleTypeParams & { diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/notifications/types.test.ts b/x-pack/plugins/siem/server/lib/detection_engine/notifications/types.test.ts similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/notifications/types.test.ts rename to x-pack/plugins/siem/server/lib/detection_engine/notifications/types.test.ts diff --git a/x-pack/plugins/siem/server/lib/detection_engine/notifications/types.ts b/x-pack/plugins/siem/server/lib/detection_engine/notifications/types.ts new file mode 100644 index 0000000000000..d740b79cb3b94 --- /dev/null +++ b/x-pack/plugins/siem/server/lib/detection_engine/notifications/types.ts @@ -0,0 +1,106 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { + AlertsClient, + PartialAlert, + AlertType, + State, + AlertExecutorOptions, +} from '../../../../../alerting/server'; +import { Alert } from '../../../../../alerting/common'; +import { NOTIFICATIONS_ID } from '../../../../common/constants'; +import { RuleAlertAction } from '../../../../common/detection_engine/types'; + +export interface RuleNotificationAlertType extends Alert { + params: { + ruleAlertId: string; + }; +} + +export interface FindNotificationParams { + alertsClient: AlertsClient; + perPage?: number; + page?: number; + sortField?: string; + filter?: string; + fields?: string[]; + sortOrder?: 'asc' | 'desc'; +} + +export interface FindNotificationsRequestParams { + per_page: number; + page: number; + search?: string; + sort_field?: string; + filter?: string; + fields?: string[]; + sort_order?: 'asc' | 'desc'; +} + +export interface Clients { + alertsClient: AlertsClient; +} + +export type UpdateNotificationParams = Omit< + NotificationAlertParams, + 'interval' | 'actions' | 'tags' +> & { + actions: RuleAlertAction[]; + interval: string | null | undefined; + ruleAlertId: string; +} & Clients; + +export type DeleteNotificationParams = Clients & { + id?: string; + ruleAlertId?: string; +}; + +export interface NotificationAlertParams { + actions: RuleAlertAction[]; + enabled: boolean; + ruleAlertId: string; + interval: string; + name: string; +} + +export type CreateNotificationParams = NotificationAlertParams & Clients; + +export interface ReadNotificationParams { + alertsClient: AlertsClient; + id?: string | null; + ruleAlertId?: string | null; +} + +export const isAlertTypes = ( + partialAlert: PartialAlert[] +): partialAlert is RuleNotificationAlertType[] => { + return partialAlert.every(rule => isAlertType(rule)); +}; + +export const isAlertType = ( + partialAlert: PartialAlert +): partialAlert is RuleNotificationAlertType => { + return partialAlert.alertTypeId === NOTIFICATIONS_ID; +}; + +export type NotificationExecutorOptions = Omit & { + params: { + ruleAlertId: string; + }; +}; + +// This returns true because by default a NotificationAlertTypeDefinition is an AlertType +// since we are only increasing the strictness of params. +export const isNotificationAlertExecutor = ( + obj: NotificationAlertTypeDefinition +): obj is AlertType => { + return true; +}; + +export type NotificationAlertTypeDefinition = Omit & { + executor: ({ services, params, state }: NotificationExecutorOptions) => Promise; +}; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/notifications/update_notifications.test.ts b/x-pack/plugins/siem/server/lib/detection_engine/notifications/update_notifications.test.ts similarity index 97% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/notifications/update_notifications.test.ts rename to x-pack/plugins/siem/server/lib/detection_engine/notifications/update_notifications.test.ts index e1f7526438c31..b9dc42b96696d 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/notifications/update_notifications.test.ts +++ b/x-pack/plugins/siem/server/lib/detection_engine/notifications/update_notifications.test.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { alertsClientMock } from '../../../../../../../plugins/alerting/server/mocks'; +import { alertsClientMock } from '../../../../../alerting/server/mocks'; import { updateNotifications } from './update_notifications'; import { readNotifications } from './read_notifications'; import { createNotifications } from './create_notifications'; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/notifications/update_notifications.ts b/x-pack/plugins/siem/server/lib/detection_engine/notifications/update_notifications.ts similarity index 95% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/notifications/update_notifications.ts rename to x-pack/plugins/siem/server/lib/detection_engine/notifications/update_notifications.ts index ac0de406aceb2..5889b0e4dcfb8 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/notifications/update_notifications.ts +++ b/x-pack/plugins/siem/server/lib/detection_engine/notifications/update_notifications.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { PartialAlert } from '../../../../../../../plugins/alerting/server'; +import { PartialAlert } from '../../../../../alerting/server'; import { readNotifications } from './read_notifications'; import { UpdateNotificationParams } from './types'; import { addTags } from './add_tags'; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/notifications/utils.test.ts b/x-pack/plugins/siem/server/lib/detection_engine/notifications/utils.test.ts similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/notifications/utils.test.ts rename to x-pack/plugins/siem/server/lib/detection_engine/notifications/utils.test.ts diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/notifications/utils.ts b/x-pack/plugins/siem/server/lib/detection_engine/notifications/utils.ts similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/notifications/utils.ts rename to x-pack/plugins/siem/server/lib/detection_engine/notifications/utils.ts diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/privileges/read_privileges.ts b/x-pack/plugins/siem/server/lib/detection_engine/privileges/read_privileges.ts similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/privileges/read_privileges.ts rename to x-pack/plugins/siem/server/lib/detection_engine/privileges/read_privileges.ts diff --git a/x-pack/plugins/siem/server/lib/detection_engine/routes/__mocks__/index.ts b/x-pack/plugins/siem/server/lib/detection_engine/routes/__mocks__/index.ts new file mode 100644 index 0000000000000..a28eb6ba3ccaa --- /dev/null +++ b/x-pack/plugins/siem/server/lib/detection_engine/routes/__mocks__/index.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 { DEFAULT_SIGNALS_INDEX, SIGNALS_INDEX_KEY } from '../../../../../common/constants'; +import { requestContextMock } from './request_context'; +import { serverMock } from './server'; +import { requestMock } from './request'; +import { responseMock } from './response_factory'; + +export { requestMock, requestContextMock, responseMock, serverMock }; + +export const createMockConfig = () => ({ + enabled: true, + [SIGNALS_INDEX_KEY]: DEFAULT_SIGNALS_INDEX, + maxRuleImportExportSize: 10000, + maxRuleImportPayloadBytes: 10485760, + maxTimelineImportExportSize: 10000, + maxTimelineImportPayloadBytes: 10485760, +}); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/__mocks__/request.ts b/x-pack/plugins/siem/server/lib/detection_engine/routes/__mocks__/request.ts similarity index 79% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/__mocks__/request.ts rename to x-pack/plugins/siem/server/lib/detection_engine/routes/__mocks__/request.ts index 8856a3463aab3..5f9246db7dfd5 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/__mocks__/request.ts +++ b/x-pack/plugins/siem/server/lib/detection_engine/routes/__mocks__/request.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { httpServerMock } from '../../../../../../../../../src/core/server/mocks'; +import { httpServerMock } from '../../../../../../../../src/core/server/mocks'; export const requestMock = { create: httpServerMock.createKibanaRequest, diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/__mocks__/request_context.ts b/x-pack/plugins/siem/server/lib/detection_engine/routes/__mocks__/request_context.ts similarity index 79% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/__mocks__/request_context.ts rename to x-pack/plugins/siem/server/lib/detection_engine/routes/__mocks__/request_context.ts index 2e5c29bc0221a..10efdb518f7b7 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/__mocks__/request_context.ts +++ b/x-pack/plugins/siem/server/lib/detection_engine/routes/__mocks__/request_context.ts @@ -4,15 +4,15 @@ * you may not use this file except in compliance with the Elastic License. */ -import { RequestHandlerContext } from '../../../../../../../../../src/core/server'; +import { RequestHandlerContext } from '../../../../../../../../src/core/server'; import { coreMock, elasticsearchServiceMock, savedObjectsClientMock, -} from '../../../../../../../../../src/core/server/mocks'; -import { alertsClientMock } from '../../../../../../../../plugins/alerting/server/mocks'; -import { actionsClientMock } from '../../../../../../../../plugins/actions/server/mocks'; -import { licensingMock } from '../../../../../../../../plugins/licensing/server/mocks'; +} from '../../../../../../../../src/core/server/mocks'; +import { alertsClientMock } from '../../../../../../alerting/server/mocks'; +import { actionsClientMock } from '../../../../../../actions/server/mocks'; +import { licensingMock } from '../../../../../../licensing/server/mocks'; const createMockClients = () => ({ actionsClient: actionsClientMock.create(), diff --git a/x-pack/plugins/siem/server/lib/detection_engine/routes/__mocks__/request_responses.ts b/x-pack/plugins/siem/server/lib/detection_engine/routes/__mocks__/request_responses.ts new file mode 100644 index 0000000000000..8c97d4436a561 --- /dev/null +++ b/x-pack/plugins/siem/server/lib/detection_engine/routes/__mocks__/request_responses.ts @@ -0,0 +1,709 @@ +/* + * 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 { SavedObjectsFindResponse } from 'kibana/server'; +import { ActionResult } from '../../../../../../actions/server'; +import { + SignalsStatusRestParams, + SignalsQueryRestParams, + SignalSearchResponse, +} from '../../signals/types'; +import { + DETECTION_ENGINE_RULES_URL, + DETECTION_ENGINE_SIGNALS_STATUS_URL, + DETECTION_ENGINE_PRIVILEGES_URL, + DETECTION_ENGINE_QUERY_SIGNALS_URL, + INTERNAL_RULE_ID_KEY, + INTERNAL_IMMUTABLE_KEY, + DETECTION_ENGINE_PREPACKAGED_URL, +} from '../../../../../common/constants'; +import { ShardsResponse } from '../../../types'; +import { + RuleAlertType, + IRuleSavedAttributesSavedObjectAttributes, + HapiReadableStream, +} from '../../rules/types'; +import { RuleAlertParamsRest, PrepackagedRules } from '../../types'; +import { requestMock } from './request'; +import { RuleNotificationAlertType } from '../../notifications/types'; + +export const mockPrepackagedRule = (): PrepackagedRules => ({ + rule_id: 'rule-1', + description: 'Detecting root and admin users', + index: ['auditbeat-*', 'filebeat-*', 'packetbeat-*', 'winlogbeat-*'], + interval: '5m', + name: 'Detect Root/Admin Users', + output_index: '.siem-signals', + risk_score: 50, + type: 'query', + from: 'now-6m', + to: 'now', + severity: 'high', + query: 'user.name: root or user.name: admin', + language: 'kuery', + threat: [ + { + framework: 'fake', + tactic: { id: 'fakeId', name: 'fakeName', reference: 'fakeRef' }, + technique: [{ id: 'techniqueId', name: 'techniqueName', reference: 'techniqueRef' }], + }, + ], + throttle: null, + enabled: true, + filters: [], + immutable: false, + references: [], + meta: {}, + tags: [], + version: 1, + false_positives: [], + max_signals: 100, + note: '', + timeline_id: 'timeline-id', + timeline_title: 'timeline-title', +}); + +export const typicalPayload = (): Partial => ({ + rule_id: 'rule-1', + description: 'Detecting root and admin users', + index: ['auditbeat-*', 'filebeat-*', 'packetbeat-*', 'winlogbeat-*'], + interval: '5m', + name: 'Detect Root/Admin Users', + output_index: '.siem-signals', + risk_score: 50, + type: 'query', + from: 'now-6m', + to: 'now', + severity: 'high', + query: 'user.name: root or user.name: admin', + language: 'kuery', + threat: [ + { + framework: 'fake', + tactic: { id: 'fakeId', name: 'fakeName', reference: 'fakeRef' }, + technique: [{ id: 'techniqueId', name: 'techniqueName', reference: 'techniqueRef' }], + }, + ], +}); + +export const typicalSetStatusSignalByIdsPayload = (): Partial => ({ + signal_ids: ['somefakeid1', 'somefakeid2'], + status: 'closed', +}); + +export const typicalSetStatusSignalByQueryPayload = (): Partial => ({ + query: { bool: { filter: { range: { '@timestamp': { gte: 'now-2M', lte: 'now/M' } } } } }, + status: 'closed', +}); + +export const typicalSignalsQuery = (): Partial => ({ + query: { match_all: {} }, +}); + +export const typicalSignalsQueryAggs = (): Partial => ({ + aggs: { statuses: { terms: { field: 'signal.status', size: 10 } } }, +}); + +export const setStatusSignalMissingIdsAndQueryPayload = (): Partial => ({ + status: 'closed', +}); + +export const getUpdateRequest = () => + requestMock.create({ + method: 'put', + path: DETECTION_ENGINE_RULES_URL, + body: typicalPayload(), + }); + +export const getPatchRequest = () => + requestMock.create({ + method: 'patch', + path: DETECTION_ENGINE_RULES_URL, + body: typicalPayload(), + }); + +export const getReadRequest = () => + requestMock.create({ + method: 'get', + path: DETECTION_ENGINE_RULES_URL, + query: { rule_id: 'rule-1' }, + }); + +export const getFindRequest = () => + requestMock.create({ + method: 'get', + path: `${DETECTION_ENGINE_RULES_URL}/_find`, + }); + +export const getReadBulkRequest = () => + requestMock.create({ + method: 'post', + path: `${DETECTION_ENGINE_RULES_URL}/_bulk_create`, + body: [typicalPayload()], + }); + +export const getUpdateBulkRequest = () => + requestMock.create({ + method: 'put', + path: `${DETECTION_ENGINE_RULES_URL}/_bulk_update`, + body: [typicalPayload()], + }); + +export const getPatchBulkRequest = () => + requestMock.create({ + method: 'patch', + path: `${DETECTION_ENGINE_RULES_URL}/_bulk_update`, + body: [typicalPayload()], + }); + +export const getDeleteBulkRequest = () => + requestMock.create({ + method: 'delete', + path: `${DETECTION_ENGINE_RULES_URL}/_bulk_delete`, + body: [{ rule_id: 'rule-1' }], + }); + +export const getDeleteBulkRequestById = () => + requestMock.create({ + method: 'delete', + path: `${DETECTION_ENGINE_RULES_URL}/_bulk_delete`, + body: [{ id: 'rule-04128c15-0d1b-4716-a4c5-46997ac7f3bd' }], + }); + +export const getDeleteAsPostBulkRequestById = () => + requestMock.create({ + method: 'post', + path: `${DETECTION_ENGINE_RULES_URL}/_bulk_delete`, + body: [{ id: 'rule-04128c15-0d1b-4716-a4c5-46997ac7f3bd' }], + }); + +export const getDeleteAsPostBulkRequest = () => + requestMock.create({ + method: 'post', + path: `${DETECTION_ENGINE_RULES_URL}/_bulk_delete`, + body: [{ rule_id: 'rule-1' }], + }); + +export const getPrivilegeRequest = () => + requestMock.create({ + method: 'get', + path: DETECTION_ENGINE_PRIVILEGES_URL, + }); + +export const addPrepackagedRulesRequest = () => + requestMock.create({ + method: 'put', + path: DETECTION_ENGINE_PREPACKAGED_URL, + }); + +export const getPrepackagedRulesStatusRequest = () => + requestMock.create({ + method: 'get', + path: `${DETECTION_ENGINE_PREPACKAGED_URL}/_status`, + }); + +export interface FindHit { + page: number; + perPage: number; + total: number; + data: T[]; +} + +export const getEmptyFindResult = (): FindHit => ({ + page: 1, + perPage: 1, + total: 0, + data: [], +}); + +export const getFindResultWithSingleHit = (): FindHit => ({ + page: 1, + perPage: 1, + total: 1, + data: [getResult()], +}); + +export const nonRuleFindResult = (): FindHit => ({ + page: 1, + perPage: 1, + total: 1, + data: [nonRuleAlert()], +}); + +export const getFindResultWithMultiHits = ({ + data, + page = 1, + perPage = 1, + total, +}: { + data: RuleAlertType[]; + page?: number; + perPage?: number; + total?: number; +}) => { + return { + page, + perPage, + total: total != null ? total : data.length, + data, + }; +}; + +export const ruleStatusRequest = () => + requestMock.create({ + method: 'post', + path: `${DETECTION_ENGINE_RULES_URL}/_find_statuses`, + body: { ids: ['someId'] }, + }); + +export const getImportRulesRequest = (hapiStream?: HapiReadableStream) => + requestMock.create({ + method: 'post', + path: `${DETECTION_ENGINE_RULES_URL}/_import`, + body: { file: hapiStream }, + }); + +export const getImportRulesRequestOverwriteTrue = (hapiStream?: HapiReadableStream) => + requestMock.create({ + method: 'post', + path: `${DETECTION_ENGINE_RULES_URL}/_import`, + body: { file: hapiStream }, + query: { overwrite: true }, + }); + +export const getDeleteRequest = () => + requestMock.create({ + method: 'delete', + path: DETECTION_ENGINE_RULES_URL, + query: { rule_id: 'rule-1' }, + }); + +export const getDeleteRequestById = () => + requestMock.create({ + method: 'delete', + path: DETECTION_ENGINE_RULES_URL, + query: { id: '04128c15-0d1b-4716-a4c5-46997ac7f3bd' }, + }); + +export const getCreateRequest = () => + requestMock.create({ + method: 'post', + path: DETECTION_ENGINE_RULES_URL, + body: typicalPayload(), + }); + +export const typicalMlRulePayload = () => { + const { query, language, index, ...mlParams } = typicalPayload(); + + return { + ...mlParams, + type: 'machine_learning', + anomaly_threshold: 58, + machine_learning_job_id: 'typical-ml-job-id', + }; +}; + +export const createMlRuleRequest = () => { + return requestMock.create({ + method: 'post', + path: DETECTION_ENGINE_RULES_URL, + body: typicalMlRulePayload(), + }); +}; + +export const createBulkMlRuleRequest = () => { + return requestMock.create({ + method: 'post', + path: DETECTION_ENGINE_RULES_URL, + body: [typicalMlRulePayload()], + }); +}; + +export const createRuleWithActionsRequest = () => { + const payload = typicalPayload(); + + return requestMock.create({ + method: 'post', + path: DETECTION_ENGINE_RULES_URL, + body: { + ...payload, + throttle: '5m', + actions: [ + { + group: 'default', + id: '99403909-ca9b-49ba-9d7a-7e5320e68d05', + params: { message: 'Rule generated {{state.signals_count}} signals' }, + action_type_id: '.slack', + }, + ], + }, + }); +}; + +export const getSetSignalStatusByIdsRequest = () => + requestMock.create({ + method: 'post', + path: DETECTION_ENGINE_SIGNALS_STATUS_URL, + body: typicalSetStatusSignalByIdsPayload(), + }); + +export const getSetSignalStatusByQueryRequest = () => + requestMock.create({ + method: 'post', + path: DETECTION_ENGINE_SIGNALS_STATUS_URL, + body: typicalSetStatusSignalByQueryPayload(), + }); + +export const getSignalsQueryRequest = () => + requestMock.create({ + method: 'post', + path: DETECTION_ENGINE_QUERY_SIGNALS_URL, + body: typicalSignalsQuery(), + }); + +export const getSignalsAggsQueryRequest = () => + requestMock.create({ + method: 'post', + path: DETECTION_ENGINE_QUERY_SIGNALS_URL, + body: typicalSignalsQueryAggs(), + }); + +export const getSignalsAggsAndQueryRequest = () => + requestMock.create({ + method: 'post', + path: DETECTION_ENGINE_QUERY_SIGNALS_URL, + body: { ...typicalSignalsQuery(), ...typicalSignalsQueryAggs() }, + }); + +export const createActionResult = (): ActionResult => ({ + id: 'result-1', + actionTypeId: 'action-id-1', + name: '', + config: {}, + isPreconfigured: false, +}); + +export const nonRuleAlert = () => ({ + ...getResult(), + id: '04128c15-0d1b-4716-a4c5-46997ac7f3bc', + name: 'Non-Rule Alert', + alertTypeId: 'something', +}); + +export const getResult = (): RuleAlertType => ({ + id: '04128c15-0d1b-4716-a4c5-46997ac7f3bd', + name: 'Detect Root/Admin Users', + tags: [`${INTERNAL_RULE_ID_KEY}:rule-1`, `${INTERNAL_IMMUTABLE_KEY}:false`], + alertTypeId: 'siem.signals', + consumer: 'siem', + params: { + anomalyThreshold: undefined, + description: 'Detecting root and admin users', + ruleId: 'rule-1', + index: ['auditbeat-*', 'filebeat-*', 'packetbeat-*', 'winlogbeat-*'], + falsePositives: [], + from: 'now-6m', + immutable: false, + query: 'user.name: root or user.name: admin', + language: 'kuery', + machineLearningJobId: undefined, + outputIndex: '.siem-signals', + timelineId: 'some-timeline-id', + timelineTitle: 'some-timeline-title', + meta: { someMeta: 'someField' }, + filters: [ + { + query: { + match_phrase: { + 'host.name': 'some-host', + }, + }, + }, + ], + riskScore: 50, + maxSignals: 100, + severity: 'high', + to: 'now', + type: 'query', + threat: [ + { + framework: 'MITRE ATT&CK', + tactic: { + id: 'TA0040', + name: 'impact', + reference: 'https://attack.mitre.org/tactics/TA0040/', + }, + technique: [ + { + id: 'T1499', + name: 'endpoint denial of service', + reference: 'https://attack.mitre.org/techniques/T1499/', + }, + ], + }, + ], + references: ['http://www.example.com', 'https://ww.example.com'], + note: '# Investigative notes', + version: 1, + exceptions_list: [ + { + field: 'source.ip', + values_operator: 'included', + values_type: 'exists', + }, + { + field: 'host.name', + values_operator: 'excluded', + values_type: 'match', + values: [ + { + name: 'rock01', + }, + ], + and: [ + { + field: 'host.id', + values_operator: 'included', + values_type: 'match_all', + values: [ + { + name: '123', + }, + { + name: '678', + }, + ], + }, + ], + }, + ], + }, + createdAt: new Date('2019-12-13T16:40:33.400Z'), + updatedAt: new Date('2019-12-13T16:40:33.400Z'), + schedule: { interval: '5m' }, + enabled: true, + actions: [], + throttle: null, + createdBy: 'elastic', + updatedBy: 'elastic', + apiKey: null, + apiKeyOwner: 'elastic', + muteAll: false, + mutedInstanceIds: [], + scheduledTaskId: '2dabe330-0702-11ea-8b50-773b89126888', +}); + +export const getMlResult = (): RuleAlertType => { + const result = getResult(); + + return { + ...result, + params: { + ...result.params, + query: undefined, + language: undefined, + filters: undefined, + index: undefined, + type: 'machine_learning', + anomalyThreshold: 44, + machineLearningJobId: 'some_job_id', + }, + }; +}; + +export const updateActionResult = (): ActionResult => ({ + id: 'result-1', + actionTypeId: 'action-id-1', + name: '', + config: {}, + isPreconfigured: false, +}); + +export const getMockPrivilegesResult = () => ({ + username: 'test-space', + has_all_requested: false, + cluster: { + monitor_ml: true, + manage_ccr: false, + manage_index_templates: true, + monitor_watcher: true, + monitor_transform: true, + read_ilm: true, + manage_api_key: false, + manage_security: false, + manage_own_api_key: false, + manage_saml: false, + all: false, + manage_ilm: true, + manage_ingest_pipelines: true, + read_ccr: false, + manage_rollup: true, + monitor: true, + manage_watcher: true, + manage: true, + manage_transform: true, + manage_token: false, + manage_ml: true, + manage_pipeline: true, + monitor_rollup: true, + transport_client: true, + create_snapshot: true, + }, + index: { + '.siem-signals-test-space': { + all: false, + manage_ilm: true, + read: false, + create_index: true, + read_cross_cluster: false, + index: false, + monitor: true, + delete: false, + manage: true, + delete_index: true, + create_doc: false, + view_index_metadata: true, + create: false, + manage_follow_index: true, + manage_leader_index: true, + write: false, + }, + }, + application: {}, +}); + +export const getFindResultStatusEmpty = (): SavedObjectsFindResponse => ({ + page: 1, + per_page: 1, + total: 0, + saved_objects: [], +}); + +export const getFindResultStatus = (): SavedObjectsFindResponse => ({ + page: 1, + per_page: 6, + total: 2, + saved_objects: [ + { + type: 'my-type', + id: 'e0b86950-4e9f-11ea-bdbd-07b56aa159b3', + attributes: { + alertId: '1ea5a820-4da1-4e82-92a1-2b43a7bece08', + statusDate: '2020-02-18T15:26:49.783Z', + status: 'succeeded', + lastFailureAt: null, + lastSuccessAt: '2020-02-18T15:26:49.783Z', + lastFailureMessage: null, + lastSuccessMessage: 'succeeded', + lastLookBackDate: new Date('2020-02-18T15:14:58.806Z').toISOString(), + gap: '500.32', + searchAfterTimeDurations: ['200.00'], + bulkCreateTimeDurations: ['800.43'], + }, + references: [], + updated_at: '2020-02-18T15:26:51.333Z', + version: 'WzQ2LDFd', + }, + { + type: 'my-type', + id: '91246bd0-5261-11ea-9650-33b954270f67', + attributes: { + alertId: '1ea5a820-4da1-4e82-92a1-2b43a7bece08', + statusDate: '2020-02-18T15:15:58.806Z', + status: 'failed', + lastFailureAt: '2020-02-18T15:15:58.806Z', + lastSuccessAt: '2020-02-13T20:31:59.855Z', + lastFailureMessage: + 'Signal rule name: "Query with a rule id Number 1", id: "1ea5a820-4da1-4e82-92a1-2b43a7bece08", rule_id: "query-rule-id-1" has a time gap of 5 days (412682928ms), and could be missing signals within that time. Consider increasing your look behind time or adding more Kibana instances.', + lastSuccessMessage: 'succeeded', + lastLookBackDate: new Date('2020-02-18T15:14:58.806Z').toISOString(), + gap: '500.32', + searchAfterTimeDurations: ['200.00'], + bulkCreateTimeDurations: ['800.43'], + }, + references: [], + updated_at: '2020-02-18T15:15:58.860Z', + version: 'WzMyLDFd', + }, + ], +}); + +export const getEmptySignalsResponse = (): SignalSearchResponse => ({ + took: 1, + timed_out: false, + _shards: { total: 1, successful: 1, skipped: 0, failed: 0 }, + hits: { total: { value: 0, relation: 'eq' }, max_score: 0, hits: [] }, + aggregations: { + signalsByGrouping: { doc_count_error_upper_bound: 0, sum_other_doc_count: 0, buckets: [] }, + }, +}); + +export const getSuccessfulSignalUpdateResponse = () => ({ + took: 18, + timed_out: false, + total: 1, + updated: 1, + deleted: 0, + batches: 1, + version_conflicts: 0, + noops: 0, + retries: { bulk: 0, search: 0 }, + throttled_millis: 0, + requests_per_second: -1, + throttled_until_millis: 0, + failures: [], +}); + +export const getIndexName = () => 'index-name'; +export const getEmptyIndex = (): { _shards: Partial } => ({ + _shards: { total: 0 }, +}); +export const getNonEmptyIndex = (): { _shards: Partial } => ({ + _shards: { total: 1 }, +}); + +export const getNotificationResult = (): RuleNotificationAlertType => ({ + id: '200dbf2f-b269-4bf9-aa85-11ba32ba73ba', + name: 'Notification for Rule Test', + tags: ['__internal_rule_alert_id:85b64e8a-2e40-4096-86af-5ac172c10825'], + alertTypeId: 'siem.notifications', + consumer: 'siem', + params: { + ruleAlertId: '85b64e8a-2e40-4096-86af-5ac172c10825', + }, + schedule: { + interval: '5m', + }, + enabled: true, + actions: [ + { + actionTypeId: '.slack', + params: { + message: + 'Rule generated {{state.signals_count}} signals\n\n{{context.rule.name}}\n{{{context.results_link}}}', + }, + group: 'default', + id: '99403909-ca9b-49ba-9d7a-7e5320e68d05', + }, + ], + throttle: null, + apiKey: null, + apiKeyOwner: 'elastic', + createdBy: 'elastic', + updatedBy: 'elastic', + createdAt: new Date('2020-03-21T11:15:13.530Z'), + muteAll: false, + mutedInstanceIds: [], + scheduledTaskId: '62b3a130-6b70-11ea-9ce9-6b9818c4cbd7', + updatedAt: new Date('2020-03-21T12:37:08.730Z'), +}); + +export const getFindNotificationsResultWithSingleHit = (): FindHit => ({ + page: 1, + perPage: 1, + total: 1, + data: [getNotificationResult()], +}); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/__mocks__/response_factory.ts b/x-pack/plugins/siem/server/lib/detection_engine/routes/__mocks__/response_factory.ts similarity index 79% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/__mocks__/response_factory.ts rename to x-pack/plugins/siem/server/lib/detection_engine/routes/__mocks__/response_factory.ts index 3e0eda9961403..e6c03d382d9db 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/__mocks__/response_factory.ts +++ b/x-pack/plugins/siem/server/lib/detection_engine/routes/__mocks__/response_factory.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { httpServerMock } from '../../../../../../../../../src/core/server/mocks'; +import { httpServerMock } from '../../../../../../../../src/core/server/mocks'; export const responseMock = { create: httpServerMock.createResponseFactory, diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/__mocks__/server.ts b/x-pack/plugins/siem/server/lib/detection_engine/routes/__mocks__/server.ts similarity index 95% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/__mocks__/server.ts rename to x-pack/plugins/siem/server/lib/detection_engine/routes/__mocks__/server.ts index 824d1f2bec334..c08e626adb323 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/__mocks__/server.ts +++ b/x-pack/plugins/siem/server/lib/detection_engine/routes/__mocks__/server.ts @@ -9,8 +9,8 @@ import { RouteConfig, KibanaRequest, RequestHandlerContext, -} from '../../../../../../../../../src/core/server'; -import { httpServiceMock } from '../../../../../../../../../src/core/server/mocks'; +} from '../../../../../../../../src/core/server'; +import { httpServiceMock } from '../../../../../../../../src/core/server/mocks'; import { requestContextMock } from './request_context'; import { responseMock as responseFactoryMock } from './response_factory'; import { requestMock } from '.'; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/__mocks__/test_adapters.ts b/x-pack/plugins/siem/server/lib/detection_engine/routes/__mocks__/test_adapters.ts similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/__mocks__/test_adapters.ts rename to x-pack/plugins/siem/server/lib/detection_engine/routes/__mocks__/test_adapters.ts diff --git a/x-pack/plugins/siem/server/lib/detection_engine/routes/__mocks__/utils.ts b/x-pack/plugins/siem/server/lib/detection_engine/routes/__mocks__/utils.ts new file mode 100644 index 0000000000000..6f628170271f3 --- /dev/null +++ b/x-pack/plugins/siem/server/lib/detection_engine/routes/__mocks__/utils.ts @@ -0,0 +1,191 @@ +/* + * 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'; + +import { OutputRuleAlertRest } from '../../types'; +import { HapiReadableStream } from '../../rules/types'; + +/** + * This is a typical simple rule for testing that is easy for most basic testing + * @param ruleId + */ +export const getSimpleRule = (ruleId = 'rule-1'): Partial => ({ + name: 'Simple Rule Query', + description: 'Simple Rule Query', + risk_score: 1, + rule_id: ruleId, + severity: 'high', + type: 'query', + query: 'user.name: root or user.name: admin', +}); + +/** + * This is a typical ML rule for testing + * @param ruleId + */ +export const getSimpleMlRule = (ruleId = 'rule-1'): Partial => ({ + name: 'Simple Rule Query', + description: 'Simple Rule Query', + risk_score: 1, + rule_id: ruleId, + severity: 'high', + type: 'machine_learning', + anomaly_threshold: 44, + machine_learning_job_id: 'some_job_id', +}); + +/** + * This is a typical simple rule for testing that is easy for most basic testing + * @param ruleId + */ +export const getSimpleRuleWithId = (id = 'rule-1'): Partial => ({ + name: 'Simple Rule Query', + description: 'Simple Rule Query', + risk_score: 1, + id, + severity: 'high', + type: 'query', + query: 'user.name: root or user.name: admin', +}); + +/** + * Given an array of rules, builds an NDJSON string of rules + * as we might import/export + * @param rules Array of rule objects with which to generate rule JSON + */ +export const rulesToNdJsonString = (rules: Array>) => { + return rules.map(rule => JSON.stringify(rule)).join('\r\n'); +}; + +/** + * Given an array of rule IDs, builds an NDJSON string of rules + * as we might import/export + * @param ruleIds Array of ruleIds with which to generate rule JSON + */ +export const ruleIdsToNdJsonString = (ruleIds: string[]) => { + const rules = ruleIds.map(ruleId => getSimpleRule(ruleId)); + return rulesToNdJsonString(rules); +}; + +/** + * Given a string, builds a hapi stream as our + * route handler would receive it. + * @param string contents of the stream + * @param filename String to declare file extension + */ +export const buildHapiStream = (string: string, filename = 'file.ndjson'): HapiReadableStream => { + const HapiStream = class extends Readable { + public readonly hapi: { filename: string }; + constructor(fileName: string) { + super(); + this.hapi = { filename: fileName }; + } + }; + + const stream = new HapiStream(filename); + stream.push(string); + stream.push(null); + + return stream; +}; + +export const getOutputRuleAlertForRest = (): Omit< + OutputRuleAlertRest, + 'machine_learning_job_id' | 'anomaly_threshold' +> => ({ + actions: [], + created_by: 'elastic', + created_at: '2019-12-13T16:40:33.400Z', + updated_at: '2019-12-13T16:40:33.400Z', + description: 'Detecting root and admin users', + enabled: true, + false_positives: [], + from: 'now-6m', + id: '04128c15-0d1b-4716-a4c5-46997ac7f3bd', + immutable: false, + index: ['auditbeat-*', 'filebeat-*', 'packetbeat-*', 'winlogbeat-*'], + interval: '5m', + risk_score: 50, + rule_id: 'rule-1', + language: 'kuery', + max_signals: 100, + name: 'Detect Root/Admin Users', + output_index: '.siem-signals', + query: 'user.name: root or user.name: admin', + references: ['http://www.example.com', 'https://ww.example.com'], + severity: 'high', + updated_by: 'elastic', + tags: [], + throttle: 'no_actions', + threat: [ + { + framework: 'MITRE ATT&CK', + tactic: { + id: 'TA0040', + name: 'impact', + reference: 'https://attack.mitre.org/tactics/TA0040/', + }, + technique: [ + { + id: 'T1499', + name: 'endpoint denial of service', + reference: 'https://attack.mitre.org/techniques/T1499/', + }, + ], + }, + ], + exceptions_list: [ + { + field: 'source.ip', + values_operator: 'included', + values_type: 'exists', + }, + { + field: 'host.name', + values_operator: 'excluded', + values_type: 'match', + values: [ + { + name: 'rock01', + }, + ], + and: [ + { + field: 'host.id', + values_operator: 'included', + values_type: 'match_all', + values: [ + { + name: '123', + }, + { + name: '678', + }, + ], + }, + ], + }, + ], + filters: [ + { + query: { + match_phrase: { + 'host.name': 'some-host', + }, + }, + }, + ], + meta: { + someMeta: 'someField', + }, + timeline_id: 'some-timeline-id', + timeline_title: 'some-timeline-title', + to: 'now', + type: 'query', + note: '# Investigative notes', + version: 1, +}); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/index/create_index_route.ts b/x-pack/plugins/siem/server/lib/detection_engine/routes/index/create_index_route.ts similarity index 97% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/index/create_index_route.ts rename to x-pack/plugins/siem/server/lib/detection_engine/routes/index/create_index_route.ts index 3195483013c19..cb48e35228858 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/index/create_index_route.ts +++ b/x-pack/plugins/siem/server/lib/detection_engine/routes/index/create_index_route.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { IRouter } from '../../../../../../../../../src/core/server'; +import { IRouter } from '../../../../../../../../src/core/server'; import { DETECTION_ENGINE_INDEX_URL } from '../../../../../common/constants'; import { transformError, buildSiemResponse } from '../utils'; import { getIndexExists } from '../../index/get_index_exists'; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/index/delete_index_route.ts b/x-pack/plugins/siem/server/lib/detection_engine/routes/index/delete_index_route.ts similarity index 97% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/index/delete_index_route.ts rename to x-pack/plugins/siem/server/lib/detection_engine/routes/index/delete_index_route.ts index c667e7ae9c463..5eff38b778492 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/index/delete_index_route.ts +++ b/x-pack/plugins/siem/server/lib/detection_engine/routes/index/delete_index_route.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { IRouter } from '../../../../../../../../../src/core/server'; +import { IRouter } from '../../../../../../../../src/core/server'; import { DETECTION_ENGINE_INDEX_URL } from '../../../../../common/constants'; import { transformError, buildSiemResponse } from '../utils'; import { getIndexExists } from '../../index/get_index_exists'; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/index/ecs_mapping.json b/x-pack/plugins/siem/server/lib/detection_engine/routes/index/ecs_mapping.json similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/index/ecs_mapping.json rename to x-pack/plugins/siem/server/lib/detection_engine/routes/index/ecs_mapping.json diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/index/get_signals_template.test.ts b/x-pack/plugins/siem/server/lib/detection_engine/routes/index/get_signals_template.test.ts similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/index/get_signals_template.test.ts rename to x-pack/plugins/siem/server/lib/detection_engine/routes/index/get_signals_template.test.ts diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/index/get_signals_template.ts b/x-pack/plugins/siem/server/lib/detection_engine/routes/index/get_signals_template.ts similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/index/get_signals_template.ts rename to x-pack/plugins/siem/server/lib/detection_engine/routes/index/get_signals_template.ts diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/index/read_index_route.ts b/x-pack/plugins/siem/server/lib/detection_engine/routes/index/read_index_route.ts similarity index 95% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/index/read_index_route.ts rename to x-pack/plugins/siem/server/lib/detection_engine/routes/index/read_index_route.ts index 047176f155611..8ff8d7461ecd1 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/index/read_index_route.ts +++ b/x-pack/plugins/siem/server/lib/detection_engine/routes/index/read_index_route.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { IRouter } from '../../../../../../../../../src/core/server'; +import { IRouter } from '../../../../../../../../src/core/server'; import { DETECTION_ENGINE_INDEX_URL } from '../../../../../common/constants'; import { transformError, buildSiemResponse } from '../utils'; import { getIndexExists } from '../../index/get_index_exists'; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/index/signals_mapping.json b/x-pack/plugins/siem/server/lib/detection_engine/routes/index/signals_mapping.json similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/index/signals_mapping.json rename to x-pack/plugins/siem/server/lib/detection_engine/routes/index/signals_mapping.json diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/index/signals_policy.json b/x-pack/plugins/siem/server/lib/detection_engine/routes/index/signals_policy.json similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/index/signals_policy.json rename to x-pack/plugins/siem/server/lib/detection_engine/routes/index/signals_policy.json diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/privileges/read_privileges_route.test.ts b/x-pack/plugins/siem/server/lib/detection_engine/routes/privileges/read_privileges_route.test.ts similarity index 97% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/privileges/read_privileges_route.test.ts rename to x-pack/plugins/siem/server/lib/detection_engine/routes/privileges/read_privileges_route.test.ts index 3209f5ce9f519..ce44f71ef7217 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/privileges/read_privileges_route.test.ts +++ b/x-pack/plugins/siem/server/lib/detection_engine/routes/privileges/read_privileges_route.test.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { securityMock } from '../../../../../../../../plugins/security/server/mocks'; +import { securityMock } from '../../../../../../security/server/mocks'; import { readPrivilegesRoute } from './read_privileges_route'; import { serverMock, requestContextMock } from '../__mocks__'; import { getPrivilegeRequest, getMockPrivilegesResult } from '../__mocks__/request_responses'; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/privileges/read_privileges_route.ts b/x-pack/plugins/siem/server/lib/detection_engine/routes/privileges/read_privileges_route.ts similarity index 96% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/privileges/read_privileges_route.ts rename to x-pack/plugins/siem/server/lib/detection_engine/routes/privileges/read_privileges_route.ts index d86880de65386..7dbbe837e656d 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/privileges/read_privileges_route.ts +++ b/x-pack/plugins/siem/server/lib/detection_engine/routes/privileges/read_privileges_route.ts @@ -6,7 +6,7 @@ import { merge } from 'lodash/fp'; -import { IRouter } from '../../../../../../../../../src/core/server'; +import { IRouter } from '../../../../../../../../src/core/server'; import { DETECTION_ENGINE_PRIVILEGES_URL } from '../../../../../common/constants'; import { SetupPlugins } from '../../../../plugin'; import { buildSiemResponse, transformError } from '../utils'; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/add_prepackaged_rules_route.test.ts b/x-pack/plugins/siem/server/lib/detection_engine/routes/rules/add_prepackaged_rules_route.test.ts similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/add_prepackaged_rules_route.test.ts rename to x-pack/plugins/siem/server/lib/detection_engine/routes/rules/add_prepackaged_rules_route.test.ts diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/add_prepackaged_rules_route.ts b/x-pack/plugins/siem/server/lib/detection_engine/routes/rules/add_prepackaged_rules_route.ts similarity index 98% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/add_prepackaged_rules_route.ts rename to x-pack/plugins/siem/server/lib/detection_engine/routes/rules/add_prepackaged_rules_route.ts index 3eba04debb21f..bfc8c9c54b2c0 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/add_prepackaged_rules_route.ts +++ b/x-pack/plugins/siem/server/lib/detection_engine/routes/rules/add_prepackaged_rules_route.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { IRouter } from '../../../../../../../../../src/core/server'; +import { IRouter } from '../../../../../../../../src/core/server'; import { DETECTION_ENGINE_PREPACKAGED_URL } from '../../../../../common/constants'; import { getIndexExists } from '../../index/get_index_exists'; import { transformError, buildSiemResponse } from '../utils'; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/create_rules_bulk_route.test.ts b/x-pack/plugins/siem/server/lib/detection_engine/routes/rules/create_rules_bulk_route.test.ts similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/create_rules_bulk_route.test.ts rename to x-pack/plugins/siem/server/lib/detection_engine/routes/rules/create_rules_bulk_route.test.ts diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/create_rules_bulk_route.ts b/x-pack/plugins/siem/server/lib/detection_engine/routes/rules/create_rules_bulk_route.ts similarity index 97% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/create_rules_bulk_route.ts rename to x-pack/plugins/siem/server/lib/detection_engine/routes/rules/create_rules_bulk_route.ts index 5377e9039785e..2d7ddb79e5af5 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/create_rules_bulk_route.ts +++ b/x-pack/plugins/siem/server/lib/detection_engine/routes/rules/create_rules_bulk_route.ts @@ -6,7 +6,7 @@ import uuid from 'uuid'; -import { IRouter } from '../../../../../../../../../src/core/server'; +import { IRouter } from '../../../../../../../../src/core/server'; import { DETECTION_ENGINE_RULES_URL } from '../../../../../common/constants'; import { createRules } from '../../rules/create_rules'; import { RuleAlertParamsRest } from '../../types'; @@ -86,7 +86,7 @@ export const createRulesBulkRoute = (router: IRouter) => { timeline_id: timelineId, timeline_title: timelineTitle, version, - lists, + exceptions_list, } = payloadRule; const ruleIdOrUuid = ruleId ?? uuid.v4(); try { @@ -143,7 +143,7 @@ export const createRulesBulkRoute = (router: IRouter) => { references, note, version, - lists, + exceptions_list, actions: throttle === 'rule' ? actions : [], // Only enable actions if throttle is set to rule, otherwise we are a notification and should not enable it, }); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/create_rules_route.test.ts b/x-pack/plugins/siem/server/lib/detection_engine/routes/rules/create_rules_route.test.ts similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/create_rules_route.test.ts rename to x-pack/plugins/siem/server/lib/detection_engine/routes/rules/create_rules_route.test.ts diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/create_rules_route.ts b/x-pack/plugins/siem/server/lib/detection_engine/routes/rules/create_rules_route.ts similarity index 97% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/create_rules_route.ts rename to x-pack/plugins/siem/server/lib/detection_engine/routes/rules/create_rules_route.ts index 9a329b78b8f12..1f0896686aca0 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/create_rules_route.ts +++ b/x-pack/plugins/siem/server/lib/detection_engine/routes/rules/create_rules_route.ts @@ -6,7 +6,7 @@ import uuid from 'uuid'; -import { IRouter } from '../../../../../../../../../src/core/server'; +import { IRouter } from '../../../../../../../../src/core/server'; import { DETECTION_ENGINE_RULES_URL } from '../../../../../common/constants'; import { createRules } from '../../rules/create_rules'; import { IRuleSavedAttributesSavedObjectAttributes } from '../../rules/types'; @@ -66,7 +66,7 @@ export const createRulesRoute = (router: IRouter): void => { type, references, note, - lists, + exceptions_list, } = request.body; const siemResponse = buildSiemResponse(response); @@ -131,7 +131,7 @@ export const createRulesRoute = (router: IRouter): void => { references, note, version: 1, - lists, + exceptions_list, actions: throttle === 'rule' ? actions : [], // Only enable actions if throttle is rule, otherwise we are a notification and should not enable it, }); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/delete_rules_bulk_route.test.ts b/x-pack/plugins/siem/server/lib/detection_engine/routes/rules/delete_rules_bulk_route.test.ts similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/delete_rules_bulk_route.test.ts rename to x-pack/plugins/siem/server/lib/detection_engine/routes/rules/delete_rules_bulk_route.test.ts diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/delete_rules_bulk_route.ts b/x-pack/plugins/siem/server/lib/detection_engine/routes/rules/delete_rules_bulk_route.ts similarity index 99% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/delete_rules_bulk_route.ts rename to x-pack/plugins/siem/server/lib/detection_engine/routes/rules/delete_rules_bulk_route.ts index 0c5ad2e060924..38748e287ab45 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/delete_rules_bulk_route.ts +++ b/x-pack/plugins/siem/server/lib/detection_engine/routes/rules/delete_rules_bulk_route.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { IRouter, RouteConfig, RequestHandler } from '../../../../../../../../../src/core/server'; +import { IRouter, RouteConfig, RequestHandler } from '../../../../../../../../src/core/server'; import { DETECTION_ENGINE_RULES_URL } from '../../../../../common/constants'; import { queryRulesBulkSchema } from '../schemas/query_rules_bulk_schema'; import { rulesBulkSchema } from '../schemas/response/rules_bulk_schema'; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/delete_rules_route.test.ts b/x-pack/plugins/siem/server/lib/detection_engine/routes/rules/delete_rules_route.test.ts similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/delete_rules_route.test.ts rename to x-pack/plugins/siem/server/lib/detection_engine/routes/rules/delete_rules_route.test.ts diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/delete_rules_route.ts b/x-pack/plugins/siem/server/lib/detection_engine/routes/rules/delete_rules_route.ts similarity index 97% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/delete_rules_route.ts rename to x-pack/plugins/siem/server/lib/detection_engine/routes/rules/delete_rules_route.ts index 71724e3ba9b58..098d556741fed 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/delete_rules_route.ts +++ b/x-pack/plugins/siem/server/lib/detection_engine/routes/rules/delete_rules_route.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { IRouter } from '../../../../../../../../../src/core/server'; +import { IRouter } from '../../../../../../../../src/core/server'; import { DETECTION_ENGINE_RULES_URL } from '../../../../../common/constants'; import { deleteRules } from '../../rules/delete_rules'; import { queryRulesSchema } from '../schemas/query_rules_schema'; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/export_rules_route.ts b/x-pack/plugins/siem/server/lib/detection_engine/routes/rules/export_rules_route.ts similarity index 90% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/export_rules_route.ts rename to x-pack/plugins/siem/server/lib/detection_engine/routes/rules/export_rules_route.ts index 50eafe163c265..8433b74adf310 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/export_rules_route.ts +++ b/x-pack/plugins/siem/server/lib/detection_engine/routes/rules/export_rules_route.ts @@ -4,9 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ -import { IRouter } from '../../../../../../../../../src/core/server'; +import { IRouter } from '../../../../../../../../src/core/server'; import { DETECTION_ENGINE_RULES_URL } from '../../../../../common/constants'; -import { LegacyServices } from '../../../../types'; +import { ConfigType } from '../../../..'; import { ExportRulesRequestParams } from '../../rules/types'; import { getNonPackagedRulesCount } from '../../rules/get_existing_prepackaged_rules'; import { exportRulesSchema, exportRulesQuerySchema } from '../schemas/export_rules_schema'; @@ -14,7 +14,7 @@ import { getExportByObjectIds } from '../../rules/get_export_by_object_ids'; import { getExportAll } from '../../rules/get_export_all'; import { transformError, buildRouteValidation, buildSiemResponse } from '../utils'; -export const exportRulesRoute = (router: IRouter, config: LegacyServices['config']) => { +export const exportRulesRoute = (router: IRouter, config: ConfigType) => { router.post( { path: `${DETECTION_ENGINE_RULES_URL}/_export`, @@ -35,7 +35,7 @@ export const exportRulesRoute = (router: IRouter, config: LegacyServices['config } try { - const exportSizeLimit = config().get('savedObjects.maxImportExportSize'); + const exportSizeLimit = config.maxRuleImportExportSize; if (request.body?.objects != null && request.body.objects.length > exportSizeLimit) { return siemResponse.error({ statusCode: 400, diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/find_rules_route.test.ts b/x-pack/plugins/siem/server/lib/detection_engine/routes/rules/find_rules_route.test.ts similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/find_rules_route.test.ts rename to x-pack/plugins/siem/server/lib/detection_engine/routes/rules/find_rules_route.test.ts diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/find_rules_route.ts b/x-pack/plugins/siem/server/lib/detection_engine/routes/rules/find_rules_route.ts similarity index 97% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/find_rules_route.ts rename to x-pack/plugins/siem/server/lib/detection_engine/routes/rules/find_rules_route.ts index 85555c1a57084..9661fac81497c 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/find_rules_route.ts +++ b/x-pack/plugins/siem/server/lib/detection_engine/routes/rules/find_rules_route.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { IRouter } from '../../../../../../../../../src/core/server'; +import { IRouter } from '../../../../../../../../src/core/server'; import { DETECTION_ENGINE_RULES_URL } from '../../../../../common/constants'; import { findRules } from '../../rules/find_rules'; import { diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/find_rules_status_route.test.ts b/x-pack/plugins/siem/server/lib/detection_engine/routes/rules/find_rules_status_route.test.ts similarity index 97% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/find_rules_status_route.test.ts rename to x-pack/plugins/siem/server/lib/detection_engine/routes/rules/find_rules_status_route.test.ts index 89c9f34027120..d7c6d317227fa 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/find_rules_status_route.test.ts +++ b/x-pack/plugins/siem/server/lib/detection_engine/routes/rules/find_rules_status_route.test.ts @@ -60,9 +60,9 @@ describe('find_statuses', () => { describe('request validation', () => { test('disallows singular id query param', async () => { const request = requestMock.create({ - method: 'get', + method: 'post', path: `${DETECTION_ENGINE_RULES_URL}/_find_statuses`, - query: { id: ['someId'] }, + body: { id: ['someId'] }, }); const result = server.validate(request); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/find_rules_status_route.ts b/x-pack/plugins/siem/server/lib/detection_engine/routes/rules/find_rules_status_route.ts similarity index 91% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/find_rules_status_route.ts rename to x-pack/plugins/siem/server/lib/detection_engine/routes/rules/find_rules_status_route.ts index 6fee4d71a904e..6b54a25a1b1c4 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/find_rules_status_route.ts +++ b/x-pack/plugins/siem/server/lib/detection_engine/routes/rules/find_rules_status_route.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { IRouter } from '../../../../../../../../../src/core/server'; +import { IRouter } from '../../../../../../../../src/core/server'; import { DETECTION_ENGINE_RULES_URL } from '../../../../../common/constants'; import { findRulesStatusesSchema } from '../schemas/find_rules_statuses_schema'; import { @@ -22,18 +22,18 @@ import { } from '../utils'; export const findRulesStatusesRoute = (router: IRouter) => { - router.get( + router.post( { path: `${DETECTION_ENGINE_RULES_URL}/_find_statuses`, validate: { - query: buildRouteValidation(findRulesStatusesSchema), + body: buildRouteValidation(findRulesStatusesSchema), }, options: { tags: ['access:siem'], }, }, async (context, request, response) => { - const { query } = request; + const { body } = request; const siemResponse = buildSiemResponse(response); const alertsClient = context.alerting?.getAlertsClient(); const savedObjectsClient = context.core.savedObjects.client; @@ -50,7 +50,7 @@ export const findRulesStatusesRoute = (router: IRouter) => { } */ try { - const statuses = await query.ids.reduce>( + const statuses = await body.ids.reduce>( async (acc, id) => { const lastFiveErrorsForId = await savedObjectsClient.find< IRuleSavedAttributesSavedObjectAttributes diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/get_prepackaged_rules_status_route.test.ts b/x-pack/plugins/siem/server/lib/detection_engine/routes/rules/get_prepackaged_rules_status_route.test.ts similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/get_prepackaged_rules_status_route.test.ts rename to x-pack/plugins/siem/server/lib/detection_engine/routes/rules/get_prepackaged_rules_status_route.test.ts diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/get_prepackaged_rules_status_route.ts b/x-pack/plugins/siem/server/lib/detection_engine/routes/rules/get_prepackaged_rules_status_route.ts similarity index 97% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/get_prepackaged_rules_status_route.ts rename to x-pack/plugins/siem/server/lib/detection_engine/routes/rules/get_prepackaged_rules_status_route.ts index 7f0bf4bf81179..67a54f3ba492a 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/get_prepackaged_rules_status_route.ts +++ b/x-pack/plugins/siem/server/lib/detection_engine/routes/rules/get_prepackaged_rules_status_route.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { IRouter } from '../../../../../../../../../src/core/server'; +import { IRouter } from '../../../../../../../../src/core/server'; import { DETECTION_ENGINE_PREPACKAGED_URL } from '../../../../../common/constants'; import { transformError, buildSiemResponse } from '../utils'; import { getPrepackagedRules } from '../../rules/get_prepackaged_rules'; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/import_rules_route.test.ts b/x-pack/plugins/siem/server/lib/detection_engine/routes/rules/import_rules_route.test.ts similarity index 95% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/import_rules_route.test.ts rename to x-pack/plugins/siem/server/lib/detection_engine/routes/rules/import_rules_route.test.ts index 61f5e6faf1bdb..8c052cfdf4024 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/import_rules_route.test.ts +++ b/x-pack/plugins/siem/server/lib/detection_engine/routes/rules/import_rules_route.test.ts @@ -23,7 +23,6 @@ import { } from '../__mocks__/request_responses'; import { createMockConfig, requestContextMock, serverMock, requestMock } from '../__mocks__'; import { importRulesRoute } from './import_rules_route'; -import { DEFAULT_SIGNALS_INDEX } from '../../../../../common/constants'; import * as createRulesStreamFromNdJson from '../../rules/create_rules_stream_from_ndjson'; import { setFeatureFlagsForTestsOnly, unSetFeatureFlagsForTestsOnly } from '../../feature_flags'; @@ -36,7 +35,7 @@ describe('import_rules_route', () => { unSetFeatureFlagsForTestsOnly(); }); - let config = createMockConfig(); + let config: ReturnType; let server: ReturnType; let request: ReturnType; let { clients, context } = requestContextMock.createTools(); @@ -51,30 +50,10 @@ describe('import_rules_route', () => { server = serverMock.create(); ({ clients, context } = requestContextMock.createTools()); + config = createMockConfig(); const hapiStream = buildHapiStream(ruleIdsToNdJsonString(['rule-1'])); request = getImportRulesRequest(hapiStream); - config = () => ({ - get: jest.fn(value => { - switch (value) { - case 'savedObjects.maxImportPayloadBytes': { - return 10000; - } - case 'savedObjects.maxImportExportSize': { - return 10000; - } - case 'xpack.siem.signalsIndex': { - return DEFAULT_SIGNALS_INDEX; - } - default: { - const dummyMock = jest.fn(); - return dummyMock(); - } - } - }), - has: jest.fn(), - }); - clients.clusterClient.callAsCurrentUser.mockResolvedValue(getNonEmptyIndex()); // index exists clients.alertsClient.find.mockResolvedValue(getEmptyFindResult()); // no extant rules diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/import_rules_route.ts b/x-pack/plugins/siem/server/lib/detection_engine/routes/rules/import_rules_route.ts similarity index 95% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/import_rules_route.ts rename to x-pack/plugins/siem/server/lib/detection_engine/routes/rules/import_rules_route.ts index 29ae5056a3ae8..527fab786910f 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/import_rules_route.ts +++ b/x-pack/plugins/siem/server/lib/detection_engine/routes/rules/import_rules_route.ts @@ -7,10 +7,10 @@ import { chunk } from 'lodash/fp'; import { extname } from 'path'; -import { IRouter } from '../../../../../../../../../src/core/server'; -import { createPromiseFromStreams } from '../../../../../../../../../src/legacy/utils/streams'; +import { IRouter } from '../../../../../../../../src/core/server'; +import { createPromiseFromStreams } from '../../../../../../../../src/legacy/utils/streams'; import { DETECTION_ENGINE_RULES_URL } from '../../../../../common/constants'; -import { LegacyServices } from '../../../../types'; +import { ConfigType } from '../../../..'; import { createRules } from '../../rules/create_rules'; import { ImportRulesRequestParams } from '../../rules/types'; import { readRules } from '../../rules/read_rules'; @@ -26,19 +26,19 @@ import { buildSiemResponse, validateLicenseForRuleType, } from '../utils'; -import { createRulesStreamFromNdJson } from '../../rules/create_rules_stream_from_ndjson'; import { ImportRuleAlertRest } from '../../types'; import { patchRules } from '../../rules/patch_rules'; import { importRulesQuerySchema, importRulesPayloadSchema } from '../schemas/import_rules_schema'; import { ImportRulesSchema, importRulesSchema } from '../schemas/response/import_rules_schema'; import { getTupleDuplicateErrorsAndUniqueRules } from './utils'; import { validate } from './validate'; +import { createRulesStreamFromNdJson } from '../../rules/create_rules_stream_from_ndjson'; type PromiseFromStreams = ImportRuleAlertRest | Error; const CHUNK_PARSED_OBJECT_SIZE = 10; -export const importRulesRoute = (router: IRouter, config: LegacyServices['config']) => { +export const importRulesRoute = (router: IRouter, config: ConfigType) => { router.post( { path: `${DETECTION_ENGINE_RULES_URL}/_import`, @@ -49,7 +49,7 @@ export const importRulesRoute = (router: IRouter, config: LegacyServices['config options: { tags: ['access:siem'], body: { - maxBytes: config().get('savedObjects.maxImportPayloadBytes'), + maxBytes: config.maxRuleImportPayloadBytes, output: 'stream', }, }, @@ -77,7 +77,7 @@ export const importRulesRoute = (router: IRouter, config: LegacyServices['config }); } - const objectLimit = config().get('savedObjects.maxImportExportSize'); + const objectLimit = config.maxRuleImportExportSize; const readStream = createRulesStreamFromNdJson(objectLimit); const parsedObjects = await createPromiseFromStreams([ request.body.file, @@ -138,7 +138,7 @@ export const importRulesRoute = (router: IRouter, config: LegacyServices['config timeline_id: timelineId, timeline_title: timelineTitle, version, - lists, + exceptions_list, } = parsedRule; try { @@ -195,7 +195,7 @@ export const importRulesRoute = (router: IRouter, config: LegacyServices['config references, note, version, - lists, + exceptions_list, actions: [], // Actions are not imported nor exported at this time }); resolve({ rule_id: ruleId, status_code: 200 }); @@ -232,7 +232,7 @@ export const importRulesRoute = (router: IRouter, config: LegacyServices['config references, note, version, - lists, + exceptions_list, anomalyThreshold, machineLearningJobId, }); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/patch_rules_bulk_route.test.ts b/x-pack/plugins/siem/server/lib/detection_engine/routes/rules/patch_rules_bulk_route.test.ts similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/patch_rules_bulk_route.test.ts rename to x-pack/plugins/siem/server/lib/detection_engine/routes/rules/patch_rules_bulk_route.test.ts diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/patch_rules_bulk_route.ts b/x-pack/plugins/siem/server/lib/detection_engine/routes/rules/patch_rules_bulk_route.ts similarity index 98% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/patch_rules_bulk_route.ts rename to x-pack/plugins/siem/server/lib/detection_engine/routes/rules/patch_rules_bulk_route.ts index 8c0fceb7a5f29..e4236f4632dcd 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/patch_rules_bulk_route.ts +++ b/x-pack/plugins/siem/server/lib/detection_engine/routes/rules/patch_rules_bulk_route.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { IRouter } from '../../../../../../../../../src/core/server'; +import { IRouter } from '../../../../../../../../src/core/server'; import { DETECTION_ENGINE_RULES_URL } from '../../../../../common/constants'; import { IRuleSavedAttributesSavedObjectAttributes, diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/patch_rules_route.test.ts b/x-pack/plugins/siem/server/lib/detection_engine/routes/rules/patch_rules_route.test.ts similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/patch_rules_route.test.ts rename to x-pack/plugins/siem/server/lib/detection_engine/routes/rules/patch_rules_route.test.ts diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/patch_rules_route.ts b/x-pack/plugins/siem/server/lib/detection_engine/routes/rules/patch_rules_route.ts similarity index 98% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/patch_rules_route.ts rename to x-pack/plugins/siem/server/lib/detection_engine/routes/rules/patch_rules_route.ts index 9c5000d70e5fe..23469144e11f8 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/patch_rules_route.ts +++ b/x-pack/plugins/siem/server/lib/detection_engine/routes/rules/patch_rules_route.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { IRouter } from '../../../../../../../../../src/core/server'; +import { IRouter } from '../../../../../../../../src/core/server'; import { DETECTION_ENGINE_RULES_URL } from '../../../../../common/constants'; import { patchRules } from '../../rules/patch_rules'; import { diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/read_rules_route.test.ts b/x-pack/plugins/siem/server/lib/detection_engine/routes/rules/read_rules_route.test.ts similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/read_rules_route.test.ts rename to x-pack/plugins/siem/server/lib/detection_engine/routes/rules/read_rules_route.test.ts diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/read_rules_route.ts b/x-pack/plugins/siem/server/lib/detection_engine/routes/rules/read_rules_route.ts similarity index 97% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/read_rules_route.ts rename to x-pack/plugins/siem/server/lib/detection_engine/routes/rules/read_rules_route.ts index 77747448e94fd..4d23e0217f2e8 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/read_rules_route.ts +++ b/x-pack/plugins/siem/server/lib/detection_engine/routes/rules/read_rules_route.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { IRouter } from '../../../../../../../../../src/core/server'; +import { IRouter } from '../../../../../../../../src/core/server'; import { DETECTION_ENGINE_RULES_URL } from '../../../../../common/constants'; import { getIdError } from './utils'; import { transformValidate } from './validate'; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/update_rules_bulk_route.test.ts b/x-pack/plugins/siem/server/lib/detection_engine/routes/rules/update_rules_bulk_route.test.ts similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/update_rules_bulk_route.test.ts rename to x-pack/plugins/siem/server/lib/detection_engine/routes/rules/update_rules_bulk_route.test.ts diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/update_rules_bulk_route.ts b/x-pack/plugins/siem/server/lib/detection_engine/routes/rules/update_rules_bulk_route.ts similarity index 97% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/update_rules_bulk_route.ts rename to x-pack/plugins/siem/server/lib/detection_engine/routes/rules/update_rules_bulk_route.ts index 36e15780f5cb3..6db91d74294fc 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/update_rules_bulk_route.ts +++ b/x-pack/plugins/siem/server/lib/detection_engine/routes/rules/update_rules_bulk_route.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { IRouter } from '../../../../../../../../../src/core/server'; +import { IRouter } from '../../../../../../../../src/core/server'; import { DETECTION_ENGINE_RULES_URL } from '../../../../../common/constants'; import { IRuleSavedAttributesSavedObjectAttributes, @@ -81,7 +81,7 @@ export const updateRulesBulkRoute = (router: IRouter) => { references, note, version, - lists, + exceptions_list, } = payloadRule; const finalIndex = outputIndex ?? siemClient.signalsIndex; const idOrRuleIdOrUnknown = id ?? ruleId ?? '(unknown id)'; @@ -121,7 +121,7 @@ export const updateRulesBulkRoute = (router: IRouter) => { references, note, version, - lists, + exceptions_list, actions, }); if (rule != null) { diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/update_rules_route.test.ts b/x-pack/plugins/siem/server/lib/detection_engine/routes/rules/update_rules_route.test.ts similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/update_rules_route.test.ts rename to x-pack/plugins/siem/server/lib/detection_engine/routes/rules/update_rules_route.test.ts diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/update_rules_route.ts b/x-pack/plugins/siem/server/lib/detection_engine/routes/rules/update_rules_route.ts similarity index 97% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/update_rules_route.ts rename to x-pack/plugins/siem/server/lib/detection_engine/routes/rules/update_rules_route.ts index 0444c757a9b31..7dbbe5a22ab46 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/update_rules_route.ts +++ b/x-pack/plugins/siem/server/lib/detection_engine/routes/rules/update_rules_route.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { IRouter } from '../../../../../../../../../src/core/server'; +import { IRouter } from '../../../../../../../../src/core/server'; import { DETECTION_ENGINE_RULES_URL } from '../../../../../common/constants'; import { UpdateRuleAlertParamsRest, @@ -67,7 +67,7 @@ export const updateRulesRoute = (router: IRouter) => { references, note, version, - lists, + exceptions_list, } = request.body; const siemResponse = buildSiemResponse(response); @@ -117,7 +117,7 @@ export const updateRulesRoute = (router: IRouter) => { references, note, version, - lists, + exceptions_list, actions: throttle === 'rule' ? actions : [], // Only enable actions if throttle is rule, otherwise we are a notification and should not enable it }); diff --git a/x-pack/plugins/siem/server/lib/detection_engine/routes/rules/utils.test.ts b/x-pack/plugins/siem/server/lib/detection_engine/routes/rules/utils.test.ts new file mode 100644 index 0000000000000..ec9e84d4fa6bb --- /dev/null +++ b/x-pack/plugins/siem/server/lib/detection_engine/routes/rules/utils.test.ts @@ -0,0 +1,628 @@ +/* + * 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'; +import { + transformAlertToRule, + getIdError, + transformFindAlerts, + transform, + transformTags, + getIdBulkError, + transformOrBulkError, + transformAlertsToRules, + transformOrImportError, + getDuplicates, + getTupleDuplicateErrorsAndUniqueRules, +} from './utils'; +import { getResult } from '../__mocks__/request_responses'; +import { INTERNAL_IDENTIFIER } from '../../../../../common/constants'; +import { ImportRuleAlertRest, RuleAlertParamsRest, RuleTypeParams } from '../../types'; +import { BulkError, ImportSuccessError } from '../utils'; +import { getSimpleRule, getOutputRuleAlertForRest } from '../__mocks__/utils'; +import { createPromiseFromStreams } from '../../../../../../../../src/legacy/utils/streams'; +import { PartialAlert } from '../../../../../../alerting/server'; +import { SanitizedAlert } from '../../../../../../alerting/server/types'; +import { createRulesStreamFromNdJson } from '../../rules/create_rules_stream_from_ndjson'; +import { RuleAlertType } from '../../rules/types'; +import { setFeatureFlagsForTestsOnly, unSetFeatureFlagsForTestsOnly } from '../../feature_flags'; + +type PromiseFromStreams = ImportRuleAlertRest | Error; + +describe('utils', () => { + beforeAll(() => { + setFeatureFlagsForTestsOnly(); + }); + + afterAll(() => { + unSetFeatureFlagsForTestsOnly(); + }); + + describe('transformAlertToRule', () => { + test('should work with a full data set', () => { + const fullRule = getResult(); + const rule = transformAlertToRule(fullRule); + expect(rule).toEqual(getOutputRuleAlertForRest()); + }); + + test('should work with a partial data set missing data', () => { + const fullRule = getResult(); + const { from, language, ...omitParams } = fullRule.params; + fullRule.params = omitParams as RuleTypeParams; + const rule = transformAlertToRule(fullRule); + const { + from: from2, + language: language2, + ...expectedWithoutFromWithoutLanguage + } = getOutputRuleAlertForRest(); + expect(rule).toEqual(expectedWithoutFromWithoutLanguage); + }); + + test('should omit query if query is null', () => { + const fullRule = getResult(); + fullRule.params.query = null; + const rule = transformAlertToRule(fullRule); + const { query, ...expectedWithoutQuery } = getOutputRuleAlertForRest(); + expect(rule).toEqual(expectedWithoutQuery); + }); + + test('should omit query if query is undefined', () => { + const fullRule = getResult(); + fullRule.params.query = undefined; + const rule = transformAlertToRule(fullRule); + const { query, ...expectedWithoutQuery } = getOutputRuleAlertForRest(); + expect(rule).toEqual(expectedWithoutQuery); + }); + + test('should omit a mix of undefined, null, and missing fields', () => { + const fullRule = getResult(); + fullRule.params.query = undefined; + fullRule.params.language = null; + const { from, ...omitParams } = fullRule.params; + fullRule.params = omitParams as RuleTypeParams; + const { enabled, ...omitEnabled } = fullRule; + const rule = transformAlertToRule(omitEnabled as RuleAlertType); + const { + from: from2, + enabled: enabled2, + language, + query, + ...expectedWithoutFromEnabledLanguageQuery + } = getOutputRuleAlertForRest(); + expect(rule).toEqual(expectedWithoutFromEnabledLanguageQuery); + }); + + test('should return enabled is equal to false', () => { + const fullRule = getResult(); + fullRule.enabled = false; + const ruleWithEnabledFalse = transformAlertToRule(fullRule); + const expected = getOutputRuleAlertForRest(); + expected.enabled = false; + expect(ruleWithEnabledFalse).toEqual(expected); + }); + + test('should return immutable is equal to false', () => { + const fullRule = getResult(); + fullRule.params.immutable = false; + const ruleWithEnabledFalse = transformAlertToRule(fullRule); + const expected = getOutputRuleAlertForRest(); + expect(ruleWithEnabledFalse).toEqual(expected); + }); + + test('should work with tags but filter out any internal tags', () => { + const fullRule = getResult(); + fullRule.tags = ['tag 1', 'tag 2', `${INTERNAL_IDENTIFIER}_some_other_value`]; + const rule = transformAlertToRule(fullRule); + const expected = getOutputRuleAlertForRest(); + expected.tags = ['tag 1', 'tag 2']; + expect(rule).toEqual(expected); + }); + + it('transforms ML Rule fields', () => { + const mlRule = getResult(); + mlRule.params.anomalyThreshold = 55; + mlRule.params.machineLearningJobId = 'some_job_id'; + mlRule.params.type = 'machine_learning'; + + const rule = transformAlertToRule(mlRule); + expect(rule).toEqual( + expect.objectContaining({ + anomaly_threshold: 55, + machine_learning_job_id: 'some_job_id', + type: 'machine_learning', + }) + ); + }); + }); + + describe('getIdError', () => { + test('it should have a status code', () => { + const error = getIdError({ id: '123', ruleId: undefined }); + expect(error).toEqual({ + message: 'id: "123" not found', + statusCode: 404, + }); + }); + + test('outputs message about id not being found if only id is defined and ruleId is undefined', () => { + const error = getIdError({ id: '123', ruleId: undefined }); + expect(error).toEqual({ + message: 'id: "123" not found', + statusCode: 404, + }); + }); + + test('outputs message about id not being found if only id is defined and ruleId is null', () => { + const error = getIdError({ id: '123', ruleId: null }); + expect(error).toEqual({ + message: 'id: "123" not found', + statusCode: 404, + }); + }); + + test('outputs message about ruleId not being found if only ruleId is defined and id is undefined', () => { + const error = getIdError({ id: undefined, ruleId: 'rule-id-123' }); + expect(error).toEqual({ + message: 'rule_id: "rule-id-123" not found', + statusCode: 404, + }); + }); + + test('outputs message about ruleId not being found if only ruleId is defined and id is null', () => { + const error = getIdError({ id: null, ruleId: 'rule-id-123' }); + expect(error).toEqual({ + message: 'rule_id: "rule-id-123" not found', + statusCode: 404, + }); + }); + + test('outputs message about both being not defined when both are undefined', () => { + const error = getIdError({ id: undefined, ruleId: undefined }); + expect(error).toEqual({ + message: 'id or rule_id should have been defined', + statusCode: 404, + }); + }); + + test('outputs message about both being not defined when both are null', () => { + const error = getIdError({ id: null, ruleId: null }); + expect(error).toEqual({ + message: 'id or rule_id should have been defined', + statusCode: 404, + }); + }); + + test('outputs message about both being not defined when id is null and ruleId is undefined', () => { + const error = getIdError({ id: null, ruleId: undefined }); + expect(error).toEqual({ + message: 'id or rule_id should have been defined', + statusCode: 404, + }); + }); + + test('outputs message about both being not defined when id is undefined and ruleId is null', () => { + const error = getIdError({ id: undefined, ruleId: null }); + expect(error).toEqual({ + message: 'id or rule_id should have been defined', + statusCode: 404, + }); + }); + }); + + describe('transformFindAlerts', () => { + test('outputs empty data set when data set is empty correct', () => { + const output = transformFindAlerts({ data: [], page: 1, perPage: 0, total: 0 }, []); + expect(output).toEqual({ data: [], page: 1, perPage: 0, total: 0 }); + }); + + test('outputs 200 if the data is of type siem alert', () => { + const output = transformFindAlerts( + { + page: 1, + perPage: 0, + total: 0, + data: [getResult()], + }, + [] + ); + const expected = getOutputRuleAlertForRest(); + expect(output).toEqual({ + page: 1, + perPage: 0, + total: 0, + data: [expected], + }); + }); + + test('returns 500 if the data is not of type siem alert', () => { + const unsafeCast = ([{ name: 'something else' }] as unknown) as SanitizedAlert[]; + const output = transformFindAlerts( + { + data: unsafeCast, + page: 1, + perPage: 1, + total: 1, + }, + [] + ); + expect(output).toBeNull(); + }); + }); + + describe('transform', () => { + test('outputs 200 if the data is of type siem alert', () => { + const output = transform(getResult()); + const expected = getOutputRuleAlertForRest(); + expect(output).toEqual(expected); + }); + + test('returns 500 if the data is not of type siem alert', () => { + const unsafeCast = ({ data: [{ random: 1 }] } as unknown) as PartialAlert; + const output = transform(unsafeCast); + expect(output).toBeNull(); + }); + }); + + describe('transformTags', () => { + test('it returns tags that have no internal structures', () => { + expect(transformTags(['tag 1', 'tag 2'])).toEqual(['tag 1', 'tag 2']); + }); + + test('it returns empty tags given empty tags', () => { + expect(transformTags([])).toEqual([]); + }); + + test('it returns tags with internal tags stripped out', () => { + expect(transformTags(['tag 1', `${INTERNAL_IDENTIFIER}_some_value`, 'tag 2'])).toEqual([ + 'tag 1', + 'tag 2', + ]); + }); + }); + + describe('getIdBulkError', () => { + test('outputs message about id and rule_id not being found if both are not null', () => { + const error = getIdBulkError({ id: '123', ruleId: '456' }); + const expected: BulkError = { + id: '123', + rule_id: '456', + error: { message: 'id: "123" and rule_id: "456" not found', status_code: 404 }, + }; + expect(error).toEqual(expected); + }); + + test('outputs message about id not being found if only id is defined and ruleId is undefined', () => { + const error = getIdBulkError({ id: '123', ruleId: undefined }); + const expected: BulkError = { + id: '123', + error: { message: 'id: "123" not found', status_code: 404 }, + }; + expect(error).toEqual(expected); + }); + + test('outputs message about id not being found if only id is defined and ruleId is null', () => { + const error = getIdBulkError({ id: '123', ruleId: null }); + const expected: BulkError = { + id: '123', + error: { message: 'id: "123" not found', status_code: 404 }, + }; + expect(error).toEqual(expected); + }); + + test('outputs message about ruleId not being found if only ruleId is defined and id is undefined', () => { + const error = getIdBulkError({ id: undefined, ruleId: 'rule-id-123' }); + const expected: BulkError = { + rule_id: 'rule-id-123', + error: { message: 'rule_id: "rule-id-123" not found', status_code: 404 }, + }; + expect(error).toEqual(expected); + }); + + test('outputs message about ruleId not being found if only ruleId is defined and id is null', () => { + const error = getIdBulkError({ id: null, ruleId: 'rule-id-123' }); + const expected: BulkError = { + rule_id: 'rule-id-123', + error: { message: 'rule_id: "rule-id-123" not found', status_code: 404 }, + }; + expect(error).toEqual(expected); + }); + + test('outputs message about both being not defined when both are undefined', () => { + const error = getIdBulkError({ id: undefined, ruleId: undefined }); + const expected: BulkError = { + rule_id: '(unknown id)', + error: { message: 'id or rule_id should have been defined', status_code: 404 }, + }; + expect(error).toEqual(expected); + }); + + test('outputs message about both being not defined when both are null', () => { + const error = getIdBulkError({ id: null, ruleId: null }); + const expected: BulkError = { + rule_id: '(unknown id)', + error: { message: 'id or rule_id should have been defined', status_code: 404 }, + }; + expect(error).toEqual(expected); + }); + + test('outputs message about both being not defined when id is null and ruleId is undefined', () => { + const error = getIdBulkError({ id: null, ruleId: undefined }); + const expected: BulkError = { + rule_id: '(unknown id)', + error: { message: 'id or rule_id should have been defined', status_code: 404 }, + }; + expect(error).toEqual(expected); + }); + + test('outputs message about both being not defined when id is undefined and ruleId is null', () => { + const error = getIdBulkError({ id: undefined, ruleId: null }); + const expected: BulkError = { + rule_id: '(unknown id)', + error: { message: 'id or rule_id should have been defined', status_code: 404 }, + }; + expect(error).toEqual(expected); + }); + }); + + describe('transformOrBulkError', () => { + test('outputs 200 if the data is of type siem alert', () => { + const output = transformOrBulkError('rule-1', getResult(), { + id: '04128c15-0d1b-4716-a4c5-46997ac7f3bd', + actions: [], + ruleThrottle: 'no_actions', + alertThrottle: null, + }); + const expected = getOutputRuleAlertForRest(); + expect(output).toEqual(expected); + }); + + test('returns 500 if the data is not of type siem alert', () => { + const unsafeCast = ({ name: 'something else' } as unknown) as PartialAlert; + const output = transformOrBulkError('rule-1', unsafeCast, { + id: '04128c15-0d1b-4716-a4c5-46997ac7f3bd', + actions: [], + ruleThrottle: 'no_actions', + alertThrottle: null, + }); + const expected: BulkError = { + rule_id: 'rule-1', + error: { message: 'Internal error transforming', status_code: 500 }, + }; + expect(output).toEqual(expected); + }); + }); + + describe('transformAlertsToRules', () => { + test('given an empty array returns an empty array', () => { + expect(transformAlertsToRules([])).toEqual([]); + }); + + test('given single alert will return the alert transformed', () => { + const result1 = getResult(); + const transformed = transformAlertsToRules([result1]); + const expected = getOutputRuleAlertForRest(); + expect(transformed).toEqual([expected]); + }); + + test('given two alerts will return the two alerts transformed', () => { + const result1 = getResult(); + const result2 = getResult(); + result2.id = 'some other id'; + result2.params.ruleId = 'some other id'; + + const transformed = transformAlertsToRules([result1, result2]); + const expected1 = getOutputRuleAlertForRest(); + const expected2 = getOutputRuleAlertForRest(); + expected2.id = 'some other id'; + expected2.rule_id = 'some other id'; + expect(transformed).toEqual([expected1, expected2]); + }); + }); + + describe('transformOrImportError', () => { + test('returns 1 given success if the alert is an alert type and the existing success count is 0', () => { + const output = transformOrImportError('rule-1', getResult(), { + success: true, + success_count: 0, + errors: [], + }); + const expected: ImportSuccessError = { + success: true, + errors: [], + success_count: 1, + }; + expect(output).toEqual(expected); + }); + + test('returns 2 given successes if the alert is an alert type and the existing success count is 1', () => { + const output = transformOrImportError('rule-1', getResult(), { + success: true, + success_count: 1, + errors: [], + }); + const expected: ImportSuccessError = { + success: true, + errors: [], + success_count: 2, + }; + expect(output).toEqual(expected); + }); + + test('returns 1 error and success of false if the data is not of type siem alert', () => { + const unsafeCast = ({ name: 'something else' } as unknown) as PartialAlert; + const output = transformOrImportError('rule-1', unsafeCast, { + success: true, + success_count: 1, + errors: [], + }); + const expected: ImportSuccessError = { + success: false, + errors: [ + { + rule_id: 'rule-1', + error: { + message: 'Internal error transforming', + status_code: 500, + }, + }, + ], + success_count: 1, + }; + expect(output).toEqual(expected); + }); + }); + + describe('getDuplicates', () => { + test("returns array of ruleIds showing the duplicate keys of 'value2' and 'value3'", () => { + const output = getDuplicates( + [ + { rule_id: 'value1' }, + { rule_id: 'value2' }, + { rule_id: 'value2' }, + { rule_id: 'value3' }, + { rule_id: 'value3' }, + {}, + {}, + ] as RuleAlertParamsRest[], + 'rule_id' + ); + const expected = ['value2', 'value3']; + expect(output).toEqual(expected); + }); + test('returns null when given a map of no duplicates', () => { + const output = getDuplicates( + [ + { rule_id: 'value1' }, + { rule_id: 'value2' }, + { rule_id: 'value3' }, + {}, + {}, + ] as RuleAlertParamsRest[], + 'rule_id' + ); + const expected: string[] = []; + expect(output).toEqual(expected); + }); + }); + + describe('getTupleDuplicateErrorsAndUniqueRules', () => { + test('returns tuple of empty duplicate errors array and rule array with instance of Syntax Error when imported rule contains parse error', async () => { + const multipartPayload = + '{"name"::"Simple Rule Query","description":"Simple Rule Query","risk_score":1,"rule_id":"rule-1","severity":"high","type":"query","query":"user.name: root or user.name: admin"}\n'; + const ndJsonStream = new Readable({ + read() { + this.push(multipartPayload); + this.push(null); + }, + }); + const rulesObjectsStream = createRulesStreamFromNdJson(1000); + const parsedObjects = await createPromiseFromStreams([ + ndJsonStream, + ...rulesObjectsStream, + ]); + const [errors, output] = getTupleDuplicateErrorsAndUniqueRules(parsedObjects, false); + const isInstanceOfError = output[0] instanceof Error; + + expect(isInstanceOfError).toEqual(true); + expect(errors.length).toEqual(0); + }); + + test('returns tuple of duplicate conflict error and single rule when rules with matching rule-ids passed in and `overwrite` is false', async () => { + const rule = getSimpleRule('rule-1'); + const rule2 = getSimpleRule('rule-1'); + const ndJsonStream = new Readable({ + read() { + this.push(`${JSON.stringify(rule)}\n`); + this.push(`${JSON.stringify(rule2)}\n`); + this.push(null); + }, + }); + const rulesObjectsStream = createRulesStreamFromNdJson(1000); + const parsedObjects = await createPromiseFromStreams([ + ndJsonStream, + ...rulesObjectsStream, + ]); + const [errors, output] = getTupleDuplicateErrorsAndUniqueRules(parsedObjects, false); + + expect(output.length).toEqual(1); + expect(errors).toEqual([ + { + error: { + message: 'More than one rule with rule-id: "rule-1" found', + status_code: 400, + }, + rule_id: 'rule-1', + }, + ]); + }); + + test('returns tuple of duplicate conflict error and single rule when rules with matching ids passed in and `overwrite` is false', async () => { + const rule = getSimpleRule('rule-1'); + delete rule.rule_id; + const rule2 = getSimpleRule('rule-1'); + delete rule2.rule_id; + const ndJsonStream = new Readable({ + read() { + this.push(`${JSON.stringify(rule)}\n`); + this.push(`${JSON.stringify(rule2)}\n`); + this.push(null); + }, + }); + const rulesObjectsStream = createRulesStreamFromNdJson(1000); + const parsedObjects = await createPromiseFromStreams([ + ndJsonStream, + ...rulesObjectsStream, + ]); + const [errors, output] = getTupleDuplicateErrorsAndUniqueRules(parsedObjects, false); + const isInstanceOfError = output[0] instanceof Error; + + expect(isInstanceOfError).toEqual(true); + expect(errors).toEqual([]); + }); + + test('returns tuple of empty duplicate errors array and single rule when rules with matching rule-ids passed in and `overwrite` is true', async () => { + const rule = getSimpleRule('rule-1'); + const rule2 = getSimpleRule('rule-1'); + const ndJsonStream = new Readable({ + read() { + this.push(`${JSON.stringify(rule)}\n`); + this.push(`${JSON.stringify(rule2)}\n`); + this.push(null); + }, + }); + const rulesObjectsStream = createRulesStreamFromNdJson(1000); + const parsedObjects = await createPromiseFromStreams([ + ndJsonStream, + ...rulesObjectsStream, + ]); + const [errors, output] = getTupleDuplicateErrorsAndUniqueRules(parsedObjects, true); + + expect(output.length).toEqual(1); + expect(errors.length).toEqual(0); + }); + + test('returns tuple of empty duplicate errors array and single rule when rules without a rule-id is passed in', async () => { + const simpleRule = getSimpleRule(); + delete simpleRule.rule_id; + const multipartPayload = `${JSON.stringify(simpleRule)}\n`; + const ndJsonStream = new Readable({ + read() { + this.push(multipartPayload); + this.push(null); + }, + }); + const rulesObjectsStream = createRulesStreamFromNdJson(1000); + const parsedObjects = await createPromiseFromStreams([ + ndJsonStream, + ...rulesObjectsStream, + ]); + const [errors, output] = getTupleDuplicateErrorsAndUniqueRules(parsedObjects, false); + const isInstanceOfError = output[0] instanceof Error; + + expect(isInstanceOfError).toEqual(true); + expect(errors.length).toEqual(0); + }); + }); +}); diff --git a/x-pack/plugins/siem/server/lib/detection_engine/routes/rules/utils.ts b/x-pack/plugins/siem/server/lib/detection_engine/routes/rules/utils.ts new file mode 100644 index 0000000000000..67b0c4462655c --- /dev/null +++ b/x-pack/plugins/siem/server/lib/detection_engine/routes/rules/utils.ts @@ -0,0 +1,294 @@ +/* + * 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 { pickBy, countBy } from 'lodash/fp'; +import { SavedObject, SavedObjectsFindResponse } from 'kibana/server'; +import uuid from 'uuid'; + +import { PartialAlert, FindResult } from '../../../../../../alerting/server'; +import { INTERNAL_IDENTIFIER } from '../../../../../common/constants'; +import { + RuleAlertType, + isAlertType, + isAlertTypes, + IRuleSavedAttributesSavedObjectAttributes, + isRuleStatusFindType, + isRuleStatusFindTypes, + isRuleStatusSavedObjectType, +} from '../../rules/types'; +import { OutputRuleAlertRest, ImportRuleAlertRest, RuleAlertParamsRest } from '../../types'; +import { + createBulkErrorObject, + BulkError, + createSuccessObject, + ImportSuccessError, + createImportErrorObject, + OutputError, +} from '../utils'; +import { hasListsFeature } from '../../feature_flags'; +// import { transformAlertToRuleAction } from '../../../../../common/detection_engine/transform_actions'; +import { RuleActions } from '../../rule_actions/types'; + +type PromiseFromStreams = ImportRuleAlertRest | Error; + +export const getIdError = ({ + id, + ruleId, +}: { + id: string | undefined | null; + ruleId: string | undefined | null; +}): OutputError => { + if (id != null) { + return { + message: `id: "${id}" not found`, + statusCode: 404, + }; + } else if (ruleId != null) { + return { + message: `rule_id: "${ruleId}" not found`, + statusCode: 404, + }; + } else { + return { + message: 'id or rule_id should have been defined', + statusCode: 404, + }; + } +}; + +export const getIdBulkError = ({ + id, + ruleId, +}: { + id: string | undefined | null; + ruleId: string | undefined | null; +}): BulkError => { + if (id != null && ruleId != null) { + return createBulkErrorObject({ + id, + ruleId, + statusCode: 404, + message: `id: "${id}" and rule_id: "${ruleId}" not found`, + }); + } else if (id != null) { + return createBulkErrorObject({ + id, + statusCode: 404, + message: `id: "${id}" not found`, + }); + } else if (ruleId != null) { + return createBulkErrorObject({ + ruleId, + statusCode: 404, + message: `rule_id: "${ruleId}" not found`, + }); + } else { + return createBulkErrorObject({ + statusCode: 404, + message: `id or rule_id should have been defined`, + }); + } +}; + +export const transformTags = (tags: string[]): string[] => { + return tags.filter(tag => !tag.startsWith(INTERNAL_IDENTIFIER)); +}; + +// Transforms the data but will remove any null or undefined it encounters and not include +// those on the export +export const transformAlertToRule = ( + alert: RuleAlertType, + ruleActions?: RuleActions | null, + ruleStatus?: SavedObject +): Partial => { + return pickBy((value: unknown) => value != null, { + actions: ruleActions?.actions ?? [], + created_at: alert.createdAt.toISOString(), + updated_at: alert.updatedAt.toISOString(), + created_by: alert.createdBy, + description: alert.params.description, + enabled: alert.enabled, + anomaly_threshold: alert.params.anomalyThreshold, + false_positives: alert.params.falsePositives, + filters: alert.params.filters, + from: alert.params.from, + id: alert.id, + immutable: alert.params.immutable, + index: alert.params.index, + interval: alert.schedule.interval, + rule_id: alert.params.ruleId, + language: alert.params.language, + output_index: alert.params.outputIndex, + max_signals: alert.params.maxSignals, + machine_learning_job_id: alert.params.machineLearningJobId, + risk_score: alert.params.riskScore, + name: alert.name, + query: alert.params.query, + references: alert.params.references, + saved_id: alert.params.savedId, + timeline_id: alert.params.timelineId, + timeline_title: alert.params.timelineTitle, + meta: alert.params.meta, + severity: alert.params.severity, + updated_by: alert.updatedBy, + tags: transformTags(alert.tags), + to: alert.params.to, + type: alert.params.type, + threat: alert.params.threat, + throttle: ruleActions?.ruleThrottle || 'no_actions', + note: alert.params.note, + version: alert.params.version, + status: ruleStatus?.attributes.status, + status_date: ruleStatus?.attributes.statusDate, + last_failure_at: ruleStatus?.attributes.lastFailureAt, + last_success_at: ruleStatus?.attributes.lastSuccessAt, + last_failure_message: ruleStatus?.attributes.lastFailureMessage, + last_success_message: ruleStatus?.attributes.lastSuccessMessage, + // TODO: (LIST-FEATURE) Remove hasListsFeature() check once we have lists available for a release + exceptions_list: hasListsFeature() ? alert.params.exceptions_list : null, + }); +}; + +export const transformAlertsToRules = ( + alerts: RuleAlertType[] +): Array> => { + return alerts.map(alert => transformAlertToRule(alert)); +}; + +export const transformFindAlerts = ( + findResults: FindResult, + ruleActions: Array, + ruleStatuses?: Array> +): { + page: number; + perPage: number; + total: number; + data: Array>; +} | null => { + if (!ruleStatuses && isAlertTypes(findResults.data)) { + return { + page: findResults.page, + perPage: findResults.perPage, + total: findResults.total, + data: findResults.data.map((alert, idx) => transformAlertToRule(alert, ruleActions[idx])), + }; + } else if (isAlertTypes(findResults.data) && isRuleStatusFindTypes(ruleStatuses)) { + return { + page: findResults.page, + perPage: findResults.perPage, + total: findResults.total, + data: findResults.data.map((alert, idx) => + transformAlertToRule(alert, ruleActions[idx], ruleStatuses[idx].saved_objects[0]) + ), + }; + } else { + return null; + } +}; + +export const transform = ( + alert: PartialAlert, + ruleActions?: RuleActions | null, + ruleStatus?: SavedObject +): Partial | null => { + if (isAlertType(alert)) { + return transformAlertToRule( + alert, + ruleActions, + isRuleStatusSavedObjectType(ruleStatus) ? ruleStatus : undefined + ); + } + + return null; +}; + +export const transformOrBulkError = ( + ruleId: string, + alert: PartialAlert, + ruleActions: RuleActions, + ruleStatus?: unknown +): Partial | BulkError => { + if (isAlertType(alert)) { + if (isRuleStatusFindType(ruleStatus) && ruleStatus?.saved_objects.length > 0) { + return transformAlertToRule(alert, ruleActions, ruleStatus?.saved_objects[0] ?? ruleStatus); + } else { + return transformAlertToRule(alert, ruleActions); + } + } else { + return createBulkErrorObject({ + ruleId, + statusCode: 500, + message: 'Internal error transforming', + }); + } +}; + +export const transformOrImportError = ( + ruleId: string, + alert: PartialAlert, + existingImportSuccessError: ImportSuccessError +): ImportSuccessError => { + if (isAlertType(alert)) { + return createSuccessObject(existingImportSuccessError); + } else { + return createImportErrorObject({ + ruleId, + statusCode: 500, + message: 'Internal error transforming', + existingImportSuccessError, + }); + } +}; + +export const getDuplicates = (ruleDefinitions: RuleAlertParamsRest[], by: 'rule_id'): string[] => { + const mappedDuplicates = countBy( + by, + ruleDefinitions.filter(r => r[by] != null) + ); + const hasDuplicates = Object.values(mappedDuplicates).some(i => i > 1); + if (hasDuplicates) { + return Object.keys(mappedDuplicates).filter(key => mappedDuplicates[key] > 1); + } + return []; +}; + +export const getTupleDuplicateErrorsAndUniqueRules = ( + rules: PromiseFromStreams[], + isOverwrite: boolean +): [BulkError[], PromiseFromStreams[]] => { + const { errors, rulesAcc } = rules.reduce( + (acc, parsedRule) => { + if (parsedRule instanceof Error) { + acc.rulesAcc.set(uuid.v4(), parsedRule); + } else { + const { rule_id: ruleId } = parsedRule; + if (ruleId != null) { + if (acc.rulesAcc.has(ruleId) && !isOverwrite) { + acc.errors.set( + uuid.v4(), + createBulkErrorObject({ + ruleId, + statusCode: 400, + message: `More than one rule with rule-id: "${ruleId}" found`, + }) + ); + } + acc.rulesAcc.set(ruleId, parsedRule); + } else { + acc.rulesAcc.set(uuid.v4(), parsedRule); + } + } + + return acc; + }, // using map (preserves ordering) + { + errors: new Map(), + rulesAcc: new Map(), + } + ); + + return [Array.from(errors.values()), Array.from(rulesAcc.values())]; +}; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/validate.test.ts b/x-pack/plugins/siem/server/lib/detection_engine/routes/rules/validate.test.ts similarity index 98% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/validate.test.ts rename to x-pack/plugins/siem/server/lib/detection_engine/routes/rules/validate.test.ts index 7537401e5a366..9069202d4d3aa 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/validate.test.ts +++ b/x-pack/plugins/siem/server/lib/detection_engine/routes/rules/validate.test.ts @@ -13,7 +13,7 @@ import { transformValidateBulkError, } from './validate'; import { getResult } from '../__mocks__/request_responses'; -import { FindResult } from '../../../../../../../../plugins/alerting/server'; +import { FindResult } from '../../../../../../alerting/server'; import { RulesSchema } from '../schemas/response/rules_schema'; import { BulkError } from '../utils'; import { setFeatureFlagsForTestsOnly, unSetFeatureFlagsForTestsOnly } from '../../feature_flags'; @@ -71,7 +71,7 @@ export const ruleOutput: RulesSchema = { }, }, ], - lists: [ + exceptions_list: [ { field: 'source.ip', values_operator: 'included', diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/validate.ts b/x-pack/plugins/siem/server/lib/detection_engine/routes/rules/validate.ts similarity index 97% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/validate.ts rename to x-pack/plugins/siem/server/lib/detection_engine/routes/rules/validate.ts index 1f3d1ec856684..c207d075331b6 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/validate.ts +++ b/x-pack/plugins/siem/server/lib/detection_engine/routes/rules/validate.ts @@ -9,7 +9,7 @@ import { fold } from 'fp-ts/lib/Either'; import { pipe } from 'fp-ts/lib/pipeable'; import * as t from 'io-ts'; -import { PartialAlert, FindResult } from '../../../../../../../../plugins/alerting/server'; +import { PartialAlert, FindResult } from '../../../../../../alerting/server'; import { formatErrors } from '../schemas/response/utils'; import { isAlertType, diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/add_prepackaged_rules_schema.test.ts b/x-pack/plugins/siem/server/lib/detection_engine/routes/schemas/add_prepackaged_rules_schema.test.ts similarity index 98% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/add_prepackaged_rules_schema.test.ts rename to x-pack/plugins/siem/server/lib/detection_engine/routes/schemas/add_prepackaged_rules_schema.test.ts index 8c741c937bf15..226dea7c20344 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/add_prepackaged_rules_schema.test.ts +++ b/x-pack/plugins/siem/server/lib/detection_engine/routes/schemas/add_prepackaged_rules_schema.test.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { AlertAction } from '../../../../../../../../plugins/alerting/common'; +import { AlertAction } from '../../../../../../alerting/common'; import { RuleAlertAction } from '../../../../../common/detection_engine/types'; import { ThreatParams, PrepackagedRules } from '../../types'; import { addPrepackagedRulesSchema } from './add_prepackaged_rules_schema'; @@ -1542,8 +1542,8 @@ describe('add prepackaged rules schema', () => { // on demand. Since they are per module, we have a an issue where the ENV variables do not take effect. It is better we change all the // schema's to be function calls to avoid global side effects or just wait until the feature is available. If you want to test this early, // you can remove the .skip and set your env variable of export ELASTIC_XPACK_SIEM_LISTS_FEATURE=true locally - describe.skip('lists', () => { - test('[rule_id, description, from, to, index, name, severity, interval, type, filter, risk_score, note, and lists] does validate', () => { + describe.skip('exceptions_list', () => { + test('[rule_id, description, from, to, index, name, severity, interval, type, filter, risk_score, note, and exceptions_list] does validate', () => { expect( addPrepackagedRulesSchema.validate>({ rule_id: 'rule-1', @@ -1558,7 +1558,7 @@ describe('add prepackaged rules schema', () => { risk_score: 50, note: '# some markdown', version: 1, - lists: [ + exceptions_list: [ { field: 'source.ip', values_operator: 'included', @@ -1594,7 +1594,7 @@ describe('add prepackaged rules schema', () => { ).toBeFalsy(); }); - test('[rule_id, description, from, to, index, name, severity, interval, type, filter, risk_score, note, and empty lists] does validate', () => { + test('[rule_id, description, from, to, index, name, severity, interval, type, filter, risk_score, note, and empty exceptions_list] does validate', () => { expect( addPrepackagedRulesSchema.validate>({ rule_id: 'rule-1', @@ -1608,15 +1608,15 @@ describe('add prepackaged rules schema', () => { type: 'query', risk_score: 50, note: '# some markdown', - lists: [], + exceptions_list: [], version: 1, }).error ).toBeFalsy(); }); - test('[rule_id, description, from, to, index, name, severity, interval, type, filter, risk_score, note, and invalid lists] does NOT validate', () => { + test('[rule_id, description, from, to, index, name, severity, interval, type, filter, risk_score, note, and invalid exceptions_list] does NOT validate', () => { expect( - addPrepackagedRulesSchema.validate>>({ + addPrepackagedRulesSchema.validate>>({ rule_id: 'rule-1', description: 'some description', from: 'now-5m', @@ -1628,17 +1628,17 @@ describe('add prepackaged rules schema', () => { type: 'query', risk_score: 50, note: '# some markdown', - lists: [{ invalid_value: 'invalid value' }], + exceptions_list: [{ invalid_value: 'invalid value' }], version: 1, }).error.message ).toEqual( - 'child "lists" fails because ["lists" at position 0 fails because [child "field" fails because ["field" is required]]]' + 'child "exceptions_list" fails because ["exceptions_list" at position 0 fails because [child "field" fails because ["field" is required]]]' ); }); - test('[rule_id, description, from, to, index, name, severity, interval, type, filter, risk_score, note, and non-existent lists] does validate with empty lists', () => { + test('[rule_id, description, from, to, index, name, severity, interval, type, filter, risk_score, note, and non-existent exceptions_list] does validate with empty exceptions_list', () => { expect( - addPrepackagedRulesSchema.validate>>({ + addPrepackagedRulesSchema.validate>>({ rule_id: 'rule-1', description: 'some description', from: 'now-5m', @@ -1651,7 +1651,7 @@ describe('add prepackaged rules schema', () => { risk_score: 50, note: '# some markdown', version: 1, - }).value.lists + }).value.exceptions_list ).toEqual([]); }); }); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/add_prepackaged_rules_schema.ts b/x-pack/plugins/siem/server/lib/detection_engine/routes/schemas/add_prepackaged_rules_schema.ts similarity index 97% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/add_prepackaged_rules_schema.ts rename to x-pack/plugins/siem/server/lib/detection_engine/routes/schemas/add_prepackaged_rules_schema.ts index 006fc81e3ee87..0e82a9b979c7b 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/add_prepackaged_rules_schema.ts +++ b/x-pack/plugins/siem/server/lib/detection_engine/routes/schemas/add_prepackaged_rules_schema.ts @@ -114,5 +114,5 @@ export const addPrepackagedRulesSchema = Joi.object({ version: version.required(), // TODO: (LIST-FEATURE) Remove the hasListsFeatures once this is ready for release - lists: hasListsFeature() ? lists.default([]) : lists.forbidden().default([]), + exceptions_list: hasListsFeature() ? lists.default([]) : lists.forbidden().default([]), }); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/create_rules_bulk_schema.test.ts b/x-pack/plugins/siem/server/lib/detection_engine/routes/schemas/create_rules_bulk_schema.test.ts similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/create_rules_bulk_schema.test.ts rename to x-pack/plugins/siem/server/lib/detection_engine/routes/schemas/create_rules_bulk_schema.test.ts diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/create_rules_bulk_schema.ts b/x-pack/plugins/siem/server/lib/detection_engine/routes/schemas/create_rules_bulk_schema.ts similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/create_rules_bulk_schema.ts rename to x-pack/plugins/siem/server/lib/detection_engine/routes/schemas/create_rules_bulk_schema.ts diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/create_rules_schema.test.ts b/x-pack/plugins/siem/server/lib/detection_engine/routes/schemas/create_rules_schema.test.ts similarity index 97% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/create_rules_schema.test.ts rename to x-pack/plugins/siem/server/lib/detection_engine/routes/schemas/create_rules_schema.test.ts index e56e8e5fe34d3..1e2941015b735 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/create_rules_schema.test.ts +++ b/x-pack/plugins/siem/server/lib/detection_engine/routes/schemas/create_rules_schema.test.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { AlertAction } from '../../../../../../../../plugins/alerting/common'; +import { AlertAction } from '../../../../../../alerting/common'; import { createRulesSchema } from './create_rules_schema'; import { PatchRuleAlertParamsRest } from '../../rules/types'; import { RuleAlertAction } from '../../../../../common/detection_engine/types'; @@ -556,7 +556,9 @@ describe('create rules schema', () => { test('language does not validate with something made up', () => { expect( - createRulesSchema.validate>({ + createRulesSchema.validate< + Partial & { language: string }> + >({ rule_id: 'rule-1', output_index: '.siem-signals', risk_score: 50, @@ -1508,8 +1510,8 @@ describe('create rules schema', () => { // on demand. Since they are per module, we have a an issue where the ENV variables do not take effect. It is better we change all the // schema's to be function calls to avoid global side effects or just wait until the feature is available. If you want to test this early, // you can remove the .skip and set your env variable of export ELASTIC_XPACK_SIEM_LISTS_FEATURE=true locally - describe.skip('lists', () => { - test('[rule_id, description, from, to, index, name, severity, interval, type, filter, risk_score, note, and lists] does validate', () => { + describe.skip('exceptions_list', () => { + test('[rule_id, description, from, to, index, name, severity, interval, type, filter, risk_score, note, and exceptions_list] does validate', () => { expect( createRulesSchema.validate>({ rule_id: 'rule-1', @@ -1523,7 +1525,7 @@ describe('create rules schema', () => { type: 'query', risk_score: 50, note: '# some markdown', - lists: [ + exceptions_list: [ { field: 'source.ip', values_operator: 'included', @@ -1559,7 +1561,7 @@ describe('create rules schema', () => { ).toBeFalsy(); }); - test('[rule_id, description, from, to, index, name, severity, interval, type, filter, risk_score, note, and empty lists] does validate', () => { + test('[rule_id, description, from, to, index, name, severity, interval, type, filter, risk_score, note, and empty exceptions_list] does validate', () => { expect( createRulesSchema.validate>({ rule_id: 'rule-1', @@ -1573,14 +1575,14 @@ describe('create rules schema', () => { type: 'query', risk_score: 50, note: '# some markdown', - lists: [], + exceptions_list: [], }).error ).toBeFalsy(); }); - test('[rule_id, description, from, to, index, name, severity, interval, type, filter, risk_score, note, and invalid lists] does NOT validate', () => { + test('[rule_id, description, from, to, index, name, severity, interval, type, filter, risk_score, note, and invalid exceptions_list] does NOT validate', () => { expect( - createRulesSchema.validate>>({ + createRulesSchema.validate>>({ rule_id: 'rule-1', description: 'some description', from: 'now-5m', @@ -1592,16 +1594,16 @@ describe('create rules schema', () => { type: 'query', risk_score: 50, note: '# some markdown', - lists: [{ invalid_value: 'invalid value' }], + exceptions_list: [{ invalid_value: 'invalid value' }], }).error.message ).toEqual( - 'child "lists" fails because ["lists" at position 0 fails because [child "field" fails because ["field" is required]]]' + 'child "exceptions_list" fails because ["exceptions_list" at position 0 fails because [child "field" fails because ["field" is required]]]' ); }); - test('[rule_id, description, from, to, index, name, severity, interval, type, filter, risk_score, note, and non-existent lists] does validate with empty lists', () => { + test('[rule_id, description, from, to, index, name, severity, interval, type, filter, risk_score, note, and non-existent exceptions_list] does validate with empty exceptions_list', () => { expect( - createRulesSchema.validate>>({ + createRulesSchema.validate>>({ rule_id: 'rule-1', description: 'some description', from: 'now-5m', @@ -1613,7 +1615,7 @@ describe('create rules schema', () => { type: 'query', risk_score: 50, note: '# some markdown', - }).value.lists + }).value.exceptions_list ).toEqual([]); }); }); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/create_rules_schema.ts b/x-pack/plugins/siem/server/lib/detection_engine/routes/schemas/create_rules_schema.ts similarity index 96% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/create_rules_schema.ts rename to x-pack/plugins/siem/server/lib/detection_engine/routes/schemas/create_rules_schema.ts index 5213f3faaf486..dec8b5ccbc790 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/create_rules_schema.ts +++ b/x-pack/plugins/siem/server/lib/detection_engine/routes/schemas/create_rules_schema.ts @@ -98,5 +98,5 @@ export const createRulesSchema = Joi.object({ version: version.default(1), // TODO: (LIST-FEATURE) Remove the hasListsFeatures once this is ready for release - lists: hasListsFeature() ? lists.default([]) : lists.forbidden().default([]), + exceptions_list: hasListsFeature() ? lists.default([]) : lists.forbidden().default([]), }); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/export_rules_schema.test.ts b/x-pack/plugins/siem/server/lib/detection_engine/routes/schemas/export_rules_schema.test.ts similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/export_rules_schema.test.ts rename to x-pack/plugins/siem/server/lib/detection_engine/routes/schemas/export_rules_schema.test.ts diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/export_rules_schema.ts b/x-pack/plugins/siem/server/lib/detection_engine/routes/schemas/export_rules_schema.ts similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/export_rules_schema.ts rename to x-pack/plugins/siem/server/lib/detection_engine/routes/schemas/export_rules_schema.ts diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/find_rules_schema.test.ts b/x-pack/plugins/siem/server/lib/detection_engine/routes/schemas/find_rules_schema.test.ts similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/find_rules_schema.test.ts rename to x-pack/plugins/siem/server/lib/detection_engine/routes/schemas/find_rules_schema.test.ts diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/find_rules_schema.ts b/x-pack/plugins/siem/server/lib/detection_engine/routes/schemas/find_rules_schema.ts similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/find_rules_schema.ts rename to x-pack/plugins/siem/server/lib/detection_engine/routes/schemas/find_rules_schema.ts diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/find_rules_statuses_schema.ts b/x-pack/plugins/siem/server/lib/detection_engine/routes/schemas/find_rules_statuses_schema.ts similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/find_rules_statuses_schema.ts rename to x-pack/plugins/siem/server/lib/detection_engine/routes/schemas/find_rules_statuses_schema.ts diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/import_rules_schema.test.ts b/x-pack/plugins/siem/server/lib/detection_engine/routes/schemas/import_rules_schema.test.ts similarity index 98% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/import_rules_schema.test.ts rename to x-pack/plugins/siem/server/lib/detection_engine/routes/schemas/import_rules_schema.test.ts index 40f7b19ea12b3..d28530ffb789e 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/import_rules_schema.test.ts +++ b/x-pack/plugins/siem/server/lib/detection_engine/routes/schemas/import_rules_schema.test.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { AlertAction } from '../../../../../../../../plugins/alerting/common'; +import { AlertAction } from '../../../../../../alerting/common'; import { importRulesSchema, importRulesQuerySchema, @@ -1729,8 +1729,8 @@ describe('import rules schema', () => { // on demand. Since they are per module, we have a an issue where the ENV variables do not take effect. It is better we change all the // schema's to be function calls to avoid global side effects or just wait until the feature is available. If you want to test this early, // you can remove the .skip and set your env variable of export ELASTIC_XPACK_SIEM_LISTS_FEATURE=true locally - describe.skip('lists', () => { - test('[rule_id, description, from, to, index, name, severity, interval, type, filter, risk_score, note, and lists] does validate', () => { + describe.skip('exceptions_list', () => { + test('[rule_id, description, from, to, index, name, severity, interval, type, filter, risk_score, note, and exceptions_list] does validate', () => { expect( importRulesSchema.validate>({ rule_id: 'rule-1', @@ -1744,7 +1744,7 @@ describe('import rules schema', () => { type: 'query', risk_score: 50, note: '# some markdown', - lists: [ + exceptions_list: [ { field: 'source.ip', values_operator: 'included', @@ -1780,7 +1780,7 @@ describe('import rules schema', () => { ).toBeFalsy(); }); - test('[rule_id, description, from, to, index, name, severity, interval, type, filter, risk_score, note, and empty lists] does validate', () => { + test('[rule_id, description, from, to, index, name, severity, interval, type, filter, risk_score, note, and empty exceptions_list] does validate', () => { expect( importRulesSchema.validate>({ rule_id: 'rule-1', @@ -1794,14 +1794,14 @@ describe('import rules schema', () => { type: 'query', risk_score: 50, note: '# some markdown', - lists: [], + exceptions_list: [], }).error ).toBeFalsy(); }); - test('[rule_id, description, from, to, index, name, severity, interval, type, filter, risk_score, note, and invalid lists] does NOT validate and lists is empty', () => { + test('[rule_id, description, from, to, index, name, severity, interval, type, filter, risk_score, note, and invalid exceptions_list] does NOT validate and exceptions_list is empty', () => { expect( - importRulesSchema.validate>>({ + importRulesSchema.validate>>({ rule_id: 'rule-1', description: 'some description', from: 'now-5m', @@ -1813,16 +1813,16 @@ describe('import rules schema', () => { type: 'query', risk_score: 50, note: '# some markdown', - lists: [{ invalid_value: 'invalid value' }], + exceptions_list: [{ invalid_value: 'invalid value' }], }).error.message ).toEqual( - 'child "lists" fails because ["lists" at position 0 fails because [child "field" fails because ["field" is required]]]' + 'child "exceptions_list" fails because ["exceptions_list" at position 0 fails because [child "field" fails because ["field" is required]]]' ); }); - test('[rule_id, description, from, to, index, name, severity, interval, type, filter, risk_score, note, and non-existent lists] does validate', () => { + test('[rule_id, description, from, to, index, name, severity, interval, type, filter, risk_score, note, and non-existent exceptions_list] does validate', () => { expect( - importRulesSchema.validate>>({ + importRulesSchema.validate>>({ rule_id: 'rule-1', description: 'some description', from: 'now-5m', @@ -1834,7 +1834,7 @@ describe('import rules schema', () => { type: 'query', risk_score: 50, note: '# some markdown', - }).value.lists + }).value.exceptions_list ).toEqual([]); }); }); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/import_rules_schema.ts b/x-pack/plugins/siem/server/lib/detection_engine/routes/schemas/import_rules_schema.ts similarity index 97% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/import_rules_schema.ts rename to x-pack/plugins/siem/server/lib/detection_engine/routes/schemas/import_rules_schema.ts index 56aa45659fda7..d3c728ebac1a9 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/import_rules_schema.ts +++ b/x-pack/plugins/siem/server/lib/detection_engine/routes/schemas/import_rules_schema.ts @@ -119,7 +119,7 @@ export const importRulesSchema = Joi.object({ updated_by, // TODO: (LIST-FEATURE) Remove the hasListsFeatures once this is ready for release - lists: hasListsFeature() ? lists.default([]) : lists.forbidden().default([]), + exceptions_list: hasListsFeature() ? lists.default([]) : lists.forbidden().default([]), }); export const importRulesQuerySchema = Joi.object({ diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/patch_rules_bulk_schema.test.ts b/x-pack/plugins/siem/server/lib/detection_engine/routes/schemas/patch_rules_bulk_schema.test.ts similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/patch_rules_bulk_schema.test.ts rename to x-pack/plugins/siem/server/lib/detection_engine/routes/schemas/patch_rules_bulk_schema.test.ts diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/patch_rules_bulk_schema.ts b/x-pack/plugins/siem/server/lib/detection_engine/routes/schemas/patch_rules_bulk_schema.ts similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/patch_rules_bulk_schema.ts rename to x-pack/plugins/siem/server/lib/detection_engine/routes/schemas/patch_rules_bulk_schema.ts diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/patch_rules_schema.test.ts b/x-pack/plugins/siem/server/lib/detection_engine/routes/schemas/patch_rules_schema.test.ts similarity index 97% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/patch_rules_schema.test.ts rename to x-pack/plugins/siem/server/lib/detection_engine/routes/schemas/patch_rules_schema.test.ts index e01a8f40fcea4..755c0b2ccaa3f 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/patch_rules_schema.test.ts +++ b/x-pack/plugins/siem/server/lib/detection_engine/routes/schemas/patch_rules_schema.test.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { AlertAction } from '../../../../../../../../plugins/alerting/common'; +import { AlertAction } from '../../../../../../alerting/common'; import { patchRulesSchema } from './patch_rules_schema'; import { PatchRuleAlertParamsRest } from '../../rules/types'; import { RuleAlertAction } from '../../../../../common/detection_engine/types'; @@ -572,7 +572,9 @@ describe('patch rules schema', () => { test('language does not validate with something made up', () => { expect( - patchRulesSchema.validate>({ + patchRulesSchema.validate< + Partial & { language: string }> + >({ id: 'rule-1', description: 'some description', from: 'now-5m', @@ -1211,8 +1213,8 @@ describe('patch rules schema', () => { // on demand. Since they are per module, we have a an issue where the ENV variables do not take effect. It is better we change all the // schema's to be function calls to avoid global side effects or just wait until the feature is available. If you want to test this early, // you can remove the .skip and set your env variable of export ELASTIC_XPACK_SIEM_LISTS_FEATURE=true locally - describe.skip('lists', () => { - test('[rule_id, description, from, to, index, name, severity, interval, type, filter, risk_score, note, and lists] does validate', () => { + describe.skip('exceptions_list', () => { + test('[rule_id, description, from, to, index, name, severity, interval, type, filter, risk_score, note, and exceptions_list] does validate', () => { expect( patchRulesSchema.validate>({ rule_id: 'rule-1', @@ -1226,7 +1228,7 @@ describe('patch rules schema', () => { type: 'query', risk_score: 50, note: '# some markdown', - lists: [ + exceptions_list: [ { field: 'source.ip', values_operator: 'included', @@ -1262,11 +1264,11 @@ describe('patch rules schema', () => { ).toBeFalsy(); }); - test('lists can be patched', () => { + test('exceptions_list can be patched', () => { expect( patchRulesSchema.validate>({ rule_id: 'some id', - lists: [ + exceptions_list: [ { field: 'source.ip', values_operator: 'included', @@ -1299,7 +1301,7 @@ describe('patch rules schema', () => { ).toBeFalsy(); }); - test('[rule_id, description, from, to, index, name, severity, interval, type, filter, risk_score, note, and empty lists] does validate', () => { + test('[rule_id, description, from, to, index, name, severity, interval, type, filter, risk_score, note, and empty exceptions_list] does validate', () => { expect( patchRulesSchema.validate>({ rule_id: 'rule-1', @@ -1313,14 +1315,14 @@ describe('patch rules schema', () => { type: 'query', risk_score: 50, note: '# some markdown', - lists: [], + exceptions_list: [], }).error ).toBeFalsy(); }); - test('[rule_id, description, from, to, index, name, severity, interval, type, filter, risk_score, note, and invalid lists] does NOT validate', () => { + test('[rule_id, description, from, to, index, name, severity, interval, type, filter, risk_score, note, and invalid exceptions_list] does NOT validate', () => { expect( - patchRulesSchema.validate>>({ + patchRulesSchema.validate>>({ rule_id: 'rule-1', description: 'some description', from: 'now-5m', @@ -1332,16 +1334,16 @@ describe('patch rules schema', () => { type: 'query', risk_score: 50, note: '# some markdown', - lists: [{ invalid_value: 'invalid value' }], + exceptions_list: [{ invalid_value: 'invalid value' }], }).error.message ).toEqual( - 'child "lists" fails because ["lists" at position 0 fails because [child "field" fails because ["field" is required]]]' + 'child "exceptions_list" fails because ["exceptions_list" at position 0 fails because [child "field" fails because ["field" is required]]]' ); }); - test('[rule_id, description, from, to, index, name, severity, interval, type, filter, risk_score, note, and non-existent lists] does validate with empty lists', () => { + test('[rule_id, description, from, to, index, name, severity, interval, type, filter, risk_score, note, and non-existent exceptions_list] does validate with empty exceptions_list', () => { expect( - patchRulesSchema.validate>>({ + patchRulesSchema.validate>>({ rule_id: 'rule-1', description: 'some description', from: 'now-5m', @@ -1353,7 +1355,7 @@ describe('patch rules schema', () => { type: 'query', risk_score: 50, note: '# some markdown', - }).value.lists + }).value.exceptions_list ).toEqual([]); }); }); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/patch_rules_schema.ts b/x-pack/plugins/siem/server/lib/detection_engine/routes/schemas/patch_rules_schema.ts similarity index 93% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/patch_rules_schema.ts rename to x-pack/plugins/siem/server/lib/detection_engine/routes/schemas/patch_rules_schema.ts index 52aefa29884c3..503bc64df237c 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/patch_rules_schema.ts +++ b/x-pack/plugins/siem/server/lib/detection_engine/routes/schemas/patch_rules_schema.ts @@ -78,5 +78,5 @@ export const patchRulesSchema = Joi.object({ version, // TODO: (LIST-FEATURE) Remove the hasListsFeatures once this is ready for release - lists: hasListsFeature() ? lists.default([]) : lists.forbidden().default([]), + exceptions_list: hasListsFeature() ? lists.default([]) : lists.forbidden().default([]), }).xor('id', 'rule_id'); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/query_rules_bulk_schema.test.ts b/x-pack/plugins/siem/server/lib/detection_engine/routes/schemas/query_rules_bulk_schema.test.ts similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/query_rules_bulk_schema.test.ts rename to x-pack/plugins/siem/server/lib/detection_engine/routes/schemas/query_rules_bulk_schema.test.ts diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/query_rules_bulk_schema.ts b/x-pack/plugins/siem/server/lib/detection_engine/routes/schemas/query_rules_bulk_schema.ts similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/query_rules_bulk_schema.ts rename to x-pack/plugins/siem/server/lib/detection_engine/routes/schemas/query_rules_bulk_schema.ts diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/query_rules_schema.test.ts b/x-pack/plugins/siem/server/lib/detection_engine/routes/schemas/query_rules_schema.test.ts similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/query_rules_schema.test.ts rename to x-pack/plugins/siem/server/lib/detection_engine/routes/schemas/query_rules_schema.test.ts diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/query_rules_schema.ts b/x-pack/plugins/siem/server/lib/detection_engine/routes/schemas/query_rules_schema.ts similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/query_rules_schema.ts rename to x-pack/plugins/siem/server/lib/detection_engine/routes/schemas/query_rules_schema.ts diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/query_signals_index_schema.test.ts b/x-pack/plugins/siem/server/lib/detection_engine/routes/schemas/query_signals_index_schema.test.ts similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/query_signals_index_schema.test.ts rename to x-pack/plugins/siem/server/lib/detection_engine/routes/schemas/query_signals_index_schema.test.ts diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/query_signals_index_schema.ts b/x-pack/plugins/siem/server/lib/detection_engine/routes/schemas/query_signals_index_schema.ts similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/query_signals_index_schema.ts rename to x-pack/plugins/siem/server/lib/detection_engine/routes/schemas/query_signals_index_schema.ts diff --git a/x-pack/plugins/siem/server/lib/detection_engine/routes/schemas/response/__mocks__/utils.ts b/x-pack/plugins/siem/server/lib/detection_engine/routes/schemas/response/__mocks__/utils.ts new file mode 100644 index 0000000000000..21f18f9db55fb --- /dev/null +++ b/x-pack/plugins/siem/server/lib/detection_engine/routes/schemas/response/__mocks__/utils.ts @@ -0,0 +1,144 @@ +/* + * 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 t from 'io-ts'; +import { fold } from 'fp-ts/lib/Either'; +import { RulesSchema } from '../rules_schema'; +import { RulesBulkSchema } from '../rules_bulk_schema'; +import { ErrorSchema } from '../error_schema'; +import { FindRulesSchema } from '../find_rules_schema'; +import { formatErrors } from '../utils'; +import { pipe } from 'fp-ts/lib/pipeable'; + +interface Message { + errors: t.Errors; + schema: T | {}; +} + +const onLeft = (errors: t.Errors): Message => { + return { schema: {}, errors }; +}; + +const onRight = (schema: T): Message => { + return { + schema, + errors: [], + }; +}; + +export const foldLeftRight = fold(onLeft, onRight); + +export const ANCHOR_DATE = '2020-02-20T03:57:54.037Z'; + +export const getBaseResponsePayload = (anchorDate: string = ANCHOR_DATE): RulesSchema => ({ + id: '7a7065d7-6e8b-4aae-8d20-c93613dec9f9', + created_at: new Date(anchorDate).toISOString(), + updated_at: new Date(anchorDate).toISOString(), + created_by: 'elastic', + description: 'some description', + enabled: true, + false_positives: ['false positive 1', 'false positive 2'], + from: 'now-6m', + immutable: false, + name: 'Query with a rule id', + query: 'user.name: root or user.name: admin', + references: ['test 1', 'test 2'], + severity: 'high', + updated_by: 'elastic_kibana', + tags: [], + to: 'now', + type: 'query', + threat: [], + version: 1, + status: 'succeeded', + status_date: '2020-02-22T16:47:50.047Z', + last_success_at: '2020-02-22T16:47:50.047Z', + last_success_message: 'succeeded', + output_index: '.siem-signals-hassanabad-frank-default', + max_signals: 100, + risk_score: 55, + language: 'kuery', + rule_id: 'query-rule-id', + interval: '5m', + exceptions_list: [ + { + field: 'source.ip', + values_operator: 'included', + values_type: 'exists', + }, + { + field: 'host.name', + values_operator: 'excluded', + values_type: 'match', + values: [ + { + name: 'rock01', + }, + ], + and: [ + { + field: 'host.id', + values_operator: 'included', + values_type: 'match_all', + values: [ + { + name: '123', + }, + { + name: '678', + }, + ], + }, + ], + }, + ], +}); + +export const getRulesBulkPayload = (): RulesBulkSchema => [getBaseResponsePayload()]; + +export const getMlRuleResponsePayload = (anchorDate: string = ANCHOR_DATE): RulesSchema => { + const basePayload = getBaseResponsePayload(anchorDate); + const { filters, index, query, language, ...rest } = basePayload; + + return { + ...rest, + type: 'machine_learning', + anomaly_threshold: 59, + machine_learning_job_id: 'some_machine_learning_job_id', + }; +}; + +export const getErrorPayload = ( + id: string = '819eded6-e9c8-445b-a647-519aea39e063' +): ErrorSchema => ({ + id, + error: { + status_code: 404, + message: 'id: "819eded6-e9c8-445b-a647-519aea39e063" not found', + }, +}); + +export const getFindResponseSingle = (): FindRulesSchema => ({ + page: 1, + perPage: 1, + total: 1, + data: [getBaseResponsePayload()], +}); + +/** + * Convenience utility to keep the error message handling within tests to be + * very concise. + * @param validation The validation to get the errors from + */ +export const getPaths = (validation: t.Validation): string[] => { + return pipe( + validation, + fold( + errors => formatErrors(errors), + () => ['no errors'] + ) + ); +}; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/response/check_type_dependents.test.ts b/x-pack/plugins/siem/server/lib/detection_engine/routes/schemas/response/check_type_dependents.test.ts similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/response/check_type_dependents.test.ts rename to x-pack/plugins/siem/server/lib/detection_engine/routes/schemas/response/check_type_dependents.test.ts diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/response/check_type_dependents.ts b/x-pack/plugins/siem/server/lib/detection_engine/routes/schemas/response/check_type_dependents.ts similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/response/check_type_dependents.ts rename to x-pack/plugins/siem/server/lib/detection_engine/routes/schemas/response/check_type_dependents.ts diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/response/error_schema.test.ts b/x-pack/plugins/siem/server/lib/detection_engine/routes/schemas/response/error_schema.test.ts similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/response/error_schema.test.ts rename to x-pack/plugins/siem/server/lib/detection_engine/routes/schemas/response/error_schema.test.ts diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/response/error_schema.ts b/x-pack/plugins/siem/server/lib/detection_engine/routes/schemas/response/error_schema.ts similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/response/error_schema.ts rename to x-pack/plugins/siem/server/lib/detection_engine/routes/schemas/response/error_schema.ts diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/response/exact_check.test.ts b/x-pack/plugins/siem/server/lib/detection_engine/routes/schemas/response/exact_check.test.ts similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/response/exact_check.test.ts rename to x-pack/plugins/siem/server/lib/detection_engine/routes/schemas/response/exact_check.test.ts diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/response/exact_check.ts b/x-pack/plugins/siem/server/lib/detection_engine/routes/schemas/response/exact_check.ts similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/response/exact_check.ts rename to x-pack/plugins/siem/server/lib/detection_engine/routes/schemas/response/exact_check.ts diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/response/find_rules_schema.test.ts b/x-pack/plugins/siem/server/lib/detection_engine/routes/schemas/response/find_rules_schema.test.ts similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/response/find_rules_schema.test.ts rename to x-pack/plugins/siem/server/lib/detection_engine/routes/schemas/response/find_rules_schema.test.ts diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/response/find_rules_schema.ts b/x-pack/plugins/siem/server/lib/detection_engine/routes/schemas/response/find_rules_schema.ts similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/response/find_rules_schema.ts rename to x-pack/plugins/siem/server/lib/detection_engine/routes/schemas/response/find_rules_schema.ts diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/response/import_rules_schema.test.ts b/x-pack/plugins/siem/server/lib/detection_engine/routes/schemas/response/import_rules_schema.test.ts similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/response/import_rules_schema.test.ts rename to x-pack/plugins/siem/server/lib/detection_engine/routes/schemas/response/import_rules_schema.test.ts diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/response/import_rules_schema.ts b/x-pack/plugins/siem/server/lib/detection_engine/routes/schemas/response/import_rules_schema.ts similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/response/import_rules_schema.ts rename to x-pack/plugins/siem/server/lib/detection_engine/routes/schemas/response/import_rules_schema.ts diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/response/prepackaged_rules_schema.test.ts b/x-pack/plugins/siem/server/lib/detection_engine/routes/schemas/response/prepackaged_rules_schema.test.ts similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/response/prepackaged_rules_schema.test.ts rename to x-pack/plugins/siem/server/lib/detection_engine/routes/schemas/response/prepackaged_rules_schema.test.ts diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/response/prepackaged_rules_schema.ts b/x-pack/plugins/siem/server/lib/detection_engine/routes/schemas/response/prepackaged_rules_schema.ts similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/response/prepackaged_rules_schema.ts rename to x-pack/plugins/siem/server/lib/detection_engine/routes/schemas/response/prepackaged_rules_schema.ts diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/response/prepackaged_rules_status_schema.test.ts b/x-pack/plugins/siem/server/lib/detection_engine/routes/schemas/response/prepackaged_rules_status_schema.test.ts similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/response/prepackaged_rules_status_schema.test.ts rename to x-pack/plugins/siem/server/lib/detection_engine/routes/schemas/response/prepackaged_rules_status_schema.test.ts diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/response/prepackaged_rules_status_schema.ts b/x-pack/plugins/siem/server/lib/detection_engine/routes/schemas/response/prepackaged_rules_status_schema.ts similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/response/prepackaged_rules_status_schema.ts rename to x-pack/plugins/siem/server/lib/detection_engine/routes/schemas/response/prepackaged_rules_status_schema.ts diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/response/rules_bulk_schema.test.ts b/x-pack/plugins/siem/server/lib/detection_engine/routes/schemas/response/rules_bulk_schema.test.ts similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/response/rules_bulk_schema.test.ts rename to x-pack/plugins/siem/server/lib/detection_engine/routes/schemas/response/rules_bulk_schema.test.ts diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/response/rules_bulk_schema.ts b/x-pack/plugins/siem/server/lib/detection_engine/routes/schemas/response/rules_bulk_schema.ts similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/response/rules_bulk_schema.ts rename to x-pack/plugins/siem/server/lib/detection_engine/routes/schemas/response/rules_bulk_schema.ts diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/response/rules_schema.test.ts b/x-pack/plugins/siem/server/lib/detection_engine/routes/schemas/response/rules_schema.test.ts similarity index 97% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/response/rules_schema.test.ts rename to x-pack/plugins/siem/server/lib/detection_engine/routes/schemas/response/rules_schema.test.ts index fb9ff2c28dc44..4bfc51c1a66aa 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/response/rules_schema.test.ts +++ b/x-pack/plugins/siem/server/lib/detection_engine/routes/schemas/response/rules_schema.test.ts @@ -207,7 +207,7 @@ describe('rules_schema', () => { }); // TODO: (LIST-FEATURE) Remove this test once the feature flag is deployed - test('it should remove lists when we need it to be removed because the feature is off but there exists a list in the data', () => { + test('it should remove exceptions_list when we need it to be removed because the feature is off but there exists a list in the data', () => { const payload = getBaseResponsePayload(); const decoded = rulesSchema.decode(payload); const listRemoved = removeList(decoded); @@ -246,9 +246,9 @@ describe('rules_schema', () => { }); }); - test('it should work with lists that are not there and not cause invalidation or errors', () => { + test('it should work with exceptions_list that are not there and not cause invalidation or errors', () => { const payload = getBaseResponsePayload(); - const { lists, ...payloadWithoutLists } = payload; + const { exceptions_list, ...payloadWithoutLists } = payload; const decoded = rulesSchema.decode(payloadWithoutLists); const listRemoved = removeList(decoded); const message = pipe(listRemoved, foldLeftRight); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/response/rules_schema.ts b/x-pack/plugins/siem/server/lib/detection_engine/routes/schemas/response/rules_schema.ts similarity index 98% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/response/rules_schema.ts rename to x-pack/plugins/siem/server/lib/detection_engine/routes/schemas/response/rules_schema.ts index 1574e8f5aa6e1..fb1ee8e670e31 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/response/rules_schema.ts +++ b/x-pack/plugins/siem/server/lib/detection_engine/routes/schemas/response/rules_schema.ts @@ -87,7 +87,7 @@ export const requiredRulesSchema = t.type({ updated_at, created_by, version, - lists: ListsDefaultArray, + exceptions_list: ListsDefaultArray, }); export type RequiredRulesSchema = t.TypeOf; @@ -172,7 +172,7 @@ export const removeList = ( ): Either => { const onLeft = (errors: t.Errors): Either => left(errors); const onRight = (decodedValue: RequiredRulesSchema): Either => { - delete decodedValue.lists; + delete decodedValue.exceptions_list; return right(decodedValue); }; const folded = fold(onLeft, onRight); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/response/schemas.ts b/x-pack/plugins/siem/server/lib/detection_engine/routes/schemas/response/schemas.ts similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/response/schemas.ts rename to x-pack/plugins/siem/server/lib/detection_engine/routes/schemas/response/schemas.ts diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/response/type_timeline_only_schema.test.ts b/x-pack/plugins/siem/server/lib/detection_engine/routes/schemas/response/type_timeline_only_schema.test.ts similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/response/type_timeline_only_schema.test.ts rename to x-pack/plugins/siem/server/lib/detection_engine/routes/schemas/response/type_timeline_only_schema.test.ts diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/response/type_timeline_only_schema.ts b/x-pack/plugins/siem/server/lib/detection_engine/routes/schemas/response/type_timeline_only_schema.ts similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/response/type_timeline_only_schema.ts rename to x-pack/plugins/siem/server/lib/detection_engine/routes/schemas/response/type_timeline_only_schema.ts diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/response/utils.test.ts b/x-pack/plugins/siem/server/lib/detection_engine/routes/schemas/response/utils.test.ts similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/response/utils.test.ts rename to x-pack/plugins/siem/server/lib/detection_engine/routes/schemas/response/utils.test.ts diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/response/utils.ts b/x-pack/plugins/siem/server/lib/detection_engine/routes/schemas/response/utils.ts similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/response/utils.ts rename to x-pack/plugins/siem/server/lib/detection_engine/routes/schemas/response/utils.ts diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/schemas.ts b/x-pack/plugins/siem/server/lib/detection_engine/routes/schemas/schemas.ts similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/schemas.ts rename to x-pack/plugins/siem/server/lib/detection_engine/routes/schemas/schemas.ts diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/set_signal_status_schema.test.ts b/x-pack/plugins/siem/server/lib/detection_engine/routes/schemas/set_signal_status_schema.test.ts similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/set_signal_status_schema.test.ts rename to x-pack/plugins/siem/server/lib/detection_engine/routes/schemas/set_signal_status_schema.test.ts diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/set_signal_status_schema.ts b/x-pack/plugins/siem/server/lib/detection_engine/routes/schemas/set_signal_status_schema.ts similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/set_signal_status_schema.ts rename to x-pack/plugins/siem/server/lib/detection_engine/routes/schemas/set_signal_status_schema.ts diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/types/iso_date_string.test.ts b/x-pack/plugins/siem/server/lib/detection_engine/routes/schemas/types/iso_date_string.test.ts similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/types/iso_date_string.test.ts rename to x-pack/plugins/siem/server/lib/detection_engine/routes/schemas/types/iso_date_string.test.ts diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/types/iso_date_string.ts b/x-pack/plugins/siem/server/lib/detection_engine/routes/schemas/types/iso_date_string.ts similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/types/iso_date_string.ts rename to x-pack/plugins/siem/server/lib/detection_engine/routes/schemas/types/iso_date_string.ts diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/types/lists_default_array.test.ts b/x-pack/plugins/siem/server/lib/detection_engine/routes/schemas/types/lists_default_array.test.ts similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/types/lists_default_array.test.ts rename to x-pack/plugins/siem/server/lib/detection_engine/routes/schemas/types/lists_default_array.test.ts diff --git a/x-pack/plugins/siem/server/lib/detection_engine/routes/schemas/types/lists_default_array.ts b/x-pack/plugins/siem/server/lib/detection_engine/routes/schemas/types/lists_default_array.ts new file mode 100644 index 0000000000000..743914ad070a2 --- /dev/null +++ b/x-pack/plugins/siem/server/lib/detection_engine/routes/schemas/types/lists_default_array.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 * as t from 'io-ts'; +import { Either } from 'fp-ts/lib/Either'; + +import { + list_and as listAnd, + list_values as listValues, + list_values_operator as listOperator, +} from '../response/schemas'; + +export type ListsDefaultArrayC = t.Type; +export type List = t.TypeOf; +export type ListValues = t.TypeOf; +export type ListOperator = t.TypeOf; + +/** + * Types the ListsDefaultArray as: + * - If null or undefined, then a default array will be set for the list + */ +export const ListsDefaultArray: ListsDefaultArrayC = new t.Type( + 'listsWithDefaultArray', + t.array(listAnd).is, + (input): Either => + input == null ? t.success([]) : t.array(listAnd).decode(input), + t.identity +); + +export type ListsDefaultArraySchema = t.TypeOf; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/types/positive_integer.ts b/x-pack/plugins/siem/server/lib/detection_engine/routes/schemas/types/positive_integer.ts similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/types/positive_integer.ts rename to x-pack/plugins/siem/server/lib/detection_engine/routes/schemas/types/positive_integer.ts diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/types/positive_integer_greater_than_zero.test.ts b/x-pack/plugins/siem/server/lib/detection_engine/routes/schemas/types/positive_integer_greater_than_zero.test.ts similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/types/positive_integer_greater_than_zero.test.ts rename to x-pack/plugins/siem/server/lib/detection_engine/routes/schemas/types/positive_integer_greater_than_zero.test.ts diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/types/positive_integer_greater_than_zero.ts b/x-pack/plugins/siem/server/lib/detection_engine/routes/schemas/types/positive_integer_greater_than_zero.ts similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/types/positive_integer_greater_than_zero.ts rename to x-pack/plugins/siem/server/lib/detection_engine/routes/schemas/types/positive_integer_greater_than_zero.ts diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/types/postive_integer.test.ts b/x-pack/plugins/siem/server/lib/detection_engine/routes/schemas/types/postive_integer.test.ts similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/types/postive_integer.test.ts rename to x-pack/plugins/siem/server/lib/detection_engine/routes/schemas/types/postive_integer.test.ts diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/types/references_default_array.test.ts b/x-pack/plugins/siem/server/lib/detection_engine/routes/schemas/types/references_default_array.test.ts similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/types/references_default_array.test.ts rename to x-pack/plugins/siem/server/lib/detection_engine/routes/schemas/types/references_default_array.test.ts diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/types/references_default_array.ts b/x-pack/plugins/siem/server/lib/detection_engine/routes/schemas/types/references_default_array.ts similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/types/references_default_array.ts rename to x-pack/plugins/siem/server/lib/detection_engine/routes/schemas/types/references_default_array.ts diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/types/risk_score.test.ts b/x-pack/plugins/siem/server/lib/detection_engine/routes/schemas/types/risk_score.test.ts similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/types/risk_score.test.ts rename to x-pack/plugins/siem/server/lib/detection_engine/routes/schemas/types/risk_score.test.ts diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/types/risk_score.ts b/x-pack/plugins/siem/server/lib/detection_engine/routes/schemas/types/risk_score.ts similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/types/risk_score.ts rename to x-pack/plugins/siem/server/lib/detection_engine/routes/schemas/types/risk_score.ts diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/types/uuid.test.ts b/x-pack/plugins/siem/server/lib/detection_engine/routes/schemas/types/uuid.test.ts similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/types/uuid.test.ts rename to x-pack/plugins/siem/server/lib/detection_engine/routes/schemas/types/uuid.test.ts diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/types/uuid.ts b/x-pack/plugins/siem/server/lib/detection_engine/routes/schemas/types/uuid.ts similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/types/uuid.ts rename to x-pack/plugins/siem/server/lib/detection_engine/routes/schemas/types/uuid.ts diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/update_rules_bulk_schema.test.ts b/x-pack/plugins/siem/server/lib/detection_engine/routes/schemas/update_rules_bulk_schema.test.ts similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/update_rules_bulk_schema.test.ts rename to x-pack/plugins/siem/server/lib/detection_engine/routes/schemas/update_rules_bulk_schema.test.ts diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/update_rules_bulk_schema.ts b/x-pack/plugins/siem/server/lib/detection_engine/routes/schemas/update_rules_bulk_schema.ts similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/update_rules_bulk_schema.ts rename to x-pack/plugins/siem/server/lib/detection_engine/routes/schemas/update_rules_bulk_schema.ts diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/update_rules_schema.test.ts b/x-pack/plugins/siem/server/lib/detection_engine/routes/schemas/update_rules_schema.test.ts similarity index 97% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/update_rules_schema.test.ts rename to x-pack/plugins/siem/server/lib/detection_engine/routes/schemas/update_rules_schema.test.ts index e8f9aad620ca0..b89df0fc0f3ab 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/update_rules_schema.test.ts +++ b/x-pack/plugins/siem/server/lib/detection_engine/routes/schemas/update_rules_schema.test.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { AlertAction } from '../../../../../../../../plugins/alerting/common'; +import { AlertAction } from '../../../../../../alerting/common'; import { updateRulesSchema } from './update_rules_schema'; import { PatchRuleAlertParamsRest } from '../../rules/types'; import { RuleAlertAction } from '../../../../../common/detection_engine/types'; @@ -575,7 +575,9 @@ describe('create rules schema', () => { test('language does not validate with something made up', () => { expect( - updateRulesSchema.validate>({ + updateRulesSchema.validate< + Partial & { language: string }> + >({ rule_id: 'rule-1', output_index: '.siem-signals', risk_score: 50, @@ -1534,8 +1536,8 @@ describe('create rules schema', () => { // on demand. Since they are per module, we have a an issue where the ENV variables do not take effect. It is better we change all the // schema's to be function calls to avoid global side effects or just wait until the feature is available. If you want to test this early, // you can remove the .skip and set your env variable of export ELASTIC_XPACK_SIEM_LISTS_FEATURE=true locally - describe.skip('lists', () => { - test('[rule_id, description, from, to, index, name, severity, interval, type, filter, risk_score, note, and lists] does validate', () => { + describe.skip('exceptions_list', () => { + test('[rule_id, description, from, to, index, name, severity, interval, type, filter, risk_score, note, and exceptions_list] does validate', () => { expect( updateRulesSchema.validate>({ rule_id: 'rule-1', @@ -1549,7 +1551,7 @@ describe('create rules schema', () => { type: 'query', risk_score: 50, note: '# some markdown', - lists: [ + exceptions_list: [ { field: 'source.ip', values_operator: 'included', @@ -1582,7 +1584,7 @@ describe('create rules schema', () => { ).toBeFalsy(); }); - test('[rule_id, description, from, to, index, name, severity, interval, type, filter, risk_score, note, and empty lists] does validate', () => { + test('[rule_id, description, from, to, index, name, severity, interval, type, filter, risk_score, note, and empty exceptions_list] does validate', () => { expect( updateRulesSchema.validate>({ rule_id: 'rule-1', @@ -1596,14 +1598,14 @@ describe('create rules schema', () => { type: 'query', risk_score: 50, note: '# some markdown', - lists: [], + exceptions_list: [], }).error ).toBeFalsy(); }); - test('[rule_id, description, from, to, index, name, severity, interval, type, filter, risk_score, note, and invalid lists] does NOT validate', () => { + test('[rule_id, description, from, to, index, name, severity, interval, type, filter, risk_score, note, and invalid exceptions_list] does NOT validate', () => { expect( - updateRulesSchema.validate>>({ + updateRulesSchema.validate>>({ rule_id: 'rule-1', description: 'some description', from: 'now-5m', @@ -1615,16 +1617,16 @@ describe('create rules schema', () => { type: 'query', risk_score: 50, note: '# some markdown', - lists: [{ invalid_value: 'invalid value' }], + exceptions_list: [{ invalid_value: 'invalid value' }], }).error.message ).toEqual( - 'child "lists" fails because ["lists" at position 0 fails because [child "field" fails because ["field" is required]]]' + 'child "exceptions_list" fails because ["exceptions_list" at position 0 fails because [child "field" fails because ["field" is required]]]' ); }); - test('[rule_id, description, from, to, index, name, severity, interval, type, filter, risk_score, note, and non-existent lists] does validate with empty lists', () => { + test('[rule_id, description, from, to, index, name, severity, interval, type, filter, risk_score, note, and non-existent exceptions_list] does validate with empty exceptions_list', () => { expect( - updateRulesSchema.validate>>({ + updateRulesSchema.validate>>({ rule_id: 'rule-1', description: 'some description', from: 'now-5m', @@ -1636,7 +1638,7 @@ describe('create rules schema', () => { type: 'query', risk_score: 50, note: '# some markdown', - }).value.lists + }).value.exceptions_list ).toEqual([]); }); }); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/update_rules_schema.ts b/x-pack/plugins/siem/server/lib/detection_engine/routes/schemas/update_rules_schema.ts similarity index 97% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/update_rules_schema.ts rename to x-pack/plugins/siem/server/lib/detection_engine/routes/schemas/update_rules_schema.ts index f842c14f41ae6..b1b37801b644f 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/update_rules_schema.ts +++ b/x-pack/plugins/siem/server/lib/detection_engine/routes/schemas/update_rules_schema.ts @@ -107,5 +107,5 @@ export const updateRulesSchema = Joi.object({ version, // TODO: (LIST-FEATURE) Remove the hasListsFeatures once this is ready for release - lists: hasListsFeature() ? lists.default([]) : lists.forbidden().default([]), + exceptions_list: hasListsFeature() ? lists.default([]) : lists.forbidden().default([]), }).xor('id', 'rule_id'); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/signals/open_close_signals.test.ts b/x-pack/plugins/siem/server/lib/detection_engine/routes/signals/open_close_signals.test.ts similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/signals/open_close_signals.test.ts rename to x-pack/plugins/siem/server/lib/detection_engine/routes/signals/open_close_signals.test.ts diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/signals/open_close_signals_route.ts b/x-pack/plugins/siem/server/lib/detection_engine/routes/signals/open_close_signals_route.ts similarity index 96% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/signals/open_close_signals_route.ts rename to x-pack/plugins/siem/server/lib/detection_engine/routes/signals/open_close_signals_route.ts index 2daf63c468593..c71761fcc39db 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/signals/open_close_signals_route.ts +++ b/x-pack/plugins/siem/server/lib/detection_engine/routes/signals/open_close_signals_route.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { IRouter } from '../../../../../../../../../src/core/server'; +import { IRouter } from '../../../../../../../../src/core/server'; import { DETECTION_ENGINE_SIGNALS_STATUS_URL } from '../../../../../common/constants'; import { SignalsStatusRestParams } from '../../signals/types'; import { setSignalsStatusSchema } from '../schemas/set_signal_status_schema'; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/signals/query_signals_route.test.ts b/x-pack/plugins/siem/server/lib/detection_engine/routes/signals/query_signals_route.test.ts similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/signals/query_signals_route.test.ts rename to x-pack/plugins/siem/server/lib/detection_engine/routes/signals/query_signals_route.test.ts diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/signals/query_signals_route.ts b/x-pack/plugins/siem/server/lib/detection_engine/routes/signals/query_signals_route.ts similarity index 96% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/signals/query_signals_route.ts rename to x-pack/plugins/siem/server/lib/detection_engine/routes/signals/query_signals_route.ts index f05f494619b9c..fd02b3371ed38 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/signals/query_signals_route.ts +++ b/x-pack/plugins/siem/server/lib/detection_engine/routes/signals/query_signals_route.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { IRouter } from '../../../../../../../../../src/core/server'; +import { IRouter } from '../../../../../../../../src/core/server'; import { DETECTION_ENGINE_QUERY_SIGNALS_URL } from '../../../../../common/constants'; import { SignalsQueryRestParams } from '../../signals/types'; import { querySignalsSchema } from '../schemas/query_signals_index_schema'; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/tags/read_tags_route.ts b/x-pack/plugins/siem/server/lib/detection_engine/routes/tags/read_tags_route.ts similarity index 94% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/tags/read_tags_route.ts rename to x-pack/plugins/siem/server/lib/detection_engine/routes/tags/read_tags_route.ts index adabc62a9456f..2b885385521dd 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/tags/read_tags_route.ts +++ b/x-pack/plugins/siem/server/lib/detection_engine/routes/tags/read_tags_route.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { IRouter } from '../../../../../../../../../src/core/server'; +import { IRouter } from '../../../../../../../../src/core/server'; import { DETECTION_ENGINE_TAGS_URL } from '../../../../../common/constants'; import { transformError, buildSiemResponse } from '../utils'; import { readTags } from '../../tags/read_tags'; diff --git a/x-pack/plugins/siem/server/lib/detection_engine/routes/utils.test.ts b/x-pack/plugins/siem/server/lib/detection_engine/routes/utils.test.ts new file mode 100644 index 0000000000000..8af5df6056913 --- /dev/null +++ b/x-pack/plugins/siem/server/lib/detection_engine/routes/utils.test.ts @@ -0,0 +1,396 @@ +/* + * 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 Boom from 'boom'; + +import { SavedObjectsFindResponse } from 'kibana/server'; +import { IRuleSavedAttributesSavedObjectAttributes, IRuleStatusAttributes } from '../rules/types'; +import { BadRequestError } from '../errors/bad_request_error'; +import { + transformError, + transformBulkError, + BulkError, + createSuccessObject, + ImportSuccessError, + createImportErrorObject, + transformImportError, + convertToSnakeCase, + SiemResponseFactory, + validateLicenseForRuleType, +} from './utils'; +import { responseMock } from './__mocks__'; +import { setFeatureFlagsForTestsOnly, unSetFeatureFlagsForTestsOnly } from '../feature_flags'; +import { licensingMock } from '../../../../../licensing/server/mocks'; + +describe('utils', () => { + beforeAll(() => { + setFeatureFlagsForTestsOnly(); + }); + + afterAll(() => { + unSetFeatureFlagsForTestsOnly(); + }); + + describe('transformError', () => { + test('returns transformed output error from boom object with a 500 and payload of internal server error', () => { + const boom = new Boom('some boom message'); + const transformed = transformError(boom); + expect(transformed).toEqual({ + message: 'An internal server error occurred', + statusCode: 500, + }); + }); + + test('returns transformed output if it is some non boom object that has a statusCode', () => { + const error: Error & { statusCode?: number } = { + statusCode: 403, + name: 'some name', + message: 'some message', + }; + const transformed = transformError(error); + expect(transformed).toEqual({ + message: 'some message', + statusCode: 403, + }); + }); + + test('returns a transformed message with the message set and statusCode', () => { + const error: Error & { statusCode?: number } = { + statusCode: 403, + name: 'some name', + message: 'some message', + }; + const transformed = transformError(error); + expect(transformed).toEqual({ + message: 'some message', + statusCode: 403, + }); + }); + + test('transforms best it can if it is some non boom object but it does not have a status Code.', () => { + const error: Error = { + name: 'some name', + message: 'some message', + }; + const transformed = transformError(error); + expect(transformed).toEqual({ + message: 'some message', + statusCode: 500, + }); + }); + + test('it detects a BadRequestError and returns a status code of 400 from that particular error type', () => { + const error: BadRequestError = new BadRequestError('I have a type error'); + const transformed = transformError(error); + expect(transformed).toEqual({ + message: 'I have a type error', + statusCode: 400, + }); + }); + + test('it detects a BadRequestError and returns a Boom status of 400', () => { + const error: BadRequestError = new BadRequestError('I have a type error'); + const transformed = transformError(error); + expect(transformed).toEqual({ + message: 'I have a type error', + statusCode: 400, + }); + }); + }); + + describe('transformBulkError', () => { + test('returns transformed object if it is a boom object', () => { + const boom = new Boom('some boom message', { statusCode: 400 }); + const transformed = transformBulkError('rule-1', boom); + const expected: BulkError = { + rule_id: 'rule-1', + error: { message: 'some boom message', status_code: 400 }, + }; + expect(transformed).toEqual(expected); + }); + + test('returns a normal error if it is some non boom object that has a statusCode', () => { + const error: Error & { statusCode?: number } = { + statusCode: 403, + name: 'some name', + message: 'some message', + }; + const transformed = transformBulkError('rule-1', error); + const expected: BulkError = { + rule_id: 'rule-1', + error: { message: 'some message', status_code: 403 }, + }; + expect(transformed).toEqual(expected); + }); + + test('returns a 500 if the status code is not set', () => { + const error: Error & { statusCode?: number } = { + name: 'some name', + message: 'some message', + }; + const transformed = transformBulkError('rule-1', error); + const expected: BulkError = { + rule_id: 'rule-1', + error: { message: 'some message', status_code: 500 }, + }; + expect(transformed).toEqual(expected); + }); + + test('it detects a BadRequestError and returns a Boom status of 400', () => { + const error: BadRequestError = new BadRequestError('I have a type error'); + const transformed = transformBulkError('rule-1', error); + const expected: BulkError = { + rule_id: 'rule-1', + error: { message: 'I have a type error', status_code: 400 }, + }; + expect(transformed).toEqual(expected); + }); + }); + + describe('createSuccessObject', () => { + test('it should increment the existing success object by 1', () => { + const success = createSuccessObject({ + success_count: 0, + success: true, + errors: [], + }); + const expected: ImportSuccessError = { + success_count: 1, + success: true, + errors: [], + }; + expect(success).toEqual(expected); + }); + + test('it should increment the existing success object by 1 and not touch the boolean or errors', () => { + const success = createSuccessObject({ + success_count: 0, + success: false, + errors: [ + { rule_id: 'rule-1', error: { status_code: 500, message: 'some sad sad sad error' } }, + ], + }); + const expected: ImportSuccessError = { + success_count: 1, + success: false, + errors: [ + { rule_id: 'rule-1', error: { status_code: 500, message: 'some sad sad sad error' } }, + ], + }; + expect(success).toEqual(expected); + }); + }); + + describe('createImportErrorObject', () => { + test('it creates an error message and does not increment the success count', () => { + const error = createImportErrorObject({ + ruleId: 'some-rule-id', + statusCode: 400, + message: 'some-message', + existingImportSuccessError: { + success_count: 1, + success: true, + errors: [], + }, + }); + const expected: ImportSuccessError = { + success_count: 1, + success: false, + errors: [{ rule_id: 'some-rule-id', error: { status_code: 400, message: 'some-message' } }], + }; + expect(error).toEqual(expected); + }); + + test('appends a second error message and does not increment the success count', () => { + const error = createImportErrorObject({ + ruleId: 'some-rule-id', + statusCode: 400, + message: 'some-message', + existingImportSuccessError: { + success_count: 1, + success: false, + errors: [ + { rule_id: 'rule-1', error: { status_code: 500, message: 'some sad sad sad error' } }, + ], + }, + }); + const expected: ImportSuccessError = { + success_count: 1, + success: false, + errors: [ + { rule_id: 'rule-1', error: { status_code: 500, message: 'some sad sad sad error' } }, + { rule_id: 'some-rule-id', error: { status_code: 400, message: 'some-message' } }, + ], + }; + expect(error).toEqual(expected); + }); + }); + + describe('transformImportError', () => { + test('returns transformed object if it is a boom object', () => { + const boom = new Boom('some boom message', { statusCode: 400 }); + const transformed = transformImportError('rule-1', boom, { + success_count: 1, + success: false, + errors: [{ rule_id: 'some-rule-id', error: { status_code: 400, message: 'some-message' } }], + }); + const expected: ImportSuccessError = { + success_count: 1, + success: false, + errors: [ + { rule_id: 'some-rule-id', error: { status_code: 400, message: 'some-message' } }, + { rule_id: 'rule-1', error: { status_code: 400, message: 'some boom message' } }, + ], + }; + expect(transformed).toEqual(expected); + }); + + test('returns a normal error if it is some non boom object that has a statusCode', () => { + const error: Error & { statusCode?: number } = { + statusCode: 403, + name: 'some name', + message: 'some message', + }; + const transformed = transformImportError('rule-1', error, { + success_count: 1, + success: false, + errors: [{ rule_id: 'some-rule-id', error: { status_code: 400, message: 'some-message' } }], + }); + const expected: ImportSuccessError = { + success_count: 1, + success: false, + errors: [ + { rule_id: 'some-rule-id', error: { status_code: 400, message: 'some-message' } }, + { rule_id: 'rule-1', error: { status_code: 403, message: 'some message' } }, + ], + }; + expect(transformed).toEqual(expected); + }); + + test('returns a 500 if the status code is not set', () => { + const error: Error & { statusCode?: number } = { + name: 'some name', + message: 'some message', + }; + const transformed = transformImportError('rule-1', error, { + success_count: 1, + success: false, + errors: [{ rule_id: 'some-rule-id', error: { status_code: 400, message: 'some-message' } }], + }); + const expected: ImportSuccessError = { + success_count: 1, + success: false, + errors: [ + { rule_id: 'some-rule-id', error: { status_code: 400, message: 'some-message' } }, + { rule_id: 'rule-1', error: { status_code: 500, message: 'some message' } }, + ], + }; + expect(transformed).toEqual(expected); + }); + + test('it detects a BadRequestError and returns a Boom status of 400', () => { + const error: BadRequestError = new BadRequestError('I have a type error'); + const transformed = transformImportError('rule-1', error, { + success_count: 1, + success: false, + errors: [{ rule_id: 'some-rule-id', error: { status_code: 400, message: 'some-message' } }], + }); + const expected: ImportSuccessError = { + success_count: 1, + success: false, + errors: [ + { rule_id: 'some-rule-id', error: { status_code: 400, message: 'some-message' } }, + { rule_id: 'rule-1', error: { status_code: 400, message: 'I have a type error' } }, + ], + }; + expect(transformed).toEqual(expected); + }); + }); + + describe('convertToSnakeCase', () => { + it('converts camelCase to snakeCase', () => { + const values = { myTestCamelCaseKey: 'something' }; + expect(convertToSnakeCase(values)).toEqual({ my_test_camel_case_key: 'something' }); + }); + it('returns empty object when object is empty', () => { + const values = {}; + expect(convertToSnakeCase(values)).toEqual({}); + }); + it('returns null when passed in undefined', () => { + // Array accessors can result in undefined but + // this is not represented in typescript for some reason, + // https://github.com/Microsoft/TypeScript/issues/11122 + const values: SavedObjectsFindResponse = { + page: 0, + per_page: 5, + total: 0, + saved_objects: [], + }; + expect( + convertToSnakeCase(values.saved_objects[0]?.attributes) // this is undefined, but it says it's not + ).toEqual(null); + }); + }); + + describe('SiemResponseFactory', () => { + it('builds a custom response', () => { + const response = responseMock.create(); + const responseFactory = new SiemResponseFactory(response); + + responseFactory.error({ statusCode: 400 }); + expect(response.custom).toHaveBeenCalled(); + }); + + it('generates a status_code key on the response', () => { + const response = responseMock.create(); + const responseFactory = new SiemResponseFactory(response); + + responseFactory.error({ statusCode: 400 }); + const [[{ statusCode, body }]] = response.custom.mock.calls; + + expect(statusCode).toEqual(400); + expect(body).toBeInstanceOf(Buffer); + expect(JSON.parse(body!.toString())).toEqual( + expect.objectContaining({ + message: 'Bad Request', + status_code: 400, + }) + ); + }); + }); + + describe('validateLicenseForRuleType', () => { + let licenseMock: ReturnType; + + beforeEach(() => { + licenseMock = licensingMock.createLicenseMock(); + }); + + it('throws a BadRequestError if operating on an ML Rule with an insufficient license', () => { + licenseMock.hasAtLeast.mockReturnValue(false); + + expect(() => + validateLicenseForRuleType({ license: licenseMock, ruleType: 'machine_learning' }) + ).toThrowError(BadRequestError); + }); + + it('does not throw if operating on an ML Rule with a sufficient license', () => { + licenseMock.hasAtLeast.mockReturnValue(true); + + expect(() => + validateLicenseForRuleType({ license: licenseMock, ruleType: 'machine_learning' }) + ).not.toThrowError(BadRequestError); + }); + + it('does not throw if operating on a query rule', () => { + licenseMock.hasAtLeast.mockReturnValue(false); + + expect(() => + validateLicenseForRuleType({ license: licenseMock, ruleType: 'query' }) + ).not.toThrowError(BadRequestError); + }); + }); +}); diff --git a/x-pack/plugins/siem/server/lib/detection_engine/routes/utils.ts b/x-pack/plugins/siem/server/lib/detection_engine/routes/utils.ts new file mode 100644 index 0000000000000..52493a9be9b8f --- /dev/null +++ b/x-pack/plugins/siem/server/lib/detection_engine/routes/utils.ts @@ -0,0 +1,321 @@ +/* + * 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 Boom from 'boom'; +import Joi from 'joi'; +import { has, snakeCase } from 'lodash/fp'; +import { i18n } from '@kbn/i18n'; + +import { + RouteValidationFunction, + KibanaResponseFactory, + CustomHttpResponseOptions, +} from '../../../../../../../src/core/server'; +import { ILicense } from '../../../../../licensing/server'; +import { MINIMUM_ML_LICENSE } from '../../../../common/constants'; +import { RuleType } from '../../../../common/detection_engine/types'; +import { isMlRule } from '../../../../common/detection_engine/ml_helpers'; +import { BadRequestError } from '../errors/bad_request_error'; + +export interface OutputError { + message: string; + statusCode: number; +} + +export const transformError = (err: Error & { statusCode?: number }): OutputError => { + if (Boom.isBoom(err)) { + return { + message: err.output.payload.message, + statusCode: err.output.statusCode, + }; + } else { + if (err.statusCode != null) { + return { + message: err.message, + statusCode: err.statusCode, + }; + } else if (err instanceof BadRequestError) { + // allows us to throw request validation errors in the absence of Boom + return { + message: err.message, + statusCode: 400, + }; + } else { + // natively return the err and allow the regular framework + // to deal with the error when it is a non Boom + return { + message: err.message ?? '(unknown error message)', + statusCode: 500, + }; + } + } +}; + +export interface BulkError { + id?: string; + rule_id?: string; + error: { + status_code: number; + message: string; + }; +} + +export const createBulkErrorObject = ({ + ruleId, + id, + statusCode, + message, +}: { + ruleId?: string; + id?: string; + statusCode: number; + message: string; +}): BulkError => { + if (id != null && ruleId != null) { + return { + id, + rule_id: ruleId, + error: { + status_code: statusCode, + message, + }, + }; + } else if (id != null) { + return { + id, + error: { + status_code: statusCode, + message, + }, + }; + } else if (ruleId != null) { + return { + rule_id: ruleId, + error: { + status_code: statusCode, + message, + }, + }; + } else { + return { + rule_id: '(unknown id)', + error: { + status_code: statusCode, + message, + }, + }; + } +}; + +export interface ImportRegular { + rule_id: string; + status_code: number; + message?: string; +} + +export type ImportRuleResponse = ImportRegular | BulkError; + +export const isBulkError = ( + importRuleResponse: ImportRuleResponse +): importRuleResponse is BulkError => { + return has('error', importRuleResponse); +}; + +export const isImportRegular = ( + importRuleResponse: ImportRuleResponse +): importRuleResponse is ImportRegular => { + return !has('error', importRuleResponse) && has('status_code', importRuleResponse); +}; + +export interface ImportSuccessError { + success: boolean; + success_count: number; + errors: BulkError[]; +} + +export const createSuccessObject = ( + existingImportSuccessError: ImportSuccessError +): ImportSuccessError => { + return { + success_count: existingImportSuccessError.success_count + 1, + success: existingImportSuccessError.success, + errors: existingImportSuccessError.errors, + }; +}; + +export const createImportErrorObject = ({ + ruleId, + statusCode, + message, + existingImportSuccessError, +}: { + ruleId: string; + statusCode: number; + message: string; + existingImportSuccessError: ImportSuccessError; +}): ImportSuccessError => { + return { + success: false, + errors: [ + ...existingImportSuccessError.errors, + createBulkErrorObject({ + ruleId, + statusCode, + message, + }), + ], + success_count: existingImportSuccessError.success_count, + }; +}; + +export const transformImportError = ( + ruleId: string, + err: Error & { statusCode?: number }, + existingImportSuccessError: ImportSuccessError +): ImportSuccessError => { + if (Boom.isBoom(err)) { + return createImportErrorObject({ + ruleId, + statusCode: err.output.statusCode, + message: err.message, + existingImportSuccessError, + }); + } else if (err instanceof BadRequestError) { + return createImportErrorObject({ + ruleId, + statusCode: 400, + message: err.message, + existingImportSuccessError, + }); + } else { + return createImportErrorObject({ + ruleId, + statusCode: err.statusCode ?? 500, + message: err.message, + existingImportSuccessError, + }); + } +}; + +export const transformBulkError = ( + ruleId: string, + err: Error & { statusCode?: number } +): BulkError => { + if (Boom.isBoom(err)) { + return createBulkErrorObject({ + ruleId, + statusCode: err.output.statusCode, + message: err.message, + }); + } else if (err instanceof BadRequestError) { + return createBulkErrorObject({ + ruleId, + statusCode: 400, + message: err.message, + }); + } else { + return createBulkErrorObject({ + ruleId, + statusCode: err.statusCode ?? 500, + message: err.message, + }); + } +}; + +export const buildRouteValidation = (schema: Joi.Schema): RouteValidationFunction => ( + payload: T, + { ok, badRequest } +) => { + const { value, error } = schema.validate(payload); + if (error) { + return badRequest(error.message); + } + return ok(value); +}; + +const statusToErrorMessage = (statusCode: number) => { + switch (statusCode) { + case 400: + return 'Bad Request'; + case 401: + return 'Unauthorized'; + case 403: + return 'Forbidden'; + case 404: + return 'Not Found'; + case 409: + return 'Conflict'; + case 500: + return 'Internal Error'; + default: + return '(unknown error)'; + } +}; + +export class SiemResponseFactory { + constructor(private response: KibanaResponseFactory) {} + + error({ statusCode, body, headers }: CustomHttpResponseOptions) { + const contentType: CustomHttpResponseOptions['headers'] = { + 'content-type': 'application/json', + }; + const defaultedHeaders: CustomHttpResponseOptions['headers'] = { + ...contentType, + ...(headers ?? {}), + }; + + return this.response.custom({ + headers: defaultedHeaders, + statusCode, + body: Buffer.from( + JSON.stringify({ + message: body ?? statusToErrorMessage(statusCode), + status_code: statusCode, + }) + ), + }); + } +} + +export const buildSiemResponse = (response: KibanaResponseFactory) => + new SiemResponseFactory(response); + +export const convertToSnakeCase = >( + obj: T +): Partial | null => { + if (!obj) { + return null; + } + return Object.keys(obj).reduce((acc, item) => { + const newKey = snakeCase(item); + return { ...acc, [newKey]: obj[item] }; + }, {}); +}; + +/** + * Checks the current Kibana License against the rule under operation. + * + * @param license ILicense representing the user license + * @param ruleType the type of the current rule + * + * @throws BadRequestError if rule and license are incompatible + */ +export const validateLicenseForRuleType = ({ + license, + ruleType, +}: { + license: ILicense; + ruleType: RuleType; +}): void => { + if (isMlRule(ruleType) && !license.hasAtLeast(MINIMUM_ML_LICENSE)) { + const message = i18n.translate('xpack.siem.licensing.unsupportedMachineLearningMessage', { + defaultMessage: + 'Your license does not support machine learning. Please upgrade your license.', + }); + + throw new BadRequestError(message); + } +}; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rule_actions/create_rule_actions_saved_object.ts b/x-pack/plugins/siem/server/lib/detection_engine/rule_actions/create_rule_actions_saved_object.ts similarity index 94% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/rule_actions/create_rule_actions_saved_object.ts rename to x-pack/plugins/siem/server/lib/detection_engine/rule_actions/create_rule_actions_saved_object.ts index 991690d901d8a..26c3b29ff2c51 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rule_actions/create_rule_actions_saved_object.ts +++ b/x-pack/plugins/siem/server/lib/detection_engine/rule_actions/create_rule_actions_saved_object.ts @@ -5,7 +5,7 @@ */ import { RuleAlertAction } from '../../../../common/detection_engine/types'; -import { AlertServices } from '../../../../../../../plugins/alerting/server'; +import { AlertServices } from '../../../../../alerting/server'; import { ruleActionsSavedObjectType } from './saved_object_mappings'; import { IRuleActionsAttributesSavedObjectAttributes } from './types'; import { getThrottleOptions, getRuleActionsFromSavedObject } from './utils'; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rule_actions/delete_rule_actions_saved_object.ts b/x-pack/plugins/siem/server/lib/detection_engine/rule_actions/delete_rule_actions_saved_object.ts similarity index 92% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/rule_actions/delete_rule_actions_saved_object.ts rename to x-pack/plugins/siem/server/lib/detection_engine/rule_actions/delete_rule_actions_saved_object.ts index 91489334940bd..251f9155f9331 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rule_actions/delete_rule_actions_saved_object.ts +++ b/x-pack/plugins/siem/server/lib/detection_engine/rule_actions/delete_rule_actions_saved_object.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { AlertServices } from '../../../../../../../plugins/alerting/server'; +import { AlertServices } from '../../../../../alerting/server'; import { ruleActionsSavedObjectType } from './saved_object_mappings'; import { getRuleActionsSavedObject } from './get_rule_actions_saved_object'; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rule_actions/get_rule_actions_saved_object.ts b/x-pack/plugins/siem/server/lib/detection_engine/rule_actions/get_rule_actions_saved_object.ts similarity index 94% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/rule_actions/get_rule_actions_saved_object.ts rename to x-pack/plugins/siem/server/lib/detection_engine/rule_actions/get_rule_actions_saved_object.ts index dad35f6cb1f96..83cd59f0a1cde 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rule_actions/get_rule_actions_saved_object.ts +++ b/x-pack/plugins/siem/server/lib/detection_engine/rule_actions/get_rule_actions_saved_object.ts @@ -5,7 +5,7 @@ */ import { RuleAlertAction } from '../../../../common/detection_engine/types'; -import { AlertServices } from '../../../../../../../plugins/alerting/server'; +import { AlertServices } from '../../../../../alerting/server'; import { ruleActionsSavedObjectType } from './saved_object_mappings'; import { IRuleActionsAttributesSavedObjectAttributes } from './types'; import { getRuleActionsFromSavedObject } from './utils'; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rule_actions/saved_object_mappings.ts b/x-pack/plugins/siem/server/lib/detection_engine/rule_actions/saved_object_mappings.ts similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/rule_actions/saved_object_mappings.ts rename to x-pack/plugins/siem/server/lib/detection_engine/rule_actions/saved_object_mappings.ts diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rule_actions/types.ts b/x-pack/plugins/siem/server/lib/detection_engine/rule_actions/types.ts similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/rule_actions/types.ts rename to x-pack/plugins/siem/server/lib/detection_engine/rule_actions/types.ts diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rule_actions/update_or_create_rule_actions_saved_object.ts b/x-pack/plugins/siem/server/lib/detection_engine/rule_actions/update_or_create_rule_actions_saved_object.ts similarity index 94% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/rule_actions/update_or_create_rule_actions_saved_object.ts rename to x-pack/plugins/siem/server/lib/detection_engine/rule_actions/update_or_create_rule_actions_saved_object.ts index d79c61f6200e3..3364827d397d2 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rule_actions/update_or_create_rule_actions_saved_object.ts +++ b/x-pack/plugins/siem/server/lib/detection_engine/rule_actions/update_or_create_rule_actions_saved_object.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { AlertServices } from '../../../../../../../plugins/alerting/server'; +import { AlertServices } from '../../../../../alerting/server'; import { RuleAlertAction } from '../../../../common/detection_engine/types'; import { getRuleActionsSavedObject } from './get_rule_actions_saved_object'; import { createRuleActionsSavedObject } from './create_rule_actions_saved_object'; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rule_actions/update_rule_actions_saved_object.ts b/x-pack/plugins/siem/server/lib/detection_engine/rule_actions/update_rule_actions_saved_object.ts similarity index 95% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/rule_actions/update_rule_actions_saved_object.ts rename to x-pack/plugins/siem/server/lib/detection_engine/rule_actions/update_rule_actions_saved_object.ts index 2a2c84838ed93..c8a3b1bbc38ad 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rule_actions/update_rule_actions_saved_object.ts +++ b/x-pack/plugins/siem/server/lib/detection_engine/rule_actions/update_rule_actions_saved_object.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { AlertServices } from '../../../../../../../plugins/alerting/server'; +import { AlertServices } from '../../../../../alerting/server'; import { ruleActionsSavedObjectType } from './saved_object_mappings'; import { RulesActionsSavedObject } from './get_rule_actions_saved_object'; import { RuleAlertAction } from '../../../../common/detection_engine/types'; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rule_actions/utils.ts b/x-pack/plugins/siem/server/lib/detection_engine/rule_actions/utils.ts similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/rule_actions/utils.ts rename to x-pack/plugins/siem/server/lib/detection_engine/rule_actions/utils.ts diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/add_tags.test.ts b/x-pack/plugins/siem/server/lib/detection_engine/rules/add_tags.test.ts similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/add_tags.test.ts rename to x-pack/plugins/siem/server/lib/detection_engine/rules/add_tags.test.ts diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/add_tags.ts b/x-pack/plugins/siem/server/lib/detection_engine/rules/add_tags.ts similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/add_tags.ts rename to x-pack/plugins/siem/server/lib/detection_engine/rules/add_tags.ts diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/create_rules.test.ts b/x-pack/plugins/siem/server/lib/detection_engine/rules/create_rules.test.ts similarity index 88% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/create_rules.test.ts rename to x-pack/plugins/siem/server/lib/detection_engine/rules/create_rules.test.ts index a60f1d4177978..6710bf02aeb2b 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/create_rules.test.ts +++ b/x-pack/plugins/siem/server/lib/detection_engine/rules/create_rules.test.ts @@ -4,8 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ -import { alertsClientMock } from '../../../../../../../plugins/alerting/server/mocks'; -import { actionsClientMock } from '../../../../../../../plugins/actions/server/mocks'; +import { alertsClientMock } from '../../../../../alerting/server/mocks'; +import { actionsClientMock } from '../../../../../actions/server/mocks'; import { getMlResult } from '../routes/__mocks__/request_responses'; import { createRules } from './create_rules'; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/create_rules.ts b/x-pack/plugins/siem/server/lib/detection_engine/rules/create_rules.ts similarity index 86% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/create_rules.ts rename to x-pack/plugins/siem/server/lib/detection_engine/rules/create_rules.ts index 91effb4741b8b..a007fe35b407e 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/create_rules.ts +++ b/x-pack/plugins/siem/server/lib/detection_engine/rules/create_rules.ts @@ -5,7 +5,7 @@ */ import { transformRuleToAlertAction } from '../../../../common/detection_engine/transform_actions'; -import { Alert } from '../../../../../../../plugins/alerting/common'; +import { Alert } from '../../../../../alerting/common'; import { APP_ID, SIGNALS_ID } from '../../../../common/constants'; import { CreateRuleParams } from './types'; import { addTags } from './add_tags'; @@ -42,11 +42,11 @@ export const createRules = async ({ references, note, version, - lists, + exceptions_list, actions, }: CreateRuleParams): Promise => { - // TODO: Remove this and use regular lists once the feature is stable for a release - const listsParam = hasListsFeature() ? { lists } : {}; + // TODO: Remove this and use regular exceptions_list once the feature is stable for a release + const exceptionsListParam = hasListsFeature() ? { exceptions_list } : {}; return alertsClient.create({ data: { name, @@ -79,7 +79,7 @@ export const createRules = async ({ references, note, version, - ...listsParam, + ...exceptionsListParam, }, schedule: { interval }, enabled, diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/create_rules_stream_from_ndjson.test.ts b/x-pack/plugins/siem/server/lib/detection_engine/rules/create_rules_stream_from_ndjson.test.ts similarity index 97% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/create_rules_stream_from_ndjson.test.ts rename to x-pack/plugins/siem/server/lib/detection_engine/rules/create_rules_stream_from_ndjson.test.ts index 695057ccc2f70..8044692ab90b1 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/create_rules_stream_from_ndjson.test.ts +++ b/x-pack/plugins/siem/server/lib/detection_engine/rules/create_rules_stream_from_ndjson.test.ts @@ -66,7 +66,7 @@ describe('create_rules_stream_from_ndjson', () => { immutable: false, query: '', language: 'kuery', - lists: [], + exceptions_list: [], max_signals: 100, tags: [], threat: [], @@ -92,7 +92,7 @@ describe('create_rules_stream_from_ndjson', () => { immutable: false, query: '', language: 'kuery', - lists: [], + exceptions_list: [], max_signals: 100, tags: [], threat: [], @@ -158,7 +158,7 @@ describe('create_rules_stream_from_ndjson', () => { language: 'kuery', max_signals: 100, tags: [], - lists: [], + exceptions_list: [], threat: [], throttle: null, references: [], @@ -183,7 +183,7 @@ describe('create_rules_stream_from_ndjson', () => { query: '', language: 'kuery', max_signals: 100, - lists: [], + exceptions_list: [], tags: [], threat: [], throttle: null, @@ -230,7 +230,7 @@ describe('create_rules_stream_from_ndjson', () => { query: '', language: 'kuery', max_signals: 100, - lists: [], + exceptions_list: [], tags: [], threat: [], throttle: null, @@ -256,7 +256,7 @@ describe('create_rules_stream_from_ndjson', () => { query: '', language: 'kuery', max_signals: 100, - lists: [], + exceptions_list: [], tags: [], threat: [], throttle: null, @@ -303,7 +303,7 @@ describe('create_rules_stream_from_ndjson', () => { query: '', language: 'kuery', max_signals: 100, - lists: [], + exceptions_list: [], tags: [], threat: [], throttle: null, @@ -330,7 +330,7 @@ describe('create_rules_stream_from_ndjson', () => { query: '', language: 'kuery', max_signals: 100, - lists: [], + exceptions_list: [], tags: [], threat: [], throttle: null, @@ -376,7 +376,7 @@ describe('create_rules_stream_from_ndjson', () => { query: '', language: 'kuery', max_signals: 100, - lists: [], + exceptions_list: [], tags: [], threat: [], throttle: null, @@ -405,7 +405,7 @@ describe('create_rules_stream_from_ndjson', () => { query: '', language: 'kuery', max_signals: 100, - lists: [], + exceptions_list: [], tags: [], threat: [], throttle: null, diff --git a/x-pack/plugins/siem/server/lib/detection_engine/rules/create_rules_stream_from_ndjson.ts b/x-pack/plugins/siem/server/lib/detection_engine/rules/create_rules_stream_from_ndjson.ts new file mode 100644 index 0000000000000..034813b8d100d --- /dev/null +++ b/x-pack/plugins/siem/server/lib/detection_engine/rules/create_rules_stream_from_ndjson.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 { Transform } from 'stream'; +import { ImportRuleAlertRest } from '../types'; +import { + createSplitStream, + createMapStream, + createConcatStream, +} from '../../../../../../../src/legacy/utils/streams'; +import { importRulesSchema } from '../routes/schemas/import_rules_schema'; +import { BadRequestError } from '../errors/bad_request_error'; +import { + parseNdjsonStrings, + filterExportedCounts, + createLimitStream, +} from '../../../utils/read_stream/create_stream_from_ndjson'; + +export const validateRules = (): Transform => { + return createMapStream((obj: ImportRuleAlertRest) => { + if (!(obj instanceof Error)) { + const validated = importRulesSchema.validate(obj); + if (validated.error != null) { + return new BadRequestError(validated.error.message); + } else { + return validated.value; + } + } else { + return obj; + } + }); +}; + +// TODO: Capture both the line number and the rule_id if you have that information for the error message +// eventually and then pass it down so we can give error messages on the line number + +/** + * Inspiration and the pattern of code followed is from: + * saved_objects/lib/create_saved_objects_stream_from_ndjson.ts + */ +export const createRulesStreamFromNdJson = (ruleLimit: number) => { + return [ + createSplitStream('\n'), + parseNdjsonStrings(), + filterExportedCounts(), + validateRules(), + createLimitStream(ruleLimit), + createConcatStream([]), + ]; +}; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/delete_rules.test.ts b/x-pack/plugins/siem/server/lib/detection_engine/rules/delete_rules.test.ts similarity index 95% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/delete_rules.test.ts rename to x-pack/plugins/siem/server/lib/detection_engine/rules/delete_rules.test.ts index 38fc1dc5d1930..68d01356a333a 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/delete_rules.test.ts +++ b/x-pack/plugins/siem/server/lib/detection_engine/rules/delete_rules.test.ts @@ -4,8 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ -import { actionsClientMock } from '../../../../../../../plugins/actions/server/mocks'; -import { alertsClientMock } from '../../../../../../../plugins/alerting/server/mocks'; +import { actionsClientMock } from '../../../../../actions/server/mocks'; +import { alertsClientMock } from '../../../../../alerting/server/mocks'; import { deleteRules } from './delete_rules'; import { readRules } from './read_rules'; jest.mock('./read_rules'); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/delete_rules.ts b/x-pack/plugins/siem/server/lib/detection_engine/rules/delete_rules.ts similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/delete_rules.ts rename to x-pack/plugins/siem/server/lib/detection_engine/rules/delete_rules.ts diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/find_rules.test.ts b/x-pack/plugins/siem/server/lib/detection_engine/rules/find_rules.test.ts similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/find_rules.test.ts rename to x-pack/plugins/siem/server/lib/detection_engine/rules/find_rules.test.ts diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/find_rules.ts b/x-pack/plugins/siem/server/lib/detection_engine/rules/find_rules.ts similarity index 92% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/find_rules.ts rename to x-pack/plugins/siem/server/lib/detection_engine/rules/find_rules.ts index f333a7c340705..ac600b0b5b218 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/find_rules.ts +++ b/x-pack/plugins/siem/server/lib/detection_engine/rules/find_rules.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { FindResult } from '../../../../../../../plugins/alerting/server'; +import { FindResult } from '../../../../../alerting/server'; import { SIGNALS_ID } from '../../../../common/constants'; import { FindRuleParams } from './types'; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/get_existing_prepackaged_rules.test.ts b/x-pack/plugins/siem/server/lib/detection_engine/rules/get_existing_prepackaged_rules.test.ts similarity index 98% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/get_existing_prepackaged_rules.test.ts rename to x-pack/plugins/siem/server/lib/detection_engine/rules/get_existing_prepackaged_rules.test.ts index 9774d10a37d6f..d79b428a2f76d 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/get_existing_prepackaged_rules.test.ts +++ b/x-pack/plugins/siem/server/lib/detection_engine/rules/get_existing_prepackaged_rules.test.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { alertsClientMock } from '../../../../../../../plugins/alerting/server/mocks'; +import { alertsClientMock } from '../../../../../alerting/server/mocks'; import { getResult, getFindResultWithSingleHit, diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/get_existing_prepackaged_rules.ts b/x-pack/plugins/siem/server/lib/detection_engine/rules/get_existing_prepackaged_rules.ts similarity index 96% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/get_existing_prepackaged_rules.ts rename to x-pack/plugins/siem/server/lib/detection_engine/rules/get_existing_prepackaged_rules.ts index b5e826ed42723..512164fc3d2e1 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/get_existing_prepackaged_rules.ts +++ b/x-pack/plugins/siem/server/lib/detection_engine/rules/get_existing_prepackaged_rules.ts @@ -5,7 +5,7 @@ */ import { INTERNAL_IMMUTABLE_KEY } from '../../../../common/constants'; -import { AlertsClient } from '../../../../../../../plugins/alerting/server'; +import { AlertsClient } from '../../../../../alerting/server'; import { RuleAlertType, isAlertTypes } from './types'; import { findRules } from './find_rules'; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/get_export_all.test.ts b/x-pack/plugins/siem/server/lib/detection_engine/rules/get_export_all.test.ts similarity index 97% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/get_export_all.test.ts rename to x-pack/plugins/siem/server/lib/detection_engine/rules/get_export_all.test.ts index dd004e3685b1d..6df250f1cf513 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/get_export_all.test.ts +++ b/x-pack/plugins/siem/server/lib/detection_engine/rules/get_export_all.test.ts @@ -9,7 +9,7 @@ import { getFindResultWithSingleHit, FindHit, } from '../routes/__mocks__/request_responses'; -import { alertsClientMock } from '../../../../../../../plugins/alerting/server/mocks'; +import { alertsClientMock } from '../../../../../alerting/server/mocks'; import { getExportAll } from './get_export_all'; import { unSetFeatureFlagsForTestsOnly, setFeatureFlagsForTestsOnly } from '../feature_flags'; @@ -79,7 +79,7 @@ describe('getExportAll', () => { throttle: 'no_actions', note: '# Investigative notes', version: 1, - lists: [ + exceptions_list: [ { field: 'source.ip', values_operator: 'included', diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/get_export_all.ts b/x-pack/plugins/siem/server/lib/detection_engine/rules/get_export_all.ts similarity index 78% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/get_export_all.ts rename to x-pack/plugins/siem/server/lib/detection_engine/rules/get_export_all.ts index 6a27abb66ce85..06e70f0bad184 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/get_export_all.ts +++ b/x-pack/plugins/siem/server/lib/detection_engine/rules/get_export_all.ts @@ -4,10 +4,11 @@ * you may not use this file except in compliance with the Elastic License. */ -import { AlertsClient } from '../../../../../../../plugins/alerting/server'; +import { AlertsClient } from '../../../../../alerting/server'; import { getNonPackagedRules } from './get_existing_prepackaged_rules'; import { getExportDetailsNdjson } from './get_export_details_ndjson'; -import { transformAlertsToRules, transformDataToNdjson } from '../routes/rules/utils'; +import { transformAlertsToRules } from '../routes/rules/utils'; +import { transformDataToNdjson } from '../../../utils/read_stream/create_stream_from_ndjson'; export const getExportAll = async ( alertsClient: AlertsClient diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/get_export_by_object_ids.test.ts b/x-pack/plugins/siem/server/lib/detection_engine/rules/get_export_by_object_ids.test.ts similarity index 98% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/get_export_by_object_ids.test.ts rename to x-pack/plugins/siem/server/lib/detection_engine/rules/get_export_by_object_ids.test.ts index 715cb23e8444a..092a9a8faf395 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/get_export_by_object_ids.test.ts +++ b/x-pack/plugins/siem/server/lib/detection_engine/rules/get_export_by_object_ids.test.ts @@ -11,7 +11,7 @@ import { FindHit, } from '../routes/__mocks__/request_responses'; import * as readRules from './read_rules'; -import { alertsClientMock } from '../../../../../../../plugins/alerting/server/mocks'; +import { alertsClientMock } from '../../../../../alerting/server/mocks'; import { setFeatureFlagsForTestsOnly, unSetFeatureFlagsForTestsOnly } from '../feature_flags'; describe('get_export_by_object_ids', () => { @@ -87,7 +87,7 @@ describe('get_export_by_object_ids', () => { throttle: 'no_actions', note: '# Investigative notes', version: 1, - lists: [ + exceptions_list: [ { field: 'source.ip', values_operator: 'included', @@ -215,7 +215,7 @@ describe('get_export_by_object_ids', () => { throttle: 'no_actions', note: '# Investigative notes', version: 1, - lists: [ + exceptions_list: [ { field: 'source.ip', values_operator: 'included', diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/get_export_by_object_ids.ts b/x-pack/plugins/siem/server/lib/detection_engine/rules/get_export_by_object_ids.ts similarity index 92% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/get_export_by_object_ids.ts rename to x-pack/plugins/siem/server/lib/detection_engine/rules/get_export_by_object_ids.ts index 6f642231ebbaf..02039b9de3c7a 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/get_export_by_object_ids.ts +++ b/x-pack/plugins/siem/server/lib/detection_engine/rules/get_export_by_object_ids.ts @@ -4,12 +4,13 @@ * you may not use this file except in compliance with the Elastic License. */ -import { AlertsClient } from '../../../../../../../plugins/alerting/server'; +import { AlertsClient } from '../../../../../alerting/server'; import { getExportDetailsNdjson } from './get_export_details_ndjson'; import { isAlertType } from '../rules/types'; import { readRules } from './read_rules'; -import { transformDataToNdjson, transformAlertToRule } from '../routes/rules/utils'; +import { transformAlertToRule } from '../routes/rules/utils'; import { OutputRuleAlertRest } from '../types'; +import { transformDataToNdjson } from '../../../utils/read_stream/create_stream_from_ndjson'; interface ExportSuccesRule { statusCode: 200; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/get_export_details_ndjson.test.ts b/x-pack/plugins/siem/server/lib/detection_engine/rules/get_export_details_ndjson.test.ts similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/get_export_details_ndjson.test.ts rename to x-pack/plugins/siem/server/lib/detection_engine/rules/get_export_details_ndjson.test.ts diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/get_export_details_ndjson.ts b/x-pack/plugins/siem/server/lib/detection_engine/rules/get_export_details_ndjson.ts similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/get_export_details_ndjson.ts rename to x-pack/plugins/siem/server/lib/detection_engine/rules/get_export_details_ndjson.ts diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/get_prepackaged_rules.test.ts b/x-pack/plugins/siem/server/lib/detection_engine/rules/get_prepackaged_rules.test.ts similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/get_prepackaged_rules.test.ts rename to x-pack/plugins/siem/server/lib/detection_engine/rules/get_prepackaged_rules.test.ts diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/get_prepackaged_rules.ts b/x-pack/plugins/siem/server/lib/detection_engine/rules/get_prepackaged_rules.ts similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/get_prepackaged_rules.ts rename to x-pack/plugins/siem/server/lib/detection_engine/rules/get_prepackaged_rules.ts diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/get_rules_to_install.test.ts b/x-pack/plugins/siem/server/lib/detection_engine/rules/get_rules_to_install.test.ts similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/get_rules_to_install.test.ts rename to x-pack/plugins/siem/server/lib/detection_engine/rules/get_rules_to_install.test.ts diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/get_rules_to_install.ts b/x-pack/plugins/siem/server/lib/detection_engine/rules/get_rules_to_install.ts similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/get_rules_to_install.ts rename to x-pack/plugins/siem/server/lib/detection_engine/rules/get_rules_to_install.ts diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/get_rules_to_update.test.ts b/x-pack/plugins/siem/server/lib/detection_engine/rules/get_rules_to_update.test.ts similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/get_rules_to_update.test.ts rename to x-pack/plugins/siem/server/lib/detection_engine/rules/get_rules_to_update.test.ts diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/get_rules_to_update.ts b/x-pack/plugins/siem/server/lib/detection_engine/rules/get_rules_to_update.ts similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/get_rules_to_update.ts rename to x-pack/plugins/siem/server/lib/detection_engine/rules/get_rules_to_update.ts diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/install_prepacked_rules.ts b/x-pack/plugins/siem/server/lib/detection_engine/rules/install_prepacked_rules.ts similarity index 88% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/install_prepacked_rules.ts rename to x-pack/plugins/siem/server/lib/detection_engine/rules/install_prepacked_rules.ts index 6d4bacb9cc243..3d3ed52b2feb2 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/install_prepacked_rules.ts +++ b/x-pack/plugins/siem/server/lib/detection_engine/rules/install_prepacked_rules.ts @@ -4,9 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ -import { Alert } from '../../../../../../../plugins/alerting/common'; -import { ActionsClient } from '../../../../../../../plugins/actions/server'; -import { AlertsClient } from '../../../../../../../plugins/alerting/server'; +import { Alert } from '../../../../../alerting/common'; +import { ActionsClient } from '../../../../../actions/server'; +import { AlertsClient } from '../../../../../alerting/server'; import { createRules } from './create_rules'; import { PrepackagedRules } from '../types'; @@ -46,7 +46,7 @@ export const installPrepackagedRules = ( references, note, version, - lists, + exceptions_list, } = rule; return [ ...acc, @@ -82,7 +82,7 @@ export const installPrepackagedRules = ( references, note, version, - lists, + exceptions_list, actions: [], // At this time there is no pre-packaged actions }), ]; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/patch_rules.test.ts b/x-pack/plugins/siem/server/lib/detection_engine/rules/patch_rules.test.ts similarity index 91% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/patch_rules.test.ts rename to x-pack/plugins/siem/server/lib/detection_engine/rules/patch_rules.test.ts index 3108fc5f3b718..f93b0aceb5e6e 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/patch_rules.test.ts +++ b/x-pack/plugins/siem/server/lib/detection_engine/rules/patch_rules.test.ts @@ -4,9 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ -import { savedObjectsClientMock } from '../../../../../../../../src/core/server/mocks'; -import { alertsClientMock } from '../../../../../../../plugins/alerting/server/mocks'; -import { actionsClientMock } from '../../../../../../../plugins/actions/server/mocks'; +import { savedObjectsClientMock } from '../../../../../../../src/core/server/mocks'; +import { alertsClientMock } from '../../../../../alerting/server/mocks'; +import { actionsClientMock } from '../../../../../actions/server/mocks'; import { getResult, getMlResult } from '../routes/__mocks__/request_responses'; import { patchRules } from './patch_rules'; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/patch_rules.ts b/x-pack/plugins/siem/server/lib/detection_engine/rules/patch_rules.ts similarity index 96% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/patch_rules.ts rename to x-pack/plugins/siem/server/lib/detection_engine/rules/patch_rules.ts index 5c4889ec5fd68..c23f539b58160 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/patch_rules.ts +++ b/x-pack/plugins/siem/server/lib/detection_engine/rules/patch_rules.ts @@ -5,7 +5,7 @@ */ import { defaults } from 'lodash/fp'; -import { PartialAlert } from '../../../../../../../plugins/alerting/server'; +import { PartialAlert } from '../../../../../alerting/server'; import { readRules } from './read_rules'; import { PatchRuleParams, IRuleSavedAttributesSavedObjectAttributes } from './types'; import { addTags } from './add_tags'; @@ -44,7 +44,7 @@ export const patchRules = async ({ references, note, version, - lists, + exceptions_list, anomalyThreshold, machineLearningJobId, }: PatchRuleParams): Promise => { @@ -78,7 +78,7 @@ export const patchRules = async ({ references, version, note, - lists, + exceptions_list, anomalyThreshold, machineLearningJobId, }); @@ -110,7 +110,7 @@ export const patchRules = async ({ references, note, version: calculatedVersion, - lists, + exceptions_list, anomalyThreshold, machineLearningJobId, } diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/403_response_to_a_post.json b/x-pack/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/403_response_to_a_post.json similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/403_response_to_a_post.json rename to x-pack/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/403_response_to_a_post.json diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/405_response_method_not_allowed.json b/x-pack/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/405_response_method_not_allowed.json similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/405_response_method_not_allowed.json rename to x-pack/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/405_response_method_not_allowed.json diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/elastic_endpoint_security_adversary_behavior_detected.json b/x-pack/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/elastic_endpoint_security_adversary_behavior_detected.json similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/elastic_endpoint_security_adversary_behavior_detected.json rename to x-pack/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/elastic_endpoint_security_adversary_behavior_detected.json diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/elastic_endpoint_security_cred_dumping_detected.json b/x-pack/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/elastic_endpoint_security_cred_dumping_detected.json similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/elastic_endpoint_security_cred_dumping_detected.json rename to x-pack/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/elastic_endpoint_security_cred_dumping_detected.json diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/elastic_endpoint_security_cred_dumping_prevented.json b/x-pack/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/elastic_endpoint_security_cred_dumping_prevented.json similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/elastic_endpoint_security_cred_dumping_prevented.json rename to x-pack/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/elastic_endpoint_security_cred_dumping_prevented.json diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/elastic_endpoint_security_cred_manipulation_detected.json b/x-pack/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/elastic_endpoint_security_cred_manipulation_detected.json similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/elastic_endpoint_security_cred_manipulation_detected.json rename to x-pack/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/elastic_endpoint_security_cred_manipulation_detected.json diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/elastic_endpoint_security_cred_manipulation_prevented.json b/x-pack/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/elastic_endpoint_security_cred_manipulation_prevented.json similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/elastic_endpoint_security_cred_manipulation_prevented.json rename to x-pack/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/elastic_endpoint_security_cred_manipulation_prevented.json diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/elastic_endpoint_security_exploit_detected.json b/x-pack/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/elastic_endpoint_security_exploit_detected.json similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/elastic_endpoint_security_exploit_detected.json rename to x-pack/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/elastic_endpoint_security_exploit_detected.json diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/elastic_endpoint_security_exploit_prevented.json b/x-pack/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/elastic_endpoint_security_exploit_prevented.json similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/elastic_endpoint_security_exploit_prevented.json rename to x-pack/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/elastic_endpoint_security_exploit_prevented.json diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/elastic_endpoint_security_malware_detected.json b/x-pack/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/elastic_endpoint_security_malware_detected.json similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/elastic_endpoint_security_malware_detected.json rename to x-pack/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/elastic_endpoint_security_malware_detected.json diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/elastic_endpoint_security_malware_prevented.json b/x-pack/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/elastic_endpoint_security_malware_prevented.json similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/elastic_endpoint_security_malware_prevented.json rename to x-pack/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/elastic_endpoint_security_malware_prevented.json diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/elastic_endpoint_security_permission_theft_detected.json b/x-pack/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/elastic_endpoint_security_permission_theft_detected.json similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/elastic_endpoint_security_permission_theft_detected.json rename to x-pack/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/elastic_endpoint_security_permission_theft_detected.json diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/elastic_endpoint_security_permission_theft_prevented.json b/x-pack/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/elastic_endpoint_security_permission_theft_prevented.json similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/elastic_endpoint_security_permission_theft_prevented.json rename to x-pack/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/elastic_endpoint_security_permission_theft_prevented.json diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/elastic_endpoint_security_process_injection_detected.json b/x-pack/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/elastic_endpoint_security_process_injection_detected.json similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/elastic_endpoint_security_process_injection_detected.json rename to x-pack/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/elastic_endpoint_security_process_injection_detected.json diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/elastic_endpoint_security_process_injection_prevented.json b/x-pack/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/elastic_endpoint_security_process_injection_prevented.json similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/elastic_endpoint_security_process_injection_prevented.json rename to x-pack/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/elastic_endpoint_security_process_injection_prevented.json diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/elastic_endpoint_security_ransomware_detected.json b/x-pack/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/elastic_endpoint_security_ransomware_detected.json similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/elastic_endpoint_security_ransomware_detected.json rename to x-pack/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/elastic_endpoint_security_ransomware_detected.json diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/elastic_endpoint_security_ransomware_prevented.json b/x-pack/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/elastic_endpoint_security_ransomware_prevented.json similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/elastic_endpoint_security_ransomware_prevented.json rename to x-pack/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/elastic_endpoint_security_ransomware_prevented.json diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/eql_adding_the_hidden_file_attribute_with_via_attribexe.json b/x-pack/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/eql_adding_the_hidden_file_attribute_with_via_attribexe.json similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/eql_adding_the_hidden_file_attribute_with_via_attribexe.json rename to x-pack/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/eql_adding_the_hidden_file_attribute_with_via_attribexe.json diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/eql_adobe_hijack_persistence.json b/x-pack/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/eql_adobe_hijack_persistence.json similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/eql_adobe_hijack_persistence.json rename to x-pack/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/eql_adobe_hijack_persistence.json diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/eql_clearing_windows_event_logs.json b/x-pack/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/eql_clearing_windows_event_logs.json similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/eql_clearing_windows_event_logs.json rename to x-pack/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/eql_clearing_windows_event_logs.json diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/eql_delete_volume_usn_journal_with_fsutil.json b/x-pack/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/eql_delete_volume_usn_journal_with_fsutil.json similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/eql_delete_volume_usn_journal_with_fsutil.json rename to x-pack/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/eql_delete_volume_usn_journal_with_fsutil.json diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/eql_deleting_backup_catalogs_with_wbadmin.json b/x-pack/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/eql_deleting_backup_catalogs_with_wbadmin.json similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/eql_deleting_backup_catalogs_with_wbadmin.json rename to x-pack/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/eql_deleting_backup_catalogs_with_wbadmin.json diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/eql_direct_outbound_smb_connection.json b/x-pack/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/eql_direct_outbound_smb_connection.json similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/eql_direct_outbound_smb_connection.json rename to x-pack/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/eql_direct_outbound_smb_connection.json diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/eql_disable_windows_firewall_rules_with_netsh.json b/x-pack/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/eql_disable_windows_firewall_rules_with_netsh.json similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/eql_disable_windows_firewall_rules_with_netsh.json rename to x-pack/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/eql_disable_windows_firewall_rules_with_netsh.json diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/eql_encoding_or_decoding_files_via_certutil.json b/x-pack/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/eql_encoding_or_decoding_files_via_certutil.json similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/eql_encoding_or_decoding_files_via_certutil.json rename to x-pack/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/eql_encoding_or_decoding_files_via_certutil.json diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/eql_local_scheduled_task_commands.json b/x-pack/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/eql_local_scheduled_task_commands.json similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/eql_local_scheduled_task_commands.json rename to x-pack/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/eql_local_scheduled_task_commands.json diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/eql_local_service_commands.json b/x-pack/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/eql_local_service_commands.json similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/eql_local_service_commands.json rename to x-pack/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/eql_local_service_commands.json diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/eql_msbuild_making_network_connections.json b/x-pack/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/eql_msbuild_making_network_connections.json similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/eql_msbuild_making_network_connections.json rename to x-pack/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/eql_msbuild_making_network_connections.json diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/eql_mshta_making_network_connections.json b/x-pack/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/eql_mshta_making_network_connections.json similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/eql_mshta_making_network_connections.json rename to x-pack/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/eql_mshta_making_network_connections.json diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/eql_psexec_lateral_movement_command.json b/x-pack/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/eql_psexec_lateral_movement_command.json similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/eql_psexec_lateral_movement_command.json rename to x-pack/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/eql_psexec_lateral_movement_command.json diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/eql_suspicious_ms_office_child_process.json b/x-pack/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/eql_suspicious_ms_office_child_process.json similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/eql_suspicious_ms_office_child_process.json rename to x-pack/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/eql_suspicious_ms_office_child_process.json diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/eql_suspicious_ms_outlook_child_process.json b/x-pack/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/eql_suspicious_ms_outlook_child_process.json similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/eql_suspicious_ms_outlook_child_process.json rename to x-pack/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/eql_suspicious_ms_outlook_child_process.json diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/eql_system_shells_via_services.json b/x-pack/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/eql_system_shells_via_services.json similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/eql_system_shells_via_services.json rename to x-pack/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/eql_system_shells_via_services.json diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/eql_unusual_network_connection_via_rundll32.json b/x-pack/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/eql_unusual_network_connection_via_rundll32.json similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/eql_unusual_network_connection_via_rundll32.json rename to x-pack/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/eql_unusual_network_connection_via_rundll32.json diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/eql_unusual_parentchild_relationship.json b/x-pack/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/eql_unusual_parentchild_relationship.json similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/eql_unusual_parentchild_relationship.json rename to x-pack/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/eql_unusual_parentchild_relationship.json diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/eql_unusual_process_network_connection.json b/x-pack/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/eql_unusual_process_network_connection.json similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/eql_unusual_process_network_connection.json rename to x-pack/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/eql_unusual_process_network_connection.json diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/eql_user_account_creation.json b/x-pack/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/eql_user_account_creation.json similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/eql_user_account_creation.json rename to x-pack/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/eql_user_account_creation.json diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/eql_volume_shadow_copy_deletion_via_vssadmin.json b/x-pack/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/eql_volume_shadow_copy_deletion_via_vssadmin.json similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/eql_volume_shadow_copy_deletion_via_vssadmin.json rename to x-pack/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/eql_volume_shadow_copy_deletion_via_vssadmin.json diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/eql_volume_shadow_copy_deletion_via_wmic.json b/x-pack/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/eql_volume_shadow_copy_deletion_via_wmic.json similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/eql_volume_shadow_copy_deletion_via_wmic.json rename to x-pack/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/eql_volume_shadow_copy_deletion_via_wmic.json diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/eql_windows_script_executing_powershell.json b/x-pack/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/eql_windows_script_executing_powershell.json similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/eql_windows_script_executing_powershell.json rename to x-pack/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/eql_windows_script_executing_powershell.json diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/index.ts b/x-pack/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/index.ts similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/index.ts rename to x-pack/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/index.ts diff --git a/x-pack/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/linux_anomalous_network_activity.json b/x-pack/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/linux_anomalous_network_activity.json new file mode 100644 index 0000000000000..41f38173dba33 --- /dev/null +++ b/x-pack/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/linux_anomalous_network_activity.json @@ -0,0 +1,25 @@ +{ + "anomaly_threshold": 50, + "description": "Identifies Linux processes that do not usually use the network but have unexpected network activity, which can indicate command-and-control, lateral movement, persistence, or data exfiltration activity. A process with unusual network activity can denote process exploitation or injection, where the process is used to run persistence mechanisms that allow a malicious actor remote access or control of the host, data exfiltration, and execution of unauthorized network applications.", + "false_positives": [ + "A newly installed program or one that rarely uses the network could trigger this signal." + ], + "from": "now-45m", + "interval": "15m", + "machine_learning_job_id": "linux_anomalous_network_activity_ecs", + "name": "Unusual Linux Network Activity", + "references": [ + "https://www.elastic.co/guide/en/siem/guide/current/prebuilt-ml-jobs.html" + ], + "risk_score": 21, + "rule_id": "52afbdc5-db15-485e-bc24-f5707f820c4b", + "severity": "low", + "tags": [ + "Elastic", + "Linux", + "ML" + ], + "type": "machine_learning", + "note": "### Investigating Unusual Network Activity ###\nSignals from this rule indicate the presence of network activity from a Linux process for which network activity is rare and unusual. Here are some possible avenues of investigation:\n- Consider the IP addresses and ports. Are these used by normal but infrequent network workflows? Are they expected or unexpected? \n- If the destination IP address is remote or external, does it associate with an expected domain, organization or geography? Note: avoid interacting directly with suspected malicious IP addresses.\n- Consider the user as identified by the username field. Is this network activity part of an expected workflow for the user who ran the program?\n- Examine the history of execution. If this process manifested only very recently, it might be part of a new software package. If it has a consistent cadence - for example if it runs monthly or quarterly - it might be part of a monthly or quarterly business or maintenance process.\n- Examine the process arguments, title and working directory. These may provide indications as to the source of the program or the nature of the tasks it is performing.", + "version": 1 +} diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/linux_anomalous_network_port_activity.json b/x-pack/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/linux_anomalous_network_port_activity.json similarity index 97% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/linux_anomalous_network_port_activity.json rename to x-pack/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/linux_anomalous_network_port_activity.json index 19dd643945b17..d435d4c10f05c 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/linux_anomalous_network_port_activity.json +++ b/x-pack/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/linux_anomalous_network_port_activity.json @@ -4,7 +4,7 @@ "false_positives": [ "A newly installed program or one that rarely uses the network could trigger this signal." ], - "from": "now-16m", + "from": "now-45m", "interval": "15m", "machine_learning_job_id": "linux_anomalous_network_port_activity_ecs", "name": "Unusual Linux Network Port Activity", diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/linux_anomalous_network_service.json b/x-pack/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/linux_anomalous_network_service.json similarity index 97% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/linux_anomalous_network_service.json rename to x-pack/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/linux_anomalous_network_service.json index e2e5803618d06..0b82ce99d0b7f 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/linux_anomalous_network_service.json +++ b/x-pack/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/linux_anomalous_network_service.json @@ -4,7 +4,7 @@ "false_positives": [ "A newly installed program or one that rarely uses the network could trigger this signal." ], - "from": "now-16m", + "from": "now-45m", "interval": "15m", "machine_learning_job_id": "linux_anomalous_network_service", "name": "Unusual Linux Network Service", diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/linux_anomalous_network_url_activity.json b/x-pack/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/linux_anomalous_network_url_activity.json similarity index 98% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/linux_anomalous_network_url_activity.json rename to x-pack/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/linux_anomalous_network_url_activity.json index 40dd2e76c7214..26af34e18a4c8 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/linux_anomalous_network_url_activity.json +++ b/x-pack/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/linux_anomalous_network_url_activity.json @@ -4,7 +4,7 @@ "false_positives": [ "A new and unusual program or artifact download in the course of software upgrades, debugging, or troubleshooting could trigger this signal." ], - "from": "now-16m", + "from": "now-45m", "interval": "15m", "machine_learning_job_id": "linux_anomalous_network_url_activity_ecs", "name": "Unusual Linux Web Activity", diff --git a/x-pack/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/linux_anomalous_process_all_hosts.json b/x-pack/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/linux_anomalous_process_all_hosts.json new file mode 100644 index 0000000000000..103171bcdfe50 --- /dev/null +++ b/x-pack/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/linux_anomalous_process_all_hosts.json @@ -0,0 +1,25 @@ +{ + "anomaly_threshold": 50, + "description": "Searches for rare processes running on multiple Linux hosts in an entire fleet or network. This reduces the detection of false positives since automated maintenance processes usually only run occasionally on a single machine but are common to all or many hosts in a fleet.", + "false_positives": [ + "A newly installed program or one that runs rarely as part of a monthly or quarterly workflow could trigger this signal." + ], + "from": "now-45m", + "interval": "15m", + "machine_learning_job_id": "linux_anomalous_process_all_hosts_ecs", + "name": "Anomalous Process For a Linux Population", + "references": [ + "https://www.elastic.co/guide/en/siem/guide/current/prebuilt-ml-jobs.html" + ], + "risk_score": 21, + "rule_id": "647fc812-7996-4795-8869-9c4ea595fe88", + "severity": "low", + "tags": [ + "Elastic", + "Linux", + "ML" + ], + "type": "machine_learning", + "note": "### Investigating an Unusual Linux Process ###\nSignals from this rule indicate the presence of a Linux process that is rare and unusual for all of the monitored Linux hosts for which Auditbeat data is available. Here are some possible avenues of investigation:\n- Consider the user as identified by the username field. Is this program part of an expected workflow for the user who ran this program on this host?\n- Examine the history of execution. If this process manifested only very recently, it might be part of a new software package. If it has a consistent cadence - for example if it runs monthly or quarterly - it might be part of a monthly or quarterly business process.\n- Examine the process arguments, title and working directory. These may provide indications as to the source of the program or the nature of the tasks it is performing.", + "version": 1 +} diff --git a/x-pack/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/linux_anomalous_user_name.json b/x-pack/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/linux_anomalous_user_name.json new file mode 100644 index 0000000000000..6642bb5d73fbd --- /dev/null +++ b/x-pack/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/linux_anomalous_user_name.json @@ -0,0 +1,25 @@ +{ + "anomaly_threshold": 50, + "description": "A machine learning job detected activity for a username that is not normally active, which can indicate unauthorized changes, activity by unauthorized users, lateral movement, or compromised credentials. In many organizations, new usernames are not often created apart from specific types of system activities, such as creating new accounts for new employees. These user accounts quickly become active and routine. Events from rarely used usernames can point to suspicious activity. Additionally, automated Linux fleets tend to see activity from rarely used usernames only when personnel log in to make authorized or unauthorized changes, or threat actors have acquired credentials and log in for malicious purposes. Unusual usernames can also indicate pivoting, where compromised credentials are used to try and move laterally from one host to another.", + "false_positives": [ + "Uncommon user activity can be due to an engineer logging onto a server instance in order to perform manual troubleshooting or reconfiguration." + ], + "from": "now-45m", + "interval": "15m", + "machine_learning_job_id": "linux_anomalous_user_name_ecs", + "name": "Unusual Linux Username", + "references": [ + "https://www.elastic.co/guide/en/siem/guide/current/prebuilt-ml-jobs.html" + ], + "risk_score": 21, + "rule_id": "b347b919-665f-4aac-b9e8-68369bf2340c", + "severity": "low", + "tags": [ + "Elastic", + "Linux", + "ML" + ], + "type": "machine_learning", + "note": "### Investigating an Unusual Linux User ###\nSignals from this rule indicate activity for a Linux user name that is rare and unusual. Here are some possible avenues of investigation:\n- Consider the user as identified by the username field. Is this program part of an expected workflow for the user who ran this program on this host? Could this be related to troubleshooting or debugging activity by a developer or site reliability engineer?\n- Examine the history of user activity. If this user manifested only very recently, it might be a service account for a new software package. If it has a consistent cadence - for example if it runs monthly or quarterly - it might be part of a monthly or quarterly business process.\n- Examine the process arguments, title and working directory. These may provide indications as to the source of the program or the nature of the tasks that the user is performing.", + "version": 1 +} diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/linux_hping_activity.json b/x-pack/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/linux_hping_activity.json similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/linux_hping_activity.json rename to x-pack/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/linux_hping_activity.json diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/linux_iodine_activity.json b/x-pack/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/linux_iodine_activity.json similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/linux_iodine_activity.json rename to x-pack/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/linux_iodine_activity.json diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/linux_kernel_module_activity.json b/x-pack/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/linux_kernel_module_activity.json similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/linux_kernel_module_activity.json rename to x-pack/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/linux_kernel_module_activity.json diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/linux_mknod_activity.json b/x-pack/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/linux_mknod_activity.json similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/linux_mknod_activity.json rename to x-pack/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/linux_mknod_activity.json diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/linux_netcat_network_connection.json b/x-pack/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/linux_netcat_network_connection.json similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/linux_netcat_network_connection.json rename to x-pack/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/linux_netcat_network_connection.json diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/linux_nmap_activity.json b/x-pack/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/linux_nmap_activity.json similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/linux_nmap_activity.json rename to x-pack/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/linux_nmap_activity.json diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/linux_nping_activity.json b/x-pack/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/linux_nping_activity.json similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/linux_nping_activity.json rename to x-pack/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/linux_nping_activity.json diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/linux_process_started_in_temp_directory.json b/x-pack/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/linux_process_started_in_temp_directory.json similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/linux_process_started_in_temp_directory.json rename to x-pack/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/linux_process_started_in_temp_directory.json diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/linux_shell_activity_by_web_server.json b/x-pack/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/linux_shell_activity_by_web_server.json similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/linux_shell_activity_by_web_server.json rename to x-pack/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/linux_shell_activity_by_web_server.json diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/linux_socat_activity.json b/x-pack/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/linux_socat_activity.json similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/linux_socat_activity.json rename to x-pack/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/linux_socat_activity.json diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/linux_strace_activity.json b/x-pack/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/linux_strace_activity.json similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/linux_strace_activity.json rename to x-pack/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/linux_strace_activity.json diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/linux_tcpdump_activity.json b/x-pack/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/linux_tcpdump_activity.json similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/linux_tcpdump_activity.json rename to x-pack/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/linux_tcpdump_activity.json diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/linux_whoami_commmand.json b/x-pack/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/linux_whoami_commmand.json similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/linux_whoami_commmand.json rename to x-pack/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/linux_whoami_commmand.json diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/network_dns_directly_to_the_internet.json b/x-pack/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/network_dns_directly_to_the_internet.json similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/network_dns_directly_to_the_internet.json rename to x-pack/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/network_dns_directly_to_the_internet.json diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/network_ftp_file_transfer_protocol_activity_to_the_internet.json b/x-pack/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/network_ftp_file_transfer_protocol_activity_to_the_internet.json similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/network_ftp_file_transfer_protocol_activity_to_the_internet.json rename to x-pack/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/network_ftp_file_transfer_protocol_activity_to_the_internet.json diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/network_irc_internet_relay_chat_protocol_activity_to_the_internet.json b/x-pack/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/network_irc_internet_relay_chat_protocol_activity_to_the_internet.json similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/network_irc_internet_relay_chat_protocol_activity_to_the_internet.json rename to x-pack/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/network_irc_internet_relay_chat_protocol_activity_to_the_internet.json diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/network_nat_traversal_port_activity.json b/x-pack/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/network_nat_traversal_port_activity.json similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/network_nat_traversal_port_activity.json rename to x-pack/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/network_nat_traversal_port_activity.json diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/network_port_26_activity.json b/x-pack/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/network_port_26_activity.json similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/network_port_26_activity.json rename to x-pack/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/network_port_26_activity.json diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/network_port_8000_activity_to_the_internet.json b/x-pack/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/network_port_8000_activity_to_the_internet.json similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/network_port_8000_activity_to_the_internet.json rename to x-pack/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/network_port_8000_activity_to_the_internet.json diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/network_pptp_point_to_point_tunneling_protocol_activity.json b/x-pack/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/network_pptp_point_to_point_tunneling_protocol_activity.json similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/network_pptp_point_to_point_tunneling_protocol_activity.json rename to x-pack/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/network_pptp_point_to_point_tunneling_protocol_activity.json diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/network_proxy_port_activity_to_the_internet.json b/x-pack/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/network_proxy_port_activity_to_the_internet.json similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/network_proxy_port_activity_to_the_internet.json rename to x-pack/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/network_proxy_port_activity_to_the_internet.json diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/network_rdp_remote_desktop_protocol_from_the_internet.json b/x-pack/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/network_rdp_remote_desktop_protocol_from_the_internet.json similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/network_rdp_remote_desktop_protocol_from_the_internet.json rename to x-pack/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/network_rdp_remote_desktop_protocol_from_the_internet.json diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/network_rdp_remote_desktop_protocol_to_the_internet.json b/x-pack/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/network_rdp_remote_desktop_protocol_to_the_internet.json similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/network_rdp_remote_desktop_protocol_to_the_internet.json rename to x-pack/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/network_rdp_remote_desktop_protocol_to_the_internet.json diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/network_rpc_remote_procedure_call_from_the_internet.json b/x-pack/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/network_rpc_remote_procedure_call_from_the_internet.json similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/network_rpc_remote_procedure_call_from_the_internet.json rename to x-pack/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/network_rpc_remote_procedure_call_from_the_internet.json diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/network_rpc_remote_procedure_call_to_the_internet.json b/x-pack/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/network_rpc_remote_procedure_call_to_the_internet.json similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/network_rpc_remote_procedure_call_to_the_internet.json rename to x-pack/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/network_rpc_remote_procedure_call_to_the_internet.json diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/network_smb_windows_file_sharing_activity_to_the_internet.json b/x-pack/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/network_smb_windows_file_sharing_activity_to_the_internet.json similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/network_smb_windows_file_sharing_activity_to_the_internet.json rename to x-pack/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/network_smb_windows_file_sharing_activity_to_the_internet.json diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/network_smtp_to_the_internet.json b/x-pack/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/network_smtp_to_the_internet.json similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/network_smtp_to_the_internet.json rename to x-pack/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/network_smtp_to_the_internet.json diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/network_sql_server_port_activity_to_the_internet.json b/x-pack/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/network_sql_server_port_activity_to_the_internet.json similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/network_sql_server_port_activity_to_the_internet.json rename to x-pack/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/network_sql_server_port_activity_to_the_internet.json diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/network_ssh_secure_shell_from_the_internet.json b/x-pack/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/network_ssh_secure_shell_from_the_internet.json similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/network_ssh_secure_shell_from_the_internet.json rename to x-pack/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/network_ssh_secure_shell_from_the_internet.json diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/network_ssh_secure_shell_to_the_internet.json b/x-pack/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/network_ssh_secure_shell_to_the_internet.json similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/network_ssh_secure_shell_to_the_internet.json rename to x-pack/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/network_ssh_secure_shell_to_the_internet.json diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/network_telnet_port_activity.json b/x-pack/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/network_telnet_port_activity.json similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/network_telnet_port_activity.json rename to x-pack/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/network_telnet_port_activity.json diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/network_tor_activity_to_the_internet.json b/x-pack/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/network_tor_activity_to_the_internet.json similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/network_tor_activity_to_the_internet.json rename to x-pack/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/network_tor_activity_to_the_internet.json diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/network_vnc_virtual_network_computing_from_the_internet.json b/x-pack/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/network_vnc_virtual_network_computing_from_the_internet.json similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/network_vnc_virtual_network_computing_from_the_internet.json rename to x-pack/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/network_vnc_virtual_network_computing_from_the_internet.json diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/network_vnc_virtual_network_computing_to_the_internet.json b/x-pack/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/network_vnc_virtual_network_computing_to_the_internet.json similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/network_vnc_virtual_network_computing_to_the_internet.json rename to x-pack/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/network_vnc_virtual_network_computing_to_the_internet.json diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/notice.ts b/x-pack/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/notice.ts similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/notice.ts rename to x-pack/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/notice.ts diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/null_user_agent.json b/x-pack/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/null_user_agent.json similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/null_user_agent.json rename to x-pack/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/null_user_agent.json diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/packetbeat_dns_tunneling.json b/x-pack/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/packetbeat_dns_tunneling.json similarity index 97% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/packetbeat_dns_tunneling.json rename to x-pack/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/packetbeat_dns_tunneling.json index c70725dcb645a..765515ffda27c 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/packetbeat_dns_tunneling.json +++ b/x-pack/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/packetbeat_dns_tunneling.json @@ -4,7 +4,7 @@ "false_positives": [ "DNS domains that use large numbers of child domains, such as software or content distribution networks, can trigger this signal and such parent domains can be excluded." ], - "from": "now-16m", + "from": "now-45m", "interval": "15m", "machine_learning_job_id": "packetbeat_dns_tunneling", "name": "DNS Tunneling", diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/packetbeat_rare_dns_question.json b/x-pack/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/packetbeat_rare_dns_question.json similarity index 98% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/packetbeat_rare_dns_question.json rename to x-pack/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/packetbeat_rare_dns_question.json index 3ed40ddf27864..79c30c5b38378 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/packetbeat_rare_dns_question.json +++ b/x-pack/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/packetbeat_rare_dns_question.json @@ -4,7 +4,7 @@ "false_positives": [ "A newly installed program or one that runs rarely as part of a monthly or quarterly workflow could trigger this signal. Network activity that occurs rarely, in small quantities, can trigger this signal. Possible examples are browsing technical support or vendor networks sparsely. A user who visits a new or unique web destination may trigger this signal." ], - "from": "now-16m", + "from": "now-45m", "interval": "15m", "machine_learning_job_id": "packetbeat_rare_dns_question", "name": "Unusual DNS Activity", diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/packetbeat_rare_server_domain.json b/x-pack/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/packetbeat_rare_server_domain.json similarity index 98% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/packetbeat_rare_server_domain.json rename to x-pack/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/packetbeat_rare_server_domain.json index c49bc95be75d2..7b14ad62f6c93 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/packetbeat_rare_server_domain.json +++ b/x-pack/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/packetbeat_rare_server_domain.json @@ -4,7 +4,7 @@ "false_positives": [ "Web activity that occurs rarely in small quantities can trigger this signal. Possible examples are browsing technical support or vendor URLs that are used very sparsely. A user who visits a new and unique web destination may trigger this signal when the activity is sparse. Web applications that generate URLs unique to a transaction may trigger this when they are used sparsely. Web domains can be excluded in cases such as these." ], - "from": "now-16m", + "from": "now-45m", "interval": "15m", "machine_learning_job_id": "packetbeat_rare_server_domain", "name": "Unusual Network Destination Domain Name", diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/packetbeat_rare_urls.json b/x-pack/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/packetbeat_rare_urls.json similarity index 98% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/packetbeat_rare_urls.json rename to x-pack/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/packetbeat_rare_urls.json index 02a4a5f729a16..76767545e794a 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/packetbeat_rare_urls.json +++ b/x-pack/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/packetbeat_rare_urls.json @@ -4,7 +4,7 @@ "false_positives": [ "Web activity that occurs rarely in small quantities can trigger this signal. Possible examples are browsing technical support or vendor URLs that are used very sparsely. A user who visits a new and unique web destination may trigger this signal when the activity is sparse. Web applications that generate URLs unique to a transaction may trigger this when they are used sparsely. Web domains can be excluded in cases such as these." ], - "from": "now-16m", + "from": "now-45m", "interval": "15m", "machine_learning_job_id": "packetbeat_rare_urls", "name": "Unusual Web Request", diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/packetbeat_rare_user_agent.json b/x-pack/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/packetbeat_rare_user_agent.json similarity index 98% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/packetbeat_rare_user_agent.json rename to x-pack/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/packetbeat_rare_user_agent.json index 76ed6b263a704..1dc49203f31c1 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/packetbeat_rare_user_agent.json +++ b/x-pack/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/packetbeat_rare_user_agent.json @@ -4,7 +4,7 @@ "false_positives": [ "Web activity that is uncommon, like security scans, may trigger this signal and may need to be excluded. A new or rarely used program that calls web services may trigger this signal." ], - "from": "now-16m", + "from": "now-45m", "interval": "15m", "machine_learning_job_id": "packetbeat_rare_user_agent", "name": "Unusual Web User Agent", diff --git a/x-pack/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/rare_process_by_host_linux.json b/x-pack/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/rare_process_by_host_linux.json new file mode 100644 index 0000000000000..8ae1b84aaf199 --- /dev/null +++ b/x-pack/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/rare_process_by_host_linux.json @@ -0,0 +1,25 @@ +{ + "anomaly_threshold": 50, + "description": "Identifies rare processes that do not usually run on individual hosts, which can indicate execution of unauthorized services, malware, or persistence mechanisms. Processes are considered rare when they only run occasionally as compared with other processes running on the host.", + "false_positives": [ + "A newly installed program or one that runs rarely as part of a monthly or quarterly workflow could trigger this signal." + ], + "from": "now-45m", + "interval": "15m", + "machine_learning_job_id": "rare_process_by_host_linux_ecs", + "name": "Unusual Process For a Linux Host", + "references": [ + "https://www.elastic.co/guide/en/siem/guide/current/prebuilt-ml-jobs.html" + ], + "risk_score": 21, + "rule_id": "46f804f5-b289-43d6-a881-9387cf594f75", + "severity": "low", + "tags": [ + "Elastic", + "Linux", + "ML" + ], + "type": "machine_learning", + "note": "### Investigating an Unusual Linux Process ###\nSignals from this rule indicate the presence of a Linux process that is rare and unusual for the host it ran on. Here are some possible avenues of investigation:\n- Consider the user as identified by the username field. Is this program part of an expected workflow for the user who ran this program on this host?\n- Examine the history of execution. If this process manifested only very recently, it might be part of a new software package. If it has a consistent cadence - for example if it runs monthly or quarterly - it might be part of a monthly or quarterly business process.\n- Examine the process arguments, title and working directory. These may provide indications as to the source of the program or the nature of the tasks it is performing.", + "version": 1 +} diff --git a/x-pack/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/rare_process_by_host_windows.json b/x-pack/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/rare_process_by_host_windows.json new file mode 100644 index 0000000000000..879cee388f5dd --- /dev/null +++ b/x-pack/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/rare_process_by_host_windows.json @@ -0,0 +1,25 @@ +{ + "anomaly_threshold": 50, + "description": "Identifies rare processes that do not usually run on individual hosts, which can indicate execution of unauthorized services, malware, or persistence mechanisms. Processes are considered rare when they only run occasionally as compared with other processes running on the host.", + "false_positives": [ + "A newly installed program or one that runs rarely as part of a monthly or quarterly workflow could trigger this signal." + ], + "from": "now-45m", + "interval": "15m", + "machine_learning_job_id": "rare_process_by_host_windows_ecs", + "name": "Unusual Process For a Windows Host", + "references": [ + "https://www.elastic.co/guide/en/siem/guide/current/prebuilt-ml-jobs.html" + ], + "risk_score": 21, + "rule_id": "6d448b96-c922-4adb-b51c-b767f1ea5b76", + "severity": "low", + "tags": [ + "Elastic", + "ML", + "Windows" + ], + "type": "machine_learning", + "note": "### Investigating an Unusual Windows Process ###\nSignals from this rule indicate the presence of a Windows process that is rare and unusual for the host it ran on. Here are some possible avenues of investigation:\n- Consider the user as identified by the username field. Is this program part of an expected workflow for the user who ran this program on this host?\n- Examine the history of execution. If this process manifested only very recently, it might be part of a new software package. If it has a consistent cadence - for example if it runs monthly or quarterly - it might be part of a monthly or quarterly business process.\n- Examine the process metadata like the values of the Company, Description and Product fields which may indicate whether the program is associated with an expected software vendor or package. \n- Examine arguments and working directory. These may provide indications as to the source of the program or the nature of the tasks it is performing.\n- Consider the same for the parent process. If the parent process is a legitimate system utility or service, this could be related to software updates or system management. If the parent process is something user-facing like an Office application, this process could be more suspicious.\n- If you have file hash values in the event data, and you suspect malware, you can optionally run a search for the file hash to see if the file is identified as malware by anti-malware tools. ", + "version": 1 +} diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/sqlmap_user_agent.json b/x-pack/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/sqlmap_user_agent.json similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/sqlmap_user_agent.json rename to x-pack/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/sqlmap_user_agent.json diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/suspicious_login_activity.json b/x-pack/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/suspicious_login_activity.json similarity index 97% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/suspicious_login_activity.json rename to x-pack/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/suspicious_login_activity.json index 915bc1bcfc051..4b94fdc6da147 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/suspicious_login_activity.json +++ b/x-pack/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/suspicious_login_activity.json @@ -4,7 +4,7 @@ "false_positives": [ "Security audits may trigger this signal. Conditions that generate bursts of failed logins, such as misconfigured applications or account lockouts could trigger this signal." ], - "from": "now-16m", + "from": "now-45m", "interval": "15m", "machine_learning_job_id": "suspicious_login_activity_ecs", "name": "Unusual Login Activity", diff --git a/x-pack/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/windows_anomalous_network_activity.json b/x-pack/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/windows_anomalous_network_activity.json new file mode 100644 index 0000000000000..1092bcb20bcc3 --- /dev/null +++ b/x-pack/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/windows_anomalous_network_activity.json @@ -0,0 +1,25 @@ +{ + "anomaly_threshold": 50, + "description": "Identifies Windows processes that do not usually use the network but have unexpected network activity, which can indicate command-and-control, lateral movement, persistence, or data exfiltration activity. A process with unusual network activity can denote process exploitation or injection, where the process is used to run persistence mechanisms that allow a malicious actor remote access or control of the host, data exfiltration, and execution of unauthorized network applications.", + "false_positives": [ + "A newly installed program or one that rarely uses the network could trigger this signal." + ], + "from": "now-45m", + "interval": "15m", + "machine_learning_job_id": "windows_anomalous_network_activity_ecs", + "name": "Unusual Windows Network Activity", + "references": [ + "https://www.elastic.co/guide/en/siem/guide/current/prebuilt-ml-jobs.html" + ], + "risk_score": 21, + "rule_id": "ba342eb2-583c-439f-b04d-1fdd7c1417cc", + "severity": "low", + "tags": [ + "Elastic", + "ML", + "Windows" + ], + "type": "machine_learning", + "note": "### Investigating Unusual Network Activity ###\nSignals from this rule indicate the presence of network activity from a Windows process for which network activity is very unusual. Here are some possible avenues of investigation:\n- Consider the IP addresses, protocol and ports. Are these used by normal but infrequent network workflows? Are they expected or unexpected? \n- If the destination IP address is remote or external, does it associate with an expected domain, organization or geography? Note: avoid interacting directly with suspected malicious IP addresses.\n- Consider the user as identified by the username field. Is this network activity part of an expected workflow for the user who ran the program?\n- Examine the history of execution. If this process manifested only very recently, it might be part of a new software package. If it has a consistent cadence - for example if it runs monthly or quarterly - it might be part of a monthly or quarterly business process.\n- Examine the process arguments, title and working directory. These may provide indications as to the source of the program or the nature of the tasks it is performing.\n- Consider the same for the parent process. If the parent process is a legitimate system utility or service, this could be related to software updates or system management. If the parent process is something user-facing like an Office application, this process could be more suspicious.\n- If you have file hash values in the event data, and you suspect malware, you can optionally run a search for the file hash to see if the file is identified as malware by anti-malware tools. ", + "version": 1 +} diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/windows_anomalous_path_activity.json b/x-pack/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/windows_anomalous_path_activity.json similarity index 98% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/windows_anomalous_path_activity.json rename to x-pack/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/windows_anomalous_path_activity.json index 082fce438ca9e..8a88607b9d5c9 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/windows_anomalous_path_activity.json +++ b/x-pack/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/windows_anomalous_path_activity.json @@ -4,7 +4,7 @@ "false_positives": [ "A new and unusual program or artifact download in the course of software upgrades, debugging, or troubleshooting could trigger this signal. Users downloading and running programs from unusual locations, such as temporary directories, browser caches, or profile paths could trigger this signal." ], - "from": "now-16m", + "from": "now-45m", "interval": "15m", "machine_learning_job_id": "windows_anomalous_path_activity_ecs", "name": "Unusual Windows Path Activity", diff --git a/x-pack/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/windows_anomalous_process_all_hosts.json b/x-pack/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/windows_anomalous_process_all_hosts.json new file mode 100644 index 0000000000000..f9adfeb830618 --- /dev/null +++ b/x-pack/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/windows_anomalous_process_all_hosts.json @@ -0,0 +1,25 @@ +{ + "anomaly_threshold": 50, + "description": "Searches for rare processes running on multiple hosts in an entire fleet or network. This reduces the detection of false positives since automated maintenance processes usually only run occasionally on a single machine but are common to all or many hosts in a fleet.", + "false_positives": [ + "A newly installed program or one that runs rarely as part of a monthly or quarterly workflow could trigger this signal." + ], + "from": "now-45m", + "interval": "15m", + "machine_learning_job_id": "windows_anomalous_process_all_hosts_ecs", + "name": "Anomalous Process For a Windows Population", + "references": [ + "https://www.elastic.co/guide/en/siem/guide/current/prebuilt-ml-jobs.html" + ], + "risk_score": 21, + "rule_id": "6e40d56f-5c0e-4ac6-aece-bee96645b172", + "severity": "low", + "tags": [ + "Elastic", + "ML", + "Windows" + ], + "type": "machine_learning", + "note": "### Investigating an Unusual Windows Process ###\nSignals from this rule indicate the presence of a Windows process that is rare and unusual for all of the Windows hosts for which Winlogbeat data is available. Here are some possible avenues of investigation:\n- Consider the user as identified by the username field. Is this program part of an expected workflow for the user who ran this program on this host?\n- Examine the history of execution. If this process manifested only very recently, it might be part of a new software package. If it has a consistent cadence - for example if it runs monthly or quarterly - it might be part of a monthly or quarterly business process.\n- Examine the process metadata like the values of the Company, Description and Product fields which may indicate whether the program is associated with an expected software vendor or package. \n- Examine arguments and working directory. These may provide indications as to the source of the program or the nature of the tasks it is performing.\n- Consider the same for the parent process. If the parent process is a legitimate system utility or service, this could be related to software updates or system management. If the parent process is something user-facing like an Office application, this process could be more suspicious.\n- If you have file hash values in the event data, and you suspect malware, you can optionally run a search for the file hash to see if the file is identified as malware by anti-malware tools. ", + "version": 1 +} diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/windows_anomalous_process_creation.json b/x-pack/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/windows_anomalous_process_creation.json similarity index 98% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/windows_anomalous_process_creation.json rename to x-pack/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/windows_anomalous_process_creation.json index 1b80e443baae6..98a078ccea4a4 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/windows_anomalous_process_creation.json +++ b/x-pack/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/windows_anomalous_process_creation.json @@ -4,7 +4,7 @@ "false_positives": [ "Users running scripts in the course of technical support operations of software upgrades could trigger this signal. A newly installed program or one that runs rarely as part of a monthly or quarterly workflow could trigger this signal." ], - "from": "now-16m", + "from": "now-45m", "interval": "15m", "machine_learning_job_id": "windows_anomalous_process_creation", "name": "Anomalous Windows Process Creation", diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/windows_anomalous_script.json b/x-pack/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/windows_anomalous_script.json similarity index 97% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/windows_anomalous_script.json rename to x-pack/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/windows_anomalous_script.json index 4de5443bcaf3f..564ca1782526f 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/windows_anomalous_script.json +++ b/x-pack/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/windows_anomalous_script.json @@ -4,7 +4,7 @@ "false_positives": [ "Certain kinds of security testing may trigger this signal. PowerShell scripts that use high levels of obfuscation or have unusual script block payloads may trigger this signal." ], - "from": "now-16m", + "from": "now-45m", "interval": "15m", "machine_learning_job_id": "windows_anomalous_script", "name": "Suspicious Powershell Script", diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/windows_anomalous_service.json b/x-pack/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/windows_anomalous_service.json similarity index 97% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/windows_anomalous_service.json rename to x-pack/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/windows_anomalous_service.json index 7e0641fee68c2..afef569f4ebb4 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/windows_anomalous_service.json +++ b/x-pack/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/windows_anomalous_service.json @@ -4,7 +4,7 @@ "false_positives": [ "A newly installed program or one that runs rarely as part of a monthly or quarterly workflow could trigger this signal." ], - "from": "now-16m", + "from": "now-45m", "interval": "15m", "machine_learning_job_id": "windows_anomalous_service", "name": "Unusual Windows Service", diff --git a/x-pack/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/windows_anomalous_user_name.json b/x-pack/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/windows_anomalous_user_name.json new file mode 100644 index 0000000000000..a0c6ff5c938f1 --- /dev/null +++ b/x-pack/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/windows_anomalous_user_name.json @@ -0,0 +1,25 @@ +{ + "anomaly_threshold": 50, + "description": "A machine learning job detected activity for a username that is not normally active, which can indicate unauthorized changes, activity by unauthorized users, lateral movement, or compromised credentials. In many organizations, new usernames are not often created apart from specific types of system activities, such as creating new accounts for new employees. These user accounts quickly become active and routine. Events from rarely used usernames can point to suspicious activity. Additionally, automated Linux fleets tend to see activity from rarely used usernames only when personnel log in to make authorized or unauthorized changes, or threat actors have acquired credentials and log in for malicious purposes. Unusual usernames can also indicate pivoting, where compromised credentials are used to try and move laterally from one host to another.", + "false_positives": [ + "Uncommon user activity can be due to an administrator or help desk technician logging onto a workstation or server in order to perform manual troubleshooting or reconfiguration." + ], + "from": "now-45m", + "interval": "15m", + "machine_learning_job_id": "windows_anomalous_user_name_ecs", + "name": "Unusual Windows Username", + "references": [ + "https://www.elastic.co/guide/en/siem/guide/current/prebuilt-ml-jobs.html" + ], + "risk_score": 21, + "rule_id": "1781d055-5c66-4adf-9c59-fc0fa58336a5", + "severity": "low", + "tags": [ + "Elastic", + "ML", + "Windows" + ], + "type": "machine_learning", + "note": "### Investigating an Unusual Windows User ###\nSignals from this rule indicate activity for a Windows user name that is rare and unusual. Here are some possible avenues of investigation:\n- Consider the user as identified by the username field. Is this program part of an expected workflow for the user who ran this program on this host? Could this be related to occasional troubleshooting or support activity?\n- Examine the history of user activity. If this user manifested only very recently, it might be a service account for a new software package. If it has a consistent cadence - for example if it runs monthly or quarterly - it might be part of a monthly or quarterly business process.\n- Examine the process arguments, title and working directory. These may provide indications as to the source of the program or the nature of the tasks that the user is performing.\n- Consider the same for the parent process. If the parent process is a legitimate system utility or service, this could be related to software updates or system management. If the parent process is something user-facing like an Office application, this process could be more suspicious.", + "version": 1 +} diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/windows_certutil_network_connection.json b/x-pack/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/windows_certutil_network_connection.json similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/windows_certutil_network_connection.json rename to x-pack/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/windows_certutil_network_connection.json diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/windows_command_prompt_connecting_to_the_internet.json b/x-pack/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/windows_command_prompt_connecting_to_the_internet.json similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/windows_command_prompt_connecting_to_the_internet.json rename to x-pack/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/windows_command_prompt_connecting_to_the_internet.json diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/windows_command_shell_started_by_powershell.json b/x-pack/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/windows_command_shell_started_by_powershell.json similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/windows_command_shell_started_by_powershell.json rename to x-pack/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/windows_command_shell_started_by_powershell.json diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/windows_command_shell_started_by_svchost.json b/x-pack/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/windows_command_shell_started_by_svchost.json similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/windows_command_shell_started_by_svchost.json rename to x-pack/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/windows_command_shell_started_by_svchost.json diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/windows_credential_dumping_msbuild.json b/x-pack/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/windows_credential_dumping_msbuild.json similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/windows_credential_dumping_msbuild.json rename to x-pack/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/windows_credential_dumping_msbuild.json diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/windows_cve_2020_0601.json b/x-pack/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/windows_cve_2020_0601.json similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/windows_cve_2020_0601.json rename to x-pack/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/windows_cve_2020_0601.json diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/windows_defense_evasion_via_filter_manager.json b/x-pack/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/windows_defense_evasion_via_filter_manager.json similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/windows_defense_evasion_via_filter_manager.json rename to x-pack/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/windows_defense_evasion_via_filter_manager.json diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/windows_execution_msbuild_started_by_office_app.json b/x-pack/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/windows_execution_msbuild_started_by_office_app.json similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/windows_execution_msbuild_started_by_office_app.json rename to x-pack/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/windows_execution_msbuild_started_by_office_app.json diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/windows_execution_msbuild_started_by_script.json b/x-pack/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/windows_execution_msbuild_started_by_script.json similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/windows_execution_msbuild_started_by_script.json rename to x-pack/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/windows_execution_msbuild_started_by_script.json diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/windows_execution_msbuild_started_by_system_process.json b/x-pack/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/windows_execution_msbuild_started_by_system_process.json similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/windows_execution_msbuild_started_by_system_process.json rename to x-pack/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/windows_execution_msbuild_started_by_system_process.json diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/windows_execution_msbuild_started_renamed.json b/x-pack/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/windows_execution_msbuild_started_renamed.json similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/windows_execution_msbuild_started_renamed.json rename to x-pack/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/windows_execution_msbuild_started_renamed.json diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/windows_execution_msbuild_started_unusal_process.json b/x-pack/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/windows_execution_msbuild_started_unusal_process.json similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/windows_execution_msbuild_started_unusal_process.json rename to x-pack/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/windows_execution_msbuild_started_unusal_process.json diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/windows_execution_via_compiled_html_file.json b/x-pack/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/windows_execution_via_compiled_html_file.json similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/windows_execution_via_compiled_html_file.json rename to x-pack/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/windows_execution_via_compiled_html_file.json diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/windows_execution_via_net_com_assemblies.json b/x-pack/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/windows_execution_via_net_com_assemblies.json similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/windows_execution_via_net_com_assemblies.json rename to x-pack/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/windows_execution_via_net_com_assemblies.json diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/windows_execution_via_regsvr32.json b/x-pack/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/windows_execution_via_regsvr32.json similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/windows_execution_via_regsvr32.json rename to x-pack/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/windows_execution_via_regsvr32.json diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/windows_execution_via_trusted_developer_utilities.json b/x-pack/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/windows_execution_via_trusted_developer_utilities.json similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/windows_execution_via_trusted_developer_utilities.json rename to x-pack/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/windows_execution_via_trusted_developer_utilities.json diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/windows_html_help_executable_program_connecting_to_the_internet.json b/x-pack/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/windows_html_help_executable_program_connecting_to_the_internet.json similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/windows_html_help_executable_program_connecting_to_the_internet.json rename to x-pack/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/windows_html_help_executable_program_connecting_to_the_internet.json diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/windows_injection_msbuild.json b/x-pack/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/windows_injection_msbuild.json similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/windows_injection_msbuild.json rename to x-pack/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/windows_injection_msbuild.json diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/windows_misc_lolbin_connecting_to_the_internet.json b/x-pack/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/windows_misc_lolbin_connecting_to_the_internet.json similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/windows_misc_lolbin_connecting_to_the_internet.json rename to x-pack/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/windows_misc_lolbin_connecting_to_the_internet.json diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/windows_modification_of_boot_config.json b/x-pack/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/windows_modification_of_boot_config.json similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/windows_modification_of_boot_config.json rename to x-pack/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/windows_modification_of_boot_config.json diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/windows_msxsl_network.json b/x-pack/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/windows_msxsl_network.json similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/windows_msxsl_network.json rename to x-pack/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/windows_msxsl_network.json diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/windows_net_command_system_account.json b/x-pack/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/windows_net_command_system_account.json similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/windows_net_command_system_account.json rename to x-pack/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/windows_net_command_system_account.json diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/windows_persistence_via_application_shimming.json b/x-pack/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/windows_persistence_via_application_shimming.json similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/windows_persistence_via_application_shimming.json rename to x-pack/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/windows_persistence_via_application_shimming.json diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/windows_priv_escalation_via_accessibility_features.json b/x-pack/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/windows_priv_escalation_via_accessibility_features.json similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/windows_priv_escalation_via_accessibility_features.json rename to x-pack/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/windows_priv_escalation_via_accessibility_features.json diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/windows_process_discovery_via_tasklist_command.json b/x-pack/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/windows_process_discovery_via_tasklist_command.json similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/windows_process_discovery_via_tasklist_command.json rename to x-pack/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/windows_process_discovery_via_tasklist_command.json diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/windows_rare_user_runas_event.json b/x-pack/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/windows_rare_user_runas_event.json similarity index 97% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/windows_rare_user_runas_event.json rename to x-pack/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/windows_rare_user_runas_event.json index 3dca119b5a28e..febaa57443f76 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/windows_rare_user_runas_event.json +++ b/x-pack/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/windows_rare_user_runas_event.json @@ -4,7 +4,7 @@ "false_positives": [ "Uncommon user privilege elevation activity can be due to an administrator, help desk technician, or a user performing manual troubleshooting or reconfiguration." ], - "from": "now-16m", + "from": "now-45m", "interval": "15m", "machine_learning_job_id": "windows_rare_user_runas_event", "name": "Unusual Windows User Privilege Elevation Activity", diff --git a/x-pack/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/windows_rare_user_type10_remote_login.json b/x-pack/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/windows_rare_user_type10_remote_login.json new file mode 100644 index 0000000000000..7318364c3aac2 --- /dev/null +++ b/x-pack/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/windows_rare_user_type10_remote_login.json @@ -0,0 +1,25 @@ +{ + "anomaly_threshold": 50, + "description": "A machine learning job detected an unusual remote desktop protocol (RDP) username, which can indicate account takeover or credentialed persistence using compromised accounts. RDP attacks, such as BlueKeep, also tend to use unusual usernames.", + "false_positives": [ + "Uncommon username activity can be due to an engineer logging onto a server instance in order to perform manual troubleshooting or reconfiguration." + ], + "from": "now-45m", + "interval": "15m", + "machine_learning_job_id": "windows_rare_user_type10_remote_login", + "name": "Unusual Windows Remote User", + "references": [ + "https://www.elastic.co/guide/en/siem/guide/current/prebuilt-ml-jobs.html" + ], + "risk_score": 21, + "rule_id": "1781d055-5c66-4adf-9e93-fc0fa69550c9", + "severity": "low", + "tags": [ + "Elastic", + "ML", + "Windows" + ], + "type": "machine_learning", + "note": "### Investigating an Unusual Windows User ###\nSignals from this rule indicate activity for a rare and unusual Windows RDP (remote desktop) user. Here are some possible avenues of investigation:\n- Consider the user as identified by the username field. Is the user part of a group who normally logs into Windows hosts using RDP (remote desktop protocol)? Is this logon activity part of an expected workflow for the user? \n- Consider the source of the login. If the source is remote, could this be related to occasional troubleshooting or support activity by a vendor or an employee working remotely?", + "version": 1 +} diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/windows_register_server_program_connecting_to_the_internet.json b/x-pack/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/windows_register_server_program_connecting_to_the_internet.json similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/windows_register_server_program_connecting_to_the_internet.json rename to x-pack/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/windows_register_server_program_connecting_to_the_internet.json diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/windows_signed_binary_proxy_execution.json b/x-pack/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/windows_signed_binary_proxy_execution.json similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/windows_signed_binary_proxy_execution.json rename to x-pack/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/windows_signed_binary_proxy_execution.json diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/windows_suspicious_pdf_reader.json b/x-pack/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/windows_suspicious_pdf_reader.json similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/windows_suspicious_pdf_reader.json rename to x-pack/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/windows_suspicious_pdf_reader.json diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/windows_suspicious_process_started_by_a_script.json b/x-pack/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/windows_suspicious_process_started_by_a_script.json similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/windows_suspicious_process_started_by_a_script.json rename to x-pack/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/windows_suspicious_process_started_by_a_script.json diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/windows_uac_bypass_event_viewer.json b/x-pack/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/windows_uac_bypass_event_viewer.json similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/windows_uac_bypass_event_viewer.json rename to x-pack/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/windows_uac_bypass_event_viewer.json diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/windows_whoami_command_activity.json b/x-pack/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/windows_whoami_command_activity.json similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/windows_whoami_command_activity.json rename to x-pack/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/windows_whoami_command_activity.json diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/read_rules.test.ts b/x-pack/plugins/siem/server/lib/detection_engine/rules/read_rules.test.ts similarity index 98% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/read_rules.test.ts rename to x-pack/plugins/siem/server/lib/detection_engine/rules/read_rules.test.ts index 38a883329318b..600848948be0c 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/read_rules.test.ts +++ b/x-pack/plugins/siem/server/lib/detection_engine/rules/read_rules.test.ts @@ -5,7 +5,7 @@ */ import { readRules } from './read_rules'; -import { alertsClientMock } from '../../../../../../../plugins/alerting/server/mocks'; +import { alertsClientMock } from '../../../../../alerting/server/mocks'; import { getResult, getFindResultWithSingleHit } from '../routes/__mocks__/request_responses'; export class TestError extends Error { diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/read_rules.ts b/x-pack/plugins/siem/server/lib/detection_engine/rules/read_rules.ts similarity index 95% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/read_rules.ts rename to x-pack/plugins/siem/server/lib/detection_engine/rules/read_rules.ts index 94e4e6357a4a0..9e0d5b3d05b3f 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/read_rules.ts +++ b/x-pack/plugins/siem/server/lib/detection_engine/rules/read_rules.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { SanitizedAlert } from '../../../../../../../plugins/alerting/common'; +import { SanitizedAlert } from '../../../../../alerting/common'; import { INTERNAL_RULE_ID_KEY } from '../../../../common/constants'; import { findRules } from './find_rules'; import { ReadRuleParams, isAlertType } from './types'; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/saved_object_mappings.ts b/x-pack/plugins/siem/server/lib/detection_engine/rules/saved_object_mappings.ts similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/saved_object_mappings.ts rename to x-pack/plugins/siem/server/lib/detection_engine/rules/saved_object_mappings.ts diff --git a/x-pack/plugins/siem/server/lib/detection_engine/rules/types.ts b/x-pack/plugins/siem/server/lib/detection_engine/rules/types.ts new file mode 100644 index 0000000000000..6fde199e0ba7d --- /dev/null +++ b/x-pack/plugins/siem/server/lib/detection_engine/rules/types.ts @@ -0,0 +1,194 @@ +/* + * 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/fp'; +import { Readable } from 'stream'; + +import { + SavedObject, + SavedObjectAttributes, + SavedObjectsFindResponse, + SavedObjectsClientContract, +} from 'kibana/server'; +import { AlertsClient, PartialAlert } from '../../../../../alerting/server'; +import { Alert } from '../../../../../alerting/common'; +import { SIGNALS_ID } from '../../../../common/constants'; +import { ActionsClient } from '../../../../../actions/server'; +import { RuleAlertParams, RuleTypeParams, RuleAlertParamsRest } from '../types'; + +export type PatchRuleAlertParamsRest = Partial & { + id: string | undefined; + rule_id: RuleAlertParams['ruleId'] | undefined; +}; + +export type UpdateRuleAlertParamsRest = RuleAlertParamsRest & { + id: string | undefined; + rule_id: RuleAlertParams['ruleId'] | undefined; +}; + +export interface FindParamsRest { + per_page: number; + page: number; + sort_field: string; + sort_order: 'asc' | 'desc'; + fields: string[]; + filter: string; +} + +export interface RuleAlertType extends Alert { + params: RuleTypeParams; +} + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export interface IRuleStatusAttributes extends Record { + alertId: string; // created alert id. + statusDate: string; + lastFailureAt: string | null | undefined; + lastFailureMessage: string | null | undefined; + lastSuccessAt: string | null | undefined; + lastSuccessMessage: string | null | undefined; + status: RuleStatusString | null | undefined; + lastLookBackDate: string | null | undefined; + gap: string | null | undefined; + bulkCreateTimeDurations: string[] | null | undefined; + searchAfterTimeDurations: string[] | null | undefined; +} + +export interface RuleStatusResponse { + [key: string]: { + current_status: IRuleStatusAttributes | null | undefined; + failures: IRuleStatusAttributes[] | null | undefined; + }; +} + +export interface IRuleSavedAttributesSavedObjectAttributes + extends IRuleStatusAttributes, + SavedObjectAttributes {} + +export interface IRuleStatusSavedObject { + type: string; + id: string; + attributes: Array>; + references: unknown[]; + updated_at: string; + version: string; +} + +export interface IRuleStatusFindType { + page: number; + per_page: number; + total: number; + saved_objects: IRuleStatusSavedObject[]; +} + +export type RuleStatusString = 'succeeded' | 'failed' | 'going to run'; + +export interface HapiReadableStream extends Readable { + hapi: { + filename: string; + }; +} +export interface ImportRulesRequestParams { + query: { overwrite: boolean }; + body: { file: HapiReadableStream }; +} + +export interface ExportRulesRequestParams { + body: { objects: Array<{ rule_id: string }> | null | undefined }; + query: { + file_name: string; + exclude_export_details: boolean; + }; +} + +export interface RuleRequestParams { + id: string | undefined; + rule_id: string | undefined; +} + +export type ReadRuleRequestParams = RuleRequestParams; +export type DeleteRuleRequestParams = RuleRequestParams; +export type DeleteRulesRequestParams = RuleRequestParams[]; + +export interface FindRuleParams { + alertsClient: AlertsClient; + perPage?: number; + page?: number; + sortField?: string; + filter?: string; + fields?: string[]; + sortOrder?: 'asc' | 'desc'; +} + +export interface FindRulesRequestParams { + per_page: number; + page: number; + search?: string; + sort_field?: string; + filter?: string; + fields?: string[]; + sort_order?: 'asc' | 'desc'; +} + +export interface FindRulesStatusesRequestParams { + ids: string[]; +} + +export interface Clients { + alertsClient: AlertsClient; + actionsClient: ActionsClient; +} + +export type PatchRuleParams = Partial> & { + id: string | undefined | null; + savedObjectsClient: SavedObjectsClientContract; +} & Clients; + +export type UpdateRuleParams = Omit & { + id: string | undefined | null; + savedObjectsClient: SavedObjectsClientContract; +} & Clients; + +export type DeleteRuleParams = Clients & { + id: string | undefined; + ruleId: string | undefined | null; +}; + +export type CreateRuleParams = Omit & { + ruleId: string; +} & Clients; + +export interface ReadRuleParams { + alertsClient: AlertsClient; + id?: string | undefined | null; + ruleId?: string | undefined | null; +} + +export const isAlertTypes = (partialAlert: PartialAlert[]): partialAlert is RuleAlertType[] => { + return partialAlert.every(rule => isAlertType(rule)); +}; + +export const isAlertType = (partialAlert: PartialAlert): partialAlert is RuleAlertType => { + return partialAlert.alertTypeId === SIGNALS_ID; +}; + +export const isRuleStatusSavedObjectType = ( + obj: unknown +): obj is SavedObject => { + return get('attributes', obj) != null; +}; + +export const isRuleStatusFindType = ( + obj: unknown +): obj is SavedObjectsFindResponse => { + return get('saved_objects', obj) != null; +}; + +export const isRuleStatusFindTypes = ( + obj: unknown[] | undefined +): obj is Array> => { + return obj ? obj.every(ruleStatus => isRuleStatusFindType(ruleStatus)) : false; +}; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/update_prepacked_rules.test.ts b/x-pack/plugins/siem/server/lib/detection_engine/rules/update_prepacked_rules.test.ts similarity index 86% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/update_prepacked_rules.test.ts rename to x-pack/plugins/siem/server/lib/detection_engine/rules/update_prepacked_rules.test.ts index 7a3f233475117..a9bbf75883d1f 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/update_prepacked_rules.test.ts +++ b/x-pack/plugins/siem/server/lib/detection_engine/rules/update_prepacked_rules.test.ts @@ -4,9 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ -import { savedObjectsClientMock } from '../../../../../../../../src/core/server/mocks'; -import { alertsClientMock } from '../../../../../../../plugins/alerting/server/mocks'; -import { actionsClientMock } from '../../../../../../../plugins/actions/server/mocks'; +import { savedObjectsClientMock } from '../../../../../../../src/core/server/mocks'; +import { alertsClientMock } from '../../../../../alerting/server/mocks'; +import { actionsClientMock } from '../../../../../actions/server/mocks'; import { mockPrepackagedRule } from '../routes/__mocks__/request_responses'; import { updatePrepackagedRules } from './update_prepacked_rules'; import { patchRules } from './patch_rules'; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/update_prepacked_rules.ts b/x-pack/plugins/siem/server/lib/detection_engine/rules/update_prepacked_rules.ts similarity index 92% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/update_prepacked_rules.ts rename to x-pack/plugins/siem/server/lib/detection_engine/rules/update_prepacked_rules.ts index 7eb0d8d1399be..b72b232c27f03 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/update_prepacked_rules.ts +++ b/x-pack/plugins/siem/server/lib/detection_engine/rules/update_prepacked_rules.ts @@ -5,8 +5,8 @@ */ import { SavedObjectsClientContract } from 'kibana/server'; -import { ActionsClient } from '../../../../../../../plugins/actions/server'; -import { AlertsClient } from '../../../../../../../plugins/alerting/server'; +import { ActionsClient } from '../../../../../actions/server'; +import { AlertsClient } from '../../../../../alerting/server'; import { patchRules } from './patch_rules'; import { PrepackagedRules } from '../types'; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/update_rules.test.ts b/x-pack/plugins/siem/server/lib/detection_engine/rules/update_rules.test.ts similarity index 91% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/update_rules.test.ts rename to x-pack/plugins/siem/server/lib/detection_engine/rules/update_rules.test.ts index 72f4cbcbe68e8..2565d269db478 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/update_rules.test.ts +++ b/x-pack/plugins/siem/server/lib/detection_engine/rules/update_rules.test.ts @@ -4,9 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ -import { savedObjectsClientMock } from '../../../../../../../../src/core/server/mocks'; -import { alertsClientMock } from '../../../../../../../plugins/alerting/server/mocks'; -import { actionsClientMock } from '../../../../../../../plugins/actions/server/mocks'; +import { savedObjectsClientMock } from '../../../../../../../src/core/server/mocks'; +import { alertsClientMock } from '../../../../../alerting/server/mocks'; +import { actionsClientMock } from '../../../../../actions/server/mocks'; import { getResult, getMlResult } from '../routes/__mocks__/request_responses'; import { updateRules } from './update_rules'; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/update_rules.ts b/x-pack/plugins/siem/server/lib/detection_engine/rules/update_rules.ts similarity index 92% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/update_rules.ts rename to x-pack/plugins/siem/server/lib/detection_engine/rules/update_rules.ts index 99326768ed33b..7ddbbd76b0661 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/update_rules.ts +++ b/x-pack/plugins/siem/server/lib/detection_engine/rules/update_rules.ts @@ -5,7 +5,7 @@ */ import { transformRuleToAlertAction } from '../../../../common/detection_engine/transform_actions'; -import { PartialAlert } from '../../../../../../../plugins/alerting/server'; +import { PartialAlert } from '../../../../../alerting/server'; import { readRules } from './read_rules'; import { IRuleSavedAttributesSavedObjectAttributes, UpdateRuleParams } from './types'; import { addTags } from './add_tags'; @@ -44,7 +44,7 @@ export const updateRules = async ({ references, version, note, - lists, + exceptions_list, anomalyThreshold, machineLearningJobId, actions, @@ -83,8 +83,8 @@ export const updateRules = async ({ machineLearningJobId, }); - // TODO: Remove this and use regular lists once the feature is stable for a release - const listsParam = hasListsFeature() ? { lists } : {}; + // TODO: Remove this and use regular exceptions_list once the feature is stable for a release + const exceptionsListParam = hasListsFeature() ? { exceptions_list } : {}; const update = await alertsClient.update({ id: rule.id, @@ -120,7 +120,7 @@ export const updateRules = async ({ version: calculatedVersion, anomalyThreshold, machineLearningJobId, - ...listsParam, + ...exceptionsListParam, }, }, }); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/update_rules_notifications.ts b/x-pack/plugins/siem/server/lib/detection_engine/rules/update_rules_notifications.ts similarity index 93% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/update_rules_notifications.ts rename to x-pack/plugins/siem/server/lib/detection_engine/rules/update_rules_notifications.ts index 994a54048b71a..ddcd34b18cae9 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/update_rules_notifications.ts +++ b/x-pack/plugins/siem/server/lib/detection_engine/rules/update_rules_notifications.ts @@ -5,7 +5,7 @@ */ import { RuleAlertAction } from '../../../../common/detection_engine/types'; -import { AlertsClient, AlertServices } from '../../../../../../../plugins/alerting/server'; +import { AlertsClient, AlertServices } from '../../../../../alerting/server'; import { updateOrCreateRuleActionsSavedObject } from '../rule_actions/update_or_create_rule_actions_saved_object'; import { updateNotifications } from '../notifications/update_notifications'; import { RuleActions } from '../rule_actions/types'; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/utils.test.ts b/x-pack/plugins/siem/server/lib/detection_engine/rules/utils.test.ts similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/utils.test.ts rename to x-pack/plugins/siem/server/lib/detection_engine/rules/utils.test.ts diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/utils.ts b/x-pack/plugins/siem/server/lib/detection_engine/rules/utils.ts similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/utils.ts rename to x-pack/plugins/siem/server/lib/detection_engine/rules/utils.ts diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/add_prepackaged_rules.sh b/x-pack/plugins/siem/server/lib/detection_engine/scripts/add_prepackaged_rules.sh similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/add_prepackaged_rules.sh rename to x-pack/plugins/siem/server/lib/detection_engine/scripts/add_prepackaged_rules.sh diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/check_env_variables.sh b/x-pack/plugins/siem/server/lib/detection_engine/scripts/check_env_variables.sh similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/check_env_variables.sh rename to x-pack/plugins/siem/server/lib/detection_engine/scripts/check_env_variables.sh diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/convert_saved_search_to_rules.sh b/x-pack/plugins/siem/server/lib/detection_engine/scripts/convert_saved_search_to_rules.sh similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/convert_saved_search_to_rules.sh rename to x-pack/plugins/siem/server/lib/detection_engine/scripts/convert_saved_search_to_rules.sh diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/delete_all_actions.sh b/x-pack/plugins/siem/server/lib/detection_engine/scripts/delete_all_actions.sh similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/delete_all_actions.sh rename to x-pack/plugins/siem/server/lib/detection_engine/scripts/delete_all_actions.sh diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/delete_all_alert_tasks.sh b/x-pack/plugins/siem/server/lib/detection_engine/scripts/delete_all_alert_tasks.sh similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/delete_all_alert_tasks.sh rename to x-pack/plugins/siem/server/lib/detection_engine/scripts/delete_all_alert_tasks.sh diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/delete_all_alerts.sh b/x-pack/plugins/siem/server/lib/detection_engine/scripts/delete_all_alerts.sh similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/delete_all_alerts.sh rename to x-pack/plugins/siem/server/lib/detection_engine/scripts/delete_all_alerts.sh diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/delete_all_api_keys.sh b/x-pack/plugins/siem/server/lib/detection_engine/scripts/delete_all_api_keys.sh similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/delete_all_api_keys.sh rename to x-pack/plugins/siem/server/lib/detection_engine/scripts/delete_all_api_keys.sh diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/delete_all_statuses.sh b/x-pack/plugins/siem/server/lib/detection_engine/scripts/delete_all_statuses.sh similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/delete_all_statuses.sh rename to x-pack/plugins/siem/server/lib/detection_engine/scripts/delete_all_statuses.sh diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/delete_bulk.sh b/x-pack/plugins/siem/server/lib/detection_engine/scripts/delete_bulk.sh similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/delete_bulk.sh rename to x-pack/plugins/siem/server/lib/detection_engine/scripts/delete_bulk.sh diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/delete_rule_by_id.sh b/x-pack/plugins/siem/server/lib/detection_engine/scripts/delete_rule_by_id.sh similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/delete_rule_by_id.sh rename to x-pack/plugins/siem/server/lib/detection_engine/scripts/delete_rule_by_id.sh diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/delete_rule_by_rule_id.sh b/x-pack/plugins/siem/server/lib/detection_engine/scripts/delete_rule_by_rule_id.sh similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/delete_rule_by_rule_id.sh rename to x-pack/plugins/siem/server/lib/detection_engine/scripts/delete_rule_by_rule_id.sh diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/delete_signal_index.sh b/x-pack/plugins/siem/server/lib/detection_engine/scripts/delete_signal_index.sh similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/delete_signal_index.sh rename to x-pack/plugins/siem/server/lib/detection_engine/scripts/delete_signal_index.sh diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/export_rules.sh b/x-pack/plugins/siem/server/lib/detection_engine/scripts/export_rules.sh similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/export_rules.sh rename to x-pack/plugins/siem/server/lib/detection_engine/scripts/export_rules.sh diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/export_rules_by_rule_id.sh b/x-pack/plugins/siem/server/lib/detection_engine/scripts/export_rules_by_rule_id.sh similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/export_rules_by_rule_id.sh rename to x-pack/plugins/siem/server/lib/detection_engine/scripts/export_rules_by_rule_id.sh diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/export_rules_by_rule_id_to_file.sh b/x-pack/plugins/siem/server/lib/detection_engine/scripts/export_rules_by_rule_id_to_file.sh similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/export_rules_by_rule_id_to_file.sh rename to x-pack/plugins/siem/server/lib/detection_engine/scripts/export_rules_by_rule_id_to_file.sh diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/export_rules_to_file.sh b/x-pack/plugins/siem/server/lib/detection_engine/scripts/export_rules_to_file.sh similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/export_rules_to_file.sh rename to x-pack/plugins/siem/server/lib/detection_engine/scripts/export_rules_to_file.sh diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/find_rule_by_filter.sh b/x-pack/plugins/siem/server/lib/detection_engine/scripts/find_rule_by_filter.sh similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/find_rule_by_filter.sh rename to x-pack/plugins/siem/server/lib/detection_engine/scripts/find_rule_by_filter.sh diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/find_rules.sh b/x-pack/plugins/siem/server/lib/detection_engine/scripts/find_rules.sh similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/find_rules.sh rename to x-pack/plugins/siem/server/lib/detection_engine/scripts/find_rules.sh diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/find_rules_sort.sh b/x-pack/plugins/siem/server/lib/detection_engine/scripts/find_rules_sort.sh similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/find_rules_sort.sh rename to x-pack/plugins/siem/server/lib/detection_engine/scripts/find_rules_sort.sh diff --git a/x-pack/plugins/siem/server/lib/detection_engine/scripts/find_rules_statuses_by_ids.sh b/x-pack/plugins/siem/server/lib/detection_engine/scripts/find_rules_statuses_by_ids.sh new file mode 100755 index 0000000000000..5ae4904e9e4ec --- /dev/null +++ b/x-pack/plugins/siem/server/lib/detection_engine/scripts/find_rules_statuses_by_ids.sh @@ -0,0 +1,21 @@ +#!/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: ./find_rules_statuses_by_ids.sh [\"12345\",\"6789abc\"] +curl -g -k \ + -s \ + -H 'Content-Type: application/json' \ + -H 'kbn-xsrf: 123' \ + -u ${ELASTICSEARCH_USERNAME}:${ELASTICSEARCH_PASSWORD} \ + -X POST "${KIBANA_URL}${SPACE_URL}/api/detection_engine/rules/_find_statuses" \ + -d "{\"ids\": $1}" \ + | jq . diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/find_saved_object.sh b/x-pack/plugins/siem/server/lib/detection_engine/scripts/find_saved_object.sh similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/find_saved_object.sh rename to x-pack/plugins/siem/server/lib/detection_engine/scripts/find_saved_object.sh diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/get_action_instances.sh b/x-pack/plugins/siem/server/lib/detection_engine/scripts/get_action_instances.sh similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/get_action_instances.sh rename to x-pack/plugins/siem/server/lib/detection_engine/scripts/get_action_instances.sh diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/get_action_types.sh b/x-pack/plugins/siem/server/lib/detection_engine/scripts/get_action_types.sh similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/get_action_types.sh rename to x-pack/plugins/siem/server/lib/detection_engine/scripts/get_action_types.sh diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/get_alert_instances.sh b/x-pack/plugins/siem/server/lib/detection_engine/scripts/get_alert_instances.sh similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/get_alert_instances.sh rename to x-pack/plugins/siem/server/lib/detection_engine/scripts/get_alert_instances.sh diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/get_alert_tasks.sh b/x-pack/plugins/siem/server/lib/detection_engine/scripts/get_alert_tasks.sh similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/get_alert_tasks.sh rename to x-pack/plugins/siem/server/lib/detection_engine/scripts/get_alert_tasks.sh diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/get_alert_types.sh b/x-pack/plugins/siem/server/lib/detection_engine/scripts/get_alert_types.sh similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/get_alert_types.sh rename to x-pack/plugins/siem/server/lib/detection_engine/scripts/get_alert_types.sh diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/get_prepackaged_rules_status.sh b/x-pack/plugins/siem/server/lib/detection_engine/scripts/get_prepackaged_rules_status.sh similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/get_prepackaged_rules_status.sh rename to x-pack/plugins/siem/server/lib/detection_engine/scripts/get_prepackaged_rules_status.sh diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/get_privileges.sh b/x-pack/plugins/siem/server/lib/detection_engine/scripts/get_privileges.sh similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/get_privileges.sh rename to x-pack/plugins/siem/server/lib/detection_engine/scripts/get_privileges.sh diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/get_rule_by_id.sh b/x-pack/plugins/siem/server/lib/detection_engine/scripts/get_rule_by_id.sh similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/get_rule_by_id.sh rename to x-pack/plugins/siem/server/lib/detection_engine/scripts/get_rule_by_id.sh diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/get_rule_by_rule_id.sh b/x-pack/plugins/siem/server/lib/detection_engine/scripts/get_rule_by_rule_id.sh similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/get_rule_by_rule_id.sh rename to x-pack/plugins/siem/server/lib/detection_engine/scripts/get_rule_by_rule_id.sh diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/get_saved_objects.sh b/x-pack/plugins/siem/server/lib/detection_engine/scripts/get_saved_objects.sh similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/get_saved_objects.sh rename to x-pack/plugins/siem/server/lib/detection_engine/scripts/get_saved_objects.sh diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/get_signal_index.sh b/x-pack/plugins/siem/server/lib/detection_engine/scripts/get_signal_index.sh similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/get_signal_index.sh rename to x-pack/plugins/siem/server/lib/detection_engine/scripts/get_signal_index.sh diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/get_tags.sh b/x-pack/plugins/siem/server/lib/detection_engine/scripts/get_tags.sh similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/get_tags.sh rename to x-pack/plugins/siem/server/lib/detection_engine/scripts/get_tags.sh diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/hard_reset.sh b/x-pack/plugins/siem/server/lib/detection_engine/scripts/hard_reset.sh similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/hard_reset.sh rename to x-pack/plugins/siem/server/lib/detection_engine/scripts/hard_reset.sh diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/import_rules.sh b/x-pack/plugins/siem/server/lib/detection_engine/scripts/import_rules.sh similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/import_rules.sh rename to x-pack/plugins/siem/server/lib/detection_engine/scripts/import_rules.sh diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/import_rules_no_overwrite.sh b/x-pack/plugins/siem/server/lib/detection_engine/scripts/import_rules_no_overwrite.sh similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/import_rules_no_overwrite.sh rename to x-pack/plugins/siem/server/lib/detection_engine/scripts/import_rules_no_overwrite.sh diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/patch_rule.sh b/x-pack/plugins/siem/server/lib/detection_engine/scripts/patch_rule.sh similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/patch_rule.sh rename to x-pack/plugins/siem/server/lib/detection_engine/scripts/patch_rule.sh diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/patch_rule_bulk.sh b/x-pack/plugins/siem/server/lib/detection_engine/scripts/patch_rule_bulk.sh similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/patch_rule_bulk.sh rename to x-pack/plugins/siem/server/lib/detection_engine/scripts/patch_rule_bulk.sh diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/post_rule.sh b/x-pack/plugins/siem/server/lib/detection_engine/scripts/post_rule.sh similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/post_rule.sh rename to x-pack/plugins/siem/server/lib/detection_engine/scripts/post_rule.sh diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/post_rule_bulk.sh b/x-pack/plugins/siem/server/lib/detection_engine/scripts/post_rule_bulk.sh similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/post_rule_bulk.sh rename to x-pack/plugins/siem/server/lib/detection_engine/scripts/post_rule_bulk.sh diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/post_signal_index.sh b/x-pack/plugins/siem/server/lib/detection_engine/scripts/post_signal_index.sh similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/post_signal_index.sh rename to x-pack/plugins/siem/server/lib/detection_engine/scripts/post_signal_index.sh diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/post_x_rules.sh b/x-pack/plugins/siem/server/lib/detection_engine/scripts/post_x_rules.sh similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/post_x_rules.sh rename to x-pack/plugins/siem/server/lib/detection_engine/scripts/post_x_rules.sh diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/regen_prepackge_rules_index.sh b/x-pack/plugins/siem/server/lib/detection_engine/scripts/regen_prepackge_rules_index.sh similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/regen_prepackge_rules_index.sh rename to x-pack/plugins/siem/server/lib/detection_engine/scripts/regen_prepackge_rules_index.sh diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/bulk/delete_by_rule_id.json b/x-pack/plugins/siem/server/lib/detection_engine/scripts/rules/bulk/delete_by_rule_id.json similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/bulk/delete_by_rule_id.json rename to x-pack/plugins/siem/server/lib/detection_engine/scripts/rules/bulk/delete_by_rule_id.json diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/bulk/multiple_ruleid_queries.json b/x-pack/plugins/siem/server/lib/detection_engine/scripts/rules/bulk/multiple_ruleid_queries.json similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/bulk/multiple_ruleid_queries.json rename to x-pack/plugins/siem/server/lib/detection_engine/scripts/rules/bulk/multiple_ruleid_queries.json diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/bulk/multiple_simplest_queries.json b/x-pack/plugins/siem/server/lib/detection_engine/scripts/rules/bulk/multiple_simplest_queries.json similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/bulk/multiple_simplest_queries.json rename to x-pack/plugins/siem/server/lib/detection_engine/scripts/rules/bulk/multiple_simplest_queries.json diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/bulk/patch_names.json b/x-pack/plugins/siem/server/lib/detection_engine/scripts/rules/bulk/patch_names.json similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/bulk/patch_names.json rename to x-pack/plugins/siem/server/lib/detection_engine/scripts/rules/bulk/patch_names.json diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/export/ruleid_queries.json b/x-pack/plugins/siem/server/lib/detection_engine/scripts/rules/export/ruleid_queries.json similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/export/ruleid_queries.json rename to x-pack/plugins/siem/server/lib/detection_engine/scripts/rules/export/ruleid_queries.json diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/import/multiple_ruleid_queries.ndjson b/x-pack/plugins/siem/server/lib/detection_engine/scripts/rules/import/multiple_ruleid_queries.ndjson similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/import/multiple_ruleid_queries.ndjson rename to x-pack/plugins/siem/server/lib/detection_engine/scripts/rules/import/multiple_ruleid_queries.ndjson diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/patches/README.md b/x-pack/plugins/siem/server/lib/detection_engine/scripts/rules/patches/README.md similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/patches/README.md rename to x-pack/plugins/siem/server/lib/detection_engine/scripts/rules/patches/README.md diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/patches/disable_rule.json b/x-pack/plugins/siem/server/lib/detection_engine/scripts/rules/patches/disable_rule.json similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/patches/disable_rule.json rename to x-pack/plugins/siem/server/lib/detection_engine/scripts/rules/patches/disable_rule.json diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/patches/enabled_rule.json b/x-pack/plugins/siem/server/lib/detection_engine/scripts/rules/patches/enabled_rule.json similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/patches/enabled_rule.json rename to x-pack/plugins/siem/server/lib/detection_engine/scripts/rules/patches/enabled_rule.json diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/patches/simplest_update_risk_score_by_id.json b/x-pack/plugins/siem/server/lib/detection_engine/scripts/rules/patches/simplest_update_risk_score_by_id.json similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/patches/simplest_update_risk_score_by_id.json rename to x-pack/plugins/siem/server/lib/detection_engine/scripts/rules/patches/simplest_update_risk_score_by_id.json diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/patches/simplest_update_risk_score_by_rule_id.json b/x-pack/plugins/siem/server/lib/detection_engine/scripts/rules/patches/simplest_update_risk_score_by_rule_id.json similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/patches/simplest_update_risk_score_by_rule_id.json rename to x-pack/plugins/siem/server/lib/detection_engine/scripts/rules/patches/simplest_update_risk_score_by_rule_id.json diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/patches/simplest_updated_name.json b/x-pack/plugins/siem/server/lib/detection_engine/scripts/rules/patches/simplest_updated_name.json similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/patches/simplest_updated_name.json rename to x-pack/plugins/siem/server/lib/detection_engine/scripts/rules/patches/simplest_updated_name.json diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/patches/update_interval.json b/x-pack/plugins/siem/server/lib/detection_engine/scripts/rules/patches/update_interval.json similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/patches/update_interval.json rename to x-pack/plugins/siem/server/lib/detection_engine/scripts/rules/patches/update_interval.json diff --git a/x-pack/plugins/siem/server/lib/detection_engine/scripts/rules/patches/update_list.json b/x-pack/plugins/siem/server/lib/detection_engine/scripts/rules/patches/update_list.json new file mode 100644 index 0000000000000..8d831f3a961d8 --- /dev/null +++ b/x-pack/plugins/siem/server/lib/detection_engine/scripts/rules/patches/update_list.json @@ -0,0 +1,32 @@ +{ + "rule_id": "query-with-list", + "exceptions_list": [ + { + "field": "source.ip", + "values_operator": "excluded", + "values_type": "exists" + }, + { + "field": "host.name", + "values_operator": "included", + "values_type": "match", + "values": [ + { + "name": "rock01" + } + ], + "and": [ + { + "field": "host.id", + "values_operator": "included", + "values_type": "match_all", + "values": [ + { + "name": "123456" + } + ] + } + ] + } + ] +} diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/patches/update_machine_learning.json b/x-pack/plugins/siem/server/lib/detection_engine/scripts/rules/patches/update_machine_learning.json similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/patches/update_machine_learning.json rename to x-pack/plugins/siem/server/lib/detection_engine/scripts/rules/patches/update_machine_learning.json diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/patches/update_note.json b/x-pack/plugins/siem/server/lib/detection_engine/scripts/rules/patches/update_note.json similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/patches/update_note.json rename to x-pack/plugins/siem/server/lib/detection_engine/scripts/rules/patches/update_note.json diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/patches/update_query_everything.json b/x-pack/plugins/siem/server/lib/detection_engine/scripts/rules/patches/update_query_everything.json similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/patches/update_query_everything.json rename to x-pack/plugins/siem/server/lib/detection_engine/scripts/rules/patches/update_query_everything.json diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/patches/update_tags.json b/x-pack/plugins/siem/server/lib/detection_engine/scripts/rules/patches/update_tags.json similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/patches/update_tags.json rename to x-pack/plugins/siem/server/lib/detection_engine/scripts/rules/patches/update_tags.json diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/patches/update_timelineid.json b/x-pack/plugins/siem/server/lib/detection_engine/scripts/rules/patches/update_timelineid.json similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/patches/update_timelineid.json rename to x-pack/plugins/siem/server/lib/detection_engine/scripts/rules/patches/update_timelineid.json diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/patches/update_version.json b/x-pack/plugins/siem/server/lib/detection_engine/scripts/rules/patches/update_version.json similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/patches/update_version.json rename to x-pack/plugins/siem/server/lib/detection_engine/scripts/rules/patches/update_version.json diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/queries/README.md b/x-pack/plugins/siem/server/lib/detection_engine/scripts/rules/queries/README.md similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/queries/README.md rename to x-pack/plugins/siem/server/lib/detection_engine/scripts/rules/queries/README.md diff --git a/x-pack/plugins/siem/server/lib/detection_engine/scripts/rules/queries/lists/query_with_and.json b/x-pack/plugins/siem/server/lib/detection_engine/scripts/rules/queries/lists/query_with_and.json new file mode 100644 index 0000000000000..1575a712e2cba --- /dev/null +++ b/x-pack/plugins/siem/server/lib/detection_engine/scripts/rules/queries/lists/query_with_and.json @@ -0,0 +1,35 @@ +{ + "name": "List - and", + "description": "Query with a list that includes and. This rule should only produce signals when host.name exists and when both event.module is endgame and event.category is anything other than file", + "rule_id": "query-with-list-and", + "risk_score": 1, + "severity": "high", + "type": "query", + "query": "host.name: *", + "interval": "30s", + "language": "kuery", + "exceptions_list": [ + { + "field": "event.module", + "values_operator": "excluded", + "values_type": "match", + "values": [ + { + "name": "endgame" + } + ], + "and": [ + { + "field": "event.category", + "values_operator": "included", + "values_type": "match", + "values": [ + { + "name": "file" + } + ] + } + ] + } + ] +} diff --git a/x-pack/plugins/siem/server/lib/detection_engine/scripts/rules/queries/lists/query_with_excluded.json b/x-pack/plugins/siem/server/lib/detection_engine/scripts/rules/queries/lists/query_with_excluded.json new file mode 100644 index 0000000000000..4e6d9403a276f --- /dev/null +++ b/x-pack/plugins/siem/server/lib/detection_engine/scripts/rules/queries/lists/query_with_excluded.json @@ -0,0 +1,23 @@ +{ + "name": "List - excluded", + "description": "Query with a list of values_operator excluded. This rule should only produce signals when host.name exists and event.module is suricata", + "rule_id": "query-with-list-excluded", + "risk_score": 1, + "severity": "high", + "type": "query", + "query": "host.name: *", + "interval": "30s", + "language": "kuery", + "exceptions_list": [ + { + "field": "event.module", + "values_operator": "excluded", + "values_type": "match", + "values": [ + { + "name": "suricata" + } + ] + } + ] +} diff --git a/x-pack/plugins/siem/server/lib/detection_engine/scripts/rules/queries/lists/query_with_exists.json b/x-pack/plugins/siem/server/lib/detection_engine/scripts/rules/queries/lists/query_with_exists.json new file mode 100644 index 0000000000000..97beace37633f --- /dev/null +++ b/x-pack/plugins/siem/server/lib/detection_engine/scripts/rules/queries/lists/query_with_exists.json @@ -0,0 +1,18 @@ +{ + "name": "List - exists", + "description": "Query with a list that includes exists. This rule should only produce signals when host.name exists and event.action does not exist", + "rule_id": "query-with-list-exists", + "risk_score": 1, + "severity": "high", + "type": "query", + "query": "host.name: *", + "interval": "30s", + "language": "kuery", + "exceptions_list": [ + { + "field": "event.action", + "values_operator": "included", + "values_type": "exists" + } + ] +} diff --git a/x-pack/plugins/siem/server/lib/detection_engine/scripts/rules/queries/lists/query_with_list.json b/x-pack/plugins/siem/server/lib/detection_engine/scripts/rules/queries/lists/query_with_list.json new file mode 100644 index 0000000000000..ad0585b5a2ec5 --- /dev/null +++ b/x-pack/plugins/siem/server/lib/detection_engine/scripts/rules/queries/lists/query_with_list.json @@ -0,0 +1,54 @@ +{ + "name": "Query with a list", + "description": "Query with a list. This rule should only produce signals when either host.name exists and event.module is system and user.name is zeek or gdm OR when host.name exists and event.module is not endgame or zeek or system.", + "rule_id": "query-with-list", + "risk_score": 2, + "severity": "high", + "type": "query", + "query": "host.name: *", + "interval": "30s", + "language": "kuery", + "exceptions_list": [ + { + "field": "event.module", + "values_operator": "excluded", + "values_type": "match", + "values": [ + { + "name": "system" + } + ], + "and": [ + { + "field": "user.name", + "values_operator": "excluded", + "values_type": "match_all", + "values": [ + { + "name": "zeek" + }, + { + "name": "gdm" + } + ] + } + ] + }, + { + "field": "event.module", + "values_operator": "included", + "values_type": "match_all", + "values": [ + { + "name": "endgame" + }, + { + "name": "zeek" + }, + { + "name": "system" + } + ] + } + ] +} diff --git a/x-pack/plugins/siem/server/lib/detection_engine/scripts/rules/queries/lists/query_with_match.json b/x-pack/plugins/siem/server/lib/detection_engine/scripts/rules/queries/lists/query_with_match.json new file mode 100644 index 0000000000000..6e6880cc28f24 --- /dev/null +++ b/x-pack/plugins/siem/server/lib/detection_engine/scripts/rules/queries/lists/query_with_match.json @@ -0,0 +1,23 @@ +{ + "name": "List - match", + "description": "Query with a list that includes match. This rule should only produce signals when host.name exists and event.module is not suricata", + "rule_id": "query-with-list-match", + "risk_score": 1, + "severity": "high", + "type": "query", + "query": "host.name: *", + "interval": "30s", + "language": "kuery", + "exceptions_list": [ + { + "field": "event.module", + "values_operator": "included", + "values_type": "match", + "values": [ + { + "name": "suricata" + } + ] + } + ] +} diff --git a/x-pack/plugins/siem/server/lib/detection_engine/scripts/rules/queries/lists/query_with_match_all.json b/x-pack/plugins/siem/server/lib/detection_engine/scripts/rules/queries/lists/query_with_match_all.json new file mode 100644 index 0000000000000..44cc26ac3315e --- /dev/null +++ b/x-pack/plugins/siem/server/lib/detection_engine/scripts/rules/queries/lists/query_with_match_all.json @@ -0,0 +1,26 @@ +{ + "name": "List - match_all", + "description": "Query with a list that includes match_all. This rule should only produce signals when host.name exists and event.module is not suricata or auditd", + "rule_id": "query-with-list-match-all", + "risk_score": 1, + "severity": "high", + "type": "query", + "query": "host.name: *", + "interval": "30s", + "language": "kuery", + "exceptions_list": [ + { + "field": "event.module", + "values_operator": "included", + "values_type": "match_all", + "values": [ + { + "name": "suricata" + }, + { + "name": "auditd" + } + ] + } + ] +} diff --git a/x-pack/plugins/siem/server/lib/detection_engine/scripts/rules/queries/lists/query_with_or.json b/x-pack/plugins/siem/server/lib/detection_engine/scripts/rules/queries/lists/query_with_or.json new file mode 100644 index 0000000000000..9c4eda559d5bc --- /dev/null +++ b/x-pack/plugins/siem/server/lib/detection_engine/scripts/rules/queries/lists/query_with_or.json @@ -0,0 +1,32 @@ +{ + "name": "List - or", + "description": "Query with a list that includes or. This rule should only produce signals when host.name exists and event.module is suricata OR when host.name exists and event.category is file", + "rule_id": "query-with-list-or", + "risk_score": 1, + "severity": "high", + "type": "query", + "query": "host.name: *", + "interval": "30s", + "exceptions_list": [ + { + "field": "event.module", + "values_operator": "excluded", + "values_type": "match", + "values": [ + { + "name": "suricata" + } + ] + }, + { + "field": "event.category", + "values_operator": "excluded", + "values_type": "match", + "values": [ + { + "name": "file" + } + ] + } + ] +} diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/queries/query_disabled.json b/x-pack/plugins/siem/server/lib/detection_engine/scripts/rules/queries/query_disabled.json similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/queries/query_disabled.json rename to x-pack/plugins/siem/server/lib/detection_engine/scripts/rules/queries/query_disabled.json diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/queries/query_lucene.json b/x-pack/plugins/siem/server/lib/detection_engine/scripts/rules/queries/query_lucene.json similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/queries/query_lucene.json rename to x-pack/plugins/siem/server/lib/detection_engine/scripts/rules/queries/query_lucene.json diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/queries/query_mitre_attack.json b/x-pack/plugins/siem/server/lib/detection_engine/scripts/rules/queries/query_mitre_attack.json similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/queries/query_mitre_attack.json rename to x-pack/plugins/siem/server/lib/detection_engine/scripts/rules/queries/query_mitre_attack.json diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/queries/query_timelineid.json b/x-pack/plugins/siem/server/lib/detection_engine/scripts/rules/queries/query_timelineid.json similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/queries/query_timelineid.json rename to x-pack/plugins/siem/server/lib/detection_engine/scripts/rules/queries/query_timelineid.json diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/queries/query_with_everything.json b/x-pack/plugins/siem/server/lib/detection_engine/scripts/rules/queries/query_with_everything.json similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/queries/query_with_everything.json rename to x-pack/plugins/siem/server/lib/detection_engine/scripts/rules/queries/query_with_everything.json diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/queries/query_with_filter.json b/x-pack/plugins/siem/server/lib/detection_engine/scripts/rules/queries/query_with_filter.json similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/queries/query_with_filter.json rename to x-pack/plugins/siem/server/lib/detection_engine/scripts/rules/queries/query_with_filter.json diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/queries/query_with_machine_learning.json b/x-pack/plugins/siem/server/lib/detection_engine/scripts/rules/queries/query_with_machine_learning.json similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/queries/query_with_machine_learning.json rename to x-pack/plugins/siem/server/lib/detection_engine/scripts/rules/queries/query_with_machine_learning.json diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/queries/query_with_meta_data.json b/x-pack/plugins/siem/server/lib/detection_engine/scripts/rules/queries/query_with_meta_data.json similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/queries/query_with_meta_data.json rename to x-pack/plugins/siem/server/lib/detection_engine/scripts/rules/queries/query_with_meta_data.json diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/queries/query_with_note.json b/x-pack/plugins/siem/server/lib/detection_engine/scripts/rules/queries/query_with_note.json similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/queries/query_with_note.json rename to x-pack/plugins/siem/server/lib/detection_engine/scripts/rules/queries/query_with_note.json diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/queries/query_with_rule_id.json b/x-pack/plugins/siem/server/lib/detection_engine/scripts/rules/queries/query_with_rule_id.json similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/queries/query_with_rule_id.json rename to x-pack/plugins/siem/server/lib/detection_engine/scripts/rules/queries/query_with_rule_id.json diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/queries/query_with_tags.json b/x-pack/plugins/siem/server/lib/detection_engine/scripts/rules/queries/query_with_tags.json similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/queries/query_with_tags.json rename to x-pack/plugins/siem/server/lib/detection_engine/scripts/rules/queries/query_with_tags.json diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/queries/simplest_filters.json b/x-pack/plugins/siem/server/lib/detection_engine/scripts/rules/queries/simplest_filters.json similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/queries/simplest_filters.json rename to x-pack/plugins/siem/server/lib/detection_engine/scripts/rules/queries/simplest_filters.json diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/queries/simplest_query.json b/x-pack/plugins/siem/server/lib/detection_engine/scripts/rules/queries/simplest_query.json similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/queries/simplest_query.json rename to x-pack/plugins/siem/server/lib/detection_engine/scripts/rules/queries/simplest_query.json diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/saved_queries/README.md b/x-pack/plugins/siem/server/lib/detection_engine/scripts/rules/saved_queries/README.md similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/saved_queries/README.md rename to x-pack/plugins/siem/server/lib/detection_engine/scripts/rules/saved_queries/README.md diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/saved_queries/saved_query_by_rule_id.json b/x-pack/plugins/siem/server/lib/detection_engine/scripts/rules/saved_queries/saved_query_by_rule_id.json similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/saved_queries/saved_query_by_rule_id.json rename to x-pack/plugins/siem/server/lib/detection_engine/scripts/rules/saved_queries/saved_query_by_rule_id.json diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/saved_queries/saved_query_with_everything.json b/x-pack/plugins/siem/server/lib/detection_engine/scripts/rules/saved_queries/saved_query_with_everything.json similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/saved_queries/saved_query_with_everything.json rename to x-pack/plugins/siem/server/lib/detection_engine/scripts/rules/saved_queries/saved_query_with_everything.json diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/saved_queries/saved_query_with_filters.json b/x-pack/plugins/siem/server/lib/detection_engine/scripts/rules/saved_queries/saved_query_with_filters.json similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/saved_queries/saved_query_with_filters.json rename to x-pack/plugins/siem/server/lib/detection_engine/scripts/rules/saved_queries/saved_query_with_filters.json diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/saved_queries/saved_query_with_query.json b/x-pack/plugins/siem/server/lib/detection_engine/scripts/rules/saved_queries/saved_query_with_query.json similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/saved_queries/saved_query_with_query.json rename to x-pack/plugins/siem/server/lib/detection_engine/scripts/rules/saved_queries/saved_query_with_query.json diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/saved_queries/saved_query_with_query_filter.json b/x-pack/plugins/siem/server/lib/detection_engine/scripts/rules/saved_queries/saved_query_with_query_filter.json similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/saved_queries/saved_query_with_query_filter.json rename to x-pack/plugins/siem/server/lib/detection_engine/scripts/rules/saved_queries/saved_query_with_query_filter.json diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/saved_queries/simplest_saved_query.json b/x-pack/plugins/siem/server/lib/detection_engine/scripts/rules/saved_queries/simplest_saved_query.json similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/saved_queries/simplest_saved_query.json rename to x-pack/plugins/siem/server/lib/detection_engine/scripts/rules/saved_queries/simplest_saved_query.json diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/test_cases/README.md b/x-pack/plugins/siem/server/lib/detection_engine/scripts/rules/test_cases/README.md similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/test_cases/README.md rename to x-pack/plugins/siem/server/lib/detection_engine/scripts/rules/test_cases/README.md diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/test_cases/imports/multiple_ruleid_queries_corrupted.ndjson b/x-pack/plugins/siem/server/lib/detection_engine/scripts/rules/test_cases/imports/multiple_ruleid_queries_corrupted.ndjson similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/test_cases/imports/multiple_ruleid_queries_corrupted.ndjson rename to x-pack/plugins/siem/server/lib/detection_engine/scripts/rules/test_cases/imports/multiple_ruleid_queries_corrupted.ndjson diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/test_cases/queries/filter_with_empty_query.json b/x-pack/plugins/siem/server/lib/detection_engine/scripts/rules/test_cases/queries/filter_with_empty_query.json similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/test_cases/queries/filter_with_empty_query.json rename to x-pack/plugins/siem/server/lib/detection_engine/scripts/rules/test_cases/queries/filter_with_empty_query.json diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/test_cases/queries/filter_without_query.json b/x-pack/plugins/siem/server/lib/detection_engine/scripts/rules/test_cases/queries/filter_without_query.json similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/test_cases/queries/filter_without_query.json rename to x-pack/plugins/siem/server/lib/detection_engine/scripts/rules/test_cases/queries/filter_without_query.json diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/test_cases/queries/query_filter_ui_meatadata_lucene.json b/x-pack/plugins/siem/server/lib/detection_engine/scripts/rules/test_cases/queries/query_filter_ui_meatadata_lucene.json similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/test_cases/queries/query_filter_ui_meatadata_lucene.json rename to x-pack/plugins/siem/server/lib/detection_engine/scripts/rules/test_cases/queries/query_filter_ui_meatadata_lucene.json diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/test_cases/queries/query_filter_ui_metadata.json b/x-pack/plugins/siem/server/lib/detection_engine/scripts/rules/test_cases/queries/query_filter_ui_metadata.json similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/test_cases/queries/query_filter_ui_metadata.json rename to x-pack/plugins/siem/server/lib/detection_engine/scripts/rules/test_cases/queries/query_filter_ui_metadata.json diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/test_cases/queries/query_with_errors.json b/x-pack/plugins/siem/server/lib/detection_engine/scripts/rules/test_cases/queries/query_with_errors.json similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/test_cases/queries/query_with_errors.json rename to x-pack/plugins/siem/server/lib/detection_engine/scripts/rules/test_cases/queries/query_with_errors.json diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/test_cases/queries/saved_query_ui_meta_empty_query.json b/x-pack/plugins/siem/server/lib/detection_engine/scripts/rules/test_cases/queries/saved_query_ui_meta_empty_query.json similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/test_cases/queries/saved_query_ui_meta_empty_query.json rename to x-pack/plugins/siem/server/lib/detection_engine/scripts/rules/test_cases/queries/saved_query_ui_meta_empty_query.json diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/test_cases/signals_on_signals/depth_test/README.md b/x-pack/plugins/siem/server/lib/detection_engine/scripts/rules/test_cases/signals_on_signals/depth_test/README.md similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/test_cases/signals_on_signals/depth_test/README.md rename to x-pack/plugins/siem/server/lib/detection_engine/scripts/rules/test_cases/signals_on_signals/depth_test/README.md diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/test_cases/signals_on_signals/depth_test/query_single_id.json b/x-pack/plugins/siem/server/lib/detection_engine/scripts/rules/test_cases/signals_on_signals/depth_test/query_single_id.json similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/test_cases/signals_on_signals/depth_test/query_single_id.json rename to x-pack/plugins/siem/server/lib/detection_engine/scripts/rules/test_cases/signals_on_signals/depth_test/query_single_id.json diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/test_cases/signals_on_signals/depth_test/signal_on_signal_depth_1.json b/x-pack/plugins/siem/server/lib/detection_engine/scripts/rules/test_cases/signals_on_signals/depth_test/signal_on_signal_depth_1.json similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/test_cases/signals_on_signals/depth_test/signal_on_signal_depth_1.json rename to x-pack/plugins/siem/server/lib/detection_engine/scripts/rules/test_cases/signals_on_signals/depth_test/signal_on_signal_depth_1.json diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/test_cases/signals_on_signals/depth_test/signal_on_signal_depth_2.json b/x-pack/plugins/siem/server/lib/detection_engine/scripts/rules/test_cases/signals_on_signals/depth_test/signal_on_signal_depth_2.json similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/test_cases/signals_on_signals/depth_test/signal_on_signal_depth_2.json rename to x-pack/plugins/siem/server/lib/detection_engine/scripts/rules/test_cases/signals_on_signals/depth_test/signal_on_signal_depth_2.json diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/test_cases/signals_on_signals/halting_test/README.md b/x-pack/plugins/siem/server/lib/detection_engine/scripts/rules/test_cases/signals_on_signals/halting_test/README.md similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/test_cases/signals_on_signals/halting_test/README.md rename to x-pack/plugins/siem/server/lib/detection_engine/scripts/rules/test_cases/signals_on_signals/halting_test/README.md diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/test_cases/signals_on_signals/halting_test/query_single_id.json b/x-pack/plugins/siem/server/lib/detection_engine/scripts/rules/test_cases/signals_on_signals/halting_test/query_single_id.json similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/test_cases/signals_on_signals/halting_test/query_single_id.json rename to x-pack/plugins/siem/server/lib/detection_engine/scripts/rules/test_cases/signals_on_signals/halting_test/query_single_id.json diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/test_cases/signals_on_signals/halting_test/signal_on_signal.json b/x-pack/plugins/siem/server/lib/detection_engine/scripts/rules/test_cases/signals_on_signals/halting_test/signal_on_signal.json similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/test_cases/signals_on_signals/halting_test/signal_on_signal.json rename to x-pack/plugins/siem/server/lib/detection_engine/scripts/rules/test_cases/signals_on_signals/halting_test/signal_on_signal.json diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/updates/README.md b/x-pack/plugins/siem/server/lib/detection_engine/scripts/rules/updates/README.md similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/updates/README.md rename to x-pack/plugins/siem/server/lib/detection_engine/scripts/rules/updates/README.md diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/updates/disable_rule.json b/x-pack/plugins/siem/server/lib/detection_engine/scripts/rules/updates/disable_rule.json similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/updates/disable_rule.json rename to x-pack/plugins/siem/server/lib/detection_engine/scripts/rules/updates/disable_rule.json diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/updates/enabled_rule.json b/x-pack/plugins/siem/server/lib/detection_engine/scripts/rules/updates/enabled_rule.json similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/updates/enabled_rule.json rename to x-pack/plugins/siem/server/lib/detection_engine/scripts/rules/updates/enabled_rule.json diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/updates/simplest_update_risk_score_by_id.json b/x-pack/plugins/siem/server/lib/detection_engine/scripts/rules/updates/simplest_update_risk_score_by_id.json similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/updates/simplest_update_risk_score_by_id.json rename to x-pack/plugins/siem/server/lib/detection_engine/scripts/rules/updates/simplest_update_risk_score_by_id.json diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/updates/simplest_update_risk_score_by_rule_id.json b/x-pack/plugins/siem/server/lib/detection_engine/scripts/rules/updates/simplest_update_risk_score_by_rule_id.json similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/updates/simplest_update_risk_score_by_rule_id.json rename to x-pack/plugins/siem/server/lib/detection_engine/scripts/rules/updates/simplest_update_risk_score_by_rule_id.json diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/updates/simplest_updated_name.json b/x-pack/plugins/siem/server/lib/detection_engine/scripts/rules/updates/simplest_updated_name.json similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/updates/simplest_updated_name.json rename to x-pack/plugins/siem/server/lib/detection_engine/scripts/rules/updates/simplest_updated_name.json diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/updates/update_interval.json b/x-pack/plugins/siem/server/lib/detection_engine/scripts/rules/updates/update_interval.json similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/updates/update_interval.json rename to x-pack/plugins/siem/server/lib/detection_engine/scripts/rules/updates/update_interval.json diff --git a/x-pack/plugins/siem/server/lib/detection_engine/scripts/rules/updates/update_list.json b/x-pack/plugins/siem/server/lib/detection_engine/scripts/rules/updates/update_list.json new file mode 100644 index 0000000000000..df22dff5c046e --- /dev/null +++ b/x-pack/plugins/siem/server/lib/detection_engine/scripts/rules/updates/update_list.json @@ -0,0 +1,38 @@ +{ + "name": "Query with a list", + "description": "Query with a list", + "rule_id": "query-with-list", + "risk_score": 1, + "severity": "high", + "type": "query", + "query": "user.name: root or user.name: admin", + "exceptions_list": [ + { + "field": "source.ip", + "values_operator": "excluded", + "values_type": "exists" + }, + { + "field": "host.name", + "values_operator": "included", + "values_type": "match", + "values": [ + { + "name": "rock01" + } + ], + "and": [ + { + "field": "host.id", + "values_operator": "included", + "values_type": "match_all", + "values": [ + { + "name": "123456" + } + ] + } + ] + } + ] +} diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/updates/update_machine_learning.json b/x-pack/plugins/siem/server/lib/detection_engine/scripts/rules/updates/update_machine_learning.json similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/updates/update_machine_learning.json rename to x-pack/plugins/siem/server/lib/detection_engine/scripts/rules/updates/update_machine_learning.json diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/updates/update_note.json b/x-pack/plugins/siem/server/lib/detection_engine/scripts/rules/updates/update_note.json similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/updates/update_note.json rename to x-pack/plugins/siem/server/lib/detection_engine/scripts/rules/updates/update_note.json diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/updates/update_query_everything.json b/x-pack/plugins/siem/server/lib/detection_engine/scripts/rules/updates/update_query_everything.json similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/updates/update_query_everything.json rename to x-pack/plugins/siem/server/lib/detection_engine/scripts/rules/updates/update_query_everything.json diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/updates/update_tags.json b/x-pack/plugins/siem/server/lib/detection_engine/scripts/rules/updates/update_tags.json similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/updates/update_tags.json rename to x-pack/plugins/siem/server/lib/detection_engine/scripts/rules/updates/update_tags.json diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/updates/update_timelineid.json b/x-pack/plugins/siem/server/lib/detection_engine/scripts/rules/updates/update_timelineid.json similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/updates/update_timelineid.json rename to x-pack/plugins/siem/server/lib/detection_engine/scripts/rules/updates/update_timelineid.json diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/updates/update_version.json b/x-pack/plugins/siem/server/lib/detection_engine/scripts/rules/updates/update_version.json similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/updates/update_version.json rename to x-pack/plugins/siem/server/lib/detection_engine/scripts/rules/updates/update_version.json diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signal_index_exists.sh b/x-pack/plugins/siem/server/lib/detection_engine/scripts/signal_index_exists.sh similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signal_index_exists.sh rename to x-pack/plugins/siem/server/lib/detection_engine/scripts/signal_index_exists.sh diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/aggs_signals.sh b/x-pack/plugins/siem/server/lib/detection_engine/scripts/signals/aggs_signals.sh similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/aggs_signals.sh rename to x-pack/plugins/siem/server/lib/detection_engine/scripts/signals/aggs_signals.sh diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/put_signal_doc.sh b/x-pack/plugins/siem/server/lib/detection_engine/scripts/signals/put_signal_doc.sh similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/put_signal_doc.sh rename to x-pack/plugins/siem/server/lib/detection_engine/scripts/signals/put_signal_doc.sh diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/query_signals.sh b/x-pack/plugins/siem/server/lib/detection_engine/scripts/signals/query_signals.sh similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/query_signals.sh rename to x-pack/plugins/siem/server/lib/detection_engine/scripts/signals/query_signals.sh diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/sample_signal.json b/x-pack/plugins/siem/server/lib/detection_engine/scripts/signals/sample_signal.json similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/sample_signal.json rename to x-pack/plugins/siem/server/lib/detection_engine/scripts/signals/sample_signal.json diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/set_status_with_id.sh b/x-pack/plugins/siem/server/lib/detection_engine/scripts/signals/set_status_with_id.sh similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/set_status_with_id.sh rename to x-pack/plugins/siem/server/lib/detection_engine/scripts/signals/set_status_with_id.sh diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/set_status_with_query.sh b/x-pack/plugins/siem/server/lib/detection_engine/scripts/signals/set_status_with_query.sh similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/set_status_with_query.sh rename to x-pack/plugins/siem/server/lib/detection_engine/scripts/signals/set_status_with_query.sh diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/update_rule.sh b/x-pack/plugins/siem/server/lib/detection_engine/scripts/update_rule.sh similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/update_rule.sh rename to x-pack/plugins/siem/server/lib/detection_engine/scripts/update_rule.sh diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/update_rule_bulk.sh b/x-pack/plugins/siem/server/lib/detection_engine/scripts/update_rule_bulk.sh similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/update_rule_bulk.sh rename to x-pack/plugins/siem/server/lib/detection_engine/scripts/update_rule_bulk.sh diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/__mocks__/es_results.ts b/x-pack/plugins/siem/server/lib/detection_engine/signals/__mocks__/es_results.ts similarity index 87% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/__mocks__/es_results.ts rename to x-pack/plugins/siem/server/lib/detection_engine/signals/__mocks__/es_results.ts index 7a211c5631da6..8a5da8e859721 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/__mocks__/es_results.ts +++ b/x-pack/plugins/siem/server/lib/detection_engine/signals/__mocks__/es_results.ts @@ -4,13 +4,13 @@ * you may not use this file except in compliance with the Elastic License. */ -import { SignalSourceHit, SignalSearchResponse } from '../types'; +import { SignalSourceHit, SignalSearchResponse, BulkResponse, BulkItem } from '../types'; import { Logger, SavedObject, SavedObjectsFindResponse, -} from '../../../../../../../../../src/core/server'; -import { loggingServiceMock } from '../../../../../../../../../src/core/server/mocks'; +} from '../../../../../../../../src/core/server'; +import { loggingServiceMock } from '../../../../../../../../src/core/server/mocks'; import { RuleTypeParams, OutputRuleAlertRest } from '../../types'; import { IRuleStatusAttributes } from '../../rules/types'; import { ruleStatusSavedObjectType } from '../../../../saved_objects'; @@ -44,7 +44,7 @@ export const sampleRuleAlertParams = ( meta: undefined, threat: undefined, version: 1, - lists: [ + exceptions_list: [ { field: 'source.ip', values_operator: 'included', @@ -416,3 +416,68 @@ export const exampleFindRuleStatusResponse: ( }); export const mockLogger: Logger = loggingServiceMock.createLogger(); + +export const sampleBulkErrorItem = ( + { + status, + reason, + }: { + status: number; + reason: string; + } = { status: 400, reason: 'Invalid call' } +): BulkItem => { + return { + create: { + _index: 'mock_index', + _id: '123', + _version: 1, + status, + _shards: { + total: 1, + successful: 0, + failed: 1, + }, + error: { + type: 'Invalid', + reason, + shard: 'shard 123', + index: 'mock_index', + }, + }, + }; +}; + +export const sampleBulkItem = (): BulkItem => { + return { + create: { + _index: 'mock_index', + _id: '123', + _version: 1, + status: 200, + result: 'some result here', + _shards: { + total: 1, + successful: 1, + failed: 0, + }, + }, + }; +}; + +export const sampleEmptyBulkResponse = (): BulkResponse => ({ + took: 0, + errors: false, + items: [], +}); + +export const sampleBulkError = (): BulkResponse => ({ + took: 0, + errors: true, + items: [sampleBulkErrorItem()], +}); + +export const sampleBulkResponse = (): BulkResponse => ({ + took: 0, + errors: true, + items: [sampleBulkItem()], +}); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/__mocks__/rule_status_saved_objects_client.mock.ts b/x-pack/plugins/siem/server/lib/detection_engine/signals/__mocks__/rule_status_saved_objects_client.mock.ts similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/__mocks__/rule_status_saved_objects_client.mock.ts rename to x-pack/plugins/siem/server/lib/detection_engine/signals/__mocks__/rule_status_saved_objects_client.mock.ts diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/build_bulk_body.test.ts b/x-pack/plugins/siem/server/lib/detection_engine/signals/build_bulk_body.test.ts similarity index 99% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/build_bulk_body.test.ts rename to x-pack/plugins/siem/server/lib/detection_engine/signals/build_bulk_body.test.ts index f1729e35ce1f0..bbd01cfaafc62 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/build_bulk_body.test.ts +++ b/x-pack/plugins/siem/server/lib/detection_engine/signals/build_bulk_body.test.ts @@ -90,7 +90,7 @@ describe('buildBulkBody', () => { version: 1, created_at: fakeSignalSourceHit.signal.rule?.created_at, updated_at: fakeSignalSourceHit.signal.rule?.updated_at, - lists: [ + exceptions_list: [ { field: 'source.ip', values_operator: 'included', @@ -216,7 +216,7 @@ describe('buildBulkBody', () => { created_at: fakeSignalSourceHit.signal.rule?.created_at, updated_at: fakeSignalSourceHit.signal.rule?.updated_at, throttle: 'no_actions', - lists: [ + exceptions_list: [ { field: 'source.ip', values_operator: 'included', @@ -340,7 +340,7 @@ describe('buildBulkBody', () => { created_at: fakeSignalSourceHit.signal.rule?.created_at, updated_at: fakeSignalSourceHit.signal.rule?.updated_at, throttle: 'no_actions', - lists: [ + exceptions_list: [ { field: 'source.ip', values_operator: 'included', @@ -457,7 +457,7 @@ describe('buildBulkBody', () => { updated_at: fakeSignalSourceHit.signal.rule?.updated_at, created_at: fakeSignalSourceHit.signal.rule?.created_at, throttle: 'no_actions', - lists: [ + exceptions_list: [ { field: 'source.ip', values_operator: 'included', diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/build_bulk_body.ts b/x-pack/plugins/siem/server/lib/detection_engine/signals/build_bulk_body.ts similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/build_bulk_body.ts rename to x-pack/plugins/siem/server/lib/detection_engine/signals/build_bulk_body.ts diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/build_event_type_signal.test.ts b/x-pack/plugins/siem/server/lib/detection_engine/signals/build_event_type_signal.test.ts similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/build_event_type_signal.test.ts rename to x-pack/plugins/siem/server/lib/detection_engine/signals/build_event_type_signal.test.ts diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/build_event_type_signal.ts b/x-pack/plugins/siem/server/lib/detection_engine/signals/build_event_type_signal.ts similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/build_event_type_signal.ts rename to x-pack/plugins/siem/server/lib/detection_engine/signals/build_event_type_signal.ts diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/build_events_query.test.ts b/x-pack/plugins/siem/server/lib/detection_engine/signals/build_events_query.test.ts similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/build_events_query.test.ts rename to x-pack/plugins/siem/server/lib/detection_engine/signals/build_events_query.test.ts diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/build_events_query.ts b/x-pack/plugins/siem/server/lib/detection_engine/signals/build_events_query.ts similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/build_events_query.ts rename to x-pack/plugins/siem/server/lib/detection_engine/signals/build_events_query.ts diff --git a/x-pack/plugins/siem/server/lib/detection_engine/signals/build_exceptions_query.test.ts b/x-pack/plugins/siem/server/lib/detection_engine/signals/build_exceptions_query.test.ts new file mode 100644 index 0000000000000..ec8db77dac725 --- /dev/null +++ b/x-pack/plugins/siem/server/lib/detection_engine/signals/build_exceptions_query.test.ts @@ -0,0 +1,1318 @@ +/* + * 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 { + buildQueryExceptions, + buildExceptions, + operatorBuilder, + buildExists, + buildMatch, + buildMatchAll, + evaluateValues, + formatQuery, + getLanguageBooleanOperator, +} from './build_exceptions_query'; +import { List } from '../routes/schemas/types/lists_default_array'; + +describe('build_exceptions_query', () => { + describe('getLanguageBooleanOperator', () => { + test('it returns value as uppercase if language is "lucene"', () => { + const result = getLanguageBooleanOperator({ language: 'lucene', value: 'not' }); + + expect(result).toEqual('NOT'); + }); + + test('it returns value as is if language is "kuery"', () => { + const result = getLanguageBooleanOperator({ language: 'kuery', value: 'not' }); + + expect(result).toEqual('not'); + }); + }); + + describe('operatorBuilder', () => { + describe('kuery', () => { + test('it returns "not " when operator is "excluded"', () => { + const operator = operatorBuilder({ operator: 'excluded', language: 'kuery' }); + + expect(operator).toEqual(' and '); + }); + + test('it returns empty string when operator is "included"', () => { + const operator = operatorBuilder({ operator: 'included', language: 'kuery' }); + + expect(operator).toEqual(' and not '); + }); + }); + + describe('lucene', () => { + test('it returns "NOT " when operator is "excluded"', () => { + const operator = operatorBuilder({ operator: 'excluded', language: 'lucene' }); + + expect(operator).toEqual(' AND '); + }); + + test('it returns empty string when operator is "included"', () => { + const operator = operatorBuilder({ operator: 'included', language: 'lucene' }); + + expect(operator).toEqual(' AND NOT '); + }); + }); + }); + + describe('buildExists', () => { + describe('kuery', () => { + test('it returns formatted wildcard string when operator is "excluded"', () => { + const query = buildExists({ operator: 'excluded', field: 'host.name', language: 'kuery' }); + + expect(query).toEqual(' and host.name:*'); + }); + + test('it returns formatted wildcard string when operator is "included"', () => { + const query = buildExists({ operator: 'included', field: 'host.name', language: 'kuery' }); + + expect(query).toEqual(' and not host.name:*'); + }); + }); + + describe('lucene', () => { + test('it returns formatted wildcard string when operator is "excluded"', () => { + const query = buildExists({ operator: 'excluded', field: 'host.name', language: 'lucene' }); + + expect(query).toEqual(' AND _exists_host.name'); + }); + + test('it returns formatted wildcard string when operator is "included"', () => { + const query = buildExists({ operator: 'included', field: 'host.name', language: 'lucene' }); + + expect(query).toEqual(' AND NOT _exists_host.name'); + }); + }); + }); + + describe('buildMatch', () => { + describe('kuery', () => { + test('it returns empty string if no items in "values"', () => { + const query = buildMatch({ + operator: 'included', + field: 'host.name', + values: [], + language: 'kuery', + }); + + expect(query).toEqual(''); + }); + + test('it returns formatted string when operator is "included"', () => { + const values = [ + { + name: 'suricata', + }, + ]; + const query = buildMatch({ + operator: 'included', + field: 'host.name', + values, + language: 'kuery', + }); + + expect(query).toEqual(' and not host.name:suricata'); + }); + + test('it returns formatted string when operator is "excluded"', () => { + const values = [ + { + name: 'suricata', + }, + ]; + const query = buildMatch({ + operator: 'excluded', + field: 'host.name', + values, + language: 'kuery', + }); + + expect(query).toEqual(' and host.name:suricata'); + }); + + // TODO: need to clean up types and maybe restrict values to one if type is 'match' + test('it returns formatted string when "values" includes more than one item', () => { + const values = [ + { + name: 'suricata', + }, + { + name: 'auditd', + }, + ]; + const query = buildMatch({ + operator: 'included', + field: 'host.name', + values, + language: 'kuery', + }); + + expect(query).toEqual(' and not host.name:suricata'); + }); + }); + + describe('lucene', () => { + test('it returns formatted string when operator is "included"', () => { + const values = [ + { + name: 'suricata', + }, + ]; + const query = buildMatch({ + operator: 'included', + field: 'host.name', + values, + language: 'lucene', + }); + + expect(query).toEqual(' AND NOT host.name:suricata'); + }); + + test('it returns formatted string when operator is "excluded"', () => { + const values = [ + { + name: 'suricata', + }, + ]; + const query = buildMatch({ + operator: 'excluded', + field: 'host.name', + values, + language: 'lucene', + }); + + expect(query).toEqual(' AND host.name:suricata'); + }); + + // TODO: need to clean up types and maybe restrict values to one if type is 'match' + test('it returns formatted string when "values" includes more than one item', () => { + const values = [ + { + name: 'suricata', + }, + { + name: 'auditd', + }, + ]; + const query = buildMatch({ + operator: 'included', + field: 'host.name', + values, + language: 'lucene', + }); + + expect(query).toEqual(' AND NOT host.name:suricata'); + }); + }); + }); + + describe('buildMatchAll', () => { + describe('kuery', () => { + test('it returns empty string if given an empty array for "values"', () => { + const exceptionSegment = buildMatchAll({ + operator: 'included', + field: 'host.name', + values: [], + language: 'kuery', + }); + + expect(exceptionSegment).toEqual(''); + }); + + test('it returns formatted string when "values" includes only one item', () => { + const values = [ + { + name: 'suricata', + }, + ]; + const exceptionSegment = buildMatchAll({ + operator: 'included', + field: 'host.name', + values, + language: 'kuery', + }); + + expect(exceptionSegment).toEqual(' and not host.name:suricata'); + }); + + test('it returns formatted string when operator is "included"', () => { + const values = [ + { + name: 'suricata', + }, + { + name: 'auditd', + }, + ]; + const exceptionSegment = buildMatchAll({ + operator: 'included', + field: 'host.name', + values, + language: 'kuery', + }); + + expect(exceptionSegment).toEqual(' and not host.name:(suricata or auditd)'); + }); + + test('it returns formatted string when operator is "excluded"', () => { + const values = [ + { + name: 'suricata', + }, + { + name: 'auditd', + }, + ]; + const exceptionSegment = buildMatchAll({ + operator: 'excluded', + field: 'host.name', + values, + language: 'kuery', + }); + + expect(exceptionSegment).toEqual(' and host.name:(suricata or auditd)'); + }); + }); + + describe('lucene', () => { + test('it returns formatted string when operator is "included"', () => { + const values = [ + { + name: 'suricata', + }, + { + name: 'auditd', + }, + ]; + const exceptionSegment = buildMatchAll({ + operator: 'included', + field: 'host.name', + values, + language: 'lucene', + }); + + expect(exceptionSegment).toEqual(' AND NOT host.name:(suricata OR auditd)'); + }); + + test('it returns formatted string when operator is "excluded"', () => { + const values = [ + { + name: 'suricata', + }, + { + name: 'auditd', + }, + ]; + const exceptionSegment = buildMatchAll({ + operator: 'excluded', + field: 'host.name', + values, + language: 'lucene', + }); + + expect(exceptionSegment).toEqual(' AND host.name:(suricata OR auditd)'); + }); + + test('it returns formatted string when "values" includes only one item', () => { + const values = [ + { + name: 'suricata', + }, + ]; + const exceptionSegment = buildMatchAll({ + operator: 'included', + field: 'host.name', + values, + language: 'lucene', + }); + + expect(exceptionSegment).toEqual(' AND NOT host.name:suricata'); + }); + }); + }); + + describe('evaluateValues', () => { + describe('kuery', () => { + test('it returns formatted wildcard string when "type" is "exists"', () => { + const list: List = { + values_operator: 'included', + values_type: 'exists', + field: 'host.name', + }; + const result = evaluateValues({ + list, + language: 'kuery', + }); + + expect(result).toEqual(' and not host.name:*'); + }); + + test('it returns formatted string when "type" is "match"', () => { + const list: List = { + values_operator: 'included', + values_type: 'match', + field: 'host.name', + values: [{ name: 'suricata' }], + }; + const result = evaluateValues({ + list, + language: 'kuery', + }); + + expect(result).toEqual(' and not host.name:suricata'); + }); + + test('it returns formatted string when "type" is "match_all"', () => { + const list: List = { + values_operator: 'included', + values_type: 'match_all', + field: 'host.name', + values: [ + { + name: 'suricata', + }, + { + name: 'auditd', + }, + ], + }; + + const result = evaluateValues({ + list, + language: 'kuery', + }); + + expect(result).toEqual(' and not host.name:(suricata or auditd)'); + }); + }); + + describe('lucene', () => { + describe('kuery', () => { + test('it returns formatted wildcard string when "type" is "exists"', () => { + const list: List = { + values_operator: 'included', + values_type: 'exists', + field: 'host.name', + }; + const result = evaluateValues({ + list, + language: 'lucene', + }); + + expect(result).toEqual(' AND NOT _exists_host.name'); + }); + + test('it returns formatted string when "type" is "match"', () => { + const list: List = { + values_operator: 'included', + values_type: 'match', + field: 'host.name', + values: [{ name: 'suricata' }], + }; + const result = evaluateValues({ + list, + language: 'lucene', + }); + + expect(result).toEqual(' AND NOT host.name:suricata'); + }); + + test('it returns formatted string when "type" is "match_all"', () => { + const list: List = { + values_operator: 'included', + values_type: 'match_all', + field: 'host.name', + values: [ + { + name: 'suricata', + }, + { + name: 'auditd', + }, + ], + }; + + const result = evaluateValues({ + list, + language: 'lucene', + }); + + expect(result).toEqual(' AND NOT host.name:(suricata OR auditd)'); + }); + }); + }); + }); + + describe('formatQuery', () => { + test('it returns query if "exceptions" is empty array', () => { + const formattedQuery = formatQuery({ exceptions: [], query: 'a:*', language: 'kuery' }); + + expect(formattedQuery).toEqual('a:*'); + }); + + test('it returns expected query string when single exception in array', () => { + const formattedQuery = formatQuery({ + exceptions: [' and b:(value-1 or value-2) and not c:*'], + query: 'a:*', + language: 'kuery', + }); + + expect(formattedQuery).toEqual('(a:* and b:(value-1 or value-2) and not c:*)'); + }); + + test('it returns expected query string when multiple exceptions in array', () => { + const formattedQuery = formatQuery({ + exceptions: [' and b:(value-1 or value-2) and not c:*', ' and not d:*'], + query: 'a:*', + language: 'kuery', + }); + + expect(formattedQuery).toEqual( + '(a:* and b:(value-1 or value-2) and not c:*) or (a:* and not d:*)' + ); + }); + }); + + describe('buildExceptions', () => { + test('it returns empty array if empty lists array passed in', () => { + const query = buildExceptions({ + query: 'a:*', + language: 'kuery', + lists: [], + }); + + expect(query).toEqual([]); + }); + + test('it returns expected query when more than one item in list', () => { + // Equal to query && !(b && !c) -> (query AND NOT b) OR (query AND c) + // https://www.dcode.fr/boolean-expressions-calculator + const lists: List[] = [ + { + field: 'b', + values_operator: 'included', + values_type: 'match_all', + values: [ + { + name: 'value-1', + }, + { + name: 'value-2', + }, + ], + }, + { + field: 'c', + values_operator: 'excluded', + values_type: 'match', + values: [ + { + name: 'value-3', + }, + ], + }, + ]; + const query = buildExceptions({ + query: 'a:*', + language: 'kuery', + lists, + }); + const expectedQuery = [' and not b:(value-1 or value-2)', ' and c:value-3']; + + expect(query).toEqual(expectedQuery); + }); + + test('it returns expected query when list item includes nested "and" value', () => { + // Equal to query && !(b || !c) -> (query AND NOT b AND c) + // https://www.dcode.fr/boolean-expressions-calculator + const lists: List[] = [ + { + field: 'b', + values_operator: 'included', + values_type: 'match_all', + values: [ + { + name: 'value-1', + }, + { + name: 'value-2', + }, + ], + and: [ + { + field: 'c', + values_operator: 'excluded', + values_type: 'match', + values: [ + { + name: 'value-3', + }, + ], + }, + ], + }, + ]; + const query = buildExceptions({ + query: 'a:*', + language: 'kuery', + lists, + }); + const expectedQuery = [' and not b:(value-1 or value-2) and c:value-3']; + + expect(query).toEqual(expectedQuery); + }); + + test('it returns expected query when list item includes nested "and" value of empty array', () => { + // Equal to query && !(b || !c) -> (query AND NOT b AND c) + // https://www.dcode.fr/boolean-expressions-calculator + const lists: List[] = [ + { + field: 'b', + values_operator: 'included', + values_type: 'match_all', + values: [ + { + name: 'value-1', + }, + { + name: 'value-2', + }, + ], + and: [], + }, + ]; + const query = buildExceptions({ + query: 'a:*', + language: 'kuery', + lists, + }); + const expectedQuery = [' and not b:(value-1 or value-2)']; + + expect(query).toEqual(expectedQuery); + }); + + test('it returns expected query when list item includes nested "and" value of null', () => { + // Equal to query && !(b || !c) -> (query AND NOT b AND c) + // https://www.dcode.fr/boolean-expressions-calculator + const lists: List[] = [ + { + field: 'b', + values_operator: 'included', + values_type: 'match_all', + values: [ + { + name: 'value-1', + }, + { + name: 'value-2', + }, + ], + and: undefined, + }, + ]; + const query = buildExceptions({ + query: 'a:*', + language: 'kuery', + lists, + }); + const expectedQuery = [' and not b:(value-1 or value-2)']; + + expect(query).toEqual(expectedQuery); + }); + + test('it returns expected query when list includes multiple items and nested "and" values', () => { + // Equal to query && !((b || !c) && d) -> (query AND NOT b AND c) OR (query AND NOT d) + // https://www.dcode.fr/boolean-expressions-calculator + const lists: List[] = [ + { + field: 'b', + values_operator: 'included', + values_type: 'match_all', + values: [ + { + name: 'value-1', + }, + { + name: 'value-2', + }, + ], + and: [ + { + field: 'c', + values_operator: 'excluded', + values_type: 'match', + values: [ + { + name: 'value-3', + }, + ], + }, + ], + }, + { + field: 'd', + values_operator: 'included', + values_type: 'exists', + }, + ]; + const query = buildExceptions({ + query: 'a:*', + language: 'kuery', + lists, + }); + const expectedQuery = [' and not b:(value-1 or value-2) and c:value-3', ' and not d:*']; + + expect(query).toEqual(expectedQuery); + }); + + test('it returns expected query when language is "lucene"', () => { + // Equal to query && !((b || !c) && !d) -> (query AND NOT b AND c) OR (query AND d) + // https://www.dcode.fr/boolean-expressions-calculator + const lists: List[] = [ + { + field: 'b', + values_operator: 'included', + values_type: 'match_all', + values: [ + { + name: 'value-1', + }, + { + name: 'value-2', + }, + ], + and: [ + { + field: 'c', + values_operator: 'excluded', + values_type: 'match', + values: [ + { + name: 'value-3', + }, + ], + }, + ], + }, + { + field: 'e', + values_operator: 'excluded', + values_type: 'exists', + }, + ]; + const query = buildExceptions({ + query: 'a:*', + language: 'lucene', + lists, + }); + const expectedQuery = [' AND NOT b:(value-1 OR value-2) AND c:value-3', ' AND _exists_e']; + + expect(query).toEqual(expectedQuery); + }); + + describe('exists', () => { + test('it returns expected query when list includes single list item with values_operator of "included"', () => { + // Equal to query && !(b) -> (query AND NOT b) + // https://www.dcode.fr/boolean-expressions-calculator + const lists: List[] = [ + { + field: 'b', + values_operator: 'included', + values_type: 'exists', + }, + ]; + const query = buildExceptions({ + query: 'a:*', + language: 'kuery', + lists, + }); + const expectedQuery = [' and not b:*']; + + expect(query).toEqual(expectedQuery); + }); + + test('it returns expected query when list includes single list item with values_operator of "excluded"', () => { + // Equal to query && !(!b) -> (query AND b) + // https://www.dcode.fr/boolean-expressions-calculator + const lists: List[] = [ + { + field: 'b', + values_operator: 'excluded', + values_type: 'exists', + }, + ]; + const query = buildExceptions({ + query: 'a:*', + language: 'kuery', + lists, + }); + const expectedQuery = [' and b:*']; + + expect(query).toEqual(expectedQuery); + }); + + test('it returns expected query when list includes list item with "and" values', () => { + // Equal to query && !(!b || !c) -> (query AND b AND c) + // https://www.dcode.fr/boolean-expressions-calculator + const lists: List[] = [ + { + field: 'b', + values_operator: 'excluded', + values_type: 'exists', + and: [ + { + field: 'c', + values_operator: 'excluded', + values_type: 'exists', + }, + ], + }, + ]; + const query = buildExceptions({ + query: 'a:*', + language: 'kuery', + lists, + }); + const expectedQuery = [' and b:* and c:*']; + + expect(query).toEqual(expectedQuery); + }); + + test('it returns expected query when list includes multiple items', () => { + // Equal to query && !((b || !c || d) && e) -> (query AND NOT b AND c AND NOT d) OR (query AND NOT e) + // https://www.dcode.fr/boolean-expressions-calculator + const lists: List[] = [ + { + field: 'b', + values_operator: 'included', + values_type: 'exists', + and: [ + { + field: 'c', + values_operator: 'excluded', + values_type: 'exists', + }, + { + field: 'd', + values_operator: 'included', + values_type: 'exists', + }, + ], + }, + { + field: 'e', + values_operator: 'included', + values_type: 'exists', + }, + ]; + const query = buildExceptions({ + query: 'a:*', + language: 'kuery', + lists, + }); + const expectedQuery = [' and not b:* and c:* and not d:*', ' and not e:*']; + + expect(query).toEqual(expectedQuery); + }); + }); + + describe('match', () => { + test('it returns expected query when list includes single list item with values_operator of "included"', () => { + // Equal to query && !(b) -> (query AND NOT b) + // https://www.dcode.fr/boolean-expressions-calculator + const lists: List[] = [ + { + field: 'b', + values_operator: 'included', + values_type: 'match', + values: [ + { + name: 'value', + }, + ], + }, + ]; + const query = buildExceptions({ + query: 'a:*', + language: 'kuery', + lists, + }); + const expectedQuery = [' and not b:value']; + + expect(query).toEqual(expectedQuery); + }); + + test('it returns expected query when list includes single list item with values_operator of "excluded"', () => { + // Equal to query && !(!b) -> (query AND b) + // https://www.dcode.fr/boolean-expressions-calculator + const lists: List[] = [ + { + field: 'b', + values_operator: 'excluded', + values_type: 'match', + values: [ + { + name: 'value', + }, + ], + }, + ]; + const query = buildExceptions({ + query: 'a:*', + language: 'kuery', + lists, + }); + const expectedQuery = [' and b:value']; + + expect(query).toEqual(expectedQuery); + }); + + test('it returns expected query when list includes list item with "and" values', () => { + // Equal to query && !(!b || !c) -> (query AND b AND c) + // https://www.dcode.fr/boolean-expressions-calculator + const lists: List[] = [ + { + field: 'b', + values_operator: 'excluded', + values_type: 'match', + values: [ + { + name: 'value', + }, + ], + and: [ + { + field: 'c', + values_operator: 'excluded', + values_type: 'match', + values: [ + { + name: 'valueC', + }, + ], + }, + ], + }, + ]; + const query = buildExceptions({ + query: 'a:*', + language: 'kuery', + lists, + }); + const expectedQuery = [' and b:value and c:valueC']; + + expect(query).toEqual(expectedQuery); + }); + + test('it returns expected query when list includes multiple items', () => { + // Equal to query && !((b || !c || d) && e) -> (query AND NOT b AND c AND NOT d) OR (query AND NOT e) + // https://www.dcode.fr/boolean-expressions-calculator + const lists: List[] = [ + { + field: 'b', + values_operator: 'included', + values_type: 'match', + values: [ + { + name: 'value', + }, + ], + and: [ + { + field: 'c', + values_operator: 'excluded', + values_type: 'match', + values: [ + { + name: 'valueC', + }, + ], + }, + { + field: 'd', + values_operator: 'included', + values_type: 'match', + values: [ + { + name: 'valueC', + }, + ], + }, + ], + }, + { + field: 'e', + values_operator: 'included', + values_type: 'match', + values: [ + { + name: 'valueC', + }, + ], + }, + ]; + const query = buildExceptions({ + query: 'a:*', + language: 'kuery', + lists, + }); + const expectedQuery = [ + ' and not b:value and c:valueC and not d:valueC', + ' and not e:valueC', + ]; + + expect(query).toEqual(expectedQuery); + }); + }); + + describe('match_all', () => { + test('it returns expected query when list includes single list item with values_operator of "included"', () => { + // Equal to query && !(b) -> (query AND NOT b) + // https://www.dcode.fr/boolean-expressions-calculator + const lists: List[] = [ + { + field: 'b', + values_operator: 'included', + values_type: 'match_all', + values: [ + { + name: 'value', + }, + { + name: 'value-1', + }, + ], + }, + ]; + const query = buildExceptions({ + query: 'a:*', + language: 'kuery', + lists, + }); + const expectedQuery = [' and not b:(value or value-1)']; + + expect(query).toEqual(expectedQuery); + }); + + test('it returns expected query when list includes single list item with values_operator of "excluded"', () => { + // Equal to query && !(!b) -> (query AND b) + // https://www.dcode.fr/boolean-expressions-calculator + const lists: List[] = [ + { + field: 'b', + values_operator: 'excluded', + values_type: 'match_all', + values: [ + { + name: 'value', + }, + { + name: 'value-1', + }, + ], + }, + ]; + const query = buildExceptions({ + query: 'a:*', + language: 'kuery', + lists, + }); + const expectedQuery = [' and b:(value or value-1)']; + + expect(query).toEqual(expectedQuery); + }); + + test('it returns expected query when list includes list item with "and" values', () => { + // Equal to query && !(!b || c) -> (query AND b AND NOT c) + // https://www.dcode.fr/boolean-expressions-calculator + const lists: List[] = [ + { + field: 'b', + values_operator: 'excluded', + values_type: 'match_all', + values: [ + { + name: 'value', + }, + { + name: 'value-1', + }, + ], + and: [ + { + field: 'c', + values_operator: 'included', + values_type: 'match_all', + values: [ + { + name: 'valueC', + }, + { + name: 'value-2', + }, + ], + }, + ], + }, + ]; + const query = buildExceptions({ + query: 'a:*', + language: 'kuery', + lists, + }); + const expectedQuery = [' and b:(value or value-1) and not c:(valueC or value-2)']; + + expect(query).toEqual(expectedQuery); + }); + + test('it returns expected query when list includes multiple items', () => { + // Equal to query && !((b || !c || d) && e) -> ((query AND NOT b AND c AND NOT d) OR (query AND NOT e) + // https://www.dcode.fr/boolean-expressions-calculator + const lists: List[] = [ + { + field: 'b', + values_operator: 'included', + values_type: 'match_all', + values: [ + { + name: 'value', + }, + { + name: 'value-1', + }, + ], + and: [ + { + field: 'c', + values_operator: 'excluded', + values_type: 'match_all', + values: [ + { + name: 'valueC', + }, + { + name: 'value-2', + }, + ], + }, + { + field: 'd', + values_operator: 'included', + values_type: 'match_all', + values: [ + { + name: 'valueD', + }, + { + name: 'value-3', + }, + ], + }, + ], + }, + { + field: 'e', + values_operator: 'included', + values_type: 'match_all', + values: [ + { + name: 'valueE', + }, + { + name: 'value-4', + }, + ], + }, + ]; + const query = buildExceptions({ + query: 'a:*', + language: 'kuery', + lists, + }); + const expectedQuery = [ + ' and not b:(value or value-1) and c:(valueC or value-2) and not d:(valueD or value-3)', + ' and not e:(valueE or value-4)', + ]; + + expect(query).toEqual(expectedQuery); + }); + }); + }); + + describe('buildQueryExceptions', () => { + test('it returns original query if no lists exist', () => { + const query = buildQueryExceptions({ + query: 'host.name: *', + language: 'kuery', + lists: undefined, + }); + const expectedQuery = 'host.name: *'; + + expect(query).toEqual([{ query: expectedQuery, language: 'kuery' }]); + }); + + test('it returns original query if lists is empty array', () => { + const query = buildQueryExceptions({ query: 'host.name: *', language: 'kuery', lists: [] }); + const expectedQuery = 'host.name: *'; + + expect(query).toEqual([{ query: expectedQuery, language: 'kuery' }]); + }); + + test('it returns original query if lists is null', () => { + const query = buildQueryExceptions({ query: 'host.name: *', language: 'kuery', lists: null }); + const expectedQuery = 'host.name: *'; + + expect(query).toEqual([{ query: expectedQuery, language: 'kuery' }]); + }); + + test('it returns original query if lists is undefined', () => { + const query = buildQueryExceptions({ + query: 'host.name: *', + language: 'kuery', + lists: undefined, + }); + const expectedQuery = 'host.name: *'; + + expect(query).toEqual([{ query: expectedQuery, language: 'kuery' }]); + }); + + test('it returns expected query when lists exist and language is "kuery"', () => { + // Equal to query && !((b || !c || d) && e) -> ((query AND NOT b AND c AND NOT d) OR (query AND NOT e) + // https://www.dcode.fr/boolean-expressions-calculator + const lists: List[] = [ + { + field: 'b', + values_operator: 'included', + values_type: 'match_all', + values: [ + { + name: 'value', + }, + { + name: 'value-1', + }, + ], + and: [ + { + field: 'c', + values_operator: 'excluded', + values_type: 'match_all', + values: [ + { + name: 'valueC', + }, + { + name: 'value-2', + }, + ], + }, + { + field: 'd', + values_operator: 'included', + values_type: 'match_all', + values: [ + { + name: 'valueD', + }, + { + name: 'value-3', + }, + ], + }, + ], + }, + { + field: 'e', + values_operator: 'included', + values_type: 'match_all', + values: [ + { + name: 'valueE', + }, + { + name: 'value-4', + }, + ], + }, + ]; + const query = buildQueryExceptions({ query: 'a:*', language: 'kuery', lists }); + const expectedQuery = + '(a:* and not b:(value or value-1) and c:(valueC or value-2) and not d:(valueD or value-3)) or (a:* and not e:(valueE or value-4))'; + + expect(query).toEqual([{ query: expectedQuery, language: 'kuery' }]); + }); + + test('it returns expected query when lists exist and language is "lucene"', () => { + // Equal to query && !((b || !c || d) && e) -> ((query AND NOT b AND c AND NOT d) OR (query AND NOT e) + // https://www.dcode.fr/boolean-expressions-calculator + const lists: List[] = [ + { + field: 'b', + values_operator: 'included', + values_type: 'match_all', + values: [ + { + name: 'value', + }, + { + name: 'value-1', + }, + ], + and: [ + { + field: 'c', + values_operator: 'excluded', + values_type: 'match_all', + values: [ + { + name: 'valueC', + }, + { + name: 'value-2', + }, + ], + }, + { + field: 'd', + values_operator: 'included', + values_type: 'match_all', + values: [ + { + name: 'valueD', + }, + { + name: 'value-3', + }, + ], + }, + ], + }, + { + field: 'e', + values_operator: 'included', + values_type: 'match_all', + values: [ + { + name: 'valueE', + }, + { + name: 'value-4', + }, + ], + }, + ]; + const query = buildQueryExceptions({ query: 'a:*', language: 'lucene', lists }); + const expectedQuery = + '(a:* AND NOT b:(value OR value-1) AND c:(valueC OR value-2) AND NOT d:(valueD OR value-3)) OR (a:* AND NOT e:(valueE OR value-4))'; + + expect(query).toEqual([{ query: expectedQuery, language: 'lucene' }]); + }); + }); +}); diff --git a/x-pack/plugins/siem/server/lib/detection_engine/signals/build_exceptions_query.ts b/x-pack/plugins/siem/server/lib/detection_engine/signals/build_exceptions_query.ts new file mode 100644 index 0000000000000..7a1564bb69546 --- /dev/null +++ b/x-pack/plugins/siem/server/lib/detection_engine/signals/build_exceptions_query.ts @@ -0,0 +1,203 @@ +/* + * 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 { Query } from '../../../../../../../src/plugins/data/server'; +import { List, ListOperator, ListValues } from '../routes/schemas/types/lists_default_array'; +import { RuleAlertParams, Language } from '../types'; + +type Operators = 'and' | 'or' | 'not'; +type LuceneOperators = 'AND' | 'OR' | 'NOT'; + +export const getLanguageBooleanOperator = ({ + language, + value, +}: { + language: Language; + value: Operators; +}): Operators | LuceneOperators => { + switch (language) { + case 'lucene': + const luceneValues: Record = { and: 'AND', or: 'OR', not: 'NOT' }; + + return luceneValues[value]; + case 'kuery': + return value; + default: + return value; + } +}; + +export const operatorBuilder = ({ + operator, + language, +}: { + operator: ListOperator; + language: Language; +}): string => { + const and = getLanguageBooleanOperator({ + language, + value: 'and', + }); + const or = getLanguageBooleanOperator({ + language, + value: 'not', + }); + + switch (operator) { + case 'excluded': + return ` ${and} `; + case 'included': + return ` ${and} ${or} `; + default: + return ''; + } +}; + +export const buildExists = ({ + operator, + field, + language, +}: { + operator: ListOperator; + field: string; + language: Language; +}): string => { + const exceptionOperator = operatorBuilder({ operator, language }); + + switch (language) { + case 'kuery': + return `${exceptionOperator}${field}:*`; + case 'lucene': + return `${exceptionOperator}_exists_${field}`; + default: + return ''; + } +}; + +export const buildMatch = ({ + operator, + field, + values, + language, +}: { + operator: ListOperator; + field: string; + values: ListValues[]; + language: Language; +}): string => { + if (values.length > 0) { + const exceptionOperator = operatorBuilder({ operator, language }); + const [exception] = values; + + return `${exceptionOperator}${field}:${exception.name}`; + } else { + return ''; + } +}; + +export const buildMatchAll = ({ + operator, + field, + values, + language, +}: { + operator: ListOperator; + field: string; + values: ListValues[]; + language: Language; +}): string => { + switch (values.length) { + case 0: + return ''; + case 1: + return buildMatch({ operator, field, values, language }); + default: + const or = getLanguageBooleanOperator({ language, value: 'or' }); + const exceptionOperator = operatorBuilder({ operator, language }); + const matchAllValues = values.map(value => { + return value.name; + }); + + return `${exceptionOperator}${field}:(${matchAllValues.join(` ${or} `)})`; + } +}; + +export const evaluateValues = ({ list, language }: { list: List; language: Language }): string => { + const { values_operator: operator, values_type: type, field, values } = list; + switch (type) { + case 'exists': + return buildExists({ operator, field, language }); + case 'match': + return buildMatch({ operator, field, values: values ?? [], language }); + case 'match_all': + return buildMatchAll({ operator, field, values: values ?? [], language }); + default: + return ''; + } +}; + +export const formatQuery = ({ + exceptions, + query, + language, +}: { + exceptions: string[]; + query: string; + language: Language; +}): string => { + if (exceptions.length > 0) { + const or = getLanguageBooleanOperator({ language, value: 'or' }); + const formattedExceptions = exceptions.map(exception => { + return `(${query}${exception})`; + }); + + return formattedExceptions.join(` ${or} `); + } else { + return query; + } +}; + +export const buildExceptions = ({ + query, + lists, + language, +}: { + query: string; + lists: List[]; + language: Language; +}): string[] => { + return lists.reduce((accum, listItem) => { + const { and, ...exceptionDetails } = { ...listItem }; + const andExceptionsSegments = and ? buildExceptions({ query, lists: and, language }) : []; + const exceptionSegment = evaluateValues({ list: exceptionDetails, language }); + const exception = [...exceptionSegment, ...andExceptionsSegments]; + + return [...accum, exception.join('')]; + }, []); +}; + +export const buildQueryExceptions = ({ + query, + language, + lists, +}: { + query: string; + language: Language; + lists: RuleAlertParams['exceptions_list']; +}): Query[] => { + if (lists && lists !== null) { + const exceptions = buildExceptions({ lists, language, query }); + const formattedQuery = formatQuery({ exceptions, language, query }); + + return [ + { + query: formattedQuery, + language, + }, + ]; + } else { + return [{ query, language }]; + } +}; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/build_rule.test.ts b/x-pack/plugins/siem/server/lib/detection_engine/signals/build_rule.test.ts similarity index 99% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/build_rule.test.ts rename to x-pack/plugins/siem/server/lib/detection_engine/signals/build_rule.test.ts index e5183ed4df7bd..b3586c884d0c7 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/build_rule.test.ts +++ b/x-pack/plugins/siem/server/lib/detection_engine/signals/build_rule.test.ts @@ -79,7 +79,7 @@ describe('buildRule', () => { query: 'host.name: Braden', }, ], - lists: [ + exceptions_list: [ { field: 'source.ip', values_operator: 'included', @@ -162,7 +162,7 @@ describe('buildRule', () => { updated_at: rule.updated_at, created_at: rule.created_at, throttle: 'no_actions', - lists: [ + exceptions_list: [ { field: 'source.ip', values_operator: 'included', @@ -244,7 +244,7 @@ describe('buildRule', () => { updated_at: rule.updated_at, created_at: rule.created_at, throttle: 'no_actions', - lists: [ + exceptions_list: [ { field: 'source.ip', values_operator: 'included', diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/build_rule.ts b/x-pack/plugins/siem/server/lib/detection_engine/signals/build_rule.ts similarity index 97% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/build_rule.ts rename to x-pack/plugins/siem/server/lib/detection_engine/signals/build_rule.ts index 9c375d7d45d5e..93d4e5e7719b2 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/build_rule.ts +++ b/x-pack/plugins/siem/server/lib/detection_engine/signals/build_rule.ts @@ -72,7 +72,7 @@ export const buildRule = ({ version: ruleParams.version, created_at: createdAt, updated_at: updatedAt, - lists: ruleParams.lists, + exceptions_list: ruleParams.exceptions_list, machine_learning_job_id: ruleParams.machineLearningJobId, anomaly_threshold: ruleParams.anomalyThreshold, }); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/build_signal.test.ts b/x-pack/plugins/siem/server/lib/detection_engine/signals/build_signal.test.ts similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/build_signal.test.ts rename to x-pack/plugins/siem/server/lib/detection_engine/signals/build_signal.test.ts diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/build_signal.ts b/x-pack/plugins/siem/server/lib/detection_engine/signals/build_signal.ts similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/build_signal.ts rename to x-pack/plugins/siem/server/lib/detection_engine/signals/build_signal.ts diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/bulk_create_ml_signals.test.ts b/x-pack/plugins/siem/server/lib/detection_engine/signals/bulk_create_ml_signals.test.ts similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/bulk_create_ml_signals.test.ts rename to x-pack/plugins/siem/server/lib/detection_engine/signals/bulk_create_ml_signals.test.ts diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/bulk_create_ml_signals.ts b/x-pack/plugins/siem/server/lib/detection_engine/signals/bulk_create_ml_signals.ts similarity index 94% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/bulk_create_ml_signals.ts rename to x-pack/plugins/siem/server/lib/detection_engine/signals/bulk_create_ml_signals.ts index ba8938f116fc6..d298f1cc7cbc6 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/bulk_create_ml_signals.ts +++ b/x-pack/plugins/siem/server/lib/detection_engine/signals/bulk_create_ml_signals.ts @@ -7,8 +7,8 @@ import { flow, set, omit } from 'lodash/fp'; import { SearchResponse } from 'elasticsearch'; -import { Logger } from '../../../../../../../../src/core/server'; -import { AlertServices } from '../../../../../../../plugins/alerting/server'; +import { Logger } from '../../../../../../../src/core/server'; +import { AlertServices } from '../../../../../alerting/server'; import { RuleAlertAction } from '../../../../common/detection_engine/types'; import { RuleTypeParams, RefreshTypes } from '../types'; import { singleBulkCreate, SingleBulkCreateResponse } from './single_bulk_create'; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/find_ml_signals.ts b/x-pack/plugins/siem/server/lib/detection_engine/signals/find_ml_signals.ts similarity index 91% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/find_ml_signals.ts rename to x-pack/plugins/siem/server/lib/detection_engine/signals/find_ml_signals.ts index b7f752e6ba5e0..8ac5a6cde39cc 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/find_ml_signals.ts +++ b/x-pack/plugins/siem/server/lib/detection_engine/signals/find_ml_signals.ts @@ -6,7 +6,7 @@ import dateMath from '@elastic/datemath'; -import { AlertServices } from '../../../../../../../plugins/alerting/server'; +import { AlertServices } from '../../../../../alerting/server'; import { getAnomalies } from '../../machine_learning'; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/get_filter.test.ts b/x-pack/plugins/siem/server/lib/detection_engine/signals/get_filter.test.ts similarity index 77% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/get_filter.test.ts rename to x-pack/plugins/siem/server/lib/detection_engine/signals/get_filter.test.ts index 86d1278031695..35ec1950cedaa 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/get_filter.test.ts +++ b/x-pack/plugins/siem/server/lib/detection_engine/signals/get_filter.test.ts @@ -5,42 +5,28 @@ */ import { getQueryFilter, getFilter } from './get_filter'; -import { savedObjectsClientMock } from 'src/core/server/mocks'; import { PartialFilter } from '../types'; -import { AlertServices } from '../../../../../../../plugins/alerting/server'; +import { alertsMock, AlertServicesMock } from '../../../../../alerting/server/mocks'; describe('get_filter', () => { - let savedObjectsClient = savedObjectsClientMock.create(); - savedObjectsClient.get = jest.fn().mockImplementation(() => ({ - attributes: { - query: { query: 'host.name: linux', language: 'kuery' }, - filters: [], - }, - })); - let servicesMock: AlertServices = { - savedObjectsClient, - callCluster: jest.fn(), - alertInstanceFactory: jest.fn(), - }; + let servicesMock: AlertServicesMock; beforeAll(() => { jest.resetAllMocks(); }); beforeEach(() => { - savedObjectsClient = savedObjectsClientMock.create(); - savedObjectsClient.get = jest.fn().mockImplementation(() => ({ + servicesMock = alertsMock.createAlertServices(); + servicesMock.savedObjectsClient.get.mockImplementation(async (type: string, id: string) => ({ + id, + type, + references: [], attributes: { query: { query: 'host.name: linux', language: 'kuery' }, language: 'kuery', filters: [], }, })); - servicesMock = { - savedObjectsClient, - callCluster: jest.fn(), - alertInstanceFactory: jest.fn(), - }; }); afterEach(() => { @@ -49,7 +35,7 @@ describe('get_filter', () => { describe('getQueryFilter', () => { test('it should work with an empty filter as kuery', () => { - const esQuery = getQueryFilter('host.name: linux', 'kuery', [], ['auditbeat-*']); + const esQuery = getQueryFilter('host.name: linux', 'kuery', [], ['auditbeat-*'], []); expect(esQuery).toEqual({ bool: { must: [], @@ -74,7 +60,7 @@ describe('get_filter', () => { }); test('it should work with an empty filter as lucene', () => { - const esQuery = getQueryFilter('host.name: linux', 'lucene', [], ['auditbeat-*']); + const esQuery = getQueryFilter('host.name: linux', 'lucene', [], ['auditbeat-*'], []); expect(esQuery).toEqual({ bool: { must: [ @@ -116,7 +102,8 @@ describe('get_filter', () => { }, }, ], - ['auditbeat-*'] + ['auditbeat-*'], + [] ); expect(esQuery).toEqual({ bool: { @@ -159,7 +146,8 @@ describe('get_filter', () => { }, }, ], - ['auditbeat-*'] + ['auditbeat-*'], + [] ); expect(esQuery).toEqual({ bool: { @@ -208,7 +196,8 @@ describe('get_filter', () => { 'host.name: windows', 'kuery', [query, exists], - ['auditbeat-*'] + ['auditbeat-*'], + [] ); expect(esQuery).toEqual({ bool: { @@ -266,7 +255,8 @@ describe('get_filter', () => { }, }, ], - ['auditbeat-*'] + ['auditbeat-*'], + [] ); expect(esQuery).toEqual({ bool: { @@ -314,7 +304,8 @@ describe('get_filter', () => { }, }, ], - ['auditbeat-*'] + ['auditbeat-*'], + [] ); expect(esQuery).toEqual({ bool: { @@ -363,7 +354,8 @@ describe('get_filter', () => { }, }, ], - ['auditbeat-*'] + ['auditbeat-*'], + [] ); expect(esQuery).toEqual({ bool: { @@ -382,6 +374,108 @@ describe('get_filter', () => { }, }); }); + + test('it should work with a list', () => { + const esQuery = getQueryFilter( + 'host.name: linux', + 'kuery', + [], + ['auditbeat-*'], + [ + { + field: 'event.module', + values_operator: 'excluded', + values_type: 'match', + values: [ + { + name: 'suricata', + }, + ], + }, + ] + ); + expect(esQuery).toEqual({ + bool: { + filter: [ + { + bool: { + filter: [ + { + bool: { + minimum_should_match: 1, + should: [ + { + match: { + 'host.name': 'linux', + }, + }, + ], + }, + }, + { + bool: { + minimum_should_match: 1, + should: [ + { + match: { + 'event.module': 'suricata', + }, + }, + ], + }, + }, + ], + }, + }, + ], + must: [], + must_not: [], + should: [], + }, + }); + }); + + test('it should work with an empty list', () => { + const esQuery = getQueryFilter('host.name: linux', 'kuery', [], ['auditbeat-*'], []); + expect(esQuery).toEqual({ + bool: { + filter: [ + { bool: { minimum_should_match: 1, should: [{ match: { 'host.name': 'linux' } }] } }, + ], + must: [], + must_not: [], + should: [], + }, + }); + }); + + test('it should work when lists has value null', () => { + const esQuery = getQueryFilter('host.name: linux', 'kuery', [], ['auditbeat-*'], null); + expect(esQuery).toEqual({ + bool: { + filter: [ + { bool: { minimum_should_match: 1, should: [{ match: { 'host.name': 'linux' } }] } }, + ], + must: [], + must_not: [], + should: [], + }, + }); + }); + + test('it should work when lists has value undefined', () => { + const esQuery = getQueryFilter('host.name: linux', 'kuery', [], ['auditbeat-*'], undefined); + expect(esQuery).toEqual({ + bool: { + filter: [ + { bool: { minimum_should_match: 1, should: [{ match: { 'host.name': 'linux' } }] } }, + ], + must: [], + must_not: [], + should: [], + }, + }); + }); }); describe('getFilter', () => { @@ -394,6 +488,7 @@ describe('get_filter', () => { savedId: undefined, services: servicesMock, index: ['auditbeat-*'], + lists: undefined, }); expect(filter).toEqual({ bool: { @@ -428,6 +523,7 @@ describe('get_filter', () => { savedId: undefined, services: servicesMock, index: ['auditbeat-*'], + lists: undefined, }) ).rejects.toThrow('query, filters, and index parameter should be defined'); }); @@ -442,6 +538,7 @@ describe('get_filter', () => { savedId: undefined, services: servicesMock, index: ['auditbeat-*'], + lists: undefined, }) ).rejects.toThrow('query, filters, and index parameter should be defined'); }); @@ -456,6 +553,7 @@ describe('get_filter', () => { savedId: undefined, services: servicesMock, index: undefined, + lists: undefined, }) ).rejects.toThrow('query, filters, and index parameter should be defined'); }); @@ -469,6 +567,7 @@ describe('get_filter', () => { savedId: 'some-id', services: servicesMock, index: ['auditbeat-*'], + lists: undefined, }); expect(filter).toEqual({ bool: { @@ -492,6 +591,7 @@ describe('get_filter', () => { savedId: undefined, services: servicesMock, index: ['auditbeat-*'], + lists: undefined, }) ).rejects.toThrow('savedId parameter should be defined'); }); @@ -506,6 +606,7 @@ describe('get_filter', () => { savedId: 'some-id', services: servicesMock, index: undefined, + lists: undefined, }) ).rejects.toThrow('savedId parameter should be defined'); }); @@ -520,6 +621,7 @@ describe('get_filter', () => { savedId: 'some-id', services: servicesMock, index: undefined, + lists: undefined, }) ).rejects.toThrow('Unsupported Rule of type "machine_learning" supplied to getFilter'); }); @@ -529,7 +631,8 @@ describe('get_filter', () => { '(event.module:suricata and event.kind:alert) and suricata.eve.alert.signature_id: (2610182 or 2610183 or 2610184 or 2610185 or 2610186 or 2610187)', 'kuery', [], - ['my custom index'] + ['my custom index'], + [] ); expect(esQuery).toEqual({ bool: { @@ -658,5 +761,68 @@ describe('get_filter', () => { }, }); }); + + test('returns a query when given a list', async () => { + const filter = await getFilter({ + type: 'query', + filters: undefined, + language: 'kuery', + query: 'host.name: siem', + savedId: undefined, + services: servicesMock, + index: ['auditbeat-*'], + lists: [ + { + field: 'event.module', + values_operator: 'excluded', + values_type: 'match', + values: [ + { + name: 'suricata', + }, + ], + }, + ], + }); + expect(filter).toEqual({ + bool: { + filter: [ + { + bool: { + filter: [ + { + bool: { + minimum_should_match: 1, + should: [ + { + match: { + 'host.name': 'siem', + }, + }, + ], + }, + }, + { + bool: { + minimum_should_match: 1, + should: [ + { + match: { + 'event.module': 'suricata', + }, + }, + ], + }, + }, + ], + }, + }, + ], + must: [], + must_not: [], + should: [], + }, + }); + }); }); }); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/get_filter.ts b/x-pack/plugins/siem/server/lib/detection_engine/signals/get_filter.ts similarity index 83% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/get_filter.ts rename to x-pack/plugins/siem/server/lib/detection_engine/signals/get_filter.ts index 82a50222dc351..d8fdab55bddeb 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/get_filter.ts +++ b/x-pack/plugins/siem/server/lib/detection_engine/signals/get_filter.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { AlertServices } from '../../../../../../../plugins/alerting/server'; +import { AlertServices } from '../../../../../alerting/server'; import { assertUnreachable } from '../../../utils/build_query'; import { Filter, @@ -12,22 +12,25 @@ import { esQuery, esFilters, IIndexPattern, -} from '../../../../../../../../src/plugins/data/server'; -import { PartialFilter, RuleAlertParams } from '../types'; +} from '../../../../../../../src/plugins/data/server'; +import { PartialFilter, RuleAlertParams, Language } from '../types'; import { BadRequestError } from '../errors/bad_request_error'; +import { buildQueryExceptions } from './build_exceptions_query'; export const getQueryFilter = ( query: string, - language: string, + language: Language, filters: PartialFilter[], - index: string[] + index: string[], + lists: RuleAlertParams['exceptions_list'] ) => { const indexPattern = { fields: [], title: index.join(), } as IIndexPattern; - const queries: Query[] = [{ query, language }]; + const queries: Query[] = buildQueryExceptions({ query, language, lists }); + const config = { allowLeadingWildcards: true, queryStringOptions: { analyze_wildcard: true }, @@ -45,18 +48,19 @@ export const getQueryFilter = ( interface GetFilterArgs { type: RuleAlertParams['type']; filters: PartialFilter[] | undefined | null; - language: string | undefined | null; + language: Language | undefined | null; query: string | undefined | null; savedId: string | undefined | null; services: AlertServices; index: string[] | undefined | null; + lists: RuleAlertParams['exceptions_list']; } interface QueryAttributes { // NOTE: doesn't match Query interface query: { query: string; - language: string; + language: Language; }; filters: PartialFilter[]; } @@ -69,11 +73,12 @@ export const getFilter = async ({ services, type, query, + lists, }: GetFilterArgs): Promise => { switch (type) { case 'query': { if (query != null && language != null && index != null) { - return getQueryFilter(query, language, filters || [], index); + return getQueryFilter(query, language, filters || [], index, lists); } else { throw new BadRequestError('query, filters, and index parameter should be defined'); } @@ -90,13 +95,14 @@ export const getFilter = async ({ savedObject.attributes.query.query, savedObject.attributes.query.language, savedObject.attributes.filters, - index + index, + lists ); } catch (err) { // saved object does not exist, so try and fall back if the user pushed // any additional language, query, filters, etc... if (query != null && language != null && index != null) { - return getQueryFilter(query, language, filters || [], index); + return getQueryFilter(query, language, filters || [], index, lists); } else { // user did not give any additional fall back mechanism for generating a rule // rethrow error for activity monitoring diff --git a/x-pack/plugins/siem/server/lib/detection_engine/signals/get_input_output_index.test.ts b/x-pack/plugins/siem/server/lib/detection_engine/signals/get_input_output_index.test.ts new file mode 100644 index 0000000000000..6fc99ada16ece --- /dev/null +++ b/x-pack/plugins/siem/server/lib/detection_engine/signals/get_input_output_index.test.ts @@ -0,0 +1,106 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { alertsMock, AlertServicesMock } from '../../../../../alerting/server/mocks'; +import { DEFAULT_INDEX_KEY, DEFAULT_INDEX_PATTERN } from '../../../../common/constants'; +import { getInputIndex } from './get_input_output_index'; + +describe('get_input_output_index', () => { + let servicesMock: AlertServicesMock; + + beforeAll(() => { + jest.resetAllMocks(); + }); + + afterAll(() => { + jest.resetAllMocks(); + }); + + beforeEach(() => { + servicesMock = alertsMock.createAlertServices(); + servicesMock.savedObjectsClient.get.mockImplementation(async (type: string, id: string) => ({ + id, + type, + references: [], + attributes: {}, + })); + }); + + describe('getInputOutputIndex', () => { + test('Returns inputIndex if inputIndex is passed in', async () => { + servicesMock.savedObjectsClient.get.mockImplementation(async (type: string, id: string) => ({ + id, + type, + references: [], + attributes: {}, + })); + const inputIndex = await getInputIndex(servicesMock, '8.0.0', ['test-input-index-1']); + expect(inputIndex).toEqual(['test-input-index-1']); + }); + + test('Returns a saved object inputIndex if passed in inputIndex is undefined', async () => { + servicesMock.savedObjectsClient.get.mockImplementation(async (type: string, id: string) => ({ + id, + type, + references: [], + attributes: { + [DEFAULT_INDEX_KEY]: ['configured-index-1', 'configured-index-2'], + }, + })); + const inputIndex = await getInputIndex(servicesMock, '8.0.0', undefined); + expect(inputIndex).toEqual(['configured-index-1', 'configured-index-2']); + }); + + test('Returns a saved object inputIndex if passed in inputIndex is null', async () => { + servicesMock.savedObjectsClient.get.mockImplementation(async (type: string, id: string) => ({ + id, + type, + references: [], + attributes: { + [DEFAULT_INDEX_KEY]: ['configured-index-1', 'configured-index-2'], + }, + })); + const inputIndex = await getInputIndex(servicesMock, '8.0.0', null); + expect(inputIndex).toEqual(['configured-index-1', 'configured-index-2']); + }); + + test('Returns a saved object inputIndex default from constants if inputIndex passed in is null and the key is also null', async () => { + servicesMock.savedObjectsClient.get.mockImplementation(async (type: string, id: string) => ({ + id, + type, + references: [], + attributes: { + [DEFAULT_INDEX_KEY]: null, + }, + })); + const inputIndex = await getInputIndex(servicesMock, '8.0.0', null); + expect(inputIndex).toEqual(DEFAULT_INDEX_PATTERN); + }); + + test('Returns a saved object inputIndex default from constants if inputIndex passed in is undefined and the key is also null', async () => { + servicesMock.savedObjectsClient.get.mockImplementation(async (type: string, id: string) => ({ + id, + type, + references: [], + attributes: { + [DEFAULT_INDEX_KEY]: null, + }, + })); + const inputIndex = await getInputIndex(servicesMock, '8.0.0', undefined); + expect(inputIndex).toEqual(DEFAULT_INDEX_PATTERN); + }); + + test('Returns a saved object inputIndex default from constants if both passed in inputIndex and configuration attributes are missing and the index is undefined', async () => { + const inputIndex = await getInputIndex(servicesMock, '8.0.0', undefined); + expect(inputIndex).toEqual(DEFAULT_INDEX_PATTERN); + }); + + test('Returns a saved object inputIndex default from constants if both passed in inputIndex and configuration attributes are missing and the index is null', async () => { + const inputIndex = await getInputIndex(servicesMock, '8.0.0', null); + expect(inputIndex).toEqual(DEFAULT_INDEX_PATTERN); + }); + }); +}); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/get_input_output_index.ts b/x-pack/plugins/siem/server/lib/detection_engine/signals/get_input_output_index.ts similarity index 75% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/get_input_output_index.ts rename to x-pack/plugins/siem/server/lib/detection_engine/signals/get_input_output_index.ts index c93990e25b52b..85e3eeac476e4 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/get_input_output_index.ts +++ b/x-pack/plugins/siem/server/lib/detection_engine/signals/get_input_output_index.ts @@ -4,9 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ -import { AlertServices } from '../../../../../../../plugins/alerting/server'; -import { defaultIndexPattern } from '../../../../default_index_pattern'; -import { DEFAULT_INDEX_KEY } from '../../../../common/constants'; +import { DEFAULT_INDEX_KEY, DEFAULT_INDEX_PATTERN } from '../../../../common/constants'; +import { AlertServices } from '../../../../../alerting/server'; export const getInputIndex = async ( services: AlertServices, @@ -22,7 +21,7 @@ export const getInputIndex = async ( if (configuration.attributes != null && configuration.attributes[DEFAULT_INDEX_KEY] != null) { return configuration.attributes[DEFAULT_INDEX_KEY]; } else { - return defaultIndexPattern; + return DEFAULT_INDEX_PATTERN; } } }; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/get_or_create_rule_statuses.ts b/x-pack/plugins/siem/server/lib/detection_engine/signals/get_or_create_rule_statuses.ts similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/get_or_create_rule_statuses.ts rename to x-pack/plugins/siem/server/lib/detection_engine/signals/get_or_create_rule_statuses.ts diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/get_rule_status_saved_objects.ts b/x-pack/plugins/siem/server/lib/detection_engine/signals/get_rule_status_saved_objects.ts similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/get_rule_status_saved_objects.ts rename to x-pack/plugins/siem/server/lib/detection_engine/signals/get_rule_status_saved_objects.ts diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/rule_messages.test.ts b/x-pack/plugins/siem/server/lib/detection_engine/signals/rule_messages.test.ts similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/rule_messages.test.ts rename to x-pack/plugins/siem/server/lib/detection_engine/signals/rule_messages.test.ts diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/rule_messages.ts b/x-pack/plugins/siem/server/lib/detection_engine/signals/rule_messages.ts similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/rule_messages.ts rename to x-pack/plugins/siem/server/lib/detection_engine/signals/rule_messages.ts diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/rule_status_saved_objects_client.ts b/x-pack/plugins/siem/server/lib/detection_engine/signals/rule_status_saved_objects_client.ts similarity index 96% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/rule_status_saved_objects_client.ts rename to x-pack/plugins/siem/server/lib/detection_engine/signals/rule_status_saved_objects_client.ts index 11cbf67304409..5f76889f238a1 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/rule_status_saved_objects_client.ts +++ b/x-pack/plugins/siem/server/lib/detection_engine/signals/rule_status_saved_objects_client.ts @@ -10,7 +10,7 @@ import { SavedObjectsUpdateResponse, SavedObjectsFindOptions, SavedObjectsFindResponse, -} from '../../../../../../../../src/core/server'; +} from '../../../../../../../src/core/server'; import { ruleStatusSavedObjectType } from '../rules/saved_object_mappings'; import { IRuleStatusAttributes } from '../rules/types'; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/rule_status_service.test.ts b/x-pack/plugins/siem/server/lib/detection_engine/signals/rule_status_service.test.ts similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/rule_status_service.test.ts rename to x-pack/plugins/siem/server/lib/detection_engine/signals/rule_status_service.test.ts diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/rule_status_service.ts b/x-pack/plugins/siem/server/lib/detection_engine/signals/rule_status_service.ts similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/rule_status_service.ts rename to x-pack/plugins/siem/server/lib/detection_engine/signals/rule_status_service.ts diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/search_after_bulk_create.test.ts b/x-pack/plugins/siem/server/lib/detection_engine/signals/search_after_bulk_create.test.ts similarity index 92% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/search_after_bulk_create.test.ts rename to x-pack/plugins/siem/server/lib/detection_engine/signals/search_after_bulk_create.test.ts index 81600b0b8dd9b..cec011ae8c445 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/search_after_bulk_create.test.ts +++ b/x-pack/plugins/siem/server/lib/detection_engine/signals/search_after_bulk_create.test.ts @@ -16,20 +16,16 @@ import { } from './__mocks__/es_results'; import { searchAfterAndBulkCreate } from './search_after_bulk_create'; import { DEFAULT_SIGNALS_INDEX } from '../../../../common/constants'; -import { savedObjectsClientMock } from 'src/core/server/mocks'; +import { alertsMock, AlertServicesMock } from '../../../../../alerting/server/mocks'; import uuid from 'uuid'; -export const mockService = { - callCluster: jest.fn(), - alertInstanceFactory: jest.fn(), - savedObjectsClient: savedObjectsClientMock.create(), -}; - describe('searchAfterAndBulkCreate', () => { + let mockService: AlertServicesMock; let inputIndexPattern: string[] = []; beforeEach(() => { jest.clearAllMocks(); inputIndexPattern = ['auditbeat-*']; + mockService = alertsMock.createAlertServices(); }); test('if successful with empty search results', async () => { @@ -65,7 +61,7 @@ describe('searchAfterAndBulkCreate', () => { const sampleParams = sampleRuleAlertParams(30); const someGuids = Array.from({ length: 13 }).map(x => uuid.v4()); mockService.callCluster - .mockReturnValueOnce({ + .mockResolvedValueOnce({ took: 100, errors: false, items: [ @@ -79,8 +75,8 @@ describe('searchAfterAndBulkCreate', () => { }, ], }) - .mockReturnValueOnce(repeatedSearchResultsWithSortId(3, 1, someGuids.slice(0, 3))) - .mockReturnValueOnce({ + .mockResolvedValueOnce(repeatedSearchResultsWithSortId(3, 1, someGuids.slice(0, 3))) + .mockResolvedValueOnce({ took: 100, errors: false, items: [ @@ -94,8 +90,8 @@ describe('searchAfterAndBulkCreate', () => { }, ], }) - .mockReturnValueOnce(repeatedSearchResultsWithSortId(3, 1, someGuids.slice(3, 6))) - .mockReturnValueOnce({ + .mockResolvedValueOnce(repeatedSearchResultsWithSortId(3, 1, someGuids.slice(3, 6))) + .mockResolvedValueOnce({ took: 100, errors: false, items: [ @@ -139,7 +135,7 @@ describe('searchAfterAndBulkCreate', () => { test('if unsuccessful first bulk create', async () => { const someGuids = Array.from({ length: 4 }).map(x => uuid.v4()); const sampleParams = sampleRuleAlertParams(10); - mockService.callCluster.mockReturnValue(sampleBulkCreateDuplicateResult); + mockService.callCluster.mockResolvedValue(sampleBulkCreateDuplicateResult); const { success, createdSignalsCount } = await searchAfterAndBulkCreate({ someResult: repeatedSearchResultsWithSortId(4, 1, someGuids), ruleParams: sampleParams, @@ -169,7 +165,7 @@ describe('searchAfterAndBulkCreate', () => { test('if unsuccessful iteration of searchAfterAndBulkCreate due to empty sort ids', async () => { const sampleParams = sampleRuleAlertParams(); - mockService.callCluster.mockReturnValueOnce({ + mockService.callCluster.mockResolvedValueOnce({ took: 100, errors: false, items: [ @@ -212,7 +208,7 @@ describe('searchAfterAndBulkCreate', () => { test('if unsuccessful iteration of searchAfterAndBulkCreate due to empty sort ids and 0 total hits', async () => { const sampleParams = sampleRuleAlertParams(); - mockService.callCluster.mockReturnValueOnce({ + mockService.callCluster.mockResolvedValueOnce({ took: 100, errors: false, items: [ @@ -256,7 +252,7 @@ describe('searchAfterAndBulkCreate', () => { const sampleParams = sampleRuleAlertParams(10); const someGuids = Array.from({ length: 4 }).map(x => uuid.v4()); mockService.callCluster - .mockReturnValueOnce({ + .mockResolvedValueOnce({ took: 100, errors: false, items: [ @@ -270,7 +266,7 @@ describe('searchAfterAndBulkCreate', () => { }, ], }) - .mockReturnValueOnce(sampleDocSearchResultsNoSortId()); + .mockResolvedValueOnce(sampleDocSearchResultsNoSortId()); const { success, createdSignalsCount } = await searchAfterAndBulkCreate({ someResult: repeatedSearchResultsWithSortId(4, 1, someGuids), ruleParams: sampleParams, @@ -301,7 +297,7 @@ describe('searchAfterAndBulkCreate', () => { const sampleParams = sampleRuleAlertParams(10); const someGuids = Array.from({ length: 4 }).map(x => uuid.v4()); mockService.callCluster - .mockReturnValueOnce({ + .mockResolvedValueOnce({ took: 100, errors: false, items: [ @@ -315,7 +311,7 @@ describe('searchAfterAndBulkCreate', () => { }, ], }) - .mockReturnValueOnce(sampleEmptyDocSearchResults()); + .mockResolvedValueOnce(sampleEmptyDocSearchResults()); const { success, createdSignalsCount } = await searchAfterAndBulkCreate({ someResult: repeatedSearchResultsWithSortId(4, 1, someGuids), ruleParams: sampleParams, @@ -346,7 +342,7 @@ describe('searchAfterAndBulkCreate', () => { const sampleParams = sampleRuleAlertParams(10); const someGuids = Array.from({ length: 4 }).map(x => uuid.v4()); mockService.callCluster - .mockReturnValueOnce({ + .mockResolvedValueOnce({ took: 100, errors: false, items: [ diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/search_after_bulk_create.ts b/x-pack/plugins/siem/server/lib/detection_engine/signals/search_after_bulk_create.ts similarity index 97% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/search_after_bulk_create.ts rename to x-pack/plugins/siem/server/lib/detection_engine/signals/search_after_bulk_create.ts index 3a964cb91fbdb..e287e33295c89 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/search_after_bulk_create.ts +++ b/x-pack/plugins/siem/server/lib/detection_engine/signals/search_after_bulk_create.ts @@ -4,10 +4,10 @@ * you may not use this file except in compliance with the Elastic License. */ -import { AlertServices } from '../../../../../../../plugins/alerting/server'; +import { AlertServices } from '../../../../../alerting/server'; import { RuleAlertAction } from '../../../../common/detection_engine/types'; import { RuleTypeParams, RefreshTypes } from '../types'; -import { Logger } from '../../../../../../../../src/core/server'; +import { Logger } from '../../../../../../../src/core/server'; import { singleSearchAfter } from './single_search_after'; import { singleBulkCreate } from './single_bulk_create'; import { SignalSearchResponse } from './types'; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/siem_rule_action_groups.ts b/x-pack/plugins/siem/server/lib/detection_engine/signals/siem_rule_action_groups.ts similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/siem_rule_action_groups.ts rename to x-pack/plugins/siem/server/lib/detection_engine/signals/siem_rule_action_groups.ts diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/signal_params_schema.ts b/x-pack/plugins/siem/server/lib/detection_engine/signals/signal_params_schema.ts similarity index 95% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/signal_params_schema.ts rename to x-pack/plugins/siem/server/lib/detection_engine/signals/signal_params_schema.ts index 58dd53b6447c5..81a6ce9b08f02 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/signal_params_schema.ts +++ b/x-pack/plugins/siem/server/lib/detection_engine/signals/signal_params_schema.ts @@ -39,5 +39,5 @@ export const signalParamsSchema = () => type: schema.string(), references: schema.arrayOf(schema.string(), { defaultValue: [] }), version: schema.number({ defaultValue: 1 }), - lists: schema.maybe(schema.arrayOf(schema.object({}, { unknowns: 'allow' }))), + exceptions_list: schema.maybe(schema.arrayOf(schema.object({}, { unknowns: 'allow' }))), }); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/signal_rule_alert_type.test.ts b/x-pack/plugins/siem/server/lib/detection_engine/signals/signal_rule_alert_type.test.ts similarity index 82% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/signal_rule_alert_type.test.ts rename to x-pack/plugins/siem/server/lib/detection_engine/signals/signal_rule_alert_type.test.ts index 03fb5832fdf42..7eecc5cb9bad0 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/signal_rule_alert_type.test.ts +++ b/x-pack/plugins/siem/server/lib/detection_engine/signals/signal_rule_alert_type.test.ts @@ -5,11 +5,10 @@ */ import moment from 'moment'; -import { savedObjectsClientMock } from 'src/core/server/mocks'; import { loggerMock } from 'src/core/server/logging/logger.mock'; import { getResult, getMlResult } from '../routes/__mocks__/request_responses'; import { signalRulesAlertType } from './signal_rule_alert_type'; -import { AlertInstance } from '../../../../../../../plugins/alerting/server'; +import { alertsMock, AlertServicesMock } from '../../../../../alerting/server/mocks'; import { ruleStatusServiceFactory } from './rule_status_service'; import { getGapBetweenRuns } from './utils'; import { RuleExecutorOptions } from './types'; @@ -28,18 +27,9 @@ jest.mock('../notifications/schedule_notification_actions'); jest.mock('./find_ml_signals'); jest.mock('./bulk_create_ml_signals'); -const getPayload = ( - ruleAlert: RuleAlertType, - alertInstanceFactoryMock: () => AlertInstance, - savedObjectsClient: ReturnType, - callClusterMock: jest.Mock -) => ({ +const getPayload = (ruleAlert: RuleAlertType, services: AlertServicesMock) => ({ alertId: ruleAlert.id, - services: { - savedObjectsClient, - alertInstanceFactory: alertInstanceFactoryMock, - callCluster: callClusterMock, - }, + services, params: { ...ruleAlert.params, actions: [], @@ -78,24 +68,14 @@ describe('rules_notification_alert_type', () => { modulesProvider: jest.fn(), resultsServiceProvider: jest.fn(), }; - let payload: RuleExecutorOptions; + let payload: jest.Mocked; let alert: ReturnType; - let alertInstanceMock: Record; - let alertInstanceFactoryMock: () => AlertInstance; - let savedObjectsClient: ReturnType; let logger: ReturnType; - let callClusterMock: jest.Mock; + let alertServices: AlertServicesMock; let ruleStatusService: Record; beforeEach(() => { - alertInstanceMock = { - scheduleActions: jest.fn(), - replaceState: jest.fn(), - }; - alertInstanceMock.replaceState.mockReturnValue(alertInstanceMock); - alertInstanceFactoryMock = jest.fn().mockReturnValue(alertInstanceMock); - callClusterMock = jest.fn(); - savedObjectsClient = savedObjectsClientMock.create(); + alertServices = alertsMock.createAlertServices(); logger = loggerMock.create(); ruleStatusService = { success: jest.fn(), @@ -111,20 +91,20 @@ describe('rules_notification_alert_type', () => { searchAfterTimes: [], createdSignalsCount: 10, }); - callClusterMock.mockResolvedValue({ + alertServices.callCluster.mockResolvedValue({ hits: { total: { value: 10 }, }, }); const ruleAlert = getResult(); - savedObjectsClient.get.mockResolvedValue({ + alertServices.savedObjectsClient.get.mockResolvedValue({ id: 'id', type: 'type', references: [], attributes: ruleAlert, }); - payload = getPayload(ruleAlert, alertInstanceFactoryMock, savedObjectsClient, callClusterMock); + payload = getPayload(ruleAlert, alertServices); alert = signalRulesAlertType({ logger, @@ -164,7 +144,7 @@ describe('rules_notification_alert_type', () => { }, ]; - savedObjectsClient.get.mockResolvedValue({ + alertServices.savedObjectsClient.get.mockResolvedValue({ id: 'id', type: 'type', references: [], @@ -195,7 +175,7 @@ describe('rules_notification_alert_type', () => { }, ]; - savedObjectsClient.get.mockResolvedValue({ + alertServices.savedObjectsClient.get.mockResolvedValue({ id: 'id', type: 'type', references: [], @@ -214,12 +194,7 @@ describe('rules_notification_alert_type', () => { describe('ML rule', () => { it('should throw an error if ML plugin was not available', async () => { const ruleAlert = getMlResult(); - payload = getPayload( - ruleAlert, - alertInstanceFactoryMock, - savedObjectsClient, - callClusterMock - ); + payload = getPayload(ruleAlert, alertServices); alert = signalRulesAlertType({ logger, version, @@ -235,12 +210,7 @@ describe('rules_notification_alert_type', () => { it('should throw an error if machineLearningJobId or anomalyThreshold was not null', async () => { const ruleAlert = getMlResult(); ruleAlert.params.anomalyThreshold = undefined; - payload = getPayload( - ruleAlert, - alertInstanceFactoryMock, - savedObjectsClient, - callClusterMock - ); + payload = getPayload(ruleAlert, alertServices); await alert.executor(payload); expect(logger.error).toHaveBeenCalled(); expect(logger.error.mock.calls[0][0]).toContain( @@ -250,12 +220,7 @@ describe('rules_notification_alert_type', () => { it('should throw an error if Machine learning job summary was null', async () => { const ruleAlert = getMlResult(); - payload = getPayload( - ruleAlert, - alertInstanceFactoryMock, - savedObjectsClient, - callClusterMock - ); + payload = getPayload(ruleAlert, alertServices); jobsSummaryMock.mockResolvedValue([]); await alert.executor(payload); expect(logger.warn).toHaveBeenCalled(); @@ -268,12 +233,7 @@ describe('rules_notification_alert_type', () => { it('should log an error if Machine learning job was not started', async () => { const ruleAlert = getMlResult(); - payload = getPayload( - ruleAlert, - alertInstanceFactoryMock, - savedObjectsClient, - callClusterMock - ); + payload = getPayload(ruleAlert, alertServices); jobsSummaryMock.mockResolvedValue([ { id: 'some_job_id', @@ -297,12 +257,7 @@ describe('rules_notification_alert_type', () => { it('should not call ruleStatusService.success if no anomalies were found', async () => { const ruleAlert = getMlResult(); - payload = getPayload( - ruleAlert, - alertInstanceFactoryMock, - savedObjectsClient, - callClusterMock - ); + payload = getPayload(ruleAlert, alertServices); jobsSummaryMock.mockResolvedValue([]); (findMlSignals as jest.Mock).mockResolvedValue({ hits: { @@ -320,12 +275,7 @@ describe('rules_notification_alert_type', () => { it('should call ruleStatusService.success if signals were created', async () => { const ruleAlert = getMlResult(); - payload = getPayload( - ruleAlert, - alertInstanceFactoryMock, - savedObjectsClient, - callClusterMock - ); + payload = getPayload(ruleAlert, alertServices); jobsSummaryMock.mockResolvedValue([ { id: 'some_job_id', @@ -360,13 +310,8 @@ describe('rules_notification_alert_type', () => { id: '99403909-ca9b-49ba-9d7a-7e5320e68d05', }, ]; - payload = getPayload( - ruleAlert, - alertInstanceFactoryMock, - savedObjectsClient, - callClusterMock - ); - savedObjectsClient.get.mockResolvedValue({ + payload = getPayload(ruleAlert, alertServices); + alertServices.savedObjectsClient.get.mockResolvedValue({ id: 'id', type: 'type', references: [], diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/signal_rule_alert_type.ts b/x-pack/plugins/siem/server/lib/detection_engine/signals/signal_rule_alert_type.ts similarity index 99% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/signal_rule_alert_type.ts rename to x-pack/plugins/siem/server/lib/detection_engine/signals/signal_rule_alert_type.ts index 0357f906f8035..137603741dc8f 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/signal_rule_alert_type.ts +++ b/x-pack/plugins/siem/server/lib/detection_engine/signals/signal_rule_alert_type.ts @@ -66,6 +66,7 @@ export const signalRulesAlertType = ({ query, to, type, + exceptions_list, } = params; const searchAfterSize = Math.min(maxSignals, DEFAULT_SEARCH_AFTER_PAGE_SIZE); let hasError: boolean = false; @@ -200,6 +201,7 @@ export const signalRulesAlertType = ({ savedId, services, index: inputIndex, + lists: exceptions_list, }); const noReIndex = buildEventsSearchQuery({ diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/single_bulk_create.test.ts b/x-pack/plugins/siem/server/lib/detection_engine/signals/single_bulk_create.test.ts similarity index 96% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/single_bulk_create.test.ts rename to x-pack/plugins/siem/server/lib/detection_engine/signals/single_bulk_create.test.ts index 45365b446cbf0..51cc0f449b17a 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/single_bulk_create.test.ts +++ b/x-pack/plugins/siem/server/lib/detection_engine/signals/single_bulk_create.test.ts @@ -16,17 +16,13 @@ import { sampleBulkCreateErrorResult, sampleDocWithAncestors, } from './__mocks__/es_results'; -import { savedObjectsClientMock } from 'src/core/server/mocks'; import { DEFAULT_SIGNALS_INDEX } from '../../../../common/constants'; import { singleBulkCreate, filterDuplicateRules } from './single_bulk_create'; - -export const mockService = { - callCluster: jest.fn(), - alertInstanceFactory: jest.fn(), - savedObjectsClient: savedObjectsClientMock.create(), -}; +import { alertsMock, AlertServicesMock } from '../../../../../alerting/server/mocks'; describe('singleBulkCreate', () => { + const mockService: AlertServicesMock = alertsMock.createAlertServices(); + beforeEach(() => { jest.clearAllMocks(); }); @@ -135,7 +131,7 @@ describe('singleBulkCreate', () => { test('create successful bulk create', async () => { const sampleParams = sampleRuleAlertParams(); - mockService.callCluster.mockReturnValueOnce({ + mockService.callCluster.mockResolvedValueOnce({ took: 100, errors: false, items: [ @@ -169,7 +165,7 @@ describe('singleBulkCreate', () => { test('create successful bulk create with docs with no versioning', async () => { const sampleParams = sampleRuleAlertParams(); - mockService.callCluster.mockReturnValueOnce({ + mockService.callCluster.mockResolvedValueOnce({ took: 100, errors: false, items: [ @@ -203,7 +199,7 @@ describe('singleBulkCreate', () => { test('create unsuccessful bulk create due to empty search results', async () => { const sampleParams = sampleRuleAlertParams(); - mockService.callCluster.mockReturnValue(false); + mockService.callCluster.mockResolvedValue(false); const { success, createdItemsCount } = await singleBulkCreate({ someResult: sampleEmptyDocSearchResults(), ruleParams: sampleParams, @@ -230,7 +226,7 @@ describe('singleBulkCreate', () => { test('create successful bulk create when bulk create has duplicate errors', async () => { const sampleParams = sampleRuleAlertParams(); const sampleSearchResult = sampleDocSearchResultsNoSortId; - mockService.callCluster.mockReturnValue(sampleBulkCreateDuplicateResult); + mockService.callCluster.mockResolvedValue(sampleBulkCreateDuplicateResult); const { success, createdItemsCount } = await singleBulkCreate({ someResult: sampleSearchResult(), ruleParams: sampleParams, @@ -259,7 +255,7 @@ describe('singleBulkCreate', () => { test('create successful bulk create when bulk create has multiple error statuses', async () => { const sampleParams = sampleRuleAlertParams(); const sampleSearchResult = sampleDocSearchResultsNoSortId; - mockService.callCluster.mockReturnValue(sampleBulkCreateErrorResult); + mockService.callCluster.mockResolvedValue(sampleBulkCreateErrorResult); const { success, createdItemsCount } = await singleBulkCreate({ someResult: sampleSearchResult(), ruleParams: sampleParams, @@ -354,7 +350,7 @@ describe('singleBulkCreate', () => { test('create successful and returns proper createdItemsCount', async () => { const sampleParams = sampleRuleAlertParams(); - mockService.callCluster.mockReturnValue(sampleBulkCreateDuplicateResult); + mockService.callCluster.mockResolvedValue(sampleBulkCreateDuplicateResult); const { success, createdItemsCount } = await singleBulkCreate({ someResult: sampleDocSearchResultsNoSortId(), ruleParams: sampleParams, diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/single_bulk_create.ts b/x-pack/plugins/siem/server/lib/detection_engine/signals/single_bulk_create.ts similarity index 86% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/single_bulk_create.ts rename to x-pack/plugins/siem/server/lib/detection_engine/signals/single_bulk_create.ts index fc33d0e15e43f..c098a4b68450d 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/single_bulk_create.ts +++ b/x-pack/plugins/siem/server/lib/detection_engine/signals/single_bulk_create.ts @@ -6,13 +6,13 @@ import { countBy, isEmpty } from 'lodash'; import { performance } from 'perf_hooks'; -import { AlertServices } from '../../../../../../../plugins/alerting/server'; +import { AlertServices } from '../../../../../alerting/server'; import { SignalSearchResponse, BulkResponse } from './types'; import { RuleAlertAction } from '../../../../common/detection_engine/types'; import { RuleTypeParams, RefreshTypes } from '../types'; -import { generateId, makeFloatString } from './utils'; +import { generateId, makeFloatString, errorAggregator } from './utils'; import { buildBulkBody } from './build_bulk_body'; -import { Logger } from '../../../../../../../../src/core/server'; +import { Logger } from '../../../../../../../src/core/server'; interface SingleBulkCreateParams { someResult: SignalSearchResponse; @@ -134,17 +134,10 @@ export const singleBulkCreate = async ({ logger.debug(`took property says bulk took: ${response.took} milliseconds`); if (response.errors) { - const itemsWithErrors = response.items.filter(item => item.create.error); - const errorCountsByStatus = countBy(itemsWithErrors, item => item.create.status); - delete errorCountsByStatus['409']; // Duplicate signals are expected - - if (!isEmpty(errorCountsByStatus)) { + const errorCountByMessage = errorAggregator(response, [409]); + if (!isEmpty(errorCountByMessage)) { logger.error( - `[-] bulkResponse had errors with response statuses:counts of...\n${JSON.stringify( - errorCountsByStatus, - null, - 2 - )}` + `[-] bulkResponse had errors with responses of: ${JSON.stringify(errorCountByMessage)}` ); } } diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/single_search_after.test.ts b/x-pack/plugins/siem/server/lib/detection_engine/signals/single_search_after.test.ts similarity index 83% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/single_search_after.test.ts rename to x-pack/plugins/siem/server/lib/detection_engine/signals/single_search_after.test.ts index 9b726c38d3d96..580080966457e 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/single_search_after.test.ts +++ b/x-pack/plugins/siem/server/lib/detection_engine/signals/single_search_after.test.ts @@ -4,28 +4,24 @@ * you may not use this file except in compliance with the Elastic License. */ -import { savedObjectsClientMock } from 'src/core/server/mocks'; import { sampleDocSearchResultsNoSortId, mockLogger, sampleDocSearchResultsWithSortId, } from './__mocks__/es_results'; import { singleSearchAfter } from './single_search_after'; - -export const mockService = { - callCluster: jest.fn(), - alertInstanceFactory: jest.fn(), - savedObjectsClient: savedObjectsClientMock.create(), -}; +import { alertsMock, AlertServicesMock } from '../../../../../alerting/server/mocks'; describe('singleSearchAfter', () => { + const mockService: AlertServicesMock = alertsMock.createAlertServices(); + beforeEach(() => { jest.clearAllMocks(); }); test('if singleSearchAfter works without a given sort id', async () => { let searchAfterSortId; - mockService.callCluster.mockReturnValue(sampleDocSearchResultsNoSortId); + mockService.callCluster.mockResolvedValue(sampleDocSearchResultsNoSortId); await expect( singleSearchAfter({ searchAfterSortId, @@ -41,7 +37,7 @@ describe('singleSearchAfter', () => { }); test('if singleSearchAfter works with a given sort id', async () => { const searchAfterSortId = '1234567891111'; - mockService.callCluster.mockReturnValue(sampleDocSearchResultsWithSortId); + mockService.callCluster.mockResolvedValue(sampleDocSearchResultsWithSortId); const { searchResult } = await singleSearchAfter({ searchAfterSortId, index: [], diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/single_search_after.ts b/x-pack/plugins/siem/server/lib/detection_engine/signals/single_search_after.ts similarity index 91% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/single_search_after.ts rename to x-pack/plugins/siem/server/lib/detection_engine/signals/single_search_after.ts index 6fc8fe4bd24d9..8071c18713c19 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/single_search_after.ts +++ b/x-pack/plugins/siem/server/lib/detection_engine/signals/single_search_after.ts @@ -5,8 +5,8 @@ */ import { performance } from 'perf_hooks'; -import { AlertServices } from '../../../../../../../plugins/alerting/server'; -import { Logger } from '../../../../../../../../src/core/server'; +import { AlertServices } from '../../../../../alerting/server'; +import { Logger } from '../../../../../../../src/core/server'; import { SignalSearchResponse } from './types'; import { buildEventsSearchQuery } from './build_events_query'; import { makeFloatString } from './utils'; diff --git a/x-pack/plugins/siem/server/lib/detection_engine/signals/types.ts b/x-pack/plugins/siem/server/lib/detection_engine/signals/types.ts new file mode 100644 index 0000000000000..b493bab8b4610 --- /dev/null +++ b/x-pack/plugins/siem/server/lib/detection_engine/signals/types.ts @@ -0,0 +1,169 @@ +/* + * 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 { AlertType, State, AlertExecutorOptions } from '../../../../../alerting/server'; +import { RuleAlertAction } from '../../../../common/detection_engine/types'; +import { RuleAlertParams, OutputRuleAlertRest } from '../types'; +import { SearchResponse } from '../../types'; + +export interface SignalsParams { + signalIds: string[] | undefined | null; + query: object | undefined | null; + status: 'open' | 'closed'; +} + +export interface SignalsStatusParams { + signalIds: string[] | undefined | null; + query: object | undefined | null; + status: 'open' | 'closed'; +} + +export interface SignalQueryParams { + query: object | undefined | null; + aggs: object | undefined | null; + _source: string[] | undefined | null; + size: number | undefined | null; + track_total_hits: boolean | undefined | null; +} + +export type SignalsStatusRestParams = Omit & { + signal_ids: SignalsStatusParams['signalIds']; +}; + +export type SignalsQueryRestParams = SignalQueryParams; + +export type SearchTypes = + | string + | string[] + | number + | number[] + | boolean + | boolean[] + | object + | object[] + | undefined; + +export interface SignalSource { + [key: string]: SearchTypes; + '@timestamp': string; + signal?: { + parent: Ancestor; + ancestors: Ancestor[]; + }; +} + +export interface BulkItem { + create: { + _index: string; + _type?: string; + _id: string; + _version: number; + result?: string; + _shards?: { + total: number; + successful: number; + failed: number; + }; + _seq_no?: number; + _primary_term?: number; + status: number; + error?: { + type: string; + reason: string; + index_uuid?: string; + shard: string; + index: string; + }; + }; +} + +export interface BulkResponse { + took: number; + errors: boolean; + items: BulkItem[]; +} + +export interface MGetResponse { + docs: GetResponse[]; +} +export interface GetResponse { + _index: string; + _type: string; + _id: string; + _version: number; + _seq_no: number; + _primary_term: number; + found: boolean; + _source: SearchTypes; +} + +export type SignalSearchResponse = SearchResponse; +export type SignalSourceHit = SignalSearchResponse['hits']['hits'][number]; + +export type RuleExecutorOptions = Omit & { + params: RuleAlertParams & { + scrollSize: number; + scrollLock: string; + }; +}; + +// This returns true because by default a RuleAlertTypeDefinition is an AlertType +// since we are only increasing the strictness of params. +export const isAlertExecutor = (obj: SignalRuleAlertTypeDefinition): obj is AlertType => { + return true; +}; + +export type SignalRuleAlertTypeDefinition = Omit & { + executor: ({ services, params, state }: RuleExecutorOptions) => Promise; +}; + +export interface Ancestor { + rule: string; + id: string; + type: string; + index: string; + depth: number; +} + +export interface Signal { + rule: Partial; + parent: Ancestor; + ancestors: Ancestor[]; + original_time: string; + original_event?: SearchTypes; + status: 'open' | 'closed'; +} + +export interface SignalHit { + '@timestamp': string; + event: object; + signal: Partial; +} + +export interface AlertAttributes { + actions: RuleAlertAction[]; + enabled: boolean; + name: string; + tags: string[]; + createdBy: string; + createdAt: string; + updatedBy: string; + schedule: { + interval: string; + }; + throttle: string; +} + +export interface RuleAlertAttributes extends AlertAttributes { + params: Omit< + RuleAlertParams, + 'ruleId' | 'name' | 'enabled' | 'interval' | 'tags' | 'actions' | 'throttle' + > & { + ruleId: string; + }; +} + +export type BulkResponseErrorAggregation = Record; diff --git a/x-pack/plugins/siem/server/lib/detection_engine/signals/utils.test.ts b/x-pack/plugins/siem/server/lib/detection_engine/signals/utils.test.ts new file mode 100644 index 0000000000000..e3a1b0c052aca --- /dev/null +++ b/x-pack/plugins/siem/server/lib/detection_engine/signals/utils.test.ts @@ -0,0 +1,566 @@ +/* + * 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 moment from 'moment'; +import sinon from 'sinon'; + +import { + generateId, + parseInterval, + parseScheduleDates, + getDriftTolerance, + getGapBetweenRuns, + errorAggregator, +} from './utils'; + +import { BulkResponseErrorAggregation } from './types'; + +import { + sampleBulkResponse, + sampleEmptyBulkResponse, + sampleBulkError, + sampleBulkErrorItem, +} from './__mocks__/es_results'; + +describe('utils', () => { + const anchor = '2020-01-01T06:06:06.666Z'; + const unix = moment(anchor).valueOf(); + let nowDate = moment('2020-01-01T00:00:00.000Z'); + let clock: sinon.SinonFakeTimers; + + beforeEach(() => { + nowDate = moment('2020-01-01T00:00:00.000Z'); + clock = sinon.useFakeTimers(unix); + }); + + afterEach(() => { + clock.restore(); + }); + + describe('generateId', () => { + test('it generates expected output', () => { + const id = generateId('index-123', 'doc-123', 'version-123', 'rule-123'); + expect(id).toEqual('10622e7d06c9e38a532e71fc90e3426c1100001fb617aec8cb974075da52db06'); + }); + + test('expected output is a hex', () => { + const id = generateId('index-123', 'doc-123', 'version-123', 'rule-123'); + expect(id).toMatch(/[a-f0-9]+/); + }); + }); + + describe('parseInterval', () => { + test('it returns a duration when given one that is valid', () => { + const duration = parseInterval('5m'); + expect(duration).not.toBeNull(); + expect(duration?.asMilliseconds()).toEqual(moment.duration(5, 'minutes').asMilliseconds()); + }); + + test('it returns null given an invalid duration', () => { + const duration = parseInterval('junk'); + expect(duration).toBeNull(); + }); + }); + + describe('parseScheduleDates', () => { + test('it returns a moment when given an ISO string', () => { + const result = parseScheduleDates('2020-01-01T00:00:00.000Z'); + expect(result).not.toBeNull(); + expect(result).toEqual(moment('2020-01-01T00:00:00.000Z')); + }); + + test('it returns a moment when given `now`', () => { + const result = parseScheduleDates('now'); + + expect(result).not.toBeNull(); + expect(moment.isMoment(result)).toBeTruthy(); + }); + + test('it returns a moment when given `now-x`', () => { + const result = parseScheduleDates('now-6m'); + + expect(result).not.toBeNull(); + expect(moment.isMoment(result)).toBeTruthy(); + }); + + test('it returns null when given a string that is not an ISO string, `now` or `now-x`', () => { + const result = parseScheduleDates('invalid'); + + expect(result).toBeNull(); + }); + }); + + describe('getDriftTolerance', () => { + test('it returns a drift tolerance in milliseconds of 1 minute when "from" overlaps "to" by 1 minute and the interval is 5 minutes', () => { + const drift = getDriftTolerance({ + from: 'now-6m', + to: 'now', + interval: moment.duration(5, 'minutes'), + }); + expect(drift).not.toBeNull(); + expect(drift?.asMilliseconds()).toEqual(moment.duration(1, 'minute').asMilliseconds()); + }); + + test('it returns a drift tolerance of 0 when "from" equals the interval', () => { + const drift = getDriftTolerance({ + from: 'now-5m', + to: 'now', + interval: moment.duration(5, 'minutes'), + }); + expect(drift?.asMilliseconds()).toEqual(0); + }); + + test('it returns a drift tolerance of 5 minutes when "from" is 10 minutes but the interval is 5 minutes', () => { + const drift = getDriftTolerance({ + from: 'now-10m', + to: 'now', + interval: moment.duration(5, 'minutes'), + }); + expect(drift).not.toBeNull(); + expect(drift?.asMilliseconds()).toEqual(moment.duration(5, 'minutes').asMilliseconds()); + }); + + test('it returns a drift tolerance of 10 minutes when "from" is 10 minutes ago and the interval is 0', () => { + const drift = getDriftTolerance({ + from: 'now-10m', + to: 'now', + interval: moment.duration(0, 'milliseconds'), + }); + expect(drift).not.toBeNull(); + expect(drift?.asMilliseconds()).toEqual(moment.duration(10, 'minutes').asMilliseconds()); + }); + + test('returns a drift tolerance of 1 minute when "from" is invalid and defaults to "now-6m" and interval is 5 minutes', () => { + const drift = getDriftTolerance({ + from: 'invalid', + to: 'now', + interval: moment.duration(5, 'minutes'), + }); + expect(drift).not.toBeNull(); + expect(drift?.asMilliseconds()).toEqual(moment.duration(1, 'minute').asMilliseconds()); + }); + + test('returns a drift tolerance of 1 minute when "from" does not include `now` and defaults to "now-6m" and interval is 5 minutes', () => { + const drift = getDriftTolerance({ + from: '10m', + to: 'now', + interval: moment.duration(5, 'minutes'), + }); + expect(drift).not.toBeNull(); + expect(drift?.asMilliseconds()).toEqual(moment.duration(1, 'minute').asMilliseconds()); + }); + + test('returns a drift tolerance of 4 minutes when "to" is "now-x", from is a valid input and interval is 5 minute', () => { + const drift = getDriftTolerance({ + from: 'now-10m', + to: 'now-1m', + interval: moment.duration(5, 'minutes'), + }); + expect(drift).not.toBeNull(); + expect(drift?.asMilliseconds()).toEqual(moment.duration(4, 'minutes').asMilliseconds()); + }); + + test('it returns expected drift tolerance when "from" is an ISO string', () => { + const drift = getDriftTolerance({ + from: moment() + .subtract(10, 'minutes') + .toISOString(), + to: 'now', + interval: moment.duration(5, 'minutes'), + }); + expect(drift).not.toBeNull(); + expect(drift?.asMilliseconds()).toEqual(moment.duration(5, 'minutes').asMilliseconds()); + }); + + test('it returns expected drift tolerance when "to" is an ISO string', () => { + const drift = getDriftTolerance({ + from: 'now-6m', + to: moment().toISOString(), + interval: moment.duration(5, 'minutes'), + }); + expect(drift).not.toBeNull(); + expect(drift?.asMilliseconds()).toEqual(moment.duration(1, 'minute').asMilliseconds()); + }); + }); + + describe('getGapBetweenRuns', () => { + test('it returns a gap of 0 when "from" and interval match each other and the previous started was from the previous interval time', () => { + const gap = getGapBetweenRuns({ + previousStartedAt: nowDate + .clone() + .subtract(5, 'minutes') + .toDate(), + interval: '5m', + from: 'now-5m', + to: 'now', + now: nowDate.clone(), + }); + expect(gap).not.toBeNull(); + expect(gap?.asMilliseconds()).toEqual(0); + }); + + test('it returns a negative gap of 1 minute when "from" overlaps to by 1 minute and the previousStartedAt was 5 minutes ago', () => { + const gap = getGapBetweenRuns({ + previousStartedAt: nowDate + .clone() + .subtract(5, 'minutes') + .toDate(), + interval: '5m', + from: 'now-6m', + to: 'now', + now: nowDate.clone(), + }); + expect(gap).not.toBeNull(); + expect(gap?.asMilliseconds()).toEqual(moment.duration(-1, 'minute').asMilliseconds()); + }); + + test('it returns a negative gap of 5 minutes when "from" overlaps to by 1 minute and the previousStartedAt was 5 minutes ago', () => { + const gap = getGapBetweenRuns({ + previousStartedAt: nowDate + .clone() + .subtract(5, 'minutes') + .toDate(), + interval: '5m', + from: 'now-10m', + to: 'now', + now: nowDate.clone(), + }); + expect(gap).not.toBeNull(); + expect(gap?.asMilliseconds()).toEqual(moment.duration(-5, 'minute').asMilliseconds()); + }); + + test('it returns a negative gap of 1 minute when "from" overlaps to by 1 minute and the previousStartedAt was 10 minutes ago and so was the interval', () => { + const gap = getGapBetweenRuns({ + previousStartedAt: nowDate + .clone() + .subtract(10, 'minutes') + .toDate(), + interval: '10m', + from: 'now-11m', + to: 'now', + now: nowDate.clone(), + }); + expect(gap).not.toBeNull(); + expect(gap?.asMilliseconds()).toEqual(moment.duration(-1, 'minute').asMilliseconds()); + }); + + test('it returns a gap of only -30 seconds when the from overlaps with now by 1 minute, the interval is 5 minutes but the previous started is 30 seconds more', () => { + const gap = getGapBetweenRuns({ + previousStartedAt: nowDate + .clone() + .subtract(5, 'minutes') + .subtract(30, 'seconds') + .toDate(), + interval: '5m', + from: 'now-6m', + to: 'now', + now: nowDate.clone(), + }); + expect(gap).not.toBeNull(); + expect(gap?.asMilliseconds()).toEqual(moment.duration(-30, 'seconds').asMilliseconds()); + }); + + test('it returns an exact 0 gap when the from overlaps with now by 1 minute, the interval is 5 minutes but the previous started is one minute late', () => { + const gap = getGapBetweenRuns({ + previousStartedAt: nowDate + .clone() + .subtract(6, 'minutes') + .toDate(), + interval: '5m', + from: 'now-6m', + to: 'now', + now: nowDate.clone(), + }); + expect(gap).not.toBeNull(); + expect(gap?.asMilliseconds()).toEqual(moment.duration(0, 'minute').asMilliseconds()); + }); + + test('it returns a gap of 30 seconds when the from overlaps with now by 1 minute, the interval is 5 minutes but the previous started is one minute and 30 seconds late', () => { + const gap = getGapBetweenRuns({ + previousStartedAt: nowDate + .clone() + .subtract(6, 'minutes') + .subtract(30, 'seconds') + .toDate(), + interval: '5m', + from: 'now-6m', + to: 'now', + now: nowDate.clone(), + }); + expect(gap).not.toBeNull(); + expect(gap?.asMilliseconds()).toEqual(moment.duration(30, 'seconds').asMilliseconds()); + }); + + test('it returns a gap of 1 minute when the from overlaps with now by 1 minute, the interval is 5 minutes but the previous started is two minutes late', () => { + const gap = getGapBetweenRuns({ + previousStartedAt: nowDate + .clone() + .subtract(7, 'minutes') + .toDate(), + interval: '5m', + from: 'now-6m', + to: 'now', + now: nowDate.clone(), + }); + expect(gap?.asMilliseconds()).not.toBeNull(); + expect(gap?.asMilliseconds()).toEqual(moment.duration(1, 'minute').asMilliseconds()); + }); + + test('it returns null if given a previousStartedAt of null', () => { + const gap = getGapBetweenRuns({ + previousStartedAt: null, + interval: '5m', + from: 'now-5m', + to: 'now', + now: nowDate.clone(), + }); + expect(gap).toBeNull(); + }); + + test('it returns null if the interval is an invalid string such as "invalid"', () => { + const gap = getGapBetweenRuns({ + previousStartedAt: nowDate.clone().toDate(), + interval: 'invalid', // if not set to "x" where x is an interval such as 6m + from: 'now-5m', + to: 'now', + now: nowDate.clone(), + }); + expect(gap).toBeNull(); + }); + + test('it returns the expected result when "from" is an invalid string such as "invalid"', () => { + const gap = getGapBetweenRuns({ + previousStartedAt: nowDate + .clone() + .subtract(7, 'minutes') + .toDate(), + interval: '5m', + from: 'invalid', + to: 'now', + now: nowDate.clone(), + }); + expect(gap?.asMilliseconds()).not.toBeNull(); + expect(gap?.asMilliseconds()).toEqual(moment.duration(1, 'minute').asMilliseconds()); + }); + + test('it returns the expected result when "to" is an invalid string such as "invalid"', () => { + const gap = getGapBetweenRuns({ + previousStartedAt: nowDate + .clone() + .subtract(7, 'minutes') + .toDate(), + interval: '5m', + from: 'now-6m', + to: 'invalid', + now: nowDate.clone(), + }); + expect(gap?.asMilliseconds()).not.toBeNull(); + expect(gap?.asMilliseconds()).toEqual(moment.duration(1, 'minute').asMilliseconds()); + }); + }); + + describe('errorAggregator', () => { + test('it should aggregate with an empty object when given an empty bulk response', () => { + const empty = sampleEmptyBulkResponse(); + const aggregated = errorAggregator(empty, []); + const expected: BulkResponseErrorAggregation = {}; + expect(aggregated).toEqual(expected); + }); + + test('it should aggregate with an empty object when given a valid bulk response with no errors', () => { + const validResponse = sampleBulkResponse(); + const aggregated = errorAggregator(validResponse, []); + const expected: BulkResponseErrorAggregation = {}; + expect(aggregated).toEqual(expected); + }); + + test('it should aggregate with a single error when given a single error item', () => { + const singleError = sampleBulkError(); + const aggregated = errorAggregator(singleError, []); + const expected: BulkResponseErrorAggregation = { + 'Invalid call': { + count: 1, + statusCode: 400, + }, + }; + expect(aggregated).toEqual(expected); + }); + + test('it should aggregate two errors with a correct count when given the same two error items', () => { + const twoAggregatedErrors = sampleBulkError(); + const item1 = sampleBulkErrorItem(); + const item2 = sampleBulkErrorItem(); + twoAggregatedErrors.items = [item1, item2]; + const aggregated = errorAggregator(twoAggregatedErrors, []); + const expected: BulkResponseErrorAggregation = { + 'Invalid call': { + count: 2, + statusCode: 400, + }, + }; + expect(aggregated).toEqual(expected); + }); + + test('it should aggregate three errors with a correct count when given the same two error items', () => { + const twoAggregatedErrors = sampleBulkError(); + const item1 = sampleBulkErrorItem(); + const item2 = sampleBulkErrorItem(); + const item3 = sampleBulkErrorItem(); + twoAggregatedErrors.items = [item1, item2, item3]; + const aggregated = errorAggregator(twoAggregatedErrors, []); + const expected: BulkResponseErrorAggregation = { + 'Invalid call': { + count: 3, + statusCode: 400, + }, + }; + expect(aggregated).toEqual(expected); + }); + + test('it should aggregate two distinct errors with the correct count of 1 for each error type', () => { + const twoAggregatedErrors = sampleBulkError(); + const item1 = sampleBulkErrorItem({ status: 400, reason: 'Parse Error' }); + const item2 = sampleBulkErrorItem({ status: 500, reason: 'Bad Network' }); + twoAggregatedErrors.items = [item1, item2]; + const aggregated = errorAggregator(twoAggregatedErrors, []); + const expected: BulkResponseErrorAggregation = { + 'Parse Error': { + count: 1, + statusCode: 400, + }, + 'Bad Network': { + count: 1, + statusCode: 500, + }, + }; + expect(aggregated).toEqual(expected); + }); + + test('it should aggregate two of the same errors with the correct count of 2 for each error type', () => { + const twoAggregatedErrors = sampleBulkError(); + const item1 = sampleBulkErrorItem({ status: 400, reason: 'Parse Error' }); + const item2 = sampleBulkErrorItem({ status: 400, reason: 'Parse Error' }); + const item3 = sampleBulkErrorItem({ status: 500, reason: 'Bad Network' }); + const item4 = sampleBulkErrorItem({ status: 500, reason: 'Bad Network' }); + twoAggregatedErrors.items = [item1, item2, item3, item4]; + const aggregated = errorAggregator(twoAggregatedErrors, []); + const expected: BulkResponseErrorAggregation = { + 'Parse Error': { + count: 2, + statusCode: 400, + }, + 'Bad Network': { + count: 2, + statusCode: 500, + }, + }; + expect(aggregated).toEqual(expected); + }); + + test('it should aggregate three of the same errors with the correct count of 2 for each error type', () => { + const twoAggregatedErrors = sampleBulkError(); + const item1 = sampleBulkErrorItem({ status: 400, reason: 'Parse Error' }); + const item2 = sampleBulkErrorItem({ status: 400, reason: 'Parse Error' }); + const item3 = sampleBulkErrorItem({ status: 500, reason: 'Bad Network' }); + const item4 = sampleBulkErrorItem({ status: 500, reason: 'Bad Network' }); + const item5 = sampleBulkErrorItem({ status: 502, reason: 'Bad Gateway' }); + const item6 = sampleBulkErrorItem({ status: 502, reason: 'Bad Gateway' }); + twoAggregatedErrors.items = [item1, item2, item3, item4, item5, item6]; + const aggregated = errorAggregator(twoAggregatedErrors, []); + const expected: BulkResponseErrorAggregation = { + 'Parse Error': { + count: 2, + statusCode: 400, + }, + 'Bad Network': { + count: 2, + statusCode: 500, + }, + 'Bad Gateway': { + count: 2, + statusCode: 502, + }, + }; + expect(aggregated).toEqual(expected); + }); + + test('it should aggregate a mix of errors with the correct aggregate count of each', () => { + const twoAggregatedErrors = sampleBulkError(); + const item1 = sampleBulkErrorItem({ status: 400, reason: 'Parse Error' }); + const item2 = sampleBulkErrorItem({ status: 500, reason: 'Bad Network' }); + const item3 = sampleBulkErrorItem({ status: 500, reason: 'Bad Network' }); + const item4 = sampleBulkErrorItem({ status: 502, reason: 'Bad Gateway' }); + const item5 = sampleBulkErrorItem({ status: 502, reason: 'Bad Gateway' }); + const item6 = sampleBulkErrorItem({ status: 502, reason: 'Bad Gateway' }); + twoAggregatedErrors.items = [item1, item2, item3, item4, item5, item6]; + const aggregated = errorAggregator(twoAggregatedErrors, []); + const expected: BulkResponseErrorAggregation = { + 'Parse Error': { + count: 1, + statusCode: 400, + }, + 'Bad Network': { + count: 2, + statusCode: 500, + }, + 'Bad Gateway': { + count: 3, + statusCode: 502, + }, + }; + expect(aggregated).toEqual(expected); + }); + + test('it will ignore error single codes such as 409', () => { + const twoAggregatedErrors = sampleBulkError(); + const item1 = sampleBulkErrorItem({ status: 409, reason: 'Conflict Error' }); + const item2 = sampleBulkErrorItem({ status: 409, reason: 'Conflict Error' }); + const item3 = sampleBulkErrorItem({ status: 500, reason: 'Bad Network' }); + const item4 = sampleBulkErrorItem({ status: 502, reason: 'Bad Gateway' }); + const item5 = sampleBulkErrorItem({ status: 502, reason: 'Bad Gateway' }); + const item6 = sampleBulkErrorItem({ status: 502, reason: 'Bad Gateway' }); + twoAggregatedErrors.items = [item1, item2, item3, item4, item5, item6]; + const aggregated = errorAggregator(twoAggregatedErrors, [409]); + const expected: BulkResponseErrorAggregation = { + 'Bad Network': { + count: 1, + statusCode: 500, + }, + 'Bad Gateway': { + count: 3, + statusCode: 502, + }, + }; + expect(aggregated).toEqual(expected); + }); + + test('it will ignore two error codes such as 409 and 502', () => { + const twoAggregatedErrors = sampleBulkError(); + const item1 = sampleBulkErrorItem({ status: 409, reason: 'Conflict Error' }); + const item2 = sampleBulkErrorItem({ status: 409, reason: 'Conflict Error' }); + const item3 = sampleBulkErrorItem({ status: 500, reason: 'Bad Network' }); + const item4 = sampleBulkErrorItem({ status: 502, reason: 'Bad Gateway' }); + const item5 = sampleBulkErrorItem({ status: 502, reason: 'Bad Gateway' }); + const item6 = sampleBulkErrorItem({ status: 502, reason: 'Bad Gateway' }); + twoAggregatedErrors.items = [item1, item2, item3, item4, item5, item6]; + const aggregated = errorAggregator(twoAggregatedErrors, [409, 502]); + const expected: BulkResponseErrorAggregation = { + 'Bad Network': { + count: 1, + statusCode: 500, + }, + }; + expect(aggregated).toEqual(expected); + }); + + test('it will return an empty object given valid inputs and status codes to ignore', () => { + const bulkResponse = sampleBulkResponse(); + const aggregated = errorAggregator(bulkResponse, [409, 502]); + const expected: BulkResponseErrorAggregation = {}; + expect(aggregated).toEqual(expected); + }); + }); +}); diff --git a/x-pack/plugins/siem/server/lib/detection_engine/signals/utils.ts b/x-pack/plugins/siem/server/lib/detection_engine/signals/utils.ts new file mode 100644 index 0000000000000..f06c765073d78 --- /dev/null +++ b/x-pack/plugins/siem/server/lib/detection_engine/signals/utils.ts @@ -0,0 +1,137 @@ +/* + * 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 { createHash } from 'crypto'; +import moment from 'moment'; +import dateMath from '@elastic/datemath'; + +import { parseDuration } from '../../../../../alerting/server'; +import { BulkResponse, BulkResponseErrorAggregation } from './types'; + +export const generateId = ( + docIndex: string, + docId: string, + version: string, + ruleId: string +): string => + createHash('sha256') + .update(docIndex.concat(docId, version, ruleId)) + .digest('hex'); + +export const parseInterval = (intervalString: string): moment.Duration | null => { + try { + return moment.duration(parseDuration(intervalString)); + } catch (err) { + return null; + } +}; + +export const parseScheduleDates = (time: string): moment.Moment | null => { + const isValidDateString = !isNaN(Date.parse(time)); + const isValidInput = isValidDateString || time.trim().startsWith('now'); + const formattedDate = isValidDateString + ? moment(time) + : isValidInput + ? dateMath.parse(time) + : null; + + return formattedDate ?? null; +}; + +export const getDriftTolerance = ({ + from, + to, + interval, + now = moment(), +}: { + from: string; + to: string; + interval: moment.Duration; + now?: moment.Moment; +}): moment.Duration | null => { + const toDate = parseScheduleDates(to) ?? now; + const fromDate = parseScheduleDates(from) ?? dateMath.parse('now-6m'); + const timeSegment = toDate.diff(fromDate); + const duration = moment.duration(timeSegment); + + if (duration !== null) { + return duration.subtract(interval); + } else { + return null; + } +}; + +export const getGapBetweenRuns = ({ + previousStartedAt, + interval, + from, + to, + now = moment(), +}: { + previousStartedAt: Date | undefined | null; + interval: string; + from: string; + to: string; + now?: moment.Moment; +}): moment.Duration | null => { + if (previousStartedAt == null) { + return null; + } + const intervalDuration = parseInterval(interval); + if (intervalDuration == null) { + return null; + } + const driftTolerance = getDriftTolerance({ from, to, interval: intervalDuration }); + if (driftTolerance == null) { + return null; + } + const diff = moment.duration(now.diff(previousStartedAt)); + const drift = diff.subtract(intervalDuration); + return drift.subtract(driftTolerance); +}; + +export const makeFloatString = (num: number): string => Number(num).toFixed(2); + +/** + * Given a BulkResponse this will return an aggregation based on the errors if any exist + * from the BulkResponse. Errors are aggregated on the reason as the unique key. + * + * Example would be: + * { + * 'Parse Error': { + * count: 100, + * statusCode: 400, + * }, + * 'Internal server error': { + * count: 3, + * statusCode: 500, + * } + * } + * If this does not return any errors then you will get an empty object like so: {} + * @param response The bulk response to aggregate based on the error message + * @param ignoreStatusCodes Optional array of status codes to ignore when creating aggregate error messages + * @returns The aggregated example as shown above. + */ +export const errorAggregator = ( + response: BulkResponse, + ignoreStatusCodes: number[] +): BulkResponseErrorAggregation => { + return response.items.reduce((accum, item) => { + if (item.create.error != null && !ignoreStatusCodes.includes(item.create.status)) { + if (accum[item.create.error.reason] == null) { + accum[item.create.error.reason] = { + count: 1, + statusCode: item.create.status, + }; + } else { + accum[item.create.error.reason] = { + count: accum[item.create.error.reason].count + 1, + statusCode: item.create.status, + }; + } + } + return accum; + }, Object.create(null)); +}; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/tags/read_tags.test.ts b/x-pack/plugins/siem/server/lib/detection_engine/tags/read_tags.test.ts similarity index 99% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/tags/read_tags.test.ts rename to x-pack/plugins/siem/server/lib/detection_engine/tags/read_tags.test.ts index 80c107c991bb7..d29d885f9797a 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/tags/read_tags.test.ts +++ b/x-pack/plugins/siem/server/lib/detection_engine/tags/read_tags.test.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { alertsClientMock } from '../../../../../../../plugins/alerting/server/mocks'; +import { alertsClientMock } from '../../../../../alerting/server/mocks'; import { getResult, getFindResultWithMultiHits } from '../routes/__mocks__/request_responses'; import { INTERNAL_RULE_ID_KEY, INTERNAL_IDENTIFIER } from '../../../../common/constants'; import { readRawTags, readTags, convertTagsToSet, convertToTags, isTags } from './read_tags'; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/tags/read_tags.ts b/x-pack/plugins/siem/server/lib/detection_engine/tags/read_tags.ts similarity index 96% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/tags/read_tags.ts rename to x-pack/plugins/siem/server/lib/detection_engine/tags/read_tags.ts index d343bca8c97bb..addd373712850 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/tags/read_tags.ts +++ b/x-pack/plugins/siem/server/lib/detection_engine/tags/read_tags.ts @@ -6,7 +6,7 @@ import { has } from 'lodash/fp'; import { INTERNAL_IDENTIFIER } from '../../../../common/constants'; -import { AlertsClient } from '../../../../../../../plugins/alerting/server'; +import { AlertsClient } from '../../../../../alerting/server'; import { findRules } from '../rules/find_rules'; export interface TagType { diff --git a/x-pack/plugins/siem/server/lib/detection_engine/types.ts b/x-pack/plugins/siem/server/lib/detection_engine/types.ts new file mode 100644 index 0000000000000..f2026804da51a --- /dev/null +++ b/x-pack/plugins/siem/server/lib/detection_engine/types.ts @@ -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 { CallAPIOptions } from '../../../../../../src/core/server'; +import { Filter } from '../../../../../../src/plugins/data/server'; +import { IRuleStatusAttributes } from './rules/types'; +import { ListsDefaultArraySchema } from './routes/schemas/types/lists_default_array'; +import { RuleAlertAction, RuleType } from '../../../common/detection_engine/types'; + +export type PartialFilter = Partial; + +export interface IMitreAttack { + id: string; + name: string; + reference: string; +} + +export interface ThreatParams { + framework: string; + tactic: IMitreAttack; + technique: IMitreAttack[]; +} + +// Notice below we are using lists: ListsAndArraySchema[]; which is coming directly from the response output section. +// TODO: Eventually this whole RuleAlertParams will be replaced with io-ts. For now we can slowly strangle it out and reduce duplicate types +// We don't have the input types defined through io-ts just yet but as we being introducing types from there we will more and more remove +// types and share them between input and output schema but have an input Rule Schema and an output Rule Schema. + +export interface Meta { + [key: string]: {} | string | undefined | null; + kibana_siem_app_url?: string | undefined; +} + +export type Language = 'kuery' | 'lucene'; + +export interface RuleAlertParams { + actions: RuleAlertAction[]; + anomalyThreshold: number | undefined; + description: string; + note: string | undefined | null; + enabled: boolean; + falsePositives: string[]; + filters: PartialFilter[] | undefined | null; + from: string; + immutable: boolean; + index: string[] | undefined | null; + interval: string; + ruleId: string | undefined | null; + language: Language | undefined | null; + maxSignals: number; + machineLearningJobId: string | undefined; + riskScore: number; + outputIndex: string; + name: string; + query: string | undefined | null; + references: string[]; + savedId?: string | undefined | null; + meta: Meta | undefined | null; + severity: string; + tags: string[]; + to: string; + timelineId: string | undefined | null; + timelineTitle: string | undefined | null; + threat: ThreatParams[] | undefined | null; + type: RuleType; + version: number; + throttle: string | undefined | null; + exceptions_list: ListsDefaultArraySchema | null | undefined; +} + +export type RuleTypeParams = Omit< + RuleAlertParams, + 'name' | 'enabled' | 'interval' | 'tags' | 'actions' | 'throttle' +>; + +export type RuleAlertParamsRest = Omit< + RuleAlertParams, + | 'anomalyThreshold' + | 'ruleId' + | 'falsePositives' + | 'immutable' + | 'maxSignals' + | 'machineLearningJobId' + | 'savedId' + | 'riskScore' + | 'timelineId' + | 'timelineTitle' + | 'outputIndex' +> & + Omit< + IRuleStatusAttributes, + | 'status' + | 'alertId' + | 'statusDate' + | 'lastFailureAt' + | 'lastSuccessAt' + | 'lastSuccessMessage' + | 'lastFailureMessage' + > & { + anomaly_threshold: RuleAlertParams['anomalyThreshold']; + rule_id: RuleAlertParams['ruleId']; + false_positives: RuleAlertParams['falsePositives']; + saved_id?: RuleAlertParams['savedId']; + timeline_id: RuleAlertParams['timelineId']; + timeline_title: RuleAlertParams['timelineTitle']; + max_signals: RuleAlertParams['maxSignals']; + machine_learning_job_id: RuleAlertParams['machineLearningJobId']; + risk_score: RuleAlertParams['riskScore']; + output_index: RuleAlertParams['outputIndex']; + created_at: string; + updated_at: string; + status?: IRuleStatusAttributes['status'] | undefined; + status_date?: IRuleStatusAttributes['statusDate'] | undefined; + last_failure_at?: IRuleStatusAttributes['lastFailureAt'] | undefined; + last_success_at?: IRuleStatusAttributes['lastSuccessAt'] | undefined; + last_failure_message?: IRuleStatusAttributes['lastFailureMessage'] | undefined; + last_success_message?: IRuleStatusAttributes['lastSuccessMessage'] | undefined; + }; + +export type OutputRuleAlertRest = RuleAlertParamsRest & { + id: string; + created_by: string | undefined | null; + updated_by: string | undefined | null; + immutable: boolean; +}; + +export type ImportRuleAlertRest = Omit & { + id: string | undefined | null; + rule_id: string; + immutable: boolean; +}; + +export type PrepackagedRules = Omit< + RuleAlertParamsRest, + | 'status' + | 'status_date' + | 'last_failure_at' + | 'last_success_at' + | 'last_failure_message' + | 'last_success_message' + | 'updated_at' + | 'created_at' +> & { rule_id: string; immutable: boolean }; + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export type CallWithRequest, V> = ( + endpoint: string, + params: T, + options?: CallAPIOptions +) => Promise; + +export type RefreshTypes = false | 'wait_for'; diff --git a/x-pack/legacy/plugins/siem/server/lib/ecs_fields/extend_map.test.ts b/x-pack/plugins/siem/server/lib/ecs_fields/extend_map.test.ts similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/ecs_fields/extend_map.test.ts rename to x-pack/plugins/siem/server/lib/ecs_fields/extend_map.test.ts diff --git a/x-pack/legacy/plugins/siem/server/lib/ecs_fields/extend_map.ts b/x-pack/plugins/siem/server/lib/ecs_fields/extend_map.ts similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/ecs_fields/extend_map.ts rename to x-pack/plugins/siem/server/lib/ecs_fields/extend_map.ts diff --git a/x-pack/legacy/plugins/siem/server/lib/ecs_fields/index.ts b/x-pack/plugins/siem/server/lib/ecs_fields/index.ts similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/ecs_fields/index.ts rename to x-pack/plugins/siem/server/lib/ecs_fields/index.ts diff --git a/x-pack/legacy/plugins/siem/server/lib/events/elasticsearch_adapter.test.ts b/x-pack/plugins/siem/server/lib/events/elasticsearch_adapter.test.ts similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/events/elasticsearch_adapter.test.ts rename to x-pack/plugins/siem/server/lib/events/elasticsearch_adapter.test.ts diff --git a/x-pack/legacy/plugins/siem/server/lib/events/elasticsearch_adapter.ts b/x-pack/plugins/siem/server/lib/events/elasticsearch_adapter.ts similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/events/elasticsearch_adapter.ts rename to x-pack/plugins/siem/server/lib/events/elasticsearch_adapter.ts diff --git a/x-pack/legacy/plugins/siem/server/lib/events/index.ts b/x-pack/plugins/siem/server/lib/events/index.ts similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/events/index.ts rename to x-pack/plugins/siem/server/lib/events/index.ts diff --git a/x-pack/plugins/siem/server/lib/events/mock.ts b/x-pack/plugins/siem/server/lib/events/mock.ts new file mode 100644 index 0000000000000..f5fb2f481ca77 --- /dev/null +++ b/x-pack/plugins/siem/server/lib/events/mock.ts @@ -0,0 +1,3411 @@ +/* + * 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 { cloneDeep } from 'lodash/fp'; +import { DEFAULT_INDEX_PATTERN } from '../../../common/constants'; +import { RequestDetailsOptions } from './types'; + +export const mockResponseSearchTimelineDetails = { + took: 5, + timed_out: false, + _shards: { + total: 1, + successful: 1, + skipped: 0, + failed: 0, + }, + hits: { + total: { + value: 1, + relation: 'eq', + }, + max_score: 1, + hits: [ + { + _index: 'auditbeat-8.0.0-2019.03.29-000003', + _type: '_doc', + _id: 'TUfUymkBCQofM5eXGBYL', + _score: 1, + _source: { + '@timestamp': '2019-03-29T19:01:23.420Z', + service: { + type: 'auditd', + }, + user: { + audit: { + id: 'unset', + }, + group: { + id: '0', + name: 'root', + }, + effective: { + group: { + id: '0', + name: 'root', + }, + id: '0', + name: 'root', + }, + filesystem: { + group: { + name: 'root', + id: '0', + }, + name: 'root', + id: '0', + }, + saved: { + group: { + id: '0', + name: 'root', + }, + id: '0', + name: 'root', + }, + id: '0', + name: 'root', + }, + process: { + executable: '/root/go/src/github.com/elastic/beats/x-pack/auditbeat/auditbeat', + working_directory: '/root/go/src/github.com/elastic/beats/x-pack/auditbeat', + pid: 15990, + ppid: 1, + title: + '/root/go/src/github.com/elastic/beats/x-pack/auditbeat/auditbeat -e -c /root/go/src/github.com/elastic/beats/x-pack/auditbeat/au', + name: 'auditbeat', + }, + host: { + architecture: 'x86_64', + os: { + name: 'Ubuntu', + kernel: '4.15.0-45-generic', + codename: 'bionic', + platform: 'ubuntu', + version: '18.04.2 LTS (Bionic Beaver)', + family: 'debian', + }, + id: '7c21f5ed03b04d0299569d221fe18bbc', + containerized: false, + name: 'zeek-london', + ip: ['46.101.3.136', '10.16.0.5', 'fe80::4066:42ff:fe19:b3b9'], + mac: ['42:66:42:19:b3:b9'], + hostname: 'zeek-london', + }, + cloud: { + provider: 'digitalocean', + instance: { + id: '136398786', + }, + region: 'lon1', + }, + file: { + device: '00:00', + inode: '3926', + mode: '0644', + uid: '0', + gid: '0', + owner: 'root', + group: 'root', + path: '/etc/passwd', + }, + auditd: { + session: 'unset', + data: { + tty: '(none)', + a3: '0', + a2: '80000', + syscall: 'openat', + a1: '7fe0f63df220', + a0: 'ffffff9c', + arch: 'x86_64', + exit: '12', + }, + summary: { + actor: { + primary: 'unset', + secondary: 'root', + }, + object: { + primary: '/etc/passwd', + type: 'file', + }, + how: '/root/go/src/github.com/elastic/beats/x-pack/auditbeat/auditbeat', + }, + paths: [ + { + rdev: '00:00', + cap_fe: '0', + nametype: 'NORMAL', + ogid: '0', + ouid: '0', + inode: '3926', + item: '0', + mode: '0100644', + name: '/etc/passwd', + cap_fi: '0000000000000000', + cap_fp: '0000000000000000', + cap_fver: '0', + dev: 'fc:01', + }, + ], + message_type: 'syscall', + sequence: 8817905, + result: 'success', + }, + event: { + category: 'audit-rule', + action: 'opened-file', + original: [ + 'type=SYSCALL msg=audit(1553886083.420:8817905): arch=c000003e syscall=257 success=yes exit=12 a0=ffffff9c a1=7fe0f63df220 a2=80000 a3=0 items=1 ppid=1 pid=15990 auid=4294967295 uid=0 gid=0 euid=0 suid=0 fsuid=0 egid=0 sgid=0 fsgid=0 tty=(none) ses=4294967295 comm="auditbeat" exe="/root/go/src/github.com/elastic/beats/x-pack/auditbeat/auditbeat" key=(null)', + 'type=CWD msg=audit(1553886083.420:8817905): cwd="/root/go/src/github.com/elastic/beats/x-pack/auditbeat"', + 'type=PATH msg=audit(1553886083.420:8817905): item=0 name="/etc/passwd" inode=3926 dev=fc:01 mode=0100644 ouid=0 ogid=0 rdev=00:00 nametype=NORMAL cap_fp=0000000000000000 cap_fi=0000000000000000 cap_fe=0 cap_fver=0', + 'type=PROCTITLE msg=audit(1553886083.420:8817905): proctitle=2F726F6F742F676F2F7372632F6769746875622E636F6D2F656C61737469632F62656174732F782D7061636B2F6175646974626561742F617564697462656174002D65002D63002F726F6F742F676F2F7372632F6769746875622E636F6D2F656C61737469632F62656174732F782D7061636B2F6175646974626561742F6175', + ], + module: 'auditd', + }, + ecs: { + version: '1.0.0', + }, + agent: { + ephemeral_id: '6d541d59-52d0-4e70-b4d2-2660c0a99ff7', + hostname: 'zeek-london', + id: 'cc1f4183-36c6-45c4-b21b-7ce70c3572db', + version: '8.0.0', + type: 'auditbeat', + }, + }, + }, + ], + }, +}; +export const mockOptions: RequestDetailsOptions = { + indexName: 'auditbeat-8.0.0-2019.03.29-000003', + eventId: 'TUfUymkBCQofM5eXGBYL', + defaultIndex: DEFAULT_INDEX_PATTERN, +}; + +export const mockRequest = { + body: { + operationName: 'GetNetworkTopNFlowQuery', + variables: { + indexName: 'auditbeat-8.0.0-2019.03.29-000003', + eventId: 'TUfUymkBCQofM5eXGBYL', + }, + query: `query GetTimelineDetailsQuery($eventId: String!, $indexName: String!) { + source(id: "default") { + TimelineDetails(eventId: $eventId, indexName: $indexName) { + data { + category + description + example + field + type + values + originalValue + } + } + } + }`, + }, +}; + +export const mockResponseMap = { + 'auditbeat-8.0.0-2019.03.29-000003': { + mappings: { + _meta: { + beat: 'auditbeat', + version: '8.0.0', + }, + dynamic_templates: [ + { + 'container.labels': { + path_match: 'container.labels.*', + match_mapping_type: 'string', + mapping: { + type: 'keyword', + }, + }, + }, + { + fields: { + path_match: 'fields.*', + match_mapping_type: 'string', + mapping: { + type: 'keyword', + }, + }, + }, + { + 'docker.container.labels': { + path_match: 'docker.container.labels.*', + match_mapping_type: 'string', + mapping: { + type: 'keyword', + }, + }, + }, + { + strings_as_keyword: { + match_mapping_type: 'string', + mapping: { + ignore_above: 1024, + type: 'keyword', + }, + }, + }, + ], + date_detection: false, + properties: { + '@timestamp': { + type: 'date', + }, + agent: { + properties: { + ephemeral_id: { + type: 'keyword', + ignore_above: 1024, + }, + hostname: { + type: 'keyword', + ignore_above: 1024, + }, + id: { + type: 'keyword', + ignore_above: 1024, + }, + name: { + type: 'keyword', + ignore_above: 1024, + }, + type: { + type: 'keyword', + ignore_above: 1024, + }, + version: { + type: 'keyword', + ignore_above: 1024, + }, + }, + }, + auditd: { + properties: { + data: { + properties: { + a0: { + type: 'keyword', + ignore_above: 1024, + }, + a1: { + type: 'keyword', + ignore_above: 1024, + }, + a2: { + type: 'keyword', + ignore_above: 1024, + }, + a3: { + type: 'keyword', + ignore_above: 1024, + }, + 'a[0-3]': { + type: 'keyword', + ignore_above: 1024, + }, + acct: { + type: 'keyword', + ignore_above: 1024, + }, + acl: { + type: 'keyword', + ignore_above: 1024, + }, + action: { + type: 'keyword', + ignore_above: 1024, + }, + added: { + type: 'keyword', + ignore_above: 1024, + }, + addr: { + type: 'keyword', + ignore_above: 1024, + }, + apparmor: { + type: 'keyword', + ignore_above: 1024, + }, + arch: { + type: 'keyword', + ignore_above: 1024, + }, + argc: { + type: 'keyword', + ignore_above: 1024, + }, + audit_backlog_limit: { + type: 'keyword', + ignore_above: 1024, + }, + audit_backlog_wait_time: { + type: 'keyword', + ignore_above: 1024, + }, + audit_enabled: { + type: 'keyword', + ignore_above: 1024, + }, + audit_failure: { + type: 'keyword', + ignore_above: 1024, + }, + banners: { + type: 'keyword', + ignore_above: 1024, + }, + bool: { + type: 'keyword', + ignore_above: 1024, + }, + bus: { + type: 'keyword', + ignore_above: 1024, + }, + cap_fe: { + type: 'keyword', + ignore_above: 1024, + }, + cap_fi: { + type: 'keyword', + ignore_above: 1024, + }, + cap_fp: { + type: 'keyword', + ignore_above: 1024, + }, + cap_fver: { + type: 'keyword', + ignore_above: 1024, + }, + cap_pe: { + type: 'keyword', + ignore_above: 1024, + }, + cap_pi: { + type: 'keyword', + ignore_above: 1024, + }, + cap_pp: { + type: 'keyword', + ignore_above: 1024, + }, + capability: { + type: 'keyword', + ignore_above: 1024, + }, + cgroup: { + type: 'keyword', + ignore_above: 1024, + }, + changed: { + type: 'keyword', + ignore_above: 1024, + }, + cipher: { + type: 'keyword', + ignore_above: 1024, + }, + class: { + type: 'keyword', + ignore_above: 1024, + }, + cmd: { + type: 'keyword', + ignore_above: 1024, + }, + code: { + type: 'keyword', + ignore_above: 1024, + }, + compat: { + type: 'keyword', + ignore_above: 1024, + }, + daddr: { + type: 'keyword', + ignore_above: 1024, + }, + data: { + type: 'keyword', + ignore_above: 1024, + }, + 'default-context': { + type: 'keyword', + ignore_above: 1024, + }, + dev: { + type: 'keyword', + ignore_above: 1024, + }, + device: { + type: 'keyword', + ignore_above: 1024, + }, + dir: { + type: 'keyword', + ignore_above: 1024, + }, + direction: { + type: 'keyword', + ignore_above: 1024, + }, + dmac: { + type: 'keyword', + ignore_above: 1024, + }, + dport: { + type: 'keyword', + ignore_above: 1024, + }, + enforcing: { + type: 'keyword', + ignore_above: 1024, + }, + entries: { + type: 'keyword', + ignore_above: 1024, + }, + exit: { + type: 'keyword', + ignore_above: 1024, + }, + fam: { + type: 'keyword', + ignore_above: 1024, + }, + family: { + type: 'keyword', + ignore_above: 1024, + }, + fd: { + type: 'keyword', + ignore_above: 1024, + }, + fe: { + type: 'keyword', + ignore_above: 1024, + }, + feature: { + type: 'keyword', + ignore_above: 1024, + }, + fi: { + type: 'keyword', + ignore_above: 1024, + }, + file: { + type: 'keyword', + ignore_above: 1024, + }, + flags: { + type: 'keyword', + ignore_above: 1024, + }, + format: { + type: 'keyword', + ignore_above: 1024, + }, + fp: { + type: 'keyword', + ignore_above: 1024, + }, + fver: { + type: 'keyword', + ignore_above: 1024, + }, + grantors: { + type: 'keyword', + ignore_above: 1024, + }, + grp: { + type: 'keyword', + ignore_above: 1024, + }, + hook: { + type: 'keyword', + ignore_above: 1024, + }, + hostname: { + type: 'keyword', + ignore_above: 1024, + }, + icmp_type: { + type: 'keyword', + ignore_above: 1024, + }, + id: { + type: 'keyword', + ignore_above: 1024, + }, + igid: { + type: 'keyword', + ignore_above: 1024, + }, + 'img-ctx': { + type: 'keyword', + ignore_above: 1024, + }, + inif: { + type: 'keyword', + ignore_above: 1024, + }, + ino: { + type: 'keyword', + ignore_above: 1024, + }, + inode: { + type: 'keyword', + ignore_above: 1024, + }, + inode_gid: { + type: 'keyword', + ignore_above: 1024, + }, + inode_uid: { + type: 'keyword', + ignore_above: 1024, + }, + invalid_context: { + type: 'keyword', + ignore_above: 1024, + }, + ioctlcmd: { + type: 'keyword', + ignore_above: 1024, + }, + ip: { + type: 'keyword', + ignore_above: 1024, + }, + ipid: { + type: 'keyword', + ignore_above: 1024, + }, + 'ipx-net': { + type: 'keyword', + ignore_above: 1024, + }, + item: { + type: 'keyword', + ignore_above: 1024, + }, + items: { + type: 'keyword', + ignore_above: 1024, + }, + iuid: { + type: 'keyword', + ignore_above: 1024, + }, + kernel: { + type: 'keyword', + ignore_above: 1024, + }, + kind: { + type: 'keyword', + ignore_above: 1024, + }, + ksize: { + type: 'keyword', + ignore_above: 1024, + }, + laddr: { + type: 'keyword', + ignore_above: 1024, + }, + len: { + type: 'keyword', + ignore_above: 1024, + }, + list: { + type: 'keyword', + ignore_above: 1024, + }, + lport: { + type: 'keyword', + ignore_above: 1024, + }, + mac: { + type: 'keyword', + ignore_above: 1024, + }, + macproto: { + type: 'keyword', + ignore_above: 1024, + }, + maj: { + type: 'keyword', + ignore_above: 1024, + }, + major: { + type: 'keyword', + ignore_above: 1024, + }, + minor: { + type: 'keyword', + ignore_above: 1024, + }, + mode: { + type: 'keyword', + ignore_above: 1024, + }, + model: { + type: 'keyword', + ignore_above: 1024, + }, + msg: { + type: 'keyword', + ignore_above: 1024, + }, + name: { + type: 'keyword', + ignore_above: 1024, + }, + nametype: { + type: 'keyword', + ignore_above: 1024, + }, + nargs: { + type: 'keyword', + ignore_above: 1024, + }, + net: { + type: 'keyword', + ignore_above: 1024, + }, + new: { + type: 'keyword', + ignore_above: 1024, + }, + 'new-chardev': { + type: 'keyword', + ignore_above: 1024, + }, + 'new-disk': { + type: 'keyword', + ignore_above: 1024, + }, + 'new-enabled': { + type: 'keyword', + ignore_above: 1024, + }, + 'new-fs': { + type: 'keyword', + ignore_above: 1024, + }, + 'new-level': { + type: 'keyword', + ignore_above: 1024, + }, + 'new-log_passwd': { + type: 'keyword', + ignore_above: 1024, + }, + 'new-mem': { + type: 'keyword', + ignore_above: 1024, + }, + 'new-net': { + type: 'keyword', + ignore_above: 1024, + }, + 'new-range': { + type: 'keyword', + ignore_above: 1024, + }, + 'new-rng': { + type: 'keyword', + ignore_above: 1024, + }, + 'new-role': { + type: 'keyword', + ignore_above: 1024, + }, + 'new-seuser': { + type: 'keyword', + ignore_above: 1024, + }, + 'new-vcpu': { + type: 'keyword', + ignore_above: 1024, + }, + new_gid: { + type: 'keyword', + ignore_above: 1024, + }, + new_lock: { + type: 'keyword', + ignore_above: 1024, + }, + new_pe: { + type: 'keyword', + ignore_above: 1024, + }, + new_pi: { + type: 'keyword', + ignore_above: 1024, + }, + new_pp: { + type: 'keyword', + ignore_above: 1024, + }, + 'nlnk-fam': { + type: 'keyword', + ignore_above: 1024, + }, + 'nlnk-grp': { + type: 'keyword', + ignore_above: 1024, + }, + 'nlnk-pid': { + type: 'keyword', + ignore_above: 1024, + }, + oauid: { + type: 'keyword', + ignore_above: 1024, + }, + obj: { + type: 'keyword', + ignore_above: 1024, + }, + obj_gid: { + type: 'keyword', + ignore_above: 1024, + }, + obj_uid: { + type: 'keyword', + ignore_above: 1024, + }, + ocomm: { + type: 'keyword', + ignore_above: 1024, + }, + oflag: { + type: 'keyword', + ignore_above: 1024, + }, + old: { + type: 'keyword', + ignore_above: 1024, + }, + 'old-auid': { + type: 'keyword', + ignore_above: 1024, + }, + 'old-chardev': { + type: 'keyword', + ignore_above: 1024, + }, + 'old-disk': { + type: 'keyword', + ignore_above: 1024, + }, + 'old-enabled': { + type: 'keyword', + ignore_above: 1024, + }, + 'old-fs': { + type: 'keyword', + ignore_above: 1024, + }, + 'old-level': { + type: 'keyword', + ignore_above: 1024, + }, + 'old-log_passwd': { + type: 'keyword', + ignore_above: 1024, + }, + 'old-mem': { + type: 'keyword', + ignore_above: 1024, + }, + 'old-net': { + type: 'keyword', + ignore_above: 1024, + }, + 'old-range': { + type: 'keyword', + ignore_above: 1024, + }, + 'old-rng': { + type: 'keyword', + ignore_above: 1024, + }, + 'old-role': { + type: 'keyword', + ignore_above: 1024, + }, + 'old-ses': { + type: 'keyword', + ignore_above: 1024, + }, + 'old-seuser': { + type: 'keyword', + ignore_above: 1024, + }, + 'old-vcpu': { + type: 'keyword', + ignore_above: 1024, + }, + old_enforcing: { + type: 'keyword', + ignore_above: 1024, + }, + old_lock: { + type: 'keyword', + ignore_above: 1024, + }, + old_pe: { + type: 'keyword', + ignore_above: 1024, + }, + old_pi: { + type: 'keyword', + ignore_above: 1024, + }, + old_pp: { + type: 'keyword', + ignore_above: 1024, + }, + old_prom: { + type: 'keyword', + ignore_above: 1024, + }, + old_val: { + type: 'keyword', + ignore_above: 1024, + }, + op: { + type: 'keyword', + ignore_above: 1024, + }, + opid: { + type: 'keyword', + ignore_above: 1024, + }, + oses: { + type: 'keyword', + ignore_above: 1024, + }, + outif: { + type: 'keyword', + ignore_above: 1024, + }, + parent: { + type: 'keyword', + ignore_above: 1024, + }, + path: { + type: 'keyword', + ignore_above: 1024, + }, + per: { + type: 'keyword', + ignore_above: 1024, + }, + perm: { + type: 'keyword', + ignore_above: 1024, + }, + perm_mask: { + type: 'keyword', + ignore_above: 1024, + }, + permissive: { + type: 'keyword', + ignore_above: 1024, + }, + pfs: { + type: 'keyword', + ignore_above: 1024, + }, + printer: { + type: 'keyword', + ignore_above: 1024, + }, + prom: { + type: 'keyword', + ignore_above: 1024, + }, + proto: { + type: 'keyword', + ignore_above: 1024, + }, + qbytes: { + type: 'keyword', + ignore_above: 1024, + }, + range: { + type: 'keyword', + ignore_above: 1024, + }, + rdev: { + type: 'keyword', + ignore_above: 1024, + }, + reason: { + type: 'keyword', + ignore_above: 1024, + }, + removed: { + type: 'keyword', + ignore_above: 1024, + }, + res: { + type: 'keyword', + ignore_above: 1024, + }, + resrc: { + type: 'keyword', + ignore_above: 1024, + }, + rport: { + type: 'keyword', + ignore_above: 1024, + }, + sauid: { + type: 'keyword', + ignore_above: 1024, + }, + scontext: { + type: 'keyword', + ignore_above: 1024, + }, + 'selected-context': { + type: 'keyword', + ignore_above: 1024, + }, + seperm: { + type: 'keyword', + ignore_above: 1024, + }, + seperms: { + type: 'keyword', + ignore_above: 1024, + }, + seqno: { + type: 'keyword', + ignore_above: 1024, + }, + seresult: { + type: 'keyword', + ignore_above: 1024, + }, + ses: { + type: 'keyword', + ignore_above: 1024, + }, + seuser: { + type: 'keyword', + ignore_above: 1024, + }, + sig: { + type: 'keyword', + ignore_above: 1024, + }, + sigev_signo: { + type: 'keyword', + ignore_above: 1024, + }, + smac: { + type: 'keyword', + ignore_above: 1024, + }, + socket: { + properties: { + addr: { + type: 'keyword', + ignore_above: 1024, + }, + family: { + type: 'keyword', + ignore_above: 1024, + }, + path: { + type: 'keyword', + ignore_above: 1024, + }, + port: { + type: 'keyword', + ignore_above: 1024, + }, + saddr: { + type: 'keyword', + ignore_above: 1024, + }, + }, + }, + spid: { + type: 'keyword', + ignore_above: 1024, + }, + sport: { + type: 'keyword', + ignore_above: 1024, + }, + state: { + type: 'keyword', + ignore_above: 1024, + }, + subj: { + type: 'keyword', + ignore_above: 1024, + }, + success: { + type: 'keyword', + ignore_above: 1024, + }, + syscall: { + type: 'keyword', + ignore_above: 1024, + }, + table: { + type: 'keyword', + ignore_above: 1024, + }, + tclass: { + type: 'keyword', + ignore_above: 1024, + }, + tcontext: { + type: 'keyword', + ignore_above: 1024, + }, + terminal: { + type: 'keyword', + ignore_above: 1024, + }, + tty: { + type: 'keyword', + ignore_above: 1024, + }, + unit: { + type: 'keyword', + ignore_above: 1024, + }, + uri: { + type: 'keyword', + ignore_above: 1024, + }, + uuid: { + type: 'keyword', + ignore_above: 1024, + }, + val: { + type: 'keyword', + ignore_above: 1024, + }, + ver: { + type: 'keyword', + ignore_above: 1024, + }, + virt: { + type: 'keyword', + ignore_above: 1024, + }, + vm: { + type: 'keyword', + ignore_above: 1024, + }, + 'vm-ctx': { + type: 'keyword', + ignore_above: 1024, + }, + 'vm-pid': { + type: 'keyword', + ignore_above: 1024, + }, + watch: { + type: 'keyword', + ignore_above: 1024, + }, + }, + }, + message_type: { + type: 'keyword', + ignore_above: 1024, + }, + paths: { + properties: { + cap_fe: { + type: 'keyword', + ignore_above: 1024, + }, + cap_fi: { + type: 'keyword', + ignore_above: 1024, + }, + cap_fp: { + type: 'keyword', + ignore_above: 1024, + }, + cap_fver: { + type: 'keyword', + ignore_above: 1024, + }, + dev: { + type: 'keyword', + ignore_above: 1024, + }, + inode: { + type: 'keyword', + ignore_above: 1024, + }, + item: { + type: 'keyword', + ignore_above: 1024, + }, + mode: { + type: 'keyword', + ignore_above: 1024, + }, + name: { + type: 'keyword', + ignore_above: 1024, + }, + nametype: { + type: 'keyword', + ignore_above: 1024, + }, + obj_domain: { + type: 'keyword', + ignore_above: 1024, + }, + obj_level: { + type: 'keyword', + ignore_above: 1024, + }, + obj_role: { + type: 'keyword', + ignore_above: 1024, + }, + obj_user: { + type: 'keyword', + ignore_above: 1024, + }, + objtype: { + type: 'keyword', + ignore_above: 1024, + }, + ogid: { + type: 'keyword', + ignore_above: 1024, + }, + ouid: { + type: 'keyword', + ignore_above: 1024, + }, + rdev: { + type: 'keyword', + ignore_above: 1024, + }, + }, + }, + result: { + type: 'keyword', + ignore_above: 1024, + }, + sequence: { + type: 'long', + }, + session: { + type: 'keyword', + ignore_above: 1024, + }, + summary: { + properties: { + actor: { + properties: { + primary: { + type: 'keyword', + ignore_above: 1024, + }, + secondary: { + type: 'keyword', + ignore_above: 1024, + }, + }, + }, + how: { + type: 'keyword', + ignore_above: 1024, + }, + object: { + properties: { + primary: { + type: 'keyword', + ignore_above: 1024, + }, + secondary: { + type: 'keyword', + ignore_above: 1024, + }, + type: { + type: 'keyword', + ignore_above: 1024, + }, + }, + }, + }, + }, + }, + }, + client: { + properties: { + address: { + type: 'keyword', + ignore_above: 1024, + }, + bytes: { + type: 'long', + }, + domain: { + type: 'keyword', + ignore_above: 1024, + }, + geo: { + properties: { + city_name: { + type: 'keyword', + ignore_above: 1024, + }, + continent_name: { + type: 'keyword', + ignore_above: 1024, + }, + country_iso_code: { + type: 'keyword', + ignore_above: 1024, + }, + country_name: { + type: 'keyword', + ignore_above: 1024, + }, + location: { + type: 'geo_point', + }, + name: { + type: 'keyword', + ignore_above: 1024, + }, + region_iso_code: { + type: 'keyword', + ignore_above: 1024, + }, + region_name: { + type: 'keyword', + ignore_above: 1024, + }, + }, + }, + ip: { + type: 'ip', + }, + mac: { + type: 'keyword', + ignore_above: 1024, + }, + packets: { + type: 'long', + }, + port: { + type: 'long', + }, + }, + }, + cloud: { + properties: { + account: { + properties: { + id: { + type: 'keyword', + ignore_above: 1024, + }, + }, + }, + availability_zone: { + type: 'keyword', + ignore_above: 1024, + }, + instance: { + properties: { + id: { + type: 'keyword', + ignore_above: 1024, + }, + name: { + type: 'keyword', + ignore_above: 1024, + }, + }, + }, + machine: { + properties: { + type: { + type: 'keyword', + ignore_above: 1024, + }, + }, + }, + project: { + properties: { + id: { + type: 'keyword', + ignore_above: 1024, + }, + }, + }, + provider: { + type: 'keyword', + ignore_above: 1024, + }, + region: { + type: 'keyword', + ignore_above: 1024, + }, + }, + }, + container: { + properties: { + id: { + type: 'keyword', + ignore_above: 1024, + }, + image: { + properties: { + name: { + type: 'keyword', + ignore_above: 1024, + }, + tag: { + type: 'keyword', + ignore_above: 1024, + }, + }, + }, + labels: { + type: 'object', + }, + name: { + type: 'keyword', + ignore_above: 1024, + }, + runtime: { + type: 'keyword', + ignore_above: 1024, + }, + }, + }, + destination: { + properties: { + address: { + type: 'keyword', + ignore_above: 1024, + }, + bytes: { + type: 'long', + }, + domain: { + type: 'keyword', + ignore_above: 1024, + }, + geo: { + properties: { + city_name: { + type: 'keyword', + ignore_above: 1024, + }, + continent_name: { + type: 'keyword', + ignore_above: 1024, + }, + country_iso_code: { + type: 'keyword', + ignore_above: 1024, + }, + country_name: { + type: 'keyword', + ignore_above: 1024, + }, + location: { + type: 'geo_point', + }, + name: { + type: 'keyword', + ignore_above: 1024, + }, + region_iso_code: { + type: 'keyword', + ignore_above: 1024, + }, + region_name: { + type: 'keyword', + ignore_above: 1024, + }, + }, + }, + ip: { + type: 'ip', + }, + mac: { + type: 'keyword', + ignore_above: 1024, + }, + packets: { + type: 'long', + }, + path: { + type: 'keyword', + ignore_above: 1024, + }, + port: { + type: 'long', + }, + }, + }, + docker: { + properties: { + container: { + properties: { + labels: { + type: 'object', + }, + }, + }, + }, + }, + ecs: { + properties: { + version: { + type: 'keyword', + ignore_above: 1024, + }, + }, + }, + error: { + properties: { + code: { + type: 'keyword', + ignore_above: 1024, + }, + id: { + type: 'keyword', + ignore_above: 1024, + }, + message: { + type: 'text', + norms: false, + }, + type: { + type: 'keyword', + ignore_above: 1024, + }, + }, + }, + event: { + properties: { + action: { + type: 'keyword', + ignore_above: 1024, + }, + category: { + type: 'keyword', + ignore_above: 1024, + }, + created: { + type: 'date', + }, + dataset: { + type: 'keyword', + ignore_above: 1024, + }, + duration: { + type: 'long', + }, + end: { + type: 'date', + }, + hash: { + type: 'keyword', + ignore_above: 1024, + }, + id: { + type: 'keyword', + ignore_above: 1024, + }, + kind: { + type: 'keyword', + ignore_above: 1024, + }, + module: { + type: 'keyword', + ignore_above: 1024, + }, + origin: { + type: 'keyword', + ignore_above: 1024, + }, + original: { + type: 'keyword', + index: false, + doc_values: false, + ignore_above: 1024, + }, + outcome: { + type: 'keyword', + ignore_above: 1024, + }, + risk_score: { + type: 'float', + }, + risk_score_norm: { + type: 'float', + }, + severity: { + type: 'long', + }, + start: { + type: 'date', + }, + timezone: { + type: 'keyword', + ignore_above: 1024, + }, + type: { + type: 'keyword', + ignore_above: 1024, + }, + }, + }, + fields: { + type: 'object', + }, + file: { + properties: { + ctime: { + type: 'date', + }, + device: { + type: 'keyword', + ignore_above: 1024, + }, + extension: { + type: 'keyword', + ignore_above: 1024, + }, + gid: { + type: 'keyword', + ignore_above: 1024, + }, + group: { + type: 'keyword', + ignore_above: 1024, + }, + inode: { + type: 'keyword', + ignore_above: 1024, + }, + mode: { + type: 'keyword', + ignore_above: 1024, + }, + mtime: { + type: 'date', + }, + origin: { + type: 'keyword', + fields: { + raw: { + type: 'keyword', + ignore_above: 1024, + }, + }, + ignore_above: 1024, + }, + owner: { + type: 'keyword', + ignore_above: 1024, + }, + path: { + type: 'keyword', + ignore_above: 1024, + }, + selinux: { + properties: { + domain: { + type: 'keyword', + ignore_above: 1024, + }, + level: { + type: 'keyword', + ignore_above: 1024, + }, + role: { + type: 'keyword', + ignore_above: 1024, + }, + user: { + type: 'keyword', + ignore_above: 1024, + }, + }, + }, + setgid: { + type: 'boolean', + }, + setuid: { + type: 'boolean', + }, + size: { + type: 'long', + }, + target_path: { + type: 'keyword', + ignore_above: 1024, + }, + type: { + type: 'keyword', + ignore_above: 1024, + }, + uid: { + type: 'keyword', + ignore_above: 1024, + }, + }, + }, + geoip: { + properties: { + city_name: { + type: 'keyword', + ignore_above: 1024, + }, + continent_name: { + type: 'keyword', + ignore_above: 1024, + }, + country_iso_code: { + type: 'keyword', + ignore_above: 1024, + }, + location: { + type: 'geo_point', + }, + region_name: { + type: 'keyword', + ignore_above: 1024, + }, + }, + }, + group: { + properties: { + id: { + type: 'keyword', + ignore_above: 1024, + }, + name: { + type: 'keyword', + ignore_above: 1024, + }, + }, + }, + hash: { + properties: { + blake2b_256: { + type: 'keyword', + ignore_above: 1024, + }, + blake2b_384: { + type: 'keyword', + ignore_above: 1024, + }, + blake2b_512: { + type: 'keyword', + ignore_above: 1024, + }, + md5: { + type: 'keyword', + ignore_above: 1024, + }, + sha1: { + type: 'keyword', + ignore_above: 1024, + }, + sha224: { + type: 'keyword', + ignore_above: 1024, + }, + sha256: { + type: 'keyword', + ignore_above: 1024, + }, + sha384: { + type: 'keyword', + ignore_above: 1024, + }, + sha3_224: { + type: 'keyword', + ignore_above: 1024, + }, + sha3_256: { + type: 'keyword', + ignore_above: 1024, + }, + sha3_384: { + type: 'keyword', + ignore_above: 1024, + }, + sha3_512: { + type: 'keyword', + ignore_above: 1024, + }, + sha512: { + type: 'keyword', + ignore_above: 1024, + }, + sha512_224: { + type: 'keyword', + ignore_above: 1024, + }, + sha512_256: { + type: 'keyword', + ignore_above: 1024, + }, + xxh64: { + type: 'keyword', + ignore_above: 1024, + }, + }, + }, + host: { + properties: { + architecture: { + type: 'keyword', + ignore_above: 1024, + }, + containerized: { + type: 'boolean', + }, + geo: { + properties: { + city_name: { + type: 'keyword', + ignore_above: 1024, + }, + continent_name: { + type: 'keyword', + ignore_above: 1024, + }, + country_iso_code: { + type: 'keyword', + ignore_above: 1024, + }, + country_name: { + type: 'keyword', + ignore_above: 1024, + }, + location: { + type: 'geo_point', + }, + name: { + type: 'keyword', + ignore_above: 1024, + }, + region_iso_code: { + type: 'keyword', + ignore_above: 1024, + }, + region_name: { + type: 'keyword', + ignore_above: 1024, + }, + }, + }, + hostname: { + type: 'keyword', + ignore_above: 1024, + }, + id: { + type: 'keyword', + ignore_above: 1024, + }, + ip: { + type: 'ip', + }, + mac: { + type: 'keyword', + ignore_above: 1024, + }, + name: { + type: 'keyword', + ignore_above: 1024, + }, + os: { + properties: { + codename: { + type: 'keyword', + ignore_above: 1024, + }, + family: { + type: 'keyword', + ignore_above: 1024, + }, + full: { + type: 'keyword', + ignore_above: 1024, + }, + kernel: { + type: 'keyword', + ignore_above: 1024, + }, + name: { + type: 'keyword', + ignore_above: 1024, + }, + platform: { + type: 'keyword', + ignore_above: 1024, + }, + version: { + type: 'keyword', + ignore_above: 1024, + }, + }, + }, + type: { + type: 'keyword', + ignore_above: 1024, + }, + }, + }, + http: { + properties: { + request: { + properties: { + body: { + properties: { + bytes: { + type: 'long', + }, + content: { + type: 'keyword', + ignore_above: 1024, + }, + }, + }, + bytes: { + type: 'long', + }, + method: { + type: 'keyword', + ignore_above: 1024, + }, + referrer: { + type: 'keyword', + ignore_above: 1024, + }, + }, + }, + response: { + properties: { + body: { + properties: { + bytes: { + type: 'long', + }, + content: { + type: 'keyword', + ignore_above: 1024, + }, + }, + }, + bytes: { + type: 'long', + }, + status_code: { + type: 'long', + }, + }, + }, + version: { + type: 'keyword', + ignore_above: 1024, + }, + }, + }, + kubernetes: { + properties: { + annotations: { + type: 'object', + }, + container: { + properties: { + image: { + type: 'keyword', + ignore_above: 1024, + }, + name: { + type: 'keyword', + ignore_above: 1024, + }, + }, + }, + labels: { + type: 'object', + }, + namespace: { + type: 'keyword', + ignore_above: 1024, + }, + node: { + properties: { + name: { + type: 'keyword', + ignore_above: 1024, + }, + }, + }, + pod: { + properties: { + name: { + type: 'keyword', + ignore_above: 1024, + }, + uid: { + type: 'keyword', + ignore_above: 1024, + }, + }, + }, + }, + }, + labels: { + type: 'object', + }, + log: { + properties: { + level: { + type: 'keyword', + ignore_above: 1024, + }, + original: { + type: 'keyword', + index: false, + doc_values: false, + ignore_above: 1024, + }, + }, + }, + message: { + type: 'text', + norms: false, + }, + network: { + properties: { + application: { + type: 'keyword', + ignore_above: 1024, + }, + bytes: { + type: 'long', + }, + community_id: { + type: 'keyword', + ignore_above: 1024, + }, + direction: { + type: 'keyword', + ignore_above: 1024, + }, + forwarded_ip: { + type: 'ip', + }, + iana_number: { + type: 'keyword', + ignore_above: 1024, + }, + name: { + type: 'keyword', + ignore_above: 1024, + }, + packets: { + type: 'long', + }, + protocol: { + type: 'keyword', + ignore_above: 1024, + }, + transport: { + type: 'keyword', + ignore_above: 1024, + }, + type: { + type: 'keyword', + ignore_above: 1024, + }, + }, + }, + observer: { + properties: { + geo: { + properties: { + city_name: { + type: 'keyword', + ignore_above: 1024, + }, + continent_name: { + type: 'keyword', + ignore_above: 1024, + }, + country_iso_code: { + type: 'keyword', + ignore_above: 1024, + }, + country_name: { + type: 'keyword', + ignore_above: 1024, + }, + location: { + type: 'geo_point', + }, + name: { + type: 'keyword', + ignore_above: 1024, + }, + region_iso_code: { + type: 'keyword', + ignore_above: 1024, + }, + region_name: { + type: 'keyword', + ignore_above: 1024, + }, + }, + }, + hostname: { + type: 'keyword', + ignore_above: 1024, + }, + ip: { + type: 'ip', + }, + mac: { + type: 'keyword', + ignore_above: 1024, + }, + os: { + properties: { + family: { + type: 'keyword', + ignore_above: 1024, + }, + full: { + type: 'keyword', + ignore_above: 1024, + }, + kernel: { + type: 'keyword', + ignore_above: 1024, + }, + name: { + type: 'keyword', + ignore_above: 1024, + }, + platform: { + type: 'keyword', + ignore_above: 1024, + }, + version: { + type: 'keyword', + ignore_above: 1024, + }, + }, + }, + serial_number: { + type: 'keyword', + ignore_above: 1024, + }, + type: { + type: 'keyword', + ignore_above: 1024, + }, + vendor: { + type: 'keyword', + ignore_above: 1024, + }, + version: { + type: 'keyword', + ignore_above: 1024, + }, + }, + }, + organization: { + properties: { + id: { + type: 'keyword', + ignore_above: 1024, + }, + name: { + type: 'keyword', + ignore_above: 1024, + }, + }, + }, + os: { + properties: { + family: { + type: 'keyword', + ignore_above: 1024, + }, + full: { + type: 'keyword', + ignore_above: 1024, + }, + kernel: { + type: 'keyword', + ignore_above: 1024, + }, + name: { + type: 'keyword', + ignore_above: 1024, + }, + platform: { + type: 'keyword', + ignore_above: 1024, + }, + version: { + type: 'keyword', + ignore_above: 1024, + }, + }, + }, + process: { + properties: { + args: { + type: 'keyword', + ignore_above: 1024, + }, + entity_id: { + type: 'keyword', + ignore_above: 1024, + }, + executable: { + type: 'keyword', + ignore_above: 1024, + }, + name: { + type: 'keyword', + ignore_above: 1024, + }, + pid: { + type: 'long', + }, + ppid: { + type: 'long', + }, + start: { + type: 'date', + }, + thread: { + properties: { + id: { + type: 'long', + }, + }, + }, + title: { + type: 'keyword', + ignore_above: 1024, + }, + working_directory: { + type: 'keyword', + ignore_above: 1024, + }, + }, + }, + related: { + properties: { + ip: { + type: 'ip', + }, + }, + }, + server: { + properties: { + address: { + type: 'keyword', + ignore_above: 1024, + }, + bytes: { + type: 'long', + }, + domain: { + type: 'keyword', + ignore_above: 1024, + }, + geo: { + properties: { + city_name: { + type: 'keyword', + ignore_above: 1024, + }, + continent_name: { + type: 'keyword', + ignore_above: 1024, + }, + country_iso_code: { + type: 'keyword', + ignore_above: 1024, + }, + country_name: { + type: 'keyword', + ignore_above: 1024, + }, + location: { + type: 'geo_point', + }, + name: { + type: 'keyword', + ignore_above: 1024, + }, + region_iso_code: { + type: 'keyword', + ignore_above: 1024, + }, + region_name: { + type: 'keyword', + ignore_above: 1024, + }, + }, + }, + ip: { + type: 'ip', + }, + mac: { + type: 'keyword', + ignore_above: 1024, + }, + packets: { + type: 'long', + }, + port: { + type: 'long', + }, + }, + }, + service: { + properties: { + ephemeral_id: { + type: 'keyword', + ignore_above: 1024, + }, + id: { + type: 'keyword', + ignore_above: 1024, + }, + name: { + type: 'keyword', + ignore_above: 1024, + }, + state: { + type: 'keyword', + ignore_above: 1024, + }, + type: { + type: 'keyword', + ignore_above: 1024, + }, + version: { + type: 'keyword', + ignore_above: 1024, + }, + }, + }, + socket: { + properties: { + entity_id: { + type: 'keyword', + ignore_above: 1024, + }, + }, + }, + source: { + properties: { + address: { + type: 'keyword', + ignore_above: 1024, + }, + bytes: { + type: 'long', + }, + domain: { + type: 'keyword', + ignore_above: 1024, + }, + geo: { + properties: { + city_name: { + type: 'keyword', + ignore_above: 1024, + }, + continent_name: { + type: 'keyword', + ignore_above: 1024, + }, + country_iso_code: { + type: 'keyword', + ignore_above: 1024, + }, + country_name: { + type: 'keyword', + ignore_above: 1024, + }, + location: { + type: 'geo_point', + }, + name: { + type: 'keyword', + ignore_above: 1024, + }, + region_iso_code: { + type: 'keyword', + ignore_above: 1024, + }, + region_name: { + type: 'keyword', + ignore_above: 1024, + }, + }, + }, + ip: { + type: 'ip', + }, + mac: { + type: 'keyword', + ignore_above: 1024, + }, + packets: { + type: 'long', + }, + path: { + type: 'keyword', + ignore_above: 1024, + }, + port: { + type: 'long', + }, + }, + }, + system: { + properties: { + audit: { + properties: { + host: { + properties: { + architecture: { + type: 'keyword', + ignore_above: 1024, + }, + boottime: { + type: 'date', + }, + containerized: { + type: 'boolean', + }, + hostname: { + type: 'keyword', + ignore_above: 1024, + }, + id: { + type: 'keyword', + ignore_above: 1024, + }, + ip: { + type: 'ip', + }, + mac: { + type: 'keyword', + ignore_above: 1024, + }, + os: { + properties: { + family: { + type: 'keyword', + ignore_above: 1024, + }, + kernel: { + type: 'keyword', + ignore_above: 1024, + }, + name: { + type: 'keyword', + ignore_above: 1024, + }, + platform: { + type: 'keyword', + ignore_above: 1024, + }, + version: { + type: 'keyword', + ignore_above: 1024, + }, + }, + }, + timezone: { + properties: { + name: { + type: 'keyword', + ignore_above: 1024, + }, + offset: { + properties: { + sec: { + type: 'long', + }, + }, + }, + }, + }, + uptime: { + type: 'long', + }, + }, + }, + package: { + properties: { + arch: { + type: 'keyword', + ignore_above: 1024, + }, + entity_id: { + type: 'keyword', + ignore_above: 1024, + }, + installtime: { + type: 'date', + }, + license: { + type: 'keyword', + ignore_above: 1024, + }, + name: { + type: 'keyword', + ignore_above: 1024, + }, + release: { + type: 'keyword', + ignore_above: 1024, + }, + size: { + type: 'long', + }, + summary: { + type: 'keyword', + ignore_above: 1024, + }, + url: { + type: 'keyword', + ignore_above: 1024, + }, + version: { + type: 'keyword', + ignore_above: 1024, + }, + }, + }, + user: { + properties: { + dir: { + type: 'keyword', + ignore_above: 1024, + }, + gid: { + type: 'keyword', + ignore_above: 1024, + }, + group: { + properties: { + gid: { + type: 'keyword', + ignore_above: 1024, + }, + name: { + type: 'keyword', + ignore_above: 1024, + }, + }, + }, + name: { + type: 'keyword', + ignore_above: 1024, + }, + password: { + properties: { + last_changed: { + type: 'date', + }, + type: { + type: 'keyword', + ignore_above: 1024, + }, + }, + }, + shell: { + type: 'keyword', + ignore_above: 1024, + }, + uid: { + type: 'keyword', + ignore_above: 1024, + }, + user_information: { + type: 'keyword', + ignore_above: 1024, + }, + }, + }, + }, + }, + }, + }, + tags: { + type: 'keyword', + ignore_above: 1024, + }, + url: { + properties: { + domain: { + type: 'keyword', + ignore_above: 1024, + }, + fragment: { + type: 'keyword', + ignore_above: 1024, + }, + full: { + type: 'keyword', + ignore_above: 1024, + }, + original: { + type: 'keyword', + ignore_above: 1024, + }, + password: { + type: 'keyword', + ignore_above: 1024, + }, + path: { + type: 'keyword', + ignore_above: 1024, + }, + port: { + type: 'long', + }, + query: { + type: 'keyword', + ignore_above: 1024, + }, + scheme: { + type: 'keyword', + ignore_above: 1024, + }, + username: { + type: 'keyword', + ignore_above: 1024, + }, + }, + }, + user: { + properties: { + audit: { + properties: { + id: { + type: 'keyword', + ignore_above: 1024, + }, + name: { + type: 'keyword', + ignore_above: 1024, + }, + }, + }, + effective: { + properties: { + group: { + properties: { + id: { + type: 'keyword', + ignore_above: 1024, + }, + name: { + type: 'keyword', + ignore_above: 1024, + }, + }, + }, + id: { + type: 'keyword', + ignore_above: 1024, + }, + name: { + type: 'keyword', + ignore_above: 1024, + }, + }, + }, + email: { + type: 'keyword', + ignore_above: 1024, + }, + entity_id: { + type: 'keyword', + ignore_above: 1024, + }, + filesystem: { + properties: { + group: { + properties: { + id: { + type: 'keyword', + ignore_above: 1024, + }, + name: { + type: 'keyword', + ignore_above: 1024, + }, + }, + }, + id: { + type: 'keyword', + ignore_above: 1024, + }, + name: { + type: 'keyword', + ignore_above: 1024, + }, + }, + }, + full_name: { + type: 'keyword', + ignore_above: 1024, + }, + group: { + properties: { + id: { + type: 'keyword', + ignore_above: 1024, + }, + name: { + type: 'keyword', + ignore_above: 1024, + }, + }, + }, + hash: { + type: 'keyword', + ignore_above: 1024, + }, + id: { + type: 'keyword', + ignore_above: 1024, + }, + name: { + type: 'keyword', + ignore_above: 1024, + }, + name_map: { + type: 'object', + }, + ogid: { + properties: { + id: { + type: 'keyword', + ignore_above: 1024, + }, + name: { + type: 'keyword', + ignore_above: 1024, + }, + }, + }, + ouid: { + properties: { + id: { + type: 'keyword', + ignore_above: 1024, + }, + name: { + type: 'keyword', + ignore_above: 1024, + }, + }, + }, + saved: { + properties: { + group: { + properties: { + id: { + type: 'keyword', + ignore_above: 1024, + }, + name: { + type: 'keyword', + ignore_above: 1024, + }, + }, + }, + id: { + type: 'keyword', + ignore_above: 1024, + }, + name: { + type: 'keyword', + ignore_above: 1024, + }, + }, + }, + selinux: { + properties: { + category: { + type: 'keyword', + ignore_above: 1024, + }, + domain: { + type: 'keyword', + ignore_above: 1024, + }, + level: { + type: 'keyword', + ignore_above: 1024, + }, + role: { + type: 'keyword', + ignore_above: 1024, + }, + user: { + type: 'keyword', + ignore_above: 1024, + }, + }, + }, + terminal: { + type: 'keyword', + ignore_above: 1024, + }, + }, + }, + user_agent: { + properties: { + device: { + properties: { + name: { + type: 'keyword', + ignore_above: 1024, + }, + }, + }, + name: { + type: 'keyword', + ignore_above: 1024, + }, + original: { + type: 'keyword', + ignore_above: 1024, + }, + os: { + properties: { + family: { + type: 'keyword', + ignore_above: 1024, + }, + full: { + type: 'keyword', + ignore_above: 1024, + }, + kernel: { + type: 'keyword', + ignore_above: 1024, + }, + name: { + type: 'keyword', + ignore_above: 1024, + }, + platform: { + type: 'keyword', + ignore_above: 1024, + }, + version: { + type: 'keyword', + ignore_above: 1024, + }, + }, + }, + version: { + type: 'keyword', + ignore_above: 1024, + }, + }, + }, + }, + }, + }, +}; + +export const mockDetailsQueryDsl = { + mockDetailsQueryDsl: 'mockDetailsQueryDsl', +}; + +export const mockQueryDsl = { + mockQueryDsl: 'mockQueryDsl', +}; + +const mockTimelineDetailsInspectResponse = cloneDeep(mockResponseSearchTimelineDetails); +delete mockTimelineDetailsInspectResponse.hits.hits[0]._source; + +export const mockTimelineDetailsResult = { + inspect: { + dsl: [JSON.stringify(mockDetailsQueryDsl, null, 2)], + response: [JSON.stringify(mockTimelineDetailsInspectResponse, null, 2)], + }, + data: [ + { + category: 'base', + field: '@timestamp', + values: '2019-03-29T19:01:23.420Z', + originalValue: '2019-03-29T19:01:23.420Z', + }, + { + category: 'service', + field: 'service.type', + values: 'auditd', + originalValue: 'auditd', + }, + { + category: 'user', + field: 'user.audit.id', + values: 'unset', + originalValue: 'unset', + }, + { + category: 'user', + field: 'user.group.id', + values: '0', + originalValue: '0', + }, + { + category: 'user', + field: 'user.group.name', + values: 'root', + originalValue: 'root', + }, + { + category: 'user', + field: 'user.effective.group.id', + values: '0', + originalValue: '0', + }, + { + category: 'user', + field: 'user.effective.group.name', + values: 'root', + originalValue: 'root', + }, + { + category: 'user', + field: 'user.effective.id', + values: '0', + originalValue: '0', + }, + { + category: 'user', + field: 'user.effective.name', + values: 'root', + originalValue: 'root', + }, + { + category: 'user', + field: 'user.filesystem.group.name', + values: 'root', + originalValue: 'root', + }, + { + category: 'user', + field: 'user.filesystem.group.id', + values: '0', + originalValue: '0', + }, + { + category: 'user', + field: 'user.filesystem.name', + values: 'root', + originalValue: 'root', + }, + { + category: 'user', + field: 'user.filesystem.id', + values: '0', + originalValue: '0', + }, + { + category: 'user', + field: 'user.saved.group.id', + values: '0', + originalValue: '0', + }, + { + category: 'user', + field: 'user.saved.group.name', + values: 'root', + originalValue: 'root', + }, + { + category: 'user', + field: 'user.saved.id', + values: '0', + originalValue: '0', + }, + { + category: 'user', + field: 'user.saved.name', + values: 'root', + originalValue: 'root', + }, + { + category: 'user', + field: 'user.id', + values: '0', + originalValue: '0', + }, + { + category: 'user', + field: 'user.name', + values: 'root', + originalValue: 'root', + }, + { + category: 'process', + field: 'process.executable', + values: '/root/go/src/github.com/elastic/beats/x-pack/auditbeat/auditbeat', + originalValue: '/root/go/src/github.com/elastic/beats/x-pack/auditbeat/auditbeat', + }, + { + category: 'process', + field: 'process.working_directory', + values: '/root/go/src/github.com/elastic/beats/x-pack/auditbeat', + originalValue: '/root/go/src/github.com/elastic/beats/x-pack/auditbeat', + }, + { + category: 'process', + field: 'process.pid', + values: 15990, + originalValue: 15990, + }, + { + category: 'process', + field: 'process.ppid', + values: 1, + originalValue: 1, + }, + { + category: 'process', + field: 'process.title', + values: + '/root/go/src/github.com/elastic/beats/x-pack/auditbeat/auditbeat -e -c /root/go/src/github.com/elastic/beats/x-pack/auditbeat/au', + originalValue: + '/root/go/src/github.com/elastic/beats/x-pack/auditbeat/auditbeat -e -c /root/go/src/github.com/elastic/beats/x-pack/auditbeat/au', + }, + { + category: 'process', + field: 'process.name', + values: 'auditbeat', + originalValue: 'auditbeat', + }, + { + category: 'host', + field: 'host.architecture', + values: 'x86_64', + originalValue: 'x86_64', + }, + { + category: 'host', + field: 'host.os.name', + values: 'Ubuntu', + originalValue: 'Ubuntu', + }, + { + category: 'host', + field: 'host.os.kernel', + values: '4.15.0-45-generic', + originalValue: '4.15.0-45-generic', + }, + { + category: 'host', + field: 'host.os.codename', + values: 'bionic', + originalValue: 'bionic', + }, + { + category: 'host', + field: 'host.os.platform', + values: 'ubuntu', + originalValue: 'ubuntu', + }, + { + category: 'host', + field: 'host.os.version', + values: '18.04.2 LTS (Bionic Beaver)', + originalValue: '18.04.2 LTS (Bionic Beaver)', + }, + { + category: 'host', + field: 'host.os.family', + values: 'debian', + originalValue: 'debian', + }, + { + category: 'host', + field: 'host.id', + values: '7c21f5ed03b04d0299569d221fe18bbc', + originalValue: '7c21f5ed03b04d0299569d221fe18bbc', + }, + { + category: 'host', + field: 'host.name', + values: 'zeek-london', + originalValue: 'zeek-london', + }, + { + category: 'host', + field: 'host.ip', + values: ['46.101.3.136', '10.16.0.5', 'fe80::4066:42ff:fe19:b3b9'], + originalValue: ['46.101.3.136', '10.16.0.5', 'fe80::4066:42ff:fe19:b3b9'], + }, + { + category: 'host', + field: 'host.mac', + values: ['42:66:42:19:b3:b9'], + originalValue: ['42:66:42:19:b3:b9'], + }, + { + category: 'host', + field: 'host.hostname', + values: 'zeek-london', + originalValue: 'zeek-london', + }, + { + category: 'cloud', + field: 'cloud.provider', + values: 'digitalocean', + originalValue: 'digitalocean', + }, + { + category: 'cloud', + field: 'cloud.instance.id', + values: '136398786', + originalValue: '136398786', + }, + { + category: 'cloud', + field: 'cloud.region', + values: 'lon1', + originalValue: 'lon1', + }, + { + category: 'file', + field: 'file.device', + values: '00:00', + originalValue: '00:00', + }, + { + category: 'file', + field: 'file.inode', + values: '3926', + originalValue: '3926', + }, + { + category: 'file', + field: 'file.mode', + values: '0644', + originalValue: '0644', + }, + { + category: 'file', + field: 'file.uid', + values: '0', + originalValue: '0', + }, + { + category: 'file', + field: 'file.gid', + values: '0', + originalValue: '0', + }, + { + category: 'file', + field: 'file.owner', + values: 'root', + originalValue: 'root', + }, + { + category: 'file', + field: 'file.group', + values: 'root', + originalValue: 'root', + }, + { + category: 'file', + field: 'file.path', + values: '/etc/passwd', + originalValue: '/etc/passwd', + }, + { + category: 'auditd', + field: 'auditd.session', + values: 'unset', + originalValue: 'unset', + }, + { + category: 'auditd', + field: 'auditd.data.tty', + values: '(none)', + originalValue: '(none)', + }, + { + category: 'auditd', + field: 'auditd.data.a3', + values: '0', + originalValue: '0', + }, + { + category: 'auditd', + field: 'auditd.data.a2', + values: '80000', + originalValue: '80000', + }, + { + category: 'auditd', + field: 'auditd.data.syscall', + values: 'openat', + originalValue: 'openat', + }, + { + category: 'auditd', + field: 'auditd.data.a1', + values: '7fe0f63df220', + originalValue: '7fe0f63df220', + }, + { + category: 'auditd', + field: 'auditd.data.a0', + values: 'ffffff9c', + originalValue: 'ffffff9c', + }, + { + category: 'auditd', + field: 'auditd.data.arch', + values: 'x86_64', + originalValue: 'x86_64', + }, + { + category: 'auditd', + field: 'auditd.data.exit', + values: '12', + originalValue: '12', + }, + { + category: 'auditd', + field: 'auditd.summary.actor.primary', + values: 'unset', + originalValue: 'unset', + }, + { + category: 'auditd', + field: 'auditd.summary.actor.secondary', + values: 'root', + originalValue: 'root', + }, + { + category: 'auditd', + field: 'auditd.summary.object.primary', + values: '/etc/passwd', + originalValue: '/etc/passwd', + }, + { + category: 'auditd', + field: 'auditd.summary.object.type', + values: 'file', + originalValue: 'file', + }, + { + category: 'auditd', + field: 'auditd.summary.how', + values: '/root/go/src/github.com/elastic/beats/x-pack/auditbeat/auditbeat', + originalValue: '/root/go/src/github.com/elastic/beats/x-pack/auditbeat/auditbeat', + }, + { + category: 'auditd', + field: 'auditd.paths', + values: [ + { + rdev: '00:00', + cap_fe: '0', + nametype: 'NORMAL', + ogid: '0', + ouid: '0', + inode: '3926', + item: '0', + mode: '0100644', + name: '/etc/passwd', + cap_fi: '0000000000000000', + cap_fp: '0000000000000000', + cap_fver: '0', + dev: 'fc:01', + }, + ], + originalValue: [ + { + rdev: '00:00', + cap_fe: '0', + nametype: 'NORMAL', + ogid: '0', + ouid: '0', + inode: '3926', + item: '0', + mode: '0100644', + name: '/etc/passwd', + cap_fi: '0000000000000000', + cap_fp: '0000000000000000', + cap_fver: '0', + dev: 'fc:01', + }, + ], + }, + { + category: 'auditd', + field: 'auditd.message_type', + values: 'syscall', + originalValue: 'syscall', + }, + { + category: 'auditd', + field: 'auditd.sequence', + values: 8817905, + originalValue: 8817905, + }, + { + category: 'auditd', + field: 'auditd.result', + values: 'success', + originalValue: 'success', + }, + { + category: 'event', + field: 'event.category', + values: 'audit-rule', + originalValue: 'audit-rule', + }, + { + category: 'event', + field: 'event.action', + values: 'opened-file', + originalValue: 'opened-file', + }, + { + category: 'event', + field: 'event.original', + values: [ + 'type=SYSCALL msg=audit(1553886083.420:8817905): arch=c000003e syscall=257 success=yes exit=12 a0=ffffff9c a1=7fe0f63df220 a2=80000 a3=0 items=1 ppid=1 pid=15990 auid=4294967295 uid=0 gid=0 euid=0 suid=0 fsuid=0 egid=0 sgid=0 fsgid=0 tty=(none) ses=4294967295 comm="auditbeat" exe="/root/go/src/github.com/elastic/beats/x-pack/auditbeat/auditbeat" key=(null)', + 'type=CWD msg=audit(1553886083.420:8817905): cwd="/root/go/src/github.com/elastic/beats/x-pack/auditbeat"', + 'type=PATH msg=audit(1553886083.420:8817905): item=0 name="/etc/passwd" inode=3926 dev=fc:01 mode=0100644 ouid=0 ogid=0 rdev=00:00 nametype=NORMAL cap_fp=0000000000000000 cap_fi=0000000000000000 cap_fe=0 cap_fver=0', + 'type=PROCTITLE msg=audit(1553886083.420:8817905): proctitle=2F726F6F742F676F2F7372632F6769746875622E636F6D2F656C61737469632F62656174732F782D7061636B2F6175646974626561742F617564697462656174002D65002D63002F726F6F742F676F2F7372632F6769746875622E636F6D2F656C61737469632F62656174732F782D7061636B2F6175646974626561742F6175', + ], + originalValue: [ + 'type=SYSCALL msg=audit(1553886083.420:8817905): arch=c000003e syscall=257 success=yes exit=12 a0=ffffff9c a1=7fe0f63df220 a2=80000 a3=0 items=1 ppid=1 pid=15990 auid=4294967295 uid=0 gid=0 euid=0 suid=0 fsuid=0 egid=0 sgid=0 fsgid=0 tty=(none) ses=4294967295 comm="auditbeat" exe="/root/go/src/github.com/elastic/beats/x-pack/auditbeat/auditbeat" key=(null)', + 'type=CWD msg=audit(1553886083.420:8817905): cwd="/root/go/src/github.com/elastic/beats/x-pack/auditbeat"', + 'type=PATH msg=audit(1553886083.420:8817905): item=0 name="/etc/passwd" inode=3926 dev=fc:01 mode=0100644 ouid=0 ogid=0 rdev=00:00 nametype=NORMAL cap_fp=0000000000000000 cap_fi=0000000000000000 cap_fe=0 cap_fver=0', + 'type=PROCTITLE msg=audit(1553886083.420:8817905): proctitle=2F726F6F742F676F2F7372632F6769746875622E636F6D2F656C61737469632F62656174732F782D7061636B2F6175646974626561742F617564697462656174002D65002D63002F726F6F742F676F2F7372632F6769746875622E636F6D2F656C61737469632F62656174732F782D7061636B2F6175646974626561742F6175', + ], + }, + { + category: 'event', + field: 'event.module', + values: 'auditd', + originalValue: 'auditd', + }, + { + category: 'ecs', + field: 'ecs.version', + values: '1.0.0', + originalValue: '1.0.0', + }, + { + category: 'agent', + field: 'agent.ephemeral_id', + values: '6d541d59-52d0-4e70-b4d2-2660c0a99ff7', + originalValue: '6d541d59-52d0-4e70-b4d2-2660c0a99ff7', + }, + { + category: 'agent', + field: 'agent.hostname', + values: 'zeek-london', + originalValue: 'zeek-london', + }, + { + category: 'agent', + field: 'agent.id', + values: 'cc1f4183-36c6-45c4-b21b-7ce70c3572db', + originalValue: 'cc1f4183-36c6-45c4-b21b-7ce70c3572db', + }, + { + category: 'agent', + field: 'agent.version', + values: '8.0.0', + originalValue: '8.0.0', + }, + { + category: 'agent', + field: 'agent.type', + values: 'auditbeat', + originalValue: 'auditbeat', + }, + { + category: '_index', + field: '_index', + values: 'auditbeat-8.0.0-2019.03.29-000003', + originalValue: 'auditbeat-8.0.0-2019.03.29-000003', + }, + { + category: '_type', + field: '_type', + values: '_doc', + originalValue: '_doc', + }, + { + category: '_id', + field: '_id', + values: 'TUfUymkBCQofM5eXGBYL', + originalValue: 'TUfUymkBCQofM5eXGBYL', + }, + { + category: '_score', + field: '_score', + values: 1, + originalValue: 1, + }, + ], +}; diff --git a/x-pack/legacy/plugins/siem/server/lib/events/query.dsl.ts b/x-pack/plugins/siem/server/lib/events/query.dsl.ts similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/events/query.dsl.ts rename to x-pack/plugins/siem/server/lib/events/query.dsl.ts diff --git a/x-pack/legacy/plugins/siem/server/lib/events/query.last_event_time.dsl.ts b/x-pack/plugins/siem/server/lib/events/query.last_event_time.dsl.ts similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/events/query.last_event_time.dsl.ts rename to x-pack/plugins/siem/server/lib/events/query.last_event_time.dsl.ts diff --git a/x-pack/legacy/plugins/siem/server/lib/events/types.ts b/x-pack/plugins/siem/server/lib/events/types.ts similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/events/types.ts rename to x-pack/plugins/siem/server/lib/events/types.ts diff --git a/x-pack/legacy/plugins/siem/server/lib/framework/index.ts b/x-pack/plugins/siem/server/lib/framework/index.ts similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/framework/index.ts rename to x-pack/plugins/siem/server/lib/framework/index.ts diff --git a/x-pack/legacy/plugins/siem/server/lib/framework/kibana_framework_adapter.ts b/x-pack/plugins/siem/server/lib/framework/kibana_framework_adapter.ts similarity index 95% rename from x-pack/legacy/plugins/siem/server/lib/framework/kibana_framework_adapter.ts rename to x-pack/plugins/siem/server/lib/framework/kibana_framework_adapter.ts index 6b41426e047ca..762416149c0fb 100644 --- a/x-pack/legacy/plugins/siem/server/lib/framework/kibana_framework_adapter.ts +++ b/x-pack/plugins/siem/server/lib/framework/kibana_framework_adapter.ts @@ -13,9 +13,9 @@ import { KibanaResponseFactory, RequestHandlerContext, KibanaRequest, -} from '../../../../../../../src/core/server'; -import { IndexPatternsFetcher } from '../../../../../../../src/plugins/data/server'; -import { AuthenticatedUser } from '../../../../../../plugins/security/common/model'; +} from '../../../../../../src/core/server'; +import { IndexPatternsFetcher } from '../../../../../../src/plugins/data/server'; +import { AuthenticatedUser } from '../../../../security/common/model'; import { CoreSetup, SetupPlugins } from '../../plugin'; import { diff --git a/x-pack/plugins/siem/server/lib/framework/types.ts b/x-pack/plugins/siem/server/lib/framework/types.ts new file mode 100644 index 0000000000000..abe572df87063 --- /dev/null +++ b/x-pack/plugins/siem/server/lib/framework/types.ts @@ -0,0 +1,135 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { IndicesGetMappingParams } from 'elasticsearch'; +import { GraphQLSchema } from 'graphql'; + +import { RequestHandlerContext, KibanaRequest } from '../../../../../../src/core/server'; +import { AuthenticatedUser } from '../../../../security/common/model'; +import { ESQuery } from '../../../common/typed_json'; +import { + PaginationInput, + PaginationInputPaginated, + SortField, + SourceConfiguration, + TimerangeInput, + Maybe, + HistogramType, +} from '../../graphql/types'; + +export * from '../../utils/typed_resolvers'; + +export const internalFrameworkRequest = Symbol('internalFrameworkRequest'); + +export interface FrameworkAdapter { + registerGraphQLEndpoint(routePath: string, schema: GraphQLSchema): void; + callWithRequest( + req: FrameworkRequest, + method: 'search', + options?: object + ): Promise>; + callWithRequest( + req: FrameworkRequest, + method: 'msearch', + options?: object + ): Promise>; + callWithRequest( + req: FrameworkRequest, + method: 'indices.getMapping', + options?: IndicesGetMappingParams // eslint-disable-line + ): Promise; + getIndexPatternsService(req: FrameworkRequest): FrameworkIndexPatternsService; +} + +export interface FrameworkRequest extends Pick { + [internalFrameworkRequest]: KibanaRequest; + context: RequestHandlerContext; + user: AuthenticatedUser | null; +} + +export interface DatabaseResponse { + took: number; + timeout: boolean; +} + +export interface DatabaseSearchResponse + extends DatabaseResponse { + _shards: { + total: number; + successful: number; + skipped: number; + failed: number; + }; + aggregations?: Aggregations; + hits: { + total: number; + hits: Hit[]; + }; +} + +export interface DatabaseMultiResponse extends DatabaseResponse { + responses: Array>; +} + +export interface MappingProperties { + type: string; + path: string; + ignore_above: number; + properties: Readonly>>; +} + +export interface MappingResponse { + [indexName: string]: { + mappings: { + _meta: { + beat: string; + version: string; + }; + dynamic_templates: object[]; + date_detection: boolean; + properties: Readonly>>; + }; + }; +} + +interface FrameworkIndexFieldDescriptor { + aggregatable: boolean; + esTypes: string[]; + name: string; + readFromDocValues: boolean; + searchable: boolean; + type: string; +} + +export interface FrameworkIndexPatternsService { + getFieldsForWildcard(options: { + pattern: string | string[]; + }): Promise; +} + +export interface RequestBasicOptions { + sourceConfiguration: SourceConfiguration; + timerange: TimerangeInput; + filterQuery: ESQuery | undefined; + defaultIndex: string[]; +} + +export interface MatrixHistogramRequestOptions extends RequestBasicOptions { + stackByField: Maybe; + histogramType: HistogramType; +} + +export interface RequestOptions extends RequestBasicOptions { + pagination: PaginationInput; + fields: readonly string[]; + sortField?: SortField; +} + +export interface RequestOptionsPaginated extends RequestBasicOptions { + pagination: PaginationInputPaginated; + fields: readonly string[]; + sortField?: SortField; +} diff --git a/x-pack/legacy/plugins/siem/server/lib/hosts/elasticsearch_adapter.test.ts b/x-pack/plugins/siem/server/lib/hosts/elasticsearch_adapter.test.ts similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/hosts/elasticsearch_adapter.test.ts rename to x-pack/plugins/siem/server/lib/hosts/elasticsearch_adapter.test.ts diff --git a/x-pack/legacy/plugins/siem/server/lib/hosts/elasticsearch_adapter.ts b/x-pack/plugins/siem/server/lib/hosts/elasticsearch_adapter.ts similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/hosts/elasticsearch_adapter.ts rename to x-pack/plugins/siem/server/lib/hosts/elasticsearch_adapter.ts diff --git a/x-pack/legacy/plugins/siem/server/lib/hosts/helpers.test.ts b/x-pack/plugins/siem/server/lib/hosts/helpers.test.ts similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/hosts/helpers.test.ts rename to x-pack/plugins/siem/server/lib/hosts/helpers.test.ts diff --git a/x-pack/legacy/plugins/siem/server/lib/hosts/helpers.ts b/x-pack/plugins/siem/server/lib/hosts/helpers.ts similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/hosts/helpers.ts rename to x-pack/plugins/siem/server/lib/hosts/helpers.ts diff --git a/x-pack/legacy/plugins/siem/server/lib/hosts/index.ts b/x-pack/plugins/siem/server/lib/hosts/index.ts similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/hosts/index.ts rename to x-pack/plugins/siem/server/lib/hosts/index.ts diff --git a/x-pack/plugins/siem/server/lib/hosts/mock.ts b/x-pack/plugins/siem/server/lib/hosts/mock.ts new file mode 100644 index 0000000000000..30082990b55f9 --- /dev/null +++ b/x-pack/plugins/siem/server/lib/hosts/mock.ts @@ -0,0 +1,566 @@ +/* + * 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 { DEFAULT_INDEX_PATTERN } from '../../../common/constants'; +import { Direction, HostsFields } from '../../graphql/types'; +import { + HostOverviewRequestOptions, + HostLastFirstSeenRequestOptions, + HostsRequestOptions, +} from '.'; + +export const mockGetHostsOptions: HostsRequestOptions = { + defaultIndex: DEFAULT_INDEX_PATTERN, + sourceConfiguration: { + fields: { + container: 'docker.container.name', + host: 'beat.hostname', + message: ['message', '@message'], + pod: 'kubernetes.pod.name', + tiebreaker: '_doc', + timestamp: '@timestamp', + }, + }, + timerange: { interval: '12h', to: 1554824274610, from: 1554737874610 }, + sort: { field: HostsFields.lastSeen, direction: Direction.asc }, + pagination: { + activePage: 0, + cursorStart: 0, + fakePossibleCount: 10, + querySize: 2, + }, + filterQuery: {}, + fields: [ + 'totalCount', + '_id', + 'host.id', + 'host.name', + 'host.os.name', + 'host.os.version', + 'edges.cursor.value', + 'pageInfo.activePage', + 'pageInfo.fakeTotalCount', + 'pageInfo.showMorePagesIndicator', + ], +}; + +export const mockGetHostsRequest = { + body: { + operationName: 'GetHostsTableQuery', + variables: { + sourceId: 'default', + timerange: { interval: '12h', from: 1554737729201, to: 1554824129202 }, + pagination: { + activePage: 0, + cursorStart: 0, + fakePossibleCount: 10, + querySize: 2, + }, + sort: { field: HostsFields.lastSeen, direction: Direction.asc }, + filterQuery: '', + }, + query: + 'query GetHostsTableQuery($sourceId: ID!, $timerange: TimerangeInput!, $pagination: PaginationInput!, $sort: HostsSortField!, $filterQuery: String) {\n source(id: $sourceId) {\n id\n Hosts(timerange: $timerange, pagination: $pagination, sort: $sort, filterQuery: $filterQuery) {\n totalCount\n edges {\n node {\n _id\n host {\n id\n name\n os {\n name\n version\n __typename\n }\n __typename\n }\n __typename\n }\n cursor {\n value\n __typename\n }\n __typename\n }\n pageInfo {\n endCursor {\n value\n __typename\n }\n hasNextPage\n __typename\n }\n __typename\n }\n __typename\n }\n}\n', + }, +}; + +export const mockGetHostsResponse = { + took: 1695, + timed_out: false, + _shards: { + total: 59, + successful: 59, + skipped: 0, + failed: 0, + }, + hits: { + total: { + value: 4018586, + relation: 'eq', + }, + max_score: null, + hits: [], + }, + aggregations: { + host_data: { + doc_count_error_upper_bound: -1, + sum_other_doc_count: 3082125, + buckets: [ + { + key: 'beats-ci-immutable-centos-7-1554823376629262884', + doc_count: 991, + lastSeen: { + value: 1554823916544, + value_as_string: '2019-04-09T15:31:56.544Z', + }, + host_os_version: { + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, + buckets: [ + { + key: '7 (Core)', + doc_count: 991, + timestamp: { + value: 1554823916544, + value_as_string: '2019-04-09T15:31:56.544Z', + }, + }, + ], + }, + firstSeen: { + value: 1554823396740, + value_as_string: '2019-04-09T15:23:16.740Z', + }, + host_os_name: { + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, + buckets: [ + { + key: 'CentOS Linux', + doc_count: 991, + timestamp: { + value: 1554823916544, + value_as_string: '2019-04-09T15:31:56.544Z', + }, + }, + ], + }, + host_name: { + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, + buckets: [ + { + key: 'beats-ci-immutable-centos-7-1554823376629262884', + doc_count: 991, + timestamp: { + value: 1554823916544, + value_as_string: '2019-04-09T15:31:56.544Z', + }, + }, + ], + }, + host_id: { + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, + buckets: [ + { + key: 'f85edea1973c34f862c376cac4ebc777', + doc_count: 991, + timestamp: { + value: 1554823916544, + value_as_string: '2019-04-09T15:31:56.544Z', + }, + }, + ], + }, + }, + { + key: 'beats-ci-immutable-centos-7-1554823376629299914', + doc_count: 571, + lastSeen: { + value: 1554823916302, + value_as_string: '2019-04-09T15:31:56.302Z', + }, + host_os_version: { + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, + buckets: [ + { + key: '7 (Core)', + doc_count: 571, + timestamp: { + value: 1554823916302, + value_as_string: '2019-04-09T15:31:56.302Z', + }, + }, + ], + }, + firstSeen: { + value: 1554823398628, + value_as_string: '2019-04-09T15:23:18.628Z', + }, + host_os_name: { + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, + buckets: [ + { + key: 'CentOS Linux', + doc_count: 571, + timestamp: { + value: 1554823916302, + value_as_string: '2019-04-09T15:31:56.302Z', + }, + }, + ], + }, + host_name: { + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, + buckets: [ + { + key: 'beats-ci-immutable-centos-7-1554823376629299914', + doc_count: 571, + timestamp: { + value: 1554823916302, + value_as_string: '2019-04-09T15:31:56.302Z', + }, + }, + ], + }, + host_id: { + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, + buckets: [ + { + key: 'f85edea1973c34f862c376cac4ebc777', + doc_count: 571, + timestamp: { + value: 1554823916302, + value_as_string: '2019-04-09T15:31:56.302Z', + }, + }, + ], + }, + }, + ], + }, + host_count: { + value: 1627, + }, + }, +}; + +export const mockGetHostsQueryDsl = { mockGetHostsQueryDsl: 'mockGetHostsQueryDsl' }; + +export const mockGetHostsResult = { + inspect: { + dsl: [JSON.stringify(mockGetHostsQueryDsl, null, 2)], + response: [JSON.stringify(mockGetHostsResponse, null, 2)], + }, + edges: [ + { + node: { + _id: 'beats-ci-immutable-centos-7-1554823376629262884', + host: { + id: 'f85edea1973c34f862c376cac4ebc777', + name: 'beats-ci-immutable-centos-7-1554823376629262884', + os: { + name: 'CentOS Linux', + version: '7 (Core)', + }, + }, + }, + cursor: { + value: 'beats-ci-immutable-centos-7-1554823376629262884', + tiebreaker: null, + }, + }, + { + node: { + _id: 'beats-ci-immutable-centos-7-1554823376629299914', + host: { + id: 'f85edea1973c34f862c376cac4ebc777', + name: 'beats-ci-immutable-centos-7-1554823376629299914', + os: { + name: 'CentOS Linux', + version: '7 (Core)', + }, + }, + }, + cursor: { + value: 'beats-ci-immutable-centos-7-1554823376629299914', + tiebreaker: null, + }, + }, + ], + totalCount: 1627, + pageInfo: { + activePage: 0, + fakeTotalCount: 10, + showMorePagesIndicator: true, + }, +}; + +export const mockGetHostOverviewOptions: HostOverviewRequestOptions = { + sourceConfiguration: { + fields: { + container: 'docker.container.name', + host: 'beat.hostname', + message: ['message', '@message'], + pod: 'kubernetes.pod.name', + tiebreaker: '_doc', + timestamp: '@timestamp', + }, + }, + timerange: { interval: '12h', to: 1554824274610, from: 1554737874610 }, + defaultIndex: DEFAULT_INDEX_PATTERN, + fields: [ + '_id', + 'host.architecture', + 'host.id', + 'host.ip', + 'host.mac', + 'host.name', + 'host.os.family', + 'host.os.name', + 'host.os.platform', + 'host.os.version', + 'host.os.__typename', + 'host.type', + 'host.__typename', + 'cloud.instance.id', + 'cloud.instance.__typename', + 'cloud.machine.type', + 'cloud.machine.__typename', + 'cloud.provider', + 'cloud.region', + 'cloud.__typename', + '__typename', + ], + hostName: 'siem-es', +}; + +export const mockGetHostOverviewRequest = { + body: { + operationName: 'GetHostOverviewQuery', + variables: { sourceId: 'default', hostName: 'siem-es' }, + query: + 'query GetHostOverviewQuery($sourceId: ID!, $hostName: String!, $timerange: TimerangeInput!) {\n source(id: $sourceId) {\n id\n HostOverview(hostName: $hostName, timerange: $timerange) {\n _id\n host {\n architecture\n id\n ip\n mac\n name\n os {\n family\n name\n platform\n version\n __typename\n }\n type\n __typename\n }\n cloud {\n instance {\n id\n __typename\n }\n machine {\n type\n __typename\n }\n provider\n region\n __typename\n }\n __typename\n }\n __typename\n }\n}\n', + }, +}; + +export const mockGetHostOverviewResponse = { + took: 2205, + timed_out: false, + _shards: { total: 59, successful: 59, skipped: 0, failed: 0 }, + hits: { total: { value: 611894, relation: 'eq' }, max_score: null, hits: [] }, + aggregations: { + host_mac: { doc_count_error_upper_bound: 0, sum_other_doc_count: 0, buckets: [] }, + host_ip: { doc_count_error_upper_bound: 0, sum_other_doc_count: 0, buckets: [] }, + cloud_region: { + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, + buckets: [ + { + key: 'us-east-1', + doc_count: 4308, + timestamp: { value: 1556903543093, value_as_string: '2019-05-03T17:12:23.093Z' }, + }, + ], + }, + cloud_provider: { + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, + buckets: [ + { + key: 'gce', + doc_count: 432808, + timestamp: { value: 1556903543093, value_as_string: '2019-05-03T17:12:23.093Z' }, + }, + ], + }, + cloud_instance_id: { + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, + buckets: [ + { + key: '5412578377715150143', + doc_count: 432808, + timestamp: { value: 1556903543093, value_as_string: '2019-05-03T17:12:23.093Z' }, + }, + ], + }, + cloud_machine_type: { + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, + buckets: [ + { + key: 'n1-standard-1', + doc_count: 432808, + timestamp: { value: 1556903543093, value_as_string: '2019-05-03T17:12:23.093Z' }, + }, + ], + }, + host_os_version: { + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, + buckets: [ + { + key: '9 (stretch)', + doc_count: 611894, + timestamp: { value: 1554826117972, value_as_string: '2019-04-09T16:08:37.972Z' }, + }, + ], + }, + host_architecture: { + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, + buckets: [ + { + key: 'x86_64', + doc_count: 611894, + timestamp: { value: 1554826117972, value_as_string: '2019-04-09T16:08:37.972Z' }, + }, + ], + }, + host_os_platform: { + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, + buckets: [ + { + key: 'debian', + doc_count: 611894, + timestamp: { value: 1554826117972, value_as_string: '2019-04-09T16:08:37.972Z' }, + }, + ], + }, + host_os_name: { + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, + buckets: [ + { + key: 'Debian GNU/Linux', + doc_count: 611894, + timestamp: { value: 1554826117972, value_as_string: '2019-04-09T16:08:37.972Z' }, + }, + ], + }, + host_os_family: { + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, + buckets: [ + { + key: 'debian', + doc_count: 611894, + timestamp: { value: 1554826117972, value_as_string: '2019-04-09T16:08:37.972Z' }, + }, + ], + }, + host_name: { + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, + buckets: [ + { + key: 'siem-es', + doc_count: 611894, + timestamp: { value: 1554826117972, value_as_string: '2019-04-09T16:08:37.972Z' }, + }, + ], + }, + host_id: { + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, + buckets: [ + { + key: 'b6d5264e4b9c8880ad1053841067a4a6', + doc_count: 611894, + timestamp: { value: 1554826117972, value_as_string: '2019-04-09T16:08:37.972Z' }, + }, + ], + }, + }, +}; + +export const mockGetHostOverviewRequestDsl = { + mockGetHostOverviewRequestDsl: 'mockGetHostOverviewRequestDsl', +}; + +export const mockGetHostOverviewResult = { + inspect: { + dsl: [JSON.stringify(mockGetHostOverviewRequestDsl, null, 2)], + response: [JSON.stringify(mockGetHostOverviewResponse, null, 2)], + }, + _id: 'siem-es', + host: { + architecture: 'x86_64', + id: 'b6d5264e4b9c8880ad1053841067a4a6', + ip: [], + mac: [], + name: 'siem-es', + os: { + family: 'debian', + name: 'Debian GNU/Linux', + platform: 'debian', + version: '9 (stretch)', + }, + }, + cloud: { + instance: { + id: ['5412578377715150143'], + }, + machine: { + type: ['n1-standard-1'], + }, + provider: ['gce'], + region: ['us-east-1'], + }, +}; + +export const mockGetHostLastFirstSeenOptions: HostLastFirstSeenRequestOptions = { + defaultIndex: DEFAULT_INDEX_PATTERN, + sourceConfiguration: { + fields: { + container: 'docker.container.name', + host: 'beat.hostname', + message: ['message', '@message'], + pod: 'kubernetes.pod.name', + tiebreaker: '_doc', + timestamp: '@timestamp', + }, + }, + hostName: 'siem-es', +}; + +export const mockGetHostLastFirstSeenRequest = { + body: { + operationName: 'GetHostLastFirstSeenQuery', + variables: { sourceId: 'default', hostName: 'siem-es' }, + query: + 'query GetHostLastFirstSeenQuery($sourceId: ID!, $hostName: String!) {\n source(id: $sourceId) {\n id\n HostLastFirstSeen(hostName: $hostName) {\n firstSeen\n lastSeen\n __typename\n }\n __typename\n }\n}\n', + }, +}; + +export const mockGetHostLastFirstSeenResponse = { + took: 60, + timed_out: false, + _shards: { + total: 59, + successful: 59, + skipped: 0, + failed: 0, + }, + hits: { + total: { + value: 612092, + relation: 'eq', + }, + max_score: null, + hits: [], + }, + aggregations: { + lastSeen: { + value: 1554826692178, + value_as_string: '2019-04-09T16:18:12.178Z', + }, + firstSeen: { + value: 1550806892826, + value_as_string: '2019-02-22T03:41:32.826Z', + }, + }, +}; + +export const mockGetHostLastFirstSeenDsl = { + mockGetHostLastFirstSeenDsl: 'mockGetHostLastFirstSeenDsl', +}; + +export const mockGetHostLastFirstSeenResult = { + inspect: { + dsl: [JSON.stringify(mockGetHostLastFirstSeenDsl, null, 2)], + response: [JSON.stringify(mockGetHostLastFirstSeenResponse, null, 2)], + }, + firstSeen: '2019-02-22T03:41:32.826Z', + lastSeen: '2019-04-09T16:18:12.178Z', +}; diff --git a/x-pack/legacy/plugins/siem/server/lib/hosts/query.detail_host.dsl.ts b/x-pack/plugins/siem/server/lib/hosts/query.detail_host.dsl.ts similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/hosts/query.detail_host.dsl.ts rename to x-pack/plugins/siem/server/lib/hosts/query.detail_host.dsl.ts diff --git a/x-pack/legacy/plugins/siem/server/lib/hosts/query.hosts.dsl.ts b/x-pack/plugins/siem/server/lib/hosts/query.hosts.dsl.ts similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/hosts/query.hosts.dsl.ts rename to x-pack/plugins/siem/server/lib/hosts/query.hosts.dsl.ts diff --git a/x-pack/legacy/plugins/siem/server/lib/hosts/query.last_first_seen_host.dsl.ts b/x-pack/plugins/siem/server/lib/hosts/query.last_first_seen_host.dsl.ts similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/hosts/query.last_first_seen_host.dsl.ts rename to x-pack/plugins/siem/server/lib/hosts/query.last_first_seen_host.dsl.ts diff --git a/x-pack/legacy/plugins/siem/server/lib/hosts/types.ts b/x-pack/plugins/siem/server/lib/hosts/types.ts similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/hosts/types.ts rename to x-pack/plugins/siem/server/lib/hosts/types.ts diff --git a/x-pack/legacy/plugins/siem/server/lib/index_fields/elasticsearch_adapter.test.ts b/x-pack/plugins/siem/server/lib/index_fields/elasticsearch_adapter.test.ts similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/index_fields/elasticsearch_adapter.test.ts rename to x-pack/plugins/siem/server/lib/index_fields/elasticsearch_adapter.test.ts diff --git a/x-pack/legacy/plugins/siem/server/lib/index_fields/elasticsearch_adapter.ts b/x-pack/plugins/siem/server/lib/index_fields/elasticsearch_adapter.ts similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/index_fields/elasticsearch_adapter.ts rename to x-pack/plugins/siem/server/lib/index_fields/elasticsearch_adapter.ts diff --git a/x-pack/legacy/plugins/siem/server/lib/index_fields/index.ts b/x-pack/plugins/siem/server/lib/index_fields/index.ts similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/index_fields/index.ts rename to x-pack/plugins/siem/server/lib/index_fields/index.ts diff --git a/x-pack/legacy/plugins/siem/server/lib/index_fields/mock.ts b/x-pack/plugins/siem/server/lib/index_fields/mock.ts similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/index_fields/mock.ts rename to x-pack/plugins/siem/server/lib/index_fields/mock.ts diff --git a/x-pack/legacy/plugins/siem/server/lib/index_fields/types.ts b/x-pack/plugins/siem/server/lib/index_fields/types.ts similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/index_fields/types.ts rename to x-pack/plugins/siem/server/lib/index_fields/types.ts diff --git a/x-pack/legacy/plugins/siem/server/lib/ip_details/elasticsearch_adapter.test.ts b/x-pack/plugins/siem/server/lib/ip_details/elasticsearch_adapter.test.ts similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/ip_details/elasticsearch_adapter.test.ts rename to x-pack/plugins/siem/server/lib/ip_details/elasticsearch_adapter.test.ts diff --git a/x-pack/legacy/plugins/siem/server/lib/ip_details/elasticsearch_adapter.ts b/x-pack/plugins/siem/server/lib/ip_details/elasticsearch_adapter.ts similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/ip_details/elasticsearch_adapter.ts rename to x-pack/plugins/siem/server/lib/ip_details/elasticsearch_adapter.ts diff --git a/x-pack/legacy/plugins/siem/server/lib/ip_details/index.ts b/x-pack/plugins/siem/server/lib/ip_details/index.ts similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/ip_details/index.ts rename to x-pack/plugins/siem/server/lib/ip_details/index.ts diff --git a/x-pack/legacy/plugins/siem/server/lib/ip_details/mock.ts b/x-pack/plugins/siem/server/lib/ip_details/mock.ts similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/ip_details/mock.ts rename to x-pack/plugins/siem/server/lib/ip_details/mock.ts diff --git a/x-pack/legacy/plugins/siem/server/lib/ip_details/query_overview.dsl.ts b/x-pack/plugins/siem/server/lib/ip_details/query_overview.dsl.ts similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/ip_details/query_overview.dsl.ts rename to x-pack/plugins/siem/server/lib/ip_details/query_overview.dsl.ts diff --git a/x-pack/legacy/plugins/siem/server/lib/ip_details/query_users.dsl.ts b/x-pack/plugins/siem/server/lib/ip_details/query_users.dsl.ts similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/ip_details/query_users.dsl.ts rename to x-pack/plugins/siem/server/lib/ip_details/query_users.dsl.ts diff --git a/x-pack/legacy/plugins/siem/server/lib/ip_details/types.ts b/x-pack/plugins/siem/server/lib/ip_details/types.ts similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/ip_details/types.ts rename to x-pack/plugins/siem/server/lib/ip_details/types.ts diff --git a/x-pack/legacy/plugins/siem/server/lib/kpi_hosts/elasticsearch_adapter.test.ts b/x-pack/plugins/siem/server/lib/kpi_hosts/elasticsearch_adapter.test.ts similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/kpi_hosts/elasticsearch_adapter.test.ts rename to x-pack/plugins/siem/server/lib/kpi_hosts/elasticsearch_adapter.test.ts diff --git a/x-pack/legacy/plugins/siem/server/lib/kpi_hosts/elasticsearch_adapter.ts b/x-pack/plugins/siem/server/lib/kpi_hosts/elasticsearch_adapter.ts similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/kpi_hosts/elasticsearch_adapter.ts rename to x-pack/plugins/siem/server/lib/kpi_hosts/elasticsearch_adapter.ts diff --git a/x-pack/legacy/plugins/siem/server/lib/kpi_hosts/helpers.test.ts b/x-pack/plugins/siem/server/lib/kpi_hosts/helpers.test.ts similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/kpi_hosts/helpers.test.ts rename to x-pack/plugins/siem/server/lib/kpi_hosts/helpers.test.ts diff --git a/x-pack/legacy/plugins/siem/server/lib/kpi_hosts/helpers.ts b/x-pack/plugins/siem/server/lib/kpi_hosts/helpers.ts similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/kpi_hosts/helpers.ts rename to x-pack/plugins/siem/server/lib/kpi_hosts/helpers.ts diff --git a/x-pack/legacy/plugins/siem/server/lib/kpi_hosts/index.ts b/x-pack/plugins/siem/server/lib/kpi_hosts/index.ts similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/kpi_hosts/index.ts rename to x-pack/plugins/siem/server/lib/kpi_hosts/index.ts diff --git a/x-pack/plugins/siem/server/lib/kpi_hosts/mock.ts b/x-pack/plugins/siem/server/lib/kpi_hosts/mock.ts new file mode 100644 index 0000000000000..a5affea2842a6 --- /dev/null +++ b/x-pack/plugins/siem/server/lib/kpi_hosts/mock.ts @@ -0,0 +1,606 @@ +/* + * 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 { DEFAULT_INDEX_PATTERN } from '../../../common/constants'; +import { RequestBasicOptions } from '../framework/types'; + +const FROM = new Date('2019-05-03T13:24:00.660Z').valueOf(); +const TO = new Date('2019-05-04T13:24:00.660Z').valueOf(); + +export const mockKpiHostsOptions: RequestBasicOptions = { + defaultIndex: DEFAULT_INDEX_PATTERN, + sourceConfiguration: { + fields: { + container: 'docker.container.name', + host: 'beat.hostname', + message: ['message', '@message'], + pod: 'kubernetes.pod.name', + tiebreaker: '_doc', + timestamp: '@timestamp', + }, + }, + timerange: { interval: '12h', to: TO, from: FROM }, + filterQuery: undefined, +}; + +export const mockKpiHostDetailsOptions: RequestBasicOptions = { + defaultIndex: DEFAULT_INDEX_PATTERN, + sourceConfiguration: { + fields: { + container: 'docker.container.name', + host: 'beat.hostname', + message: ['message', '@message'], + pod: 'kubernetes.pod.name', + tiebreaker: '_doc', + timestamp: '@timestamp', + }, + }, + timerange: { interval: '12h', to: TO, from: FROM }, + filterQuery: { term: { 'host.name': 'beats-ci-immutable-ubuntu-1604-1560970771368235343' } }, +}; + +export const mockKpiHostsRequest = { + body: { + operationName: 'GetKpiHostsQuery', + variables: { + sourceId: 'default', + timerange: { interval: '12h', from: FROM, to: TO }, + filterQuery: '', + }, + query: + 'fragment KpiHostChartFields on KpiHostHistogramData {\n x\n y\n __typename\n}\n\nquery GetKpiHostsQuery($sourceId: ID!, $timerange: TimerangeInput!, $filterQuery: String, $defaultIndex: [String!]!) {\n source(id: $sourceId) {\n id\n KpiHosts(timerange: $timerange, filterQuery: $filterQuery, defaultIndex: $defaultIndex) {\n hosts\n hostsHistogram {\n ...KpiHostChartFields\n __typename\n }\n authSuccess\n authSuccessHistogram {\n ...KpiHostChartFields\n __typename\n }\n authFailure\n authFailureHistogram {\n ...KpiHostChartFields\n __typename\n }\n uniqueSourceIps\n uniqueSourceIpsHistogram {\n ...KpiHostChartFields\n __typename\n }\n uniqueDestinationIps\n uniqueDestinationIpsHistogram {\n ...KpiHostChartFields\n __typename\n }\n __typename\n }\n __typename\n }\n}\n', + }, +}; + +export const mockKpiHostDetailsRequest = { + body: { + operationName: 'GetKpiHostDetailsQuery', + variables: { + sourceId: 'default', + timerange: { interval: '12h', from: FROM, to: TO }, + filterQuery: { term: { 'host.name': 'beats-ci-immutable-ubuntu-1604-1560970771368235343' } }, + }, + query: + 'fragment KpiHostDetailsChartFields on KpiHostHistogramData {\n x\n y\n __typename\n}\n\nquery GetKpiHostDetailsQuery($sourceId: ID!, $timerange: TimerangeInput!, $filterQuery: String, $defaultIndex: [String!]!, $hostName: String!) {\n source(id: $sourceId) {\n id\n KpiHostDetails(timerange: $timerange, filterQuery: $filterQuery, defaultIndex: $defaultIndex, hostName: $hostName) {\n authSuccess\n authSuccessHistogram {\n ...KpiHostDetailsChartFields\n __typename\n }\n authFailure\n authFailureHistogram {\n ...KpiHostDetailsChartFields\n __typename\n }\n uniqueSourceIps\n uniqueSourceIpsHistogram {\n ...KpiHostDetailsChartFields\n __typename\n }\n uniqueDestinationIps\n uniqueDestinationIpsHistogram {\n ...KpiHostDetailsChartFields\n __typename\n }\n __typename\n }\n __typename\n }\n}\n', + }, +}; + +const mockUniqueIpsResponse = { + took: 1234, + timed_out: false, + _shards: { + total: 71, + successful: 71, + skipped: 65, + failed: 0, + }, + hits: { + max_score: null, + hits: [], + }, + aggregations: { + unique_destination_ips: { + value: 1954, + }, + unique_destination_ips_histogram: { + buckets: [ + { + key_as_string: '2019-05-03T13:00:00.000Z', + key: 1556888400000, + doc_count: 3158515, + count: { + value: 1809, + }, + }, + { + key_as_string: '2019-05-04T01:00:00.000Z', + key: 1556931600000, + doc_count: 703032, + count: { + value: 407, + }, + }, + { + key_as_string: '2019-05-04T13:00:00.000Z', + key: 1556974800000, + doc_count: 1780, + count: { + value: 64, + }, + }, + ], + interval: '12h', + }, + unique_source_ips: { + value: 1407, + }, + unique_source_ips_histogram: { + buckets: [ + { + key_as_string: '2019-05-03T13:00:00.000Z', + key: 1556888400000, + doc_count: 3158515, + count: { + value: 1182, + }, + }, + { + key_as_string: '2019-05-04T01:00:00.000Z', + key: 1556931600000, + doc_count: 703032, + count: { + value: 364, + }, + }, + { + key_as_string: '2019-05-04T13:00:00.000Z', + key: 1556974800000, + doc_count: 1780, + count: { + value: 63, + }, + }, + ], + interval: '12h', + }, + }, + status: 200, +}; + +const mockAuthResponse = { + took: 320, + timed_out: false, + _shards: { + total: 71, + successful: 71, + skipped: 65, + failed: 0, + }, + hits: { + max_score: null, + hits: [], + }, + aggregations: { + authentication_success: { + doc_count: 61, + }, + authentication_failure: { + doc_count: 15722, + }, + authentication_failure_histogram: { + buckets: [ + { + key_as_string: '2019-05-03T13:00:00.000Z', + key: 1556888400000, + doc_count: 11739, + count: { + doc_count: 11731, + }, + }, + { + key_as_string: '2019-05-04T01:00:00.000Z', + key: 1556931600000, + doc_count: 4031, + count: { + doc_count: 3979, + }, + }, + { + key_as_string: '2019-05-04T13:00:00.000Z', + key: 1556974800000, + doc_count: 13, + count: { + doc_count: 12, + }, + }, + ], + interval: '12h', + }, + authentication_success_histogram: { + buckets: [ + { + key_as_string: '2019-05-03T13:00:00.000Z', + key: 1556888400000, + doc_count: 11739, + count: { + doc_count: 8, + }, + }, + { + key_as_string: '2019-05-04T01:00:00.000Z', + key: 1556931600000, + doc_count: 4031, + count: { + doc_count: 52, + }, + }, + { + key_as_string: '2019-05-04T13:00:00.000Z', + key: 1556974800000, + doc_count: 13, + count: { + doc_count: 1, + }, + }, + ], + interval: '12h', + }, + }, + status: 200, +}; + +const mockHostsReponse = { + took: 1234, + timed_out: false, + _shards: { + total: 71, + successful: 71, + skipped: 65, + failed: 0, + }, + hits: { + max_score: null, + hits: [], + }, + aggregations: { + hosts: { + value: 986, + }, + hosts_histogram: { + buckets: [ + { + key_as_string: '2019-05-03T13:00:00.000Z', + key: 1556888400000, + doc_count: 3158515, + count: { + value: 919, + }, + }, + { + key_as_string: '2019-05-04T01:00:00.000Z', + key: 1556931600000, + doc_count: 703032, + count: { + value: 82, + }, + }, + { + key_as_string: '2019-05-04T13:00:00.000Z', + key: 1556974800000, + doc_count: 1780, + count: { + value: 4, + }, + }, + ], + interval: '12h', + }, + }, + status: 200, +}; + +export const mockKpiHostsResponse = { + took: 4405, + responses: [mockHostsReponse, mockAuthResponse, mockUniqueIpsResponse], +}; + +export const mockKpiHostsResponseNodata = { responses: [null, null, null] }; + +const mockMsearchHeader = { + index: DEFAULT_INDEX_PATTERN, + allowNoIndices: true, + ignoreUnavailable: true, +}; + +const mockHostNameFilter = { + term: { 'host.name': 'beats-ci-immutable-ubuntu-1604-1560970771368235343' }, +}; +const mockTimerangeFilter = { range: { '@timestamp': { gte: FROM, lte: TO } } }; + +export const mockHostsQuery = [ + mockMsearchHeader, + { + aggregations: { + hosts: { cardinality: { field: 'host.name' } }, + hosts_histogram: { + auto_date_histogram: { field: '@timestamp', buckets: '6' }, + aggs: { count: { cardinality: { field: 'host.name' } } }, + }, + }, + query: { + bool: { filter: [{ range: { '@timestamp': mockTimerangeFilter } }] }, + }, + size: 0, + track_total_hits: false, + }, +]; + +const mockUniqueIpsAggs = { + unique_source_ips: { cardinality: { field: 'source.ip' } }, + unique_source_ips_histogram: { + auto_date_histogram: { field: '@timestamp', buckets: '6' }, + aggs: { count: { cardinality: { field: 'source.ip' } } }, + }, + unique_destination_ips: { cardinality: { field: 'destination.ip' } }, + unique_destination_ips_histogram: { + auto_date_histogram: { field: '@timestamp', buckets: '6' }, + aggs: { count: { cardinality: { field: 'destination.ip' } } }, + }, +}; + +export const mockKpiHostsUniqueIpsQuery = [ + mockMsearchHeader, + { + aggregations: mockUniqueIpsAggs, + query: { + bool: { filter: [mockTimerangeFilter] }, + }, + size: 0, + track_total_hits: false, + }, +]; + +export const mockKpiHostDetailsUniqueIpsQuery = [ + mockMsearchHeader, + { + aggregations: mockUniqueIpsAggs, + query: { + bool: { filter: [mockHostNameFilter, mockTimerangeFilter] }, + }, + size: 0, + track_total_hits: false, + }, +]; + +const mockAuthAggs = { + authentication_success: { filter: { term: { 'event.outcome': 'success' } } }, + authentication_success_histogram: { + auto_date_histogram: { field: '@timestamp', buckets: '6' }, + aggs: { count: { filter: { term: { 'event.outcome': 'success' } } } }, + }, + authentication_failure: { filter: { term: { 'event.outcome': 'failure' } } }, + authentication_failure_histogram: { + auto_date_histogram: { field: '@timestamp', buckets: '6' }, + aggs: { count: { filter: { term: { 'event.outcome': 'failure' } } } }, + }, +}; + +const mockAuthFilter = { + bool: { + filter: [ + { + term: { + 'event.category': 'authentication', + }, + }, + ], + }, +}; + +export const mockKpiHostsAuthQuery = [ + mockMsearchHeader, + { + aggs: mockAuthAggs, + query: { + bool: { + filter: [mockAuthFilter, mockTimerangeFilter], + }, + }, + size: 0, + track_total_hits: false, + }, +]; + +export const mockKpiHostDetailsAuthQuery = [ + mockMsearchHeader, + { + aggs: mockAuthAggs, + query: { + bool: { + filter: [mockHostNameFilter, mockAuthFilter, mockTimerangeFilter], + }, + }, + size: 0, + track_total_hits: false, + }, +]; + +export const mockKpiHostsMsearchOptions = { + body: [...mockHostsQuery, ...mockKpiHostsAuthQuery, ...mockKpiHostsUniqueIpsQuery], +}; + +export const mockKpiHostDetailsMsearchOptions = { + body: [...mockKpiHostDetailsAuthQuery, ...mockKpiHostDetailsUniqueIpsQuery], +}; + +export const mockKpiHostsQueryDsl = [ + JSON.stringify({ ...mockHostsQuery[0], body: mockHostsQuery[1] }, null, 2), + JSON.stringify({ ...mockKpiHostsAuthQuery[0], body: mockKpiHostsAuthQuery[1] }, null, 2), + JSON.stringify( + { ...mockKpiHostsUniqueIpsQuery[0], body: mockKpiHostsUniqueIpsQuery[1] }, + null, + 2 + ), +]; + +export const mockKpiHostsResult = { + inspect: { + dsl: mockKpiHostsQueryDsl, + response: [ + JSON.stringify(mockKpiHostsResponse.responses[0], null, 2), + JSON.stringify(mockKpiHostsResponse.responses[1], null, 2), + JSON.stringify(mockKpiHostsResponse.responses[2], null, 2), + ], + }, + hosts: 986, + hostsHistogram: [ + { + x: new Date('2019-05-03T13:00:00.000Z').valueOf(), + y: 919, + }, + { + x: new Date('2019-05-04T01:00:00.000Z').valueOf(), + y: 82, + }, + { + x: new Date('2019-05-04T13:00:00.000Z').valueOf(), + y: 4, + }, + ], + authSuccess: 61, + authSuccessHistogram: [ + { + x: new Date('2019-05-03T13:00:00.000Z').valueOf(), + y: 8, + }, + { + x: new Date('2019-05-04T01:00:00.000Z').valueOf(), + y: 52, + }, + { + x: new Date('2019-05-04T13:00:00.000Z').valueOf(), + y: 1, + }, + ], + authFailure: 15722, + authFailureHistogram: [ + { + x: new Date('2019-05-03T13:00:00.000Z').valueOf(), + y: 11731, + }, + { + x: new Date('2019-05-04T01:00:00.000Z').valueOf(), + y: 3979, + }, + { + x: new Date('2019-05-04T13:00:00.000Z').valueOf(), + y: 12, + }, + ], + uniqueSourceIps: 1407, + uniqueSourceIpsHistogram: [ + { + x: new Date('2019-05-03T13:00:00.000Z').valueOf(), + y: 1182, + }, + { + x: new Date('2019-05-04T01:00:00.000Z').valueOf(), + y: 364, + }, + { + x: new Date('2019-05-04T13:00:00.000Z').valueOf(), + y: 63, + }, + ], + uniqueDestinationIps: 1954, + uniqueDestinationIpsHistogram: [ + { + x: new Date('2019-05-03T13:00:00.000Z').valueOf(), + y: 1809, + }, + { + x: new Date('2019-05-04T01:00:00.000Z').valueOf(), + y: 407, + }, + { + x: new Date('2019-05-04T13:00:00.000Z').valueOf(), + y: 64, + }, + ], +}; + +export const mockKpiHostDetailsResponse = { + took: 4405, + responses: [mockAuthResponse, mockUniqueIpsResponse], +}; + +export const mockKpiHostDetailsResponseNoData = { + took: 4405, + responses: [null, null], +}; + +export const mockKpiHostDetailsDsl = [ + JSON.stringify( + { ...mockKpiHostDetailsAuthQuery[0], body: mockKpiHostDetailsAuthQuery[1] }, + null, + 2 + ), + JSON.stringify( + { ...mockKpiHostDetailsUniqueIpsQuery[0], body: mockKpiHostDetailsUniqueIpsQuery[1] }, + null, + 2 + ), +]; + +export const mockKpiHostDetailsResult = { + inspect: { + dsl: mockKpiHostDetailsDsl, + response: [ + JSON.stringify(mockKpiHostDetailsResponse.responses[0], null, 2), + JSON.stringify(mockKpiHostDetailsResponse.responses[1], null, 2), + ], + }, + authSuccess: 61, + authSuccessHistogram: [ + { + x: new Date('2019-05-03T13:00:00.000Z').valueOf(), + y: 8, + }, + { + x: new Date('2019-05-04T01:00:00.000Z').valueOf(), + y: 52, + }, + { + x: new Date('2019-05-04T13:00:00.000Z').valueOf(), + y: 1, + }, + ], + authFailure: 15722, + authFailureHistogram: [ + { + x: new Date('2019-05-03T13:00:00.000Z').valueOf(), + y: 11731, + }, + { + x: new Date('2019-05-04T01:00:00.000Z').valueOf(), + y: 3979, + }, + { + x: new Date('2019-05-04T13:00:00.000Z').valueOf(), + y: 12, + }, + ], + uniqueSourceIps: 1407, + uniqueSourceIpsHistogram: [ + { + x: new Date('2019-05-03T13:00:00.000Z').valueOf(), + y: 1182, + }, + { + x: new Date('2019-05-04T01:00:00.000Z').valueOf(), + y: 364, + }, + { + x: new Date('2019-05-04T13:00:00.000Z').valueOf(), + y: 63, + }, + ], + uniqueDestinationIps: 1954, + uniqueDestinationIpsHistogram: [ + { + x: new Date('2019-05-03T13:00:00.000Z').valueOf(), + y: 1809, + }, + { + x: new Date('2019-05-04T01:00:00.000Z').valueOf(), + y: 407, + }, + { + x: new Date('2019-05-04T13:00:00.000Z').valueOf(), + y: 64, + }, + ], +}; diff --git a/x-pack/legacy/plugins/siem/server/lib/kpi_hosts/query_authentication.dsl.test.ts b/x-pack/plugins/siem/server/lib/kpi_hosts/query_authentication.dsl.test.ts similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/kpi_hosts/query_authentication.dsl.test.ts rename to x-pack/plugins/siem/server/lib/kpi_hosts/query_authentication.dsl.test.ts diff --git a/x-pack/legacy/plugins/siem/server/lib/kpi_hosts/query_authentication.dsl.ts b/x-pack/plugins/siem/server/lib/kpi_hosts/query_authentication.dsl.ts similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/kpi_hosts/query_authentication.dsl.ts rename to x-pack/plugins/siem/server/lib/kpi_hosts/query_authentication.dsl.ts diff --git a/x-pack/legacy/plugins/siem/server/lib/kpi_hosts/query_hosts.dsl.ts b/x-pack/plugins/siem/server/lib/kpi_hosts/query_hosts.dsl.ts similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/kpi_hosts/query_hosts.dsl.ts rename to x-pack/plugins/siem/server/lib/kpi_hosts/query_hosts.dsl.ts diff --git a/x-pack/legacy/plugins/siem/server/lib/kpi_hosts/query_unique_ips.dsl.test.ts b/x-pack/plugins/siem/server/lib/kpi_hosts/query_unique_ips.dsl.test.ts similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/kpi_hosts/query_unique_ips.dsl.test.ts rename to x-pack/plugins/siem/server/lib/kpi_hosts/query_unique_ips.dsl.test.ts diff --git a/x-pack/legacy/plugins/siem/server/lib/kpi_hosts/query_unique_ips.dsl.ts b/x-pack/plugins/siem/server/lib/kpi_hosts/query_unique_ips.dsl.ts similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/kpi_hosts/query_unique_ips.dsl.ts rename to x-pack/plugins/siem/server/lib/kpi_hosts/query_unique_ips.dsl.ts diff --git a/x-pack/legacy/plugins/siem/server/lib/kpi_hosts/types.ts b/x-pack/plugins/siem/server/lib/kpi_hosts/types.ts similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/kpi_hosts/types.ts rename to x-pack/plugins/siem/server/lib/kpi_hosts/types.ts diff --git a/x-pack/legacy/plugins/siem/server/lib/kpi_network/elastic_adapter.test.ts b/x-pack/plugins/siem/server/lib/kpi_network/elastic_adapter.test.ts similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/kpi_network/elastic_adapter.test.ts rename to x-pack/plugins/siem/server/lib/kpi_network/elastic_adapter.test.ts diff --git a/x-pack/legacy/plugins/siem/server/lib/kpi_network/elasticsearch_adapter.ts b/x-pack/plugins/siem/server/lib/kpi_network/elasticsearch_adapter.ts similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/kpi_network/elasticsearch_adapter.ts rename to x-pack/plugins/siem/server/lib/kpi_network/elasticsearch_adapter.ts diff --git a/x-pack/legacy/plugins/siem/server/lib/kpi_network/helpers.ts b/x-pack/plugins/siem/server/lib/kpi_network/helpers.ts similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/kpi_network/helpers.ts rename to x-pack/plugins/siem/server/lib/kpi_network/helpers.ts diff --git a/x-pack/legacy/plugins/siem/server/lib/kpi_network/index.ts b/x-pack/plugins/siem/server/lib/kpi_network/index.ts similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/kpi_network/index.ts rename to x-pack/plugins/siem/server/lib/kpi_network/index.ts diff --git a/x-pack/plugins/siem/server/lib/kpi_network/mock.ts b/x-pack/plugins/siem/server/lib/kpi_network/mock.ts new file mode 100644 index 0000000000000..cc0849ccdf1d2 --- /dev/null +++ b/x-pack/plugins/siem/server/lib/kpi_network/mock.ts @@ -0,0 +1,331 @@ +/* + * 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 { DEFAULT_INDEX_PATTERN } from '../../../common/constants'; +import { RequestBasicOptions } from '../framework/types'; + +export const mockOptions: RequestBasicOptions = { + defaultIndex: DEFAULT_INDEX_PATTERN, + sourceConfiguration: { + fields: { + container: 'docker.container.name', + host: 'beat.hostname', + message: ['message', '@message'], + pod: 'kubernetes.pod.name', + tiebreaker: '_doc', + timestamp: '@timestamp', + }, + }, + timerange: { interval: '12h', to: 1549852006071, from: 1549765606071 }, + filterQuery: {}, +}; + +export const mockRequest = { + body: { + operationName: 'GetKpiNetworkQuery', + variables: { + sourceId: 'default', + timerange: { interval: '12h', from: 1557445721842, to: 1557532121842 }, + filterQuery: '', + }, + query: + 'fragment KpiNetworkChartFields on KpiNetworkHistogramData {\n x\n y\n __typename\n}\n\nquery GetKpiNetworkQuery($sourceId: ID!, $timerange: TimerangeInput!, $filterQuery: String, $defaultIndex: [String!]!) {\n source(id: $sourceId) {\n id\n KpiNetwork(timerange: $timerange, filterQuery: $filterQuery, defaultIndex: $defaultIndex) {\n networkEvents\n uniqueFlowId\n uniqueSourcePrivateIps\n uniqueSourcePrivateIpsHistogram {\n ...KpiNetworkChartFields\n __typename\n }\n uniqueDestinationPrivateIps\n uniqueDestinationPrivateIpsHistogram {\n ...KpiNetworkChartFields\n __typename\n }\n dnsQueries\n tlsHandshakes\n __typename\n }\n __typename\n }\n}\n', + }, +}; + +export const mockResponse = { + responses: [ + { + took: 384, + timed_out: false, + _shards: { + total: 10, + successful: 10, + skipped: 0, + failed: 0, + }, + hits: { + total: { + value: 733106, + relation: 'eq', + }, + max_score: null, + hits: [], + }, + status: 200, + }, + { + took: 64, + timed_out: false, + _shards: { + total: 10, + successful: 10, + skipped: 0, + failed: 0, + }, + hits: { + total: { + value: 10942, + relation: 'eq', + }, + max_score: null, + hits: [], + }, + status: 200, + }, + { + took: 224, + timed_out: false, + _shards: { + total: 10, + successful: 10, + skipped: 0, + failed: 0, + }, + hits: { + total: { + value: 480755, + relation: 'eq', + }, + max_score: null, + hits: [], + }, + aggregations: { + source: { + histogram: { + buckets: [ + { + key_as_string: '2019-05-09T23:00:00.000Z', + key: 1557442800000, + doc_count: 42109, + count: { + value: 14, + }, + }, + { + key_as_string: '2019-05-10T11:00:00.000Z', + key: 1557486000000, + doc_count: 437160, + count: { + value: 385, + }, + }, + { + key_as_string: '2019-05-10T23:00:00.000Z', + key: 1557529200000, + doc_count: 1486, + count: { + value: 7, + }, + }, + ], + interval: '12h', + }, + unique_private_ips: { + value: 387, + }, + }, + destination: { + histogram: { + buckets: [ + { + key_as_string: '2019-05-09T23:00:00.000Z', + key: 1557442800000, + doc_count: 36253, + count: { + value: 11, + }, + }, + { + key_as_string: '2019-05-10T11:00:00.000Z', + key: 1557486000000, + doc_count: 421719, + count: { + value: 877, + }, + }, + { + key_as_string: '2019-05-10T23:00:00.000Z', + key: 1557529200000, + doc_count: 1311, + count: { + value: 7, + }, + }, + ], + interval: '12h', + }, + unique_private_ips: { + value: 878, + }, + }, + }, + status: 200, + }, + { + took: 384, + timed_out: false, + _shards: { + total: 10, + successful: 10, + skipped: 0, + failed: 0, + }, + hits: { + total: { + value: 733106, + relation: 'eq', + }, + max_score: null, + hits: [], + }, + aggregations: { + unique_flow_id: { + value: 195415, + }, + }, + status: 200, + }, + { + took: 57, + timed_out: false, + _shards: { + total: 10, + successful: 10, + skipped: 0, + failed: 0, + }, + hits: { + total: { + value: 54482, + relation: 'eq', + }, + max_score: null, + hits: [], + }, + status: 200, + }, + ], +}; +const mockMsearchHeader = { + index: 'defaultIndex', + allowNoIndices: true, + ignoreUnavailable: true, +}; +const mockMsearchBody = { + query: {}, + aggregations: {}, + size: 0, + track_total_hits: false, +}; +export const mockNetworkEventsQueryDsl = [mockMsearchHeader, mockMsearchBody]; +export const mockUniqueFlowIdsQueryDsl = [ + mockMsearchHeader, + { mockUniqueFlowIdsQueryDsl: 'mockUniqueFlowIdsQueryDsl' }, +]; +export const mockUniquePrvateIpsQueryDsl = [ + mockMsearchHeader, + { mockUniquePrvateIpsQueryDsl: 'mockUniquePrvateIpsQueryDsl' }, +]; +export const mockDnsQueryDsl = [mockMsearchHeader, { mockDnsQueryDsl: 'mockDnsQueryDsl' }]; +export const mockTlsHandshakesQueryDsl = [ + mockMsearchHeader, + { mockTlsHandshakesQueryDsl: 'mockTlsHandshakesQueryDsl' }, +]; + +export const mockMsearchOptions = { + body: [ + ...mockNetworkEventsQueryDsl, + ...mockDnsQueryDsl, + ...mockUniquePrvateIpsQueryDsl, + ...mockUniqueFlowIdsQueryDsl, + ...mockTlsHandshakesQueryDsl, + ], +}; + +const mockDsl = [ + JSON.stringify({ ...mockNetworkEventsQueryDsl[0], body: mockNetworkEventsQueryDsl[1] }, null, 2), + JSON.stringify({ ...mockDnsQueryDsl[0], body: mockDnsQueryDsl[1] }, null, 2), + JSON.stringify( + { ...mockUniquePrvateIpsQueryDsl[0], body: mockUniquePrvateIpsQueryDsl[1] }, + null, + 2 + ), + JSON.stringify({ ...mockUniqueFlowIdsQueryDsl[0], body: mockUniqueFlowIdsQueryDsl[1] }, null, 2), + JSON.stringify({ ...mockTlsHandshakesQueryDsl[0], body: mockTlsHandshakesQueryDsl[1] }, null, 2), +]; + +export const mockResult = { + inspect: { + dsl: mockDsl, + response: [ + JSON.stringify(mockResponse.responses[0], null, 2), + JSON.stringify(mockResponse.responses[1], null, 2), + JSON.stringify(mockResponse.responses[2], null, 2), + JSON.stringify(mockResponse.responses[3], null, 2), + JSON.stringify(mockResponse.responses[4], null, 2), + ], + }, + dnsQueries: 10942, + networkEvents: 733106, + tlsHandshakes: 54482, + uniqueDestinationPrivateIps: 878, + uniqueDestinationPrivateIpsHistogram: [ + { + x: new Date('2019-05-09T23:00:00.000Z').valueOf(), + y: 11, + }, + { + x: new Date('2019-05-10T11:00:00.000Z').valueOf(), + y: 877, + }, + { + x: new Date('2019-05-10T23:00:00.000Z').valueOf(), + y: 7, + }, + ], + uniqueFlowId: 195415, + uniqueSourcePrivateIps: 387, + uniqueSourcePrivateIpsHistogram: [ + { + x: new Date('2019-05-09T23:00:00.000Z').valueOf(), + y: 14, + }, + { + x: new Date('2019-05-10T11:00:00.000Z').valueOf(), + y: 385, + }, + { + x: new Date('2019-05-10T23:00:00.000Z').valueOf(), + y: 7, + }, + ], +}; + +export const mockResponseNoData = { + responses: [null, null, null, null, null], +}; + +export const mockResultNoData = { + inspect: { + dsl: mockDsl, + response: [ + JSON.stringify(mockResponseNoData.responses[0], null, 2), + JSON.stringify(mockResponseNoData.responses[1], null, 2), + JSON.stringify(mockResponseNoData.responses[2], null, 2), + JSON.stringify(mockResponseNoData.responses[3], null, 2), + JSON.stringify(mockResponseNoData.responses[4], null, 2), + ], + }, + networkEvents: null, + uniqueFlowId: null, + uniqueSourcePrivateIps: null, + uniqueSourcePrivateIpsHistogram: null, + uniqueDestinationPrivateIps: null, + uniqueDestinationPrivateIpsHistogram: null, + dnsQueries: null, + tlsHandshakes: null, +}; diff --git a/x-pack/legacy/plugins/siem/server/lib/kpi_network/query_dns.dsl.ts b/x-pack/plugins/siem/server/lib/kpi_network/query_dns.dsl.ts similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/kpi_network/query_dns.dsl.ts rename to x-pack/plugins/siem/server/lib/kpi_network/query_dns.dsl.ts diff --git a/x-pack/legacy/plugins/siem/server/lib/kpi_network/query_network_events.ts b/x-pack/plugins/siem/server/lib/kpi_network/query_network_events.ts similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/kpi_network/query_network_events.ts rename to x-pack/plugins/siem/server/lib/kpi_network/query_network_events.ts diff --git a/x-pack/legacy/plugins/siem/server/lib/kpi_network/query_tls_handshakes.dsl.ts b/x-pack/plugins/siem/server/lib/kpi_network/query_tls_handshakes.dsl.ts similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/kpi_network/query_tls_handshakes.dsl.ts rename to x-pack/plugins/siem/server/lib/kpi_network/query_tls_handshakes.dsl.ts diff --git a/x-pack/legacy/plugins/siem/server/lib/kpi_network/query_unique_flow.ts b/x-pack/plugins/siem/server/lib/kpi_network/query_unique_flow.ts similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/kpi_network/query_unique_flow.ts rename to x-pack/plugins/siem/server/lib/kpi_network/query_unique_flow.ts diff --git a/x-pack/legacy/plugins/siem/server/lib/kpi_network/query_unique_private_ips.dsl.ts b/x-pack/plugins/siem/server/lib/kpi_network/query_unique_private_ips.dsl.ts similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/kpi_network/query_unique_private_ips.dsl.ts rename to x-pack/plugins/siem/server/lib/kpi_network/query_unique_private_ips.dsl.ts diff --git a/x-pack/legacy/plugins/siem/server/lib/kpi_network/types.ts b/x-pack/plugins/siem/server/lib/kpi_network/types.ts similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/kpi_network/types.ts rename to x-pack/plugins/siem/server/lib/kpi_network/types.ts diff --git a/x-pack/plugins/siem/server/lib/machine_learning/index.ts b/x-pack/plugins/siem/server/lib/machine_learning/index.ts new file mode 100644 index 0000000000000..35789b5e202e2 --- /dev/null +++ b/x-pack/plugins/siem/server/lib/machine_learning/index.ts @@ -0,0 +1,90 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { SearchResponse } from 'elasticsearch'; + +import { AlertServices } from '../../../../alerting/server'; +import { AnomalyRecordDoc as Anomaly } from '../../../../ml/common/types/anomalies'; + +export { Anomaly }; +export type AnomalyResults = SearchResponse; + +export interface AnomaliesSearchParams { + jobIds: string[]; + threshold: number; + earliestMs: number; + latestMs: number; + maxRecords?: number; +} + +export const getAnomalies = async ( + params: AnomaliesSearchParams, + callCluster: AlertServices['callCluster'] +): Promise => { + const boolCriteria = buildCriteria(params); + + return callCluster('search', { + index: '.ml-anomalies-*', + size: params.maxRecords || 100, + body: { + query: { + bool: { + filter: [ + { + query_string: { + query: 'result_type:record', + analyze_wildcard: false, + }, + }, + { + bool: { + must: boolCriteria, + }, + }, + ], + }, + }, + sort: [{ record_score: { order: 'desc' } }], + }, + }); +}; + +const buildCriteria = (params: AnomaliesSearchParams): object[] => { + const { earliestMs, jobIds, latestMs, threshold } = params; + const jobIdsFilterable = jobIds.length > 0 && !(jobIds.length === 1 && jobIds[0] === '*'); + + const boolCriteria: object[] = [ + { + range: { + timestamp: { + gte: earliestMs, + lte: latestMs, + format: 'epoch_millis', + }, + }, + }, + { + range: { + record_score: { + gte: threshold, + }, + }, + }, + ]; + + if (jobIdsFilterable) { + const jobIdFilter = jobIds.map(jobId => `job_id:${jobId}`).join(' OR '); + + boolCriteria.push({ + query_string: { + analyze_wildcard: false, + query: jobIdFilter, + }, + }); + } + + return boolCriteria; +}; diff --git a/x-pack/legacy/plugins/siem/server/lib/matrix_histogram/elasticsearch_adapter.ts b/x-pack/plugins/siem/server/lib/matrix_histogram/elasticsearch_adapter.ts similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/matrix_histogram/elasticsearch_adapter.ts rename to x-pack/plugins/siem/server/lib/matrix_histogram/elasticsearch_adapter.ts diff --git a/x-pack/legacy/plugins/siem/server/lib/matrix_histogram/elasticseatch_adapter.test.ts b/x-pack/plugins/siem/server/lib/matrix_histogram/elasticseatch_adapter.test.ts similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/matrix_histogram/elasticseatch_adapter.test.ts rename to x-pack/plugins/siem/server/lib/matrix_histogram/elasticseatch_adapter.test.ts diff --git a/x-pack/legacy/plugins/siem/server/lib/matrix_histogram/index.ts b/x-pack/plugins/siem/server/lib/matrix_histogram/index.ts similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/matrix_histogram/index.ts rename to x-pack/plugins/siem/server/lib/matrix_histogram/index.ts diff --git a/x-pack/plugins/siem/server/lib/matrix_histogram/mock.ts b/x-pack/plugins/siem/server/lib/matrix_histogram/mock.ts new file mode 100644 index 0000000000000..1d1ebfff936d2 --- /dev/null +++ b/x-pack/plugins/siem/server/lib/matrix_histogram/mock.ts @@ -0,0 +1,118 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { DEFAULT_INDEX_PATTERN } from '../../../common/constants'; +import { HistogramType } from '../../graphql/types'; + +export const mockAlertsHistogramDataResponse = { + took: 513, + timed_out: false, + _shards: { + total: 62, + successful: 61, + skipped: 0, + failed: 1, + failures: [ + { + shard: 0, + index: 'auditbeat-7.2.0', + node: 'jBC5kcOeT1exvECDMrk5Ug', + reason: { + type: 'illegal_argument_exception', + reason: + 'Fielddata is disabled on text fields by default. Set fielddata=true on [event.module] in order to load fielddata in memory by uninverting the inverted index. Note that this can however use significant memory. Alternatively use a keyword field instead.', + }, + }, + ], + }, + hits: { + total: { + value: 1599508, + relation: 'eq', + }, + max_score: null, + hits: [], + }, + aggregations: { + alertsGroup: { + doc_count_error_upper_bound: 0, + sum_other_doc_count: 802087, + buckets: [ + { + key: 'All others', + doc_count: 451519, + alerts: { + buckets: [ + { + key_as_string: '2019-12-15T09:30:00.000Z', + key: 1576402200000, + doc_count: 3008, + }, + { + key_as_string: '2019-12-15T10:00:00.000Z', + key: 1576404000000, + doc_count: 8671, + }, + ], + }, + }, + { + key: 'suricata', + doc_count: 345902, + alerts: { + buckets: [ + { + key_as_string: '2019-12-15T09:30:00.000Z', + key: 1576402200000, + doc_count: 1785, + }, + { + key_as_string: '2019-12-15T10:00:00.000Z', + key: 1576404000000, + doc_count: 5342, + }, + ], + }, + }, + ], + }, + }, +}; +export const mockAlertsHistogramDataFormattedResponse = [ + { + x: 1576402200000, + y: 3008, + g: 'All others', + }, + { + x: 1576404000000, + y: 8671, + g: 'All others', + }, + { + x: 1576402200000, + y: 1785, + g: 'suricata', + }, + { + x: 1576404000000, + y: 5342, + g: 'suricata', + }, +]; +export const mockAlertsHistogramQueryDsl = 'mockAlertsHistogramQueryDsl'; +export const mockRequest = 'mockRequest'; +export const mockOptions = { + sourceConfiguration: { field: {} }, + timerange: { + to: 9999, + from: 1234, + }, + defaultIndex: DEFAULT_INDEX_PATTERN, + filterQuery: '', + stackByField: 'event.module', + histogramType: HistogramType.alerts, +}; diff --git a/x-pack/legacy/plugins/siem/server/lib/matrix_histogram/query.anomalies_over_time.dsl.ts b/x-pack/plugins/siem/server/lib/matrix_histogram/query.anomalies_over_time.dsl.ts similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/matrix_histogram/query.anomalies_over_time.dsl.ts rename to x-pack/plugins/siem/server/lib/matrix_histogram/query.anomalies_over_time.dsl.ts diff --git a/x-pack/legacy/plugins/siem/server/lib/matrix_histogram/query.authentications_over_time.dsl.ts b/x-pack/plugins/siem/server/lib/matrix_histogram/query.authentications_over_time.dsl.ts similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/matrix_histogram/query.authentications_over_time.dsl.ts rename to x-pack/plugins/siem/server/lib/matrix_histogram/query.authentications_over_time.dsl.ts diff --git a/x-pack/legacy/plugins/siem/server/lib/matrix_histogram/query.events_over_time.dsl.ts b/x-pack/plugins/siem/server/lib/matrix_histogram/query.events_over_time.dsl.ts similarity index 83% rename from x-pack/legacy/plugins/siem/server/lib/matrix_histogram/query.events_over_time.dsl.ts rename to x-pack/plugins/siem/server/lib/matrix_histogram/query.events_over_time.dsl.ts index 3a4281b980cc4..63649a1064b02 100644 --- a/x-pack/legacy/plugins/siem/server/lib/matrix_histogram/query.events_over_time.dsl.ts +++ b/x-pack/plugins/siem/server/lib/matrix_histogram/query.events_over_time.dsl.ts @@ -3,9 +3,13 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ + +import { showAllOthersBucket } from '../../../common/constants'; import { createQueryFilterClauses, calculateTimeSeriesInterval } from '../../utils/build_query'; import { MatrixHistogramRequestOptions } from '../framework'; +import * as i18n from './translations'; + export const buildEventsOverTimeQuery = ({ filterQuery, timerange: { from, to }, @@ -41,11 +45,19 @@ export const buildEventsOverTimeQuery = ({ }, }, }; + + const missing = + stackByField != null && showAllOthersBucket.includes(stackByField) + ? { + missing: stackByField?.endsWith('.ip') ? '0.0.0.0' : i18n.ALL_OTHERS, + } + : {}; + return { eventActionGroup: { terms: { field: stackByField, - missing: 'All others', + ...missing, order: { _count: 'desc', }, diff --git a/x-pack/legacy/plugins/siem/server/lib/matrix_histogram/query_alerts.dsl.ts b/x-pack/plugins/siem/server/lib/matrix_histogram/query_alerts.dsl.ts similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/matrix_histogram/query_alerts.dsl.ts rename to x-pack/plugins/siem/server/lib/matrix_histogram/query_alerts.dsl.ts diff --git a/x-pack/legacy/plugins/siem/server/lib/matrix_histogram/query_dns_histogram.dsl.ts b/x-pack/plugins/siem/server/lib/matrix_histogram/query_dns_histogram.dsl.ts similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/matrix_histogram/query_dns_histogram.dsl.ts rename to x-pack/plugins/siem/server/lib/matrix_histogram/query_dns_histogram.dsl.ts diff --git a/x-pack/plugins/siem/server/lib/matrix_histogram/translations.ts b/x-pack/plugins/siem/server/lib/matrix_histogram/translations.ts new file mode 100644 index 0000000000000..413acaa2d4b0a --- /dev/null +++ b/x-pack/plugins/siem/server/lib/matrix_histogram/translations.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 { i18n } from '@kbn/i18n'; + +export const ALL_OTHERS = i18n.translate( + 'xpack.siem.detectionEngine.signals.histogram.allOthersGroupingLabel', + { + defaultMessage: 'All others', + } +); diff --git a/x-pack/legacy/plugins/siem/server/lib/matrix_histogram/types.ts b/x-pack/plugins/siem/server/lib/matrix_histogram/types.ts similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/matrix_histogram/types.ts rename to x-pack/plugins/siem/server/lib/matrix_histogram/types.ts diff --git a/x-pack/legacy/plugins/siem/server/lib/matrix_histogram/utils.ts b/x-pack/plugins/siem/server/lib/matrix_histogram/utils.ts similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/matrix_histogram/utils.ts rename to x-pack/plugins/siem/server/lib/matrix_histogram/utils.ts diff --git a/x-pack/legacy/plugins/siem/server/lib/network/__snapshots__/elastic_adapter.test.ts.snap b/x-pack/plugins/siem/server/lib/network/__snapshots__/elastic_adapter.test.ts.snap similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/network/__snapshots__/elastic_adapter.test.ts.snap rename to x-pack/plugins/siem/server/lib/network/__snapshots__/elastic_adapter.test.ts.snap diff --git a/x-pack/legacy/plugins/siem/server/lib/network/elastic_adapter.test.ts b/x-pack/plugins/siem/server/lib/network/elastic_adapter.test.ts similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/network/elastic_adapter.test.ts rename to x-pack/plugins/siem/server/lib/network/elastic_adapter.test.ts diff --git a/x-pack/legacy/plugins/siem/server/lib/network/elasticsearch_adapter.ts b/x-pack/plugins/siem/server/lib/network/elasticsearch_adapter.ts similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/network/elasticsearch_adapter.ts rename to x-pack/plugins/siem/server/lib/network/elasticsearch_adapter.ts diff --git a/x-pack/legacy/plugins/siem/server/lib/network/index.ts b/x-pack/plugins/siem/server/lib/network/index.ts similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/network/index.ts rename to x-pack/plugins/siem/server/lib/network/index.ts diff --git a/x-pack/plugins/siem/server/lib/network/mock.ts b/x-pack/plugins/siem/server/lib/network/mock.ts new file mode 100644 index 0000000000000..38e82a4f19dca --- /dev/null +++ b/x-pack/plugins/siem/server/lib/network/mock.ts @@ -0,0 +1,1675 @@ +/* + * 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 { DEFAULT_INDEX_PATTERN } from '../../../common/constants'; +import { Direction, FlowTargetSourceDest, NetworkTopTablesFields } from '../../graphql/types'; + +import { NetworkTopNFlowRequestOptions } from '.'; + +export const mockOptions: NetworkTopNFlowRequestOptions = { + defaultIndex: DEFAULT_INDEX_PATTERN, + sourceConfiguration: { + fields: { + container: 'docker.container.name', + host: 'beat.hostname', + message: ['message', '@message'], + pod: 'kubernetes.pod.name', + tiebreaker: '_doc', + timestamp: '@timestamp', + }, + }, + timerange: { interval: '12h', to: 1549852006071, from: 1549765606071 }, + pagination: { + activePage: 0, + cursorStart: 0, + fakePossibleCount: 50, + querySize: 10, + }, + filterQuery: {}, + fields: [ + 'totalCount', + 'source.ip', + 'source.domain', + 'source.__typename', + 'destination.ip', + 'destination.domain', + 'destination.__typename', + 'event.duration', + 'event.__typename', + 'network.bytes_in', + 'network.bytes_out', + 'network.__typename', + '__typename', + 'edges.cursor.value', + 'edges.cursor.__typename', + 'edges.__typename', + 'pageInfo.activePage', + 'pageInfo.__typename', + 'pageInfo.fakeTotalCount', + 'pageInfo.__typename', + 'pageInfo.showMorePagesIndicator', + 'pageInfo.__typename', + '__typename', + ], + networkTopNFlowSort: { field: NetworkTopTablesFields.bytes_out, direction: Direction.desc }, + flowTarget: FlowTargetSourceDest.source, +}; + +export const mockRequest = { + body: { + operationName: 'GetNetworkTopNFlowQuery', + variables: { + filterQuery: '', + flowTarget: FlowTargetSourceDest.source, + pagination: { + activePage: 0, + cursorStart: 0, + fakePossibleCount: 50, + querySize: 10, + }, + sourceId: 'default', + timerange: { interval: '12h', from: 1549765830772, to: 1549852230772 }, + }, + query: ` + query GetNetworkTopNFlowQuery( + $sourceId: ID! + $ip: String + $filterQuery: String + $pagination: PaginationInputPaginated! + $sort: NetworkTopTablesSortField! + $flowTarget: FlowTargetSourceDest! + $timerange: TimerangeInput! + $defaultIndex: [String!]! + $inspect: Boolean! + ) { + source(id: $sourceId) { + id + NetworkTopNFlow( + filterQuery: $filterQuery + flowTarget: $flowTarget + ip: $ip + pagination: $pagination + sort: $sort + timerange: $timerange + defaultIndex: $defaultIndex + ) { + totalCount + edges { + node { + source { + autonomous_system { + name + number + } + domain + ip + location { + geo { + continent_name + country_name + country_iso_code + city_name + region_iso_code + region_name + } + flowTarget + } + flows + destination_ips + } + destination { + autonomous_system { + name + number + } + domain + ip + location { + geo { + continent_name + country_name + country_iso_code + city_name + region_iso_code + region_name + } + flowTarget + } + flows + source_ips + } + network { + bytes_in + bytes_out + } + } + cursor { + value + } + } + pageInfo { + activePage + fakeTotalCount + showMorePagesIndicator + } + inspect @include(if: $inspect) { + dsl + response + } + } + } + } +`, + }, +}; + +export const mockResponse = { + took: 122, + timed_out: false, + _shards: { + total: 11, + successful: 11, + skipped: 0, + failed: 0, + }, + hits: { + max_score: null, + hits: [], + }, + aggregations: { + top_n_flow_count: { + value: 545, + }, + [FlowTargetSourceDest.source]: { + buckets: [ + { + key: '1.1.1.1', + flows: { value: 1234567 }, + destination_ips: { value: 345345 }, + bytes_in: { + value: 11276023407, + }, + bytes_out: { + value: 1025631, + }, + location: { + doc_count: 14, + top_geo: { + hits: { + total: { + value: 14, + relation: 'eq', + }, + max_score: 1, + hits: [ + { + _index: 'filebeat-8.0.0-2019.06.19-000005', + _type: '_doc', + _id: 'dd4fa2d4bd-692279846149410', + _score: 1, + _source: { + source: { + geo: { + continent_name: 'North America', + region_iso_code: 'US-PA', + city_name: 'Philadelphia', + country_iso_code: 'US', + region_name: 'Pennsylvania', + location: { + lon: -75.1534, + lat: 39.9359, + }, + }, + }, + }, + }, + ], + }, + }, + }, + autonomous_system: { + doc_count: 14, + top_as: { + hits: { + total: { + value: 14, + relation: 'eq', + }, + max_score: 1, + hits: [ + { + _index: 'filebeat-8.0.0-2019.06.19-000005', + _type: '_doc', + _id: 'dd4fa2d4bd-692279846149410', + _score: 1, + _source: { + source: { + as: { + number: 3356, + organization: { + name: 'Level 3 Parent, LLC', + }, + }, + }, + }, + }, + ], + }, + }, + }, + domain: { + buckets: [ + { + key: 'test.1.net', + }, + ], + }, + }, + { + key: '2.2.2.2', + flows: { value: 1234567 }, + destination_ips: { value: 345345 }, + bytes_in: { + value: 5469323342, + }, + bytes_out: { + value: 2811441, + }, + location: { + doc_count: 14, + top_geo: { + hits: { + total: { + value: 14, + relation: 'eq', + }, + max_score: 1, + hits: [ + { + _index: 'filebeat-8.0.0-2019.06.19-000005', + _type: '_doc', + _id: 'dd4fa2d4bd-692279846149410', + _score: 1, + _source: { + source: { + geo: { + continent_name: 'North America', + region_iso_code: 'US-PA', + city_name: 'Philadelphia', + country_iso_code: 'US', + region_name: 'Pennsylvania', + location: { + lon: -75.1534, + lat: 39.9359, + }, + }, + }, + }, + }, + ], + }, + }, + }, + autonomous_system: { + doc_count: 14, + top_as: { + hits: { + total: { + value: 14, + relation: 'eq', + }, + max_score: 1, + hits: [ + { + _index: 'filebeat-8.0.0-2019.06.19-000005', + _type: '_doc', + _id: 'dd4fa2d4bd-692279846149410', + _score: 1, + _source: { + source: { + as: { + number: 3356, + organization: { + name: 'Level 3 Parent, LLC', + }, + }, + }, + }, + }, + ], + }, + }, + }, + domain: { + buckets: [ + { + key: 'test.2.net', + }, + ], + }, + }, + { + key: '3.3.3.3', + flows: { value: 1234567 }, + destination_ips: { value: 345345 }, + bytes_in: { + value: 3807671322, + }, + bytes_out: { + value: 4494034, + }, + location: { + doc_count: 14, + top_geo: { + hits: { + total: { + value: 14, + relation: 'eq', + }, + max_score: 1, + hits: [ + { + _index: 'filebeat-8.0.0-2019.06.19-000005', + _type: '_doc', + _id: 'dd4fa2d4bd-692279846149410', + _score: 1, + _source: { + source: { + geo: { + continent_name: 'North America', + region_iso_code: 'US-PA', + city_name: 'Philadelphia', + country_iso_code: 'US', + region_name: 'Pennsylvania', + location: { + lon: -75.1534, + lat: 39.9359, + }, + }, + }, + }, + }, + ], + }, + }, + }, + autonomous_system: { + doc_count: 14, + top_as: { + hits: { + total: { + value: 14, + relation: 'eq', + }, + max_score: 1, + hits: [ + { + _index: 'filebeat-8.0.0-2019.06.19-000005', + _type: '_doc', + _id: 'dd4fa2d4bd-692279846149410', + _score: 1, + _source: { + source: { + as: { + number: 3356, + organization: { + name: 'Level 3 Parent, LLC', + }, + }, + }, + }, + }, + ], + }, + }, + }, + domain: { + buckets: [ + { + key: 'test.3.com', + }, + { + key: 'test.3-duplicate.com', + }, + ], + }, + }, + { + key: '4.4.4.4', + flows: { value: 1234567 }, + destination_ips: { value: 345345 }, + bytes_in: { + value: 166517626, + }, + bytes_out: { + value: 3194782, + }, + location: { + doc_count: 14, + top_geo: { + hits: { + total: { + value: 14, + relation: 'eq', + }, + max_score: 1, + hits: [ + { + _index: 'filebeat-8.0.0-2019.06.19-000005', + _type: '_doc', + _id: 'dd4fa2d4bd-692279846149410', + _score: 1, + _source: { + source: { + geo: { + continent_name: 'North America', + region_iso_code: 'US-PA', + city_name: 'Philadelphia', + country_iso_code: 'US', + region_name: 'Pennsylvania', + location: { + lon: -75.1534, + lat: 39.9359, + }, + }, + }, + }, + }, + ], + }, + }, + }, + autonomous_system: { + doc_count: 14, + top_as: { + hits: { + total: { + value: 14, + relation: 'eq', + }, + max_score: 1, + hits: [ + { + _index: 'filebeat-8.0.0-2019.06.19-000005', + _type: '_doc', + _id: 'dd4fa2d4bd-692279846149410', + _score: 1, + _source: { + source: { + as: { + number: 3356, + organization: { + name: 'Level 3 Parent, LLC', + }, + }, + }, + }, + }, + ], + }, + }, + }, + domain: { + buckets: [ + { + key: 'test.4.com', + }, + ], + }, + }, + { + key: '5.5.5.5', + flows: { value: 1234567 }, + destination_ips: { value: 345345 }, + bytes_in: { + value: 104785026, + }, + bytes_out: { + value: 1838597, + }, + location: { + doc_count: 14, + top_geo: { + hits: { + total: { + value: 14, + relation: 'eq', + }, + max_score: 1, + hits: [ + { + _index: 'filebeat-8.0.0-2019.06.19-000005', + _type: '_doc', + _id: 'dd4fa2d4bd-692279846149410', + _score: 1, + _source: { + source: { + geo: { + continent_name: 'North America', + region_iso_code: 'US-PA', + city_name: 'Philadelphia', + country_iso_code: 'US', + region_name: 'Pennsylvania', + location: { + lon: -75.1534, + lat: 39.9359, + }, + }, + }, + }, + }, + ], + }, + }, + }, + autonomous_system: { + doc_count: 14, + top_as: { + hits: { + total: { + value: 14, + relation: 'eq', + }, + max_score: 1, + hits: [ + { + _index: 'filebeat-8.0.0-2019.06.19-000005', + _type: '_doc', + _id: 'dd4fa2d4bd-692279846149410', + _score: 1, + _source: { + source: { + as: { + number: 3356, + organization: { + name: 'Level 3 Parent, LLC', + }, + }, + }, + }, + }, + ], + }, + }, + }, + domain: { + buckets: [ + { + key: 'test.5.com', + }, + ], + }, + }, + { + key: '6.6.6.6', + flows: { value: 1234567 }, + destination_ips: { value: 345345 }, + bytes_in: { + value: 28804250, + }, + bytes_out: { + value: 482982, + }, + location: { + doc_count: 14, + top_geo: { + hits: { + total: { + value: 14, + relation: 'eq', + }, + max_score: 1, + hits: [ + { + _index: 'filebeat-8.0.0-2019.06.19-000005', + _type: '_doc', + _id: 'dd4fa2d4bd-692279846149410', + _score: 1, + _source: { + source: { + geo: { + continent_name: 'North America', + region_iso_code: 'US-PA', + city_name: 'Philadelphia', + country_iso_code: 'US', + region_name: 'Pennsylvania', + location: { + lon: -75.1534, + lat: 39.9359, + }, + }, + }, + }, + }, + ], + }, + }, + }, + autonomous_system: { + doc_count: 14, + top_as: { + hits: { + total: { + value: 14, + relation: 'eq', + }, + max_score: 1, + hits: [ + { + _index: 'filebeat-8.0.0-2019.06.19-000005', + _type: '_doc', + _id: 'dd4fa2d4bd-692279846149410', + _score: 1, + _source: { + source: { + as: { + number: 3356, + organization: { + name: 'Level 3 Parent, LLC', + }, + }, + }, + }, + }, + ], + }, + }, + }, + domain: { + doc_count_error_upper_bound: 0, + sum_other_doc_count: 31, + buckets: [ + { + key: 'test.6.com', + }, + ], + }, + }, + { + key: '7.7.7.7', + flows: { value: 1234567 }, + destination_ips: { value: 345345 }, + bytes_in: { + value: 23032363, + }, + bytes_out: { + value: 400623, + }, + location: { + doc_count: 14, + top_geo: { + hits: { + total: { + value: 14, + relation: 'eq', + }, + max_score: 1, + hits: [ + { + _index: 'filebeat-8.0.0-2019.06.19-000005', + _type: '_doc', + _id: 'dd4fa2d4bd-692279846149410', + _score: 1, + _source: { + source: { + geo: { + continent_name: 'North America', + region_iso_code: 'US-PA', + city_name: 'Philadelphia', + country_iso_code: 'US', + region_name: 'Pennsylvania', + location: { + lon: -75.1534, + lat: 39.9359, + }, + }, + }, + }, + }, + ], + }, + }, + }, + autonomous_system: { + doc_count: 14, + top_as: { + hits: { + total: { + value: 14, + relation: 'eq', + }, + max_score: 1, + hits: [ + { + _index: 'filebeat-8.0.0-2019.06.19-000005', + _type: '_doc', + _id: 'dd4fa2d4bd-692279846149410', + _score: 1, + _source: { + source: { + as: { + number: 3356, + organization: { + name: 'Level 3 Parent, LLC', + }, + }, + }, + }, + }, + ], + }, + }, + }, + domain: { + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, + buckets: [ + { + key: 'test.7.com', + }, + ], + }, + }, + { + key: '8.8.8.8', + flows: { value: 1234567 }, + destination_ips: { value: 345345 }, + bytes_in: { + value: 21424889, + }, + bytes_out: { + value: 344357, + }, + location: { + doc_count: 14, + top_geo: { + hits: { + total: { + value: 14, + relation: 'eq', + }, + max_score: 1, + hits: [ + { + _index: 'filebeat-8.0.0-2019.06.19-000005', + _type: '_doc', + _id: 'dd4fa2d4bd-692279846149410', + _score: 1, + _source: { + source: { + geo: { + continent_name: 'North America', + region_iso_code: 'US-PA', + city_name: 'Philadelphia', + country_iso_code: 'US', + region_name: 'Pennsylvania', + location: { + lon: -75.1534, + lat: 39.9359, + }, + }, + }, + }, + }, + ], + }, + }, + }, + autonomous_system: { + doc_count: 14, + top_as: { + hits: { + total: { + value: 14, + relation: 'eq', + }, + max_score: 1, + hits: [ + { + _index: 'filebeat-8.0.0-2019.06.19-000005', + _type: '_doc', + _id: 'dd4fa2d4bd-692279846149410', + _score: 1, + _source: { + source: { + as: { + number: 3356, + organization: { + name: 'Level 3 Parent, LLC', + }, + }, + }, + }, + }, + ], + }, + }, + }, + domain: { + buckets: [ + { + key: 'test.8.com', + }, + ], + }, + }, + { + key: '9.9.9.9', + flows: { value: 1234567 }, + destination_ips: { value: 345345 }, + bytes_in: { + value: 19205000, + }, + bytes_out: { + value: 355663, + }, + location: { + doc_count: 14, + top_geo: { + hits: { + total: { + value: 14, + relation: 'eq', + }, + max_score: 1, + hits: [ + { + _index: 'filebeat-8.0.0-2019.06.19-000005', + _type: '_doc', + _id: 'dd4fa2d4bd-692279846149410', + _score: 1, + _source: { + source: { + geo: { + continent_name: 'North America', + region_iso_code: 'US-PA', + city_name: 'Philadelphia', + country_iso_code: 'US', + region_name: 'Pennsylvania', + location: { + lon: -75.1534, + lat: 39.9359, + }, + }, + }, + }, + }, + ], + }, + }, + }, + autonomous_system: { + doc_count: 14, + top_as: { + hits: { + total: { + value: 14, + relation: 'eq', + }, + max_score: 1, + hits: [ + { + _index: 'filebeat-8.0.0-2019.06.19-000005', + _type: '_doc', + _id: 'dd4fa2d4bd-692279846149410', + _score: 1, + _source: { + source: { + as: { + number: 3356, + organization: { + name: 'Level 3 Parent, LLC', + }, + }, + }, + }, + }, + ], + }, + }, + }, + domain: { + buckets: [ + { + key: 'test.9.com', + }, + ], + }, + }, + { + key: '10.10.10.10', + flows: { value: 1234567 }, + destination_ips: { value: 345345 }, + bytes_in: { + value: 11407633, + }, + bytes_out: { + value: 199360, + }, + location: { + doc_count: 14, + top_geo: { + hits: { + total: { + value: 14, + relation: 'eq', + }, + max_score: 1, + hits: [ + { + _index: 'filebeat-8.0.0-2019.06.19-000005', + _type: '_doc', + _id: 'dd4fa2d4bd-692279846149410', + _score: 1, + _source: { + source: { + geo: { + continent_name: 'North America', + region_iso_code: 'US-PA', + city_name: 'Philadelphia', + country_iso_code: 'US', + region_name: 'Pennsylvania', + location: { + lon: -75.1534, + lat: 39.9359, + }, + }, + }, + }, + }, + ], + }, + }, + }, + autonomous_system: { + doc_count: 14, + top_as: { + hits: { + total: { + value: 14, + relation: 'eq', + }, + max_score: 1, + hits: [ + { + _index: 'filebeat-8.0.0-2019.06.19-000005', + _type: '_doc', + _id: 'dd4fa2d4bd-692279846149410', + _score: 1, + _source: { + source: { + as: { + number: 3356, + organization: { + name: 'Level 3 Parent, LLC', + }, + }, + }, + }, + }, + ], + }, + }, + }, + domain: { + buckets: [ + { + key: 'test.10.com', + }, + ], + }, + }, + { + key: '11.11.11.11', + flows: { value: 1234567 }, + destination_ips: { value: 345345 }, + bytes_in: { + value: 11393327, + }, + bytes_out: { + value: 195914, + }, + location: { + doc_count: 14, + top_geo: { + hits: { + total: { + value: 14, + relation: 'eq', + }, + max_score: 1, + hits: [ + { + _index: 'filebeat-8.0.0-2019.06.19-000005', + _type: '_doc', + _id: 'dd4fa2d4bd-692279846149410', + _score: 1, + _source: { + source: { + geo: { + continent_name: 'North America', + region_iso_code: 'US-PA', + city_name: 'Philadelphia', + country_iso_code: 'US', + region_name: 'Pennsylvania', + location: { + lon: -75.1534, + lat: 39.9359, + }, + }, + }, + }, + }, + ], + }, + }, + }, + autonomous_system: { + doc_count: 14, + top_as: { + hits: { + total: { + value: 14, + relation: 'eq', + }, + max_score: 1, + hits: [ + { + _index: 'filebeat-8.0.0-2019.06.19-000005', + _type: '_doc', + _id: 'dd4fa2d4bd-692279846149410', + _score: 1, + _source: { + source: { + as: { + number: 3356, + organization: { + name: 'Level 3 Parent, LLC', + }, + }, + }, + }, + }, + ], + }, + }, + }, + domain: { + buckets: [ + { + key: 'test.11.com', + }, + ], + }, + }, + ], + }, + }, +}; + +export const mockTopNFlowQueryDsl = { + mockTopNFlowQueryDsl: 'mockTopNFlowQueryDsl', +}; + +export const mockResult = { + inspect: { + dsl: [JSON.stringify(mockTopNFlowQueryDsl, null, 2)], + response: [JSON.stringify(mockResponse, null, 2)], + }, + edges: [ + { + cursor: { + tiebreaker: null, + value: '1.1.1.1', + }, + node: { + _id: '1.1.1.1', + network: { + bytes_in: 11276023407, + bytes_out: 1025631, + }, + source: { + domain: ['test.1.net'], + ip: '1.1.1.1', + autonomous_system: { + name: 'Level 3 Parent, LLC', + number: 3356, + }, + location: { + flowTarget: 'source', + geo: { + city_name: 'Philadelphia', + continent_name: 'North America', + country_iso_code: 'US', + location: { + lat: 39.9359, + lon: -75.1534, + }, + region_iso_code: 'US-PA', + region_name: 'Pennsylvania', + }, + }, + flows: 1234567, + destination_ips: 345345, + }, + }, + }, + { + cursor: { + tiebreaker: null, + value: '2.2.2.2', + }, + node: { + _id: '2.2.2.2', + network: { + bytes_in: 5469323342, + bytes_out: 2811441, + }, + source: { + domain: ['test.2.net'], + ip: '2.2.2.2', + location: { + flowTarget: 'source', + geo: { + city_name: 'Philadelphia', + continent_name: 'North America', + country_iso_code: 'US', + location: { + lat: 39.9359, + lon: -75.1534, + }, + region_iso_code: 'US-PA', + region_name: 'Pennsylvania', + }, + }, + autonomous_system: { + name: 'Level 3 Parent, LLC', + number: 3356, + }, + flows: 1234567, + destination_ips: 345345, + }, + }, + }, + { + cursor: { + tiebreaker: null, + value: '3.3.3.3', + }, + node: { + _id: '3.3.3.3', + network: { + bytes_in: 3807671322, + bytes_out: 4494034, + }, + source: { + domain: ['test.3.com', 'test.3-duplicate.com'], + ip: '3.3.3.3', + location: { + flowTarget: 'source', + geo: { + city_name: 'Philadelphia', + continent_name: 'North America', + country_iso_code: 'US', + location: { + lat: 39.9359, + lon: -75.1534, + }, + region_iso_code: 'US-PA', + region_name: 'Pennsylvania', + }, + }, + autonomous_system: { + name: 'Level 3 Parent, LLC', + number: 3356, + }, + flows: 1234567, + destination_ips: 345345, + }, + }, + }, + { + cursor: { + tiebreaker: null, + value: '4.4.4.4', + }, + node: { + _id: '4.4.4.4', + network: { + bytes_in: 166517626, + bytes_out: 3194782, + }, + source: { + domain: ['test.4.com'], + ip: '4.4.4.4', + location: { + flowTarget: 'source', + geo: { + city_name: 'Philadelphia', + continent_name: 'North America', + country_iso_code: 'US', + location: { + lat: 39.9359, + lon: -75.1534, + }, + region_iso_code: 'US-PA', + region_name: 'Pennsylvania', + }, + }, + autonomous_system: { + name: 'Level 3 Parent, LLC', + number: 3356, + }, + flows: 1234567, + destination_ips: 345345, + }, + }, + }, + { + cursor: { + tiebreaker: null, + value: '5.5.5.5', + }, + node: { + _id: '5.5.5.5', + network: { + bytes_in: 104785026, + bytes_out: 1838597, + }, + source: { + domain: ['test.5.com'], + ip: '5.5.5.5', + location: { + flowTarget: 'source', + geo: { + city_name: 'Philadelphia', + continent_name: 'North America', + country_iso_code: 'US', + location: { + lat: 39.9359, + lon: -75.1534, + }, + region_iso_code: 'US-PA', + region_name: 'Pennsylvania', + }, + }, + autonomous_system: { + name: 'Level 3 Parent, LLC', + number: 3356, + }, + flows: 1234567, + destination_ips: 345345, + }, + }, + }, + { + cursor: { + tiebreaker: null, + value: '6.6.6.6', + }, + node: { + _id: '6.6.6.6', + network: { + bytes_in: 28804250, + bytes_out: 482982, + }, + source: { + domain: ['test.6.com'], + ip: '6.6.6.6', + location: { + flowTarget: 'source', + geo: { + city_name: 'Philadelphia', + continent_name: 'North America', + country_iso_code: 'US', + location: { + lat: 39.9359, + lon: -75.1534, + }, + region_iso_code: 'US-PA', + region_name: 'Pennsylvania', + }, + }, + autonomous_system: { + name: 'Level 3 Parent, LLC', + number: 3356, + }, + flows: 1234567, + destination_ips: 345345, + }, + }, + }, + { + cursor: { + tiebreaker: null, + value: '7.7.7.7', + }, + node: { + _id: '7.7.7.7', + network: { + bytes_in: 23032363, + bytes_out: 400623, + }, + source: { + domain: ['test.7.com'], + ip: '7.7.7.7', + location: { + flowTarget: 'source', + geo: { + city_name: 'Philadelphia', + continent_name: 'North America', + country_iso_code: 'US', + location: { + lat: 39.9359, + lon: -75.1534, + }, + region_iso_code: 'US-PA', + region_name: 'Pennsylvania', + }, + }, + autonomous_system: { + name: 'Level 3 Parent, LLC', + number: 3356, + }, + flows: 1234567, + destination_ips: 345345, + }, + }, + }, + { + cursor: { + tiebreaker: null, + value: '8.8.8.8', + }, + node: { + _id: '8.8.8.8', + network: { + bytes_in: 21424889, + bytes_out: 344357, + }, + source: { + domain: ['test.8.com'], + ip: '8.8.8.8', + location: { + flowTarget: 'source', + geo: { + city_name: 'Philadelphia', + continent_name: 'North America', + country_iso_code: 'US', + location: { + lat: 39.9359, + lon: -75.1534, + }, + region_iso_code: 'US-PA', + region_name: 'Pennsylvania', + }, + }, + autonomous_system: { + name: 'Level 3 Parent, LLC', + number: 3356, + }, + flows: 1234567, + destination_ips: 345345, + }, + }, + }, + { + cursor: { + tiebreaker: null, + value: '9.9.9.9', + }, + node: { + _id: '9.9.9.9', + network: { + bytes_in: 19205000, + bytes_out: 355663, + }, + source: { + domain: ['test.9.com'], + ip: '9.9.9.9', + location: { + flowTarget: 'source', + geo: { + city_name: 'Philadelphia', + continent_name: 'North America', + country_iso_code: 'US', + location: { + lat: 39.9359, + lon: -75.1534, + }, + region_iso_code: 'US-PA', + region_name: 'Pennsylvania', + }, + }, + autonomous_system: { + name: 'Level 3 Parent, LLC', + number: 3356, + }, + flows: 1234567, + destination_ips: 345345, + }, + }, + }, + { + cursor: { + tiebreaker: null, + value: '10.10.10.10', + }, + node: { + _id: '10.10.10.10', + network: { + bytes_in: 11407633, + bytes_out: 199360, + }, + source: { + domain: ['test.10.com'], + ip: '10.10.10.10', + location: { + flowTarget: 'source', + geo: { + city_name: 'Philadelphia', + continent_name: 'North America', + country_iso_code: 'US', + location: { + lat: 39.9359, + lon: -75.1534, + }, + region_iso_code: 'US-PA', + region_name: 'Pennsylvania', + }, + }, + autonomous_system: { + name: 'Level 3 Parent, LLC', + number: 3356, + }, + flows: 1234567, + destination_ips: 345345, + }, + }, + }, + ], + pageInfo: { + activePage: 0, + fakeTotalCount: 50, + showMorePagesIndicator: true, + }, + totalCount: 545, +}; + +export const mockOptionsIp: NetworkTopNFlowRequestOptions = { + ...mockOptions, + ip: '1.1.1.1', +}; + +export const mockRequestIp = { + ...mockRequest, + body: { + ...mockRequest.body, + variables: { + ...mockRequest.body.variables, + ip: '1.1.1.1', + }, + }, +}; + +export const mockResponseIp = { + took: 122, + timed_out: false, + _shards: { + total: 1, + successful: 1, + skipped: 0, + failed: 0, + }, + hits: { + max_score: null, + hits: [], + }, + aggregations: { + top_n_flow_count: { + value: 1, + }, + [FlowTargetSourceDest.source]: { + buckets: [ + { + key: '1.1.1.1', + flows: { value: 1234567 }, + destination_ips: { value: 345345 }, + bytes_in: { + value: 11276023407, + }, + bytes_out: { + value: 1025631, + }, + location: { + doc_count: 14, + top_geo: { + hits: { + total: { + value: 14, + relation: 'eq', + }, + max_score: 1, + hits: [ + { + _index: 'filebeat-8.0.0-2019.06.19-000005', + _type: '_doc', + _id: 'dd4fa2d4bd-692279846149410', + _score: 1, + _source: { + source: { + geo: { + continent_name: 'North America', + region_iso_code: 'US-PA', + city_name: 'Philadelphia', + country_iso_code: 'US', + region_name: 'Pennsylvania', + location: { + lon: -75.1534, + lat: 39.9359, + }, + }, + }, + }, + }, + ], + }, + }, + }, + autonomous_system: { + doc_count: 14, + top_as: { + hits: { + total: { + value: 14, + relation: 'eq', + }, + max_score: 1, + hits: [ + { + _index: 'filebeat-8.0.0-2019.06.19-000005', + _type: '_doc', + _id: 'dd4fa2d4bd-692279846149410', + _score: 1, + _source: { + source: { + as: { + number: 3356, + organization: { + name: 'Level 3 Parent, LLC', + }, + }, + }, + }, + }, + ], + }, + }, + }, + domain: { + buckets: [ + { + key: 'test.1.net', + }, + ], + }, + }, + ], + }, + }, +}; + +export const mockResultIp = { + inspect: { + dsl: [JSON.stringify(mockTopNFlowQueryDsl, null, 2)], + response: [JSON.stringify(mockResponseIp, null, 2)], + }, + edges: [ + { + cursor: { + tiebreaker: null, + value: '1.1.1.1', + }, + node: { + _id: '1.1.1.1', + network: { + bytes_in: 11276023407, + bytes_out: 1025631, + }, + source: { + domain: ['test.1.net'], + ip: '1.1.1.1', + autonomous_system: { + name: 'Level 3 Parent, LLC', + number: 3356, + }, + location: { + flowTarget: 'source', + geo: { + city_name: 'Philadelphia', + continent_name: 'North America', + country_iso_code: 'US', + location: { + lat: 39.9359, + lon: -75.1534, + }, + region_iso_code: 'US-PA', + region_name: 'Pennsylvania', + }, + }, + flows: 1234567, + destination_ips: 345345, + }, + }, + }, + ], + pageInfo: { + activePage: 0, + fakeTotalCount: 1, + showMorePagesIndicator: false, + }, + totalCount: 1, +}; diff --git a/x-pack/legacy/plugins/siem/server/lib/network/query_dns.dsl.ts b/x-pack/plugins/siem/server/lib/network/query_dns.dsl.ts similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/network/query_dns.dsl.ts rename to x-pack/plugins/siem/server/lib/network/query_dns.dsl.ts diff --git a/x-pack/legacy/plugins/siem/server/lib/network/query_http.dsl.ts b/x-pack/plugins/siem/server/lib/network/query_http.dsl.ts similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/network/query_http.dsl.ts rename to x-pack/plugins/siem/server/lib/network/query_http.dsl.ts diff --git a/x-pack/legacy/plugins/siem/server/lib/network/query_top_countries.dsl.ts b/x-pack/plugins/siem/server/lib/network/query_top_countries.dsl.ts similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/network/query_top_countries.dsl.ts rename to x-pack/plugins/siem/server/lib/network/query_top_countries.dsl.ts diff --git a/x-pack/legacy/plugins/siem/server/lib/network/query_top_n_flow.dsl.ts b/x-pack/plugins/siem/server/lib/network/query_top_n_flow.dsl.ts similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/network/query_top_n_flow.dsl.ts rename to x-pack/plugins/siem/server/lib/network/query_top_n_flow.dsl.ts diff --git a/x-pack/legacy/plugins/siem/server/lib/network/types.ts b/x-pack/plugins/siem/server/lib/network/types.ts similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/network/types.ts rename to x-pack/plugins/siem/server/lib/network/types.ts diff --git a/x-pack/plugins/siem/server/lib/note/saved_object.ts b/x-pack/plugins/siem/server/lib/note/saved_object.ts new file mode 100644 index 0000000000000..2b94fd4516786 --- /dev/null +++ b/x-pack/plugins/siem/server/lib/note/saved_object.ts @@ -0,0 +1,239 @@ +/* + * 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 { failure } from 'io-ts/lib/PathReporter'; +import { getOr } from 'lodash/fp'; +import uuid from 'uuid'; + +import { pipe } from 'fp-ts/lib/pipeable'; +import { map, fold } from 'fp-ts/lib/Either'; +import { identity } from 'fp-ts/lib/function'; + +import { SavedObjectsFindOptions } from '../../../../../../src/core/server'; +import { AuthenticatedUser } from '../../../../security/common/model'; +import { UNAUTHENTICATED_USER } from '../../../common/constants'; +import { + PageInfoNote, + ResponseNote, + ResponseNotes, + SortNote, + NoteResult, +} from '../../graphql/types'; +import { FrameworkRequest } from '../framework'; +import { SavedNote, NoteSavedObjectRuntimeType, NoteSavedObject } from './types'; +import { noteSavedObjectType } from './saved_object_mappings'; +import { timelineSavedObjectType } from '../../saved_objects'; +import { pickSavedTimeline } from '../timeline/pick_saved_timeline'; +import { convertSavedObjectToSavedTimeline } from '../timeline/convert_saved_object_to_savedtimeline'; + +export class Note { + public async deleteNote(request: FrameworkRequest, noteIds: string[]) { + const savedObjectsClient = request.context.core.savedObjects.client; + + await Promise.all( + noteIds.map(noteId => savedObjectsClient.delete(noteSavedObjectType, noteId)) + ); + } + + public async deleteNoteByTimelineId(request: FrameworkRequest, timelineId: string) { + const options: SavedObjectsFindOptions = { + type: noteSavedObjectType, + search: timelineId, + searchFields: ['timelineId'], + }; + const notesToBeDeleted = await this.getAllSavedNote(request, options); + const savedObjectsClient = request.context.core.savedObjects.client; + + await Promise.all( + notesToBeDeleted.notes.map(note => + savedObjectsClient.delete(noteSavedObjectType, note.noteId) + ) + ); + } + + public async getNote(request: FrameworkRequest, noteId: string): Promise { + return this.getSavedNote(request, noteId); + } + + public async getNotesByEventId( + request: FrameworkRequest, + eventId: string + ): Promise { + const options: SavedObjectsFindOptions = { + type: noteSavedObjectType, + search: eventId, + searchFields: ['eventId'], + }; + const notesByEventId = await this.getAllSavedNote(request, options); + return notesByEventId.notes; + } + + public async getNotesByTimelineId( + request: FrameworkRequest, + timelineId: string + ): Promise { + const options: SavedObjectsFindOptions = { + type: noteSavedObjectType, + search: timelineId, + searchFields: ['timelineId'], + }; + const notesByTimelineId = await this.getAllSavedNote(request, options); + return notesByTimelineId.notes; + } + + public async getAllNotes( + request: FrameworkRequest, + pageInfo: PageInfoNote | null, + search: string | null, + sort: SortNote | null + ): Promise { + const options: SavedObjectsFindOptions = { + type: noteSavedObjectType, + perPage: pageInfo != null ? pageInfo.pageSize : undefined, + page: pageInfo != null ? pageInfo.pageIndex : undefined, + search: search != null ? search : undefined, + searchFields: ['note'], + sortField: sort != null ? sort.sortField : undefined, + sortOrder: sort != null ? sort.sortOrder : undefined, + }; + return this.getAllSavedNote(request, options); + } + + public async persistNote( + request: FrameworkRequest, + noteId: string | null, + version: string | null, + note: SavedNote + ): Promise { + try { + const savedObjectsClient = request.context.core.savedObjects.client; + + if (noteId == null) { + const timelineVersionSavedObject = + note.timelineId == null + ? await (async () => { + const timelineResult = convertSavedObjectToSavedTimeline( + await savedObjectsClient.create( + timelineSavedObjectType, + pickSavedTimeline(null, {}, request.user) + ) + ); + note.timelineId = timelineResult.savedObjectId; + return timelineResult.version; + })() + : null; + + // Create new note + return { + code: 200, + message: 'success', + note: convertSavedObjectToSavedNote( + await savedObjectsClient.create( + noteSavedObjectType, + pickSavedNote(noteId, note, request.user) + ), + timelineVersionSavedObject != null ? timelineVersionSavedObject : undefined + ), + }; + } + + // Update new note + + const existingNote = await this.getSavedNote(request, noteId); + return { + code: 200, + message: 'success', + note: convertSavedObjectToSavedNote( + await savedObjectsClient.update( + noteSavedObjectType, + noteId, + pickSavedNote(noteId, note, request.user), + { + version: existingNote.version || undefined, + } + ) + ), + }; + } catch (err) { + if (getOr(null, 'output.statusCode', err) === 403) { + const noteToReturn: NoteResult = { + ...note, + noteId: uuid.v1(), + version: '', + timelineId: '', + timelineVersion: '', + }; + return { + code: 403, + message: err.message, + note: noteToReturn, + }; + } + throw err; + } + } + + private async getSavedNote(request: FrameworkRequest, NoteId: string) { + const savedObjectsClient = request.context.core.savedObjects.client; + const savedObject = await savedObjectsClient.get(noteSavedObjectType, NoteId); + + return convertSavedObjectToSavedNote(savedObject); + } + + private async getAllSavedNote(request: FrameworkRequest, options: SavedObjectsFindOptions) { + const savedObjectsClient = request.context.core.savedObjects.client; + const savedObjects = await savedObjectsClient.find(options); + + return { + totalCount: savedObjects.total, + notes: savedObjects.saved_objects.map(savedObject => + convertSavedObjectToSavedNote(savedObject) + ), + }; + } +} + +export const convertSavedObjectToSavedNote = ( + savedObject: unknown, + timelineVersion?: string | undefined | null +): NoteSavedObject => + pipe( + NoteSavedObjectRuntimeType.decode(savedObject), + map(savedNote => ({ + noteId: savedNote.id, + version: savedNote.version, + timelineVersion, + ...savedNote.attributes, + })), + fold(errors => { + throw new Error(failure(errors).join('\n')); + }, identity) + ); + +// we have to use any here because the SavedObjectAttributes interface is like below +// export interface SavedObjectAttributes { +// [key: string]: SavedObjectAttributes | string | number | boolean | null; +// } +// then this interface does not allow types without index signature +// this is limiting us with our type for now so the easy way was to use any + +const pickSavedNote = ( + noteId: string | null, + savedNote: SavedNote, + userInfo: AuthenticatedUser | null + // eslint-disable-next-line @typescript-eslint/no-explicit-any +): any => { + if (noteId == null) { + savedNote.created = new Date().valueOf(); + savedNote.createdBy = userInfo?.username ?? UNAUTHENTICATED_USER; + savedNote.updated = new Date().valueOf(); + savedNote.updatedBy = userInfo?.username ?? UNAUTHENTICATED_USER; + } else if (noteId != null) { + savedNote.updated = new Date().valueOf(); + savedNote.updatedBy = userInfo?.username ?? UNAUTHENTICATED_USER; + } + return savedNote; +}; diff --git a/x-pack/legacy/plugins/siem/server/lib/note/saved_object_mappings.ts b/x-pack/plugins/siem/server/lib/note/saved_object_mappings.ts similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/note/saved_object_mappings.ts rename to x-pack/plugins/siem/server/lib/note/saved_object_mappings.ts diff --git a/x-pack/legacy/plugins/siem/server/lib/note/types.ts b/x-pack/plugins/siem/server/lib/note/types.ts similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/note/types.ts rename to x-pack/plugins/siem/server/lib/note/types.ts diff --git a/x-pack/legacy/plugins/siem/server/lib/overview/elastic_adapter.test.ts b/x-pack/plugins/siem/server/lib/overview/elastic_adapter.test.ts similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/overview/elastic_adapter.test.ts rename to x-pack/plugins/siem/server/lib/overview/elastic_adapter.test.ts diff --git a/x-pack/legacy/plugins/siem/server/lib/overview/elasticsearch_adapter.ts b/x-pack/plugins/siem/server/lib/overview/elasticsearch_adapter.ts similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/overview/elasticsearch_adapter.ts rename to x-pack/plugins/siem/server/lib/overview/elasticsearch_adapter.ts diff --git a/x-pack/legacy/plugins/siem/server/lib/overview/index.ts b/x-pack/plugins/siem/server/lib/overview/index.ts similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/overview/index.ts rename to x-pack/plugins/siem/server/lib/overview/index.ts diff --git a/x-pack/plugins/siem/server/lib/overview/mock.ts b/x-pack/plugins/siem/server/lib/overview/mock.ts new file mode 100644 index 0000000000000..51d8a258569a8 --- /dev/null +++ b/x-pack/plugins/siem/server/lib/overview/mock.ts @@ -0,0 +1,168 @@ +/* + * 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 { DEFAULT_INDEX_PATTERN } from '../../../common/constants'; +import { RequestBasicOptions } from '../framework/types'; + +export const mockOptionsNetwork: RequestBasicOptions = { + defaultIndex: DEFAULT_INDEX_PATTERN, + sourceConfiguration: { + fields: { + container: 'docker.container.name', + host: 'beat.hostname', + message: ['message', '@message'], + pod: 'kubernetes.pod.name', + tiebreaker: '_doc', + timestamp: '@timestamp', + }, + }, + timerange: { interval: '12h', to: 1549852006071, from: 1549765606071 }, + filterQuery: {}, +}; + +export const mockRequestNetwork = { + body: { + operationName: 'GetOverviewNetworkQuery', + variables: { + sourceId: 'default', + timerange: { interval: '12h', from: 1549765830772, to: 1549852230772 }, + filterQuery: '', + }, + query: + 'query GetOverviewNetworkQuery(\n $sourceId: ID!\n $timerange: TimerangeInput!\n $filterQuery: String\n ) {\n source(id: $sourceId) {\n id\n OverviewNetwork(timerange: $timerange, filterQuery: $filterQuery) {\n packetbeatFlow\n packetbeatDNS\n filebeatSuricata\n filebeatZeek\n auditbeatSocket\n }\n }\n }', + }, +}; + +export const mockResponseNetwork = { + took: 89, + timed_out: false, + _shards: { total: 18, successful: 18, skipped: 0, failed: 0 }, + hits: { total: { value: 950867, relation: 'eq' }, max_score: null, hits: [] }, + aggregations: { + unique_flow_count: { doc_count: 50243 }, + unique_dns_count: { doc_count: 15000 }, + unique_suricata_count: { doc_count: 2375 }, + unique_zeek_count: { doc_count: 456 }, + unique_socket_count: { doc_count: 13 }, + unique_filebeat_count: { + doc_count: 456756, + unique_cisco_count: { doc_count: 14 }, + unique_netflow_count: { doc_count: 992 }, + unique_panw_count: { doc_count: 225 }, + }, + unique_packetbeat_count: { doc_count: 7897896, unique_tls_count: { doc_count: 2009 } }, + }, +}; + +export const mockBuildOverviewHostQuery = { buildOverviewHostQuery: 'buildOverviewHostQuery' }; +export const mockBuildOverviewNetworkQuery = { + buildOverviewNetworkQuery: 'buildOverviewNetworkQuery', +}; + +export const mockResultNetwork = { + inspect: { + dsl: [JSON.stringify(mockBuildOverviewNetworkQuery, null, 2)], + response: [JSON.stringify(mockResponseNetwork, null, 2)], + }, + packetbeatFlow: 50243, + packetbeatDNS: 15000, + filebeatSuricata: 2375, + filebeatZeek: 456, + auditbeatSocket: 13, + filebeatCisco: 14, + filebeatNetflow: 992, + filebeatPanw: 225, + packetbeatTLS: 2009, +}; + +export const mockOptionsHost: RequestBasicOptions = { + defaultIndex: DEFAULT_INDEX_PATTERN, + sourceConfiguration: { + fields: { + container: 'docker.container.name', + host: 'beat.hostname', + message: ['message', '@message'], + pod: 'kubernetes.pod.name', + tiebreaker: '_doc', + timestamp: '@timestamp', + }, + }, + timerange: { interval: '12h', to: 1549852006071, from: 1549765606071 }, + filterQuery: {}, +}; + +export const mockRequestHost = { + body: { + operationName: 'GetOverviewHostQuery', + variables: { + sourceId: 'default', + timerange: { interval: '12h', from: 1549765830772, to: 1549852230772 }, + filterQuery: '', + }, + query: + 'query GetOverviewHostQuery(\n $sourceId: ID!\n $timerange: TimerangeInput!\n $filterQuery: String\n ) {\n source(id: $sourceId) {\n id\n OverviewHost(timerange: $timerange, filterQuery: $filterQuery) {\n auditbeatAuditd\n auditbeatFIM\n auditbeatLogin\n auditbeatPackage\n auditbeatProcess\n auditbeatUser\n }\n }\n }', + }, +}; + +export const mockResponseHost = { + took: 89, + timed_out: false, + _shards: { total: 18, successful: 18, skipped: 0, failed: 0 }, + hits: { total: { value: 950867, relation: 'eq' }, max_score: null, hits: [] }, + aggregations: { + auditd_count: { doc_count: 73847 }, + endgame_module: { + doc_count: 6258, + dns_event_count: { doc_count: 891 }, + file_event_count: { doc_count: 892 }, + image_load_event_count: { doc_count: 893 }, + network_event_count: { doc_count: 894 }, + process_event_count: { doc_count: 895 }, + registry_event: { doc_count: 896 }, + security_event_count: { doc_count: 897 }, + }, + fim_count: { doc_count: 107307 }, + system_module: { + doc_count: 20000000, + login_count: { doc_count: 60015 }, + package_count: { doc_count: 2003 }, + process_count: { doc_count: 1200 }, + user_count: { doc_count: 1979 }, + filebeat_count: { doc_count: 225 }, + }, + winlog_module: { + security_event_count: { + doc_count: 523, + }, + mwsysmon_operational_event_count: { + doc_count: 214, + }, + }, + }, +}; + +export const mockResultHost = { + inspect: { + dsl: [JSON.stringify(mockBuildOverviewHostQuery, null, 2)], + response: [JSON.stringify(mockResponseHost, null, 2)], + }, + auditbeatAuditd: 73847, + auditbeatFIM: 107307, + auditbeatLogin: 60015, + auditbeatPackage: 2003, + auditbeatProcess: 1200, + auditbeatUser: 1979, + endgameDns: 891, + endgameFile: 892, + endgameImageLoad: 893, + endgameNetwork: 894, + endgameProcess: 895, + endgameRegistry: 896, + endgameSecurity: 897, + filebeatSystemModule: 225, + winlogbeatSecurity: 523, + winlogbeatMWSysmonOperational: 214, +}; diff --git a/x-pack/legacy/plugins/siem/server/lib/overview/query.dsl.ts b/x-pack/plugins/siem/server/lib/overview/query.dsl.ts similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/overview/query.dsl.ts rename to x-pack/plugins/siem/server/lib/overview/query.dsl.ts diff --git a/x-pack/legacy/plugins/siem/server/lib/overview/types.ts b/x-pack/plugins/siem/server/lib/overview/types.ts similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/overview/types.ts rename to x-pack/plugins/siem/server/lib/overview/types.ts diff --git a/x-pack/plugins/siem/server/lib/pinned_event/saved_object.ts b/x-pack/plugins/siem/server/lib/pinned_event/saved_object.ts new file mode 100644 index 0000000000000..7fc23d86d8218 --- /dev/null +++ b/x-pack/plugins/siem/server/lib/pinned_event/saved_object.ts @@ -0,0 +1,225 @@ +/* + * 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 { failure } from 'io-ts/lib/PathReporter'; +import { getOr } from 'lodash/fp'; +import { pipe } from 'fp-ts/lib/pipeable'; +import { map, fold } from 'fp-ts/lib/Either'; +import { identity } from 'fp-ts/lib/function'; + +import { SavedObjectsFindOptions } from '../../../../../../src/core/server'; +import { AuthenticatedUser } from '../../../../security/common/model'; +import { UNAUTHENTICATED_USER } from '../../../common/constants'; +import { FrameworkRequest } from '../framework'; +import { + PinnedEventSavedObject, + PinnedEventSavedObjectRuntimeType, + SavedPinnedEvent, +} from './types'; +import { PageInfoNote, SortNote, PinnedEvent as PinnedEventResponse } from '../../graphql/types'; +import { pinnedEventSavedObjectType, timelineSavedObjectType } from '../../saved_objects'; +import { pickSavedTimeline } from '../timeline/pick_saved_timeline'; +import { convertSavedObjectToSavedTimeline } from '../timeline/convert_saved_object_to_savedtimeline'; + +export class PinnedEvent { + public async deletePinnedEventOnTimeline(request: FrameworkRequest, pinnedEventIds: string[]) { + const savedObjectsClient = request.context.core.savedObjects.client; + + await Promise.all( + pinnedEventIds.map(pinnedEventId => + savedObjectsClient.delete(pinnedEventSavedObjectType, pinnedEventId) + ) + ); + } + + public async deleteAllPinnedEventsOnTimeline(request: FrameworkRequest, timelineId: string) { + const savedObjectsClient = request.context.core.savedObjects.client; + const options: SavedObjectsFindOptions = { + type: pinnedEventSavedObjectType, + search: timelineId, + searchFields: ['timelineId'], + }; + const pinnedEventToBeDeleted = await this.getAllSavedPinnedEvents(request, options); + await Promise.all( + pinnedEventToBeDeleted.map(pinnedEvent => + savedObjectsClient.delete(pinnedEventSavedObjectType, pinnedEvent.pinnedEventId) + ) + ); + } + + public async getPinnedEvent( + request: FrameworkRequest, + pinnedEventId: string + ): Promise { + return this.getSavedPinnedEvent(request, pinnedEventId); + } + + public async getAllPinnedEventsByTimelineId( + request: FrameworkRequest, + timelineId: string + ): Promise { + const options: SavedObjectsFindOptions = { + type: pinnedEventSavedObjectType, + search: timelineId, + searchFields: ['timelineId'], + }; + return this.getAllSavedPinnedEvents(request, options); + } + + public async getAllPinnedEvents( + request: FrameworkRequest, + pageInfo: PageInfoNote | null, + search: string | null, + sort: SortNote | null + ): Promise { + const options: SavedObjectsFindOptions = { + type: pinnedEventSavedObjectType, + perPage: pageInfo != null ? pageInfo.pageSize : undefined, + page: pageInfo != null ? pageInfo.pageIndex : undefined, + search: search != null ? search : undefined, + searchFields: ['timelineId', 'eventId'], + sortField: sort != null ? sort.sortField : undefined, + sortOrder: sort != null ? sort.sortOrder : undefined, + }; + return this.getAllSavedPinnedEvents(request, options); + } + + public async persistPinnedEventOnTimeline( + request: FrameworkRequest, + pinnedEventId: string | null, // pinned event saved object id + eventId: string, + timelineId: string | null + ): Promise { + const savedObjectsClient = request.context.core.savedObjects.client; + + try { + if (pinnedEventId == null) { + const timelineVersionSavedObject = + timelineId == null + ? await (async () => { + const timelineResult = convertSavedObjectToSavedTimeline( + await savedObjectsClient.create( + timelineSavedObjectType, + pickSavedTimeline(null, {}, request.user || null) + ) + ); + timelineId = timelineResult.savedObjectId; // eslint-disable-line no-param-reassign + return timelineResult.version; + })() + : null; + + if (timelineId != null) { + const allPinnedEventId = await this.getAllPinnedEventsByTimelineId(request, timelineId); + const isPinnedAlreadyExisting = allPinnedEventId.filter( + pinnedEvent => pinnedEvent.eventId === eventId + ); + + if (isPinnedAlreadyExisting.length === 0) { + const savedPinnedEvent: SavedPinnedEvent = { + eventId, + timelineId, + }; + // create Pinned Event on Timeline + return convertSavedObjectToSavedPinnedEvent( + await savedObjectsClient.create( + pinnedEventSavedObjectType, + pickSavedPinnedEvent(pinnedEventId, savedPinnedEvent, request.user || null) + ), + timelineVersionSavedObject != null ? timelineVersionSavedObject : undefined + ); + } + return isPinnedAlreadyExisting[0]; + } + throw new Error('You can NOT pinned event without a timelineID'); + } + // Delete Pinned Event on Timeline + await this.deletePinnedEventOnTimeline(request, [pinnedEventId]); + return null; + } catch (err) { + if (getOr(null, 'output.statusCode', err) === 404) { + /* + * Why we are doing that, because if it is not found for sure that it will be unpinned + * There is no need to bring back this error since we can assume that it is unpinned + */ + return null; + } + if (getOr(null, 'output.statusCode', err) === 403) { + return pinnedEventId != null + ? { + code: 403, + message: err.message, + pinnedEventId: eventId, + timelineId: '', + timelineVersion: '', + } + : null; + } + throw err; + } + } + + private async getSavedPinnedEvent(request: FrameworkRequest, pinnedEventId: string) { + const savedObjectsClient = request.context.core.savedObjects.client; + const savedObject = await savedObjectsClient.get(pinnedEventSavedObjectType, pinnedEventId); + + return convertSavedObjectToSavedPinnedEvent(savedObject); + } + + private async getAllSavedPinnedEvents( + request: FrameworkRequest, + options: SavedObjectsFindOptions + ) { + const savedObjectsClient = request.context.core.savedObjects.client; + const savedObjects = await savedObjectsClient.find(options); + + return savedObjects.saved_objects.map(savedObject => + convertSavedObjectToSavedPinnedEvent(savedObject) + ); + } +} + +export const convertSavedObjectToSavedPinnedEvent = ( + savedObject: unknown, + timelineVersion?: string | undefined | null +): PinnedEventSavedObject => + pipe( + PinnedEventSavedObjectRuntimeType.decode(savedObject), + map(savedPinnedEvent => ({ + pinnedEventId: savedPinnedEvent.id, + version: savedPinnedEvent.version, + timelineVersion, + ...savedPinnedEvent.attributes, + })), + fold(errors => { + throw new Error(failure(errors).join('\n')); + }, identity) + ); + +// we have to use any here because the SavedObjectAttributes interface is like below +// export interface SavedObjectAttributes { +// [key: string]: SavedObjectAttributes | string | number | boolean | null; +// } +// then this interface does not allow types without index signature +// this is limiting us with our type for now so the easy way was to use any + +export const pickSavedPinnedEvent = ( + pinnedEventId: string | null, + savedPinnedEvent: SavedPinnedEvent, + userInfo: AuthenticatedUser | null + // eslint-disable-next-line @typescript-eslint/no-explicit-any +): any => { + const dateNow = new Date().valueOf(); + if (pinnedEventId == null) { + savedPinnedEvent.created = dateNow; + savedPinnedEvent.createdBy = userInfo?.username ?? UNAUTHENTICATED_USER; + savedPinnedEvent.updated = dateNow; + savedPinnedEvent.updatedBy = userInfo?.username ?? UNAUTHENTICATED_USER; + } else if (pinnedEventId != null) { + savedPinnedEvent.updated = dateNow; + savedPinnedEvent.updatedBy = userInfo?.username ?? UNAUTHENTICATED_USER; + } + return savedPinnedEvent; +}; diff --git a/x-pack/legacy/plugins/siem/server/lib/pinned_event/saved_object_mappings.ts b/x-pack/plugins/siem/server/lib/pinned_event/saved_object_mappings.ts similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/pinned_event/saved_object_mappings.ts rename to x-pack/plugins/siem/server/lib/pinned_event/saved_object_mappings.ts diff --git a/x-pack/legacy/plugins/siem/server/lib/pinned_event/types.ts b/x-pack/plugins/siem/server/lib/pinned_event/types.ts similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/pinned_event/types.ts rename to x-pack/plugins/siem/server/lib/pinned_event/types.ts diff --git a/x-pack/legacy/plugins/siem/server/lib/source_status/elasticsearch_adapter.ts b/x-pack/plugins/siem/server/lib/source_status/elasticsearch_adapter.ts similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/source_status/elasticsearch_adapter.ts rename to x-pack/plugins/siem/server/lib/source_status/elasticsearch_adapter.ts diff --git a/x-pack/legacy/plugins/siem/server/lib/source_status/index.ts b/x-pack/plugins/siem/server/lib/source_status/index.ts similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/source_status/index.ts rename to x-pack/plugins/siem/server/lib/source_status/index.ts diff --git a/x-pack/legacy/plugins/siem/server/lib/source_status/query.dsl.ts b/x-pack/plugins/siem/server/lib/source_status/query.dsl.ts similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/source_status/query.dsl.ts rename to x-pack/plugins/siem/server/lib/source_status/query.dsl.ts diff --git a/x-pack/legacy/plugins/siem/server/lib/source_status/types.ts b/x-pack/plugins/siem/server/lib/source_status/types.ts similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/source_status/types.ts rename to x-pack/plugins/siem/server/lib/source_status/types.ts diff --git a/x-pack/legacy/plugins/siem/server/lib/sources/configuration.test.ts b/x-pack/plugins/siem/server/lib/sources/configuration.test.ts similarity index 96% rename from x-pack/legacy/plugins/siem/server/lib/sources/configuration.test.ts rename to x-pack/plugins/siem/server/lib/sources/configuration.test.ts index b1b149d17a9f5..00fca7b77de49 100644 --- a/x-pack/legacy/plugins/siem/server/lib/sources/configuration.test.ts +++ b/x-pack/plugins/siem/server/lib/sources/configuration.test.ts @@ -4,9 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ +import { DEFAULT_INDEX_PATTERN } from '../../../common/constants'; import { InmemoryConfigurationAdapter } from '../configuration/inmemory_configuration_adapter'; -import { defaultIndexPattern } from '../../../default_index_pattern'; - import { ConfigurationSourcesAdapter } from './configuration'; import { PartialSourceConfiguration } from './types'; @@ -76,7 +75,7 @@ describe('the ConfigurationSourcesAdapter', () => { new InmemoryConfigurationAdapter({ sources: { sourceOne: { - defaultIndex: defaultIndexPattern, + defaultIndex: DEFAULT_INDEX_PATTERN, fields: { container: 'DIFFERENT_CONTAINER_FIELD', }, diff --git a/x-pack/legacy/plugins/siem/server/lib/sources/configuration.ts b/x-pack/plugins/siem/server/lib/sources/configuration.ts similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/sources/configuration.ts rename to x-pack/plugins/siem/server/lib/sources/configuration.ts diff --git a/x-pack/legacy/plugins/siem/server/lib/sources/index.ts b/x-pack/plugins/siem/server/lib/sources/index.ts similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/sources/index.ts rename to x-pack/plugins/siem/server/lib/sources/index.ts diff --git a/x-pack/legacy/plugins/siem/server/lib/sources/types.ts b/x-pack/plugins/siem/server/lib/sources/types.ts similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/sources/types.ts rename to x-pack/plugins/siem/server/lib/sources/types.ts diff --git a/x-pack/legacy/plugins/siem/server/lib/timeline/convert_saved_object_to_savedtimeline.ts b/x-pack/plugins/siem/server/lib/timeline/convert_saved_object_to_savedtimeline.ts similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/timeline/convert_saved_object_to_savedtimeline.ts rename to x-pack/plugins/siem/server/lib/timeline/convert_saved_object_to_savedtimeline.ts diff --git a/x-pack/plugins/siem/server/lib/timeline/create_timelines_stream_from_ndjson.ts b/x-pack/plugins/siem/server/lib/timeline/create_timelines_stream_from_ndjson.ts new file mode 100644 index 0000000000000..abe8de9bf5b94 --- /dev/null +++ b/x-pack/plugins/siem/server/lib/timeline/create_timelines_stream_from_ndjson.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 * as rt from 'io-ts'; +import { Transform } from 'stream'; +import { pipe } from 'fp-ts/lib/pipeable'; +import { fold } from 'fp-ts/lib/Either'; +import { failure } from 'io-ts/lib/PathReporter'; +import { identity } from 'fp-ts/lib/function'; +import { + createConcatStream, + createSplitStream, + createMapStream, +} from '../../../../../../src/legacy/utils'; +import { + parseNdjsonStrings, + filterExportedCounts, + createLimitStream, +} from '../../utils/read_stream/create_stream_from_ndjson'; + +import { ImportTimelineResponse } from './routes/utils/import_timelines'; +import { ImportTimelinesSchemaRt } from './routes/schemas/import_timelines_schema'; + +type ErrorFactory = (message: string) => Error; + +export const createPlainError = (message: string) => new Error(message); + +export const throwErrors = (createError: ErrorFactory) => (errors: rt.Errors) => { + throw createError(failure(errors).join('\n')); +}; + +export const decodeOrThrow = ( + runtimeType: rt.Type, + createError: ErrorFactory = createPlainError +) => (inputValue: I) => + pipe(runtimeType.decode(inputValue), fold(throwErrors(createError), identity)); + +export const validateTimelines = (): Transform => + createMapStream((obj: ImportTimelineResponse) => decodeOrThrow(ImportTimelinesSchemaRt)(obj)); + +export const createTimelinesStreamFromNdJson = (ruleLimit: number) => { + return [ + createSplitStream('\n'), + parseNdjsonStrings(), + filterExportedCounts(), + validateTimelines(), + createLimitStream(ruleLimit), + createConcatStream([]), + ]; +}; diff --git a/x-pack/legacy/plugins/siem/server/lib/timeline/pick_saved_timeline.ts b/x-pack/plugins/siem/server/lib/timeline/pick_saved_timeline.ts similarity index 92% rename from x-pack/legacy/plugins/siem/server/lib/timeline/pick_saved_timeline.ts rename to x-pack/plugins/siem/server/lib/timeline/pick_saved_timeline.ts index 5b60086ae81b6..19adb7ac1045a 100644 --- a/x-pack/legacy/plugins/siem/server/lib/timeline/pick_saved_timeline.ts +++ b/x-pack/plugins/siem/server/lib/timeline/pick_saved_timeline.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { AuthenticatedUser } from '../../../../../../plugins/security/common/model'; +import { AuthenticatedUser } from '../../../../security/common/model'; import { UNAUTHENTICATED_USER } from '../../../common/constants'; import { SavedTimeline } from './types'; diff --git a/x-pack/legacy/plugins/siem/server/lib/timeline/routes/__mocks__/import_timelines.ts b/x-pack/plugins/siem/server/lib/timeline/routes/__mocks__/import_timelines.ts similarity index 98% rename from x-pack/legacy/plugins/siem/server/lib/timeline/routes/__mocks__/import_timelines.ts rename to x-pack/plugins/siem/server/lib/timeline/routes/__mocks__/import_timelines.ts index 74d3744e29299..686f2b491cf88 100644 --- a/x-pack/legacy/plugins/siem/server/lib/timeline/routes/__mocks__/import_timelines.ts +++ b/x-pack/plugins/siem/server/lib/timeline/routes/__mocks__/import_timelines.ts @@ -163,13 +163,6 @@ export const mockParsedTimelineObject = omit( mockUniqueParsedObjects[0] ); -export const mockConfig = { - get: () => { - return 100000000; - }, - has: jest.fn(), -}; - export const mockGetCurrentUser = { user: { username: 'mockUser', diff --git a/x-pack/plugins/siem/server/lib/timeline/routes/__mocks__/request_responses.ts b/x-pack/plugins/siem/server/lib/timeline/routes/__mocks__/request_responses.ts new file mode 100644 index 0000000000000..a83c443773302 --- /dev/null +++ b/x-pack/plugins/siem/server/lib/timeline/routes/__mocks__/request_responses.ts @@ -0,0 +1,275 @@ +/* + * 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 { TIMELINE_EXPORT_URL, TIMELINE_IMPORT_URL } from '../../../../../common/constants'; +import { requestMock } from '../../../detection_engine/routes/__mocks__'; +import stream from 'stream'; +const readable = new stream.Readable(); +export const getExportTimelinesRequest = () => + requestMock.create({ + method: 'get', + path: TIMELINE_EXPORT_URL, + query: { + file_name: 'mock_export_timeline.ndjson', + exclude_export_details: 'false', + }, + body: { + ids: ['f0e58720-57b6-11ea-b88d-3f1a31716be8', '890b8ae0-57df-11ea-a7c9-3976b7f1cb37'], + }, + }); + +export const getImportTimelinesRequest = (filename?: string) => + requestMock.create({ + method: 'post', + path: TIMELINE_IMPORT_URL, + query: { overwrite: false }, + body: { + file: { ...readable, hapi: { filename: filename ?? 'filename.ndjson' } }, + }, + }); + +export const getImportTimelinesRequestEnableOverwrite = (filename?: string) => + requestMock.create({ + method: 'post', + path: TIMELINE_IMPORT_URL, + query: { overwrite: true }, + body: { + file: { hapi: { filename: filename ?? 'filename.ndjson' } }, + }, + }); + +export const mockTimelinesSavedObjects = () => ({ + saved_objects: [ + { + id: 'f0e58720-57b6-11ea-b88d-3f1a31716be8', + type: 'fakeType', + attributes: {}, + references: [], + }, + { + id: '890b8ae0-57df-11ea-a7c9-3976b7f1cb37', + type: 'fakeType', + attributes: {}, + references: [], + }, + ], +}); + +export const mockTimelines = () => ({ + saved_objects: [ + { + savedObjectId: 'f0e58720-57b6-11ea-b88d-3f1a31716be8', + version: 'Wzk0OSwxXQ==', + columns: [ + { + indexes: null, + name: null, + columnHeaderType: 'not-filtered', + id: '@timestamp', + searchable: null, + }, + { + indexes: null, + name: null, + columnHeaderType: 'not-filtered', + id: 'message', + searchable: null, + }, + { + indexes: null, + name: null, + columnHeaderType: 'not-filtered', + id: 'event.category', + searchable: null, + }, + { + indexes: null, + name: null, + columnHeaderType: 'not-filtered', + id: 'event.action', + searchable: null, + }, + { + indexes: null, + name: null, + columnHeaderType: 'not-filtered', + id: 'host.name', + searchable: null, + }, + { + indexes: null, + name: null, + columnHeaderType: 'not-filtered', + id: 'source.ip', + searchable: null, + }, + { + indexes: null, + name: null, + columnHeaderType: 'not-filtered', + id: 'destination.ip', + searchable: null, + }, + { + indexes: null, + name: null, + columnHeaderType: 'not-filtered', + id: 'user.name', + searchable: null, + }, + ], + dataProviders: [], + description: 'with a global note', + eventType: 'raw', + filters: [], + kqlMode: 'filter', + kqlQuery: { + filterQuery: { + kuery: { kind: 'kuery', expression: 'zeek.files.sha1 : * ' }, + serializedQuery: + '{"bool":{"should":[{"exists":{"field":"zeek.files.sha1"}}],"minimum_should_match":1}}', + }, + }, + title: 'test no.2', + dateRange: { start: 1582538951145, end: 1582625351145 }, + savedQueryId: null, + sort: { columnId: '@timestamp', sortDirection: 'desc' }, + created: 1582625382448, + createdBy: 'elastic', + updated: 1583741197521, + updatedBy: 'elastic', + }, + { + savedObjectId: '890b8ae0-57df-11ea-a7c9-3976b7f1cb37', + version: 'Wzk0NywxXQ==', + columns: [ + { + indexes: null, + name: null, + columnHeaderType: 'not-filtered', + id: '@timestamp', + searchable: null, + }, + { + indexes: null, + name: null, + columnHeaderType: 'not-filtered', + id: 'message', + searchable: null, + }, + { + indexes: null, + name: null, + columnHeaderType: 'not-filtered', + id: 'event.category', + searchable: null, + }, + { + indexes: null, + name: null, + columnHeaderType: 'not-filtered', + id: 'event.action', + searchable: null, + }, + { + indexes: null, + name: null, + columnHeaderType: 'not-filtered', + id: 'host.name', + searchable: null, + }, + { + indexes: null, + name: null, + columnHeaderType: 'not-filtered', + id: 'source.ip', + searchable: null, + }, + { + indexes: null, + name: null, + columnHeaderType: 'not-filtered', + id: 'destination.ip', + searchable: null, + }, + { + indexes: null, + name: null, + columnHeaderType: 'not-filtered', + id: 'user.name', + searchable: null, + }, + ], + dataProviders: [], + description: 'with an event note', + eventType: 'raw', + filters: [], + kqlMode: 'filter', + kqlQuery: { + filterQuery: { + serializedQuery: + '{"bool":{"should":[{"exists":{"field":"zeek.files.sha1"}}],"minimum_should_match":1}}', + kuery: { expression: 'zeek.files.sha1 : * ', kind: 'kuery' }, + }, + }, + title: 'test no.3', + dateRange: { start: 1582538951145, end: 1582625351145 }, + savedQueryId: null, + sort: { columnId: '@timestamp', sortDirection: 'desc' }, + created: 1582642817439, + createdBy: 'elastic', + updated: 1583741175216, + updatedBy: 'elastic', + }, + ], +}); + +export const mockNotesSavedObjects = () => ({ + saved_objects: [ + { + id: 'eb3f3930-61dc-11ea-8a49-e77254c5b742', + type: 'fakeType', + attributes: {}, + references: [], + }, + { + id: '706e7510-5d52-11ea-8f07-0392944939c1', + type: 'fakeType', + attributes: {}, + references: [], + }, + ], +}); + +export const mockNotes = () => ({ + saved_objects: [ + { + noteId: 'eb3f3930-61dc-11ea-8a49-e77254c5b742', + version: 'Wzk1MCwxXQ==', + note: 'Global note', + timelineId: 'f0e58720-57b6-11ea-b88d-3f1a31716be8', + created: 1583741205473, + createdBy: 'elastic', + updated: 1583741205473, + updatedBy: 'elastic', + }, + { + noteId: '706e7510-5d52-11ea-8f07-0392944939c1', + version: 'WzEwMiwxXQ==', + eventId: '6HW_eHABMQha2n6bHvQ0', + note: 'this is a note!!', + timelineId: '890b8ae0-57df-11ea-a7c9-3976b7f1cb37', + created: 1583241924223, + createdBy: 'elastic', + updated: 1583241924223, + updatedBy: 'elastic', + }, + ], +}); + +export const mockPinnedEvents = () => ({ + saved_objects: [], +}); diff --git a/x-pack/plugins/siem/server/lib/timeline/routes/export_timelines_route.test.ts b/x-pack/plugins/siem/server/lib/timeline/routes/export_timelines_route.test.ts new file mode 100644 index 0000000000000..47ca25e16bd50 --- /dev/null +++ b/x-pack/plugins/siem/server/lib/timeline/routes/export_timelines_route.test.ts @@ -0,0 +1,109 @@ +/* + * 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 { + mockTimelines, + mockNotes, + mockTimelinesSavedObjects, + mockPinnedEvents, + getExportTimelinesRequest, +} from './__mocks__/request_responses'; +import { exportTimelinesRoute } from './export_timelines_route'; +import { + serverMock, + requestContextMock, + requestMock, + createMockConfig, +} from '../../detection_engine/routes/__mocks__'; +import { TIMELINE_EXPORT_URL } from '../../../../common/constants'; +import { convertSavedObjectToSavedNote } from '../../note/saved_object'; +import { convertSavedObjectToSavedPinnedEvent } from '../../pinned_event/saved_object'; +import { convertSavedObjectToSavedTimeline } from '../convert_saved_object_to_savedtimeline'; +jest.mock('../convert_saved_object_to_savedtimeline', () => { + return { + convertSavedObjectToSavedTimeline: jest.fn(), + }; +}); + +jest.mock('../../note/saved_object', () => { + return { + convertSavedObjectToSavedNote: jest.fn(), + }; +}); + +jest.mock('../../pinned_event/saved_object', () => { + return { + convertSavedObjectToSavedPinnedEvent: jest.fn(), + }; +}); +describe('export timelines', () => { + let server: ReturnType; + let { clients, context } = requestContextMock.createTools(); + + beforeEach(() => { + server = serverMock.create(); + ({ clients, context } = requestContextMock.createTools()); + + clients.savedObjectsClient.bulkGet.mockResolvedValue(mockTimelinesSavedObjects()); + + ((convertSavedObjectToSavedTimeline as unknown) as jest.Mock).mockReturnValue(mockTimelines()); + ((convertSavedObjectToSavedNote as unknown) as jest.Mock).mockReturnValue(mockNotes()); + ((convertSavedObjectToSavedPinnedEvent as unknown) as jest.Mock).mockReturnValue( + mockPinnedEvents() + ); + exportTimelinesRoute(server.router, createMockConfig()); + }); + + describe('status codes', () => { + test('returns 200 when finding selected timelines', async () => { + const response = await server.inject(getExportTimelinesRequest(), context); + expect(response.status).toEqual(200); + }); + + test('catch error when status search throws error', async () => { + clients.savedObjectsClient.bulkGet.mockReset(); + clients.savedObjectsClient.bulkGet.mockRejectedValue(new Error('Test error')); + const response = await server.inject(getExportTimelinesRequest(), context); + expect(response.status).toEqual(500); + expect(response.body).toEqual({ + message: 'Test error', + status_code: 500, + }); + }); + }); + + describe('request validation', () => { + test('return validation error for request body', async () => { + const request = requestMock.create({ + method: 'get', + path: TIMELINE_EXPORT_URL, + body: { id: 'someId' }, + }); + const result = server.validate(request); + + expect(result.badRequest.mock.calls[0][0]).toEqual( + 'Invalid value undefined supplied to : { ids: Array }/ids: Array' + ); + }); + + test('return validation error for request params', async () => { + const request = requestMock.create({ + method: 'get', + path: TIMELINE_EXPORT_URL, + body: { id: 'someId' }, + }); + const result = server.validate(request); + + expect(result.badRequest.mock.calls[1][0]).toEqual( + [ + 'Invalid value undefined supplied to : { file_name: string, exclude_export_details: ("true" | "false") }/file_name: string', + 'Invalid value undefined supplied to : { file_name: string, exclude_export_details: ("true" | "false") }/exclude_export_details: ("true" | "false")/0: "true"', + 'Invalid value undefined supplied to : { file_name: string, exclude_export_details: ("true" | "false") }/exclude_export_details: ("true" | "false")/1: "false"', + ].join('\n') + ); + }); + }); +}); diff --git a/x-pack/plugins/siem/server/lib/timeline/routes/export_timelines_route.ts b/x-pack/plugins/siem/server/lib/timeline/routes/export_timelines_route.ts new file mode 100644 index 0000000000000..c59f6eb6ce3da --- /dev/null +++ b/x-pack/plugins/siem/server/lib/timeline/routes/export_timelines_route.ts @@ -0,0 +1,69 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { set as _set } from 'lodash/fp'; + +import { TIMELINE_EXPORT_URL } from '../../../../common/constants'; +import { IRouter } from '../../../../../../../src/core/server'; +import { ConfigType } from '../../..'; +import { transformError, buildSiemResponse } from '../../detection_engine/routes/utils'; + +import { getExportTimelineByObjectIds } from './utils/export_timelines'; +import { + exportTimelinesQuerySchema, + exportTimelinesRequestBodySchema, +} from './schemas/export_timelines_schema'; +import { buildRouteValidation } from '../../../utils/build_validation/route_validation'; + +export const exportTimelinesRoute = (router: IRouter, config: ConfigType) => { + router.post( + { + path: TIMELINE_EXPORT_URL, + validate: { + query: buildRouteValidation(exportTimelinesQuerySchema), + body: buildRouteValidation(exportTimelinesRequestBodySchema), + }, + options: { + tags: ['access:siem'], + }, + }, + async (context, request, response) => { + try { + const siemResponse = buildSiemResponse(response); + const savedObjectsClient = context.core.savedObjects.client; + const exportSizeLimit = config.maxTimelineImportExportSize; + + if (request.body?.ids != null && request.body.ids.length > exportSizeLimit) { + return siemResponse.error({ + statusCode: 400, + body: `Can't export more than ${exportSizeLimit} timelines`, + }); + } + + const responseBody = await getExportTimelineByObjectIds({ + client: savedObjectsClient, + ids: request.body.ids, + }); + + return response.ok({ + headers: { + 'Content-Disposition': `attachment; filename="${request.query.file_name}"`, + 'Content-Type': 'application/ndjson', + }, + body: responseBody, + }); + } catch (err) { + const error = transformError(err); + const siemResponse = buildSiemResponse(response); + + return siemResponse.error({ + body: error.message, + statusCode: error.statusCode, + }); + } + } + ); +}; diff --git a/x-pack/legacy/plugins/siem/server/lib/timeline/routes/import_timelines_route.test.ts b/x-pack/plugins/siem/server/lib/timeline/routes/import_timelines_route.test.ts similarity index 94% rename from x-pack/legacy/plugins/siem/server/lib/timeline/routes/import_timelines_route.test.ts rename to x-pack/plugins/siem/server/lib/timeline/routes/import_timelines_route.test.ts index e89aef4c70ecb..3931bf0e5bea5 100644 --- a/x-pack/legacy/plugins/siem/server/lib/timeline/routes/import_timelines_route.test.ts +++ b/x-pack/plugins/siem/server/lib/timeline/routes/import_timelines_route.test.ts @@ -6,15 +6,15 @@ import { getImportTimelinesRequest } from './__mocks__/request_responses'; import { + createMockConfig, serverMock, requestContextMock, requestMock, } from '../../detection_engine/routes/__mocks__'; import { TIMELINE_EXPORT_URL } from '../../../../common/constants'; -import { SecurityPluginSetup } from '../../../../../../../plugins/security/server'; +import { SecurityPluginSetup } from '../../../../../security/server'; import { - mockConfig, mockUniqueParsedObjects, mockParsedObjects, mockDuplicateIdErrors, @@ -24,7 +24,7 @@ import { } from './__mocks__/import_timelines'; describe('import timelines', () => { - let config: jest.Mock; + let config: ReturnType; let server: ReturnType; let request: ReturnType; let securitySetup: SecurityPluginSetup; @@ -43,9 +43,7 @@ describe('import timelines', () => { server = serverMock.create(); context = requestContextMock.createTools().context; - config = jest.fn().mockImplementation(() => { - return mockConfig; - }); + config = createMockConfig(); securitySetup = ({ authc: { @@ -65,7 +63,7 @@ describe('import timelines', () => { }; }); - jest.doMock('../../../../../../../../src/legacy/utils', () => { + jest.doMock('../../../../../../../src/legacy/utils', () => { return { createPromiseFromStreams: jest.fn().mockReturnValue(mockParsedObjects), }; @@ -334,7 +332,10 @@ describe('import timelines', () => { const result = server.validate(request); expect(result.badRequest).toHaveBeenCalledWith( - 'child "file" fails because ["file" is required]' + [ + 'Invalid value undefined supplied to : { file: (ReadableRt & { hapi: { filename: string } }) }/file: (ReadableRt & { hapi: { filename: string } })/0: ReadableRt', + 'Invalid value undefined supplied to : { file: (ReadableRt & { hapi: { filename: string } }) }/file: (ReadableRt & { hapi: { filename: string } })/1: { hapi: { filename: string } }', + ].join('\n') ); }); }); diff --git a/x-pack/legacy/plugins/siem/server/lib/timeline/routes/import_timelines_route.ts b/x-pack/plugins/siem/server/lib/timeline/routes/import_timelines_route.ts similarity index 85% rename from x-pack/legacy/plugins/siem/server/lib/timeline/routes/import_timelines_route.ts rename to x-pack/plugins/siem/server/lib/timeline/routes/import_timelines_route.ts index 2b41b4e7843a7..258ef9faf671b 100644 --- a/x-pack/legacy/plugins/siem/server/lib/timeline/routes/import_timelines_route.ts +++ b/x-pack/plugins/siem/server/lib/timeline/routes/import_timelines_route.ts @@ -6,8 +6,9 @@ import { extname } from 'path'; import { chunk, omit, set } from 'lodash/fp'; + +import { TIMELINE_IMPORT_URL } from '../../../../common/constants'; import { - buildRouteValidation, buildSiemResponse, createBulkErrorObject, BulkError, @@ -15,7 +16,7 @@ import { } from '../../detection_engine/routes/utils'; import { createTimelinesStreamFromNdJson } from '../create_timelines_stream_from_ndjson'; -import { createPromiseFromStreams } from '../../../../../../../../src/legacy/utils'; +import { createPromiseFromStreams } from '../../../../../../../src/legacy/utils'; import { createTimelines, @@ -23,70 +24,68 @@ import { isBulkError, isImportRegular, ImportTimelineResponse, - ImportTimelinesRequestParams, ImportTimelinesSchema, PromiseFromStreams, } from './utils/import_timelines'; -import { IRouter } from '../../../../../../../../src/core/server'; -import { TIMELINE_IMPORT_URL } from '../../../../common/constants'; +import { IRouter } from '../../../../../../../src/core/server'; import { SetupPlugins } from '../../../plugin'; -import { importTimelinesPayloadSchema } from './schemas/import_timelines_schema'; +import { ImportTimelinesPayloadSchemaRt } from './schemas/import_timelines_schema'; import { importRulesSchema } from '../../detection_engine/routes/schemas/response/import_rules_schema'; -import { LegacyServices } from '../../../types'; +import { ConfigType } from '../../..'; import { Timeline } from '../saved_object'; import { validate } from '../../detection_engine/routes/rules/validate'; import { FrameworkRequest } from '../../framework'; - +import { buildRouteValidation } from '../../../utils/build_validation/route_validation'; const CHUNK_PARSED_OBJECT_SIZE = 10; const timelineLib = new Timeline(); export const importTimelinesRoute = ( router: IRouter, - config: LegacyServices['config'], + config: ConfigType, security: SetupPlugins['security'] ) => { router.post( { path: `${TIMELINE_IMPORT_URL}`, validate: { - body: buildRouteValidation( - importTimelinesPayloadSchema - ), + body: buildRouteValidation(ImportTimelinesPayloadSchemaRt), }, options: { tags: ['access:siem'], body: { - maxBytes: config().get('savedObjects.maxImportPayloadBytes'), + maxBytes: config.maxTimelineImportPayloadBytes, output: 'stream', }, }, }, async (context, request, response) => { - const siemResponse = buildSiemResponse(response); - const savedObjectsClient = context.core.savedObjects.client; - if (!savedObjectsClient) { - return siemResponse.error({ statusCode: 404 }); - } - const { filename } = request.body.file.hapi; + try { + const siemResponse = buildSiemResponse(response); + const savedObjectsClient = context.core.savedObjects.client; + if (!savedObjectsClient) { + return siemResponse.error({ statusCode: 404 }); + } - const fileExtension = extname(filename).toLowerCase(); + const { file } = request.body; + const { filename } = file.hapi; - if (fileExtension !== '.ndjson') { - return siemResponse.error({ - statusCode: 400, - body: `Invalid file extension ${fileExtension}`, - }); - } + const fileExtension = extname(filename).toLowerCase(); - const objectLimit = config().get('savedObjects.maxImportExportSize'); + if (fileExtension !== '.ndjson') { + return siemResponse.error({ + statusCode: 400, + body: `Invalid file extension ${fileExtension}`, + }); + } + + const objectLimit = config.maxTimelineImportExportSize; - try { const readStream = createTimelinesStreamFromNdJson(objectLimit); const parsedObjects = await createPromiseFromStreams([ - request.body.file, + file, ...readStream, ]); const [duplicateIdErrors, uniqueParsedObjects] = getTupleDuplicateErrorsAndUniqueTimeline( @@ -215,6 +214,7 @@ export const importTimelinesRoute = ( } } catch (err) { const error = transformError(err); + const siemResponse = buildSiemResponse(response); return siemResponse.error({ body: error.message, diff --git a/x-pack/plugins/siem/server/lib/timeline/routes/schemas/export_timelines_schema.ts b/x-pack/plugins/siem/server/lib/timeline/routes/schemas/export_timelines_schema.ts new file mode 100644 index 0000000000000..6f8265903b2a7 --- /dev/null +++ b/x-pack/plugins/siem/server/lib/timeline/routes/schemas/export_timelines_schema.ts @@ -0,0 +1,16 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import * as rt from 'io-ts'; + +export const exportTimelinesQuerySchema = rt.type({ + file_name: rt.string, + exclude_export_details: rt.union([rt.literal('true'), rt.literal('false')]), +}); + +export const exportTimelinesRequestBodySchema = rt.type({ + ids: rt.array(rt.string), +}); diff --git a/x-pack/plugins/siem/server/lib/timeline/routes/schemas/import_timelines_schema.ts b/x-pack/plugins/siem/server/lib/timeline/routes/schemas/import_timelines_schema.ts new file mode 100644 index 0000000000000..056fdaf0d2515 --- /dev/null +++ b/x-pack/plugins/siem/server/lib/timeline/routes/schemas/import_timelines_schema.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 * as rt from 'io-ts'; + +import { Readable } from 'stream'; +import { either } from 'fp-ts/lib/Either'; +import { eventNotes, globalNotes, pinnedEventIds } from './schemas'; +import { SavedTimelineRuntimeType } from '../../types'; + +export const ImportTimelinesSchemaRt = rt.intersection([ + SavedTimelineRuntimeType, + rt.type({ + savedObjectId: rt.string, + version: rt.string, + }), + rt.type({ + globalNotes, + eventNotes, + pinnedEventIds, + }), +]); + +const ReadableRt = new rt.Type( + 'ReadableRt', + (u): u is Readable => u instanceof Readable, + (u, c) => + either.chain(rt.object.validate(u, c), s => { + const d = s as Readable; + return d.readable ? rt.success(d) : rt.failure(u, c); + }), + a => a +); +export const ImportTimelinesPayloadSchemaRt = rt.type({ + file: rt.intersection([ + ReadableRt, + rt.type({ + hapi: rt.type({ filename: rt.string }), + }), + ]), +}); diff --git a/x-pack/plugins/siem/server/lib/timeline/routes/schemas/schemas.ts b/x-pack/plugins/siem/server/lib/timeline/routes/schemas/schemas.ts new file mode 100644 index 0000000000000..71627363ef0f8 --- /dev/null +++ b/x-pack/plugins/siem/server/lib/timeline/routes/schemas/schemas.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 * as runtimeTypes from 'io-ts'; +import { unionWithNullType } from '../../../framework'; +import { SavedNoteRuntimeType } from '../../../note/types'; + +export const eventNotes = runtimeTypes.array(unionWithNullType(SavedNoteRuntimeType)); +export const globalNotes = runtimeTypes.array(unionWithNullType(SavedNoteRuntimeType)); +export const pinnedEventIds = runtimeTypes.array(unionWithNullType(runtimeTypes.string)); diff --git a/x-pack/legacy/plugins/siem/server/lib/timeline/routes/utils/export_timelines.ts b/x-pack/plugins/siem/server/lib/timeline/routes/utils/export_timelines.ts similarity index 88% rename from x-pack/legacy/plugins/siem/server/lib/timeline/routes/utils/export_timelines.ts rename to x-pack/plugins/siem/server/lib/timeline/routes/utils/export_timelines.ts index 8a28100fbae82..edd4abe0d76b5 100644 --- a/x-pack/legacy/plugins/siem/server/lib/timeline/routes/utils/export_timelines.ts +++ b/x-pack/plugins/siem/server/lib/timeline/routes/utils/export_timelines.ts @@ -21,17 +21,16 @@ import { SavedObjectsClient, SavedObjectsFindOptions, SavedObjectsFindResponse, -} from '../../../../../../../../../src/core/server'; +} from '../../../../../../../../src/core/server'; import { ExportedTimelines, ExportTimelineSavedObjectsClient, - ExportTimelineRequest, ExportedNotes, TimelineSavedObject, } from '../../types'; +import { transformDataToNdjson } from '../../../../utils/read_stream/create_stream_from_ndjson'; -import { transformDataToNdjson } from '../../../detection_engine/routes/rules/utils'; export type TimelineSavedObjectsClient = Pick< SavedObjectsClient, | 'get' @@ -142,23 +141,17 @@ const getTimelines = async ( const getTimelinesFromObjects = async ( savedObjectsClient: ExportTimelineSavedObjectsClient, - request: ExportTimelineRequest + ids: string[] ): Promise => { - const timelines: TimelineSavedObject[] = await getTimelines(savedObjectsClient, request.body.ids); + const timelines: TimelineSavedObject[] = await getTimelines(savedObjectsClient, ids); // To Do for feature freeze // if (timelines.length !== request.body.ids.length) { // //figure out which is missing to tell user // } const [notes, pinnedEventIds] = await Promise.all([ - Promise.all( - request.body.ids.map(timelineId => getNotesByTimelineId(savedObjectsClient, timelineId)) - ), - Promise.all( - request.body.ids.map(timelineId => - getPinnedEventsByTimelineId(savedObjectsClient, timelineId) - ) - ), + Promise.all(ids.map(timelineId => getNotesByTimelineId(savedObjectsClient, timelineId))), + Promise.all(ids.map(timelineId => getPinnedEventsByTimelineId(savedObjectsClient, timelineId))), ]); const myNotes = notes.reduce( @@ -171,7 +164,7 @@ const getTimelinesFromObjects = async ( [] ); - const myResponse = request.body.ids.reduce((acc, timelineId) => { + const myResponse = ids.reduce((acc, timelineId) => { const myTimeline = timelines.find(t => t.savedObjectId === timelineId); if (myTimeline != null) { const timelineNotes = myNotes.filter(n => n.timelineId === timelineId); @@ -193,11 +186,11 @@ const getTimelinesFromObjects = async ( export const getExportTimelineByObjectIds = async ({ client, - request, + ids, }: { client: ExportTimelineSavedObjectsClient; - request: ExportTimelineRequest; + ids: string[]; }) => { - const timeline = await getTimelinesFromObjects(client, request); + const timeline = await getTimelinesFromObjects(client, ids); return transformDataToNdjson(timeline); }; diff --git a/x-pack/legacy/plugins/siem/server/lib/timeline/routes/utils/import_timelines.ts b/x-pack/plugins/siem/server/lib/timeline/routes/utils/import_timelines.ts similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/timeline/routes/utils/import_timelines.ts rename to x-pack/plugins/siem/server/lib/timeline/routes/utils/import_timelines.ts diff --git a/x-pack/legacy/plugins/siem/server/lib/timeline/saved_object.test.ts b/x-pack/plugins/siem/server/lib/timeline/saved_object.test.ts similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/timeline/saved_object.test.ts rename to x-pack/plugins/siem/server/lib/timeline/saved_object.test.ts diff --git a/x-pack/plugins/siem/server/lib/timeline/saved_object.ts b/x-pack/plugins/siem/server/lib/timeline/saved_object.ts new file mode 100644 index 0000000000000..e8cd27947589f --- /dev/null +++ b/x-pack/plugins/siem/server/lib/timeline/saved_object.ts @@ -0,0 +1,293 @@ +/* + * 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 { getOr } from 'lodash/fp'; + +import { SavedObjectsFindOptions } from '../../../../../../src/core/server'; +import { UNAUTHENTICATED_USER } from '../../../common/constants'; +import { + ResponseTimeline, + PageInfoTimeline, + SortTimeline, + ResponseFavoriteTimeline, + TimelineResult, +} from '../../graphql/types'; +import { FrameworkRequest } from '../framework'; +import { Note } from '../note/saved_object'; +import { NoteSavedObject } from '../note/types'; +import { PinnedEventSavedObject } from '../pinned_event/types'; +import { PinnedEvent } from '../pinned_event/saved_object'; +import { convertSavedObjectToSavedTimeline } from './convert_saved_object_to_savedtimeline'; +import { pickSavedTimeline } from './pick_saved_timeline'; +import { timelineSavedObjectType } from './saved_object_mappings'; +import { SavedTimeline, TimelineSavedObject } from './types'; + +interface ResponseTimelines { + timeline: TimelineSavedObject[]; + totalCount: number; +} + +export class Timeline { + private readonly note = new Note(); + private readonly pinnedEvent = new PinnedEvent(); + + public async getTimeline( + request: FrameworkRequest, + timelineId: string + ): Promise { + return this.getSavedTimeline(request, timelineId); + } + + public async getAllTimeline( + request: FrameworkRequest, + onlyUserFavorite: boolean | null, + pageInfo: PageInfoTimeline | null, + search: string | null, + sort: SortTimeline | null + ): Promise { + const options: SavedObjectsFindOptions = { + type: timelineSavedObjectType, + perPage: pageInfo != null ? pageInfo.pageSize : undefined, + page: pageInfo != null ? pageInfo.pageIndex : undefined, + search: search != null ? search : undefined, + searchFields: onlyUserFavorite + ? ['title', 'description', 'favorite.keySearch'] + : ['title', 'description'], + sortField: sort != null ? sort.sortField : undefined, + sortOrder: sort != null ? sort.sortOrder : undefined, + }; + + return this.getAllSavedTimeline(request, options); + } + + public async persistFavorite( + request: FrameworkRequest, + timelineId: string | null + ): Promise { + const userName = request.user?.username ?? UNAUTHENTICATED_USER; + const fullName = request.user?.full_name ?? ''; + try { + let timeline: SavedTimeline = {}; + if (timelineId != null) { + const { + eventIdToNoteIds, + notes, + noteIds, + pinnedEventIds, + pinnedEventsSaveObject, + savedObjectId, + version, + ...savedTimeline + } = await this.getBasicSavedTimeline(request, timelineId); + timelineId = savedObjectId; // eslint-disable-line no-param-reassign + timeline = savedTimeline; + } + + const userFavoriteTimeline = { + keySearch: userName != null ? convertStringToBase64(userName) : null, + favoriteDate: new Date().valueOf(), + fullName, + userName, + }; + if (timeline.favorite != null) { + const alreadyExistsTimelineFavoriteByUser = timeline.favorite.findIndex( + user => user.userName === userName + ); + + timeline.favorite = + alreadyExistsTimelineFavoriteByUser > -1 + ? [ + ...timeline.favorite.slice(0, alreadyExistsTimelineFavoriteByUser), + ...timeline.favorite.slice(alreadyExistsTimelineFavoriteByUser + 1), + ] + : [...timeline.favorite, userFavoriteTimeline]; + } else if (timeline.favorite == null) { + timeline.favorite = [userFavoriteTimeline]; + } + + const persistResponse = await this.persistTimeline(request, timelineId, null, timeline); + return { + savedObjectId: persistResponse.timeline.savedObjectId, + version: persistResponse.timeline.version, + favorite: + persistResponse.timeline.favorite != null + ? persistResponse.timeline.favorite.filter(fav => fav.userName === userName) + : [], + }; + } catch (err) { + if (getOr(null, 'output.statusCode', err) === 403) { + return { + savedObjectId: '', + version: '', + favorite: [], + code: 403, + message: err.message, + }; + } + throw err; + } + } + + public async persistTimeline( + request: FrameworkRequest, + timelineId: string | null, + version: string | null, + timeline: SavedTimeline + ): Promise { + const savedObjectsClient = request.context.core.savedObjects.client; + try { + if (timelineId == null) { + // Create new timeline + const newTimeline = convertSavedObjectToSavedTimeline( + await savedObjectsClient.create( + timelineSavedObjectType, + pickSavedTimeline(timelineId, timeline, request.user) + ) + ); + return { + code: 200, + message: 'success', + timeline: newTimeline, + }; + } + // Update Timeline + await savedObjectsClient.update( + timelineSavedObjectType, + timelineId, + pickSavedTimeline(timelineId, timeline, request.user), + { + version: version || undefined, + } + ); + + return { + code: 200, + message: 'success', + timeline: await this.getSavedTimeline(request, timelineId), + }; + } catch (err) { + if (timelineId != null && savedObjectsClient.errors.isConflictError(err)) { + return { + code: 409, + message: err.message, + timeline: await this.getSavedTimeline(request, timelineId), + }; + } else if (getOr(null, 'output.statusCode', err) === 403) { + const timelineToReturn: TimelineResult = { + ...timeline, + savedObjectId: '', + version: '', + }; + return { + code: 403, + message: err.message, + timeline: timelineToReturn, + }; + } + throw err; + } + } + + public async deleteTimeline(request: FrameworkRequest, timelineIds: string[]) { + const savedObjectsClient = request.context.core.savedObjects.client; + + await Promise.all( + timelineIds.map(timelineId => + Promise.all([ + savedObjectsClient.delete(timelineSavedObjectType, timelineId), + this.note.deleteNoteByTimelineId(request, timelineId), + this.pinnedEvent.deleteAllPinnedEventsOnTimeline(request, timelineId), + ]) + ) + ); + } + + private async getBasicSavedTimeline(request: FrameworkRequest, timelineId: string) { + const savedObjectsClient = request.context.core.savedObjects.client; + const savedObject = await savedObjectsClient.get(timelineSavedObjectType, timelineId); + + return convertSavedObjectToSavedTimeline(savedObject); + } + + private async getSavedTimeline(request: FrameworkRequest, timelineId: string) { + const userName = request.user?.username ?? UNAUTHENTICATED_USER; + + const savedObjectsClient = request.context.core.savedObjects.client; + const savedObject = await savedObjectsClient.get(timelineSavedObjectType, timelineId); + const timelineSaveObject = convertSavedObjectToSavedTimeline(savedObject); + const timelineWithNotesAndPinnedEvents = await Promise.all([ + this.note.getNotesByTimelineId(request, timelineSaveObject.savedObjectId), + this.pinnedEvent.getAllPinnedEventsByTimelineId(request, timelineSaveObject.savedObjectId), + Promise.resolve(timelineSaveObject), + ]); + + const [notes, pinnedEvents, timeline] = timelineWithNotesAndPinnedEvents; + + return timelineWithReduxProperties(notes, pinnedEvents, timeline, userName); + } + + private async getAllSavedTimeline(request: FrameworkRequest, options: SavedObjectsFindOptions) { + const userName = request.user?.username ?? UNAUTHENTICATED_USER; + const savedObjectsClient = request.context.core.savedObjects.client; + if (options.searchFields != null && options.searchFields.includes('favorite.keySearch')) { + options.search = `${options.search != null ? options.search : ''} ${ + userName != null ? convertStringToBase64(userName) : null + }`; + } + + const savedObjects = await savedObjectsClient.find(options); + + const timelinesWithNotesAndPinnedEvents = await Promise.all( + savedObjects.saved_objects.map(async savedObject => { + const timelineSaveObject = convertSavedObjectToSavedTimeline(savedObject); + return Promise.all([ + this.note.getNotesByTimelineId(request, timelineSaveObject.savedObjectId), + this.pinnedEvent.getAllPinnedEventsByTimelineId( + request, + timelineSaveObject.savedObjectId + ), + Promise.resolve(timelineSaveObject), + ]); + }) + ); + + return { + totalCount: savedObjects.total, + timeline: timelinesWithNotesAndPinnedEvents.map(([notes, pinnedEvents, timeline]) => + timelineWithReduxProperties(notes, pinnedEvents, timeline, userName) + ), + }; + } +} + +export const convertStringToBase64 = (text: string): string => Buffer.from(text).toString('base64'); + +// we have to use any here because the SavedObjectAttributes interface is like below +// export interface SavedObjectAttributes { +// [key: string]: SavedObjectAttributes | string | number | boolean | null; +// } +// then this interface does not allow types without index signature +// this is limiting us with our type for now so the easy way was to use any + +export const timelineWithReduxProperties = ( + notes: NoteSavedObject[], + pinnedEvents: PinnedEventSavedObject[], + timeline: TimelineSavedObject, + userName: string +): TimelineSavedObject => ({ + ...timeline, + favorite: + timeline.favorite != null && userName != null + ? timeline.favorite.filter(fav => fav.userName === userName) + : [], + eventIdToNoteIds: notes.filter(note => note.eventId != null), + noteIds: notes + .filter(note => note.eventId == null && note.noteId != null) + .map(note => note.noteId), + notes, + pinnedEventIds: pinnedEvents.map(pinnedEvent => pinnedEvent.eventId), + pinnedEventsSaveObject: pinnedEvents, +}); diff --git a/x-pack/legacy/plugins/siem/server/lib/timeline/saved_object_mappings.ts b/x-pack/plugins/siem/server/lib/timeline/saved_object_mappings.ts similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/timeline/saved_object_mappings.ts rename to x-pack/plugins/siem/server/lib/timeline/saved_object_mappings.ts diff --git a/x-pack/plugins/siem/server/lib/timeline/types.ts b/x-pack/plugins/siem/server/lib/timeline/types.ts new file mode 100644 index 0000000000000..0bce3300591c2 --- /dev/null +++ b/x-pack/plugins/siem/server/lib/timeline/types.ts @@ -0,0 +1,245 @@ +/* + * 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. + */ + +/* eslint-disable @typescript-eslint/no-empty-interface */ + +import * as runtimeTypes from 'io-ts'; + +import { unionWithNullType } from '../framework'; +import { NoteSavedObjectToReturnRuntimeType, NoteSavedObject } from '../note/types'; +import { + PinnedEventToReturnSavedObjectRuntimeType, + PinnedEventSavedObject, +} from '../pinned_event/types'; +import { SavedObjectsClient } from '../../../../../../src/core/server'; + +/* + * ColumnHeader Types + */ +const SavedColumnHeaderRuntimeType = runtimeTypes.partial({ + aggregatable: unionWithNullType(runtimeTypes.boolean), + category: unionWithNullType(runtimeTypes.string), + columnHeaderType: unionWithNullType(runtimeTypes.string), + description: unionWithNullType(runtimeTypes.string), + example: unionWithNullType(runtimeTypes.string), + indexes: unionWithNullType(runtimeTypes.array(runtimeTypes.string)), + id: unionWithNullType(runtimeTypes.string), + name: unionWithNullType(runtimeTypes.string), + placeholder: unionWithNullType(runtimeTypes.string), + searchable: unionWithNullType(runtimeTypes.boolean), + type: unionWithNullType(runtimeTypes.string), +}); + +/* + * DataProvider Types + */ +const SavedDataProviderQueryMatchBasicRuntimeType = runtimeTypes.partial({ + field: unionWithNullType(runtimeTypes.string), + displayField: unionWithNullType(runtimeTypes.string), + value: unionWithNullType(runtimeTypes.string), + displayValue: unionWithNullType(runtimeTypes.string), + operator: unionWithNullType(runtimeTypes.string), +}); + +const SavedDataProviderQueryMatchRuntimeType = runtimeTypes.partial({ + id: unionWithNullType(runtimeTypes.string), + name: unionWithNullType(runtimeTypes.string), + enabled: unionWithNullType(runtimeTypes.boolean), + excluded: unionWithNullType(runtimeTypes.boolean), + kqlQuery: unionWithNullType(runtimeTypes.string), + queryMatch: unionWithNullType(SavedDataProviderQueryMatchBasicRuntimeType), +}); + +const SavedDataProviderRuntimeType = runtimeTypes.partial({ + id: unionWithNullType(runtimeTypes.string), + name: unionWithNullType(runtimeTypes.string), + enabled: unionWithNullType(runtimeTypes.boolean), + excluded: unionWithNullType(runtimeTypes.boolean), + kqlQuery: unionWithNullType(runtimeTypes.string), + queryMatch: unionWithNullType(SavedDataProviderQueryMatchBasicRuntimeType), + and: unionWithNullType(runtimeTypes.array(SavedDataProviderQueryMatchRuntimeType)), +}); + +/* + * Filters Types + */ +const SavedFilterMetaRuntimeType = runtimeTypes.partial({ + alias: unionWithNullType(runtimeTypes.string), + controlledBy: unionWithNullType(runtimeTypes.string), + disabled: unionWithNullType(runtimeTypes.boolean), + field: unionWithNullType(runtimeTypes.string), + formattedValue: unionWithNullType(runtimeTypes.string), + index: unionWithNullType(runtimeTypes.string), + key: unionWithNullType(runtimeTypes.string), + negate: unionWithNullType(runtimeTypes.boolean), + params: unionWithNullType(runtimeTypes.string), + type: unionWithNullType(runtimeTypes.string), + value: unionWithNullType(runtimeTypes.string), +}); + +const SavedFilterRuntimeType = runtimeTypes.partial({ + exists: unionWithNullType(runtimeTypes.string), + meta: unionWithNullType(SavedFilterMetaRuntimeType), + match_all: unionWithNullType(runtimeTypes.string), + missing: unionWithNullType(runtimeTypes.string), + query: unionWithNullType(runtimeTypes.string), + range: unionWithNullType(runtimeTypes.string), + script: unionWithNullType(runtimeTypes.string), +}); + +/* + * kqlQuery -> filterQuery Types + */ +const SavedKueryFilterQueryRuntimeType = runtimeTypes.partial({ + kind: unionWithNullType(runtimeTypes.string), + expression: unionWithNullType(runtimeTypes.string), +}); + +const SavedSerializedFilterQueryQueryRuntimeType = runtimeTypes.partial({ + kuery: unionWithNullType(SavedKueryFilterQueryRuntimeType), + serializedQuery: unionWithNullType(runtimeTypes.string), +}); + +const SavedFilterQueryQueryRuntimeType = runtimeTypes.partial({ + filterQuery: unionWithNullType(SavedSerializedFilterQueryQueryRuntimeType), +}); + +/* + * DatePicker Range Types + */ +const SavedDateRangePickerRuntimeType = runtimeTypes.partial({ + start: unionWithNullType(runtimeTypes.number), + end: unionWithNullType(runtimeTypes.number), +}); + +/* + * Favorite Types + */ +const SavedFavoriteRuntimeType = runtimeTypes.partial({ + keySearch: unionWithNullType(runtimeTypes.string), + favoriteDate: unionWithNullType(runtimeTypes.number), + fullName: unionWithNullType(runtimeTypes.string), + userName: unionWithNullType(runtimeTypes.string), +}); + +/* + * Sort Types + */ +const SavedSortRuntimeType = runtimeTypes.partial({ + columnId: unionWithNullType(runtimeTypes.string), + sortDirection: unionWithNullType(runtimeTypes.string), +}); + +/* + * Timeline Types + */ +export const SavedTimelineRuntimeType = runtimeTypes.partial({ + columns: unionWithNullType(runtimeTypes.array(SavedColumnHeaderRuntimeType)), + dataProviders: unionWithNullType(runtimeTypes.array(SavedDataProviderRuntimeType)), + description: unionWithNullType(runtimeTypes.string), + eventType: unionWithNullType(runtimeTypes.string), + favorite: unionWithNullType(runtimeTypes.array(SavedFavoriteRuntimeType)), + filters: unionWithNullType(runtimeTypes.array(SavedFilterRuntimeType)), + kqlMode: unionWithNullType(runtimeTypes.string), + kqlQuery: unionWithNullType(SavedFilterQueryQueryRuntimeType), + title: unionWithNullType(runtimeTypes.string), + dateRange: unionWithNullType(SavedDateRangePickerRuntimeType), + savedQueryId: unionWithNullType(runtimeTypes.string), + sort: unionWithNullType(SavedSortRuntimeType), + created: unionWithNullType(runtimeTypes.number), + createdBy: unionWithNullType(runtimeTypes.string), + updated: unionWithNullType(runtimeTypes.number), + updatedBy: unionWithNullType(runtimeTypes.string), +}); + +export interface SavedTimeline extends runtimeTypes.TypeOf {} + +export interface SavedTimelineNote extends runtimeTypes.TypeOf {} + +/** + * Timeline Saved object type with metadata + */ + +export const TimelineSavedObjectRuntimeType = runtimeTypes.intersection([ + runtimeTypes.type({ + id: runtimeTypes.string, + attributes: SavedTimelineRuntimeType, + version: runtimeTypes.string, + }), + runtimeTypes.partial({ + savedObjectId: runtimeTypes.string, + }), +]); + +export const TimelineSavedToReturnObjectRuntimeType = runtimeTypes.intersection([ + SavedTimelineRuntimeType, + runtimeTypes.type({ + savedObjectId: runtimeTypes.string, + version: runtimeTypes.string, + }), + runtimeTypes.partial({ + eventIdToNoteIds: runtimeTypes.array(NoteSavedObjectToReturnRuntimeType), + noteIds: runtimeTypes.array(runtimeTypes.string), + notes: runtimeTypes.array(NoteSavedObjectToReturnRuntimeType), + pinnedEventIds: runtimeTypes.array(runtimeTypes.string), + pinnedEventsSaveObject: runtimeTypes.array(PinnedEventToReturnSavedObjectRuntimeType), + }), +]); + +export interface TimelineSavedObject + extends runtimeTypes.TypeOf {} + +/** + * All Timeline Saved object type with metadata + */ + +export const AllTimelineSavedObjectRuntimeType = runtimeTypes.type({ + total: runtimeTypes.number, + data: TimelineSavedToReturnObjectRuntimeType, +}); + +export interface AllTimelineSavedObject + extends runtimeTypes.TypeOf {} + +/** + * Import/export timelines + */ + +export type ExportTimelineSavedObjectsClient = Pick< + SavedObjectsClient, + | 'get' + | 'errors' + | 'create' + | 'bulkCreate' + | 'delete' + | 'find' + | 'bulkGet' + | 'update' + | 'bulkUpdate' +>; + +export type ExportedGlobalNotes = Array>; +export type ExportedEventNotes = NoteSavedObject[]; + +export interface ExportedNotes { + eventNotes: ExportedEventNotes; + globalNotes: ExportedGlobalNotes; +} + +export type ExportedTimelines = TimelineSavedObject & + ExportedNotes & { + pinnedEventIds: string[]; + }; + +export interface BulkGetInput { + type: string; + id: string; +} + +export type NotesAndPinnedEventsByTimelineId = Record< + string, + { notes: NoteSavedObject[]; pinnedEvents: PinnedEventSavedObject[] } +>; diff --git a/x-pack/legacy/plugins/siem/server/lib/tls/elasticsearch_adapter.test.ts b/x-pack/plugins/siem/server/lib/tls/elasticsearch_adapter.test.ts similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/tls/elasticsearch_adapter.test.ts rename to x-pack/plugins/siem/server/lib/tls/elasticsearch_adapter.test.ts diff --git a/x-pack/legacy/plugins/siem/server/lib/tls/elasticsearch_adapter.ts b/x-pack/plugins/siem/server/lib/tls/elasticsearch_adapter.ts similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/tls/elasticsearch_adapter.ts rename to x-pack/plugins/siem/server/lib/tls/elasticsearch_adapter.ts diff --git a/x-pack/legacy/plugins/siem/server/lib/tls/index.ts b/x-pack/plugins/siem/server/lib/tls/index.ts similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/tls/index.ts rename to x-pack/plugins/siem/server/lib/tls/index.ts diff --git a/x-pack/legacy/plugins/siem/server/lib/tls/mock.ts b/x-pack/plugins/siem/server/lib/tls/mock.ts similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/tls/mock.ts rename to x-pack/plugins/siem/server/lib/tls/mock.ts diff --git a/x-pack/legacy/plugins/siem/server/lib/tls/query_tls.dsl.ts b/x-pack/plugins/siem/server/lib/tls/query_tls.dsl.ts similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/tls/query_tls.dsl.ts rename to x-pack/plugins/siem/server/lib/tls/query_tls.dsl.ts diff --git a/x-pack/plugins/siem/server/lib/tls/types.ts b/x-pack/plugins/siem/server/lib/tls/types.ts new file mode 100644 index 0000000000000..f18ddc04e14a0 --- /dev/null +++ b/x-pack/plugins/siem/server/lib/tls/types.ts @@ -0,0 +1,36 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { FrameworkRequest, RequestBasicOptions } from '../framework'; +import { TlsData } from '../../graphql/types'; + +export interface TlsAdapter { + getTls(request: FrameworkRequest, options: RequestBasicOptions): Promise; +} + +export interface TlsBuckets { + key: string; + timestamp?: { + value: number; + value_as_string: string; + }; + + subjects: { + buckets: Readonly>; + }; + + ja3: { + buckets: Readonly>; + }; + + issuers: { + buckets: Readonly>; + }; + + not_after: { + buckets: Readonly>; + }; +} diff --git a/x-pack/plugins/siem/server/lib/types.ts b/x-pack/plugins/siem/server/lib/types.ts new file mode 100644 index 0000000000000..a74fe8f778ba9 --- /dev/null +++ b/x-pack/plugins/siem/server/lib/types.ts @@ -0,0 +1,184 @@ +/* + * 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 { AuthenticatedUser } from '../../../security/public'; +import { RequestHandlerContext } from '../../../../../src/core/server'; +export { ConfigType as Configuration } from '../'; + +import { Authentications } from './authentications'; +import { Events } from './events'; +import { FrameworkAdapter, FrameworkRequest } from './framework'; +import { Hosts } from './hosts'; +import { IndexFields } from './index_fields'; +import { IpDetails } from './ip_details'; +import { KpiHosts } from './kpi_hosts'; +import { KpiNetwork } from './kpi_network'; +import { Network } from './network'; +import { Overview } from './overview'; +import { SourceStatus } from './source_status'; +import { Sources } from './sources'; +import { UncommonProcesses } from './uncommon_processes'; +import { Note } from './note/saved_object'; +import { PinnedEvent } from './pinned_event/saved_object'; +import { Timeline } from './timeline/saved_object'; +import { TLS } from './tls'; +import { MatrixHistogram } from './matrix_histogram'; + +export * from './hosts'; + +export interface AppDomainLibs { + authentications: Authentications; + events: Events; + fields: IndexFields; + hosts: Hosts; + ipDetails: IpDetails; + matrixHistogram: MatrixHistogram; + network: Network; + kpiNetwork: KpiNetwork; + overview: Overview; + uncommonProcesses: UncommonProcesses; + kpiHosts: KpiHosts; + tls: TLS; +} + +export interface AppBackendLibs extends AppDomainLibs { + framework: FrameworkAdapter; + sources: Sources; + sourceStatus: SourceStatus; + timeline: Timeline; + note: Note; + pinnedEvent: PinnedEvent; +} + +export interface SiemContext { + req: FrameworkRequest; + context: RequestHandlerContext; + user: AuthenticatedUser | null; +} + +export interface TotalValue { + value: number; + relation: string; +} + +export interface SearchResponse { + took: number; + timed_out: boolean; + _scroll_id?: string; + _shards: ShardsResponse; + hits: { + total: TotalValue | number; + max_score: number; + hits: Array<{ + _index: string; + _type: string; + _id: string; + _score: number; + _source: T; + _version?: number; + _explanation?: Explanation; + fields?: string[]; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + highlight?: any; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + inner_hits?: any; + matched_queries?: string[]; + sort?: string[]; + }>; + }; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + aggregations?: any; +} + +export interface ShardsResponse { + total: number; + successful: number; + failed: number; + skipped: number; +} + +export interface Explanation { + value: number; + description: string; + details: Explanation[]; +} + +export type SearchHit = SearchResponse['hits']['hits'][0]; + +export interface TermAggregation { + [agg: string]: { + buckets: Array<{ + key: string; + doc_count: number; + }>; + }; +} + +export interface TotalHit { + value: number; + relation: string; +} + +export interface Hit { + _index: string; + _type: string; + _id: string; + _score: number | null; +} + +export interface Hits { + hits: { + total: T; + max_score: number | null; + hits: U[]; + }; +} +export type SortRequestDirection = 'asc' | 'desc'; + +interface SortRequestField { + [field: string]: SortRequestDirection; +} + +export type SortRequest = SortRequestField[]; + +export interface MSearchHeader { + index: string[] | string; + allowNoIndices?: boolean; + ignoreUnavailable?: boolean; +} + +export interface AggregationRequest { + [aggField: string]: { + terms?: { + field: string; + size?: number; + order?: { + [aggSortField: string]: SortRequestDirection; + }; + }; + max?: { + field: string; + }; + aggs?: { + [aggSortField: string]: { + [aggType: string]: { + field: string; + }; + }; + }; + top_hits?: { + size?: number; + sort?: Array<{ + [aggSortField: string]: { + order: SortRequestDirection; + }; + }>; + _source: { + includes: string[]; + }; + }; + }; +} diff --git a/x-pack/legacy/plugins/siem/server/lib/uncommon_processes/elasticsearch_adapter.test.ts b/x-pack/plugins/siem/server/lib/uncommon_processes/elasticsearch_adapter.test.ts similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/uncommon_processes/elasticsearch_adapter.test.ts rename to x-pack/plugins/siem/server/lib/uncommon_processes/elasticsearch_adapter.test.ts diff --git a/x-pack/legacy/plugins/siem/server/lib/uncommon_processes/elasticsearch_adapter.ts b/x-pack/plugins/siem/server/lib/uncommon_processes/elasticsearch_adapter.ts similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/uncommon_processes/elasticsearch_adapter.ts rename to x-pack/plugins/siem/server/lib/uncommon_processes/elasticsearch_adapter.ts diff --git a/x-pack/legacy/plugins/siem/server/lib/uncommon_processes/index.ts b/x-pack/plugins/siem/server/lib/uncommon_processes/index.ts similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/uncommon_processes/index.ts rename to x-pack/plugins/siem/server/lib/uncommon_processes/index.ts diff --git a/x-pack/legacy/plugins/siem/server/lib/uncommon_processes/query.dsl.ts b/x-pack/plugins/siem/server/lib/uncommon_processes/query.dsl.ts similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/uncommon_processes/query.dsl.ts rename to x-pack/plugins/siem/server/lib/uncommon_processes/query.dsl.ts diff --git a/x-pack/legacy/plugins/siem/server/lib/uncommon_processes/types.ts b/x-pack/plugins/siem/server/lib/uncommon_processes/types.ts similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/uncommon_processes/types.ts rename to x-pack/plugins/siem/server/lib/uncommon_processes/types.ts diff --git a/x-pack/plugins/siem/server/plugin.ts b/x-pack/plugins/siem/server/plugin.ts index ccc6aef1452b2..b9ec1c2e92438 100644 --- a/x-pack/plugins/siem/server/plugin.ts +++ b/x-pack/plugins/siem/server/plugin.ts @@ -5,33 +5,203 @@ */ import { Observable } from 'rxjs'; +import { first } from 'rxjs/operators'; +import { i18n } from '@kbn/i18n'; -import { CoreSetup, PluginInitializerContext, Logger } from '../../../../src/core/server'; +import { + CoreSetup, + CoreStart, + PluginInitializerContext, + Logger, +} from '../../../../src/core/server'; +import { + PluginStartContract as AlertingStart, + PluginSetupContract as AlertingSetup, +} from '../../alerting/server'; +import { SecurityPluginSetup as SecuritySetup } from '../../security/server'; +import { PluginSetupContract as FeaturesSetup } from '../../features/server'; +import { MlPluginSetup as MlSetup } from '../../ml/server'; +import { EncryptedSavedObjectsPluginSetup as EncryptedSavedObjectsSetup } from '../../encrypted_saved_objects/server'; +import { SpacesPluginSetup as SpacesSetup } from '../../spaces/server'; +import { PluginStartContract as ActionsStart } from '../../actions/server'; +import { LicensingPluginSetup } from '../../licensing/server'; +import { initServer } from './init_server'; +import { compose } from './lib/compose/kibana'; +import { initRoutes } from './routes'; +import { isAlertExecutor } from './lib/detection_engine/signals/types'; +import { signalRulesAlertType } from './lib/detection_engine/signals/signal_rule_alert_type'; +import { rulesNotificationAlertType } from './lib/detection_engine/notifications/rules_notification_alert_type'; +import { isNotificationAlertExecutor } from './lib/detection_engine/notifications/types'; +import { hasListsFeature, listsEnvFeatureFlagName } from './lib/detection_engine/feature_flags'; +import { + noteSavedObjectType, + pinnedEventSavedObjectType, + timelineSavedObjectType, + ruleStatusSavedObjectType, + ruleActionsSavedObjectType, +} from './saved_objects'; +import { SiemClientFactory } from './client'; import { createConfig$, ConfigType } from './config'; +export { CoreSetup, CoreStart }; + +export interface SetupPlugins { + alerting: AlertingSetup; + encryptedSavedObjects?: EncryptedSavedObjectsSetup; + features: FeaturesSetup; + licensing: LicensingPluginSetup; + security?: SecuritySetup; + spaces?: SpacesSetup; + ml?: MlSetup; +} + +export interface StartPlugins { + actions: ActionsStart; + alerting: AlertingStart; +} + export class Plugin { readonly name = 'siem'; private readonly logger: Logger; - // @ts-ignore-next-line TODO(rylnd): use it or lose it private readonly config$: Observable; + private context: PluginInitializerContext; + private siemClientFactory: SiemClientFactory; constructor(context: PluginInitializerContext) { - const { logger } = context; - this.logger = logger.get(); - this.logger.debug('plugin initialized'); - + this.context = context; + this.logger = context.logger.get('plugins', this.name); this.config$ = createConfig$(context); + this.siemClientFactory = new SiemClientFactory(); + + this.logger.debug('plugin initialized'); } - public setup(core: CoreSetup, plugins: {}) { + public async setup(core: CoreSetup, plugins: SetupPlugins) { this.logger.debug('plugin setup'); - } - public start() { - this.logger.debug('plugin started'); - } + if (hasListsFeature()) { + // TODO: Remove this once we have the lists feature supported + this.logger.error( + `You have activated the lists feature flag which is NOT currently supported for SIEM! You should turn this feature flag off immediately by un-setting the environment variable: ${listsEnvFeatureFlagName} and restarting Kibana` + ); + } + + const router = core.http.createRouter(); + core.http.registerRouteHandlerContext(this.name, (context, request, response) => ({ + getSiemClient: () => this.siemClientFactory.create(request), + })); + + const config = await this.config$.pipe(first()).toPromise(); + + this.siemClientFactory.setup({ + getSpaceId: plugins.spaces?.spacesService?.getSpaceId, + config, + }); + + initRoutes( + router, + config, + plugins.encryptedSavedObjects?.usingEphemeralEncryptionKey ?? false, + plugins.security + ); - public stop() { - this.logger.debug('plugin stopped'); + plugins.features.registerFeature({ + id: this.name, + name: i18n.translate('xpack.siem.featureRegistry.linkSiemTitle', { + defaultMessage: 'SIEM', + }), + order: 1100, + icon: 'securityAnalyticsApp', + navLinkId: 'siem', + app: ['siem', 'kibana'], + catalogue: ['siem'], + privileges: { + all: { + app: ['siem', 'kibana'], + catalogue: ['siem'], + api: ['siem', 'actions-read', 'actions-all', 'alerting-read', 'alerting-all'], + savedObject: { + all: [ + 'alert', + 'action', + 'action_task_params', + noteSavedObjectType, + pinnedEventSavedObjectType, + timelineSavedObjectType, + ruleStatusSavedObjectType, + ruleActionsSavedObjectType, + 'cases', + 'cases-comments', + 'cases-configure', + 'cases-user-actions', + ], + read: ['config'], + }, + ui: [ + 'show', + 'crud', + 'alerting:show', + 'actions:show', + 'alerting:save', + 'actions:save', + 'alerting:delete', + 'actions:delete', + ], + }, + read: { + app: ['siem', 'kibana'], + catalogue: ['siem'], + api: ['siem', 'actions-read', 'actions-all', 'alerting-read', 'alerting-all'], + savedObject: { + all: ['alert', 'action', 'action_task_params'], + read: [ + 'config', + noteSavedObjectType, + pinnedEventSavedObjectType, + timelineSavedObjectType, + ruleStatusSavedObjectType, + ruleActionsSavedObjectType, + 'cases', + 'cases-comments', + 'cases-configure', + 'cases-user-actions', + ], + }, + ui: [ + 'show', + 'alerting:show', + 'actions:show', + 'alerting:save', + 'actions:save', + 'alerting:delete', + 'actions:delete', + ], + }, + }, + }); + + if (plugins.alerting != null) { + const signalRuleType = signalRulesAlertType({ + logger: this.logger, + version: this.context.env.packageInfo.version, + ml: plugins.ml, + }); + const ruleNotificationType = rulesNotificationAlertType({ + logger: this.logger, + }); + + if (isAlertExecutor(signalRuleType)) { + plugins.alerting.registerType(signalRuleType); + } + + if (isNotificationAlertExecutor(ruleNotificationType)) { + plugins.alerting.registerType(ruleNotificationType); + } + } + + const libs = compose(core, plugins, this.context.env.mode.prod); + initServer(libs); } + + public start(core: CoreStart, plugins: StartPlugins) {} } diff --git a/x-pack/plugins/siem/server/routes/index.ts b/x-pack/plugins/siem/server/routes/index.ts new file mode 100644 index 0000000000000..64b232a2686b8 --- /dev/null +++ b/x-pack/plugins/siem/server/routes/index.ts @@ -0,0 +1,83 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { IRouter } from '../../../../../src/core/server'; + +import { createRulesRoute } from '../lib/detection_engine/routes/rules/create_rules_route'; +import { createIndexRoute } from '../lib/detection_engine/routes/index/create_index_route'; +import { readIndexRoute } from '../lib/detection_engine/routes/index/read_index_route'; +import { readRulesRoute } from '../lib/detection_engine/routes/rules/read_rules_route'; +import { findRulesRoute } from '../lib/detection_engine/routes/rules/find_rules_route'; +import { deleteRulesRoute } from '../lib/detection_engine/routes/rules/delete_rules_route'; +import { updateRulesRoute } from '../lib/detection_engine/routes/rules/update_rules_route'; +import { patchRulesRoute } from '../lib/detection_engine/routes/rules/patch_rules_route'; +import { setSignalsStatusRoute } from '../lib/detection_engine/routes/signals/open_close_signals_route'; +import { querySignalsRoute } from '../lib/detection_engine/routes/signals/query_signals_route'; +import { deleteIndexRoute } from '../lib/detection_engine/routes/index/delete_index_route'; +import { readTagsRoute } from '../lib/detection_engine/routes/tags/read_tags_route'; +import { readPrivilegesRoute } from '../lib/detection_engine/routes/privileges/read_privileges_route'; +import { addPrepackedRulesRoute } from '../lib/detection_engine/routes/rules/add_prepackaged_rules_route'; +import { createRulesBulkRoute } from '../lib/detection_engine/routes/rules/create_rules_bulk_route'; +import { updateRulesBulkRoute } from '../lib/detection_engine/routes/rules/update_rules_bulk_route'; +import { patchRulesBulkRoute } from '../lib/detection_engine/routes/rules/patch_rules_bulk_route'; +import { deleteRulesBulkRoute } from '../lib/detection_engine/routes/rules/delete_rules_bulk_route'; +import { importRulesRoute } from '../lib/detection_engine/routes/rules/import_rules_route'; +import { exportRulesRoute } from '../lib/detection_engine/routes/rules/export_rules_route'; +import { findRulesStatusesRoute } from '../lib/detection_engine/routes/rules/find_rules_status_route'; +import { getPrepackagedRulesStatusRoute } from '../lib/detection_engine/routes/rules/get_prepackaged_rules_status_route'; +import { importTimelinesRoute } from '../lib/timeline/routes/import_timelines_route'; +import { exportTimelinesRoute } from '../lib/timeline/routes/export_timelines_route'; +import { SetupPlugins } from '../plugin'; +import { ConfigType } from '..'; + +export const initRoutes = ( + router: IRouter, + config: ConfigType, + usingEphemeralEncryptionKey: boolean, + security: SetupPlugins['security'] +) => { + // Detection Engine Rule routes that have the REST endpoints of /api/detection_engine/rules + // All REST rule creation, deletion, updating, etc...... + createRulesRoute(router); + readRulesRoute(router); + updateRulesRoute(router); + patchRulesRoute(router); + deleteRulesRoute(router); + findRulesRoute(router); + + addPrepackedRulesRoute(router); + getPrepackagedRulesStatusRoute(router); + createRulesBulkRoute(router); + updateRulesBulkRoute(router); + patchRulesBulkRoute(router); + deleteRulesBulkRoute(router); + + importRulesRoute(router, config); + exportRulesRoute(router, config); + + importTimelinesRoute(router, config, security); + exportTimelinesRoute(router, config); + + findRulesStatusesRoute(router); + + // Detection Engine Signals routes that have the REST endpoints of /api/detection_engine/signals + // POST /api/detection_engine/signals/status + // Example usage can be found in siem/server/lib/detection_engine/scripts/signals + setSignalsStatusRoute(router); + querySignalsRoute(router); + + // Detection Engine index routes that have the REST endpoints of /api/detection_engine/index + // All REST index creation, policy management for spaces + createIndexRoute(router); + readIndexRoute(router); + deleteIndexRoute(router); + + // Detection Engine tags routes that have the REST endpoints of /api/detection_engine/tags + readTagsRoute(router); + + // Privileges API to get the generic user privileges + readPrivilegesRoute(router, security, usingEphemeralEncryptionKey); +}; diff --git a/x-pack/legacy/plugins/siem/server/saved_objects.ts b/x-pack/plugins/siem/server/saved_objects.ts similarity index 100% rename from x-pack/legacy/plugins/siem/server/saved_objects.ts rename to x-pack/plugins/siem/server/saved_objects.ts diff --git a/x-pack/plugins/siem/server/types.ts b/x-pack/plugins/siem/server/types.ts new file mode 100644 index 0000000000000..3a5c6cf94c652 --- /dev/null +++ b/x-pack/plugins/siem/server/types.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. + */ + +import { SiemClient } from './client'; + +export { SiemClient }; + +export interface SiemRequestContext { + getSiemClient: () => SiemClient; +} + +declare module 'src/core/server' { + interface RequestHandlerContext { + siem?: SiemRequestContext; + } +} diff --git a/x-pack/legacy/plugins/siem/server/utils/beat_schema/8.0.0/auditbeat.ts b/x-pack/plugins/siem/server/utils/beat_schema/8.0.0/auditbeat.ts similarity index 100% rename from x-pack/legacy/plugins/siem/server/utils/beat_schema/8.0.0/auditbeat.ts rename to x-pack/plugins/siem/server/utils/beat_schema/8.0.0/auditbeat.ts diff --git a/x-pack/legacy/plugins/siem/server/utils/beat_schema/8.0.0/ecs.ts b/x-pack/plugins/siem/server/utils/beat_schema/8.0.0/ecs.ts similarity index 100% rename from x-pack/legacy/plugins/siem/server/utils/beat_schema/8.0.0/ecs.ts rename to x-pack/plugins/siem/server/utils/beat_schema/8.0.0/ecs.ts diff --git a/x-pack/legacy/plugins/siem/server/utils/beat_schema/8.0.0/filebeat.ts b/x-pack/plugins/siem/server/utils/beat_schema/8.0.0/filebeat.ts similarity index 100% rename from x-pack/legacy/plugins/siem/server/utils/beat_schema/8.0.0/filebeat.ts rename to x-pack/plugins/siem/server/utils/beat_schema/8.0.0/filebeat.ts diff --git a/x-pack/legacy/plugins/siem/server/utils/beat_schema/8.0.0/index.ts b/x-pack/plugins/siem/server/utils/beat_schema/8.0.0/index.ts similarity index 100% rename from x-pack/legacy/plugins/siem/server/utils/beat_schema/8.0.0/index.ts rename to x-pack/plugins/siem/server/utils/beat_schema/8.0.0/index.ts diff --git a/x-pack/legacy/plugins/siem/server/utils/beat_schema/8.0.0/packetbeat.ts b/x-pack/plugins/siem/server/utils/beat_schema/8.0.0/packetbeat.ts similarity index 100% rename from x-pack/legacy/plugins/siem/server/utils/beat_schema/8.0.0/packetbeat.ts rename to x-pack/plugins/siem/server/utils/beat_schema/8.0.0/packetbeat.ts diff --git a/x-pack/legacy/plugins/siem/server/utils/beat_schema/8.0.0/winlogbeat.ts b/x-pack/plugins/siem/server/utils/beat_schema/8.0.0/winlogbeat.ts similarity index 100% rename from x-pack/legacy/plugins/siem/server/utils/beat_schema/8.0.0/winlogbeat.ts rename to x-pack/plugins/siem/server/utils/beat_schema/8.0.0/winlogbeat.ts diff --git a/x-pack/legacy/plugins/siem/server/utils/beat_schema/index.test.ts b/x-pack/plugins/siem/server/utils/beat_schema/index.test.ts similarity index 100% rename from x-pack/legacy/plugins/siem/server/utils/beat_schema/index.test.ts rename to x-pack/plugins/siem/server/utils/beat_schema/index.test.ts diff --git a/x-pack/legacy/plugins/siem/server/utils/beat_schema/index.ts b/x-pack/plugins/siem/server/utils/beat_schema/index.ts similarity index 100% rename from x-pack/legacy/plugins/siem/server/utils/beat_schema/index.ts rename to x-pack/plugins/siem/server/utils/beat_schema/index.ts diff --git a/x-pack/legacy/plugins/siem/server/utils/beat_schema/type.ts b/x-pack/plugins/siem/server/utils/beat_schema/type.ts similarity index 100% rename from x-pack/legacy/plugins/siem/server/utils/beat_schema/type.ts rename to x-pack/plugins/siem/server/utils/beat_schema/type.ts diff --git a/x-pack/legacy/plugins/siem/server/utils/build_query/calculate_timeseries_interval.ts b/x-pack/plugins/siem/server/utils/build_query/calculate_timeseries_interval.ts similarity index 100% rename from x-pack/legacy/plugins/siem/server/utils/build_query/calculate_timeseries_interval.ts rename to x-pack/plugins/siem/server/utils/build_query/calculate_timeseries_interval.ts diff --git a/x-pack/legacy/plugins/siem/server/utils/build_query/create_options.test.ts b/x-pack/plugins/siem/server/utils/build_query/create_options.test.ts similarity index 93% rename from x-pack/legacy/plugins/siem/server/utils/build_query/create_options.test.ts rename to x-pack/plugins/siem/server/utils/build_query/create_options.test.ts index 8262b5b670d30..5ca67ad6ae51f 100644 --- a/x-pack/legacy/plugins/siem/server/utils/build_query/create_options.test.ts +++ b/x-pack/plugins/siem/server/utils/build_query/create_options.test.ts @@ -6,7 +6,7 @@ import { omit } from 'lodash/fp'; -import { defaultIndexPattern } from '../../../default_index_pattern'; +import { DEFAULT_INDEX_PATTERN } from '../../../common/constants'; import { Direction } from '../../graphql/types'; import { RequestOptions } from '../../lib/framework'; @@ -30,7 +30,7 @@ describe('createOptions', () => { }, }; args = { - defaultIndex: defaultIndexPattern, + defaultIndex: DEFAULT_INDEX_PATTERN, pagination: { limit: 5, }, @@ -57,7 +57,7 @@ describe('createOptions', () => { test('should create options given all input including sort field', () => { const options = createOptions(source, args, info); const expected: RequestOptions = { - defaultIndex: defaultIndexPattern, + defaultIndex: DEFAULT_INDEX_PATTERN, sourceConfiguration: { fields: { host: 'host-1', @@ -87,7 +87,7 @@ describe('createOptions', () => { const argsWithoutSort: Args = omit('sortField', args); const options = createOptions(source, argsWithoutSort, info); const expected: RequestOptions = { - defaultIndex: defaultIndexPattern, + defaultIndex: DEFAULT_INDEX_PATTERN, sourceConfiguration: { fields: { host: 'host-1', diff --git a/x-pack/legacy/plugins/siem/server/utils/build_query/create_options.ts b/x-pack/plugins/siem/server/utils/build_query/create_options.ts similarity index 100% rename from x-pack/legacy/plugins/siem/server/utils/build_query/create_options.ts rename to x-pack/plugins/siem/server/utils/build_query/create_options.ts diff --git a/x-pack/legacy/plugins/siem/server/utils/build_query/field.mock.ts b/x-pack/plugins/siem/server/utils/build_query/field.mock.ts similarity index 100% rename from x-pack/legacy/plugins/siem/server/utils/build_query/field.mock.ts rename to x-pack/plugins/siem/server/utils/build_query/field.mock.ts diff --git a/x-pack/legacy/plugins/siem/server/utils/build_query/fields.test.ts b/x-pack/plugins/siem/server/utils/build_query/fields.test.ts similarity index 100% rename from x-pack/legacy/plugins/siem/server/utils/build_query/fields.test.ts rename to x-pack/plugins/siem/server/utils/build_query/fields.test.ts diff --git a/x-pack/legacy/plugins/siem/server/utils/build_query/fields.ts b/x-pack/plugins/siem/server/utils/build_query/fields.ts similarity index 100% rename from x-pack/legacy/plugins/siem/server/utils/build_query/fields.ts rename to x-pack/plugins/siem/server/utils/build_query/fields.ts diff --git a/x-pack/legacy/plugins/siem/server/utils/build_query/filters.ts b/x-pack/plugins/siem/server/utils/build_query/filters.ts similarity index 100% rename from x-pack/legacy/plugins/siem/server/utils/build_query/filters.ts rename to x-pack/plugins/siem/server/utils/build_query/filters.ts diff --git a/x-pack/legacy/plugins/siem/server/utils/build_query/index.ts b/x-pack/plugins/siem/server/utils/build_query/index.ts similarity index 100% rename from x-pack/legacy/plugins/siem/server/utils/build_query/index.ts rename to x-pack/plugins/siem/server/utils/build_query/index.ts diff --git a/x-pack/legacy/plugins/siem/server/utils/build_query/merge_fields_with_hits.test.ts b/x-pack/plugins/siem/server/utils/build_query/merge_fields_with_hits.test.ts similarity index 100% rename from x-pack/legacy/plugins/siem/server/utils/build_query/merge_fields_with_hits.test.ts rename to x-pack/plugins/siem/server/utils/build_query/merge_fields_with_hits.test.ts diff --git a/x-pack/legacy/plugins/siem/server/utils/build_query/merge_fields_with_hits.ts b/x-pack/plugins/siem/server/utils/build_query/merge_fields_with_hits.ts similarity index 100% rename from x-pack/legacy/plugins/siem/server/utils/build_query/merge_fields_with_hits.ts rename to x-pack/plugins/siem/server/utils/build_query/merge_fields_with_hits.ts diff --git a/x-pack/legacy/plugins/siem/server/utils/build_query/reduce_fields.test.ts b/x-pack/plugins/siem/server/utils/build_query/reduce_fields.test.ts similarity index 100% rename from x-pack/legacy/plugins/siem/server/utils/build_query/reduce_fields.test.ts rename to x-pack/plugins/siem/server/utils/build_query/reduce_fields.test.ts diff --git a/x-pack/legacy/plugins/siem/server/utils/build_query/reduce_fields.ts b/x-pack/plugins/siem/server/utils/build_query/reduce_fields.ts similarity index 100% rename from x-pack/legacy/plugins/siem/server/utils/build_query/reduce_fields.ts rename to x-pack/plugins/siem/server/utils/build_query/reduce_fields.ts diff --git a/x-pack/plugins/siem/server/utils/build_validation/route_validation.test.ts b/x-pack/plugins/siem/server/utils/build_validation/route_validation.test.ts new file mode 100644 index 0000000000000..d17a8457ff81b --- /dev/null +++ b/x-pack/plugins/siem/server/utils/build_validation/route_validation.test.ts @@ -0,0 +1,39 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { buildRouteValidation } from './route_validation'; +import * as rt from 'io-ts'; +import { RouteValidationResultFactory } from '../../../../../../src/core/server/http'; + +describe('buildRouteValidation', () => { + const schema = rt.type({ + ids: rt.array(rt.string), + }); + const validationResult: RouteValidationResultFactory = { + ok: jest.fn().mockImplementation(validatedInput => validatedInput), + badRequest: jest.fn().mockImplementation(e => e), + }; + + beforeEach(() => { + jest.clearAllMocks(); + }); + + test('return validation error', () => { + const input = { id: 'someId' }; + const result = buildRouteValidation(schema)(input, validationResult); + + expect(result).toEqual( + 'Invalid value undefined supplied to : { ids: Array }/ids: Array' + ); + }); + + test('return validated input', () => { + const input = { ids: ['someId'] }; + const result = buildRouteValidation(schema)(input, validationResult); + + expect(result).toEqual(input); + }); +}); diff --git a/x-pack/plugins/siem/server/utils/build_validation/route_validation.ts b/x-pack/plugins/siem/server/utils/build_validation/route_validation.ts new file mode 100644 index 0000000000000..bfcd0998fe690 --- /dev/null +++ b/x-pack/plugins/siem/server/utils/build_validation/route_validation.ts @@ -0,0 +1,39 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { fold } from 'fp-ts/lib/Either'; +import { pipe } from 'fp-ts/lib/pipeable'; +import * as rt from 'io-ts'; +import { failure } from 'io-ts/lib/PathReporter'; +import { + RouteValidationFunction, + RouteValidationResultFactory, + RouteValidationError, +} from '../../../../../../src/core/server'; + +type RequestValidationResult = + | { + value: T; + error?: undefined; + } + | { + value?: undefined; + error: RouteValidationError; + }; + +export const buildRouteValidation = >( + schema: T +): RouteValidationFunction => ( + inputValue: unknown, + validationResult: RouteValidationResultFactory +) => + pipe( + schema.decode(inputValue), + fold>( + (errors: rt.Errors) => validationResult.badRequest(failure(errors).join('\n')), + (validatedInput: A) => validationResult.ok(validatedInput) + ) + ); diff --git a/x-pack/plugins/siem/server/utils/read_stream/create_stream_from_ndjson.test.ts b/x-pack/plugins/siem/server/utils/read_stream/create_stream_from_ndjson.test.ts new file mode 100644 index 0000000000000..2b5b34edca140 --- /dev/null +++ b/x-pack/plugins/siem/server/utils/read_stream/create_stream_from_ndjson.test.ts @@ -0,0 +1,69 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { transformDataToNdjson } from './create_stream_from_ndjson'; +import { ImportRuleAlertRest } from '../../lib/detection_engine/types'; +import { sampleRule } from '../../lib/detection_engine/signals/__mocks__/es_results'; + +export const getOutputSample = (): Partial => ({ + rule_id: 'rule-1', + output_index: '.siem-signals', + risk_score: 50, + description: 'some description', + from: 'now-5m', + to: 'now', + index: ['index-1'], + name: 'some-name', + severity: 'low', + interval: '5m', + type: 'query', +}); + +export const getSampleAsNdjson = (sample: Partial): string => { + return `${JSON.stringify(sample)}\n`; +}; + +describe('create_rules_stream_from_ndjson', () => { + describe('transformDataToNdjson', () => { + test('if rules are empty it returns an empty string', () => { + const ruleNdjson = transformDataToNdjson([]); + expect(ruleNdjson).toEqual(''); + }); + + test('single rule will transform with new line ending character for ndjson', () => { + const rule = sampleRule(); + const ruleNdjson = transformDataToNdjson([rule]); + expect(ruleNdjson.endsWith('\n')).toBe(true); + }); + + test('multiple rules will transform with two new line ending characters for ndjson', () => { + const result1 = sampleRule(); + const result2 = sampleRule(); + result2.id = 'some other id'; + result2.rule_id = 'some other id'; + result2.name = 'Some other rule'; + + const ruleNdjson = transformDataToNdjson([result1, result2]); + // this is how we count characters in JavaScript :-) + const count = ruleNdjson.split('\n').length - 1; + expect(count).toBe(2); + }); + + test('you can parse two rules back out without errors', () => { + const result1 = sampleRule(); + const result2 = sampleRule(); + result2.id = 'some other id'; + result2.rule_id = 'some other id'; + result2.name = 'Some other rule'; + + const ruleNdjson = transformDataToNdjson([result1, result2]); + const ruleStrings = ruleNdjson.split('\n'); + const reParsed1 = JSON.parse(ruleStrings[0]); + const reParsed2 = JSON.parse(ruleStrings[1]); + expect(reParsed1).toEqual(result1); + expect(reParsed2).toEqual(result2); + }); + }); +}); diff --git a/x-pack/plugins/siem/server/utils/read_stream/create_stream_from_ndjson.ts b/x-pack/plugins/siem/server/utils/read_stream/create_stream_from_ndjson.ts new file mode 100644 index 0000000000000..2d630d0b92c68 --- /dev/null +++ b/x-pack/plugins/siem/server/utils/read_stream/create_stream_from_ndjson.ts @@ -0,0 +1,73 @@ +/* + * 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 { Transform } from 'stream'; +import { has, isString } from 'lodash/fp'; +import { ImportRuleAlertRest } from '../../lib/detection_engine/types'; +import { createMapStream, createFilterStream } from '../../../../../../src/legacy/utils/streams'; +import { importRulesSchema } from '../../lib/detection_engine/routes/schemas/import_rules_schema'; +import { BadRequestError } from '../../lib/detection_engine/errors/bad_request_error'; + +export interface RulesObjectsExportResultDetails { + /** number of successfully exported objects */ + exportedCount: number; +} + +export const parseNdjsonStrings = (): Transform => { + return createMapStream((ndJsonStr: string) => { + if (isString(ndJsonStr) && ndJsonStr.trim() !== '') { + try { + return JSON.parse(ndJsonStr); + } catch (err) { + return err; + } + } + }); +}; + +export const filterExportedCounts = (): Transform => { + return createFilterStream( + obj => obj != null && !has('exported_count', obj) + ); +}; + +export const validateRules = (): Transform => { + return createMapStream((obj: ImportRuleAlertRest) => { + if (!(obj instanceof Error)) { + const validated = importRulesSchema.validate(obj); + if (validated.error != null) { + return new BadRequestError(validated.error.message); + } else { + return validated.value; + } + } else { + return obj; + } + }); +}; + +// Adaptation from: saved_objects/import/create_limit_stream.ts +export const createLimitStream = (limit: number): Transform => { + let counter = 0; + return new Transform({ + objectMode: true, + async transform(obj, _, done) { + if (counter >= limit) { + return done(new Error(`Can't import more than ${limit} rules`)); + } + counter++; + done(undefined, obj); + }, + }); +}; + +export const transformDataToNdjson = (data: unknown[]): string => { + if (data.length !== 0) { + const dataString = data.map(rule => JSON.stringify(rule)).join('\n'); + return `${dataString}\n`; + } else { + return ''; + } +}; diff --git a/x-pack/legacy/plugins/siem/server/utils/serialized_query.ts b/x-pack/plugins/siem/server/utils/serialized_query.ts similarity index 92% rename from x-pack/legacy/plugins/siem/server/utils/serialized_query.ts rename to x-pack/plugins/siem/server/utils/serialized_query.ts index 1ba6eb8b9f9a6..09b227d8c5a32 100644 --- a/x-pack/legacy/plugins/siem/server/utils/serialized_query.ts +++ b/x-pack/plugins/siem/server/utils/serialized_query.ts @@ -7,7 +7,7 @@ import { UserInputError } from 'apollo-server-errors'; import { isEmpty, isPlainObject, isString } from 'lodash/fp'; -import { JsonObject } from '../../../../../../src/plugins/kibana_utils/public'; +import { JsonObject } from '../../../../../src/plugins/kibana_utils/public'; export const parseFilterQuery = (filterQuery: string): JsonObject => { try { diff --git a/x-pack/legacy/plugins/siem/server/utils/typed_elasticsearch_mappings.ts b/x-pack/plugins/siem/server/utils/typed_elasticsearch_mappings.ts similarity index 100% rename from x-pack/legacy/plugins/siem/server/utils/typed_elasticsearch_mappings.ts rename to x-pack/plugins/siem/server/utils/typed_elasticsearch_mappings.ts diff --git a/x-pack/legacy/plugins/siem/server/utils/typed_resolvers.ts b/x-pack/plugins/siem/server/utils/typed_resolvers.ts similarity index 100% rename from x-pack/legacy/plugins/siem/server/utils/typed_resolvers.ts rename to x-pack/plugins/siem/server/utils/typed_resolvers.ts diff --git a/x-pack/plugins/siem/yarn.lock b/x-pack/plugins/siem/yarn.lock new file mode 120000 index 0000000000000..6e09764ec763b --- /dev/null +++ b/x-pack/plugins/siem/yarn.lock @@ -0,0 +1 @@ +../../../yarn.lock \ No newline at end of file diff --git a/x-pack/plugins/spaces/public/copy_saved_objects_to_space/copy_saved_objects_to_space_service.test.ts b/x-pack/plugins/spaces/public/copy_saved_objects_to_space/copy_saved_objects_to_space_service.test.ts index 7d680f0ee0ed6..4e61756d933c9 100644 --- a/x-pack/plugins/spaces/public/copy_saved_objects_to_space/copy_saved_objects_to_space_service.test.ts +++ b/x-pack/plugins/spaces/public/copy_saved_objects_to_space/copy_saved_objects_to_space_service.test.ts @@ -22,8 +22,8 @@ describe('CopySavedObjectsToSpaceService', () => { const service = new CopySavedObjectsToSpaceService(); service.setup(deps); - expect(deps.savedObjectsManagementSetup.actionRegistry.register).toHaveBeenCalledTimes(1); - expect(deps.savedObjectsManagementSetup.actionRegistry.register).toHaveBeenCalledWith( + expect(deps.savedObjectsManagementSetup.actions.register).toHaveBeenCalledTimes(1); + expect(deps.savedObjectsManagementSetup.actions.register).toHaveBeenCalledWith( expect.any(CopyToSpaceSavedObjectsManagementAction) ); }); diff --git a/x-pack/plugins/spaces/public/copy_saved_objects_to_space/copy_saved_objects_to_space_service.ts b/x-pack/plugins/spaces/public/copy_saved_objects_to_space/copy_saved_objects_to_space_service.ts index d564514beebff..93d0f92744d41 100644 --- a/x-pack/plugins/spaces/public/copy_saved_objects_to_space/copy_saved_objects_to_space_service.ts +++ b/x-pack/plugins/spaces/public/copy_saved_objects_to_space/copy_saved_objects_to_space_service.ts @@ -18,6 +18,6 @@ interface SetupDeps { export class CopySavedObjectsToSpaceService { public setup({ spacesManager, savedObjectsManagementSetup, notificationsSetup }: SetupDeps) { const action = new CopyToSpaceSavedObjectsManagementAction(spacesManager, notificationsSetup); - savedObjectsManagementSetup.actionRegistry.register(action); + savedObjectsManagementSetup.actions.register(action); } } diff --git a/x-pack/plugins/spaces/public/management/edit_space/delete_spaces_button.tsx b/x-pack/plugins/spaces/public/management/edit_space/delete_spaces_button.tsx index 28e45bc8cfd2a..ea63905e27b26 100644 --- a/x-pack/plugins/spaces/public/management/edit_space/delete_spaces_button.tsx +++ b/x-pack/plugins/spaces/public/management/edit_space/delete_spaces_button.tsx @@ -99,10 +99,14 @@ export class DeleteSpacesButton extends Component { public deleteSpaces = async () => { const { spacesManager, space } = this.props; + this.setState({ + showConfirmDeleteModal: false, + }); + try { await spacesManager.deleteSpace(space); } catch (error) { - const { message: errorMessage = '' } = error.data || {}; + const { message: errorMessage = '' } = error.data || error.body || {}; this.props.notifications.toasts.addDanger( i18n.translate('xpack.spaces.management.deleteSpacesButton.deleteSpaceErrorTitle', { @@ -110,12 +114,9 @@ export class DeleteSpacesButton extends Component { values: { errorMessage }, }) ); + return; } - this.setState({ - showConfirmDeleteModal: false, - }); - const message = i18n.translate( 'xpack.spaces.management.deleteSpacesButton.spaceSuccessfullyDeletedNotificationMessage', { diff --git a/x-pack/plugins/spaces/public/management/spaces_grid/spaces_grid_page.tsx b/x-pack/plugins/spaces/public/management/spaces_grid/spaces_grid_page.tsx index ff4be84207832..df5e6a2ca34af 100644 --- a/x-pack/plugins/spaces/public/management/spaces_grid/spaces_grid_page.tsx +++ b/x-pack/plugins/spaces/public/management/spaces_grid/spaces_grid_page.tsx @@ -176,10 +176,14 @@ export class SpacesGridPage extends Component { return; } + this.setState({ + showConfirmDeleteModal: false, + }); + try { await spacesManager.deleteSpace(space); } catch (error) { - const { message: errorMessage = '' } = error.data || {}; + const { message: errorMessage = '' } = error.data || error.body || {}; this.props.notifications.toasts.addDanger( i18n.translate('xpack.spaces.management.spacesGridPage.errorDeletingSpaceErrorMessage', { @@ -189,12 +193,9 @@ export class SpacesGridPage extends Component { }, }) ); + return; } - this.setState({ - showConfirmDeleteModal: false, - }); - this.loadGrid(); const message = i18n.translate( diff --git a/x-pack/plugins/spaces/server/lib/copy_to_spaces/copy_to_spaces.test.ts b/x-pack/plugins/spaces/server/lib/copy_to_spaces/copy_to_spaces.test.ts index 59e157c3fc2db..d842f07cdb205 100644 --- a/x-pack/plugins/spaces/server/lib/copy_to_spaces/copy_to_spaces.test.ts +++ b/x-pack/plugins/spaces/server/lib/copy_to_spaces/copy_to_spaces.test.ts @@ -57,26 +57,26 @@ describe('copySavedObjectsToSpaces', () => { typeRegistry.getAllTypes.mockReturnValue([ { name: 'dashboard', - namespaceAgnostic: false, + namespaceType: 'single', hidden: false, mappings: { properties: {} }, }, { name: 'visualization', - namespaceAgnostic: false, + namespaceType: 'single', hidden: false, mappings: { properties: {} }, }, { name: 'globaltype', - namespaceAgnostic: true, + namespaceType: 'agnostic', hidden: false, mappings: { properties: {} }, }, ]); typeRegistry.isNamespaceAgnostic.mockImplementation((type: string) => - typeRegistry.getAllTypes().some(t => t.name === type && t.namespaceAgnostic) + typeRegistry.getAllTypes().some(t => t.name === type && t.namespaceType === 'agnostic') ); coreStart.savedObjects.getTypeRegistry.mockReturnValue(typeRegistry); @@ -188,11 +188,13 @@ describe('copySavedObjectsToSpaces', () => { }, ], "savedObjectsClient": Object { + "addToNamespaces": [MockFunction], "bulkCreate": [MockFunction], "bulkGet": [MockFunction], "bulkUpdate": [MockFunction], "create": [MockFunction], "delete": [MockFunction], + "deleteFromNamespaces": [MockFunction], "errors": [Function], "find": [MockFunction], "get": [MockFunction], @@ -252,11 +254,13 @@ describe('copySavedObjectsToSpaces', () => { "readable": false, }, "savedObjectsClient": Object { + "addToNamespaces": [MockFunction], "bulkCreate": [MockFunction], "bulkGet": [MockFunction], "bulkUpdate": [MockFunction], "create": [MockFunction], "delete": [MockFunction], + "deleteFromNamespaces": [MockFunction], "errors": [Function], "find": [MockFunction], "get": [MockFunction], @@ -315,11 +319,13 @@ describe('copySavedObjectsToSpaces', () => { "readable": false, }, "savedObjectsClient": Object { + "addToNamespaces": [MockFunction], "bulkCreate": [MockFunction], "bulkGet": [MockFunction], "bulkUpdate": [MockFunction], "create": [MockFunction], "delete": [MockFunction], + "deleteFromNamespaces": [MockFunction], "errors": [Function], "find": [MockFunction], "get": [MockFunction], diff --git a/x-pack/plugins/spaces/server/lib/copy_to_spaces/resolve_copy_conflicts.test.ts b/x-pack/plugins/spaces/server/lib/copy_to_spaces/resolve_copy_conflicts.test.ts index 7809f1f8be66f..0654712ecff0e 100644 --- a/x-pack/plugins/spaces/server/lib/copy_to_spaces/resolve_copy_conflicts.test.ts +++ b/x-pack/plugins/spaces/server/lib/copy_to_spaces/resolve_copy_conflicts.test.ts @@ -57,26 +57,26 @@ describe('resolveCopySavedObjectsToSpacesConflicts', () => { typeRegistry.getAllTypes.mockReturnValue([ { name: 'dashboard', - namespaceAgnostic: false, + namespaceType: 'single', hidden: false, mappings: { properties: {} }, }, { name: 'visualization', - namespaceAgnostic: false, + namespaceType: 'single', hidden: false, mappings: { properties: {} }, }, { name: 'globaltype', - namespaceAgnostic: true, + namespaceType: 'agnostic', hidden: false, mappings: { properties: {} }, }, ]); typeRegistry.isNamespaceAgnostic.mockImplementation((type: string) => - typeRegistry.getAllTypes().some(t => t.name === type && t.namespaceAgnostic) + typeRegistry.getAllTypes().some(t => t.name === type && t.namespaceType === 'agnostic') ); coreStart.savedObjects.getTypeRegistry.mockReturnValue(typeRegistry); @@ -204,11 +204,13 @@ describe('resolveCopySavedObjectsToSpacesConflicts', () => { }, ], "savedObjectsClient": Object { + "addToNamespaces": [MockFunction], "bulkCreate": [MockFunction], "bulkGet": [MockFunction], "bulkUpdate": [MockFunction], "create": [MockFunction], "delete": [MockFunction], + "deleteFromNamespaces": [MockFunction], "errors": [Function], "find": [MockFunction], "get": [MockFunction], @@ -275,11 +277,13 @@ describe('resolveCopySavedObjectsToSpacesConflicts', () => { }, ], "savedObjectsClient": Object { + "addToNamespaces": [MockFunction], "bulkCreate": [MockFunction], "bulkGet": [MockFunction], "bulkUpdate": [MockFunction], "create": [MockFunction], "delete": [MockFunction], + "deleteFromNamespaces": [MockFunction], "errors": [Function], "find": [MockFunction], "get": [MockFunction], @@ -345,11 +349,13 @@ describe('resolveCopySavedObjectsToSpacesConflicts', () => { }, ], "savedObjectsClient": Object { + "addToNamespaces": [MockFunction], "bulkCreate": [MockFunction], "bulkGet": [MockFunction], "bulkUpdate": [MockFunction], "create": [MockFunction], "delete": [MockFunction], + "deleteFromNamespaces": [MockFunction], "errors": [Function], "find": [MockFunction], "get": [MockFunction], diff --git a/x-pack/plugins/spaces/server/lib/spaces_client/spaces_client.test.ts b/x-pack/plugins/spaces/server/lib/spaces_client/spaces_client.test.ts index 74e75fb8f12c7..c83830f6feace 100644 --- a/x-pack/plugins/spaces/server/lib/spaces_client/spaces_client.test.ts +++ b/x-pack/plugins/spaces/server/lib/spaces_client/spaces_client.test.ts @@ -250,14 +250,10 @@ describe('#getAll', () => { mockAuthorization.mode.useRbacForRequest.mockReturnValue(true); mockCheckPrivilegesAtSpaces.mockReturnValue({ username, - spacePrivileges: { - [savedObjects[0].id]: { - [privilege]: false, - }, - [savedObjects[1].id]: { - [privilege]: false, - }, - }, + privileges: [ + { resource: savedObjects[0].id, privilege, authorized: false }, + { resource: savedObjects[1].id, privilege, authorized: false }, + ], }); const maxSpaces = 1234; const mockConfig = createMockConfig({ @@ -314,14 +310,10 @@ describe('#getAll', () => { mockAuthorization.mode.useRbacForRequest.mockReturnValue(true); mockCheckPrivilegesAtSpaces.mockReturnValue({ username, - spacePrivileges: { - [savedObjects[0].id]: { - [privilege]: true, - }, - [savedObjects[1].id]: { - [privilege]: false, - }, - }, + privileges: [ + { resource: savedObjects[0].id, privilege, authorized: true }, + { resource: savedObjects[1].id, privilege, authorized: false }, + ], }); const mockInternalRepository = { find: jest.fn().mockReturnValue({ diff --git a/x-pack/plugins/spaces/server/lib/spaces_client/spaces_client.ts b/x-pack/plugins/spaces/server/lib/spaces_client/spaces_client.ts index 22c34c03368e3..0c066fb76994f 100644 --- a/x-pack/plugins/spaces/server/lib/spaces_client/spaces_client.ts +++ b/x-pack/plugins/spaces/server/lib/spaces_client/spaces_client.ts @@ -74,16 +74,14 @@ export class SpacesClient { const privilege = privilegeFactory(this.authorization!); - const { username, spacePrivileges } = await checkPrivileges.atSpaces(spaceIds, privilege); + const { username, privileges } = await checkPrivileges.atSpaces(spaceIds, privilege); - const authorized = Object.keys(spacePrivileges).filter(spaceId => { - return spacePrivileges[spaceId][privilege]; - }); + const authorized = privileges.filter(x => x.authorized).map(x => x.resource); this.debugLogger( `SpacesClient.getAll(), authorized for ${ authorized.length - } spaces, derived from ES privilege check: ${JSON.stringify(spacePrivileges)}` + } spaces, derived from ES privilege check: ${JSON.stringify(privileges)}` ); if (authorized.length === 0) { @@ -94,7 +92,7 @@ export class SpacesClient { throw Boom.forbidden(); } - this.auditLogger.spacesAuthorizationSuccess(username, 'getAll', authorized); + this.auditLogger.spacesAuthorizationSuccess(username, 'getAll', authorized as string[]); const filteredSpaces: Space[] = spaces.filter((space: any) => authorized.includes(space.id)); this.debugLogger( `SpacesClient.getAll(), using RBAC. returning spaces: ${filteredSpaces @@ -211,9 +209,9 @@ export class SpacesClient { throw Boom.badRequest('This Space cannot be deleted because it is reserved.'); } - await repository.delete('space', id); - await repository.deleteByNamespace(id); + + await repository.delete('space', id); } private useRbac(): boolean { diff --git a/x-pack/plugins/spaces/server/plugin.test.ts b/x-pack/plugins/spaces/server/plugin.test.ts index 0b9905d5e9c95..75ddee772b168 100644 --- a/x-pack/plugins/spaces/server/plugin.test.ts +++ b/x-pack/plugins/spaces/server/plugin.test.ts @@ -81,7 +81,7 @@ describe('Spaces Plugin', () => { expect(core.savedObjects.registerType).toHaveBeenCalledWith({ name: 'space', - namespaceAgnostic: true, + namespaceType: 'agnostic', hidden: true, mappings: expect.any(Object), migrations: expect.any(Object), diff --git a/x-pack/plugins/spaces/server/plugin.ts b/x-pack/plugins/spaces/server/plugin.ts index a24d626c2a85d..09b38adb70682 100644 --- a/x-pack/plugins/spaces/server/plugin.ts +++ b/x-pack/plugins/spaces/server/plugin.ts @@ -115,10 +115,8 @@ export class Plugin { const savedObjectsService = new SpacesSavedObjectsService(); savedObjectsService.setup({ core, spacesService }); - const viewRouter = core.http.createRouter(); initSpacesViewsRoutes({ - viewRouter, - cspHeader: core.http.csp.header, + httpResources: core.http.resources, }); const externalRouter = core.http.createRouter(); diff --git a/x-pack/plugins/spaces/server/routes/api/__fixtures__/create_mock_so_service.ts b/x-pack/plugins/spaces/server/routes/api/__fixtures__/create_mock_so_service.ts index d8c318369834e..cbe832e485d66 100644 --- a/x-pack/plugins/spaces/server/routes/api/__fixtures__/create_mock_so_service.ts +++ b/x-pack/plugins/spaces/server/routes/api/__fixtures__/create_mock_so_service.ts @@ -46,37 +46,37 @@ export const createMockSavedObjectsService = (spaces: any[] = []) => { typeRegistry.getAllTypes.mockReturnValue([ { name: 'visualization', - namespaceAgnostic: false, + namespaceType: 'single', hidden: false, mappings: { properties: {} }, }, { name: 'dashboard', - namespaceAgnostic: false, + namespaceType: 'single', hidden: false, mappings: { properties: {} }, }, { name: 'index-pattern', - namespaceAgnostic: false, + namespaceType: 'single', hidden: false, mappings: { properties: {} }, }, { name: 'globalType', - namespaceAgnostic: true, + namespaceType: 'agnostic', hidden: false, mappings: { properties: {} }, }, { name: 'space', - namespaceAgnostic: true, + namespaceType: 'agnostic', hidden: true, mappings: { properties: {} }, }, ]); typeRegistry.isNamespaceAgnostic.mockImplementation((type: string) => - typeRegistry.getAllTypes().some(t => t.name === type && t.namespaceAgnostic) + typeRegistry.getAllTypes().some(t => t.name === type && t.namespaceType === 'agnostic') ); savedObjects.getTypeRegistry.mockReturnValue(typeRegistry); diff --git a/x-pack/plugins/spaces/server/routes/api/external/delete.test.ts b/x-pack/plugins/spaces/server/routes/api/external/delete.test.ts index f2ba8785f5a3f..511e9676940d2 100644 --- a/x-pack/plugins/spaces/server/routes/api/external/delete.test.ts +++ b/x-pack/plugins/spaces/server/routes/api/external/delete.test.ts @@ -11,7 +11,13 @@ import { mockRouteContext, mockRouteContextWithInvalidLicense, } from '../__fixtures__'; -import { CoreSetup, IRouter, kibanaResponseFactory, RouteValidatorConfig } from 'src/core/server'; +import { + CoreSetup, + IRouter, + kibanaResponseFactory, + RouteValidatorConfig, + SavedObjectsErrorHelpers, +} from 'src/core/server'; import { loggingServiceMock, httpServiceMock, @@ -75,6 +81,7 @@ describe('Spaces Public API', () => { return { routeValidation: routeDefinition.validate as RouteValidatorConfig<{}, {}, {}>, routeHandler, + savedObjectsRepositoryMock, }; }; @@ -143,6 +150,27 @@ describe('Spaces Public API', () => { expect(status).toEqual(404); }); + it(`returns http/400 when scripts cannot be executed in Elasticsearch`, async () => { + const { routeHandler, savedObjectsRepositoryMock } = await setup(); + + const request = httpServerMock.createKibanaRequest({ + params: { + id: 'a-space', + }, + method: 'delete', + }); + // @ts-ignore + savedObjectsRepositoryMock.deleteByNamespace.mockRejectedValue( + SavedObjectsErrorHelpers.decorateEsCannotExecuteScriptError(new Error()) + ); + const response = await routeHandler(mockRouteContext, request, kibanaResponseFactory); + + const { status, payload } = response; + + expect(status).toEqual(400); + expect(payload.message).toEqual('Cannot execute script in Elasticsearch query'); + }); + it(`DELETE spaces/{id}' cannot delete reserved spaces`, async () => { const { routeHandler } = await setup(); diff --git a/x-pack/plugins/spaces/server/routes/api/external/delete.ts b/x-pack/plugins/spaces/server/routes/api/external/delete.ts index 4b7e6b00182ac..150f1d198cdf6 100644 --- a/x-pack/plugins/spaces/server/routes/api/external/delete.ts +++ b/x-pack/plugins/spaces/server/routes/api/external/delete.ts @@ -4,6 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ +import Boom from 'boom'; import { schema } from '@kbn/config-schema'; import { SavedObjectsErrorHelpers } from '../../../../../../../src/core/server'; import { wrapError } from '../../../lib/errors'; @@ -12,7 +13,7 @@ import { ExternalRouteDeps } from '.'; import { createLicensedRouteHandler } from '../../lib'; export function initDeleteSpacesApi(deps: ExternalRouteDeps) { - const { externalRouter, spacesService } = deps; + const { externalRouter, log, spacesService } = deps; externalRouter.delete( { @@ -33,6 +34,13 @@ export function initDeleteSpacesApi(deps: ExternalRouteDeps) { } catch (error) { if (SavedObjectsErrorHelpers.isNotFoundError(error)) { return response.notFound(); + } else if (SavedObjectsErrorHelpers.isEsCannotExecuteScriptError(error)) { + log.error( + `Failed to delete space '${id}', cannot execute script in Elasticsearch query: ${error.message}` + ); + return response.customError( + wrapError(Boom.badRequest('Cannot execute script in Elasticsearch query')) + ); } return response.customError(wrapError(error)); } diff --git a/x-pack/plugins/spaces/server/routes/api/external/index.ts b/x-pack/plugins/spaces/server/routes/api/external/index.ts index 1bdb7ceb8a3f7..079f690bfe546 100644 --- a/x-pack/plugins/spaces/server/routes/api/external/index.ts +++ b/x-pack/plugins/spaces/server/routes/api/external/index.ts @@ -12,6 +12,8 @@ import { initPostSpacesApi } from './post'; import { initPutSpacesApi } from './put'; import { SpacesServiceSetup } from '../../../spaces_service/spaces_service'; import { initCopyToSpacesApi } from './copy_to_space'; +import { initShareAddSpacesApi } from './share_add_spaces'; +import { initShareRemoveSpacesApi } from './share_remove_spaces'; export interface ExternalRouteDeps { externalRouter: IRouter; @@ -28,4 +30,6 @@ export function initExternalSpacesApi(deps: ExternalRouteDeps) { initPostSpacesApi(deps); initPutSpacesApi(deps); initCopyToSpacesApi(deps); + initShareAddSpacesApi(deps); + initShareRemoveSpacesApi(deps); } diff --git a/x-pack/plugins/spaces/server/routes/api/external/share_add_spaces.ts b/x-pack/plugins/spaces/server/routes/api/external/share_add_spaces.ts new file mode 100644 index 0000000000000..f40cc5cc50572 --- /dev/null +++ b/x-pack/plugins/spaces/server/routes/api/external/share_add_spaces.ts @@ -0,0 +1,62 @@ +/* + * 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 { wrapError } from '../../../lib/errors'; +import { ExternalRouteDeps } from '.'; +import { SPACE_ID_REGEX } from '../../../lib/space_schema'; +import { createLicensedRouteHandler } from '../../lib'; + +const uniq = (arr: T[]): T[] => Array.from(new Set(arr)); +export function initShareAddSpacesApi(deps: ExternalRouteDeps) { + const { externalRouter, getStartServices } = deps; + + externalRouter.post( + { + path: '/api/spaces/_share_saved_object_add', + validate: { + body: schema.object({ + spaces: schema.arrayOf( + schema.string({ + validate: value => { + if (!SPACE_ID_REGEX.test(value)) { + return `lower case, a-z, 0-9, "_", and "-" are allowed`; + } + }, + }), + { + validate: spaceIds => { + if (!spaceIds.length) { + return 'must specify one or more space ids'; + } else if (uniq(spaceIds).length !== spaceIds.length) { + return 'duplicate space ids are not allowed'; + } + }, + } + ), + object: schema.object({ + type: schema.string(), + id: schema.string(), + }), + }), + }, + }, + createLicensedRouteHandler(async (_context, request, response) => { + const [startServices] = await getStartServices(); + const scopedClient = startServices.savedObjects.getScopedClient(request); + + const spaces = request.body.spaces; + const { type, id } = request.body.object; + + try { + await scopedClient.addToNamespaces(type, id, spaces); + } catch (error) { + return response.customError(wrapError(error)); + } + return response.noContent(); + }) + ); +} diff --git a/x-pack/plugins/spaces/server/routes/api/external/share_remove_spaces.ts b/x-pack/plugins/spaces/server/routes/api/external/share_remove_spaces.ts new file mode 100644 index 0000000000000..5f58a5dfd5e5f --- /dev/null +++ b/x-pack/plugins/spaces/server/routes/api/external/share_remove_spaces.ts @@ -0,0 +1,62 @@ +/* + * 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 { wrapError } from '../../../lib/errors'; +import { ExternalRouteDeps } from '.'; +import { SPACE_ID_REGEX } from '../../../lib/space_schema'; +import { createLicensedRouteHandler } from '../../lib'; + +const uniq = (arr: T[]): T[] => Array.from(new Set(arr)); +export function initShareRemoveSpacesApi(deps: ExternalRouteDeps) { + const { externalRouter, getStartServices } = deps; + + externalRouter.post( + { + path: '/api/spaces/_share_saved_object_remove', + validate: { + body: schema.object({ + spaces: schema.arrayOf( + schema.string({ + validate: value => { + if (!SPACE_ID_REGEX.test(value)) { + return `lower case, a-z, 0-9, "_", and "-" are allowed`; + } + }, + }), + { + validate: spaceIds => { + if (!spaceIds.length) { + return 'must specify one or more space ids'; + } else if (uniq(spaceIds).length !== spaceIds.length) { + return 'duplicate space ids are not allowed'; + } + }, + } + ), + object: schema.object({ + type: schema.string(), + id: schema.string(), + }), + }), + }, + }, + createLicensedRouteHandler(async (_context, request, response) => { + const [startServices] = await getStartServices(); + const scopedClient = startServices.savedObjects.getScopedClient(request); + + const spaces = request.body.spaces; + const { type, id } = request.body.object; + + try { + await scopedClient.deleteFromNamespaces(type, id, spaces); + } catch (error) { + return response.customError(wrapError(error)); + } + return response.noContent(); + }) + ); +} diff --git a/x-pack/plugins/spaces/server/routes/views/index.ts b/x-pack/plugins/spaces/server/routes/views/index.ts index 2a346c7e5241a..57ad8872ce558 100644 --- a/x-pack/plugins/spaces/server/routes/views/index.ts +++ b/x-pack/plugins/spaces/server/routes/views/index.ts @@ -4,26 +4,15 @@ * you may not use this file except in compliance with the Elastic License. */ -import { IRouter } from 'src/core/server'; +import { HttpResources } from 'src/core/server'; export interface ViewRouteDeps { - viewRouter: IRouter; - cspHeader: string; + httpResources: HttpResources; } export function initSpacesViewsRoutes(deps: ViewRouteDeps) { - deps.viewRouter.get( - { - path: '/spaces/space_selector', - validate: false, - }, - async (context, request, response) => { - return response.ok({ - headers: { - 'Content-Security-Policy': deps.cspHeader, - }, - body: await context.core.rendering.render({ includeUserSettings: true }), - }); - } + deps.httpResources.register( + { path: '/spaces/space_selector', validate: false }, + (context, request, response) => response.renderCoreApp() ); } diff --git a/x-pack/plugins/spaces/server/saved_objects/__snapshots__/spaces_saved_objects_client.test.ts.snap b/x-pack/plugins/spaces/server/saved_objects/__snapshots__/spaces_saved_objects_client.test.ts.snap deleted file mode 100644 index 8b1a258138355..0000000000000 --- a/x-pack/plugins/spaces/server/saved_objects/__snapshots__/spaces_saved_objects_client.test.ts.snap +++ /dev/null @@ -1,29 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`default space #bulkCreate throws error if options.namespace is specified 1`] = `"Spaces currently determines the namespaces"`; - -exports[`default space #bulkGet throws error if options.namespace is specified 1`] = `"Spaces currently determines the namespaces"`; - -exports[`default space #create throws error if options.namespace is specified 1`] = `"Spaces currently determines the namespaces"`; - -exports[`default space #delete throws error if options.namespace is specified 1`] = `"Spaces currently determines the namespaces"`; - -exports[`default space #find throws error if options.namespace is specified 1`] = `"Spaces currently determines the namespaces"`; - -exports[`default space #get throws error if options.namespace is specified 1`] = `"Spaces currently determines the namespaces"`; - -exports[`default space #update throws error if options.namespace is specified 1`] = `"Spaces currently determines the namespaces"`; - -exports[`space_1 space #bulkCreate throws error if options.namespace is specified 1`] = `"Spaces currently determines the namespaces"`; - -exports[`space_1 space #bulkGet throws error if options.namespace is specified 1`] = `"Spaces currently determines the namespaces"`; - -exports[`space_1 space #create throws error if options.namespace is specified 1`] = `"Spaces currently determines the namespaces"`; - -exports[`space_1 space #delete throws error if options.namespace is specified 1`] = `"Spaces currently determines the namespaces"`; - -exports[`space_1 space #find throws error if options.namespace is specified 1`] = `"Spaces currently determines the namespaces"`; - -exports[`space_1 space #get throws error if options.namespace is specified 1`] = `"Spaces currently determines the namespaces"`; - -exports[`space_1 space #update throws error if options.namespace is specified 1`] = `"Spaces currently determines the namespaces"`; diff --git a/x-pack/plugins/spaces/server/saved_objects/saved_objects_service.test.ts b/x-pack/plugins/spaces/server/saved_objects/saved_objects_service.test.ts index 4a9756d9e03f8..31f2c98d74c96 100644 --- a/x-pack/plugins/spaces/server/saved_objects/saved_objects_service.test.ts +++ b/x-pack/plugins/spaces/server/saved_objects/saved_objects_service.test.ts @@ -58,7 +58,7 @@ describe('SpacesSavedObjectsService', () => { "6.6.0": [Function], }, "name": "space", - "namespaceAgnostic": true, + "namespaceType": "agnostic", }, ] `); diff --git a/x-pack/plugins/spaces/server/saved_objects/saved_objects_service.ts b/x-pack/plugins/spaces/server/saved_objects/saved_objects_service.ts index 40ea49573e3c1..58aa1fe08558a 100644 --- a/x-pack/plugins/spaces/server/saved_objects/saved_objects_service.ts +++ b/x-pack/plugins/spaces/server/saved_objects/saved_objects_service.ts @@ -20,7 +20,7 @@ export class SpacesSavedObjectsService { core.savedObjects.registerType({ name: 'space', hidden: true, - namespaceAgnostic: true, + namespaceType: 'agnostic', mappings: SpacesSavedObjectMappings, migrations: { '6.6.0': migrateToKibana660, diff --git a/x-pack/plugins/spaces/server/saved_objects/spaces_saved_objects_client.test.ts b/x-pack/plugins/spaces/server/saved_objects/spaces_saved_objects_client.test.ts index 2d6fe36792c40..569219a0b8990 100644 --- a/x-pack/plugins/spaces/server/saved_objects/spaces_saved_objects_client.test.ts +++ b/x-pack/plugins/spaces/server/saved_objects/spaces_saved_objects_client.test.ts @@ -13,21 +13,21 @@ import { SavedObjectTypeRegistry } from 'src/core/server'; const typeRegistry = new SavedObjectTypeRegistry(); typeRegistry.registerType({ name: 'foo', - namespaceAgnostic: false, + namespaceType: 'single', hidden: false, mappings: { properties: {} }, }); typeRegistry.registerType({ name: 'bar', - namespaceAgnostic: false, + namespaceType: 'single', hidden: false, mappings: { properties: {} }, }); typeRegistry.registerType({ name: 'space', - namespaceAgnostic: true, + namespaceType: 'agnostic', hidden: true, mappings: { properties: {} }, }); @@ -50,42 +50,41 @@ const createMockResponse = () => ({ references: [], }); +const ERROR_NAMESPACE_SPECIFIED = 'Spaces currently determines the namespaces'; + [ { id: DEFAULT_SPACE_ID, expectedNamespace: undefined }, { id: 'space_1', expectedNamespace: 'space_1' }, ].forEach(currentSpace => { describe(`${currentSpace.id} space`, () => { + const createSpacesSavedObjectsClient = async () => { + const request = createMockRequest(); + const baseClient = createMockClient(); + const spacesService = await createSpacesService(currentSpace.id); + + const client = new SpacesSavedObjectsClient({ + request, + baseClient, + spacesService, + typeRegistry, + }); + return { client, baseClient }; + }; + describe('#get', () => { test(`throws error if options.namespace is specified`, async () => { - const request = createMockRequest(); - const baseClient = createMockClient(); - const spacesService = await createSpacesService(currentSpace.id); - - const client = new SpacesSavedObjectsClient({ - request, - baseClient, - spacesService, - typeRegistry, - }); + const { client } = await createSpacesSavedObjectsClient(); - await expect( - client.get('foo', '', { namespace: 'bar' }) - ).rejects.toThrowErrorMatchingSnapshot(); + await expect(client.get('foo', '', { namespace: 'bar' })).rejects.toThrow( + ERROR_NAMESPACE_SPECIFIED + ); }); - test(`supplements options with undefined namespace`, async () => { - const request = createMockRequest(); - const baseClient = createMockClient(); + test(`supplements options with the current namespace`, async () => { + const { client, baseClient } = await createSpacesSavedObjectsClient(); const expectedReturnValue = createMockResponse(); baseClient.get.mockReturnValue(Promise.resolve(expectedReturnValue)); - const spacesService = await createSpacesService(currentSpace.id); - const client = new SpacesSavedObjectsClient({ - request, - baseClient, - spacesService, - typeRegistry, - }); const type = Symbol(); const id = Symbol(); const options = Object.freeze({ foo: 'bar' }); @@ -102,37 +101,17 @@ const createMockResponse = () => ({ describe('#bulkGet', () => { test(`throws error if options.namespace is specified`, async () => { - const request = createMockRequest(); - const baseClient = createMockClient(); - const spacesService = await createSpacesService(currentSpace.id); - - const client = new SpacesSavedObjectsClient({ - request, - baseClient, - spacesService, - typeRegistry, - }); + const { client } = await createSpacesSavedObjectsClient(); await expect( client.bulkGet([{ id: '', type: 'foo' }], { namespace: 'bar' }) - ).rejects.toThrowErrorMatchingSnapshot(); + ).rejects.toThrow(ERROR_NAMESPACE_SPECIFIED); }); - test(`supplements options with undefined namespace`, async () => { - const request = createMockRequest(); - const baseClient = createMockClient(); - const expectedReturnValue = { - saved_objects: [createMockResponse()], - }; + test(`supplements options with the current namespace`, async () => { + const { client, baseClient } = await createSpacesSavedObjectsClient(); + const expectedReturnValue = { saved_objects: [createMockResponse()] }; baseClient.bulkGet.mockReturnValue(Promise.resolve(expectedReturnValue)); - const spacesService = await createSpacesService(currentSpace.id); - - const client = new SpacesSavedObjectsClient({ - request, - baseClient, - spacesService, - typeRegistry, - }); const objects = [{ type: 'foo' }]; const options = Object.freeze({ foo: 'bar' }); @@ -149,25 +128,15 @@ const createMockResponse = () => ({ describe('#find', () => { test(`throws error if options.namespace is specified`, async () => { - const request = createMockRequest(); - const baseClient = createMockClient(); - const spacesService = await createSpacesService(currentSpace.id); - - const client = new SpacesSavedObjectsClient({ - request, - baseClient, - spacesService, - typeRegistry, - }); + const { client } = await createSpacesSavedObjectsClient(); - await expect( - client.find({ type: 'foo', namespace: 'bar' }) - ).rejects.toThrowErrorMatchingSnapshot(); + await expect(client.find({ type: 'foo', namespace: 'bar' })).rejects.toThrow( + ERROR_NAMESPACE_SPECIFIED + ); }); test(`passes options.type to baseClient if valid singular type specified`, async () => { - const request = createMockRequest(); - const baseClient = createMockClient(); + const { client, baseClient } = await createSpacesSavedObjectsClient(); const expectedReturnValue = { saved_objects: [createMockResponse()], total: 1, @@ -175,16 +144,8 @@ const createMockResponse = () => ({ page: 0, }; baseClient.find.mockReturnValue(Promise.resolve(expectedReturnValue)); - const spacesService = await createSpacesService(currentSpace.id); - const client = new SpacesSavedObjectsClient({ - request, - baseClient, - spacesService, - typeRegistry, - }); const options = Object.freeze({ type: 'foo' }); - const actualReturnValue = await client.find(options); expect(actualReturnValue).toBe(expectedReturnValue); @@ -194,9 +155,8 @@ const createMockResponse = () => ({ }); }); - test(`supplements options with undefined namespace`, async () => { - const request = createMockRequest(); - const baseClient = createMockClient(); + test(`supplements options with the current namespace`, async () => { + const { client, baseClient } = await createSpacesSavedObjectsClient(); const expectedReturnValue = { saved_objects: [createMockResponse()], total: 1, @@ -204,14 +164,6 @@ const createMockResponse = () => ({ page: 0, }; baseClient.find.mockReturnValue(Promise.resolve(expectedReturnValue)); - const spacesService = await createSpacesService(currentSpace.id); - - const client = new SpacesSavedObjectsClient({ - request, - baseClient, - spacesService, - typeRegistry, - }); const options = Object.freeze({ type: ['foo', 'bar'] }); const actualReturnValue = await client.find(options); @@ -226,35 +178,17 @@ const createMockResponse = () => ({ describe('#create', () => { test(`throws error if options.namespace is specified`, async () => { - const request = createMockRequest(); - const baseClient = createMockClient(); - const spacesService = await createSpacesService(currentSpace.id); - - const client = new SpacesSavedObjectsClient({ - request, - baseClient, - spacesService, - typeRegistry, - }); + const { client } = await createSpacesSavedObjectsClient(); - await expect( - client.create('foo', {}, { namespace: 'bar' }) - ).rejects.toThrowErrorMatchingSnapshot(); + await expect(client.create('foo', {}, { namespace: 'bar' })).rejects.toThrow( + ERROR_NAMESPACE_SPECIFIED + ); }); - test(`supplements options with undefined namespace`, async () => { - const request = createMockRequest(); - const baseClient = createMockClient(); + test(`supplements options with the current namespace`, async () => { + const { client, baseClient } = await createSpacesSavedObjectsClient(); const expectedReturnValue = createMockResponse(); baseClient.create.mockReturnValue(Promise.resolve(expectedReturnValue)); - const spacesService = await createSpacesService(currentSpace.id); - - const client = new SpacesSavedObjectsClient({ - request, - baseClient, - spacesService, - typeRegistry, - }); const type = Symbol(); const attributes = Symbol(); @@ -272,37 +206,17 @@ const createMockResponse = () => ({ describe('#bulkCreate', () => { test(`throws error if options.namespace is specified`, async () => { - const request = createMockRequest(); - const baseClient = createMockClient(); - const spacesService = await createSpacesService(currentSpace.id); - - const client = new SpacesSavedObjectsClient({ - request, - baseClient, - spacesService, - typeRegistry, - }); + const { client } = await createSpacesSavedObjectsClient(); await expect( client.bulkCreate([{ id: '', type: 'foo', attributes: {} }], { namespace: 'bar' }) - ).rejects.toThrowErrorMatchingSnapshot(); + ).rejects.toThrow(ERROR_NAMESPACE_SPECIFIED); }); - test(`supplements options with undefined namespace`, async () => { - const request = createMockRequest(); - const baseClient = createMockClient(); - const expectedReturnValue = { - saved_objects: [createMockResponse()], - }; + test(`supplements options with the current namespace`, async () => { + const { client, baseClient } = await createSpacesSavedObjectsClient(); + const expectedReturnValue = { saved_objects: [createMockResponse()] }; baseClient.bulkCreate.mockReturnValue(Promise.resolve(expectedReturnValue)); - const spacesService = await createSpacesService(currentSpace.id); - - const client = new SpacesSavedObjectsClient({ - request, - baseClient, - spacesService, - typeRegistry, - }); const objects = [{ type: 'foo' }]; const options = Object.freeze({ foo: 'bar' }); @@ -319,36 +233,18 @@ const createMockResponse = () => ({ describe('#update', () => { test(`throws error if options.namespace is specified`, async () => { - const request = createMockRequest(); - const baseClient = createMockClient(); - const spacesService = await createSpacesService(currentSpace.id); - - const client = new SpacesSavedObjectsClient({ - request, - baseClient, - spacesService, - typeRegistry, - }); + const { client } = await createSpacesSavedObjectsClient(); await expect( // @ts-ignore client.update(null, null, null, { namespace: 'bar' }) - ).rejects.toThrowErrorMatchingSnapshot(); + ).rejects.toThrow(ERROR_NAMESPACE_SPECIFIED); }); - test(`supplements options with undefined namespace`, async () => { - const request = createMockRequest(); - const baseClient = createMockClient(); + test(`supplements options with the current namespace`, async () => { + const { client, baseClient } = await createSpacesSavedObjectsClient(); const expectedReturnValue = createMockResponse(); baseClient.update.mockReturnValue(Promise.resolve(expectedReturnValue)); - const spacesService = await createSpacesService(currentSpace.id); - - const client = new SpacesSavedObjectsClient({ - request, - baseClient, - spacesService, - typeRegistry, - }); const type = Symbol(); const id = Symbol(); @@ -366,21 +262,19 @@ const createMockResponse = () => ({ }); describe('#bulkUpdate', () => { - test(`supplements options with the spaces namespace`, async () => { - const request = createMockRequest(); - const baseClient = createMockClient(); - const expectedReturnValue = { - saved_objects: [createMockResponse()], - }; - baseClient.bulkUpdate.mockReturnValue(Promise.resolve(expectedReturnValue)); - const spacesService = await createSpacesService(currentSpace.id); + test(`throws error if options.namespace is specified`, async () => { + const { client } = await createSpacesSavedObjectsClient(); - const client = new SpacesSavedObjectsClient({ - request, - baseClient, - spacesService, - typeRegistry, - }); + await expect( + // @ts-ignore + client.bulkUpdate(null, { namespace: 'bar' }) + ).rejects.toThrow(ERROR_NAMESPACE_SPECIFIED); + }); + + test(`supplements options with the current namespace`, async () => { + const { client, baseClient } = await createSpacesSavedObjectsClient(); + const expectedReturnValue = { saved_objects: [createMockResponse()] }; + baseClient.bulkUpdate.mockReturnValue(Promise.resolve(expectedReturnValue)); const actualReturnValue = await client.bulkUpdate([ { id: 'id', type: 'foo', attributes: {}, references: [] }, @@ -403,36 +297,18 @@ const createMockResponse = () => ({ describe('#delete', () => { test(`throws error if options.namespace is specified`, async () => { - const request = createMockRequest(); - const baseClient = createMockClient(); - const spacesService = await createSpacesService(currentSpace.id); - - const client = new SpacesSavedObjectsClient({ - request, - baseClient, - spacesService, - typeRegistry, - }); + const { client } = await createSpacesSavedObjectsClient(); await expect( // @ts-ignore client.delete(null, null, { namespace: 'bar' }) - ).rejects.toThrowErrorMatchingSnapshot(); + ).rejects.toThrow(ERROR_NAMESPACE_SPECIFIED); }); - test(`supplements options with undefined namespace`, async () => { - const request = createMockRequest(); - const baseClient = createMockClient(); + test(`supplements options with the current namespace`, async () => { + const { client, baseClient } = await createSpacesSavedObjectsClient(); const expectedReturnValue = createMockResponse(); baseClient.delete.mockReturnValue(Promise.resolve(expectedReturnValue)); - const spacesService = await createSpacesService(currentSpace.id); - - const client = new SpacesSavedObjectsClient({ - request, - baseClient, - spacesService, - typeRegistry, - }); const type = Symbol(); const id = Symbol(); @@ -447,5 +323,65 @@ const createMockResponse = () => ({ }); }); }); + + describe('#addToNamespaces', () => { + test(`throws error if options.namespace is specified`, async () => { + const { client } = await createSpacesSavedObjectsClient(); + + await expect( + // @ts-ignore + client.addToNamespaces(null, null, null, { namespace: 'bar' }) + ).rejects.toThrow(ERROR_NAMESPACE_SPECIFIED); + }); + + test(`supplements options with the current namespace`, async () => { + const { client, baseClient } = await createSpacesSavedObjectsClient(); + const expectedReturnValue = createMockResponse(); + baseClient.addToNamespaces.mockReturnValue(Promise.resolve(expectedReturnValue)); + + const type = Symbol(); + const id = Symbol(); + const namespaces = Symbol(); + const options = Object.freeze({ foo: 'bar' }); + // @ts-ignore + const actualReturnValue = await client.addToNamespaces(type, id, namespaces, options); + + expect(actualReturnValue).toBe(expectedReturnValue); + expect(baseClient.addToNamespaces).toHaveBeenCalledWith(type, id, namespaces, { + foo: 'bar', + namespace: currentSpace.expectedNamespace, + }); + }); + }); + + describe('#deleteFromNamespaces', () => { + test(`throws error if options.namespace is specified`, async () => { + const { client } = await createSpacesSavedObjectsClient(); + + await expect( + // @ts-ignore + client.deleteFromNamespaces(null, null, null, { namespace: 'bar' }) + ).rejects.toThrow(ERROR_NAMESPACE_SPECIFIED); + }); + + test(`supplements options with the current namespace`, async () => { + const { client, baseClient } = await createSpacesSavedObjectsClient(); + const expectedReturnValue = createMockResponse(); + baseClient.deleteFromNamespaces.mockReturnValue(Promise.resolve(expectedReturnValue)); + + const type = Symbol(); + const id = Symbol(); + const namespaces = Symbol(); + const options = Object.freeze({ foo: 'bar' }); + // @ts-ignore + const actualReturnValue = await client.deleteFromNamespaces(type, id, namespaces, options); + + expect(actualReturnValue).toBe(expectedReturnValue); + expect(baseClient.deleteFromNamespaces).toHaveBeenCalledWith(type, id, namespaces, { + foo: 'bar', + namespace: currentSpace.expectedNamespace, + }); + }); + }); }); }); diff --git a/x-pack/plugins/spaces/server/saved_objects/spaces_saved_objects_client.ts b/x-pack/plugins/spaces/server/saved_objects/spaces_saved_objects_client.ts index f216d5743cf89..e31bc7cef6900 100644 --- a/x-pack/plugins/spaces/server/saved_objects/spaces_saved_objects_client.ts +++ b/x-pack/plugins/spaces/server/saved_objects/spaces_saved_objects_client.ts @@ -13,6 +13,8 @@ import { SavedObjectsCreateOptions, SavedObjectsFindOptions, SavedObjectsUpdateOptions, + SavedObjectsAddToNamespacesOptions, + SavedObjectsDeleteFromNamespacesOptions, ISavedObjectTypeRegistry, } from 'src/core/server'; import { SpacesServiceSetup } from '../spaces_service/spaces_service'; @@ -213,6 +215,50 @@ export class SpacesSavedObjectsClient implements SavedObjectsClientContract { }); } + /** + * Adds namespaces to a SavedObject + * + * @param type + * @param id + * @param namespaces + * @param options + */ + public async addToNamespaces( + type: string, + id: string, + namespaces: string[], + options: SavedObjectsAddToNamespacesOptions = {} + ) { + throwErrorIfNamespaceSpecified(options); + + return await this.client.addToNamespaces(type, id, namespaces, { + ...options, + namespace: spaceIdToNamespace(this.spaceId), + }); + } + + /** + * Removes namespaces from a SavedObject + * + * @param type + * @param id + * @param namespaces + * @param options + */ + public async deleteFromNamespaces( + type: string, + id: string, + namespaces: string[], + options: SavedObjectsDeleteFromNamespacesOptions = {} + ) { + throwErrorIfNamespaceSpecified(options); + + return await this.client.deleteFromNamespaces(type, id, namespaces, { + ...options, + namespace: spaceIdToNamespace(this.spaceId), + }); + } + /** * Updates an array of objects by id * diff --git a/x-pack/plugins/task_manager/server/lib/fill_pool.test.ts b/x-pack/plugins/task_manager/server/lib/fill_pool.test.ts index 3863fdaf9da62..ebb72c3ed36d6 100644 --- a/x-pack/plugins/task_manager/server/lib/fill_pool.test.ts +++ b/x-pack/plugins/task_manager/server/lib/fill_pool.test.ts @@ -10,7 +10,7 @@ import { fillPool } from './fill_pool'; import { TaskPoolRunResult } from '../task_pool'; describe('fillPool', () => { - test('stops filling when there are no more tasks in the store', async () => { + test('stops filling when pool runs all claimed tasks, even if there is more capacity', async () => { const tasks = [ [1, 2, 3], [4, 5], @@ -22,7 +22,7 @@ describe('fillPool', () => { await fillPool(fetchAvailableTasks, converter, run); - expect(_.flattenDeep(run.args)).toEqual([1, 2, 3, 4, 5]); + expect(_.flattenDeep(run.args)).toEqual([1, 2, 3]); }); test('stops filling when the pool has no more capacity', async () => { diff --git a/x-pack/plugins/task_manager/server/lib/fill_pool.ts b/x-pack/plugins/task_manager/server/lib/fill_pool.ts index 60470b22c00a9..9e4894587203d 100644 --- a/x-pack/plugins/task_manager/server/lib/fill_pool.ts +++ b/x-pack/plugins/task_manager/server/lib/fill_pool.ts @@ -5,12 +5,12 @@ */ import { performance } from 'perf_hooks'; -import { after } from 'lodash'; import { TaskPoolRunResult } from '../task_pool'; export enum FillPoolResult { NoTasksClaimed = 'NoTasksClaimed', RanOutOfCapacity = 'RanOutOfCapacity', + PoolFilled = 'PoolFilled', } type BatchRun = (tasks: T[]) => Promise; @@ -35,33 +35,28 @@ export async function fillPool( run: BatchRun ): Promise { performance.mark('fillPool.start'); - const markClaimedTasksOnRerunCycle = after(2, () => - performance.mark('fillPool.claimedOnRerunCycle') - ); - while (true) { - const instances = await fetchAvailableTasks(); + const instances = await fetchAvailableTasks(); - if (!instances.length) { - performance.mark('fillPool.bailNoTasks'); - performance.measure( - 'fillPool.activityDurationUntilNoTasks', - 'fillPool.start', - 'fillPool.bailNoTasks' - ); - return FillPoolResult.NoTasksClaimed; - } - markClaimedTasksOnRerunCycle(); - const tasks = instances.map(converter); + if (!instances.length) { + performance.mark('fillPool.bailNoTasks'); + performance.measure( + 'fillPool.activityDurationUntilNoTasks', + 'fillPool.start', + 'fillPool.bailNoTasks' + ); + return FillPoolResult.NoTasksClaimed; + } + const tasks = instances.map(converter); - if ((await run(tasks)) === TaskPoolRunResult.RanOutOfCapacity) { - performance.mark('fillPool.bailExhaustedCapacity'); - performance.measure( - 'fillPool.activityDurationUntilExhaustedCapacity', - 'fillPool.start', - 'fillPool.bailExhaustedCapacity' - ); - return FillPoolResult.RanOutOfCapacity; - } - performance.mark('fillPool.cycle'); + if ((await run(tasks)) === TaskPoolRunResult.RanOutOfCapacity) { + performance.mark('fillPool.bailExhaustedCapacity'); + performance.measure( + 'fillPool.activityDurationUntilExhaustedCapacity', + 'fillPool.start', + 'fillPool.bailExhaustedCapacity' + ); + return FillPoolResult.RanOutOfCapacity; } + performance.mark('fillPool.cycle'); + return FillPoolResult.PoolFilled; } diff --git a/x-pack/plugins/task_manager/server/lib/is_task_not_found_error.test.ts b/x-pack/plugins/task_manager/server/lib/is_task_not_found_error.test.ts new file mode 100644 index 0000000000000..65922ea8e6de7 --- /dev/null +++ b/x-pack/plugins/task_manager/server/lib/is_task_not_found_error.test.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 { isTaskSavedObjectNotFoundError } from './is_task_not_found_error'; +import { SavedObjectsErrorHelpers } from '../../../../../src/core/server'; +import uuid from 'uuid'; + +describe('isTaskSavedObjectNotFoundError', () => { + test('identifies SavedObjects Not Found errors', () => { + const id = uuid.v4(); + // ensure the error created by SO parses as a string with the format we expect + expect( + `${SavedObjectsErrorHelpers.createGenericNotFoundError('task', id)}`.includes(`task/${id}`) + ).toBe(true); + + const errorBySavedObjectsHelper = SavedObjectsErrorHelpers.createGenericNotFoundError( + 'task', + id + ); + + expect(isTaskSavedObjectNotFoundError(errorBySavedObjectsHelper, id)).toBe(true); + }); + + test('identifies generic errors', () => { + const id = uuid.v4(); + expect(isTaskSavedObjectNotFoundError(new Error(`not found`), id)).toBe(false); + }); +}); diff --git a/x-pack/plugins/task_manager/server/lib/is_task_not_found_error.ts b/x-pack/plugins/task_manager/server/lib/is_task_not_found_error.ts new file mode 100644 index 0000000000000..8cc1c08f2a967 --- /dev/null +++ b/x-pack/plugins/task_manager/server/lib/is_task_not_found_error.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 { SavedObjectsErrorHelpers } from '../../../../../src/core/server'; + +export function isTaskSavedObjectNotFoundError(err: Error, taskId: string) { + return SavedObjectsErrorHelpers.isNotFoundError(err) && `${err}`.includes(taskId); +} diff --git a/x-pack/plugins/task_manager/server/plugin.ts b/x-pack/plugins/task_manager/server/plugin.ts index fdfe0c068afcf..e837fcd9c0dec 100644 --- a/x-pack/plugins/task_manager/server/plugin.ts +++ b/x-pack/plugins/task_manager/server/plugin.ts @@ -38,17 +38,16 @@ export class TaskManagerPlugin public setup(core: CoreSetup, plugins: any): TaskManagerSetupContract { const logger = this.initContext.logger.get('taskManager'); const config$ = this.initContext.config.create(); - const elasticsearch = core.elasticsearch.adminClient; return { registerLegacyAPI: once((__LEGACY: PluginLegacyDependencies) => { config$.subscribe(async config => { - const [{ savedObjects }] = await core.getStartServices(); + const [{ savedObjects, elasticsearch }] = await core.getStartServices(); const savedObjectsRepository = savedObjects.createInternalRepository(['task']); this.legacyTaskManager$.next( createTaskManager(core, { logger, config, - elasticsearch, + elasticsearch: elasticsearch.legacy.client, savedObjectsRepository, savedObjectsSerializer: savedObjects.createSerializer(), }) diff --git a/x-pack/plugins/task_manager/server/queries/mark_available_tasks_as_claimed.ts b/x-pack/plugins/task_manager/server/queries/mark_available_tasks_as_claimed.ts index b0d9dc61c9667..8f7cc47f936b2 100644 --- a/x-pack/plugins/task_manager/server/queries/mark_available_tasks_as_claimed.ts +++ b/x-pack/plugins/task_manager/server/queries/mark_available_tasks_as_claimed.ts @@ -11,7 +11,7 @@ import { RangeFilter, mustBeAllOf, MustCondition, - MustNotCondition, + BoolClauseWithAnyCondition, } from './query_clauses'; export const TaskWithSchedule: ExistsFilter = { @@ -54,15 +54,16 @@ export const IdleTaskWithExpiredRunAt: MustCondition = }, }; -export const InactiveTasks: MustNotCondition = { +// TODO: Fix query clauses to support this +export const InactiveTasks: BoolClauseWithAnyCondition = { bool: { must_not: [ { bool: { should: [{ term: { 'task.status': 'running' } }, { term: { 'task.status': 'claiming' } }], + must: { range: { 'task.retryAt': { gt: 'now' } } }, }, }, - { range: { 'task.retryAt': { gt: 'now' } } }, ], }, }; diff --git a/x-pack/plugins/task_manager/server/task_manager.ts b/x-pack/plugins/task_manager/server/task_manager.ts index c3f24a4aae88a..a7c67d190e72e 100644 --- a/x-pack/plugins/task_manager/server/task_manager.ts +++ b/x-pack/plugins/task_manager/server/task_manager.ts @@ -9,7 +9,7 @@ import { filter } from 'rxjs/operators'; import { performance } from 'perf_hooks'; import { pipe } from 'fp-ts/lib/pipeable'; -import { Option, none, some, map as mapOptional } from 'fp-ts/lib/Option'; +import { Option, some, map as mapOptional } from 'fp-ts/lib/Option'; import { SavedObjectsSerializer, IScopedClusterClient, @@ -156,8 +156,8 @@ export class TaskManager { this.events$.next(event); }; - private attemptToRun(task: Option = none) { - this.claimRequests$.next(task); + private attemptToRun(task: string) { + this.claimRequests$.next(some(task)); } private createTaskRunnerForTask = (instance: ConcreteTaskInstance) => { @@ -280,9 +280,7 @@ export class TaskManager { ...options, taskInstance: ensureDeprecatedFieldsAreCorrected(taskInstance, this.logger), }); - const result = await this.store.schedule(modifiedTask); - this.attemptToRun(); - return result; + return await this.store.schedule(modifiedTask); } /** @@ -298,7 +296,7 @@ export class TaskManager { .then(resolve) .catch(reject); - this.attemptToRun(some(taskId)); + this.attemptToRun(taskId); }); } diff --git a/x-pack/plugins/task_manager/server/task_pool.test.ts b/x-pack/plugins/task_manager/server/task_pool.test.ts index a827c1436c9bd..fb87b6290a3da 100644 --- a/x-pack/plugins/task_manager/server/task_pool.test.ts +++ b/x-pack/plugins/task_manager/server/task_pool.test.ts @@ -8,6 +8,7 @@ import sinon from 'sinon'; import { TaskPool, TaskPoolRunResult } from './task_pool'; import { mockLogger, resolvable, sleep } from './test_utils'; import { asOk } from './lib/result_type'; +import { SavedObjectsErrorHelpers } from '../../../../src/core/server'; describe('TaskPool', () => { test('occupiedWorkers are a sum of running tasks', async () => { @@ -101,6 +102,30 @@ describe('TaskPool', () => { expect(result).toEqual(TaskPoolRunResult.RunningAllClaimedTasks); }); + test('should not log when running a Task fails due to the Task SO having been deleted while in flight', async () => { + const logger = mockLogger(); + const pool = new TaskPool({ + maxWorkers: 3, + logger, + }); + + const taskFailedToRun = mockTask(); + taskFailedToRun.run.mockImplementation(async () => { + throw SavedObjectsErrorHelpers.createGenericNotFoundError('task', taskFailedToRun.id); + }); + + const result = await pool.run([mockTask(), taskFailedToRun, mockTask()]); + + expect(logger.debug.mock.calls[0]).toMatchInlineSnapshot(` + Array [ + "Task TaskType \\"shooooo\\" failed in attempt to run: Saved object [task/foo] not found", + ] + `); + expect(logger.warn).not.toHaveBeenCalled(); + + expect(result).toEqual(TaskPoolRunResult.RunningAllClaimedTasks); + }); + test('Running a task which fails still takes up capacity', async () => { const logger = mockLogger(); const pool = new TaskPool({ diff --git a/x-pack/plugins/task_manager/server/task_pool.ts b/x-pack/plugins/task_manager/server/task_pool.ts index 90f1880a159da..8999fb48680ce 100644 --- a/x-pack/plugins/task_manager/server/task_pool.ts +++ b/x-pack/plugins/task_manager/server/task_pool.ts @@ -11,6 +11,7 @@ import { performance } from 'perf_hooks'; import { Logger } from './types'; import { TaskRunner } from './task_runner'; +import { isTaskSavedObjectNotFoundError } from './lib/is_task_not_found_error'; interface Opts { maxWorkers: number; @@ -125,7 +126,17 @@ export class TaskPool { taskRunner .run() .catch(err => { - this.logger.warn(`Task ${taskRunner.toString()} failed in attempt to run: ${err.message}`); + // If a task Saved Object can't be found by an in flight task runner + // we asssume the underlying task has been deleted while it was running + // so we will log this as a debug, rather than a warn + const errorLogLine = `Task ${taskRunner.toString()} failed in attempt to run: ${ + err.message + }`; + if (isTaskSavedObjectNotFoundError(err, taskRunner.id)) { + this.logger.debug(errorLogLine); + } else { + this.logger.warn(errorLogLine); + } }) .then(() => this.running.delete(taskRunner)); } diff --git a/x-pack/plugins/task_manager/server/task_store.test.ts b/x-pack/plugins/task_manager/server/task_store.test.ts index 97794311fb3d2..4ecefcb7984eb 100644 --- a/x-pack/plugins/task_manager/server/task_store.test.ts +++ b/x-pack/plugins/task_manager/server/task_store.test.ts @@ -407,9 +407,9 @@ describe('TaskStore', () => { { term: { 'task.status': 'running' } }, { term: { 'task.status': 'claiming' } }, ], + must: { range: { 'task.retryAt': { gt: 'now' } } }, }, }, - { range: { 'task.retryAt': { gt: 'now' } } }, ], }, }, @@ -553,9 +553,9 @@ describe('TaskStore', () => { { term: { 'task.status': 'running' } }, { term: { 'task.status': 'claiming' } }, ], + must: { range: { 'task.retryAt': { gt: 'now' } } }, }, }, - { range: { 'task.retryAt': { gt: 'now' } } }, ], }, }, diff --git a/x-pack/plugins/telemetry_collection_xpack/server/telemetry_collection/get_stats_with_xpack.test.ts b/x-pack/plugins/telemetry_collection_xpack/server/telemetry_collection/get_stats_with_xpack.test.ts index f2a9995098e59..5dfe3d3e99a7f 100644 --- a/x-pack/plugins/telemetry_collection_xpack/server/telemetry_collection/get_stats_with_xpack.test.ts +++ b/x-pack/plugins/telemetry_collection_xpack/server/telemetry_collection/get_stats_with_xpack.test.ts @@ -31,7 +31,6 @@ const kibana = { const getContext = () => ({ version: '8675309-snapshot', - isDev: true, logger: coreMock.createPluginInitializerContext().logger.get('test'), }); diff --git a/x-pack/plugins/transform/public/app/hooks/use_x_json_mode.ts b/x-pack/plugins/transform/public/app/hooks/use_x_json_mode.ts deleted file mode 100644 index 1017ce198ff29..0000000000000 --- a/x-pack/plugins/transform/public/app/hooks/use_x_json_mode.ts +++ /dev/null @@ -1,20 +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 { useState } from 'react'; -import { collapseLiteralStrings, expandLiteralStrings, XJsonMode } from '../../shared_imports'; - -export const xJsonMode = new XJsonMode(); - -export const useXJsonMode = (json: string) => { - const [xJson, setXJson] = useState(expandLiteralStrings(json)); - - return { - xJson, - setXJson, - xJsonMode, - convertToJson: collapseLiteralStrings, - }; -}; diff --git a/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/step_define_form.tsx b/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/step_define_form.tsx index 5e0eb7ee08361..320e405b5d437 100644 --- a/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/step_define_form.tsx +++ b/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/step_define_form.tsx @@ -33,11 +33,12 @@ import { QueryStringInput, } from '../../../../../../../../../src/plugins/data/public'; +import { useXJsonMode } from '../../../../../../../../../src/plugins/es_ui_shared/static/ace_x_json/hooks'; + import { PivotPreview } from '../../../../components/pivot_preview'; import { useDocumentationLinks } from '../../../../hooks/use_documentation_links'; import { SavedSearchQuery, SearchItems } from '../../../../hooks/use_search_items'; -import { useXJsonMode, xJsonMode } from '../../../../hooks/use_x_json_mode'; import { useToastNotifications } from '../../../../app_dependencies'; import { TransformPivotConfig } from '../../../../common'; import { dictionaryToArray, Dictionary } from '../../../../../../common/types/common'; @@ -432,6 +433,7 @@ export const StepDefineForm: FC = React.memo(({ overrides = {}, onChange, convertToJson, setXJson: setAdvancedEditorConfig, xJson: advancedEditorConfig, + xJsonMode, } = useXJsonMode(stringifiedPivotConfig); useEffect(() => { diff --git a/x-pack/plugins/transform/public/shared_imports.ts b/x-pack/plugins/transform/public/shared_imports.ts index 8eb42ad677c0f..494b6db6aafe0 100644 --- a/x-pack/plugins/transform/public/shared_imports.ts +++ b/x-pack/plugins/transform/public/shared_imports.ts @@ -5,11 +5,11 @@ */ export { createSavedSearchesLoader } from '../../../../src/plugins/discover/public'; -export { XJsonMode } from '../../es_ui_shared/console_lang/ace/modes/x_json'; export { + XJsonMode, collapseLiteralStrings, expandLiteralStrings, -} from '../../../../src/plugins/es_ui_shared/console_lang/lib'; +} from '../../../../src/plugins/es_ui_shared/public'; export { UseRequestConfig, diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 687834a683c4d..209a3f626272f 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -130,6 +130,14 @@ "charts.colormaps.greysText": "グレー", "charts.colormaps.redsText": "赤", "charts.colormaps.yellowToRedText": "黄色から赤", + "charts.controls.colorRanges.errorText": "各範囲は前の範囲よりも大きくなければなりません。", + "charts.controls.colorSchema.colorSchemaLabel": "配色", + "charts.controls.colorSchema.howToChangeColorsDescription": "それぞれの色は凡例で変更できます。", + "charts.controls.colorSchema.resetColorsButtonLabel": "色をリセット", + "charts.controls.colorSchema.reverseColorSchemaLabel": "図表を反転", + "charts.controls.rangeErrorMessage": "値は {min} と {max} の間でなければなりません", + "charts.controls.vislibBasicOptions.legendPositionLabel": "凡例位置", + "charts.controls.vislibBasicOptions.showTooltipLabel": "ツールヒントを表示", "common.ui.errorAutoCreateIndex.breadcrumbs.errorText": "エラー", "common.ui.errorAutoCreateIndex.errorDescription": "Elasticsearch クラスターの {autoCreateIndexActionConfig} 設定が原因で、Kibana が保存されたオブジェクトを格納するインデックスを自動的に作成できないようです。Kibana は、保存されたオブジェクトインデックスが適切なマッピング/スキーマを使用し Kibana から Elasticsearch へのポーリングの回数を減らすための最適な手段であるため、この Elasticsearch の機能を使用します。", "common.ui.errorAutoCreateIndex.errorDisclaimer": "申し訳ございませんが、この問題が解決されるまで Kibana で何も保存することができません。", @@ -311,8 +319,6 @@ "common.ui.stateManagement.unableToStoreHistoryInSessionErrorMessage": "セッションがいっぱいで安全に削除できるアイテムが見つからないため、Kibana は履歴アイテムを保存できません。\n\nこれは大抵新規タブに移動することで解決されますが、より大きな問題が原因である可能性もあります。このメッセージが定期的に表示される場合は、{gitHubIssuesUrl} で問題を報告してください。", "common.ui.url.replacementFailedErrorMessage": "置換に失敗、未解決の表現式: {expr}", "common.ui.url.savedObjectIsMissingNotificationMessage": "保存されたオブジェクトがありません", - "common.ui.vis.kibanaMap.leaflet.fitDataBoundsAriaLabel": "データバウンドを合わせる", - "common.ui.vis.kibanaMap.zoomWarning": "ズームレベルが最大に達しました。完全にズームインするには、Elasticsearch と Kibana の {defaultDistribution} にアップグレードしてください。{ems} でより多くのズームレベルが利用できます。または、独自のマップサーバーを構成できます。詳細は { wms } または { configSettings} をご覧ください。", "console.autocomplete.addMethodMetaText": "メソド", "console.consoleDisplayName": "コンソール", "console.consoleMenu.copyAsCurlMessage": "リクエストが URL としてコピーされました", @@ -792,8 +798,6 @@ "data.search.searchBar.savedQueryDescriptionLabelText": "説明", "data.search.searchBar.savedQueryDescriptionText": "再度使用するクエリテキストとフィルターを保存します。", "data.search.searchBar.savedQueryForm.titleConflictText": "タイトルが既に保存されているクエリに使用されています", - "data.search.searchBar.savedQueryForm.titleMissingText": "名前が必要です", - "data.search.searchBar.savedQueryForm.whitespaceErrorText": "タイトルの始めと終わりにはスペースを使用できません", "data.search.searchBar.savedQueryFormCancelButtonText": "キャンセル", "data.search.searchBar.savedQueryFormSaveButtonText": "保存", "data.search.searchBar.savedQueryFormTitle": "クエリを保存", @@ -843,8 +847,6 @@ "data.search.searchSource.indexPatternIdDescription": "{kibanaIndexPattern} インデックス内の ID です。", "data.search.searchSource.indexPatternIdLabel": "インデックスパターン ID", "data.search.searchSource.indexPatternLabel": "インデックスパターン", - "data.search.searchSource.noSearchStrategyRegisteredErrorMessageDescription": "検索リクエストの検索方法が見つかりませんでした", - "data.search.searchSource.noSearchStrategyRegisteredErrorMessageTitle": "検索方法が登録されていません", "data.search.searchSource.queryTimeDescription": "クエリの処理の所要時間です。リクエストの送信やブラウザでのパースの時間は含まれません。", "data.search.searchSource.queryTimeLabel": "クエリ時間", "data.search.searchSource.queryTimeValue": "{queryTime}ms", @@ -2000,10 +2002,8 @@ "kbn.context.reloadPageDescription.selectValidAnchorDocumentTextMessage": "にアクセスして有効な別のドキュメントを選択してください。", "kbn.context.unableToLoadAnchorDocumentDescription": "別のドキュメントが読み込めません", "kbn.context.unableToLoadDocumentDescription": "ドキュメントが読み込めません", - "kbn.dashboard.listing.table.descriptionColumnName": "説明", "kbn.dashboard.panel.unableToMigratePanelDataForSixOneZeroErrorMessage": "「6.1.0」のダッシュボードの互換性のため、パネルデータを移行できませんでした。パネルには想定された列または行フィールドがありません", "kbn.dashboard.panel.unableToMigratePanelDataForSixThreeZeroErrorMessage": "「6.3.0」のダッシュボードの互換性のため、パネルデータを移行できませんでした。パネルに必要なフィールドがありません: {key}", - "kbn.dashboard.savedDashboardsTitle": "ダッシュボード", "kbn.dashboardTitle": "ダッシュボード", "kbn.devToolsTitle": "開発ツール", "kbn.discover.backToTopLinkText": "最上部へ戻る。", @@ -2244,7 +2244,6 @@ "kbn.management.editIndexPattern.scripted.table.nameHeader": "名前", "kbn.management.editIndexPattern.scripted.table.scriptDescription": "フィールドのスクリプトです", "kbn.management.editIndexPattern.scripted.table.scriptHeader": "スクリプト", - "kbn.management.editIndexPattern.scripted.unknownModeErrorMessage": "不明なフィールド設定モード {mode}", "kbn.management.editIndexPattern.scriptedHeader": "スクリプトフィールド", "kbn.management.editIndexPattern.scriptedLabel": "ビジュアライゼーションにスクリプトフィールドを使用し、ドキュメントに表示させることができます。但し、スクリプトフィールドは検索できません。", "kbn.management.editIndexPattern.setDefaultAria": "デフォルトのインデックスに設定", @@ -2273,9 +2272,6 @@ "kbn.management.editIndexPattern.timeFilterLabel.mappingAPILink": "マッピング API", "kbn.management.editIndexPattern.timeFilterLabel.timeFilterDetail": "このページは {indexPatternTitle} インデックス内のすべてのフィールドと、Elasticsearch に記録された各フィールドのコアタイプを一覧表示します。フィールドタイプを変更するには Elasticsearch を使用します", "kbn.management.editIndexPatternLiveRegionAriaLabel": "インデックスパターン", - "kbn.management.indexPattern.confirmOverwriteButton": "上書き", - "kbn.management.indexPattern.confirmOverwriteLabel": "「{title}」に上書きしてよろしいですか?", - "kbn.management.indexPattern.confirmOverwriteTitle": "{type} を上書きしますか?", "kbn.management.indexPattern.goToPatternButtonLabel": "既存のパターンに移動", "kbn.management.indexPattern.sectionsHeader": "インデックスパターン", "kbn.management.indexPattern.titleExistsLabel": "「{title}」というタイトルのインデックスパターンが既に存在します。", @@ -2298,167 +2294,129 @@ "kbn.management.indexPatternTable.title": "インデックスパターン", "kbn.management.landing.header": "Kibana {version} 管理", "kbn.management.landing.subhead": "インデックス、インデックスパターン、保存されたオブジェクト、Kibana の設定、その他を管理します。", - "kbn.management.landing.text": "アプリの一覧は左側のメニューにあります。", - "kbn.management.objects.confirmModalOptions.deleteButtonLabel": "削除", - "kbn.management.objects.confirmModalOptions.modalDescription": "このアクションはオブジェクトを Kibana から永久に削除します。", - "kbn.management.objects.confirmModalOptions.modalTitle": "「{title}」を削除しますか?", - "kbn.management.objects.deleteSavedObjectsConfirmModalDescription": "この操作は次の保存されたオブジェクトを削除します:", - "kbn.management.objects.field.offLabel": "オフ", - "kbn.management.objects.field.onLabel": "オン", - "kbn.management.objects.objectsTable.deleteSavedObjectsConfirmModal.cancelButtonLabel": "キャンセル", - "kbn.management.objects.objectsTable.deleteSavedObjectsConfirmModal.deleteButtonLabel": "削除", - "kbn.management.objects.objectsTable.deleteSavedObjectsConfirmModal.deleteProcessButtonLabel": "削除中…", - "kbn.management.objects.objectsTable.deleteSavedObjectsConfirmModal.idColumnName": "ID", - "kbn.management.objects.objectsTable.deleteSavedObjectsConfirmModal.titleColumnName": "タイトル", - "kbn.management.objects.objectsTable.deleteSavedObjectsConfirmModal.typeColumnName": "タイプ", - "kbn.management.objects.objectsTable.deleteSavedObjectsConfirmModalTitle": "保存されたオブジェクトの削除", - "kbn.management.objects.objectsTable.export.dangerNotification": "エクスポートを生成できません", - "kbn.management.objects.objectsTable.export.successNotification": "ファイルはバックグラウンドでダウンロード中です", - "kbn.management.objects.objectsTable.export.successWithMissingRefsNotification": "ファイルはバックグラウンドでダウンロード中です。一部の関連オブジェクトが見つかりませんでした。足りないオブジェクトの一覧は、エクスポートされたファイルの最後の行をご覧ください。", - "kbn.management.objects.objectsTable.exportObjectsConfirmModal.cancelButtonLabel": "キャンセル", - "kbn.management.objects.objectsTable.exportObjectsConfirmModal.exportAllButtonLabel": "すべてエクスポート:", - "kbn.management.objects.objectsTable.exportObjectsConfirmModal.exportOptionsLabel": "オプション", - "kbn.management.objects.objectsTable.exportObjectsConfirmModal.includeReferencesDeepLabel": "関連オブジェクトを含める", - "kbn.management.objects.objectsTable.exportObjectsConfirmModalDescription": "エクスポートするタイプを選択してください", - "kbn.management.objects.objectsTable.exportObjectsConfirmModalTitle": "{filteredItemCount, plural, one{# オブジェクト} other {# オブジェクト}}をエクスポート", - "kbn.management.objects.objectsTable.flyout.confirmLegacyImport.resolvingConflictsLoadingMessage": "矛盾を解決中…", - "kbn.management.objects.objectsTable.flyout.confirmLegacyImport.retryingFailedObjectsLoadingMessage": "失敗したオブジェクトを再試行中…", - "kbn.management.objects.objectsTable.flyout.confirmLegacyImport.savedSearchAreLinkedProperlyLoadingMessage": "保存された検索が正しくリンクされていることを確認してください…", - "kbn.management.objects.objectsTable.flyout.confirmLegacyImport.savingConflictsLoadingMessage": "矛盾を保存中…", - "kbn.management.objects.objectsTable.flyout.confirmOverwriteBody": "{title} を上書きしてよろしいですか?", - "kbn.management.objects.objectsTable.flyout.confirmOverwriteCancelButtonText": "キャンセル", - "kbn.management.objects.objectsTable.flyout.confirmOverwriteOverwriteButtonText": "上書き", - "kbn.management.objects.objectsTable.flyout.confirmOverwriteTitle": "{type} を上書きしますか?", - "kbn.management.objects.objectsTable.flyout.errorCalloutTitle": "申し訳ございませんが、エラーが発生しました", - "kbn.management.objects.objectsTable.flyout.import.cancelButtonLabel": "キャンセル", - "kbn.management.objects.objectsTable.flyout.import.confirmButtonLabel": "インポート", - "kbn.management.objects.objectsTable.flyout.importFailedDescription": "{totalImportCount} 個中 {failedImportCount} 個のオブジェクトのインポートに失敗しました。インポート失敗", - "kbn.management.objects.objectsTable.flyout.importFailedMissingReference": "{type} [id={id}] は {refType} [id={refId}] を見つけられませんでした", - "kbn.management.objects.objectsTable.flyout.importFailedTitle": "インポート失敗", - "kbn.management.objects.objectsTable.flyout.importFailedUnsupportedType": "{type} [id={id}] サポートされていないタイプ", - "kbn.management.objects.objectsTable.flyout.importFileErrorMessage": "ファイルを処理できませんでした。", - "kbn.management.objects.objectsTable.flyout.importLegacyFileErrorMessage": "ファイルを処理できませんでした。", - "kbn.management.objects.objectsTable.flyout.importPromptText": "インポート", - "kbn.management.objects.objectsTable.flyout.importSavedObjectTitle": "保存されたオブジェクトのインポート", - "kbn.management.objects.objectsTable.flyout.importSuccessful.confirmAllChangesButtonLabel": "すべての変更を確定", - "kbn.management.objects.objectsTable.flyout.importSuccessful.confirmButtonLabel": "完了", - "kbn.management.objects.objectsTable.flyout.importSuccessfulCallout.noObjectsImportedTitle": "オブジェクトがインポートされませんでした", - "kbn.management.objects.objectsTable.flyout.importSuccessfulDescription": "{importCount} 個のオブジェクトがインポートされました。", - "kbn.management.objects.objectsTable.flyout.importSuccessfulTitle": "インポート成功", - "kbn.management.objects.objectsTable.flyout.indexPatternConflictsCalloutLinkText": "新規インデックスパターンを作成", - "kbn.management.objects.objectsTable.flyout.indexPatternConflictsDescription": "次の保存されたオブジェクトは、存在しないインデックスパターンを使用しています。別のデックスパターンを選択してください。必要に応じて {indexPatternLink} できます。", - "kbn.management.objects.objectsTable.flyout.indexPatternConflictsTitle": "インデックスパターンの矛盾", - "kbn.management.objects.objectsTable.flyout.invalidFormatOfImportedFileErrorMessage": "保存されたオブジェクトのファイル形式が無効なため、インポートできません。", - "kbn.management.objects.objectsTable.flyout.legacyFileUsedBody": "最新のレポートで NDJSON ファイルを作成すれば完了です。", - "kbn.management.objects.objectsTable.flyout.legacyFileUsedTitle": "JSON ファイルのサポートが終了します", - "kbn.management.objects.objectsTable.flyout.overwriteSavedObjectsLabel": "すべての保存されたオブジェクトを自動的に上書きしますか?", - "kbn.management.objects.objectsTable.flyout.renderConflicts.columnCountDescription": "影響されるオブジェクトの数です", - "kbn.management.objects.objectsTable.flyout.renderConflicts.columnCountName": "カウント", - "kbn.management.objects.objectsTable.flyout.renderConflicts.columnIdDescription": "インデックスパターンの ID です", - "kbn.management.objects.objectsTable.flyout.renderConflicts.columnIdName": "ID", - "kbn.management.objects.objectsTable.flyout.renderConflicts.columnNewIndexPatternName": "新規インデックスパターン", - "kbn.management.objects.objectsTable.flyout.renderConflicts.columnSampleOfAffectedObjectsDescription": "影響されるオブジェクトのサンプルです", - "kbn.management.objects.objectsTable.flyout.renderConflicts.columnSampleOfAffectedObjectsName": "影響されるオブジェクトのサンプル", - "kbn.management.objects.objectsTable.flyout.resolveImportErrorsFileErrorMessage": "ファイルを処理できませんでした。", - "kbn.management.objects.objectsTable.flyout.selectFileToImportFormRowLabel": "インポートするファイルを選択してください", - "kbn.management.objects.objectsTable.header.exportButtonLabel": "{filteredCount, plural, one{# オブジェクト} other {# オブジェクト}}をエクスポート", - "kbn.management.objects.objectsTable.header.importButtonLabel": "インポート", - "kbn.management.objects.objectsTable.header.refreshButtonLabel": "更新", - "kbn.management.objects.objectsTable.header.savedObjectsTitle": "保存されたオブジェクト", - "kbn.management.objects.objectsTable.howToDeleteSavedObjectsDescription": "ここから保存された検索などの保存されたオブジェクトを削除できます。保存されたオブジェクトの生データを編集することもできます。通常、オブジェクトは関連アプリケーションでのみ編集され、こn画面で編集するよりもそちらのほうが賢明です。", - "kbn.management.objects.objectsTable.relationships.columnActions.inspectActionDescription": "この保存されたオブジェクトを確認してください", - "kbn.management.objects.objectsTable.relationships.columnActions.inspectActionName": "検査", - "kbn.management.objects.objectsTable.relationships.columnActionsName": "アクション", - "kbn.management.objects.objectsTable.relationships.columnRelationship.childAsValue": "子", - "kbn.management.objects.objectsTable.relationships.columnRelationship.parentAsValue": "ペアレント", - "kbn.management.objects.objectsTable.relationships.columnRelationshipName": "直接関係", - "kbn.management.objects.objectsTable.relationships.columnTitleDescription": "保存されたオブジェクトのタイトルです", - "kbn.management.objects.objectsTable.relationships.columnTitleName": "タイトル", - "kbn.management.objects.objectsTable.relationships.columnTypeDescription": "保存されたオブジェクトのタイプです", - "kbn.management.objects.objectsTable.relationships.columnTypeName": "タイプ", - "kbn.management.objects.objectsTable.relationships.relationshipsTitle": "{title} に関連する保存されたオブジェクトはこちらです。この {type} を削除すると、親オブジェクトに影響がありますが、子オブジェクトには影響はありません。", - "kbn.management.objects.objectsTable.relationships.renderErrorMessage": "エラー", - "kbn.management.objects.objectsTable.relationships.search.filters.relationship.childAsValue.view": "子", - "kbn.management.objects.objectsTable.relationships.search.filters.relationship.name": "直接関係", - "kbn.management.objects.objectsTable.relationships.search.filters.relationship.parentAsValue.view": "親", - "kbn.management.objects.objectsTable.relationships.search.filters.type.name": "タイプ", - "kbn.management.objects.objectsTable.searchBar.unableToParseQueryErrorMessage": "クエリをパースできません", - "kbn.management.objects.objectsTable.table.columnActions.inspectActionDescription": "この保存されたオブジェクトを確認してください", - "kbn.management.objects.objectsTable.table.columnActions.inspectActionName": "検査", - "kbn.management.objects.objectsTable.table.columnActions.viewRelationshipsActionDescription": "この保存されたオブジェクトと他の保存されたオブジェクトとの関係性を表示します", - "kbn.management.objects.objectsTable.table.columnActions.viewRelationshipsActionName": "関係性", - "kbn.management.objects.objectsTable.table.columnActionsName": "アクション", - "kbn.management.objects.objectsTable.table.columnTitleDescription": "保存されたオブジェクトのタイトルです", - "kbn.management.objects.objectsTable.table.columnTitleName": "タイトル", - "kbn.management.objects.objectsTable.table.columnTypeDescription": "保存されたオブジェクトのタイプです", - "kbn.management.objects.objectsTable.table.columnTypeName": "タイプ", - "kbn.management.objects.objectsTable.table.deleteButtonLabel": "削除", - "kbn.management.objects.objectsTable.table.deleteButtonTitle": "保存されたオブジェクトを削除できません", - "kbn.management.objects.objectsTable.table.exportButtonLabel": "エクスポート", - "kbn.management.objects.objectsTable.table.exportPopoverButtonLabel": "エクスポート", - "kbn.management.objects.objectsTable.table.typeFilterName": "タイプ", - "kbn.management.objects.objectsTable.unableFindSavedObjectsNotificationMessage": "保存されたオブジェクトが見つかりません", - "kbn.management.objects.parsingFieldErrorMessage": "{fieldName} をインデックスパターン {indexName} 用にパース中にエラーが発生しました: {errorMessage}", - "kbn.management.objects.savedObjectsSectionLabel": "保存されたオブジェクト", - "kbn.management.objects.view.cancelButtonAriaLabel": "キャンセル", - "kbn.management.objects.view.cancelButtonLabel": "キャンセル", - "kbn.management.objects.view.deleteItemButtonLabel": "{title} を削除", - "kbn.management.objects.view.editItemTitle": "{title} の編集", - "kbn.management.objects.view.fieldDoesNotExistErrorMessage": "このオブジェクトに関連付けられたフィールドは、現在このインデックスパターンに存在しません。", - "kbn.management.objects.view.howToFixErrorDescription": "このエラーの原因がわかる場合は修正してください。わからない場合は上の削除ボタンをクリックしてください。", - "kbn.management.objects.view.howToModifyObjectDescription": "オブジェクトの編集は上級ユーザー向けです。オブジェクトのプロパティが検証されておらず、無効なオブジェクトはエラー、データ損失、またはそれ以上の問題の原因となります。コードを熟知した人に指示されていない限り、この設定は変更しない方が無難です。", - "kbn.management.objects.view.howToModifyObjectTitle": "十分ご注意ください!", - "kbn.management.objects.view.indexPatternDoesNotExistErrorMessage": "このオブジェクトに関連付けられたインデックスパターンは現在存在しません。", - "kbn.management.objects.view.saveButtonAriaLabel": "{ title } オブジェクトを保存", - "kbn.management.objects.view.saveButtonLabel": "{ title } オブジェクトを保存", - "kbn.management.objects.view.savedObjectProblemErrorMessage": "この保存されたオブジェクトに問題があります", - "kbn.management.objects.view.savedSearchDoesNotExistErrorMessage": "このオブジェクトに関連付けられた保存された検索は現在存在しません。", - "kbn.management.objects.view.viewItemButtonLabel": "{title} を表示", - "kbn.management.objects.view.viewItemTitle": "{title} を表示", - "kbn.management.savedObjects.editBreadcrumb": "{savedObjectType} を編集", - "kbn.management.savedObjects.indexBreadcrumb": "保存されたオブジェクト", + "kbn.management.landing.text": "すべてのツールの一覧は、左のメニューにあります。", + "savedObjectsManagement.indexPattern.confirmOverwriteButton": "上書き", + "savedObjectsManagement.indexPattern.confirmOverwriteLabel": "「{title}」に上書きしてよろしいですか?", + "savedObjectsManagement.indexPattern.confirmOverwriteTitle": "{type} を上書きしますか?", + "savedObjectsManagement.deleteConfirm.modalDeleteButtonLabel": "削除", + "savedObjectsManagement.deleteConfirm.modalDescription": "このアクションはオブジェクトを Kibana から永久に削除します。", + "savedObjectsManagement.deleteConfirm.modalTitle": "「{title}」を削除しますか?", + "savedObjectsManagement.deleteSavedObjectsConfirmModalDescription": "この操作は次の保存されたオブジェクトを削除します:", + "savedObjectsManagement.objectsTable.deleteSavedObjectsConfirmModal.cancelButtonLabel": "キャンセル", + "savedObjectsManagement.objectsTable.deleteSavedObjectsConfirmModal.deleteButtonLabel": "削除", + "savedObjectsManagement.objectsTable.deleteSavedObjectsConfirmModal.deleteProcessButtonLabel": "削除中…", + "savedObjectsManagement.objectsTable.deleteSavedObjectsConfirmModal.idColumnName": "ID", + "savedObjectsManagement.objectsTable.deleteSavedObjectsConfirmModal.titleColumnName": "タイトル", + "savedObjectsManagement.objectsTable.deleteSavedObjectsConfirmModal.typeColumnName": "タイプ", + "savedObjectsManagement.objectsTable.deleteSavedObjectsConfirmModalTitle": "保存されたオブジェクトの削除", + "savedObjectsManagement.objectsTable.export.dangerNotification": "エクスポートを生成できません", + "savedObjectsManagement.objectsTable.export.successNotification": "ファイルはバックグラウンドでダウンロード中です", + "savedObjectsManagement.objectsTable.export.successWithMissingRefsNotification": "ファイルはバックグラウンドでダウンロード中です。一部の関連オブジェクトが見つかりませんでした。足りないオブジェクトの一覧は、エクスポートされたファイルの最後の行をご覧ください。", + "savedObjectsManagement.objectsTable.exportObjectsConfirmModal.cancelButtonLabel": "キャンセル", + "savedObjectsManagement.objectsTable.exportObjectsConfirmModal.exportAllButtonLabel": "すべてエクスポート:", + "savedObjectsManagement.objectsTable.exportObjectsConfirmModal.exportOptionsLabel": "オプション", + "savedObjectsManagement.objectsTable.exportObjectsConfirmModal.includeReferencesDeepLabel": "関連オブジェクトを含める", + "savedObjectsManagement.objectsTable.exportObjectsConfirmModalDescription": "エクスポートするタイプを選択してください", + "savedObjectsManagement.objectsTable.exportObjectsConfirmModalTitle": "{filteredItemCount, plural, one{# オブジェクト} other {# オブジェクト}}をエクスポート", + "savedObjectsManagement.objectsTable.flyout.confirmLegacyImport.resolvingConflictsLoadingMessage": "矛盾を解決中…", + "savedObjectsManagement.objectsTable.flyout.confirmLegacyImport.retryingFailedObjectsLoadingMessage": "失敗したオブジェクトを再試行中…", + "savedObjectsManagement.objectsTable.flyout.confirmLegacyImport.savedSearchAreLinkedProperlyLoadingMessage": "保存された検索が正しくリンクされていることを確認してください…", + "savedObjectsManagement.objectsTable.flyout.confirmLegacyImport.savingConflictsLoadingMessage": "矛盾を保存中…", + "savedObjectsManagement.objectsTable.flyout.confirmOverwriteBody": "{title} を上書きしてよろしいですか?", + "savedObjectsManagement.objectsTable.flyout.confirmOverwriteCancelButtonText": "キャンセル", + "savedObjectsManagement.objectsTable.flyout.confirmOverwriteOverwriteButtonText": "上書き", + "savedObjectsManagement.objectsTable.flyout.confirmOverwriteTitle": "{type} を上書きしますか?", + "savedObjectsManagement.objectsTable.flyout.errorCalloutTitle": "申し訳ございませんが、エラーが発生しました", + "savedObjectsManagement.objectsTable.flyout.import.cancelButtonLabel": "キャンセル", + "savedObjectsManagement.objectsTable.flyout.import.confirmButtonLabel": "インポート", + "savedObjectsManagement.objectsTable.flyout.importFailedDescription": "{totalImportCount} 個中 {failedImportCount} 個のオブジェクトのインポートに失敗しました。インポート失敗", + "savedObjectsManagement.objectsTable.flyout.importFailedMissingReference": "{type} [id={id}] は {refType} [id={refId}] を見つけられませんでした", + "savedObjectsManagement.objectsTable.flyout.importFailedTitle": "インポート失敗", + "savedObjectsManagement.objectsTable.flyout.importFailedUnsupportedType": "{type} [id={id}] サポートされていないタイプ", + "savedObjectsManagement.objectsTable.flyout.importFileErrorMessage": "ファイルを処理できませんでした。", + "savedObjectsManagement.objectsTable.flyout.importLegacyFileErrorMessage": "ファイルを処理できませんでした。", + "savedObjectsManagement.objectsTable.flyout.importPromptText": "インポート", + "savedObjectsManagement.objectsTable.flyout.importSavedObjectTitle": "保存されたオブジェクトのインポート", + "savedObjectsManagement.objectsTable.flyout.importSuccessful.confirmAllChangesButtonLabel": "すべての変更を確定", + "savedObjectsManagement.objectsTable.flyout.importSuccessful.confirmButtonLabel": "完了", + "savedObjectsManagement.objectsTable.flyout.importSuccessfulCallout.noObjectsImportedTitle": "オブジェクトがインポートされませんでした", + "savedObjectsManagement.objectsTable.flyout.importSuccessfulDescription": "{importCount} 個のオブジェクトがインポートされました。", + "savedObjectsManagement.objectsTable.flyout.importSuccessfulTitle": "インポート成功", + "savedObjectsManagement.objectsTable.flyout.indexPatternConflictsCalloutLinkText": "新規インデックスパターンを作成", + "savedObjectsManagement.objectsTable.flyout.indexPatternConflictsDescription": "次の保存されたオブジェクトは、存在しないインデックスパターンを使用しています。別のデックスパターンを選択してください。必要に応じて {indexPatternLink} できます。", + "savedObjectsManagement.objectsTable.flyout.indexPatternConflictsTitle": "インデックスパターンの矛盾", + "savedObjectsManagement.objectsTable.flyout.invalidFormatOfImportedFileErrorMessage": "保存されたオブジェクトのファイル形式が無効なため、インポートできません。", + "savedObjectsManagement.objectsTable.flyout.legacyFileUsedBody": "最新のレポートで NDJSON ファイルを作成すれば完了です。", + "savedObjectsManagement.objectsTable.flyout.legacyFileUsedTitle": "JSON ファイルのサポートが終了します", + "savedObjectsManagement.objectsTable.flyout.overwriteSavedObjectsLabel": "すべての保存されたオブジェクトを自動的に上書きしますか?", + "savedObjectsManagement.objectsTable.flyout.renderConflicts.columnCountDescription": "影響されるオブジェクトの数です", + "savedObjectsManagement.objectsTable.flyout.renderConflicts.columnCountName": "カウント", + "savedObjectsManagement.objectsTable.flyout.renderConflicts.columnIdDescription": "インデックスパターンの ID です", + "savedObjectsManagement.objectsTable.flyout.renderConflicts.columnIdName": "ID", + "savedObjectsManagement.objectsTable.flyout.renderConflicts.columnNewIndexPatternName": "新規インデックスパターン", + "savedObjectsManagement.objectsTable.flyout.renderConflicts.columnSampleOfAffectedObjectsDescription": "影響されるオブジェクトのサンプルです", + "savedObjectsManagement.objectsTable.flyout.renderConflicts.columnSampleOfAffectedObjectsName": "影響されるオブジェクトのサンプル", + "savedObjectsManagement.objectsTable.flyout.resolveImportErrorsFileErrorMessage": "ファイルを処理できませんでした。", + "savedObjectsManagement.objectsTable.flyout.selectFileToImportFormRowLabel": "インポートするファイルを選択してください", + "savedObjectsManagement.objectsTable.header.exportButtonLabel": "{filteredCount, plural, one{# オブジェクト} other {# オブジェクト}}をエクスポート", + "savedObjectsManagement.objectsTable.header.importButtonLabel": "インポート", + "savedObjectsManagement.objectsTable.header.refreshButtonLabel": "更新", + "savedObjectsManagement.objectsTable.header.savedObjectsTitle": "保存されたオブジェクト", + "savedObjectsManagement.objectsTable.howToDeleteSavedObjectsDescription": "ここから保存された検索などの保存されたオブジェクトを削除できます。保存されたオブジェクトの生データを編集することもできます。通常、オブジェクトは関連アプリケーションでのみ編集され、こn画面で編集するよりもそちらのほうが賢明です。", + "savedObjectsManagement.objectsTable.relationships.columnActions.inspectActionDescription": "この保存されたオブジェクトを確認してください", + "savedObjectsManagement.objectsTable.relationships.columnActions.inspectActionName": "検査", + "savedObjectsManagement.objectsTable.relationships.columnActionsName": "アクション", + "savedObjectsManagement.objectsTable.relationships.columnRelationship.childAsValue": "子", + "savedObjectsManagement.objectsTable.relationships.columnRelationship.parentAsValue": "ペアレント", + "savedObjectsManagement.objectsTable.relationships.columnRelationshipName": "直接関係", + "savedObjectsManagement.objectsTable.relationships.columnTitleDescription": "保存されたオブジェクトのタイトルです", + "savedObjectsManagement.objectsTable.relationships.columnTitleName": "タイトル", + "savedObjectsManagement.objectsTable.relationships.columnTypeDescription": "保存されたオブジェクトのタイプです", + "savedObjectsManagement.objectsTable.relationships.columnTypeName": "タイプ", + "savedObjectsManagement.objectsTable.relationships.relationshipsTitle": "{title} に関連する保存されたオブジェクトはこちらです。この {type} を削除すると、親オブジェクトに影響がありますが、子オブジェクトには影響はありません。", + "savedObjectsManagement.objectsTable.relationships.renderErrorMessage": "エラー", + "savedObjectsManagement.objectsTable.relationships.search.filters.relationship.childAsValue.view": "子", + "savedObjectsManagement.objectsTable.relationships.search.filters.relationship.name": "直接関係", + "savedObjectsManagement.objectsTable.relationships.search.filters.relationship.parentAsValue.view": "親", + "savedObjectsManagement.objectsTable.relationships.search.filters.type.name": "タイプ", + "savedObjectsManagement.objectsTable.searchBar.unableToParseQueryErrorMessage": "クエリをパースできません", + "savedObjectsManagement.objectsTable.table.columnActions.inspectActionDescription": "この保存されたオブジェクトを確認してください", + "savedObjectsManagement.objectsTable.table.columnActions.inspectActionName": "検査", + "savedObjectsManagement.objectsTable.table.columnActions.viewRelationshipsActionDescription": "この保存されたオブジェクトと他の保存されたオブジェクトとの関係性を表示します", + "savedObjectsManagement.objectsTable.table.columnActions.viewRelationshipsActionName": "関係性", + "savedObjectsManagement.objectsTable.table.columnActionsName": "アクション", + "savedObjectsManagement.objectsTable.table.columnTitleDescription": "保存されたオブジェクトのタイトルです", + "savedObjectsManagement.objectsTable.table.columnTitleName": "タイトル", + "savedObjectsManagement.objectsTable.table.columnTypeDescription": "保存されたオブジェクトのタイプです", + "savedObjectsManagement.objectsTable.table.columnTypeName": "タイプ", + "savedObjectsManagement.objectsTable.table.deleteButtonLabel": "削除", + "savedObjectsManagement.objectsTable.table.deleteButtonTitle": "保存されたオブジェクトを削除できません", + "savedObjectsManagement.objectsTable.table.exportButtonLabel": "エクスポート", + "savedObjectsManagement.objectsTable.table.exportPopoverButtonLabel": "エクスポート", + "savedObjectsManagement.objectsTable.table.typeFilterName": "タイプ", + "savedObjectsManagement.objectsTable.unableFindSavedObjectsNotificationMessage": "保存されたオブジェクトが見つかりません", + "savedObjectsManagement.parsingFieldErrorMessage": "{fieldName} をインデックスパターン {indexName} 用にパース中にエラーが発生しました: {errorMessage}", + "savedObjectsManagement.managementSectionLabel": "保存されたオブジェクト", + "savedObjectsManagement.view.cancelButtonAriaLabel": "キャンセル", + "savedObjectsManagement.view.cancelButtonLabel": "キャンセル", + "savedObjectsManagement.view.deleteItemButtonLabel": "{title} を削除", + "savedObjectsManagement.view.editItemTitle": "{title} の編集", + "savedObjectsManagement.view.fieldDoesNotExistErrorMessage": "このオブジェクトに関連付けられたフィールドは、現在このインデックスパターンに存在しません。", + "savedObjectsManagement.view.howToFixErrorDescription": "このエラーの原因がわかる場合は修正してください。わからない場合は上の削除ボタンをクリックしてください。", + "savedObjectsManagement.view.howToModifyObjectDescription": " オブジェクトの編集は上級ユーザー向けです。オブジェクトのプロパティが検証されておらず、無効なオブジェクトはエラー、データ損失、またはそれ以上の問題の原因となります。コードを熟知した人に指示されていない限り、この設定は変更しない方が無難です。", + "savedObjectsManagement.view.howToModifyObjectTitle": "十分ご注意ください!", + "savedObjectsManagement.view.indexPatternDoesNotExistErrorMessage": "このオブジェクトに関連付けられたインデックスパターンは現在存在しません。", + "savedObjectsManagement.view.saveButtonAriaLabel": "{ title } オブジェクトを保存", + "savedObjectsManagement.view.saveButtonLabel": "{ title } オブジェクトを保存", + "savedObjectsManagement.view.savedObjectProblemErrorMessage": "この保存されたオブジェクトに問題があります", + "savedObjectsManagement.view.savedSearchDoesNotExistErrorMessage": "このオブジェクトに関連付けられた保存された検索は現在存在しません。", + "savedObjectsManagement.view.viewItemButtonLabel": "{title} を表示", + "savedObjectsManagement.view.viewItemTitle": "{title} を表示", + "savedObjectsManagement.breadcrumb.edit": "{savedObjectType} を編集", + "savedObjectsManagement.breadcrumb.index": "保存されたオブジェクト", + "savedObjectsManagement.field.offLabel": "オフ", + "savedObjectsManagement.field.onLabel": "オン", "kbn.managementTitle": "管理", - "kbn.topNavMenu.openInspectorButtonLabel": "検査", - "kbn.topNavMenu.refreshButtonLabel": "更新", - "kbn.topNavMenu.saveVisualizationButtonLabel": "保存", - "kbn.topNavMenu.shareVisualizationButtonLabel": "共有", - "kbn.visualize.badge.readOnly.text": "読み込み専用", - "kbn.visualize.badge.readOnly.tooltip": "ビジュアライゼーションを保存できません", - "kbn.visualize.createVisualization.noIndexPatternOrSavedSearchIdErrorMessage": "indexPattern または savedSearchId が必要です", - "kbn.visualize.editor.createBreadcrumb": "作成", - "kbn.visualize.experimentalVisInfoText": "このビジュアライゼーションは実験的なものです。", - "kbn.visualize.helpMenu.appName": "可視化", - "kbn.visualize.linkedToSearch.unlinkSuccessNotificationText": "保存された検索「{searchTitle}」からリンクが解除されました", - "kbn.visualize.listing.betaTitle": "ベータ", - "kbn.visualize.listing.betaTooltip": "このビジュアライゼーションはベータ段階で、変更される可能性があります。デザインとコードはオフィシャル GA 機能よりも完成度が低く、現状のまま保証なしで提供されています。ベータ機能にはオフィシャル GA 機能の SLA が適用されません", - "kbn.visualize.listing.breadcrumb": "可視化", - "kbn.visualize.listing.createNew.createButtonLabel": "新規ビジュアライゼーションを追加", - "kbn.visualize.listing.createNew.description": "データに基づき異なるビジュアライゼーションを作成できます。", - "kbn.visualize.listing.createNew.title": "最初のビジュアライゼーションの作成", - "kbn.visualize.listing.experimentalTitle": "実験的", - "kbn.visualize.listing.experimentalTooltip": "このビジュアライゼーションは今後のリリースで変更または削除される可能性があり、SLA のサポート対象になりません。", - "kbn.visualize.listing.noItemsMessage": "ビジュアライゼーションがないようです。", - "kbn.visualize.listing.table.entityName": "ビジュアライゼーション", - "kbn.visualize.listing.table.entityNamePlural": "ビジュアライゼーション", - "kbn.visualize.listing.table.listTitle": "ビジュアライゼーション", - "kbn.visualize.listing.table.titleColumnName": "タイトル", - "kbn.visualize.listing.table.typeColumnName": "タイプ", - "kbn.visualize.pageHeading": "{chartName} {chartType} ビジュアライゼーション", - "kbn.visualize.saveDialog.saveAndAddToDashboardButtonLabel": "保存してダッシュボードに追加", - "kbn.visualize.topNavMenu.openInspectorButtonAriaLabel": "ビジュアライゼーションのインスペクターを開く", - "kbn.visualize.topNavMenu.openInspectorDisabledButtonTooltip": "このビジュアライゼーションはインスペクターをサポートしていません。", - "kbn.visualize.topNavMenu.refreshButtonAriaLabel": "更新", - "kbn.visualize.topNavMenu.saveVisualization.failureNotificationText": "「{visTitle}」の保存中にエラーが発生しました", - "kbn.visualize.topNavMenu.saveVisualization.successNotificationText": "「{visTitle}」が保存されました", - "kbn.visualize.topNavMenu.saveVisualizationButtonAriaLabel": "ビジュアライゼーションを保存", - "kbn.visualize.topNavMenu.saveVisualizationDisabledButtonTooltip": "保存する前に変更を適用または破棄", - "kbn.visualize.topNavMenu.shareVisualizationButtonAriaLabel": "ビジュアライゼーションを共有", - "kbn.visualize.visualizationTypeInvalidNotificationMessage": "無効なビジュアライゼーションタイプ", - "kbn.visualize.visualizeDescription": "ビジュアライゼーションを作成して Elasticsearch インデックスに保存されたデータを集約します。", - "kbn.visualize.visualizeListingBreadcrumbsTitle": "可視化", - "kbn.visualize.visualizeListingDeleteErrorTitle": "ビジュアライゼーションの削除中にエラーが発生", - "kbn.visualize.wizard.step1Breadcrumb": "作成", - "kbn.visualize.wizard.step2Breadcrumb": "作成", "kbn.visualizeTitle": "可視化", "kibana_legacy.bigUrlWarningNotificationMessage": "{advancedSettingsLink}で{storeInSessionStorageParam}オプションを有効にするか、オンスクリーンビジュアルを簡素化してください。", "kibana_legacy.bigUrlWarningNotificationMessage.advancedSettingsLinkText": "高度な設定", @@ -2503,13 +2461,13 @@ "management.breadcrumb": "管理", "management.connectDataDisplayName": "データに接続", "management.displayName": "管理", + "management.nav.label": "管理", + "management.nav.menu": "管理メニュー", + "management.stackManagement.managementDescription": "Elastic Stack の管理を行うセンターコンソールです。", "indexPatternManagement.editIndexPattern.createIndex.defaultButtonDescription": "すべてのデータに完全集約を実行", "indexPatternManagement.editIndexPattern.createIndex.defaultButtonText": "標準インデックスパターン", "indexPatternManagement.editIndexPattern.createIndex.defaultTypeName": "インデックスパターン", "indexPatternManagement.editIndexPattern.list.defaultIndexPatternListName": "デフォルト", - "management.nav.label": "管理", - "management.nav.menu": "管理メニュー", - "management.stackManagement.managementDescription": "Elastic Stack の管理を行うセンターコンソールです。", "newsfeed.emptyPrompt.noNewsText": "Kibanaインスタンスがインターネットにアクセスできない場合、管理者にこの機能を無効にするように依頼してください。そうでない場合は、ニュースを取り込み続けます。", "newsfeed.emptyPrompt.noNewsTitle": "ニュースがない場合", "newsfeed.flyoutList.closeButtonLabel": "閉じる", @@ -3831,11 +3789,6 @@ "visTypeVislib.chartTypes.areaText": "エリア", "visTypeVislib.chartTypes.barText": "バー", "visTypeVislib.chartTypes.lineText": "折れ線", - "visTypeVislib.controls.colorRanges.errorText": "各範囲は前の範囲よりも大きくなければなりません。", - "visTypeVislib.controls.colorSchema.colorSchemaLabel": "配色", - "visTypeVislib.controls.colorSchema.howToChangeColorsDescription": "それぞれの色は凡例で変更できます。", - "visTypeVislib.controls.colorSchema.resetColorsButtonLabel": "色をリセット", - "visTypeVislib.controls.colorSchema.reverseColorSchemaLabel": "図表を反転", "visTypeVislib.controls.gaugeOptions.alignmentLabel": "アラインメント", "visTypeVislib.controls.gaugeOptions.autoExtendRangeLabel": "範囲を自動拡張", "visTypeVislib.controls.gaugeOptions.displayWarningsLabel": "警告を表示", @@ -3902,10 +3855,7 @@ "visTypeVislib.controls.pointSeries.valueAxes.toggleCustomExtendsAriaLabel": "カスタム範囲を切り替える", "visTypeVislib.controls.pointSeries.valueAxes.toggleOptionsAriaLabel": "{axisName} オプションを切り替える", "visTypeVislib.controls.pointSeries.valueAxes.yAxisTitle": "Y 軸", - "visTypeVislib.controls.rangeErrorMessage": "値は {min} と {max} の間でなければなりません", "visTypeVislib.controls.truncateLabel": "切り捨て", - "visTypeVislib.controls.vislibBasicOptions.legendPositionLabel": "凡例位置", - "visTypeVislib.controls.vislibBasicOptions.showTooltipLabel": "ツールヒントを表示", "visTypeVislib.editors.heatmap.basicSettingsTitle": "基本設定", "visTypeVislib.editors.heatmap.heatmapSettingsTitle": "ヒートマップ設定", "visTypeVislib.editors.heatmap.highlightLabel": "ハイライト範囲", @@ -4028,6 +3978,46 @@ "visualizations.newVisWizard.visTypeAliasDescription": "Visualize 外で Kibana アプリケーションを開きます。", "visualizations.newVisWizard.visTypeAliasTitle": "Kibana アプリケーション", "visualizations.savedObjectName": "ビジュアライゼーション", + "visualize.listing.table.descriptionColumnName": "説明", + "visualize.topNavMenu.openInspectorButtonLabel": "検査", + "visualize.topNavMenu.saveVisualizationButtonLabel": "保存", + "visualize.topNavMenu.shareVisualizationButtonLabel": "共有", + "visualize.badge.readOnly.text": "読み込み専用", + "visualize.badge.readOnly.tooltip": "ビジュアライゼーションを保存できません", + "visualize.createVisualization.noIndexPatternOrSavedSearchIdErrorMessage": "indexPattern または savedSearchId が必要です", + "visualize.editor.createBreadcrumb": "作成", + "visualize.experimentalVisInfoText": "このビジュアライゼーションは実験的なものです。", + "visualize.helpMenu.appName": "可視化", + "visualize.linkedToSearch.unlinkSuccessNotificationText": "保存された検索「{searchTitle}」からリンクが解除されました", + "visualize.listing.betaTitle": "ベータ", + "visualize.listing.betaTooltip": "このビジュアライゼーションはベータ段階で、変更される可能性があります。デザインとコードはオフィシャル GA 機能よりも完成度が低く、現状のまま保証なしで提供されています。ベータ機能にはオフィシャル GA 機能の SLA が適用されません", + "visualize.listing.breadcrumb": "可視化", + "visualize.listing.createNew.createButtonLabel": "新規ビジュアライゼーションを追加", + "visualize.listing.createNew.description": "データに基づき異なるビジュアライゼーションを作成できます。", + "visualize.listing.createNew.title": "最初のビジュアライゼーションの作成", + "visualize.listing.experimentalTitle": "実験的", + "visualize.listing.experimentalTooltip": "このビジュアライゼーションは今後のリリースで変更または削除される可能性があり、SLA のサポート対象になりません。", + "visualize.listing.noItemsMessage": "ビジュアライゼーションがないようです。", + "visualize.listing.table.entityName": "ビジュアライゼーション", + "visualize.listing.table.entityNamePlural": "ビジュアライゼーション", + "visualize.listing.table.listTitle": "ビジュアライゼーション", + "visualize.listing.table.titleColumnName": "タイトル", + "visualize.listing.table.typeColumnName": "タイプ", + "visualize.pageHeading": "{chartName} {chartType} ビジュアライゼーション", + "visualize.saveDialog.saveAndAddToDashboardButtonLabel": "保存してダッシュボードに追加", + "visualize.topNavMenu.openInspectorButtonAriaLabel": "ビジュアライゼーションのインスペクターを開く", + "visualize.topNavMenu.openInspectorDisabledButtonTooltip": "このビジュアライゼーションはインスペクターをサポートしていません。", + "visualize.topNavMenu.saveVisualization.failureNotificationText": "「{visTitle}」の保存中にエラーが発生しました", + "visualize.topNavMenu.saveVisualization.successNotificationText": "「{visTitle}」が保存されました", + "visualize.topNavMenu.saveVisualizationButtonAriaLabel": "ビジュアライゼーションを保存", + "visualize.topNavMenu.saveVisualizationDisabledButtonTooltip": "保存する前に変更を適用または破棄", + "visualize.topNavMenu.shareVisualizationButtonAriaLabel": "ビジュアライゼーションを共有", + "visualize.visualizationTypeInvalidNotificationMessage": "無効なビジュアライゼーションタイプ", + "visualize.visualizeDescription": "ビジュアライゼーションを作成して Elasticsearch インデックスに保存されたデータを集約します。", + "visualize.visualizeListingBreadcrumbsTitle": "可視化", + "visualize.visualizeListingDeleteErrorTitle": "ビジュアライゼーションの削除中にエラーが発生", + "visualize.wizard.step1Breadcrumb": "作成", + "visualize.wizard.step2Breadcrumb": "作成", "xpack.actions.actionTypeRegistry.get.missingActionTypeErrorMessage": "アクションタイプ \"{id}\" は登録されていません。", "xpack.actions.actionTypeRegistry.register.duplicateActionTypeErrorMessage": "アクションタイプ \"{id}\" は既に登録されています。", "xpack.actions.appName": "アクション", @@ -7459,9 +7449,6 @@ "xpack.indexLifecycleMgmt.activePhaseMessage": "アクティブ", "xpack.indexLifecycleMgmt.addLifecyclePolicyActionButtonLabel": "ライフサイクルポリシーを追加", "xpack.indexLifecycleMgmt.appTitle": "インデックスライフサイクルポリシー", - "xpack.indexLifecycleMgmt.checkLicense.errorExpiredMessage": "{licenseType} ライセンスが期限切れのため {pluginName} を使用できません。", - "xpack.indexLifecycleMgmt.checkLicense.errorUnavailableMessage": "現在ライセンス情報が利用できないため {pluginName} を使用できません。", - "xpack.indexLifecycleMgmt.checkLicense.errorUnsupportedMessage": "ご使用の {licenseType} ライセンスは {pluginName} をサポートしていません。ライセンスをアップグレードしてください。", "xpack.indexLifecycleMgmt.coldPhase.freezeIndexLabel": "インデックスを凍結", "xpack.indexLifecycleMgmt.coldPhase.numberOfReplicasLabel": "複製の数", "xpack.indexLifecycleMgmt.coldPhase.replicaCountHelpText": "デフォルトで、複製の数は同じままになります。", @@ -8287,7 +8274,6 @@ "xpack.ingestManager.agentConfigForm.systemMonitoringFieldLabel": "オプション", "xpack.ingestManager.agentConfigForm.systemMonitoringText": "システムメトリックを収集", "xpack.ingestManager.agentConfigForm.systemMonitoringTooltipText": "このオプションを有効にすると、システムメトリックと情報を収集するデータソースで構成をブートストラップできます。", - "xpack.ingestManager.agentConfigInfo.yamlTabName": "YAML", "xpack.ingestManager.agentConfigList.actionsColumnTitle": "アクション", "xpack.ingestManager.agentConfigList.actionsMenuText": "開く", "xpack.ingestManager.agentConfigList.addButton": "エージェント構成を作成", @@ -8425,21 +8411,14 @@ "xpack.ingestManager.createAgentConfig.flyoutTitleDescription": "エージェント構成は、エージェントのグループ全体にわたる設定を管理する目的で使用されます。エージェント構成にデータソースを追加すると、エージェントで収集するデータを指定できます。エージェント構成の編集時には、フリートを使用して、指定したエージェントのグループに更新をデプロイできます。", "xpack.ingestManager.createAgentConfig.submitButtonLabel": "エージェント構成を作成", "xpack.ingestManager.createAgentConfig.successNotificationTitle": "エージェント構成「{name}」を作成しました", - "xpack.ingestManager.createDatasource.addDatasourceButtonText": "データソースに構成を追加", "xpack.ingestManager.createDatasource.agentConfigurationNameLabel": "構成", "xpack.ingestManager.createDatasource.cancelLinkText": "キャンセル", - "xpack.ingestManager.createDatasource.changeConfigLinkText": "構成を変更", - "xpack.ingestManager.createDatasource.changePackageLinkText": "パッケージを変更", - "xpack.ingestManager.createDatasource.continueButtonText": "続行", - "xpack.ingestManager.createDatasource.editDatasourceLinkText": "データソースを編集", "xpack.ingestManager.createDatasource.packageNameLabel": "パッケージ", "xpack.ingestManager.createDatasource.pageTitle": "データソースを作成", "xpack.ingestManager.createDatasource.stepConfigure.advancedOptionsToggleLinkText": "高度なオプション", - "xpack.ingestManager.createDatasource.stepConfigure.chooseDataTitle": "収集したいデータを選択してください", "xpack.ingestManager.createDatasource.stepConfigure.datasourceDescriptionInputLabel": "説明", "xpack.ingestManager.createDatasource.stepConfigure.datasourceNameInputLabel": "データソース名", "xpack.ingestManager.createDatasource.stepConfigure.datasourceNamespaceInputLabel": "名前空間", - "xpack.ingestManager.createDatasource.stepConfigure.defineDatasourceTitle": "データソースを定義する", "xpack.ingestManager.createDatasource.stepConfigure.hideStreamsAriaLabel": "{type} ストリームを隠す", "xpack.ingestManager.createDatasource.stepConfigure.inputSettingsDescription": "次の設定はすべてのストリームに適用されます。", "xpack.ingestManager.createDatasource.stepConfigure.inputSettingsTitle": "設定", @@ -8448,27 +8427,15 @@ "xpack.ingestManager.createDatasource.stepConfigure.showStreamsAriaLabel": "{type} ストリームを表示", "xpack.ingestManager.createDatasource.stepConfigure.streamsEnabledCountText": "{count} / {total, plural, one {# ストリーム} other {# ストリーム}}が有効です", "xpack.ingestManager.createDatasource.stepConfigure.toggleAdvancedOptionsButtonText": "高度なオプション", - "xpack.ingestManager.createDatasource.stepConfigureDatasourceLabel": "構成データソース", - "xpack.ingestManager.createDatasource.stepReview.agentsAffectedCalloutText": "選択されたエージェント構成 {configName} が一部のエージェントで既に使用されていることをフリートが検出しました。このアクションの結果として、フリートはこの構成に登録されているすべてのエージェントを更新します。", - "xpack.ingestManager.createDatasource.stepReview.agentsAffectedCalloutTitle": "このアクションは {count, plural, one {# エージェント} other {# エージェント}}に影響します", - "xpack.ingestManager.createDatasource.stepReview.confirmAgentDisclaimerText": "このアクションによって、この構成に登録されているすべてのエージェントが更新されることを理解しています。", - "xpack.ingestManager.createDatasource.stepReview.confirmAgentDisclaimerTitle": "意思決定を確認", - "xpack.ingestManager.createDatasource.stepReview.reviewTitle": "変更の見直し", - "xpack.ingestManager.createDatasource.stepReviewLabel": "見直し", "xpack.ingestManager.createDatasource.StepSelectConfig.agentConfigAgentsCountText": "{count, plural, one {# エージェント} other {# エージェント}}", - "xpack.ingestManager.createDatasource.StepSelectConfig.createNewConfigButtonText": "新しい構成を作成", "xpack.ingestManager.createDatasource.StepSelectConfig.errorLoadingAgentConfigsTitle": "エージェント構成の読み込みエラー", "xpack.ingestManager.createDatasource.StepSelectConfig.errorLoadingPackageTitle": "パッケージ情報の読み込みエラー", "xpack.ingestManager.createDatasource.StepSelectConfig.errorLoadingSelectedAgentConfigTitle": "選択したエージェント構成の読み込みエラー", "xpack.ingestManager.createDatasource.StepSelectConfig.filterAgentConfigsInputPlaceholder": "エージェント構成の検索", - "xpack.ingestManager.createDatasource.StepSelectConfig.selectAgentConfigTitle": "エージェント構成を選択する", - "xpack.ingestManager.createDatasource.stepSelectConfigLabel": "構成を選択", "xpack.ingestManager.createDatasource.stepSelectPackage.errorLoadingConfigTitle": "エージェント構成情報の読み込みエラー", "xpack.ingestManager.createDatasource.stepSelectPackage.errorLoadingPackagesTitle": "パッケージの読み込みエラー", "xpack.ingestManager.createDatasource.stepSelectPackage.errorLoadingSelectedPackageTitle": "選択したパッケージの読み込みエラー", "xpack.ingestManager.createDatasource.stepSelectPackage.filterPackagesInputPlaceholder": "パッケージの検索", - "xpack.ingestManager.createDatasource.stepSelectPackage.selectPackageTitle": "パッケージを選択する", - "xpack.ingestManager.createDatasource.stepSelectPackageLabel": "パッケージを選択", "xpack.ingestManager.deleteAgentConfigs.confirmModal.affectedAgentsMessage": "{agentsCount, plural, one {# エージェントを} other {# エージェントを}}{agentConfigsCount, plural, one {このエージェント構成に} other {これらのエージェント構成に}}割り当てました。 {agentsCount, plural, one {このエージェント} other {これらのエージェント}}の登録が解除されます。", "xpack.ingestManager.deleteAgentConfigs.confirmModal.cancelButtonLabel": "キャンセル", "xpack.ingestManager.deleteAgentConfigs.confirmModal.confirmAndReassignButtonLabel": "{agentConfigsCount, plural, one {エージェント構成} other {エージェント構成}} and unenroll {agentsCount, plural, one {エージェント} other {エージェント}} を削除", @@ -9510,8 +9477,6 @@ "xpack.ml.dataframe.analytics.classificationExploration.evaluateJobIdTitle": "分類ジョブID {jobId}の評価", "xpack.ml.dataframe.analytics.classificationExploration.firstDocumentsShownHelpText": "予測がある最初の{searchSize}のドキュメントを示す", "xpack.ml.dataframe.analytics.classificationExploration.generalizationDocsCount": "{docsCount, plural, one {# doc} other {# docs}}が評価されました", - "xpack.ml.dataframe.analytics.classificationExploration.indexArrayBadgeContent": "配列", - "xpack.ml.dataframe.analytics.classificationExploration.indexArrayToolTipContent": "この配列ベースの列の完全なコンテンツは表示できません。", "xpack.ml.dataframe.analytics.classificationExploration.jobCapsFetchError": "結果を取得できません。インデックスのフィールドデータの読み込み中にエラーが発生しました。", "xpack.ml.dataframe.analytics.classificationExploration.jobConfigurationFetchError": "結果を取得できません。ジョブ構成データの読み込み中にエラーが発生しました。", "xpack.ml.dataframe.analytics.classificationExploration.jobConfigurationNoResultsMessage": "結果が見つかりませんでした。", @@ -9605,11 +9570,7 @@ "xpack.ml.dataframe.analytics.regressionExploration.generalError": "データの読み込み中にエラーが発生しました。", "xpack.ml.dataframe.analytics.regressionExploration.generalizationDocsCount": "{docsCount, plural, one {# doc} other {# docs}}が評価されました", "xpack.ml.dataframe.analytics.regressionExploration.generalizationErrorTitle": "一般化エラー", - "xpack.ml.dataframe.analytics.regressionExploration.indexArrayBadgeContent": "配列", - "xpack.ml.dataframe.analytics.regressionExploration.indexArrayToolTipContent": "この配列ベースの列の完全なコンテンツは表示できません。", "xpack.ml.dataframe.analytics.regressionExploration.indexError": "インデックスデータの読み込み中にエラーが発生しました。", - "xpack.ml.dataframe.analytics.regressionExploration.isTestingLabel": "テスト", - "xpack.ml.dataframe.analytics.regressionExploration.isTrainingLabel": "トレーニング", "xpack.ml.dataframe.analytics.regressionExploration.jobCapsFetchError": "結果を取得できません。インデックスのフィールドデータの読み込み中にエラーが発生しました。", "xpack.ml.dataframe.analytics.regressionExploration.jobConfigurationFetchError": "結果を取得できません。ジョブ構成データの読み込み中にエラーが発生しました。", "xpack.ml.dataframe.analytics.regressionExploration.meanSquaredErrorText": "平均二乗エラー", @@ -9621,9 +9582,6 @@ "xpack.ml.dataframe.analytics.regressionExploration.queryParsingErrorMessage": "クエリをパースできません。", "xpack.ml.dataframe.analytics.regressionExploration.rSquaredText": "R の二乗", "xpack.ml.dataframe.analytics.regressionExploration.rSquaredTooltipContent": "適合度を表します。モデルによる観察された結果の複製の効果を測定します。", - "xpack.ml.dataframe.analytics.regressionExploration.searchBoxPlaceholder": "例: 平均>0.5", - "xpack.ml.dataframe.analytics.regressionExploration.selectColumnsAriaLabel": "列を選択", - "xpack.ml.dataframe.analytics.regressionExploration.selectFieldsPopoverTitle": "フィールドを選択", "xpack.ml.dataframe.analytics.regressionExploration.tableJobIdTitle": "回帰ジョブID {jobId}のデスティネーションインデックス", "xpack.ml.dataframe.analytics.regressionExploration.trainingDocsCount": "{docsCount, plural, one {# doc} other {# docs}}が評価されました", "xpack.ml.dataframe.analytics.regressionExploration.trainingErrorTitle": "トレーニングエラー", @@ -12459,7 +12417,6 @@ "xpack.reporting.screenCapturePanelContent.optimizeForPrintingLabel": "印刷用に最適化", "xpack.reporting.selfCheck.ok": "レポートプラグイン自己チェックOK!", "xpack.reporting.selfCheck.warning": "レポートプラグイン自己チェックで警告が発生しました: {err}", - "xpack.reporting.selfCheckEncryptionKey.warning": "{setting}のランダムキーを生成しています。保留中のレポートの再開が失敗しないように、kibana.ymlで{setting}を設定してください", "xpack.reporting.shareContextMenu.csvReportsButtonLabel": "CSV レポート", "xpack.reporting.shareContextMenu.pdfReportsButtonLabel": "PDF レポート", "xpack.reporting.shareContextMenu.pngReportsButtonLabel": "PNG レポート", @@ -15993,7 +15950,7 @@ "xpack.triggersActionsUI.sections.alertAdd.saveButtonLabel": "保存", "xpack.triggersActionsUI.sections.alertAdd.saveErrorNotificationText": "アラートを作成できません。", "xpack.triggersActionsUI.sections.alertAdd.saveSuccessNotificationText": "「{alertName}」 を保存しました", - "xpack.triggersActionsUI.sections.alertAdd.selectIndex": "インデックスを選択してください。", + "xpack.triggersActionsUI.sections.alertAdd.selectIndex": "インデックスを選択してください", "xpack.triggersActionsUI.sections.alertAdd.threshold.closeIndexPopoverLabel": "閉じる", "xpack.triggersActionsUI.sections.alertAdd.threshold.fixErrorInExpressionBelowValidationMessage": "下の表現のエラーを修正してください。", "xpack.triggersActionsUI.sections.alertAdd.threshold.howToBroadenSearchQueryDescription": "* で検索クエリの範囲を広げます。", @@ -16077,7 +16034,6 @@ "xpack.triggersActionsUI.sections.alertsList.bulkActionPopover.muteAllTitle": "ミュート", "xpack.triggersActionsUI.sections.alertsList.bulkActionPopover.unmuteAllTitle": "ミュート解除", "xpack.triggersActionsUI.sections.alertsList.collapsedItemActons.deleteTitle": "削除", - "xpack.triggersActionsUI.sections.alertsList.collapsedItemActons.enableTitle": "有効にする", "xpack.triggersActionsUI.sections.alertsList.collapsedItemActons.muteTitle": "ミュート", "xpack.triggersActionsUI.sections.alertsList.collapsedItemActons.popoverButtonTitle": "アクション", "xpack.triggersActionsUI.components.emptyPrompt.emptyButton": "アラートの作成", @@ -16105,7 +16061,6 @@ "xpack.triggersActionsUI.sections.builtinActionTypes.emailAction.userTextFieldLabel": "ユーザー名", "xpack.triggersActionsUI.sections.editConnectorForm.betaBadgeTooltipContent": "{pluginName} はベータ段階で、変更される可能性があります。デザインとコードはオフィシャル GA 機能よりも完成度が低く、現状のまま保証なしで提供されています。ベータ機能にはオフィシャル GA 機能の SLA が適用されません。", "xpack.triggersActionsUI.sections.editConnectorForm.cancelButtonLabel": "キャンセル", - "xpack.triggersActionsUI.sections.editConnectorForm.flyoutTitle": "コネクターを編集", "xpack.triggersActionsUI.sections.editConnectorForm.saveButtonLabel": "保存", "xpack.triggersActionsUI.sections.editConnectorForm.updateErrorNotificationText": "コネクターを更新できません。", "xpack.triggersActionsUI.sections.editConnectorForm.updateSuccessNotificationText": "「{connectorName}」を更新しました", @@ -16266,16 +16221,10 @@ "xpack.uptime.components.embeddables.embeddedMap.embeddablePanelTitle": "オブザーバー位置情報マップを監視", "xpack.uptime.durationChart.emptyPrompt.description": "このモニターは選択された時間範囲で一度も {emphasizedText} していません。", "xpack.uptime.durationChart.emptyPrompt.title": "利用可能な期間データがありません", - "xpack.uptime.emptyState.configureHeartbeatLinkText": "Heartbeat を構成", - "xpack.uptime.emptyState.configureHeartbeatToGetStartedMessage": "アップタイムデータの収集を開始するには {configureHeartbeatLink}。", "xpack.uptime.emptyState.loadingMessage": "読み込み中…", - "xpack.uptime.emptyState.noDataMessage": "アップタイムデータが見つかりませんでした", - "xpack.uptime.emptyState.noDataTitle": "利用可能なアップタイムデータがありません", - "xpack.uptime.emptyState.noIndexTitle": "アップタイムインデックスが見つかりません", "xpack.uptime.emptyStateError.notAuthorized": "アップタイムデータの表示が承認されていません。システム管理者にお問い合わせください。", "xpack.uptime.emptyStateError.notFoundPage": "ページが見つかりません", "xpack.uptime.emptyStateError.title": "エラー", - "xpack.uptime.errorMessage": "エラー: {message}", "xpack.uptime.featureCatalogueDescription": "エンドポイントヘルスチェックとアップタイム監視を行います。", "xpack.uptime.featureRegistry.uptimeFeatureName": "アップタイム", "xpack.uptime.filterBar.ariaLabel": "概要ページのインプットフィルター基準", @@ -16297,7 +16246,6 @@ "xpack.uptime.locationName.helpLinkAnnotation": "場所を追加", "xpack.uptime.ml.durationChart.exploreInMlApp": "ML アプリで探索", "xpack.uptime.ml.enableAnomalyDetectionPanel.anomalyDetectionTitle": "異常検知", - "xpack.uptime.ml.enableAnomalyDetectionPanel.callout.jobExistsDescription.viewJobLinkText": "既存のジョブを表示", "xpack.uptime.ml.enableAnomalyDetectionPanel.createMLJobDescription": "ここでは稼働状況監視の応答時間について異常スコアを計算する機械学習ジョブを作成できます。\n 有効にすると、詳細ページの監視期間チャートに予想範囲が表示され、グラフに異常の注釈が付きます。\n 地理的な地域にわたって遅延が増える期間を特定することもできます。", "xpack.uptime.ml.enableAnomalyDetectionPanel.createNewJobButtonLabel": "新規ジョブを作成", "xpack.uptime.ml.enableAnomalyDetectionPanel.disableAnomalyDetectionTitle": "異常検知を無効にする", @@ -16792,4 +16740,4 @@ "xpack.watcher.watchEdit.thresholdWatchExpression.aggType.fieldIsRequiredValidationMessage": "フィールドを選択してください。", "xpack.watcher.watcherDescription": "アラートの作成、管理、監視によりデータへの変更を検知します。" } -} +} \ No newline at end of file diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 58905787da8d5..5d8d733f2b5b6 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -130,6 +130,14 @@ "charts.colormaps.greysText": "灰色", "charts.colormaps.redsText": "红色", "charts.colormaps.yellowToRedText": "黄到红", + "charts.controls.colorRanges.errorText": "每个范围应大于前一范围。", + "charts.controls.colorSchema.colorSchemaLabel": "颜色模式", + "charts.controls.colorSchema.howToChangeColorsDescription": "可以更改图例中的各个颜色。", + "charts.controls.colorSchema.resetColorsButtonLabel": "重置颜色", + "charts.controls.colorSchema.reverseColorSchemaLabel": "反转模式", + "charts.controls.rangeErrorMessage": "值必须是在 {min} 到 {max} 的范围内", + "charts.controls.vislibBasicOptions.legendPositionLabel": "图例位置", + "charts.controls.vislibBasicOptions.showTooltipLabel": "显示工具提示", "common.ui.errorAutoCreateIndex.breadcrumbs.errorText": "错误", "common.ui.errorAutoCreateIndex.errorDescription": "似乎 Elasticsearch 集群的 {autoCreateIndexActionConfig} 设置使 Kibana 无法自动创建用于存储已保存对象的索引。Kibana 将使用此 Elasticsearch 功能,因为这是确保已保存对象索引使用正确映射/架构的最好方式,而且其允许 Kibana 较少地轮询 Elasticsearch。", "common.ui.errorAutoCreateIndex.errorDisclaimer": "但是,只有解决了此问题后,您才能在 Kibana 保存内容。", @@ -311,8 +319,6 @@ "common.ui.stateManagement.unableToStoreHistoryInSessionErrorMessage": "Kibana 无法将历史记录项存储在您的会话中,因为其已满,并且似乎没有任何可安全删除的项。\n\n通常可通过移至新的标签页来解决此问题,但这会导致更大的问题。如果您有规律地看到此消息,请在 {gitHubIssuesUrl} 提交问题。", "common.ui.url.replacementFailedErrorMessage": "替换失败,未解析的表达式:{expr}", "common.ui.url.savedObjectIsMissingNotificationMessage": "已保存对象缺失", - "common.ui.vis.kibanaMap.leaflet.fitDataBoundsAriaLabel": "适应数据边界", - "common.ui.vis.kibanaMap.zoomWarning": "已达到缩放级别最大数目。要一直放大,请升级到 Elasticsearch 和 Kibana 的 {defaultDistribution}。您可以通过 {ems} 免费使用其他缩放级别。或者,您可以配置自己的地图服务器。请前往 { wms } 或 { configSettings} 以获取详细信息。", "console.autocomplete.addMethodMetaText": "方法", "console.consoleDisplayName": "控制台", "console.consoleMenu.copyAsCurlMessage": "请求已复制为 cURL", @@ -793,8 +799,6 @@ "data.search.searchBar.savedQueryDescriptionLabelText": "描述", "data.search.searchBar.savedQueryDescriptionText": "保存想要再次使用的查询文本和筛选。", "data.search.searchBar.savedQueryForm.titleConflictText": "标题与现有已保存查询有冲突", - "data.search.searchBar.savedQueryForm.titleMissingText": "“名称”必填", - "data.search.searchBar.savedQueryForm.whitespaceErrorText": "标题不能包含前导或尾随空格", "data.search.searchBar.savedQueryFormCancelButtonText": "取消", "data.search.searchBar.savedQueryFormSaveButtonText": "保存", "data.search.searchBar.savedQueryFormTitle": "保存查询", @@ -844,8 +848,6 @@ "data.search.searchSource.indexPatternIdDescription": "{kibanaIndexPattern} 索引中的 ID。", "data.search.searchSource.indexPatternIdLabel": "索引模式 ID", "data.search.searchSource.indexPatternLabel": "索引模式", - "data.search.searchSource.noSearchStrategyRegisteredErrorMessageDescription": "无法为该搜索请求找到搜索策略", - "data.search.searchSource.noSearchStrategyRegisteredErrorMessageTitle": "未注册任何搜索策略", "data.search.searchSource.queryTimeDescription": "处理查询所花费的时间。不包括发送请求或在浏览器中解析它的时间。", "data.search.searchSource.queryTimeLabel": "查询时间", "data.search.searchSource.queryTimeValue": "{queryTime}ms", @@ -2001,10 +2003,8 @@ "kbn.context.reloadPageDescription.selectValidAnchorDocumentTextMessage": "以选择有效地定位点文档。", "kbn.context.unableToLoadAnchorDocumentDescription": "无法加载该定位点文档", "kbn.context.unableToLoadDocumentDescription": "无法加载文档", - "kbn.dashboard.listing.table.descriptionColumnName": "描述", "kbn.dashboard.panel.unableToMigratePanelDataForSixOneZeroErrorMessage": "无法迁移用于“6.1.0”向后兼容的面板数据,面板不包含所需的列和/或行字段", "kbn.dashboard.panel.unableToMigratePanelDataForSixThreeZeroErrorMessage": "无法迁移用于“6.3.0”向后兼容的面板数据,面板不包含预期字段:{key}", - "kbn.dashboard.savedDashboardsTitle": "仪表板", "kbn.dashboardTitle": "仪表板", "kbn.devToolsTitle": "开发工具", "kbn.discover.backToTopLinkText": "返至顶部。", @@ -2245,7 +2245,6 @@ "kbn.management.editIndexPattern.scripted.table.nameHeader": "名称", "kbn.management.editIndexPattern.scripted.table.scriptDescription": "字段的脚本", "kbn.management.editIndexPattern.scripted.table.scriptHeader": "脚本", - "kbn.management.editIndexPattern.scripted.unknownModeErrorMessage": "未知 fieldSettings 模式 {mode}", "kbn.management.editIndexPattern.scriptedHeader": "脚本字段", "kbn.management.editIndexPattern.scriptedLabel": "可以在可视化中使用脚本字段,并在您的文档中显示它们。但是,您不能搜索脚本字段。", "kbn.management.editIndexPattern.setDefaultAria": "设置为默认索引", @@ -2274,9 +2273,6 @@ "kbn.management.editIndexPattern.timeFilterLabel.mappingAPILink": "映射 API", "kbn.management.editIndexPattern.timeFilterLabel.timeFilterDetail": "此页根据 Elasticsearch 的记录列出“{indexPatternTitle}”索引中的每个字段以及字段的关联核心类型。要更改字段类型,请使用 Elasticsearch", "kbn.management.editIndexPatternLiveRegionAriaLabel": "索引模式", - "kbn.management.indexPattern.confirmOverwriteButton": "覆盖", - "kbn.management.indexPattern.confirmOverwriteLabel": "确定要覆盖 “{title}”?", - "kbn.management.indexPattern.confirmOverwriteTitle": "覆盖“{type}”?", "kbn.management.indexPattern.goToPatternButtonLabel": "前往现有模式", "kbn.management.indexPattern.sectionsHeader": "索引模式", "kbn.management.indexPattern.titleExistsLabel": "具有标题 “{title}” 的索引模式已存在。", @@ -2300,166 +2296,128 @@ "kbn.management.landing.header": "Kibana {version} 管理", "kbn.management.landing.subhead": "管理您的索引、索引模式、已保存对象、Kibana 设置等等。", "kbn.management.landing.text": "应用的完整列表位于左侧菜单中。", - "kbn.management.objects.confirmModalOptions.deleteButtonLabel": "删除", - "kbn.management.objects.confirmModalOptions.modalDescription": "此操作会将对象从 Kibana 永久移除。", - "kbn.management.objects.confirmModalOptions.modalTitle": "删除“{title}”?", - "kbn.management.objects.deleteSavedObjectsConfirmModalDescription": "此操作将删除以下已保存对象:", - "kbn.management.objects.field.offLabel": "关闭", - "kbn.management.objects.field.onLabel": "开启", - "kbn.management.objects.objectsTable.deleteSavedObjectsConfirmModal.cancelButtonLabel": "取消", - "kbn.management.objects.objectsTable.deleteSavedObjectsConfirmModal.deleteButtonLabel": "删除", - "kbn.management.objects.objectsTable.deleteSavedObjectsConfirmModal.deleteProcessButtonLabel": "正在删除……", - "kbn.management.objects.objectsTable.deleteSavedObjectsConfirmModal.idColumnName": "ID", - "kbn.management.objects.objectsTable.deleteSavedObjectsConfirmModal.titleColumnName": "标题", - "kbn.management.objects.objectsTable.deleteSavedObjectsConfirmModal.typeColumnName": "类型", - "kbn.management.objects.objectsTable.deleteSavedObjectsConfirmModalTitle": "删除已保存对象", - "kbn.management.objects.objectsTable.export.dangerNotification": "无法生成报告", - "kbn.management.objects.objectsTable.export.successNotification": "您的文件正在后台下载", - "kbn.management.objects.objectsTable.export.successWithMissingRefsNotification": "您的文件正在后台下载。找不到某些相关对象。有关缺失对象列表,请查看导出文件的最后一行。", - "kbn.management.objects.objectsTable.exportObjectsConfirmModal.cancelButtonLabel": "取消", - "kbn.management.objects.objectsTable.exportObjectsConfirmModal.exportAllButtonLabel": "全部导出", - "kbn.management.objects.objectsTable.exportObjectsConfirmModal.exportOptionsLabel": "选项", - "kbn.management.objects.objectsTable.exportObjectsConfirmModal.includeReferencesDeepLabel": "包括相关对象", - "kbn.management.objects.objectsTable.exportObjectsConfirmModalDescription": "选择要导出的类型。", - "kbn.management.objects.objectsTable.exportObjectsConfirmModalTitle": "导出 {filteredItemCount, plural, one{# 个对象} other {# 个对象}}", - "kbn.management.objects.objectsTable.flyout.confirmLegacyImport.resolvingConflictsLoadingMessage": "正在解决冲突……", - "kbn.management.objects.objectsTable.flyout.confirmLegacyImport.retryingFailedObjectsLoadingMessage": "正在重试失败的对象……", - "kbn.management.objects.objectsTable.flyout.confirmLegacyImport.savedSearchAreLinkedProperlyLoadingMessage": "确保已保存搜索已正确链接……", - "kbn.management.objects.objectsTable.flyout.confirmLegacyImport.savingConflictsLoadingMessage": "正在保存冲突……", - "kbn.management.objects.objectsTable.flyout.confirmOverwriteBody": "确定要覆盖 “{title}”?", - "kbn.management.objects.objectsTable.flyout.confirmOverwriteCancelButtonText": "取消", - "kbn.management.objects.objectsTable.flyout.confirmOverwriteOverwriteButtonText": "覆盖", - "kbn.management.objects.objectsTable.flyout.confirmOverwriteTitle": "覆盖“{type}”?", - "kbn.management.objects.objectsTable.flyout.errorCalloutTitle": "抱歉,出现了错误", - "kbn.management.objects.objectsTable.flyout.import.cancelButtonLabel": "取消", - "kbn.management.objects.objectsTable.flyout.import.confirmButtonLabel": "导入", - "kbn.management.objects.objectsTable.flyout.importFailedDescription": "无法导入 {failedImportCount} 个对象,共 {totalImportCount} 个。导入失败", - "kbn.management.objects.objectsTable.flyout.importFailedMissingReference": "{type} [id={id}] 无法找到 {refType} [id={refId}]", - "kbn.management.objects.objectsTable.flyout.importFailedTitle": "导入失败", - "kbn.management.objects.objectsTable.flyout.importFailedUnsupportedType": "{type} [id={id}] 不受支持的类型", - "kbn.management.objects.objectsTable.flyout.importFileErrorMessage": "无法处理该文件。", - "kbn.management.objects.objectsTable.flyout.importLegacyFileErrorMessage": "无法处理该文件。", - "kbn.management.objects.objectsTable.flyout.importPromptText": "导入", - "kbn.management.objects.objectsTable.flyout.importSavedObjectTitle": "导入已保存对象", - "kbn.management.objects.objectsTable.flyout.importSuccessful.confirmAllChangesButtonLabel": "确认所有更改", - "kbn.management.objects.objectsTable.flyout.importSuccessful.confirmButtonLabel": "完成", - "kbn.management.objects.objectsTable.flyout.importSuccessfulCallout.noObjectsImportedTitle": "未导入任何对象", - "kbn.management.objects.objectsTable.flyout.importSuccessfulDescription": "已成功导入 {importCount} 个对象。", - "kbn.management.objects.objectsTable.flyout.importSuccessfulTitle": "导入成功", - "kbn.management.objects.objectsTable.flyout.indexPatternConflictsCalloutLinkText": "创建新的索引模式", - "kbn.management.objects.objectsTable.flyout.indexPatternConflictsDescription": "以下已保存对象使用不存在的索引模式。请选择要重新关联的索引模式。必要时,您可以{indexPatternLink}。", - "kbn.management.objects.objectsTable.flyout.indexPatternConflictsTitle": "索引模式冲突", - "kbn.management.objects.objectsTable.flyout.invalidFormatOfImportedFileErrorMessage": "已保存对象文件格式无效,无法导入。", - "kbn.management.objects.objectsTable.flyout.legacyFileUsedBody": "只需使用更新的导出功能生成 NDJSON 文件,便万事俱备。", - "kbn.management.objects.objectsTable.flyout.legacyFileUsedTitle": "将不再支持 JSON 文件", - "kbn.management.objects.objectsTable.flyout.overwriteSavedObjectsLabel": "自动覆盖所有已保存对象?", - "kbn.management.objects.objectsTable.flyout.renderConflicts.columnCountDescription": "受影响对象数目", - "kbn.management.objects.objectsTable.flyout.renderConflicts.columnCountName": "计数", - "kbn.management.objects.objectsTable.flyout.renderConflicts.columnIdDescription": "索引模式的 ID", - "kbn.management.objects.objectsTable.flyout.renderConflicts.columnIdName": "ID", - "kbn.management.objects.objectsTable.flyout.renderConflicts.columnNewIndexPatternName": "新建索引模式", - "kbn.management.objects.objectsTable.flyout.renderConflicts.columnSampleOfAffectedObjectsDescription": "受影响对象样例", - "kbn.management.objects.objectsTable.flyout.renderConflicts.columnSampleOfAffectedObjectsName": "受影响对象样例", - "kbn.management.objects.objectsTable.flyout.resolveImportErrorsFileErrorMessage": "无法处理该文件。", - "kbn.management.objects.objectsTable.flyout.selectFileToImportFormRowLabel": "请选择要导入的文件", - "kbn.management.objects.objectsTable.header.exportButtonLabel": "导出 {filteredCount, plural, one{# 个对象} other {# 个对象}}", - "kbn.management.objects.objectsTable.header.importButtonLabel": "导入", - "kbn.management.objects.objectsTable.header.refreshButtonLabel": "刷新", - "kbn.management.objects.objectsTable.header.savedObjectsTitle": "已保存对象", - "kbn.management.objects.objectsTable.howToDeleteSavedObjectsDescription": "从这里您可以删除已保存对象,如已保存搜索。还可以编辑已保存对象的原始数据。通常,对象只能通过其关联的应用程序进行修改;或许您应该遵循这一原则,而非使用此屏幕进行修改。", - "kbn.management.objects.objectsTable.relationships.columnActions.inspectActionDescription": "检查此已保存对象", - "kbn.management.objects.objectsTable.relationships.columnActions.inspectActionName": "检查", - "kbn.management.objects.objectsTable.relationships.columnActionsName": "操作", - "kbn.management.objects.objectsTable.relationships.columnRelationship.childAsValue": "子项", - "kbn.management.objects.objectsTable.relationships.columnRelationship.parentAsValue": "父项", - "kbn.management.objects.objectsTable.relationships.columnRelationshipName": "直接关系", - "kbn.management.objects.objectsTable.relationships.columnTitleDescription": "已保存对象的标题", - "kbn.management.objects.objectsTable.relationships.columnTitleName": "标题", - "kbn.management.objects.objectsTable.relationships.columnTypeDescription": "已保存对象的类型", - "kbn.management.objects.objectsTable.relationships.columnTypeName": "类型", - "kbn.management.objects.objectsTable.relationships.relationshipsTitle": "以下是与 {title} 相关的已保存对象。删除此{type}将影响其父级对象,但不会影响其子级对象。", - "kbn.management.objects.objectsTable.relationships.renderErrorMessage": "错误", - "kbn.management.objects.objectsTable.relationships.search.filters.relationship.childAsValue.view": "子项", - "kbn.management.objects.objectsTable.relationships.search.filters.relationship.name": "直接关系", - "kbn.management.objects.objectsTable.relationships.search.filters.relationship.parentAsValue.view": "父项", - "kbn.management.objects.objectsTable.relationships.search.filters.type.name": "类型", - "kbn.management.objects.objectsTable.searchBar.unableToParseQueryErrorMessage": "无法解析查询", - "kbn.management.objects.objectsTable.table.columnActions.inspectActionDescription": "检查此已保存对象", - "kbn.management.objects.objectsTable.table.columnActions.inspectActionName": "检查", - "kbn.management.objects.objectsTable.table.columnActions.viewRelationshipsActionDescription": "查看此已保存对象与其他已保存对象的关系", - "kbn.management.objects.objectsTable.table.columnActions.viewRelationshipsActionName": "关系", - "kbn.management.objects.objectsTable.table.columnActionsName": "操作", - "kbn.management.objects.objectsTable.table.columnTitleDescription": "已保存对象的标题", - "kbn.management.objects.objectsTable.table.columnTitleName": "标题", - "kbn.management.objects.objectsTable.table.columnTypeDescription": "已保存对象的类型", - "kbn.management.objects.objectsTable.table.columnTypeName": "类型", - "kbn.management.objects.objectsTable.table.deleteButtonLabel": "删除", - "kbn.management.objects.objectsTable.table.deleteButtonTitle": "无法删除已保存对象", - "kbn.management.objects.objectsTable.table.exportButtonLabel": "导出", - "kbn.management.objects.objectsTable.table.exportPopoverButtonLabel": "导出", - "kbn.management.objects.objectsTable.table.typeFilterName": "类型", - "kbn.management.objects.objectsTable.unableFindSavedObjectsNotificationMessage": "找不到已保存对象", - "kbn.management.objects.parsingFieldErrorMessage": "为索引模式 “{indexName}” 解析 “{fieldName}” 时发生错误:{errorMessage}", - "kbn.management.objects.savedObjectsSectionLabel": "已保存对象", - "kbn.management.objects.view.cancelButtonAriaLabel": "取消", - "kbn.management.objects.view.cancelButtonLabel": "取消", - "kbn.management.objects.view.deleteItemButtonLabel": "删除“{title}”", - "kbn.management.objects.view.editItemTitle": "编辑“{title}", - "kbn.management.objects.view.fieldDoesNotExistErrorMessage": "与此对象关联的字段在该索引模式中已不存在。", - "kbn.management.objects.view.howToFixErrorDescription": "如果您清楚此错误的含义,请修复该错误 — 否则单击上面的删除按钮。", - "kbn.management.objects.view.howToModifyObjectDescription": "修改对象仅适用于高级用户。对象属性未得到验证,无效的对象可能会导致错误、数据丢失或更坏的情况发生。除非熟悉该代码的人让您来这里,否则您可能不应该来这里。", - "kbn.management.objects.view.howToModifyObjectTitle": "谨慎操作!", - "kbn.management.objects.view.indexPatternDoesNotExistErrorMessage": "与此对象关联的索引模式已不存在。", - "kbn.management.objects.view.saveButtonAriaLabel": "保存 { title } 对象", - "kbn.management.objects.view.saveButtonLabel": "保存 { title } 对象", - "kbn.management.objects.view.savedObjectProblemErrorMessage": "此已保存对象有问题", - "kbn.management.objects.view.savedSearchDoesNotExistErrorMessage": "与此对象关联的已保存搜索已不存在。", - "kbn.management.objects.view.viewItemButtonLabel": "查看“{title}”", - "kbn.management.objects.view.viewItemTitle": "查看“{title}”", - "kbn.management.savedObjects.editBreadcrumb": "编辑 {savedObjectType}", - "kbn.management.savedObjects.indexBreadcrumb": "已保存对象", + "savedObjectsManagement.indexPattern.confirmOverwriteButton": "覆盖", + "savedObjectsManagement.indexPattern.confirmOverwriteLabel": "确定要覆盖 “{title}”?", + "savedObjectsManagement.indexPattern.confirmOverwriteTitle": "覆盖“{type}”?", + "savedObjectsManagement.deleteConfirm.modalDeleteButtonLabel": "删除", + "savedObjectsManagement.deleteConfirm.modalDescription": "此操作会将对象从 Kibana 永久移除。", + "savedObjectsManagement.deleteConfirm.modalTitle": "删除“{title}”?", + "savedObjectsManagement.deleteSavedObjectsConfirmModalDescription": "此操作将删除以下已保存对象:", + "savedObjectsManagement.field.offLabel": "关闭", + "savedObjectsManagement.field.onLabel": "开启", + "savedObjectsManagement.objectsTable.deleteSavedObjectsConfirmModal.cancelButtonLabel": "取消", + "savedObjectsManagement.objectsTable.deleteSavedObjectsConfirmModal.deleteButtonLabel": "删除", + "savedObjectsManagement.objectsTable.deleteSavedObjectsConfirmModal.deleteProcessButtonLabel": "正在删除……", + "savedObjectsManagement.objectsTable.deleteSavedObjectsConfirmModal.idColumnName": "ID", + "savedObjectsManagement.objectsTable.deleteSavedObjectsConfirmModal.titleColumnName": "标题", + "savedObjectsManagement.objectsTable.deleteSavedObjectsConfirmModal.typeColumnName": "类型", + "savedObjectsManagement.objectsTable.deleteSavedObjectsConfirmModalTitle": "删除已保存对象", + "savedObjectsManagement.objectsTable.export.dangerNotification": "无法生成报告", + "savedObjectsManagement.objectsTable.export.successNotification": "您的文件正在后台下载", + "savedObjectsManagement.objectsTable.export.successWithMissingRefsNotification": "您的文件正在后台下载。找不到某些相关对象。有关缺失对象列表,请查看导出文件的最后一行。", + "savedObjectsManagement.objectsTable.exportObjectsConfirmModal.cancelButtonLabel": "取消", + "savedObjectsManagement.objectsTable.exportObjectsConfirmModal.exportAllButtonLabel": "全部导出", + "savedObjectsManagement.objectsTable.exportObjectsConfirmModal.exportOptionsLabel": "选项", + "savedObjectsManagement.objectsTable.exportObjectsConfirmModal.includeReferencesDeepLabel": "包括相关对象", + "savedObjectsManagement.objectsTable.exportObjectsConfirmModalDescription": "选择要导出的类型。", + "savedObjectsManagement.objectsTable.exportObjectsConfirmModalTitle": "导出 {filteredItemCount, plural, one{# 个对象} other {# 个对象}}", + "savedObjectsManagement.objectsTable.flyout.confirmLegacyImport.resolvingConflictsLoadingMessage": "正在解决冲突……", + "savedObjectsManagement.objectsTable.flyout.confirmLegacyImport.retryingFailedObjectsLoadingMessage": "正在重试失败的对象……", + "savedObjectsManagement.objectsTable.flyout.confirmLegacyImport.savedSearchAreLinkedProperlyLoadingMessage": "确保已保存搜索已正确链接……", + "savedObjectsManagement.objectsTable.flyout.confirmLegacyImport.savingConflictsLoadingMessage": "正在保存冲突……", + "savedObjectsManagement.objectsTable.flyout.confirmOverwriteBody": "确定要覆盖 “{title}”?", + "savedObjectsManagement.objectsTable.flyout.confirmOverwriteCancelButtonText": "取消", + "savedObjectsManagement.objectsTable.flyout.confirmOverwriteOverwriteButtonText": "覆盖", + "savedObjectsManagement.objectsTable.flyout.confirmOverwriteTitle": "覆盖“{type}”?", + "savedObjectsManagement.objectsTable.flyout.errorCalloutTitle": "抱歉,出现了错误", + "savedObjectsManagement.objectsTable.flyout.import.cancelButtonLabel": "取消", + "savedObjectsManagement.objectsTable.flyout.import.confirmButtonLabel": "导入", + "savedObjectsManagement.objectsTable.flyout.importFailedDescription": "无法导入 {failedImportCount} 个对象,共 {totalImportCount} 个。导入失败", + "savedObjectsManagement.objectsTable.flyout.importFailedMissingReference": "{type} [id={id}] 无法找到 {refType} [id={refId}]", + "savedObjectsManagement.objectsTable.flyout.importFailedTitle": "导入失败", + "savedObjectsManagement.objectsTable.flyout.importFailedUnsupportedType": "{type} [id={id}] 不受支持的类型", + "savedObjectsManagement.objectsTable.flyout.importFileErrorMessage": "无法处理该文件。", + "savedObjectsManagement.objectsTable.flyout.importLegacyFileErrorMessage": "无法处理该文件。", + "savedObjectsManagement.objectsTable.flyout.importPromptText": "导入", + "savedObjectsManagement.objectsTable.flyout.importSavedObjectTitle": "导入已保存对象", + "savedObjectsManagement.objectsTable.flyout.importSuccessful.confirmAllChangesButtonLabel": "确认所有更改", + "savedObjectsManagement.objectsTable.flyout.importSuccessful.confirmButtonLabel": "完成", + "savedObjectsManagement.objectsTable.flyout.importSuccessfulCallout.noObjectsImportedTitle": "未导入任何对象", + "savedObjectsManagement.objectsTable.flyout.importSuccessfulDescription": "已成功导入 {importCount} 个对象。", + "savedObjectsManagement.objectsTable.flyout.importSuccessfulTitle": "导入成功", + "savedObjectsManagement.objectsTable.flyout.indexPatternConflictsCalloutLinkText": "创建新的索引模式", + "savedObjectsManagement.objectsTable.flyout.indexPatternConflictsDescription": "以下已保存对象使用不存在的索引模式。请选择要重新关联的索引模式。必要时,您可以{indexPatternLink}。", + "savedObjectsManagement.objectsTable.flyout.indexPatternConflictsTitle": "索引模式冲突", + "savedObjectsManagement.objectsTable.flyout.invalidFormatOfImportedFileErrorMessage": "已保存对象文件格式无效,无法导入。", + "savedObjectsManagement.objectsTable.flyout.legacyFileUsedBody": "只需使用更新的导出功能生成 NDJSON 文件,便万事俱备。", + "savedObjectsManagement.objectsTable.flyout.legacyFileUsedTitle": "将不再支持 JSON 文件", + "savedObjectsManagement.objectsTable.flyout.overwriteSavedObjectsLabel": "自动覆盖所有已保存对象?", + "savedObjectsManagement.objectsTable.flyout.renderConflicts.columnCountDescription": "受影响对象数目", + "savedObjectsManagement.objectsTable.flyout.renderConflicts.columnCountName": "计数", + "savedObjectsManagement.objectsTable.flyout.renderConflicts.columnIdDescription": "索引模式的 ID", + "savedObjectsManagement.objectsTable.flyout.renderConflicts.columnIdName": "ID", + "savedObjectsManagement.objectsTable.flyout.renderConflicts.columnNewIndexPatternName": "新建索引模式", + "savedObjectsManagement.objectsTable.flyout.renderConflicts.columnSampleOfAffectedObjectsDescription": "受影响对象样例", + "savedObjectsManagement.objectsTable.flyout.renderConflicts.columnSampleOfAffectedObjectsName": "受影响对象样例", + "savedObjectsManagement.objectsTable.flyout.resolveImportErrorsFileErrorMessage": "无法处理该文件。", + "savedObjectsManagement.objectsTable.flyout.selectFileToImportFormRowLabel": "请选择要导入的文件", + "savedObjectsManagement.objectsTable.header.exportButtonLabel": "导出 {filteredCount, plural, one{# 个对象} other {# 个对象}}", + "savedObjectsManagement.objectsTable.header.importButtonLabel": "导入", + "savedObjectsManagement.objectsTable.header.refreshButtonLabel": "刷新", + "savedObjectsManagement.objectsTable.header.savedObjectsTitle": "已保存对象", + "savedObjectsManagement.objectsTable.howToDeleteSavedObjectsDescription": "从这里您可以删除已保存对象,如已保存搜索。还可以编辑已保存对象的原始数据。通常,对象只能通过其关联的应用程序进行修改;或许您应该遵循这一原则,而非使用此屏幕进行修改。", + "savedObjectsManagement.objectsTable.relationships.columnActions.inspectActionDescription": "检查此已保存对象", + "savedObjectsManagement.objectsTable.relationships.columnActions.inspectActionName": "检查", + "savedObjectsManagement.objectsTable.relationships.columnActionsName": "操作", + "savedObjectsManagement.objectsTable.relationships.columnRelationship.childAsValue": "子项", + "savedObjectsManagement.objectsTable.relationships.columnRelationship.parentAsValue": "父项", + "savedObjectsManagement.objectsTable.relationships.columnRelationshipName": "直接关系", + "savedObjectsManagement.objectsTable.relationships.columnTitleDescription": "已保存对象的标题", + "savedObjectsManagement.objectsTable.relationships.columnTitleName": "标题", + "savedObjectsManagement.objectsTable.relationships.columnTypeDescription": "已保存对象的类型", + "savedObjectsManagement.objectsTable.relationships.columnTypeName": "类型", + "savedObjectsManagement.objectsTable.relationships.relationshipsTitle": "以下是与 {title} 相关的已保存对象。删除此{type}将影响其父级对象,但不会影响其子级对象。", + "savedObjectsManagement.objectsTable.relationships.renderErrorMessage": "错误", + "savedObjectsManagement.objectsTable.relationships.search.filters.relationship.childAsValue.view": "子项", + "savedObjectsManagement.objectsTable.relationships.search.filters.relationship.name": "直接关系", + "savedObjectsManagement.objectsTable.relationships.search.filters.relationship.parentAsValue.view": "父项", + "savedObjectsManagement.objectsTable.relationships.search.filters.type.name": "类型", + "savedObjectsManagement.objectsTable.searchBar.unableToParseQueryErrorMessage": "无法解析查询", + "savedObjectsManagement.objectsTable.table.columnActions.inspectActionDescription": "检查此已保存对象", + "savedObjectsManagement.objectsTable.table.columnActions.inspectActionName": "检查", + "savedObjectsManagement.objectsTable.table.columnActions.viewRelationshipsActionDescription": "查看此已保存对象与其他已保存对象的关系", + "savedObjectsManagement.objectsTable.table.columnActions.viewRelationshipsActionName": "关系", + "savedObjectsManagement.objectsTable.table.columnActionsName": "操作", + "savedObjectsManagement.objectsTable.table.columnTitleDescription": "已保存对象的标题", + "savedObjectsManagement.objectsTable.table.columnTitleName": "标题", + "savedObjectsManagement.objectsTable.table.columnTypeDescription": "已保存对象的类型", + "savedObjectsManagement.objectsTable.table.columnTypeName": "类型", + "savedObjectsManagement.objectsTable.table.deleteButtonLabel": "删除", + "savedObjectsManagement.objectsTable.table.deleteButtonTitle": "无法删除已保存对象", + "savedObjectsManagement.objectsTable.table.exportButtonLabel": "导出", + "savedObjectsManagement.objectsTable.table.exportPopoverButtonLabel": "导出", + "savedObjectsManagement.objectsTable.table.typeFilterName": "类型", + "savedObjectsManagement.objectsTable.unableFindSavedObjectsNotificationMessage": "找不到已保存对象", + "savedObjectsManagement.parsingFieldErrorMessage": "为索引模式 “{indexName}” 解析 “{fieldName}” 时发生错误:{errorMessage}", + "savedObjectsManagement.managementSectionLabel": "已保存对象", + "savedObjectsManagement.view.cancelButtonAriaLabel": "取消", + "savedObjectsManagement.view.cancelButtonLabel": "取消", + "savedObjectsManagement.view.deleteItemButtonLabel": "删除“{title}”", + "savedObjectsManagement.view.editItemTitle": "编辑“{title}", + "savedObjectsManagement.view.fieldDoesNotExistErrorMessage": "与此对象关联的字段在该索引模式中已不存在。", + "savedObjectsManagement.view.howToFixErrorDescription": "如果您清楚此错误的含义,请修复该错误 — 否则单击上面的删除按钮。", + "savedObjectsManagement.view.howToModifyObjectDescription": " 修改对象仅适用于高级用户。对象属性未得到验证,无效的对象可能会导致错误、数据丢失或更坏的情况发生。除非熟悉该代码的人让您来这里,否则您可能不应该来这里。", + "savedObjectsManagement.view.howToModifyObjectTitle": "谨慎操作!", + "savedObjectsManagement.view.indexPatternDoesNotExistErrorMessage": "与此对象关联的索引模式已不存在。", + "savedObjectsManagement.view.saveButtonAriaLabel": "保存 { title } 对象", + "savedObjectsManagement.view.saveButtonLabel": "保存 { title } 对象", + "savedObjectsManagement.view.savedObjectProblemErrorMessage": "此已保存对象有问题", + "savedObjectsManagement.view.savedSearchDoesNotExistErrorMessage": "与此对象关联的已保存搜索已不存在。", + "savedObjectsManagement.view.viewItemButtonLabel": "查看“{title}”", + "savedObjectsManagement.view.viewItemTitle": "查看“{title}”", + "savedObjectsManagement.breadcrumb.edit": "编辑 {savedObjectType}", + "savedObjectsManagement.breadcrumb.index": "已保存对象", "kbn.managementTitle": "管理", - "kbn.topNavMenu.openInspectorButtonLabel": "检查", - "kbn.topNavMenu.refreshButtonLabel": "刷新", - "kbn.topNavMenu.saveVisualizationButtonLabel": "保存", - "kbn.topNavMenu.shareVisualizationButtonLabel": "共享", - "kbn.visualize.badge.readOnly.text": "只读", - "kbn.visualize.badge.readOnly.tooltip": "无法保存可视化", - "kbn.visualize.createVisualization.noIndexPatternOrSavedSearchIdErrorMessage": "必须提供 indexPattern 或 savedSearchId", - "kbn.visualize.editor.createBreadcrumb": "创建", - "kbn.visualize.experimentalVisInfoText": "此可视化标记为“实验”。", - "kbn.visualize.helpMenu.appName": "Visualize", - "kbn.visualize.linkedToSearch.unlinkSuccessNotificationText": "取消与已保存搜索 “{searchTitle}” 的链接", - "kbn.visualize.listing.betaTitle": "公测版", - "kbn.visualize.listing.betaTooltip": "此可视化为公测版,可能会进行更改。设计和代码相对于正式发行版功能还不够成熟,将按原样提供,且不提供任何保证。公测版功能不受正式发行版功能支持 SLA 的约束", - "kbn.visualize.listing.breadcrumb": "可视化", - "kbn.visualize.listing.createNew.createButtonLabel": "新建可视化", - "kbn.visualize.listing.createNew.description": "可以根据您的数据创建不同的可视化。", - "kbn.visualize.listing.createNew.title": "创建首个可视化", - "kbn.visualize.listing.experimentalTitle": "实验性", - "kbn.visualize.listing.experimentalTooltip": "未来版本可能会更改或删除此可视化,其不受支持 SLA 的约束。", - "kbn.visualize.listing.noItemsMessage": "看起来您还没有任何可视化。", - "kbn.visualize.listing.table.entityName": "可视化", - "kbn.visualize.listing.table.entityNamePlural": "可视化", - "kbn.visualize.listing.table.listTitle": "可视化", - "kbn.visualize.listing.table.titleColumnName": "标题", - "kbn.visualize.listing.table.typeColumnName": "类型", - "kbn.visualize.pageHeading": "{chartName} {chartType}可视化", - "kbn.visualize.saveDialog.saveAndAddToDashboardButtonLabel": "保存并添加到仪表板", - "kbn.visualize.topNavMenu.openInspectorButtonAriaLabel": "打开检查器查看可视化", - "kbn.visualize.topNavMenu.openInspectorDisabledButtonTooltip": "此可视化不支持任何检查器。", - "kbn.visualize.topNavMenu.refreshButtonAriaLabel": "刷新", - "kbn.visualize.topNavMenu.saveVisualization.failureNotificationText": "保存 “{visTitle}” 时出错", - "kbn.visualize.topNavMenu.saveVisualization.successNotificationText": "已保存“{visTitle}”", - "kbn.visualize.topNavMenu.saveVisualizationButtonAriaLabel": "保存可视化", - "kbn.visualize.topNavMenu.saveVisualizationDisabledButtonTooltip": "应用或放弃所做更改,然后保存", - "kbn.visualize.topNavMenu.shareVisualizationButtonAriaLabel": "共享可视化", - "kbn.visualize.visualizationTypeInvalidNotificationMessage": "无效的可视化类型", - "kbn.visualize.visualizeDescription": "创建可视化并聚合存储在 Elasticsearch 索引中的数据。", - "kbn.visualize.visualizeListingBreadcrumbsTitle": "可视化", - "kbn.visualize.visualizeListingDeleteErrorTitle": "删除可视化时出错", - "kbn.visualize.wizard.step1Breadcrumb": "创建", - "kbn.visualize.wizard.step2Breadcrumb": "创建", "kbn.visualizeTitle": "可视化", "kibana_legacy.bigUrlWarningNotificationMessage": "在{advancedSettingsLink}中启用“{storeInSessionStorageParam}”选项或简化屏幕视觉效果。", "kibana_legacy.bigUrlWarningNotificationMessage.advancedSettingsLinkText": "高级设置", @@ -2504,13 +2462,13 @@ "management.breadcrumb": "管理", "management.connectDataDisplayName": "连接数据", "management.displayName": "管理", + "management.nav.label": "管理", + "management.nav.menu": "管理菜单", + "management.stackManagement.managementDescription": "您用于管理 Elastic Stack 的中心控制台。", "indexPatternManagement.editIndexPattern.createIndex.defaultButtonDescription": "对任何数据执行完全聚合", "indexPatternManagement.editIndexPattern.createIndex.defaultButtonText": "标准索引模式", "indexPatternManagement.editIndexPattern.createIndex.defaultTypeName": "索引模式", "indexPatternManagement.editIndexPattern.list.defaultIndexPatternListName": "默认值", - "management.nav.label": "管理", - "management.nav.menu": "管理菜单", - "management.stackManagement.managementDescription": "您用于管理 Elastic Stack 的中心控制台。", "newsfeed.emptyPrompt.noNewsText": "如果您的 Kibana 实例没有 Internet 连接,请让您的管理员禁用此功能。否则,我们将不断尝试获取新闻。", "newsfeed.emptyPrompt.noNewsTitle": "无新闻?", "newsfeed.flyoutList.closeButtonLabel": "鍏抽棴", @@ -3832,11 +3790,6 @@ "visTypeVislib.chartTypes.areaText": "面积图", "visTypeVislib.chartTypes.barText": "条形图", "visTypeVislib.chartTypes.lineText": "折线图", - "visTypeVislib.controls.colorRanges.errorText": "每个范围应大于前一范围。", - "visTypeVislib.controls.colorSchema.colorSchemaLabel": "颜色模式", - "visTypeVislib.controls.colorSchema.howToChangeColorsDescription": "可以更改图例中的各个颜色。", - "visTypeVislib.controls.colorSchema.resetColorsButtonLabel": "重置颜色", - "visTypeVislib.controls.colorSchema.reverseColorSchemaLabel": "反转模式", "visTypeVislib.controls.gaugeOptions.alignmentLabel": "对齐方式", "visTypeVislib.controls.gaugeOptions.autoExtendRangeLabel": "自动扩展范围", "visTypeVislib.controls.gaugeOptions.displayWarningsLabel": "显示警告", @@ -3903,10 +3856,7 @@ "visTypeVislib.controls.pointSeries.valueAxes.toggleCustomExtendsAriaLabel": "切换定制范围", "visTypeVislib.controls.pointSeries.valueAxes.toggleOptionsAriaLabel": "切换 {axisName} 选项", "visTypeVislib.controls.pointSeries.valueAxes.yAxisTitle": "Y 轴", - "visTypeVislib.controls.rangeErrorMessage": "值必须是在 {min} 到 {max} 的范围内", "visTypeVislib.controls.truncateLabel": "截断", - "visTypeVislib.controls.vislibBasicOptions.legendPositionLabel": "图例位置", - "visTypeVislib.controls.vislibBasicOptions.showTooltipLabel": "显示工具提示", "visTypeVislib.editors.heatmap.basicSettingsTitle": "基本设置", "visTypeVislib.editors.heatmap.heatmapSettingsTitle": "热图设置", "visTypeVislib.editors.heatmap.highlightLabel": "高亮范围", @@ -4029,6 +3979,46 @@ "visualizations.newVisWizard.visTypeAliasDescription": "打开 Visualize 外部的 Kibana 应用程序。", "visualizations.newVisWizard.visTypeAliasTitle": "Kibana 应用程序", "visualizations.savedObjectName": "可视化", + "visualize.listing.table.descriptionColumnName": "描述", + "visualize.topNavMenu.openInspectorButtonLabel": "检查", + "visualize.topNavMenu.saveVisualizationButtonLabel": "保存", + "visualize.topNavMenu.shareVisualizationButtonLabel": "共享", + "visualize.badge.readOnly.text": "只读", + "visualize.badge.readOnly.tooltip": "无法保存可视化", + "visualize.createVisualization.noIndexPatternOrSavedSearchIdErrorMessage": "必须提供 indexPattern 或 savedSearchId", + "visualize.editor.createBreadcrumb": "创建", + "visualize.experimentalVisInfoText": "此可视化标记为“实验”。", + "visualize.helpMenu.appName": "Visualize", + "visualize.linkedToSearch.unlinkSuccessNotificationText": "取消与已保存搜索 “{searchTitle}” 的链接", + "visualize.listing.betaTitle": "公测版", + "visualize.listing.betaTooltip": "此可视化为公测版,可能会进行更改。设计和代码相对于正式发行版功能还不够成熟,将按原样提供,且不提供任何保证。公测版功能不受正式发行版功能支持 SLA 的约束", + "visualize.listing.breadcrumb": "可视化", + "visualize.listing.createNew.createButtonLabel": "新建可视化", + "visualize.listing.createNew.description": "可以根据您的数据创建不同的可视化。", + "visualize.listing.createNew.title": "创建首个可视化", + "visualize.listing.experimentalTitle": "实验性", + "visualize.listing.experimentalTooltip": "未来版本可能会更改或删除此可视化,其不受支持 SLA 的约束。", + "visualize.listing.noItemsMessage": "看起来您还没有任何可视化。", + "visualize.listing.table.entityName": "可视化", + "visualize.listing.table.entityNamePlural": "可视化", + "visualize.listing.table.listTitle": "可视化", + "visualize.listing.table.titleColumnName": "标题", + "visualize.listing.table.typeColumnName": "类型", + "visualize.pageHeading": "{chartName} {chartType}可视化", + "visualize.saveDialog.saveAndAddToDashboardButtonLabel": "保存并添加到仪表板", + "visualize.topNavMenu.openInspectorButtonAriaLabel": "打开检查器查看可视化", + "visualize.topNavMenu.openInspectorDisabledButtonTooltip": "此可视化不支持任何检查器。", + "visualize.topNavMenu.saveVisualization.failureNotificationText": "保存 “{visTitle}” 时出错", + "visualize.topNavMenu.saveVisualization.successNotificationText": "已保存“{visTitle}”", + "visualize.topNavMenu.saveVisualizationButtonAriaLabel": "保存可视化", + "visualize.topNavMenu.saveVisualizationDisabledButtonTooltip": "应用或放弃所做更改,然后保存", + "visualize.topNavMenu.shareVisualizationButtonAriaLabel": "共享可视化", + "visualize.visualizationTypeInvalidNotificationMessage": "无效的可视化类型", + "visualize.visualizeDescription": "创建可视化并聚合存储在 Elasticsearch 索引中的数据。", + "visualize.visualizeListingBreadcrumbsTitle": "可视化", + "visualize.visualizeListingDeleteErrorTitle": "删除可视化时出错", + "visualize.wizard.step1Breadcrumb": "创建", + "visualize.wizard.step2Breadcrumb": "创建", "xpack.actions.actionTypeRegistry.get.missingActionTypeErrorMessage": "未注册操作类型“{id}”。", "xpack.actions.actionTypeRegistry.register.duplicateActionTypeErrorMessage": "操作类型“{id}”已注册。", "xpack.actions.appName": "操作", @@ -7462,9 +7452,6 @@ "xpack.indexLifecycleMgmt.activePhaseMessage": "有效", "xpack.indexLifecycleMgmt.addLifecyclePolicyActionButtonLabel": "添加生命周期策略", "xpack.indexLifecycleMgmt.appTitle": "索引生命周期策略", - "xpack.indexLifecycleMgmt.checkLicense.errorExpiredMessage": "您不能使用 {pluginName},因为您的 {licenseType} 许可已过期。", - "xpack.indexLifecycleMgmt.checkLicense.errorUnavailableMessage": "您不能使用 {pluginName},因为许可证信息当前不可用。", - "xpack.indexLifecycleMgmt.checkLicense.errorUnsupportedMessage": "您的 {licenseType} 许可证不支持 {pluginName}。请升级您的许可。", "xpack.indexLifecycleMgmt.coldPhase.freezeIndexLabel": "冻结索引", "xpack.indexLifecycleMgmt.coldPhase.numberOfReplicasLabel": "副本分片数目", "xpack.indexLifecycleMgmt.coldPhase.replicaCountHelpText": "默认情况下,副本分片数目仍一样。", @@ -8290,7 +8277,6 @@ "xpack.ingestManager.agentConfigForm.systemMonitoringFieldLabel": "可选", "xpack.ingestManager.agentConfigForm.systemMonitoringText": "收集系统指标", "xpack.ingestManager.agentConfigForm.systemMonitoringTooltipText": "启用此选项可使用收集系统指标和信息的数据源启动您的配置。", - "xpack.ingestManager.agentConfigInfo.yamlTabName": "YAML", "xpack.ingestManager.agentConfigList.actionsColumnTitle": "操作", "xpack.ingestManager.agentConfigList.actionsMenuText": "打开", "xpack.ingestManager.agentConfigList.addButton": "创建代理配置", @@ -8428,21 +8414,14 @@ "xpack.ingestManager.createAgentConfig.flyoutTitleDescription": "代理配置用于管理整个代理组的设置。您可以将数据源添加到代理配置以指定代理收集的数据。编辑代理配置时,可以使用 Fleet 将更新部署到指定的代理组。", "xpack.ingestManager.createAgentConfig.submitButtonLabel": "创建代理配置", "xpack.ingestManager.createAgentConfig.successNotificationTitle": "代理配置“{name}”已创建", - "xpack.ingestManager.createDatasource.addDatasourceButtonText": "将数据源添加到配置", "xpack.ingestManager.createDatasource.agentConfigurationNameLabel": "配置", "xpack.ingestManager.createDatasource.cancelLinkText": "取消", - "xpack.ingestManager.createDatasource.changeConfigLinkText": "更改配置", - "xpack.ingestManager.createDatasource.changePackageLinkText": "更改软件包", - "xpack.ingestManager.createDatasource.continueButtonText": "继续", - "xpack.ingestManager.createDatasource.editDatasourceLinkText": "编辑数据源", "xpack.ingestManager.createDatasource.packageNameLabel": "软件包", "xpack.ingestManager.createDatasource.pageTitle": "创建数据源", "xpack.ingestManager.createDatasource.stepConfigure.advancedOptionsToggleLinkText": "高级选项", - "xpack.ingestManager.createDatasource.stepConfigure.chooseDataTitle": "选择要收集的数据", "xpack.ingestManager.createDatasource.stepConfigure.datasourceDescriptionInputLabel": "描述", "xpack.ingestManager.createDatasource.stepConfigure.datasourceNameInputLabel": "数据源名称", "xpack.ingestManager.createDatasource.stepConfigure.datasourceNamespaceInputLabel": "命名空间", - "xpack.ingestManager.createDatasource.stepConfigure.defineDatasourceTitle": "定义您的数据源", "xpack.ingestManager.createDatasource.stepConfigure.hideStreamsAriaLabel": "隐藏 {type} 流", "xpack.ingestManager.createDatasource.stepConfigure.inputSettingsDescription": "以下设置适用于所有流。", "xpack.ingestManager.createDatasource.stepConfigure.inputSettingsTitle": "设置", @@ -8451,27 +8430,15 @@ "xpack.ingestManager.createDatasource.stepConfigure.showStreamsAriaLabel": "显示 {type} 流", "xpack.ingestManager.createDatasource.stepConfigure.streamsEnabledCountText": "{count} / {total, plural, one {# 个流} other {# 个流}}已启用", "xpack.ingestManager.createDatasource.stepConfigure.toggleAdvancedOptionsButtonText": "高级选项", - "xpack.ingestManager.createDatasource.stepConfigureDatasourceLabel": "配置数据源", - "xpack.ingestManager.createDatasource.stepReview.agentsAffectedCalloutText": "Fleet 检测到所选代理配置 {configName} 已由您的部分代理使用。此操作的结果是,Fleet 将更新用此配置进行注册的所有代理。", - "xpack.ingestManager.createDatasource.stepReview.agentsAffectedCalloutTitle": "此操作将影响 {count, plural, one {# 个代理} other {# 个代理}}", - "xpack.ingestManager.createDatasource.stepReview.confirmAgentDisclaimerText": "我理解此操作将更新注册到此配置的所有代理。", - "xpack.ingestManager.createDatasource.stepReview.confirmAgentDisclaimerTitle": "确认您的决定", - "xpack.ingestManager.createDatasource.stepReview.reviewTitle": "复查更改", - "xpack.ingestManager.createDatasource.stepReviewLabel": "复查", "xpack.ingestManager.createDatasource.StepSelectConfig.agentConfigAgentsCountText": "{count, plural, one {# 个代理} other {# 个代理}}", - "xpack.ingestManager.createDatasource.StepSelectConfig.createNewConfigButtonText": "创建新配置", "xpack.ingestManager.createDatasource.StepSelectConfig.errorLoadingAgentConfigsTitle": "加载代理配置时出错", "xpack.ingestManager.createDatasource.StepSelectConfig.errorLoadingPackageTitle": "加载软件包信息时出错", "xpack.ingestManager.createDatasource.StepSelectConfig.errorLoadingSelectedAgentConfigTitle": "加载选定代理配置时出错", "xpack.ingestManager.createDatasource.StepSelectConfig.filterAgentConfigsInputPlaceholder": "搜索代理配置", - "xpack.ingestManager.createDatasource.StepSelectConfig.selectAgentConfigTitle": "选择代理配置", - "xpack.ingestManager.createDatasource.stepSelectConfigLabel": "选择配置", "xpack.ingestManager.createDatasource.stepSelectPackage.errorLoadingConfigTitle": "加载代理配置信息时出错", "xpack.ingestManager.createDatasource.stepSelectPackage.errorLoadingPackagesTitle": "加载软件包时出错", "xpack.ingestManager.createDatasource.stepSelectPackage.errorLoadingSelectedPackageTitle": "加载选定软件包时出错", "xpack.ingestManager.createDatasource.stepSelectPackage.filterPackagesInputPlaceholder": "搜索软件包", - "xpack.ingestManager.createDatasource.stepSelectPackage.selectPackageTitle": "选择软件包", - "xpack.ingestManager.createDatasource.stepSelectPackageLabel": "选择软件包", "xpack.ingestManager.deleteAgentConfigs.confirmModal.affectedAgentsMessage": "{agentsCount, plural, one {# 个代理} other {# 个代理}}已分配{agentConfigsCount, plural, one {给此代理配置} other {给这些代理配置}}。将取消注册{agentsCount, plural, one {此代理} other {这些代理}}。", "xpack.ingestManager.deleteAgentConfigs.confirmModal.cancelButtonLabel": "取消", "xpack.ingestManager.deleteAgentConfigs.confirmModal.confirmAndReassignButtonLabel": "删除{agentConfigsCount, plural, one {代理配置} other {代理配置}}并取消注册{agentsCount, plural, one {代理} other {代理}}", @@ -9513,8 +9480,6 @@ "xpack.ml.dataframe.analytics.classificationExploration.evaluateJobIdTitle": "分类作业 ID {jobId} 的评估", "xpack.ml.dataframe.analytics.classificationExploration.firstDocumentsShownHelpText": "正在显示有相关预测存在的前 {searchSize} 个文档", "xpack.ml.dataframe.analytics.classificationExploration.generalizationDocsCount": "{docsCount, plural, one {# 个文档} other {# 个文档}}已评估", - "xpack.ml.dataframe.analytics.classificationExploration.indexArrayBadgeContent": "数组", - "xpack.ml.dataframe.analytics.classificationExploration.indexArrayToolTipContent": "此基于数组的列的完整内容无法显示。", "xpack.ml.dataframe.analytics.classificationExploration.jobCapsFetchError": "无法提取结果。加载索引的字段数据时发生错误。", "xpack.ml.dataframe.analytics.classificationExploration.jobConfigurationFetchError": "无法提取结果。加载作业配置数据时发生错误。", "xpack.ml.dataframe.analytics.classificationExploration.jobConfigurationNoResultsMessage": "未找到结果。", @@ -9608,11 +9573,7 @@ "xpack.ml.dataframe.analytics.regressionExploration.generalError": "加载数据时出错。", "xpack.ml.dataframe.analytics.regressionExploration.generalizationDocsCount": "{docsCount, plural, one {# 个文档} other {# 个文档}}已评估", "xpack.ml.dataframe.analytics.regressionExploration.generalizationErrorTitle": "泛化误差", - "xpack.ml.dataframe.analytics.regressionExploration.indexArrayBadgeContent": "数组", - "xpack.ml.dataframe.analytics.regressionExploration.indexArrayToolTipContent": "此基于数组的列的完整内容无法显示。", "xpack.ml.dataframe.analytics.regressionExploration.indexError": "加载索引数据时出错。", - "xpack.ml.dataframe.analytics.regressionExploration.isTestingLabel": "测试", - "xpack.ml.dataframe.analytics.regressionExploration.isTrainingLabel": "培训", "xpack.ml.dataframe.analytics.regressionExploration.jobCapsFetchError": "无法提取结果。加载索引的字段数据时发生错误。", "xpack.ml.dataframe.analytics.regressionExploration.jobConfigurationFetchError": "无法提取结果。加载作业配置数据时发生错误。", "xpack.ml.dataframe.analytics.regressionExploration.meanSquaredErrorText": "均方误差", @@ -9624,9 +9585,6 @@ "xpack.ml.dataframe.analytics.regressionExploration.queryParsingErrorMessage": "无法解析查询。", "xpack.ml.dataframe.analytics.regressionExploration.rSquaredText": "R 平方", "xpack.ml.dataframe.analytics.regressionExploration.rSquaredTooltipContent": "表示拟合优度。度量模型复制被观察结果的优良性。", - "xpack.ml.dataframe.analytics.regressionExploration.searchBoxPlaceholder": "例如 avg>0.5", - "xpack.ml.dataframe.analytics.regressionExploration.selectColumnsAriaLabel": "选择列", - "xpack.ml.dataframe.analytics.regressionExploration.selectFieldsPopoverTitle": "选择字段", "xpack.ml.dataframe.analytics.regressionExploration.tableJobIdTitle": "回归作业 ID {jobId} 的目标索引", "xpack.ml.dataframe.analytics.regressionExploration.trainingDocsCount": "{docsCount, plural, one {# 个文档} other {# 个文档}}已评估", "xpack.ml.dataframe.analytics.regressionExploration.trainingErrorTitle": "训练误差", @@ -12463,7 +12421,6 @@ "xpack.reporting.screenCapturePanelContent.optimizeForPrintingLabel": "打印优化", "xpack.reporting.selfCheck.ok": "Reporting 插件自检正常!", "xpack.reporting.selfCheck.warning": "Reporting 插件自检生成警告:{err}", - "xpack.reporting.selfCheckEncryptionKey.warning": "正在为 {setting} 生成随机密钥。要防止待处理报告在重新启动时失败,请在 kibana.yml 中设置 {setting}", "xpack.reporting.shareContextMenu.csvReportsButtonLabel": "CSV 报告", "xpack.reporting.shareContextMenu.pdfReportsButtonLabel": "PDF 报告", "xpack.reporting.shareContextMenu.pngReportsButtonLabel": "PNG 报告", @@ -15990,7 +15947,7 @@ "xpack.triggersActionsUI.sections.addModalConnectorForm.updateSuccessNotificationText": "已创建“{connectorName}”", "xpack.triggersActionsUI.sections.alertAdd.betaBadgeTooltipContent": "{pluginName} 为公测版,可能会进行更改。设计和代码相对于正式发行版功能还不够成熟,将按原样提供,且不提供任何保证。公测版功能不受正式发行版功能支持 SLA 的约束。", "xpack.triggersActionsUI.sections.alertAdd.cancelButtonLabel": "取消", - "xpack.triggersActionsUI.sections.alertAdd.conditionPrompt": "定义条件。", + "xpack.triggersActionsUI.sections.alertAdd.conditionPrompt": "定义条件", "xpack.triggersActionsUI.sections.alertAdd.errorLoadingAlertVisualizationTitle": "无法加载告警可视化", "xpack.triggersActionsUI.sections.alertAdd.flyoutTitle": "创建告警", "xpack.triggersActionsUI.sections.alertAdd.loadingAlertVisualizationDescription": "正在加载告警可视化……", @@ -15998,7 +15955,7 @@ "xpack.triggersActionsUI.sections.alertAdd.saveButtonLabel": "保存", "xpack.triggersActionsUI.sections.alertAdd.saveErrorNotificationText": "无法创建告警。", "xpack.triggersActionsUI.sections.alertAdd.saveSuccessNotificationText": "已保存“{alertName}”", - "xpack.triggersActionsUI.sections.alertAdd.selectIndex": "选择索引。", + "xpack.triggersActionsUI.sections.alertAdd.selectIndex": "选择索引", "xpack.triggersActionsUI.sections.alertAdd.threshold.closeIndexPopoverLabel": "关闭", "xpack.triggersActionsUI.sections.alertAdd.threshold.fixErrorInExpressionBelowValidationMessage": "表达式包含错误。", "xpack.triggersActionsUI.sections.alertAdd.threshold.howToBroadenSearchQueryDescription": "使用 * 可扩大您的查询范围。", @@ -16082,7 +16039,6 @@ "xpack.triggersActionsUI.sections.alertsList.bulkActionPopover.muteAllTitle": "静音", "xpack.triggersActionsUI.sections.alertsList.bulkActionPopover.unmuteAllTitle": "取消静音", "xpack.triggersActionsUI.sections.alertsList.collapsedItemActons.deleteTitle": "删除", - "xpack.triggersActionsUI.sections.alertsList.collapsedItemActons.enableTitle": "启用", "xpack.triggersActionsUI.sections.alertsList.collapsedItemActons.muteTitle": "静音", "xpack.triggersActionsUI.sections.alertsList.collapsedItemActons.popoverButtonTitle": "操作", "xpack.triggersActionsUI.components.emptyPrompt.emptyButton": "创建告警", @@ -16110,7 +16066,6 @@ "xpack.triggersActionsUI.sections.builtinActionTypes.emailAction.userTextFieldLabel": "用户名", "xpack.triggersActionsUI.sections.editConnectorForm.betaBadgeTooltipContent": "{pluginName} 为公测版,可能会进行更改。设计和代码相对于正式发行版功能还不够成熟,将按原样提供,且不提供任何保证。公测版功能不受正式发行版功能支持 SLA 的约束。", "xpack.triggersActionsUI.sections.editConnectorForm.cancelButtonLabel": "取消", - "xpack.triggersActionsUI.sections.editConnectorForm.flyoutTitle": "编辑连接器", "xpack.triggersActionsUI.sections.editConnectorForm.saveButtonLabel": "保存", "xpack.triggersActionsUI.sections.editConnectorForm.updateErrorNotificationText": "无法更新连接器。", "xpack.triggersActionsUI.sections.editConnectorForm.updateSuccessNotificationText": "已更新“{connectorName}”", @@ -16271,16 +16226,10 @@ "xpack.uptime.components.embeddables.embeddedMap.embeddablePanelTitle": "监测观察者位置地图", "xpack.uptime.durationChart.emptyPrompt.description": "在选定时间范围内此监测从未{emphasizedText}。", "xpack.uptime.durationChart.emptyPrompt.title": "没有持续时间数据", - "xpack.uptime.emptyState.configureHeartbeatLinkText": "配置 Heartbeat", - "xpack.uptime.emptyState.configureHeartbeatToGetStartedMessage": "{configureHeartbeatLink}以开始收集运行时间数据。", "xpack.uptime.emptyState.loadingMessage": "正在加载……", - "xpack.uptime.emptyState.noDataMessage": "未找到任何运行时间数据", - "xpack.uptime.emptyState.noDataTitle": "没有可用的运行时间数据", - "xpack.uptime.emptyState.noIndexTitle": "找不到运行时间索引", "xpack.uptime.emptyStateError.notAuthorized": "您无权查看 Uptime 数据,请联系系统管理员。", "xpack.uptime.emptyStateError.notFoundPage": "未找到页面", "xpack.uptime.emptyStateError.title": "错误", - "xpack.uptime.errorMessage": "错误:{message}", "xpack.uptime.featureCatalogueDescription": "执行终端节点运行状况检查和运行时间监测。", "xpack.uptime.featureRegistry.uptimeFeatureName": "运行时间", "xpack.uptime.filterBar.ariaLabel": "概览页面的输入筛选条件", @@ -16302,7 +16251,6 @@ "xpack.uptime.locationName.helpLinkAnnotation": "添加位置", "xpack.uptime.ml.durationChart.exploreInMlApp": "在 ML 应用中浏览", "xpack.uptime.ml.enableAnomalyDetectionPanel.anomalyDetectionTitle": "异常检测", - "xpack.uptime.ml.enableAnomalyDetectionPanel.callout.jobExistsDescription.viewJobLinkText": "查看现有作业", "xpack.uptime.ml.enableAnomalyDetectionPanel.createMLJobDescription": "在此处可以创建 Machine Learning 作业,以便为运行时间监测计算\n 响应持续时间的异常分数。启用后,详情页面上的监测持续时间图表\n 将显示预期边界并使用异常标注图表。您还可能\n 识别在所有地理区域的延迟增长时段。", "xpack.uptime.ml.enableAnomalyDetectionPanel.createNewJobButtonLabel": "创建新作业", "xpack.uptime.ml.enableAnomalyDetectionPanel.disableAnomalyDetectionTitle": "禁用异常检测", @@ -16797,4 +16745,4 @@ "xpack.watcher.watchEdit.thresholdWatchExpression.aggType.fieldIsRequiredValidationMessage": "此字段必填。", "xpack.watcher.watcherDescription": "通过创建、管理和监测警报来检测数据中的更改。" } -} +} \ No newline at end of file diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/add_message_variables.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/add_message_variables.tsx index ab9b5c2586c17..957c79a5c5123 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/add_message_variables.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/add_message_variables.tsx @@ -22,9 +22,10 @@ export const AddMessageVariables: React.FunctionComponent = ({ const [isVariablesPopoverOpen, setIsVariablesPopoverOpen] = useState(false); const getMessageVariables = () => - messageVariables?.map((variable: string) => ( + messageVariables?.map((variable: string, i: number) => ( { onSelectEventHandler(variable); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/es_index.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/es_index.tsx index 9bd6a39d216e3..15f68e6a9f441 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/es_index.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/es_index.tsx @@ -17,6 +17,7 @@ import { } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; import { i18n } from '@kbn/i18n'; +import { useXJsonMode } from '../../../../../../../src/plugins/es_ui_shared/static/ace_x_json/hooks'; import { ActionTypeModel, ActionConnectorFieldsProps, @@ -31,7 +32,6 @@ import { getIndexOptions, getIndexPatterns, } from '../../../common/index_controls'; -import { useXJsonMode } from '../../lib/use_x_json_mode'; import { AddMessageVariables } from '../add_message_variables'; export function getActionType(): ActionTypeModel { diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_alert_types/threshold/expression.scss b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_alert_types/threshold/expression.scss index d0a7039ae24e1..3734cf5d5218b 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_alert_types/threshold/expression.scss +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_alert_types/threshold/expression.scss @@ -1,3 +1,13 @@ .actAlertVisualization__chart { - height: $euiSize * 15; + height: $euiSize * 14; +} + +.actAddAlertSteps { + .euiStep__titleWrapper { + align-items: center; + } + + .euiStep__title { + @include euiTitle('xs'); + } } diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_alert_types/threshold/expression.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_alert_types/threshold/expression.tsx index 5bbec1221a3ac..43955db97f295 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_alert_types/threshold/expression.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_alert_types/threshold/expression.tsx @@ -283,7 +283,7 @@ export const IndexThresholdAlertTypeExpression: React.FunctionComponent @@ -328,6 +328,7 @@ export const IndexThresholdAlertTypeExpression: React.FunctionComponent @@ -444,12 +445,10 @@ export const IndexThresholdAlertTypeExpression: React.FunctionComponent ) : null} - - +
{canShowVizualization ? ( - { const description = queryByRole(/banner/i); expect(description!.textContent).toMatchInlineSnapshot( - `"To create an alert, set a value for xpack.encrypted_saved_objects.encryptionKey in your kibana.yml file. Learn how."` + `"To create an alert, set a value for xpack.encryptedSavedObjects.encryptionKey in your kibana.yml file. Learn how."` ); const action = queryByText(/Learn/i); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/health_check.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/health_check.tsx index c967cf5de0771..afd5e08f52f25 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/health_check.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/health_check.tsx @@ -132,7 +132,7 @@ const EncryptionError = ({ defaultMessage: 'To create an alert, set a value for ', } )} - {'xpack.encrypted_saved_objects.encryptionKey'} + {'xpack.encryptedSavedObjects.encryptionKey'} {i18n.translate( 'xpack.triggersActionsUI.components.healthCheck.encryptionErrorAfterKey', { diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/prompts/empty_prompt.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/prompts/empty_prompt.tsx index df593d587de3f..303c9c5c4cdd1 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/prompts/empty_prompt.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/prompts/empty_prompt.tsx @@ -33,8 +33,6 @@ export const EmptyPrompt = ({ onCTAClicked }: { onCTAClicked: () => void }) => ( data-test-subj="createFirstAlertButton" key="create-action" fill - iconType="plusInCircle" - iconSide="left" onClick={onCTAClicked} > | null) => { - const [xJson, setXJson] = useState( - json === null ? '' : expandLiteralStrings(JSON.stringify(json, null, 2)) - ); - - return { - xJson, - setXJson, - xJsonMode, - convertToJson: collapseLiteralStrings, - }; -}; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_form.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_form.test.tsx index 41564146bb84d..d4def86b07b1f 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_form.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_form.test.tsx @@ -87,6 +87,14 @@ describe('action_form', () => { config: {}, isPreconfigured: false, }, + { + secrets: {}, + id: 'test2', + actionTypeId: actionType.id, + name: 'Test connector 2', + config: {}, + isPreconfigured: true, + }, ]); const mockes = coreMock.createSetup(); deps = { @@ -100,6 +108,7 @@ describe('action_form', () => { disabledByLicenseActionType, ]); actionTypeRegistry.has.mockReturnValue(true); + actionTypeRegistry.get.mockReturnValue(actionType); const initialAlert = ({ name: 'test', @@ -206,6 +215,29 @@ describe('action_form', () => { expect(actionOption.exists()).toBeFalsy(); }); + it(`renders available connectors for the selected action type`, async () => { + await setup(); + const actionOption = wrapper.find( + `[data-test-subj="${actionType.id}-ActionTypeSelectOption"]` + ); + actionOption.first().simulate('click'); + const combobox = wrapper.find(`[data-test-subj="selectActionConnector"]`); + expect((combobox.first().props() as any).options).toMatchInlineSnapshot(` + Array [ + Object { + "id": "test", + "key": "test", + "label": "Test connector ", + }, + Object { + "id": "test2", + "key": "test2", + "label": "Test connector 2 (preconfigured)", + }, + ] + `); + }); + it('renders action types disabled by license', async () => { await setup(); const actionOption = wrapper.find( diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_form.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_form.tsx index 6b011ac84bc6f..4199cfb7b4b7f 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_form.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_form.tsx @@ -24,6 +24,9 @@ import { EuiToolTip, EuiIconTip, EuiLink, + EuiCallOut, + EuiHorizontalRule, + EuiText, } from '@elastic/eui'; import { HttpSetup, ToastsApi } from 'kibana/public'; import { loadActionTypes, loadAllActions } from '../../lib/action_connector_api'; @@ -85,8 +88,10 @@ export const ActionForm = ({ ); const [isAddActionPanelOpen, setIsAddActionPanelOpen] = useState(true); const [connectors, setConnectors] = useState([]); + const [isLoadingConnectors, setIsLoadingConnectors] = useState(false); const [isLoadingActionTypes, setIsLoadingActionTypes] = useState(false); const [actionTypesIndex, setActionTypesIndex] = useState(undefined); + const [emptyActionsIds, setEmptyActionsIds] = useState([]); // load action types useEffect(() => { @@ -128,6 +133,7 @@ export const ActionForm = ({ async function loadConnectors() { try { + setIsLoadingConnectors(true); const actionsResponse = await loadAllActions({ http }); setConnectors(actionsResponse); } catch (e) { @@ -139,18 +145,28 @@ export const ActionForm = ({ } ), }); + } finally { + setIsLoadingConnectors(false); } } + const preconfiguredMessage = i18n.translate( + 'xpack.triggersActionsUI.sections.actionForm.preconfiguredTitleMessage', + { + defaultMessage: '(preconfigured)', + } + ); const getSelectedOptions = (actionItemId: string) => { const val = connectors.find(connector => connector.id === actionItemId); if (!val) { return []; } + const optionTitle = `${val.name} ${val.isPreconfigured ? preconfiguredMessage : ''}`; return [ { - label: val.name, - value: val.name, + label: optionTitle, + value: optionTitle, id: actionItemId, + 'data-test-subj': 'itemActionConnector', }, ]; }; @@ -164,13 +180,9 @@ export const ActionForm = ({ index: number ) => { const optionsList = connectors - .filter( - connectorItem => - connectorItem.actionTypeId === actionItem.actionTypeId && - connectorItem.id === actionItem.id - ) - .map(({ name, id }) => ({ - label: name, + .filter(connectorItem => connectorItem.actionTypeId === actionItem.actionTypeId) + .map(({ name, id, isPreconfigured }) => ({ + label: `${name} ${isPreconfigured ? preconfiguredMessage : ''}`, key: id, id, })); @@ -201,6 +213,7 @@ export const ActionForm = ({ labelAppend={ { setActiveActionItem({ actionTypeId: actionItem.actionTypeId, index }); setAddModalVisibility(true); @@ -217,6 +230,8 @@ export const ActionForm = ({ fullWidth singleSelection={{ asPlainText: true }} options={optionsList} + id={`selectActionConnector-${actionItem.id}`} + data-test-subj="selectActionConnector" selectedOptions={getSelectedOptions(actionItem.id)} onChange={selectedOptions => { setActionIdByIndex(selectedOptions[0].id ?? '', index); @@ -243,79 +258,87 @@ export const ActionForm = ({ ); return ( - - - - - - -
- - - - - - {checkEnabledResult.isEnabled === false && ( - - - - )} - - -
-
-
- - } - extraAction={ - { - const updatedActions = actions.filter((_item: AlertAction, i: number) => i !== index); - setAlertProperty(updatedActions); - setIsAddActionPanelOpen( - updatedActions.filter((item: AlertAction) => item.id !== actionItem.id).length === 0 - ); - setActiveActionItem(undefined); - }} - /> - } - paddingSize="l" - > - {accordionContent} -
+ + + + + + + +

+ + + + + + {checkEnabledResult.isEnabled === false && ( + + + + )} + + +

+
+
+ + } + extraAction={ + { + const updatedActions = actions.filter( + (_item: AlertAction, i: number) => i !== index + ); + setAlertProperty(updatedActions); + setIsAddActionPanelOpen( + updatedActions.filter((item: AlertAction) => item.id !== actionItem.id).length === + 0 + ); + setActiveActionItem(undefined); + }} + /> + } + paddingSize="l" + > + {accordionContent} +
+ +
); }; @@ -326,84 +349,103 @@ export const ActionForm = ({ const actionTypeRegistered = actionTypeRegistry.get(actionItem.actionTypeId); if (!actionTypeRegistered || actionItem.group !== defaultActionGroupId) return null; return ( - - - - - - -
- -
-
-
- - } - extraAction={ - { - const updatedActions = actions.filter((_item: AlertAction, i: number) => i !== index); - setAlertProperty(updatedActions); - setIsAddActionPanelOpen( - updatedActions.filter((item: AlertAction) => item.id !== actionItem.id).length === 0 - ); - setActiveActionItem(undefined); - }} - /> - } - paddingSize="l" - > - + + + + + + + +

+ +

+
+
+ } - actions={[ - { - setActiveActionItem({ actionTypeId: actionItem.actionTypeId, index }); - setAddModalVisibility(true); + const updatedActions = actions.filter( + (_item: AlertAction, i: number) => i !== index + ); + setAlertProperty(updatedActions); + setIsAddActionPanelOpen( + updatedActions.filter((item: AlertAction) => item.id !== actionItem.id).length === + 0 + ); + setActiveActionItem(undefined); }} - > - - , - ]} - /> -
+ /> + } + paddingSize="l" + > + actionItem.id === emptyId) ? ( + + ) : ( + + ) + } + actions={[ + { + setActiveActionItem({ actionTypeId: actionItem.actionTypeId, index }); + setAddModalVisibility(true); + }} + > + + , + ]} + /> +
+ +
); }; @@ -420,6 +462,7 @@ export const ActionForm = ({ const actionTypeConnectors = connectors.filter( field => field.actionTypeId === actionTypeModel.id ); + if (actionTypeConnectors.length > 0) { actions.push({ id: '', @@ -439,6 +482,7 @@ export const ActionForm = ({ params: {}, }); setActionIdByIndex(actions.length.toString(), actions.length - 1); + setEmptyActionsIds([...emptyActionsIds, actions.length.toString()]); } } @@ -483,81 +527,110 @@ export const ActionForm = ({ }); } - return ( - - {actions.map((actionItem: AlertAction, index: number) => { - const actionConnector = connectors.find(field => field.id === actionItem.id); - // connectors doesn't exists - if (!actionConnector) { - return getAddConnectorsForm(actionItem, index); - } + const alertActionsList = actions.map((actionItem: AlertAction, index: number) => { + const actionConnector = connectors.find(field => field.id === actionItem.id); + // connectors doesn't exists + if (!actionConnector) { + return getAddConnectorsForm(actionItem, index); + } - const actionErrors: { errors: IErrorObject } = actionTypeRegistry - .get(actionItem.actionTypeId) - ?.validateParams(actionItem.params); + const actionErrors: { errors: IErrorObject } = actionTypeRegistry + .get(actionItem.actionTypeId) + ?.validateParams(actionItem.params); - return getActionTypeForm(actionItem, actionConnector, actionErrors, index); - })} - - {isAddActionPanelOpen === false ? ( - setIsAddActionPanelOpen(true)} - > + return getActionTypeForm(actionItem, actionConnector, actionErrors, index); + }); + + return ( + + {isLoadingConnectors ? ( + - - ) : null} - {isAddActionPanelOpen ? ( + + ) : ( - - - -
- -
-
-
- {hasDisabledByLicenseActionTypes && ( - - -
- + +

+ +

+
+ + {alertActionsList} + {isAddActionPanelOpen === false ? ( +
+ + + + setIsAddActionPanelOpen(true)} + > + + + + +
+ ) : null} + {isAddActionPanelOpen ? ( + + + + +
- -
-
-
- )} -
- - - {isLoadingActionTypes ? ( - - - - ) : ( - actionTypeNodes - )} - +
+
+
+ {hasDisabledByLicenseActionTypes && ( + + +
+ + + +
+
+
+ )} +
+ + + {isLoadingActionTypes ? ( + + + + ) : ( + actionTypeNodes + )} + +
+ ) : null}
- ) : null} + )} {actionTypesIndex && activeActionItem ? ( { expect(connectorNameField.exists()).toBeTruthy(); expect(connectorNameField.first().prop('value')).toBe('action-connector'); }); + + test('if preconfigured connector rendered correct in the edit form', () => { + const connector = { + secrets: {}, + id: 'test', + actionTypeId: 'test-action-type-id', + actionType: 'test-action-type-name', + name: 'preconfigured-connector', + isPreconfigured: true, + referencedByCount: 0, + config: {}, + }; + + const actionType = { + id: 'test-action-type-id', + iconClass: 'test', + selectMessage: 'test', + validateConnector: (): ValidationResult => { + return { errors: {} }; + }, + validateParams: (): ValidationResult => { + const validationResult = { errors: {} }; + return validationResult; + }, + actionConnectorFields: null, + actionParamsFields: null, + }; + actionTypeRegistry.get.mockReturnValue(actionType); + actionTypeRegistry.has.mockReturnValue(true); + + const wrapper = mountWithIntl( + + { + return new Promise(() => {}); + }, + }} + > + {}} + /> + + + ); + + const preconfiguredBadge = wrapper.find('[data-test-subj="preconfiguredBadge"]'); + expect(preconfiguredBadge.exists()).toBeTruthy(); + expect(wrapper.find('[data-test-subj="saveEditedActionButton"]').exists()).toBeFalsy(); + }); }); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_edit_flyout.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_edit_flyout.tsx index ed8811d26331b..690a64ef4f1f3 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_edit_flyout.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_edit_flyout.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, { useCallback, useReducer, useState } from 'react'; +import React, { useCallback, useReducer, useState, Fragment } from 'react'; import { FormattedMessage } from '@kbn/i18n/react'; import { EuiTitle, @@ -17,6 +17,8 @@ import { EuiButtonEmpty, EuiButton, EuiBetaBadge, + EuiText, + EuiLink, } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { ActionConnectorForm, validateBaseProperties } from './action_connector_form'; @@ -91,8 +93,77 @@ export const ConnectorEditFlyout = ({ return undefined; }); + const flyoutTitle = connector.isPreconfigured ? ( + + +

+ +   + +   + +

+
+ + + +
+ ) : ( + +

+ +   + +

+
+ ); + return ( - + {actionTypeModel ? ( @@ -100,41 +171,37 @@ export const ConnectorEditFlyout = ({ ) : null} - - -

- -   - -

-
-
+ {flyoutTitle}
- + {!connector.isPreconfigured ? ( + + ) : ( + + + {i18n.translate( + 'xpack.triggersActionsUI.sections.editConnectorForm.descriptionText', + { + defaultMessage: 'This connector is readonly.', + } + )} + + + + + + )} @@ -148,7 +215,7 @@ export const ConnectorEditFlyout = ({ )} - {canSave && actionTypeModel ? ( + {canSave && actionTypeModel && !connector.isPreconfigured ? ( { id: '1', actionTypeId: 'test', description: 'My test', + isPreconfigured: false, referencedByCount: 1, config: {}, }, @@ -119,6 +120,15 @@ describe('actions_connectors_list component with items', () => { actionTypeId: 'test2', description: 'My test 2', referencedByCount: 1, + isPreconfigured: false, + config: {}, + }, + { + id: '3', + actionTypeId: 'test2', + description: 'My preconfigured test 2', + referencedByCount: 1, + isPreconfigured: true, config: {}, }, ]); @@ -185,7 +195,11 @@ describe('actions_connectors_list component with items', () => { it('renders table of connectors', () => { expect(wrapper.find('EuiInMemoryTable')).toHaveLength(1); - expect(wrapper.find('EuiTableRow')).toHaveLength(2); + expect(wrapper.find('EuiTableRow')).toHaveLength(3); + }); + + it('renders table with preconfigured connectors', () => { + expect(wrapper.find('[data-test-subj="preConfiguredTitleMessage"]')).toHaveLength(2); }); test('if select item for edit should render ConnectorEditFlyout', () => { diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/actions_connectors_list/components/actions_connectors_list.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/actions_connectors_list/components/actions_connectors_list.tsx index 47e058f473946..566a6030d72be 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/actions_connectors_list/components/actions_connectors_list.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/actions_connectors_list/components/actions_connectors_list.tsx @@ -15,6 +15,9 @@ import { EuiIconTip, EuiFlexGroup, EuiFlexItem, + EuiBetaBadge, + EuiToolTip, + EuiButtonIcon, } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; @@ -200,31 +203,58 @@ export const ActionsConnectorsList: React.FunctionComponent = () => { }, }, { - field: '', + field: 'isPreconfigured', name: '', - actions: [ - { - enabled: () => canDelete, - 'data-test-subj': 'deleteConnector', - name: i18n.translate( - 'xpack.triggersActionsUI.sections.actionsConnectorsList.connectorsListTable.columns.actions.deleteActionName', - { defaultMessage: 'Delete' } - ), - description: canDelete - ? i18n.translate( - 'xpack.triggersActionsUI.sections.actionsConnectorsList.connectorsListTable.columns.actions.deleteActionDescription', - { defaultMessage: 'Delete this connector' } - ) - : i18n.translate( - 'xpack.triggersActionsUI.sections.actionsConnectorsList.connectorsListTable.columns.actions.deleteActionDisabledDescription', - { defaultMessage: 'Unable to delete connectors' } - ), - type: 'icon', - icon: 'trash', - color: 'danger', - onClick: (item: ActionConnectorTableItem) => setConnectorsToDelete([item.id]), - }, - ], + render: (value: number, item: ActionConnectorTableItem) => { + if (item.isPreconfigured) { + return ( + + + + + + ); + } + return ( + + + + setConnectorsToDelete([item.id])} + iconType={'trash'} + /> + + + + ); + }, }, ]; @@ -308,8 +338,6 @@ export const ActionsConnectorsList: React.FunctionComponent = () => { data-test-subj="createActionButton" key="create-action" fill - iconType="plusInCircle" - iconSide="left" onClick={() => setAddFlyoutVisibility(true)} > { key="create-alert" data-test-subj="createAlertButton" fill - iconType="plusInCircle" - iconSide="left" onClick={() => setAlertFlyoutVisibility(true)} > = ({ isOpen={isPopoverOpen} closePopover={() => setIsPopoverOpen(false)} ownFocus + panelPaddingSize="none" data-test-subj="collapsedItemActions" > - - { - if (item.enabled) { - await disableAlert(item); - } else { - await enableAlert(item); + +
+ { + if (item.enabled) { + await disableAlert(item); + } else { + await enableAlert(item); + } + onAlertChanged(); + }} + label={ + } - onAlertChanged(); - }} - label={ + /> + + - } - /> - - - { - if (item.muteAll) { - await unmuteAlert(item); - } else { - await muteAlert(item); + +
+
+ { + if (item.muteAll) { + await unmuteAlert(item); + } else { + await muteAlert(item); + } + onAlertChanged(); + }} + label={ + } - onAlertChanged(); - }} - label={ + /> + + - } - /> - - - - setAlertsToDelete([item.id])} - > - - - - + +
+ + setAlertsToDelete([item.id])} + > +
+
+ +
+
+ +

+ +

+
+
+
+
+
); }; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/common/components/alert_quick_edit_buttons.scss b/x-pack/plugins/triggers_actions_ui/public/application/sections/common/components/alert_quick_edit_buttons.scss new file mode 100644 index 0000000000000..a5b4cc258c1b5 --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/common/components/alert_quick_edit_buttons.scss @@ -0,0 +1,5 @@ +.actBulkActionPopover__deleteAll { + .euiButtonEmpty__text { + padding-top: $euiSizeXS; + } +} diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/common/components/alert_quick_edit_buttons.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/common/components/alert_quick_edit_buttons.tsx index eeae0cf54f1a1..26bc8b869a06b 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/common/components/alert_quick_edit_buttons.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/common/components/alert_quick_edit_buttons.tsx @@ -5,9 +5,9 @@ */ import { i18n } from '@kbn/i18n'; -import React, { useState, Fragment } from 'react'; +import React, { useState } from 'react'; import { FormattedMessage } from '@kbn/i18n/react'; -import { EuiButtonEmpty } from '@elastic/eui'; +import { EuiButtonEmpty, EuiFlexItem, EuiFlexGroup } from '@elastic/eui'; import { Alert } from '../../../../types'; import { useAppDependencies } from '../../../app_context'; @@ -15,6 +15,7 @@ import { withBulkAlertOperations, ComponentOpts as BulkOperationsComponentOpts, } from './with_bulk_alert_api_operations'; +import './alert_quick_edit_buttons.scss'; export type ComponentOpts = { selectedItems: Alert[]; @@ -147,72 +148,84 @@ export const AlertQuickEditButtons: React.FunctionComponent = ({ } return ( - + {!allAlertsMuted && ( - - - + + + + + )} {allAlertsMuted && ( - - - + + + + + )} {allAlertsDisabled && ( - - - + + + + + )} {!allAlertsDisabled && ( + + + + + + )} + - )} - - - - - +
+
); }; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/common/components/bulk_operation_popover.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/common/components/bulk_operation_popover.tsx index d0fd0e1792818..935b0bd8985ca 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/common/components/bulk_operation_popover.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/common/components/bulk_operation_popover.tsx @@ -4,9 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { useState } from 'react'; +import React, { useState, Fragment } from 'react'; import { FormattedMessage } from '@kbn/i18n/react'; -import { EuiButton, EuiFormRow, EuiPopover } from '@elastic/eui'; +import { EuiButton, EuiPopover } from '@elastic/eui'; export const BulkOperationPopover: React.FunctionComponent = ({ children }) => { const [isPopoverOpen, setIsPopoverOpen] = useState(false); @@ -16,6 +16,7 @@ export const BulkOperationPopover: React.FunctionComponent = ({ children }) => { isOpen={isPopoverOpen} closePopover={() => setIsPopoverOpen(false)} data-test-subj="bulkAction" + panelPaddingSize="s" button={ { > {children && React.Children.map(children, child => - React.isValidElement(child) ? ( - {React.cloneElement(child, {})} - ) : ( - child - ) + React.isValidElement(child) ? {React.cloneElement(child, {})} : child )} ); diff --git a/x-pack/plugins/triggers_actions_ui/public/common/expression_items/group_by_over.tsx b/x-pack/plugins/triggers_actions_ui/public/common/expression_items/group_by_over.tsx index 6ad52a5416163..619d85d99719b 100644 --- a/x-pack/plugins/triggers_actions_ui/public/common/expression_items/group_by_over.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/common/expression_items/group_by_over.tsx @@ -146,16 +146,13 @@ export const GroupByExpression = ({ {groupByTypes[groupBy].sizeRequired ? ( - 0 && termSize !== undefined} - error={errors.termSize} - > + 0} error={errors.termSize}> 0 && termSize !== undefined} - value={termSize} + isInvalid={errors.termSize.length > 0} + value={termSize || ''} onChange={e => { const { value } = e.target; - const termSizeVal = value !== '' ? parseFloat(value) : MIN_TERM_SIZE; + const termSizeVal = value !== '' ? parseFloat(value) : undefined; onChangeSelectedTermSize(termSizeVal); }} min={MIN_TERM_SIZE} diff --git a/x-pack/plugins/upgrade_assistant/server/lib/telemetry/usage_collector.test.ts b/x-pack/plugins/upgrade_assistant/server/lib/telemetry/usage_collector.test.ts index a4833d9a3d7fe..2af1a9ac38e44 100644 --- a/x-pack/plugins/upgrade_assistant/server/lib/telemetry/usage_collector.test.ts +++ b/x-pack/plugins/upgrade_assistant/server/lib/telemetry/usage_collector.test.ts @@ -58,7 +58,7 @@ describe('Upgrade Assistant Usage Collector', () => { }), }, elasticsearch: { - adminClient: clusterClient, + legacy: { client: clusterClient }, }, }; }); diff --git a/x-pack/plugins/upgrade_assistant/server/lib/telemetry/usage_collector.ts b/x-pack/plugins/upgrade_assistant/server/lib/telemetry/usage_collector.ts index 79d6e53c64ec0..9c2946db7f084 100644 --- a/x-pack/plugins/upgrade_assistant/server/lib/telemetry/usage_collector.ts +++ b/x-pack/plugins/upgrade_assistant/server/lib/telemetry/usage_collector.ts @@ -7,7 +7,7 @@ import { set } from 'lodash'; import { APICaller, - ElasticsearchServiceSetup, + ElasticsearchServiceStart, ISavedObjectsRepository, SavedObjectsServiceStart, } from 'src/core/server'; @@ -51,7 +51,7 @@ async function getDeprecationLoggingStatusValue(callAsCurrentUser: APICaller): P } export async function fetchUpgradeAssistantMetrics( - { adminClient }: ElasticsearchServiceSetup, + { legacy: { client: esClient } }: ElasticsearchServiceStart, savedObjects: SavedObjectsServiceStart ): Promise { const savedObjectsRepository = savedObjects.createInternalRepository(); @@ -60,7 +60,7 @@ export async function fetchUpgradeAssistantMetrics( UPGRADE_ASSISTANT_TYPE, UPGRADE_ASSISTANT_DOC_ID ); - const callAsInternalUser = adminClient.callAsInternalUser.bind(adminClient); + const callAsInternalUser = esClient.callAsInternalUser.bind(esClient); const deprecationLoggingStatusValue = await getDeprecationLoggingStatusValue(callAsInternalUser); const getTelemetrySavedObject = ( @@ -107,7 +107,7 @@ export async function fetchUpgradeAssistantMetrics( } interface Dependencies { - elasticsearch: ElasticsearchServiceSetup; + elasticsearch: ElasticsearchServiceStart; savedObjects: SavedObjectsServiceStart; usageCollection: UsageCollectionSetup; } diff --git a/x-pack/plugins/upgrade_assistant/server/plugin.ts b/x-pack/plugins/upgrade_assistant/server/plugin.ts index 6ccd073a9e020..bdca506cc7338 100644 --- a/x-pack/plugins/upgrade_assistant/server/plugin.ts +++ b/x-pack/plugins/upgrade_assistant/server/plugin.ts @@ -11,7 +11,6 @@ import { CoreStart, PluginInitializerContext, Logger, - ElasticsearchServiceSetup, SavedObjectsClient, SavedObjectsServiceStart, } from '../../../../src/core/server'; @@ -40,7 +39,6 @@ export class UpgradeAssistantServerPlugin implements Plugin { // Properties set at setup private licensing?: LicensingPluginSetup; - private elasticSearchService?: ElasticsearchServiceSetup; // Properties set at start private savedObjectsServiceStart?: SavedObjectsServiceStart; @@ -59,10 +57,9 @@ export class UpgradeAssistantServerPlugin implements Plugin { } setup( - { http, elasticsearch, getStartServices, capabilities }: CoreSetup, + { http, getStartServices, capabilities }: CoreSetup, { usageCollection, cloud, licensing }: PluginsSetup ) { - this.elasticSearchService = elasticsearch; this.licensing = licensing; const router = http.createRouter(); @@ -88,13 +85,13 @@ export class UpgradeAssistantServerPlugin implements Plugin { registerTelemetryRoutes(dependencies); if (usageCollection) { - getStartServices().then(([{ savedObjects }]) => { + getStartServices().then(([{ savedObjects, elasticsearch }]) => { registerUpgradeAssistantUsageCollector({ elasticsearch, usageCollection, savedObjects }); }); } } - start({ savedObjects }: CoreStart) { + start({ savedObjects, elasticsearch }: CoreStart) { this.savedObjectsServiceStart = savedObjects; // The ReindexWorker uses a map of request headers that contain the authentication credentials @@ -107,7 +104,7 @@ export class UpgradeAssistantServerPlugin implements Plugin { this.worker = createReindexWorker({ credentialStore: this.credentialStore, licensing: this.licensing!, - elasticsearchService: this.elasticSearchService!, + elasticsearchService: elasticsearch, logger: this.logger, savedObjects: new SavedObjectsClient( this.savedObjectsServiceStart.createInternalRepository() diff --git a/x-pack/plugins/upgrade_assistant/server/routes/reindex_indices/reindex_indices.ts b/x-pack/plugins/upgrade_assistant/server/routes/reindex_indices/reindex_indices.ts index 686f93b771e62..3d15916b17ff9 100644 --- a/x-pack/plugins/upgrade_assistant/server/routes/reindex_indices/reindex_indices.ts +++ b/x-pack/plugins/upgrade_assistant/server/routes/reindex_indices/reindex_indices.ts @@ -5,7 +5,7 @@ */ import { schema } from '@kbn/config-schema'; import { - ElasticsearchServiceSetup, + ElasticsearchServiceStart, kibanaResponseFactory, Logger, SavedObjectsClient, @@ -38,7 +38,7 @@ import { GetBatchQueueResponse, PostBatchResponse } from './types'; interface CreateReindexWorker { logger: Logger; - elasticsearchService: ElasticsearchServiceSetup; + elasticsearchService: ElasticsearchServiceStart; credentialStore: CredentialStore; savedObjects: SavedObjectsClient; licensing: LicensingPluginSetup; @@ -51,8 +51,8 @@ export function createReindexWorker({ savedObjects, licensing, }: CreateReindexWorker) { - const { adminClient } = elasticsearchService; - return new ReindexWorker(savedObjects, credentialStore, adminClient, logger, licensing); + const esClient = elasticsearchService.legacy.client; + return new ReindexWorker(savedObjects, credentialStore, esClient, logger, licensing); } const mapAnyErrorToKibanaHttpResponse = (e: any) => { diff --git a/x-pack/plugins/uptime/server/graphql/constants.ts b/x-pack/plugins/uptime/server/graphql/constants.ts deleted file mode 100644 index aba58f5c6c4a5..0000000000000 --- a/x-pack/plugins/uptime/server/graphql/constants.ts +++ /dev/null @@ -1,7 +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. - */ - -export const DEFAULT_GRAPHQL_PATH = '/api/uptime/graphql'; diff --git a/x-pack/plugins/uptime/server/graphql/index.ts b/x-pack/plugins/uptime/server/graphql/index.ts deleted file mode 100644 index 49ba5583b417b..0000000000000 --- a/x-pack/plugins/uptime/server/graphql/index.ts +++ /dev/null @@ -1,18 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { createMonitorStatesResolvers, monitorStatesSchema } from './monitor_states'; -import { createPingsResolvers, pingsSchema } from './pings'; -import { CreateUMGraphQLResolvers } from './types'; -import { unsignedIntegerResolverFunctions, unsignedIntegerSchema } from './unsigned_int_scalar'; - -export { DEFAULT_GRAPHQL_PATH } from './constants'; -export const resolvers: CreateUMGraphQLResolvers[] = [ - createMonitorStatesResolvers, - createPingsResolvers, - unsignedIntegerResolverFunctions, -]; -export const typeDefs: any[] = [pingsSchema, unsignedIntegerSchema, monitorStatesSchema]; diff --git a/x-pack/plugins/uptime/server/graphql/monitor_states/index.ts b/x-pack/plugins/uptime/server/graphql/monitor_states/index.ts deleted file mode 100644 index fb0893dbc4dbf..0000000000000 --- a/x-pack/plugins/uptime/server/graphql/monitor_states/index.ts +++ /dev/null @@ -1,8 +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. - */ - -export { createMonitorStatesResolvers } from './resolvers'; -export { monitorStatesSchema } from './schema.gql'; diff --git a/x-pack/plugins/uptime/server/graphql/monitor_states/resolvers.ts b/x-pack/plugins/uptime/server/graphql/monitor_states/resolvers.ts deleted file mode 100644 index 479c06234ca66..0000000000000 --- a/x-pack/plugins/uptime/server/graphql/monitor_states/resolvers.ts +++ /dev/null @@ -1,76 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { CreateUMGraphQLResolvers, UMContext } from '../types'; -import { UMServerLibs } from '../../lib/lib'; -import { UMResolver } from '../../../../../legacy/plugins/uptime/common/graphql/resolver_types'; -import { - GetMonitorStatesQueryArgs, - MonitorSummaryResult, -} from '../../../../../legacy/plugins/uptime/common/graphql/types'; -import { CONTEXT_DEFAULTS } from '../../../../../legacy/plugins/uptime/common/constants'; -import { savedObjectsAdapter } from '../../lib/saved_objects'; - -export type UMGetMonitorStatesResolver = UMResolver< - MonitorSummaryResult | Promise, - any, - GetMonitorStatesQueryArgs, - UMContext ->; - -export const createMonitorStatesResolvers: CreateUMGraphQLResolvers = ( - libs: UMServerLibs -): { - Query: { - getMonitorStates: UMGetMonitorStatesResolver; - }; -} => { - return { - Query: { - async getMonitorStates( - _resolver, - { dateRangeStart, dateRangeEnd, filters, pagination, statusFilter, pageSize }, - { APICaller, savedObjectsClient } - ): Promise { - const dynamicSettings = await savedObjectsAdapter.getUptimeDynamicSettings( - savedObjectsClient - ); - - const decodedPagination = pagination - ? JSON.parse(decodeURIComponent(pagination)) - : CONTEXT_DEFAULTS.CURSOR_PAGINATION; - const [ - indexStatus, - { summaries, nextPagePagination, prevPagePagination }, - ] = await Promise.all([ - libs.requests.getIndexStatus({ callES: APICaller, dynamicSettings }), - libs.requests.getMonitorStates({ - callES: APICaller, - dynamicSettings, - dateRangeStart, - dateRangeEnd, - pagination: decodedPagination, - pageSize, - filters, - // this is added to make typescript happy, - // this sort of reassignment used to be further downstream but I've moved it here - // because this code is going to be decomissioned soon - statusFilter: statusFilter || undefined, - }), - ]); - - const totalSummaryCount = indexStatus?.docCount ?? 0; - - return { - summaries, - nextPagePagination, - prevPagePagination, - totalSummaryCount, - }; - }, - }, - }; -}; diff --git a/x-pack/plugins/uptime/server/graphql/monitor_states/schema.gql.ts b/x-pack/plugins/uptime/server/graphql/monitor_states/schema.gql.ts deleted file mode 100644 index 6ab564fdeb532..0000000000000 --- a/x-pack/plugins/uptime/server/graphql/monitor_states/schema.gql.ts +++ /dev/null @@ -1,183 +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 gql from 'graphql-tag'; - -export const monitorStatesSchema = gql` - "Represents a monitor's statuses for a period of time." - type SummaryHistogramPoint { - "The time at which these data were collected." - timestamp: UnsignedInteger! - "The number of _up_ documents." - up: Int! - "The number of _down_ documents." - down: Int! - } - - "Monitor status data over time." - type SummaryHistogram { - "The number of documents used to assemble the histogram." - count: Int! - "The individual histogram data points." - points: [SummaryHistogramPoint!]! - } - - type Agent { - id: String! - } - - type Check { - agent: Agent - container: StateContainer - kubernetes: StateKubernetes - monitor: CheckMonitor! - observer: CheckObserver - timestamp: String! - } - - type StateContainer { - id: String - } - - type StateKubernetes { - pod: StatePod - } - - type StatePod { - uid: String - } - - type CheckMonitor { - ip: String - name: String - status: String! - } - - type Location { - lat: Float - lon: Float - } - - type CheckGeo { - name: String - location: Location - } - - type CheckObserver { - geo: CheckGeo - } - - type StateGeo { - name: [String] - location: Location - } - - type StateObserver { - geo: StateGeo - } - - type MonitorState { - status: String - name: String - id: String - type: String - } - - type Summary { - up: Int - down: Int - geo: CheckGeo - } - - type MonitorSummaryUrl { - domain: String - fragment: String - full: String - original: String - password: String - path: String - port: Int - query: String - scheme: String - username: String - } - - type StateUrl { - domain: String - full: String - path: String - port: Int - scheme: String - } - - "Contains monitor transmission encryption information." - type StateTLS { - "The date and time after which the certificate is invalid." - certificate_not_valid_after: String - certificate_not_valid_before: String - certificates: String - rtt: RTT - } - - "Unifies the subsequent data for an uptime monitor." - type State { - "The agent processing the monitor." - agent: Agent - "There is a check object for each instance of the monitoring agent." - checks: [Check!] - geo: StateGeo - observer: StateObserver - monitor: MonitorState - summary: Summary! - timestamp: UnsignedInteger! - "Transport encryption information." - tls: [StateTLS] - url: StateUrl - } - - "Represents the current state and associated data for an Uptime monitor." - type MonitorSummary { - "The ID assigned by the config or generated by the user." - monitor_id: String! - "The state of the monitor and its associated details." - state: State! - histogram: SummaryHistogram - } - - "The primary object returned for monitor states." - type MonitorSummaryResult { - "Used to go to the next page of results" - prevPagePagination: String - "Used to go to the previous page of results" - nextPagePagination: String - "The objects representing the state of a series of heartbeat monitors." - summaries: [MonitorSummary!] - "The number of summaries." - totalSummaryCount: Int! - } - - enum CursorDirection { - AFTER - BEFORE - } - - enum SortOrder { - ASC - DESC - } - - extend type Query { - "Fetches the current state of Uptime monitors for the given parameters." - getMonitorStates( - dateRangeStart: String! - dateRangeEnd: String! - pagination: String - filters: String - statusFilter: String - pageSize: Int - ): MonitorSummaryResult - } -`; diff --git a/x-pack/plugins/uptime/server/graphql/pings/index.ts b/x-pack/plugins/uptime/server/graphql/pings/index.ts deleted file mode 100644 index 57ec3242a7aa9..0000000000000 --- a/x-pack/plugins/uptime/server/graphql/pings/index.ts +++ /dev/null @@ -1,8 +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. - */ - -export { createPingsResolvers } from './resolvers'; -export { pingsSchema } from './schema.gql'; diff --git a/x-pack/plugins/uptime/server/graphql/pings/resolvers.ts b/x-pack/plugins/uptime/server/graphql/pings/resolvers.ts deleted file mode 100644 index 2bb1e13bc4b1f..0000000000000 --- a/x-pack/plugins/uptime/server/graphql/pings/resolvers.ts +++ /dev/null @@ -1,59 +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 { UMResolver } from '../../../../../legacy/plugins/uptime/common/graphql/resolver_types'; -import { - AllPingsQueryArgs, - PingResults, -} from '../../../../../legacy/plugins/uptime/common/graphql/types'; -import { UMServerLibs } from '../../lib/lib'; -import { UMContext } from '../types'; -import { CreateUMGraphQLResolvers } from '../types'; -import { savedObjectsAdapter } from '../../lib/saved_objects'; - -export type UMAllPingsResolver = UMResolver< - PingResults | Promise, - any, - AllPingsQueryArgs, - UMContext ->; - -export interface UMPingResolver { - allPings: () => PingResults; -} - -export const createPingsResolvers: CreateUMGraphQLResolvers = ( - libs: UMServerLibs -): { - Query: { - allPings: UMAllPingsResolver; - }; -} => ({ - Query: { - async allPings( - _resolver, - { monitorId, sort, size, status, dateRangeStart, dateRangeEnd, location, page }, - { APICaller, savedObjectsClient } - ): Promise { - const dynamicSettings = await savedObjectsAdapter.getUptimeDynamicSettings( - savedObjectsClient - ); - - return await libs.requests.getPings({ - callES: APICaller, - dynamicSettings, - dateRangeStart, - dateRangeEnd, - monitorId, - status, - sort, - size, - location, - page, - }); - }, - }, -}); diff --git a/x-pack/plugins/uptime/server/graphql/pings/schema.gql.ts b/x-pack/plugins/uptime/server/graphql/pings/schema.gql.ts deleted file mode 100644 index 25767fb544104..0000000000000 --- a/x-pack/plugins/uptime/server/graphql/pings/schema.gql.ts +++ /dev/null @@ -1,290 +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 gql from 'graphql-tag'; - -export const pingsSchema = gql` - schema { - query: Query - } - - type PingResults { - "Total number of matching pings" - total: UnsignedInteger! - "Unique list of all locations the query matched" - locations: [String!]! - "List of pings " - pings: [Ping!]! - } - - type Query { - "Get a list of all recorded pings for all monitors" - allPings( - "Optional: the direction to sort by. Accepts 'asc' and 'desc'. Defaults to 'desc'." - sort: String - "Optional: the number of results to return." - size: Int - "Optional: the monitor ID filter." - monitorId: String - "Optional: the check status to filter by." - status: String - "The lower limit of the date range." - dateRangeStart: String! - "The upper limit of the date range." - dateRangeEnd: String! - "Optional: agent location to filter by." - location: String - "Optional: current page." - page: Int - ): PingResults! - } - - type ContainerImage { - name: String - tag: String - } - - type Container { - id: String - image: ContainerImage - name: String - runtime: String - } - - type DocCount { - count: UnsignedInteger! - } - - "The monitor's status for a ping" - type Duration { - us: UnsignedInteger - } - - "An agent for recording a beat" - type Beat { - hostname: String - name: String - timezone: String - type: String - } - - type Docker { - id: String - image: String - name: String - } - - type ECS { - version: String - } - - type Error { - code: Int - message: String - type: String - } - - type OS { - family: String - kernel: String - platform: String - version: String - name: String - build: String - } - - "Geolocation data added via processors to enrich events." - type Geo { - "Name of the city in which the agent is running." - city_name: String - "The name of the continent on which the agent is running." - continent_name: String - "ISO designation for the agent's country." - country_iso_code: String - "The name of the agent's country." - country_name: String - "The lat/long of the agent." - location: String - "A name for the host's location, e.g. 'us-east-1' or 'LAX'." - name: String - "ISO designation of the agent's region." - region_iso_code: String - "Name of the region hosting the agent." - region_name: String - } - - type Host { - architecture: String - id: String - hostname: String - ip: String - mac: String - name: String - os: OS - } - - type HttpRTT { - content: Duration - response_header: Duration - total: Duration - validate: Duration - validate_body: Duration - write_request: Duration - } - - type HTTPBody { - "Size of HTTP response body in bytes" - bytes: UnsignedInteger - "Hash of the HTTP response body" - hash: String - "Response body of the HTTP Response. May be truncated based on client settings." - content: String - "Byte length of the content string, taking into account multibyte chars." - content_bytes: UnsignedInteger - } - - type HTTPResponse { - status_code: UnsignedInteger - body: HTTPBody - } - - type HTTP { - response: HTTPResponse - rtt: HttpRTT - url: String - } - - type ICMP { - requests: Int - rtt: Int - } - - type KubernetesContainer { - image: String - name: String - } - - type KubernetesNode { - name: String - } - - type KubernetesPod { - name: String - uid: String - } - - type Kubernetes { - container: KubernetesContainer - namespace: String - node: KubernetesNode - pod: KubernetesPod - } - - type MetaCloud { - availability_zone: String - instance_id: String - instance_name: String - machine_type: String - project_id: String - provider: String - region: String - } - - type Meta { - cloud: MetaCloud - } - - type Monitor { - duration: Duration - host: String - "The id of the monitor" - id: String - "The IP pinged by the monitor" - ip: String - "The name of the protocol being monitored" - name: String - "The protocol scheme of the monitored host" - scheme: String - "The status of the monitored host" - status: String - "The type of host being monitored" - type: String - check_group: String - } - - "Metadata added by a proccessor, which is specified in its configuration." - type Observer { - "Geolocation data for the agent." - geo: Geo - } - - type Resolve { - host: String - ip: String - rtt: Duration - } - - type RTT { - connect: Duration - handshake: Duration - validate: Duration - } - - type Socks5 { - rtt: RTT - } - - type TCP { - port: Int - rtt: RTT - } - - "Contains monitor transmission encryption information." - type PingTLS { - "The date and time after which the certificate is invalid." - certificate_not_valid_after: String - certificate_not_valid_before: String - certificates: String - rtt: RTT - } - - type URL { - full: String - scheme: String - domain: String - port: Int - path: String - query: String - } - - "A request sent from a monitor to a host" - type Ping { - "unique ID for this ping" - id: String! - "The timestamp of the ping's creation" - timestamp: String! - "The agent that recorded the ping" - beat: Beat - container: Container - docker: Docker - ecs: ECS - error: Error - host: Host - http: HTTP - icmp: ICMP - kubernetes: Kubernetes - meta: Meta - monitor: Monitor - observer: Observer - resolve: Resolve - socks5: Socks5 - summary: Summary - tags: String - tcp: TCP - tls: PingTLS - url: URL - } -`; diff --git a/x-pack/plugins/uptime/server/graphql/types.ts b/x-pack/plugins/uptime/server/graphql/types.ts deleted file mode 100644 index 5f0a6749eb599..0000000000000 --- a/x-pack/plugins/uptime/server/graphql/types.ts +++ /dev/null @@ -1,23 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { RequestHandlerContext, CallAPIOptions, SavedObjectsClient } from 'src/core/server'; -import { UMServerLibs } from '../lib/lib'; - -export type UMContext = RequestHandlerContext & { - APICaller: ( - endpoint: string, - clientParams?: Record, - options?: CallAPIOptions | undefined - ) => Promise; - savedObjectsClient: SavedObjectsClient; -}; - -export interface UMGraphQLResolver { - Query?: any; -} - -export type CreateUMGraphQLResolvers = (libs: UMServerLibs) => any; diff --git a/x-pack/plugins/uptime/server/graphql/unsigned_int_scalar/__tests__/parse_literal.test.ts b/x-pack/plugins/uptime/server/graphql/unsigned_int_scalar/__tests__/parse_literal.test.ts deleted file mode 100644 index 7c357abcf8e1d..0000000000000 --- a/x-pack/plugins/uptime/server/graphql/unsigned_int_scalar/__tests__/parse_literal.test.ts +++ /dev/null @@ -1,42 +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 { parseLiteral } from '../resolvers'; - -describe('parseLiteral', () => { - it('parses string literal of type IntValue', () => { - const result = parseLiteral({ - kind: 'IntValue', - value: '1562605032000', - }); - expect(result).toBe(1562605032000); - }); - - it('parses string literal of type FloatValue', () => { - const result = parseLiteral({ - kind: 'FloatValue', - value: '1562605032000.0', - }); - expect(result).toBe(1562605032000); - }); - - it('parses string literal of type String', () => { - const result = parseLiteral({ - kind: 'StringValue', - value: '1562605032000', - }); - expect(result).toBe(1562605032000); - }); - - it('returns `null` for unsupported types', () => { - expect( - parseLiteral({ - kind: 'EnumValue', - value: 'false', - }) - ).toBeNull(); - }); -}); diff --git a/x-pack/plugins/uptime/server/graphql/unsigned_int_scalar/__tests__/parse_value.test.ts b/x-pack/plugins/uptime/server/graphql/unsigned_int_scalar/__tests__/parse_value.test.ts deleted file mode 100644 index cb8958918c6dc..0000000000000 --- a/x-pack/plugins/uptime/server/graphql/unsigned_int_scalar/__tests__/parse_value.test.ts +++ /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 { parseValue } from '../resolvers'; - -describe('parseValue', () => { - it(`parses a number value and returns it if its > 0`, () => { - const result = parseValue('1562605032000'); - expect(result).toBe(1562605032000); - }); - - it(`parses a number and returns null if its value is < 0`, () => { - const result = parseValue('-1562605032000'); - expect(result).toBeNull(); - }); -}); diff --git a/x-pack/plugins/uptime/server/graphql/unsigned_int_scalar/__tests__/serialize.test.ts b/x-pack/plugins/uptime/server/graphql/unsigned_int_scalar/__tests__/serialize.test.ts deleted file mode 100644 index 2271d12ee7e13..0000000000000 --- a/x-pack/plugins/uptime/server/graphql/unsigned_int_scalar/__tests__/serialize.test.ts +++ /dev/null @@ -1,24 +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 { serialize } from '../resolvers'; - -describe('serialize', () => { - it('serializes date strings correctly', () => { - const result = serialize('2019-07-08T16:59:09.796Z'); - expect(result).toBe(1562605149796); - }); - - it('serializes timestamp strings correctly', () => { - const result = serialize('1562605032000'); - expect(result).toBe(1562605032000); - }); - - it('serializes non-date and non-numeric values to NaN', () => { - const result = serialize('foo'); - expect(result).toBeNaN(); - }); -}); diff --git a/x-pack/plugins/uptime/server/graphql/unsigned_int_scalar/index.ts b/x-pack/plugins/uptime/server/graphql/unsigned_int_scalar/index.ts deleted file mode 100644 index a8819f543a851..0000000000000 --- a/x-pack/plugins/uptime/server/graphql/unsigned_int_scalar/index.ts +++ /dev/null @@ -1,8 +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. - */ - -export { unsignedIntegerResolverFunctions } from './resolvers'; -export { unsignedIntegerSchema } from './schema.gql'; diff --git a/x-pack/plugins/uptime/server/graphql/unsigned_int_scalar/resolvers.ts b/x-pack/plugins/uptime/server/graphql/unsigned_int_scalar/resolvers.ts deleted file mode 100644 index 9b8fe145e7ff5..0000000000000 --- a/x-pack/plugins/uptime/server/graphql/unsigned_int_scalar/resolvers.ts +++ /dev/null @@ -1,51 +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 { GraphQLScalarType, Kind, ValueNode } from 'graphql'; -import { UMServerLibs } from '../../lib/lib'; -import { CreateUMGraphQLResolvers } from '../types'; - -export const serialize = (value: any): number => { - // `parseInt` will yield `2019` for a value such as "2019-07-08T16:59:09.796Z" - if (isNaN(Number(value))) { - return Date.parse(value); - } - return parseInt(value, 10); -}; - -export const parseValue = (value: any) => { - const parsed = parseInt(value, 10); - if (parsed < 0) { - return null; - } - return parsed; -}; - -export const parseLiteral = (ast: ValueNode) => { - switch (ast.kind) { - case Kind.INT: - case Kind.FLOAT: - case Kind.STRING: - return parseInt(ast.value, 10); - } - return null; -}; - -const unsignedIntegerScalar = new GraphQLScalarType({ - name: 'UnsignedInteger', - description: 'Represents an unsigned 32-bit integer', - serialize, - parseValue, - parseLiteral, -}); - -/** - * This scalar resolver will parse an integer string of > 32 bits and return a value of type `number`. - * This assumes that the code is running in an environment that supports big ints. - */ -export const unsignedIntegerResolverFunctions: CreateUMGraphQLResolvers = (libs: UMServerLibs) => ({ - UnsignedInteger: unsignedIntegerScalar, -}); diff --git a/x-pack/plugins/uptime/server/graphql/unsigned_int_scalar/schema.gql.ts b/x-pack/plugins/uptime/server/graphql/unsigned_int_scalar/schema.gql.ts deleted file mode 100644 index 6af2c8bc8827f..0000000000000 --- a/x-pack/plugins/uptime/server/graphql/unsigned_int_scalar/schema.gql.ts +++ /dev/null @@ -1,11 +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 gql from 'graphql-tag'; - -export const unsignedIntegerSchema = gql` - scalar UnsignedInteger -`; diff --git a/x-pack/plugins/uptime/server/lib/adapters/framework/adapter_types.ts b/x-pack/plugins/uptime/server/lib/adapters/framework/adapter_types.ts index 47fe5f2af4263..98c6be5aa3c8e 100644 --- a/x-pack/plugins/uptime/server/lib/adapters/framework/adapter_types.ts +++ b/x-pack/plugins/uptime/server/lib/adapters/framework/adapter_types.ts @@ -4,7 +4,6 @@ * you may not use this file except in compliance with the Elastic License. */ -import { GraphQLSchema } from 'graphql'; import { UsageCollectionSetup } from 'src/plugins/usage_collection/server'; import { IRouter, @@ -44,5 +43,4 @@ export interface UptimeCorePlugins { export interface UMBackendFrameworkAdapter { registerRoute(route: UMKibanaRoute): void; - registerGraphQLEndpoint(routePath: string, schema: GraphQLSchema): void; } diff --git a/x-pack/plugins/uptime/server/lib/adapters/framework/kibana_framework_adapter.ts b/x-pack/plugins/uptime/server/lib/adapters/framework/kibana_framework_adapter.ts index 1f92c8212b393..0176471aec1be 100644 --- a/x-pack/plugins/uptime/server/lib/adapters/framework/kibana_framework_adapter.ts +++ b/x-pack/plugins/uptime/server/lib/adapters/framework/kibana_framework_adapter.ts @@ -4,9 +4,6 @@ * you may not use this file except in compliance with the Elastic License. */ -import { GraphQLSchema } from 'graphql'; -import { schema as kbnSchema } from '@kbn/config-schema'; -import { runHttpQuery } from 'apollo-server-core'; import { UptimeCoreSetup } from './adapter_types'; import { UMBackendFrameworkAdapter } from './adapter_types'; import { UMKibanaRoute } from '../../../rest_api'; @@ -33,71 +30,4 @@ export class UMKibanaBackendFrameworkAdapter implements UMBackendFrameworkAdapte throw new Error(`Handler for method ${method} is not defined`); } } - - public registerGraphQLEndpoint(routePath: string, schema: GraphQLSchema): void { - this.server.route.post( - { - path: routePath, - validate: { - body: kbnSchema.object({ - operationName: kbnSchema.nullable(kbnSchema.string()), - query: kbnSchema.string(), - variables: kbnSchema.recordOf(kbnSchema.string(), kbnSchema.any()), - }), - }, - options: { - tags: ['access:uptime-read'], - }, - }, - async (context, request, resp): Promise => { - const { - core: { - elasticsearch: { - dataClient: { callAsCurrentUser }, - }, - }, - } = context; - const options = { - graphQLOptions: (_req: any) => { - return { - context: { - ...context, - APICaller: callAsCurrentUser, - savedObjectsClient: context.core.savedObjects.client, - }, - schema, - }; - }, - path: routePath, - route: { - tags: ['access:uptime-read'], - }, - }; - try { - const query = request.body as Record; - - const graphQLResponse = await runHttpQuery([request], { - method: 'POST', - options: options.graphQLOptions, - query, - }); - - return resp.ok({ - body: graphQLResponse, - headers: { - 'content-type': 'application/json', - }, - }); - } catch (error) { - if (error.isGraphQLError === true) { - return resp.internalError({ - body: { message: error.message }, - headers: { 'content-type': 'application/json' }, - }); - } - return resp.internalError(); - } - } - ); - } } diff --git a/x-pack/plugins/uptime/server/lib/alerts/__tests__/status_check.test.ts b/x-pack/plugins/uptime/server/lib/alerts/__tests__/status_check.test.ts index 08a3bc75fa8bd..2cc6f23ebaae5 100644 --- a/x-pack/plugins/uptime/server/lib/alerts/__tests__/status_check.test.ts +++ b/x-pack/plugins/uptime/server/lib/alerts/__tests__/status_check.test.ts @@ -17,6 +17,7 @@ import { IRouter } from 'kibana/server'; import { UMServerLibs } from '../../lib'; import { UptimeCoreSetup } from '../../adapters'; import { defaultDynamicSettings } from '../../../../../../legacy/plugins/uptime/common/runtime_types'; +import { alertsMock, AlertServicesMock } from '../../../../../alerting/server/mocks'; /** * The alert takes some dependencies as parameters; these are things like @@ -44,16 +45,21 @@ const bootstrapDependencies = (customRequests?: any) => { */ const mockOptions = ( params = { numTimes: 5, locations: [], timerange: { from: 'now-15m', to: 'now' } }, - services = { callCluster: 'mockESFunction', savedObjectsClient: mockSavedObjectsClient }, + services = alertsMock.createAlertServices(), state = {} -): any => ({ - params, - services, - state, -}); - -const mockSavedObjectsClient = { get: jest.fn() }; -mockSavedObjectsClient.get.mockReturnValue(defaultDynamicSettings); +): any => { + services.savedObjectsClient.get.mockResolvedValue({ + id: '', + type: '', + references: [], + attributes: defaultDynamicSettings, + }); + return { + params, + services, + state, + }; +}; describe('status check alert', () => { let toISOStringSpy: jest.SpyInstance; @@ -80,8 +86,14 @@ describe('status check alert', () => { expect(mockGetter.mock.calls[0]).toMatchInlineSnapshot(` Array [ Object { - "callES": "mockESFunction", - "dynamicSettings": undefined, + "callES": [MockFunction], + "dynamicSettings": Object { + "certificatesThresholds": Object { + "errorState": 7, + "warningState": 30, + }, + "heartbeatIndices": "heartbeat-8*", + }, "locations": Array [], "numTimes": 5, "timerange": Object { @@ -112,27 +124,23 @@ describe('status check alert', () => { ]); const { server, libs } = bootstrapDependencies({ getMonitorStatus: mockGetter }); const alert = statusCheckAlertFactory(server, libs); - const mockInstanceFactory = jest.fn(); - const mockReplaceState = jest.fn(); - const mockScheduleActions = jest.fn(); - mockInstanceFactory.mockReturnValue({ - replaceState: mockReplaceState, - scheduleActions: mockScheduleActions, - }); const options = mockOptions(); - options.services = { - ...options.services, - alertInstanceFactory: mockInstanceFactory, - }; + const alertServices: AlertServicesMock = options.services; // @ts-ignore the executor can return `void`, but ours never does const state: Record = await alert.executor(options); expect(mockGetter).toHaveBeenCalledTimes(1); - expect(mockInstanceFactory).toHaveBeenCalledTimes(1); + expect(alertServices.alertInstanceFactory).toHaveBeenCalledTimes(1); expect(mockGetter.mock.calls[0]).toMatchInlineSnapshot(` Array [ Object { - "callES": "mockESFunction", - "dynamicSettings": undefined, + "callES": [MockFunction], + "dynamicSettings": Object { + "certificatesThresholds": Object { + "errorState": 7, + "warningState": 30, + }, + "heartbeatIndices": "heartbeat-8*", + }, "locations": Array [], "numTimes": 5, "timerange": Object { @@ -142,8 +150,9 @@ describe('status check alert', () => { }, ] `); - expect(mockReplaceState).toHaveBeenCalledTimes(1); - expect(mockReplaceState.mock.calls[0]).toMatchInlineSnapshot(` + const [{ value: alertInstanceMock }] = alertServices.alertInstanceFactory.mock.results; + expect(alertInstanceMock.replaceState).toHaveBeenCalledTimes(1); + expect(alertInstanceMock.replaceState.mock.calls[0]).toMatchInlineSnapshot(` Array [ Object { "currentTriggerStarted": "foo date string", @@ -170,8 +179,8 @@ describe('status check alert', () => { }, ] `); - expect(mockScheduleActions).toHaveBeenCalledTimes(1); - expect(mockScheduleActions.mock.calls[0]).toMatchInlineSnapshot(` + expect(alertInstanceMock.scheduleActions).toHaveBeenCalledTimes(1); + expect(alertInstanceMock.scheduleActions.mock.calls[0]).toMatchInlineSnapshot(` Array [ "xpack.uptime.alerts.actionGroups.monitorStatus", Object { diff --git a/x-pack/plugins/uptime/server/lib/requests/__tests__/__snapshots__/get_monitor_charts.test.ts.snap b/x-pack/plugins/uptime/server/lib/requests/__tests__/__snapshots__/get_monitor_charts.test.ts.snap index 7b717949c70c5..97b97f8440758 100644 --- a/x-pack/plugins/uptime/server/lib/requests/__tests__/__snapshots__/get_monitor_charts.test.ts.snap +++ b/x-pack/plugins/uptime/server/lib/requests/__tests__/__snapshots__/get_monitor_charts.test.ts.snap @@ -15,13 +15,6 @@ Array [ "field": "monitor.duration.us", }, }, - "status": Object { - "terms": Object { - "field": "monitor.status", - "shard_size": 2, - "size": 2, - }, - }, }, "terms": Object { "field": "observer.geo.name", @@ -55,8 +48,10 @@ Array [ }, }, Object { - "term": Object { - "monitor.status": "up", + "range": Object { + "monitor.duration.us": Object { + "gt": 0, + }, }, }, ], @@ -71,7 +66,6 @@ Array [ exports[`ElasticsearchMonitorsAdapter inserts empty buckets for missing data 1`] = ` Object { - "durationMaxValue": 0, "locationDurationLines": Array [ Object { "line": Array [ @@ -244,128 +238,5 @@ Object { "name": "us-west-4", }, ], - "status": Array [ - Object { - "down": null, - "total": 4, - "up": null, - "x": 1568411568000, - }, - Object { - "down": null, - "total": 0, - "up": null, - "x": 1568411604000, - }, - Object { - "down": null, - "total": 8, - "up": null, - "x": 1568411640000, - }, - Object { - "down": null, - "total": 8, - "up": null, - "x": 1568411784000, - }, - Object { - "down": null, - "total": 0, - "up": null, - "x": 1568411820000, - }, - Object { - "down": null, - "total": 0, - "up": null, - "x": 1568411856000, - }, - Object { - "down": null, - "total": 0, - "up": null, - "x": 1568411892000, - }, - Object { - "down": null, - "total": 4, - "up": null, - "x": 1568411928000, - }, - Object { - "down": null, - "total": 7, - "up": null, - "x": 1568411964000, - }, - Object { - "down": null, - "total": 5, - "up": null, - "x": 1568412036000, - }, - Object { - "down": null, - "total": 4, - "up": null, - "x": 1568412072000, - }, - Object { - "down": null, - "total": 3, - "up": null, - "x": 1568412108000, - }, - Object { - "down": null, - "total": 4, - "up": null, - "x": 1568412144000, - }, - Object { - "down": null, - "total": 4, - "up": null, - "x": 1568412180000, - }, - Object { - "down": null, - "total": 3, - "up": null, - "x": 1568412216000, - }, - Object { - "down": null, - "total": 1, - "up": null, - "x": 1568412252000, - }, - Object { - "down": null, - "total": 3, - "up": null, - "x": 1568412288000, - }, - Object { - "down": null, - "total": 8, - "up": null, - "x": 1568412324000, - }, - Object { - "down": null, - "total": 8, - "up": null, - "x": 1568412432000, - }, - Object { - "down": null, - "total": 1, - "up": null, - "x": 1568412468000, - }, - ], - "statusMaxCount": 0, } `; diff --git a/x-pack/plugins/uptime/server/lib/requests/__tests__/get_certs.test.ts b/x-pack/plugins/uptime/server/lib/requests/__tests__/get_certs.test.ts new file mode 100644 index 0000000000000..539344dfca791 --- /dev/null +++ b/x-pack/plugins/uptime/server/lib/requests/__tests__/get_certs.test.ts @@ -0,0 +1,208 @@ +/* + * 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 { getCerts } from '../get_certs'; + +describe('getCerts', () => { + let mockHits: any; + let mockCallES: jest.Mock; + + beforeEach(() => { + mockHits = [ + { + _index: 'heartbeat-8.0.0-2020.04.16-000001', + _id: 'DJwmhHEBnyP8RKDrEYVK', + _score: 0, + _source: { + tls: { + certificate_not_valid_before: '2019-08-16T01:40:25.000Z', + server: { + x509: { + subject: { + common_name: 'r2.shared.global.fastly.net', + }, + issuer: { + common_name: 'GlobalSign CloudSSL CA - SHA256 - G3', + }, + }, + hash: { + sha1: 'b7b4b89ef0d0caf39d223736f0fdbb03c7b426f1', + sha256: '12b00d04db0db8caa302bfde043e88f95baceb91e86ac143e93830b4bbec726d', + }, + }, + certificate_not_valid_after: '2020-07-16T03:15:39.000Z', + }, + monitor: { + name: 'Real World Test', + id: 'real-world-test', + }, + }, + fields: { + 'tls.server.hash.sha256': [ + '12b00d04db0db8caa302bfde043e88f95baceb91e86ac143e93830b4bbec726d', + ], + }, + inner_hits: { + monitors: { + hits: { + total: { + value: 32, + relation: 'eq', + }, + max_score: null, + hits: [ + { + _index: 'heartbeat-8.0.0-2020.04.16-000001', + _id: 'DJwmhHEBnyP8RKDrEYVK', + _score: null, + _source: { + monitor: { + name: 'Real World Test', + id: 'real-world-test', + }, + }, + fields: { + 'monitor.id': ['real-world-test'], + }, + sort: ['real-world-test'], + }, + ], + }, + }, + }, + }, + ]; + mockCallES = jest.fn(); + mockCallES.mockImplementation(() => ({ + hits: { + hits: mockHits, + }, + })); + }); + + it('parses query result and returns expected values', async () => { + const result = await getCerts({ + callES: mockCallES, + dynamicSettings: { heartbeatIndices: 'heartbeat*' }, + index: 1, + from: 'now-2d', + to: 'now+1h', + search: 'my_common_name', + size: 30, + }); + expect(result).toMatchInlineSnapshot(` + Array [ + Object { + "certificate_not_valid_after": "2020-07-16T03:15:39.000Z", + "certificate_not_valid_before": "2019-08-16T01:40:25.000Z", + "common_name": "r2.shared.global.fastly.net", + "issuer": "GlobalSign CloudSSL CA - SHA256 - G3", + "monitors": Array [ + Object { + "id": "real-world-test", + "name": "Real World Test", + }, + ], + "sha1": "b7b4b89ef0d0caf39d223736f0fdbb03c7b426f1", + "sha256": "12b00d04db0db8caa302bfde043e88f95baceb91e86ac143e93830b4bbec726d", + }, + ] + `); + expect(mockCallES.mock.calls).toMatchInlineSnapshot(` + Array [ + Array [ + "search", + Object { + "body": Object { + "_source": Array [ + "monitor.id", + "monitor.name", + "tls.server.x509.issuer.common_name", + "tls.server.x509.subject.common_name", + "tls.server.hash.sha1", + "tls.server.hash.sha256", + "tls.certificate_not_valid_before", + "tls.certificate_not_valid_after", + ], + "collapse": Object { + "field": "tls.server.hash.sha256", + "inner_hits": Object { + "_source": Object { + "includes": Array [ + "monitor.id", + "monitor.name", + ], + }, + "collapse": Object { + "field": "monitor.id", + }, + "name": "monitors", + "sort": Array [ + Object { + "monitor.id": "asc", + }, + ], + }, + }, + "from": 1, + "query": Object { + "bool": Object { + "filter": Array [ + Object { + "exists": Object { + "field": "tls", + }, + }, + Object { + "range": Object { + "@timestamp": Object { + "gte": "now-2d", + "lte": "now+1h", + }, + }, + }, + ], + "should": Array [ + Object { + "wildcard": Object { + "tls.server.issuer": Object { + "value": "*my_common_name*", + }, + }, + }, + Object { + "wildcard": Object { + "tls.common_name": Object { + "value": "*my_common_name*", + }, + }, + }, + Object { + "wildcard": Object { + "monitor.id": Object { + "value": "*my_common_name*", + }, + }, + }, + Object { + "wildcard": Object { + "monitor.name": Object { + "value": "*my_common_name*", + }, + }, + }, + ], + }, + }, + "size": 30, + }, + "index": "heartbeat*", + }, + ], + ] + `); + }); +}); diff --git a/x-pack/plugins/uptime/server/lib/requests/__tests__/get_latest_monitor.test.ts b/x-pack/plugins/uptime/server/lib/requests/__tests__/get_latest_monitor.test.ts index 112c8e97d4c00..cf8414a3b0a68 100644 --- a/x-pack/plugins/uptime/server/lib/requests/__tests__/get_latest_monitor.test.ts +++ b/x-pack/plugins/uptime/server/lib/requests/__tests__/get_latest_monitor.test.ts @@ -42,10 +42,16 @@ describe('getLatestMonitor', () => { hits: { hits: [ { + _id: 'fejwio32', _source: { - timestamp: 123456, + '@timestamp': '123456', monitor: { + duration: { + us: 12345, + }, id: 'testMonitor', + status: 'down', + type: 'http', }, }, }, @@ -64,7 +70,22 @@ describe('getLatestMonitor', () => { monitorId: 'testMonitor', }); - expect(result.timestamp).toBe(123456); + expect(result).toMatchInlineSnapshot(` + Object { + "@timestamp": "123456", + "docId": "fejwio32", + "monitor": Object { + "duration": Object { + "us": 12345, + }, + "id": "testMonitor", + "status": "down", + "type": "http", + }, + "timestamp": "123456", + } + `); + expect(result.timestamp).toBe('123456'); expect(result.monitor).not.toBeFalsy(); expect(result?.monitor?.id).toBe('testMonitor'); expect(mockEsClient).toHaveBeenCalledWith('search', expectedGetLatestSearchParams); diff --git a/x-pack/plugins/uptime/server/lib/requests/__tests__/get_pings.test.ts b/x-pack/plugins/uptime/server/lib/requests/__tests__/get_pings.test.ts index 9145ccca1b6d1..fcf773db23de6 100644 --- a/x-pack/plugins/uptime/server/lib/requests/__tests__/get_pings.test.ts +++ b/x-pack/plugins/uptime/server/lib/requests/__tests__/get_pings.test.ts @@ -17,16 +17,34 @@ describe('getAll', () => { { _source: { '@timestamp': '2018-10-30T18:51:59.792Z', + monitor: { + duration: { us: 2134 }, + id: 'foo', + status: 'up', + type: 'http', + }, }, }, { _source: { '@timestamp': '2018-10-30T18:53:59.792Z', + monitor: { + duration: { us: 2131 }, + id: 'foo', + status: 'up', + type: 'http', + }, }, }, { _source: { '@timestamp': '2018-10-30T18:55:59.792Z', + monitor: { + duration: { us: 2132 }, + id: 'foo', + status: 'up', + type: 'http', + }, }, }, ]; @@ -48,7 +66,7 @@ describe('getAll', () => { body: { query: { bool: { - filter: [{ range: { '@timestamp': { gte: 'now-1h', lte: 'now' } } }], + filter: [{ range: { timestamp: { gte: 'now-1h', lte: 'now' } } }], }, }, aggregations: { @@ -60,8 +78,7 @@ describe('getAll', () => { }, }, }, - sort: [{ '@timestamp': { order: 'desc' } }], - size: 12, + sort: [{ timestamp: { order: 'desc' } }], }, }; }); @@ -72,8 +89,7 @@ describe('getAll', () => { const result = await getPings({ callES: mockEsClient, dynamicSettings: defaultDynamicSettings, - dateRangeStart: 'now-1h', - dateRangeEnd: 'now', + dateRange: { from: 'now-1h', to: 'now' }, sort: 'asc', size: 12, }); @@ -95,15 +111,54 @@ describe('getAll', () => { await getPings({ callES: mockEsClient, dynamicSettings: defaultDynamicSettings, - dateRangeStart: 'now-1h', - dateRangeEnd: 'now', + dateRange: { from: 'now-1h', to: 'now' }, sort: 'asc', size: 12, }); - set(expectedGetAllParams, 'body.sort[0]', { '@timestamp': { order: 'asc' } }); + set(expectedGetAllParams, 'body.sort[0]', { timestamp: { order: 'asc' } }); expect(mockEsClient).toHaveBeenCalledTimes(1); - expect(mockEsClient).toHaveBeenCalledWith('search', expectedGetAllParams); + expect(mockEsClient.mock.calls[0]).toMatchInlineSnapshot(` + Array [ + "search", + Object { + "body": Object { + "aggregations": Object { + "locations": Object { + "terms": Object { + "field": "observer.geo.name", + "missing": "N/A", + "size": 1000, + }, + }, + }, + "query": Object { + "bool": Object { + "filter": Array [ + Object { + "range": Object { + "@timestamp": Object { + "gte": "now-1h", + "lte": "now", + }, + }, + }, + ], + }, + }, + "size": 12, + "sort": Array [ + Object { + "@timestamp": Object { + "order": "asc", + }, + }, + ], + }, + "index": "heartbeat-8*", + }, + ] + `); }); it('omits the sort param when no sort passed', async () => { @@ -112,12 +167,52 @@ describe('getAll', () => { await getPings({ callES: mockEsClient, dynamicSettings: defaultDynamicSettings, - dateRangeStart: 'now-1h', - dateRangeEnd: 'now', + dateRange: { from: 'now-1h', to: 'now' }, size: 12, }); - expect(mockEsClient).toHaveBeenCalledWith('search', expectedGetAllParams); + expect(mockEsClient).toHaveBeenCalledTimes(1); + expect(mockEsClient.mock.calls[0]).toMatchInlineSnapshot(` + Array [ + "search", + Object { + "body": Object { + "aggregations": Object { + "locations": Object { + "terms": Object { + "field": "observer.geo.name", + "missing": "N/A", + "size": 1000, + }, + }, + }, + "query": Object { + "bool": Object { + "filter": Array [ + Object { + "range": Object { + "@timestamp": Object { + "gte": "now-1h", + "lte": "now", + }, + }, + }, + ], + }, + }, + "size": 12, + "sort": Array [ + Object { + "@timestamp": Object { + "order": "desc", + }, + }, + ], + }, + "index": "heartbeat-8*", + }, + ] + `); }); it('omits the size param when no size passed', async () => { @@ -126,14 +221,52 @@ describe('getAll', () => { await getPings({ callES: mockEsClient, dynamicSettings: defaultDynamicSettings, - dateRangeStart: 'now-1h', - dateRangeEnd: 'now', + dateRange: { from: 'now-1h', to: 'now' }, sort: 'desc', }); - delete expectedGetAllParams.body.size; - set(expectedGetAllParams, 'body.sort[0].@timestamp.order', 'desc'); - expect(mockEsClient).toHaveBeenCalledWith('search', expectedGetAllParams); + expect(mockEsClient).toHaveBeenCalledTimes(1); + expect(mockEsClient.mock.calls[0]).toMatchInlineSnapshot(` + Array [ + "search", + Object { + "body": Object { + "aggregations": Object { + "locations": Object { + "terms": Object { + "field": "observer.geo.name", + "missing": "N/A", + "size": 1000, + }, + }, + }, + "query": Object { + "bool": Object { + "filter": Array [ + Object { + "range": Object { + "@timestamp": Object { + "gte": "now-1h", + "lte": "now", + }, + }, + }, + ], + }, + }, + "size": 25, + "sort": Array [ + Object { + "@timestamp": Object { + "order": "desc", + }, + }, + ], + }, + "index": "heartbeat-8*", + }, + ] + `); }); it('adds a filter for monitor ID', async () => { @@ -142,14 +275,57 @@ describe('getAll', () => { await getPings({ callES: mockEsClient, dynamicSettings: defaultDynamicSettings, - dateRangeStart: 'now-1h', - dateRangeEnd: 'now', + dateRange: { from: 'now-1h', to: 'now' }, monitorId: 'testmonitorid', }); - delete expectedGetAllParams.body.size; - expectedGetAllParams.body.query.bool.filter.push({ term: { 'monitor.id': 'testmonitorid' } }); - expect(mockEsClient).toHaveBeenCalledWith('search', expectedGetAllParams); + expect(mockEsClient).toHaveBeenCalledTimes(1); + expect(mockEsClient.mock.calls[0]).toMatchInlineSnapshot(` + Array [ + "search", + Object { + "body": Object { + "aggregations": Object { + "locations": Object { + "terms": Object { + "field": "observer.geo.name", + "missing": "N/A", + "size": 1000, + }, + }, + }, + "query": Object { + "bool": Object { + "filter": Array [ + Object { + "range": Object { + "@timestamp": Object { + "gte": "now-1h", + "lte": "now", + }, + }, + }, + Object { + "term": Object { + "monitor.id": "testmonitorid", + }, + }, + ], + }, + }, + "size": 25, + "sort": Array [ + Object { + "@timestamp": Object { + "order": "desc", + }, + }, + ], + }, + "index": "heartbeat-8*", + }, + ] + `); }); it('adds a filter for monitor status', async () => { @@ -158,13 +334,56 @@ describe('getAll', () => { await getPings({ callES: mockEsClient, dynamicSettings: defaultDynamicSettings, - dateRangeStart: 'now-1h', - dateRangeEnd: 'now', + dateRange: { from: 'now-1h', to: 'now' }, status: 'down', }); - delete expectedGetAllParams.body.size; - expectedGetAllParams.body.query.bool.filter.push({ term: { 'monitor.status': 'down' } }); - expect(mockEsClient).toHaveBeenCalledWith('search', expectedGetAllParams); + expect(mockEsClient).toHaveBeenCalledTimes(1); + expect(mockEsClient.mock.calls[0]).toMatchInlineSnapshot(` + Array [ + "search", + Object { + "body": Object { + "aggregations": Object { + "locations": Object { + "terms": Object { + "field": "observer.geo.name", + "missing": "N/A", + "size": 1000, + }, + }, + }, + "query": Object { + "bool": Object { + "filter": Array [ + Object { + "range": Object { + "@timestamp": Object { + "gte": "now-1h", + "lte": "now", + }, + }, + }, + Object { + "term": Object { + "monitor.status": "down", + }, + }, + ], + }, + }, + "size": 25, + "sort": Array [ + Object { + "@timestamp": Object { + "order": "desc", + }, + }, + ], + }, + "index": "heartbeat-8*", + }, + ] + `); }); }); diff --git a/x-pack/plugins/uptime/server/lib/requests/get_certs.ts b/x-pack/plugins/uptime/server/lib/requests/get_certs.ts new file mode 100644 index 0000000000000..4f99fbe94d54c --- /dev/null +++ b/x-pack/plugins/uptime/server/lib/requests/get_certs.ts @@ -0,0 +1,135 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { UMElasticsearchQueryFn } from '../adapters'; +import { Cert, GetCertsParams } from '../../../../../legacy/plugins/uptime/common/runtime_types'; + +export const getCerts: UMElasticsearchQueryFn = async ({ + callES, + dynamicSettings, + index, + from, + to, + search, + size, +}) => { + const searchWrapper = `*${search}*`; + const params: any = { + index: dynamicSettings.heartbeatIndices, + body: { + from: index, + size, + query: { + bool: { + filter: [ + { + exists: { + field: 'tls', + }, + }, + { + range: { + '@timestamp': { + gte: from, + lte: to, + }, + }, + }, + ], + }, + }, + _source: [ + 'monitor.id', + 'monitor.name', + 'tls.server.x509.issuer.common_name', + 'tls.server.x509.subject.common_name', + 'tls.server.hash.sha1', + 'tls.server.hash.sha256', + 'tls.certificate_not_valid_before', + 'tls.certificate_not_valid_after', + ], + collapse: { + field: 'tls.server.hash.sha256', + inner_hits: { + _source: { + includes: ['monitor.id', 'monitor.name'], + }, + collapse: { + field: 'monitor.id', + }, + name: 'monitors', + sort: [{ 'monitor.id': 'asc' }], + }, + }, + }, + }; + + if (search) { + params.body.query.bool.should = [ + { + wildcard: { + 'tls.server.issuer': { + value: searchWrapper, + }, + }, + }, + { + wildcard: { + 'tls.common_name': { + value: searchWrapper, + }, + }, + }, + { + wildcard: { + 'monitor.id': { + value: searchWrapper, + }, + }, + }, + { + wildcard: { + 'monitor.name': { + value: searchWrapper, + }, + }, + }, + ]; + } + + const result = await callES('search', params); + const formatted = (result?.hits?.hits ?? []).map((hit: any) => { + const { + _source: { + tls: { + server: { + x509: { + issuer: { common_name: issuer }, + subject: { common_name }, + }, + hash: { sha1, sha256 }, + }, + certificate_not_valid_after, + certificate_not_valid_before, + }, + }, + } = hit; + const monitors = hit.inner_hits.monitors.hits.hits.map((monitor: any) => ({ + name: monitor._source?.monitor.name, + id: monitor._source?.monitor.id, + })); + return { + monitors, + certificate_not_valid_after, + certificate_not_valid_before, + issuer, + sha1, + sha256, + common_name, + }; + }); + return formatted; +}; diff --git a/x-pack/plugins/uptime/server/lib/requests/get_latest_monitor.ts b/x-pack/plugins/uptime/server/lib/requests/get_latest_monitor.ts index 299e3eb6ca3cf..a8e9ccb875a08 100644 --- a/x-pack/plugins/uptime/server/lib/requests/get_latest_monitor.ts +++ b/x-pack/plugins/uptime/server/lib/requests/get_latest_monitor.ts @@ -5,7 +5,7 @@ */ import { UMElasticsearchQueryFn } from '../adapters'; -import { Ping } from '../../../../../legacy/plugins/uptime/common/graphql/types'; +import { Ping } from '../../../../../legacy/plugins/uptime/common/runtime_types'; export interface GetLatestMonitorParams { /** @member dateRangeStart timestamp bounds */ @@ -53,11 +53,9 @@ export const getLatestMonitor: UMElasticsearchQueryFn { - let up = null; - let down = null; - - buckets.forEach((bucket: any) => { - if (bucket.key === 'up') { - up = bucket.doc_count; - } else if (bucket.key === 'down') { - down = bucket.doc_count; - } - }); - - return { - x: time, - up, - down, - total: docCount, - }; -}; - /** * Fetches data used to populate monitor charts */ @@ -55,7 +35,7 @@ export const getMonitorDurationChart: UMElasticsearchQueryFn< filter: [ { range: { '@timestamp': { gte: dateStart, lte: dateEnd } } }, { term: { 'monitor.id': monitorId } }, - { term: { 'monitor.status': 'up' } }, + { range: { 'monitor.duration.us': { gt: 0 } } }, ], }, }, @@ -73,7 +53,6 @@ export const getMonitorDurationChart: UMElasticsearchQueryFn< missing: 'N/A', }, aggs: { - status: { terms: { field: 'monitor.status', size: 2, shard_size: 2 } }, duration: { stats: { field: 'monitor.duration.us' } }, }, }, @@ -94,15 +73,10 @@ export const getMonitorDurationChart: UMElasticsearchQueryFn< * * The third list is for an area chart expressing a range, and it requires an (x,y,y0) structure, * where y0 is the min value for the point and y is the max. - * - * Additionally, we supply the maximum value for duration and status, so the corresponding charts know - * what the domain size should be. */ + const monitorChartsData: MonitorDurationResult = { locationDurationLines: [], - status: [], - durationMaxValue: 0, - statusMaxCount: 0, }; /** @@ -119,9 +93,9 @@ export const getMonitorDurationChart: UMElasticsearchQueryFn< // a set of all the locations found for this result const resultLocations = new Set(); const linesByLocation: { [key: string]: LocationDurationLine } = {}; + dateHistogramBuckets.forEach(dateHistogramBucket => { const x = dateHistogramBucket.key; - const docCount = dateHistogramBucket?.doc_count ?? 0; // a set of all the locations for the current bucket const bucketLocations = new Set(); @@ -161,10 +135,6 @@ export const getMonitorDurationChart: UMElasticsearchQueryFn< } }); } - - monitorChartsData.status.push( - formatStatusBuckets(x, dateHistogramBucket?.status?.buckets ?? [], docCount) - ); }); return monitorChartsData; diff --git a/x-pack/plugins/uptime/server/lib/requests/get_monitor_states.ts b/x-pack/plugins/uptime/server/lib/requests/get_monitor_states.ts index d7842d1a0b4aa..4b40943a85705 100644 --- a/x-pack/plugins/uptime/server/lib/requests/get_monitor_states.ts +++ b/x-pack/plugins/uptime/server/lib/requests/get_monitor_states.ts @@ -8,10 +8,11 @@ import { CONTEXT_DEFAULTS } from '../../../../../legacy/plugins/uptime/common/co import { fetchPage } from './search'; import { UMElasticsearchQueryFn } from '../adapters'; import { - MonitorSummary, SortOrder, CursorDirection, -} from '../../../../../legacy/plugins/uptime/common/graphql/types'; + MonitorSummary, +} from '../../../../../legacy/plugins/uptime/common/runtime_types'; + import { QueryContext } from './search'; export interface CursorPagination { diff --git a/x-pack/plugins/uptime/server/lib/requests/get_ping_histogram.ts b/x-pack/plugins/uptime/server/lib/requests/get_ping_histogram.ts index a49e4c8931142..5a8927764ea5c 100644 --- a/x-pack/plugins/uptime/server/lib/requests/get_ping_histogram.ts +++ b/x-pack/plugins/uptime/server/lib/requests/get_ping_histogram.ts @@ -7,8 +7,10 @@ import { UMElasticsearchQueryFn } from '../adapters'; import { QUERY } from '../../../../../legacy/plugins/uptime/common/constants'; import { getFilterClause } from '../helper'; -import { HistogramQueryResult } from './types'; -import { HistogramResult } from '../../../../../legacy/plugins/uptime/common/types'; +import { + HistogramResult, + HistogramQueryResult, +} from '../../../../../legacy/plugins/uptime/common/runtime_types'; export interface GetPingHistogramParams { /** @member dateRangeStart timestamp bounds */ diff --git a/x-pack/plugins/uptime/server/lib/requests/get_pings.ts b/x-pack/plugins/uptime/server/lib/requests/get_pings.ts index c64b5f3ad4af4..6eccfdb13cef7 100644 --- a/x-pack/plugins/uptime/server/lib/requests/get_pings.ts +++ b/x-pack/plugins/uptime/server/lib/requests/get_pings.ts @@ -6,52 +6,28 @@ import { UMElasticsearchQueryFn } from '../adapters/framework'; import { - PingResults, + GetPingsParams, + HttpResponseBody, + PingsResponse, Ping, - HttpBody, -} from '../../../../../legacy/plugins/uptime/common/graphql/types'; +} from '../../../../../legacy/plugins/uptime/common/runtime_types'; -export interface GetPingsParams { - /** @member dateRangeStart timestamp bounds */ - dateRangeStart: string; +const DEFAULT_PAGE_SIZE = 25; - /** @member dateRangeEnd timestamp bounds */ - dateRangeEnd: string; - - /** @member monitorId optional limit by monitorId */ - monitorId?: string | null; - - /** @member status optional limit by check statuses */ - status?: string | null; - - /** @member sort optional sort by timestamp */ - sort?: string | null; - - /** @member size optional limit query size */ - size?: number | null; - - /** @member location optional location value for use in filtering*/ - location?: string | null; - - /** @member page the number to provide to Elasticsearch as the "from" parameter */ - page?: number; -} - -export const getPings: UMElasticsearchQueryFn = async ({ +export const getPings: UMElasticsearchQueryFn = async ({ callES, dynamicSettings, - dateRangeStart, - dateRangeEnd, + dateRange: { from, to }, + index, monitorId, status, sort, - size, + size: sizeParam, location, - page, }) => { + const size = sizeParam ?? DEFAULT_PAGE_SIZE; const sortParam = { sort: [{ '@timestamp': { order: sort ?? 'desc' } }] }; - const sizeParam = size ? { size } : undefined; - const filter: any[] = [{ range: { '@timestamp': { gte: dateRangeStart, lte: dateRangeEnd } } }]; + const filter: any[] = [{ range: { '@timestamp': { gte: from, lte: to } } }]; if (monitorId) { filter.push({ term: { 'monitor.id': monitorId } }); } @@ -71,7 +47,7 @@ export const getPings: UMElasticsearchQueryFn = asy ...queryContext, }, ...sortParam, - ...sizeParam, + size, aggregations: { locations: { terms: { @@ -85,8 +61,8 @@ export const getPings: UMElasticsearchQueryFn = asy }, }; - if (page) { - params.body.from = page * (size ?? 25); + if (index) { + params.body.from = index * size; } const { @@ -96,25 +72,22 @@ export const getPings: UMElasticsearchQueryFn = asy const locations = aggs?.locations ?? { buckets: [{ key: 'N/A', doc_count: 0 }] }; - const pings: Ping[] = hits.map(({ _id, _source }: any) => { - const timestamp = _source['@timestamp']; - + const pings: Ping[] = hits.map((doc: any) => { + const { _id, _source } = doc; // Calculate here the length of the content string in bytes, this is easier than in client JS, where // we don't have access to Buffer.byteLength. There are some hacky ways to do this in the // client but this is cleaner. - const httpBody: HttpBody | undefined = _source?.http?.response?.body; + const httpBody: HttpResponseBody | undefined = _source?.http?.response?.body; if (httpBody && httpBody.content) { httpBody.content_bytes = Buffer.byteLength(httpBody.content); } - return { id: _id, timestamp, ..._source }; + return { ..._source, timestamp: _source['@timestamp'], docId: _id }; }); - const results: PingResults = { + return { total: total.value, locations: locations.buckets.map((bucket: { key: string }) => bucket.key), pings, }; - - return results; }; diff --git a/x-pack/plugins/uptime/server/lib/requests/index.ts b/x-pack/plugins/uptime/server/lib/requests/index.ts index 445adc3c15a93..243bb089cc7b4 100644 --- a/x-pack/plugins/uptime/server/lib/requests/index.ts +++ b/x-pack/plugins/uptime/server/lib/requests/index.ts @@ -4,6 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ +export { getCerts } from './get_certs'; export { getFilterBar, GetFilterBarParams } from './get_filter_bar'; export { getUptimeIndexPattern as getIndexPattern } from './get_index_pattern'; export { getLatestMonitor, GetLatestMonitorParams } from './get_latest_monitor'; @@ -13,7 +14,7 @@ export { getMonitorLocations, GetMonitorLocationsParams } from './get_monitor_lo export { getMonitorStates, GetMonitorStatesParams } from './get_monitor_states'; export { getMonitorStatus, GetMonitorStatusParams } from './get_monitor_status'; export * from './get_monitor_status'; -export { getPings, GetPingsParams } from './get_pings'; +export { getPings } from './get_pings'; export { getPingHistogram, GetPingHistogramParams } from './get_ping_histogram'; export { UptimeRequests } from './uptime_requests'; export { getSnapshotCount, GetSnapshotCountParams } from './get_snapshot_counts'; diff --git a/x-pack/plugins/uptime/server/lib/requests/search/__tests__/fetch_page.test.ts b/x-pack/plugins/uptime/server/lib/requests/search/__tests__/fetch_page.test.ts index f542773f32796..2a8f681ab3453 100644 --- a/x-pack/plugins/uptime/server/lib/requests/search/__tests__/fetch_page.test.ts +++ b/x-pack/plugins/uptime/server/lib/requests/search/__tests__/fetch_page.test.ts @@ -12,7 +12,7 @@ import { MonitorGroupsPage, } from '../fetch_page'; import { QueryContext } from '../query_context'; -import { MonitorSummary } from '../../../../../../../legacy/plugins/uptime/common/graphql/types'; +import { MonitorSummary } from '../../../../../../../legacy/plugins/uptime/common/runtime_types'; import { nextPagination, prevPagination, simpleQueryContext } from './test_helpers'; const simpleFixture: MonitorGroups[] = [ @@ -53,12 +53,16 @@ const simpleFetcher = (monitorGroups: MonitorGroups[]): MonitorGroupsFetcher => }; const simpleEnricher = (monitorGroups: MonitorGroups[]): MonitorEnricher => { - return async (queryContext: QueryContext, checkGroups: string[]): Promise => { + return async (_queryContext: QueryContext, checkGroups: string[]): Promise => { return checkGroups.map(cg => { const monitorGroup = monitorGroups.find(mg => mg.groups.some(g => g.checkGroup === cg))!; return { monitor_id: monitorGroup.id, - state: { summary: {}, timestamp: new Date(Date.parse('1999-12-31')).toISOString() }, + state: { + summary: {}, + timestamp: new Date(Date.parse('1999-12-31')).valueOf().toString(), + url: {}, + }, }; }); }; @@ -71,16 +75,37 @@ describe('fetching a page', () => { simpleFetcher(simpleFixture), simpleEnricher(simpleFixture) ); - expect(res).toEqual({ - items: [ - { - monitor_id: 'foo', - state: { summary: {}, timestamp: '1999-12-31T00:00:00.000Z' }, + expect(res).toMatchInlineSnapshot(` + Object { + "items": Array [ + Object { + "monitor_id": "foo", + "state": Object { + "summary": Object {}, + "timestamp": "946598400000", + "url": Object {}, + }, + }, + Object { + "monitor_id": "bar", + "state": Object { + "summary": Object {}, + "timestamp": "946598400000", + "url": Object {}, + }, + }, + ], + "nextPagePagination": Object { + "cursorDirection": "AFTER", + "cursorKey": "bar", + "sortOrder": "ASC", }, - { monitor_id: 'bar', state: { summary: {}, timestamp: '1999-12-31T00:00:00.000Z' } }, - ], - nextPagePagination: { cursorDirection: 'AFTER', sortOrder: 'ASC', cursorKey: 'bar' }, - prevPagePagination: { cursorDirection: 'BEFORE', sortOrder: 'ASC', cursorKey: 'foo' }, - }); + "prevPagePagination": Object { + "cursorDirection": "BEFORE", + "cursorKey": "foo", + "sortOrder": "ASC", + }, + } + `); }); }); diff --git a/x-pack/plugins/uptime/server/lib/requests/search/__tests__/query_context.test.ts b/x-pack/plugins/uptime/server/lib/requests/search/__tests__/query_context.test.ts index a6c98541fba1d..84774cdeed856 100644 --- a/x-pack/plugins/uptime/server/lib/requests/search/__tests__/query_context.test.ts +++ b/x-pack/plugins/uptime/server/lib/requests/search/__tests__/query_context.test.ts @@ -9,7 +9,7 @@ import { CursorPagination } from '../types'; import { CursorDirection, SortOrder, -} from '../../../../../../../legacy/plugins/uptime/common/graphql/types'; +} from '../../../../../../../legacy/plugins/uptime/common/runtime_types'; describe(QueryContext, () => { // 10 minute range diff --git a/x-pack/plugins/uptime/server/lib/requests/search/__tests__/test_helpers.ts b/x-pack/plugins/uptime/server/lib/requests/search/__tests__/test_helpers.ts index d1212daf5304f..47034c2130116 100644 --- a/x-pack/plugins/uptime/server/lib/requests/search/__tests__/test_helpers.ts +++ b/x-pack/plugins/uptime/server/lib/requests/search/__tests__/test_helpers.ts @@ -8,7 +8,7 @@ import { CursorPagination } from '../types'; import { CursorDirection, SortOrder, -} from '../../../../../../../legacy/plugins/uptime/common/graphql/types'; +} from '../../../../../../../legacy/plugins/uptime/common/runtime_types'; import { QueryContext } from '../query_context'; export const prevPagination = (key: any): CursorPagination => { diff --git a/x-pack/plugins/uptime/server/lib/requests/search/enrich_monitor_groups.ts b/x-pack/plugins/uptime/server/lib/requests/search/enrich_monitor_groups.ts index 1798550875276..4739c804d24e7 100644 --- a/x-pack/plugins/uptime/server/lib/requests/search/enrich_monitor_groups.ts +++ b/x-pack/plugins/uptime/server/lib/requests/search/enrich_monitor_groups.ts @@ -8,12 +8,12 @@ import { get, sortBy } from 'lodash'; import { QueryContext } from './query_context'; import { QUERY, STATES } from '../../../../../../legacy/plugins/uptime/common/constants'; import { - MonitorSummary, - SummaryHistogram, Check, + Histogram, + MonitorSummary, CursorDirection, SortOrder, -} from '../../../../../../legacy/plugins/uptime/common/graphql/types'; +} from '../../../../../../legacy/plugins/uptime/common/runtime_types'; import { MonitorEnricher } from './fetch_page'; export const enrichMonitorGroups: MonitorEnricher = async ( @@ -250,11 +250,8 @@ export const enrichMonitorGroups: MonitorEnricher = async ( const summaries: MonitorSummary[] = monitorBuckets.map((monitor: any) => { const monitorId = get(monitor, 'key.monitor_id'); monitorIds.push(monitorId); - let state = get(monitor, 'state.value'); - state = { - ...state, - timestamp: state['@timestamp'], - }; + const state: any = monitor.state?.value; + state.timestamp = state['@timestamp']; const { checks } = state; if (checks) { state.checks = sortBy(checks, checksSortBy); @@ -289,7 +286,7 @@ export const enrichMonitorGroups: MonitorEnricher = async ( const getHistogramForMonitors = async ( queryContext: QueryContext, monitorIds: string[] -): Promise<{ [key: string]: SummaryHistogram }> => { +): Promise<{ [key: string]: Histogram }> => { const params = { index: queryContext.heartbeatIndices, body: { diff --git a/x-pack/plugins/uptime/server/lib/requests/search/fetch_page.ts b/x-pack/plugins/uptime/server/lib/requests/search/fetch_page.ts index 62144dacbd377..84167840d5d9b 100644 --- a/x-pack/plugins/uptime/server/lib/requests/search/fetch_page.ts +++ b/x-pack/plugins/uptime/server/lib/requests/search/fetch_page.ts @@ -12,7 +12,7 @@ import { CursorDirection, MonitorSummary, SortOrder, -} from '../../../../../../legacy/plugins/uptime/common/graphql/types'; +} from '../../../../../../legacy/plugins/uptime/common/runtime_types'; import { enrichMonitorGroups } from './enrich_monitor_groups'; import { MonitorGroupIterator } from './monitor_group_iterator'; diff --git a/x-pack/plugins/uptime/server/lib/requests/search/find_potential_matches.ts b/x-pack/plugins/uptime/server/lib/requests/search/find_potential_matches.ts index 424c097853ad3..3449febfa5b05 100644 --- a/x-pack/plugins/uptime/server/lib/requests/search/find_potential_matches.ts +++ b/x-pack/plugins/uptime/server/lib/requests/search/find_potential_matches.ts @@ -5,7 +5,7 @@ */ import { get, set } from 'lodash'; -import { CursorDirection } from '../../../../../../legacy/plugins/uptime/common/graphql/types'; +import { CursorDirection } from '../../../../../../legacy/plugins/uptime/common/runtime_types'; import { QueryContext } from './query_context'; // This is the first phase of the query. In it, we find the most recent check groups that matched the given query. diff --git a/x-pack/plugins/uptime/server/lib/requests/search/monitor_group_iterator.ts b/x-pack/plugins/uptime/server/lib/requests/search/monitor_group_iterator.ts index 267551907c5e8..31d9166eb1e73 100644 --- a/x-pack/plugins/uptime/server/lib/requests/search/monitor_group_iterator.ts +++ b/x-pack/plugins/uptime/server/lib/requests/search/monitor_group_iterator.ts @@ -6,7 +6,7 @@ import { QueryContext } from './query_context'; import { fetchChunk } from './fetch_chunk'; -import { CursorDirection } from '../../../../../../legacy/plugins/uptime/common/graphql/types'; +import { CursorDirection } from '../../../../../../legacy/plugins/uptime/common/runtime_types'; import { MonitorGroups } from './fetch_page'; import { CursorPagination } from './types'; diff --git a/x-pack/plugins/uptime/server/lib/requests/search/refine_potential_matches.ts b/x-pack/plugins/uptime/server/lib/requests/search/refine_potential_matches.ts index 218eb2f121a81..43fc54fb25808 100644 --- a/x-pack/plugins/uptime/server/lib/requests/search/refine_potential_matches.ts +++ b/x-pack/plugins/uptime/server/lib/requests/search/refine_potential_matches.ts @@ -5,7 +5,7 @@ */ import { QueryContext } from './query_context'; -import { CursorDirection } from '../../../../../../legacy/plugins/uptime/common/graphql/types'; +import { CursorDirection } from '../../../../../../legacy/plugins/uptime/common/runtime_types'; import { MonitorGroups, MonitorLocCheckGroup } from './fetch_page'; /** diff --git a/x-pack/plugins/uptime/server/lib/requests/search/types.ts b/x-pack/plugins/uptime/server/lib/requests/search/types.ts index 42c98ace6e8f5..2ec52d400b597 100644 --- a/x-pack/plugins/uptime/server/lib/requests/search/types.ts +++ b/x-pack/plugins/uptime/server/lib/requests/search/types.ts @@ -7,7 +7,7 @@ import { CursorDirection, SortOrder, -} from '../../../../../../legacy/plugins/uptime/common/graphql/types'; +} from '../../../../../../legacy/plugins/uptime/common/runtime_types'; export interface CursorPagination { cursorKey?: any; diff --git a/x-pack/plugins/uptime/server/lib/requests/types.ts b/x-pack/plugins/uptime/server/lib/requests/types.ts deleted file mode 100644 index 53a4e989e3789..0000000000000 --- a/x-pack/plugins/uptime/server/lib/requests/types.ts +++ /dev/null @@ -1,72 +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 { Ping, PingResults } from '../../../../../legacy/plugins/uptime/common/graphql/types'; -import { UMElasticsearchQueryFn } from '../adapters'; -import { - GetPingHistogramParams, - HistogramResult, -} from '../../../../../legacy/plugins/uptime/common/types'; - -export interface GetAllParams { - /** @member dateRangeStart timestamp bounds */ - dateRangeStart: string; - - /** @member dateRangeEnd timestamp bounds */ - dateRangeEnd: string; - - /** @member monitorId optional limit by monitorId */ - monitorId?: string | null; - - /** @member status optional limit by check statuses */ - status?: string | null; - - /** @member sort optional sort by timestamp */ - sort?: string | null; - - /** @member size optional limit query size */ - size?: number | null; - - /** @member location optional location value for use in filtering*/ - location?: string | null; -} - -export interface GetLatestMonitorDocsParams { - /** @member dateRangeStart timestamp bounds */ - dateStart?: string; - - /** @member dateRangeEnd timestamp bounds */ - dateEnd?: string; - - /** @member monitorId optional limit to monitorId */ - monitorId?: string | null; -} - -/** - * Count the number of documents in heartbeat indices - */ -export interface UMPingsAdapter { - getAll: UMElasticsearchQueryFn; - - // Get the monitor meta info regardless of timestamp - getMonitor: UMElasticsearchQueryFn; - - getLatestMonitorStatus: UMElasticsearchQueryFn; - - getPingHistogram: UMElasticsearchQueryFn; -} - -export interface HistogramQueryResult { - key: number; - key_as_string: string; - doc_count: number; - down: { - doc_count: number; - }; - up: { - doc_count: number; - }; -} diff --git a/x-pack/plugins/uptime/server/lib/requests/uptime_requests.ts b/x-pack/plugins/uptime/server/lib/requests/uptime_requests.ts index 9d3fa5aa08aed..84154429b9188 100644 --- a/x-pack/plugins/uptime/server/lib/requests/uptime_requests.ts +++ b/x-pack/plugins/uptime/server/lib/requests/uptime_requests.ts @@ -5,7 +5,13 @@ */ import { UMElasticsearchQueryFn } from '../adapters'; -import { Ping, PingResults } from '../../../../../legacy/plugins/uptime/common/graphql/types'; +import { + HistogramResult, + Ping, + PingsResponse as PingResults, + GetCertsParams, + GetPingsParams, +} from '../../../../../legacy/plugins/uptime/common/runtime_types'; import { GetFilterBarParams, GetLatestMonitorParams, @@ -13,7 +19,6 @@ import { GetMonitorDetailsParams, GetMonitorLocationsParams, GetMonitorStatesParams, - GetPingsParams, GetPingHistogramParams, GetMonitorStatusParams, GetMonitorStatusResult, @@ -24,17 +29,16 @@ import { MonitorLocations, Snapshot, StatesIndexStatus, + Cert, } from '../../../../../legacy/plugins/uptime/common/runtime_types'; import { GetMonitorStatesResult } from './get_monitor_states'; import { GetSnapshotCountParams } from './get_snapshot_counts'; -import { - HistogramResult, - MonitorDurationResult, -} from '../../../../../legacy/plugins/uptime/common/types'; +import { MonitorDurationResult } from '../../../../../legacy/plugins/uptime/common/types'; type ESQ = UMElasticsearchQueryFn; export interface UptimeRequests { + getCerts: ESQ; getFilterBar: ESQ; getIndexPattern: ESQ<{}, {}>; getLatestMonitor: ESQ; diff --git a/x-pack/plugins/uptime/server/lib/saved_objects.ts b/x-pack/plugins/uptime/server/lib/saved_objects.ts index 175634ef797cc..3ccfd498c44bf 100644 --- a/x-pack/plugins/uptime/server/lib/saved_objects.ts +++ b/x-pack/plugins/uptime/server/lib/saved_objects.ts @@ -7,14 +7,10 @@ import { DynamicSettings, defaultDynamicSettings, -} from '../../../../legacy/plugins/uptime/common/runtime_types/dynamic_settings'; +} from '../../../../legacy/plugins/uptime/common/runtime_types'; import { SavedObjectsType, SavedObjectsErrorHelpers } from '../../../../../src/core/server'; import { UMSavedObjectsQueryFn } from './adapters'; -export interface UMDynamicSettingsType { - heartbeatIndices: string; -} - export interface UMSavedObjectsAdapter { getUptimeDynamicSettings: UMSavedObjectsQueryFn; setUptimeDynamicSettings: UMSavedObjectsQueryFn; @@ -26,12 +22,22 @@ export const settingsObjectId = 'uptime-dynamic-settings-singleton'; export const umDynamicSettings: SavedObjectsType = { name: settingsObjectType, hidden: false, - namespaceAgnostic: false, + namespaceType: 'single', mappings: { properties: { heartbeatIndices: { type: 'keyword', }, + certificatesThresholds: { + properties: { + errorState: { + type: 'long', + }, + warningState: { + type: 'long', + }, + }, + }, }, }, }; diff --git a/x-pack/plugins/uptime/server/rest_api/certs.ts b/x-pack/plugins/uptime/server/rest_api/certs.ts new file mode 100644 index 0000000000000..31fb3f4ab96a7 --- /dev/null +++ b/x-pack/plugins/uptime/server/rest_api/certs.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 { schema } from '@kbn/config-schema'; +import { UMServerLibs } from '../lib/lib'; +import { UMRestApiRouteFactory } from '.'; +import { API_URLS } from '../../../../legacy/plugins/uptime/common/constants/rest_api'; + +const DEFAULT_INDEX = 0; +const DEFAULT_SIZE = 25; +const DEFAULT_FROM = 'now-1d'; +const DEFAULT_TO = 'now'; + +export const createGetCertsRoute: UMRestApiRouteFactory = (libs: UMServerLibs) => ({ + method: 'GET', + path: API_URLS.CERTS, + validate: { + query: schema.object({ + from: schema.maybe(schema.string()), + to: schema.maybe(schema.string()), + search: schema.maybe(schema.string()), + index: schema.maybe(schema.number()), + size: schema.maybe(schema.number()), + }), + }, + writeAccess: false, + options: { + tags: ['access:uptime-read'], + }, + handler: async ({ callES, dynamicSettings }, _context, request, response): Promise => { + const index = request.query?.index ?? DEFAULT_INDEX; + const size = request.query?.size ?? DEFAULT_SIZE; + const from = request.query?.from ?? DEFAULT_FROM; + const to = request.query?.to ?? DEFAULT_TO; + const { search } = request.query; + + return response.ok({ + body: { + certs: await libs.requests.getCerts({ + callES, + dynamicSettings, + index, + search, + size, + from, + to, + }), + }, + }); + }, +}); diff --git a/x-pack/plugins/uptime/server/rest_api/index.ts b/x-pack/plugins/uptime/server/rest_api/index.ts index c84ea71037953..a7a63342d11d4 100644 --- a/x-pack/plugins/uptime/server/rest_api/index.ts +++ b/x-pack/plugins/uptime/server/rest_api/index.ts @@ -4,26 +4,27 @@ * you may not use this file except in compliance with the Elastic License. */ +import { createGetCertsRoute } from './certs'; import { createGetOverviewFilters } from './overview_filters'; -import { createGetPingsRoute } from './pings'; +import { createGetPingHistogramRoute, createGetPingsRoute } from './pings'; import { createGetDynamicSettingsRoute, createPostDynamicSettingsRoute } from './dynamic_settings'; import { createLogPageViewRoute } from './telemetry'; import { createGetSnapshotCount } from './snapshot'; import { UMRestApiRouteFactory } from './types'; import { createGetMonitorDetailsRoute, + createMonitorListRoute, createGetMonitorLocationsRoute, createGetStatusBarRoute, } from './monitors'; -import { createGetPingHistogramRoute } from './pings/get_ping_histogram'; import { createGetMonitorDurationRoute } from './monitors/monitors_durations'; import { createGetIndexPatternRoute, createGetIndexStatusRoute } from './index_state'; - export * from './types'; export { createRouteWithAuth } from './create_route_with_auth'; export { uptimeRouteWrapper } from './uptime_route_wrapper'; export const restApiRoutes: UMRestApiRouteFactory[] = [ + createGetCertsRoute, createGetOverviewFilters, createGetPingsRoute, createGetIndexPatternRoute, @@ -32,6 +33,7 @@ export const restApiRoutes: UMRestApiRouteFactory[] = [ createPostDynamicSettingsRoute, createGetMonitorDetailsRoute, createGetMonitorLocationsRoute, + createMonitorListRoute, createGetStatusBarRoute, createGetSnapshotCount, createLogPageViewRoute, diff --git a/x-pack/plugins/uptime/server/rest_api/monitors/index.ts b/x-pack/plugins/uptime/server/rest_api/monitors/index.ts index 51b39037049b5..256f885d550ed 100644 --- a/x-pack/plugins/uptime/server/rest_api/monitors/index.ts +++ b/x-pack/plugins/uptime/server/rest_api/monitors/index.ts @@ -5,5 +5,6 @@ */ export { createGetMonitorDetailsRoute } from './monitors_details'; +export { createMonitorListRoute } from './monitor_list'; export { createGetMonitorLocationsRoute } from './monitor_locations'; export { createGetStatusBarRoute } from './monitor_status'; diff --git a/x-pack/plugins/uptime/server/rest_api/monitors/monitor_list.ts b/x-pack/plugins/uptime/server/rest_api/monitors/monitor_list.ts new file mode 100644 index 0000000000000..5cb4e8a6241b7 --- /dev/null +++ b/x-pack/plugins/uptime/server/rest_api/monitors/monitor_list.ts @@ -0,0 +1,69 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { schema } from '@kbn/config-schema'; +import { UMRestApiRouteFactory } from '../types'; +import { CONTEXT_DEFAULTS } from '../../../../../legacy/plugins/uptime/common/constants'; +import { API_URLS } from '../../../../../legacy/plugins/uptime/common/constants/rest_api'; + +export const createMonitorListRoute: UMRestApiRouteFactory = libs => ({ + method: 'GET', + path: API_URLS.MONITOR_LIST, + validate: { + query: schema.object({ + dateRangeStart: schema.string(), + dateRangeEnd: schema.string(), + filters: schema.maybe(schema.string()), + pagination: schema.maybe(schema.string()), + statusFilter: schema.maybe(schema.string()), + pageSize: schema.number(), + }), + }, + options: { + tags: ['access:uptime-read'], + }, + handler: async ({ callES, dynamicSettings }, _context, request, response): Promise => { + const { + dateRangeStart, + dateRangeEnd, + filters, + pagination, + statusFilter, + pageSize, + } = request.query; + + const decodedPagination = pagination + ? JSON.parse(decodeURIComponent(pagination)) + : CONTEXT_DEFAULTS.CURSOR_PAGINATION; + const [indexStatus, { summaries, nextPagePagination, prevPagePagination }] = await Promise.all([ + libs.requests.getIndexStatus({ callES, dynamicSettings }), + libs.requests.getMonitorStates({ + callES, + dynamicSettings, + dateRangeStart, + dateRangeEnd, + pagination: decodedPagination, + pageSize, + filters, + // this is added to make typescript happy, + // this sort of reassignment used to be further downstream but I've moved it here + // because this code is going to be decomissioned soon + statusFilter: statusFilter || undefined, + }), + ]); + + const totalSummaryCount = indexStatus?.docCount ?? 0; + + return response.ok({ + body: { + summaries, + nextPagePagination, + prevPagePagination, + totalSummaryCount, + }, + }); + }, +}); diff --git a/x-pack/plugins/uptime/server/rest_api/pings/get_all.ts b/x-pack/plugins/uptime/server/rest_api/pings/get_all.ts deleted file mode 100644 index c76892103da6b..0000000000000 --- a/x-pack/plugins/uptime/server/rest_api/pings/get_all.ts +++ /dev/null @@ -1,47 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { schema } from '@kbn/config-schema'; -import { UMServerLibs } from '../../lib/lib'; -import { UMRestApiRouteFactory } from '../types'; -import { API_URLS } from '../../../../../legacy/plugins/uptime/common/constants/rest_api'; - -export const createGetAllRoute: UMRestApiRouteFactory = (libs: UMServerLibs) => ({ - method: 'GET', - path: API_URLS.PINGS, - validate: { - query: schema.object({ - dateRangeStart: schema.string(), - dateRangeEnd: schema.string(), - location: schema.maybe(schema.string()), - monitorId: schema.maybe(schema.string()), - size: schema.maybe(schema.number()), - sort: schema.maybe(schema.string()), - status: schema.maybe(schema.string()), - }), - }, - handler: async ({ callES, dynamicSettings }, _context, request, response): Promise => { - const { dateRangeStart, dateRangeEnd, location, monitorId, size, sort, status } = request.query; - - const result = await libs.requests.getPings({ - callES, - dynamicSettings, - dateRangeStart, - dateRangeEnd, - monitorId, - status, - sort, - size, - location, - }); - - return response.ok({ - body: { - ...result, - }, - }); - }, -}); diff --git a/x-pack/plugins/uptime/server/rest_api/pings/get_pings.ts b/x-pack/plugins/uptime/server/rest_api/pings/get_pings.ts index cde9a8c4e47ea..80a887a7f64a9 100644 --- a/x-pack/plugins/uptime/server/rest_api/pings/get_pings.ts +++ b/x-pack/plugins/uptime/server/rest_api/pings/get_pings.ts @@ -5,37 +5,41 @@ */ import { schema } from '@kbn/config-schema'; +import { isLeft } from 'fp-ts/lib/Either'; +import { PathReporter } from 'io-ts/lib/PathReporter'; import { UMServerLibs } from '../../lib/lib'; import { UMRestApiRouteFactory } from '../types'; import { API_URLS } from '../../../../../legacy/plugins/uptime/common/constants/rest_api'; +import { GetPingsParamsType } from '../../../../../legacy/plugins/uptime/common/runtime_types'; export const createGetPingsRoute: UMRestApiRouteFactory = (libs: UMServerLibs) => ({ method: 'GET', path: API_URLS.PINGS, validate: { query: schema.object({ - dateRangeStart: schema.string(), - dateRangeEnd: schema.string(), + from: schema.string(), + to: schema.string(), location: schema.maybe(schema.string()), monitorId: schema.maybe(schema.string()), + index: schema.maybe(schema.number()), size: schema.maybe(schema.number()), sort: schema.maybe(schema.string()), status: schema.maybe(schema.string()), }), }, handler: async ({ callES, dynamicSettings }, _context, request, response): Promise => { - const { dateRangeStart, dateRangeEnd, location, monitorId, size, sort, status } = request.query; + const { from, to, ...optional } = request.query; + const params = GetPingsParamsType.decode({ dateRange: { from, to }, ...optional }); + if (isLeft(params)) { + // eslint-disable-next-line no-console + console.error(new Error(PathReporter.report(params).join(';'))); + return response.badRequest({ body: { message: 'Received invalid request parameters.' } }); + } const result = await libs.requests.getPings({ callES, dynamicSettings, - dateRangeStart, - dateRangeEnd, - monitorId, - status, - sort, - size, - location, + ...params.right, }); return response.ok({ diff --git a/x-pack/plugins/uptime/server/rest_api/pings/index.ts b/x-pack/plugins/uptime/server/rest_api/pings/index.ts index abb7da26f994f..a10ab435e4b0a 100644 --- a/x-pack/plugins/uptime/server/rest_api/pings/index.ts +++ b/x-pack/plugins/uptime/server/rest_api/pings/index.ts @@ -5,3 +5,4 @@ */ export { createGetPingsRoute } from './get_pings'; +export { createGetPingHistogramRoute } from './get_ping_histogram'; diff --git a/x-pack/plugins/uptime/server/rest_api/types.ts b/x-pack/plugins/uptime/server/rest_api/types.ts index aecb099b7bed5..e05e7a4d7faf1 100644 --- a/x-pack/plugins/uptime/server/rest_api/types.ts +++ b/x-pack/plugins/uptime/server/rest_api/types.ts @@ -10,7 +10,7 @@ import { RouteConfig, RouteMethod, CallAPIOptions, - SavedObjectsClient, + SavedObjectsClientContract, RequestHandlerContext, KibanaRequest, KibanaResponseFactory, @@ -69,18 +69,7 @@ export interface UMRouteParams { options?: CallAPIOptions | undefined ) => Promise; dynamicSettings: DynamicSettings; - savedObjectsClient: Pick< - SavedObjectsClient, - | 'errors' - | 'create' - | 'bulkCreate' - | 'delete' - | 'find' - | 'bulkGet' - | 'get' - | 'update' - | 'bulkUpdate' - >; + savedObjectsClient: SavedObjectsClientContract; } /** diff --git a/x-pack/plugins/uptime/server/uptime_server.ts b/x-pack/plugins/uptime/server/uptime_server.ts index d4b38b8ad27a0..cc6fe85b80cb2 100644 --- a/x-pack/plugins/uptime/server/uptime_server.ts +++ b/x-pack/plugins/uptime/server/uptime_server.ts @@ -4,8 +4,6 @@ * you may not use this file except in compliance with the Elastic License. */ -import { makeExecutableSchema } from 'graphql-tools'; -import { DEFAULT_GRAPHQL_PATH, resolvers, typeDefs } from './graphql'; import { UMServerLibs } from './lib/lib'; import { createRouteWithAuth, restApiRoutes, uptimeRouteWrapper } from './rest_api'; import { UptimeCoreSetup, UptimeCorePlugins } from './lib/adapters'; @@ -23,10 +21,4 @@ export const initUptimeServer = ( uptimeAlertTypeFactories.forEach(alertTypeFactory => plugins.alerting.registerType(alertTypeFactory(server, libs)) ); - - const graphQLSchema = makeExecutableSchema({ - resolvers: resolvers.map(createResolversFn => createResolversFn(libs)), - typeDefs, - }); - libs.framework.registerGraphQLEndpoint(DEFAULT_GRAPHQL_PATH, graphQLSchema); }; diff --git a/x-pack/plugins/watcher/__jest__/client_integration/watch_create_json.test.ts b/x-pack/plugins/watcher/__jest__/client_integration/watch_create_json.test.ts index 2096c0dd61bd8..a35dd1346dc90 100644 --- a/x-pack/plugins/watcher/__jest__/client_integration/watch_create_json.test.ts +++ b/x-pack/plugins/watcher/__jest__/client_integration/watch_create_json.test.ts @@ -4,8 +4,6 @@ * you may not use this file except in compliance with the Elastic License. */ -import '../../../es_ui_shared/console_lang/mocks'; - import { act } from 'react-dom/test-utils'; import { setupEnvironment, pageHelpers, nextTick, wrapBodyResponse } from './helpers'; import { WatchCreateJsonTestBed } from './helpers/watch_create_json.helpers'; @@ -108,6 +106,7 @@ describe(' create route', () => { name: watch.name, type: watch.type, isNew: true, + isActive: true, actions: [ { id: DEFAULT_LOGGING_ACTION_ID, @@ -185,6 +184,7 @@ describe(' create route', () => { id, type, isNew: true, + isActive: true, actions: [], watch: defaultWatchJson, }; @@ -246,6 +246,7 @@ describe(' create route', () => { id, type, isNew: true, + isActive: true, actions: [], watch: defaultWatchJson, }; diff --git a/x-pack/plugins/watcher/__jest__/client_integration/watch_create_threshold.test.tsx b/x-pack/plugins/watcher/__jest__/client_integration/watch_create_threshold.test.tsx index 943233d3c14ed..89cd5207fe250 100644 --- a/x-pack/plugins/watcher/__jest__/client_integration/watch_create_threshold.test.tsx +++ b/x-pack/plugins/watcher/__jest__/client_integration/watch_create_threshold.test.tsx @@ -4,8 +4,6 @@ * you may not use this file except in compliance with the Elastic License. */ -import '../../../es_ui_shared/console_lang/mocks'; - import React from 'react'; import { act } from 'react-dom/test-utils'; import axiosXhrAdapter from 'axios/lib/adapters/xhr'; @@ -244,6 +242,7 @@ describe(' create route', () => { name: WATCH_NAME, type: WATCH_TYPES.THRESHOLD, isNew: true, + isActive: true, actions: [ { id: 'logging_1', @@ -314,6 +313,7 @@ describe(' create route', () => { name: WATCH_NAME, type: WATCH_TYPES.THRESHOLD, isNew: true, + isActive: true, actions: [ { id: 'index_1', @@ -376,6 +376,7 @@ describe(' create route', () => { name: WATCH_NAME, type: WATCH_TYPES.THRESHOLD, isNew: true, + isActive: true, actions: [ { id: 'slack_1', @@ -448,6 +449,7 @@ describe(' create route', () => { name: WATCH_NAME, type: WATCH_TYPES.THRESHOLD, isNew: true, + isActive: true, actions: [ { id: 'email_1', @@ -540,6 +542,7 @@ describe(' create route', () => { name: WATCH_NAME, type: WATCH_TYPES.THRESHOLD, isNew: true, + isActive: true, actions: [ { id: 'webhook_1', @@ -629,6 +632,7 @@ describe(' create route', () => { name: WATCH_NAME, type: WATCH_TYPES.THRESHOLD, isNew: true, + isActive: true, actions: [ { id: 'jira_1', @@ -709,6 +713,7 @@ describe(' create route', () => { name: WATCH_NAME, type: WATCH_TYPES.THRESHOLD, isNew: true, + isActive: true, actions: [ { id: 'pagerduty_1', @@ -772,6 +777,7 @@ describe(' create route', () => { name: WATCH_NAME, type: WATCH_TYPES.THRESHOLD, isNew: true, + isActive: true, actions: [], index: MATCH_INDICES, timeField: WATCH_TIME_FIELD, diff --git a/x-pack/plugins/watcher/__jest__/client_integration/watch_edit.test.ts b/x-pack/plugins/watcher/__jest__/client_integration/watch_edit.test.ts index 51285a5786b00..18acf7339580c 100644 --- a/x-pack/plugins/watcher/__jest__/client_integration/watch_edit.test.ts +++ b/x-pack/plugins/watcher/__jest__/client_integration/watch_edit.test.ts @@ -3,7 +3,6 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -import '../../../es_ui_shared/console_lang/mocks'; import { act } from 'react-dom/test-utils'; import axiosXhrAdapter from 'axios/lib/adapters/xhr'; @@ -98,6 +97,7 @@ describe('', () => { name: EDITED_WATCH_NAME, type: watch.type, isNew: false, + isActive: true, actions: [ { id: DEFAULT_LOGGING_ACTION_ID, @@ -191,6 +191,7 @@ describe('', () => { name: EDITED_WATCH_NAME, type, isNew: false, + isActive: true, actions: [], timeField, triggerIntervalSize, diff --git a/x-pack/plugins/watcher/__jest__/client_integration/watch_list.test.ts b/x-pack/plugins/watcher/__jest__/client_integration/watch_list.test.ts index 3370e42b2417f..a0327c6dfa1db 100644 --- a/x-pack/plugins/watcher/__jest__/client_integration/watch_list.test.ts +++ b/x-pack/plugins/watcher/__jest__/client_integration/watch_list.test.ts @@ -3,7 +3,6 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -import '../../../es_ui_shared/console_lang/mocks'; import { act } from 'react-dom/test-utils'; import * as fixtures from '../../test/fixtures'; diff --git a/x-pack/plugins/watcher/__jest__/client_integration/watch_status.test.ts b/x-pack/plugins/watcher/__jest__/client_integration/watch_status.test.ts index 20b7b526705c0..e9cbb2c92491a 100644 --- a/x-pack/plugins/watcher/__jest__/client_integration/watch_status.test.ts +++ b/x-pack/plugins/watcher/__jest__/client_integration/watch_status.test.ts @@ -3,7 +3,6 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -import '../../../es_ui_shared/console_lang/mocks'; import { act } from 'react-dom/test-utils'; import { setupEnvironment, pageHelpers, nextTick } from './helpers'; diff --git a/x-pack/plugins/watcher/public/application/models/watch/base_watch.js b/x-pack/plugins/watcher/public/application/models/watch/base_watch.js index 3fe4fb006d241..af2e45b35501e 100644 --- a/x-pack/plugins/watcher/public/application/models/watch/base_watch.js +++ b/x-pack/plugins/watcher/public/application/models/watch/base_watch.js @@ -32,6 +32,7 @@ export class BaseWatch { this.isSystemWatch = Boolean(get(props, 'isSystemWatch')); this.watchStatus = WatchStatus.fromUpstreamJson(get(props, 'watchStatus')); this.watchErrors = WatchErrors.fromUpstreamJson(get(props, 'watchErrors')); + this.isActive = this.watchStatus.isActive ?? true; const actions = get(props, 'actions', []); this.actions = actions.map(Action.fromUpstreamJson); @@ -115,6 +116,7 @@ export class BaseWatch { name: this.name, type: this.type, isNew: this.isNew, + isActive: this.isActive, actions: map(this.actions, action => action.upstreamJson), }; } diff --git a/x-pack/plugins/watcher/public/application/sections/watch_edit/components/json_watch_edit/json_watch_edit_form.tsx b/x-pack/plugins/watcher/public/application/sections/watch_edit/components/json_watch_edit/json_watch_edit_form.tsx index b3a09d3bc0e65..b5835d862000f 100644 --- a/x-pack/plugins/watcher/public/application/sections/watch_edit/components/json_watch_edit/json_watch_edit_form.tsx +++ b/x-pack/plugins/watcher/public/application/sections/watch_edit/components/json_watch_edit/json_watch_edit_form.tsx @@ -23,14 +23,13 @@ import { FormattedMessage } from '@kbn/i18n/react'; import { serializeJsonWatch } from '../../../../../../common/lib/serialization'; import { ErrableFormRow, SectionError, Error as ServerError } from '../../../../components'; +import { useXJsonMode } from '../../../../shared_imports'; import { onWatchSave } from '../../watch_edit_actions'; import { WatchContext } from '../../watch_context'; import { goToWatchList } from '../../../../lib/navigation'; import { RequestFlyout } from '../request_flyout'; import { useAppContext } from '../../../../app_context'; -import { useXJsonMode } from './use_x_json_mode'; - export const JsonWatchEditForm = () => { const { links: { putWatchApiUrl }, diff --git a/x-pack/plugins/watcher/public/application/sections/watch_edit/components/json_watch_edit/json_watch_edit_simulate.tsx b/x-pack/plugins/watcher/public/application/sections/watch_edit/components/json_watch_edit/json_watch_edit_simulate.tsx index b9fce52b480ef..fa0b9b0a2566f 100644 --- a/x-pack/plugins/watcher/public/application/sections/watch_edit/components/json_watch_edit/json_watch_edit_simulate.tsx +++ b/x-pack/plugins/watcher/public/application/sections/watch_edit/components/json_watch_edit/json_watch_edit_simulate.tsx @@ -40,7 +40,7 @@ import { JsonWatchEditSimulateResults } from './json_watch_edit_simulate_results import { getTimeUnitLabel } from '../../../../lib/get_time_unit_label'; import { useAppContext } from '../../../../app_context'; -import { useXJsonMode } from './use_x_json_mode'; +import { useXJsonMode } from '../../../../shared_imports'; const actionModeOptions = Object.keys(ACTION_MODES).map(mode => ({ text: ACTION_MODES[mode], diff --git a/x-pack/plugins/watcher/public/application/sections/watch_edit/components/json_watch_edit/use_x_json_mode.ts b/x-pack/plugins/watcher/public/application/sections/watch_edit/components/json_watch_edit/use_x_json_mode.ts deleted file mode 100644 index 7aefb0554e0e8..0000000000000 --- a/x-pack/plugins/watcher/public/application/sections/watch_edit/components/json_watch_edit/use_x_json_mode.ts +++ /dev/null @@ -1,24 +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 { useState } from 'react'; -import { XJsonMode } from '../../../../../../../es_ui_shared/console_lang'; -import { - collapseLiteralStrings, - expandLiteralStrings, -} from '../../../../../../../../../src/plugins/es_ui_shared/console_lang/lib'; - -export const xJsonMode = new XJsonMode(); - -export const useXJsonMode = (json: string) => { - const [xJson, setXJson] = useState(expandLiteralStrings(json)); - - return { - xJson, - setXJson, - xJsonMode, - convertToJson: collapseLiteralStrings, - }; -}; diff --git a/x-pack/plugins/watcher/public/application/shared_imports.ts b/x-pack/plugins/watcher/public/application/shared_imports.ts index cbc4dde7448ff..94ef7af1c28d1 100644 --- a/x-pack/plugins/watcher/public/application/shared_imports.ts +++ b/x-pack/plugins/watcher/public/application/shared_imports.ts @@ -11,3 +11,5 @@ export { sendRequest, useRequest, } from '../../../../../src/plugins/es_ui_shared/public/request/np_ready_request'; + +export { useXJsonMode } from '../../../../../src/plugins/es_ui_shared/static/ace_x_json/hooks'; diff --git a/x-pack/plugins/watcher/server/lib/elasticsearch_js_plugin.ts b/x-pack/plugins/watcher/server/lib/elasticsearch_js_plugin.ts index 240e93e160fe0..3cb8dfb623fac 100644 --- a/x-pack/plugins/watcher/server/lib/elasticsearch_js_plugin.ts +++ b/x-pack/plugins/watcher/server/lib/elasticsearch_js_plugin.ts @@ -174,6 +174,10 @@ export const elasticsearchJsPlugin = (Client: any, config: any, components: any) name: 'master_timeout', type: 'duration', }, + active: { + name: 'active', + type: 'boolean', + }, }, url: { fmt: '/_watcher/watch/<%=id%>', diff --git a/x-pack/plugins/watcher/server/routes/api/watch/register_save_route.ts b/x-pack/plugins/watcher/server/routes/api/watch/register_save_route.ts index 61d167bb9bbcd..10ee0c4857862 100644 --- a/x-pack/plugins/watcher/server/routes/api/watch/register_save_route.ts +++ b/x-pack/plugins/watcher/server/routes/api/watch/register_save_route.ts @@ -5,7 +5,6 @@ */ import { schema } from '@kbn/config-schema'; -import { IScopedClusterClient } from 'kibana/server'; import { i18n } from '@kbn/i18n'; import { WATCH_TYPES } from '../../../../common/constants'; import { serializeJsonWatch, serializeThresholdWatch } from '../../../../common/lib/serialization'; @@ -21,23 +20,11 @@ const bodySchema = schema.object( { type: schema.string(), isNew: schema.boolean(), + isActive: schema.boolean(), }, { unknowns: 'allow' } ); -function fetchWatch(dataClient: IScopedClusterClient, watchId: string) { - return dataClient.callAsCurrentUser('watcher.getWatch', { - id: watchId, - }); -} - -function saveWatch(dataClient: IScopedClusterClient, id: string, body: any) { - return dataClient.callAsCurrentUser('watcher.putWatch', { - id, - body, - }); -} - export function registerSaveRoute(deps: RouteDependencies) { deps.router.put( { @@ -49,12 +36,16 @@ export function registerSaveRoute(deps: RouteDependencies) { }, licensePreRoutingFactory(deps, async (ctx, request, response) => { const { id } = request.params; - const { type, isNew, ...watchConfig } = request.body; + const { type, isNew, isActive, ...watchConfig } = request.body; + + const dataClient = ctx.watcher!.client; // For new watches, verify watch with the same ID doesn't already exist if (isNew) { try { - const existingWatch = await fetchWatch(ctx.watcher!.client, id); + const existingWatch = await dataClient.callAsCurrentUser('watcher.getWatch', { + id, + }); if (existingWatch.found) { return response.conflict({ body: { @@ -92,7 +83,11 @@ export function registerSaveRoute(deps: RouteDependencies) { try { // Create new watch return response.ok({ - body: await saveWatch(ctx.watcher!.client, id, serializedWatch), + body: await dataClient.callAsCurrentUser('watcher.putWatch', { + id, + active: isActive, + body: serializedWatch, + }), }); } catch (e) { // Case: Error from Elasticsearch JS client diff --git a/x-pack/scripts/functional_tests.js b/x-pack/scripts/functional_tests.js index 061c9e4a0d921..974f3eb6db60f 100644 --- a/x-pack/scripts/functional_tests.js +++ b/x-pack/scripts/functional_tests.js @@ -4,13 +4,18 @@ * you may not use this file except in compliance with the Elastic License. */ -const alwaysImportedTests = [require.resolve('../test/functional/config.js')]; +const alwaysImportedTests = [ + require.resolve('../test/functional/config.js'), + require.resolve('../test/functional_endpoint_ingest_failure/config.ts'), + require.resolve('../test/functional_endpoint/config.ts'), + require.resolve('../test/functional_with_es_ssl/config.ts'), + require.resolve('../test/functional/config_security_basic.js'), + require.resolve('../test/plugin_functional/config.ts'), +]; const onlyNotInCoverageTests = [ require.resolve('../test/reporting/configs/chromium_api.js'), require.resolve('../test/reporting/configs/chromium_functional.js'), require.resolve('../test/reporting/configs/generate_api.js'), - require.resolve('../test/functional_with_es_ssl/config.ts'), - require.resolve('../test/functional/config_security_basic.js'), require.resolve('../test/api_integration/config_security_basic.js'), require.resolve('../test/api_integration/config.js'), require.resolve('../test/alerting_api_integration/basic/config.ts'), @@ -18,7 +23,6 @@ const onlyNotInCoverageTests = [ require.resolve('../test/alerting_api_integration/security_and_spaces/config.ts'), require.resolve('../test/detection_engine_api_integration/security_and_spaces/config.ts'), require.resolve('../test/plugin_api_integration/config.ts'), - require.resolve('../test/plugin_functional/config.ts'), require.resolve('../test/kerberos_api_integration/config.ts'), require.resolve('../test/kerberos_api_integration/anonymous_access.config.ts'), require.resolve('../test/saml_api_integration/config.ts'), @@ -43,8 +47,7 @@ const onlyNotInCoverageTests = [ require.resolve('../test/licensing_plugin/config.ts'), require.resolve('../test/licensing_plugin/config.public.ts'), require.resolve('../test/licensing_plugin/config.legacy.ts'), - require.resolve('../test/functional_endpoint_ingest_failure/config.ts'), - require.resolve('../test/functional_endpoint/config.ts'), + require.resolve('../test/endpoint_api_integration_no_ingest/config.ts'), ]; require('@kbn/plugin-helpers').babelRegister(); diff --git a/x-pack/test/alerting_api_integration/common/config.ts b/x-pack/test/alerting_api_integration/common/config.ts index 4d32a5ae9f53c..457b7621e84bd 100644 --- a/x-pack/test/alerting_api_integration/common/config.ts +++ b/x-pack/test/alerting_api_integration/common/config.ts @@ -100,6 +100,27 @@ export function createTestConfig(name: string, options: CreateTestConfigOptions) xyzSecret2: 'credential2', }, }, + { + id: 'preconfigured-es-index-action', + actionTypeId: '.index', + name: 'preconfigured_es_index_action', + config: { + index: 'functional-test-actions-index-preconfigured', + refresh: true, + executionTimeField: 'timestamp', + }, + }, + { + id: 'preconfigured.test.index-record', + actionTypeId: 'test.index-record', + name: 'Test:_Preconfigured_Index_Record', + config: { + unencrypted: 'ignored-but-required', + }, + secrets: { + encrypted: 'this-is-also-ignored-and-also-required', + }, + }, ])}`, ...disabledPlugins.map(key => `--xpack.${key}.enabled=false`), `--plugin-path=${path.join(__dirname, 'fixtures', 'plugins', 'alerts')}`, diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/es_index_preconfigured.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/es_index_preconfigured.ts new file mode 100644 index 0000000000000..b04bc13ffc5e4 --- /dev/null +++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/es_index_preconfigured.ts @@ -0,0 +1,68 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import expect from '@kbn/expect'; + +import { FtrProviderContext } from '../../../../common/ftr_provider_context'; + +// from: x-pack/test/alerting_api_integration/common/config.ts +const ACTION_ID = 'preconfigured-es-index-action'; +const ES_TEST_INDEX_NAME = 'functional-test-actions-index-preconfigured'; + +// eslint-disable-next-line import/no-default-export +export default function indexTest({ getService }: FtrProviderContext) { + const es = getService('legacyEs'); + const supertest = getService('supertest'); + const esArchiver = getService('esArchiver'); + + describe('preconfigured index action', () => { + after(() => esArchiver.unload('empty_kibana')); + beforeEach(() => clearTestIndex(es)); + + it('should execute successfully when expected for a single body', async () => { + const { body: result } = await supertest + .post(`/api/action/${ACTION_ID}/_execute`) + .set('kbn-xsrf', 'foo') + .send({ + params: { + documents: [{ testing: [4, 5, 6] }], + }, + }) + .expect(200); + expect(result.status).to.eql('ok'); + + const items = await getTestIndexItems(es); + expect(items.length).to.eql(1); + + // check document sans timestamp + const document = items[0]._source; + const timestamp = document.timestamp; + delete document.timestamp; + expect(document).to.eql({ testing: [4, 5, 6] }); + + // check timestamp + const timestampTime = new Date(timestamp).getTime(); + const timeNow = Date.now(); + const timeMinuteAgo = timeNow - 1000 * 60; + expect(timestampTime).to.be.within(timeMinuteAgo, timeNow); + }); + }); +} + +async function clearTestIndex(es: any) { + return await es.indices.delete({ + index: ES_TEST_INDEX_NAME, + ignoreUnavailable: true, + }); +} + +async function getTestIndexItems(es: any) { + const result = await es.search({ + index: ES_TEST_INDEX_NAME, + }); + + return result.hits.hits; +} diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/get_all.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/get_all.ts index 80b512f3fb5e3..0b637326d4667 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/get_all.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/get_all.ts @@ -68,6 +68,18 @@ export default function getAllActionTests({ getService }: FtrProviderContext) { }, referencedByCount: 0, }, + { + id: 'preconfigured-es-index-action', + isPreconfigured: true, + actionTypeId: '.index', + name: 'preconfigured_es_index_action', + config: { + index: 'functional-test-actions-index-preconfigured', + refresh: true, + executionTimeField: 'timestamp', + }, + referencedByCount: 0, + }, { id: 'my-slack1', isPreconfigured: true, @@ -90,6 +102,16 @@ export default function getAllActionTests({ getService }: FtrProviderContext) { }, referencedByCount: 0, }, + { + id: 'preconfigured.test.index-record', + isPreconfigured: true, + actionTypeId: 'test.index-record', + name: 'Test:_Preconfigured_Index_Record', + config: { + unencrypted: 'ignored-but-required', + }, + referencedByCount: 0, + }, ]); break; default: @@ -167,6 +189,18 @@ export default function getAllActionTests({ getService }: FtrProviderContext) { }, referencedByCount: 1, }, + { + id: 'preconfigured-es-index-action', + isPreconfigured: true, + actionTypeId: '.index', + name: 'preconfigured_es_index_action', + config: { + index: 'functional-test-actions-index-preconfigured', + refresh: true, + executionTimeField: 'timestamp', + }, + referencedByCount: 0, + }, { id: 'my-slack1', isPreconfigured: true, @@ -189,6 +223,16 @@ export default function getAllActionTests({ getService }: FtrProviderContext) { }, referencedByCount: 0, }, + { + id: 'preconfigured.test.index-record', + isPreconfigured: true, + actionTypeId: 'test.index-record', + name: 'Test:_Preconfigured_Index_Record', + config: { + unencrypted: 'ignored-but-required', + }, + referencedByCount: 0, + }, ]); break; default: @@ -232,6 +276,18 @@ export default function getAllActionTests({ getService }: FtrProviderContext) { case 'superuser at space1': expect(response.statusCode).to.eql(200); expect(response.body).to.eql([ + { + id: 'preconfigured-es-index-action', + isPreconfigured: true, + actionTypeId: '.index', + name: 'preconfigured_es_index_action', + config: { + index: 'functional-test-actions-index-preconfigured', + refresh: true, + executionTimeField: 'timestamp', + }, + referencedByCount: 0, + }, { id: 'my-slack1', isPreconfigured: true, @@ -254,6 +310,16 @@ export default function getAllActionTests({ getService }: FtrProviderContext) { }, referencedByCount: 0, }, + { + id: 'preconfigured.test.index-record', + isPreconfigured: true, + actionTypeId: 'test.index-record', + name: 'Test:_Preconfigured_Index_Record', + config: { + unencrypted: 'ignored-but-required', + }, + referencedByCount: 0, + }, ]); break; default: diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/index.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/index.ts index d7ec2e78ccb30..8e002bcc8d3da 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/index.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/index.ts @@ -11,6 +11,7 @@ export default function actionsTests({ loadTestFile }: FtrProviderContext) { describe('Actions', () => { loadTestFile(require.resolve('./builtin_action_types/email')); loadTestFile(require.resolve('./builtin_action_types/es_index')); + loadTestFile(require.resolve('./builtin_action_types/es_index_preconfigured')); loadTestFile(require.resolve('./builtin_action_types/pagerduty')); loadTestFile(require.resolve('./builtin_action_types/server_log')); loadTestFile(require.resolve('./builtin_action_types/servicenow')); diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/alerts.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/alerts.ts index 6eed28cc381dd..d8e4f808f5cd2 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/alerts.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/alerts.ts @@ -165,6 +165,100 @@ instanceStateValue: true } }); + it('should schedule task, run alert and schedule preconfigured actions when appropriate', async () => { + const testStart = new Date(); + const reference = alertUtils.generateReference(); + const response = await alertUtils.createAlwaysFiringAction({ + reference, + indexRecordActionId: 'preconfigured.test.index-record', + }); + + switch (scenario.id) { + case 'no_kibana_privileges at space1': + case 'global_read at space1': + case 'space_1_all at space2': + expect(response.statusCode).to.eql(404); + expect(response.body).to.eql({ + statusCode: 404, + error: 'Not Found', + message: 'Not Found', + }); + break; + case 'superuser at space1': + case 'space_1_all at space1': + expect(response.statusCode).to.eql(200); + + // Wait for the action to index a document before disabling the alert and waiting for tasks to finish + await esTestIndexTool.waitForDocs('action:test.index-record', reference); + + await taskManagerUtils.waitForAllTasksIdle(testStart); + + const alertId = response.body.id; + await alertUtils.disable(alertId); + await taskManagerUtils.waitForEmpty(testStart); + + // Ensure only 1 alert executed with proper params + const alertSearchResult = await esTestIndexTool.search( + 'alert:test.always-firing', + reference + ); + expect(alertSearchResult.hits.total.value).to.eql(1); + expect(alertSearchResult.hits.hits[0]._source).to.eql({ + source: 'alert:test.always-firing', + reference, + state: {}, + params: { + index: ES_TEST_INDEX_NAME, + reference, + }, + alertInfo: { + alertId, + spaceId: space.id, + namespace: space.id, + name: 'abc', + tags: ['tag-A', 'tag-B'], + createdBy: user.fullName, + updatedBy: user.fullName, + }, + }); + + // Ensure only 1 action executed with proper params + const actionSearchResult = await esTestIndexTool.search( + 'action:test.index-record', + reference + ); + expect(actionSearchResult.hits.total.value).to.eql(1); + expect(actionSearchResult.hits.hits[0]._source).to.eql({ + config: { + unencrypted: 'ignored-but-required', + }, + secrets: { + encrypted: 'this-is-also-ignored-and-also-required', + }, + params: { + index: ES_TEST_INDEX_NAME, + reference, + message: ` +alertId: ${alertId}, +alertName: abc, +spaceId: ${space.id}, +tags: tag-A,tag-B, +alertInstanceId: 1, +instanceContextValue: true, +instanceStateValue: true +`.trim(), + }, + reference, + source: 'action:test.index-record', + }); + + await taskManagerUtils.waitForActionTaskParamsToBeCleanedUp(testStart); + break; + default: + throw new Error(`Scenario untested: ${JSON.stringify(scenario)}`); + } + }); + it('should pass updated alert params to executor', async () => { const testStart = new Date(); // create an alert diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/actions/get_all.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/actions/get_all.ts index 517c64f178af5..ec59e56b08308 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/actions/get_all.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/actions/get_all.ts @@ -45,6 +45,18 @@ export default function getAllActionTests({ getService }: FtrProviderContext) { }, referencedByCount: 0, }, + { + id: 'preconfigured-es-index-action', + isPreconfigured: true, + actionTypeId: '.index', + name: 'preconfigured_es_index_action', + config: { + index: 'functional-test-actions-index-preconfigured', + refresh: true, + executionTimeField: 'timestamp', + }, + referencedByCount: 0, + }, { id: 'my-slack1', isPreconfigured: true, @@ -67,6 +79,16 @@ export default function getAllActionTests({ getService }: FtrProviderContext) { }, referencedByCount: 0, }, + { + id: 'preconfigured.test.index-record', + isPreconfigured: true, + actionTypeId: 'test.index-record', + name: 'Test:_Preconfigured_Index_Record', + config: { + unencrypted: 'ignored-but-required', + }, + referencedByCount: 0, + }, ]); }); @@ -88,6 +110,18 @@ export default function getAllActionTests({ getService }: FtrProviderContext) { objectRemover.add(Spaces.space1.id, createdAction.id, 'action'); await supertest.get(`${getUrlPrefix(Spaces.other.id)}/api/action/_getAll`).expect(200, [ + { + id: 'preconfigured-es-index-action', + isPreconfigured: true, + actionTypeId: '.index', + name: 'preconfigured_es_index_action', + config: { + index: 'functional-test-actions-index-preconfigured', + refresh: true, + executionTimeField: 'timestamp', + }, + referencedByCount: 0, + }, { id: 'my-slack1', isPreconfigured: true, @@ -110,6 +144,16 @@ export default function getAllActionTests({ getService }: FtrProviderContext) { }, referencedByCount: 0, }, + { + id: 'preconfigured.test.index-record', + isPreconfigured: true, + actionTypeId: 'test.index-record', + name: 'Test:_Preconfigured_Index_Record', + config: { + unencrypted: 'ignored-but-required', + }, + referencedByCount: 0, + }, ]); }); }); diff --git a/x-pack/test/api_integration/apis/endpoint/index.ts b/x-pack/test/api_integration/apis/endpoint/index.ts index 4ffd0c3b6044b..0a5f9aa595b8a 100644 --- a/x-pack/test/api_integration/apis/endpoint/index.ts +++ b/x-pack/test/api_integration/apis/endpoint/index.ts @@ -6,9 +6,17 @@ import { FtrProviderContext } from '../../ftr_provider_context'; -export default function endpointAPIIntegrationTests({ loadTestFile }: FtrProviderContext) { +export default function endpointAPIIntegrationTests({ + loadTestFile, + getService, +}: FtrProviderContext) { describe('Endpoint plugin', function() { + const ingestManager = getService('ingestManager'); this.tags(['endpoint']); + before(async () => { + await ingestManager.setup(); + }); + loadTestFile(require.resolve('./index_pattern')); loadTestFile(require.resolve('./resolver')); loadTestFile(require.resolve('./metadata')); loadTestFile(require.resolve('./alerts')); diff --git a/x-pack/test/api_integration/apis/endpoint/index_pattern.ts b/x-pack/test/api_integration/apis/endpoint/index_pattern.ts new file mode 100644 index 0000000000000..d3ffd67defef1 --- /dev/null +++ b/x-pack/test/api_integration/apis/endpoint/index_pattern.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 expect from '@kbn/expect/expect.js'; +import { FtrProviderContext } from '../../ftr_provider_context'; + +export default function({ getService }: FtrProviderContext) { + const supertest = getService('supertest'); + + describe('Endpoint index pattern API', () => { + it('should retrieve the index pattern for events', async () => { + const { body } = await supertest.get('/api/endpoint/index_pattern/events').expect(200); + expect(body.indexPattern).to.eql('events-endpoint-*'); + }); + + it('should retrieve the index pattern for metadata', async () => { + const { body } = await supertest.get('/api/endpoint/index_pattern/metadata').expect(200); + expect(body.indexPattern).to.eql('metrics-endpoint-*'); + }); + + it('should not retrieve the index pattern for an invalid key', async () => { + await supertest.get('/api/endpoint/index_pattern/blah').expect(404); + }); + }); +} diff --git a/x-pack/test/api_integration/apis/endpoint/metadata.ts b/x-pack/test/api_integration/apis/endpoint/metadata.ts index 887be6b85b100..943782c79ada7 100644 --- a/x-pack/test/api_integration/apis/endpoint/metadata.ts +++ b/x-pack/test/api_integration/apis/endpoint/metadata.ts @@ -159,7 +159,7 @@ export default function({ getService }: FtrProviderContext) { .post('/api/endpoint/metadata') .set('kbn-xsrf', 'xxx') .send({ - filter: `host.os.variant.keyword:${variantValue}`, + filter: `host.os.variant:${variantValue}`, }) .expect(200); expect(body.total).to.eql(2); diff --git a/x-pack/test/api_integration/apis/endpoint/resolver.ts b/x-pack/test/api_integration/apis/endpoint/resolver.ts index c1e9240c09951..e8d336e875b99 100644 --- a/x-pack/test/api_integration/apis/endpoint/resolver.ts +++ b/x-pack/test/api_integration/apis/endpoint/resolver.ts @@ -17,8 +17,8 @@ export default function resolverAPIIntegrationTests({ getService }: FtrProviderC const esArchiver = getService('esArchiver'); describe('Resolver', () => { - before(() => esArchiver.load('endpoint/resolver/api_feature')); - after(() => esArchiver.unload('endpoint/resolver/api_feature')); + before(async () => await esArchiver.load('endpoint/resolver/api_feature')); + after(async () => await esArchiver.unload('endpoint/resolver/api_feature')); describe('related events endpoint', () => { const endpointID = '5a0c957f-b8e7-4538-965e-57e8bb86ad3a'; diff --git a/x-pack/test/api_integration/apis/infra/metadata.ts b/x-pack/test/api_integration/apis/infra/metadata.ts index b693881abcdf7..5187cc5e3ec26 100644 --- a/x-pack/test/api_integration/apis/infra/metadata.ts +++ b/x-pack/test/api_integration/apis/infra/metadata.ts @@ -12,6 +12,28 @@ import { } from '../../../../plugins/infra/common/http_api/metadata_api'; import { FtrProviderContext } from '../../ftr_provider_context'; +import { DATES } from './constants'; + +const timeRange700 = { + from: DATES['7.0.0'].hosts.min, + to: DATES[`7.0.0`].hosts.max, +}; + +const timeRange660 = { + from: DATES['6.6.0'].docker.min, + to: DATES[`6.6.0`].docker.max, +}; + +const timeRange800withAws = { + from: DATES['8.0.0'].logs_and_metrics_with_aws.min, + to: DATES[`8.0.0`].logs_and_metrics_with_aws.max, +}; + +const timeRange800 = { + from: DATES['8.0.0'].logs_and_metrics.min, + to: DATES[`8.0.0`].logs_and_metrics.max, +}; + export default function({ getService }: FtrProviderContext) { const esArchiver = getService('esArchiver'); const supertest = getService('supertest'); @@ -34,6 +56,7 @@ export default function({ getService }: FtrProviderContext) { sourceId: 'default', nodeId: 'demo-stack-mysql-01', nodeType: InfraNodeType.host, + timeRange: timeRange700, }); if (metadata) { expect(metadata.features.length).to.be(12); @@ -53,6 +76,7 @@ export default function({ getService }: FtrProviderContext) { sourceId: 'default', nodeId: '631f36a845514442b93c3fdd2dc91bcd8feb680b8ac5832c7fb8fdc167bb938e', nodeType: InfraNodeType.container, + timeRange: timeRange660, }); if (metadata) { expect(metadata.features.length).to.be(10); @@ -74,6 +98,7 @@ export default function({ getService }: FtrProviderContext) { sourceId: 'default', nodeId: 'gke-observability-8--observability-8--bc1afd95-f0zc', nodeType: InfraNodeType.host, + timeRange: timeRange800withAws, }); if (metadata) { expect(metadata.features.length).to.be(58); @@ -114,6 +139,7 @@ export default function({ getService }: FtrProviderContext) { sourceId: 'default', nodeId: 'ip-172-31-47-9.us-east-2.compute.internal', nodeType: InfraNodeType.host, + timeRange: timeRange800withAws, }); if (metadata) { expect(metadata.features.length).to.be(19); @@ -155,6 +181,7 @@ export default function({ getService }: FtrProviderContext) { sourceId: 'default', nodeId: '14887487-99f8-11e9-9a96-42010a84004d', nodeType: InfraNodeType.pod, + timeRange: timeRange800withAws, }); if (metadata) { expect(metadata.features.length).to.be(29); @@ -200,6 +227,7 @@ export default function({ getService }: FtrProviderContext) { sourceId: 'default', nodeId: 'c74b04834c6d7cc1800c3afbe31d0c8c0c267f06e9eb45c2b0c2df3e6cee40c5', nodeType: InfraNodeType.container, + timeRange: timeRange800withAws, }); if (metadata) { expect(metadata.features.length).to.be(26); @@ -251,6 +279,7 @@ export default function({ getService }: FtrProviderContext) { sourceId: 'default', nodeId: 'gke-observability-8--observability-8--bc1afd95-f0zc', nodeType: 'host', + timeRange: timeRange800, }); if (metadata) { expect( @@ -265,6 +294,7 @@ export default function({ getService }: FtrProviderContext) { sourceId: 'default', nodeId: 'c1031331-9ae0-11e9-9a96-42010a84004d', nodeType: 'pod', + timeRange: timeRange800, }); if (metadata) { expect( diff --git a/x-pack/test/api_integration/apis/logstash/cluster/index.js b/x-pack/test/api_integration/apis/logstash/cluster/index.js deleted file mode 100644 index f016fde97ee4b..0000000000000 --- a/x-pack/test/api_integration/apis/logstash/cluster/index.js +++ /dev/null @@ -1,11 +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. - */ - -export default function({ loadTestFile }) { - describe('cluster', () => { - loadTestFile(require.resolve('./load')); - }); -} diff --git a/x-pack/test/api_integration/apis/logstash/cluster/index.ts b/x-pack/test/api_integration/apis/logstash/cluster/index.ts new file mode 100644 index 0000000000000..1d4fbd40b252b --- /dev/null +++ b/x-pack/test/api_integration/apis/logstash/cluster/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 { FtrProviderContext } from '../../../ftr_provider_context'; + +export default function({ loadTestFile }: FtrProviderContext) { + describe('cluster', () => { + loadTestFile(require.resolve('./load')); + }); +} diff --git a/x-pack/test/api_integration/apis/logstash/cluster/load.js b/x-pack/test/api_integration/apis/logstash/cluster/load.js deleted file mode 100644 index a348c6e4857b1..0000000000000 --- a/x-pack/test/api_integration/apis/logstash/cluster/load.js +++ /dev/null @@ -1,21 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import expect from '@kbn/expect'; - -export default function({ getService }) { - const supertest = getService('supertest'); - const es = getService('legacyEs'); - - describe('load', () => { - it('should return the ES cluster info', async () => { - const { body } = await supertest.get('/api/logstash/cluster').expect(200); - - const responseFromES = await es.info(); - expect(body.cluster.uuid).to.eql(responseFromES.cluster_uuid); - }); - }); -} diff --git a/x-pack/test/api_integration/apis/logstash/cluster/load.ts b/x-pack/test/api_integration/apis/logstash/cluster/load.ts new file mode 100644 index 0000000000000..0678c5faf82dc --- /dev/null +++ b/x-pack/test/api_integration/apis/logstash/cluster/load.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 expect from '@kbn/expect'; +import { FtrProviderContext } from '../../../ftr_provider_context'; + +export default function({ getService }: FtrProviderContext) { + const supertest = getService('supertest'); + const es = getService('legacyEs'); + + describe('load', () => { + it('should return the ES cluster info', async () => { + const { body } = await supertest.get('/api/logstash/cluster').expect(200); + + const responseFromES = await es.info(); + expect(body.cluster.uuid).to.eql(responseFromES.cluster_uuid); + }); + }); +} diff --git a/x-pack/test/api_integration/apis/logstash/index.js b/x-pack/test/api_integration/apis/logstash/index.js deleted file mode 100644 index 53293e5ff9423..0000000000000 --- a/x-pack/test/api_integration/apis/logstash/index.js +++ /dev/null @@ -1,13 +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. - */ - -export default function({ loadTestFile }) { - describe('logstash', () => { - loadTestFile(require.resolve('./pipelines')); - loadTestFile(require.resolve('./pipeline')); - loadTestFile(require.resolve('./cluster')); - }); -} diff --git a/x-pack/test/api_integration/apis/logstash/index.ts b/x-pack/test/api_integration/apis/logstash/index.ts new file mode 100644 index 0000000000000..582bef5a53bf2 --- /dev/null +++ b/x-pack/test/api_integration/apis/logstash/index.ts @@ -0,0 +1,14 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { FtrProviderContext } from '../../ftr_provider_context'; + +export default function({ loadTestFile }: FtrProviderContext) { + describe('logstash', () => { + loadTestFile(require.resolve('./pipelines')); + loadTestFile(require.resolve('./pipeline')); + loadTestFile(require.resolve('./cluster')); + }); +} diff --git a/x-pack/test/api_integration/apis/logstash/pipeline/delete.js b/x-pack/test/api_integration/apis/logstash/pipeline/delete.js deleted file mode 100644 index 85813f4ed04d1..0000000000000 --- a/x-pack/test/api_integration/apis/logstash/pipeline/delete.js +++ /dev/null @@ -1,43 +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. - */ - -export default function({ getService }) { - const supertest = getService('supertest'); - const esArchiver = getService('esArchiver'); - describe('delete', () => { - const archive = 'logstash/empty'; - - before('load pipelines archive', async () => { - await esArchiver.load(archive); - - await supertest - .put('/api/logstash/pipeline/fast_generator') - .set('kbn-xsrf', 'xxx') - .send({ - id: 'fast_generator', - description: 'foobar baz', - username: 'seger', - pipeline: 'input { generator {} }\n\n output { stdout {} }', - }) - .expect(204); - - await supertest.get('/api/logstash/pipeline/fast_generator').expect(200); - }); - - after('unload pipelines archive', () => { - return esArchiver.unload(archive); - }); - - it('should delete the specified pipeline', async () => { - await supertest - .delete('/api/logstash/pipeline/fast_generator') - .set('kbn-xsrf', 'xxx') - .expect(204); - - await supertest.get('/api/logstash/pipeline/fast_generator').expect(404); - }); - }); -} diff --git a/x-pack/test/api_integration/apis/logstash/pipeline/delete.ts b/x-pack/test/api_integration/apis/logstash/pipeline/delete.ts new file mode 100644 index 0000000000000..cdbf5a3e6a1fe --- /dev/null +++ b/x-pack/test/api_integration/apis/logstash/pipeline/delete.ts @@ -0,0 +1,44 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { FtrProviderContext } from '../../../ftr_provider_context'; + +export default function({ getService }: FtrProviderContext) { + const supertest = getService('supertest'); + const esArchiver = getService('esArchiver'); + describe('delete', () => { + const archive = 'logstash/empty'; + + before('load pipelines archive', async () => { + await esArchiver.load(archive); + + await supertest + .put('/api/logstash/pipeline/fast_generator') + .set('kbn-xsrf', 'xxx') + .send({ + id: 'fast_generator', + description: 'foobar baz', + username: 'seger', + pipeline: 'input { generator {} }\n\n output { stdout {} }', + }) + .expect(204); + + await supertest.get('/api/logstash/pipeline/fast_generator').expect(200); + }); + + after('unload pipelines archive', () => { + return esArchiver.unload(archive); + }); + + it('should delete the specified pipeline', async () => { + await supertest + .delete('/api/logstash/pipeline/fast_generator') + .set('kbn-xsrf', 'xxx') + .expect(204); + + await supertest.get('/api/logstash/pipeline/fast_generator').expect(404); + }); + }); +} diff --git a/x-pack/test/api_integration/apis/logstash/pipeline/index.js b/x-pack/test/api_integration/apis/logstash/pipeline/index.js deleted file mode 100644 index dcc8a01378e37..0000000000000 --- a/x-pack/test/api_integration/apis/logstash/pipeline/index.js +++ /dev/null @@ -1,13 +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. - */ - -export default function({ loadTestFile }) { - describe('pipeline', () => { - loadTestFile(require.resolve('./load')); - loadTestFile(require.resolve('./save')); - loadTestFile(require.resolve('./delete')); - }); -} diff --git a/x-pack/test/api_integration/apis/logstash/pipeline/index.ts b/x-pack/test/api_integration/apis/logstash/pipeline/index.ts new file mode 100644 index 0000000000000..2697f7f428f5f --- /dev/null +++ b/x-pack/test/api_integration/apis/logstash/pipeline/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. + */ +import { FtrProviderContext } from '../../../ftr_provider_context'; +export default function({ loadTestFile }: FtrProviderContext) { + describe('pipeline', () => { + loadTestFile(require.resolve('./load')); + loadTestFile(require.resolve('./save')); + loadTestFile(require.resolve('./delete')); + }); +} diff --git a/x-pack/test/api_integration/apis/logstash/pipeline/load.js b/x-pack/test/api_integration/apis/logstash/pipeline/load.js deleted file mode 100644 index eb2ab6500a9dc..0000000000000 --- a/x-pack/test/api_integration/apis/logstash/pipeline/load.js +++ /dev/null @@ -1,30 +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 expect from '@kbn/expect'; -import pipeline from './fixtures/load'; - -export default function({ getService }) { - const supertest = getService('supertest'); - const esArchiver = getService('esArchiver'); - describe('list', () => { - const archive = 'logstash/example_pipelines'; - - before('load pipelines archive', () => { - return esArchiver.load(archive); - }); - - after('unload pipelines archive', () => { - return esArchiver.unload(archive); - }); - - it('should return the specified pipeline', async () => { - const { body } = await supertest.get('/api/logstash/pipeline/tweets_and_beats').expect(200); - - expect(body).to.eql(pipeline); - }); - }); -} diff --git a/x-pack/test/api_integration/apis/logstash/pipeline/load.ts b/x-pack/test/api_integration/apis/logstash/pipeline/load.ts new file mode 100644 index 0000000000000..a892f527a6e61 --- /dev/null +++ b/x-pack/test/api_integration/apis/logstash/pipeline/load.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 expect from '@kbn/expect'; +import { FtrProviderContext } from '../../../ftr_provider_context'; + +import pipeline from './fixtures/load.json'; + +export default function({ getService }: FtrProviderContext) { + const supertest = getService('supertest'); + const esArchiver = getService('esArchiver'); + describe('list', () => { + const archive = 'logstash/example_pipelines'; + + before('load pipelines archive', () => { + return esArchiver.load(archive); + }); + + after('unload pipelines archive', () => { + return esArchiver.unload(archive); + }); + + it('should return the specified pipeline', async () => { + const { body } = await supertest.get('/api/logstash/pipeline/tweets_and_beats').expect(200); + + expect(body).to.eql(pipeline); + }); + }); +} diff --git a/x-pack/test/api_integration/apis/logstash/pipeline/save.js b/x-pack/test/api_integration/apis/logstash/pipeline/save.js deleted file mode 100644 index ad35ee21f00fc..0000000000000 --- a/x-pack/test/api_integration/apis/logstash/pipeline/save.js +++ /dev/null @@ -1,40 +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 expect from '@kbn/expect'; - -export default function({ getService }) { - const supertest = getService('supertest'); - const esArchiver = getService('esArchiver'); - describe('save', () => { - const archive = 'logstash/empty'; - - before('load pipelines archive', () => { - return esArchiver.load(archive); - }); - - after('unload pipelines archive', () => { - return esArchiver.unload(archive); - }); - - it('should create the specified pipeline', async () => { - await supertest - .put('/api/logstash/pipeline/fast_generator') - .set('kbn-xsrf', 'xxx') - .send({ - id: 'fast_generator', - description: 'foobar baz', - username: 'seger', - pipeline: 'input { generator {} }\n\n output { stdout {} }', - }) - .expect(204); - - const { body } = await supertest.get('/api/logstash/pipeline/fast_generator').expect(200); - - expect(body.description).to.eql('foobar baz'); - }); - }); -} diff --git a/x-pack/test/api_integration/apis/logstash/pipeline/save.ts b/x-pack/test/api_integration/apis/logstash/pipeline/save.ts new file mode 100644 index 0000000000000..2ca9fbe7d68e0 --- /dev/null +++ b/x-pack/test/api_integration/apis/logstash/pipeline/save.ts @@ -0,0 +1,41 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import expect from '@kbn/expect'; +import { FtrProviderContext } from '../../../ftr_provider_context'; + +export default function({ getService }: FtrProviderContext) { + const supertest = getService('supertest'); + const esArchiver = getService('esArchiver'); + describe('save', () => { + const archive = 'logstash/empty'; + + before('load pipelines archive', () => { + return esArchiver.load(archive); + }); + + after('unload pipelines archive', () => { + return esArchiver.unload(archive); + }); + + it('should create the specified pipeline', async () => { + await supertest + .put('/api/logstash/pipeline/fast_generator') + .set('kbn-xsrf', 'xxx') + .send({ + id: 'fast_generator', + description: 'foobar baz', + username: 'seger', + pipeline: 'input { generator {} }\n\n output { stdout {} }', + }) + .expect(204); + + const { body } = await supertest.get('/api/logstash/pipeline/fast_generator').expect(200); + + expect(body.description).to.eql('foobar baz'); + }); + }); +} diff --git a/x-pack/test/api_integration/apis/logstash/pipelines/delete.js b/x-pack/test/api_integration/apis/logstash/pipelines/delete.js deleted file mode 100644 index 98ff5b99ea744..0000000000000 --- a/x-pack/test/api_integration/apis/logstash/pipelines/delete.js +++ /dev/null @@ -1,36 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -export default function({ getService }) { - const supertest = getService('supertest'); - const esArchiver = getService('esArchiver'); - describe('delete', () => { - const archive = 'logstash/example_pipelines'; - - before('load pipelines archive', async () => { - await esArchiver.load(archive); - await supertest.get('/api/logstash/pipeline/empty_pipeline_1').expect(200); - await supertest.get('/api/logstash/pipeline/empty_pipeline_2').expect(200); - }); - - after('unload pipelines archive', () => { - return esArchiver.unload(archive); - }); - - it('should delete the specified pipelines', async () => { - await supertest - .post('/api/logstash/pipelines/delete') - .set('kbn-xsrf', 'xxx') - .send({ - pipelineIds: ['empty_pipeline_1', 'empty_pipeline_2'], - }) - .expect(200); - - await supertest.get('/api/logstash/pipeline/empty_pipeline_1').expect(404); - await supertest.get('/api/logstash/pipeline/empty_pipeline_2').expect(404); - }); - }); -} diff --git a/x-pack/test/api_integration/apis/logstash/pipelines/delete.ts b/x-pack/test/api_integration/apis/logstash/pipelines/delete.ts new file mode 100644 index 0000000000000..e71dc7f08ddc9 --- /dev/null +++ b/x-pack/test/api_integration/apis/logstash/pipelines/delete.ts @@ -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 { FtrProviderContext } from '../../../ftr_provider_context'; + +export default function({ getService }: FtrProviderContext) { + const supertest = getService('supertest'); + const esArchiver = getService('esArchiver'); + describe('delete', () => { + const archive = 'logstash/example_pipelines'; + + before('load pipelines archive', async () => { + await esArchiver.load(archive); + await supertest.get('/api/logstash/pipeline/empty_pipeline_1').expect(200); + await supertest.get('/api/logstash/pipeline/empty_pipeline_2').expect(200); + }); + + after('unload pipelines archive', () => { + return esArchiver.unload(archive); + }); + + it('should delete the specified pipelines', async () => { + await supertest + .post('/api/logstash/pipelines/delete') + .set('kbn-xsrf', 'xxx') + .send({ + pipelineIds: ['empty_pipeline_1', 'empty_pipeline_2'], + }) + .expect(200); + + await supertest.get('/api/logstash/pipeline/empty_pipeline_1').expect(404); + await supertest.get('/api/logstash/pipeline/empty_pipeline_2').expect(404); + }); + }); +} diff --git a/x-pack/test/api_integration/apis/logstash/pipelines/index.js b/x-pack/test/api_integration/apis/logstash/pipelines/index.js deleted file mode 100644 index 3abe2ee5ac43d..0000000000000 --- a/x-pack/test/api_integration/apis/logstash/pipelines/index.js +++ /dev/null @@ -1,12 +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. - */ - -export default function({ loadTestFile }) { - describe('pipelines', () => { - loadTestFile(require.resolve('./list')); - loadTestFile(require.resolve('./delete')); - }); -} diff --git a/x-pack/test/api_integration/apis/logstash/pipelines/index.ts b/x-pack/test/api_integration/apis/logstash/pipelines/index.ts new file mode 100644 index 0000000000000..510bd625b54a0 --- /dev/null +++ b/x-pack/test/api_integration/apis/logstash/pipelines/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. + */ +import { FtrProviderContext } from '../../../ftr_provider_context'; + +export default function({ loadTestFile }: FtrProviderContext) { + describe('pipelines', () => { + loadTestFile(require.resolve('./list')); + loadTestFile(require.resolve('./delete')); + }); +} diff --git a/x-pack/test/api_integration/apis/logstash/pipelines/list.js b/x-pack/test/api_integration/apis/logstash/pipelines/list.js deleted file mode 100644 index fe5c3222a2ab1..0000000000000 --- a/x-pack/test/api_integration/apis/logstash/pipelines/list.js +++ /dev/null @@ -1,30 +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 expect from '@kbn/expect'; -import pipelineList from './fixtures/list'; - -export default function({ getService }) { - const supertest = getService('supertest'); - const esArchiver = getService('esArchiver'); - describe('list', () => { - const archive = 'logstash/example_pipelines'; - - before('load pipelines archive', () => { - return esArchiver.load(archive); - }); - - after('unload pipelines archive', () => { - return esArchiver.unload(archive); - }); - - it('should return all the pipelines', async () => { - const { body } = await supertest.get('/api/logstash/pipelines').expect(200); - - expect(body).to.eql(pipelineList); - }); - }); -} diff --git a/x-pack/test/api_integration/apis/logstash/pipelines/list.ts b/x-pack/test/api_integration/apis/logstash/pipelines/list.ts new file mode 100644 index 0000000000000..a4ef52791ab70 --- /dev/null +++ b/x-pack/test/api_integration/apis/logstash/pipelines/list.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 expect from '@kbn/expect'; +import { FtrProviderContext } from '../../../ftr_provider_context'; +import pipelineList from './fixtures/list.json'; + +export default function({ getService }: FtrProviderContext) { + const supertest = getService('supertest'); + const esArchiver = getService('esArchiver'); + describe('list', () => { + const archive = 'logstash/example_pipelines'; + + before('load pipelines archive', () => { + return esArchiver.load(archive); + }); + + after('unload pipelines archive', () => { + return esArchiver.unload(archive); + }); + + it('should return all the pipelines', async () => { + const { body } = await supertest.get('/api/logstash/pipelines').expect(200); + + expect(body).to.eql(pipelineList); + }); + }); +} diff --git a/x-pack/test/api_integration/apis/management/cross_cluster_replication/follower_indices.helpers.js b/x-pack/test/api_integration/apis/management/cross_cluster_replication/follower_indices.helpers.js index dbcb6bf819749..294db29f6dce4 100644 --- a/x-pack/test/api_integration/apis/management/cross_cluster_replication/follower_indices.helpers.js +++ b/x-pack/test/api_integration/apis/management/cross_cluster_replication/follower_indices.helpers.js @@ -13,7 +13,45 @@ export const registerHelpers = supertest => { const loadFollowerIndices = () => supertest.get(`${API_BASE_PATH}/follower_indices`); - const getFollowerIndex = name => supertest.get(`${API_BASE_PATH}/follower_indices/${name}`); + const getFollowerIndex = (name, waitUntilIsActive = false) => { + const maxRetries = 10; + const delayBetweenRetries = 500; + let retryCount = 0; + + const proceed = async () => { + const response = await supertest.get(`${API_BASE_PATH}/follower_indices/${name}`); + + if (waitUntilIsActive && response.body.status !== 'active') { + retryCount += 1; + + if (retryCount > maxRetries) { + throw new Error('Error waiting for follower index to be active.'); + } + + return new Promise(resolve => setTimeout(resolve, delayBetweenRetries)).then(proceed); + } + + return response; + }; + + return { + expect: status => + new Promise((resolve, reject) => + proceed() + .then(response => { + if (status !== response.status) { + reject(new Error(`Expected status ${status} but got ${response.status}`)); + } + return resolve(response); + }) + .catch(reject) + ), + then: (resolve, reject) => + proceed() + .then(resolve) + .catch(reject), + }; + }; const createFollowerIndex = (name = getRandomString(), payload = getFollowerIndexPayload()) => { followerIndicesCreated.push(name); @@ -24,6 +62,13 @@ export const registerHelpers = supertest => { .send({ ...payload, name }); }; + const updateFollowerIndex = (name, payload) => { + return supertest + .put(`${API_BASE_PATH}/follower_indices/${name}`) + .set('kbn-xsrf', 'xxx') + .send(payload); + }; + const unfollowLeaderIndex = followerIndex => { const followerIndices = Array.isArray(followerIndex) ? followerIndex : [followerIndex]; const followerIndicesToEncodedString = followerIndices @@ -51,6 +96,7 @@ export const registerHelpers = supertest => { loadFollowerIndices, getFollowerIndex, createFollowerIndex, + updateFollowerIndex, unfollowAll, }; }; diff --git a/x-pack/test/api_integration/apis/management/cross_cluster_replication/follower_indices.js b/x-pack/test/api_integration/apis/management/cross_cluster_replication/follower_indices.js index 5f9ebbd2a0a3f..eabf474120f2b 100644 --- a/x-pack/test/api_integration/apis/management/cross_cluster_replication/follower_indices.js +++ b/x-pack/test/api_integration/apis/management/cross_cluster_replication/follower_indices.js @@ -21,6 +21,7 @@ export default function({ getService }) { loadFollowerIndices, getFollowerIndex, createFollowerIndex, + updateFollowerIndex, unfollowAll, } = registerFollowerIndicesnHelpers(supertest); @@ -92,6 +93,31 @@ export default function({ getService }) { }); }); + describe('update()', () => { + it('should update a follower index advanced settings', async () => { + // Create a follower index + const leaderIndex = await createIndex(); + const followerIndex = getRandomString(); + const initialValue = 1234; + const payload = getFollowerIndexPayload(leaderIndex, undefined, { + maxReadRequestOperationCount: initialValue, + }); + await createFollowerIndex(followerIndex, payload); + + // Verify that its advanced settings are correctly set + const { body } = await getFollowerIndex(followerIndex, true); + expect(body.maxReadRequestOperationCount).to.be(initialValue); + + // Update the follower index + const updatedValue = 7777; + await updateFollowerIndex(followerIndex, { maxReadRequestOperationCount: updatedValue }); + + // Verify that the advanced settings are updated + const { body: updatedBody } = await getFollowerIndex(followerIndex, true); + expect(updatedBody.maxReadRequestOperationCount).to.be(updatedValue); + }); + }); + describe('Advanced settings', () => { it('hard-coded values should match Elasticsearch default values', async () => { /** diff --git a/x-pack/test/api_integration/apis/management/index_lifecycle_management/templates.helpers.js b/x-pack/test/api_integration/apis/management/index_lifecycle_management/templates.helpers.js index 8778ab5c807f5..257cca36b2ecf 100644 --- a/x-pack/test/api_integration/apis/management/index_lifecycle_management/templates.helpers.js +++ b/x-pack/test/api_integration/apis/management/index_lifecycle_management/templates.helpers.js @@ -9,8 +9,6 @@ import { API_BASE_PATH } from './constants'; export const registerHelpers = ({ supertest }) => { const loadTemplates = () => supertest.get(`${API_BASE_PATH}/templates`); - const getTemplate = name => supertest.get(`${API_BASE_PATH}/templates/${name}`); - const addPolicyToTemplate = (templateName, policyName, aliasName) => supertest .post(`${API_BASE_PATH}/template`) @@ -23,7 +21,6 @@ export const registerHelpers = ({ supertest }) => { return { loadTemplates, - getTemplate, addPolicyToTemplate, }; }; diff --git a/x-pack/test/api_integration/apis/management/index_lifecycle_management/templates.js b/x-pack/test/api_integration/apis/management/index_lifecycle_management/templates.js index 374577825cd94..d30c20527471b 100644 --- a/x-pack/test/api_integration/apis/management/index_lifecycle_management/templates.js +++ b/x-pack/test/api_integration/apis/management/index_lifecycle_management/templates.js @@ -16,7 +16,7 @@ export default function({ getService }) { const { createIndexTemplate, cleanUp: cleanUpEsResources } = initElasticsearchHelpers(es); - const { loadTemplates, getTemplate, addPolicyToTemplate } = registerTemplatesHelpers({ + const { loadTemplates, addPolicyToTemplate } = registerTemplatesHelpers({ supertest, }); @@ -48,18 +48,6 @@ export default function({ getService }) { }); }); - describe('get', () => { - it('should fetch a single template', async () => { - // Create a template with the ES client - const templateName = getRandomString(); - const template = getTemplatePayload(); - await createIndexTemplate(templateName, template); - - const { body } = await getTemplate(templateName).expect(200); - expect(body.index_patterns).to.eql(template.index_patterns); - }); - }); - describe('update', () => { it('should add a policy to a template', async () => { // Create policy @@ -78,12 +66,13 @@ export default function({ getService }) { await addPolicyToTemplate(templateName, policyName, rolloverAlias).expect(200); // Fetch the template and verify that the policy has been attached - const { body } = await getTemplate(templateName); + const { body } = await loadTemplates(); + const fetchedTemplate = body.find(({ name }) => templateName === name); const { settings: { index: { lifecycle }, }, - } = body; + } = fetchedTemplate; expect(lifecycle.name).to.equal(policyName); expect(lifecycle.rollover_alias).to.equal(rolloverAlias); }); diff --git a/x-pack/test/api_integration/apis/maps/migrations.js b/x-pack/test/api_integration/apis/maps/migrations.js index c4587530e160b..cd575899118a3 100644 --- a/x-pack/test/api_integration/apis/maps/migrations.js +++ b/x-pack/test/api_integration/apis/maps/migrations.js @@ -41,7 +41,7 @@ export default function({ getService }) { type: 'index-pattern', }, ]); - expect(resp.body.migrationVersion).to.eql({ map: '7.7.0' }); + expect(resp.body.migrationVersion).to.eql({ map: '7.8.0' }); expect(resp.body.attributes.layerListJSON.includes('indexPatternRefName')).to.be(true); }); }); diff --git a/x-pack/test/api_integration/apis/ml/bucket_span_estimator.ts b/x-pack/test/api_integration/apis/ml/bucket_span_estimator.ts index 3f56fb927d131..bc0dc3019d7c9 100644 --- a/x-pack/test/api_integration/apis/ml/bucket_span_estimator.ts +++ b/x-pack/test/api_integration/apis/ml/bucket_span_estimator.ts @@ -28,7 +28,7 @@ export default ({ getService }: FtrProviderContext) => { aggTypes: ['avg'], duration: { start: 1560297859000, end: 1562975136000 }, fields: ['taxless_total_price'], - index: 'ecommerce', + index: 'ft_ecommerce', query: { bool: { must: [{ match_all: {} }] } }, timeField: 'order_date', }, @@ -44,7 +44,7 @@ export default ({ getService }: FtrProviderContext) => { aggTypes: ['avg', 'sum'], duration: { start: 1560297859000, end: 1562975136000 }, fields: ['products.base_price', 'products.base_unit_price'], - index: 'ecommerce', + index: 'ft_ecommerce', query: { bool: { must: [{ match_all: {} }] } }, timeField: 'order_date', }, @@ -60,7 +60,7 @@ export default ({ getService }: FtrProviderContext) => { aggTypes: ['avg'], duration: { start: 1560297859000, end: 1562975136000 }, fields: ['taxless_total_price'], - index: 'ecommerce', + index: 'ft_ecommerce', query: { bool: { must: [{ match_all: {} }] } }, splitField: 'customer_first_name.keyword', timeField: 'order_date', @@ -78,7 +78,7 @@ export default ({ getService }: FtrProviderContext) => { duration: { start: 1560297859000, end: 1562975136000 }, fields: ['taxless_total_price'], filters: [], - index: 'ecommerce', + index: 'ft_ecommerce', query: { bool: { must: [{ match_all: {} }] } }, timeField: 'order_date', }, @@ -91,11 +91,8 @@ export default ({ getService }: FtrProviderContext) => { describe('bucket span estimator', function() { before(async () => { - await esArchiver.load('ml/ecommerce'); - }); - - after(async () => { - await esArchiver.unload('ml/ecommerce'); + await esArchiver.loadIfNeeded('ml/ecommerce'); + await ml.testResources.setKibanaTimeZoneToUTC(); }); describe('with default settings', function() { diff --git a/x-pack/test/api_integration/apis/ml/calculate_model_memory_limit.ts b/x-pack/test/api_integration/apis/ml/calculate_model_memory_limit.ts index 975a10c2aed2a..59e3dfcca00f9 100644 --- a/x-pack/test/api_integration/apis/ml/calculate_model_memory_limit.ts +++ b/x-pack/test/api_integration/apis/ml/calculate_model_memory_limit.ts @@ -22,7 +22,7 @@ export default ({ getService }: FtrProviderContext) => { testTitleSuffix: 'when no partition field is provided with regular function', user: USER.ML_POWERUSER, requestBody: { - indexPattern: 'ecommerce', + indexPattern: 'ft_ecommerce', analysisConfig: { bucket_span: '15m', detectors: [ @@ -51,7 +51,7 @@ export default ({ getService }: FtrProviderContext) => { testTitleSuffix: 'with 1 metric and 1 influencer same as split field', user: USER.ML_POWERUSER, requestBody: { - indexPattern: 'ecommerce', + indexPattern: 'ft_ecommerce', analysisConfig: { bucket_span: '15m', detectors: [ @@ -77,7 +77,7 @@ export default ({ getService }: FtrProviderContext) => { testTitleSuffix: 'with 3 influencers, split by city', user: USER.ML_POWERUSER, requestBody: { - indexPattern: 'ecommerce', + indexPattern: 'ft_ecommerce', analysisConfig: { bucket_span: '15m', detectors: [ @@ -104,7 +104,7 @@ export default ({ getService }: FtrProviderContext) => { '2 detectors split by city and manufacturer, 4 influencers, filtering by country code', user: USER.ML_POWERUSER, requestBody: { - indexPattern: 'ecommerce', + indexPattern: 'ft_ecommerce', analysisConfig: { bucket_span: '2d', detectors: [ @@ -148,11 +148,8 @@ export default ({ getService }: FtrProviderContext) => { describe('calculate model memory limit', function() { before(async () => { - await esArchiver.load('ml/ecommerce'); - }); - - after(async () => { - await esArchiver.unload('ml/ecommerce'); + await esArchiver.loadIfNeeded('ml/ecommerce'); + await ml.testResources.setKibanaTimeZoneToUTC(); }); for (const testData of testDataList) { diff --git a/x-pack/test/api_integration/apis/ml/categorization_field_examples.ts b/x-pack/test/api_integration/apis/ml/categorization_field_examples.ts index b8ee2e7f6562c..df0153f965942 100644 --- a/x-pack/test/api_integration/apis/ml/categorization_field_examples.ts +++ b/x-pack/test/api_integration/apis/ml/categorization_field_examples.ts @@ -66,7 +66,7 @@ const analyzer = { ], }; const defaultRequestBody = { - indexPatternTitle: 'categorization_functional_test', + indexPatternTitle: 'ft_categorization', query: { bool: { must: [{ match_all: {} }] } }, size: 5, timeField: '@timestamp', @@ -289,11 +289,8 @@ export default ({ getService }: FtrProviderContext) => { describe('Categorization example endpoint - ', function() { before(async () => { - await esArchiver.load('ml/categorization'); - }); - - after(async () => { - await esArchiver.unload('ml/categorization'); + await esArchiver.loadIfNeeded('ml/categorization'); + await ml.testResources.setKibanaTimeZoneToUTC(); }); for (const testData of testDataList) { diff --git a/x-pack/test/api_integration/apis/ml/get_module.ts b/x-pack/test/api_integration/apis/ml/get_module.ts index 6dcd9594fc9aa..a50d3c0abe430 100644 --- a/x-pack/test/api_integration/apis/ml/get_module.ts +++ b/x-pack/test/api_integration/apis/ml/get_module.ts @@ -50,6 +50,10 @@ export default ({ getService }: FtrProviderContext) => { } describe('get_module', function() { + before(async () => { + await ml.testResources.setKibanaTimeZoneToUTC(); + }); + it('lists all modules', async () => { const rspBody = await executeGetModuleRequest('', USER.ML_POWERUSER, 200); expect(rspBody).to.be.an(Array); diff --git a/x-pack/test/api_integration/apis/ml/index.ts b/x-pack/test/api_integration/apis/ml/index.ts index 4e21faa610bfe..f012883c46ca3 100644 --- a/x-pack/test/api_integration/apis/ml/index.ts +++ b/x-pack/test/api_integration/apis/ml/index.ts @@ -7,6 +7,7 @@ import { FtrProviderContext } from '../../ftr_provider_context'; export default function({ getService, loadTestFile }: FtrProviderContext) { + const esArchiver = getService('esArchiver'); const ml = getService('ml'); describe('Machine Learning', function() { @@ -20,6 +21,14 @@ export default function({ getService, loadTestFile }: FtrProviderContext) { after(async () => { await ml.securityCommon.cleanMlUsers(); await ml.securityCommon.cleanMlRoles(); + + await ml.testResources.deleteIndexPattern('kibana_sample_data_logs'); + + await esArchiver.unload('ml/ecommerce'); + await esArchiver.unload('ml/categorization'); + await esArchiver.unload('ml/sample_logs'); + + await ml.testResources.resetKibanaTimeZone(); }); loadTestFile(require.resolve('./bucket_span_estimator')); diff --git a/x-pack/test/api_integration/apis/ml/recognize_module.ts b/x-pack/test/api_integration/apis/ml/recognize_module.ts index 2110bded7394c..8e360579c1459 100644 --- a/x-pack/test/api_integration/apis/ml/recognize_module.ts +++ b/x-pack/test/api_integration/apis/ml/recognize_module.ts @@ -32,7 +32,6 @@ export default ({ getService }: FtrProviderContext) => { }, { testTitleSuffix: 'for non existent index pattern', - sourceDataArchive: 'empty_kibana', indexPattern: 'non-existent-index-pattern', user: USER.ML_POWERUSER, expected: { @@ -53,14 +52,16 @@ export default ({ getService }: FtrProviderContext) => { } describe('module recognizer', function() { + before(async () => { + await ml.testResources.setKibanaTimeZoneToUTC(); + }); + for (const testData of testDataList) { describe('lists matching modules', function() { before(async () => { - await esArchiver.load(testData.sourceDataArchive); - }); - - after(async () => { - await esArchiver.unload(testData.sourceDataArchive); + if (testData.hasOwnProperty('sourceDataArchive')) { + await esArchiver.loadIfNeeded(testData.sourceDataArchive!); + } }); it(testData.testTitleSuffix, async () => { diff --git a/x-pack/test/api_integration/apis/ml/setup_module.ts b/x-pack/test/api_integration/apis/ml/setup_module.ts index 71f3910cd4e93..e603782b25717 100644 --- a/x-pack/test/api_integration/apis/ml/setup_module.ts +++ b/x-pack/test/api_integration/apis/ml/setup_module.ts @@ -25,6 +25,7 @@ export default ({ getService }: FtrProviderContext) => { { testTitleSuffix: 'for sample logs dataset with prefix and startDatafeed false', sourceDataArchive: 'ml/sample_logs', + indexPattern: { name: 'kibana_sample_data_logs', timeField: '@timestamp' }, module: 'sample_data_weblogs', user: USER.ML_POWERUSER, requestBody: { @@ -58,7 +59,6 @@ export default ({ getService }: FtrProviderContext) => { const testDataListNegative = [ { testTitleSuffix: 'for non existent index pattern', - sourceDataArchive: 'empty_kibana', module: 'sample_data_weblogs', user: USER.ML_POWERUSER, requestBody: { @@ -75,6 +75,7 @@ export default ({ getService }: FtrProviderContext) => { { testTitleSuffix: 'for unauthorized user', sourceDataArchive: 'ml/sample_logs', + indexPattern: { name: 'kibana_sample_data_logs', timeField: '@timestamp' }, module: 'sample_data_weblogs', user: USER.ML_UNAUTHORIZED, requestBody: { @@ -118,14 +119,21 @@ export default ({ getService }: FtrProviderContext) => { } describe('module setup', function() { + before(async () => { + await ml.testResources.setKibanaTimeZoneToUTC(); + }); + for (const testData of testDataListPositive) { describe('sets up module data', function() { before(async () => { - await esArchiver.load(testData.sourceDataArchive); + await esArchiver.loadIfNeeded(testData.sourceDataArchive); + await ml.testResources.createIndexPatternIfNeeded( + testData.indexPattern.name, + testData.indexPattern.timeField + ); }); after(async () => { - await esArchiver.unload(testData.sourceDataArchive); await ml.api.cleanMlIndices(); }); @@ -199,11 +207,18 @@ export default ({ getService }: FtrProviderContext) => { for (const testData of testDataListNegative) { describe('rejects request', function() { before(async () => { - await esArchiver.load(testData.sourceDataArchive); + if (testData.hasOwnProperty('sourceDataArchive')) { + await esArchiver.loadIfNeeded(testData.sourceDataArchive!); + } + if (testData.hasOwnProperty('indexPattern')) { + await ml.testResources.createIndexPatternIfNeeded( + testData.indexPattern!.name as string, + testData.indexPattern!.timeField as string + ); + } }); after(async () => { - await esArchiver.unload(testData.sourceDataArchive); await ml.api.cleanMlIndices(); }); diff --git a/x-pack/test/api_integration/apis/siem/overview_host.ts b/x-pack/test/api_integration/apis/siem/overview_host.ts index d32eeaec884fa..7e5cbd7673af7 100644 --- a/x-pack/test/api_integration/apis/siem/overview_host.ts +++ b/x-pack/test/api_integration/apis/siem/overview_host.ts @@ -5,10 +5,11 @@ */ import expect from '@kbn/expect'; + +import { DEFAULT_INDEX_PATTERN } from '../../../../plugins/siem/common/constants'; import { overviewHostQuery } from '../../../../legacy/plugins/siem/public/containers/overview/overview_host/index.gql_query'; import { GetOverviewHostQuery } from '../../../../legacy/plugins/siem/public/graphql/types'; import { FtrProviderContext } from '../../ftr_provider_context'; -import { defaultIndexPattern } from '../../../../legacy/plugins/siem/default_index_pattern'; export default function({ getService }: FtrProviderContext) { const esArchiver = getService('esArchiver'); @@ -51,7 +52,7 @@ export default function({ getService }: FtrProviderContext) { to: TO, from: FROM, }, - defaultIndex: defaultIndexPattern, + defaultIndex: DEFAULT_INDEX_PATTERN, inspect: false, }, }) diff --git a/x-pack/test/api_integration/apis/spaces/saved_objects.ts b/x-pack/test/api_integration/apis/spaces/saved_objects.ts index 05f14a50f2170..7584be7fd8498 100644 --- a/x-pack/test/api_integration/apis/spaces/saved_objects.ts +++ b/x-pack/test/api_integration/apis/spaces/saved_objects.ts @@ -89,7 +89,7 @@ export default function({ getService }: FtrProviderContext) { .expect(404) .then((response: Record) => { expect(response.body).to.eql({ - message: 'Not Found', + message: 'Saved object [space/default] not found', statusCode: 404, error: 'Not Found', }); diff --git a/x-pack/test/api_integration/apis/uptime/feature_controls.ts b/x-pack/test/api_integration/apis/uptime/feature_controls.ts index 4c3b7f97c9544..6d125807e169d 100644 --- a/x-pack/test/api_integration/apis/uptime/feature_controls.ts +++ b/x-pack/test/api_integration/apis/uptime/feature_controls.ts @@ -26,21 +26,10 @@ export default function featureControlsTests({ getService }: FtrProviderContext) expect(result.response).to.have.property('statusCode', 200); }; - const executeRESTAPIQuery = async (username: string, password: string, spaceId?: string) => { - const basePath = spaceId ? `/s/${spaceId}` : ''; - - return await supertest - .get(basePath + API_URLS.INDEX_STATUS) - .auth(username, password) - .set('kbn-xsrf', 'foo') - .then((response: any) => ({ error: undefined, response })) - .catch((error: any) => ({ error, response: undefined })); - }; - const executePingsRequest = async (username: string, password: string, spaceId?: string) => { const basePath = spaceId ? `/s/${spaceId}` : ''; - const url = `${basePath}${API_URLS.PINGS}?sort=desc&dateRangeStart=${PINGS_DATE_RANGE_START}&dateRangeEnd=${PINGS_DATE_RANGE_END}`; + const url = `${basePath}${API_URLS.PINGS}?sort=desc&from=${PINGS_DATE_RANGE_START}&to=${PINGS_DATE_RANGE_END}`; return await supertest .get(url) .auth(username, password) @@ -72,9 +61,6 @@ export default function featureControlsTests({ getService }: FtrProviderContext) full_name: 'a kibana user', }); - const graphQLResult = await executeRESTAPIQuery(username, password); - expect404(graphQLResult); - const pingsResult = await executePingsRequest(username, password); expect404(pingsResult); } finally { @@ -111,9 +97,6 @@ export default function featureControlsTests({ getService }: FtrProviderContext) full_name: 'a kibana user', }); - const graphQLResult = await executeRESTAPIQuery(username, password); - expectResponse(graphQLResult); - const pingsResult = await executePingsRequest(username, password); expectResponse(pingsResult); } finally { @@ -153,9 +136,6 @@ export default function featureControlsTests({ getService }: FtrProviderContext) full_name: 'a kibana user', }); - const graphQLResult = await executeRESTAPIQuery(username, password); - expect404(graphQLResult); - const pingsResult = await executePingsRequest(username, password); expect404(pingsResult); } finally { @@ -222,17 +202,11 @@ export default function featureControlsTests({ getService }: FtrProviderContext) }); it('user_1 can access APIs in space_1', async () => { - const graphQLResult = await executeRESTAPIQuery(username, password, space1Id); - expectResponse(graphQLResult); - const pingsResult = await executePingsRequest(username, password, space1Id); expectResponse(pingsResult); }); it(`user_1 can't access APIs in space_2`, async () => { - const graphQLResult = await executeRESTAPIQuery(username, password); - expect404(graphQLResult); - const pingsResult = await executePingsRequest(username, password); expect404(pingsResult); }); diff --git a/x-pack/test/api_integration/apis/uptime/get_all_pings.ts b/x-pack/test/api_integration/apis/uptime/get_all_pings.ts index 666986e7008b7..0b3f5faccb044 100644 --- a/x-pack/test/api_integration/apis/uptime/get_all_pings.ts +++ b/x-pack/test/api_integration/apis/uptime/get_all_pings.ts @@ -22,7 +22,7 @@ export default function({ getService }: FtrProviderContext) { it('should get all pings stored in index', async () => { const { body: apiResponse } = await supertest .get( - `/api/uptime/pings?sort=desc&dateRangeStart=${PINGS_DATE_RANGE_START}&dateRangeEnd=${PINGS_DATE_RANGE_END}` + `/api/uptime/pings?sort=desc&from=${PINGS_DATE_RANGE_START}&to=${PINGS_DATE_RANGE_END}` ) .expect(200); @@ -33,21 +33,19 @@ export default function({ getService }: FtrProviderContext) { it('should sort pings according to timestamp', async () => { const { body: apiResponse } = await supertest - .get( - `/api/uptime/pings?sort=asc&dateRangeStart=${PINGS_DATE_RANGE_START}&dateRangeEnd=${PINGS_DATE_RANGE_END}` - ) + .get(`/api/uptime/pings?sort=asc&from=${PINGS_DATE_RANGE_START}&to=${PINGS_DATE_RANGE_END}`) .expect(200); expect(apiResponse.total).to.be(2); expect(apiResponse.pings.length).to.be(2); - expect(apiResponse.pings[0].timestamp).to.be('2018-10-30T14:49:23.889Z'); - expect(apiResponse.pings[1].timestamp).to.be('2018-10-30T18:51:56.792Z'); + expect(apiResponse.pings[0]['@timestamp']).to.be('2018-10-30T14:49:23.889Z'); + expect(apiResponse.pings[1]['@timestamp']).to.be('2018-10-30T18:51:56.792Z'); }); it('should return results of n length', async () => { const { body: apiResponse } = await supertest .get( - `/api/uptime/pings?sort=desc&size=1&dateRangeStart=${PINGS_DATE_RANGE_START}&dateRangeEnd=${PINGS_DATE_RANGE_END}` + `/api/uptime/pings?sort=desc&size=1&from=${PINGS_DATE_RANGE_START}&to=${PINGS_DATE_RANGE_END}` ) .expect(200); @@ -57,10 +55,10 @@ export default function({ getService }: FtrProviderContext) { }); it('should miss pings outside of date range', async () => { - const dateRangeStart = moment('2002-01-01').valueOf(); - const dateRangeEnd = moment('2002-01-02').valueOf(); + const from = moment('2002-01-01').valueOf(); + const to = moment('2002-01-02').valueOf(); const { body: apiResponse } = await supertest - .get(`/api/uptime/pings?dateRangeStart=${dateRangeStart}&dateRangeEnd=${dateRangeEnd}`) + .get(`/api/uptime/pings?from=${from}&to=${to}`) .expect(200); expect(apiResponse.total).to.be(0); diff --git a/x-pack/test/api_integration/apis/uptime/graphql/fixtures/monitor_states.json b/x-pack/test/api_integration/apis/uptime/graphql/fixtures/monitor_states.json deleted file mode 100644 index a748225dda7cf..0000000000000 --- a/x-pack/test/api_integration/apis/uptime/graphql/fixtures/monitor_states.json +++ /dev/null @@ -1,1609 +0,0 @@ -{ - "monitorStates": { - "prevPagePagination": null, - "nextPagePagination": "{\"cursorDirection\":\"AFTER\",\"sortOrder\":\"ASC\",\"cursorKey\":{\"monitor_id\":\"0009-up\"}}", - "totalSummaryCount": 2000, - "summaries": [ - { - "monitor_id": "0000-intermittent", - "histogram": { - "count": 20, - "points": [ - { - "timestamp": 1568172664000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172694000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172724000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172754000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172784000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172814000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172844000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172874000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172904000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172934000, - "up": 0, - "down": 1 - }, - { - "timestamp": 1568172964000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172994000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173024000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173054000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173084000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173114000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173144000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173174000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173204000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173234000, - "up": 1, - "down": 0 - } - ] - }, - "state": { - "agent": null, - "checks": [ - { - "agent": { - "id": "04e1d082-65bc-4929-8d65-d0768a2621c4" - }, - "container": null, - "kubernetes": null, - "monitor": { - "ip": "127.0.0.1", - "name": "", - "status": "up" - }, - "observer": { - "geo": { - "name": "mpls", - "location": { - "lat": 37.926867976784706, - "lon": -78.02490200847387 - } - } - }, - "timestamp": "1568173234371" - } - ], - "geo": null, - "observer": { - "geo": { - "name": [ - "mpls" - ], - "location": null - } - }, - "monitor": { - "id": null, - "name": null, - "status": "up", - "type": null - }, - "summary": { - "up": 1, - "down": 0, - "geo": null - }, - "url": { - "full": "http://localhost:5678/pattern?r=200x5,500x1", - "domain": "localhost" - }, - "timestamp": 1568173234371 - } - }, - { - "monitor_id": "0001-up", - "histogram": { - "count": 20, - "points": [ - { - "timestamp": 1568172664000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172694000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172724000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172754000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172784000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172814000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172844000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172874000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172904000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172934000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172964000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172994000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173024000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173054000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173084000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173114000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173144000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173174000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173204000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173234000, - "up": 1, - "down": 0 - } - ] - }, - "state": { - "agent": null, - "checks": [ - { - "agent": { - "id": "04e1d082-65bc-4929-8d65-d0768a2621c4" - }, - "container": null, - "kubernetes": null, - "monitor": { - "ip": "127.0.0.1", - "name": "", - "status": "up" - }, - "observer": { - "geo": { - "name": "mpls", - "location": { - "lat": 37.926867976784706, - "lon": -78.02490200847387 - } - } - }, - "timestamp": "1568173234371" - } - ], - "geo": null, - "observer": { - "geo": { - "name": [ - "mpls" - ], - "location": null - } - }, - "monitor": { - "id": null, - "name": null, - "status": "up", - "type": null - }, - "summary": { - "up": 1, - "down": 0, - "geo": null - }, - "url": { - "full": "http://localhost:5678/pattern?r=200x1", - "domain": "localhost" - }, - "timestamp": 1568173234371 - } - }, - { - "monitor_id": "0002-up", - "histogram": { - "count": 20, - "points": [ - { - "timestamp": 1568172664000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172694000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172724000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172754000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172784000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172814000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172844000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172874000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172904000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172934000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172964000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172994000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173024000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173054000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173084000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173114000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173144000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173174000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173204000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173234000, - "up": 1, - "down": 0 - } - ] - }, - "state": { - "agent": null, - "checks": [ - { - "agent": { - "id": "04e1d082-65bc-4929-8d65-d0768a2621c4" - }, - "container": null, - "kubernetes": null, - "monitor": { - "ip": "127.0.0.1", - "name": "", - "status": "up" - }, - "observer": { - "geo": { - "name": "mpls", - "location": { - "lat": 37.926867976784706, - "lon": -78.02490200847387 - } - } - }, - "timestamp": "1568173234371" - } - ], - "geo": null, - "observer": { - "geo": { - "name": [ - "mpls" - ], - "location": null - } - }, - "monitor": { - "id": null, - "name": null, - "status": "up", - "type": null - }, - "summary": { - "up": 1, - "down": 0, - "geo": null - }, - "url": { - "full": "http://localhost:5678/pattern?r=200x1", - "domain": "localhost" - }, - "timestamp": 1568173234371 - } - }, - { - "monitor_id": "0003-up", - "histogram": { - "count": 20, - "points": [ - { - "timestamp": 1568172664000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172694000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172724000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172754000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172784000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172814000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172844000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172874000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172904000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172934000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172964000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172994000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173024000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173054000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173084000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173114000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173144000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173174000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173204000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173234000, - "up": 1, - "down": 0 - } - ] - }, - "state": { - "agent": null, - "checks": [ - { - "agent": { - "id": "04e1d082-65bc-4929-8d65-d0768a2621c4" - }, - "container": null, - "kubernetes": null, - "monitor": { - "ip": "127.0.0.1", - "name": "", - "status": "up" - }, - "observer": { - "geo": { - "name": "mpls", - "location": { - "lat": 37.926867976784706, - "lon": -78.02490200847387 - } - } - }, - "timestamp": "1568173234371" - } - ], - "geo": null, - "observer": { - "geo": { - "name": [ - "mpls" - ], - "location": null - } - }, - "monitor": { - "id": null, - "name": null, - "status": "up", - "type": null - }, - "summary": { - "up": 1, - "down": 0, - "geo": null - }, - "url": { - "full": "http://localhost:5678/pattern?r=200x1", - "domain": "localhost" - }, - "timestamp": 1568173234371 - } - }, - { - "monitor_id": "0004-up", - "histogram": { - "count": 20, - "points": [ - { - "timestamp": 1568172664000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172694000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172724000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172754000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172784000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172814000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172844000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172874000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172904000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172934000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172964000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172994000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173024000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173054000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173084000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173114000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173144000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173174000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173204000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173234000, - "up": 1, - "down": 0 - } - ] - }, - "state": { - "agent": null, - "checks": [ - { - "agent": { - "id": "04e1d082-65bc-4929-8d65-d0768a2621c4" - }, - "container": null, - "kubernetes": null, - "monitor": { - "ip": "127.0.0.1", - "name": "", - "status": "up" - }, - "observer": { - "geo": { - "name": "mpls", - "location": { - "lat": 37.926867976784706, - "lon": -78.02490200847387 - } - } - }, - "timestamp": "1568173234371" - } - ], - "geo": null, - "observer": { - "geo": { - "name": [ - "mpls" - ], - "location": null - } - }, - "monitor": { - "id": null, - "name": null, - "status": "up", - "type": null - }, - "summary": { - "up": 1, - "down": 0, - "geo": null - }, - "url": { - "full": "http://localhost:5678/pattern?r=200x1", - "domain": "localhost" - }, - "timestamp": 1568173234371 - } - }, - { - "monitor_id": "0005-up", - "histogram": { - "count": 20, - "points": [ - { - "timestamp": 1568172664000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172694000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172724000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172754000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172784000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172814000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172844000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172874000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172904000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172934000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172964000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172994000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173024000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173054000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173084000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173114000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173144000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173174000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173204000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173234000, - "up": 1, - "down": 0 - } - ] - }, - "state": { - "agent": null, - "checks": [ - { - "agent": { - "id": "04e1d082-65bc-4929-8d65-d0768a2621c4" - }, - "container": null, - "kubernetes": null, - "monitor": { - "ip": "127.0.0.1", - "name": "", - "status": "up" - }, - "observer": { - "geo": { - "name": "mpls", - "location": { - "lat": 37.926867976784706, - "lon": -78.02490200847387 - } - } - }, - "timestamp": "1568173234371" - } - ], - "geo": null, - "observer": { - "geo": { - "name": [ - "mpls" - ], - "location": null - } - }, - "monitor": { - "id": null, - "name": null, - "status": "up", - "type": null - }, - "summary": { - "up": 1, - "down": 0, - "geo": null - }, - "url": { - "full": "http://localhost:5678/pattern?r=200x1", - "domain": "localhost" - }, - "timestamp": 1568173234371 - } - }, - { - "monitor_id": "0006-up", - "histogram": { - "count": 20, - "points": [ - { - "timestamp": 1568172664000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172694000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172724000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172754000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172784000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172814000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172844000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172874000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172904000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172934000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172964000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172994000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173024000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173054000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173084000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173114000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173144000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173174000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173204000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173234000, - "up": 1, - "down": 0 - } - ] - }, - "state": { - "agent": null, - "checks": [ - { - "agent": { - "id": "04e1d082-65bc-4929-8d65-d0768a2621c4" - }, - "container": null, - "kubernetes": null, - "monitor": { - "ip": "127.0.0.1", - "name": "", - "status": "up" - }, - "observer": { - "geo": { - "name": "mpls", - "location": { - "lat": 37.926867976784706, - "lon": -78.02490200847387 - } - } - }, - "timestamp": "1568173234371" - } - ], - "geo": null, - "observer": { - "geo": { - "name": [ - "mpls" - ], - "location": null - } - }, - "monitor": { - "id": null, - "name": null, - "status": "up", - "type": null - }, - "summary": { - "up": 1, - "down": 0, - "geo": null - }, - "url": { - "full": "http://localhost:5678/pattern?r=200x1", - "domain": "localhost" - }, - "timestamp": 1568173234371 - } - }, - { - "monitor_id": "0007-up", - "histogram": { - "count": 20, - "points": [ - { - "timestamp": 1568172664000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172694000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172724000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172754000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172784000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172814000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172844000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172874000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172904000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172934000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172964000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172994000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173024000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173054000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173084000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173114000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173144000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173174000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173204000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173234000, - "up": 1, - "down": 0 - } - ] - }, - "state": { - "agent": null, - "checks": [ - { - "agent": { - "id": "04e1d082-65bc-4929-8d65-d0768a2621c4" - }, - "container": null, - "kubernetes": null, - "monitor": { - "ip": "127.0.0.1", - "name": "", - "status": "up" - }, - "observer": { - "geo": { - "name": "mpls", - "location": { - "lat": 37.926867976784706, - "lon": -78.02490200847387 - } - } - }, - "timestamp": "1568173234371" - } - ], - "geo": null, - "observer": { - "geo": { - "name": [ - "mpls" - ], - "location": null - } - }, - "monitor": { - "id": null, - "name": null, - "status": "up", - "type": null - }, - "summary": { - "up": 1, - "down": 0, - "geo": null - }, - "url": { - "full": "http://localhost:5678/pattern?r=200x1", - "domain": "localhost" - }, - "timestamp": 1568173234371 - } - }, - { - "monitor_id": "0008-up", - "histogram": { - "count": 20, - "points": [ - { - "timestamp": 1568172664000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172694000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172724000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172754000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172784000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172814000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172844000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172874000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172904000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172934000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172964000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172994000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173024000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173054000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173084000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173114000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173144000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173174000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173204000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173234000, - "up": 1, - "down": 0 - } - ] - }, - "state": { - "agent": null, - "checks": [ - { - "agent": { - "id": "04e1d082-65bc-4929-8d65-d0768a2621c4" - }, - "container": null, - "kubernetes": null, - "monitor": { - "ip": "127.0.0.1", - "name": "", - "status": "up" - }, - "observer": { - "geo": { - "name": "mpls", - "location": { - "lat": 37.926867976784706, - "lon": -78.02490200847387 - } - } - }, - "timestamp": "1568173234371" - } - ], - "geo": null, - "observer": { - "geo": { - "name": [ - "mpls" - ], - "location": null - } - }, - "monitor": { - "id": null, - "name": null, - "status": "up", - "type": null - }, - "summary": { - "up": 1, - "down": 0, - "geo": null - }, - "url": { - "full": "http://localhost:5678/pattern?r=200x1", - "domain": "localhost" - }, - "timestamp": 1568173234371 - } - }, - { - "monitor_id": "0009-up", - "histogram": { - "count": 20, - "points": [ - { - "timestamp": 1568172664000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172694000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172724000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172754000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172784000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172814000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172844000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172874000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172904000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172934000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172964000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172994000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173024000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173054000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173084000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173114000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173144000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173174000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173204000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173234000, - "up": 1, - "down": 0 - } - ] - }, - "state": { - "agent": null, - "checks": [ - { - "agent": { - "id": "04e1d082-65bc-4929-8d65-d0768a2621c4" - }, - "container": null, - "kubernetes": null, - "monitor": { - "ip": "127.0.0.1", - "name": "", - "status": "up" - }, - "observer": { - "geo": { - "name": "mpls", - "location": { - "lat": 37.926867976784706, - "lon": -78.02490200847387 - } - } - }, - "timestamp": "1568173234371" - } - ], - "geo": null, - "observer": { - "geo": { - "name": [ - "mpls" - ], - "location": null - } - }, - "monitor": { - "id": null, - "name": null, - "status": "up", - "type": null - }, - "summary": { - "up": 1, - "down": 0, - "geo": null - }, - "url": { - "full": "http://localhost:5678/pattern?r=200x1", - "domain": "localhost" - }, - "timestamp": 1568173234371 - } - } - ] - } -} \ No newline at end of file diff --git a/x-pack/test/api_integration/apis/uptime/graphql/fixtures/monitor_states_id_filtered.json b/x-pack/test/api_integration/apis/uptime/graphql/fixtures/monitor_states_id_filtered.json deleted file mode 100644 index 44644be5a0724..0000000000000 --- a/x-pack/test/api_integration/apis/uptime/graphql/fixtures/monitor_states_id_filtered.json +++ /dev/null @@ -1,169 +0,0 @@ -{ - "monitorStates": { - "prevPagePagination": null, - "nextPagePagination": null, - "totalSummaryCount": 2000, - "summaries": [ - { - "monitor_id": "0002-up", - "histogram": { - "count": 20, - "points": [ - { - "timestamp": 1568172664000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172694000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172724000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172754000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172784000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172814000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172844000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172874000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172904000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172934000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172964000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172994000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173024000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173054000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173084000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173114000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173144000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173174000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173204000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173234000, - "up": 1, - "down": 0 - } - ] - }, - "state": { - "agent": null, - "checks": [ - { - "agent": { - "id": "04e1d082-65bc-4929-8d65-d0768a2621c4" - }, - "container": null, - "kubernetes": null, - "monitor": { - "ip": "127.0.0.1", - "name": "", - "status": "up" - }, - "observer": { - "geo": { - "name": "mpls", - "location": { - "lat": 37.926867976784706, - "lon": -78.02490200847387 - } - } - }, - "timestamp": "1568173234371" - } - ], - "geo": null, - "observer": { - "geo": { - "name": [ - "mpls" - ], - "location": null - } - }, - "monitor": { - "id": null, - "name": null, - "status": "up", - "type": null - }, - "summary": { - "up": 1, - "down": 0, - "geo": null - }, - "url": { - "full": "http://localhost:5678/pattern?r=200x1", - "domain": "localhost" - }, - "timestamp": 1568173234371 - } - } - ] - } -} \ No newline at end of file diff --git a/x-pack/test/api_integration/apis/uptime/graphql/fixtures/monitor_states_page_1.json b/x-pack/test/api_integration/apis/uptime/graphql/fixtures/monitor_states_page_1.json deleted file mode 100644 index a748225dda7cf..0000000000000 --- a/x-pack/test/api_integration/apis/uptime/graphql/fixtures/monitor_states_page_1.json +++ /dev/null @@ -1,1609 +0,0 @@ -{ - "monitorStates": { - "prevPagePagination": null, - "nextPagePagination": "{\"cursorDirection\":\"AFTER\",\"sortOrder\":\"ASC\",\"cursorKey\":{\"monitor_id\":\"0009-up\"}}", - "totalSummaryCount": 2000, - "summaries": [ - { - "monitor_id": "0000-intermittent", - "histogram": { - "count": 20, - "points": [ - { - "timestamp": 1568172664000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172694000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172724000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172754000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172784000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172814000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172844000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172874000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172904000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172934000, - "up": 0, - "down": 1 - }, - { - "timestamp": 1568172964000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172994000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173024000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173054000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173084000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173114000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173144000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173174000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173204000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173234000, - "up": 1, - "down": 0 - } - ] - }, - "state": { - "agent": null, - "checks": [ - { - "agent": { - "id": "04e1d082-65bc-4929-8d65-d0768a2621c4" - }, - "container": null, - "kubernetes": null, - "monitor": { - "ip": "127.0.0.1", - "name": "", - "status": "up" - }, - "observer": { - "geo": { - "name": "mpls", - "location": { - "lat": 37.926867976784706, - "lon": -78.02490200847387 - } - } - }, - "timestamp": "1568173234371" - } - ], - "geo": null, - "observer": { - "geo": { - "name": [ - "mpls" - ], - "location": null - } - }, - "monitor": { - "id": null, - "name": null, - "status": "up", - "type": null - }, - "summary": { - "up": 1, - "down": 0, - "geo": null - }, - "url": { - "full": "http://localhost:5678/pattern?r=200x5,500x1", - "domain": "localhost" - }, - "timestamp": 1568173234371 - } - }, - { - "monitor_id": "0001-up", - "histogram": { - "count": 20, - "points": [ - { - "timestamp": 1568172664000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172694000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172724000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172754000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172784000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172814000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172844000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172874000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172904000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172934000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172964000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172994000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173024000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173054000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173084000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173114000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173144000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173174000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173204000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173234000, - "up": 1, - "down": 0 - } - ] - }, - "state": { - "agent": null, - "checks": [ - { - "agent": { - "id": "04e1d082-65bc-4929-8d65-d0768a2621c4" - }, - "container": null, - "kubernetes": null, - "monitor": { - "ip": "127.0.0.1", - "name": "", - "status": "up" - }, - "observer": { - "geo": { - "name": "mpls", - "location": { - "lat": 37.926867976784706, - "lon": -78.02490200847387 - } - } - }, - "timestamp": "1568173234371" - } - ], - "geo": null, - "observer": { - "geo": { - "name": [ - "mpls" - ], - "location": null - } - }, - "monitor": { - "id": null, - "name": null, - "status": "up", - "type": null - }, - "summary": { - "up": 1, - "down": 0, - "geo": null - }, - "url": { - "full": "http://localhost:5678/pattern?r=200x1", - "domain": "localhost" - }, - "timestamp": 1568173234371 - } - }, - { - "monitor_id": "0002-up", - "histogram": { - "count": 20, - "points": [ - { - "timestamp": 1568172664000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172694000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172724000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172754000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172784000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172814000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172844000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172874000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172904000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172934000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172964000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172994000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173024000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173054000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173084000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173114000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173144000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173174000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173204000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173234000, - "up": 1, - "down": 0 - } - ] - }, - "state": { - "agent": null, - "checks": [ - { - "agent": { - "id": "04e1d082-65bc-4929-8d65-d0768a2621c4" - }, - "container": null, - "kubernetes": null, - "monitor": { - "ip": "127.0.0.1", - "name": "", - "status": "up" - }, - "observer": { - "geo": { - "name": "mpls", - "location": { - "lat": 37.926867976784706, - "lon": -78.02490200847387 - } - } - }, - "timestamp": "1568173234371" - } - ], - "geo": null, - "observer": { - "geo": { - "name": [ - "mpls" - ], - "location": null - } - }, - "monitor": { - "id": null, - "name": null, - "status": "up", - "type": null - }, - "summary": { - "up": 1, - "down": 0, - "geo": null - }, - "url": { - "full": "http://localhost:5678/pattern?r=200x1", - "domain": "localhost" - }, - "timestamp": 1568173234371 - } - }, - { - "monitor_id": "0003-up", - "histogram": { - "count": 20, - "points": [ - { - "timestamp": 1568172664000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172694000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172724000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172754000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172784000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172814000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172844000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172874000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172904000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172934000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172964000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172994000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173024000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173054000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173084000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173114000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173144000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173174000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173204000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173234000, - "up": 1, - "down": 0 - } - ] - }, - "state": { - "agent": null, - "checks": [ - { - "agent": { - "id": "04e1d082-65bc-4929-8d65-d0768a2621c4" - }, - "container": null, - "kubernetes": null, - "monitor": { - "ip": "127.0.0.1", - "name": "", - "status": "up" - }, - "observer": { - "geo": { - "name": "mpls", - "location": { - "lat": 37.926867976784706, - "lon": -78.02490200847387 - } - } - }, - "timestamp": "1568173234371" - } - ], - "geo": null, - "observer": { - "geo": { - "name": [ - "mpls" - ], - "location": null - } - }, - "monitor": { - "id": null, - "name": null, - "status": "up", - "type": null - }, - "summary": { - "up": 1, - "down": 0, - "geo": null - }, - "url": { - "full": "http://localhost:5678/pattern?r=200x1", - "domain": "localhost" - }, - "timestamp": 1568173234371 - } - }, - { - "monitor_id": "0004-up", - "histogram": { - "count": 20, - "points": [ - { - "timestamp": 1568172664000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172694000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172724000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172754000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172784000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172814000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172844000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172874000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172904000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172934000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172964000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172994000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173024000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173054000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173084000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173114000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173144000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173174000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173204000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173234000, - "up": 1, - "down": 0 - } - ] - }, - "state": { - "agent": null, - "checks": [ - { - "agent": { - "id": "04e1d082-65bc-4929-8d65-d0768a2621c4" - }, - "container": null, - "kubernetes": null, - "monitor": { - "ip": "127.0.0.1", - "name": "", - "status": "up" - }, - "observer": { - "geo": { - "name": "mpls", - "location": { - "lat": 37.926867976784706, - "lon": -78.02490200847387 - } - } - }, - "timestamp": "1568173234371" - } - ], - "geo": null, - "observer": { - "geo": { - "name": [ - "mpls" - ], - "location": null - } - }, - "monitor": { - "id": null, - "name": null, - "status": "up", - "type": null - }, - "summary": { - "up": 1, - "down": 0, - "geo": null - }, - "url": { - "full": "http://localhost:5678/pattern?r=200x1", - "domain": "localhost" - }, - "timestamp": 1568173234371 - } - }, - { - "monitor_id": "0005-up", - "histogram": { - "count": 20, - "points": [ - { - "timestamp": 1568172664000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172694000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172724000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172754000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172784000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172814000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172844000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172874000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172904000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172934000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172964000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172994000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173024000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173054000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173084000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173114000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173144000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173174000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173204000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173234000, - "up": 1, - "down": 0 - } - ] - }, - "state": { - "agent": null, - "checks": [ - { - "agent": { - "id": "04e1d082-65bc-4929-8d65-d0768a2621c4" - }, - "container": null, - "kubernetes": null, - "monitor": { - "ip": "127.0.0.1", - "name": "", - "status": "up" - }, - "observer": { - "geo": { - "name": "mpls", - "location": { - "lat": 37.926867976784706, - "lon": -78.02490200847387 - } - } - }, - "timestamp": "1568173234371" - } - ], - "geo": null, - "observer": { - "geo": { - "name": [ - "mpls" - ], - "location": null - } - }, - "monitor": { - "id": null, - "name": null, - "status": "up", - "type": null - }, - "summary": { - "up": 1, - "down": 0, - "geo": null - }, - "url": { - "full": "http://localhost:5678/pattern?r=200x1", - "domain": "localhost" - }, - "timestamp": 1568173234371 - } - }, - { - "monitor_id": "0006-up", - "histogram": { - "count": 20, - "points": [ - { - "timestamp": 1568172664000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172694000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172724000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172754000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172784000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172814000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172844000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172874000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172904000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172934000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172964000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172994000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173024000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173054000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173084000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173114000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173144000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173174000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173204000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173234000, - "up": 1, - "down": 0 - } - ] - }, - "state": { - "agent": null, - "checks": [ - { - "agent": { - "id": "04e1d082-65bc-4929-8d65-d0768a2621c4" - }, - "container": null, - "kubernetes": null, - "monitor": { - "ip": "127.0.0.1", - "name": "", - "status": "up" - }, - "observer": { - "geo": { - "name": "mpls", - "location": { - "lat": 37.926867976784706, - "lon": -78.02490200847387 - } - } - }, - "timestamp": "1568173234371" - } - ], - "geo": null, - "observer": { - "geo": { - "name": [ - "mpls" - ], - "location": null - } - }, - "monitor": { - "id": null, - "name": null, - "status": "up", - "type": null - }, - "summary": { - "up": 1, - "down": 0, - "geo": null - }, - "url": { - "full": "http://localhost:5678/pattern?r=200x1", - "domain": "localhost" - }, - "timestamp": 1568173234371 - } - }, - { - "monitor_id": "0007-up", - "histogram": { - "count": 20, - "points": [ - { - "timestamp": 1568172664000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172694000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172724000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172754000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172784000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172814000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172844000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172874000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172904000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172934000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172964000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172994000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173024000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173054000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173084000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173114000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173144000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173174000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173204000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173234000, - "up": 1, - "down": 0 - } - ] - }, - "state": { - "agent": null, - "checks": [ - { - "agent": { - "id": "04e1d082-65bc-4929-8d65-d0768a2621c4" - }, - "container": null, - "kubernetes": null, - "monitor": { - "ip": "127.0.0.1", - "name": "", - "status": "up" - }, - "observer": { - "geo": { - "name": "mpls", - "location": { - "lat": 37.926867976784706, - "lon": -78.02490200847387 - } - } - }, - "timestamp": "1568173234371" - } - ], - "geo": null, - "observer": { - "geo": { - "name": [ - "mpls" - ], - "location": null - } - }, - "monitor": { - "id": null, - "name": null, - "status": "up", - "type": null - }, - "summary": { - "up": 1, - "down": 0, - "geo": null - }, - "url": { - "full": "http://localhost:5678/pattern?r=200x1", - "domain": "localhost" - }, - "timestamp": 1568173234371 - } - }, - { - "monitor_id": "0008-up", - "histogram": { - "count": 20, - "points": [ - { - "timestamp": 1568172664000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172694000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172724000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172754000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172784000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172814000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172844000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172874000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172904000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172934000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172964000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172994000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173024000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173054000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173084000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173114000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173144000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173174000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173204000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173234000, - "up": 1, - "down": 0 - } - ] - }, - "state": { - "agent": null, - "checks": [ - { - "agent": { - "id": "04e1d082-65bc-4929-8d65-d0768a2621c4" - }, - "container": null, - "kubernetes": null, - "monitor": { - "ip": "127.0.0.1", - "name": "", - "status": "up" - }, - "observer": { - "geo": { - "name": "mpls", - "location": { - "lat": 37.926867976784706, - "lon": -78.02490200847387 - } - } - }, - "timestamp": "1568173234371" - } - ], - "geo": null, - "observer": { - "geo": { - "name": [ - "mpls" - ], - "location": null - } - }, - "monitor": { - "id": null, - "name": null, - "status": "up", - "type": null - }, - "summary": { - "up": 1, - "down": 0, - "geo": null - }, - "url": { - "full": "http://localhost:5678/pattern?r=200x1", - "domain": "localhost" - }, - "timestamp": 1568173234371 - } - }, - { - "monitor_id": "0009-up", - "histogram": { - "count": 20, - "points": [ - { - "timestamp": 1568172664000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172694000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172724000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172754000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172784000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172814000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172844000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172874000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172904000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172934000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172964000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172994000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173024000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173054000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173084000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173114000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173144000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173174000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173204000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173234000, - "up": 1, - "down": 0 - } - ] - }, - "state": { - "agent": null, - "checks": [ - { - "agent": { - "id": "04e1d082-65bc-4929-8d65-d0768a2621c4" - }, - "container": null, - "kubernetes": null, - "monitor": { - "ip": "127.0.0.1", - "name": "", - "status": "up" - }, - "observer": { - "geo": { - "name": "mpls", - "location": { - "lat": 37.926867976784706, - "lon": -78.02490200847387 - } - } - }, - "timestamp": "1568173234371" - } - ], - "geo": null, - "observer": { - "geo": { - "name": [ - "mpls" - ], - "location": null - } - }, - "monitor": { - "id": null, - "name": null, - "status": "up", - "type": null - }, - "summary": { - "up": 1, - "down": 0, - "geo": null - }, - "url": { - "full": "http://localhost:5678/pattern?r=200x1", - "domain": "localhost" - }, - "timestamp": 1568173234371 - } - } - ] - } -} \ No newline at end of file diff --git a/x-pack/test/api_integration/apis/uptime/graphql/fixtures/monitor_states_page_10.json b/x-pack/test/api_integration/apis/uptime/graphql/fixtures/monitor_states_page_10.json deleted file mode 100644 index fbd0776fade62..0000000000000 --- a/x-pack/test/api_integration/apis/uptime/graphql/fixtures/monitor_states_page_10.json +++ /dev/null @@ -1,1609 +0,0 @@ -{ - "monitorStates": { - "prevPagePagination": "{\"cursorKey\":{\"monitor_id\":\"0090-intermittent\"},\"sortOrder\":\"ASC\",\"cursorDirection\":\"BEFORE\"}", - "nextPagePagination": null, - "totalSummaryCount": 2000, - "summaries": [ - { - "monitor_id": "0090-intermittent", - "histogram": { - "count": 20, - "points": [ - { - "timestamp": 1568172664000, - "up": 0, - "down": 1 - }, - { - "timestamp": 1568172694000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172724000, - "up": 0, - "down": 1 - }, - { - "timestamp": 1568172754000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172784000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172814000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172844000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172874000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172904000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172934000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172964000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172994000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173024000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173054000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173084000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173114000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173144000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173174000, - "up": 0, - "down": 1 - }, - { - "timestamp": 1568173204000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173234000, - "up": 1, - "down": 0 - } - ] - }, - "state": { - "agent": null, - "checks": [ - { - "agent": { - "id": "04e1d082-65bc-4929-8d65-d0768a2621c4" - }, - "container": null, - "kubernetes": null, - "monitor": { - "ip": "127.0.0.1", - "name": "", - "status": "up" - }, - "observer": { - "geo": { - "name": "mpls", - "location": { - "lat": 37.926867976784706, - "lon": -78.02490200847387 - } - } - }, - "timestamp": "1568173234374" - } - ], - "geo": null, - "observer": { - "geo": { - "name": [ - "mpls" - ], - "location": null - } - }, - "monitor": { - "id": null, - "name": null, - "status": "up", - "type": null - }, - "summary": { - "up": 1, - "down": 0, - "geo": null - }, - "url": { - "full": "http://localhost:5678/pattern?r=200x5,500x1", - "domain": "localhost" - }, - "timestamp": 1568173234374 - } - }, - { - "monitor_id": "0091-up", - "histogram": { - "count": 20, - "points": [ - { - "timestamp": 1568172664000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172694000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172724000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172754000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172784000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172814000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172844000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172874000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172904000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172934000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172964000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172994000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173024000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173054000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173084000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173114000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173144000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173174000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173204000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173234000, - "up": 1, - "down": 0 - } - ] - }, - "state": { - "agent": null, - "checks": [ - { - "agent": { - "id": "04e1d082-65bc-4929-8d65-d0768a2621c4" - }, - "container": null, - "kubernetes": null, - "monitor": { - "ip": "127.0.0.1", - "name": "", - "status": "up" - }, - "observer": { - "geo": { - "name": "mpls", - "location": { - "lat": 37.926867976784706, - "lon": -78.02490200847387 - } - } - }, - "timestamp": "1568173234374" - } - ], - "geo": null, - "observer": { - "geo": { - "name": [ - "mpls" - ], - "location": null - } - }, - "monitor": { - "id": null, - "name": null, - "status": "up", - "type": null - }, - "summary": { - "up": 1, - "down": 0, - "geo": null - }, - "url": { - "full": "http://localhost:5678/pattern?r=200x1", - "domain": "localhost" - }, - "timestamp": 1568173234374 - } - }, - { - "monitor_id": "0092-up", - "histogram": { - "count": 20, - "points": [ - { - "timestamp": 1568172664000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172694000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172724000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172754000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172784000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172814000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172844000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172874000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172904000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172934000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172964000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172994000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173024000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173054000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173084000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173114000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173144000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173174000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173204000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173234000, - "up": 1, - "down": 0 - } - ] - }, - "state": { - "agent": null, - "checks": [ - { - "agent": { - "id": "04e1d082-65bc-4929-8d65-d0768a2621c4" - }, - "container": null, - "kubernetes": null, - "monitor": { - "ip": "127.0.0.1", - "name": "", - "status": "up" - }, - "observer": { - "geo": { - "name": "mpls", - "location": { - "lat": 37.926867976784706, - "lon": -78.02490200847387 - } - } - }, - "timestamp": "1568173234375" - } - ], - "geo": null, - "observer": { - "geo": { - "name": [ - "mpls" - ], - "location": null - } - }, - "monitor": { - "id": null, - "name": null, - "status": "up", - "type": null - }, - "summary": { - "up": 1, - "down": 0, - "geo": null - }, - "url": { - "full": "http://localhost:5678/pattern?r=200x1", - "domain": "localhost" - }, - "timestamp": 1568173234375 - } - }, - { - "monitor_id": "0093-up", - "histogram": { - "count": 20, - "points": [ - { - "timestamp": 1568172664000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172694000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172724000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172754000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172784000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172814000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172844000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172874000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172904000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172934000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172964000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172994000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173024000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173054000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173084000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173114000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173144000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173174000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173204000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173234000, - "up": 1, - "down": 0 - } - ] - }, - "state": { - "agent": null, - "checks": [ - { - "agent": { - "id": "04e1d082-65bc-4929-8d65-d0768a2621c4" - }, - "container": null, - "kubernetes": null, - "monitor": { - "ip": "127.0.0.1", - "name": "", - "status": "up" - }, - "observer": { - "geo": { - "name": "mpls", - "location": { - "lat": 37.926867976784706, - "lon": -78.02490200847387 - } - } - }, - "timestamp": "1568173234375" - } - ], - "geo": null, - "observer": { - "geo": { - "name": [ - "mpls" - ], - "location": null - } - }, - "monitor": { - "id": null, - "name": null, - "status": "up", - "type": null - }, - "summary": { - "up": 1, - "down": 0, - "geo": null - }, - "url": { - "full": "http://localhost:5678/pattern?r=200x1", - "domain": "localhost" - }, - "timestamp": 1568173234375 - } - }, - { - "monitor_id": "0094-up", - "histogram": { - "count": 20, - "points": [ - { - "timestamp": 1568172664000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172694000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172724000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172754000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172784000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172814000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172844000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172874000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172904000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172934000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172964000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172994000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173024000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173054000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173084000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173114000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173144000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173174000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173204000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173234000, - "up": 1, - "down": 0 - } - ] - }, - "state": { - "agent": null, - "checks": [ - { - "agent": { - "id": "04e1d082-65bc-4929-8d65-d0768a2621c4" - }, - "container": null, - "kubernetes": null, - "monitor": { - "ip": "127.0.0.1", - "name": "", - "status": "up" - }, - "observer": { - "geo": { - "name": "mpls", - "location": { - "lat": 37.926867976784706, - "lon": -78.02490200847387 - } - } - }, - "timestamp": "1568173234375" - } - ], - "geo": null, - "observer": { - "geo": { - "name": [ - "mpls" - ], - "location": null - } - }, - "monitor": { - "id": null, - "name": null, - "status": "up", - "type": null - }, - "summary": { - "up": 1, - "down": 0, - "geo": null - }, - "url": { - "full": "http://localhost:5678/pattern?r=200x1", - "domain": "localhost" - }, - "timestamp": 1568173234375 - } - }, - { - "monitor_id": "0095-up", - "histogram": { - "count": 20, - "points": [ - { - "timestamp": 1568172664000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172694000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172724000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172754000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172784000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172814000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172844000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172874000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172904000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172934000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172964000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172994000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173024000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173054000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173084000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173114000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173144000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173174000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173204000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173234000, - "up": 1, - "down": 0 - } - ] - }, - "state": { - "agent": null, - "checks": [ - { - "agent": { - "id": "04e1d082-65bc-4929-8d65-d0768a2621c4" - }, - "container": null, - "kubernetes": null, - "monitor": { - "ip": "127.0.0.1", - "name": "", - "status": "up" - }, - "observer": { - "geo": { - "name": "mpls", - "location": { - "lat": 37.926867976784706, - "lon": -78.02490200847387 - } - } - }, - "timestamp": "1568173234375" - } - ], - "geo": null, - "observer": { - "geo": { - "name": [ - "mpls" - ], - "location": null - } - }, - "monitor": { - "id": null, - "name": null, - "status": "up", - "type": null - }, - "summary": { - "up": 1, - "down": 0, - "geo": null - }, - "url": { - "full": "http://localhost:5678/pattern?r=200x1", - "domain": "localhost" - }, - "timestamp": 1568173234375 - } - }, - { - "monitor_id": "0096-up", - "histogram": { - "count": 20, - "points": [ - { - "timestamp": 1568172664000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172694000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172724000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172754000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172784000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172814000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172844000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172874000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172904000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172934000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172964000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172994000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173024000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173054000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173084000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173114000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173144000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173174000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173204000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173234000, - "up": 1, - "down": 0 - } - ] - }, - "state": { - "agent": null, - "checks": [ - { - "agent": { - "id": "04e1d082-65bc-4929-8d65-d0768a2621c4" - }, - "container": null, - "kubernetes": null, - "monitor": { - "ip": "127.0.0.1", - "name": "", - "status": "up" - }, - "observer": { - "geo": { - "name": "mpls", - "location": { - "lat": 37.926867976784706, - "lon": -78.02490200847387 - } - } - }, - "timestamp": "1568173234376" - } - ], - "geo": null, - "observer": { - "geo": { - "name": [ - "mpls" - ], - "location": null - } - }, - "monitor": { - "id": null, - "name": null, - "status": "up", - "type": null - }, - "summary": { - "up": 1, - "down": 0, - "geo": null - }, - "url": { - "full": "http://localhost:5678/pattern?r=200x1", - "domain": "localhost" - }, - "timestamp": 1568173234376 - } - }, - { - "monitor_id": "0097-up", - "histogram": { - "count": 20, - "points": [ - { - "timestamp": 1568172664000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172694000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172724000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172754000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172784000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172814000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172844000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172874000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172904000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172934000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172964000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172994000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173024000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173054000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173084000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173114000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173144000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173174000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173204000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173234000, - "up": 1, - "down": 0 - } - ] - }, - "state": { - "agent": null, - "checks": [ - { - "agent": { - "id": "04e1d082-65bc-4929-8d65-d0768a2621c4" - }, - "container": null, - "kubernetes": null, - "monitor": { - "ip": "127.0.0.1", - "name": "", - "status": "up" - }, - "observer": { - "geo": { - "name": "mpls", - "location": { - "lat": 37.926867976784706, - "lon": -78.02490200847387 - } - } - }, - "timestamp": "1568173234405" - } - ], - "geo": null, - "observer": { - "geo": { - "name": [ - "mpls" - ], - "location": null - } - }, - "monitor": { - "id": null, - "name": null, - "status": "up", - "type": null - }, - "summary": { - "up": 1, - "down": 0, - "geo": null - }, - "url": { - "full": "http://localhost:5678/pattern?r=200x1", - "domain": "localhost" - }, - "timestamp": 1568173234405 - } - }, - { - "monitor_id": "0098-up", - "histogram": { - "count": 20, - "points": [ - { - "timestamp": 1568172664000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172694000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172724000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172754000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172784000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172814000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172844000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172874000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172904000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172934000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172964000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172994000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173024000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173054000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173084000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173114000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173144000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173174000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173204000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173234000, - "up": 1, - "down": 0 - } - ] - }, - "state": { - "agent": null, - "checks": [ - { - "agent": { - "id": "04e1d082-65bc-4929-8d65-d0768a2621c4" - }, - "container": null, - "kubernetes": null, - "monitor": { - "ip": "127.0.0.1", - "name": "", - "status": "up" - }, - "observer": { - "geo": { - "name": "mpls", - "location": { - "lat": 37.926867976784706, - "lon": -78.02490200847387 - } - } - }, - "timestamp": "1568173234406" - } - ], - "geo": null, - "observer": { - "geo": { - "name": [ - "mpls" - ], - "location": null - } - }, - "monitor": { - "id": null, - "name": null, - "status": "up", - "type": null - }, - "summary": { - "up": 1, - "down": 0, - "geo": null - }, - "url": { - "full": "http://localhost:5678/pattern?r=200x1", - "domain": "localhost" - }, - "timestamp": 1568173234406 - } - }, - { - "monitor_id": "0099-up", - "histogram": { - "count": 20, - "points": [ - { - "timestamp": 1568172664000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172694000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172724000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172754000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172784000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172814000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172844000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172874000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172904000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172934000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172964000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172994000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173024000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173054000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173084000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173114000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173144000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173174000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173204000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173234000, - "up": 1, - "down": 0 - } - ] - }, - "state": { - "agent": null, - "checks": [ - { - "agent": { - "id": "04e1d082-65bc-4929-8d65-d0768a2621c4" - }, - "container": null, - "kubernetes": null, - "monitor": { - "ip": "127.0.0.1", - "name": "", - "status": "up" - }, - "observer": { - "geo": { - "name": "mpls", - "location": { - "lat": 37.926867976784706, - "lon": -78.02490200847387 - } - } - }, - "timestamp": "1568173234406" - } - ], - "geo": null, - "observer": { - "geo": { - "name": [ - "mpls" - ], - "location": null - } - }, - "monitor": { - "id": null, - "name": null, - "status": "up", - "type": null - }, - "summary": { - "up": 1, - "down": 0, - "geo": null - }, - "url": { - "full": "http://localhost:5678/pattern?r=200x1", - "domain": "localhost" - }, - "timestamp": 1568173234406 - } - } - ] - } -} \ No newline at end of file diff --git a/x-pack/test/api_integration/apis/uptime/graphql/fixtures/monitor_states_page_10_previous.json b/x-pack/test/api_integration/apis/uptime/graphql/fixtures/monitor_states_page_10_previous.json deleted file mode 100644 index e630e227f473b..0000000000000 --- a/x-pack/test/api_integration/apis/uptime/graphql/fixtures/monitor_states_page_10_previous.json +++ /dev/null @@ -1,1609 +0,0 @@ -{ - "monitorStates": { - "prevPagePagination": "{\"cursorKey\":{\"monitor_id\":\"0080-down\"},\"sortOrder\":\"ASC\",\"cursorDirection\":\"BEFORE\"}", - "nextPagePagination": "{\"cursorKey\":{\"monitor_id\":\"0089-up\"},\"sortOrder\":\"ASC\",\"cursorDirection\":\"AFTER\"}", - "totalSummaryCount": 2000, - "summaries": [ - { - "monitor_id": "0080-down", - "histogram": { - "count": 20, - "points": [ - { - "timestamp": 1568172664000, - "up": 0, - "down": 1 - }, - { - "timestamp": 1568172694000, - "up": 0, - "down": 1 - }, - { - "timestamp": 1568172724000, - "up": 0, - "down": 1 - }, - { - "timestamp": 1568172754000, - "up": 0, - "down": 1 - }, - { - "timestamp": 1568172784000, - "up": 0, - "down": 1 - }, - { - "timestamp": 1568172814000, - "up": 0, - "down": 1 - }, - { - "timestamp": 1568172844000, - "up": 0, - "down": 1 - }, - { - "timestamp": 1568172874000, - "up": 0, - "down": 1 - }, - { - "timestamp": 1568172904000, - "up": 0, - "down": 1 - }, - { - "timestamp": 1568172934000, - "up": 0, - "down": 1 - }, - { - "timestamp": 1568172964000, - "up": 0, - "down": 1 - }, - { - "timestamp": 1568172994000, - "up": 0, - "down": 1 - }, - { - "timestamp": 1568173024000, - "up": 0, - "down": 1 - }, - { - "timestamp": 1568173054000, - "up": 0, - "down": 1 - }, - { - "timestamp": 1568173084000, - "up": 0, - "down": 1 - }, - { - "timestamp": 1568173114000, - "up": 0, - "down": 1 - }, - { - "timestamp": 1568173144000, - "up": 0, - "down": 1 - }, - { - "timestamp": 1568173174000, - "up": 0, - "down": 1 - }, - { - "timestamp": 1568173204000, - "up": 0, - "down": 1 - }, - { - "timestamp": 1568173234000, - "up": 0, - "down": 1 - } - ] - }, - "state": { - "agent": null, - "checks": [ - { - "agent": { - "id": "04e1d082-65bc-4929-8d65-d0768a2621c4" - }, - "container": null, - "kubernetes": null, - "monitor": { - "ip": "127.0.0.1", - "name": "", - "status": "down" - }, - "observer": { - "geo": { - "name": "mpls", - "location": { - "lat": 37.926867976784706, - "lon": -78.02490200847387 - } - } - }, - "timestamp": "1568173234371" - } - ], - "geo": null, - "observer": { - "geo": { - "name": [ - "mpls" - ], - "location": null - } - }, - "monitor": { - "id": null, - "name": null, - "status": "down", - "type": null - }, - "summary": { - "up": 0, - "down": 1, - "geo": null - }, - "url": { - "full": "http://localhost:5678/pattern?r=400x1", - "domain": "localhost" - }, - "timestamp": 1568173234371 - } - }, - { - "monitor_id": "0081-up", - "histogram": { - "count": 20, - "points": [ - { - "timestamp": 1568172664000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172694000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172724000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172754000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172784000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172814000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172844000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172874000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172904000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172934000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172964000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172994000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173024000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173054000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173084000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173114000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173144000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173174000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173204000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173234000, - "up": 1, - "down": 0 - } - ] - }, - "state": { - "agent": null, - "checks": [ - { - "agent": { - "id": "04e1d082-65bc-4929-8d65-d0768a2621c4" - }, - "container": null, - "kubernetes": null, - "monitor": { - "ip": "127.0.0.1", - "name": "", - "status": "up" - }, - "observer": { - "geo": { - "name": "mpls", - "location": { - "lat": 37.926867976784706, - "lon": -78.02490200847387 - } - } - }, - "timestamp": "1568173234372" - } - ], - "geo": null, - "observer": { - "geo": { - "name": [ - "mpls" - ], - "location": null - } - }, - "monitor": { - "id": null, - "name": null, - "status": "up", - "type": null - }, - "summary": { - "up": 1, - "down": 0, - "geo": null - }, - "url": { - "full": "http://localhost:5678/pattern?r=200x1", - "domain": "localhost" - }, - "timestamp": 1568173234372 - } - }, - { - "monitor_id": "0082-up", - "histogram": { - "count": 20, - "points": [ - { - "timestamp": 1568172664000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172694000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172724000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172754000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172784000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172814000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172844000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172874000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172904000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172934000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172964000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172994000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173024000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173054000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173084000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173114000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173144000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173174000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173204000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173234000, - "up": 1, - "down": 0 - } - ] - }, - "state": { - "agent": null, - "checks": [ - { - "agent": { - "id": "04e1d082-65bc-4929-8d65-d0768a2621c4" - }, - "container": null, - "kubernetes": null, - "monitor": { - "ip": "127.0.0.1", - "name": "", - "status": "up" - }, - "observer": { - "geo": { - "name": "mpls", - "location": { - "lat": 37.926867976784706, - "lon": -78.02490200847387 - } - } - }, - "timestamp": "1568173234372" - } - ], - "geo": null, - "observer": { - "geo": { - "name": [ - "mpls" - ], - "location": null - } - }, - "monitor": { - "id": null, - "name": null, - "status": "up", - "type": null - }, - "summary": { - "up": 1, - "down": 0, - "geo": null - }, - "url": { - "full": "http://localhost:5678/pattern?r=200x1", - "domain": "localhost" - }, - "timestamp": 1568173234372 - } - }, - { - "monitor_id": "0083-up", - "histogram": { - "count": 20, - "points": [ - { - "timestamp": 1568172664000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172694000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172724000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172754000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172784000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172814000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172844000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172874000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172904000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172934000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172964000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172994000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173024000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173054000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173084000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173114000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173144000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173174000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173204000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173234000, - "up": 1, - "down": 0 - } - ] - }, - "state": { - "agent": null, - "checks": [ - { - "agent": { - "id": "04e1d082-65bc-4929-8d65-d0768a2621c4" - }, - "container": null, - "kubernetes": null, - "monitor": { - "ip": "127.0.0.1", - "name": "", - "status": "up" - }, - "observer": { - "geo": { - "name": "mpls", - "location": { - "lat": 37.926867976784706, - "lon": -78.02490200847387 - } - } - }, - "timestamp": "1568173234373" - } - ], - "geo": null, - "observer": { - "geo": { - "name": [ - "mpls" - ], - "location": null - } - }, - "monitor": { - "id": null, - "name": null, - "status": "up", - "type": null - }, - "summary": { - "up": 1, - "down": 0, - "geo": null - }, - "url": { - "full": "http://localhost:5678/pattern?r=200x1", - "domain": "localhost" - }, - "timestamp": 1568173234373 - } - }, - { - "monitor_id": "0084-up", - "histogram": { - "count": 20, - "points": [ - { - "timestamp": 1568172664000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172694000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172724000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172754000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172784000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172814000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172844000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172874000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172904000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172934000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172964000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172994000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173024000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173054000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173084000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173114000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173144000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173174000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173204000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173234000, - "up": 1, - "down": 0 - } - ] - }, - "state": { - "agent": null, - "checks": [ - { - "agent": { - "id": "04e1d082-65bc-4929-8d65-d0768a2621c4" - }, - "container": null, - "kubernetes": null, - "monitor": { - "ip": "127.0.0.1", - "name": "", - "status": "up" - }, - "observer": { - "geo": { - "name": "mpls", - "location": { - "lat": 37.926867976784706, - "lon": -78.02490200847387 - } - } - }, - "timestamp": "1568173234373" - } - ], - "geo": null, - "observer": { - "geo": { - "name": [ - "mpls" - ], - "location": null - } - }, - "monitor": { - "id": null, - "name": null, - "status": "up", - "type": null - }, - "summary": { - "up": 1, - "down": 0, - "geo": null - }, - "url": { - "full": "http://localhost:5678/pattern?r=200x1", - "domain": "localhost" - }, - "timestamp": 1568173234373 - } - }, - { - "monitor_id": "0085-up", - "histogram": { - "count": 20, - "points": [ - { - "timestamp": 1568172664000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172694000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172724000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172754000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172784000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172814000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172844000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172874000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172904000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172934000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172964000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172994000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173024000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173054000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173084000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173114000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173144000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173174000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173204000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173234000, - "up": 1, - "down": 0 - } - ] - }, - "state": { - "agent": null, - "checks": [ - { - "agent": { - "id": "04e1d082-65bc-4929-8d65-d0768a2621c4" - }, - "container": null, - "kubernetes": null, - "monitor": { - "ip": "127.0.0.1", - "name": "", - "status": "up" - }, - "observer": { - "geo": { - "name": "mpls", - "location": { - "lat": 37.926867976784706, - "lon": -78.02490200847387 - } - } - }, - "timestamp": "1568173234373" - } - ], - "geo": null, - "observer": { - "geo": { - "name": [ - "mpls" - ], - "location": null - } - }, - "monitor": { - "id": null, - "name": null, - "status": "up", - "type": null - }, - "summary": { - "up": 1, - "down": 0, - "geo": null - }, - "url": { - "full": "http://localhost:5678/pattern?r=200x1", - "domain": "localhost" - }, - "timestamp": 1568173234373 - } - }, - { - "monitor_id": "0086-up", - "histogram": { - "count": 20, - "points": [ - { - "timestamp": 1568172664000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172694000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172724000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172754000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172784000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172814000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172844000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172874000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172904000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172934000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172964000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172994000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173024000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173054000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173084000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173114000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173144000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173174000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173204000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173234000, - "up": 1, - "down": 0 - } - ] - }, - "state": { - "agent": null, - "checks": [ - { - "agent": { - "id": "04e1d082-65bc-4929-8d65-d0768a2621c4" - }, - "container": null, - "kubernetes": null, - "monitor": { - "ip": "127.0.0.1", - "name": "", - "status": "up" - }, - "observer": { - "geo": { - "name": "mpls", - "location": { - "lat": 37.926867976784706, - "lon": -78.02490200847387 - } - } - }, - "timestamp": "1568173234373" - } - ], - "geo": null, - "observer": { - "geo": { - "name": [ - "mpls" - ], - "location": null - } - }, - "monitor": { - "id": null, - "name": null, - "status": "up", - "type": null - }, - "summary": { - "up": 1, - "down": 0, - "geo": null - }, - "url": { - "full": "http://localhost:5678/pattern?r=200x1", - "domain": "localhost" - }, - "timestamp": 1568173234373 - } - }, - { - "monitor_id": "0087-up", - "histogram": { - "count": 20, - "points": [ - { - "timestamp": 1568172664000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172694000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172724000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172754000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172784000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172814000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172844000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172874000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172904000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172934000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172964000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172994000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173024000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173054000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173084000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173114000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173144000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173174000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173204000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173234000, - "up": 1, - "down": 0 - } - ] - }, - "state": { - "agent": null, - "checks": [ - { - "agent": { - "id": "04e1d082-65bc-4929-8d65-d0768a2621c4" - }, - "container": null, - "kubernetes": null, - "monitor": { - "ip": "127.0.0.1", - "name": "", - "status": "up" - }, - "observer": { - "geo": { - "name": "mpls", - "location": { - "lat": 37.926867976784706, - "lon": -78.02490200847387 - } - } - }, - "timestamp": "1568173234373" - } - ], - "geo": null, - "observer": { - "geo": { - "name": [ - "mpls" - ], - "location": null - } - }, - "monitor": { - "id": null, - "name": null, - "status": "up", - "type": null - }, - "summary": { - "up": 1, - "down": 0, - "geo": null - }, - "url": { - "full": "http://localhost:5678/pattern?r=200x1", - "domain": "localhost" - }, - "timestamp": 1568173234373 - } - }, - { - "monitor_id": "0088-up", - "histogram": { - "count": 20, - "points": [ - { - "timestamp": 1568172664000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172694000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172724000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172754000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172784000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172814000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172844000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172874000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172904000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172934000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172964000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172994000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173024000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173054000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173084000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173114000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173144000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173174000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173204000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173234000, - "up": 1, - "down": 0 - } - ] - }, - "state": { - "agent": null, - "checks": [ - { - "agent": { - "id": "04e1d082-65bc-4929-8d65-d0768a2621c4" - }, - "container": null, - "kubernetes": null, - "monitor": { - "ip": "127.0.0.1", - "name": "", - "status": "up" - }, - "observer": { - "geo": { - "name": "mpls", - "location": { - "lat": 37.926867976784706, - "lon": -78.02490200847387 - } - } - }, - "timestamp": "1568173234373" - } - ], - "geo": null, - "observer": { - "geo": { - "name": [ - "mpls" - ], - "location": null - } - }, - "monitor": { - "id": null, - "name": null, - "status": "up", - "type": null - }, - "summary": { - "up": 1, - "down": 0, - "geo": null - }, - "url": { - "full": "http://localhost:5678/pattern?r=200x1", - "domain": "localhost" - }, - "timestamp": 1568173234373 - } - }, - { - "monitor_id": "0089-up", - "histogram": { - "count": 20, - "points": [ - { - "timestamp": 1568172664000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172694000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172724000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172754000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172784000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172814000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172844000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172874000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172904000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172934000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172964000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172994000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173024000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173054000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173084000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173114000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173144000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173174000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173204000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173234000, - "up": 1, - "down": 0 - } - ] - }, - "state": { - "agent": null, - "checks": [ - { - "agent": { - "id": "04e1d082-65bc-4929-8d65-d0768a2621c4" - }, - "container": null, - "kubernetes": null, - "monitor": { - "ip": "127.0.0.1", - "name": "", - "status": "up" - }, - "observer": { - "geo": { - "name": "mpls", - "location": { - "lat": 37.926867976784706, - "lon": -78.02490200847387 - } - } - }, - "timestamp": "1568173234373" - } - ], - "geo": null, - "observer": { - "geo": { - "name": [ - "mpls" - ], - "location": null - } - }, - "monitor": { - "id": null, - "name": null, - "status": "up", - "type": null - }, - "summary": { - "up": 1, - "down": 0, - "geo": null - }, - "url": { - "full": "http://localhost:5678/pattern?r=200x1", - "domain": "localhost" - }, - "timestamp": 1568173234373 - } - } - ] - } -} \ No newline at end of file diff --git a/x-pack/test/api_integration/apis/uptime/graphql/fixtures/monitor_states_page_2.json b/x-pack/test/api_integration/apis/uptime/graphql/fixtures/monitor_states_page_2.json deleted file mode 100644 index 26b4b1a195567..0000000000000 --- a/x-pack/test/api_integration/apis/uptime/graphql/fixtures/monitor_states_page_2.json +++ /dev/null @@ -1,1609 +0,0 @@ -{ - "monitorStates": { - "prevPagePagination": "{\"cursorKey\":{\"monitor_id\":\"0010-down\"},\"sortOrder\":\"ASC\",\"cursorDirection\":\"BEFORE\"}", - "nextPagePagination": "{\"cursorDirection\":\"AFTER\",\"sortOrder\":\"ASC\",\"cursorKey\":{\"monitor_id\":\"0019-up\"}}", - "totalSummaryCount": 2000, - "summaries": [ - { - "monitor_id": "0010-down", - "histogram": { - "count": 20, - "points": [ - { - "timestamp": 1568172664000, - "up": 0, - "down": 1 - }, - { - "timestamp": 1568172694000, - "up": 0, - "down": 1 - }, - { - "timestamp": 1568172724000, - "up": 0, - "down": 1 - }, - { - "timestamp": 1568172754000, - "up": 0, - "down": 1 - }, - { - "timestamp": 1568172784000, - "up": 0, - "down": 1 - }, - { - "timestamp": 1568172814000, - "up": 0, - "down": 1 - }, - { - "timestamp": 1568172844000, - "up": 0, - "down": 1 - }, - { - "timestamp": 1568172874000, - "up": 0, - "down": 1 - }, - { - "timestamp": 1568172904000, - "up": 0, - "down": 1 - }, - { - "timestamp": 1568172934000, - "up": 0, - "down": 1 - }, - { - "timestamp": 1568172964000, - "up": 0, - "down": 1 - }, - { - "timestamp": 1568172994000, - "up": 0, - "down": 1 - }, - { - "timestamp": 1568173024000, - "up": 0, - "down": 1 - }, - { - "timestamp": 1568173054000, - "up": 0, - "down": 1 - }, - { - "timestamp": 1568173084000, - "up": 0, - "down": 1 - }, - { - "timestamp": 1568173114000, - "up": 0, - "down": 1 - }, - { - "timestamp": 1568173144000, - "up": 0, - "down": 1 - }, - { - "timestamp": 1568173174000, - "up": 0, - "down": 1 - }, - { - "timestamp": 1568173204000, - "up": 0, - "down": 1 - }, - { - "timestamp": 1568173234000, - "up": 0, - "down": 1 - } - ] - }, - "state": { - "agent": null, - "checks": [ - { - "agent": { - "id": "04e1d082-65bc-4929-8d65-d0768a2621c4" - }, - "container": null, - "kubernetes": null, - "monitor": { - "ip": "127.0.0.1", - "name": "", - "status": "down" - }, - "observer": { - "geo": { - "name": "mpls", - "location": { - "lat": 37.926867976784706, - "lon": -78.02490200847387 - } - } - }, - "timestamp": "1568173234371" - } - ], - "geo": null, - "observer": { - "geo": { - "name": [ - "mpls" - ], - "location": null - } - }, - "monitor": { - "id": null, - "name": null, - "status": "down", - "type": null - }, - "summary": { - "up": 0, - "down": 1, - "geo": null - }, - "url": { - "full": "http://localhost:5678/pattern?r=400x1", - "domain": "localhost" - }, - "timestamp": 1568173234371 - } - }, - { - "monitor_id": "0011-up", - "histogram": { - "count": 20, - "points": [ - { - "timestamp": 1568172664000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172694000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172724000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172754000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172784000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172814000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172844000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172874000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172904000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172934000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172964000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172994000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173024000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173054000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173084000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173114000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173144000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173174000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173204000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173234000, - "up": 1, - "down": 0 - } - ] - }, - "state": { - "agent": null, - "checks": [ - { - "agent": { - "id": "04e1d082-65bc-4929-8d65-d0768a2621c4" - }, - "container": null, - "kubernetes": null, - "monitor": { - "ip": "127.0.0.1", - "name": "", - "status": "up" - }, - "observer": { - "geo": { - "name": "mpls", - "location": { - "lat": 37.926867976784706, - "lon": -78.02490200847387 - } - } - }, - "timestamp": "1568173234371" - } - ], - "geo": null, - "observer": { - "geo": { - "name": [ - "mpls" - ], - "location": null - } - }, - "monitor": { - "id": null, - "name": null, - "status": "up", - "type": null - }, - "summary": { - "up": 1, - "down": 0, - "geo": null - }, - "url": { - "full": "http://localhost:5678/pattern?r=200x1", - "domain": "localhost" - }, - "timestamp": 1568173234371 - } - }, - { - "monitor_id": "0012-up", - "histogram": { - "count": 20, - "points": [ - { - "timestamp": 1568172664000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172694000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172724000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172754000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172784000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172814000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172844000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172874000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172904000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172934000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172964000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172994000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173024000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173054000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173084000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173114000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173144000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173174000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173204000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173234000, - "up": 1, - "down": 0 - } - ] - }, - "state": { - "agent": null, - "checks": [ - { - "agent": { - "id": "04e1d082-65bc-4929-8d65-d0768a2621c4" - }, - "container": null, - "kubernetes": null, - "monitor": { - "ip": "127.0.0.1", - "name": "", - "status": "up" - }, - "observer": { - "geo": { - "name": "mpls", - "location": { - "lat": 37.926867976784706, - "lon": -78.02490200847387 - } - } - }, - "timestamp": "1568173234371" - } - ], - "geo": null, - "observer": { - "geo": { - "name": [ - "mpls" - ], - "location": null - } - }, - "monitor": { - "id": null, - "name": null, - "status": "up", - "type": null - }, - "summary": { - "up": 1, - "down": 0, - "geo": null - }, - "url": { - "full": "http://localhost:5678/pattern?r=200x1", - "domain": "localhost" - }, - "timestamp": 1568173234371 - } - }, - { - "monitor_id": "0013-up", - "histogram": { - "count": 20, - "points": [ - { - "timestamp": 1568172664000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172694000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172724000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172754000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172784000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172814000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172844000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172874000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172904000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172934000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172964000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172994000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173024000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173054000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173084000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173114000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173144000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173174000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173204000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173234000, - "up": 1, - "down": 0 - } - ] - }, - "state": { - "agent": null, - "checks": [ - { - "agent": { - "id": "04e1d082-65bc-4929-8d65-d0768a2621c4" - }, - "container": null, - "kubernetes": null, - "monitor": { - "ip": "127.0.0.1", - "name": "", - "status": "up" - }, - "observer": { - "geo": { - "name": "mpls", - "location": { - "lat": 37.926867976784706, - "lon": -78.02490200847387 - } - } - }, - "timestamp": "1568173234372" - } - ], - "geo": null, - "observer": { - "geo": { - "name": [ - "mpls" - ], - "location": null - } - }, - "monitor": { - "id": null, - "name": null, - "status": "up", - "type": null - }, - "summary": { - "up": 1, - "down": 0, - "geo": null - }, - "url": { - "full": "http://localhost:5678/pattern?r=200x1", - "domain": "localhost" - }, - "timestamp": 1568173234372 - } - }, - { - "monitor_id": "0014-up", - "histogram": { - "count": 20, - "points": [ - { - "timestamp": 1568172664000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172694000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172724000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172754000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172784000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172814000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172844000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172874000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172904000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172934000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172964000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172994000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173024000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173054000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173084000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173114000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173144000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173174000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173204000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173234000, - "up": 1, - "down": 0 - } - ] - }, - "state": { - "agent": null, - "checks": [ - { - "agent": { - "id": "04e1d082-65bc-4929-8d65-d0768a2621c4" - }, - "container": null, - "kubernetes": null, - "monitor": { - "ip": "127.0.0.1", - "name": "", - "status": "up" - }, - "observer": { - "geo": { - "name": "mpls", - "location": { - "lat": 37.926867976784706, - "lon": -78.02490200847387 - } - } - }, - "timestamp": "1568173234372" - } - ], - "geo": null, - "observer": { - "geo": { - "name": [ - "mpls" - ], - "location": null - } - }, - "monitor": { - "id": null, - "name": null, - "status": "up", - "type": null - }, - "summary": { - "up": 1, - "down": 0, - "geo": null - }, - "url": { - "full": "http://localhost:5678/pattern?r=200x1", - "domain": "localhost" - }, - "timestamp": 1568173234372 - } - }, - { - "monitor_id": "0015-intermittent", - "histogram": { - "count": 20, - "points": [ - { - "timestamp": 1568172664000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172694000, - "up": 0, - "down": 1 - }, - { - "timestamp": 1568172724000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172754000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172784000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172814000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172844000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172874000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172904000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172934000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172964000, - "up": 0, - "down": 1 - }, - { - "timestamp": 1568172994000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173024000, - "up": 0, - "down": 1 - }, - { - "timestamp": 1568173054000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173084000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173114000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173144000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173174000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173204000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173234000, - "up": 1, - "down": 0 - } - ] - }, - "state": { - "agent": null, - "checks": [ - { - "agent": { - "id": "04e1d082-65bc-4929-8d65-d0768a2621c4" - }, - "container": null, - "kubernetes": null, - "monitor": { - "ip": "127.0.0.1", - "name": "", - "status": "up" - }, - "observer": { - "geo": { - "name": "mpls", - "location": { - "lat": 37.926867976784706, - "lon": -78.02490200847387 - } - } - }, - "timestamp": "1568173234371" - } - ], - "geo": null, - "observer": { - "geo": { - "name": [ - "mpls" - ], - "location": null - } - }, - "monitor": { - "id": null, - "name": null, - "status": "up", - "type": null - }, - "summary": { - "up": 1, - "down": 0, - "geo": null - }, - "url": { - "full": "http://localhost:5678/pattern?r=200x5,500x1", - "domain": "localhost" - }, - "timestamp": 1568173234371 - } - }, - { - "monitor_id": "0016-up", - "histogram": { - "count": 20, - "points": [ - { - "timestamp": 1568172664000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172694000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172724000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172754000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172784000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172814000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172844000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172874000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172904000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172934000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172964000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172994000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173024000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173054000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173084000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173114000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173144000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173174000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173204000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173234000, - "up": 1, - "down": 0 - } - ] - }, - "state": { - "agent": null, - "checks": [ - { - "agent": { - "id": "04e1d082-65bc-4929-8d65-d0768a2621c4" - }, - "container": null, - "kubernetes": null, - "monitor": { - "ip": "127.0.0.1", - "name": "", - "status": "up" - }, - "observer": { - "geo": { - "name": "mpls", - "location": { - "lat": 37.926867976784706, - "lon": -78.02490200847387 - } - } - }, - "timestamp": "1568173234371" - } - ], - "geo": null, - "observer": { - "geo": { - "name": [ - "mpls" - ], - "location": null - } - }, - "monitor": { - "id": null, - "name": null, - "status": "up", - "type": null - }, - "summary": { - "up": 1, - "down": 0, - "geo": null - }, - "url": { - "full": "http://localhost:5678/pattern?r=200x1", - "domain": "localhost" - }, - "timestamp": 1568173234371 - } - }, - { - "monitor_id": "0017-up", - "histogram": { - "count": 20, - "points": [ - { - "timestamp": 1568172664000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172694000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172724000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172754000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172784000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172814000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172844000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172874000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172904000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172934000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172964000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172994000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173024000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173054000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173084000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173114000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173144000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173174000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173204000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173234000, - "up": 1, - "down": 0 - } - ] - }, - "state": { - "agent": null, - "checks": [ - { - "agent": { - "id": "04e1d082-65bc-4929-8d65-d0768a2621c4" - }, - "container": null, - "kubernetes": null, - "monitor": { - "ip": "127.0.0.1", - "name": "", - "status": "up" - }, - "observer": { - "geo": { - "name": "mpls", - "location": { - "lat": 37.926867976784706, - "lon": -78.02490200847387 - } - } - }, - "timestamp": "1568173234371" - } - ], - "geo": null, - "observer": { - "geo": { - "name": [ - "mpls" - ], - "location": null - } - }, - "monitor": { - "id": null, - "name": null, - "status": "up", - "type": null - }, - "summary": { - "up": 1, - "down": 0, - "geo": null - }, - "url": { - "full": "http://localhost:5678/pattern?r=200x1", - "domain": "localhost" - }, - "timestamp": 1568173234371 - } - }, - { - "monitor_id": "0018-up", - "histogram": { - "count": 20, - "points": [ - { - "timestamp": 1568172664000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172694000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172724000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172754000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172784000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172814000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172844000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172874000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172904000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172934000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172964000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172994000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173024000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173054000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173084000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173114000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173144000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173174000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173204000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173234000, - "up": 1, - "down": 0 - } - ] - }, - "state": { - "agent": null, - "checks": [ - { - "agent": { - "id": "04e1d082-65bc-4929-8d65-d0768a2621c4" - }, - "container": null, - "kubernetes": null, - "monitor": { - "ip": "127.0.0.1", - "name": "", - "status": "up" - }, - "observer": { - "geo": { - "name": "mpls", - "location": { - "lat": 37.926867976784706, - "lon": -78.02490200847387 - } - } - }, - "timestamp": "1568173234371" - } - ], - "geo": null, - "observer": { - "geo": { - "name": [ - "mpls" - ], - "location": null - } - }, - "monitor": { - "id": null, - "name": null, - "status": "up", - "type": null - }, - "summary": { - "up": 1, - "down": 0, - "geo": null - }, - "url": { - "full": "http://localhost:5678/pattern?r=200x1", - "domain": "localhost" - }, - "timestamp": 1568173234371 - } - }, - { - "monitor_id": "0019-up", - "histogram": { - "count": 20, - "points": [ - { - "timestamp": 1568172664000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172694000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172724000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172754000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172784000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172814000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172844000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172874000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172904000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172934000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172964000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172994000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173024000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173054000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173084000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173114000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173144000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173174000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173204000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173234000, - "up": 1, - "down": 0 - } - ] - }, - "state": { - "agent": null, - "checks": [ - { - "agent": { - "id": "04e1d082-65bc-4929-8d65-d0768a2621c4" - }, - "container": null, - "kubernetes": null, - "monitor": { - "ip": "127.0.0.1", - "name": "", - "status": "up" - }, - "observer": { - "geo": { - "name": "mpls", - "location": { - "lat": 37.926867976784706, - "lon": -78.02490200847387 - } - } - }, - "timestamp": "1568173234371" - } - ], - "geo": null, - "observer": { - "geo": { - "name": [ - "mpls" - ], - "location": null - } - }, - "monitor": { - "id": null, - "name": null, - "status": "up", - "type": null - }, - "summary": { - "up": 1, - "down": 0, - "geo": null - }, - "url": { - "full": "http://localhost:5678/pattern?r=200x1", - "domain": "localhost" - }, - "timestamp": 1568173234371 - } - } - ] - } -} \ No newline at end of file diff --git a/x-pack/test/api_integration/apis/uptime/graphql/fixtures/monitor_states_page_2_previous.json b/x-pack/test/api_integration/apis/uptime/graphql/fixtures/monitor_states_page_2_previous.json deleted file mode 100644 index 0b93e66f50246..0000000000000 --- a/x-pack/test/api_integration/apis/uptime/graphql/fixtures/monitor_states_page_2_previous.json +++ /dev/null @@ -1,1609 +0,0 @@ -{ - "monitorStates": { - "prevPagePagination": null, - "nextPagePagination": "{\"cursorKey\":{\"monitor_id\":\"0009-up\"},\"sortOrder\":\"ASC\",\"cursorDirection\":\"AFTER\"}", - "totalSummaryCount": 2000, - "summaries": [ - { - "monitor_id": "0000-intermittent", - "histogram": { - "count": 20, - "points": [ - { - "timestamp": 1568172664000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172694000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172724000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172754000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172784000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172814000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172844000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172874000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172904000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172934000, - "up": 0, - "down": 1 - }, - { - "timestamp": 1568172964000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172994000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173024000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173054000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173084000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173114000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173144000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173174000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173204000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173234000, - "up": 1, - "down": 0 - } - ] - }, - "state": { - "agent": null, - "checks": [ - { - "agent": { - "id": "04e1d082-65bc-4929-8d65-d0768a2621c4" - }, - "container": null, - "kubernetes": null, - "monitor": { - "ip": "127.0.0.1", - "name": "", - "status": "up" - }, - "observer": { - "geo": { - "name": "mpls", - "location": { - "lat": 37.926867976784706, - "lon": -78.02490200847387 - } - } - }, - "timestamp": "1568173234371" - } - ], - "geo": null, - "observer": { - "geo": { - "name": [ - "mpls" - ], - "location": null - } - }, - "monitor": { - "id": null, - "name": null, - "status": "up", - "type": null - }, - "summary": { - "up": 1, - "down": 0, - "geo": null - }, - "url": { - "full": "http://localhost:5678/pattern?r=200x5,500x1", - "domain": "localhost" - }, - "timestamp": 1568173234371 - } - }, - { - "monitor_id": "0001-up", - "histogram": { - "count": 20, - "points": [ - { - "timestamp": 1568172664000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172694000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172724000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172754000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172784000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172814000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172844000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172874000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172904000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172934000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172964000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172994000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173024000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173054000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173084000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173114000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173144000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173174000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173204000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173234000, - "up": 1, - "down": 0 - } - ] - }, - "state": { - "agent": null, - "checks": [ - { - "agent": { - "id": "04e1d082-65bc-4929-8d65-d0768a2621c4" - }, - "container": null, - "kubernetes": null, - "monitor": { - "ip": "127.0.0.1", - "name": "", - "status": "up" - }, - "observer": { - "geo": { - "name": "mpls", - "location": { - "lat": 37.926867976784706, - "lon": -78.02490200847387 - } - } - }, - "timestamp": "1568173234371" - } - ], - "geo": null, - "observer": { - "geo": { - "name": [ - "mpls" - ], - "location": null - } - }, - "monitor": { - "id": null, - "name": null, - "status": "up", - "type": null - }, - "summary": { - "up": 1, - "down": 0, - "geo": null - }, - "url": { - "full": "http://localhost:5678/pattern?r=200x1", - "domain": "localhost" - }, - "timestamp": 1568173234371 - } - }, - { - "monitor_id": "0002-up", - "histogram": { - "count": 20, - "points": [ - { - "timestamp": 1568172664000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172694000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172724000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172754000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172784000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172814000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172844000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172874000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172904000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172934000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172964000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172994000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173024000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173054000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173084000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173114000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173144000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173174000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173204000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173234000, - "up": 1, - "down": 0 - } - ] - }, - "state": { - "agent": null, - "checks": [ - { - "agent": { - "id": "04e1d082-65bc-4929-8d65-d0768a2621c4" - }, - "container": null, - "kubernetes": null, - "monitor": { - "ip": "127.0.0.1", - "name": "", - "status": "up" - }, - "observer": { - "geo": { - "name": "mpls", - "location": { - "lat": 37.926867976784706, - "lon": -78.02490200847387 - } - } - }, - "timestamp": "1568173234371" - } - ], - "geo": null, - "observer": { - "geo": { - "name": [ - "mpls" - ], - "location": null - } - }, - "monitor": { - "id": null, - "name": null, - "status": "up", - "type": null - }, - "summary": { - "up": 1, - "down": 0, - "geo": null - }, - "url": { - "full": "http://localhost:5678/pattern?r=200x1", - "domain": "localhost" - }, - "timestamp": 1568173234371 - } - }, - { - "monitor_id": "0003-up", - "histogram": { - "count": 20, - "points": [ - { - "timestamp": 1568172664000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172694000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172724000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172754000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172784000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172814000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172844000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172874000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172904000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172934000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172964000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172994000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173024000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173054000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173084000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173114000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173144000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173174000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173204000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173234000, - "up": 1, - "down": 0 - } - ] - }, - "state": { - "agent": null, - "checks": [ - { - "agent": { - "id": "04e1d082-65bc-4929-8d65-d0768a2621c4" - }, - "container": null, - "kubernetes": null, - "monitor": { - "ip": "127.0.0.1", - "name": "", - "status": "up" - }, - "observer": { - "geo": { - "name": "mpls", - "location": { - "lat": 37.926867976784706, - "lon": -78.02490200847387 - } - } - }, - "timestamp": "1568173234371" - } - ], - "geo": null, - "observer": { - "geo": { - "name": [ - "mpls" - ], - "location": null - } - }, - "monitor": { - "id": null, - "name": null, - "status": "up", - "type": null - }, - "summary": { - "up": 1, - "down": 0, - "geo": null - }, - "url": { - "full": "http://localhost:5678/pattern?r=200x1", - "domain": "localhost" - }, - "timestamp": 1568173234371 - } - }, - { - "monitor_id": "0004-up", - "histogram": { - "count": 20, - "points": [ - { - "timestamp": 1568172664000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172694000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172724000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172754000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172784000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172814000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172844000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172874000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172904000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172934000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172964000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172994000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173024000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173054000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173084000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173114000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173144000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173174000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173204000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173234000, - "up": 1, - "down": 0 - } - ] - }, - "state": { - "agent": null, - "checks": [ - { - "agent": { - "id": "04e1d082-65bc-4929-8d65-d0768a2621c4" - }, - "container": null, - "kubernetes": null, - "monitor": { - "ip": "127.0.0.1", - "name": "", - "status": "up" - }, - "observer": { - "geo": { - "name": "mpls", - "location": { - "lat": 37.926867976784706, - "lon": -78.02490200847387 - } - } - }, - "timestamp": "1568173234371" - } - ], - "geo": null, - "observer": { - "geo": { - "name": [ - "mpls" - ], - "location": null - } - }, - "monitor": { - "id": null, - "name": null, - "status": "up", - "type": null - }, - "summary": { - "up": 1, - "down": 0, - "geo": null - }, - "url": { - "full": "http://localhost:5678/pattern?r=200x1", - "domain": "localhost" - }, - "timestamp": 1568173234371 - } - }, - { - "monitor_id": "0005-up", - "histogram": { - "count": 20, - "points": [ - { - "timestamp": 1568172664000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172694000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172724000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172754000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172784000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172814000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172844000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172874000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172904000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172934000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172964000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172994000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173024000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173054000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173084000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173114000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173144000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173174000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173204000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173234000, - "up": 1, - "down": 0 - } - ] - }, - "state": { - "agent": null, - "checks": [ - { - "agent": { - "id": "04e1d082-65bc-4929-8d65-d0768a2621c4" - }, - "container": null, - "kubernetes": null, - "monitor": { - "ip": "127.0.0.1", - "name": "", - "status": "up" - }, - "observer": { - "geo": { - "name": "mpls", - "location": { - "lat": 37.926867976784706, - "lon": -78.02490200847387 - } - } - }, - "timestamp": "1568173234371" - } - ], - "geo": null, - "observer": { - "geo": { - "name": [ - "mpls" - ], - "location": null - } - }, - "monitor": { - "id": null, - "name": null, - "status": "up", - "type": null - }, - "summary": { - "up": 1, - "down": 0, - "geo": null - }, - "url": { - "full": "http://localhost:5678/pattern?r=200x1", - "domain": "localhost" - }, - "timestamp": 1568173234371 - } - }, - { - "monitor_id": "0006-up", - "histogram": { - "count": 20, - "points": [ - { - "timestamp": 1568172664000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172694000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172724000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172754000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172784000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172814000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172844000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172874000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172904000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172934000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172964000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172994000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173024000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173054000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173084000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173114000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173144000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173174000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173204000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173234000, - "up": 1, - "down": 0 - } - ] - }, - "state": { - "agent": null, - "checks": [ - { - "agent": { - "id": "04e1d082-65bc-4929-8d65-d0768a2621c4" - }, - "container": null, - "kubernetes": null, - "monitor": { - "ip": "127.0.0.1", - "name": "", - "status": "up" - }, - "observer": { - "geo": { - "name": "mpls", - "location": { - "lat": 37.926867976784706, - "lon": -78.02490200847387 - } - } - }, - "timestamp": "1568173234371" - } - ], - "geo": null, - "observer": { - "geo": { - "name": [ - "mpls" - ], - "location": null - } - }, - "monitor": { - "id": null, - "name": null, - "status": "up", - "type": null - }, - "summary": { - "up": 1, - "down": 0, - "geo": null - }, - "url": { - "full": "http://localhost:5678/pattern?r=200x1", - "domain": "localhost" - }, - "timestamp": 1568173234371 - } - }, - { - "monitor_id": "0007-up", - "histogram": { - "count": 20, - "points": [ - { - "timestamp": 1568172664000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172694000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172724000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172754000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172784000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172814000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172844000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172874000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172904000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172934000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172964000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172994000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173024000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173054000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173084000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173114000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173144000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173174000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173204000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173234000, - "up": 1, - "down": 0 - } - ] - }, - "state": { - "agent": null, - "checks": [ - { - "agent": { - "id": "04e1d082-65bc-4929-8d65-d0768a2621c4" - }, - "container": null, - "kubernetes": null, - "monitor": { - "ip": "127.0.0.1", - "name": "", - "status": "up" - }, - "observer": { - "geo": { - "name": "mpls", - "location": { - "lat": 37.926867976784706, - "lon": -78.02490200847387 - } - } - }, - "timestamp": "1568173234371" - } - ], - "geo": null, - "observer": { - "geo": { - "name": [ - "mpls" - ], - "location": null - } - }, - "monitor": { - "id": null, - "name": null, - "status": "up", - "type": null - }, - "summary": { - "up": 1, - "down": 0, - "geo": null - }, - "url": { - "full": "http://localhost:5678/pattern?r=200x1", - "domain": "localhost" - }, - "timestamp": 1568173234371 - } - }, - { - "monitor_id": "0008-up", - "histogram": { - "count": 20, - "points": [ - { - "timestamp": 1568172664000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172694000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172724000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172754000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172784000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172814000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172844000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172874000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172904000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172934000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172964000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172994000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173024000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173054000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173084000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173114000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173144000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173174000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173204000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173234000, - "up": 1, - "down": 0 - } - ] - }, - "state": { - "agent": null, - "checks": [ - { - "agent": { - "id": "04e1d082-65bc-4929-8d65-d0768a2621c4" - }, - "container": null, - "kubernetes": null, - "monitor": { - "ip": "127.0.0.1", - "name": "", - "status": "up" - }, - "observer": { - "geo": { - "name": "mpls", - "location": { - "lat": 37.926867976784706, - "lon": -78.02490200847387 - } - } - }, - "timestamp": "1568173234371" - } - ], - "geo": null, - "observer": { - "geo": { - "name": [ - "mpls" - ], - "location": null - } - }, - "monitor": { - "id": null, - "name": null, - "status": "up", - "type": null - }, - "summary": { - "up": 1, - "down": 0, - "geo": null - }, - "url": { - "full": "http://localhost:5678/pattern?r=200x1", - "domain": "localhost" - }, - "timestamp": 1568173234371 - } - }, - { - "monitor_id": "0009-up", - "histogram": { - "count": 20, - "points": [ - { - "timestamp": 1568172664000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172694000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172724000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172754000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172784000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172814000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172844000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172874000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172904000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172934000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172964000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172994000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173024000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173054000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173084000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173114000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173144000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173174000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173204000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173234000, - "up": 1, - "down": 0 - } - ] - }, - "state": { - "agent": null, - "checks": [ - { - "agent": { - "id": "04e1d082-65bc-4929-8d65-d0768a2621c4" - }, - "container": null, - "kubernetes": null, - "monitor": { - "ip": "127.0.0.1", - "name": "", - "status": "up" - }, - "observer": { - "geo": { - "name": "mpls", - "location": { - "lat": 37.926867976784706, - "lon": -78.02490200847387 - } - } - }, - "timestamp": "1568173234371" - } - ], - "geo": null, - "observer": { - "geo": { - "name": [ - "mpls" - ], - "location": null - } - }, - "monitor": { - "id": null, - "name": null, - "status": "up", - "type": null - }, - "summary": { - "up": 1, - "down": 0, - "geo": null - }, - "url": { - "full": "http://localhost:5678/pattern?r=200x1", - "domain": "localhost" - }, - "timestamp": 1568173234371 - } - } - ] - } -} \ No newline at end of file diff --git a/x-pack/test/api_integration/apis/uptime/graphql/fixtures/monitor_states_page_3.json b/x-pack/test/api_integration/apis/uptime/graphql/fixtures/monitor_states_page_3.json deleted file mode 100644 index 7b47742f8859a..0000000000000 --- a/x-pack/test/api_integration/apis/uptime/graphql/fixtures/monitor_states_page_3.json +++ /dev/null @@ -1,1609 +0,0 @@ -{ - "monitorStates": { - "prevPagePagination": "{\"cursorKey\":{\"monitor_id\":\"0020-down\"},\"sortOrder\":\"ASC\",\"cursorDirection\":\"BEFORE\"}", - "nextPagePagination": "{\"cursorDirection\":\"AFTER\",\"sortOrder\":\"ASC\",\"cursorKey\":{\"monitor_id\":\"0029-up\"}}", - "totalSummaryCount": 2000, - "summaries": [ - { - "monitor_id": "0020-down", - "histogram": { - "count": 20, - "points": [ - { - "timestamp": 1568172664000, - "up": 0, - "down": 1 - }, - { - "timestamp": 1568172694000, - "up": 0, - "down": 1 - }, - { - "timestamp": 1568172724000, - "up": 0, - "down": 1 - }, - { - "timestamp": 1568172754000, - "up": 0, - "down": 1 - }, - { - "timestamp": 1568172784000, - "up": 0, - "down": 1 - }, - { - "timestamp": 1568172814000, - "up": 0, - "down": 1 - }, - { - "timestamp": 1568172844000, - "up": 0, - "down": 1 - }, - { - "timestamp": 1568172874000, - "up": 0, - "down": 1 - }, - { - "timestamp": 1568172904000, - "up": 0, - "down": 1 - }, - { - "timestamp": 1568172934000, - "up": 0, - "down": 1 - }, - { - "timestamp": 1568172964000, - "up": 0, - "down": 1 - }, - { - "timestamp": 1568172994000, - "up": 0, - "down": 1 - }, - { - "timestamp": 1568173024000, - "up": 0, - "down": 1 - }, - { - "timestamp": 1568173054000, - "up": 0, - "down": 1 - }, - { - "timestamp": 1568173084000, - "up": 0, - "down": 1 - }, - { - "timestamp": 1568173114000, - "up": 0, - "down": 1 - }, - { - "timestamp": 1568173144000, - "up": 0, - "down": 1 - }, - { - "timestamp": 1568173174000, - "up": 0, - "down": 1 - }, - { - "timestamp": 1568173204000, - "up": 0, - "down": 1 - }, - { - "timestamp": 1568173234000, - "up": 0, - "down": 1 - } - ] - }, - "state": { - "agent": null, - "checks": [ - { - "agent": { - "id": "04e1d082-65bc-4929-8d65-d0768a2621c4" - }, - "container": null, - "kubernetes": null, - "monitor": { - "ip": "127.0.0.1", - "name": "", - "status": "down" - }, - "observer": { - "geo": { - "name": "mpls", - "location": { - "lat": 37.926867976784706, - "lon": -78.02490200847387 - } - } - }, - "timestamp": "1568173234372" - } - ], - "geo": null, - "observer": { - "geo": { - "name": [ - "mpls" - ], - "location": null - } - }, - "monitor": { - "id": null, - "name": null, - "status": "down", - "type": null - }, - "summary": { - "up": 0, - "down": 1, - "geo": null - }, - "url": { - "full": "http://localhost:5678/pattern?r=400x1", - "domain": "localhost" - }, - "timestamp": 1568173234372 - } - }, - { - "monitor_id": "0021-up", - "histogram": { - "count": 20, - "points": [ - { - "timestamp": 1568172664000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172694000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172724000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172754000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172784000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172814000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172844000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172874000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172904000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172934000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172964000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172994000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173024000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173054000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173084000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173114000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173144000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173174000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173204000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173234000, - "up": 1, - "down": 0 - } - ] - }, - "state": { - "agent": null, - "checks": [ - { - "agent": { - "id": "04e1d082-65bc-4929-8d65-d0768a2621c4" - }, - "container": null, - "kubernetes": null, - "monitor": { - "ip": "127.0.0.1", - "name": "", - "status": "up" - }, - "observer": { - "geo": { - "name": "mpls", - "location": { - "lat": 37.926867976784706, - "lon": -78.02490200847387 - } - } - }, - "timestamp": "1568173234372" - } - ], - "geo": null, - "observer": { - "geo": { - "name": [ - "mpls" - ], - "location": null - } - }, - "monitor": { - "id": null, - "name": null, - "status": "up", - "type": null - }, - "summary": { - "up": 1, - "down": 0, - "geo": null - }, - "url": { - "full": "http://localhost:5678/pattern?r=200x1", - "domain": "localhost" - }, - "timestamp": 1568173234372 - } - }, - { - "monitor_id": "0022-up", - "histogram": { - "count": 20, - "points": [ - { - "timestamp": 1568172664000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172694000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172724000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172754000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172784000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172814000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172844000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172874000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172904000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172934000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172964000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172994000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173024000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173054000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173084000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173114000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173144000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173174000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173204000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173234000, - "up": 1, - "down": 0 - } - ] - }, - "state": { - "agent": null, - "checks": [ - { - "agent": { - "id": "04e1d082-65bc-4929-8d65-d0768a2621c4" - }, - "container": null, - "kubernetes": null, - "monitor": { - "ip": "127.0.0.1", - "name": "", - "status": "up" - }, - "observer": { - "geo": { - "name": "mpls", - "location": { - "lat": 37.926867976784706, - "lon": -78.02490200847387 - } - } - }, - "timestamp": "1568173234372" - } - ], - "geo": null, - "observer": { - "geo": { - "name": [ - "mpls" - ], - "location": null - } - }, - "monitor": { - "id": null, - "name": null, - "status": "up", - "type": null - }, - "summary": { - "up": 1, - "down": 0, - "geo": null - }, - "url": { - "full": "http://localhost:5678/pattern?r=200x1", - "domain": "localhost" - }, - "timestamp": 1568173234372 - } - }, - { - "monitor_id": "0023-up", - "histogram": { - "count": 20, - "points": [ - { - "timestamp": 1568172664000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172694000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172724000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172754000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172784000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172814000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172844000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172874000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172904000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172934000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172964000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172994000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173024000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173054000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173084000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173114000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173144000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173174000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173204000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173234000, - "up": 1, - "down": 0 - } - ] - }, - "state": { - "agent": null, - "checks": [ - { - "agent": { - "id": "04e1d082-65bc-4929-8d65-d0768a2621c4" - }, - "container": null, - "kubernetes": null, - "monitor": { - "ip": "127.0.0.1", - "name": "", - "status": "up" - }, - "observer": { - "geo": { - "name": "mpls", - "location": { - "lat": 37.926867976784706, - "lon": -78.02490200847387 - } - } - }, - "timestamp": "1568173234372" - } - ], - "geo": null, - "observer": { - "geo": { - "name": [ - "mpls" - ], - "location": null - } - }, - "monitor": { - "id": null, - "name": null, - "status": "up", - "type": null - }, - "summary": { - "up": 1, - "down": 0, - "geo": null - }, - "url": { - "full": "http://localhost:5678/pattern?r=200x1", - "domain": "localhost" - }, - "timestamp": 1568173234372 - } - }, - { - "monitor_id": "0024-up", - "histogram": { - "count": 20, - "points": [ - { - "timestamp": 1568172664000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172694000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172724000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172754000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172784000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172814000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172844000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172874000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172904000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172934000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172964000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172994000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173024000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173054000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173084000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173114000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173144000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173174000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173204000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173234000, - "up": 1, - "down": 0 - } - ] - }, - "state": { - "agent": null, - "checks": [ - { - "agent": { - "id": "04e1d082-65bc-4929-8d65-d0768a2621c4" - }, - "container": null, - "kubernetes": null, - "monitor": { - "ip": "127.0.0.1", - "name": "", - "status": "up" - }, - "observer": { - "geo": { - "name": "mpls", - "location": { - "lat": 37.926867976784706, - "lon": -78.02490200847387 - } - } - }, - "timestamp": "1568173234372" - } - ], - "geo": null, - "observer": { - "geo": { - "name": [ - "mpls" - ], - "location": null - } - }, - "monitor": { - "id": null, - "name": null, - "status": "up", - "type": null - }, - "summary": { - "up": 1, - "down": 0, - "geo": null - }, - "url": { - "full": "http://localhost:5678/pattern?r=200x1", - "domain": "localhost" - }, - "timestamp": 1568173234372 - } - }, - { - "monitor_id": "0025-up", - "histogram": { - "count": 20, - "points": [ - { - "timestamp": 1568172664000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172694000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172724000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172754000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172784000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172814000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172844000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172874000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172904000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172934000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172964000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172994000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173024000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173054000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173084000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173114000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173144000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173174000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173204000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173234000, - "up": 1, - "down": 0 - } - ] - }, - "state": { - "agent": null, - "checks": [ - { - "agent": { - "id": "04e1d082-65bc-4929-8d65-d0768a2621c4" - }, - "container": null, - "kubernetes": null, - "monitor": { - "ip": "127.0.0.1", - "name": "", - "status": "up" - }, - "observer": { - "geo": { - "name": "mpls", - "location": { - "lat": 37.926867976784706, - "lon": -78.02490200847387 - } - } - }, - "timestamp": "1568173234373" - } - ], - "geo": null, - "observer": { - "geo": { - "name": [ - "mpls" - ], - "location": null - } - }, - "monitor": { - "id": null, - "name": null, - "status": "up", - "type": null - }, - "summary": { - "up": 1, - "down": 0, - "geo": null - }, - "url": { - "full": "http://localhost:5678/pattern?r=200x1", - "domain": "localhost" - }, - "timestamp": 1568173234373 - } - }, - { - "monitor_id": "0026-up", - "histogram": { - "count": 20, - "points": [ - { - "timestamp": 1568172664000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172694000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172724000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172754000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172784000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172814000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172844000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172874000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172904000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172934000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172964000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172994000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173024000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173054000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173084000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173114000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173144000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173174000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173204000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173234000, - "up": 1, - "down": 0 - } - ] - }, - "state": { - "agent": null, - "checks": [ - { - "agent": { - "id": "04e1d082-65bc-4929-8d65-d0768a2621c4" - }, - "container": null, - "kubernetes": null, - "monitor": { - "ip": "127.0.0.1", - "name": "", - "status": "up" - }, - "observer": { - "geo": { - "name": "mpls", - "location": { - "lat": 37.926867976784706, - "lon": -78.02490200847387 - } - } - }, - "timestamp": "1568173234373" - } - ], - "geo": null, - "observer": { - "geo": { - "name": [ - "mpls" - ], - "location": null - } - }, - "monitor": { - "id": null, - "name": null, - "status": "up", - "type": null - }, - "summary": { - "up": 1, - "down": 0, - "geo": null - }, - "url": { - "full": "http://localhost:5678/pattern?r=200x1", - "domain": "localhost" - }, - "timestamp": 1568173234373 - } - }, - { - "monitor_id": "0027-up", - "histogram": { - "count": 20, - "points": [ - { - "timestamp": 1568172664000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172694000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172724000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172754000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172784000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172814000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172844000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172874000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172904000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172934000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172964000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172994000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173024000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173054000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173084000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173114000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173144000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173174000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173204000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173234000, - "up": 1, - "down": 0 - } - ] - }, - "state": { - "agent": null, - "checks": [ - { - "agent": { - "id": "04e1d082-65bc-4929-8d65-d0768a2621c4" - }, - "container": null, - "kubernetes": null, - "monitor": { - "ip": "127.0.0.1", - "name": "", - "status": "up" - }, - "observer": { - "geo": { - "name": "mpls", - "location": { - "lat": 37.926867976784706, - "lon": -78.02490200847387 - } - } - }, - "timestamp": "1568173234373" - } - ], - "geo": null, - "observer": { - "geo": { - "name": [ - "mpls" - ], - "location": null - } - }, - "monitor": { - "id": null, - "name": null, - "status": "up", - "type": null - }, - "summary": { - "up": 1, - "down": 0, - "geo": null - }, - "url": { - "full": "http://localhost:5678/pattern?r=200x1", - "domain": "localhost" - }, - "timestamp": 1568173234373 - } - }, - { - "monitor_id": "0028-up", - "histogram": { - "count": 20, - "points": [ - { - "timestamp": 1568172664000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172694000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172724000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172754000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172784000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172814000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172844000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172874000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172904000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172934000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172964000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172994000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173024000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173054000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173084000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173114000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173144000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173174000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173204000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173234000, - "up": 1, - "down": 0 - } - ] - }, - "state": { - "agent": null, - "checks": [ - { - "agent": { - "id": "04e1d082-65bc-4929-8d65-d0768a2621c4" - }, - "container": null, - "kubernetes": null, - "monitor": { - "ip": "127.0.0.1", - "name": "", - "status": "up" - }, - "observer": { - "geo": { - "name": "mpls", - "location": { - "lat": 37.926867976784706, - "lon": -78.02490200847387 - } - } - }, - "timestamp": "1568173234373" - } - ], - "geo": null, - "observer": { - "geo": { - "name": [ - "mpls" - ], - "location": null - } - }, - "monitor": { - "id": null, - "name": null, - "status": "up", - "type": null - }, - "summary": { - "up": 1, - "down": 0, - "geo": null - }, - "url": { - "full": "http://localhost:5678/pattern?r=200x1", - "domain": "localhost" - }, - "timestamp": 1568173234373 - } - }, - { - "monitor_id": "0029-up", - "histogram": { - "count": 20, - "points": [ - { - "timestamp": 1568172664000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172694000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172724000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172754000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172784000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172814000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172844000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172874000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172904000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172934000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172964000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172994000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173024000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173054000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173084000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173114000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173144000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173174000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173204000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173234000, - "up": 1, - "down": 0 - } - ] - }, - "state": { - "agent": null, - "checks": [ - { - "agent": { - "id": "04e1d082-65bc-4929-8d65-d0768a2621c4" - }, - "container": null, - "kubernetes": null, - "monitor": { - "ip": "127.0.0.1", - "name": "", - "status": "up" - }, - "observer": { - "geo": { - "name": "mpls", - "location": { - "lat": 37.926867976784706, - "lon": -78.02490200847387 - } - } - }, - "timestamp": "1568173234373" - } - ], - "geo": null, - "observer": { - "geo": { - "name": [ - "mpls" - ], - "location": null - } - }, - "monitor": { - "id": null, - "name": null, - "status": "up", - "type": null - }, - "summary": { - "up": 1, - "down": 0, - "geo": null - }, - "url": { - "full": "http://localhost:5678/pattern?r=200x1", - "domain": "localhost" - }, - "timestamp": 1568173234373 - } - } - ] - } -} \ No newline at end of file diff --git a/x-pack/test/api_integration/apis/uptime/graphql/fixtures/monitor_states_page_3_previous.json b/x-pack/test/api_integration/apis/uptime/graphql/fixtures/monitor_states_page_3_previous.json deleted file mode 100644 index 0d5a76059d004..0000000000000 --- a/x-pack/test/api_integration/apis/uptime/graphql/fixtures/monitor_states_page_3_previous.json +++ /dev/null @@ -1,1609 +0,0 @@ -{ - "monitorStates": { - "prevPagePagination": "{\"cursorKey\":{\"monitor_id\":\"0010-down\"},\"sortOrder\":\"ASC\",\"cursorDirection\":\"BEFORE\"}", - "nextPagePagination": "{\"cursorKey\":{\"monitor_id\":\"0019-up\"},\"sortOrder\":\"ASC\",\"cursorDirection\":\"AFTER\"}", - "totalSummaryCount": 2000, - "summaries": [ - { - "monitor_id": "0010-down", - "histogram": { - "count": 20, - "points": [ - { - "timestamp": 1568172664000, - "up": 0, - "down": 1 - }, - { - "timestamp": 1568172694000, - "up": 0, - "down": 1 - }, - { - "timestamp": 1568172724000, - "up": 0, - "down": 1 - }, - { - "timestamp": 1568172754000, - "up": 0, - "down": 1 - }, - { - "timestamp": 1568172784000, - "up": 0, - "down": 1 - }, - { - "timestamp": 1568172814000, - "up": 0, - "down": 1 - }, - { - "timestamp": 1568172844000, - "up": 0, - "down": 1 - }, - { - "timestamp": 1568172874000, - "up": 0, - "down": 1 - }, - { - "timestamp": 1568172904000, - "up": 0, - "down": 1 - }, - { - "timestamp": 1568172934000, - "up": 0, - "down": 1 - }, - { - "timestamp": 1568172964000, - "up": 0, - "down": 1 - }, - { - "timestamp": 1568172994000, - "up": 0, - "down": 1 - }, - { - "timestamp": 1568173024000, - "up": 0, - "down": 1 - }, - { - "timestamp": 1568173054000, - "up": 0, - "down": 1 - }, - { - "timestamp": 1568173084000, - "up": 0, - "down": 1 - }, - { - "timestamp": 1568173114000, - "up": 0, - "down": 1 - }, - { - "timestamp": 1568173144000, - "up": 0, - "down": 1 - }, - { - "timestamp": 1568173174000, - "up": 0, - "down": 1 - }, - { - "timestamp": 1568173204000, - "up": 0, - "down": 1 - }, - { - "timestamp": 1568173234000, - "up": 0, - "down": 1 - } - ] - }, - "state": { - "agent": null, - "checks": [ - { - "agent": { - "id": "04e1d082-65bc-4929-8d65-d0768a2621c4" - }, - "container": null, - "kubernetes": null, - "monitor": { - "ip": "127.0.0.1", - "name": "", - "status": "down" - }, - "observer": { - "geo": { - "name": "mpls", - "location": { - "lat": 37.926867976784706, - "lon": -78.02490200847387 - } - } - }, - "timestamp": "1568173234371" - } - ], - "geo": null, - "observer": { - "geo": { - "name": [ - "mpls" - ], - "location": null - } - }, - "monitor": { - "id": null, - "name": null, - "status": "down", - "type": null - }, - "summary": { - "up": 0, - "down": 1, - "geo": null - }, - "url": { - "full": "http://localhost:5678/pattern?r=400x1", - "domain": "localhost" - }, - "timestamp": 1568173234371 - } - }, - { - "monitor_id": "0011-up", - "histogram": { - "count": 20, - "points": [ - { - "timestamp": 1568172664000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172694000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172724000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172754000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172784000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172814000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172844000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172874000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172904000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172934000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172964000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172994000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173024000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173054000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173084000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173114000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173144000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173174000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173204000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173234000, - "up": 1, - "down": 0 - } - ] - }, - "state": { - "agent": null, - "checks": [ - { - "agent": { - "id": "04e1d082-65bc-4929-8d65-d0768a2621c4" - }, - "container": null, - "kubernetes": null, - "monitor": { - "ip": "127.0.0.1", - "name": "", - "status": "up" - }, - "observer": { - "geo": { - "name": "mpls", - "location": { - "lat": 37.926867976784706, - "lon": -78.02490200847387 - } - } - }, - "timestamp": "1568173234371" - } - ], - "geo": null, - "observer": { - "geo": { - "name": [ - "mpls" - ], - "location": null - } - }, - "monitor": { - "id": null, - "name": null, - "status": "up", - "type": null - }, - "summary": { - "up": 1, - "down": 0, - "geo": null - }, - "url": { - "full": "http://localhost:5678/pattern?r=200x1", - "domain": "localhost" - }, - "timestamp": 1568173234371 - } - }, - { - "monitor_id": "0012-up", - "histogram": { - "count": 20, - "points": [ - { - "timestamp": 1568172664000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172694000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172724000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172754000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172784000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172814000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172844000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172874000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172904000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172934000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172964000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172994000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173024000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173054000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173084000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173114000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173144000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173174000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173204000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173234000, - "up": 1, - "down": 0 - } - ] - }, - "state": { - "agent": null, - "checks": [ - { - "agent": { - "id": "04e1d082-65bc-4929-8d65-d0768a2621c4" - }, - "container": null, - "kubernetes": null, - "monitor": { - "ip": "127.0.0.1", - "name": "", - "status": "up" - }, - "observer": { - "geo": { - "name": "mpls", - "location": { - "lat": 37.926867976784706, - "lon": -78.02490200847387 - } - } - }, - "timestamp": "1568173234371" - } - ], - "geo": null, - "observer": { - "geo": { - "name": [ - "mpls" - ], - "location": null - } - }, - "monitor": { - "id": null, - "name": null, - "status": "up", - "type": null - }, - "summary": { - "up": 1, - "down": 0, - "geo": null - }, - "url": { - "full": "http://localhost:5678/pattern?r=200x1", - "domain": "localhost" - }, - "timestamp": 1568173234371 - } - }, - { - "monitor_id": "0013-up", - "histogram": { - "count": 20, - "points": [ - { - "timestamp": 1568172664000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172694000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172724000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172754000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172784000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172814000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172844000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172874000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172904000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172934000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172964000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172994000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173024000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173054000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173084000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173114000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173144000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173174000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173204000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173234000, - "up": 1, - "down": 0 - } - ] - }, - "state": { - "agent": null, - "checks": [ - { - "agent": { - "id": "04e1d082-65bc-4929-8d65-d0768a2621c4" - }, - "container": null, - "kubernetes": null, - "monitor": { - "ip": "127.0.0.1", - "name": "", - "status": "up" - }, - "observer": { - "geo": { - "name": "mpls", - "location": { - "lat": 37.926867976784706, - "lon": -78.02490200847387 - } - } - }, - "timestamp": "1568173234372" - } - ], - "geo": null, - "observer": { - "geo": { - "name": [ - "mpls" - ], - "location": null - } - }, - "monitor": { - "id": null, - "name": null, - "status": "up", - "type": null - }, - "summary": { - "up": 1, - "down": 0, - "geo": null - }, - "url": { - "full": "http://localhost:5678/pattern?r=200x1", - "domain": "localhost" - }, - "timestamp": 1568173234372 - } - }, - { - "monitor_id": "0014-up", - "histogram": { - "count": 20, - "points": [ - { - "timestamp": 1568172664000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172694000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172724000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172754000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172784000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172814000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172844000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172874000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172904000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172934000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172964000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172994000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173024000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173054000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173084000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173114000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173144000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173174000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173204000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173234000, - "up": 1, - "down": 0 - } - ] - }, - "state": { - "agent": null, - "checks": [ - { - "agent": { - "id": "04e1d082-65bc-4929-8d65-d0768a2621c4" - }, - "container": null, - "kubernetes": null, - "monitor": { - "ip": "127.0.0.1", - "name": "", - "status": "up" - }, - "observer": { - "geo": { - "name": "mpls", - "location": { - "lat": 37.926867976784706, - "lon": -78.02490200847387 - } - } - }, - "timestamp": "1568173234372" - } - ], - "geo": null, - "observer": { - "geo": { - "name": [ - "mpls" - ], - "location": null - } - }, - "monitor": { - "id": null, - "name": null, - "status": "up", - "type": null - }, - "summary": { - "up": 1, - "down": 0, - "geo": null - }, - "url": { - "full": "http://localhost:5678/pattern?r=200x1", - "domain": "localhost" - }, - "timestamp": 1568173234372 - } - }, - { - "monitor_id": "0015-intermittent", - "histogram": { - "count": 20, - "points": [ - { - "timestamp": 1568172664000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172694000, - "up": 0, - "down": 1 - }, - { - "timestamp": 1568172724000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172754000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172784000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172814000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172844000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172874000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172904000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172934000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172964000, - "up": 0, - "down": 1 - }, - { - "timestamp": 1568172994000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173024000, - "up": 0, - "down": 1 - }, - { - "timestamp": 1568173054000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173084000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173114000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173144000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173174000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173204000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173234000, - "up": 1, - "down": 0 - } - ] - }, - "state": { - "agent": null, - "checks": [ - { - "agent": { - "id": "04e1d082-65bc-4929-8d65-d0768a2621c4" - }, - "container": null, - "kubernetes": null, - "monitor": { - "ip": "127.0.0.1", - "name": "", - "status": "up" - }, - "observer": { - "geo": { - "name": "mpls", - "location": { - "lat": 37.926867976784706, - "lon": -78.02490200847387 - } - } - }, - "timestamp": "1568173234371" - } - ], - "geo": null, - "observer": { - "geo": { - "name": [ - "mpls" - ], - "location": null - } - }, - "monitor": { - "id": null, - "name": null, - "status": "up", - "type": null - }, - "summary": { - "up": 1, - "down": 0, - "geo": null - }, - "url": { - "full": "http://localhost:5678/pattern?r=200x5,500x1", - "domain": "localhost" - }, - "timestamp": 1568173234371 - } - }, - { - "monitor_id": "0016-up", - "histogram": { - "count": 20, - "points": [ - { - "timestamp": 1568172664000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172694000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172724000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172754000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172784000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172814000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172844000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172874000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172904000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172934000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172964000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172994000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173024000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173054000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173084000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173114000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173144000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173174000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173204000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173234000, - "up": 1, - "down": 0 - } - ] - }, - "state": { - "agent": null, - "checks": [ - { - "agent": { - "id": "04e1d082-65bc-4929-8d65-d0768a2621c4" - }, - "container": null, - "kubernetes": null, - "monitor": { - "ip": "127.0.0.1", - "name": "", - "status": "up" - }, - "observer": { - "geo": { - "name": "mpls", - "location": { - "lat": 37.926867976784706, - "lon": -78.02490200847387 - } - } - }, - "timestamp": "1568173234371" - } - ], - "geo": null, - "observer": { - "geo": { - "name": [ - "mpls" - ], - "location": null - } - }, - "monitor": { - "id": null, - "name": null, - "status": "up", - "type": null - }, - "summary": { - "up": 1, - "down": 0, - "geo": null - }, - "url": { - "full": "http://localhost:5678/pattern?r=200x1", - "domain": "localhost" - }, - "timestamp": 1568173234371 - } - }, - { - "monitor_id": "0017-up", - "histogram": { - "count": 20, - "points": [ - { - "timestamp": 1568172664000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172694000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172724000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172754000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172784000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172814000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172844000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172874000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172904000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172934000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172964000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172994000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173024000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173054000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173084000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173114000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173144000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173174000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173204000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173234000, - "up": 1, - "down": 0 - } - ] - }, - "state": { - "agent": null, - "checks": [ - { - "agent": { - "id": "04e1d082-65bc-4929-8d65-d0768a2621c4" - }, - "container": null, - "kubernetes": null, - "monitor": { - "ip": "127.0.0.1", - "name": "", - "status": "up" - }, - "observer": { - "geo": { - "name": "mpls", - "location": { - "lat": 37.926867976784706, - "lon": -78.02490200847387 - } - } - }, - "timestamp": "1568173234371" - } - ], - "geo": null, - "observer": { - "geo": { - "name": [ - "mpls" - ], - "location": null - } - }, - "monitor": { - "id": null, - "name": null, - "status": "up", - "type": null - }, - "summary": { - "up": 1, - "down": 0, - "geo": null - }, - "url": { - "full": "http://localhost:5678/pattern?r=200x1", - "domain": "localhost" - }, - "timestamp": 1568173234371 - } - }, - { - "monitor_id": "0018-up", - "histogram": { - "count": 20, - "points": [ - { - "timestamp": 1568172664000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172694000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172724000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172754000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172784000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172814000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172844000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172874000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172904000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172934000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172964000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172994000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173024000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173054000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173084000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173114000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173144000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173174000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173204000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173234000, - "up": 1, - "down": 0 - } - ] - }, - "state": { - "agent": null, - "checks": [ - { - "agent": { - "id": "04e1d082-65bc-4929-8d65-d0768a2621c4" - }, - "container": null, - "kubernetes": null, - "monitor": { - "ip": "127.0.0.1", - "name": "", - "status": "up" - }, - "observer": { - "geo": { - "name": "mpls", - "location": { - "lat": 37.926867976784706, - "lon": -78.02490200847387 - } - } - }, - "timestamp": "1568173234371" - } - ], - "geo": null, - "observer": { - "geo": { - "name": [ - "mpls" - ], - "location": null - } - }, - "monitor": { - "id": null, - "name": null, - "status": "up", - "type": null - }, - "summary": { - "up": 1, - "down": 0, - "geo": null - }, - "url": { - "full": "http://localhost:5678/pattern?r=200x1", - "domain": "localhost" - }, - "timestamp": 1568173234371 - } - }, - { - "monitor_id": "0019-up", - "histogram": { - "count": 20, - "points": [ - { - "timestamp": 1568172664000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172694000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172724000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172754000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172784000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172814000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172844000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172874000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172904000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172934000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172964000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172994000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173024000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173054000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173084000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173114000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173144000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173174000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173204000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173234000, - "up": 1, - "down": 0 - } - ] - }, - "state": { - "agent": null, - "checks": [ - { - "agent": { - "id": "04e1d082-65bc-4929-8d65-d0768a2621c4" - }, - "container": null, - "kubernetes": null, - "monitor": { - "ip": "127.0.0.1", - "name": "", - "status": "up" - }, - "observer": { - "geo": { - "name": "mpls", - "location": { - "lat": 37.926867976784706, - "lon": -78.02490200847387 - } - } - }, - "timestamp": "1568173234371" - } - ], - "geo": null, - "observer": { - "geo": { - "name": [ - "mpls" - ], - "location": null - } - }, - "monitor": { - "id": null, - "name": null, - "status": "up", - "type": null - }, - "summary": { - "up": 1, - "down": 0, - "geo": null - }, - "url": { - "full": "http://localhost:5678/pattern?r=200x1", - "domain": "localhost" - }, - "timestamp": 1568173234371 - } - } - ] - } -} \ No newline at end of file diff --git a/x-pack/test/api_integration/apis/uptime/graphql/fixtures/monitor_states_page_4.json b/x-pack/test/api_integration/apis/uptime/graphql/fixtures/monitor_states_page_4.json deleted file mode 100644 index 4caff800ac96e..0000000000000 --- a/x-pack/test/api_integration/apis/uptime/graphql/fixtures/monitor_states_page_4.json +++ /dev/null @@ -1,1609 +0,0 @@ -{ - "monitorStates": { - "prevPagePagination": "{\"cursorKey\":{\"monitor_id\":\"0030-intermittent\"},\"sortOrder\":\"ASC\",\"cursorDirection\":\"BEFORE\"}", - "nextPagePagination": "{\"cursorDirection\":\"AFTER\",\"sortOrder\":\"ASC\",\"cursorKey\":{\"monitor_id\":\"0039-up\"}}", - "totalSummaryCount": 2000, - "summaries": [ - { - "monitor_id": "0030-intermittent", - "histogram": { - "count": 20, - "points": [ - { - "timestamp": 1568172664000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172694000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172724000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172754000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172784000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172814000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172844000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172874000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172904000, - "up": 0, - "down": 1 - }, - { - "timestamp": 1568172934000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172964000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172994000, - "up": 0, - "down": 1 - }, - { - "timestamp": 1568173024000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173054000, - "up": 0, - "down": 1 - }, - { - "timestamp": 1568173084000, - "up": 0, - "down": 1 - }, - { - "timestamp": 1568173114000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173144000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173174000, - "up": 0, - "down": 1 - }, - { - "timestamp": 1568173204000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173234000, - "up": 0, - "down": 1 - } - ] - }, - "state": { - "agent": null, - "checks": [ - { - "agent": { - "id": "04e1d082-65bc-4929-8d65-d0768a2621c4" - }, - "container": null, - "kubernetes": null, - "monitor": { - "ip": "127.0.0.1", - "name": "", - "status": "down" - }, - "observer": { - "geo": { - "name": "mpls", - "location": { - "lat": 37.926867976784706, - "lon": -78.02490200847387 - } - } - }, - "timestamp": "1568173234373" - } - ], - "geo": null, - "observer": { - "geo": { - "name": [ - "mpls" - ], - "location": null - } - }, - "monitor": { - "id": null, - "name": null, - "status": "down", - "type": null - }, - "summary": { - "up": 0, - "down": 1, - "geo": null - }, - "url": { - "full": "http://localhost:5678/pattern?r=200x5,500x1", - "domain": "localhost" - }, - "timestamp": 1568173234373 - } - }, - { - "monitor_id": "0031-up", - "histogram": { - "count": 20, - "points": [ - { - "timestamp": 1568172664000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172694000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172724000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172754000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172784000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172814000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172844000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172874000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172904000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172934000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172964000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172994000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173024000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173054000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173084000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173114000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173144000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173174000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173204000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173234000, - "up": 1, - "down": 0 - } - ] - }, - "state": { - "agent": null, - "checks": [ - { - "agent": { - "id": "04e1d082-65bc-4929-8d65-d0768a2621c4" - }, - "container": null, - "kubernetes": null, - "monitor": { - "ip": "127.0.0.1", - "name": "", - "status": "up" - }, - "observer": { - "geo": { - "name": "mpls", - "location": { - "lat": 37.926867976784706, - "lon": -78.02490200847387 - } - } - }, - "timestamp": "1568173234374" - } - ], - "geo": null, - "observer": { - "geo": { - "name": [ - "mpls" - ], - "location": null - } - }, - "monitor": { - "id": null, - "name": null, - "status": "up", - "type": null - }, - "summary": { - "up": 1, - "down": 0, - "geo": null - }, - "url": { - "full": "http://localhost:5678/pattern?r=200x1", - "domain": "localhost" - }, - "timestamp": 1568173234374 - } - }, - { - "monitor_id": "0032-up", - "histogram": { - "count": 20, - "points": [ - { - "timestamp": 1568172664000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172694000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172724000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172754000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172784000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172814000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172844000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172874000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172904000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172934000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172964000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172994000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173024000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173054000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173084000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173114000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173144000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173174000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173204000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173234000, - "up": 1, - "down": 0 - } - ] - }, - "state": { - "agent": null, - "checks": [ - { - "agent": { - "id": "04e1d082-65bc-4929-8d65-d0768a2621c4" - }, - "container": null, - "kubernetes": null, - "monitor": { - "ip": "127.0.0.1", - "name": "", - "status": "up" - }, - "observer": { - "geo": { - "name": "mpls", - "location": { - "lat": 37.926867976784706, - "lon": -78.02490200847387 - } - } - }, - "timestamp": "1568173234375" - } - ], - "geo": null, - "observer": { - "geo": { - "name": [ - "mpls" - ], - "location": null - } - }, - "monitor": { - "id": null, - "name": null, - "status": "up", - "type": null - }, - "summary": { - "up": 1, - "down": 0, - "geo": null - }, - "url": { - "full": "http://localhost:5678/pattern?r=200x1", - "domain": "localhost" - }, - "timestamp": 1568173234375 - } - }, - { - "monitor_id": "0033-up", - "histogram": { - "count": 20, - "points": [ - { - "timestamp": 1568172664000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172694000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172724000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172754000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172784000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172814000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172844000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172874000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172904000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172934000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172964000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172994000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173024000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173054000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173084000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173114000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173144000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173174000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173204000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173234000, - "up": 1, - "down": 0 - } - ] - }, - "state": { - "agent": null, - "checks": [ - { - "agent": { - "id": "04e1d082-65bc-4929-8d65-d0768a2621c4" - }, - "container": null, - "kubernetes": null, - "monitor": { - "ip": "127.0.0.1", - "name": "", - "status": "up" - }, - "observer": { - "geo": { - "name": "mpls", - "location": { - "lat": 37.926867976784706, - "lon": -78.02490200847387 - } - } - }, - "timestamp": "1568173234371" - } - ], - "geo": null, - "observer": { - "geo": { - "name": [ - "mpls" - ], - "location": null - } - }, - "monitor": { - "id": null, - "name": null, - "status": "up", - "type": null - }, - "summary": { - "up": 1, - "down": 0, - "geo": null - }, - "url": { - "full": "http://localhost:5678/pattern?r=200x1", - "domain": "localhost" - }, - "timestamp": 1568173234371 - } - }, - { - "monitor_id": "0034-up", - "histogram": { - "count": 20, - "points": [ - { - "timestamp": 1568172664000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172694000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172724000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172754000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172784000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172814000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172844000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172874000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172904000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172934000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172964000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172994000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173024000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173054000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173084000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173114000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173144000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173174000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173204000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173234000, - "up": 1, - "down": 0 - } - ] - }, - "state": { - "agent": null, - "checks": [ - { - "agent": { - "id": "04e1d082-65bc-4929-8d65-d0768a2621c4" - }, - "container": null, - "kubernetes": null, - "monitor": { - "ip": "127.0.0.1", - "name": "", - "status": "up" - }, - "observer": { - "geo": { - "name": "mpls", - "location": { - "lat": 37.926867976784706, - "lon": -78.02490200847387 - } - } - }, - "timestamp": "1568173234371" - } - ], - "geo": null, - "observer": { - "geo": { - "name": [ - "mpls" - ], - "location": null - } - }, - "monitor": { - "id": null, - "name": null, - "status": "up", - "type": null - }, - "summary": { - "up": 1, - "down": 0, - "geo": null - }, - "url": { - "full": "http://localhost:5678/pattern?r=200x1", - "domain": "localhost" - }, - "timestamp": 1568173234371 - } - }, - { - "monitor_id": "0035-up", - "histogram": { - "count": 20, - "points": [ - { - "timestamp": 1568172664000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172694000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172724000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172754000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172784000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172814000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172844000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172874000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172904000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172934000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172964000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172994000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173024000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173054000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173084000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173114000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173144000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173174000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173204000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173234000, - "up": 1, - "down": 0 - } - ] - }, - "state": { - "agent": null, - "checks": [ - { - "agent": { - "id": "04e1d082-65bc-4929-8d65-d0768a2621c4" - }, - "container": null, - "kubernetes": null, - "monitor": { - "ip": "127.0.0.1", - "name": "", - "status": "up" - }, - "observer": { - "geo": { - "name": "mpls", - "location": { - "lat": 37.926867976784706, - "lon": -78.02490200847387 - } - } - }, - "timestamp": "1568173234372" - } - ], - "geo": null, - "observer": { - "geo": { - "name": [ - "mpls" - ], - "location": null - } - }, - "monitor": { - "id": null, - "name": null, - "status": "up", - "type": null - }, - "summary": { - "up": 1, - "down": 0, - "geo": null - }, - "url": { - "full": "http://localhost:5678/pattern?r=200x1", - "domain": "localhost" - }, - "timestamp": 1568173234372 - } - }, - { - "monitor_id": "0036-up", - "histogram": { - "count": 20, - "points": [ - { - "timestamp": 1568172664000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172694000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172724000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172754000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172784000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172814000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172844000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172874000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172904000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172934000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172964000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172994000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173024000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173054000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173084000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173114000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173144000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173174000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173204000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173234000, - "up": 1, - "down": 0 - } - ] - }, - "state": { - "agent": null, - "checks": [ - { - "agent": { - "id": "04e1d082-65bc-4929-8d65-d0768a2621c4" - }, - "container": null, - "kubernetes": null, - "monitor": { - "ip": "127.0.0.1", - "name": "", - "status": "up" - }, - "observer": { - "geo": { - "name": "mpls", - "location": { - "lat": 37.926867976784706, - "lon": -78.02490200847387 - } - } - }, - "timestamp": "1568173234372" - } - ], - "geo": null, - "observer": { - "geo": { - "name": [ - "mpls" - ], - "location": null - } - }, - "monitor": { - "id": null, - "name": null, - "status": "up", - "type": null - }, - "summary": { - "up": 1, - "down": 0, - "geo": null - }, - "url": { - "full": "http://localhost:5678/pattern?r=200x1", - "domain": "localhost" - }, - "timestamp": 1568173234372 - } - }, - { - "monitor_id": "0037-up", - "histogram": { - "count": 20, - "points": [ - { - "timestamp": 1568172664000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172694000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172724000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172754000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172784000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172814000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172844000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172874000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172904000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172934000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172964000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172994000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173024000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173054000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173084000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173114000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173144000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173174000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173204000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173234000, - "up": 1, - "down": 0 - } - ] - }, - "state": { - "agent": null, - "checks": [ - { - "agent": { - "id": "04e1d082-65bc-4929-8d65-d0768a2621c4" - }, - "container": null, - "kubernetes": null, - "monitor": { - "ip": "127.0.0.1", - "name": "", - "status": "up" - }, - "observer": { - "geo": { - "name": "mpls", - "location": { - "lat": 37.926867976784706, - "lon": -78.02490200847387 - } - } - }, - "timestamp": "1568173234372" - } - ], - "geo": null, - "observer": { - "geo": { - "name": [ - "mpls" - ], - "location": null - } - }, - "monitor": { - "id": null, - "name": null, - "status": "up", - "type": null - }, - "summary": { - "up": 1, - "down": 0, - "geo": null - }, - "url": { - "full": "http://localhost:5678/pattern?r=200x1", - "domain": "localhost" - }, - "timestamp": 1568173234372 - } - }, - { - "monitor_id": "0038-up", - "histogram": { - "count": 20, - "points": [ - { - "timestamp": 1568172664000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172694000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172724000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172754000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172784000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172814000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172844000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172874000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172904000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172934000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172964000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172994000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173024000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173054000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173084000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173114000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173144000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173174000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173204000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173234000, - "up": 1, - "down": 0 - } - ] - }, - "state": { - "agent": null, - "checks": [ - { - "agent": { - "id": "04e1d082-65bc-4929-8d65-d0768a2621c4" - }, - "container": null, - "kubernetes": null, - "monitor": { - "ip": "127.0.0.1", - "name": "", - "status": "up" - }, - "observer": { - "geo": { - "name": "mpls", - "location": { - "lat": 37.926867976784706, - "lon": -78.02490200847387 - } - } - }, - "timestamp": "1568173234372" - } - ], - "geo": null, - "observer": { - "geo": { - "name": [ - "mpls" - ], - "location": null - } - }, - "monitor": { - "id": null, - "name": null, - "status": "up", - "type": null - }, - "summary": { - "up": 1, - "down": 0, - "geo": null - }, - "url": { - "full": "http://localhost:5678/pattern?r=200x1", - "domain": "localhost" - }, - "timestamp": 1568173234372 - } - }, - { - "monitor_id": "0039-up", - "histogram": { - "count": 20, - "points": [ - { - "timestamp": 1568172664000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172694000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172724000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172754000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172784000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172814000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172844000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172874000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172904000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172934000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172964000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172994000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173024000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173054000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173084000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173114000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173144000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173174000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173204000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173234000, - "up": 1, - "down": 0 - } - ] - }, - "state": { - "agent": null, - "checks": [ - { - "agent": { - "id": "04e1d082-65bc-4929-8d65-d0768a2621c4" - }, - "container": null, - "kubernetes": null, - "monitor": { - "ip": "127.0.0.1", - "name": "", - "status": "up" - }, - "observer": { - "geo": { - "name": "mpls", - "location": { - "lat": 37.926867976784706, - "lon": -78.02490200847387 - } - } - }, - "timestamp": "1568173234372" - } - ], - "geo": null, - "observer": { - "geo": { - "name": [ - "mpls" - ], - "location": null - } - }, - "monitor": { - "id": null, - "name": null, - "status": "up", - "type": null - }, - "summary": { - "up": 1, - "down": 0, - "geo": null - }, - "url": { - "full": "http://localhost:5678/pattern?r=200x1", - "domain": "localhost" - }, - "timestamp": 1568173234372 - } - } - ] - } -} \ No newline at end of file diff --git a/x-pack/test/api_integration/apis/uptime/graphql/fixtures/monitor_states_page_4_previous.json b/x-pack/test/api_integration/apis/uptime/graphql/fixtures/monitor_states_page_4_previous.json deleted file mode 100644 index 02bd149b50247..0000000000000 --- a/x-pack/test/api_integration/apis/uptime/graphql/fixtures/monitor_states_page_4_previous.json +++ /dev/null @@ -1,1609 +0,0 @@ -{ - "monitorStates": { - "prevPagePagination": "{\"cursorKey\":{\"monitor_id\":\"0020-down\"},\"sortOrder\":\"ASC\",\"cursorDirection\":\"BEFORE\"}", - "nextPagePagination": "{\"cursorKey\":{\"monitor_id\":\"0029-up\"},\"sortOrder\":\"ASC\",\"cursorDirection\":\"AFTER\"}", - "totalSummaryCount": 2000, - "summaries": [ - { - "monitor_id": "0020-down", - "histogram": { - "count": 20, - "points": [ - { - "timestamp": 1568172664000, - "up": 0, - "down": 1 - }, - { - "timestamp": 1568172694000, - "up": 0, - "down": 1 - }, - { - "timestamp": 1568172724000, - "up": 0, - "down": 1 - }, - { - "timestamp": 1568172754000, - "up": 0, - "down": 1 - }, - { - "timestamp": 1568172784000, - "up": 0, - "down": 1 - }, - { - "timestamp": 1568172814000, - "up": 0, - "down": 1 - }, - { - "timestamp": 1568172844000, - "up": 0, - "down": 1 - }, - { - "timestamp": 1568172874000, - "up": 0, - "down": 1 - }, - { - "timestamp": 1568172904000, - "up": 0, - "down": 1 - }, - { - "timestamp": 1568172934000, - "up": 0, - "down": 1 - }, - { - "timestamp": 1568172964000, - "up": 0, - "down": 1 - }, - { - "timestamp": 1568172994000, - "up": 0, - "down": 1 - }, - { - "timestamp": 1568173024000, - "up": 0, - "down": 1 - }, - { - "timestamp": 1568173054000, - "up": 0, - "down": 1 - }, - { - "timestamp": 1568173084000, - "up": 0, - "down": 1 - }, - { - "timestamp": 1568173114000, - "up": 0, - "down": 1 - }, - { - "timestamp": 1568173144000, - "up": 0, - "down": 1 - }, - { - "timestamp": 1568173174000, - "up": 0, - "down": 1 - }, - { - "timestamp": 1568173204000, - "up": 0, - "down": 1 - }, - { - "timestamp": 1568173234000, - "up": 0, - "down": 1 - } - ] - }, - "state": { - "agent": null, - "checks": [ - { - "agent": { - "id": "04e1d082-65bc-4929-8d65-d0768a2621c4" - }, - "container": null, - "kubernetes": null, - "monitor": { - "ip": "127.0.0.1", - "name": "", - "status": "down" - }, - "observer": { - "geo": { - "name": "mpls", - "location": { - "lat": 37.926867976784706, - "lon": -78.02490200847387 - } - } - }, - "timestamp": "1568173234372" - } - ], - "geo": null, - "observer": { - "geo": { - "name": [ - "mpls" - ], - "location": null - } - }, - "monitor": { - "id": null, - "name": null, - "status": "down", - "type": null - }, - "summary": { - "up": 0, - "down": 1, - "geo": null - }, - "url": { - "full": "http://localhost:5678/pattern?r=400x1", - "domain": "localhost" - }, - "timestamp": 1568173234372 - } - }, - { - "monitor_id": "0021-up", - "histogram": { - "count": 20, - "points": [ - { - "timestamp": 1568172664000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172694000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172724000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172754000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172784000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172814000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172844000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172874000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172904000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172934000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172964000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172994000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173024000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173054000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173084000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173114000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173144000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173174000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173204000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173234000, - "up": 1, - "down": 0 - } - ] - }, - "state": { - "agent": null, - "checks": [ - { - "agent": { - "id": "04e1d082-65bc-4929-8d65-d0768a2621c4" - }, - "container": null, - "kubernetes": null, - "monitor": { - "ip": "127.0.0.1", - "name": "", - "status": "up" - }, - "observer": { - "geo": { - "name": "mpls", - "location": { - "lat": 37.926867976784706, - "lon": -78.02490200847387 - } - } - }, - "timestamp": "1568173234372" - } - ], - "geo": null, - "observer": { - "geo": { - "name": [ - "mpls" - ], - "location": null - } - }, - "monitor": { - "id": null, - "name": null, - "status": "up", - "type": null - }, - "summary": { - "up": 1, - "down": 0, - "geo": null - }, - "url": { - "full": "http://localhost:5678/pattern?r=200x1", - "domain": "localhost" - }, - "timestamp": 1568173234372 - } - }, - { - "monitor_id": "0022-up", - "histogram": { - "count": 20, - "points": [ - { - "timestamp": 1568172664000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172694000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172724000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172754000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172784000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172814000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172844000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172874000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172904000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172934000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172964000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172994000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173024000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173054000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173084000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173114000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173144000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173174000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173204000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173234000, - "up": 1, - "down": 0 - } - ] - }, - "state": { - "agent": null, - "checks": [ - { - "agent": { - "id": "04e1d082-65bc-4929-8d65-d0768a2621c4" - }, - "container": null, - "kubernetes": null, - "monitor": { - "ip": "127.0.0.1", - "name": "", - "status": "up" - }, - "observer": { - "geo": { - "name": "mpls", - "location": { - "lat": 37.926867976784706, - "lon": -78.02490200847387 - } - } - }, - "timestamp": "1568173234372" - } - ], - "geo": null, - "observer": { - "geo": { - "name": [ - "mpls" - ], - "location": null - } - }, - "monitor": { - "id": null, - "name": null, - "status": "up", - "type": null - }, - "summary": { - "up": 1, - "down": 0, - "geo": null - }, - "url": { - "full": "http://localhost:5678/pattern?r=200x1", - "domain": "localhost" - }, - "timestamp": 1568173234372 - } - }, - { - "monitor_id": "0023-up", - "histogram": { - "count": 20, - "points": [ - { - "timestamp": 1568172664000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172694000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172724000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172754000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172784000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172814000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172844000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172874000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172904000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172934000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172964000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172994000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173024000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173054000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173084000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173114000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173144000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173174000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173204000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173234000, - "up": 1, - "down": 0 - } - ] - }, - "state": { - "agent": null, - "checks": [ - { - "agent": { - "id": "04e1d082-65bc-4929-8d65-d0768a2621c4" - }, - "container": null, - "kubernetes": null, - "monitor": { - "ip": "127.0.0.1", - "name": "", - "status": "up" - }, - "observer": { - "geo": { - "name": "mpls", - "location": { - "lat": 37.926867976784706, - "lon": -78.02490200847387 - } - } - }, - "timestamp": "1568173234372" - } - ], - "geo": null, - "observer": { - "geo": { - "name": [ - "mpls" - ], - "location": null - } - }, - "monitor": { - "id": null, - "name": null, - "status": "up", - "type": null - }, - "summary": { - "up": 1, - "down": 0, - "geo": null - }, - "url": { - "full": "http://localhost:5678/pattern?r=200x1", - "domain": "localhost" - }, - "timestamp": 1568173234372 - } - }, - { - "monitor_id": "0024-up", - "histogram": { - "count": 20, - "points": [ - { - "timestamp": 1568172664000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172694000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172724000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172754000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172784000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172814000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172844000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172874000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172904000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172934000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172964000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172994000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173024000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173054000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173084000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173114000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173144000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173174000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173204000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173234000, - "up": 1, - "down": 0 - } - ] - }, - "state": { - "agent": null, - "checks": [ - { - "agent": { - "id": "04e1d082-65bc-4929-8d65-d0768a2621c4" - }, - "container": null, - "kubernetes": null, - "monitor": { - "ip": "127.0.0.1", - "name": "", - "status": "up" - }, - "observer": { - "geo": { - "name": "mpls", - "location": { - "lat": 37.926867976784706, - "lon": -78.02490200847387 - } - } - }, - "timestamp": "1568173234372" - } - ], - "geo": null, - "observer": { - "geo": { - "name": [ - "mpls" - ], - "location": null - } - }, - "monitor": { - "id": null, - "name": null, - "status": "up", - "type": null - }, - "summary": { - "up": 1, - "down": 0, - "geo": null - }, - "url": { - "full": "http://localhost:5678/pattern?r=200x1", - "domain": "localhost" - }, - "timestamp": 1568173234372 - } - }, - { - "monitor_id": "0025-up", - "histogram": { - "count": 20, - "points": [ - { - "timestamp": 1568172664000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172694000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172724000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172754000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172784000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172814000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172844000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172874000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172904000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172934000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172964000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172994000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173024000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173054000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173084000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173114000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173144000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173174000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173204000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173234000, - "up": 1, - "down": 0 - } - ] - }, - "state": { - "agent": null, - "checks": [ - { - "agent": { - "id": "04e1d082-65bc-4929-8d65-d0768a2621c4" - }, - "container": null, - "kubernetes": null, - "monitor": { - "ip": "127.0.0.1", - "name": "", - "status": "up" - }, - "observer": { - "geo": { - "name": "mpls", - "location": { - "lat": 37.926867976784706, - "lon": -78.02490200847387 - } - } - }, - "timestamp": "1568173234373" - } - ], - "geo": null, - "observer": { - "geo": { - "name": [ - "mpls" - ], - "location": null - } - }, - "monitor": { - "id": null, - "name": null, - "status": "up", - "type": null - }, - "summary": { - "up": 1, - "down": 0, - "geo": null - }, - "url": { - "full": "http://localhost:5678/pattern?r=200x1", - "domain": "localhost" - }, - "timestamp": 1568173234373 - } - }, - { - "monitor_id": "0026-up", - "histogram": { - "count": 20, - "points": [ - { - "timestamp": 1568172664000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172694000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172724000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172754000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172784000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172814000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172844000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172874000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172904000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172934000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172964000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172994000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173024000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173054000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173084000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173114000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173144000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173174000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173204000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173234000, - "up": 1, - "down": 0 - } - ] - }, - "state": { - "agent": null, - "checks": [ - { - "agent": { - "id": "04e1d082-65bc-4929-8d65-d0768a2621c4" - }, - "container": null, - "kubernetes": null, - "monitor": { - "ip": "127.0.0.1", - "name": "", - "status": "up" - }, - "observer": { - "geo": { - "name": "mpls", - "location": { - "lat": 37.926867976784706, - "lon": -78.02490200847387 - } - } - }, - "timestamp": "1568173234373" - } - ], - "geo": null, - "observer": { - "geo": { - "name": [ - "mpls" - ], - "location": null - } - }, - "monitor": { - "id": null, - "name": null, - "status": "up", - "type": null - }, - "summary": { - "up": 1, - "down": 0, - "geo": null - }, - "url": { - "full": "http://localhost:5678/pattern?r=200x1", - "domain": "localhost" - }, - "timestamp": 1568173234373 - } - }, - { - "monitor_id": "0027-up", - "histogram": { - "count": 20, - "points": [ - { - "timestamp": 1568172664000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172694000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172724000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172754000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172784000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172814000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172844000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172874000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172904000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172934000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172964000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172994000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173024000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173054000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173084000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173114000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173144000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173174000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173204000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173234000, - "up": 1, - "down": 0 - } - ] - }, - "state": { - "agent": null, - "checks": [ - { - "agent": { - "id": "04e1d082-65bc-4929-8d65-d0768a2621c4" - }, - "container": null, - "kubernetes": null, - "monitor": { - "ip": "127.0.0.1", - "name": "", - "status": "up" - }, - "observer": { - "geo": { - "name": "mpls", - "location": { - "lat": 37.926867976784706, - "lon": -78.02490200847387 - } - } - }, - "timestamp": "1568173234373" - } - ], - "geo": null, - "observer": { - "geo": { - "name": [ - "mpls" - ], - "location": null - } - }, - "monitor": { - "id": null, - "name": null, - "status": "up", - "type": null - }, - "summary": { - "up": 1, - "down": 0, - "geo": null - }, - "url": { - "full": "http://localhost:5678/pattern?r=200x1", - "domain": "localhost" - }, - "timestamp": 1568173234373 - } - }, - { - "monitor_id": "0028-up", - "histogram": { - "count": 20, - "points": [ - { - "timestamp": 1568172664000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172694000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172724000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172754000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172784000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172814000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172844000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172874000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172904000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172934000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172964000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172994000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173024000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173054000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173084000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173114000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173144000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173174000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173204000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173234000, - "up": 1, - "down": 0 - } - ] - }, - "state": { - "agent": null, - "checks": [ - { - "agent": { - "id": "04e1d082-65bc-4929-8d65-d0768a2621c4" - }, - "container": null, - "kubernetes": null, - "monitor": { - "ip": "127.0.0.1", - "name": "", - "status": "up" - }, - "observer": { - "geo": { - "name": "mpls", - "location": { - "lat": 37.926867976784706, - "lon": -78.02490200847387 - } - } - }, - "timestamp": "1568173234373" - } - ], - "geo": null, - "observer": { - "geo": { - "name": [ - "mpls" - ], - "location": null - } - }, - "monitor": { - "id": null, - "name": null, - "status": "up", - "type": null - }, - "summary": { - "up": 1, - "down": 0, - "geo": null - }, - "url": { - "full": "http://localhost:5678/pattern?r=200x1", - "domain": "localhost" - }, - "timestamp": 1568173234373 - } - }, - { - "monitor_id": "0029-up", - "histogram": { - "count": 20, - "points": [ - { - "timestamp": 1568172664000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172694000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172724000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172754000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172784000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172814000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172844000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172874000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172904000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172934000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172964000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172994000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173024000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173054000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173084000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173114000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173144000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173174000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173204000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173234000, - "up": 1, - "down": 0 - } - ] - }, - "state": { - "agent": null, - "checks": [ - { - "agent": { - "id": "04e1d082-65bc-4929-8d65-d0768a2621c4" - }, - "container": null, - "kubernetes": null, - "monitor": { - "ip": "127.0.0.1", - "name": "", - "status": "up" - }, - "observer": { - "geo": { - "name": "mpls", - "location": { - "lat": 37.926867976784706, - "lon": -78.02490200847387 - } - } - }, - "timestamp": "1568173234373" - } - ], - "geo": null, - "observer": { - "geo": { - "name": [ - "mpls" - ], - "location": null - } - }, - "monitor": { - "id": null, - "name": null, - "status": "up", - "type": null - }, - "summary": { - "up": 1, - "down": 0, - "geo": null - }, - "url": { - "full": "http://localhost:5678/pattern?r=200x1", - "domain": "localhost" - }, - "timestamp": 1568173234373 - } - } - ] - } -} \ No newline at end of file diff --git a/x-pack/test/api_integration/apis/uptime/graphql/fixtures/monitor_states_page_5.json b/x-pack/test/api_integration/apis/uptime/graphql/fixtures/monitor_states_page_5.json deleted file mode 100644 index 11e880f1ec329..0000000000000 --- a/x-pack/test/api_integration/apis/uptime/graphql/fixtures/monitor_states_page_5.json +++ /dev/null @@ -1,1609 +0,0 @@ -{ - "monitorStates": { - "prevPagePagination": "{\"cursorKey\":{\"monitor_id\":\"0040-down\"},\"sortOrder\":\"ASC\",\"cursorDirection\":\"BEFORE\"}", - "nextPagePagination": "{\"cursorDirection\":\"AFTER\",\"sortOrder\":\"ASC\",\"cursorKey\":{\"monitor_id\":\"0049-up\"}}", - "totalSummaryCount": 2000, - "summaries": [ - { - "monitor_id": "0040-down", - "histogram": { - "count": 20, - "points": [ - { - "timestamp": 1568172664000, - "up": 0, - "down": 1 - }, - { - "timestamp": 1568172694000, - "up": 0, - "down": 1 - }, - { - "timestamp": 1568172724000, - "up": 0, - "down": 1 - }, - { - "timestamp": 1568172754000, - "up": 0, - "down": 1 - }, - { - "timestamp": 1568172784000, - "up": 0, - "down": 1 - }, - { - "timestamp": 1568172814000, - "up": 0, - "down": 1 - }, - { - "timestamp": 1568172844000, - "up": 0, - "down": 1 - }, - { - "timestamp": 1568172874000, - "up": 0, - "down": 1 - }, - { - "timestamp": 1568172904000, - "up": 0, - "down": 1 - }, - { - "timestamp": 1568172934000, - "up": 0, - "down": 1 - }, - { - "timestamp": 1568172964000, - "up": 0, - "down": 1 - }, - { - "timestamp": 1568172994000, - "up": 0, - "down": 1 - }, - { - "timestamp": 1568173024000, - "up": 0, - "down": 1 - }, - { - "timestamp": 1568173054000, - "up": 0, - "down": 1 - }, - { - "timestamp": 1568173084000, - "up": 0, - "down": 1 - }, - { - "timestamp": 1568173114000, - "up": 0, - "down": 1 - }, - { - "timestamp": 1568173144000, - "up": 0, - "down": 1 - }, - { - "timestamp": 1568173174000, - "up": 0, - "down": 1 - }, - { - "timestamp": 1568173204000, - "up": 0, - "down": 1 - }, - { - "timestamp": 1568173234000, - "up": 0, - "down": 1 - } - ] - }, - "state": { - "agent": null, - "checks": [ - { - "agent": { - "id": "04e1d082-65bc-4929-8d65-d0768a2621c4" - }, - "container": null, - "kubernetes": null, - "monitor": { - "ip": "127.0.0.1", - "name": "", - "status": "down" - }, - "observer": { - "geo": { - "name": "mpls", - "location": { - "lat": 37.926867976784706, - "lon": -78.02490200847387 - } - } - }, - "timestamp": "1568173234372" - } - ], - "geo": null, - "observer": { - "geo": { - "name": [ - "mpls" - ], - "location": null - } - }, - "monitor": { - "id": null, - "name": null, - "status": "down", - "type": null - }, - "summary": { - "up": 0, - "down": 1, - "geo": null - }, - "url": { - "full": "http://localhost:5678/pattern?r=400x1", - "domain": "localhost" - }, - "timestamp": 1568173234372 - } - }, - { - "monitor_id": "0041-up", - "histogram": { - "count": 20, - "points": [ - { - "timestamp": 1568172664000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172694000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172724000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172754000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172784000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172814000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172844000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172874000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172904000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172934000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172964000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172994000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173024000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173054000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173084000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173114000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173144000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173174000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173204000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173234000, - "up": 1, - "down": 0 - } - ] - }, - "state": { - "agent": null, - "checks": [ - { - "agent": { - "id": "04e1d082-65bc-4929-8d65-d0768a2621c4" - }, - "container": null, - "kubernetes": null, - "monitor": { - "ip": "127.0.0.1", - "name": "", - "status": "up" - }, - "observer": { - "geo": { - "name": "mpls", - "location": { - "lat": 37.926867976784706, - "lon": -78.02490200847387 - } - } - }, - "timestamp": "1568173234373" - } - ], - "geo": null, - "observer": { - "geo": { - "name": [ - "mpls" - ], - "location": null - } - }, - "monitor": { - "id": null, - "name": null, - "status": "up", - "type": null - }, - "summary": { - "up": 1, - "down": 0, - "geo": null - }, - "url": { - "full": "http://localhost:5678/pattern?r=200x1", - "domain": "localhost" - }, - "timestamp": 1568173234373 - } - }, - { - "monitor_id": "0042-up", - "histogram": { - "count": 20, - "points": [ - { - "timestamp": 1568172664000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172694000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172724000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172754000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172784000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172814000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172844000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172874000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172904000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172934000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172964000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172994000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173024000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173054000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173084000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173114000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173144000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173174000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173204000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173234000, - "up": 1, - "down": 0 - } - ] - }, - "state": { - "agent": null, - "checks": [ - { - "agent": { - "id": "04e1d082-65bc-4929-8d65-d0768a2621c4" - }, - "container": null, - "kubernetes": null, - "monitor": { - "ip": "127.0.0.1", - "name": "", - "status": "up" - }, - "observer": { - "geo": { - "name": "mpls", - "location": { - "lat": 37.926867976784706, - "lon": -78.02490200847387 - } - } - }, - "timestamp": "1568173234373" - } - ], - "geo": null, - "observer": { - "geo": { - "name": [ - "mpls" - ], - "location": null - } - }, - "monitor": { - "id": null, - "name": null, - "status": "up", - "type": null - }, - "summary": { - "up": 1, - "down": 0, - "geo": null - }, - "url": { - "full": "http://localhost:5678/pattern?r=200x1", - "domain": "localhost" - }, - "timestamp": 1568173234373 - } - }, - { - "monitor_id": "0043-up", - "histogram": { - "count": 20, - "points": [ - { - "timestamp": 1568172664000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172694000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172724000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172754000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172784000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172814000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172844000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172874000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172904000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172934000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172964000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172994000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173024000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173054000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173084000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173114000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173144000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173174000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173204000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173234000, - "up": 1, - "down": 0 - } - ] - }, - "state": { - "agent": null, - "checks": [ - { - "agent": { - "id": "04e1d082-65bc-4929-8d65-d0768a2621c4" - }, - "container": null, - "kubernetes": null, - "monitor": { - "ip": "127.0.0.1", - "name": "", - "status": "up" - }, - "observer": { - "geo": { - "name": "mpls", - "location": { - "lat": 37.926867976784706, - "lon": -78.02490200847387 - } - } - }, - "timestamp": "1568173234373" - } - ], - "geo": null, - "observer": { - "geo": { - "name": [ - "mpls" - ], - "location": null - } - }, - "monitor": { - "id": null, - "name": null, - "status": "up", - "type": null - }, - "summary": { - "up": 1, - "down": 0, - "geo": null - }, - "url": { - "full": "http://localhost:5678/pattern?r=200x1", - "domain": "localhost" - }, - "timestamp": 1568173234373 - } - }, - { - "monitor_id": "0044-up", - "histogram": { - "count": 20, - "points": [ - { - "timestamp": 1568172664000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172694000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172724000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172754000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172784000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172814000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172844000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172874000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172904000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172934000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172964000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172994000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173024000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173054000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173084000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173114000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173144000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173174000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173204000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173234000, - "up": 1, - "down": 0 - } - ] - }, - "state": { - "agent": null, - "checks": [ - { - "agent": { - "id": "04e1d082-65bc-4929-8d65-d0768a2621c4" - }, - "container": null, - "kubernetes": null, - "monitor": { - "ip": "127.0.0.1", - "name": "", - "status": "up" - }, - "observer": { - "geo": { - "name": "mpls", - "location": { - "lat": 37.926867976784706, - "lon": -78.02490200847387 - } - } - }, - "timestamp": "1568173234373" - } - ], - "geo": null, - "observer": { - "geo": { - "name": [ - "mpls" - ], - "location": null - } - }, - "monitor": { - "id": null, - "name": null, - "status": "up", - "type": null - }, - "summary": { - "up": 1, - "down": 0, - "geo": null - }, - "url": { - "full": "http://localhost:5678/pattern?r=200x1", - "domain": "localhost" - }, - "timestamp": 1568173234373 - } - }, - { - "monitor_id": "0045-intermittent", - "histogram": { - "count": 20, - "points": [ - { - "timestamp": 1568172664000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172694000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172724000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172754000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172784000, - "up": 0, - "down": 1 - }, - { - "timestamp": 1568172814000, - "up": 0, - "down": 1 - }, - { - "timestamp": 1568172844000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172874000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172904000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172934000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172964000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172994000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173024000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173054000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173084000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173114000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173144000, - "up": 0, - "down": 1 - }, - { - "timestamp": 1568173174000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173204000, - "up": 0, - "down": 1 - }, - { - "timestamp": 1568173234000, - "up": 1, - "down": 0 - } - ] - }, - "state": { - "agent": null, - "checks": [ - { - "agent": { - "id": "04e1d082-65bc-4929-8d65-d0768a2621c4" - }, - "container": null, - "kubernetes": null, - "monitor": { - "ip": "127.0.0.1", - "name": "", - "status": "up" - }, - "observer": { - "geo": { - "name": "mpls", - "location": { - "lat": 37.926867976784706, - "lon": -78.02490200847387 - } - } - }, - "timestamp": "1568173234373" - } - ], - "geo": null, - "observer": { - "geo": { - "name": [ - "mpls" - ], - "location": null - } - }, - "monitor": { - "id": null, - "name": null, - "status": "up", - "type": null - }, - "summary": { - "up": 1, - "down": 0, - "geo": null - }, - "url": { - "full": "http://localhost:5678/pattern?r=200x5,500x1", - "domain": "localhost" - }, - "timestamp": 1568173234373 - } - }, - { - "monitor_id": "0046-up", - "histogram": { - "count": 20, - "points": [ - { - "timestamp": 1568172664000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172694000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172724000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172754000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172784000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172814000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172844000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172874000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172904000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172934000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172964000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172994000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173024000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173054000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173084000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173114000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173144000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173174000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173204000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173234000, - "up": 1, - "down": 0 - } - ] - }, - "state": { - "agent": null, - "checks": [ - { - "agent": { - "id": "04e1d082-65bc-4929-8d65-d0768a2621c4" - }, - "container": null, - "kubernetes": null, - "monitor": { - "ip": "127.0.0.1", - "name": "", - "status": "up" - }, - "observer": { - "geo": { - "name": "mpls", - "location": { - "lat": 37.926867976784706, - "lon": -78.02490200847387 - } - } - }, - "timestamp": "1568173234374" - } - ], - "geo": null, - "observer": { - "geo": { - "name": [ - "mpls" - ], - "location": null - } - }, - "monitor": { - "id": null, - "name": null, - "status": "up", - "type": null - }, - "summary": { - "up": 1, - "down": 0, - "geo": null - }, - "url": { - "full": "http://localhost:5678/pattern?r=200x1", - "domain": "localhost" - }, - "timestamp": 1568173234374 - } - }, - { - "monitor_id": "0047-up", - "histogram": { - "count": 20, - "points": [ - { - "timestamp": 1568172664000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172694000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172724000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172754000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172784000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172814000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172844000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172874000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172904000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172934000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172964000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172994000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173024000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173054000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173084000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173114000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173144000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173174000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173204000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173234000, - "up": 1, - "down": 0 - } - ] - }, - "state": { - "agent": null, - "checks": [ - { - "agent": { - "id": "04e1d082-65bc-4929-8d65-d0768a2621c4" - }, - "container": null, - "kubernetes": null, - "monitor": { - "ip": "127.0.0.1", - "name": "", - "status": "up" - }, - "observer": { - "geo": { - "name": "mpls", - "location": { - "lat": 37.926867976784706, - "lon": -78.02490200847387 - } - } - }, - "timestamp": "1568173234390" - } - ], - "geo": null, - "observer": { - "geo": { - "name": [ - "mpls" - ], - "location": null - } - }, - "monitor": { - "id": null, - "name": null, - "status": "up", - "type": null - }, - "summary": { - "up": 1, - "down": 0, - "geo": null - }, - "url": { - "full": "http://localhost:5678/pattern?r=200x1", - "domain": "localhost" - }, - "timestamp": 1568173234390 - } - }, - { - "monitor_id": "0048-up", - "histogram": { - "count": 20, - "points": [ - { - "timestamp": 1568172664000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172694000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172724000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172754000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172784000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172814000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172844000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172874000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172904000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172934000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172964000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172994000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173024000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173054000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173084000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173114000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173144000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173174000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173204000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173234000, - "up": 1, - "down": 0 - } - ] - }, - "state": { - "agent": null, - "checks": [ - { - "agent": { - "id": "04e1d082-65bc-4929-8d65-d0768a2621c4" - }, - "container": null, - "kubernetes": null, - "monitor": { - "ip": "127.0.0.1", - "name": "", - "status": "up" - }, - "observer": { - "geo": { - "name": "mpls", - "location": { - "lat": 37.926867976784706, - "lon": -78.02490200847387 - } - } - }, - "timestamp": "1568173234386" - } - ], - "geo": null, - "observer": { - "geo": { - "name": [ - "mpls" - ], - "location": null - } - }, - "monitor": { - "id": null, - "name": null, - "status": "up", - "type": null - }, - "summary": { - "up": 1, - "down": 0, - "geo": null - }, - "url": { - "full": "http://localhost:5678/pattern?r=200x1", - "domain": "localhost" - }, - "timestamp": 1568173234386 - } - }, - { - "monitor_id": "0049-up", - "histogram": { - "count": 20, - "points": [ - { - "timestamp": 1568172664000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172694000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172724000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172754000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172784000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172814000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172844000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172874000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172904000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172934000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172964000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172994000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173024000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173054000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173084000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173114000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173144000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173174000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173204000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173234000, - "up": 1, - "down": 0 - } - ] - }, - "state": { - "agent": null, - "checks": [ - { - "agent": { - "id": "04e1d082-65bc-4929-8d65-d0768a2621c4" - }, - "container": null, - "kubernetes": null, - "monitor": { - "ip": "127.0.0.1", - "name": "", - "status": "up" - }, - "observer": { - "geo": { - "name": "mpls", - "location": { - "lat": 37.926867976784706, - "lon": -78.02490200847387 - } - } - }, - "timestamp": "1568173234405" - } - ], - "geo": null, - "observer": { - "geo": { - "name": [ - "mpls" - ], - "location": null - } - }, - "monitor": { - "id": null, - "name": null, - "status": "up", - "type": null - }, - "summary": { - "up": 1, - "down": 0, - "geo": null - }, - "url": { - "full": "http://localhost:5678/pattern?r=200x1", - "domain": "localhost" - }, - "timestamp": 1568173234405 - } - } - ] - } -} \ No newline at end of file diff --git a/x-pack/test/api_integration/apis/uptime/graphql/fixtures/monitor_states_page_5_previous.json b/x-pack/test/api_integration/apis/uptime/graphql/fixtures/monitor_states_page_5_previous.json deleted file mode 100644 index 26cfa7c7162e8..0000000000000 --- a/x-pack/test/api_integration/apis/uptime/graphql/fixtures/monitor_states_page_5_previous.json +++ /dev/null @@ -1,1609 +0,0 @@ -{ - "monitorStates": { - "prevPagePagination": "{\"cursorKey\":{\"monitor_id\":\"0030-intermittent\"},\"sortOrder\":\"ASC\",\"cursorDirection\":\"BEFORE\"}", - "nextPagePagination": "{\"cursorKey\":{\"monitor_id\":\"0039-up\"},\"sortOrder\":\"ASC\",\"cursorDirection\":\"AFTER\"}", - "totalSummaryCount": 2000, - "summaries": [ - { - "monitor_id": "0030-intermittent", - "histogram": { - "count": 20, - "points": [ - { - "timestamp": 1568172664000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172694000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172724000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172754000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172784000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172814000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172844000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172874000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172904000, - "up": 0, - "down": 1 - }, - { - "timestamp": 1568172934000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172964000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172994000, - "up": 0, - "down": 1 - }, - { - "timestamp": 1568173024000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173054000, - "up": 0, - "down": 1 - }, - { - "timestamp": 1568173084000, - "up": 0, - "down": 1 - }, - { - "timestamp": 1568173114000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173144000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173174000, - "up": 0, - "down": 1 - }, - { - "timestamp": 1568173204000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173234000, - "up": 0, - "down": 1 - } - ] - }, - "state": { - "agent": null, - "checks": [ - { - "agent": { - "id": "04e1d082-65bc-4929-8d65-d0768a2621c4" - }, - "container": null, - "kubernetes": null, - "monitor": { - "ip": "127.0.0.1", - "name": "", - "status": "down" - }, - "observer": { - "geo": { - "name": "mpls", - "location": { - "lat": 37.926867976784706, - "lon": -78.02490200847387 - } - } - }, - "timestamp": "1568173234373" - } - ], - "geo": null, - "observer": { - "geo": { - "name": [ - "mpls" - ], - "location": null - } - }, - "monitor": { - "id": null, - "name": null, - "status": "down", - "type": null - }, - "summary": { - "up": 0, - "down": 1, - "geo": null - }, - "url": { - "full": "http://localhost:5678/pattern?r=200x5,500x1", - "domain": "localhost" - }, - "timestamp": 1568173234373 - } - }, - { - "monitor_id": "0031-up", - "histogram": { - "count": 20, - "points": [ - { - "timestamp": 1568172664000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172694000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172724000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172754000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172784000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172814000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172844000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172874000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172904000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172934000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172964000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172994000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173024000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173054000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173084000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173114000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173144000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173174000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173204000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173234000, - "up": 1, - "down": 0 - } - ] - }, - "state": { - "agent": null, - "checks": [ - { - "agent": { - "id": "04e1d082-65bc-4929-8d65-d0768a2621c4" - }, - "container": null, - "kubernetes": null, - "monitor": { - "ip": "127.0.0.1", - "name": "", - "status": "up" - }, - "observer": { - "geo": { - "name": "mpls", - "location": { - "lat": 37.926867976784706, - "lon": -78.02490200847387 - } - } - }, - "timestamp": "1568173234374" - } - ], - "geo": null, - "observer": { - "geo": { - "name": [ - "mpls" - ], - "location": null - } - }, - "monitor": { - "id": null, - "name": null, - "status": "up", - "type": null - }, - "summary": { - "up": 1, - "down": 0, - "geo": null - }, - "url": { - "full": "http://localhost:5678/pattern?r=200x1", - "domain": "localhost" - }, - "timestamp": 1568173234374 - } - }, - { - "monitor_id": "0032-up", - "histogram": { - "count": 20, - "points": [ - { - "timestamp": 1568172664000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172694000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172724000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172754000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172784000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172814000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172844000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172874000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172904000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172934000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172964000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172994000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173024000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173054000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173084000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173114000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173144000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173174000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173204000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173234000, - "up": 1, - "down": 0 - } - ] - }, - "state": { - "agent": null, - "checks": [ - { - "agent": { - "id": "04e1d082-65bc-4929-8d65-d0768a2621c4" - }, - "container": null, - "kubernetes": null, - "monitor": { - "ip": "127.0.0.1", - "name": "", - "status": "up" - }, - "observer": { - "geo": { - "name": "mpls", - "location": { - "lat": 37.926867976784706, - "lon": -78.02490200847387 - } - } - }, - "timestamp": "1568173234375" - } - ], - "geo": null, - "observer": { - "geo": { - "name": [ - "mpls" - ], - "location": null - } - }, - "monitor": { - "id": null, - "name": null, - "status": "up", - "type": null - }, - "summary": { - "up": 1, - "down": 0, - "geo": null - }, - "url": { - "full": "http://localhost:5678/pattern?r=200x1", - "domain": "localhost" - }, - "timestamp": 1568173234375 - } - }, - { - "monitor_id": "0033-up", - "histogram": { - "count": 20, - "points": [ - { - "timestamp": 1568172664000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172694000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172724000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172754000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172784000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172814000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172844000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172874000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172904000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172934000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172964000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172994000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173024000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173054000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173084000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173114000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173144000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173174000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173204000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173234000, - "up": 1, - "down": 0 - } - ] - }, - "state": { - "agent": null, - "checks": [ - { - "agent": { - "id": "04e1d082-65bc-4929-8d65-d0768a2621c4" - }, - "container": null, - "kubernetes": null, - "monitor": { - "ip": "127.0.0.1", - "name": "", - "status": "up" - }, - "observer": { - "geo": { - "name": "mpls", - "location": { - "lat": 37.926867976784706, - "lon": -78.02490200847387 - } - } - }, - "timestamp": "1568173234371" - } - ], - "geo": null, - "observer": { - "geo": { - "name": [ - "mpls" - ], - "location": null - } - }, - "monitor": { - "id": null, - "name": null, - "status": "up", - "type": null - }, - "summary": { - "up": 1, - "down": 0, - "geo": null - }, - "url": { - "full": "http://localhost:5678/pattern?r=200x1", - "domain": "localhost" - }, - "timestamp": 1568173234371 - } - }, - { - "monitor_id": "0034-up", - "histogram": { - "count": 20, - "points": [ - { - "timestamp": 1568172664000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172694000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172724000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172754000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172784000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172814000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172844000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172874000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172904000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172934000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172964000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172994000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173024000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173054000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173084000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173114000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173144000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173174000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173204000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173234000, - "up": 1, - "down": 0 - } - ] - }, - "state": { - "agent": null, - "checks": [ - { - "agent": { - "id": "04e1d082-65bc-4929-8d65-d0768a2621c4" - }, - "container": null, - "kubernetes": null, - "monitor": { - "ip": "127.0.0.1", - "name": "", - "status": "up" - }, - "observer": { - "geo": { - "name": "mpls", - "location": { - "lat": 37.926867976784706, - "lon": -78.02490200847387 - } - } - }, - "timestamp": "1568173234371" - } - ], - "geo": null, - "observer": { - "geo": { - "name": [ - "mpls" - ], - "location": null - } - }, - "monitor": { - "id": null, - "name": null, - "status": "up", - "type": null - }, - "summary": { - "up": 1, - "down": 0, - "geo": null - }, - "url": { - "full": "http://localhost:5678/pattern?r=200x1", - "domain": "localhost" - }, - "timestamp": 1568173234371 - } - }, - { - "monitor_id": "0035-up", - "histogram": { - "count": 20, - "points": [ - { - "timestamp": 1568172664000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172694000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172724000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172754000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172784000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172814000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172844000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172874000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172904000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172934000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172964000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172994000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173024000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173054000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173084000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173114000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173144000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173174000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173204000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173234000, - "up": 1, - "down": 0 - } - ] - }, - "state": { - "agent": null, - "checks": [ - { - "agent": { - "id": "04e1d082-65bc-4929-8d65-d0768a2621c4" - }, - "container": null, - "kubernetes": null, - "monitor": { - "ip": "127.0.0.1", - "name": "", - "status": "up" - }, - "observer": { - "geo": { - "name": "mpls", - "location": { - "lat": 37.926867976784706, - "lon": -78.02490200847387 - } - } - }, - "timestamp": "1568173234372" - } - ], - "geo": null, - "observer": { - "geo": { - "name": [ - "mpls" - ], - "location": null - } - }, - "monitor": { - "id": null, - "name": null, - "status": "up", - "type": null - }, - "summary": { - "up": 1, - "down": 0, - "geo": null - }, - "url": { - "full": "http://localhost:5678/pattern?r=200x1", - "domain": "localhost" - }, - "timestamp": 1568173234372 - } - }, - { - "monitor_id": "0036-up", - "histogram": { - "count": 20, - "points": [ - { - "timestamp": 1568172664000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172694000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172724000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172754000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172784000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172814000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172844000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172874000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172904000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172934000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172964000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172994000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173024000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173054000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173084000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173114000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173144000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173174000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173204000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173234000, - "up": 1, - "down": 0 - } - ] - }, - "state": { - "agent": null, - "checks": [ - { - "agent": { - "id": "04e1d082-65bc-4929-8d65-d0768a2621c4" - }, - "container": null, - "kubernetes": null, - "monitor": { - "ip": "127.0.0.1", - "name": "", - "status": "up" - }, - "observer": { - "geo": { - "name": "mpls", - "location": { - "lat": 37.926867976784706, - "lon": -78.02490200847387 - } - } - }, - "timestamp": "1568173234372" - } - ], - "geo": null, - "observer": { - "geo": { - "name": [ - "mpls" - ], - "location": null - } - }, - "monitor": { - "id": null, - "name": null, - "status": "up", - "type": null - }, - "summary": { - "up": 1, - "down": 0, - "geo": null - }, - "url": { - "full": "http://localhost:5678/pattern?r=200x1", - "domain": "localhost" - }, - "timestamp": 1568173234372 - } - }, - { - "monitor_id": "0037-up", - "histogram": { - "count": 20, - "points": [ - { - "timestamp": 1568172664000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172694000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172724000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172754000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172784000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172814000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172844000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172874000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172904000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172934000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172964000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172994000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173024000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173054000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173084000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173114000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173144000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173174000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173204000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173234000, - "up": 1, - "down": 0 - } - ] - }, - "state": { - "agent": null, - "checks": [ - { - "agent": { - "id": "04e1d082-65bc-4929-8d65-d0768a2621c4" - }, - "container": null, - "kubernetes": null, - "monitor": { - "ip": "127.0.0.1", - "name": "", - "status": "up" - }, - "observer": { - "geo": { - "name": "mpls", - "location": { - "lat": 37.926867976784706, - "lon": -78.02490200847387 - } - } - }, - "timestamp": "1568173234372" - } - ], - "geo": null, - "observer": { - "geo": { - "name": [ - "mpls" - ], - "location": null - } - }, - "monitor": { - "id": null, - "name": null, - "status": "up", - "type": null - }, - "summary": { - "up": 1, - "down": 0, - "geo": null - }, - "url": { - "full": "http://localhost:5678/pattern?r=200x1", - "domain": "localhost" - }, - "timestamp": 1568173234372 - } - }, - { - "monitor_id": "0038-up", - "histogram": { - "count": 20, - "points": [ - { - "timestamp": 1568172664000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172694000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172724000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172754000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172784000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172814000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172844000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172874000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172904000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172934000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172964000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172994000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173024000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173054000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173084000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173114000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173144000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173174000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173204000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173234000, - "up": 1, - "down": 0 - } - ] - }, - "state": { - "agent": null, - "checks": [ - { - "agent": { - "id": "04e1d082-65bc-4929-8d65-d0768a2621c4" - }, - "container": null, - "kubernetes": null, - "monitor": { - "ip": "127.0.0.1", - "name": "", - "status": "up" - }, - "observer": { - "geo": { - "name": "mpls", - "location": { - "lat": 37.926867976784706, - "lon": -78.02490200847387 - } - } - }, - "timestamp": "1568173234372" - } - ], - "geo": null, - "observer": { - "geo": { - "name": [ - "mpls" - ], - "location": null - } - }, - "monitor": { - "id": null, - "name": null, - "status": "up", - "type": null - }, - "summary": { - "up": 1, - "down": 0, - "geo": null - }, - "url": { - "full": "http://localhost:5678/pattern?r=200x1", - "domain": "localhost" - }, - "timestamp": 1568173234372 - } - }, - { - "monitor_id": "0039-up", - "histogram": { - "count": 20, - "points": [ - { - "timestamp": 1568172664000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172694000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172724000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172754000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172784000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172814000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172844000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172874000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172904000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172934000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172964000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172994000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173024000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173054000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173084000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173114000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173144000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173174000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173204000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173234000, - "up": 1, - "down": 0 - } - ] - }, - "state": { - "agent": null, - "checks": [ - { - "agent": { - "id": "04e1d082-65bc-4929-8d65-d0768a2621c4" - }, - "container": null, - "kubernetes": null, - "monitor": { - "ip": "127.0.0.1", - "name": "", - "status": "up" - }, - "observer": { - "geo": { - "name": "mpls", - "location": { - "lat": 37.926867976784706, - "lon": -78.02490200847387 - } - } - }, - "timestamp": "1568173234372" - } - ], - "geo": null, - "observer": { - "geo": { - "name": [ - "mpls" - ], - "location": null - } - }, - "monitor": { - "id": null, - "name": null, - "status": "up", - "type": null - }, - "summary": { - "up": 1, - "down": 0, - "geo": null - }, - "url": { - "full": "http://localhost:5678/pattern?r=200x1", - "domain": "localhost" - }, - "timestamp": 1568173234372 - } - } - ] - } -} \ No newline at end of file diff --git a/x-pack/test/api_integration/apis/uptime/graphql/fixtures/monitor_states_page_6.json b/x-pack/test/api_integration/apis/uptime/graphql/fixtures/monitor_states_page_6.json deleted file mode 100644 index 8f4b5d4c52e71..0000000000000 --- a/x-pack/test/api_integration/apis/uptime/graphql/fixtures/monitor_states_page_6.json +++ /dev/null @@ -1,1609 +0,0 @@ -{ - "monitorStates": { - "prevPagePagination": "{\"cursorKey\":{\"monitor_id\":\"0050-down\"},\"sortOrder\":\"ASC\",\"cursorDirection\":\"BEFORE\"}", - "nextPagePagination": "{\"cursorDirection\":\"AFTER\",\"sortOrder\":\"ASC\",\"cursorKey\":{\"monitor_id\":\"0059-up\"}}", - "totalSummaryCount": 2000, - "summaries": [ - { - "monitor_id": "0050-down", - "histogram": { - "count": 20, - "points": [ - { - "timestamp": 1568172664000, - "up": 0, - "down": 1 - }, - { - "timestamp": 1568172694000, - "up": 0, - "down": 1 - }, - { - "timestamp": 1568172724000, - "up": 0, - "down": 1 - }, - { - "timestamp": 1568172754000, - "up": 0, - "down": 1 - }, - { - "timestamp": 1568172784000, - "up": 0, - "down": 1 - }, - { - "timestamp": 1568172814000, - "up": 0, - "down": 1 - }, - { - "timestamp": 1568172844000, - "up": 0, - "down": 1 - }, - { - "timestamp": 1568172874000, - "up": 0, - "down": 1 - }, - { - "timestamp": 1568172904000, - "up": 0, - "down": 1 - }, - { - "timestamp": 1568172934000, - "up": 0, - "down": 1 - }, - { - "timestamp": 1568172964000, - "up": 0, - "down": 1 - }, - { - "timestamp": 1568172994000, - "up": 0, - "down": 1 - }, - { - "timestamp": 1568173024000, - "up": 0, - "down": 1 - }, - { - "timestamp": 1568173054000, - "up": 0, - "down": 1 - }, - { - "timestamp": 1568173084000, - "up": 0, - "down": 1 - }, - { - "timestamp": 1568173114000, - "up": 0, - "down": 1 - }, - { - "timestamp": 1568173144000, - "up": 0, - "down": 1 - }, - { - "timestamp": 1568173174000, - "up": 0, - "down": 1 - }, - { - "timestamp": 1568173204000, - "up": 0, - "down": 1 - }, - { - "timestamp": 1568173234000, - "up": 0, - "down": 1 - } - ] - }, - "state": { - "agent": null, - "checks": [ - { - "agent": { - "id": "04e1d082-65bc-4929-8d65-d0768a2621c4" - }, - "container": null, - "kubernetes": null, - "monitor": { - "ip": "127.0.0.1", - "name": "", - "status": "down" - }, - "observer": { - "geo": { - "name": "mpls", - "location": { - "lat": 37.926867976784706, - "lon": -78.02490200847387 - } - } - }, - "timestamp": "1568173234386" - } - ], - "geo": null, - "observer": { - "geo": { - "name": [ - "mpls" - ], - "location": null - } - }, - "monitor": { - "id": null, - "name": null, - "status": "down", - "type": null - }, - "summary": { - "up": 0, - "down": 1, - "geo": null - }, - "url": { - "full": "http://localhost:5678/pattern?r=400x1", - "domain": "localhost" - }, - "timestamp": 1568173234386 - } - }, - { - "monitor_id": "0051-up", - "histogram": { - "count": 20, - "points": [ - { - "timestamp": 1568172664000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172694000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172724000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172754000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172784000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172814000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172844000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172874000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172904000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172934000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172964000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172994000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173024000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173054000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173084000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173114000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173144000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173174000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173204000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173234000, - "up": 1, - "down": 0 - } - ] - }, - "state": { - "agent": null, - "checks": [ - { - "agent": { - "id": "04e1d082-65bc-4929-8d65-d0768a2621c4" - }, - "container": null, - "kubernetes": null, - "monitor": { - "ip": "127.0.0.1", - "name": "", - "status": "up" - }, - "observer": { - "geo": { - "name": "mpls", - "location": { - "lat": 37.926867976784706, - "lon": -78.02490200847387 - } - } - }, - "timestamp": "1568173234371" - } - ], - "geo": null, - "observer": { - "geo": { - "name": [ - "mpls" - ], - "location": null - } - }, - "monitor": { - "id": null, - "name": null, - "status": "up", - "type": null - }, - "summary": { - "up": 1, - "down": 0, - "geo": null - }, - "url": { - "full": "http://localhost:5678/pattern?r=200x1", - "domain": "localhost" - }, - "timestamp": 1568173234371 - } - }, - { - "monitor_id": "0052-up", - "histogram": { - "count": 20, - "points": [ - { - "timestamp": 1568172664000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172694000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172724000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172754000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172784000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172814000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172844000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172874000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172904000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172934000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172964000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172994000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173024000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173054000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173084000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173114000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173144000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173174000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173204000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173234000, - "up": 1, - "down": 0 - } - ] - }, - "state": { - "agent": null, - "checks": [ - { - "agent": { - "id": "04e1d082-65bc-4929-8d65-d0768a2621c4" - }, - "container": null, - "kubernetes": null, - "monitor": { - "ip": "127.0.0.1", - "name": "", - "status": "up" - }, - "observer": { - "geo": { - "name": "mpls", - "location": { - "lat": 37.926867976784706, - "lon": -78.02490200847387 - } - } - }, - "timestamp": "1568173234371" - } - ], - "geo": null, - "observer": { - "geo": { - "name": [ - "mpls" - ], - "location": null - } - }, - "monitor": { - "id": null, - "name": null, - "status": "up", - "type": null - }, - "summary": { - "up": 1, - "down": 0, - "geo": null - }, - "url": { - "full": "http://localhost:5678/pattern?r=200x1", - "domain": "localhost" - }, - "timestamp": 1568173234371 - } - }, - { - "monitor_id": "0053-up", - "histogram": { - "count": 20, - "points": [ - { - "timestamp": 1568172664000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172694000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172724000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172754000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172784000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172814000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172844000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172874000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172904000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172934000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172964000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172994000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173024000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173054000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173084000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173114000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173144000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173174000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173204000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173234000, - "up": 1, - "down": 0 - } - ] - }, - "state": { - "agent": null, - "checks": [ - { - "agent": { - "id": "04e1d082-65bc-4929-8d65-d0768a2621c4" - }, - "container": null, - "kubernetes": null, - "monitor": { - "ip": "127.0.0.1", - "name": "", - "status": "up" - }, - "observer": { - "geo": { - "name": "mpls", - "location": { - "lat": 37.926867976784706, - "lon": -78.02490200847387 - } - } - }, - "timestamp": "1568173234371" - } - ], - "geo": null, - "observer": { - "geo": { - "name": [ - "mpls" - ], - "location": null - } - }, - "monitor": { - "id": null, - "name": null, - "status": "up", - "type": null - }, - "summary": { - "up": 1, - "down": 0, - "geo": null - }, - "url": { - "full": "http://localhost:5678/pattern?r=200x1", - "domain": "localhost" - }, - "timestamp": 1568173234371 - } - }, - { - "monitor_id": "0054-up", - "histogram": { - "count": 20, - "points": [ - { - "timestamp": 1568172664000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172694000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172724000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172754000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172784000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172814000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172844000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172874000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172904000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172934000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172964000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172994000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173024000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173054000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173084000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173114000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173144000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173174000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173204000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173234000, - "up": 1, - "down": 0 - } - ] - }, - "state": { - "agent": null, - "checks": [ - { - "agent": { - "id": "04e1d082-65bc-4929-8d65-d0768a2621c4" - }, - "container": null, - "kubernetes": null, - "monitor": { - "ip": "127.0.0.1", - "name": "", - "status": "up" - }, - "observer": { - "geo": { - "name": "mpls", - "location": { - "lat": 37.926867976784706, - "lon": -78.02490200847387 - } - } - }, - "timestamp": "1568173234371" - } - ], - "geo": null, - "observer": { - "geo": { - "name": [ - "mpls" - ], - "location": null - } - }, - "monitor": { - "id": null, - "name": null, - "status": "up", - "type": null - }, - "summary": { - "up": 1, - "down": 0, - "geo": null - }, - "url": { - "full": "http://localhost:5678/pattern?r=200x1", - "domain": "localhost" - }, - "timestamp": 1568173234371 - } - }, - { - "monitor_id": "0055-up", - "histogram": { - "count": 20, - "points": [ - { - "timestamp": 1568172664000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172694000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172724000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172754000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172784000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172814000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172844000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172874000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172904000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172934000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172964000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172994000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173024000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173054000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173084000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173114000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173144000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173174000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173204000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173234000, - "up": 1, - "down": 0 - } - ] - }, - "state": { - "agent": null, - "checks": [ - { - "agent": { - "id": "04e1d082-65bc-4929-8d65-d0768a2621c4" - }, - "container": null, - "kubernetes": null, - "monitor": { - "ip": "127.0.0.1", - "name": "", - "status": "up" - }, - "observer": { - "geo": { - "name": "mpls", - "location": { - "lat": 37.926867976784706, - "lon": -78.02490200847387 - } - } - }, - "timestamp": "1568173234371" - } - ], - "geo": null, - "observer": { - "geo": { - "name": [ - "mpls" - ], - "location": null - } - }, - "monitor": { - "id": null, - "name": null, - "status": "up", - "type": null - }, - "summary": { - "up": 1, - "down": 0, - "geo": null - }, - "url": { - "full": "http://localhost:5678/pattern?r=200x1", - "domain": "localhost" - }, - "timestamp": 1568173234371 - } - }, - { - "monitor_id": "0056-up", - "histogram": { - "count": 20, - "points": [ - { - "timestamp": 1568172664000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172694000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172724000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172754000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172784000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172814000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172844000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172874000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172904000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172934000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172964000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172994000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173024000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173054000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173084000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173114000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173144000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173174000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173204000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173234000, - "up": 1, - "down": 0 - } - ] - }, - "state": { - "agent": null, - "checks": [ - { - "agent": { - "id": "04e1d082-65bc-4929-8d65-d0768a2621c4" - }, - "container": null, - "kubernetes": null, - "monitor": { - "ip": "127.0.0.1", - "name": "", - "status": "up" - }, - "observer": { - "geo": { - "name": "mpls", - "location": { - "lat": 37.926867976784706, - "lon": -78.02490200847387 - } - } - }, - "timestamp": "1568173234371" - } - ], - "geo": null, - "observer": { - "geo": { - "name": [ - "mpls" - ], - "location": null - } - }, - "monitor": { - "id": null, - "name": null, - "status": "up", - "type": null - }, - "summary": { - "up": 1, - "down": 0, - "geo": null - }, - "url": { - "full": "http://localhost:5678/pattern?r=200x1", - "domain": "localhost" - }, - "timestamp": 1568173234371 - } - }, - { - "monitor_id": "0057-up", - "histogram": { - "count": 20, - "points": [ - { - "timestamp": 1568172664000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172694000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172724000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172754000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172784000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172814000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172844000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172874000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172904000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172934000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172964000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172994000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173024000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173054000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173084000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173114000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173144000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173174000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173204000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173234000, - "up": 1, - "down": 0 - } - ] - }, - "state": { - "agent": null, - "checks": [ - { - "agent": { - "id": "04e1d082-65bc-4929-8d65-d0768a2621c4" - }, - "container": null, - "kubernetes": null, - "monitor": { - "ip": "127.0.0.1", - "name": "", - "status": "up" - }, - "observer": { - "geo": { - "name": "mpls", - "location": { - "lat": 37.926867976784706, - "lon": -78.02490200847387 - } - } - }, - "timestamp": "1568173234372" - } - ], - "geo": null, - "observer": { - "geo": { - "name": [ - "mpls" - ], - "location": null - } - }, - "monitor": { - "id": null, - "name": null, - "status": "up", - "type": null - }, - "summary": { - "up": 1, - "down": 0, - "geo": null - }, - "url": { - "full": "http://localhost:5678/pattern?r=200x1", - "domain": "localhost" - }, - "timestamp": 1568173234372 - } - }, - { - "monitor_id": "0058-up", - "histogram": { - "count": 20, - "points": [ - { - "timestamp": 1568172664000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172694000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172724000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172754000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172784000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172814000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172844000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172874000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172904000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172934000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172964000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172994000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173024000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173054000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173084000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173114000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173144000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173174000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173204000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173234000, - "up": 1, - "down": 0 - } - ] - }, - "state": { - "agent": null, - "checks": [ - { - "agent": { - "id": "04e1d082-65bc-4929-8d65-d0768a2621c4" - }, - "container": null, - "kubernetes": null, - "monitor": { - "ip": "127.0.0.1", - "name": "", - "status": "up" - }, - "observer": { - "geo": { - "name": "mpls", - "location": { - "lat": 37.926867976784706, - "lon": -78.02490200847387 - } - } - }, - "timestamp": "1568173234372" - } - ], - "geo": null, - "observer": { - "geo": { - "name": [ - "mpls" - ], - "location": null - } - }, - "monitor": { - "id": null, - "name": null, - "status": "up", - "type": null - }, - "summary": { - "up": 1, - "down": 0, - "geo": null - }, - "url": { - "full": "http://localhost:5678/pattern?r=200x1", - "domain": "localhost" - }, - "timestamp": 1568173234372 - } - }, - { - "monitor_id": "0059-up", - "histogram": { - "count": 20, - "points": [ - { - "timestamp": 1568172664000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172694000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172724000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172754000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172784000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172814000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172844000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172874000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172904000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172934000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172964000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172994000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173024000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173054000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173084000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173114000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173144000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173174000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173204000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173234000, - "up": 1, - "down": 0 - } - ] - }, - "state": { - "agent": null, - "checks": [ - { - "agent": { - "id": "04e1d082-65bc-4929-8d65-d0768a2621c4" - }, - "container": null, - "kubernetes": null, - "monitor": { - "ip": "127.0.0.1", - "name": "", - "status": "up" - }, - "observer": { - "geo": { - "name": "mpls", - "location": { - "lat": 37.926867976784706, - "lon": -78.02490200847387 - } - } - }, - "timestamp": "1568173234372" - } - ], - "geo": null, - "observer": { - "geo": { - "name": [ - "mpls" - ], - "location": null - } - }, - "monitor": { - "id": null, - "name": null, - "status": "up", - "type": null - }, - "summary": { - "up": 1, - "down": 0, - "geo": null - }, - "url": { - "full": "http://localhost:5678/pattern?r=200x1", - "domain": "localhost" - }, - "timestamp": 1568173234372 - } - } - ] - } -} \ No newline at end of file diff --git a/x-pack/test/api_integration/apis/uptime/graphql/fixtures/monitor_states_page_6_previous.json b/x-pack/test/api_integration/apis/uptime/graphql/fixtures/monitor_states_page_6_previous.json deleted file mode 100644 index 50f8f61b13d68..0000000000000 --- a/x-pack/test/api_integration/apis/uptime/graphql/fixtures/monitor_states_page_6_previous.json +++ /dev/null @@ -1,1609 +0,0 @@ -{ - "monitorStates": { - "prevPagePagination": "{\"cursorKey\":{\"monitor_id\":\"0040-down\"},\"sortOrder\":\"ASC\",\"cursorDirection\":\"BEFORE\"}", - "nextPagePagination": "{\"cursorKey\":{\"monitor_id\":\"0049-up\"},\"sortOrder\":\"ASC\",\"cursorDirection\":\"AFTER\"}", - "totalSummaryCount": 2000, - "summaries": [ - { - "monitor_id": "0040-down", - "histogram": { - "count": 20, - "points": [ - { - "timestamp": 1568172664000, - "up": 0, - "down": 1 - }, - { - "timestamp": 1568172694000, - "up": 0, - "down": 1 - }, - { - "timestamp": 1568172724000, - "up": 0, - "down": 1 - }, - { - "timestamp": 1568172754000, - "up": 0, - "down": 1 - }, - { - "timestamp": 1568172784000, - "up": 0, - "down": 1 - }, - { - "timestamp": 1568172814000, - "up": 0, - "down": 1 - }, - { - "timestamp": 1568172844000, - "up": 0, - "down": 1 - }, - { - "timestamp": 1568172874000, - "up": 0, - "down": 1 - }, - { - "timestamp": 1568172904000, - "up": 0, - "down": 1 - }, - { - "timestamp": 1568172934000, - "up": 0, - "down": 1 - }, - { - "timestamp": 1568172964000, - "up": 0, - "down": 1 - }, - { - "timestamp": 1568172994000, - "up": 0, - "down": 1 - }, - { - "timestamp": 1568173024000, - "up": 0, - "down": 1 - }, - { - "timestamp": 1568173054000, - "up": 0, - "down": 1 - }, - { - "timestamp": 1568173084000, - "up": 0, - "down": 1 - }, - { - "timestamp": 1568173114000, - "up": 0, - "down": 1 - }, - { - "timestamp": 1568173144000, - "up": 0, - "down": 1 - }, - { - "timestamp": 1568173174000, - "up": 0, - "down": 1 - }, - { - "timestamp": 1568173204000, - "up": 0, - "down": 1 - }, - { - "timestamp": 1568173234000, - "up": 0, - "down": 1 - } - ] - }, - "state": { - "agent": null, - "checks": [ - { - "agent": { - "id": "04e1d082-65bc-4929-8d65-d0768a2621c4" - }, - "container": null, - "kubernetes": null, - "monitor": { - "ip": "127.0.0.1", - "name": "", - "status": "down" - }, - "observer": { - "geo": { - "name": "mpls", - "location": { - "lat": 37.926867976784706, - "lon": -78.02490200847387 - } - } - }, - "timestamp": "1568173234372" - } - ], - "geo": null, - "observer": { - "geo": { - "name": [ - "mpls" - ], - "location": null - } - }, - "monitor": { - "id": null, - "name": null, - "status": "down", - "type": null - }, - "summary": { - "up": 0, - "down": 1, - "geo": null - }, - "url": { - "full": "http://localhost:5678/pattern?r=400x1", - "domain": "localhost" - }, - "timestamp": 1568173234372 - } - }, - { - "monitor_id": "0041-up", - "histogram": { - "count": 20, - "points": [ - { - "timestamp": 1568172664000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172694000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172724000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172754000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172784000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172814000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172844000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172874000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172904000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172934000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172964000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172994000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173024000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173054000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173084000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173114000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173144000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173174000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173204000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173234000, - "up": 1, - "down": 0 - } - ] - }, - "state": { - "agent": null, - "checks": [ - { - "agent": { - "id": "04e1d082-65bc-4929-8d65-d0768a2621c4" - }, - "container": null, - "kubernetes": null, - "monitor": { - "ip": "127.0.0.1", - "name": "", - "status": "up" - }, - "observer": { - "geo": { - "name": "mpls", - "location": { - "lat": 37.926867976784706, - "lon": -78.02490200847387 - } - } - }, - "timestamp": "1568173234373" - } - ], - "geo": null, - "observer": { - "geo": { - "name": [ - "mpls" - ], - "location": null - } - }, - "monitor": { - "id": null, - "name": null, - "status": "up", - "type": null - }, - "summary": { - "up": 1, - "down": 0, - "geo": null - }, - "url": { - "full": "http://localhost:5678/pattern?r=200x1", - "domain": "localhost" - }, - "timestamp": 1568173234373 - } - }, - { - "monitor_id": "0042-up", - "histogram": { - "count": 20, - "points": [ - { - "timestamp": 1568172664000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172694000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172724000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172754000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172784000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172814000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172844000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172874000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172904000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172934000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172964000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172994000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173024000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173054000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173084000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173114000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173144000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173174000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173204000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173234000, - "up": 1, - "down": 0 - } - ] - }, - "state": { - "agent": null, - "checks": [ - { - "agent": { - "id": "04e1d082-65bc-4929-8d65-d0768a2621c4" - }, - "container": null, - "kubernetes": null, - "monitor": { - "ip": "127.0.0.1", - "name": "", - "status": "up" - }, - "observer": { - "geo": { - "name": "mpls", - "location": { - "lat": 37.926867976784706, - "lon": -78.02490200847387 - } - } - }, - "timestamp": "1568173234373" - } - ], - "geo": null, - "observer": { - "geo": { - "name": [ - "mpls" - ], - "location": null - } - }, - "monitor": { - "id": null, - "name": null, - "status": "up", - "type": null - }, - "summary": { - "up": 1, - "down": 0, - "geo": null - }, - "url": { - "full": "http://localhost:5678/pattern?r=200x1", - "domain": "localhost" - }, - "timestamp": 1568173234373 - } - }, - { - "monitor_id": "0043-up", - "histogram": { - "count": 20, - "points": [ - { - "timestamp": 1568172664000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172694000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172724000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172754000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172784000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172814000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172844000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172874000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172904000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172934000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172964000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172994000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173024000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173054000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173084000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173114000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173144000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173174000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173204000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173234000, - "up": 1, - "down": 0 - } - ] - }, - "state": { - "agent": null, - "checks": [ - { - "agent": { - "id": "04e1d082-65bc-4929-8d65-d0768a2621c4" - }, - "container": null, - "kubernetes": null, - "monitor": { - "ip": "127.0.0.1", - "name": "", - "status": "up" - }, - "observer": { - "geo": { - "name": "mpls", - "location": { - "lat": 37.926867976784706, - "lon": -78.02490200847387 - } - } - }, - "timestamp": "1568173234373" - } - ], - "geo": null, - "observer": { - "geo": { - "name": [ - "mpls" - ], - "location": null - } - }, - "monitor": { - "id": null, - "name": null, - "status": "up", - "type": null - }, - "summary": { - "up": 1, - "down": 0, - "geo": null - }, - "url": { - "full": "http://localhost:5678/pattern?r=200x1", - "domain": "localhost" - }, - "timestamp": 1568173234373 - } - }, - { - "monitor_id": "0044-up", - "histogram": { - "count": 20, - "points": [ - { - "timestamp": 1568172664000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172694000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172724000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172754000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172784000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172814000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172844000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172874000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172904000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172934000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172964000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172994000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173024000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173054000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173084000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173114000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173144000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173174000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173204000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173234000, - "up": 1, - "down": 0 - } - ] - }, - "state": { - "agent": null, - "checks": [ - { - "agent": { - "id": "04e1d082-65bc-4929-8d65-d0768a2621c4" - }, - "container": null, - "kubernetes": null, - "monitor": { - "ip": "127.0.0.1", - "name": "", - "status": "up" - }, - "observer": { - "geo": { - "name": "mpls", - "location": { - "lat": 37.926867976784706, - "lon": -78.02490200847387 - } - } - }, - "timestamp": "1568173234373" - } - ], - "geo": null, - "observer": { - "geo": { - "name": [ - "mpls" - ], - "location": null - } - }, - "monitor": { - "id": null, - "name": null, - "status": "up", - "type": null - }, - "summary": { - "up": 1, - "down": 0, - "geo": null - }, - "url": { - "full": "http://localhost:5678/pattern?r=200x1", - "domain": "localhost" - }, - "timestamp": 1568173234373 - } - }, - { - "monitor_id": "0045-intermittent", - "histogram": { - "count": 20, - "points": [ - { - "timestamp": 1568172664000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172694000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172724000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172754000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172784000, - "up": 0, - "down": 1 - }, - { - "timestamp": 1568172814000, - "up": 0, - "down": 1 - }, - { - "timestamp": 1568172844000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172874000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172904000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172934000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172964000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172994000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173024000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173054000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173084000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173114000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173144000, - "up": 0, - "down": 1 - }, - { - "timestamp": 1568173174000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173204000, - "up": 0, - "down": 1 - }, - { - "timestamp": 1568173234000, - "up": 1, - "down": 0 - } - ] - }, - "state": { - "agent": null, - "checks": [ - { - "agent": { - "id": "04e1d082-65bc-4929-8d65-d0768a2621c4" - }, - "container": null, - "kubernetes": null, - "monitor": { - "ip": "127.0.0.1", - "name": "", - "status": "up" - }, - "observer": { - "geo": { - "name": "mpls", - "location": { - "lat": 37.926867976784706, - "lon": -78.02490200847387 - } - } - }, - "timestamp": "1568173234373" - } - ], - "geo": null, - "observer": { - "geo": { - "name": [ - "mpls" - ], - "location": null - } - }, - "monitor": { - "id": null, - "name": null, - "status": "up", - "type": null - }, - "summary": { - "up": 1, - "down": 0, - "geo": null - }, - "url": { - "full": "http://localhost:5678/pattern?r=200x5,500x1", - "domain": "localhost" - }, - "timestamp": 1568173234373 - } - }, - { - "monitor_id": "0046-up", - "histogram": { - "count": 20, - "points": [ - { - "timestamp": 1568172664000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172694000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172724000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172754000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172784000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172814000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172844000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172874000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172904000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172934000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172964000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172994000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173024000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173054000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173084000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173114000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173144000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173174000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173204000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173234000, - "up": 1, - "down": 0 - } - ] - }, - "state": { - "agent": null, - "checks": [ - { - "agent": { - "id": "04e1d082-65bc-4929-8d65-d0768a2621c4" - }, - "container": null, - "kubernetes": null, - "monitor": { - "ip": "127.0.0.1", - "name": "", - "status": "up" - }, - "observer": { - "geo": { - "name": "mpls", - "location": { - "lat": 37.926867976784706, - "lon": -78.02490200847387 - } - } - }, - "timestamp": "1568173234374" - } - ], - "geo": null, - "observer": { - "geo": { - "name": [ - "mpls" - ], - "location": null - } - }, - "monitor": { - "id": null, - "name": null, - "status": "up", - "type": null - }, - "summary": { - "up": 1, - "down": 0, - "geo": null - }, - "url": { - "full": "http://localhost:5678/pattern?r=200x1", - "domain": "localhost" - }, - "timestamp": 1568173234374 - } - }, - { - "monitor_id": "0047-up", - "histogram": { - "count": 20, - "points": [ - { - "timestamp": 1568172664000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172694000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172724000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172754000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172784000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172814000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172844000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172874000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172904000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172934000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172964000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172994000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173024000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173054000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173084000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173114000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173144000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173174000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173204000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173234000, - "up": 1, - "down": 0 - } - ] - }, - "state": { - "agent": null, - "checks": [ - { - "agent": { - "id": "04e1d082-65bc-4929-8d65-d0768a2621c4" - }, - "container": null, - "kubernetes": null, - "monitor": { - "ip": "127.0.0.1", - "name": "", - "status": "up" - }, - "observer": { - "geo": { - "name": "mpls", - "location": { - "lat": 37.926867976784706, - "lon": -78.02490200847387 - } - } - }, - "timestamp": "1568173234390" - } - ], - "geo": null, - "observer": { - "geo": { - "name": [ - "mpls" - ], - "location": null - } - }, - "monitor": { - "id": null, - "name": null, - "status": "up", - "type": null - }, - "summary": { - "up": 1, - "down": 0, - "geo": null - }, - "url": { - "full": "http://localhost:5678/pattern?r=200x1", - "domain": "localhost" - }, - "timestamp": 1568173234390 - } - }, - { - "monitor_id": "0048-up", - "histogram": { - "count": 20, - "points": [ - { - "timestamp": 1568172664000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172694000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172724000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172754000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172784000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172814000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172844000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172874000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172904000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172934000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172964000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172994000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173024000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173054000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173084000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173114000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173144000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173174000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173204000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173234000, - "up": 1, - "down": 0 - } - ] - }, - "state": { - "agent": null, - "checks": [ - { - "agent": { - "id": "04e1d082-65bc-4929-8d65-d0768a2621c4" - }, - "container": null, - "kubernetes": null, - "monitor": { - "ip": "127.0.0.1", - "name": "", - "status": "up" - }, - "observer": { - "geo": { - "name": "mpls", - "location": { - "lat": 37.926867976784706, - "lon": -78.02490200847387 - } - } - }, - "timestamp": "1568173234386" - } - ], - "geo": null, - "observer": { - "geo": { - "name": [ - "mpls" - ], - "location": null - } - }, - "monitor": { - "id": null, - "name": null, - "status": "up", - "type": null - }, - "summary": { - "up": 1, - "down": 0, - "geo": null - }, - "url": { - "full": "http://localhost:5678/pattern?r=200x1", - "domain": "localhost" - }, - "timestamp": 1568173234386 - } - }, - { - "monitor_id": "0049-up", - "histogram": { - "count": 20, - "points": [ - { - "timestamp": 1568172664000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172694000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172724000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172754000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172784000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172814000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172844000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172874000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172904000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172934000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172964000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172994000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173024000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173054000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173084000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173114000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173144000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173174000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173204000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173234000, - "up": 1, - "down": 0 - } - ] - }, - "state": { - "agent": null, - "checks": [ - { - "agent": { - "id": "04e1d082-65bc-4929-8d65-d0768a2621c4" - }, - "container": null, - "kubernetes": null, - "monitor": { - "ip": "127.0.0.1", - "name": "", - "status": "up" - }, - "observer": { - "geo": { - "name": "mpls", - "location": { - "lat": 37.926867976784706, - "lon": -78.02490200847387 - } - } - }, - "timestamp": "1568173234405" - } - ], - "geo": null, - "observer": { - "geo": { - "name": [ - "mpls" - ], - "location": null - } - }, - "monitor": { - "id": null, - "name": null, - "status": "up", - "type": null - }, - "summary": { - "up": 1, - "down": 0, - "geo": null - }, - "url": { - "full": "http://localhost:5678/pattern?r=200x1", - "domain": "localhost" - }, - "timestamp": 1568173234405 - } - } - ] - } -} \ No newline at end of file diff --git a/x-pack/test/api_integration/apis/uptime/graphql/fixtures/monitor_states_page_7.json b/x-pack/test/api_integration/apis/uptime/graphql/fixtures/monitor_states_page_7.json deleted file mode 100644 index 18ab2c6fdf336..0000000000000 --- a/x-pack/test/api_integration/apis/uptime/graphql/fixtures/monitor_states_page_7.json +++ /dev/null @@ -1,1609 +0,0 @@ -{ - "monitorStates": { - "prevPagePagination": "{\"cursorKey\":{\"monitor_id\":\"0060-intermittent\"},\"sortOrder\":\"ASC\",\"cursorDirection\":\"BEFORE\"}", - "nextPagePagination": "{\"cursorDirection\":\"AFTER\",\"sortOrder\":\"ASC\",\"cursorKey\":{\"monitor_id\":\"0069-up\"}}", - "totalSummaryCount": 2000, - "summaries": [ - { - "monitor_id": "0060-intermittent", - "histogram": { - "count": 20, - "points": [ - { - "timestamp": 1568172664000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172694000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172724000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172754000, - "up": 0, - "down": 1 - }, - { - "timestamp": 1568172784000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172814000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172844000, - "up": 0, - "down": 1 - }, - { - "timestamp": 1568172874000, - "up": 0, - "down": 1 - }, - { - "timestamp": 1568172904000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172934000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172964000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172994000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173024000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173054000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173084000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173114000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173144000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173174000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173204000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173234000, - "up": 1, - "down": 0 - } - ] - }, - "state": { - "agent": null, - "checks": [ - { - "agent": { - "id": "04e1d082-65bc-4929-8d65-d0768a2621c4" - }, - "container": null, - "kubernetes": null, - "monitor": { - "ip": "127.0.0.1", - "name": "", - "status": "up" - }, - "observer": { - "geo": { - "name": "mpls", - "location": { - "lat": 37.926867976784706, - "lon": -78.02490200847387 - } - } - }, - "timestamp": "1568173234372" - } - ], - "geo": null, - "observer": { - "geo": { - "name": [ - "mpls" - ], - "location": null - } - }, - "monitor": { - "id": null, - "name": null, - "status": "up", - "type": null - }, - "summary": { - "up": 1, - "down": 0, - "geo": null - }, - "url": { - "full": "http://localhost:5678/pattern?r=200x5,500x1", - "domain": "localhost" - }, - "timestamp": 1568173234372 - } - }, - { - "monitor_id": "0061-up", - "histogram": { - "count": 20, - "points": [ - { - "timestamp": 1568172664000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172694000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172724000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172754000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172784000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172814000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172844000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172874000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172904000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172934000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172964000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172994000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173024000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173054000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173084000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173114000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173144000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173174000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173204000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173234000, - "up": 1, - "down": 0 - } - ] - }, - "state": { - "agent": null, - "checks": [ - { - "agent": { - "id": "04e1d082-65bc-4929-8d65-d0768a2621c4" - }, - "container": null, - "kubernetes": null, - "monitor": { - "ip": "127.0.0.1", - "name": "", - "status": "up" - }, - "observer": { - "geo": { - "name": "mpls", - "location": { - "lat": 37.926867976784706, - "lon": -78.02490200847387 - } - } - }, - "timestamp": "1568173234373" - } - ], - "geo": null, - "observer": { - "geo": { - "name": [ - "mpls" - ], - "location": null - } - }, - "monitor": { - "id": null, - "name": null, - "status": "up", - "type": null - }, - "summary": { - "up": 1, - "down": 0, - "geo": null - }, - "url": { - "full": "http://localhost:5678/pattern?r=200x1", - "domain": "localhost" - }, - "timestamp": 1568173234373 - } - }, - { - "monitor_id": "0062-up", - "histogram": { - "count": 20, - "points": [ - { - "timestamp": 1568172664000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172694000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172724000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172754000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172784000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172814000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172844000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172874000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172904000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172934000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172964000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172994000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173024000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173054000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173084000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173114000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173144000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173174000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173204000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173234000, - "up": 1, - "down": 0 - } - ] - }, - "state": { - "agent": null, - "checks": [ - { - "agent": { - "id": "04e1d082-65bc-4929-8d65-d0768a2621c4" - }, - "container": null, - "kubernetes": null, - "monitor": { - "ip": "127.0.0.1", - "name": "", - "status": "up" - }, - "observer": { - "geo": { - "name": "mpls", - "location": { - "lat": 37.926867976784706, - "lon": -78.02490200847387 - } - } - }, - "timestamp": "1568173234373" - } - ], - "geo": null, - "observer": { - "geo": { - "name": [ - "mpls" - ], - "location": null - } - }, - "monitor": { - "id": null, - "name": null, - "status": "up", - "type": null - }, - "summary": { - "up": 1, - "down": 0, - "geo": null - }, - "url": { - "full": "http://localhost:5678/pattern?r=200x1", - "domain": "localhost" - }, - "timestamp": 1568173234373 - } - }, - { - "monitor_id": "0063-up", - "histogram": { - "count": 20, - "points": [ - { - "timestamp": 1568172664000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172694000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172724000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172754000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172784000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172814000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172844000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172874000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172904000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172934000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172964000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172994000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173024000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173054000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173084000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173114000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173144000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173174000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173204000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173234000, - "up": 1, - "down": 0 - } - ] - }, - "state": { - "agent": null, - "checks": [ - { - "agent": { - "id": "04e1d082-65bc-4929-8d65-d0768a2621c4" - }, - "container": null, - "kubernetes": null, - "monitor": { - "ip": "127.0.0.1", - "name": "", - "status": "up" - }, - "observer": { - "geo": { - "name": "mpls", - "location": { - "lat": 37.926867976784706, - "lon": -78.02490200847387 - } - } - }, - "timestamp": "1568173234373" - } - ], - "geo": null, - "observer": { - "geo": { - "name": [ - "mpls" - ], - "location": null - } - }, - "monitor": { - "id": null, - "name": null, - "status": "up", - "type": null - }, - "summary": { - "up": 1, - "down": 0, - "geo": null - }, - "url": { - "full": "http://localhost:5678/pattern?r=200x1", - "domain": "localhost" - }, - "timestamp": 1568173234373 - } - }, - { - "monitor_id": "0064-up", - "histogram": { - "count": 20, - "points": [ - { - "timestamp": 1568172664000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172694000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172724000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172754000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172784000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172814000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172844000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172874000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172904000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172934000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172964000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172994000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173024000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173054000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173084000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173114000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173144000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173174000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173204000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173234000, - "up": 1, - "down": 0 - } - ] - }, - "state": { - "agent": null, - "checks": [ - { - "agent": { - "id": "04e1d082-65bc-4929-8d65-d0768a2621c4" - }, - "container": null, - "kubernetes": null, - "monitor": { - "ip": "127.0.0.1", - "name": "", - "status": "up" - }, - "observer": { - "geo": { - "name": "mpls", - "location": { - "lat": 37.926867976784706, - "lon": -78.02490200847387 - } - } - }, - "timestamp": "1568173234373" - } - ], - "geo": null, - "observer": { - "geo": { - "name": [ - "mpls" - ], - "location": null - } - }, - "monitor": { - "id": null, - "name": null, - "status": "up", - "type": null - }, - "summary": { - "up": 1, - "down": 0, - "geo": null - }, - "url": { - "full": "http://localhost:5678/pattern?r=200x1", - "domain": "localhost" - }, - "timestamp": 1568173234373 - } - }, - { - "monitor_id": "0065-up", - "histogram": { - "count": 20, - "points": [ - { - "timestamp": 1568172664000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172694000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172724000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172754000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172784000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172814000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172844000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172874000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172904000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172934000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172964000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172994000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173024000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173054000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173084000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173114000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173144000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173174000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173204000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173234000, - "up": 1, - "down": 0 - } - ] - }, - "state": { - "agent": null, - "checks": [ - { - "agent": { - "id": "04e1d082-65bc-4929-8d65-d0768a2621c4" - }, - "container": null, - "kubernetes": null, - "monitor": { - "ip": "127.0.0.1", - "name": "", - "status": "up" - }, - "observer": { - "geo": { - "name": "mpls", - "location": { - "lat": 37.926867976784706, - "lon": -78.02490200847387 - } - } - }, - "timestamp": "1568173234373" - } - ], - "geo": null, - "observer": { - "geo": { - "name": [ - "mpls" - ], - "location": null - } - }, - "monitor": { - "id": null, - "name": null, - "status": "up", - "type": null - }, - "summary": { - "up": 1, - "down": 0, - "geo": null - }, - "url": { - "full": "http://localhost:5678/pattern?r=200x1", - "domain": "localhost" - }, - "timestamp": 1568173234373 - } - }, - { - "monitor_id": "0066-up", - "histogram": { - "count": 20, - "points": [ - { - "timestamp": 1568172664000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172694000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172724000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172754000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172784000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172814000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172844000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172874000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172904000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172934000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172964000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172994000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173024000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173054000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173084000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173114000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173144000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173174000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173204000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173234000, - "up": 1, - "down": 0 - } - ] - }, - "state": { - "agent": null, - "checks": [ - { - "agent": { - "id": "04e1d082-65bc-4929-8d65-d0768a2621c4" - }, - "container": null, - "kubernetes": null, - "monitor": { - "ip": "127.0.0.1", - "name": "", - "status": "up" - }, - "observer": { - "geo": { - "name": "mpls", - "location": { - "lat": 37.926867976784706, - "lon": -78.02490200847387 - } - } - }, - "timestamp": "1568173234374" - } - ], - "geo": null, - "observer": { - "geo": { - "name": [ - "mpls" - ], - "location": null - } - }, - "monitor": { - "id": null, - "name": null, - "status": "up", - "type": null - }, - "summary": { - "up": 1, - "down": 0, - "geo": null - }, - "url": { - "full": "http://localhost:5678/pattern?r=200x1", - "domain": "localhost" - }, - "timestamp": 1568173234374 - } - }, - { - "monitor_id": "0067-up", - "histogram": { - "count": 20, - "points": [ - { - "timestamp": 1568172664000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172694000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172724000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172754000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172784000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172814000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172844000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172874000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172904000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172934000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172964000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172994000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173024000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173054000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173084000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173114000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173144000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173174000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173204000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173234000, - "up": 1, - "down": 0 - } - ] - }, - "state": { - "agent": null, - "checks": [ - { - "agent": { - "id": "04e1d082-65bc-4929-8d65-d0768a2621c4" - }, - "container": null, - "kubernetes": null, - "monitor": { - "ip": "127.0.0.1", - "name": "", - "status": "up" - }, - "observer": { - "geo": { - "name": "mpls", - "location": { - "lat": 37.926867976784706, - "lon": -78.02490200847387 - } - } - }, - "timestamp": "1568173234374" - } - ], - "geo": null, - "observer": { - "geo": { - "name": [ - "mpls" - ], - "location": null - } - }, - "monitor": { - "id": null, - "name": null, - "status": "up", - "type": null - }, - "summary": { - "up": 1, - "down": 0, - "geo": null - }, - "url": { - "full": "http://localhost:5678/pattern?r=200x1", - "domain": "localhost" - }, - "timestamp": 1568173234374 - } - }, - { - "monitor_id": "0068-up", - "histogram": { - "count": 20, - "points": [ - { - "timestamp": 1568172664000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172694000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172724000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172754000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172784000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172814000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172844000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172874000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172904000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172934000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172964000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172994000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173024000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173054000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173084000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173114000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173144000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173174000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173204000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173234000, - "up": 1, - "down": 0 - } - ] - }, - "state": { - "agent": null, - "checks": [ - { - "agent": { - "id": "04e1d082-65bc-4929-8d65-d0768a2621c4" - }, - "container": null, - "kubernetes": null, - "monitor": { - "ip": "127.0.0.1", - "name": "", - "status": "up" - }, - "observer": { - "geo": { - "name": "mpls", - "location": { - "lat": 37.926867976784706, - "lon": -78.02490200847387 - } - } - }, - "timestamp": "1568173234374" - } - ], - "geo": null, - "observer": { - "geo": { - "name": [ - "mpls" - ], - "location": null - } - }, - "monitor": { - "id": null, - "name": null, - "status": "up", - "type": null - }, - "summary": { - "up": 1, - "down": 0, - "geo": null - }, - "url": { - "full": "http://localhost:5678/pattern?r=200x1", - "domain": "localhost" - }, - "timestamp": 1568173234374 - } - }, - { - "monitor_id": "0069-up", - "histogram": { - "count": 20, - "points": [ - { - "timestamp": 1568172664000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172694000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172724000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172754000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172784000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172814000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172844000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172874000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172904000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172934000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172964000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172994000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173024000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173054000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173084000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173114000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173144000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173174000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173204000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173234000, - "up": 1, - "down": 0 - } - ] - }, - "state": { - "agent": null, - "checks": [ - { - "agent": { - "id": "04e1d082-65bc-4929-8d65-d0768a2621c4" - }, - "container": null, - "kubernetes": null, - "monitor": { - "ip": "127.0.0.1", - "name": "", - "status": "up" - }, - "observer": { - "geo": { - "name": "mpls", - "location": { - "lat": 37.926867976784706, - "lon": -78.02490200847387 - } - } - }, - "timestamp": "1568173234375" - } - ], - "geo": null, - "observer": { - "geo": { - "name": [ - "mpls" - ], - "location": null - } - }, - "monitor": { - "id": null, - "name": null, - "status": "up", - "type": null - }, - "summary": { - "up": 1, - "down": 0, - "geo": null - }, - "url": { - "full": "http://localhost:5678/pattern?r=200x1", - "domain": "localhost" - }, - "timestamp": 1568173234375 - } - } - ] - } -} \ No newline at end of file diff --git a/x-pack/test/api_integration/apis/uptime/graphql/fixtures/monitor_states_page_7_previous.json b/x-pack/test/api_integration/apis/uptime/graphql/fixtures/monitor_states_page_7_previous.json deleted file mode 100644 index 825d6365e3a9d..0000000000000 --- a/x-pack/test/api_integration/apis/uptime/graphql/fixtures/monitor_states_page_7_previous.json +++ /dev/null @@ -1,1609 +0,0 @@ -{ - "monitorStates": { - "prevPagePagination": "{\"cursorKey\":{\"monitor_id\":\"0050-down\"},\"sortOrder\":\"ASC\",\"cursorDirection\":\"BEFORE\"}", - "nextPagePagination": "{\"cursorKey\":{\"monitor_id\":\"0059-up\"},\"sortOrder\":\"ASC\",\"cursorDirection\":\"AFTER\"}", - "totalSummaryCount": 2000, - "summaries": [ - { - "monitor_id": "0050-down", - "histogram": { - "count": 20, - "points": [ - { - "timestamp": 1568172664000, - "up": 0, - "down": 1 - }, - { - "timestamp": 1568172694000, - "up": 0, - "down": 1 - }, - { - "timestamp": 1568172724000, - "up": 0, - "down": 1 - }, - { - "timestamp": 1568172754000, - "up": 0, - "down": 1 - }, - { - "timestamp": 1568172784000, - "up": 0, - "down": 1 - }, - { - "timestamp": 1568172814000, - "up": 0, - "down": 1 - }, - { - "timestamp": 1568172844000, - "up": 0, - "down": 1 - }, - { - "timestamp": 1568172874000, - "up": 0, - "down": 1 - }, - { - "timestamp": 1568172904000, - "up": 0, - "down": 1 - }, - { - "timestamp": 1568172934000, - "up": 0, - "down": 1 - }, - { - "timestamp": 1568172964000, - "up": 0, - "down": 1 - }, - { - "timestamp": 1568172994000, - "up": 0, - "down": 1 - }, - { - "timestamp": 1568173024000, - "up": 0, - "down": 1 - }, - { - "timestamp": 1568173054000, - "up": 0, - "down": 1 - }, - { - "timestamp": 1568173084000, - "up": 0, - "down": 1 - }, - { - "timestamp": 1568173114000, - "up": 0, - "down": 1 - }, - { - "timestamp": 1568173144000, - "up": 0, - "down": 1 - }, - { - "timestamp": 1568173174000, - "up": 0, - "down": 1 - }, - { - "timestamp": 1568173204000, - "up": 0, - "down": 1 - }, - { - "timestamp": 1568173234000, - "up": 0, - "down": 1 - } - ] - }, - "state": { - "agent": null, - "checks": [ - { - "agent": { - "id": "04e1d082-65bc-4929-8d65-d0768a2621c4" - }, - "container": null, - "kubernetes": null, - "monitor": { - "ip": "127.0.0.1", - "name": "", - "status": "down" - }, - "observer": { - "geo": { - "name": "mpls", - "location": { - "lat": 37.926867976784706, - "lon": -78.02490200847387 - } - } - }, - "timestamp": "1568173234386" - } - ], - "geo": null, - "observer": { - "geo": { - "name": [ - "mpls" - ], - "location": null - } - }, - "monitor": { - "id": null, - "name": null, - "status": "down", - "type": null - }, - "summary": { - "up": 0, - "down": 1, - "geo": null - }, - "url": { - "full": "http://localhost:5678/pattern?r=400x1", - "domain": "localhost" - }, - "timestamp": 1568173234386 - } - }, - { - "monitor_id": "0051-up", - "histogram": { - "count": 20, - "points": [ - { - "timestamp": 1568172664000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172694000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172724000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172754000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172784000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172814000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172844000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172874000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172904000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172934000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172964000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172994000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173024000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173054000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173084000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173114000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173144000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173174000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173204000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173234000, - "up": 1, - "down": 0 - } - ] - }, - "state": { - "agent": null, - "checks": [ - { - "agent": { - "id": "04e1d082-65bc-4929-8d65-d0768a2621c4" - }, - "container": null, - "kubernetes": null, - "monitor": { - "ip": "127.0.0.1", - "name": "", - "status": "up" - }, - "observer": { - "geo": { - "name": "mpls", - "location": { - "lat": 37.926867976784706, - "lon": -78.02490200847387 - } - } - }, - "timestamp": "1568173234371" - } - ], - "geo": null, - "observer": { - "geo": { - "name": [ - "mpls" - ], - "location": null - } - }, - "monitor": { - "id": null, - "name": null, - "status": "up", - "type": null - }, - "summary": { - "up": 1, - "down": 0, - "geo": null - }, - "url": { - "full": "http://localhost:5678/pattern?r=200x1", - "domain": "localhost" - }, - "timestamp": 1568173234371 - } - }, - { - "monitor_id": "0052-up", - "histogram": { - "count": 20, - "points": [ - { - "timestamp": 1568172664000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172694000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172724000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172754000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172784000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172814000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172844000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172874000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172904000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172934000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172964000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172994000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173024000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173054000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173084000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173114000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173144000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173174000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173204000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173234000, - "up": 1, - "down": 0 - } - ] - }, - "state": { - "agent": null, - "checks": [ - { - "agent": { - "id": "04e1d082-65bc-4929-8d65-d0768a2621c4" - }, - "container": null, - "kubernetes": null, - "monitor": { - "ip": "127.0.0.1", - "name": "", - "status": "up" - }, - "observer": { - "geo": { - "name": "mpls", - "location": { - "lat": 37.926867976784706, - "lon": -78.02490200847387 - } - } - }, - "timestamp": "1568173234371" - } - ], - "geo": null, - "observer": { - "geo": { - "name": [ - "mpls" - ], - "location": null - } - }, - "monitor": { - "id": null, - "name": null, - "status": "up", - "type": null - }, - "summary": { - "up": 1, - "down": 0, - "geo": null - }, - "url": { - "full": "http://localhost:5678/pattern?r=200x1", - "domain": "localhost" - }, - "timestamp": 1568173234371 - } - }, - { - "monitor_id": "0053-up", - "histogram": { - "count": 20, - "points": [ - { - "timestamp": 1568172664000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172694000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172724000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172754000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172784000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172814000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172844000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172874000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172904000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172934000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172964000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172994000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173024000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173054000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173084000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173114000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173144000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173174000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173204000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173234000, - "up": 1, - "down": 0 - } - ] - }, - "state": { - "agent": null, - "checks": [ - { - "agent": { - "id": "04e1d082-65bc-4929-8d65-d0768a2621c4" - }, - "container": null, - "kubernetes": null, - "monitor": { - "ip": "127.0.0.1", - "name": "", - "status": "up" - }, - "observer": { - "geo": { - "name": "mpls", - "location": { - "lat": 37.926867976784706, - "lon": -78.02490200847387 - } - } - }, - "timestamp": "1568173234371" - } - ], - "geo": null, - "observer": { - "geo": { - "name": [ - "mpls" - ], - "location": null - } - }, - "monitor": { - "id": null, - "name": null, - "status": "up", - "type": null - }, - "summary": { - "up": 1, - "down": 0, - "geo": null - }, - "url": { - "full": "http://localhost:5678/pattern?r=200x1", - "domain": "localhost" - }, - "timestamp": 1568173234371 - } - }, - { - "monitor_id": "0054-up", - "histogram": { - "count": 20, - "points": [ - { - "timestamp": 1568172664000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172694000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172724000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172754000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172784000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172814000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172844000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172874000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172904000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172934000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172964000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172994000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173024000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173054000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173084000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173114000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173144000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173174000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173204000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173234000, - "up": 1, - "down": 0 - } - ] - }, - "state": { - "agent": null, - "checks": [ - { - "agent": { - "id": "04e1d082-65bc-4929-8d65-d0768a2621c4" - }, - "container": null, - "kubernetes": null, - "monitor": { - "ip": "127.0.0.1", - "name": "", - "status": "up" - }, - "observer": { - "geo": { - "name": "mpls", - "location": { - "lat": 37.926867976784706, - "lon": -78.02490200847387 - } - } - }, - "timestamp": "1568173234371" - } - ], - "geo": null, - "observer": { - "geo": { - "name": [ - "mpls" - ], - "location": null - } - }, - "monitor": { - "id": null, - "name": null, - "status": "up", - "type": null - }, - "summary": { - "up": 1, - "down": 0, - "geo": null - }, - "url": { - "full": "http://localhost:5678/pattern?r=200x1", - "domain": "localhost" - }, - "timestamp": 1568173234371 - } - }, - { - "monitor_id": "0055-up", - "histogram": { - "count": 20, - "points": [ - { - "timestamp": 1568172664000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172694000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172724000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172754000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172784000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172814000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172844000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172874000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172904000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172934000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172964000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172994000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173024000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173054000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173084000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173114000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173144000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173174000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173204000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173234000, - "up": 1, - "down": 0 - } - ] - }, - "state": { - "agent": null, - "checks": [ - { - "agent": { - "id": "04e1d082-65bc-4929-8d65-d0768a2621c4" - }, - "container": null, - "kubernetes": null, - "monitor": { - "ip": "127.0.0.1", - "name": "", - "status": "up" - }, - "observer": { - "geo": { - "name": "mpls", - "location": { - "lat": 37.926867976784706, - "lon": -78.02490200847387 - } - } - }, - "timestamp": "1568173234371" - } - ], - "geo": null, - "observer": { - "geo": { - "name": [ - "mpls" - ], - "location": null - } - }, - "monitor": { - "id": null, - "name": null, - "status": "up", - "type": null - }, - "summary": { - "up": 1, - "down": 0, - "geo": null - }, - "url": { - "full": "http://localhost:5678/pattern?r=200x1", - "domain": "localhost" - }, - "timestamp": 1568173234371 - } - }, - { - "monitor_id": "0056-up", - "histogram": { - "count": 20, - "points": [ - { - "timestamp": 1568172664000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172694000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172724000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172754000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172784000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172814000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172844000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172874000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172904000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172934000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172964000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172994000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173024000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173054000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173084000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173114000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173144000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173174000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173204000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173234000, - "up": 1, - "down": 0 - } - ] - }, - "state": { - "agent": null, - "checks": [ - { - "agent": { - "id": "04e1d082-65bc-4929-8d65-d0768a2621c4" - }, - "container": null, - "kubernetes": null, - "monitor": { - "ip": "127.0.0.1", - "name": "", - "status": "up" - }, - "observer": { - "geo": { - "name": "mpls", - "location": { - "lat": 37.926867976784706, - "lon": -78.02490200847387 - } - } - }, - "timestamp": "1568173234371" - } - ], - "geo": null, - "observer": { - "geo": { - "name": [ - "mpls" - ], - "location": null - } - }, - "monitor": { - "id": null, - "name": null, - "status": "up", - "type": null - }, - "summary": { - "up": 1, - "down": 0, - "geo": null - }, - "url": { - "full": "http://localhost:5678/pattern?r=200x1", - "domain": "localhost" - }, - "timestamp": 1568173234371 - } - }, - { - "monitor_id": "0057-up", - "histogram": { - "count": 20, - "points": [ - { - "timestamp": 1568172664000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172694000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172724000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172754000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172784000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172814000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172844000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172874000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172904000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172934000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172964000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172994000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173024000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173054000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173084000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173114000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173144000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173174000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173204000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173234000, - "up": 1, - "down": 0 - } - ] - }, - "state": { - "agent": null, - "checks": [ - { - "agent": { - "id": "04e1d082-65bc-4929-8d65-d0768a2621c4" - }, - "container": null, - "kubernetes": null, - "monitor": { - "ip": "127.0.0.1", - "name": "", - "status": "up" - }, - "observer": { - "geo": { - "name": "mpls", - "location": { - "lat": 37.926867976784706, - "lon": -78.02490200847387 - } - } - }, - "timestamp": "1568173234372" - } - ], - "geo": null, - "observer": { - "geo": { - "name": [ - "mpls" - ], - "location": null - } - }, - "monitor": { - "id": null, - "name": null, - "status": "up", - "type": null - }, - "summary": { - "up": 1, - "down": 0, - "geo": null - }, - "url": { - "full": "http://localhost:5678/pattern?r=200x1", - "domain": "localhost" - }, - "timestamp": 1568173234372 - } - }, - { - "monitor_id": "0058-up", - "histogram": { - "count": 20, - "points": [ - { - "timestamp": 1568172664000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172694000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172724000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172754000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172784000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172814000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172844000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172874000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172904000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172934000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172964000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172994000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173024000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173054000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173084000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173114000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173144000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173174000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173204000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173234000, - "up": 1, - "down": 0 - } - ] - }, - "state": { - "agent": null, - "checks": [ - { - "agent": { - "id": "04e1d082-65bc-4929-8d65-d0768a2621c4" - }, - "container": null, - "kubernetes": null, - "monitor": { - "ip": "127.0.0.1", - "name": "", - "status": "up" - }, - "observer": { - "geo": { - "name": "mpls", - "location": { - "lat": 37.926867976784706, - "lon": -78.02490200847387 - } - } - }, - "timestamp": "1568173234372" - } - ], - "geo": null, - "observer": { - "geo": { - "name": [ - "mpls" - ], - "location": null - } - }, - "monitor": { - "id": null, - "name": null, - "status": "up", - "type": null - }, - "summary": { - "up": 1, - "down": 0, - "geo": null - }, - "url": { - "full": "http://localhost:5678/pattern?r=200x1", - "domain": "localhost" - }, - "timestamp": 1568173234372 - } - }, - { - "monitor_id": "0059-up", - "histogram": { - "count": 20, - "points": [ - { - "timestamp": 1568172664000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172694000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172724000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172754000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172784000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172814000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172844000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172874000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172904000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172934000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172964000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172994000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173024000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173054000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173084000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173114000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173144000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173174000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173204000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173234000, - "up": 1, - "down": 0 - } - ] - }, - "state": { - "agent": null, - "checks": [ - { - "agent": { - "id": "04e1d082-65bc-4929-8d65-d0768a2621c4" - }, - "container": null, - "kubernetes": null, - "monitor": { - "ip": "127.0.0.1", - "name": "", - "status": "up" - }, - "observer": { - "geo": { - "name": "mpls", - "location": { - "lat": 37.926867976784706, - "lon": -78.02490200847387 - } - } - }, - "timestamp": "1568173234372" - } - ], - "geo": null, - "observer": { - "geo": { - "name": [ - "mpls" - ], - "location": null - } - }, - "monitor": { - "id": null, - "name": null, - "status": "up", - "type": null - }, - "summary": { - "up": 1, - "down": 0, - "geo": null - }, - "url": { - "full": "http://localhost:5678/pattern?r=200x1", - "domain": "localhost" - }, - "timestamp": 1568173234372 - } - } - ] - } -} \ No newline at end of file diff --git a/x-pack/test/api_integration/apis/uptime/graphql/fixtures/monitor_states_page_8.json b/x-pack/test/api_integration/apis/uptime/graphql/fixtures/monitor_states_page_8.json deleted file mode 100644 index abb9bcdd804ed..0000000000000 --- a/x-pack/test/api_integration/apis/uptime/graphql/fixtures/monitor_states_page_8.json +++ /dev/null @@ -1,1609 +0,0 @@ -{ - "monitorStates": { - "prevPagePagination": "{\"cursorKey\":{\"monitor_id\":\"0070-down\"},\"sortOrder\":\"ASC\",\"cursorDirection\":\"BEFORE\"}", - "nextPagePagination": "{\"cursorDirection\":\"AFTER\",\"sortOrder\":\"ASC\",\"cursorKey\":{\"monitor_id\":\"0079-up\"}}", - "totalSummaryCount": 2000, - "summaries": [ - { - "monitor_id": "0070-down", - "histogram": { - "count": 20, - "points": [ - { - "timestamp": 1568172664000, - "up": 0, - "down": 1 - }, - { - "timestamp": 1568172694000, - "up": 0, - "down": 1 - }, - { - "timestamp": 1568172724000, - "up": 0, - "down": 1 - }, - { - "timestamp": 1568172754000, - "up": 0, - "down": 1 - }, - { - "timestamp": 1568172784000, - "up": 0, - "down": 1 - }, - { - "timestamp": 1568172814000, - "up": 0, - "down": 1 - }, - { - "timestamp": 1568172844000, - "up": 0, - "down": 1 - }, - { - "timestamp": 1568172874000, - "up": 0, - "down": 1 - }, - { - "timestamp": 1568172904000, - "up": 0, - "down": 1 - }, - { - "timestamp": 1568172934000, - "up": 0, - "down": 1 - }, - { - "timestamp": 1568172964000, - "up": 0, - "down": 1 - }, - { - "timestamp": 1568172994000, - "up": 0, - "down": 1 - }, - { - "timestamp": 1568173024000, - "up": 0, - "down": 1 - }, - { - "timestamp": 1568173054000, - "up": 0, - "down": 1 - }, - { - "timestamp": 1568173084000, - "up": 0, - "down": 1 - }, - { - "timestamp": 1568173114000, - "up": 0, - "down": 1 - }, - { - "timestamp": 1568173144000, - "up": 0, - "down": 1 - }, - { - "timestamp": 1568173174000, - "up": 0, - "down": 1 - }, - { - "timestamp": 1568173204000, - "up": 0, - "down": 1 - }, - { - "timestamp": 1568173234000, - "up": 0, - "down": 1 - } - ] - }, - "state": { - "agent": null, - "checks": [ - { - "agent": { - "id": "04e1d082-65bc-4929-8d65-d0768a2621c4" - }, - "container": null, - "kubernetes": null, - "monitor": { - "ip": "127.0.0.1", - "name": "", - "status": "down" - }, - "observer": { - "geo": { - "name": "mpls", - "location": { - "lat": 37.926867976784706, - "lon": -78.02490200847387 - } - } - }, - "timestamp": "1568173234375" - } - ], - "geo": null, - "observer": { - "geo": { - "name": [ - "mpls" - ], - "location": null - } - }, - "monitor": { - "id": null, - "name": null, - "status": "down", - "type": null - }, - "summary": { - "up": 0, - "down": 1, - "geo": null - }, - "url": { - "full": "http://localhost:5678/pattern?r=400x1", - "domain": "localhost" - }, - "timestamp": 1568173234375 - } - }, - { - "monitor_id": "0071-up", - "histogram": { - "count": 20, - "points": [ - { - "timestamp": 1568172664000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172694000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172724000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172754000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172784000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172814000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172844000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172874000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172904000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172934000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172964000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172994000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173024000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173054000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173084000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173114000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173144000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173174000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173204000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173234000, - "up": 1, - "down": 0 - } - ] - }, - "state": { - "agent": null, - "checks": [ - { - "agent": { - "id": "04e1d082-65bc-4929-8d65-d0768a2621c4" - }, - "container": null, - "kubernetes": null, - "monitor": { - "ip": "127.0.0.1", - "name": "", - "status": "up" - }, - "observer": { - "geo": { - "name": "mpls", - "location": { - "lat": 37.926867976784706, - "lon": -78.02490200847387 - } - } - }, - "timestamp": "1568173234375" - } - ], - "geo": null, - "observer": { - "geo": { - "name": [ - "mpls" - ], - "location": null - } - }, - "monitor": { - "id": null, - "name": null, - "status": "up", - "type": null - }, - "summary": { - "up": 1, - "down": 0, - "geo": null - }, - "url": { - "full": "http://localhost:5678/pattern?r=200x1", - "domain": "localhost" - }, - "timestamp": 1568173234375 - } - }, - { - "monitor_id": "0072-up", - "histogram": { - "count": 20, - "points": [ - { - "timestamp": 1568172664000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172694000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172724000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172754000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172784000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172814000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172844000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172874000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172904000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172934000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172964000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172994000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173024000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173054000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173084000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173114000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173144000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173174000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173204000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173234000, - "up": 1, - "down": 0 - } - ] - }, - "state": { - "agent": null, - "checks": [ - { - "agent": { - "id": "04e1d082-65bc-4929-8d65-d0768a2621c4" - }, - "container": null, - "kubernetes": null, - "monitor": { - "ip": "127.0.0.1", - "name": "", - "status": "up" - }, - "observer": { - "geo": { - "name": "mpls", - "location": { - "lat": 37.926867976784706, - "lon": -78.02490200847387 - } - } - }, - "timestamp": "1568173234376" - } - ], - "geo": null, - "observer": { - "geo": { - "name": [ - "mpls" - ], - "location": null - } - }, - "monitor": { - "id": null, - "name": null, - "status": "up", - "type": null - }, - "summary": { - "up": 1, - "down": 0, - "geo": null - }, - "url": { - "full": "http://localhost:5678/pattern?r=200x1", - "domain": "localhost" - }, - "timestamp": 1568173234376 - } - }, - { - "monitor_id": "0073-up", - "histogram": { - "count": 20, - "points": [ - { - "timestamp": 1568172664000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172694000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172724000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172754000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172784000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172814000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172844000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172874000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172904000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172934000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172964000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172994000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173024000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173054000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173084000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173114000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173144000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173174000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173204000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173234000, - "up": 1, - "down": 0 - } - ] - }, - "state": { - "agent": null, - "checks": [ - { - "agent": { - "id": "04e1d082-65bc-4929-8d65-d0768a2621c4" - }, - "container": null, - "kubernetes": null, - "monitor": { - "ip": "127.0.0.1", - "name": "", - "status": "up" - }, - "observer": { - "geo": { - "name": "mpls", - "location": { - "lat": 37.926867976784706, - "lon": -78.02490200847387 - } - } - }, - "timestamp": "1568173234406" - } - ], - "geo": null, - "observer": { - "geo": { - "name": [ - "mpls" - ], - "location": null - } - }, - "monitor": { - "id": null, - "name": null, - "status": "up", - "type": null - }, - "summary": { - "up": 1, - "down": 0, - "geo": null - }, - "url": { - "full": "http://localhost:5678/pattern?r=200x1", - "domain": "localhost" - }, - "timestamp": 1568173234406 - } - }, - { - "monitor_id": "0074-up", - "histogram": { - "count": 20, - "points": [ - { - "timestamp": 1568172664000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172694000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172724000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172754000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172784000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172814000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172844000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172874000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172904000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172934000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172964000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172994000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173024000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173054000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173084000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173114000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173144000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173174000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173204000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173234000, - "up": 1, - "down": 0 - } - ] - }, - "state": { - "agent": null, - "checks": [ - { - "agent": { - "id": "04e1d082-65bc-4929-8d65-d0768a2621c4" - }, - "container": null, - "kubernetes": null, - "monitor": { - "ip": "127.0.0.1", - "name": "", - "status": "up" - }, - "observer": { - "geo": { - "name": "mpls", - "location": { - "lat": 37.926867976784706, - "lon": -78.02490200847387 - } - } - }, - "timestamp": "1568173234410" - } - ], - "geo": null, - "observer": { - "geo": { - "name": [ - "mpls" - ], - "location": null - } - }, - "monitor": { - "id": null, - "name": null, - "status": "up", - "type": null - }, - "summary": { - "up": 1, - "down": 0, - "geo": null - }, - "url": { - "full": "http://localhost:5678/pattern?r=200x1", - "domain": "localhost" - }, - "timestamp": 1568173234410 - } - }, - { - "monitor_id": "0075-intermittent", - "histogram": { - "count": 20, - "points": [ - { - "timestamp": 1568172664000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172694000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172724000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172754000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172784000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172814000, - "up": 0, - "down": 1 - }, - { - "timestamp": 1568172844000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172874000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172904000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172934000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172964000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172994000, - "up": 0, - "down": 1 - }, - { - "timestamp": 1568173024000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173054000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173084000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173114000, - "up": 0, - "down": 1 - }, - { - "timestamp": 1568173144000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173174000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173204000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173234000, - "up": 1, - "down": 0 - } - ] - }, - "state": { - "agent": null, - "checks": [ - { - "agent": { - "id": "04e1d082-65bc-4929-8d65-d0768a2621c4" - }, - "container": null, - "kubernetes": null, - "monitor": { - "ip": "127.0.0.1", - "name": "", - "status": "up" - }, - "observer": { - "geo": { - "name": "mpls", - "location": { - "lat": 37.926867976784706, - "lon": -78.02490200847387 - } - } - }, - "timestamp": "1568173234406" - } - ], - "geo": null, - "observer": { - "geo": { - "name": [ - "mpls" - ], - "location": null - } - }, - "monitor": { - "id": null, - "name": null, - "status": "up", - "type": null - }, - "summary": { - "up": 1, - "down": 0, - "geo": null - }, - "url": { - "full": "http://localhost:5678/pattern?r=200x5,500x1", - "domain": "localhost" - }, - "timestamp": 1568173234406 - } - }, - { - "monitor_id": "0076-up", - "histogram": { - "count": 20, - "points": [ - { - "timestamp": 1568172664000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172694000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172724000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172754000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172784000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172814000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172844000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172874000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172904000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172934000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172964000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172994000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173024000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173054000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173084000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173114000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173144000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173174000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173204000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173234000, - "up": 1, - "down": 0 - } - ] - }, - "state": { - "agent": null, - "checks": [ - { - "agent": { - "id": "04e1d082-65bc-4929-8d65-d0768a2621c4" - }, - "container": null, - "kubernetes": null, - "monitor": { - "ip": "127.0.0.1", - "name": "", - "status": "up" - }, - "observer": { - "geo": { - "name": "mpls", - "location": { - "lat": 37.926867976784706, - "lon": -78.02490200847387 - } - } - }, - "timestamp": "1568173234387" - } - ], - "geo": null, - "observer": { - "geo": { - "name": [ - "mpls" - ], - "location": null - } - }, - "monitor": { - "id": null, - "name": null, - "status": "up", - "type": null - }, - "summary": { - "up": 1, - "down": 0, - "geo": null - }, - "url": { - "full": "http://localhost:5678/pattern?r=200x1", - "domain": "localhost" - }, - "timestamp": 1568173234387 - } - }, - { - "monitor_id": "0077-up", - "histogram": { - "count": 20, - "points": [ - { - "timestamp": 1568172664000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172694000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172724000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172754000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172784000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172814000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172844000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172874000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172904000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172934000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172964000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172994000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173024000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173054000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173084000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173114000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173144000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173174000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173204000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173234000, - "up": 1, - "down": 0 - } - ] - }, - "state": { - "agent": null, - "checks": [ - { - "agent": { - "id": "04e1d082-65bc-4929-8d65-d0768a2621c4" - }, - "container": null, - "kubernetes": null, - "monitor": { - "ip": "127.0.0.1", - "name": "", - "status": "up" - }, - "observer": { - "geo": { - "name": "mpls", - "location": { - "lat": 37.926867976784706, - "lon": -78.02490200847387 - } - } - }, - "timestamp": "1568173234389" - } - ], - "geo": null, - "observer": { - "geo": { - "name": [ - "mpls" - ], - "location": null - } - }, - "monitor": { - "id": null, - "name": null, - "status": "up", - "type": null - }, - "summary": { - "up": 1, - "down": 0, - "geo": null - }, - "url": { - "full": "http://localhost:5678/pattern?r=200x1", - "domain": "localhost" - }, - "timestamp": 1568173234389 - } - }, - { - "monitor_id": "0078-up", - "histogram": { - "count": 20, - "points": [ - { - "timestamp": 1568172664000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172694000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172724000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172754000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172784000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172814000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172844000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172874000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172904000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172934000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172964000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172994000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173024000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173054000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173084000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173114000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173144000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173174000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173204000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173234000, - "up": 1, - "down": 0 - } - ] - }, - "state": { - "agent": null, - "checks": [ - { - "agent": { - "id": "04e1d082-65bc-4929-8d65-d0768a2621c4" - }, - "container": null, - "kubernetes": null, - "monitor": { - "ip": "127.0.0.1", - "name": "", - "status": "up" - }, - "observer": { - "geo": { - "name": "mpls", - "location": { - "lat": 37.926867976784706, - "lon": -78.02490200847387 - } - } - }, - "timestamp": "1568173234371" - } - ], - "geo": null, - "observer": { - "geo": { - "name": [ - "mpls" - ], - "location": null - } - }, - "monitor": { - "id": null, - "name": null, - "status": "up", - "type": null - }, - "summary": { - "up": 1, - "down": 0, - "geo": null - }, - "url": { - "full": "http://localhost:5678/pattern?r=200x1", - "domain": "localhost" - }, - "timestamp": 1568173234371 - } - }, - { - "monitor_id": "0079-up", - "histogram": { - "count": 20, - "points": [ - { - "timestamp": 1568172664000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172694000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172724000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172754000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172784000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172814000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172844000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172874000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172904000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172934000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172964000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172994000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173024000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173054000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173084000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173114000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173144000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173174000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173204000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173234000, - "up": 1, - "down": 0 - } - ] - }, - "state": { - "agent": null, - "checks": [ - { - "agent": { - "id": "04e1d082-65bc-4929-8d65-d0768a2621c4" - }, - "container": null, - "kubernetes": null, - "monitor": { - "ip": "127.0.0.1", - "name": "", - "status": "up" - }, - "observer": { - "geo": { - "name": "mpls", - "location": { - "lat": 37.926867976784706, - "lon": -78.02490200847387 - } - } - }, - "timestamp": "1568173234371" - } - ], - "geo": null, - "observer": { - "geo": { - "name": [ - "mpls" - ], - "location": null - } - }, - "monitor": { - "id": null, - "name": null, - "status": "up", - "type": null - }, - "summary": { - "up": 1, - "down": 0, - "geo": null - }, - "url": { - "full": "http://localhost:5678/pattern?r=200x1", - "domain": "localhost" - }, - "timestamp": 1568173234371 - } - } - ] - } -} \ No newline at end of file diff --git a/x-pack/test/api_integration/apis/uptime/graphql/fixtures/monitor_states_page_8_previous.json b/x-pack/test/api_integration/apis/uptime/graphql/fixtures/monitor_states_page_8_previous.json deleted file mode 100644 index 46a5f195e6a82..0000000000000 --- a/x-pack/test/api_integration/apis/uptime/graphql/fixtures/monitor_states_page_8_previous.json +++ /dev/null @@ -1,1609 +0,0 @@ -{ - "monitorStates": { - "prevPagePagination": "{\"cursorKey\":{\"monitor_id\":\"0060-intermittent\"},\"sortOrder\":\"ASC\",\"cursorDirection\":\"BEFORE\"}", - "nextPagePagination": "{\"cursorKey\":{\"monitor_id\":\"0069-up\"},\"sortOrder\":\"ASC\",\"cursorDirection\":\"AFTER\"}", - "totalSummaryCount": 2000, - "summaries": [ - { - "monitor_id": "0060-intermittent", - "histogram": { - "count": 20, - "points": [ - { - "timestamp": 1568172664000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172694000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172724000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172754000, - "up": 0, - "down": 1 - }, - { - "timestamp": 1568172784000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172814000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172844000, - "up": 0, - "down": 1 - }, - { - "timestamp": 1568172874000, - "up": 0, - "down": 1 - }, - { - "timestamp": 1568172904000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172934000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172964000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172994000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173024000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173054000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173084000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173114000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173144000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173174000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173204000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173234000, - "up": 1, - "down": 0 - } - ] - }, - "state": { - "agent": null, - "checks": [ - { - "agent": { - "id": "04e1d082-65bc-4929-8d65-d0768a2621c4" - }, - "container": null, - "kubernetes": null, - "monitor": { - "ip": "127.0.0.1", - "name": "", - "status": "up" - }, - "observer": { - "geo": { - "name": "mpls", - "location": { - "lat": 37.926867976784706, - "lon": -78.02490200847387 - } - } - }, - "timestamp": "1568173234372" - } - ], - "geo": null, - "observer": { - "geo": { - "name": [ - "mpls" - ], - "location": null - } - }, - "monitor": { - "id": null, - "name": null, - "status": "up", - "type": null - }, - "summary": { - "up": 1, - "down": 0, - "geo": null - }, - "url": { - "full": "http://localhost:5678/pattern?r=200x5,500x1", - "domain": "localhost" - }, - "timestamp": 1568173234372 - } - }, - { - "monitor_id": "0061-up", - "histogram": { - "count": 20, - "points": [ - { - "timestamp": 1568172664000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172694000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172724000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172754000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172784000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172814000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172844000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172874000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172904000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172934000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172964000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172994000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173024000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173054000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173084000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173114000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173144000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173174000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173204000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173234000, - "up": 1, - "down": 0 - } - ] - }, - "state": { - "agent": null, - "checks": [ - { - "agent": { - "id": "04e1d082-65bc-4929-8d65-d0768a2621c4" - }, - "container": null, - "kubernetes": null, - "monitor": { - "ip": "127.0.0.1", - "name": "", - "status": "up" - }, - "observer": { - "geo": { - "name": "mpls", - "location": { - "lat": 37.926867976784706, - "lon": -78.02490200847387 - } - } - }, - "timestamp": "1568173234373" - } - ], - "geo": null, - "observer": { - "geo": { - "name": [ - "mpls" - ], - "location": null - } - }, - "monitor": { - "id": null, - "name": null, - "status": "up", - "type": null - }, - "summary": { - "up": 1, - "down": 0, - "geo": null - }, - "url": { - "full": "http://localhost:5678/pattern?r=200x1", - "domain": "localhost" - }, - "timestamp": 1568173234373 - } - }, - { - "monitor_id": "0062-up", - "histogram": { - "count": 20, - "points": [ - { - "timestamp": 1568172664000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172694000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172724000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172754000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172784000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172814000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172844000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172874000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172904000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172934000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172964000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172994000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173024000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173054000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173084000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173114000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173144000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173174000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173204000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173234000, - "up": 1, - "down": 0 - } - ] - }, - "state": { - "agent": null, - "checks": [ - { - "agent": { - "id": "04e1d082-65bc-4929-8d65-d0768a2621c4" - }, - "container": null, - "kubernetes": null, - "monitor": { - "ip": "127.0.0.1", - "name": "", - "status": "up" - }, - "observer": { - "geo": { - "name": "mpls", - "location": { - "lat": 37.926867976784706, - "lon": -78.02490200847387 - } - } - }, - "timestamp": "1568173234373" - } - ], - "geo": null, - "observer": { - "geo": { - "name": [ - "mpls" - ], - "location": null - } - }, - "monitor": { - "id": null, - "name": null, - "status": "up", - "type": null - }, - "summary": { - "up": 1, - "down": 0, - "geo": null - }, - "url": { - "full": "http://localhost:5678/pattern?r=200x1", - "domain": "localhost" - }, - "timestamp": 1568173234373 - } - }, - { - "monitor_id": "0063-up", - "histogram": { - "count": 20, - "points": [ - { - "timestamp": 1568172664000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172694000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172724000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172754000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172784000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172814000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172844000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172874000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172904000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172934000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172964000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172994000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173024000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173054000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173084000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173114000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173144000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173174000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173204000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173234000, - "up": 1, - "down": 0 - } - ] - }, - "state": { - "agent": null, - "checks": [ - { - "agent": { - "id": "04e1d082-65bc-4929-8d65-d0768a2621c4" - }, - "container": null, - "kubernetes": null, - "monitor": { - "ip": "127.0.0.1", - "name": "", - "status": "up" - }, - "observer": { - "geo": { - "name": "mpls", - "location": { - "lat": 37.926867976784706, - "lon": -78.02490200847387 - } - } - }, - "timestamp": "1568173234373" - } - ], - "geo": null, - "observer": { - "geo": { - "name": [ - "mpls" - ], - "location": null - } - }, - "monitor": { - "id": null, - "name": null, - "status": "up", - "type": null - }, - "summary": { - "up": 1, - "down": 0, - "geo": null - }, - "url": { - "full": "http://localhost:5678/pattern?r=200x1", - "domain": "localhost" - }, - "timestamp": 1568173234373 - } - }, - { - "monitor_id": "0064-up", - "histogram": { - "count": 20, - "points": [ - { - "timestamp": 1568172664000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172694000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172724000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172754000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172784000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172814000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172844000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172874000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172904000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172934000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172964000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172994000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173024000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173054000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173084000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173114000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173144000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173174000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173204000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173234000, - "up": 1, - "down": 0 - } - ] - }, - "state": { - "agent": null, - "checks": [ - { - "agent": { - "id": "04e1d082-65bc-4929-8d65-d0768a2621c4" - }, - "container": null, - "kubernetes": null, - "monitor": { - "ip": "127.0.0.1", - "name": "", - "status": "up" - }, - "observer": { - "geo": { - "name": "mpls", - "location": { - "lat": 37.926867976784706, - "lon": -78.02490200847387 - } - } - }, - "timestamp": "1568173234373" - } - ], - "geo": null, - "observer": { - "geo": { - "name": [ - "mpls" - ], - "location": null - } - }, - "monitor": { - "id": null, - "name": null, - "status": "up", - "type": null - }, - "summary": { - "up": 1, - "down": 0, - "geo": null - }, - "url": { - "full": "http://localhost:5678/pattern?r=200x1", - "domain": "localhost" - }, - "timestamp": 1568173234373 - } - }, - { - "monitor_id": "0065-up", - "histogram": { - "count": 20, - "points": [ - { - "timestamp": 1568172664000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172694000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172724000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172754000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172784000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172814000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172844000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172874000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172904000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172934000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172964000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172994000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173024000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173054000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173084000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173114000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173144000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173174000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173204000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173234000, - "up": 1, - "down": 0 - } - ] - }, - "state": { - "agent": null, - "checks": [ - { - "agent": { - "id": "04e1d082-65bc-4929-8d65-d0768a2621c4" - }, - "container": null, - "kubernetes": null, - "monitor": { - "ip": "127.0.0.1", - "name": "", - "status": "up" - }, - "observer": { - "geo": { - "name": "mpls", - "location": { - "lat": 37.926867976784706, - "lon": -78.02490200847387 - } - } - }, - "timestamp": "1568173234373" - } - ], - "geo": null, - "observer": { - "geo": { - "name": [ - "mpls" - ], - "location": null - } - }, - "monitor": { - "id": null, - "name": null, - "status": "up", - "type": null - }, - "summary": { - "up": 1, - "down": 0, - "geo": null - }, - "url": { - "full": "http://localhost:5678/pattern?r=200x1", - "domain": "localhost" - }, - "timestamp": 1568173234373 - } - }, - { - "monitor_id": "0066-up", - "histogram": { - "count": 20, - "points": [ - { - "timestamp": 1568172664000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172694000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172724000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172754000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172784000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172814000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172844000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172874000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172904000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172934000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172964000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172994000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173024000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173054000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173084000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173114000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173144000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173174000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173204000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173234000, - "up": 1, - "down": 0 - } - ] - }, - "state": { - "agent": null, - "checks": [ - { - "agent": { - "id": "04e1d082-65bc-4929-8d65-d0768a2621c4" - }, - "container": null, - "kubernetes": null, - "monitor": { - "ip": "127.0.0.1", - "name": "", - "status": "up" - }, - "observer": { - "geo": { - "name": "mpls", - "location": { - "lat": 37.926867976784706, - "lon": -78.02490200847387 - } - } - }, - "timestamp": "1568173234374" - } - ], - "geo": null, - "observer": { - "geo": { - "name": [ - "mpls" - ], - "location": null - } - }, - "monitor": { - "id": null, - "name": null, - "status": "up", - "type": null - }, - "summary": { - "up": 1, - "down": 0, - "geo": null - }, - "url": { - "full": "http://localhost:5678/pattern?r=200x1", - "domain": "localhost" - }, - "timestamp": 1568173234374 - } - }, - { - "monitor_id": "0067-up", - "histogram": { - "count": 20, - "points": [ - { - "timestamp": 1568172664000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172694000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172724000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172754000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172784000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172814000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172844000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172874000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172904000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172934000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172964000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172994000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173024000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173054000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173084000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173114000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173144000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173174000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173204000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173234000, - "up": 1, - "down": 0 - } - ] - }, - "state": { - "agent": null, - "checks": [ - { - "agent": { - "id": "04e1d082-65bc-4929-8d65-d0768a2621c4" - }, - "container": null, - "kubernetes": null, - "monitor": { - "ip": "127.0.0.1", - "name": "", - "status": "up" - }, - "observer": { - "geo": { - "name": "mpls", - "location": { - "lat": 37.926867976784706, - "lon": -78.02490200847387 - } - } - }, - "timestamp": "1568173234374" - } - ], - "geo": null, - "observer": { - "geo": { - "name": [ - "mpls" - ], - "location": null - } - }, - "monitor": { - "id": null, - "name": null, - "status": "up", - "type": null - }, - "summary": { - "up": 1, - "down": 0, - "geo": null - }, - "url": { - "full": "http://localhost:5678/pattern?r=200x1", - "domain": "localhost" - }, - "timestamp": 1568173234374 - } - }, - { - "monitor_id": "0068-up", - "histogram": { - "count": 20, - "points": [ - { - "timestamp": 1568172664000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172694000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172724000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172754000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172784000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172814000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172844000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172874000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172904000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172934000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172964000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172994000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173024000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173054000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173084000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173114000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173144000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173174000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173204000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173234000, - "up": 1, - "down": 0 - } - ] - }, - "state": { - "agent": null, - "checks": [ - { - "agent": { - "id": "04e1d082-65bc-4929-8d65-d0768a2621c4" - }, - "container": null, - "kubernetes": null, - "monitor": { - "ip": "127.0.0.1", - "name": "", - "status": "up" - }, - "observer": { - "geo": { - "name": "mpls", - "location": { - "lat": 37.926867976784706, - "lon": -78.02490200847387 - } - } - }, - "timestamp": "1568173234374" - } - ], - "geo": null, - "observer": { - "geo": { - "name": [ - "mpls" - ], - "location": null - } - }, - "monitor": { - "id": null, - "name": null, - "status": "up", - "type": null - }, - "summary": { - "up": 1, - "down": 0, - "geo": null - }, - "url": { - "full": "http://localhost:5678/pattern?r=200x1", - "domain": "localhost" - }, - "timestamp": 1568173234374 - } - }, - { - "monitor_id": "0069-up", - "histogram": { - "count": 20, - "points": [ - { - "timestamp": 1568172664000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172694000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172724000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172754000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172784000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172814000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172844000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172874000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172904000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172934000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172964000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172994000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173024000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173054000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173084000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173114000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173144000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173174000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173204000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173234000, - "up": 1, - "down": 0 - } - ] - }, - "state": { - "agent": null, - "checks": [ - { - "agent": { - "id": "04e1d082-65bc-4929-8d65-d0768a2621c4" - }, - "container": null, - "kubernetes": null, - "monitor": { - "ip": "127.0.0.1", - "name": "", - "status": "up" - }, - "observer": { - "geo": { - "name": "mpls", - "location": { - "lat": 37.926867976784706, - "lon": -78.02490200847387 - } - } - }, - "timestamp": "1568173234375" - } - ], - "geo": null, - "observer": { - "geo": { - "name": [ - "mpls" - ], - "location": null - } - }, - "monitor": { - "id": null, - "name": null, - "status": "up", - "type": null - }, - "summary": { - "up": 1, - "down": 0, - "geo": null - }, - "url": { - "full": "http://localhost:5678/pattern?r=200x1", - "domain": "localhost" - }, - "timestamp": 1568173234375 - } - } - ] - } -} \ No newline at end of file diff --git a/x-pack/test/api_integration/apis/uptime/graphql/fixtures/monitor_states_page_9.json b/x-pack/test/api_integration/apis/uptime/graphql/fixtures/monitor_states_page_9.json deleted file mode 100644 index 035baf0ab5b5e..0000000000000 --- a/x-pack/test/api_integration/apis/uptime/graphql/fixtures/monitor_states_page_9.json +++ /dev/null @@ -1,1609 +0,0 @@ -{ - "monitorStates": { - "prevPagePagination": "{\"cursorKey\":{\"monitor_id\":\"0080-down\"},\"sortOrder\":\"ASC\",\"cursorDirection\":\"BEFORE\"}", - "nextPagePagination": "{\"cursorDirection\":\"AFTER\",\"sortOrder\":\"ASC\",\"cursorKey\":{\"monitor_id\":\"0089-up\"}}", - "totalSummaryCount": 2000, - "summaries": [ - { - "monitor_id": "0080-down", - "histogram": { - "count": 20, - "points": [ - { - "timestamp": 1568172664000, - "up": 0, - "down": 1 - }, - { - "timestamp": 1568172694000, - "up": 0, - "down": 1 - }, - { - "timestamp": 1568172724000, - "up": 0, - "down": 1 - }, - { - "timestamp": 1568172754000, - "up": 0, - "down": 1 - }, - { - "timestamp": 1568172784000, - "up": 0, - "down": 1 - }, - { - "timestamp": 1568172814000, - "up": 0, - "down": 1 - }, - { - "timestamp": 1568172844000, - "up": 0, - "down": 1 - }, - { - "timestamp": 1568172874000, - "up": 0, - "down": 1 - }, - { - "timestamp": 1568172904000, - "up": 0, - "down": 1 - }, - { - "timestamp": 1568172934000, - "up": 0, - "down": 1 - }, - { - "timestamp": 1568172964000, - "up": 0, - "down": 1 - }, - { - "timestamp": 1568172994000, - "up": 0, - "down": 1 - }, - { - "timestamp": 1568173024000, - "up": 0, - "down": 1 - }, - { - "timestamp": 1568173054000, - "up": 0, - "down": 1 - }, - { - "timestamp": 1568173084000, - "up": 0, - "down": 1 - }, - { - "timestamp": 1568173114000, - "up": 0, - "down": 1 - }, - { - "timestamp": 1568173144000, - "up": 0, - "down": 1 - }, - { - "timestamp": 1568173174000, - "up": 0, - "down": 1 - }, - { - "timestamp": 1568173204000, - "up": 0, - "down": 1 - }, - { - "timestamp": 1568173234000, - "up": 0, - "down": 1 - } - ] - }, - "state": { - "agent": null, - "checks": [ - { - "agent": { - "id": "04e1d082-65bc-4929-8d65-d0768a2621c4" - }, - "container": null, - "kubernetes": null, - "monitor": { - "ip": "127.0.0.1", - "name": "", - "status": "down" - }, - "observer": { - "geo": { - "name": "mpls", - "location": { - "lat": 37.926867976784706, - "lon": -78.02490200847387 - } - } - }, - "timestamp": "1568173234371" - } - ], - "geo": null, - "observer": { - "geo": { - "name": [ - "mpls" - ], - "location": null - } - }, - "monitor": { - "id": null, - "name": null, - "status": "down", - "type": null - }, - "summary": { - "up": 0, - "down": 1, - "geo": null - }, - "url": { - "full": "http://localhost:5678/pattern?r=400x1", - "domain": "localhost" - }, - "timestamp": 1568173234371 - } - }, - { - "monitor_id": "0081-up", - "histogram": { - "count": 20, - "points": [ - { - "timestamp": 1568172664000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172694000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172724000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172754000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172784000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172814000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172844000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172874000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172904000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172934000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172964000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172994000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173024000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173054000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173084000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173114000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173144000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173174000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173204000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173234000, - "up": 1, - "down": 0 - } - ] - }, - "state": { - "agent": null, - "checks": [ - { - "agent": { - "id": "04e1d082-65bc-4929-8d65-d0768a2621c4" - }, - "container": null, - "kubernetes": null, - "monitor": { - "ip": "127.0.0.1", - "name": "", - "status": "up" - }, - "observer": { - "geo": { - "name": "mpls", - "location": { - "lat": 37.926867976784706, - "lon": -78.02490200847387 - } - } - }, - "timestamp": "1568173234372" - } - ], - "geo": null, - "observer": { - "geo": { - "name": [ - "mpls" - ], - "location": null - } - }, - "monitor": { - "id": null, - "name": null, - "status": "up", - "type": null - }, - "summary": { - "up": 1, - "down": 0, - "geo": null - }, - "url": { - "full": "http://localhost:5678/pattern?r=200x1", - "domain": "localhost" - }, - "timestamp": 1568173234372 - } - }, - { - "monitor_id": "0082-up", - "histogram": { - "count": 20, - "points": [ - { - "timestamp": 1568172664000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172694000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172724000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172754000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172784000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172814000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172844000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172874000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172904000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172934000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172964000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172994000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173024000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173054000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173084000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173114000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173144000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173174000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173204000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173234000, - "up": 1, - "down": 0 - } - ] - }, - "state": { - "agent": null, - "checks": [ - { - "agent": { - "id": "04e1d082-65bc-4929-8d65-d0768a2621c4" - }, - "container": null, - "kubernetes": null, - "monitor": { - "ip": "127.0.0.1", - "name": "", - "status": "up" - }, - "observer": { - "geo": { - "name": "mpls", - "location": { - "lat": 37.926867976784706, - "lon": -78.02490200847387 - } - } - }, - "timestamp": "1568173234372" - } - ], - "geo": null, - "observer": { - "geo": { - "name": [ - "mpls" - ], - "location": null - } - }, - "monitor": { - "id": null, - "name": null, - "status": "up", - "type": null - }, - "summary": { - "up": 1, - "down": 0, - "geo": null - }, - "url": { - "full": "http://localhost:5678/pattern?r=200x1", - "domain": "localhost" - }, - "timestamp": 1568173234372 - } - }, - { - "monitor_id": "0083-up", - "histogram": { - "count": 20, - "points": [ - { - "timestamp": 1568172664000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172694000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172724000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172754000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172784000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172814000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172844000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172874000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172904000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172934000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172964000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172994000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173024000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173054000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173084000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173114000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173144000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173174000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173204000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173234000, - "up": 1, - "down": 0 - } - ] - }, - "state": { - "agent": null, - "checks": [ - { - "agent": { - "id": "04e1d082-65bc-4929-8d65-d0768a2621c4" - }, - "container": null, - "kubernetes": null, - "monitor": { - "ip": "127.0.0.1", - "name": "", - "status": "up" - }, - "observer": { - "geo": { - "name": "mpls", - "location": { - "lat": 37.926867976784706, - "lon": -78.02490200847387 - } - } - }, - "timestamp": "1568173234373" - } - ], - "geo": null, - "observer": { - "geo": { - "name": [ - "mpls" - ], - "location": null - } - }, - "monitor": { - "id": null, - "name": null, - "status": "up", - "type": null - }, - "summary": { - "up": 1, - "down": 0, - "geo": null - }, - "url": { - "full": "http://localhost:5678/pattern?r=200x1", - "domain": "localhost" - }, - "timestamp": 1568173234373 - } - }, - { - "monitor_id": "0084-up", - "histogram": { - "count": 20, - "points": [ - { - "timestamp": 1568172664000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172694000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172724000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172754000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172784000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172814000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172844000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172874000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172904000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172934000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172964000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172994000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173024000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173054000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173084000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173114000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173144000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173174000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173204000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173234000, - "up": 1, - "down": 0 - } - ] - }, - "state": { - "agent": null, - "checks": [ - { - "agent": { - "id": "04e1d082-65bc-4929-8d65-d0768a2621c4" - }, - "container": null, - "kubernetes": null, - "monitor": { - "ip": "127.0.0.1", - "name": "", - "status": "up" - }, - "observer": { - "geo": { - "name": "mpls", - "location": { - "lat": 37.926867976784706, - "lon": -78.02490200847387 - } - } - }, - "timestamp": "1568173234373" - } - ], - "geo": null, - "observer": { - "geo": { - "name": [ - "mpls" - ], - "location": null - } - }, - "monitor": { - "id": null, - "name": null, - "status": "up", - "type": null - }, - "summary": { - "up": 1, - "down": 0, - "geo": null - }, - "url": { - "full": "http://localhost:5678/pattern?r=200x1", - "domain": "localhost" - }, - "timestamp": 1568173234373 - } - }, - { - "monitor_id": "0085-up", - "histogram": { - "count": 20, - "points": [ - { - "timestamp": 1568172664000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172694000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172724000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172754000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172784000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172814000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172844000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172874000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172904000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172934000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172964000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172994000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173024000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173054000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173084000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173114000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173144000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173174000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173204000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173234000, - "up": 1, - "down": 0 - } - ] - }, - "state": { - "agent": null, - "checks": [ - { - "agent": { - "id": "04e1d082-65bc-4929-8d65-d0768a2621c4" - }, - "container": null, - "kubernetes": null, - "monitor": { - "ip": "127.0.0.1", - "name": "", - "status": "up" - }, - "observer": { - "geo": { - "name": "mpls", - "location": { - "lat": 37.926867976784706, - "lon": -78.02490200847387 - } - } - }, - "timestamp": "1568173234373" - } - ], - "geo": null, - "observer": { - "geo": { - "name": [ - "mpls" - ], - "location": null - } - }, - "monitor": { - "id": null, - "name": null, - "status": "up", - "type": null - }, - "summary": { - "up": 1, - "down": 0, - "geo": null - }, - "url": { - "full": "http://localhost:5678/pattern?r=200x1", - "domain": "localhost" - }, - "timestamp": 1568173234373 - } - }, - { - "monitor_id": "0086-up", - "histogram": { - "count": 20, - "points": [ - { - "timestamp": 1568172664000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172694000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172724000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172754000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172784000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172814000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172844000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172874000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172904000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172934000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172964000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172994000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173024000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173054000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173084000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173114000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173144000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173174000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173204000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173234000, - "up": 1, - "down": 0 - } - ] - }, - "state": { - "agent": null, - "checks": [ - { - "agent": { - "id": "04e1d082-65bc-4929-8d65-d0768a2621c4" - }, - "container": null, - "kubernetes": null, - "monitor": { - "ip": "127.0.0.1", - "name": "", - "status": "up" - }, - "observer": { - "geo": { - "name": "mpls", - "location": { - "lat": 37.926867976784706, - "lon": -78.02490200847387 - } - } - }, - "timestamp": "1568173234373" - } - ], - "geo": null, - "observer": { - "geo": { - "name": [ - "mpls" - ], - "location": null - } - }, - "monitor": { - "id": null, - "name": null, - "status": "up", - "type": null - }, - "summary": { - "up": 1, - "down": 0, - "geo": null - }, - "url": { - "full": "http://localhost:5678/pattern?r=200x1", - "domain": "localhost" - }, - "timestamp": 1568173234373 - } - }, - { - "monitor_id": "0087-up", - "histogram": { - "count": 20, - "points": [ - { - "timestamp": 1568172664000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172694000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172724000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172754000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172784000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172814000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172844000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172874000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172904000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172934000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172964000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172994000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173024000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173054000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173084000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173114000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173144000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173174000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173204000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173234000, - "up": 1, - "down": 0 - } - ] - }, - "state": { - "agent": null, - "checks": [ - { - "agent": { - "id": "04e1d082-65bc-4929-8d65-d0768a2621c4" - }, - "container": null, - "kubernetes": null, - "monitor": { - "ip": "127.0.0.1", - "name": "", - "status": "up" - }, - "observer": { - "geo": { - "name": "mpls", - "location": { - "lat": 37.926867976784706, - "lon": -78.02490200847387 - } - } - }, - "timestamp": "1568173234373" - } - ], - "geo": null, - "observer": { - "geo": { - "name": [ - "mpls" - ], - "location": null - } - }, - "monitor": { - "id": null, - "name": null, - "status": "up", - "type": null - }, - "summary": { - "up": 1, - "down": 0, - "geo": null - }, - "url": { - "full": "http://localhost:5678/pattern?r=200x1", - "domain": "localhost" - }, - "timestamp": 1568173234373 - } - }, - { - "monitor_id": "0088-up", - "histogram": { - "count": 20, - "points": [ - { - "timestamp": 1568172664000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172694000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172724000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172754000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172784000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172814000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172844000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172874000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172904000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172934000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172964000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172994000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173024000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173054000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173084000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173114000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173144000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173174000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173204000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173234000, - "up": 1, - "down": 0 - } - ] - }, - "state": { - "agent": null, - "checks": [ - { - "agent": { - "id": "04e1d082-65bc-4929-8d65-d0768a2621c4" - }, - "container": null, - "kubernetes": null, - "monitor": { - "ip": "127.0.0.1", - "name": "", - "status": "up" - }, - "observer": { - "geo": { - "name": "mpls", - "location": { - "lat": 37.926867976784706, - "lon": -78.02490200847387 - } - } - }, - "timestamp": "1568173234373" - } - ], - "geo": null, - "observer": { - "geo": { - "name": [ - "mpls" - ], - "location": null - } - }, - "monitor": { - "id": null, - "name": null, - "status": "up", - "type": null - }, - "summary": { - "up": 1, - "down": 0, - "geo": null - }, - "url": { - "full": "http://localhost:5678/pattern?r=200x1", - "domain": "localhost" - }, - "timestamp": 1568173234373 - } - }, - { - "monitor_id": "0089-up", - "histogram": { - "count": 20, - "points": [ - { - "timestamp": 1568172664000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172694000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172724000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172754000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172784000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172814000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172844000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172874000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172904000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172934000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172964000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172994000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173024000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173054000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173084000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173114000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173144000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173174000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173204000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173234000, - "up": 1, - "down": 0 - } - ] - }, - "state": { - "agent": null, - "checks": [ - { - "agent": { - "id": "04e1d082-65bc-4929-8d65-d0768a2621c4" - }, - "container": null, - "kubernetes": null, - "monitor": { - "ip": "127.0.0.1", - "name": "", - "status": "up" - }, - "observer": { - "geo": { - "name": "mpls", - "location": { - "lat": 37.926867976784706, - "lon": -78.02490200847387 - } - } - }, - "timestamp": "1568173234373" - } - ], - "geo": null, - "observer": { - "geo": { - "name": [ - "mpls" - ], - "location": null - } - }, - "monitor": { - "id": null, - "name": null, - "status": "up", - "type": null - }, - "summary": { - "up": 1, - "down": 0, - "geo": null - }, - "url": { - "full": "http://localhost:5678/pattern?r=200x1", - "domain": "localhost" - }, - "timestamp": 1568173234373 - } - } - ] - } -} \ No newline at end of file diff --git a/x-pack/test/api_integration/apis/uptime/graphql/fixtures/monitor_states_page_9_previous.json b/x-pack/test/api_integration/apis/uptime/graphql/fixtures/monitor_states_page_9_previous.json deleted file mode 100644 index a6d274056eec6..0000000000000 --- a/x-pack/test/api_integration/apis/uptime/graphql/fixtures/monitor_states_page_9_previous.json +++ /dev/null @@ -1,1609 +0,0 @@ -{ - "monitorStates": { - "prevPagePagination": "{\"cursorKey\":{\"monitor_id\":\"0070-down\"},\"sortOrder\":\"ASC\",\"cursorDirection\":\"BEFORE\"}", - "nextPagePagination": "{\"cursorKey\":{\"monitor_id\":\"0079-up\"},\"sortOrder\":\"ASC\",\"cursorDirection\":\"AFTER\"}", - "totalSummaryCount": 2000, - "summaries": [ - { - "monitor_id": "0070-down", - "histogram": { - "count": 20, - "points": [ - { - "timestamp": 1568172664000, - "up": 0, - "down": 1 - }, - { - "timestamp": 1568172694000, - "up": 0, - "down": 1 - }, - { - "timestamp": 1568172724000, - "up": 0, - "down": 1 - }, - { - "timestamp": 1568172754000, - "up": 0, - "down": 1 - }, - { - "timestamp": 1568172784000, - "up": 0, - "down": 1 - }, - { - "timestamp": 1568172814000, - "up": 0, - "down": 1 - }, - { - "timestamp": 1568172844000, - "up": 0, - "down": 1 - }, - { - "timestamp": 1568172874000, - "up": 0, - "down": 1 - }, - { - "timestamp": 1568172904000, - "up": 0, - "down": 1 - }, - { - "timestamp": 1568172934000, - "up": 0, - "down": 1 - }, - { - "timestamp": 1568172964000, - "up": 0, - "down": 1 - }, - { - "timestamp": 1568172994000, - "up": 0, - "down": 1 - }, - { - "timestamp": 1568173024000, - "up": 0, - "down": 1 - }, - { - "timestamp": 1568173054000, - "up": 0, - "down": 1 - }, - { - "timestamp": 1568173084000, - "up": 0, - "down": 1 - }, - { - "timestamp": 1568173114000, - "up": 0, - "down": 1 - }, - { - "timestamp": 1568173144000, - "up": 0, - "down": 1 - }, - { - "timestamp": 1568173174000, - "up": 0, - "down": 1 - }, - { - "timestamp": 1568173204000, - "up": 0, - "down": 1 - }, - { - "timestamp": 1568173234000, - "up": 0, - "down": 1 - } - ] - }, - "state": { - "agent": null, - "checks": [ - { - "agent": { - "id": "04e1d082-65bc-4929-8d65-d0768a2621c4" - }, - "container": null, - "kubernetes": null, - "monitor": { - "ip": "127.0.0.1", - "name": "", - "status": "down" - }, - "observer": { - "geo": { - "name": "mpls", - "location": { - "lat": 37.926867976784706, - "lon": -78.02490200847387 - } - } - }, - "timestamp": "1568173234375" - } - ], - "geo": null, - "observer": { - "geo": { - "name": [ - "mpls" - ], - "location": null - } - }, - "monitor": { - "id": null, - "name": null, - "status": "down", - "type": null - }, - "summary": { - "up": 0, - "down": 1, - "geo": null - }, - "url": { - "full": "http://localhost:5678/pattern?r=400x1", - "domain": "localhost" - }, - "timestamp": 1568173234375 - } - }, - { - "monitor_id": "0071-up", - "histogram": { - "count": 20, - "points": [ - { - "timestamp": 1568172664000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172694000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172724000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172754000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172784000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172814000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172844000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172874000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172904000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172934000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172964000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172994000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173024000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173054000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173084000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173114000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173144000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173174000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173204000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173234000, - "up": 1, - "down": 0 - } - ] - }, - "state": { - "agent": null, - "checks": [ - { - "agent": { - "id": "04e1d082-65bc-4929-8d65-d0768a2621c4" - }, - "container": null, - "kubernetes": null, - "monitor": { - "ip": "127.0.0.1", - "name": "", - "status": "up" - }, - "observer": { - "geo": { - "name": "mpls", - "location": { - "lat": 37.926867976784706, - "lon": -78.02490200847387 - } - } - }, - "timestamp": "1568173234375" - } - ], - "geo": null, - "observer": { - "geo": { - "name": [ - "mpls" - ], - "location": null - } - }, - "monitor": { - "id": null, - "name": null, - "status": "up", - "type": null - }, - "summary": { - "up": 1, - "down": 0, - "geo": null - }, - "url": { - "full": "http://localhost:5678/pattern?r=200x1", - "domain": "localhost" - }, - "timestamp": 1568173234375 - } - }, - { - "monitor_id": "0072-up", - "histogram": { - "count": 20, - "points": [ - { - "timestamp": 1568172664000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172694000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172724000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172754000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172784000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172814000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172844000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172874000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172904000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172934000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172964000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172994000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173024000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173054000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173084000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173114000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173144000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173174000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173204000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173234000, - "up": 1, - "down": 0 - } - ] - }, - "state": { - "agent": null, - "checks": [ - { - "agent": { - "id": "04e1d082-65bc-4929-8d65-d0768a2621c4" - }, - "container": null, - "kubernetes": null, - "monitor": { - "ip": "127.0.0.1", - "name": "", - "status": "up" - }, - "observer": { - "geo": { - "name": "mpls", - "location": { - "lat": 37.926867976784706, - "lon": -78.02490200847387 - } - } - }, - "timestamp": "1568173234376" - } - ], - "geo": null, - "observer": { - "geo": { - "name": [ - "mpls" - ], - "location": null - } - }, - "monitor": { - "id": null, - "name": null, - "status": "up", - "type": null - }, - "summary": { - "up": 1, - "down": 0, - "geo": null - }, - "url": { - "full": "http://localhost:5678/pattern?r=200x1", - "domain": "localhost" - }, - "timestamp": 1568173234376 - } - }, - { - "monitor_id": "0073-up", - "histogram": { - "count": 20, - "points": [ - { - "timestamp": 1568172664000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172694000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172724000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172754000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172784000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172814000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172844000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172874000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172904000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172934000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172964000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172994000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173024000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173054000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173084000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173114000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173144000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173174000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173204000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173234000, - "up": 1, - "down": 0 - } - ] - }, - "state": { - "agent": null, - "checks": [ - { - "agent": { - "id": "04e1d082-65bc-4929-8d65-d0768a2621c4" - }, - "container": null, - "kubernetes": null, - "monitor": { - "ip": "127.0.0.1", - "name": "", - "status": "up" - }, - "observer": { - "geo": { - "name": "mpls", - "location": { - "lat": 37.926867976784706, - "lon": -78.02490200847387 - } - } - }, - "timestamp": "1568173234406" - } - ], - "geo": null, - "observer": { - "geo": { - "name": [ - "mpls" - ], - "location": null - } - }, - "monitor": { - "id": null, - "name": null, - "status": "up", - "type": null - }, - "summary": { - "up": 1, - "down": 0, - "geo": null - }, - "url": { - "full": "http://localhost:5678/pattern?r=200x1", - "domain": "localhost" - }, - "timestamp": 1568173234406 - } - }, - { - "monitor_id": "0074-up", - "histogram": { - "count": 20, - "points": [ - { - "timestamp": 1568172664000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172694000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172724000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172754000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172784000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172814000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172844000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172874000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172904000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172934000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172964000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172994000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173024000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173054000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173084000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173114000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173144000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173174000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173204000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173234000, - "up": 1, - "down": 0 - } - ] - }, - "state": { - "agent": null, - "checks": [ - { - "agent": { - "id": "04e1d082-65bc-4929-8d65-d0768a2621c4" - }, - "container": null, - "kubernetes": null, - "monitor": { - "ip": "127.0.0.1", - "name": "", - "status": "up" - }, - "observer": { - "geo": { - "name": "mpls", - "location": { - "lat": 37.926867976784706, - "lon": -78.02490200847387 - } - } - }, - "timestamp": "1568173234410" - } - ], - "geo": null, - "observer": { - "geo": { - "name": [ - "mpls" - ], - "location": null - } - }, - "monitor": { - "id": null, - "name": null, - "status": "up", - "type": null - }, - "summary": { - "up": 1, - "down": 0, - "geo": null - }, - "url": { - "full": "http://localhost:5678/pattern?r=200x1", - "domain": "localhost" - }, - "timestamp": 1568173234410 - } - }, - { - "monitor_id": "0075-intermittent", - "histogram": { - "count": 20, - "points": [ - { - "timestamp": 1568172664000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172694000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172724000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172754000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172784000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172814000, - "up": 0, - "down": 1 - }, - { - "timestamp": 1568172844000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172874000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172904000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172934000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172964000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172994000, - "up": 0, - "down": 1 - }, - { - "timestamp": 1568173024000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173054000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173084000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173114000, - "up": 0, - "down": 1 - }, - { - "timestamp": 1568173144000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173174000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173204000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173234000, - "up": 1, - "down": 0 - } - ] - }, - "state": { - "agent": null, - "checks": [ - { - "agent": { - "id": "04e1d082-65bc-4929-8d65-d0768a2621c4" - }, - "container": null, - "kubernetes": null, - "monitor": { - "ip": "127.0.0.1", - "name": "", - "status": "up" - }, - "observer": { - "geo": { - "name": "mpls", - "location": { - "lat": 37.926867976784706, - "lon": -78.02490200847387 - } - } - }, - "timestamp": "1568173234406" - } - ], - "geo": null, - "observer": { - "geo": { - "name": [ - "mpls" - ], - "location": null - } - }, - "monitor": { - "id": null, - "name": null, - "status": "up", - "type": null - }, - "summary": { - "up": 1, - "down": 0, - "geo": null - }, - "url": { - "full": "http://localhost:5678/pattern?r=200x5,500x1", - "domain": "localhost" - }, - "timestamp": 1568173234406 - } - }, - { - "monitor_id": "0076-up", - "histogram": { - "count": 20, - "points": [ - { - "timestamp": 1568172664000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172694000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172724000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172754000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172784000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172814000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172844000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172874000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172904000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172934000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172964000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172994000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173024000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173054000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173084000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173114000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173144000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173174000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173204000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173234000, - "up": 1, - "down": 0 - } - ] - }, - "state": { - "agent": null, - "checks": [ - { - "agent": { - "id": "04e1d082-65bc-4929-8d65-d0768a2621c4" - }, - "container": null, - "kubernetes": null, - "monitor": { - "ip": "127.0.0.1", - "name": "", - "status": "up" - }, - "observer": { - "geo": { - "name": "mpls", - "location": { - "lat": 37.926867976784706, - "lon": -78.02490200847387 - } - } - }, - "timestamp": "1568173234387" - } - ], - "geo": null, - "observer": { - "geo": { - "name": [ - "mpls" - ], - "location": null - } - }, - "monitor": { - "id": null, - "name": null, - "status": "up", - "type": null - }, - "summary": { - "up": 1, - "down": 0, - "geo": null - }, - "url": { - "full": "http://localhost:5678/pattern?r=200x1", - "domain": "localhost" - }, - "timestamp": 1568173234387 - } - }, - { - "monitor_id": "0077-up", - "histogram": { - "count": 20, - "points": [ - { - "timestamp": 1568172664000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172694000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172724000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172754000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172784000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172814000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172844000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172874000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172904000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172934000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172964000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172994000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173024000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173054000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173084000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173114000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173144000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173174000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173204000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173234000, - "up": 1, - "down": 0 - } - ] - }, - "state": { - "agent": null, - "checks": [ - { - "agent": { - "id": "04e1d082-65bc-4929-8d65-d0768a2621c4" - }, - "container": null, - "kubernetes": null, - "monitor": { - "ip": "127.0.0.1", - "name": "", - "status": "up" - }, - "observer": { - "geo": { - "name": "mpls", - "location": { - "lat": 37.926867976784706, - "lon": -78.02490200847387 - } - } - }, - "timestamp": "1568173234389" - } - ], - "geo": null, - "observer": { - "geo": { - "name": [ - "mpls" - ], - "location": null - } - }, - "monitor": { - "id": null, - "name": null, - "status": "up", - "type": null - }, - "summary": { - "up": 1, - "down": 0, - "geo": null - }, - "url": { - "full": "http://localhost:5678/pattern?r=200x1", - "domain": "localhost" - }, - "timestamp": 1568173234389 - } - }, - { - "monitor_id": "0078-up", - "histogram": { - "count": 20, - "points": [ - { - "timestamp": 1568172664000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172694000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172724000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172754000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172784000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172814000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172844000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172874000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172904000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172934000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172964000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172994000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173024000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173054000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173084000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173114000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173144000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173174000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173204000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173234000, - "up": 1, - "down": 0 - } - ] - }, - "state": { - "agent": null, - "checks": [ - { - "agent": { - "id": "04e1d082-65bc-4929-8d65-d0768a2621c4" - }, - "container": null, - "kubernetes": null, - "monitor": { - "ip": "127.0.0.1", - "name": "", - "status": "up" - }, - "observer": { - "geo": { - "name": "mpls", - "location": { - "lat": 37.926867976784706, - "lon": -78.02490200847387 - } - } - }, - "timestamp": "1568173234371" - } - ], - "geo": null, - "observer": { - "geo": { - "name": [ - "mpls" - ], - "location": null - } - }, - "monitor": { - "id": null, - "name": null, - "status": "up", - "type": null - }, - "summary": { - "up": 1, - "down": 0, - "geo": null - }, - "url": { - "full": "http://localhost:5678/pattern?r=200x1", - "domain": "localhost" - }, - "timestamp": 1568173234371 - } - }, - { - "monitor_id": "0079-up", - "histogram": { - "count": 20, - "points": [ - { - "timestamp": 1568172664000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172694000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172724000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172754000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172784000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172814000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172844000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172874000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172904000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172934000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172964000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568172994000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173024000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173054000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173084000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173114000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173144000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173174000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173204000, - "up": 1, - "down": 0 - }, - { - "timestamp": 1568173234000, - "up": 1, - "down": 0 - } - ] - }, - "state": { - "agent": null, - "checks": [ - { - "agent": { - "id": "04e1d082-65bc-4929-8d65-d0768a2621c4" - }, - "container": null, - "kubernetes": null, - "monitor": { - "ip": "127.0.0.1", - "name": "", - "status": "up" - }, - "observer": { - "geo": { - "name": "mpls", - "location": { - "lat": 37.926867976784706, - "lon": -78.02490200847387 - } - } - }, - "timestamp": "1568173234371" - } - ], - "geo": null, - "observer": { - "geo": { - "name": [ - "mpls" - ], - "location": null - } - }, - "monitor": { - "id": null, - "name": null, - "status": "up", - "type": null - }, - "summary": { - "up": 1, - "down": 0, - "geo": null - }, - "url": { - "full": "http://localhost:5678/pattern?r=200x1", - "domain": "localhost" - }, - "timestamp": 1568173234371 - } - } - ] - } -} \ No newline at end of file diff --git a/x-pack/test/api_integration/apis/uptime/graphql/fixtures/ping_list.json b/x-pack/test/api_integration/apis/uptime/graphql/fixtures/ping_list.json deleted file mode 100644 index 330ec83931a62..0000000000000 --- a/x-pack/test/api_integration/apis/uptime/graphql/fixtures/ping_list.json +++ /dev/null @@ -1,320 +0,0 @@ -{ - "allPings": { - "total": 2000, - "locations": [ - "mpls" - ], - "pings": [ - { - "timestamp": "2019-09-11T03:40:34.410Z", - "http": { - "response": { - "status_code": 200, - "body": { - "bytes": 3, - "hash": "27badc983df1780b60c2b3fa9d3a19a00e46aac798451f0febdca52920faaddf", - "content": null, - "content_bytes": null - } - } - }, - "error": null, - "monitor": { - "duration": { - "us": 413 - }, - "id": "0074-up", - "ip": "127.0.0.1", - "name": "", - "scheme": null, - "status": "up", - "type": "http" - }, - "observer": { - "geo": { - "name": "mpls" - } - } - }, - { - "timestamp": "2019-09-11T03:40:34.406Z", - "http": { - "response": { - "status_code": 200, - "body": { - "bytes": 3, - "hash": "27badc983df1780b60c2b3fa9d3a19a00e46aac798451f0febdca52920faaddf", - "content": null, - "content_bytes": null - } - } - }, - "error": null, - "monitor": { - "duration": { - "us": 441 - }, - "id": "0073-up", - "ip": "127.0.0.1", - "name": "", - "scheme": null, - "status": "up", - "type": "http" - }, - "observer": { - "geo": { - "name": "mpls" - } - } - }, - { - "timestamp": "2019-09-11T03:40:34.406Z", - "http": { - "response": { - "status_code": 200, - "body": { - "bytes": 3, - "hash": "27badc983df1780b60c2b3fa9d3a19a00e46aac798451f0febdca52920faaddf", - "content": null, - "content_bytes": null - } - } - }, - "error": null, - "monitor": { - "duration": { - "us": 482 - }, - "id": "0099-up", - "ip": "127.0.0.1", - "name": "", - "scheme": null, - "status": "up", - "type": "http" - }, - "observer": { - "geo": { - "name": "mpls" - } - } - }, - { - "timestamp": "2019-09-11T03:40:34.406Z", - "http": { - "response": { - "status_code": 200, - "body": { - "bytes": 3, - "hash": "27badc983df1780b60c2b3fa9d3a19a00e46aac798451f0febdca52920faaddf", - "content": null, - "content_bytes": null - } - } - }, - "error": null, - "monitor": { - "duration": { - "us": 558 - }, - "id": "0098-up", - "ip": "127.0.0.1", - "name": "", - "scheme": null, - "status": "up", - "type": "http" - }, - "observer": { - "geo": { - "name": "mpls" - } - } - }, - { - "timestamp": "2019-09-11T03:40:34.406Z", - "http": { - "response": { - "status_code": 200, - "body": { - "bytes": 3, - "hash": "27badc983df1780b60c2b3fa9d3a19a00e46aac798451f0febdca52920faaddf", - "content": null, - "content_bytes": null - } - } - }, - "error": null, - "monitor": { - "duration": { - "us": 304 - }, - "id": "0075-intermittent", - "ip": "127.0.0.1", - "name": "", - "scheme": null, - "status": "up", - "type": "http" - }, - "observer": { - "geo": { - "name": "mpls" - } - } - }, - { - "timestamp": "2019-09-11T03:40:34.405Z", - "http": { - "response": { - "status_code": 200, - "body": { - "bytes": 3, - "hash": "27badc983df1780b60c2b3fa9d3a19a00e46aac798451f0febdca52920faaddf", - "content": null, - "content_bytes": null - } - } - }, - "error": null, - "monitor": { - "duration": { - "us": 487 - }, - "id": "0097-up", - "ip": "127.0.0.1", - "name": "", - "scheme": null, - "status": "up", - "type": "http" - }, - "observer": { - "geo": { - "name": "mpls" - } - } - }, - { - "timestamp": "2019-09-11T03:40:34.405Z", - "http": { - "response": { - "status_code": 200, - "body": { - "bytes": 3, - "hash": "27badc983df1780b60c2b3fa9d3a19a00e46aac798451f0febdca52920faaddf", - "content": null, - "content_bytes": null - } - } - }, - "error": null, - "monitor": { - "duration": { - "us": 602 - }, - "id": "0049-up", - "ip": "127.0.0.1", - "name": "", - "scheme": null, - "status": "up", - "type": "http" - }, - "observer": { - "geo": { - "name": "mpls" - } - } - }, - { - "timestamp": "2019-09-11T03:40:34.390Z", - "http": { - "response": { - "status_code": 200, - "body": { - "bytes": 3, - "hash": "27badc983df1780b60c2b3fa9d3a19a00e46aac798451f0febdca52920faaddf", - "content": null, - "content_bytes": null - } - } - }, - "error": null, - "monitor": { - "duration": { - "us": 365 - }, - "id": "0047-up", - "ip": "127.0.0.1", - "name": "", - "scheme": null, - "status": "up", - "type": "http" - }, - "observer": { - "geo": { - "name": "mpls" - } - } - }, - { - "timestamp": "2019-09-11T03:40:34.389Z", - "http": { - "response": { - "status_code": 200, - "body": { - "bytes": 3, - "hash": "27badc983df1780b60c2b3fa9d3a19a00e46aac798451f0febdca52920faaddf", - "content": null, - "content_bytes": null - } - } - }, - "error": null, - "monitor": { - "duration": { - "us": 870 - }, - "id": "0077-up", - "ip": "127.0.0.1", - "name": "", - "scheme": null, - "status": "up", - "type": "http" - }, - "observer": { - "geo": { - "name": "mpls" - } - } - }, - { - "timestamp": "2019-09-11T03:40:34.387Z", - "http": { - "response": { - "status_code": 200, - "body": { - "bytes": 3, - "hash": "27badc983df1780b60c2b3fa9d3a19a00e46aac798451f0febdca52920faaddf", - "content": null, - "content_bytes": null - } - } - }, - "error": null, - "monitor": { - "duration": { - "us": 2808 - }, - "id": "0076-up", - "ip": "127.0.0.1", - "name": "", - "scheme": null, - "status": "up", - "type": "http" - }, - "observer": { - "geo": { - "name": "mpls" - } - } - } - ] - } -} \ No newline at end of file diff --git a/x-pack/test/api_integration/apis/uptime/graphql/fixtures/ping_list_count.json b/x-pack/test/api_integration/apis/uptime/graphql/fixtures/ping_list_count.json deleted file mode 100644 index 3a619f517626a..0000000000000 --- a/x-pack/test/api_integration/apis/uptime/graphql/fixtures/ping_list_count.json +++ /dev/null @@ -1,1569 +0,0 @@ -{ - "allPings": { - "total": 2000, - "locations": [ - "mpls" - ], - "pings": [ - { - "timestamp": "2019-09-11T03:40:34.410Z", - "http": { - "response": { - "status_code": 200, - "body": { - "bytes": 3, - "hash": "27badc983df1780b60c2b3fa9d3a19a00e46aac798451f0febdca52920faaddf", - "content": null, - "content_bytes": null - } - } - }, - "error": null, - "monitor": { - "duration": { - "us": 413 - }, - "id": "0074-up", - "ip": "127.0.0.1", - "name": "", - "scheme": null, - "status": "up", - "type": "http" - }, - "observer": { - "geo": { - "name": "mpls" - } - } - }, - { - "timestamp": "2019-09-11T03:40:34.406Z", - "http": { - "response": { - "status_code": 200, - "body": { - "bytes": 3, - "hash": "27badc983df1780b60c2b3fa9d3a19a00e46aac798451f0febdca52920faaddf", - "content": null, - "content_bytes": null - } - } - }, - "error": null, - "monitor": { - "duration": { - "us": 441 - }, - "id": "0073-up", - "ip": "127.0.0.1", - "name": "", - "scheme": null, - "status": "up", - "type": "http" - }, - "observer": { - "geo": { - "name": "mpls" - } - } - }, - { - "timestamp": "2019-09-11T03:40:34.406Z", - "http": { - "response": { - "status_code": 200, - "body": { - "bytes": 3, - "hash": "27badc983df1780b60c2b3fa9d3a19a00e46aac798451f0febdca52920faaddf", - "content": null, - "content_bytes": null - } - } - }, - "error": null, - "monitor": { - "duration": { - "us": 482 - }, - "id": "0099-up", - "ip": "127.0.0.1", - "name": "", - "scheme": null, - "status": "up", - "type": "http" - }, - "observer": { - "geo": { - "name": "mpls" - } - } - }, - { - "timestamp": "2019-09-11T03:40:34.406Z", - "http": { - "response": { - "status_code": 200, - "body": { - "bytes": 3, - "hash": "27badc983df1780b60c2b3fa9d3a19a00e46aac798451f0febdca52920faaddf", - "content": null, - "content_bytes": null - } - } - }, - "error": null, - "monitor": { - "duration": { - "us": 558 - }, - "id": "0098-up", - "ip": "127.0.0.1", - "name": "", - "scheme": null, - "status": "up", - "type": "http" - }, - "observer": { - "geo": { - "name": "mpls" - } - } - }, - { - "timestamp": "2019-09-11T03:40:34.406Z", - "http": { - "response": { - "status_code": 200, - "body": { - "bytes": 3, - "hash": "27badc983df1780b60c2b3fa9d3a19a00e46aac798451f0febdca52920faaddf", - "content": null, - "content_bytes": null - } - } - }, - "error": null, - "monitor": { - "duration": { - "us": 304 - }, - "id": "0075-intermittent", - "ip": "127.0.0.1", - "name": "", - "scheme": null, - "status": "up", - "type": "http" - }, - "observer": { - "geo": { - "name": "mpls" - } - } - }, - { - "timestamp": "2019-09-11T03:40:34.405Z", - "http": { - "response": { - "status_code": 200, - "body": { - "bytes": 3, - "hash": "27badc983df1780b60c2b3fa9d3a19a00e46aac798451f0febdca52920faaddf", - "content": null, - "content_bytes": null - } - } - }, - "error": null, - "monitor": { - "duration": { - "us": 487 - }, - "id": "0097-up", - "ip": "127.0.0.1", - "name": "", - "scheme": null, - "status": "up", - "type": "http" - }, - "observer": { - "geo": { - "name": "mpls" - } - } - }, - { - "timestamp": "2019-09-11T03:40:34.405Z", - "http": { - "response": { - "status_code": 200, - "body": { - "bytes": 3, - "hash": "27badc983df1780b60c2b3fa9d3a19a00e46aac798451f0febdca52920faaddf", - "content": null, - "content_bytes": null - } - } - }, - "error": null, - "monitor": { - "duration": { - "us": 602 - }, - "id": "0049-up", - "ip": "127.0.0.1", - "name": "", - "scheme": null, - "status": "up", - "type": "http" - }, - "observer": { - "geo": { - "name": "mpls" - } - } - }, - { - "timestamp": "2019-09-11T03:40:34.390Z", - "http": { - "response": { - "status_code": 200, - "body": { - "bytes": 3, - "hash": "27badc983df1780b60c2b3fa9d3a19a00e46aac798451f0febdca52920faaddf", - "content": null, - "content_bytes": null - } - } - }, - "error": null, - "monitor": { - "duration": { - "us": 365 - }, - "id": "0047-up", - "ip": "127.0.0.1", - "name": "", - "scheme": null, - "status": "up", - "type": "http" - }, - "observer": { - "geo": { - "name": "mpls" - } - } - }, - { - "timestamp": "2019-09-11T03:40:34.389Z", - "http": { - "response": { - "status_code": 200, - "body": { - "bytes": 3, - "hash": "27badc983df1780b60c2b3fa9d3a19a00e46aac798451f0febdca52920faaddf", - "content": null, - "content_bytes": null - } - } - }, - "error": null, - "monitor": { - "duration": { - "us": 870 - }, - "id": "0077-up", - "ip": "127.0.0.1", - "name": "", - "scheme": null, - "status": "up", - "type": "http" - }, - "observer": { - "geo": { - "name": "mpls" - } - } - }, - { - "timestamp": "2019-09-11T03:40:34.387Z", - "http": { - "response": { - "status_code": 200, - "body": { - "bytes": 3, - "hash": "27badc983df1780b60c2b3fa9d3a19a00e46aac798451f0febdca52920faaddf", - "content": null, - "content_bytes": null - } - } - }, - "error": null, - "monitor": { - "duration": { - "us": 2808 - }, - "id": "0076-up", - "ip": "127.0.0.1", - "name": "", - "scheme": null, - "status": "up", - "type": "http" - }, - "observer": { - "geo": { - "name": "mpls" - } - } - }, - { - "timestamp": "2019-09-11T03:40:34.386Z", - "http": { - "response": { - "status_code": 400, - "body": { - "bytes": 3, - "hash": "26d228663f13a88592a12d16cf9587caab0388b262d6d9f126ed62f9333aca94", - "content": "400", - "content_bytes": 3 - } - } - }, - "error": { - "message": "400 Bad Request", - "type": "validate" - }, - "monitor": { - "duration": { - "us": 4258 - }, - "id": "0050-down", - "ip": "127.0.0.1", - "name": "", - "scheme": null, - "status": "down", - "type": "http" - }, - "observer": { - "geo": { - "name": "mpls" - } - } - }, - { - "timestamp": "2019-09-11T03:40:34.386Z", - "http": { - "response": { - "status_code": 200, - "body": { - "bytes": 3, - "hash": "27badc983df1780b60c2b3fa9d3a19a00e46aac798451f0febdca52920faaddf", - "content": null, - "content_bytes": null - } - } - }, - "error": null, - "monitor": { - "duration": { - "us": 4784 - }, - "id": "0048-up", - "ip": "127.0.0.1", - "name": "", - "scheme": null, - "status": "up", - "type": "http" - }, - "observer": { - "geo": { - "name": "mpls" - } - } - }, - { - "timestamp": "2019-09-11T03:40:34.376Z", - "http": { - "response": { - "status_code": 200, - "body": { - "bytes": 3, - "hash": "27badc983df1780b60c2b3fa9d3a19a00e46aac798451f0febdca52920faaddf", - "content": null, - "content_bytes": null - } - } - }, - "error": null, - "monitor": { - "duration": { - "us": 14580 - }, - "id": "0072-up", - "ip": "127.0.0.1", - "name": "", - "scheme": null, - "status": "up", - "type": "http" - }, - "observer": { - "geo": { - "name": "mpls" - } - } - }, - { - "timestamp": "2019-09-11T03:40:34.376Z", - "http": { - "response": { - "status_code": 200, - "body": { - "bytes": 3, - "hash": "27badc983df1780b60c2b3fa9d3a19a00e46aac798451f0febdca52920faaddf", - "content": null, - "content_bytes": null - } - } - }, - "error": null, - "monitor": { - "duration": { - "us": 14679 - }, - "id": "0096-up", - "ip": "127.0.0.1", - "name": "", - "scheme": null, - "status": "up", - "type": "http" - }, - "observer": { - "geo": { - "name": "mpls" - } - } - }, - { - "timestamp": "2019-09-11T03:40:34.375Z", - "http": { - "response": { - "status_code": 200, - "body": { - "bytes": 3, - "hash": "27badc983df1780b60c2b3fa9d3a19a00e46aac798451f0febdca52920faaddf", - "content": null, - "content_bytes": null - } - } - }, - "error": null, - "monitor": { - "duration": { - "us": 15308 - }, - "id": "0092-up", - "ip": "127.0.0.1", - "name": "", - "scheme": null, - "status": "up", - "type": "http" - }, - "observer": { - "geo": { - "name": "mpls" - } - } - }, - { - "timestamp": "2019-09-11T03:40:34.375Z", - "http": { - "response": { - "status_code": 200, - "body": { - "bytes": 3, - "hash": "27badc983df1780b60c2b3fa9d3a19a00e46aac798451f0febdca52920faaddf", - "content": null, - "content_bytes": null - } - } - }, - "error": null, - "monitor": { - "duration": { - "us": 15183 - }, - "id": "0069-up", - "ip": "127.0.0.1", - "name": "", - "scheme": null, - "status": "up", - "type": "http" - }, - "observer": { - "geo": { - "name": "mpls" - } - } - }, - { - "timestamp": "2019-09-11T03:40:34.375Z", - "http": { - "response": { - "status_code": 200, - "body": { - "bytes": 3, - "hash": "27badc983df1780b60c2b3fa9d3a19a00e46aac798451f0febdca52920faaddf", - "content": null, - "content_bytes": null - } - } - }, - "error": null, - "monitor": { - "duration": { - "us": 15013 - }, - "id": "0093-up", - "ip": "127.0.0.1", - "name": "", - "scheme": null, - "status": "up", - "type": "http" - }, - "observer": { - "geo": { - "name": "mpls" - } - } - }, - { - "timestamp": "2019-09-11T03:40:34.375Z", - "http": { - "response": { - "status_code": 400, - "body": { - "bytes": 3, - "hash": "26d228663f13a88592a12d16cf9587caab0388b262d6d9f126ed62f9333aca94", - "content": "400", - "content_bytes": 3 - } - } - }, - "error": { - "message": "400 Bad Request", - "type": "validate" - }, - "monitor": { - "duration": { - "us": 15117 - }, - "id": "0070-down", - "ip": "127.0.0.1", - "name": "", - "scheme": null, - "status": "down", - "type": "http" - }, - "observer": { - "geo": { - "name": "mpls" - } - } - }, - { - "timestamp": "2019-09-11T03:40:34.375Z", - "http": { - "response": { - "status_code": 200, - "body": { - "bytes": 3, - "hash": "27badc983df1780b60c2b3fa9d3a19a00e46aac798451f0febdca52920faaddf", - "content": null, - "content_bytes": null - } - } - }, - "error": null, - "monitor": { - "duration": { - "us": 14875 - }, - "id": "0071-up", - "ip": "127.0.0.1", - "name": "", - "scheme": null, - "status": "up", - "type": "http" - }, - "observer": { - "geo": { - "name": "mpls" - } - } - }, - { - "timestamp": "2019-09-11T03:40:34.375Z", - "http": { - "response": { - "status_code": 200, - "body": { - "bytes": 3, - "hash": "27badc983df1780b60c2b3fa9d3a19a00e46aac798451f0febdca52920faaddf", - "content": null, - "content_bytes": null - } - } - }, - "error": null, - "monitor": { - "duration": { - "us": 14801 - }, - "id": "0095-up", - "ip": "127.0.0.1", - "name": "", - "scheme": null, - "status": "up", - "type": "http" - }, - "observer": { - "geo": { - "name": "mpls" - } - } - }, - { - "timestamp": "2019-09-11T03:40:34.375Z", - "http": { - "response": { - "status_code": 200, - "body": { - "bytes": 3, - "hash": "27badc983df1780b60c2b3fa9d3a19a00e46aac798451f0febdca52920faaddf", - "content": null, - "content_bytes": null - } - } - }, - "error": null, - "monitor": { - "duration": { - "us": 15065 - }, - "id": "0032-up", - "ip": "127.0.0.1", - "name": "", - "scheme": null, - "status": "up", - "type": "http" - }, - "observer": { - "geo": { - "name": "mpls" - } - } - }, - { - "timestamp": "2019-09-11T03:40:34.375Z", - "http": { - "response": { - "status_code": 200, - "body": { - "bytes": 3, - "hash": "27badc983df1780b60c2b3fa9d3a19a00e46aac798451f0febdca52920faaddf", - "content": null, - "content_bytes": null - } - } - }, - "error": null, - "monitor": { - "duration": { - "us": 14911 - }, - "id": "0094-up", - "ip": "127.0.0.1", - "name": "", - "scheme": null, - "status": "up", - "type": "http" - }, - "observer": { - "geo": { - "name": "mpls" - } - } - }, - { - "timestamp": "2019-09-11T03:40:34.374Z", - "http": { - "response": { - "status_code": 200, - "body": { - "bytes": 3, - "hash": "27badc983df1780b60c2b3fa9d3a19a00e46aac798451f0febdca52920faaddf", - "content": null, - "content_bytes": null - } - } - }, - "error": null, - "monitor": { - "duration": { - "us": 16135 - }, - "id": "0046-up", - "ip": "127.0.0.1", - "name": "", - "scheme": null, - "status": "up", - "type": "http" - }, - "observer": { - "geo": { - "name": "mpls" - } - } - }, - { - "timestamp": "2019-09-11T03:40:34.374Z", - "http": { - "response": { - "status_code": 200, - "body": { - "bytes": 3, - "hash": "27badc983df1780b60c2b3fa9d3a19a00e46aac798451f0febdca52920faaddf", - "content": null, - "content_bytes": null - } - } - }, - "error": null, - "monitor": { - "duration": { - "us": 15428 - }, - "id": "0091-up", - "ip": "127.0.0.1", - "name": "", - "scheme": null, - "status": "up", - "type": "http" - }, - "observer": { - "geo": { - "name": "mpls" - } - } - }, - { - "timestamp": "2019-09-11T03:40:34.374Z", - "http": { - "response": { - "status_code": 200, - "body": { - "bytes": 3, - "hash": "27badc983df1780b60c2b3fa9d3a19a00e46aac798451f0febdca52920faaddf", - "content": null, - "content_bytes": null - } - } - }, - "error": null, - "monitor": { - "duration": { - "us": 15499 - }, - "id": "0067-up", - "ip": "127.0.0.1", - "name": "", - "scheme": null, - "status": "up", - "type": "http" - }, - "observer": { - "geo": { - "name": "mpls" - } - } - }, - { - "timestamp": "2019-09-11T03:40:34.374Z", - "http": { - "response": { - "status_code": 200, - "body": { - "bytes": 3, - "hash": "27badc983df1780b60c2b3fa9d3a19a00e46aac798451f0febdca52920faaddf", - "content": null, - "content_bytes": null - } - } - }, - "error": null, - "monitor": { - "duration": { - "us": 15464 - }, - "id": "0068-up", - "ip": "127.0.0.1", - "name": "", - "scheme": null, - "status": "up", - "type": "http" - }, - "observer": { - "geo": { - "name": "mpls" - } - } - }, - { - "timestamp": "2019-09-11T03:40:34.374Z", - "http": { - "response": { - "status_code": 200, - "body": { - "bytes": 3, - "hash": "27badc983df1780b60c2b3fa9d3a19a00e46aac798451f0febdca52920faaddf", - "content": null, - "content_bytes": null - } - } - }, - "error": null, - "monitor": { - "duration": { - "us": 21736 - }, - "id": "0090-intermittent", - "ip": "127.0.0.1", - "name": "", - "scheme": null, - "status": "up", - "type": "http" - }, - "observer": { - "geo": { - "name": "mpls" - } - } - }, - { - "timestamp": "2019-09-11T03:40:34.374Z", - "http": { - "response": { - "status_code": 200, - "body": { - "bytes": 3, - "hash": "27badc983df1780b60c2b3fa9d3a19a00e46aac798451f0febdca52920faaddf", - "content": null, - "content_bytes": null - } - } - }, - "error": null, - "monitor": { - "duration": { - "us": 21874 - }, - "id": "0031-up", - "ip": "127.0.0.1", - "name": "", - "scheme": null, - "status": "up", - "type": "http" - }, - "observer": { - "geo": { - "name": "mpls" - } - } - }, - { - "timestamp": "2019-09-11T03:40:34.374Z", - "http": { - "response": { - "status_code": 200, - "body": { - "bytes": 3, - "hash": "27badc983df1780b60c2b3fa9d3a19a00e46aac798451f0febdca52920faaddf", - "content": null, - "content_bytes": null - } - } - }, - "error": null, - "monitor": { - "duration": { - "us": 36584 - }, - "id": "0066-up", - "ip": "127.0.0.1", - "name": "", - "scheme": null, - "status": "up", - "type": "http" - }, - "observer": { - "geo": { - "name": "mpls" - } - } - }, - { - "timestamp": "2019-09-11T03:40:34.373Z", - "http": { - "response": { - "status_code": 200, - "body": { - "bytes": 3, - "hash": "27badc983df1780b60c2b3fa9d3a19a00e46aac798451f0febdca52920faaddf", - "content": null, - "content_bytes": null - } - } - }, - "error": null, - "monitor": { - "duration": { - "us": 3148 - }, - "id": "0084-up", - "ip": "127.0.0.1", - "name": "", - "scheme": null, - "status": "up", - "type": "http" - }, - "observer": { - "geo": { - "name": "mpls" - } - } - }, - { - "timestamp": "2019-09-11T03:40:34.373Z", - "http": { - "response": { - "status_code": 200, - "body": { - "bytes": 3, - "hash": "27badc983df1780b60c2b3fa9d3a19a00e46aac798451f0febdca52920faaddf", - "content": null, - "content_bytes": null - } - } - }, - "error": null, - "monitor": { - "duration": { - "us": 13442 - }, - "id": "0083-up", - "ip": "127.0.0.1", - "name": "", - "scheme": null, - "status": "up", - "type": "http" - }, - "observer": { - "geo": { - "name": "mpls" - } - } - }, - { - "timestamp": "2019-09-11T03:40:34.373Z", - "http": { - "response": { - "status_code": 200, - "body": { - "bytes": 3, - "hash": "27badc983df1780b60c2b3fa9d3a19a00e46aac798451f0febdca52920faaddf", - "content": null, - "content_bytes": null - } - } - }, - "error": null, - "monitor": { - "duration": { - "us": 13666 - }, - "id": "0041-up", - "ip": "127.0.0.1", - "name": "", - "scheme": null, - "status": "up", - "type": "http" - }, - "observer": { - "geo": { - "name": "mpls" - } - } - }, - { - "timestamp": "2019-09-11T03:40:34.373Z", - "http": { - "response": { - "status_code": 200, - "body": { - "bytes": 3, - "hash": "27badc983df1780b60c2b3fa9d3a19a00e46aac798451f0febdca52920faaddf", - "content": null, - "content_bytes": null - } - } - }, - "error": null, - "monitor": { - "duration": { - "us": 16290 - }, - "id": "0045-intermittent", - "ip": "127.0.0.1", - "name": "", - "scheme": null, - "status": "up", - "type": "http" - }, - "observer": { - "geo": { - "name": "mpls" - } - } - }, - { - "timestamp": "2019-09-11T03:40:34.373Z", - "http": { - "response": { - "status_code": 200, - "body": { - "bytes": 3, - "hash": "27badc983df1780b60c2b3fa9d3a19a00e46aac798451f0febdca52920faaddf", - "content": null, - "content_bytes": null - } - } - }, - "error": null, - "monitor": { - "duration": { - "us": 17255 - }, - "id": "0042-up", - "ip": "127.0.0.1", - "name": "", - "scheme": null, - "status": "up", - "type": "http" - }, - "observer": { - "geo": { - "name": "mpls" - } - } - }, - { - "timestamp": "2019-09-11T03:40:34.373Z", - "http": { - "response": { - "status_code": 500, - "body": { - "bytes": 3, - "hash": "0604cd3138feed202ef293e062da2f4720f77a05d25ee036a7a01c9cfcdd1f0a", - "content": "500", - "content_bytes": 3 - } - } - }, - "error": { - "message": "500 Internal Server Error", - "type": "validate" - }, - "monitor": { - "duration": { - "us": 17146 - }, - "id": "0030-intermittent", - "ip": "127.0.0.1", - "name": "", - "scheme": null, - "status": "down", - "type": "http" - }, - "observer": { - "geo": { - "name": "mpls" - } - } - }, - { - "timestamp": "2019-09-11T03:40:34.373Z", - "http": { - "response": { - "status_code": 200, - "body": { - "bytes": 3, - "hash": "27badc983df1780b60c2b3fa9d3a19a00e46aac798451f0febdca52920faaddf", - "content": null, - "content_bytes": null - } - } - }, - "error": null, - "monitor": { - "duration": { - "us": 17770 - }, - "id": "0063-up", - "ip": "127.0.0.1", - "name": "", - "scheme": null, - "status": "up", - "type": "http" - }, - "observer": { - "geo": { - "name": "mpls" - } - } - }, - { - "timestamp": "2019-09-11T03:40:34.373Z", - "http": { - "response": { - "status_code": 200, - "body": { - "bytes": 3, - "hash": "27badc983df1780b60c2b3fa9d3a19a00e46aac798451f0febdca52920faaddf", - "content": null, - "content_bytes": null - } - } - }, - "error": null, - "monitor": { - "duration": { - "us": 18194 - }, - "id": "0061-up", - "ip": "127.0.0.1", - "name": "", - "scheme": null, - "status": "up", - "type": "http" - }, - "observer": { - "geo": { - "name": "mpls" - } - } - }, - { - "timestamp": "2019-09-11T03:40:34.373Z", - "http": { - "response": { - "status_code": 200, - "body": { - "bytes": 3, - "hash": "27badc983df1780b60c2b3fa9d3a19a00e46aac798451f0febdca52920faaddf", - "content": null, - "content_bytes": null - } - } - }, - "error": null, - "monitor": { - "duration": { - "us": 17587 - }, - "id": "0065-up", - "ip": "127.0.0.1", - "name": "", - "scheme": null, - "status": "up", - "type": "http" - }, - "observer": { - "geo": { - "name": "mpls" - } - } - }, - { - "timestamp": "2019-09-11T03:40:34.373Z", - "http": { - "response": { - "status_code": 200, - "body": { - "bytes": 3, - "hash": "27badc983df1780b60c2b3fa9d3a19a00e46aac798451f0febdca52920faaddf", - "content": null, - "content_bytes": null - } - } - }, - "error": null, - "monitor": { - "duration": { - "us": 22666 - }, - "id": "0062-up", - "ip": "127.0.0.1", - "name": "", - "scheme": null, - "status": "up", - "type": "http" - }, - "observer": { - "geo": { - "name": "mpls" - } - } - }, - { - "timestamp": "2019-09-11T03:40:34.373Z", - "http": { - "response": { - "status_code": 200, - "body": { - "bytes": 3, - "hash": "27badc983df1780b60c2b3fa9d3a19a00e46aac798451f0febdca52920faaddf", - "content": null, - "content_bytes": null - } - } - }, - "error": null, - "monitor": { - "duration": { - "us": 33311 - }, - "id": "0026-up", - "ip": "127.0.0.1", - "name": "", - "scheme": null, - "status": "up", - "type": "http" - }, - "observer": { - "geo": { - "name": "mpls" - } - } - }, - { - "timestamp": "2019-09-11T03:40:34.373Z", - "http": { - "response": { - "status_code": 200, - "body": { - "bytes": 3, - "hash": "27badc983df1780b60c2b3fa9d3a19a00e46aac798451f0febdca52920faaddf", - "content": null, - "content_bytes": null - } - } - }, - "error": null, - "monitor": { - "duration": { - "us": 33506 - }, - "id": "0085-up", - "ip": "127.0.0.1", - "name": "", - "scheme": null, - "status": "up", - "type": "http" - }, - "observer": { - "geo": { - "name": "mpls" - } - } - }, - { - "timestamp": "2019-09-11T03:40:34.373Z", - "http": { - "response": { - "status_code": 200, - "body": { - "bytes": 3, - "hash": "27badc983df1780b60c2b3fa9d3a19a00e46aac798451f0febdca52920faaddf", - "content": null, - "content_bytes": null - } - } - }, - "error": null, - "monitor": { - "duration": { - "us": 33974 - }, - "id": "0025-up", - "ip": "127.0.0.1", - "name": "", - "scheme": null, - "status": "up", - "type": "http" - }, - "observer": { - "geo": { - "name": "mpls" - } - } - }, - { - "timestamp": "2019-09-11T03:40:34.373Z", - "http": { - "response": { - "status_code": 200, - "body": { - "bytes": 3, - "hash": "27badc983df1780b60c2b3fa9d3a19a00e46aac798451f0febdca52920faaddf", - "content": null, - "content_bytes": null - } - } - }, - "error": null, - "monitor": { - "duration": { - "us": 33693 - }, - "id": "0088-up", - "ip": "127.0.0.1", - "name": "", - "scheme": null, - "status": "up", - "type": "http" - }, - "observer": { - "geo": { - "name": "mpls" - } - } - }, - { - "timestamp": "2019-09-11T03:40:34.373Z", - "http": { - "response": { - "status_code": 200, - "body": { - "bytes": 3, - "hash": "27badc983df1780b60c2b3fa9d3a19a00e46aac798451f0febdca52920faaddf", - "content": null, - "content_bytes": null - } - } - }, - "error": null, - "monitor": { - "duration": { - "us": 33833 - }, - "id": "0089-up", - "ip": "127.0.0.1", - "name": "", - "scheme": null, - "status": "up", - "type": "http" - }, - "observer": { - "geo": { - "name": "mpls" - } - } - }, - { - "timestamp": "2019-09-11T03:40:34.373Z", - "http": { - "response": { - "status_code": 200, - "body": { - "bytes": 3, - "hash": "27badc983df1780b60c2b3fa9d3a19a00e46aac798451f0febdca52920faaddf", - "content": null, - "content_bytes": null - } - } - }, - "error": null, - "monitor": { - "duration": { - "us": 34600 - }, - "id": "0087-up", - "ip": "127.0.0.1", - "name": "", - "scheme": null, - "status": "up", - "type": "http" - }, - "observer": { - "geo": { - "name": "mpls" - } - } - }, - { - "timestamp": "2019-09-11T03:40:34.373Z", - "http": { - "response": { - "status_code": 200, - "body": { - "bytes": 3, - "hash": "27badc983df1780b60c2b3fa9d3a19a00e46aac798451f0febdca52920faaddf", - "content": null, - "content_bytes": null - } - } - }, - "error": null, - "monitor": { - "duration": { - "us": 35573 - }, - "id": "0028-up", - "ip": "127.0.0.1", - "name": "", - "scheme": null, - "status": "up", - "type": "http" - }, - "observer": { - "geo": { - "name": "mpls" - } - } - }, - { - "timestamp": "2019-09-11T03:40:34.373Z", - "http": { - "response": { - "status_code": 200, - "body": { - "bytes": 3, - "hash": "27badc983df1780b60c2b3fa9d3a19a00e46aac798451f0febdca52920faaddf", - "content": null, - "content_bytes": null - } - } - }, - "error": null, - "monitor": { - "duration": { - "us": 35830 - }, - "id": "0086-up", - "ip": "127.0.0.1", - "name": "", - "scheme": null, - "status": "up", - "type": "http" - }, - "observer": { - "geo": { - "name": "mpls" - } - } - }, - { - "timestamp": "2019-09-11T03:40:34.373Z", - "http": { - "response": { - "status_code": 200, - "body": { - "bytes": 3, - "hash": "27badc983df1780b60c2b3fa9d3a19a00e46aac798451f0febdca52920faaddf", - "content": null, - "content_bytes": null - } - } - }, - "error": null, - "monitor": { - "duration": { - "us": 35698 - }, - "id": "0064-up", - "ip": "127.0.0.1", - "name": "", - "scheme": null, - "status": "up", - "type": "http" - }, - "observer": { - "geo": { - "name": "mpls" - } - } - }, - { - "timestamp": "2019-09-11T03:40:34.373Z", - "http": { - "response": { - "status_code": 200, - "body": { - "bytes": 3, - "hash": "27badc983df1780b60c2b3fa9d3a19a00e46aac798451f0febdca52920faaddf", - "content": null, - "content_bytes": null - } - } - }, - "error": null, - "monitor": { - "duration": { - "us": 35594 - }, - "id": "0029-up", - "ip": "127.0.0.1", - "name": "", - "scheme": null, - "status": "up", - "type": "http" - }, - "observer": { - "geo": { - "name": "mpls" - } - } - }, - { - "timestamp": "2019-09-11T03:40:34.373Z", - "http": { - "response": { - "status_code": 200, - "body": { - "bytes": 3, - "hash": "27badc983df1780b60c2b3fa9d3a19a00e46aac798451f0febdca52920faaddf", - "content": null, - "content_bytes": null - } - } - }, - "error": null, - "monitor": { - "duration": { - "us": 35652 - }, - "id": "0044-up", - "ip": "127.0.0.1", - "name": "", - "scheme": null, - "status": "up", - "type": "http" - }, - "observer": { - "geo": { - "name": "mpls" - } - } - } - ] - } -} \ No newline at end of file diff --git a/x-pack/test/api_integration/apis/uptime/graphql/fixtures/ping_list_monitor_id.json b/x-pack/test/api_integration/apis/uptime/graphql/fixtures/ping_list_monitor_id.json deleted file mode 100644 index 5826fd9f3f540..0000000000000 --- a/x-pack/test/api_integration/apis/uptime/graphql/fixtures/ping_list_monitor_id.json +++ /dev/null @@ -1,475 +0,0 @@ -{ - "allPings": { - "total": 20, - "locations": [ - "mpls" - ], - "pings": [ - { - "timestamp": "2019-09-11T03:40:34.371Z", - "http": { - "response": { - "status_code": 200, - "body": { - "bytes": 3, - "hash": "27badc983df1780b60c2b3fa9d3a19a00e46aac798451f0febdca52920faaddf", - "content": null, - "content_bytes": null - } - } - }, - "error": null, - "monitor": { - "duration": { - "us": 35534 - }, - "id": "0001-up", - "ip": "127.0.0.1", - "name": "", - "scheme": null, - "status": "up", - "type": "http" - }, - "observer": { - "geo": { - "name": "mpls" - } - } - }, - { - "timestamp": "2019-09-11T03:40:04.370Z", - "http": { - "response": { - "status_code": 200, - "body": { - "bytes": 3, - "hash": "27badc983df1780b60c2b3fa9d3a19a00e46aac798451f0febdca52920faaddf", - "content": null, - "content_bytes": null - } - } - }, - "error": null, - "monitor": { - "duration": { - "us": 3080 - }, - "id": "0001-up", - "ip": "127.0.0.1", - "name": "", - "scheme": null, - "status": "up", - "type": "http" - }, - "observer": { - "geo": { - "name": "mpls" - } - } - }, - { - "timestamp": "2019-09-11T03:39:34.370Z", - "http": { - "response": { - "status_code": 200, - "body": { - "bytes": 3, - "hash": "27badc983df1780b60c2b3fa9d3a19a00e46aac798451f0febdca52920faaddf", - "content": null, - "content_bytes": null - } - } - }, - "error": null, - "monitor": { - "duration": { - "us": 7810 - }, - "id": "0001-up", - "ip": "127.0.0.1", - "name": "", - "scheme": null, - "status": "up", - "type": "http" - }, - "observer": { - "geo": { - "name": "mpls" - } - } - }, - { - "timestamp": "2019-09-11T03:39:04.371Z", - "http": { - "response": { - "status_code": 200, - "body": { - "bytes": 3, - "hash": "27badc983df1780b60c2b3fa9d3a19a00e46aac798451f0febdca52920faaddf", - "content": null, - "content_bytes": null - } - } - }, - "error": null, - "monitor": { - "duration": { - "us": 1575 - }, - "id": "0001-up", - "ip": "127.0.0.1", - "name": "", - "scheme": null, - "status": "up", - "type": "http" - }, - "observer": { - "geo": { - "name": "mpls" - } - } - }, - { - "timestamp": "2019-09-11T03:38:34.370Z", - "http": { - "response": { - "status_code": 200, - "body": { - "bytes": 3, - "hash": "27badc983df1780b60c2b3fa9d3a19a00e46aac798451f0febdca52920faaddf", - "content": null, - "content_bytes": null - } - } - }, - "error": null, - "monitor": { - "duration": { - "us": 1787 - }, - "id": "0001-up", - "ip": "127.0.0.1", - "name": "", - "scheme": null, - "status": "up", - "type": "http" - }, - "observer": { - "geo": { - "name": "mpls" - } - } - }, - { - "timestamp": "2019-09-11T03:38:04.370Z", - "http": { - "response": { - "status_code": 200, - "body": { - "bytes": 3, - "hash": "27badc983df1780b60c2b3fa9d3a19a00e46aac798451f0febdca52920faaddf", - "content": null, - "content_bytes": null - } - } - }, - "error": null, - "monitor": { - "duration": { - "us": 654 - }, - "id": "0001-up", - "ip": "127.0.0.1", - "name": "", - "scheme": null, - "status": "up", - "type": "http" - }, - "observer": { - "geo": { - "name": "mpls" - } - } - }, - { - "timestamp": "2019-09-11T03:37:34.370Z", - "http": { - "response": { - "status_code": 200, - "body": { - "bytes": 3, - "hash": "27badc983df1780b60c2b3fa9d3a19a00e46aac798451f0febdca52920faaddf", - "content": null, - "content_bytes": null - } - } - }, - "error": null, - "monitor": { - "duration": { - "us": 15915 - }, - "id": "0001-up", - "ip": "127.0.0.1", - "name": "", - "scheme": null, - "status": "up", - "type": "http" - }, - "observer": { - "geo": { - "name": "mpls" - } - } - }, - { - "timestamp": "2019-09-11T03:37:04.370Z", - "http": { - "response": { - "status_code": 200, - "body": { - "bytes": 3, - "hash": "27badc983df1780b60c2b3fa9d3a19a00e46aac798451f0febdca52920faaddf", - "content": null, - "content_bytes": null - } - } - }, - "error": null, - "monitor": { - "duration": { - "us": 2679 - }, - "id": "0001-up", - "ip": "127.0.0.1", - "name": "", - "scheme": null, - "status": "up", - "type": "http" - }, - "observer": { - "geo": { - "name": "mpls" - } - } - }, - { - "timestamp": "2019-09-11T03:36:34.371Z", - "http": { - "response": { - "status_code": 200, - "body": { - "bytes": 3, - "hash": "27badc983df1780b60c2b3fa9d3a19a00e46aac798451f0febdca52920faaddf", - "content": null, - "content_bytes": null - } - } - }, - "error": null, - "monitor": { - "duration": { - "us": 2104 - }, - "id": "0001-up", - "ip": "127.0.0.1", - "name": "", - "scheme": null, - "status": "up", - "type": "http" - }, - "observer": { - "geo": { - "name": "mpls" - } - } - }, - { - "timestamp": "2019-09-11T03:36:04.370Z", - "http": { - "response": { - "status_code": 200, - "body": { - "bytes": 3, - "hash": "27badc983df1780b60c2b3fa9d3a19a00e46aac798451f0febdca52920faaddf", - "content": null, - "content_bytes": null - } - } - }, - "error": null, - "monitor": { - "duration": { - "us": 5759 - }, - "id": "0001-up", - "ip": "127.0.0.1", - "name": "", - "scheme": null, - "status": "up", - "type": "http" - }, - "observer": { - "geo": { - "name": "mpls" - } - } - }, - { - "timestamp": "2019-09-11T03:35:34.373Z", - "http": { - "response": { - "status_code": 200, - "body": { - "bytes": 3, - "hash": "27badc983df1780b60c2b3fa9d3a19a00e46aac798451f0febdca52920faaddf", - "content": null, - "content_bytes": null - } - } - }, - "error": null, - "monitor": { - "duration": { - "us": 7166 - }, - "id": "0001-up", - "ip": "127.0.0.1", - "name": "", - "scheme": null, - "status": "up", - "type": "http" - }, - "observer": { - "geo": { - "name": "mpls" - } - } - }, - { - "timestamp": "2019-09-11T03:35:04.371Z", - "http": { - "response": { - "status_code": 200, - "body": { - "bytes": 3, - "hash": "27badc983df1780b60c2b3fa9d3a19a00e46aac798451f0febdca52920faaddf", - "content": null, - "content_bytes": null - } - } - }, - "error": null, - "monitor": { - "duration": { - "us": 26830 - }, - "id": "0001-up", - "ip": "127.0.0.1", - "name": "", - "scheme": null, - "status": "up", - "type": "http" - }, - "observer": { - "geo": { - "name": "mpls" - } - } - }, - { - "timestamp": "2019-09-11T03:34:34.371Z", - "http": { - "response": { - "status_code": 200, - "body": { - "bytes": 3, - "hash": "27badc983df1780b60c2b3fa9d3a19a00e46aac798451f0febdca52920faaddf", - "content": null, - "content_bytes": null - } - } - }, - "error": null, - "monitor": { - "duration": { - "us": 993 - }, - "id": "0001-up", - "ip": "127.0.0.1", - "name": "", - "scheme": null, - "status": "up", - "type": "http" - }, - "observer": { - "geo": { - "name": "mpls" - } - } - }, - { - "timestamp": "2019-09-11T03:34:04.381Z", - "http": { - "response": { - "status_code": 200, - "body": { - "bytes": 3, - "hash": "27badc983df1780b60c2b3fa9d3a19a00e46aac798451f0febdca52920faaddf", - "content": null, - "content_bytes": null - } - } - }, - "error": null, - "monitor": { - "duration": { - "us": 3880 - }, - "id": "0001-up", - "ip": "127.0.0.1", - "name": "", - "scheme": null, - "status": "up", - "type": "http" - }, - "observer": { - "geo": { - "name": "mpls" - } - } - }, - { - "timestamp": "2019-09-11T03:33:34.371Z", - "http": { - "response": { - "status_code": 200, - "body": { - "bytes": 3, - "hash": "27badc983df1780b60c2b3fa9d3a19a00e46aac798451f0febdca52920faaddf", - "content": null, - "content_bytes": null - } - } - }, - "error": null, - "monitor": { - "duration": { - "us": 1604 - }, - "id": "0001-up", - "ip": "127.0.0.1", - "name": "", - "scheme": null, - "status": "up", - "type": "http" - }, - "observer": { - "geo": { - "name": "mpls" - } - } - } - ] - } -} \ No newline at end of file diff --git a/x-pack/test/api_integration/apis/uptime/graphql/fixtures/ping_list_sort.json b/x-pack/test/api_integration/apis/uptime/graphql/fixtures/ping_list_sort.json deleted file mode 100644 index b9b8deae2e564..0000000000000 --- a/x-pack/test/api_integration/apis/uptime/graphql/fixtures/ping_list_sort.json +++ /dev/null @@ -1,165 +0,0 @@ -{ - "allPings": { - "total": 20, - "locations": [ - "mpls" - ], - "pings": [ - { - "timestamp": "2019-09-11T03:31:04.380Z", - "http": { - "response": { - "status_code": 200, - "body": { - "bytes": 3, - "hash": "27badc983df1780b60c2b3fa9d3a19a00e46aac798451f0febdca52920faaddf", - "content": null, - "content_bytes": null - } - } - }, - "error": null, - "monitor": { - "duration": { - "us": 56940 - }, - "id": "0001-up", - "ip": "127.0.0.1", - "name": "", - "scheme": null, - "status": "up", - "type": "http" - }, - "observer": { - "geo": { - "name": "mpls" - } - } - }, - { - "timestamp": "2019-09-11T03:31:34.366Z", - "http": { - "response": { - "status_code": 200, - "body": { - "bytes": 3, - "hash": "27badc983df1780b60c2b3fa9d3a19a00e46aac798451f0febdca52920faaddf", - "content": null, - "content_bytes": null - } - } - }, - "error": null, - "monitor": { - "duration": { - "us": 9861 - }, - "id": "0001-up", - "ip": "127.0.0.1", - "name": "", - "scheme": null, - "status": "up", - "type": "http" - }, - "observer": { - "geo": { - "name": "mpls" - } - } - }, - { - "timestamp": "2019-09-11T03:32:04.372Z", - "http": { - "response": { - "status_code": 200, - "body": { - "bytes": 3, - "hash": "27badc983df1780b60c2b3fa9d3a19a00e46aac798451f0febdca52920faaddf", - "content": null, - "content_bytes": null - } - } - }, - "error": null, - "monitor": { - "duration": { - "us": 2924 - }, - "id": "0001-up", - "ip": "127.0.0.1", - "name": "", - "scheme": null, - "status": "up", - "type": "http" - }, - "observer": { - "geo": { - "name": "mpls" - } - } - }, - { - "timestamp": "2019-09-11T03:32:34.375Z", - "http": { - "response": { - "status_code": 200, - "body": { - "bytes": 3, - "hash": "27badc983df1780b60c2b3fa9d3a19a00e46aac798451f0febdca52920faaddf", - "content": null, - "content_bytes": null - } - } - }, - "error": null, - "monitor": { - "duration": { - "us": 21665 - }, - "id": "0001-up", - "ip": "127.0.0.1", - "name": "", - "scheme": null, - "status": "up", - "type": "http" - }, - "observer": { - "geo": { - "name": "mpls" - } - } - }, - { - "timestamp": "2019-09-11T03:33:04.370Z", - "http": { - "response": { - "status_code": 200, - "body": { - "bytes": 3, - "hash": "27badc983df1780b60c2b3fa9d3a19a00e46aac798451f0febdca52920faaddf", - "content": null, - "content_bytes": null - } - } - }, - "error": null, - "monitor": { - "duration": { - "us": 2128 - }, - "id": "0001-up", - "ip": "127.0.0.1", - "name": "", - "scheme": null, - "status": "up", - "type": "http" - }, - "observer": { - "geo": { - "name": "mpls" - } - } - } - ] - } -} \ No newline at end of file diff --git a/x-pack/test/api_integration/apis/uptime/graphql/index.ts b/x-pack/test/api_integration/apis/uptime/graphql/index.ts deleted file mode 100644 index 2e0b5e2eea2a5..0000000000000 --- a/x-pack/test/api_integration/apis/uptime/graphql/index.ts +++ /dev/null @@ -1,18 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { FtrProviderContext } from '../../../ftr_provider_context'; - -export default function({ loadTestFile }: FtrProviderContext) { - describe('graphql', () => { - // each of these test files imports a GQL query from - // the uptime app and runs it against the live HTTP server, - // verifying the pre-loaded documents are returned in a way that - // matches the snapshots contained in './fixtures' - loadTestFile(require.resolve('./monitor_states')); - loadTestFile(require.resolve('./ping_list')); - }); -} diff --git a/x-pack/test/api_integration/apis/uptime/graphql/monitor_states.ts b/x-pack/test/api_integration/apis/uptime/graphql/monitor_states.ts deleted file mode 100644 index 216560583249c..0000000000000 --- a/x-pack/test/api_integration/apis/uptime/graphql/monitor_states.ts +++ /dev/null @@ -1,245 +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 expect from '@kbn/expect'; -import { expectFixtureEql } from './helpers/expect_fixture_eql'; -import { FtrProviderContext } from '../../../ftr_provider_context'; -import { makeChecksWithStatus } from './helpers/make_checks'; -import { monitorStatesQueryString } from '../../../../../legacy/plugins/uptime/public/queries/monitor_states_query'; -import { MonitorSummary } from '../../../../../legacy/plugins/uptime/common/graphql/types'; - -export default function({ getService }: FtrProviderContext) { - const supertest = getService('supertest'); - let dateRangeStart: string; - let dateRangeEnd: string; - - const getMonitorStates = async (variables: { [key: string]: any } = {}) => { - const query = { - operationName: 'MonitorStates', - query: monitorStatesQueryString, - variables: { - dateRangeStart, - dateRangeEnd, - pageSize: 10, - ...variables, - }, - }; - - const { - body: { data }, - } = await supertest - .post('/api/uptime/graphql') - .set('kbn-xsrf', 'foo') - .send({ ...query }); - - return data; - }; - - describe('monitor states', async () => { - describe('with real world data', () => { - before('load heartbeat data', () => getService('esArchiver').load('uptime/full_heartbeat')); - after('unload heartbeat index', () => - getService('esArchiver').unload('uptime/full_heartbeat') - ); - - before('set start/end', () => { - dateRangeStart = '2019-09-11T03:31:04.380Z'; - dateRangeEnd = '2019-09-11T03:40:34.410Z'; - }); - - it('will fetch monitor state data for the given filters and range', async () => { - const data: any = await getMonitorStates({ - statusFilter: 'up', - filters: - '{"bool":{"must":[{"match":{"monitor.id":{"query":"0002-up","operator":"and"}}}]}}', - }); - // await new Promise(r => setTimeout(r, 90000)); - expectFixtureEql(data, 'monitor_states_id_filtered'); - }); - - it('will fetch monitor state data for the given date range', async () => { - expectFixtureEql(await getMonitorStates(), 'monitor_states'); - }); - - it('can navigate forward and backward using pagination', async () => { - const expectedResultsCount = 100; - const expectedPageCount = expectedResultsCount / 10; - - let pagination: string | null = null; - for (let page = 1; page <= expectedPageCount; page++) { - const data: any = await getMonitorStates({ pagination }); - pagination = data.monitorStates.nextPagePagination; - expectFixtureEql(data, `monitor_states_page_${page}`); - - // Test to see if the previous page pagination works on every page (other than the first) - if (page > 1) { - const prevData = await getMonitorStates({ - pagination: data.monitorStates.prevPagePagination, - }); - expectFixtureEql(prevData, `monitor_states_page_${page}_previous`); - } - } - }); - }); - - describe('monitor state scoping', async () => { - const numIps = 4; // Must be > 2 for IP uniqueness checks - - before('load heartbeat data', () => getService('esArchiver').load('uptime/blank')); - after('unload heartbeat index', () => getService('esArchiver').unload('uptime/blank')); - - describe('query document scoping with mismatched check statuses', async () => { - let checks: any[] = []; - let nonSummaryIp: string | null = null; - const testMonitorId = 'scope-test-id'; - const makeApiParams = (monitorId: string, filterClauses: any[] = []): any => { - return { - filters: JSON.stringify({ - bool: { - filter: [{ match: { 'monitor.id': monitorId } }, ...filterClauses], - }, - }), - }; - }; - - before(async () => { - const es = getService('legacyEs'); - dateRangeStart = new Date().toISOString(); - checks = await makeChecksWithStatus(es, testMonitorId, 1, numIps, 1, {}, 'up', d => { - // turn an all up status into having at least one down - if (d.summary) { - d.monitor.status = 'down'; - d.summary.up--; - d.summary.down++; - } - return d; - }); - - dateRangeEnd = new Date().toISOString(); - nonSummaryIp = checks[0][0].monitor.ip; - }); - - it('should return all IPs', async () => { - const res = await getMonitorStates(makeApiParams(testMonitorId)); - - const uniqueIps = new Set(); - res.monitorStates.summaries[0].state.checks.forEach((c: any) => - uniqueIps.add(c.monitor.ip) - ); - - expect(uniqueIps.size).to.eql(4); - }); - - it('should match non summary documents without a status filter', async () => { - const params = makeApiParams(testMonitorId, [{ match: { 'monitor.ip': nonSummaryIp } }]); - - const nonSummaryRes = await getMonitorStates(params); - expect(nonSummaryRes.monitorStates.summaries.length).to.eql(1); - }); - - it('should not match non summary documents if the check status does not match the document status', async () => { - const params = makeApiParams(testMonitorId, [{ match: { 'monitor.ip': nonSummaryIp } }]); - params.statusFilter = 'down'; - - const nonSummaryRes = await getMonitorStates(params); - expect(nonSummaryRes.monitorStates.summaries.length).to.eql(0); - }); - - it('should not non match non summary documents if the check status does not match', async () => { - const params = makeApiParams(testMonitorId, [{ match: { 'monitor.ip': nonSummaryIp } }]); - params.statusFilter = 'up'; - - const nonSummaryRes = await getMonitorStates(params); - expect(nonSummaryRes.monitorStates.summaries.length).to.eql(0); - }); - - describe('matching outside of the date range', async () => { - before('set date range to future', () => { - const futureDate = new Date(); - - // Set dateRangeStart to one day from now - futureDate.setDate(futureDate.getDate() + 1); - dateRangeStart = futureDate.toISOString(); - - // Set dateRangeStart to two days from now - futureDate.setDate(futureDate.getDate() + 1); - dateRangeEnd = futureDate.toISOString(); - }); - - it('should not match any documents', async () => { - const params = makeApiParams(testMonitorId); - params.statusFilter = 'up'; - - const nonSummaryRes = await getMonitorStates(params); - expect(nonSummaryRes.monitorStates.summaries.length).to.eql(0); - }); - }); - }); - }); - - describe(' test status filter', async () => { - const upMonitorId = 'up-test-id'; - const downMonitorId = 'down-test-id'; - const mixMonitorId = 'mix-test-id'; - before('generate three monitors with up, down, mix state', async () => { - await getService('esArchiver').load('uptime/blank'); - - const es = getService('legacyEs'); - - const observer = { - geo: { - name: 'US-East', - location: '40.7128, -74.0060', - }, - }; - - // Generating three monitors each with two geo locations, - // One in a down state , - // One in an up state, - // One in a mix state - - dateRangeStart = new Date().toISOString(); - - await makeChecksWithStatus(es, upMonitorId, 1, 4, 1, {}, 'up'); - await makeChecksWithStatus(es, upMonitorId, 1, 4, 1, { observer }, 'up'); - - await makeChecksWithStatus(es, downMonitorId, 1, 4, 1, {}, 'down'); - await makeChecksWithStatus(es, downMonitorId, 1, 4, 1, { observer }, 'down'); - - await makeChecksWithStatus(es, mixMonitorId, 1, 4, 1, {}, 'up'); - await makeChecksWithStatus(es, mixMonitorId, 1, 4, 1, { observer }, 'down'); - - dateRangeEnd = new Date().toISOString(); - }); - - after('unload heartbeat index', () => getService('esArchiver').unload('uptime/blank')); - - it('should return all monitor when no status filter', async () => { - const { monitorStates } = await getMonitorStates({}); - expect(monitorStates.summaries.length).to.eql(3); - // Summaries are by default sorted by monitor names - expect( - monitorStates.summaries.map((summary: MonitorSummary) => summary.monitor_id) - ).to.eql([downMonitorId, mixMonitorId, upMonitorId]); - }); - - it('should return a monitor with mix state if check status filter is down', async () => { - const { monitorStates } = await getMonitorStates({ statusFilter: 'down' }); - expect(monitorStates.summaries.length).to.eql(2); - monitorStates.summaries.forEach((summary: MonitorSummary) => { - expect(summary.monitor_id).to.not.eql(upMonitorId); - }); - }); - - it('should not return a monitor with mix state if check status filter is up', async () => { - const { monitorStates } = await getMonitorStates({ statusFilter: 'up' }); - - expect(monitorStates.summaries.length).to.eql(1); - expect(monitorStates.summaries[0].monitor_id).to.eql(upMonitorId); - }); - }); - }); -} diff --git a/x-pack/test/api_integration/apis/uptime/graphql/ping_list.ts b/x-pack/test/api_integration/apis/uptime/graphql/ping_list.ts deleted file mode 100644 index c84b9c382acdd..0000000000000 --- a/x-pack/test/api_integration/apis/uptime/graphql/ping_list.ts +++ /dev/null @@ -1,116 +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 expect from '@kbn/expect'; -import { pingsQueryString } from '../../../../../legacy/plugins/uptime/public/queries'; -import { expectFixtureEql } from './helpers/expect_fixture_eql'; -import { Ping, PingResults } from '../../../../../legacy/plugins/uptime/common/graphql/types'; - -const expectPingFixtureEql = (data: { allPings: PingResults }, fixtureName: string) => { - expectFixtureEql(data, fixtureName, d => d.allPings.pings.forEach((p: Ping) => delete p.id)); -}; - -export default function({ getService }: any) { - describe('pingList query', () => { - before('load heartbeat data', () => getService('esArchiver').load('uptime/full_heartbeat')); - after('unload heartbeat index', () => getService('esArchiver').unload('uptime/full_heartbeat')); - - const supertest = getService('supertest'); - - it('returns a list of pings for the given date range and default size', async () => { - const getPingsQuery = { - operationName: 'PingList', - query: pingsQueryString, - variables: { - dateRangeStart: '2019-01-28T17:40:08.078Z', - dateRangeEnd: '2025-01-28T19:00:16.078Z', - }, - }; - const { - body: { data }, - } = await supertest - .post('/api/uptime/graphql') - .set('kbn-xsrf', 'foo') - .send({ ...getPingsQuery }); - const { - allPings: { pings }, - } = data; - expect(pings).length(10); - - expectPingFixtureEql(data, 'ping_list'); - }); - - it('returns a list of pings for the date range and given size', async () => { - const SIZE = 50; - const getPingsQuery = { - operationName: 'PingList', - query: pingsQueryString, - variables: { - dateRangeStart: '2019-01-28T17:40:08.078Z', - dateRangeEnd: '2025-01-28T19:00:16.078Z', - size: SIZE, - }, - }; - const { - body: { data }, - } = await supertest - .post('/api/uptime/graphql') - .set('kbn-xsrf', 'foo') - .send({ ...getPingsQuery }); - const { - allPings: { pings }, - } = data; - expect(pings).length(SIZE); - expectPingFixtureEql(data, 'ping_list_count'); - }); - - it('returns a list of pings for a monitor ID', async () => { - const SIZE = 15; - const MONITOR_ID = '0001-up'; - const getPingsQuery = { - operationName: 'PingList', - query: pingsQueryString, - variables: { - dateRangeStart: '2019-01-28T17:40:08.078Z', - dateRangeEnd: '2025-01-28T19:00:16.078Z', - monitorId: MONITOR_ID, - size: SIZE, - }, - }; - const { - body: { data }, - } = await supertest - .post('/api/uptime/graphql') - .set('kbn-xsrf', 'foo') - .send({ ...getPingsQuery }); - expectPingFixtureEql(data, 'ping_list_monitor_id'); - }); - - it('returns a list of pings sorted ascending', async () => { - const SIZE = 5; - const MONITOR_ID = '0001-up'; - const getPingsQuery = { - operationName: 'PingList', - query: pingsQueryString, - variables: { - dateRangeStart: '2019-01-28T17:40:08.078Z', - dateRangeEnd: '2025-01-28T19:00:16.078Z', - monitorId: MONITOR_ID, - size: SIZE, - sort: 'asc', - }, - }; - const { - body: { data }, - } = await supertest - .post('/api/uptime/graphql') - .set('kbn-xsrf', 'foo') - .send({ ...getPingsQuery }); - - expectPingFixtureEql(data, 'ping_list_sort'); - }); - }); -} diff --git a/x-pack/test/api_integration/apis/uptime/index.ts b/x-pack/test/api_integration/apis/uptime/index.ts index a21db08d58c4d..13af60196f00d 100644 --- a/x-pack/test/api_integration/apis/uptime/index.ts +++ b/x-pack/test/api_integration/apis/uptime/index.ts @@ -10,16 +10,16 @@ export default function({ getService, loadTestFile }: FtrProviderContext) { const es = getService('legacyEs'); describe('uptime', () => { - before(() => - es.indices.delete({ - index: 'heartbeat*', - ignore: [404], - }) + before( + async () => + await es.indices.delete({ + index: 'heartbeat*', + ignore: [404], + }) ); loadTestFile(require.resolve('./feature_controls')); loadTestFile(require.resolve('./get_all_pings')); - loadTestFile(require.resolve('./graphql')); loadTestFile(require.resolve('./rest')); }); } diff --git a/x-pack/test/api_integration/apis/uptime/rest/certs.ts b/x-pack/test/api_integration/apis/uptime/rest/certs.ts new file mode 100644 index 0000000000000..7510ea3f34d28 --- /dev/null +++ b/x-pack/test/api_integration/apis/uptime/rest/certs.ts @@ -0,0 +1,89 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import expect from '@kbn/expect'; +import moment from 'moment'; +import { isRight } from 'fp-ts/lib/Either'; +import { FtrProviderContext } from '../../../ftr_provider_context'; +import { API_URLS } from '../../../../../legacy/plugins/uptime/common/constants'; +import { CertType } from '../../../../../legacy/plugins/uptime/common/runtime_types'; +import { makeChecksWithStatus } from './helper/make_checks'; + +export default function({ getService }: FtrProviderContext) { + const supertest = getService('supertest'); + const legacyEsService = getService('legacyEs'); + const esArchiver = getService('esArchiver'); + + describe('certs api', () => { + describe('empty index', async () => { + it('returns empty array for no data', async () => { + const apiResponse = await supertest.get(API_URLS.CERTS); + expect(JSON.stringify(apiResponse.body)).to.eql('{"certs":[]}'); + }); + }); + + describe('when data is present', async () => { + const now = moment(); + const cnva = now.add(6, 'months').toISOString(); + const cnvb = now.subtract(23, 'weeks').toISOString(); + const monitorId = 'monitor1'; + before(async () => { + makeChecksWithStatus( + legacyEsService, + monitorId, + 3, + 1, + 10000, + { + tls: { + certificate_not_valid_after: cnva, + certificate_not_valid_before: cnvb, + server: { + x509: { + issuer: { + common_name: 'issuer-common-name', + }, + subject: { + common_name: 'subject-common-name', + }, + }, + hash: { + sha1: 'fewjio23r3', + sha256: 'few9023fijoefw', + }, + }, + }, + }, + 'up', + (d: any) => d + ); + }); + after('unload test docs', () => { + esArchiver.unload('uptime/blank'); + }); + + it('retrieves expected cert data', async () => { + const apiResponse = await supertest.get(API_URLS.CERTS); + const { body } = apiResponse; + + expect(body.certs).not.to.be(undefined); + expect(Array.isArray(body.certs)).to.be(true); + expect(body.certs).to.have.length(1); + + const decoded = CertType.decode(body.certs[0]); + expect(isRight(decoded)).to.be(true); + + const cert = body.certs[0]; + expect(Array.isArray(cert.monitors)).to.be(true); + expect(cert.monitors[0]).to.eql({ id: monitorId }); + expect(cert.certificate_not_valid_after).to.eql(cnva); + expect(cert.certificate_not_valid_before).to.eql(cnvb); + expect(cert.common_name).to.eql('subject-common-name'); + expect(cert.issuer).to.eql('issuer-common-name'); + }); + }); + }); +} diff --git a/x-pack/test/api_integration/apis/uptime/rest/doc_count.ts b/x-pack/test/api_integration/apis/uptime/rest/doc_count.ts index 3f42511dd165c..5258426cf193c 100644 --- a/x-pack/test/api_integration/apis/uptime/rest/doc_count.ts +++ b/x-pack/test/api_integration/apis/uptime/rest/doc_count.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ import { FtrProviderContext } from '../../../ftr_provider_context'; -import { expectFixtureEql } from '../graphql/helpers/expect_fixture_eql'; +import { expectFixtureEql } from './helper/expect_fixture_eql'; import { API_URLS } from '../../../../../legacy/plugins/uptime/common/constants'; export default function({ getService }: FtrProviderContext) { diff --git a/x-pack/test/api_integration/apis/uptime/rest/dynamic_settings.ts b/x-pack/test/api_integration/apis/uptime/rest/dynamic_settings.ts index f4dd7c244f8b5..a1b731169f0a0 100644 --- a/x-pack/test/api_integration/apis/uptime/rest/dynamic_settings.ts +++ b/x-pack/test/api_integration/apis/uptime/rest/dynamic_settings.ts @@ -18,7 +18,13 @@ export default function({ getService }: FtrProviderContext) { }); it('can change the settings', async () => { - const newSettings = { heartbeatIndices: 'myIndex1*' }; + const newSettings = { + heartbeatIndices: 'myIndex1*', + certificatesThresholds: { + errorState: 5, + warningState: 15, + }, + }; const postResponse = await supertest .post(`/api/uptime/dynamic_settings`) .set('kbn-xsrf', 'true') diff --git a/x-pack/test/api_integration/apis/uptime/rest/filters.ts b/x-pack/test/api_integration/apis/uptime/rest/filters.ts index 6cec6143a6d7c..35bf32a1d404d 100644 --- a/x-pack/test/api_integration/apis/uptime/rest/filters.ts +++ b/x-pack/test/api_integration/apis/uptime/rest/filters.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { expectFixtureEql } from '../graphql/helpers/expect_fixture_eql'; +import { expectFixtureEql } from './helper/expect_fixture_eql'; import { FtrProviderContext } from '../../../ftr_provider_context'; const getApiPath = (dateRangeStart: string, dateRangeEnd: string, filters?: string) => diff --git a/x-pack/test/api_integration/apis/uptime/graphql/fixtures/filters.json b/x-pack/test/api_integration/apis/uptime/rest/fixtures/filters.json similarity index 100% rename from x-pack/test/api_integration/apis/uptime/graphql/fixtures/filters.json rename to x-pack/test/api_integration/apis/uptime/rest/fixtures/filters.json diff --git a/x-pack/test/api_integration/apis/uptime/rest/fixtures/monitor_charts.json b/x-pack/test/api_integration/apis/uptime/rest/fixtures/monitor_charts.json index 8edcff158b0ae..e96b3b3b562b9 100644 --- a/x-pack/test/api_integration/apis/uptime/rest/fixtures/monitor_charts.json +++ b/x-pack/test/api_integration/apis/uptime/rest/fixtures/monitor_charts.json @@ -85,129 +85,5 @@ } ] } - ], - "status": [ - { - "x": 1568172664000, - "up": null, - "down": null, - "total": 1 - }, - { - "x": 1568172694000, - "up": null, - "down": null, - "total": 1 - }, - { - "x": 1568172724000, - "up": null, - "down": null, - "total": 1 - }, - { - "x": 1568172754000, - "up": null, - "down": null, - "total": 1 - }, - { - "x": 1568172784000, - "up": null, - "down": null, - "total": 1 - }, - { - "x": 1568172814000, - "up": null, - "down": null, - "total": 1 - }, - { - "x": 1568172844000, - "up": null, - "down": null, - "total": 1 - }, - { - "x": 1568172874000, - "up": null, - "down": null, - "total": 1 - }, - { - "x": 1568172904000, - "up": null, - "down": null, - "total": 1 - }, - { - "x": 1568172934000, - "up": null, - "down": null, - "total": 1 - }, - { - "x": 1568172964000, - "up": null, - "down": null, - "total": 1 - }, - { - "x": 1568172994000, - "up": null, - "down": null, - "total": 1 - }, - { - "x": 1568173024000, - "up": null, - "down": null, - "total": 1 - }, - { - "x": 1568173054000, - "up": null, - "down": null, - "total": 1 - }, - { - "x": 1568173084000, - "up": null, - "down": null, - "total": 1 - }, - { - "x": 1568173114000, - "up": null, - "down": null, - "total": 1 - }, - { - "x": 1568173144000, - "up": null, - "down": null, - "total": 1 - }, - { - "x": 1568173174000, - "up": null, - "down": null, - "total": 1 - }, - { - "x": 1568173204000, - "up": null, - "down": null, - "total": 1 - }, - { - "x": 1568173234000, - "up": null, - "down": null, - "total": 1 - } - ], - "durationMaxValue": 0, - "statusMaxCount": 0 + ] } \ No newline at end of file diff --git a/x-pack/test/api_integration/apis/uptime/rest/fixtures/monitor_charts_empty_sets.json b/x-pack/test/api_integration/apis/uptime/rest/fixtures/monitor_charts_empty_sets.json index 674338101bc5b..5157eace006cf 100644 --- a/x-pack/test/api_integration/apis/uptime/rest/fixtures/monitor_charts_empty_sets.json +++ b/x-pack/test/api_integration/apis/uptime/rest/fixtures/monitor_charts_empty_sets.json @@ -1,6 +1,3 @@ { - "locationDurationLines": [], - "status": [], - "durationMaxValue": 0, - "statusMaxCount": 0 + "locationDurationLines": [] } \ No newline at end of file diff --git a/x-pack/test/api_integration/apis/uptime/rest/fixtures/monitor_latest_status.json b/x-pack/test/api_integration/apis/uptime/rest/fixtures/monitor_latest_status.json index 2e5854f4d9866..9a33be807670e 100644 --- a/x-pack/test/api_integration/apis/uptime/rest/fixtures/monitor_latest_status.json +++ b/x-pack/test/api_integration/apis/uptime/rest/fixtures/monitor_latest_status.json @@ -1,5 +1,4 @@ { - "timestamp": "2019-09-11T03:40:34.371Z", "observer": { "geo": { "name": "mpls", @@ -7,6 +6,7 @@ }, "hostname": "avc-x1x" }, + "@timestamp": "2019-09-11T03:40:34.371Z", "monitor": { "duration": { "us": 24627 @@ -25,5 +25,7 @@ "domain": "localhost", "query": "r=200x1", "full": "http://localhost:5678/pattern?r=200x1" - } -} + }, + "docId": "h5toHm0B0I9WX_CznN_V", + "timestamp": "2019-09-11T03:40:34.371Z" +} \ No newline at end of file diff --git a/x-pack/test/api_integration/apis/uptime/graphql/fixtures/monitor_status.json b/x-pack/test/api_integration/apis/uptime/rest/fixtures/monitor_status.json similarity index 100% rename from x-pack/test/api_integration/apis/uptime/graphql/fixtures/monitor_status.json rename to x-pack/test/api_integration/apis/uptime/rest/fixtures/monitor_status.json diff --git a/x-pack/test/api_integration/apis/uptime/graphql/fixtures/monitor_status_all.json b/x-pack/test/api_integration/apis/uptime/rest/fixtures/monitor_status_all.json similarity index 100% rename from x-pack/test/api_integration/apis/uptime/graphql/fixtures/monitor_status_all.json rename to x-pack/test/api_integration/apis/uptime/rest/fixtures/monitor_status_all.json diff --git a/x-pack/test/api_integration/apis/uptime/graphql/fixtures/monitors_with_location.json b/x-pack/test/api_integration/apis/uptime/rest/fixtures/monitors_with_location.json similarity index 100% rename from x-pack/test/api_integration/apis/uptime/graphql/fixtures/monitors_with_location.json rename to x-pack/test/api_integration/apis/uptime/rest/fixtures/monitors_with_location.json diff --git a/x-pack/test/api_integration/apis/uptime/graphql/fixtures/snapshot.json b/x-pack/test/api_integration/apis/uptime/rest/fixtures/snapshot.json similarity index 100% rename from x-pack/test/api_integration/apis/uptime/graphql/fixtures/snapshot.json rename to x-pack/test/api_integration/apis/uptime/rest/fixtures/snapshot.json diff --git a/x-pack/test/api_integration/apis/uptime/graphql/fixtures/snapshot_empty.json b/x-pack/test/api_integration/apis/uptime/rest/fixtures/snapshot_empty.json similarity index 100% rename from x-pack/test/api_integration/apis/uptime/graphql/fixtures/snapshot_empty.json rename to x-pack/test/api_integration/apis/uptime/rest/fixtures/snapshot_empty.json diff --git a/x-pack/test/api_integration/apis/uptime/graphql/fixtures/snapshot_filtered_by_down.json b/x-pack/test/api_integration/apis/uptime/rest/fixtures/snapshot_filtered_by_down.json similarity index 100% rename from x-pack/test/api_integration/apis/uptime/graphql/fixtures/snapshot_filtered_by_down.json rename to x-pack/test/api_integration/apis/uptime/rest/fixtures/snapshot_filtered_by_down.json diff --git a/x-pack/test/api_integration/apis/uptime/graphql/fixtures/snapshot_filtered_by_up.json b/x-pack/test/api_integration/apis/uptime/rest/fixtures/snapshot_filtered_by_up.json similarity index 100% rename from x-pack/test/api_integration/apis/uptime/graphql/fixtures/snapshot_filtered_by_up.json rename to x-pack/test/api_integration/apis/uptime/rest/fixtures/snapshot_filtered_by_up.json diff --git a/x-pack/test/api_integration/apis/uptime/graphql/helpers/expect_fixture_eql.ts b/x-pack/test/api_integration/apis/uptime/rest/helper/expect_fixture_eql.ts similarity index 87% rename from x-pack/test/api_integration/apis/uptime/graphql/helpers/expect_fixture_eql.ts rename to x-pack/test/api_integration/apis/uptime/rest/helper/expect_fixture_eql.ts index d5a4f3976e079..abf5ec6f697b0 100644 --- a/x-pack/test/api_integration/apis/uptime/graphql/helpers/expect_fixture_eql.ts +++ b/x-pack/test/api_integration/apis/uptime/rest/helper/expect_fixture_eql.ts @@ -10,7 +10,6 @@ import { join } from 'path'; import { cloneDeep, isEqual } from 'lodash'; const fixturesDir = join(__dirname, '..', 'fixtures'); -const restFixturesDir = join(__dirname, '../../rest/', 'fixtures'); const excludeFieldsFrom = (from: any, excluder?: (d: any) => any): any => { const clone = cloneDeep(from); @@ -24,10 +23,7 @@ export const expectFixtureEql = (data: T, fixtureName: string, excluder?: (d: expect(data).not.to.eql(null); expect(data).not.to.eql(undefined); - let fixturePath = join(fixturesDir, `${fixtureName}.json`); - if (!fs.existsSync(fixturePath)) { - fixturePath = join(restFixturesDir, `${fixtureName}.json`); - } + const fixturePath = join(fixturesDir, `${fixtureName}.json`); excluder = excluder || (d => d); const dataExcluded = excludeFieldsFrom(data, excluder); diff --git a/x-pack/test/api_integration/apis/uptime/graphql/helpers/make_checks.ts b/x-pack/test/api_integration/apis/uptime/rest/helper/make_checks.ts similarity index 100% rename from x-pack/test/api_integration/apis/uptime/graphql/helpers/make_checks.ts rename to x-pack/test/api_integration/apis/uptime/rest/helper/make_checks.ts diff --git a/x-pack/test/api_integration/apis/uptime/rest/index.ts b/x-pack/test/api_integration/apis/uptime/rest/index.ts index 9b0cd61c22462..f77c14e960ab2 100644 --- a/x-pack/test/api_integration/apis/uptime/rest/index.ts +++ b/x-pack/test/api_integration/apis/uptime/rest/index.ts @@ -37,20 +37,24 @@ export default function({ getService, loadTestFile }: FtrProviderContext) { }); describe('with generated data', () => { - before('load heartbeat data', async () => await esArchiver.load('uptime/blank')); + beforeEach('load heartbeat data', async () => await esArchiver.loadIfNeeded('uptime/blank')); after('unload', async () => await esArchiver.unload('uptime/blank')); - loadTestFile(require.resolve('./snapshot')); + loadTestFile(require.resolve('./certs')); loadTestFile(require.resolve('./dynamic_settings')); + loadTestFile(require.resolve('./snapshot')); + loadTestFile(require.resolve('./monitor_states_generated')); loadTestFile(require.resolve('./telemetry_collectors')); }); describe('with real-world data', () => { - before('load heartbeat data', async () => await esArchiver.load('uptime/full_heartbeat')); - after('unload', async () => await esArchiver.unload('uptime/full_heartbeat')); + beforeEach('load heartbeat data', async () => await esArchiver.load('uptime/full_heartbeat')); + afterEach('unload', async () => await esArchiver.unload('uptime/full_heartbeat')); loadTestFile(require.resolve('./monitor_latest_status')); loadTestFile(require.resolve('./ping_histogram')); + loadTestFile(require.resolve('./ping_list')); loadTestFile(require.resolve('./monitor_duration')); loadTestFile(require.resolve('./doc_count')); + loadTestFile(require.resolve('./monitor_states_real_data')); }); }); } diff --git a/x-pack/test/api_integration/apis/uptime/rest/monitor_duration.ts b/x-pack/test/api_integration/apis/uptime/rest/monitor_duration.ts index acc50e9b8f3d6..7e93f9cfff8a1 100644 --- a/x-pack/test/api_integration/apis/uptime/rest/monitor_duration.ts +++ b/x-pack/test/api_integration/apis/uptime/rest/monitor_duration.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { expectFixtureEql } from '../graphql/helpers/expect_fixture_eql'; +import { expectFixtureEql } from './helper/expect_fixture_eql'; import { FtrProviderContext } from '../../../ftr_provider_context'; export default function({ getService }: FtrProviderContext) { diff --git a/x-pack/test/api_integration/apis/uptime/rest/monitor_latest_status.ts b/x-pack/test/api_integration/apis/uptime/rest/monitor_latest_status.ts index 749b304c87ee3..6547816bb7c16 100644 --- a/x-pack/test/api_integration/apis/uptime/rest/monitor_latest_status.ts +++ b/x-pack/test/api_integration/apis/uptime/rest/monitor_latest_status.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { expectFixtureEql } from '../graphql/helpers/expect_fixture_eql'; +import { expectFixtureEql } from './helper/expect_fixture_eql'; import { FtrProviderContext } from '../../../ftr_provider_context'; export default function({ getService }: FtrProviderContext) { diff --git a/x-pack/test/api_integration/apis/uptime/rest/monitor_states_generated.ts b/x-pack/test/api_integration/apis/uptime/rest/monitor_states_generated.ts new file mode 100644 index 0000000000000..3c17370532f91 --- /dev/null +++ b/x-pack/test/api_integration/apis/uptime/rest/monitor_states_generated.ts @@ -0,0 +1,193 @@ +/* + * 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 expect from '@kbn/expect'; +import { FtrProviderContext } from '../../../ftr_provider_context'; +import { makeChecksWithStatus } from './helper/make_checks'; +import { API_URLS } from '../../../../../legacy/plugins/uptime/common/constants'; +import { MonitorSummary } from '../../../../../legacy/plugins/uptime/common/runtime_types'; + +export default function({ getService }: FtrProviderContext) { + const supertest = getService('supertest'); + describe('monitor state scoping', async () => { + const numIps = 4; // Must be > 2 for IP uniqueness checks + + let dateRangeStart: string; + let dateRangeEnd: string; + + const getBaseUrl = (from: string, to: string) => + `${API_URLS.MONITOR_LIST}?dateRangeStart=${from}&dateRangeEnd=${to}&pageSize=10`; + + before('load heartbeat data', () => getService('esArchiver').load('uptime/blank')); + after('unload heartbeat index', () => getService('esArchiver').unload('uptime/blank')); + + describe('query document scoping with mismatched check statuses', async () => { + let checks: any[] = []; + let nonSummaryIp: string | null = null; + const testMonitorId = 'scope-test-id'; + const makeApiParams = (monitorId: string, filterClauses: any[] = []): any => { + return JSON.stringify({ + bool: { + filter: [{ match: { 'monitor.id': monitorId } }, ...filterClauses], + }, + }); + }; + + before(async () => { + const es = getService('legacyEs'); + dateRangeStart = new Date().toISOString(); + checks = await makeChecksWithStatus(es, testMonitorId, 1, numIps, 1, {}, 'up', d => { + // turn an all up status into having at least one down + if (d.summary) { + d.monitor.status = 'down'; + d.summary.up--; + d.summary.down++; + } + return d; + }); + + dateRangeEnd = new Date().toISOString(); + nonSummaryIp = checks[0][0].monitor.ip; + }); + + it('should return all IPs', async () => { + const filters = makeApiParams(testMonitorId); + const url = getBaseUrl(dateRangeStart, dateRangeEnd) + `&filters=${filters}`; + const apiResponse = await supertest.get(url); + const res = apiResponse.body; + + const uniqueIps = new Set(); + res.summaries[0].state.checks.forEach((c: any) => uniqueIps.add(c.monitor.ip)); + + expect(uniqueIps.size).to.eql(4); + }); + + it('should match non summary documents without a status filter', async () => { + const filters = makeApiParams(testMonitorId, [{ match: { 'monitor.ip': nonSummaryIp } }]); + + const url = getBaseUrl(dateRangeStart, dateRangeEnd) + `&filters=${filters}`; + const apiResponse = await supertest.get(url); + const nonSummaryRes = apiResponse.body; + expect(nonSummaryRes.summaries.length).to.eql(1); + }); + + it('should not match non summary documents if the check status does not match the document status', async () => { + const filters = makeApiParams(testMonitorId, [{ match: { 'monitor.ip': nonSummaryIp } }]); + const url = + getBaseUrl(dateRangeStart, dateRangeEnd) + `&filters=${filters}&statusFilter=down`; + + const apiResponse = await supertest.get(url); + const nonSummaryRes = apiResponse.body; + expect(nonSummaryRes.summaries.length).to.eql(0); + }); + + it('should not non match non summary documents if the check status does not match', async () => { + const filters = makeApiParams(testMonitorId, [{ match: { 'monitor.ip': nonSummaryIp } }]); + const url = + getBaseUrl(dateRangeStart, dateRangeEnd) + `&filters=${filters}&statusFilter=up`; + + const apiResponse = await supertest.get(url); + const nonSummaryRes = apiResponse.body; + expect(nonSummaryRes.summaries.length).to.eql(0); + }); + + describe('matching outside of the date range', async () => { + before('set date range to future', () => { + const futureDate = new Date(); + + // Set dateRangeStart to one day from now + futureDate.setDate(futureDate.getDate() + 1); + dateRangeStart = futureDate.toISOString(); + + // Set dateRangeStart to two days from now + futureDate.setDate(futureDate.getDate() + 1); + dateRangeEnd = futureDate.toISOString(); + }); + + it('should not match any documents', async () => { + const filters = makeApiParams(testMonitorId); + const url = + getBaseUrl(dateRangeStart, dateRangeEnd) + `&filters=${filters}&statusFilter=up`; + + const apiResponse = await supertest.get(url); + const nonSummaryRes = apiResponse.body; + expect(nonSummaryRes.summaries.length).to.eql(0); + }); + }); + }); + + describe('test status filter', async () => { + const upMonitorId = 'up-test-id'; + const downMonitorId = 'down-test-id'; + const mixMonitorId = 'mix-test-id'; + before('generate three monitors with up, down, mix state', async () => { + await getService('esArchiver').load('uptime/blank'); + + const es = getService('legacyEs'); + + const observer = { + geo: { + name: 'US-East', + location: '40.7128, -74.0060', + }, + }; + + // Generating three monitors each with two geo locations, + // One in a down state , + // One in an up state, + // One in a mix state + + dateRangeStart = new Date().toISOString(); + + await makeChecksWithStatus(es, upMonitorId, 1, 4, 1, {}, 'up'); + await makeChecksWithStatus(es, upMonitorId, 1, 4, 1, { observer }, 'up'); + + await makeChecksWithStatus(es, downMonitorId, 1, 4, 1, {}, 'down'); + await makeChecksWithStatus(es, downMonitorId, 1, 4, 1, { observer }, 'down'); + + await makeChecksWithStatus(es, mixMonitorId, 1, 4, 1, {}, 'up'); + await makeChecksWithStatus(es, mixMonitorId, 1, 4, 1, { observer }, 'down'); + + dateRangeEnd = new Date().toISOString(); + }); + + after('unload heartbeat index', () => getService('esArchiver').unload('uptime/blank')); + + it('should return all monitor when no status filter', async () => { + const apiResponse = await supertest.get(getBaseUrl(dateRangeStart, dateRangeEnd)); + const { summaries } = apiResponse.body; + expect(summaries.length).to.eql(3); + // Summaries are by default sorted by monitor names + expect(summaries.map((summary: MonitorSummary) => summary.monitor_id)).to.eql([ + downMonitorId, + mixMonitorId, + upMonitorId, + ]); + }); + + it('should return a monitor with mix state if check status filter is down', async () => { + const apiResponse = await supertest.get( + getBaseUrl(dateRangeStart, dateRangeEnd) + '&statusFilter=down' + ); + const { summaries } = apiResponse.body; + expect(summaries.length).to.eql(2); + summaries.forEach((summary: MonitorSummary) => { + expect(summary.monitor_id).to.not.eql(upMonitorId); + }); + }); + + it('should not return a monitor with mix state if check status filter is up', async () => { + const apiResponse = await supertest.get( + getBaseUrl(dateRangeStart, dateRangeEnd) + '&statusFilter=up' + ); + const { summaries } = apiResponse.body; + + expect(summaries.length).to.eql(1); + expect(summaries[0].monitor_id).to.eql(upMonitorId); + }); + }); + }); +} diff --git a/x-pack/test/api_integration/apis/uptime/rest/monitor_states_real_data.ts b/x-pack/test/api_integration/apis/uptime/rest/monitor_states_real_data.ts new file mode 100644 index 0000000000000..f1e37bff405fd --- /dev/null +++ b/x-pack/test/api_integration/apis/uptime/rest/monitor_states_real_data.ts @@ -0,0 +1,525 @@ +/* + * 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 expect from '@kbn/expect'; +import { isRight } from 'fp-ts/lib/Either'; +import { FtrProviderContext } from '../../../ftr_provider_context'; +import { API_URLS } from '../../../../../legacy/plugins/uptime/common/constants'; +import { MonitorSummaryResultType } from '../../../../../legacy/plugins/uptime/common/runtime_types'; + +interface ExpectedMonitorStatesPage { + response: any; + statesIds: string[]; + statuses: string[]; + absFrom: number; + absTo: number; + size: number; + totalCount: number; + prevPagination: null | string; + nextPagination: null | string; +} + +type PendingExpectedMonitorStatesPage = Pick< + ExpectedMonitorStatesPage, + 'statesIds' | 'statuses' | 'prevPagination' | 'nextPagination' +>; + +const checkMonitorStatesResponse = ({ + response, + statesIds, + statuses, + absFrom, + absTo, + size, + totalCount, + prevPagination, + nextPagination, +}: ExpectedMonitorStatesPage) => { + const decoded = MonitorSummaryResultType.decode(response); + expect(isRight(decoded)).to.be.ok(); + if (isRight(decoded)) { + const { summaries, prevPagePagination, nextPagePagination, totalSummaryCount } = decoded.right; + expect(summaries).to.have.length(size); + expect(summaries?.map(s => s.monitor_id)).to.eql(statesIds); + expect( + summaries?.map(s => (s.state.summary?.up && !s.state.summary?.down ? 'up' : 'down')) + ).to.eql(statuses); + (summaries ?? []).forEach(s => { + expect(s.state.url.full).to.be.ok(); + expect(s.histogram?.count).to.be(20); + (s.histogram?.points ?? []).forEach(point => { + expect(point.timestamp).to.be.greaterThan(absFrom); + expect(point.timestamp).to.be.lessThan(absTo); + }); + }); + expect(totalSummaryCount).to.be(totalCount); + expect(prevPagePagination).to.be(prevPagination); + expect(nextPagePagination).to.eql(nextPagination); + } +}; + +export default function({ getService }: FtrProviderContext) { + const supertest = getService('supertest'); + describe('monitor states endpoint', () => { + const from = '2019-09-11T03:30:04.380Z'; + const to = '2019-09-11T03:40:34.410Z'; + const absFrom = new Date(from).valueOf(); + const absTo = new Date(to).valueOf(); + + it('will fetch monitor state data for the given filters and range', async () => { + const statusFilter = 'up'; + const size = 10; + const filters = + '{"bool":{"must":[{"match":{"monitor.id":{"query":"0002-up","operator":"and"}}}]}}'; + const apiResponse = await supertest.get( + `${API_URLS.MONITOR_LIST}?dateRangeStart=${from}&dateRangeEnd=${to}&statusFilter=${statusFilter}&filters=${filters}&pageSize=${size}` + ); + checkMonitorStatesResponse({ + response: apiResponse.body, + statesIds: ['0002-up'], + statuses: ['up'], + absFrom, + absTo, + size: 1, + totalCount: 2000, + prevPagination: null, + nextPagination: null, + }); + }); + + it('can navigate forward and backward using pagination', async () => { + const expectedResultsCount = 100; + const size = 10; + const expectedPageCount = expectedResultsCount / size; + const expectedNextResults: PendingExpectedMonitorStatesPage[] = [ + { + statesIds: [ + '0000-intermittent', + '0001-up', + '0002-up', + '0003-up', + '0004-up', + '0005-up', + '0006-up', + '0007-up', + '0008-up', + '0009-up', + ], + statuses: ['up', 'up', 'up', 'up', 'up', 'up', 'up', 'up', 'up', 'up'], + nextPagination: + '{"cursorDirection":"AFTER","sortOrder":"ASC","cursorKey":{"monitor_id":"0009-up"}}', + prevPagination: null, + }, + { + statesIds: [ + '0010-down', + '0011-up', + '0012-up', + '0013-up', + '0014-up', + '0015-intermittent', + '0016-up', + '0017-up', + '0018-up', + '0019-up', + ], + statuses: ['down', 'up', 'up', 'up', 'up', 'up', 'up', 'up', 'up', 'up'], + nextPagination: + '{"cursorDirection":"AFTER","sortOrder":"ASC","cursorKey":{"monitor_id":"0019-up"}}', + prevPagination: + '{"cursorKey":{"monitor_id":"0010-down"},"sortOrder":"ASC","cursorDirection":"BEFORE"}', + }, + { + statesIds: [ + '0020-down', + '0021-up', + '0022-up', + '0023-up', + '0024-up', + '0025-up', + '0026-up', + '0027-up', + '0028-up', + '0029-up', + ], + statuses: ['down', 'up', 'up', 'up', 'up', 'up', 'up', 'up', 'up', 'up'], + nextPagination: + '{"cursorDirection":"AFTER","sortOrder":"ASC","cursorKey":{"monitor_id":"0029-up"}}', + prevPagination: + '{"cursorKey":{"monitor_id":"0020-down"},"sortOrder":"ASC","cursorDirection":"BEFORE"}', + }, + { + statesIds: [ + '0030-intermittent', + '0031-up', + '0032-up', + '0033-up', + '0034-up', + '0035-up', + '0036-up', + '0037-up', + '0038-up', + '0039-up', + ], + statuses: ['down', 'up', 'up', 'up', 'up', 'up', 'up', 'up', 'up', 'up'], + nextPagination: + '{"cursorDirection":"AFTER","sortOrder":"ASC","cursorKey":{"monitor_id":"0039-up"}}', + prevPagination: + '{"cursorKey":{"monitor_id":"0030-intermittent"},"sortOrder":"ASC","cursorDirection":"BEFORE"}', + }, + { + statesIds: [ + '0040-down', + '0041-up', + '0042-up', + '0043-up', + '0044-up', + '0045-intermittent', + '0046-up', + '0047-up', + '0048-up', + '0049-up', + ], + statuses: ['down', 'up', 'up', 'up', 'up', 'up', 'up', 'up', 'up', 'up'], + nextPagination: + '{"cursorDirection":"AFTER","sortOrder":"ASC","cursorKey":{"monitor_id":"0049-up"}}', + prevPagination: + '{"cursorKey":{"monitor_id":"0040-down"},"sortOrder":"ASC","cursorDirection":"BEFORE"}', + }, + { + statesIds: [ + '0050-down', + '0051-up', + '0052-up', + '0053-up', + '0054-up', + '0055-up', + '0056-up', + '0057-up', + '0058-up', + '0059-up', + ], + statuses: ['down', 'up', 'up', 'up', 'up', 'up', 'up', 'up', 'up', 'up'], + nextPagination: + '{"cursorDirection":"AFTER","sortOrder":"ASC","cursorKey":{"monitor_id":"0059-up"}}', + prevPagination: + '{"cursorKey":{"monitor_id":"0050-down"},"sortOrder":"ASC","cursorDirection":"BEFORE"}', + }, + { + statesIds: [ + '0060-intermittent', + '0061-up', + '0062-up', + '0063-up', + '0064-up', + '0065-up', + '0066-up', + '0067-up', + '0068-up', + '0069-up', + ], + statuses: ['up', 'up', 'up', 'up', 'up', 'up', 'up', 'up', 'up', 'up'], + nextPagination: + '{"cursorDirection":"AFTER","sortOrder":"ASC","cursorKey":{"monitor_id":"0069-up"}}', + prevPagination: + '{"cursorKey":{"monitor_id":"0060-intermittent"},"sortOrder":"ASC","cursorDirection":"BEFORE"}', + }, + { + statesIds: [ + '0070-down', + '0071-up', + '0072-up', + '0073-up', + '0074-up', + '0075-intermittent', + '0076-up', + '0077-up', + '0078-up', + '0079-up', + ], + statuses: ['down', 'up', 'up', 'up', 'up', 'up', 'up', 'up', 'up', 'up'], + nextPagination: + '{"cursorDirection":"AFTER","sortOrder":"ASC","cursorKey":{"monitor_id":"0079-up"}}', + prevPagination: + '{"cursorKey":{"monitor_id":"0070-down"},"sortOrder":"ASC","cursorDirection":"BEFORE"}', + }, + { + statesIds: [ + '0080-down', + '0081-up', + '0082-up', + '0083-up', + '0084-up', + '0085-up', + '0086-up', + '0087-up', + '0088-up', + '0089-up', + ], + statuses: ['down', 'up', 'up', 'up', 'up', 'up', 'up', 'up', 'up', 'up'], + nextPagination: + '{"cursorDirection":"AFTER","sortOrder":"ASC","cursorKey":{"monitor_id":"0089-up"}}', + prevPagination: + '{"cursorKey":{"monitor_id":"0080-down"},"sortOrder":"ASC","cursorDirection":"BEFORE"}', + }, + { + statesIds: [ + '0090-intermittent', + '0091-up', + '0092-up', + '0093-up', + '0094-up', + '0095-up', + '0096-up', + '0097-up', + '0098-up', + '0099-up', + ], + statuses: ['up', 'up', 'up', 'up', 'up', 'up', 'up', 'up', 'up', 'up'], + nextPagination: null, + prevPagination: + '{"cursorKey":{"monitor_id":"0090-intermittent"},"sortOrder":"ASC","cursorDirection":"BEFORE"}', + }, + ]; + + const expectedPrevResults: PendingExpectedMonitorStatesPage[] = [ + { + statesIds: [ + '0000-intermittent', + '0001-up', + '0002-up', + '0003-up', + '0004-up', + '0005-up', + '0006-up', + '0007-up', + '0008-up', + '0009-up', + ], + statuses: ['up', 'up', 'up', 'up', 'up', 'up', 'up', 'up', 'up', 'up'], + nextPagination: + '{"cursorKey":{"monitor_id":"0009-up"},"sortOrder":"ASC","cursorDirection":"AFTER"}', + prevPagination: null, + }, + { + statesIds: [ + '0010-down', + '0011-up', + '0012-up', + '0013-up', + '0014-up', + '0015-intermittent', + '0016-up', + '0017-up', + '0018-up', + '0019-up', + ], + statuses: ['down', 'up', 'up', 'up', 'up', 'up', 'up', 'up', 'up', 'up'], + nextPagination: + '{"cursorKey":{"monitor_id":"0019-up"},"sortOrder":"ASC","cursorDirection":"AFTER"}', + prevPagination: + '{"cursorKey":{"monitor_id":"0010-down"},"sortOrder":"ASC","cursorDirection":"BEFORE"}', + }, + { + statesIds: [ + '0020-down', + '0021-up', + '0022-up', + '0023-up', + '0024-up', + '0025-up', + '0026-up', + '0027-up', + '0028-up', + '0029-up', + ], + statuses: ['down', 'up', 'up', 'up', 'up', 'up', 'up', 'up', 'up', 'up'], + nextPagination: + '{"cursorKey":{"monitor_id":"0029-up"},"sortOrder":"ASC","cursorDirection":"AFTER"}', + prevPagination: + '{"cursorKey":{"monitor_id":"0020-down"},"sortOrder":"ASC","cursorDirection":"BEFORE"}', + }, + { + statesIds: [ + '0030-intermittent', + '0031-up', + '0032-up', + '0033-up', + '0034-up', + '0035-up', + '0036-up', + '0037-up', + '0038-up', + '0039-up', + ], + statuses: ['down', 'up', 'up', 'up', 'up', 'up', 'up', 'up', 'up', 'up'], + nextPagination: + '{"cursorKey":{"monitor_id":"0039-up"},"sortOrder":"ASC","cursorDirection":"AFTER"}', + prevPagination: + '{"cursorKey":{"monitor_id":"0030-intermittent"},"sortOrder":"ASC","cursorDirection":"BEFORE"}', + }, + { + statesIds: [ + '0040-down', + '0041-up', + '0042-up', + '0043-up', + '0044-up', + '0045-intermittent', + '0046-up', + '0047-up', + '0048-up', + '0049-up', + ], + statuses: ['down', 'up', 'up', 'up', 'up', 'up', 'up', 'up', 'up', 'up'], + nextPagination: + '{"cursorKey":{"monitor_id":"0049-up"},"sortOrder":"ASC","cursorDirection":"AFTER"}', + prevPagination: + '{"cursorKey":{"monitor_id":"0040-down"},"sortOrder":"ASC","cursorDirection":"BEFORE"}', + }, + { + statesIds: [ + '0050-down', + '0051-up', + '0052-up', + '0053-up', + '0054-up', + '0055-up', + '0056-up', + '0057-up', + '0058-up', + '0059-up', + ], + statuses: ['down', 'up', 'up', 'up', 'up', 'up', 'up', 'up', 'up', 'up'], + nextPagination: + '{"cursorKey":{"monitor_id":"0059-up"},"sortOrder":"ASC","cursorDirection":"AFTER"}', + prevPagination: + '{"cursorKey":{"monitor_id":"0050-down"},"sortOrder":"ASC","cursorDirection":"BEFORE"}', + }, + { + statesIds: [ + '0060-intermittent', + '0061-up', + '0062-up', + '0063-up', + '0064-up', + '0065-up', + '0066-up', + '0067-up', + '0068-up', + '0069-up', + ], + statuses: ['up', 'up', 'up', 'up', 'up', 'up', 'up', 'up', 'up', 'up'], + nextPagination: + '{"cursorKey":{"monitor_id":"0069-up"},"sortOrder":"ASC","cursorDirection":"AFTER"}', + prevPagination: + '{"cursorKey":{"monitor_id":"0060-intermittent"},"sortOrder":"ASC","cursorDirection":"BEFORE"}', + }, + { + statesIds: [ + '0070-down', + '0071-up', + '0072-up', + '0073-up', + '0074-up', + '0075-intermittent', + '0076-up', + '0077-up', + '0078-up', + '0079-up', + ], + statuses: ['down', 'up', 'up', 'up', 'up', 'up', 'up', 'up', 'up', 'up'], + nextPagination: + '{"cursorKey":{"monitor_id":"0079-up"},"sortOrder":"ASC","cursorDirection":"AFTER"}', + prevPagination: + '{"cursorKey":{"monitor_id":"0070-down"},"sortOrder":"ASC","cursorDirection":"BEFORE"}', + }, + { + statesIds: [ + '0080-down', + '0081-up', + '0082-up', + '0083-up', + '0084-up', + '0085-up', + '0086-up', + '0087-up', + '0088-up', + '0089-up', + ], + statuses: ['down', 'up', 'up', 'up', 'up', 'up', 'up', 'up', 'up', 'up'], + nextPagination: + '{"cursorKey":{"monitor_id":"0089-up"},"sortOrder":"ASC","cursorDirection":"AFTER"}', + prevPagination: + '{"cursorKey":{"monitor_id":"0080-down"},"sortOrder":"ASC","cursorDirection":"BEFORE"}', + }, + ]; + + const totalCount = 2000; + let pagination: string | null = null; + for (let page = 1; page <= expectedPageCount; page++) { + const baseUrl = `${API_URLS.MONITOR_LIST}?dateRangeStart=${from}&dateRangeEnd=${to}&pageSize=${size}`; + const nextUrl: string = baseUrl + `&pagination=${pagination ?? ''}`; + const nextApiResponse = await supertest.get(nextUrl); + const nextData = nextApiResponse.body; + pagination = nextData.nextPagePagination; + checkMonitorStatesResponse({ + response: nextData, + ...expectedNextResults[page - 1], + absFrom, + absTo, + size, + totalCount, + }); + + // Test to see if the previous page pagination works on every page (other than the first) + if (page > 1) { + const prevUrl: string = baseUrl + `&pagination=${nextData.prevPagePagination}`; + const prevApiResponse = await supertest.get(prevUrl); + const prevData = prevApiResponse.body; + checkMonitorStatesResponse({ + response: prevData, + ...expectedPrevResults[page - 2], + absFrom, + absTo, + size, + totalCount, + }); + } + } + }); + + it('will fetch monitor state data for the given date range', async () => { + const LENGTH = 10; + const { body } = await supertest.get( + `${API_URLS.MONITOR_LIST}?dateRangeStart=${from}&dateRangeEnd=${to}&pageSize=${LENGTH}` + ); + checkMonitorStatesResponse({ + response: body, + statesIds: [ + '0000-intermittent', + '0001-up', + '0002-up', + '0003-up', + '0004-up', + '0005-up', + '0006-up', + '0007-up', + '0008-up', + '0009-up', + ], + statuses: ['up', 'up', 'up', 'up', 'up', 'up', 'up', 'up', 'up', 'up'], + absFrom, + absTo, + size: LENGTH, + totalCount: 2000, + prevPagination: null, + nextPagination: + '{"cursorDirection":"AFTER","sortOrder":"ASC","cursorKey":{"monitor_id":"0009-up"}}', + }); + }); + }); +} diff --git a/x-pack/test/api_integration/apis/uptime/rest/ping_histogram.ts b/x-pack/test/api_integration/apis/uptime/rest/ping_histogram.ts index 0982d5fef7cb4..b1afe4c8e0d7d 100644 --- a/x-pack/test/api_integration/apis/uptime/rest/ping_histogram.ts +++ b/x-pack/test/api_integration/apis/uptime/rest/ping_histogram.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { expectFixtureEql } from '../graphql/helpers/expect_fixture_eql'; +import { expectFixtureEql } from './helper/expect_fixture_eql'; import { FtrProviderContext } from '../../../ftr_provider_context'; import { assertCloseTo } from '../../../../../plugins/uptime/server/lib/helper'; diff --git a/x-pack/test/api_integration/apis/uptime/rest/ping_list.ts b/x-pack/test/api_integration/apis/uptime/rest/ping_list.ts new file mode 100644 index 0000000000000..a261763d5991f --- /dev/null +++ b/x-pack/test/api_integration/apis/uptime/rest/ping_list.ts @@ -0,0 +1,178 @@ +/* + * 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 expect from '@kbn/expect'; +import { isLeft } from 'fp-ts/lib/Either'; +import { PathReporter } from 'io-ts/lib/PathReporter'; +import { PingsResponseType } from '../../../../../legacy/plugins/uptime/common/runtime_types'; +import { FtrProviderContext } from '../../../ftr_provider_context'; + +function decodePingsResponseData(response: any) { + const decoded = PingsResponseType.decode(response); + if (isLeft(decoded)) { + throw Error(JSON.stringify(PathReporter.report(decoded), null, 2)); + } + return decoded.right; +} + +export default function({ getService }: FtrProviderContext) { + const supertest = getService('supertest'); + describe('pingList query', () => { + before('load heartbeat data', () => getService('esArchiver').load('uptime/full_heartbeat')); + after('unload heartbeat index', () => getService('esArchiver').unload('uptime/full_heartbeat')); + + it('returns a list of pings for the given date range and default size', async () => { + const from = '2019-01-28T17:40:08.078Z'; + const to = '2025-01-28T19:00:16.078Z'; + + const apiResponse = await supertest.get(`/api/uptime/pings?from=${from}&to=${to}&size=10`); + + const { total, locations, pings } = decodePingsResponseData(apiResponse.body); + + expect(total).to.be(2000); + expect(locations).to.eql(['mpls']); + expect(pings).length(10); + expect(pings.map(({ monitor: { id } }) => id)).to.eql([ + '0074-up', + '0073-up', + '0099-up', + '0098-up', + '0075-intermittent', + '0097-up', + '0049-up', + '0047-up', + '0077-up', + '0076-up', + ]); + }); + + it('returns a list of pings for the date range and given size', async () => { + const from = '2019-01-28T17:40:08.078Z'; + const to = '2025-01-28T19:00:16.078Z'; + const size = 50; + + const apiResponse = await supertest.get( + `/api/uptime/pings?from=${from}&to=${to}&size=${size}` + ); + + const { total, locations, pings } = decodePingsResponseData(apiResponse.body); + + expect(total).to.be(2000); + expect(locations).to.eql(['mpls']); + expect(pings).length(50); + expect(pings.map(({ monitor: { id } }) => id)).to.eql([ + '0074-up', + '0073-up', + '0099-up', + '0098-up', + '0075-intermittent', + '0097-up', + '0049-up', + '0047-up', + '0077-up', + '0076-up', + '0050-down', + '0048-up', + '0072-up', + '0096-up', + '0092-up', + '0069-up', + '0093-up', + '0070-down', + '0071-up', + '0095-up', + '0032-up', + '0094-up', + '0046-up', + '0091-up', + '0067-up', + '0068-up', + '0090-intermittent', + '0031-up', + '0066-up', + '0084-up', + '0083-up', + '0041-up', + '0045-intermittent', + '0042-up', + '0030-intermittent', + '0063-up', + '0061-up', + '0065-up', + '0062-up', + '0026-up', + '0085-up', + '0025-up', + '0088-up', + '0089-up', + '0087-up', + '0028-up', + '0086-up', + '0064-up', + '0029-up', + '0044-up', + ]); + }); + + it('returns a list of pings for a monitor ID', async () => { + const from = '2019-01-28T17:40:08.078Z'; + const to = '2025-01-28T19:00:16.078Z'; + const monitorId = '0001-up'; + const size = 15; + + const apiResponse = await supertest.get( + `/api/uptime/pings?from=${from}&to=${to}&monitorId=${monitorId}&size=${size}` + ); + + const { total, locations, pings } = decodePingsResponseData(apiResponse.body); + + expect(total).to.be(20); + expect(locations).to.eql(['mpls']); + pings.forEach(({ monitor: { id } }) => expect(id).to.eql('0001-up')); + expect(pings.map(({ timestamp }) => timestamp)).to.eql([ + '2019-09-11T03:40:34.371Z', + '2019-09-11T03:40:04.370Z', + '2019-09-11T03:39:34.370Z', + '2019-09-11T03:39:04.371Z', + '2019-09-11T03:38:34.370Z', + '2019-09-11T03:38:04.370Z', + '2019-09-11T03:37:34.370Z', + '2019-09-11T03:37:04.370Z', + '2019-09-11T03:36:34.371Z', + '2019-09-11T03:36:04.370Z', + '2019-09-11T03:35:34.373Z', + '2019-09-11T03:35:04.371Z', + '2019-09-11T03:34:34.371Z', + '2019-09-11T03:34:04.381Z', + '2019-09-11T03:33:34.371Z', + ]); + }); + + it('returns a list of pings sorted ascending', async () => { + const from = '2019-01-28T17:40:08.078Z'; + const to = '2025-01-28T19:00:16.078Z'; + const monitorId = '0001-up'; + const size = 5; + const sort = 'asc'; + + const apiResponse = await supertest.get( + `/api/uptime/pings?from=${from}&to=${to}&monitorId=${monitorId}&size=${size}&sort=${sort}` + ); + + const { total, locations, pings } = decodePingsResponseData(apiResponse.body); + + expect(total).to.be(20); + expect(locations).to.eql(['mpls']); + expect(pings.map(({ timestamp }) => timestamp)).to.eql([ + '2019-09-11T03:31:04.380Z', + '2019-09-11T03:31:34.366Z', + '2019-09-11T03:32:04.372Z', + '2019-09-11T03:32:34.375Z', + '2019-09-11T03:33:04.370Z', + ]); + }); + }); +} diff --git a/x-pack/test/api_integration/apis/uptime/rest/snapshot.ts b/x-pack/test/api_integration/apis/uptime/rest/snapshot.ts index 20fe59d149ae8..9a8951741948e 100644 --- a/x-pack/test/api_integration/apis/uptime/rest/snapshot.ts +++ b/x-pack/test/api_integration/apis/uptime/rest/snapshot.ts @@ -4,9 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ -import { expectFixtureEql } from '../graphql/helpers/expect_fixture_eql'; +import { expectFixtureEql } from './helper/expect_fixture_eql'; import { FtrProviderContext } from '../../../ftr_provider_context'; -import { makeChecksWithStatus, getChecksDateRange } from '../graphql/helpers/make_checks'; +import { makeChecksWithStatus, getChecksDateRange } from './helper/make_checks'; export default function({ getService }: FtrProviderContext) { const supertest = getService('supertest'); diff --git a/x-pack/test/api_integration/apis/uptime/rest/telemetry_collectors.ts b/x-pack/test/api_integration/apis/uptime/rest/telemetry_collectors.ts index b2ec96be0f288..017ef02afe5ea 100644 --- a/x-pack/test/api_integration/apis/uptime/rest/telemetry_collectors.ts +++ b/x-pack/test/api_integration/apis/uptime/rest/telemetry_collectors.ts @@ -7,7 +7,7 @@ import expect from '@kbn/expect'; import { FtrProviderContext } from '../../../ftr_provider_context'; import { API_URLS } from '../../../../../legacy/plugins/uptime/common/constants'; -import { makeChecksWithStatus } from '../graphql/helpers/make_checks'; +import { makeChecksWithStatus } from './helper/make_checks'; export default function({ getService }: FtrProviderContext) { const supertest = getService('supertest'); diff --git a/x-pack/test/api_integration/config.js b/x-pack/test/api_integration/config.js index b62368bf2d608..0eac7c58044e6 100644 --- a/x-pack/test/api_integration/config.js +++ b/x-pack/test/api_integration/config.js @@ -27,6 +27,8 @@ export async function getApiIntegrationConfig({ readConfigFile }) { '--xpack.security.session.idleTimeout=3600000', // 1 hour '--optimize.enabled=false', '--xpack.endpoint.enabled=true', + '--telemetry.optIn=true', + '--xpack.endpoint.enabled=true', '--xpack.ingestManager.enabled=true', '--xpack.ingestManager.fleet.enabled=true', '--xpack.endpoint.alertResultListDefaultDateRange.from=2018-01-10T00:00:00.000Z', diff --git a/x-pack/test/api_integration/services/index.ts b/x-pack/test/api_integration/services/index.ts index 9c945f557a2d8..84b8476bd1dd1 100644 --- a/x-pack/test/api_integration/services/index.ts +++ b/x-pack/test/api_integration/services/index.ts @@ -22,6 +22,7 @@ import { import { SiemGraphQLClientProvider, SiemGraphQLClientFactoryProvider } from './siem_graphql_client'; import { InfraOpsSourceConfigurationProvider } from './infraops_source_configuration'; import { MachineLearningProvider } from './ml'; +import { IngestManagerProvider } from './ingest_manager'; export const services = { ...commonServices, @@ -39,4 +40,5 @@ export const services = { supertestWithoutAuth: SupertestWithoutAuthProvider, usageAPI: UsageAPIProvider, ml: MachineLearningProvider, + ingestManager: IngestManagerProvider, }; diff --git a/x-pack/test/api_integration/services/ingest_manager.ts b/x-pack/test/api_integration/services/ingest_manager.ts new file mode 100644 index 0000000000000..2b70a20ca0362 --- /dev/null +++ b/x-pack/test/api_integration/services/ingest_manager.ts @@ -0,0 +1,28 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { FtrProviderContext } from '../ftr_provider_context'; +import { setupRouteService, fleetSetupRouteService } from '../../../plugins/ingest_manager/common'; + +export function IngestManagerProvider({ getService }: FtrProviderContext) { + const supertest = getService('supertest'); + return { + async setup() { + const headers = { accept: 'application/json', 'kbn-xsrf': 'some-xsrf-token' }; + + const { body } = await supertest + .get(fleetSetupRouteService.getFleetSetupPath()) + .set(headers) + .expect(200); + + if (!body.isInitialized) { + await supertest + .post(setupRouteService.getSetupPath()) + .set(headers) + .expect(200); + } + }, + }; +} diff --git a/x-pack/test/api_integration/services/ml.ts b/x-pack/test/api_integration/services/ml.ts index 841b200b87080..c295af7dc73ab 100644 --- a/x-pack/test/api_integration/services/ml.ts +++ b/x-pack/test/api_integration/services/ml.ts @@ -9,14 +9,17 @@ import { FtrProviderContext } from '../../functional/ftr_provider_context'; import { MachineLearningAPIProvider, MachineLearningSecurityCommonProvider, + MachineLearningTestResourcesProvider, } from '../../functional/services/machine_learning'; export function MachineLearningProvider(context: FtrProviderContext) { const api = MachineLearningAPIProvider(context); const securityCommon = MachineLearningSecurityCommonProvider(context); + const testResources = MachineLearningTestResourcesProvider(context); return { api, securityCommon, + testResources, }; } diff --git a/x-pack/test/detection_engine_api_integration/common/config.ts b/x-pack/test/detection_engine_api_integration/common/config.ts index 89ebd902834b9..e89352118990a 100644 --- a/x-pack/test/detection_engine_api_integration/common/config.ts +++ b/x-pack/test/detection_engine_api_integration/common/config.ts @@ -8,7 +8,7 @@ import path from 'path'; import { CA_CERT_PATH } from '@kbn/dev-utils'; import { FtrConfigProviderContext } from '@kbn/test/types/ftr'; import { services } from './services'; -import { listsEnvFeatureFlagName } from '../../../legacy/plugins/siem/server/lib/detection_engine/feature_flags'; +import { listsEnvFeatureFlagName } from '../../../plugins/siem/server/lib/detection_engine/feature_flags'; interface CreateTestConfigOptions { license: string; diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/add_prepackaged_rules.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/add_prepackaged_rules.ts index 6ee65d5d28aa4..e787a3594dfe6 100644 --- a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/add_prepackaged_rules.ts +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/add_prepackaged_rules.ts @@ -6,7 +6,7 @@ import expect from '@kbn/expect'; -import { DETECTION_ENGINE_PREPACKAGED_URL } from '../../../../legacy/plugins/siem/common/constants'; +import { DETECTION_ENGINE_PREPACKAGED_URL } from '../../../../plugins/siem/common/constants'; import { FtrProviderContext } from '../../common/ftr_provider_context'; import { createSignalsIndex, deleteAllAlerts, deleteSignalsIndex } from './utils'; diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/create_rules.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/create_rules.ts index 91088acb7a51c..46645a9b5a944 100644 --- a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/create_rules.ts +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/create_rules.ts @@ -6,7 +6,7 @@ import expect from '@kbn/expect'; -import { DETECTION_ENGINE_RULES_URL } from '../../../../legacy/plugins/siem/common/constants'; +import { DETECTION_ENGINE_RULES_URL } from '../../../../plugins/siem/common/constants'; import { FtrProviderContext } from '../../common/ftr_provider_context'; import { createSignalsIndex, diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/create_rules_bulk.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/create_rules_bulk.ts index 8e951a31b525c..117300be417d5 100644 --- a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/create_rules_bulk.ts +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/create_rules_bulk.ts @@ -6,7 +6,7 @@ import expect from '@kbn/expect'; -import { DETECTION_ENGINE_RULES_URL } from '../../../../legacy/plugins/siem/common/constants'; +import { DETECTION_ENGINE_RULES_URL } from '../../../../plugins/siem/common/constants'; import { FtrProviderContext } from '../../common/ftr_provider_context'; import { createSignalsIndex, diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/delete_rules.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/delete_rules.ts index a886a5fb07a6c..fb701681419d8 100644 --- a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/delete_rules.ts +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/delete_rules.ts @@ -6,7 +6,7 @@ import expect from '@kbn/expect'; -import { DETECTION_ENGINE_RULES_URL } from '../../../../legacy/plugins/siem/common/constants'; +import { DETECTION_ENGINE_RULES_URL } from '../../../../plugins/siem/common/constants'; import { FtrProviderContext } from '../../common/ftr_provider_context'; import { createSignalsIndex, diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/delete_rules_bulk.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/delete_rules_bulk.ts index 9e9071b82884f..ac58ba4c77e4e 100644 --- a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/delete_rules_bulk.ts +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/delete_rules_bulk.ts @@ -6,7 +6,7 @@ import expect from '@kbn/expect'; -import { DETECTION_ENGINE_RULES_URL } from '../../../../legacy/plugins/siem/common/constants'; +import { DETECTION_ENGINE_RULES_URL } from '../../../../plugins/siem/common/constants'; import { FtrProviderContext } from '../../common/ftr_provider_context'; import { createSignalsIndex, diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/export_rules.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/export_rules.ts index a8f841db94bbc..51bdb9e45dc0c 100644 --- a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/export_rules.ts +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/export_rules.ts @@ -6,7 +6,7 @@ import expect from '@kbn/expect'; -import { DETECTION_ENGINE_RULES_URL } from '../../../../legacy/plugins/siem/common/constants'; +import { DETECTION_ENGINE_RULES_URL } from '../../../../plugins/siem/common/constants'; import { FtrProviderContext } from '../../common/ftr_provider_context'; import { binaryToString, diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/find_rules.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/find_rules.ts index abbc8f77e0077..feb4ecd125f7e 100644 --- a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/find_rules.ts +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/find_rules.ts @@ -6,7 +6,7 @@ import expect from '@kbn/expect'; -import { DETECTION_ENGINE_RULES_URL } from '../../../../legacy/plugins/siem/common/constants'; +import { DETECTION_ENGINE_RULES_URL } from '../../../../plugins/siem/common/constants'; import { FtrProviderContext } from '../../common/ftr_provider_context'; import { createSignalsIndex, diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/find_statuses.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/find_statuses.ts new file mode 100644 index 0000000000000..44847d5c8146c --- /dev/null +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/find_statuses.ts @@ -0,0 +1,67 @@ +/* + * 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 expect from '@kbn/expect'; + +import { DETECTION_ENGINE_RULES_URL } from '../../../../plugins/siem/common/constants'; +import { FtrProviderContext } from '../../common/ftr_provider_context'; +import { + createSignalsIndex, + deleteAllAlerts, + deleteSignalsIndex, + deleteAllRulesStatuses, + getSimpleRule, +} from './utils'; + +// eslint-disable-next-line import/no-default-export +export default ({ getService }: FtrProviderContext): void => { + const supertest = getService('supertest'); + const es = getService('legacyEs'); + + describe('find_statuses', () => { + beforeEach(async () => { + await createSignalsIndex(supertest); + }); + + afterEach(async () => { + await deleteSignalsIndex(supertest); + await deleteAllAlerts(es); + await deleteAllRulesStatuses(es); + }); + + it('should return an empty find statuses body correctly if no statuses are loaded', async () => { + const { body } = await supertest + .post(`${DETECTION_ENGINE_RULES_URL}/_find_statuses`) + .set('kbn-xsrf', 'true') + .send({ ids: [] }) + .expect(200); + + expect(body).to.eql({}); + }); + + it('should return a single rule status when a single rule is loaded from a find status with defaults added', async () => { + // add a single rule + const { body: resBody } = await supertest + .post(DETECTION_ENGINE_RULES_URL) + .set('kbn-xsrf', 'true') + .send(getSimpleRule()) + .expect(200); + + // wait for Task Manager to execute the rule and update status + await new Promise(resolve => setTimeout(resolve, 5000)); + + // query the single rule from _find + const { body } = await supertest + .post(`${DETECTION_ENGINE_RULES_URL}/_find_statuses`) + .set('kbn-xsrf', 'true') + .send({ ids: [resBody.id] }) + .expect(200); + + // expected result for status should be 'going to run' or 'succeeded + expect(['succeeded', 'going to run']).to.contain(body[resBody.id].current_status.status); + }); + }); +}; diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/get_prepackaged_rules_status.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/get_prepackaged_rules_status.ts index 49cf150126fda..e2dce77c1d70a 100644 --- a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/get_prepackaged_rules_status.ts +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/get_prepackaged_rules_status.ts @@ -9,7 +9,7 @@ import expect from '@kbn/expect'; import { DETECTION_ENGINE_PREPACKAGED_URL, DETECTION_ENGINE_RULES_URL, -} from '../../../../legacy/plugins/siem/common/constants'; +} from '../../../../plugins/siem/common/constants'; import { FtrProviderContext } from '../../common/ftr_provider_context'; import { createSignalsIndex, deleteAllAlerts, deleteSignalsIndex, getSimpleRule } from './utils'; diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/import_rules.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/import_rules.ts index ae4589e32ec11..4def508fabbc3 100644 --- a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/import_rules.ts +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/import_rules.ts @@ -6,7 +6,7 @@ import expect from '@kbn/expect'; -import { DETECTION_ENGINE_RULES_URL } from '../../../../legacy/plugins/siem/common/constants'; +import { DETECTION_ENGINE_RULES_URL } from '../../../../plugins/siem/common/constants'; import { FtrProviderContext } from '../../common/ftr_provider_context'; import { createSignalsIndex, diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/index.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/index.ts index b8034fd92e988..917654e50cb99 100644 --- a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/index.ts +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/index.ts @@ -18,6 +18,7 @@ export default ({ loadTestFile }: FtrProviderContext): void => { loadTestFile(require.resolve('./delete_rules_bulk')); loadTestFile(require.resolve('./export_rules')); loadTestFile(require.resolve('./find_rules')); + loadTestFile(require.resolve('./find_statuses')); loadTestFile(require.resolve('./get_prepackaged_rules_status')); loadTestFile(require.resolve('./import_rules')); loadTestFile(require.resolve('./read_rules')); diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/open_close_signals.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/open_close_signals.ts index e9e3e4299d108..3c8c20646885a 100644 --- a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/open_close_signals.ts +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/open_close_signals.ts @@ -6,7 +6,7 @@ import expect from '@kbn/expect'; -import { DETECTION_ENGINE_SIGNALS_STATUS_URL } from '../../../../legacy/plugins/siem/common/constants'; +import { DETECTION_ENGINE_SIGNALS_STATUS_URL } from '../../../../plugins/siem/common/constants'; import { FtrProviderContext } from '../../common/ftr_provider_context'; import { createSignalsIndex, diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/patch_rules.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/patch_rules.ts index 53a3d15690efc..c3ecf79e58955 100644 --- a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/patch_rules.ts +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/patch_rules.ts @@ -6,7 +6,7 @@ import expect from '@kbn/expect'; -import { DETECTION_ENGINE_RULES_URL } from '../../../../legacy/plugins/siem/common/constants'; +import { DETECTION_ENGINE_RULES_URL } from '../../../../plugins/siem/common/constants'; import { FtrProviderContext } from '../../common/ftr_provider_context'; import { createSignalsIndex, diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/patch_rules_bulk.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/patch_rules_bulk.ts index c13e8909dacf9..8515d1cf404ea 100644 --- a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/patch_rules_bulk.ts +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/patch_rules_bulk.ts @@ -6,7 +6,7 @@ import expect from '@kbn/expect'; -import { DETECTION_ENGINE_RULES_URL } from '../../../../legacy/plugins/siem/common/constants'; +import { DETECTION_ENGINE_RULES_URL } from '../../../../plugins/siem/common/constants'; import { FtrProviderContext } from '../../common/ftr_provider_context'; import { createSignalsIndex, diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/query_signals.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/query_signals.ts index 6fa62412ed467..7c8bd8981db10 100644 --- a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/query_signals.ts +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/query_signals.ts @@ -6,7 +6,7 @@ import expect from '@kbn/expect'; -import { DETECTION_ENGINE_QUERY_SIGNALS_URL } from '../../../../legacy/plugins/siem/common/constants'; +import { DETECTION_ENGINE_QUERY_SIGNALS_URL } from '../../../../plugins/siem/common/constants'; import { FtrProviderContext } from '../../common/ftr_provider_context'; import { getSignalStatus, createSignalsIndex, deleteSignalsIndex } from './utils'; diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/read_rules.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/read_rules.ts index 2ea62b0756f73..4d7449dae2dbd 100644 --- a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/read_rules.ts +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/read_rules.ts @@ -6,7 +6,7 @@ import expect from '@kbn/expect'; -import { DETECTION_ENGINE_RULES_URL } from '../../../../legacy/plugins/siem/common/constants'; +import { DETECTION_ENGINE_RULES_URL } from '../../../../plugins/siem/common/constants'; import { FtrProviderContext } from '../../common/ftr_provider_context'; import { createSignalsIndex, diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/update_rules.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/update_rules.ts index 92c78be72bf01..4b81b7d4304b2 100644 --- a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/update_rules.ts +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/update_rules.ts @@ -6,7 +6,7 @@ import expect from '@kbn/expect'; -import { DETECTION_ENGINE_RULES_URL } from '../../../../legacy/plugins/siem/common/constants'; +import { DETECTION_ENGINE_RULES_URL } from '../../../../plugins/siem/common/constants'; import { FtrProviderContext } from '../../common/ftr_provider_context'; import { createSignalsIndex, diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/update_rules_bulk.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/update_rules_bulk.ts index 220a4af4c5c5e..760e17ae1752e 100644 --- a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/update_rules_bulk.ts +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/update_rules_bulk.ts @@ -6,7 +6,7 @@ import expect from '@kbn/expect'; -import { DETECTION_ENGINE_RULES_URL } from '../../../../legacy/plugins/siem/common/constants'; +import { DETECTION_ENGINE_RULES_URL } from '../../../../plugins/siem/common/constants'; import { FtrProviderContext } from '../../common/ftr_provider_context'; import { createSignalsIndex, diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/utils.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/utils.ts index 7b725a7830c56..e508cf1aaa2e0 100644 --- a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/utils.ts +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/utils.ts @@ -4,8 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ -import { OutputRuleAlertRest } from '../../../../legacy/plugins/siem/server/lib/detection_engine/types'; -import { DETECTION_ENGINE_INDEX_URL } from '../../../../legacy/plugins/siem/common/constants'; +import { OutputRuleAlertRest } from '../../../../plugins/siem/server/lib/detection_engine/types'; +import { DETECTION_ENGINE_INDEX_URL } from '../../../../plugins/siem/common/constants'; /** * This will remove server generated properties such as date times, etc... @@ -154,7 +154,7 @@ export const getSimpleRuleOutput = (ruleId = 'rule-1'): Partial => { }); }; +/** + * Remove all rules statuses from the .kibana index + * @param es The ElasticSearch handle + */ +export const deleteAllRulesStatuses = async (es: any): Promise => { + await es.deleteByQuery({ + index: '.kibana', + q: 'type:siem-detection-engine-rule-status', + waitForCompletion: true, + refresh: 'wait_for', + }); +}; + /** * Creates the signals index for use inside of beforeEach blocks of tests * @param supertest The supertest client library @@ -413,5 +426,5 @@ export const getComplexRuleOutput = (ruleId = 'rule-1'): Partial = core.savedObjects.registerType({ name: SAVED_OBJECT_WITH_SECRET_TYPE, hidden: false, - namespaceAgnostic: false, + namespaceType: 'single', mappings: deepFreeze({ properties: { publicProperty: { type: 'keyword' }, diff --git a/x-pack/test/endpoint_api_integration_no_ingest/apis/alerts.ts b/x-pack/test/endpoint_api_integration_no_ingest/apis/alerts.ts new file mode 100644 index 0000000000000..b75d69238d653 --- /dev/null +++ b/x-pack/test/endpoint_api_integration_no_ingest/apis/alerts.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 { FtrProviderContext } from '../ftr_provider_context'; + +export default function({ getService }: FtrProviderContext) { + const esArchiver = getService('esArchiver'); + const supertest = getService('supertest'); + + describe('Endpoint alert API without ingest manager initialized', () => { + before(async () => { + await esArchiver.load('endpoint/alerts/api_feature'); + await esArchiver.load('endpoint/alerts/host_api_feature'); + }); + + after(async () => { + await esArchiver.unload('endpoint/alerts/api_feature'); + await esArchiver.unload('endpoint/alerts/host_api_feature'); + }); + + it('should return a 500', async () => { + await supertest.get('/api/endpoint/alerts').expect(500); + }); + }); +} diff --git a/x-pack/test/endpoint_api_integration_no_ingest/apis/index.ts b/x-pack/test/endpoint_api_integration_no_ingest/apis/index.ts new file mode 100644 index 0000000000000..6110f398df5a0 --- /dev/null +++ b/x-pack/test/endpoint_api_integration_no_ingest/apis/index.ts @@ -0,0 +1,16 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { FtrProviderContext } from '../ftr_provider_context'; + +export default function endpointAPIIntegrationTests({ loadTestFile }: FtrProviderContext) { + describe('Endpoint plugin', function() { + this.tags('ciGroup7'); + loadTestFile(require.resolve('./index_pattern')); + loadTestFile(require.resolve('./metadata')); + loadTestFile(require.resolve('./alerts')); + }); +} diff --git a/x-pack/test/endpoint_api_integration_no_ingest/apis/index_pattern.ts b/x-pack/test/endpoint_api_integration_no_ingest/apis/index_pattern.ts new file mode 100644 index 0000000000000..664ef7d96847c --- /dev/null +++ b/x-pack/test/endpoint_api_integration_no_ingest/apis/index_pattern.ts @@ -0,0 +1,16 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { FtrProviderContext } from '../ftr_provider_context'; + +export default function({ getService }: FtrProviderContext) { + const supertest = getService('supertest'); + + describe('Endpoint index pattern API without ingest manager initialized', () => { + it('should not retrieve the index pattern for events', async () => { + await supertest.get('/api/endpoint/index_pattern/events').expect(404); + }); + }); +} diff --git a/x-pack/test/endpoint_api_integration_no_ingest/apis/metadata.ts b/x-pack/test/endpoint_api_integration_no_ingest/apis/metadata.ts new file mode 100644 index 0000000000000..886d3cf3d9516 --- /dev/null +++ b/x-pack/test/endpoint_api_integration_no_ingest/apis/metadata.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 { FtrProviderContext } from '../ftr_provider_context'; + +export default function({ getService }: FtrProviderContext) { + const esArchiver = getService('esArchiver'); + const supertest = getService('supertest'); + describe('test metadata api when ingest manager is not initialized', () => { + before(async () => await esArchiver.load('endpoint/metadata/api_feature')); + after(async () => await esArchiver.unload('endpoint/metadata/api_feature')); + it('metadata api should return a 500', async () => { + await supertest + .post('/api/endpoint/metadata') + .set('kbn-xsrf', 'xxx') + .send() + .expect(500); + }); + }); +} diff --git a/x-pack/test/endpoint_api_integration_no_ingest/config.ts b/x-pack/test/endpoint_api_integration_no_ingest/config.ts new file mode 100644 index 0000000000000..bf8b68a7e991c --- /dev/null +++ b/x-pack/test/endpoint_api_integration_no_ingest/config.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. + */ + +import { FtrConfigProviderContext } from '@kbn/test/types/ftr'; + +export default async function({ readConfigFile }: FtrConfigProviderContext) { + const xPackAPITestsConfig = await readConfigFile(require.resolve('../api_integration/config.js')); + + return { + ...xPackAPITestsConfig.getAll(), + testFiles: [require.resolve('./apis')], + junit: { + reportName: 'X-Pack Endpoint API Integration Without Ingest Tests', + }, + }; +} diff --git a/x-pack/test/endpoint_api_integration_no_ingest/ftr_provider_context.d.ts b/x-pack/test/endpoint_api_integration_no_ingest/ftr_provider_context.d.ts new file mode 100644 index 0000000000000..2751dbcdc6539 --- /dev/null +++ b/x-pack/test/endpoint_api_integration_no_ingest/ftr_provider_context.d.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 { GenericFtrProviderContext } from '@kbn/test/types/ftr'; + +import { services } from '../api_integration/services'; + +export type FtrProviderContext = GenericFtrProviderContext; diff --git a/x-pack/test/functional/apps/discover/preserve_url.ts b/x-pack/test/functional/apps/discover/preserve_url.ts index 85142336a0a6b..9b9b2e2c60840 100644 --- a/x-pack/test/functional/apps/discover/preserve_url.ts +++ b/x-pack/test/functional/apps/discover/preserve_url.ts @@ -10,7 +10,6 @@ import { FtrProviderContext } from '../../ftr_provider_context'; export default function({ getService, getPageObjects }: FtrProviderContext) { const esArchiver = getService('esArchiver'); const PageObjects = getPageObjects(['common', 'discover', 'spaceSelector', 'header']); - const appsMenu = getService('appsMenu'); const globalNav = getService('globalNav'); describe('preserve url', function() { @@ -26,8 +25,7 @@ export default function({ getService, getPageObjects }: FtrProviderContext) { await PageObjects.common.navigateToApp('discover'); await PageObjects.discover.saveSearch('A Search'); await PageObjects.common.navigateToApp('home'); - await appsMenu.clickLink('Discover'); - await PageObjects.discover.waitUntilSearchingHasFinished(); + await PageObjects.header.clickDiscover(); const activeTitle = await globalNav.getLastBreadcrumb(); expect(activeTitle).to.be('A Search'); }); @@ -42,7 +40,7 @@ export default function({ getService, getPageObjects }: FtrProviderContext) { await PageObjects.spaceSelector.expectHomePage('another-space'); // other space - await appsMenu.clickLink('Discover'); + await PageObjects.header.clickDiscover(); await PageObjects.discover.saveSearch('A Search in another space'); await PageObjects.spaceSelector.openSpacesNav(); @@ -50,7 +48,7 @@ export default function({ getService, getPageObjects }: FtrProviderContext) { await PageObjects.spaceSelector.expectHomePage('default'); // default space - await appsMenu.clickLink('Discover'); + await PageObjects.header.clickDiscover(); await PageObjects.discover.waitUntilSearchingHasFinished(); const activeTitleDefaultSpace = await globalNav.getLastBreadcrumb(); expect(activeTitleDefaultSpace).to.be('A Search'); @@ -60,7 +58,7 @@ export default function({ getService, getPageObjects }: FtrProviderContext) { await PageObjects.spaceSelector.expectHomePage('another-space'); // other space - await appsMenu.clickLink('Discover'); + await PageObjects.header.clickDiscover(); await PageObjects.discover.waitUntilSearchingHasFinished(); const activeTitleOtherSpace = await globalNav.getLastBreadcrumb(); expect(activeTitleOtherSpace).to.be('A Search in another space'); diff --git a/x-pack/test/functional/apps/machine_learning/anomaly_detection/advanced_job.ts b/x-pack/test/functional/apps/machine_learning/anomaly_detection/advanced_job.ts index 53b1cb83c524b..a5faf325aa6cb 100644 --- a/x-pack/test/functional/apps/machine_learning/anomaly_detection/advanced_job.ts +++ b/x-pack/test/functional/apps/machine_learning/anomaly_detection/advanced_job.ts @@ -110,13 +110,13 @@ export default function({ getService }: FtrProviderContext) { const testDataList = [ { suiteTitle: 'with multiple metric detectors and custom datafeed settings', - jobSource: 'ecommerce', + jobSource: 'ft_ecommerce', jobId: `ec_advanced_1_${Date.now()}`, get jobIdClone(): string { return `${this.jobId}_clone`; }, jobDescription: - 'Create advanced job from ecommerce dataset with multiple metric detectors and custom datafeed settings', + 'Create advanced job from ft_ecommerce dataset with multiple metric detectors and custom datafeed settings', jobGroups: ['automated', 'ecommerce', 'advanced'], get jobGroupsClone(): string[] { return [...this.jobGroups, 'clone']; @@ -207,13 +207,13 @@ export default function({ getService }: FtrProviderContext) { }, { suiteTitle: 'with categorization detector and default datafeed settings', - jobSource: 'ecommerce', + jobSource: 'ft_ecommerce', jobId: `ec_advanced_2_${Date.now()}`, get jobIdClone(): string { return `${this.jobId}_clone`; }, jobDescription: - 'Create advanced job from ecommerce dataset with a categorization detector and default datafeed settings', + 'Create advanced job from ft_ecommerce dataset with a categorization detector and default datafeed settings', jobGroups: ['automated', 'ecommerce', 'advanced'], get jobGroupsClone(): string[] { return [...this.jobGroups, 'clone']; @@ -274,16 +274,20 @@ export default function({ getService }: FtrProviderContext) { }, ]; + const calendarId = `wizard-test-calendar_${Date.now()}`; + describe('advanced job', function() { this.tags(['smoke', 'mlqa']); before(async () => { - await esArchiver.load('ml/ecommerce'); - await ml.api.createCalendar('wizard-test-calendar'); + await esArchiver.loadIfNeeded('ml/ecommerce'); + await ml.testResources.createIndexPatternIfNeeded('ft_ecommerce', 'order_date'); + await ml.testResources.setKibanaTimeZoneToUTC(); + + await ml.api.createCalendar(calendarId); await ml.securityUI.loginAsMlPowerUser(); }); after(async () => { - await esArchiver.unload('ml/ecommerce'); await ml.api.cleanMlIndices(); }); @@ -475,7 +479,7 @@ export default function({ getService }: FtrProviderContext) { }); it('job creation assigns calendars', async () => { - await ml.jobWizardCommon.addCalendar('wizard-test-calendar'); + await ml.jobWizardCommon.addCalendar(calendarId); }); it('job creation displays the model plot switch', async () => { @@ -734,7 +738,7 @@ export default function({ getService }: FtrProviderContext) { }); it('job cloning persists assigned calendars', async () => { - await ml.jobWizardCommon.assertCalendarsSelection(['wizard-test-calendar']); + await ml.jobWizardCommon.assertCalendarsSelection([calendarId]); }); it('job cloning pre-fills the model plot switch', async () => { diff --git a/x-pack/test/functional/apps/machine_learning/anomaly_detection/anomaly_explorer.ts b/x-pack/test/functional/apps/machine_learning/anomaly_detection/anomaly_explorer.ts index 83e9c01a46319..8827559a5f470 100644 --- a/x-pack/test/functional/apps/machine_learning/anomaly_detection/anomaly_explorer.ts +++ b/x-pack/test/functional/apps/machine_learning/anomaly_detection/anomaly_explorer.ts @@ -29,7 +29,7 @@ const JOB_CONFIG: Job = { const DATAFEED_CONFIG: Datafeed = { datafeed_id: 'datafeed-fq_multi_1_se', - indices: ['farequote'], + indices: ['ft_farequote'], job_id: 'fq_multi_1_ae', query: { bool: { must: [{ match_all: {} }] } }, }; @@ -59,12 +59,11 @@ export default function({ getService }: FtrProviderContext) { describe('anomaly explorer', function() { this.tags(['smoke', 'mlqa']); before(async () => { - await esArchiver.load('ml/farequote'); - await ml.securityUI.loginAsMlPowerUser(); - }); + await esArchiver.loadIfNeeded('ml/farequote'); + await ml.testResources.createIndexPatternIfNeeded('ft_farequote', '@timestamp'); + await ml.testResources.setKibanaTimeZoneToUTC(); - after(async () => { - await esArchiver.unload('ml/farequote'); + await ml.securityUI.loginAsMlPowerUser(); }); for (const testData of testDataList) { diff --git a/x-pack/test/functional/apps/machine_learning/anomaly_detection/categorization_job.ts b/x-pack/test/functional/apps/machine_learning/anomaly_detection/categorization_job.ts index 6408c6de1f928..9b5ae171d4115 100644 --- a/x-pack/test/functional/apps/machine_learning/anomaly_detection/categorization_job.ts +++ b/x-pack/test/functional/apps/machine_learning/anomaly_detection/categorization_job.ts @@ -16,7 +16,7 @@ export default function({ getService }: FtrProviderContext) { const jobId = `categorization_${Date.now()}`; const jobIdClone = `${jobId}_clone`; const jobDescription = - 'Create categorization job based on the categorization_functional_test dataset with a count rare'; + 'Create categorization job based on the ft_categorization dataset with a count rare'; const jobGroups = ['automated', 'categorization']; const jobGroupsClone = [...jobGroups, 'clone']; const detectorTypeIdentifier = 'Rare'; @@ -74,16 +74,20 @@ export default function({ getService }: FtrProviderContext) { }; } + const calendarId = `wizard-test-calendar_${Date.now()}`; + describe('categorization', function() { this.tags(['smoke', 'mlqa']); before(async () => { - await esArchiver.load('ml/categorization'); - await ml.api.createCalendar('wizard-test-calendar'); + await esArchiver.loadIfNeeded('ml/categorization'); + await ml.testResources.createIndexPatternIfNeeded('ft_categorization', '@timestamp'); + await ml.testResources.setKibanaTimeZoneToUTC(); + + await ml.api.createCalendar(calendarId); await ml.securityUI.loginAsMlPowerUser(); }); after(async () => { - await esArchiver.unload('ml/categorization'); await ml.api.cleanMlIndices(); }); @@ -97,9 +101,7 @@ export default function({ getService }: FtrProviderContext) { }); it('job creation loads the job type selection page', async () => { - await ml.jobSourceSelection.selectSourceForAnomalyDetectionJob( - 'categorization_functional_test' - ); + await ml.jobSourceSelection.selectSourceForAnomalyDetectionJob('ft_categorization'); }); it('job creation loads the categorization job wizard page', async () => { @@ -178,7 +180,7 @@ export default function({ getService }: FtrProviderContext) { }); it('job creation assigns calendars', async () => { - await ml.jobWizardCommon.addCalendar('wizard-test-calendar'); + await ml.jobWizardCommon.addCalendar(calendarId); }); it('job creation opens the advanced section', async () => { @@ -310,7 +312,7 @@ export default function({ getService }: FtrProviderContext) { }); it('job cloning persists assigned calendars', async () => { - await ml.jobWizardCommon.assertCalendarsSelection(['wizard-test-calendar']); + await ml.jobWizardCommon.assertCalendarsSelection([calendarId]); }); it('job cloning opens the advanced section', async () => { diff --git a/x-pack/test/functional/apps/machine_learning/anomaly_detection/date_nanos_job.ts b/x-pack/test/functional/apps/machine_learning/anomaly_detection/date_nanos_job.ts index 2a9824f46778d..570deee01c684 100644 --- a/x-pack/test/functional/apps/machine_learning/anomaly_detection/date_nanos_job.ts +++ b/x-pack/test/functional/apps/machine_learning/anomaly_detection/date_nanos_job.ts @@ -104,7 +104,7 @@ export default function({ getService }: FtrProviderContext) { const testDataList = [ { suiteTitle: 'with count detector and model plot disabled', - jobSource: 'event_rate_gen_trend_nanos', + jobSource: 'ft_event_rate_gen_trend_nanos', jobId: `event_rate_nanos_count_1_${Date.now()}`, jobDescription: 'Create advanced job based on the event rate dataset with a date_nanos time field, 30m bucketspan and count', @@ -168,12 +168,18 @@ export default function({ getService }: FtrProviderContext) { describe('job on data set with date_nanos time field', function() { this.tags(['smoke', 'mlqa']); before(async () => { - await esArchiver.load('ml/event_rate_nanos'); + await esArchiver.loadIfNeeded('ml/event_rate_nanos'); + await ml.testResources.createIndexPatternIfNeeded( + 'ft_event_rate_gen_trend_nanos', + '@timestamp' + ); + await ml.testResources.setKibanaTimeZoneToUTC(); + + await esArchiver.loadIfNeeded('ml/event_rate_nanos'); await ml.securityUI.loginAsMlPowerUser(); }); after(async () => { - await esArchiver.unload('ml/event_rate_nanos'); await ml.api.cleanMlIndices(); }); diff --git a/x-pack/test/functional/apps/machine_learning/anomaly_detection/multi_metric_job.ts b/x-pack/test/functional/apps/machine_learning/anomaly_detection/multi_metric_job.ts index 08175b7946259..4739f987541d6 100644 --- a/x-pack/test/functional/apps/machine_learning/anomaly_detection/multi_metric_job.ts +++ b/x-pack/test/functional/apps/machine_learning/anomaly_detection/multi_metric_job.ts @@ -71,16 +71,20 @@ export default function({ getService }: FtrProviderContext) { }; } + const calendarId = `wizard-test-calendar_${Date.now()}`; + describe('multi metric', function() { this.tags(['smoke', 'mlqa']); before(async () => { - await esArchiver.load('ml/farequote'); - await ml.api.createCalendar('wizard-test-calendar'); + await esArchiver.loadIfNeeded('ml/farequote'); + await ml.testResources.createIndexPatternIfNeeded('ft_farequote', '@timestamp'); + await ml.testResources.setKibanaTimeZoneToUTC(); + + await ml.api.createCalendar(calendarId); await ml.securityUI.loginAsMlPowerUser(); }); after(async () => { - await esArchiver.unload('ml/farequote'); await ml.api.cleanMlIndices(); }); @@ -94,7 +98,7 @@ export default function({ getService }: FtrProviderContext) { }); it('job creation loads the job type selection page', async () => { - await ml.jobSourceSelection.selectSourceForAnomalyDetectionJob('farequote'); + await ml.jobSourceSelection.selectSourceForAnomalyDetectionJob('ft_farequote'); }); it('job creation loads the multi metric job wizard page', async () => { @@ -181,7 +185,7 @@ export default function({ getService }: FtrProviderContext) { }); it('job creation assigns calendars', async () => { - await ml.jobWizardCommon.addCalendar('wizard-test-calendar'); + await ml.jobWizardCommon.addCalendar(calendarId); }); it('job creation opens the advanced section', async () => { @@ -329,7 +333,7 @@ export default function({ getService }: FtrProviderContext) { }); it('job cloning persists assigned calendars', async () => { - await ml.jobWizardCommon.assertCalendarsSelection(['wizard-test-calendar']); + await ml.jobWizardCommon.assertCalendarsSelection([calendarId]); }); it('job cloning opens the advanced section', async () => { diff --git a/x-pack/test/functional/apps/machine_learning/anomaly_detection/population_job.ts b/x-pack/test/functional/apps/machine_learning/anomaly_detection/population_job.ts index 512d13307ea05..0279c70bb73a9 100644 --- a/x-pack/test/functional/apps/machine_learning/anomaly_detection/population_job.ts +++ b/x-pack/test/functional/apps/machine_learning/anomaly_detection/population_job.ts @@ -85,16 +85,20 @@ export default function({ getService }: FtrProviderContext) { }; } + const calendarId = `wizard-test-calendar_${Date.now()}`; + describe('population', function() { this.tags(['smoke', 'mlqa']); before(async () => { - await esArchiver.load('ml/ecommerce'); - await ml.api.createCalendar('wizard-test-calendar'); + await esArchiver.loadIfNeeded('ml/ecommerce'); + await ml.testResources.createIndexPatternIfNeeded('ft_ecommerce', 'order_date'); + await ml.testResources.setKibanaTimeZoneToUTC(); + + await ml.api.createCalendar(calendarId); await ml.securityUI.loginAsMlPowerUser(); }); after(async () => { - await esArchiver.unload('ml/farequote'); await ml.api.cleanMlIndices(); }); @@ -108,7 +112,7 @@ export default function({ getService }: FtrProviderContext) { }); it('job creation loads the job type selection page', async () => { - await ml.jobSourceSelection.selectSourceForAnomalyDetectionJob('ecommerce'); + await ml.jobSourceSelection.selectSourceForAnomalyDetectionJob('ft_ecommerce'); }); it('job creation loads the population job wizard page', async () => { @@ -208,7 +212,7 @@ export default function({ getService }: FtrProviderContext) { }); it('job creation assigns calendars', async () => { - await ml.jobWizardCommon.addCalendar('wizard-test-calendar'); + await ml.jobWizardCommon.addCalendar(calendarId); }); it('job creation opens the advanced section', async () => { @@ -367,7 +371,7 @@ export default function({ getService }: FtrProviderContext) { }); it('job cloning persists assigned calendars', async () => { - await ml.jobWizardCommon.assertCalendarsSelection(['wizard-test-calendar']); + await ml.jobWizardCommon.assertCalendarsSelection([calendarId]); }); it('job cloning opens the advanced section', async () => { diff --git a/x-pack/test/functional/apps/machine_learning/anomaly_detection/saved_search_job.ts b/x-pack/test/functional/apps/machine_learning/anomaly_detection/saved_search_job.ts index a13cf3d61128e..a5652d76358eb 100644 --- a/x-pack/test/functional/apps/machine_learning/anomaly_detection/saved_search_job.ts +++ b/x-pack/test/functional/apps/machine_learning/anomaly_detection/saved_search_job.ts @@ -15,7 +15,7 @@ export default function({ getService }: FtrProviderContext) { const testDataList = [ { suiteTitle: 'with filter', - jobSource: 'farequote_filter', + jobSource: 'ft_farequote_filter', jobId: `fq_saved_search_1_${Date.now()}`, jobDescription: 'Create multi metric job based on a saved search with filter', jobGroups: ['automated', 'farequote', 'multi-metric', 'saved-search'], @@ -66,7 +66,7 @@ export default function({ getService }: FtrProviderContext) { }, { suiteTitle: 'with lucene query', - jobSource: 'farequote_lucene', + jobSource: 'ft_farequote_lucene', jobId: `fq_saved_search_2_${Date.now()}`, jobDescription: 'Create multi metric job based on a saved search with lucene query', jobGroups: ['automated', 'farequote', 'multi-metric', 'saved-search'], @@ -117,7 +117,7 @@ export default function({ getService }: FtrProviderContext) { }, { suiteTitle: 'with kuery query', - jobSource: 'farequote_kuery', + jobSource: 'ft_farequote_kuery', jobId: `fq_saved_search_3_${Date.now()}`, jobDescription: 'Create multi metric job based on a saved search with kuery query', jobGroups: ['automated', 'farequote', 'multi-metric', 'saved-search'], @@ -168,7 +168,7 @@ export default function({ getService }: FtrProviderContext) { }, { suiteTitle: 'with filter and lucene query', - jobSource: 'farequote_filter_and_lucene', + jobSource: 'ft_farequote_filter_and_lucene', jobId: `fq_saved_search_4_${Date.now()}`, jobDescription: 'Create multi metric job based on a saved search with filter and lucene query', @@ -220,7 +220,7 @@ export default function({ getService }: FtrProviderContext) { }, { suiteTitle: 'with filter and kuery query', - jobSource: 'farequote_filter_and_kuery', + jobSource: 'ft_farequote_filter_and_kuery', jobId: `fq_saved_search_5_${Date.now()}`, jobDescription: 'Create multi metric job based on a saved search with filter and kuery query', jobGroups: ['automated', 'farequote', 'multi-metric', 'saved-search'], @@ -274,12 +274,19 @@ export default function({ getService }: FtrProviderContext) { describe('saved search', function() { this.tags(['smoke', 'mlqa']); before(async () => { - await esArchiver.load('ml/farequote'); + await esArchiver.loadIfNeeded('ml/farequote'); + await ml.testResources.createIndexPatternIfNeeded('ft_farequote', '@timestamp'); + await ml.testResources.createSavedSearchFarequoteFilterIfNeeded(); + await ml.testResources.createSavedSearchFarequoteLuceneIfNeeded(); + await ml.testResources.createSavedSearchFarequoteKueryIfNeeded(); + await ml.testResources.createSavedSearchFarequoteFilterAndLuceneIfNeeded(); + await ml.testResources.createSavedSearchFarequoteFilterAndKueryIfNeeded(); + await ml.testResources.setKibanaTimeZoneToUTC(); + await ml.securityUI.loginAsMlPowerUser(); }); after(async () => { - await esArchiver.unload('ml/farequote'); await ml.api.cleanMlIndices(); }); diff --git a/x-pack/test/functional/apps/machine_learning/anomaly_detection/single_metric_job.ts b/x-pack/test/functional/apps/machine_learning/anomaly_detection/single_metric_job.ts index 4e6d480c12d82..43053decb3924 100644 --- a/x-pack/test/functional/apps/machine_learning/anomaly_detection/single_metric_job.ts +++ b/x-pack/test/functional/apps/machine_learning/anomaly_detection/single_metric_job.ts @@ -70,16 +70,20 @@ export default function({ getService }: FtrProviderContext) { }; } + const calendarId = `wizard-test-calendar_${Date.now()}`; + describe('single metric', function() { this.tags(['smoke', 'mlqa']); before(async () => { - await esArchiver.load('ml/farequote'); - await ml.api.createCalendar('wizard-test-calendar'); + await esArchiver.loadIfNeeded('ml/farequote'); + await ml.testResources.createIndexPatternIfNeeded('ft_farequote', '@timestamp'); + await ml.testResources.setKibanaTimeZoneToUTC(); + + await ml.api.createCalendar(calendarId); await ml.securityUI.loginAsMlPowerUser(); }); after(async () => { - await esArchiver.unload('ml/farequote'); await ml.api.cleanMlIndices(); }); @@ -93,7 +97,7 @@ export default function({ getService }: FtrProviderContext) { }); it('job creation loads the job type selection page', async () => { - await ml.jobSourceSelection.selectSourceForAnomalyDetectionJob('farequote'); + await ml.jobSourceSelection.selectSourceForAnomalyDetectionJob('ft_farequote'); }); it('job creation loads the single metric job wizard page', async () => { @@ -162,7 +166,7 @@ export default function({ getService }: FtrProviderContext) { }); it('job creation assigns calendars', async () => { - await ml.jobWizardCommon.addCalendar('wizard-test-calendar'); + await ml.jobWizardCommon.addCalendar(calendarId); }); it('job creation opens the advanced section', async () => { @@ -294,7 +298,7 @@ export default function({ getService }: FtrProviderContext) { }); it('job cloning persists assigned calendars', async () => { - await ml.jobWizardCommon.assertCalendarsSelection(['wizard-test-calendar']); + await ml.jobWizardCommon.assertCalendarsSelection([calendarId]); }); it('job cloning opens the advanced section', async () => { diff --git a/x-pack/test/functional/apps/machine_learning/anomaly_detection/single_metric_viewer.ts b/x-pack/test/functional/apps/machine_learning/anomaly_detection/single_metric_viewer.ts index 62b801daa3479..cc7c9828ce87d 100644 --- a/x-pack/test/functional/apps/machine_learning/anomaly_detection/single_metric_viewer.ts +++ b/x-pack/test/functional/apps/machine_learning/anomaly_detection/single_metric_viewer.ts @@ -29,7 +29,7 @@ const JOB_CONFIG: Job = { const DATAFEED_CONFIG: Datafeed = { datafeed_id: 'datafeed-fq_single_1_smv', - indices: ['farequote'], + indices: ['ft_farequote'], job_id: 'fq_single_1_smv', query: { bool: { must: [{ match_all: {} }] } }, }; @@ -42,13 +42,15 @@ export default function({ getService }: FtrProviderContext) { describe('single metric viewer', function() { this.tags(['smoke', 'mlqa']); before(async () => { - await esArchiver.load('ml/farequote'); + await esArchiver.loadIfNeeded('ml/farequote'); + await ml.testResources.createIndexPatternIfNeeded('ft_farequote', '@timestamp'); + await ml.testResources.setKibanaTimeZoneToUTC(); + await ml.api.createAndRunAnomalyDetectionLookbackJob(JOB_CONFIG, DATAFEED_CONFIG); await ml.securityUI.loginAsMlPowerUser(); }); after(async () => { - await esArchiver.unload('ml/farequote'); await ml.api.cleanMlIndices(); }); diff --git a/x-pack/test/functional/apps/machine_learning/data_frame_analytics/classification_creation.ts b/x-pack/test/functional/apps/machine_learning/data_frame_analytics/classification_creation.ts index a7c92cac2072f..8a6741bd88daa 100644 --- a/x-pack/test/functional/apps/machine_learning/data_frame_analytics/classification_creation.ts +++ b/x-pack/test/functional/apps/machine_learning/data_frame_analytics/classification_creation.ts @@ -14,13 +14,15 @@ export default function({ getService }: FtrProviderContext) { describe('classification creation', function() { this.tags(['smoke']); before(async () => { - await esArchiver.load('ml/bm_classification'); + await esArchiver.loadIfNeeded('ml/bm_classification'); + await ml.testResources.createIndexPatternIfNeeded('ft_bank_marketing', '@timestamp'); + await ml.testResources.setKibanaTimeZoneToUTC(); + await ml.securityUI.loginAsMlPowerUser(); }); after(async () => { await ml.api.cleanMlIndices(); - await esArchiver.unload('ml/bm_classification'); }); const testDataList = [ @@ -29,8 +31,8 @@ export default function({ getService }: FtrProviderContext) { jobType: 'classification', jobId: `bm_1_${Date.now()}`, jobDescription: - "Classification job based on 'bank-marketing' dataset with dependentVariable 'y' and trainingPercent '20'", - source: 'bank-marketing*', + "Classification job based on 'ft_bank_marketing' dataset with dependentVariable 'y' and trainingPercent '20'", + source: 'ft_bank_marketing', get destinationIndex(): string { return `user-${this.jobId}`; }, @@ -51,6 +53,7 @@ export default function({ getService }: FtrProviderContext) { describe(`${testData.suiteTitle}`, function() { after(async () => { await ml.api.deleteIndices(testData.destinationIndex); + await ml.testResources.deleteIndexPattern(testData.destinationIndex); }); it('loads the data frame analytics page', async () => { diff --git a/x-pack/test/functional/apps/machine_learning/data_frame_analytics/cloning.ts b/x-pack/test/functional/apps/machine_learning/data_frame_analytics/cloning.ts index caf382b532273..d98d8feaaf4fe 100644 --- a/x-pack/test/functional/apps/machine_learning/data_frame_analytics/cloning.ts +++ b/x-pack/test/functional/apps/machine_learning/data_frame_analytics/cloning.ts @@ -19,6 +19,7 @@ export default function({ getService }: FtrProviderContext) { const testDataList: Array<{ suiteTitle: string; archive: string; + indexPattern: { name: string; timeField: string }; job: DeepPartial; }> = (() => { const timestamp = Date.now(); @@ -27,12 +28,13 @@ export default function({ getService }: FtrProviderContext) { { suiteTitle: 'classification job supported by the form', archive: 'ml/bm_classification', + indexPattern: { name: 'ft_bank_marketing', timeField: '@timestamp' }, job: { id: `bm_1_${timestamp}`, description: - "Classification job based on 'bank-marketing' dataset with dependentVariable 'y' and trainingPercent '20'", + "Classification job based on 'ft_bank_marketing' dataset with dependentVariable 'y' and trainingPercent '20'", source: { - index: ['bank-marketing*'], + index: ['ft_bank_marketing'], query: { match_all: {}, }, @@ -60,11 +62,12 @@ export default function({ getService }: FtrProviderContext) { { suiteTitle: 'outlier detection job supported by the form', archive: 'ml/ihp_outlier', + indexPattern: { name: 'ft_ihp_outlier', timeField: '@timestamp' }, job: { id: `ihp_1_${timestamp}`, description: 'This is the job description', source: { - index: ['ihp_outlier'], + index: ['ft_ihp_outlier'], query: { match_all: {}, }, @@ -88,11 +91,12 @@ export default function({ getService }: FtrProviderContext) { { suiteTitle: 'regression job supported by the form', archive: 'ml/egs_regression', + indexPattern: { name: 'ft_egs_regression', timeField: '@timestamp' }, job: { id: `egs_1_${timestamp}`, description: 'This is the job description', source: { - index: ['egs_regression'], + index: ['ft_egs_regression'], query: { match_all: {}, }, @@ -120,6 +124,7 @@ export default function({ getService }: FtrProviderContext) { })(); before(async () => { + await ml.testResources.setKibanaTimeZoneToUTC(); await ml.securityUI.loginAsMlPowerUser(); }); @@ -133,7 +138,11 @@ export default function({ getService }: FtrProviderContext) { const cloneDestIndex = `${testData.job!.dest!.index}_clone`; before(async () => { - await esArchiver.load(testData.archive); + await esArchiver.loadIfNeeded(testData.archive); + await ml.testResources.createIndexPatternIfNeeded( + testData.indexPattern.name, + testData.indexPattern.timeField + ); await ml.api.createDataFrameAnalyticsJob(testData.job as DataFrameAnalyticsConfig); await ml.navigation.navigateToMl(); @@ -146,7 +155,7 @@ export default function({ getService }: FtrProviderContext) { after(async () => { await ml.api.deleteIndices(cloneDestIndex); await ml.api.deleteIndices(testData.job.dest!.index as string); - await esArchiver.unload(testData.archive); + await ml.testResources.deleteIndexPattern(testData.job.dest!.index as string); }); it('should open the flyout with a proper header', async () => { diff --git a/x-pack/test/functional/apps/machine_learning/data_frame_analytics/outlier_detection_creation.ts b/x-pack/test/functional/apps/machine_learning/data_frame_analytics/outlier_detection_creation.ts index 5481977351d8b..8dfe058cf6885 100644 --- a/x-pack/test/functional/apps/machine_learning/data_frame_analytics/outlier_detection_creation.ts +++ b/x-pack/test/functional/apps/machine_learning/data_frame_analytics/outlier_detection_creation.ts @@ -14,13 +14,15 @@ export default function({ getService }: FtrProviderContext) { describe('outlier detection creation', function() { this.tags(['smoke']); before(async () => { - await esArchiver.load('ml/ihp_outlier'); + await esArchiver.loadIfNeeded('ml/ihp_outlier'); + await ml.testResources.createIndexPatternIfNeeded('ft_ihp_outlier', '@timestamp'); + await ml.testResources.setKibanaTimeZoneToUTC(); + await ml.securityUI.loginAsMlPowerUser(); }); after(async () => { await ml.api.cleanMlIndices(); - await esArchiver.unload('ml/ihp_outlier'); }); const testDataList = [ @@ -29,7 +31,7 @@ export default function({ getService }: FtrProviderContext) { jobType: 'outlier_detection', jobId: `ihp_1_${Date.now()}`, jobDescription: 'This is the job description', - source: 'ihp_outlier', + source: 'ft_ihp_outlier', get destinationIndex(): string { return `user-${this.jobId}`; }, @@ -49,6 +51,7 @@ export default function({ getService }: FtrProviderContext) { describe(`${testData.suiteTitle}`, function() { after(async () => { await ml.api.deleteIndices(testData.destinationIndex); + await ml.testResources.deleteIndexPattern(testData.destinationIndex); }); it('loads the data frame analytics page', async () => { diff --git a/x-pack/test/functional/apps/machine_learning/data_frame_analytics/regression_creation.ts b/x-pack/test/functional/apps/machine_learning/data_frame_analytics/regression_creation.ts index aa1a133c81187..271f3e2018dad 100644 --- a/x-pack/test/functional/apps/machine_learning/data_frame_analytics/regression_creation.ts +++ b/x-pack/test/functional/apps/machine_learning/data_frame_analytics/regression_creation.ts @@ -14,13 +14,15 @@ export default function({ getService }: FtrProviderContext) { describe('regression creation', function() { this.tags(['smoke']); before(async () => { - await esArchiver.load('ml/egs_regression'); + await esArchiver.loadIfNeeded('ml/egs_regression'); + await ml.testResources.createIndexPatternIfNeeded('ft_egs_regression', '@timestamp'); + await ml.testResources.setKibanaTimeZoneToUTC(); + await ml.securityUI.loginAsMlPowerUser(); }); after(async () => { await ml.api.cleanMlIndices(); - await esArchiver.unload('ml/egs_regression'); }); const testDataList = [ @@ -29,7 +31,7 @@ export default function({ getService }: FtrProviderContext) { jobType: 'regression', jobId: `egs_1_${Date.now()}`, jobDescription: 'This is the job description', - source: 'egs_regression', + source: 'ft_egs_regression', get destinationIndex(): string { return `user-${this.jobId}`; }, @@ -51,6 +53,7 @@ export default function({ getService }: FtrProviderContext) { describe(`${testData.suiteTitle}`, function() { after(async () => { await ml.api.deleteIndices(testData.destinationIndex); + await ml.testResources.deleteIndexPattern(testData.destinationIndex); }); it('loads the data frame analytics page', async () => { diff --git a/x-pack/test/functional/apps/machine_learning/data_visualizer/file_data_visualizer.ts b/x-pack/test/functional/apps/machine_learning/data_visualizer/file_data_visualizer.ts index 94b28e5035edf..ae958ad7f570f 100644 --- a/x-pack/test/functional/apps/machine_learning/data_visualizer/file_data_visualizer.ts +++ b/x-pack/test/functional/apps/machine_learning/data_visualizer/file_data_visualizer.ts @@ -36,6 +36,8 @@ export default function({ getService }: FtrProviderContext) { describe('file based', function() { this.tags(['smoke', 'mlqa']); before(async () => { + await ml.testResources.setKibanaTimeZoneToUTC(); + await ml.securityUI.loginAsMlPowerUser(); await ml.navigation.navigateToMl(); }); diff --git a/x-pack/test/functional/apps/machine_learning/data_visualizer/index_data_visualizer.ts b/x-pack/test/functional/apps/machine_learning/data_visualizer/index_data_visualizer.ts index 1ee74368c72fa..e71b57a4562e7 100644 --- a/x-pack/test/functional/apps/machine_learning/data_visualizer/index_data_visualizer.ts +++ b/x-pack/test/functional/apps/machine_learning/data_visualizer/index_data_visualizer.ts @@ -45,7 +45,7 @@ export default function({ getService }: FtrProviderContext) { const farequoteIndexPatternTestData: TestData = { suiteTitle: 'index pattern', - sourceIndexOrSavedSearch: 'farequote', + sourceIndexOrSavedSearch: 'ft_farequote', advancedJobWizardDatafeedQuery: `{ "bool": { "must": [ @@ -128,7 +128,7 @@ export default function({ getService }: FtrProviderContext) { const farequoteKQLSearchTestData: TestData = { suiteTitle: 'KQL saved search', - sourceIndexOrSavedSearch: 'farequote_kuery', + sourceIndexOrSavedSearch: 'ft_farequote_kuery', advancedJobWizardDatafeedQuery: `{ "bool": { "must": [ @@ -211,7 +211,7 @@ export default function({ getService }: FtrProviderContext) { const farequoteLuceneSearchTestData: TestData = { suiteTitle: 'lucene saved search', - sourceIndexOrSavedSearch: 'farequote_lucene', + sourceIndexOrSavedSearch: 'ft_farequote_lucene', advancedJobWizardDatafeedQuery: `{ "bool": { "must": [ @@ -378,12 +378,13 @@ export default function({ getService }: FtrProviderContext) { describe('index based', function() { this.tags(['smoke', 'mlqa']); before(async () => { - await esArchiver.load('ml/farequote'); - await ml.securityUI.loginAsMlPowerUser(); - }); + await esArchiver.loadIfNeeded('ml/farequote'); + await ml.testResources.createIndexPatternIfNeeded('ft_farequote', '@timestamp'); + await ml.testResources.createSavedSearchFarequoteLuceneIfNeeded(); + await ml.testResources.createSavedSearchFarequoteKueryIfNeeded(); + await ml.testResources.setKibanaTimeZoneToUTC(); - after(async () => { - await esArchiver.unload('ml/farequote'); + await ml.securityUI.loginAsMlPowerUser(); }); // TODO - add tests for diff --git a/x-pack/test/functional/apps/machine_learning/feature_controls/ml_security.ts b/x-pack/test/functional/apps/machine_learning/feature_controls/ml_security.ts index f3731f46a5bce..ec4b708152a4c 100644 --- a/x-pack/test/functional/apps/machine_learning/feature_controls/ml_security.ts +++ b/x-pack/test/functional/apps/machine_learning/feature_controls/ml_security.ts @@ -7,15 +7,12 @@ import expect from '@kbn/expect'; import { FtrProviderContext } from '../../../ftr_provider_context'; export default function({ getPageObjects, getService }: FtrProviderContext) { - const esArchiver = getService('esArchiver'); const security = getService('security'); const appsMenu = getService('appsMenu'); const PageObjects = getPageObjects(['common', 'security']); describe('security', function() { before(async () => { - await esArchiver.load('empty_kibana'); - await security.role.create('global_all_role', { elasticsearch: { indices: [{ names: ['logstash-*'], privileges: ['read', 'view_index_metadata'] }], @@ -33,7 +30,6 @@ export default function({ getPageObjects, getService }: FtrProviderContext) { }); after(async () => { - await esArchiver.unload('empty_kibana'); await security.role.delete('global_all_role'); // logout, so the other tests don't accidentally run as the custom users we're testing below diff --git a/x-pack/test/functional/apps/machine_learning/feature_controls/ml_spaces.ts b/x-pack/test/functional/apps/machine_learning/feature_controls/ml_spaces.ts index fc94688e98811..828d7bcc0ae2f 100644 --- a/x-pack/test/functional/apps/machine_learning/feature_controls/ml_spaces.ts +++ b/x-pack/test/functional/apps/machine_learning/feature_controls/ml_spaces.ts @@ -7,21 +7,12 @@ import expect from '@kbn/expect'; import { FtrProviderContext } from '../../../ftr_provider_context'; export default function({ getPageObjects, getService }: FtrProviderContext) { - const esArchiver = getService('esArchiver'); const spacesService = getService('spaces'); const PageObjects = getPageObjects(['common', 'dashboard', 'security', 'error']); const appsMenu = getService('appsMenu'); const testSubjects = getService('testSubjects'); describe('spaces', () => { - before(async () => { - await esArchiver.load('empty_kibana'); - }); - - after(async () => { - await esArchiver.unload('empty_kibana'); - }); - describe('space with no features disabled', () => { before(async () => { await spacesService.create({ diff --git a/x-pack/test/functional/apps/machine_learning/index.ts b/x-pack/test/functional/apps/machine_learning/index.ts index 47c699e309491..3e1991120d2d2 100644 --- a/x-pack/test/functional/apps/machine_learning/index.ts +++ b/x-pack/test/functional/apps/machine_learning/index.ts @@ -6,6 +6,7 @@ import { FtrProviderContext } from '../../ftr_provider_context'; export default function({ getService, loadTestFile }: FtrProviderContext) { + const esArchiver = getService('esArchiver'); const ml = getService('ml'); describe('machine learning', function() { @@ -19,6 +20,26 @@ export default function({ getService, loadTestFile }: FtrProviderContext) { after(async () => { await ml.securityCommon.cleanMlUsers(); await ml.securityCommon.cleanMlRoles(); + + await ml.testResources.deleteSavedSearches(); + + await ml.testResources.deleteIndexPattern('ft_farequote'); + await ml.testResources.deleteIndexPattern('ft_ecommerce'); + await ml.testResources.deleteIndexPattern('ft_categorization'); + await ml.testResources.deleteIndexPattern('ft_event_rate_gen_trend_nanos'); + await ml.testResources.deleteIndexPattern('ft_bank_marketing'); + await ml.testResources.deleteIndexPattern('ft_ihp_outlier'); + await ml.testResources.deleteIndexPattern('ft_egs_regression'); + + await esArchiver.unload('ml/farequote'); + await esArchiver.unload('ml/ecommerce'); + await esArchiver.unload('ml/categorization'); + await esArchiver.unload('ml/event_rate_nanos'); + await esArchiver.unload('ml/bm_classification'); + await esArchiver.unload('ml/ihp_outlier'); + await esArchiver.unload('ml/egs_regression'); + + await ml.testResources.resetKibanaTimeZone(); }); loadTestFile(require.resolve('./feature_controls')); diff --git a/x-pack/test/functional/apps/machine_learning/pages.ts b/x-pack/test/functional/apps/machine_learning/pages.ts index 78c6ec4f1b2ff..95930f18061fa 100644 --- a/x-pack/test/functional/apps/machine_learning/pages.ts +++ b/x-pack/test/functional/apps/machine_learning/pages.ts @@ -7,20 +7,15 @@ import { FtrProviderContext } from '../../ftr_provider_context'; export default function({ getService }: FtrProviderContext) { - const esArchiver = getService('esArchiver'); const ml = getService('ml'); describe('page navigation', function() { this.tags(['smoke', 'mlqa']); before(async () => { - await esArchiver.load('empty_kibana'); + await ml.api.cleanMlIndices(); await ml.securityUI.loginAsMlPowerUser(); }); - after(async () => { - await esArchiver.unload('empty_kibana'); - }); - it('loads the home page', async () => { await ml.navigation.navigateToMl(); }); diff --git a/x-pack/test/functional/apps/maps/embeddable/dashboard.js b/x-pack/test/functional/apps/maps/embeddable/dashboard.js index 43d5cccb20905..9027bb5309ff8 100644 --- a/x-pack/test/functional/apps/maps/embeddable/dashboard.js +++ b/x-pack/test/functional/apps/maps/embeddable/dashboard.js @@ -13,6 +13,7 @@ export default function({ getPageObjects, getService }) { const dashboardPanelActions = getService('dashboardPanelActions'); const inspector = getService('inspector'); const testSubjects = getService('testSubjects'); + const browser = getService('browser'); describe('embed in dashboard', () => { before(async () => { @@ -111,5 +112,15 @@ export default function({ getPageObjects, getService }) { const afterRefreshTimerTimestamp = await getRequestTimestamp(); expect(beforeRefreshTimerTimestamp).not.to.equal(afterRefreshTimerTimestamp); }); + + // see https://github.com/elastic/kibana/issues/61596 on why it is specific to maps + it("dashboard's back button should navigate to previous page", async () => { + await PageObjects.common.navigateToApp('dashboard'); + await PageObjects.dashboard.preserveCrossAppState(); + await PageObjects.dashboard.loadSavedDashboard('map embeddable example'); + await PageObjects.dashboard.waitForRenderComplete(); + await browser.goBack(); + expect(await PageObjects.dashboard.onDashboardLandingPage()).to.be(true); + }); }); } diff --git a/x-pack/test/functional/apps/maps/joins.js b/x-pack/test/functional/apps/maps/joins.js index 89a6c6ea82e53..4b36109a4de9c 100644 --- a/x-pack/test/functional/apps/maps/joins.js +++ b/x-pack/test/functional/apps/maps/joins.js @@ -9,7 +9,7 @@ import _ from 'lodash'; import { MAPBOX_STYLES } from './mapbox_styles'; -const JOIN_PROPERTY_NAME = '__kbnjoin__max_of_prop1_groupby_meta_for_geo_shapes*.shape_name'; +const JOIN_PROPERTY_NAME = '__kbnjoin__max_of_prop1__855ccb86-fe42-11e8-8eb2-f2801f1b9fd1'; const EXPECTED_JOIN_VALUES = { alpha: 10, bravo: 3, diff --git a/x-pack/test/functional/apps/maps/mapbox_styles.js b/x-pack/test/functional/apps/maps/mapbox_styles.js index 508a019db1764..63bfc331d8886 100644 --- a/x-pack/test/functional/apps/maps/mapbox_styles.js +++ b/x-pack/test/functional/apps/maps/mapbox_styles.js @@ -27,7 +27,7 @@ export const MAPBOX_STYLES = { 'case', [ '==', - ['feature-state', '__kbnjoin__max_of_prop1_groupby_meta_for_geo_shapes*.shape_name'], + ['feature-state', '__kbnjoin__max_of_prop1__855ccb86-fe42-11e8-8eb2-f2801f1b9fd1'], null, ], 2, @@ -39,7 +39,7 @@ export const MAPBOX_STYLES = { 'to-number', [ 'feature-state', - '__kbnjoin__max_of_prop1_groupby_meta_for_geo_shapes*.shape_name', + '__kbnjoin__max_of_prop1__855ccb86-fe42-11e8-8eb2-f2801f1b9fd1', ], ], 12, @@ -97,7 +97,7 @@ export const MAPBOX_STYLES = { 'case', [ '==', - ['feature-state', '__kbnjoin__max_of_prop1_groupby_meta_for_geo_shapes*.shape_name'], + ['feature-state', '__kbnjoin__max_of_prop1__855ccb86-fe42-11e8-8eb2-f2801f1b9fd1'], null, ], 2, @@ -109,7 +109,7 @@ export const MAPBOX_STYLES = { 'to-number', [ 'feature-state', - '__kbnjoin__max_of_prop1_groupby_meta_for_geo_shapes*.shape_name', + '__kbnjoin__max_of_prop1__855ccb86-fe42-11e8-8eb2-f2801f1b9fd1', ], ], 12, diff --git a/x-pack/test/functional/apps/transform/cloning.ts b/x-pack/test/functional/apps/transform/cloning.ts index 5f05fdb093d10..e6e12f60f0bcc 100644 --- a/x-pack/test/functional/apps/transform/cloning.ts +++ b/x-pack/test/functional/apps/transform/cloning.ts @@ -11,7 +11,7 @@ function getTransformConfig(): TransformPivotConfig { const date = Date.now(); return { id: `ec_2_${date}`, - source: { index: ['ecommerce'] }, + source: { index: ['ft_ecommerce'] }, pivot: { group_by: { category: { terms: { field: 'category.keyword' } } }, aggregations: { 'products.base_price.avg': { avg: { field: 'products.base_price' } } }, @@ -31,13 +31,16 @@ export default function({ getService }: FtrProviderContext) { const transformConfig = getTransformConfig(); before(async () => { - await esArchiver.load('ml/ecommerce'); + await esArchiver.loadIfNeeded('ml/ecommerce'); + await transform.testResources.createIndexPatternIfNeeded('ft_ecommerce', 'order_date'); await transform.api.createAndRunTransform(transformConfig); + await transform.testResources.setKibanaTimeZoneToUTC(); + await transform.securityUI.loginAsTransformPowerUser(); }); after(async () => { - await esArchiver.unload('ml/ecommerce'); + await transform.testResources.deleteIndexPattern(transformConfig.dest.index); await transform.api.deleteIndices(transformConfig.dest.index); await transform.api.cleanTransformIndices(); }); diff --git a/x-pack/test/functional/apps/transform/creation_index_pattern.ts b/x-pack/test/functional/apps/transform/creation_index_pattern.ts index f3cc4ab8d7601..bea6b814ee8a3 100644 --- a/x-pack/test/functional/apps/transform/creation_index_pattern.ts +++ b/x-pack/test/functional/apps/transform/creation_index_pattern.ts @@ -20,19 +20,21 @@ export default function({ getService }: FtrProviderContext) { describe('creation_index_pattern', function() { this.tags(['smoke']); before(async () => { - await esArchiver.load('ml/ecommerce'); + await esArchiver.loadIfNeeded('ml/ecommerce'); + await transform.testResources.createIndexPatternIfNeeded('ft_ecommerce', 'order_date'); + await transform.testResources.setKibanaTimeZoneToUTC(); + await transform.securityUI.loginAsTransformPowerUser(); }); after(async () => { - await esArchiver.unload('ml/ecommerce'); await transform.api.cleanTransformIndices(); }); const testDataList = [ { suiteTitle: 'batch transform with terms+date_histogram groups and avg agg', - source: 'ecommerce', + source: 'ft_ecommerce', groupByEntries: [ { identifier: 'terms(category.keyword)', @@ -96,7 +98,7 @@ export default function({ getService }: FtrProviderContext) { }, { suiteTitle: 'batch transform with terms group and percentiles agg', - source: 'ecommerce', + source: 'ft_ecommerce', groupByEntries: [ { identifier: 'terms(geoip.country_iso_code)', @@ -154,6 +156,7 @@ export default function({ getService }: FtrProviderContext) { describe(`${testData.suiteTitle}`, function() { after(async () => { await transform.api.deleteIndices(testData.destinationIndex); + await transform.testResources.deleteIndexPattern(testData.destinationIndex); }); it('loads the home page', async () => { diff --git a/x-pack/test/functional/apps/transform/creation_saved_search.ts b/x-pack/test/functional/apps/transform/creation_saved_search.ts index bf501c65bc79b..993bd3a79abbc 100644 --- a/x-pack/test/functional/apps/transform/creation_saved_search.ts +++ b/x-pack/test/functional/apps/transform/creation_saved_search.ts @@ -20,19 +20,22 @@ export default function({ getService }: FtrProviderContext) { describe('creation_saved_search', function() { this.tags(['smoke']); before(async () => { - await esArchiver.load('ml/farequote'); + await esArchiver.loadIfNeeded('ml/farequote'); + await transform.testResources.createIndexPatternIfNeeded('ft_farequote', '@timestamp'); + await transform.testResources.createSavedSearchFarequoteFilterIfNeeded(); + await transform.testResources.setKibanaTimeZoneToUTC(); + await transform.securityUI.loginAsTransformPowerUser(); }); after(async () => { - await esArchiver.unload('ml/farequote'); await transform.api.cleanTransformIndices(); }); const testDataList = [ { suiteTitle: 'batch transform with terms groups and avg agg with saved search filter', - source: 'farequote_filter', + source: 'ft_farequote_filter', groupByEntries: [ { identifier: 'terms(airline)', @@ -61,7 +64,7 @@ export default function({ getService }: FtrProviderContext) { mode: 'batch', progress: '100', }, - sourceIndex: 'farequote', + sourceIndex: 'ft_farequote', sourcePreview: { column: 2, values: ['ASA'], @@ -74,6 +77,7 @@ export default function({ getService }: FtrProviderContext) { describe(`${testData.suiteTitle}`, function() { after(async () => { await transform.api.deleteIndices(testData.destinationIndex); + await transform.testResources.deleteIndexPattern(testData.destinationIndex); }); it('loads the home page', async () => { diff --git a/x-pack/test/functional/apps/transform/index.ts b/x-pack/test/functional/apps/transform/index.ts index 60b72f122f113..bf720ce85d824 100644 --- a/x-pack/test/functional/apps/transform/index.ts +++ b/x-pack/test/functional/apps/transform/index.ts @@ -6,6 +6,7 @@ import { FtrProviderContext } from '../../ftr_provider_context'; export default function({ getService, loadTestFile }: FtrProviderContext) { + const esArchiver = getService('esArchiver'); const transform = getService('transform'); describe('transform', function() { @@ -19,6 +20,16 @@ export default function({ getService, loadTestFile }: FtrProviderContext) { after(async () => { await transform.securityCommon.cleanTransformUsers(); await transform.securityCommon.cleanTransformRoles(); + + await transform.testResources.deleteSavedSearches(); + + await transform.testResources.deleteIndexPattern('ft_farequote'); + await transform.testResources.deleteIndexPattern('ft_ecommerce'); + + await esArchiver.unload('ml/farequote'); + await esArchiver.unload('ml/ecommerce'); + + await transform.testResources.resetKibanaTimeZone(); }); loadTestFile(require.resolve('./creation_index_pattern')); diff --git a/x-pack/test/functional/apps/uptime/index.ts b/x-pack/test/functional/apps/uptime/index.ts index 3789351263b98..f47214dc2ad2f 100644 --- a/x-pack/test/functional/apps/uptime/index.ts +++ b/x-pack/test/functional/apps/uptime/index.ts @@ -16,6 +16,7 @@ export default ({ loadTestFile, getService }: FtrProviderContext) => { const esArchiver = getService('esArchiver'); const kibanaServer = getService('kibanaServer'); const server = getService('kibanaServer'); + const uptime = getService('uptime'); describe('Uptime app', function() { this.tags('ciGroup6'); @@ -58,12 +59,14 @@ export default ({ loadTestFile, getService }: FtrProviderContext) => { await esArchiver.unload(ARCHIVE); await esArchiver.load(ARCHIVE); await kibanaServer.uiSettings.replace({ 'dateFormat:tz': 'UTC' }); + await uptime.navigation.goToUptime(); }); after(async () => await esArchiver.unload(ARCHIVE)); - loadTestFile(require.resolve('./feature_controls')); loadTestFile(require.resolve('./overview')); loadTestFile(require.resolve('./monitor')); + loadTestFile(require.resolve('./ml_anomaly')); + loadTestFile(require.resolve('./feature_controls')); }); }); }; diff --git a/x-pack/test/functional/apps/uptime/locations.ts b/x-pack/test/functional/apps/uptime/locations.ts index bbf50344f3493..e266594e6a762 100644 --- a/x-pack/test/functional/apps/uptime/locations.ts +++ b/x-pack/test/functional/apps/uptime/locations.ts @@ -5,7 +5,7 @@ */ import moment from 'moment'; -import { makeChecksWithStatus } from '../../../api_integration/apis/uptime/graphql/helpers/make_checks'; +import { makeChecksWithStatus } from '../../../api_integration/apis/uptime/rest/helper/make_checks'; import { FtrProviderContext } from '../../ftr_provider_context'; export default ({ getPageObjects, getService }: FtrProviderContext) => { diff --git a/x-pack/test/functional/apps/uptime/ml_anomaly.ts b/x-pack/test/functional/apps/uptime/ml_anomaly.ts new file mode 100644 index 0000000000000..bcd165cc1afb7 --- /dev/null +++ b/x-pack/test/functional/apps/uptime/ml_anomaly.ts @@ -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. + */ +import expect from '@kbn/expect'; +import { FtrProviderContext } from '../../ftr_provider_context'; + +export default ({ getService }: FtrProviderContext) => { + const uptime = getService('uptime'); + const log = getService('log'); + + describe('uptime ml anomaly', function() { + this.tags(['skipFirefox']); + const dateStart = 'Sep 10, 2019 @ 12:40:08.078'; + const dateEnd = 'Sep 11, 2019 @ 19:40:08.078'; + const monitorId = '0000-intermittent'; + + before(async () => { + if (!(await uptime.navigation.checkIfOnMonitorPage(monitorId))) { + await uptime.navigation.loadDataAndGoToMonitorPage(dateStart, dateEnd, monitorId); + } + if (await uptime.ml.alreadyHasJob()) { + log.info('Jon already exists so lets delete it to start fresh.'); + await uptime.ml.deleteMLJob(); + } + }); + + it('can open ml flyout', async () => { + await uptime.ml.openMLFlyout(); + }); + + it('has permission to create job', async () => { + expect(uptime.ml.canCreateJob()).to.eql(true); + expect(uptime.ml.hasNoLicenseInfo()).to.eql(false); + }); + + it('can create job successfully', async () => { + await uptime.ml.createMLJob(); + // await uptime.navigation.refreshApp(); + }); + + it('can open ML Manage Menu', async () => { + await uptime.ml.openMLManageMenu(); + }); + + it('can delete job successfully', async () => { + await uptime.ml.deleteMLJob(); + }); + }); +}; diff --git a/x-pack/test/functional/apps/uptime/monitor.ts b/x-pack/test/functional/apps/uptime/monitor.ts index e15750eb6157b..388d660f21eb3 100644 --- a/x-pack/test/functional/apps/uptime/monitor.ts +++ b/x-pack/test/functional/apps/uptime/monitor.ts @@ -17,7 +17,6 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { const dateStart = 'Sep 10, 2019 @ 12:40:08.078'; const dateEnd = 'Sep 11, 2019 @ 19:40:08.078'; const monitorId = '0000-intermittent'; - const monitorName = '0000-intermittent'; before(async () => { await esArchiver.loadIfNeeded(archive); @@ -28,8 +27,29 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { await esArchiver.unload(archive); }); - it('loads and displays uptime data based on date range', async () => { - await uptime.loadDataAndGoToMonitorPage(dateStart, dateEnd, monitorId, monitorName); + describe('navigation to monitor page', () => { + before(async () => { + await uptime.loadDataAndGoToMonitorPage(dateStart, dateEnd, monitorId); + }); + + it('displays ping data as expected', async () => { + await uptime.checkPingListInteractions( + [ + 'XZtoHm0B0I9WX_CznN-6', + '7ZtoHm0B0I9WX_CzJ96M', + 'pptnHm0B0I9WX_Czst5X', + 'I5tnHm0B0I9WX_CzPd46', + 'y5tmHm0B0I9WX_Czx93x', + 'XZtmHm0B0I9WX_CzUt3H', + '-JtlHm0B0I9WX_Cz3dyX', + 'k5tlHm0B0I9WX_CzaNxm', + 'NZtkHm0B0I9WX_Cz89w9', + 'zJtkHm0B0I9WX_CzftsN', + ], + 'mpls', + 'up' + ); + }); }); }); }; diff --git a/x-pack/test/functional/apps/uptime/overview.ts b/x-pack/test/functional/apps/uptime/overview.ts index 8195e6bbb6035..d0dfca64634f6 100644 --- a/x-pack/test/functional/apps/uptime/overview.ts +++ b/x-pack/test/functional/apps/uptime/overview.ts @@ -14,6 +14,13 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { describe('overview page', function() { const DEFAULT_DATE_START = 'Sep 10, 2019 @ 12:40:08.078'; const DEFAULT_DATE_END = 'Sep 11, 2019 @ 19:40:08.078'; + + beforeEach(async () => { + await uptime.goToRoot(); + await uptime.setDateRange(DEFAULT_DATE_START, DEFAULT_DATE_END); + await uptime.resetFilters(); + }); + it('loads and displays uptime data based on date range', async () => { await uptime.goToUptimeOverviewAndLoadData( DEFAULT_DATE_START, @@ -22,13 +29,7 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { ); }); - it('runs filter query without issues', async () => { - await uptime.inputFilterQuery('monitor.status:up and monitor.id:"0000-intermittent"'); - await uptime.pageHasExpectedIds(['0000-intermittent']); - }); - it('applies filters for multiple fields', async () => { - await uptime.goToUptimePageAndSetDateRange(DEFAULT_DATE_START, DEFAULT_DATE_END); await uptime.selectFilterItems({ location: ['mpls'], port: ['5678'], @@ -49,7 +50,6 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { }); it('pagination is cleared when filter criteria changes', async () => { - await uptime.goToUptimePageAndSetDateRange(DEFAULT_DATE_START, DEFAULT_DATE_END); await uptime.changePage('next'); await uptime.pageHasExpectedIds([ '0010-down', @@ -83,7 +83,6 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { }); it('clears pagination parameters when size changes', async () => { - await uptime.goToUptimePageAndSetDateRange(DEFAULT_DATE_START, DEFAULT_DATE_END); await uptime.changePage('next'); await uptime.pageUrlContains('pagination'); await uptime.setMonitorListPageSize(50); @@ -92,7 +91,6 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { }); it('pagination size updates to reflect current selection', async () => { - await uptime.goToUptimePageAndSetDateRange(DEFAULT_DATE_START, DEFAULT_DATE_END); await uptime.pageHasExpectedIds([ '0000-intermittent', '0001-up', @@ -162,7 +160,6 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { describe('snapshot counts', () => { it('updates the snapshot count when status filter is set to down', async () => { - await uptime.goToUptimePageAndSetDateRange(DEFAULT_DATE_START, DEFAULT_DATE_END); await uptime.setStatusFilter('down'); await retry.tryForTime(12000, async () => { @@ -172,13 +169,18 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { }); it('updates the snapshot count when status filter is set to up', async () => { - await uptime.goToUptimePageAndSetDateRange(DEFAULT_DATE_START, DEFAULT_DATE_END); await uptime.setStatusFilter('up'); await retry.tryForTime(12000, async () => { const counts = await uptime.getSnapshotCount(); expect(counts).to.eql({ up: '93', down: '0' }); }); }); + + it('runs filter query without issues', async () => { + await uptime.inputFilterQuery('monitor.status:up and monitor.id:"0000-intermittent"'); + await uptime.pageHasExpectedIds(['0000-intermittent']); + await uptime.resetFilters(); + }); }); }); }; diff --git a/x-pack/test/functional/apps/uptime/settings.ts b/x-pack/test/functional/apps/uptime/settings.ts index 3294d928b61b3..e81bbc5ae42f9 100644 --- a/x-pack/test/functional/apps/uptime/settings.ts +++ b/x-pack/test/functional/apps/uptime/settings.ts @@ -10,7 +10,7 @@ import { defaultDynamicSettings, DynamicSettings, } from '../../../../legacy/plugins/uptime/common/runtime_types'; -import { makeChecks } from '../../../api_integration/apis/uptime/graphql/helpers/make_checks'; +import { makeChecks } from '../../../api_integration/apis/uptime/rest/helper/make_checks'; export default ({ getPageObjects, getService }: FtrProviderContext) => { const { uptime: uptimePage } = getPageObjects(['uptime']); @@ -74,7 +74,41 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { // Verify that the settings page shows the value we previously saved await settings.go(); const fields = await settings.loadFields(); - expect(fields).to.eql(newFieldValues); + expect(fields.heartbeatIndices).to.eql(newFieldValues.heartbeatIndices); + }); + + it('changing certificate expiration error threshold is reflected in settings page', async () => { + const settings = uptimeService.settings; + + await settings.go(); + + const newErrorThreshold = '5'; + await settings.changeErrorThresholdInput(newErrorThreshold); + await settings.apply(); + + await uptimePage.goToRoot(); + + // Verify that the settings page shows the value we previously saved + await settings.go(); + const fields = await settings.loadFields(); + expect(fields.certificatesThresholds.errorState).to.eql(newErrorThreshold); + }); + + it('changing certificate expiration warning threshold is reflected in settings page', async () => { + const settings = uptimeService.settings; + + await settings.go(); + + const newWarningThreshold = '15'; + await settings.changeWarningThresholdInput(newWarningThreshold); + await settings.apply(); + + await uptimePage.goToRoot(); + + // Verify that the settings page shows the value we previously saved + await settings.go(); + const fields = await settings.loadFields(); + expect(fields.certificatesThresholds.warningState).to.eql(newWarningThreshold); }); }); }; diff --git a/x-pack/test/functional/apps/visualize/feature_controls/visualize_spaces.ts b/x-pack/test/functional/apps/visualize/feature_controls/visualize_spaces.ts index 4f12dd16247f6..cbd03110b0f14 100644 --- a/x-pack/test/functional/apps/visualize/feature_controls/visualize_spaces.ts +++ b/x-pack/test/functional/apps/visualize/feature_controls/visualize_spaces.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ import expect from '@kbn/expect'; -import { VisualizeConstants } from '../../../../../../src/legacy/core_plugins/kibana/public/visualize/np_ready/visualize_constants'; +import { VisualizeConstants } from '../../../../../../src/plugins/visualize/public/application/visualize_constants'; import { FtrProviderContext } from '../../../ftr_provider_context'; export default function({ getPageObjects, getService }: FtrProviderContext) { diff --git a/x-pack/test/functional/config.js b/x-pack/test/functional/config.js index bc9a67da731cc..f26110513a9b3 100644 --- a/x-pack/test/functional/config.js +++ b/x-pack/test/functional/config.js @@ -85,7 +85,6 @@ export default async function({ readConfigFile }) { '--stats.maximumWaitTimeForAllCollectorsInS=1', '--xpack.security.encryptionKey="wuGNaIhoMpk5sO4UBxgr3NyW1sFcLgIf"', // server restarts should not invalidate active sessions '--xpack.encryptedSavedObjects.encryptionKey="DkdXazszSCYexXqz4YktBGHCRkV6hyNK"', - '--telemetry.banner=false', '--timelion.ui.enabled=true', ], }, diff --git a/x-pack/test/functional/config_security_basic.js b/x-pack/test/functional/config_security_basic.js index 12d94e922a97c..2bb59796b5517 100644 --- a/x-pack/test/functional/config_security_basic.js +++ b/x-pack/test/functional/config_security_basic.js @@ -42,7 +42,6 @@ export default async function({ readConfigFile }) { ...kibanaCommonConfig.get('kbnTestServer.serverArgs'), '--server.uuid=5b2de169-2785-441b-ae8c-186a1936b17d', '--xpack.security.encryptionKey="wuGNaIhoMpk5sO4UBxgr3NyW1sFcLgIf"', // server restarts should not invalidate active sessions - '--telemetry.banner=false', ], }, uiSettings: { diff --git a/x-pack/test/functional/es_archives/endpoint/alerts/host_api_feature/data.json.gz b/x-pack/test/functional/es_archives/endpoint/alerts/host_api_feature/data.json.gz index 94a96c54ee9cb..a71281c0ecfec 100644 Binary files a/x-pack/test/functional/es_archives/endpoint/alerts/host_api_feature/data.json.gz and b/x-pack/test/functional/es_archives/endpoint/alerts/host_api_feature/data.json.gz differ diff --git a/x-pack/test/functional/es_archives/endpoint/alerts/host_api_feature/mappings.json b/x-pack/test/functional/es_archives/endpoint/alerts/host_api_feature/mappings.json index 61ddf3c4e65db..f9d5de0d0a94c 100644 --- a/x-pack/test/functional/es_archives/endpoint/alerts/host_api_feature/mappings.json +++ b/x-pack/test/functional/es_archives/endpoint/alerts/host_api_feature/mappings.json @@ -1,33 +1,62 @@ { "type": "index", "value": { - "aliases": { - }, - "index": "endpoint-agent-1", + "aliases": {}, + "index": "metrics-endpoint-default-1", "mappings": { + "_meta": { + "version": "1.5.0-dev" + }, + "date_detection": false, + "dynamic_templates": [ + { + "strings_as_keyword": { + "mapping": { + "ignore_above": 1024, + "type": "keyword" + }, + "match_mapping_type": "string" + } + } + ], "properties": { "@timestamp": { - "type": "long" + "type": "date" }, - "agent": { + "elastic": { "properties": { - "id": { - "fields": { - "keyword": { - "ignore_above": 256, + "agent": { + "properties": { + "id": { + "ignore_above": 1024, "type": "keyword" } }, - "type": "text" + "type": "object" + } + } + }, + "agent": { + "properties": { + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" }, "version": { - "fields": { - "keyword": { - "ignore_above": 256, - "type": "keyword" - } - }, - "type": "text" + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "ecs": { + "properties": { + "version": { + "ignore_above": 1024, + "type": "keyword" } } }, @@ -36,109 +65,71 @@ "policy": { "properties": { "id": { - "fields": { - "keyword": { - "ignore_above": 256, - "type": "keyword" - } - }, - "type": "text" - }, - "name": { - "fields": { - "keyword": { - "ignore_above": 256, - "type": "keyword" - } - }, - "type": "text" + "ignore_above": 1024, + "type": "keyword" } - } + }, + "type": "object" } } }, "event": { "properties": { "created": { - "type": "long" + "type": "date" } } }, "host": { "properties": { + "architecture": { + "ignore_above": 1024, + "type": "keyword" + }, "hostname": { - "fields": { - "keyword": { - "ignore_above": 256, - "type": "keyword" - } - }, - "type": "text" + "ignore_above": 1024, + "type": "keyword" }, "id": { - "fields": { - "keyword": { - "ignore_above": 256, - "type": "keyword" - } - }, - "type": "text" + "ignore_above": 1024, + "type": "keyword" }, "ip": { - "fields": { - "keyword": { - "ignore_above": 256, - "type": "keyword" - } - }, - "type": "text" + "type": "ip" }, "mac": { - "fields": { - "keyword": { - "ignore_above": 256, - "type": "keyword" - } - }, - "type": "text" + "ignore_above": 1024, + "type": "keyword" }, "os": { "properties": { "full": { "fields": { - "keyword": { - "ignore_above": 256, - "type": "keyword" + "text": { + "norms": false, + "type": "text" } }, - "type": "text" + "ignore_above": 1024, + "type": "keyword" }, "name": { "fields": { - "keyword": { - "ignore_above": 256, - "type": "keyword" + "text": { + "norms": false, + "type": "text" } }, - "type": "text" + "ignore_above": 1024, + "type": "keyword" }, "variant": { - "fields": { - "keyword": { - "ignore_above": 256, - "type": "keyword" - } - }, - "type": "text" + "ignore_above": 1024, + "type": "keyword" }, "version": { - "fields": { - "keyword": { - "ignore_above": 256, - "type": "keyword" - } - }, - "type": "text" + "ignore_above": 1024, + "type": "keyword" } } } @@ -146,11 +137,16 @@ } } }, + "order": 1, "settings": { "index": { - "number_of_replicas": "1", - "number_of_shards": "1" + "mapping": { + "total_fields": { + "limit": 10000 + } + }, + "refresh_interval": "5s" } } } -} \ No newline at end of file +} diff --git a/x-pack/test/functional/es_archives/endpoint/metadata/api_feature/data.json b/x-pack/test/functional/es_archives/endpoint/metadata/api_feature/data.json index 2046f46db9f53..d3617dc236375 100644 --- a/x-pack/test/functional/es_archives/endpoint/metadata/api_feature/data.json +++ b/x-pack/test/functional/es_archives/endpoint/metadata/api_feature/data.json @@ -2,7 +2,7 @@ "type": "doc", "value": { "id": "3KVN2G8BYQH1gtPUuYk7", - "index": "endpoint-agent-1", + "index": "metrics-endpoint-default-1", "source": { "@timestamp": 1579881969541, "agent": { @@ -51,7 +51,7 @@ "type": "doc", "value": { "id": "3aVN2G8BYQH1gtPUuYk7", - "index": "endpoint-agent-1", + "index": "metrics-endpoint-default-1", "source": { "@timestamp": 1579881969541, "agent": { @@ -99,7 +99,7 @@ "type": "doc", "value": { "id": "3qVN2G8BYQH1gtPUuYk7", - "index": "endpoint-agent-1", + "index": "metrics-endpoint-default-1", "source": { "@timestamp": 1579881969541, "agent": { @@ -145,7 +145,7 @@ "type": "doc", "value": { "id": "36VN2G8BYQH1gtPUuYk7", - "index": "endpoint-agent-1", + "index": "metrics-endpoint-default-1", "source": { "@timestamp": 1579878369541, "agent": { @@ -194,7 +194,7 @@ "type": "doc", "value": { "id": "4KVN2G8BYQH1gtPUuYk7", - "index": "endpoint-agent-1", + "index": "metrics-endpoint-default-1", "source": { "@timestamp": 1579878369541, "agent": { @@ -241,7 +241,7 @@ "type": "doc", "value": { "id": "4aVN2G8BYQH1gtPUuYk7", - "index": "endpoint-agent-1", + "index": "metrics-endpoint-default-1", "source": { "@timestamp": 1579878369541, "agent": { @@ -288,7 +288,7 @@ "type": "doc", "value": { "id": "4qVN2G8BYQH1gtPUuYk7", - "index": "endpoint-agent-1", + "index": "metrics-endpoint-default-1", "source": { "@timestamp": 1579874769541, "agent": { @@ -336,7 +336,7 @@ "type": "doc", "value": { "id": "46VN2G8BYQH1gtPUuYk7", - "index": "endpoint-agent-1", + "index": "metrics-endpoint-default-1", "source": { "@timestamp": 1579874769541, "agent": { @@ -383,7 +383,7 @@ "type": "doc", "value": { "id": "5KVN2G8BYQH1gtPUuYk7", - "index": "endpoint-agent-1", + "index": "metrics-endpoint-default-1", "source": { "@timestamp": 1579874769541, "agent": { diff --git a/x-pack/test/functional/es_archives/endpoint/metadata/api_feature/mappings.json b/x-pack/test/functional/es_archives/endpoint/metadata/api_feature/mappings.json index c9a6c183f0489..f9d5de0d0a94c 100644 --- a/x-pack/test/functional/es_archives/endpoint/metadata/api_feature/mappings.json +++ b/x-pack/test/functional/es_archives/endpoint/metadata/api_feature/mappings.json @@ -1,50 +1,62 @@ { "type": "index", "value": { - "aliases": { - }, - "index": "endpoint-agent-1", + "aliases": {}, + "index": "metrics-endpoint-default-1", "mappings": { + "_meta": { + "version": "1.5.0-dev" + }, + "date_detection": false, + "dynamic_templates": [ + { + "strings_as_keyword": { + "mapping": { + "ignore_above": 1024, + "type": "keyword" + }, + "match_mapping_type": "string" + } + } + ], "properties": { "@timestamp": { - "type": "long" + "type": "date" }, "elastic": { "properties": { "agent": { "properties": { "id": { - "fields": { - "keyword": { - "ignore_above": 256, - "type": "keyword" - } - }, - "type": "text" + "ignore_above": 1024, + "type": "keyword" } - } + }, + "type": "object" } } }, "agent": { "properties": { "id": { - "fields": { - "keyword": { - "ignore_above": 256, - "type": "keyword" - } - }, - "type": "text" + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" }, "version": { - "fields": { - "keyword": { - "ignore_above": 256, - "type": "keyword" - } - }, - "type": "text" + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "ecs": { + "properties": { + "version": { + "ignore_above": 1024, + "type": "keyword" } } }, @@ -53,109 +65,71 @@ "policy": { "properties": { "id": { - "fields": { - "keyword": { - "ignore_above": 256, - "type": "keyword" - } - }, - "type": "text" - }, - "name": { - "fields": { - "keyword": { - "ignore_above": 256, - "type": "keyword" - } - }, - "type": "text" + "ignore_above": 1024, + "type": "keyword" } - } + }, + "type": "object" } } }, "event": { "properties": { "created": { - "type": "long" + "type": "date" } } }, "host": { "properties": { + "architecture": { + "ignore_above": 1024, + "type": "keyword" + }, "hostname": { - "fields": { - "keyword": { - "ignore_above": 256, - "type": "keyword" - } - }, - "type": "text" + "ignore_above": 1024, + "type": "keyword" }, "id": { - "fields": { - "keyword": { - "ignore_above": 256, - "type": "keyword" - } - }, - "type": "text" + "ignore_above": 1024, + "type": "keyword" }, "ip": { - "fields": { - "keyword": { - "ignore_above": 256, - "type": "keyword" - } - }, - "type": "text" + "type": "ip" }, "mac": { - "fields": { - "keyword": { - "ignore_above": 256, - "type": "keyword" - } - }, - "type": "text" + "ignore_above": 1024, + "type": "keyword" }, "os": { "properties": { "full": { "fields": { - "keyword": { - "ignore_above": 256, - "type": "keyword" + "text": { + "norms": false, + "type": "text" } }, - "type": "text" + "ignore_above": 1024, + "type": "keyword" }, "name": { "fields": { - "keyword": { - "ignore_above": 256, - "type": "keyword" + "text": { + "norms": false, + "type": "text" } }, - "type": "text" + "ignore_above": 1024, + "type": "keyword" }, "variant": { - "fields": { - "keyword": { - "ignore_above": 256, - "type": "keyword" - } - }, - "type": "text" + "ignore_above": 1024, + "type": "keyword" }, "version": { - "fields": { - "keyword": { - "ignore_above": 256, - "type": "keyword" - } - }, - "type": "text" + "ignore_above": 1024, + "type": "keyword" } } } @@ -163,10 +137,15 @@ } } }, + "order": 1, "settings": { "index": { - "number_of_replicas": "1", - "number_of_shards": "1" + "mapping": { + "total_fields": { + "limit": 10000 + } + }, + "refresh_interval": "5s" } } } diff --git a/x-pack/test/functional/es_archives/ml/bm_classification/data.json.gz b/x-pack/test/functional/es_archives/ml/bm_classification/data.json.gz index 12ccf6ae60512..8c0dc38d3dedb 100644 Binary files a/x-pack/test/functional/es_archives/ml/bm_classification/data.json.gz and b/x-pack/test/functional/es_archives/ml/bm_classification/data.json.gz differ diff --git a/x-pack/test/functional/es_archives/ml/bm_classification/mappings.json b/x-pack/test/functional/es_archives/ml/bm_classification/mappings.json index 9d2cca22bf300..380a0d8fa4d15 100644 --- a/x-pack/test/functional/es_archives/ml/bm_classification/mappings.json +++ b/x-pack/test/functional/es_archives/ml/bm_classification/mappings.json @@ -3,7 +3,7 @@ "value": { "aliases": { }, - "index": "bank-marketing", + "index": "ft_bank_marketing", "mappings": { "properties": { "age": { @@ -94,1455 +94,3 @@ } } } - -{ - "type": "index", - "value": { - "aliases": { - ".kibana": { - } - }, - "index": ".kibana_1", - "mappings": { - "_meta": { - "migrationMappingPropertyHashes": { - "action": "c0c235fba02ebd2a2412bcda79009b58", - "action_task_params": "a9d49f184ee89641044be0ca2950fa3a", - "alert": "e588043a01d3d43477e7cad7efa0f5d8", - "apm-indices": "9bb9b2bf1fa636ed8619cbab5ce6a1dd", - "apm-services-telemetry": "07ee1939fa4302c62ddc052ec03fed90", - "canvas-element": "7390014e1091044523666d97247392fc", - "canvas-workpad": "b0a1706d356228dbdcb4a17e6b9eb231", - "config": "87aca8fdb053154f11383fce3dbf3edf", - "dashboard": "d00f614b29a80360e1190193fd333bab", - "file-upload-telemetry": "0ed4d3e1983d1217a30982630897092e", - "graph-workspace": "cd7ba1330e6682e9cc00b78850874be1", - "index-pattern": "66eccb05066c5a89924f48a9e9736499", - "infrastructure-ui-source": "ddc0ecb18383f6b26101a2fadb2dab0c", - "inventory-view": "84b320fd67209906333ffce261128462", - "kql-telemetry": "d12a98a6f19a2d273696597547e064ee", - "lens": "21c3ea0763beb1ecb0162529706b88c5", - "lens-ui-telemetry": "509bfa5978586998e05f9e303c07a327", - "map": "23d7aa4a720d4938ccde3983f87bd58d", - "maps-telemetry": "268da3a48066123fc5baf35abaa55014", - "metrics-explorer-view": "53c5365793677328df0ccb6138bf3cdd", - "migrationVersion": "4a1746014a75ade3a714e1db5763276f", - "ml-telemetry": "257fd1d4b4fdbb9cb4b8a3b27da201e9", - "namespace": "2f4316de49999235636386fe51dc06c1", - "query": "11aaeb7f5f7fa5bb43f25e18ce26e7d9", - "references": "7997cf5a56cc02bdc9c93361bde732b0", - "sample-data-telemetry": "7d3cfeb915303c9641c59681967ffeb4", - "search": "181661168bbadd1eff5902361e2a0d5c", - "server": "ec97f1c5da1a19609a60874e5af1100c", - "siem-detection-engine-rule-status": "0367e4d775814b56a4bee29384f9aafe", - "siem-ui-timeline": "ac8020190f5950dd3250b6499144e7fb", - "siem-ui-timeline-note": "8874706eedc49059d4cf0f5094559084", - "siem-ui-timeline-pinned-event": "20638091112f0e14f0e443d512301c29", - "space": "c5ca8acafa0beaa4d08d014a97b6bc6b", - "telemetry": "358ffaa88ba34a97d55af0933a117de4", - "timelion-sheet": "9a2a2748877c7a7b582fef201ab1d4cf", - "tsvb-validation-telemetry": "3a37ef6c8700ae6fc97d5c7da00e9215", - "type": "2f4316de49999235636386fe51dc06c1", - "ui-metric": "0d409297dc5ebe1e3a1da691c6ee32e3", - "updated_at": "00da57df13e94e9d98437d13ace4bfe0", - "upgrade-assistant-reindex-operation": "a53a20fe086b72c9a86da3cc12dad8a6", - "upgrade-assistant-telemetry": "56702cec857e0a9dacfb696655b4ff7b", - "url": "c7f66a0df8b1b52f17c28c4adb111105", - "visualization": "52d7a13ad68a150c4525b292d23e12cc" - } - }, - "dynamic": "strict", - "properties": { - "action": { - "properties": { - "actionTypeId": { - "type": "keyword" - }, - "config": { - "enabled": false, - "type": "object" - }, - "name": { - "type": "text" - }, - "secrets": { - "type": "binary" - } - } - }, - "action_task_params": { - "properties": { - "actionId": { - "type": "keyword" - }, - "apiKey": { - "type": "binary" - }, - "params": { - "enabled": false, - "type": "object" - } - } - }, - "alert": { - "properties": { - "actions": { - "properties": { - "actionRef": { - "type": "keyword" - }, - "actionTypeId": { - "type": "keyword" - }, - "group": { - "type": "keyword" - }, - "params": { - "enabled": false, - "type": "object" - } - }, - "type": "nested" - }, - "alertTypeId": { - "type": "keyword" - }, - "apiKey": { - "type": "binary" - }, - "apiKeyOwner": { - "type": "keyword" - }, - "consumer": { - "type": "keyword" - }, - "createdAt": { - "type": "date" - }, - "createdBy": { - "type": "keyword" - }, - "enabled": { - "type": "boolean" - }, - "muteAll": { - "type": "boolean" - }, - "mutedInstanceIds": { - "type": "keyword" - }, - "name": { - "type": "text" - }, - "params": { - "enabled": false, - "type": "object" - }, - "schedule": { - "properties": { - "interval": { - "type": "keyword" - } - } - }, - "scheduledTaskId": { - "type": "keyword" - }, - "tags": { - "type": "keyword" - }, - "throttle": { - "type": "keyword" - }, - "updatedBy": { - "type": "keyword" - } - } - }, - "apm-indices": { - "properties": { - "apm_oss": { - "properties": { - "errorIndices": { - "type": "keyword" - }, - "metricsIndices": { - "type": "keyword" - }, - "onboardingIndices": { - "type": "keyword" - }, - "sourcemapIndices": { - "type": "keyword" - }, - "spanIndices": { - "type": "keyword" - }, - "transactionIndices": { - "type": "keyword" - } - } - } - } - }, - "apm-services-telemetry": { - "properties": { - "has_any_services": { - "type": "boolean" - }, - "services_per_agent": { - "properties": { - "dotnet": { - "null_value": 0, - "type": "long" - }, - "go": { - "null_value": 0, - "type": "long" - }, - "java": { - "null_value": 0, - "type": "long" - }, - "js-base": { - "null_value": 0, - "type": "long" - }, - "nodejs": { - "null_value": 0, - "type": "long" - }, - "python": { - "null_value": 0, - "type": "long" - }, - "ruby": { - "null_value": 0, - "type": "long" - }, - "rum-js": { - "null_value": 0, - "type": "long" - } - } - } - } - }, - "canvas-element": { - "dynamic": "false", - "properties": { - "@created": { - "type": "date" - }, - "@timestamp": { - "type": "date" - }, - "content": { - "type": "text" - }, - "help": { - "type": "text" - }, - "image": { - "type": "text" - }, - "name": { - "fields": { - "keyword": { - "type": "keyword" - } - }, - "type": "text" - } - } - }, - "canvas-workpad": { - "dynamic": "false", - "properties": { - "@created": { - "type": "date" - }, - "@timestamp": { - "type": "date" - }, - "name": { - "fields": { - "keyword": { - "type": "keyword" - } - }, - "type": "text" - } - } - }, - "config": { - "dynamic": "true", - "properties": { - "buildNum": { - "type": "keyword" - }, - "dateFormat:tz": { - "fields": { - "keyword": { - "ignore_above": 256, - "type": "keyword" - } - }, - "type": "text" - }, - "defaultIndex": { - "fields": { - "keyword": { - "ignore_above": 256, - "type": "keyword" - } - }, - "type": "text" - } - } - }, - "dashboard": { - "properties": { - "description": { - "type": "text" - }, - "hits": { - "type": "integer" - }, - "kibanaSavedObjectMeta": { - "properties": { - "searchSourceJSON": { - "type": "text" - } - } - }, - "optionsJSON": { - "type": "text" - }, - "panelsJSON": { - "type": "text" - }, - "refreshInterval": { - "properties": { - "display": { - "type": "keyword" - }, - "pause": { - "type": "boolean" - }, - "section": { - "type": "integer" - }, - "value": { - "type": "integer" - } - } - }, - "timeFrom": { - "type": "keyword" - }, - "timeRestore": { - "type": "boolean" - }, - "timeTo": { - "type": "keyword" - }, - "title": { - "type": "text" - }, - "version": { - "type": "integer" - } - } - }, - "file-upload-telemetry": { - "properties": { - "filesUploadedTotalCount": { - "type": "long" - } - } - }, - "graph-workspace": { - "properties": { - "description": { - "type": "text" - }, - "kibanaSavedObjectMeta": { - "properties": { - "searchSourceJSON": { - "type": "text" - } - } - }, - "numLinks": { - "type": "integer" - }, - "numVertices": { - "type": "integer" - }, - "title": { - "type": "text" - }, - "version": { - "type": "integer" - }, - "wsState": { - "type": "text" - } - } - }, - "index-pattern": { - "properties": { - "fieldFormatMap": { - "type": "text" - }, - "fields": { - "type": "text" - }, - "intervalName": { - "type": "keyword" - }, - "notExpandable": { - "type": "boolean" - }, - "sourceFilters": { - "type": "text" - }, - "timeFieldName": { - "type": "keyword" - }, - "title": { - "type": "text" - }, - "type": { - "type": "keyword" - }, - "typeMeta": { - "type": "keyword" - } - } - }, - "infrastructure-ui-source": { - "properties": { - "description": { - "type": "text" - }, - "fields": { - "properties": { - "container": { - "type": "keyword" - }, - "host": { - "type": "keyword" - }, - "pod": { - "type": "keyword" - }, - "tiebreaker": { - "type": "keyword" - }, - "timestamp": { - "type": "keyword" - } - } - }, - "logAlias": { - "type": "keyword" - }, - "logColumns": { - "properties": { - "fieldColumn": { - "properties": { - "field": { - "type": "keyword" - }, - "id": { - "type": "keyword" - } - } - }, - "messageColumn": { - "properties": { - "id": { - "type": "keyword" - } - } - }, - "timestampColumn": { - "properties": { - "id": { - "type": "keyword" - } - } - } - }, - "type": "nested" - }, - "metricAlias": { - "type": "keyword" - }, - "name": { - "type": "text" - } - } - }, - "inventory-view": { - "properties": { - "autoBounds": { - "type": "boolean" - }, - "autoReload": { - "type": "boolean" - }, - "boundsOverride": { - "properties": { - "max": { - "type": "integer" - }, - "min": { - "type": "integer" - } - } - }, - "customOptions": { - "properties": { - "field": { - "type": "keyword" - }, - "text": { - "type": "keyword" - } - }, - "type": "nested" - }, - "filterQuery": { - "properties": { - "expression": { - "type": "keyword" - }, - "kind": { - "type": "keyword" - } - } - }, - "groupBy": { - "properties": { - "field": { - "type": "keyword" - }, - "label": { - "type": "keyword" - } - }, - "type": "nested" - }, - "metric": { - "properties": { - "type": { - "type": "keyword" - } - } - }, - "name": { - "type": "keyword" - }, - "nodeType": { - "type": "keyword" - }, - "time": { - "type": "integer" - }, - "view": { - "type": "keyword" - } - } - }, - "kql-telemetry": { - "properties": { - "optInCount": { - "type": "long" - }, - "optOutCount": { - "type": "long" - } - } - }, - "lens": { - "properties": { - "expression": { - "index": false, - "type": "keyword" - }, - "state": { - "type": "flattened" - }, - "title": { - "type": "text" - }, - "visualizationType": { - "type": "keyword" - } - } - }, - "lens-ui-telemetry": { - "properties": { - "count": { - "type": "integer" - }, - "date": { - "type": "date" - }, - "name": { - "type": "keyword" - }, - "type": { - "type": "keyword" - } - } - }, - "map": { - "properties": { - "bounds": { - "type": "geo_shape" - }, - "description": { - "type": "text" - }, - "layerListJSON": { - "type": "text" - }, - "mapStateJSON": { - "type": "text" - }, - "title": { - "type": "text" - }, - "uiStateJSON": { - "type": "text" - }, - "version": { - "type": "integer" - } - } - }, - "maps-telemetry": { - "properties": { - "attributesPerMap": { - "properties": { - "dataSourcesCount": { - "properties": { - "avg": { - "type": "long" - }, - "max": { - "type": "long" - }, - "min": { - "type": "long" - } - } - }, - "emsVectorLayersCount": { - "dynamic": "true", - "type": "object" - }, - "layerTypesCount": { - "dynamic": "true", - "type": "object" - }, - "layersCount": { - "properties": { - "avg": { - "type": "long" - }, - "max": { - "type": "long" - }, - "min": { - "type": "long" - } - } - } - } - }, - "indexPatternsWithGeoFieldCount": { - "type": "long" - }, - "mapsTotalCount": { - "type": "long" - }, - "settings": { - "properties": { - "showMapVisualizationTypes": { - "type": "boolean" - } - } - }, - "timeCaptured": { - "type": "date" - } - } - }, - "metrics-explorer-view": { - "properties": { - "chartOptions": { - "properties": { - "stack": { - "type": "boolean" - }, - "type": { - "type": "keyword" - }, - "yAxisMode": { - "type": "keyword" - } - } - }, - "currentTimerange": { - "properties": { - "from": { - "type": "keyword" - }, - "interval": { - "type": "keyword" - }, - "to": { - "type": "keyword" - } - } - }, - "name": { - "type": "keyword" - }, - "options": { - "properties": { - "aggregation": { - "type": "keyword" - }, - "filterQuery": { - "type": "keyword" - }, - "groupBy": { - "type": "keyword" - }, - "limit": { - "type": "integer" - }, - "metrics": { - "properties": { - "aggregation": { - "type": "keyword" - }, - "color": { - "type": "keyword" - }, - "field": { - "type": "keyword" - }, - "label": { - "type": "keyword" - } - }, - "type": "nested" - } - } - } - } - }, - "migrationVersion": { - "dynamic": "true", - "properties": { - "index-pattern": { - "fields": { - "keyword": { - "ignore_above": 256, - "type": "keyword" - } - }, - "type": "text" - }, - "space": { - "fields": { - "keyword": { - "ignore_above": 256, - "type": "keyword" - } - }, - "type": "text" - } - } - }, - "ml-telemetry": { - "properties": { - "file_data_visualizer": { - "properties": { - "index_creation_count": { - "type": "long" - } - } - } - } - }, - "namespace": { - "type": "keyword" - }, - "query": { - "properties": { - "description": { - "type": "text" - }, - "filters": { - "enabled": false, - "type": "object" - }, - "query": { - "properties": { - "language": { - "type": "keyword" - }, - "query": { - "index": false, - "type": "keyword" - } - } - }, - "timefilter": { - "enabled": false, - "type": "object" - }, - "title": { - "type": "text" - } - } - }, - "references": { - "properties": { - "id": { - "type": "keyword" - }, - "name": { - "type": "keyword" - }, - "type": { - "type": "keyword" - } - }, - "type": "nested" - }, - "sample-data-telemetry": { - "properties": { - "installCount": { - "type": "long" - }, - "unInstallCount": { - "type": "long" - } - } - }, - "search": { - "properties": { - "columns": { - "type": "keyword" - }, - "description": { - "type": "text" - }, - "hits": { - "type": "integer" - }, - "kibanaSavedObjectMeta": { - "properties": { - "searchSourceJSON": { - "type": "text" - } - } - }, - "sort": { - "type": "keyword" - }, - "title": { - "type": "text" - }, - "version": { - "type": "integer" - } - } - }, - "server": { - "properties": { - "uuid": { - "type": "keyword" - } - } - }, - "siem-detection-engine-rule-status": { - "properties": { - "alertId": { - "type": "keyword" - }, - "lastFailureAt": { - "type": "date" - }, - "lastFailureMessage": { - "type": "text" - }, - "lastSuccessAt": { - "type": "date" - }, - "lastSuccessMessage": { - "type": "text" - }, - "status": { - "type": "keyword" - }, - "statusDate": { - "type": "date" - } - } - }, - "siem-ui-timeline": { - "properties": { - "columns": { - "properties": { - "aggregatable": { - "type": "boolean" - }, - "category": { - "type": "keyword" - }, - "columnHeaderType": { - "type": "keyword" - }, - "description": { - "type": "text" - }, - "example": { - "type": "text" - }, - "id": { - "type": "keyword" - }, - "indexes": { - "type": "keyword" - }, - "name": { - "type": "text" - }, - "placeholder": { - "type": "text" - }, - "searchable": { - "type": "boolean" - }, - "type": { - "type": "keyword" - } - } - }, - "created": { - "type": "date" - }, - "createdBy": { - "type": "text" - }, - "dataProviders": { - "properties": { - "and": { - "properties": { - "enabled": { - "type": "boolean" - }, - "excluded": { - "type": "boolean" - }, - "id": { - "type": "keyword" - }, - "kqlQuery": { - "type": "text" - }, - "name": { - "type": "text" - }, - "queryMatch": { - "properties": { - "displayField": { - "type": "text" - }, - "displayValue": { - "type": "text" - }, - "field": { - "type": "text" - }, - "operator": { - "type": "text" - }, - "value": { - "type": "text" - } - } - } - } - }, - "enabled": { - "type": "boolean" - }, - "excluded": { - "type": "boolean" - }, - "id": { - "type": "keyword" - }, - "kqlQuery": { - "type": "text" - }, - "name": { - "type": "text" - }, - "queryMatch": { - "properties": { - "displayField": { - "type": "text" - }, - "displayValue": { - "type": "text" - }, - "field": { - "type": "text" - }, - "operator": { - "type": "text" - }, - "value": { - "type": "text" - } - } - } - } - }, - "dateRange": { - "properties": { - "end": { - "type": "date" - }, - "start": { - "type": "date" - } - } - }, - "description": { - "type": "text" - }, - "eventType": { - "type": "keyword" - }, - "favorite": { - "properties": { - "favoriteDate": { - "type": "date" - }, - "fullName": { - "type": "text" - }, - "keySearch": { - "type": "text" - }, - "userName": { - "type": "text" - } - } - }, - "filters": { - "properties": { - "exists": { - "type": "text" - }, - "match_all": { - "type": "text" - }, - "meta": { - "properties": { - "alias": { - "type": "text" - }, - "controlledBy": { - "type": "text" - }, - "disabled": { - "type": "boolean" - }, - "field": { - "type": "text" - }, - "formattedValue": { - "type": "text" - }, - "index": { - "type": "keyword" - }, - "key": { - "type": "keyword" - }, - "negate": { - "type": "boolean" - }, - "params": { - "type": "text" - }, - "type": { - "type": "keyword" - }, - "value": { - "type": "text" - } - } - }, - "missing": { - "type": "text" - }, - "query": { - "type": "text" - }, - "range": { - "type": "text" - }, - "script": { - "type": "text" - } - } - }, - "kqlMode": { - "type": "keyword" - }, - "kqlQuery": { - "properties": { - "filterQuery": { - "properties": { - "kuery": { - "properties": { - "expression": { - "type": "text" - }, - "kind": { - "type": "keyword" - } - } - }, - "serializedQuery": { - "type": "text" - } - } - } - } - }, - "savedQueryId": { - "type": "keyword" - }, - "sort": { - "properties": { - "columnId": { - "type": "keyword" - }, - "sortDirection": { - "type": "keyword" - } - } - }, - "title": { - "type": "text" - }, - "updated": { - "type": "date" - }, - "updatedBy": { - "type": "text" - } - } - }, - "siem-ui-timeline-note": { - "properties": { - "created": { - "type": "date" - }, - "createdBy": { - "type": "text" - }, - "eventId": { - "type": "keyword" - }, - "note": { - "type": "text" - }, - "timelineId": { - "type": "keyword" - }, - "updated": { - "type": "date" - }, - "updatedBy": { - "type": "text" - } - } - }, - "siem-ui-timeline-pinned-event": { - "properties": { - "created": { - "type": "date" - }, - "createdBy": { - "type": "text" - }, - "eventId": { - "type": "keyword" - }, - "timelineId": { - "type": "keyword" - }, - "updated": { - "type": "date" - }, - "updatedBy": { - "type": "text" - } - } - }, - "space": { - "properties": { - "_reserved": { - "type": "boolean" - }, - "color": { - "type": "keyword" - }, - "description": { - "type": "text" - }, - "disabledFeatures": { - "type": "keyword" - }, - "imageUrl": { - "index": false, - "type": "text" - }, - "initials": { - "type": "keyword" - }, - "name": { - "fields": { - "keyword": { - "ignore_above": 2048, - "type": "keyword" - } - }, - "type": "text" - } - } - }, - "telemetry": { - "properties": { - "enabled": { - "type": "boolean" - }, - "lastReported": { - "type": "date" - }, - "lastVersionChecked": { - "ignore_above": 256, - "type": "keyword" - }, - "sendUsageFrom": { - "ignore_above": 256, - "type": "keyword" - }, - "userHasSeenNotice": { - "type": "boolean" - } - } - }, - "timelion-sheet": { - "properties": { - "description": { - "type": "text" - }, - "hits": { - "type": "integer" - }, - "kibanaSavedObjectMeta": { - "properties": { - "searchSourceJSON": { - "type": "text" - } - } - }, - "timelion_chart_height": { - "type": "integer" - }, - "timelion_columns": { - "type": "integer" - }, - "timelion_interval": { - "type": "keyword" - }, - "timelion_other_interval": { - "type": "keyword" - }, - "timelion_rows": { - "type": "integer" - }, - "timelion_sheet": { - "type": "text" - }, - "title": { - "type": "text" - }, - "version": { - "type": "integer" - } - } - }, - "tsvb-validation-telemetry": { - "properties": { - "failedRequests": { - "type": "long" - } - } - }, - "type": { - "type": "keyword" - }, - "ui-metric": { - "properties": { - "count": { - "type": "integer" - } - } - }, - "updated_at": { - "type": "date" - }, - "upgrade-assistant-reindex-operation": { - "dynamic": "true", - "properties": { - "indexName": { - "type": "keyword" - }, - "status": { - "type": "integer" - } - } - }, - "upgrade-assistant-telemetry": { - "properties": { - "features": { - "properties": { - "deprecation_logging": { - "properties": { - "enabled": { - "null_value": true, - "type": "boolean" - } - } - } - } - }, - "ui_open": { - "properties": { - "cluster": { - "null_value": 0, - "type": "long" - }, - "indices": { - "null_value": 0, - "type": "long" - }, - "overview": { - "null_value": 0, - "type": "long" - } - } - }, - "ui_reindex": { - "properties": { - "close": { - "null_value": 0, - "type": "long" - }, - "open": { - "null_value": 0, - "type": "long" - }, - "start": { - "null_value": 0, - "type": "long" - }, - "stop": { - "null_value": 0, - "type": "long" - } - } - } - } - }, - "url": { - "properties": { - "accessCount": { - "type": "long" - }, - "accessDate": { - "type": "date" - }, - "createDate": { - "type": "date" - }, - "url": { - "fields": { - "keyword": { - "ignore_above": 2048, - "type": "keyword" - } - }, - "type": "text" - } - } - }, - "visualization": { - "properties": { - "description": { - "type": "text" - }, - "kibanaSavedObjectMeta": { - "properties": { - "searchSourceJSON": { - "type": "text" - } - } - }, - "savedSearchRefName": { - "type": "keyword" - }, - "title": { - "type": "text" - }, - "uiStateJSON": { - "type": "text" - }, - "version": { - "type": "integer" - }, - "visState": { - "type": "text" - } - } - } - } - }, - "settings": { - "index": { - "auto_expand_replicas": "0-1", - "number_of_replicas": "0", - "number_of_shards": "1" - } - } - } -} \ No newline at end of file diff --git a/x-pack/test/functional/es_archives/ml/categorization/data.json.gz b/x-pack/test/functional/es_archives/ml/categorization/data.json.gz index a66b68d815943..30b25cf944db8 100644 Binary files a/x-pack/test/functional/es_archives/ml/categorization/data.json.gz and b/x-pack/test/functional/es_archives/ml/categorization/data.json.gz differ diff --git a/x-pack/test/functional/es_archives/ml/categorization/mappings.json b/x-pack/test/functional/es_archives/ml/categorization/mappings.json index 5c97427b6fb0a..56280dbdf0d57 100644 --- a/x-pack/test/functional/es_archives/ml/categorization/mappings.json +++ b/x-pack/test/functional/es_archives/ml/categorization/mappings.json @@ -2,7 +2,7 @@ "type": "index", "value": { "aliases": {}, - "index": "categorization_functional_test", + "index": "ft_categorization", "mappings": { "properties": { "@timestamp": { @@ -39,835 +39,3 @@ } } } - -{ - "type": "index", - "value": { - "aliases": { - ".kibana": {} - }, - "index": ".kibana_1", - "mappings": { - "_meta": { - "migrationMappingPropertyHashes": { - "action": "c0c235fba02ebd2a2412bcda79009b58", - "action_task_params": "a9d49f184ee89641044be0ca2950fa3a", - "alert": "e588043a01d3d43477e7cad7efa0f5d8", - "apm-indices": "9bb9b2bf1fa636ed8619cbab5ce6a1dd", - "apm-services-telemetry": "07ee1939fa4302c62ddc052ec03fed90", - "config": "87aca8fdb053154f11383fce3dbf3edf", - "dashboard": "d00f614b29a80360e1190193fd333bab", - "file-upload-telemetry": "0ed4d3e1983d1217a30982630897092e", - "index-pattern": "66eccb05066c5a89924f48a9e9736499", - "kql-telemetry": "d12a98a6f19a2d273696597547e064ee", - "lens": "21c3ea0763beb1ecb0162529706b88c5", - "lens-ui-telemetry": "509bfa5978586998e05f9e303c07a327", - "map": "23d7aa4a720d4938ccde3983f87bd58d", - "maps-telemetry": "268da3a48066123fc5baf35abaa55014", - "migrationVersion": "4a1746014a75ade3a714e1db5763276f", - "ml-telemetry": "257fd1d4b4fdbb9cb4b8a3b27da201e9", - "namespace": "2f4316de49999235636386fe51dc06c1", - "query": "11aaeb7f5f7fa5bb43f25e18ce26e7d9", - "references": "7997cf5a56cc02bdc9c93361bde732b0", - "sample-data-telemetry": "7d3cfeb915303c9641c59681967ffeb4", - "search": "181661168bbadd1eff5902361e2a0d5c", - "server": "ec97f1c5da1a19609a60874e5af1100c", - "space": "c5ca8acafa0beaa4d08d014a97b6bc6b", - "telemetry": "358ffaa88ba34a97d55af0933a117de4", - "timelion-sheet": "9a2a2748877c7a7b582fef201ab1d4cf", - "tsvb-validation-telemetry": "3a37ef6c8700ae6fc97d5c7da00e9215", - "type": "2f4316de49999235636386fe51dc06c1", - "ui-metric": "0d409297dc5ebe1e3a1da691c6ee32e3", - "updated_at": "00da57df13e94e9d98437d13ace4bfe0", - "upgrade-assistant-reindex-operation": "a53a20fe086b72c9a86da3cc12dad8a6", - "upgrade-assistant-telemetry": "56702cec857e0a9dacfb696655b4ff7b", - "url": "c7f66a0df8b1b52f17c28c4adb111105", - "visualization": "52d7a13ad68a150c4525b292d23e12cc" - } - }, - "dynamic": "strict", - "properties": { - "action": { - "properties": { - "actionTypeId": { - "type": "keyword" - }, - "config": { - "enabled": false, - "type": "object" - }, - "name": { - "type": "text" - }, - "secrets": { - "type": "binary" - } - } - }, - "action_task_params": { - "properties": { - "actionId": { - "type": "keyword" - }, - "apiKey": { - "type": "binary" - }, - "params": { - "enabled": false, - "type": "object" - } - } - }, - "alert": { - "properties": { - "actions": { - "properties": { - "actionRef": { - "type": "keyword" - }, - "actionTypeId": { - "type": "keyword" - }, - "group": { - "type": "keyword" - }, - "params": { - "enabled": false, - "type": "object" - } - }, - "type": "nested" - }, - "alertTypeId": { - "type": "keyword" - }, - "apiKey": { - "type": "binary" - }, - "apiKeyOwner": { - "type": "keyword" - }, - "consumer": { - "type": "keyword" - }, - "createdAt": { - "type": "date" - }, - "createdBy": { - "type": "keyword" - }, - "enabled": { - "type": "boolean" - }, - "muteAll": { - "type": "boolean" - }, - "mutedInstanceIds": { - "type": "keyword" - }, - "name": { - "type": "text" - }, - "params": { - "enabled": false, - "type": "object" - }, - "schedule": { - "properties": { - "interval": { - "type": "keyword" - } - } - }, - "scheduledTaskId": { - "type": "keyword" - }, - "tags": { - "type": "keyword" - }, - "throttle": { - "type": "keyword" - }, - "updatedBy": { - "type": "keyword" - } - } - }, - "apm-indices": { - "properties": { - "apm_oss": { - "properties": { - "errorIndices": { - "type": "keyword" - }, - "metricsIndices": { - "type": "keyword" - }, - "onboardingIndices": { - "type": "keyword" - }, - "sourcemapIndices": { - "type": "keyword" - }, - "spanIndices": { - "type": "keyword" - }, - "transactionIndices": { - "type": "keyword" - } - } - } - } - }, - "apm-services-telemetry": { - "properties": { - "has_any_services": { - "type": "boolean" - }, - "services_per_agent": { - "properties": { - "dotnet": { - "null_value": 0, - "type": "long" - }, - "go": { - "null_value": 0, - "type": "long" - }, - "java": { - "null_value": 0, - "type": "long" - }, - "js-base": { - "null_value": 0, - "type": "long" - }, - "nodejs": { - "null_value": 0, - "type": "long" - }, - "python": { - "null_value": 0, - "type": "long" - }, - "ruby": { - "null_value": 0, - "type": "long" - }, - "rum-js": { - "null_value": 0, - "type": "long" - } - } - } - } - }, - "config": { - "dynamic": "true", - "properties": { - "buildNum": { - "type": "keyword" - }, - "dateFormat:tz": { - "fields": { - "keyword": { - "ignore_above": 256, - "type": "keyword" - } - }, - "type": "text" - }, - "defaultIndex": { - "fields": { - "keyword": { - "ignore_above": 256, - "type": "keyword" - } - }, - "type": "text" - } - } - }, - "dashboard": { - "properties": { - "description": { - "type": "text" - }, - "hits": { - "type": "integer" - }, - "kibanaSavedObjectMeta": { - "properties": { - "searchSourceJSON": { - "type": "text" - } - } - }, - "optionsJSON": { - "type": "text" - }, - "panelsJSON": { - "type": "text" - }, - "refreshInterval": { - "properties": { - "display": { - "type": "keyword" - }, - "pause": { - "type": "boolean" - }, - "section": { - "type": "integer" - }, - "value": { - "type": "integer" - } - } - }, - "timeFrom": { - "type": "keyword" - }, - "timeRestore": { - "type": "boolean" - }, - "timeTo": { - "type": "keyword" - }, - "title": { - "type": "text" - }, - "version": { - "type": "integer" - } - } - }, - "file-upload-telemetry": { - "properties": { - "filesUploadedTotalCount": { - "type": "long" - } - } - }, - "index-pattern": { - "properties": { - "fieldFormatMap": { - "type": "text" - }, - "fields": { - "type": "text" - }, - "intervalName": { - "type": "keyword" - }, - "notExpandable": { - "type": "boolean" - }, - "sourceFilters": { - "type": "text" - }, - "timeFieldName": { - "type": "keyword" - }, - "title": { - "type": "text" - }, - "type": { - "type": "keyword" - }, - "typeMeta": { - "type": "keyword" - } - } - }, - "kql-telemetry": { - "properties": { - "optInCount": { - "type": "long" - }, - "optOutCount": { - "type": "long" - } - } - }, - "lens": { - "properties": { - "expression": { - "index": false, - "type": "keyword" - }, - "state": { - "type": "keyword" - }, - "title": { - "type": "text" - }, - "visualizationType": { - "type": "keyword" - } - } - }, - "lens-ui-telemetry": { - "properties": { - "count": { - "type": "integer" - }, - "date": { - "type": "date" - }, - "name": { - "type": "keyword" - }, - "type": { - "type": "keyword" - } - } - }, - "map": { - "properties": { - "bounds": { - "type": "geo_shape" - }, - "description": { - "type": "text" - }, - "layerListJSON": { - "type": "text" - }, - "mapStateJSON": { - "type": "text" - }, - "title": { - "type": "text" - }, - "uiStateJSON": { - "type": "text" - }, - "version": { - "type": "integer" - } - } - }, - "maps-telemetry": { - "properties": { - "attributesPerMap": { - "properties": { - "dataSourcesCount": { - "properties": { - "avg": { - "type": "long" - }, - "max": { - "type": "long" - }, - "min": { - "type": "long" - } - } - }, - "emsVectorLayersCount": { - "dynamic": "true", - "type": "object" - }, - "layerTypesCount": { - "dynamic": "true", - "type": "object" - }, - "layersCount": { - "properties": { - "avg": { - "type": "long" - }, - "max": { - "type": "long" - }, - "min": { - "type": "long" - } - } - } - } - }, - "indexPatternsWithGeoFieldCount": { - "type": "long" - }, - "mapsTotalCount": { - "type": "long" - }, - "settings": { - "properties": { - "showMapVisualizationTypes": { - "type": "boolean" - } - } - }, - "timeCaptured": { - "type": "date" - } - } - }, - "migrationVersion": { - "dynamic": "true", - "properties": { - "index-pattern": { - "fields": { - "keyword": { - "ignore_above": 256, - "type": "keyword" - } - }, - "type": "text" - }, - "space": { - "fields": { - "keyword": { - "ignore_above": 256, - "type": "keyword" - } - }, - "type": "text" - } - } - }, - "ml-telemetry": { - "properties": { - "file_data_visualizer": { - "properties": { - "index_creation_count": { - "type": "long" - } - } - } - } - }, - "namespace": { - "type": "keyword" - }, - "query": { - "properties": { - "description": { - "type": "text" - }, - "filters": { - "enabled": false, - "type": "object" - }, - "query": { - "properties": { - "language": { - "type": "keyword" - }, - "query": { - "index": false, - "type": "keyword" - } - } - }, - "timefilter": { - "enabled": false, - "type": "object" - }, - "title": { - "type": "text" - } - } - }, - "references": { - "properties": { - "id": { - "type": "keyword" - }, - "name": { - "type": "keyword" - }, - "type": { - "type": "keyword" - } - }, - "type": "nested" - }, - "sample-data-telemetry": { - "properties": { - "installCount": { - "type": "long" - }, - "unInstallCount": { - "type": "long" - } - } - }, - "search": { - "properties": { - "columns": { - "type": "keyword" - }, - "description": { - "type": "text" - }, - "hits": { - "type": "integer" - }, - "kibanaSavedObjectMeta": { - "properties": { - "searchSourceJSON": { - "type": "text" - } - } - }, - "sort": { - "type": "keyword" - }, - "title": { - "type": "text" - }, - "version": { - "type": "integer" - } - } - }, - "server": { - "properties": { - "uuid": { - "type": "keyword" - } - } - }, - "space": { - "properties": { - "_reserved": { - "type": "boolean" - }, - "color": { - "type": "keyword" - }, - "description": { - "type": "text" - }, - "disabledFeatures": { - "type": "keyword" - }, - "imageUrl": { - "index": false, - "type": "text" - }, - "initials": { - "type": "keyword" - }, - "name": { - "fields": { - "keyword": { - "ignore_above": 2048, - "type": "keyword" - } - }, - "type": "text" - } - } - }, - "telemetry": { - "properties": { - "enabled": { - "type": "boolean" - }, - "lastReported": { - "type": "date" - }, - "lastVersionChecked": { - "ignore_above": 256, - "type": "keyword" - }, - "sendUsageFrom": { - "ignore_above": 256, - "type": "keyword" - }, - "userHasSeenNotice": { - "type": "boolean" - } - } - }, - "timelion-sheet": { - "properties": { - "description": { - "type": "text" - }, - "hits": { - "type": "integer" - }, - "kibanaSavedObjectMeta": { - "properties": { - "searchSourceJSON": { - "type": "text" - } - } - }, - "timelion_chart_height": { - "type": "integer" - }, - "timelion_columns": { - "type": "integer" - }, - "timelion_interval": { - "type": "keyword" - }, - "timelion_other_interval": { - "type": "keyword" - }, - "timelion_rows": { - "type": "integer" - }, - "timelion_sheet": { - "type": "text" - }, - "title": { - "type": "text" - }, - "version": { - "type": "integer" - } - } - }, - "tsvb-validation-telemetry": { - "properties": { - "failedRequests": { - "type": "long" - } - } - }, - "type": { - "type": "keyword" - }, - "ui-metric": { - "properties": { - "count": { - "type": "integer" - } - } - }, - "updated_at": { - "type": "date" - }, - "upgrade-assistant-reindex-operation": { - "dynamic": "true", - "properties": { - "indexName": { - "type": "keyword" - }, - "status": { - "type": "integer" - } - } - }, - "upgrade-assistant-telemetry": { - "properties": { - "features": { - "properties": { - "deprecation_logging": { - "properties": { - "enabled": { - "null_value": true, - "type": "boolean" - } - } - } - } - }, - "ui_open": { - "properties": { - "cluster": { - "null_value": 0, - "type": "long" - }, - "indices": { - "null_value": 0, - "type": "long" - }, - "overview": { - "null_value": 0, - "type": "long" - } - } - }, - "ui_reindex": { - "properties": { - "close": { - "null_value": 0, - "type": "long" - }, - "open": { - "null_value": 0, - "type": "long" - }, - "start": { - "null_value": 0, - "type": "long" - }, - "stop": { - "null_value": 0, - "type": "long" - } - } - } - } - }, - "url": { - "properties": { - "accessCount": { - "type": "long" - }, - "accessDate": { - "type": "date" - }, - "createDate": { - "type": "date" - }, - "url": { - "fields": { - "keyword": { - "ignore_above": 2048, - "type": "keyword" - } - }, - "type": "text" - } - } - }, - "visualization": { - "properties": { - "description": { - "type": "text" - }, - "kibanaSavedObjectMeta": { - "properties": { - "searchSourceJSON": { - "type": "text" - } - } - }, - "savedSearchRefName": { - "type": "keyword" - }, - "title": { - "type": "text" - }, - "uiStateJSON": { - "type": "text" - }, - "version": { - "type": "integer" - }, - "visState": { - "type": "text" - } - } - } - } - }, - "settings": { - "index": { - "auto_expand_replicas": "0-1", - "number_of_replicas": "0", - "number_of_shards": "1" - } - } - } -} diff --git a/x-pack/test/functional/es_archives/ml/ecommerce/data.json.gz b/x-pack/test/functional/es_archives/ml/ecommerce/data.json.gz index b38981c03417e..75fbf6fcdb845 100644 Binary files a/x-pack/test/functional/es_archives/ml/ecommerce/data.json.gz and b/x-pack/test/functional/es_archives/ml/ecommerce/data.json.gz differ diff --git a/x-pack/test/functional/es_archives/ml/ecommerce/mappings.json b/x-pack/test/functional/es_archives/ml/ecommerce/mappings.json index 9e3275bd40bfe..2910c4639162b 100644 --- a/x-pack/test/functional/es_archives/ml/ecommerce/mappings.json +++ b/x-pack/test/functional/es_archives/ml/ecommerce/mappings.json @@ -3,7 +3,7 @@ "value": { "aliases": { }, - "index": "ecommerce", + "index": "ft_ecommerce", "mappings": { "properties": { "category": { @@ -209,1018 +209,3 @@ } } } - -{ - "type": "index", - "value": { - "aliases": { - ".kibana": { - } - }, - "index": ".kibana_1", - "mappings": { - "_meta": { - "migrationMappingPropertyHashes": { - "apm-telemetry": "07ee1939fa4302c62ddc052ec03fed90", - "canvas-element": "7390014e1091044523666d97247392fc", - "canvas-workpad": "b0a1706d356228dbdcb4a17e6b9eb231", - "config": "87aca8fdb053154f11383fce3dbf3edf", - "dashboard": "d00f614b29a80360e1190193fd333bab", - "file-upload-telemetry": "0ed4d3e1983d1217a30982630897092e", - "graph-workspace": "cd7ba1330e6682e9cc00b78850874be1", - "index-pattern": "66eccb05066c5a89924f48a9e9736499", - "infrastructure-ui-source": "ddc0ecb18383f6b26101a2fadb2dab0c", - "kql-telemetry": "d12a98a6f19a2d273696597547e064ee", - "map": "23d7aa4a720d4938ccde3983f87bd58d", - "maps-telemetry": "a4229f8b16a6820c6d724b7e0c1f729d", - "migrationVersion": "4a1746014a75ade3a714e1db5763276f", - "ml-telemetry": "257fd1d4b4fdbb9cb4b8a3b27da201e9", - "namespace": "2f4316de49999235636386fe51dc06c1", - "query": "11aaeb7f5f7fa5bb43f25e18ce26e7d9", - "references": "7997cf5a56cc02bdc9c93361bde732b0", - "sample-data-telemetry": "7d3cfeb915303c9641c59681967ffeb4", - "search": "181661168bbadd1eff5902361e2a0d5c", - "server": "ec97f1c5da1a19609a60874e5af1100c", - "siem-ui-timeline": "1f6f0860ad7bc0dba3e42467ca40470d", - "siem-ui-timeline-note": "8874706eedc49059d4cf0f5094559084", - "siem-ui-timeline-pinned-event": "20638091112f0e14f0e443d512301c29", - "space": "25de8c2deec044392922989cfcf24c54", - "telemetry": "e1c8bc94e443aefd9458932cc0697a4d", - "timelion-sheet": "9a2a2748877c7a7b582fef201ab1d4cf", - "type": "2f4316de49999235636386fe51dc06c1", - "ui-metric": "0d409297dc5ebe1e3a1da691c6ee32e3", - "updated_at": "00da57df13e94e9d98437d13ace4bfe0", - "upgrade-assistant-reindex-operation": "a53a20fe086b72c9a86da3cc12dad8a6", - "upgrade-assistant-telemetry": "56702cec857e0a9dacfb696655b4ff7b", - "url": "c7f66a0df8b1b52f17c28c4adb111105", - "visualization": "52d7a13ad68a150c4525b292d23e12cc" - } - }, - "dynamic": "strict", - "properties": { - "apm-telemetry": { - "properties": { - "has_any_services": { - "type": "boolean" - }, - "services_per_agent": { - "properties": { - "dotnet": { - "null_value": 0, - "type": "long" - }, - "go": { - "null_value": 0, - "type": "long" - }, - "java": { - "null_value": 0, - "type": "long" - }, - "js-base": { - "null_value": 0, - "type": "long" - }, - "nodejs": { - "null_value": 0, - "type": "long" - }, - "python": { - "null_value": 0, - "type": "long" - }, - "ruby": { - "null_value": 0, - "type": "long" - }, - "rum-js": { - "null_value": 0, - "type": "long" - } - } - } - } - }, - "canvas-element": { - "dynamic": "false", - "properties": { - "@created": { - "type": "date" - }, - "@timestamp": { - "type": "date" - }, - "content": { - "type": "text" - }, - "help": { - "type": "text" - }, - "image": { - "type": "text" - }, - "name": { - "fields": { - "keyword": { - "type": "keyword" - } - }, - "type": "text" - } - } - }, - "canvas-workpad": { - "dynamic": "false", - "properties": { - "@created": { - "type": "date" - }, - "@timestamp": { - "type": "date" - }, - "name": { - "fields": { - "keyword": { - "type": "keyword" - } - }, - "type": "text" - } - } - }, - "config": { - "dynamic": "true", - "properties": { - "buildNum": { - "type": "keyword" - }, - "dateFormat:tz": { - "fields": { - "keyword": { - "ignore_above": 256, - "type": "keyword" - } - }, - "type": "text" - }, - "defaultIndex": { - "fields": { - "keyword": { - "ignore_above": 256, - "type": "keyword" - } - }, - "type": "text" - } - } - }, - "dashboard": { - "properties": { - "description": { - "type": "text" - }, - "hits": { - "type": "integer" - }, - "kibanaSavedObjectMeta": { - "properties": { - "searchSourceJSON": { - "type": "text" - } - } - }, - "optionsJSON": { - "type": "text" - }, - "panelsJSON": { - "type": "text" - }, - "refreshInterval": { - "properties": { - "display": { - "type": "keyword" - }, - "pause": { - "type": "boolean" - }, - "section": { - "type": "integer" - }, - "value": { - "type": "integer" - } - } - }, - "timeFrom": { - "type": "keyword" - }, - "timeRestore": { - "type": "boolean" - }, - "timeTo": { - "type": "keyword" - }, - "title": { - "type": "text" - }, - "version": { - "type": "integer" - } - } - }, - "file-upload-telemetry": { - "properties": { - "filesUploadedTotalCount": { - "type": "long" - } - } - }, - "graph-workspace": { - "properties": { - "description": { - "type": "text" - }, - "kibanaSavedObjectMeta": { - "properties": { - "searchSourceJSON": { - "type": "text" - } - } - }, - "numLinks": { - "type": "integer" - }, - "numVertices": { - "type": "integer" - }, - "title": { - "type": "text" - }, - "version": { - "type": "integer" - }, - "wsState": { - "type": "text" - } - } - }, - "index-pattern": { - "properties": { - "fieldFormatMap": { - "type": "text" - }, - "fields": { - "type": "text" - }, - "intervalName": { - "type": "keyword" - }, - "notExpandable": { - "type": "boolean" - }, - "sourceFilters": { - "type": "text" - }, - "timeFieldName": { - "type": "keyword" - }, - "title": { - "type": "text" - }, - "type": { - "type": "keyword" - }, - "typeMeta": { - "type": "keyword" - } - } - }, - "infrastructure-ui-source": { - "properties": { - "description": { - "type": "text" - }, - "fields": { - "properties": { - "container": { - "type": "keyword" - }, - "host": { - "type": "keyword" - }, - "pod": { - "type": "keyword" - }, - "tiebreaker": { - "type": "keyword" - }, - "timestamp": { - "type": "keyword" - } - } - }, - "logAlias": { - "type": "keyword" - }, - "logColumns": { - "properties": { - "fieldColumn": { - "properties": { - "field": { - "type": "keyword" - }, - "id": { - "type": "keyword" - } - } - }, - "messageColumn": { - "properties": { - "id": { - "type": "keyword" - } - } - }, - "timestampColumn": { - "properties": { - "id": { - "type": "keyword" - } - } - } - }, - "type": "nested" - }, - "metricAlias": { - "type": "keyword" - }, - "name": { - "type": "text" - } - } - }, - "kql-telemetry": { - "properties": { - "optInCount": { - "type": "long" - }, - "optOutCount": { - "type": "long" - } - } - }, - "map": { - "properties": { - "bounds": { - "type": "geo_shape" - }, - "description": { - "type": "text" - }, - "layerListJSON": { - "type": "text" - }, - "mapStateJSON": { - "type": "text" - }, - "title": { - "type": "text" - }, - "uiStateJSON": { - "type": "text" - }, - "version": { - "type": "integer" - } - } - }, - "maps-telemetry": { - "properties": { - "attributesPerMap": { - "properties": { - "dataSourcesCount": { - "properties": { - "avg": { - "type": "long" - }, - "max": { - "type": "long" - }, - "min": { - "type": "long" - } - } - }, - "emsVectorLayersCount": { - "dynamic": "true", - "type": "object" - }, - "layerTypesCount": { - "dynamic": "true", - "type": "object" - }, - "layersCount": { - "properties": { - "avg": { - "type": "long" - }, - "max": { - "type": "long" - }, - "min": { - "type": "long" - } - } - } - } - }, - "mapsTotalCount": { - "type": "long" - }, - "timeCaptured": { - "type": "date" - } - } - }, - "migrationVersion": { - "dynamic": "true", - "properties": { - "index-pattern": { - "fields": { - "keyword": { - "ignore_above": 256, - "type": "keyword" - } - }, - "type": "text" - }, - "space": { - "fields": { - "keyword": { - "ignore_above": 256, - "type": "keyword" - } - }, - "type": "text" - } - } - }, - "ml-telemetry": { - "properties": { - "file_data_visualizer": { - "properties": { - "index_creation_count": { - "type": "long" - } - } - } - } - }, - "namespace": { - "type": "keyword" - }, - "query": { - "properties": { - "description": { - "type": "text" - }, - "filters": { - "enabled": false, - "type": "object" - }, - "query": { - "properties": { - "language": { - "type": "keyword" - }, - "query": { - "index": false, - "type": "keyword" - } - } - }, - "timefilter": { - "enabled": false, - "type": "object" - }, - "title": { - "type": "text" - } - } - }, - "references": { - "properties": { - "id": { - "type": "keyword" - }, - "name": { - "type": "keyword" - }, - "type": { - "type": "keyword" - } - }, - "type": "nested" - }, - "sample-data-telemetry": { - "properties": { - "installCount": { - "type": "long" - }, - "unInstallCount": { - "type": "long" - } - } - }, - "search": { - "properties": { - "columns": { - "type": "keyword" - }, - "description": { - "type": "text" - }, - "hits": { - "type": "integer" - }, - "kibanaSavedObjectMeta": { - "properties": { - "searchSourceJSON": { - "type": "text" - } - } - }, - "sort": { - "type": "keyword" - }, - "title": { - "type": "text" - }, - "version": { - "type": "integer" - } - } - }, - "server": { - "properties": { - "uuid": { - "type": "keyword" - } - } - }, - "siem-ui-timeline": { - "properties": { - "columns": { - "properties": { - "aggregatable": { - "type": "boolean" - }, - "category": { - "type": "keyword" - }, - "columnHeaderType": { - "type": "keyword" - }, - "description": { - "type": "text" - }, - "example": { - "type": "text" - }, - "id": { - "type": "keyword" - }, - "indexes": { - "type": "keyword" - }, - "name": { - "type": "text" - }, - "placeholder": { - "type": "text" - }, - "searchable": { - "type": "boolean" - }, - "type": { - "type": "keyword" - } - } - }, - "created": { - "type": "date" - }, - "createdBy": { - "type": "text" - }, - "dataProviders": { - "properties": { - "and": { - "properties": { - "enabled": { - "type": "boolean" - }, - "excluded": { - "type": "boolean" - }, - "id": { - "type": "keyword" - }, - "kqlQuery": { - "type": "text" - }, - "name": { - "type": "text" - }, - "queryMatch": { - "properties": { - "displayField": { - "type": "text" - }, - "displayValue": { - "type": "text" - }, - "field": { - "type": "text" - }, - "operator": { - "type": "text" - }, - "value": { - "type": "text" - } - } - } - } - }, - "enabled": { - "type": "boolean" - }, - "excluded": { - "type": "boolean" - }, - "id": { - "type": "keyword" - }, - "kqlQuery": { - "type": "text" - }, - "name": { - "type": "text" - }, - "queryMatch": { - "properties": { - "displayField": { - "type": "text" - }, - "displayValue": { - "type": "text" - }, - "field": { - "type": "text" - }, - "operator": { - "type": "text" - }, - "value": { - "type": "text" - } - } - } - } - }, - "dateRange": { - "properties": { - "end": { - "type": "date" - }, - "start": { - "type": "date" - } - } - }, - "description": { - "type": "text" - }, - "favorite": { - "properties": { - "favoriteDate": { - "type": "date" - }, - "fullName": { - "type": "text" - }, - "keySearch": { - "type": "text" - }, - "userName": { - "type": "text" - } - } - }, - "kqlMode": { - "type": "keyword" - }, - "kqlQuery": { - "properties": { - "filterQuery": { - "properties": { - "kuery": { - "properties": { - "expression": { - "type": "text" - }, - "kind": { - "type": "keyword" - } - } - }, - "serializedQuery": { - "type": "text" - } - } - } - } - }, - "sort": { - "properties": { - "columnId": { - "type": "keyword" - }, - "sortDirection": { - "type": "keyword" - } - } - }, - "title": { - "type": "text" - }, - "updated": { - "type": "date" - }, - "updatedBy": { - "type": "text" - } - } - }, - "siem-ui-timeline-note": { - "properties": { - "created": { - "type": "date" - }, - "createdBy": { - "type": "text" - }, - "eventId": { - "type": "keyword" - }, - "note": { - "type": "text" - }, - "timelineId": { - "type": "keyword" - }, - "updated": { - "type": "date" - }, - "updatedBy": { - "type": "text" - } - } - }, - "siem-ui-timeline-pinned-event": { - "properties": { - "created": { - "type": "date" - }, - "createdBy": { - "type": "text" - }, - "eventId": { - "type": "keyword" - }, - "timelineId": { - "type": "keyword" - }, - "updated": { - "type": "date" - }, - "updatedBy": { - "type": "text" - } - } - }, - "space": { - "properties": { - "_reserved": { - "type": "boolean" - }, - "color": { - "type": "keyword" - }, - "description": { - "type": "text" - }, - "disabledFeatures": { - "type": "keyword" - }, - "initials": { - "type": "keyword" - }, - "name": { - "fields": { - "keyword": { - "ignore_above": 2048, - "type": "keyword" - } - }, - "type": "text" - } - } - }, - "telemetry": { - "properties": { - "enabled": { - "type": "boolean" - } - } - }, - "timelion-sheet": { - "properties": { - "description": { - "type": "text" - }, - "hits": { - "type": "integer" - }, - "kibanaSavedObjectMeta": { - "properties": { - "searchSourceJSON": { - "type": "text" - } - } - }, - "timelion_chart_height": { - "type": "integer" - }, - "timelion_columns": { - "type": "integer" - }, - "timelion_interval": { - "type": "keyword" - }, - "timelion_other_interval": { - "type": "keyword" - }, - "timelion_rows": { - "type": "integer" - }, - "timelion_sheet": { - "type": "text" - }, - "title": { - "type": "text" - }, - "version": { - "type": "integer" - } - } - }, - "type": { - "type": "keyword" - }, - "ui-metric": { - "properties": { - "count": { - "type": "integer" - } - } - }, - "updated_at": { - "type": "date" - }, - "upgrade-assistant-reindex-operation": { - "dynamic": "true", - "properties": { - "indexName": { - "type": "keyword" - }, - "status": { - "type": "integer" - } - } - }, - "upgrade-assistant-telemetry": { - "properties": { - "features": { - "properties": { - "deprecation_logging": { - "properties": { - "enabled": { - "null_value": true, - "type": "boolean" - } - } - } - } - }, - "ui_open": { - "properties": { - "cluster": { - "null_value": 0, - "type": "long" - }, - "indices": { - "null_value": 0, - "type": "long" - }, - "overview": { - "null_value": 0, - "type": "long" - } - } - }, - "ui_reindex": { - "properties": { - "close": { - "null_value": 0, - "type": "long" - }, - "open": { - "null_value": 0, - "type": "long" - }, - "start": { - "null_value": 0, - "type": "long" - }, - "stop": { - "null_value": 0, - "type": "long" - } - } - } - } - }, - "url": { - "properties": { - "accessCount": { - "type": "long" - }, - "accessDate": { - "type": "date" - }, - "createDate": { - "type": "date" - }, - "url": { - "fields": { - "keyword": { - "ignore_above": 2048, - "type": "keyword" - } - }, - "type": "text" - } - } - }, - "visualization": { - "properties": { - "description": { - "type": "text" - }, - "kibanaSavedObjectMeta": { - "properties": { - "searchSourceJSON": { - "type": "text" - } - } - }, - "savedSearchRefName": { - "type": "keyword" - }, - "title": { - "type": "text" - }, - "uiStateJSON": { - "type": "text" - }, - "version": { - "type": "integer" - }, - "visState": { - "type": "text" - } - } - } - } - }, - "settings": { - "index": { - "auto_expand_replicas": "0-1", - "number_of_replicas": "0", - "number_of_shards": "1" - } - } - } -} \ No newline at end of file diff --git a/x-pack/test/functional/es_archives/ml/egs_regression/data.json.gz b/x-pack/test/functional/es_archives/ml/egs_regression/data.json.gz index 78a8b65b4a124..477c2e638ee0f 100644 Binary files a/x-pack/test/functional/es_archives/ml/egs_regression/data.json.gz and b/x-pack/test/functional/es_archives/ml/egs_regression/data.json.gz differ diff --git a/x-pack/test/functional/es_archives/ml/egs_regression/mappings.json b/x-pack/test/functional/es_archives/ml/egs_regression/mappings.json index bfaecc6820469..72a411819b0ac 100644 --- a/x-pack/test/functional/es_archives/ml/egs_regression/mappings.json +++ b/x-pack/test/functional/es_archives/ml/egs_regression/mappings.json @@ -3,7 +3,7 @@ "value": { "aliases": { }, - "index": "egs_regression", + "index": "ft_egs_regression", "mappings": { "properties": { "g1": { @@ -58,1395 +58,3 @@ } } } - -{ - "type": "index", - "value": { - "aliases": { - ".kibana": { - } - }, - "index": ".kibana_1", - "mappings": { - "_meta": { - "migrationMappingPropertyHashes": { - "action": "ecc01e367a369542bc2b15dae1fb1773", - "action_task_params": "a9d49f184ee89641044be0ca2950fa3a", - "alert": "3cdf52bff6f482e53b825b45686604db", - "apm-indices": "9bb9b2bf1fa636ed8619cbab5ce6a1dd", - "apm-services-telemetry": "07ee1939fa4302c62ddc052ec03fed90", - "canvas-element": "7390014e1091044523666d97247392fc", - "canvas-workpad": "b0a1706d356228dbdcb4a17e6b9eb231", - "config": "87aca8fdb053154f11383fce3dbf3edf", - "dashboard": "d00f614b29a80360e1190193fd333bab", - "file-upload-telemetry": "0ed4d3e1983d1217a30982630897092e", - "graph-workspace": "cd7ba1330e6682e9cc00b78850874be1", - "index-pattern": "66eccb05066c5a89924f48a9e9736499", - "infrastructure-ui-source": "ddc0ecb18383f6b26101a2fadb2dab0c", - "inventory-view": "84b320fd67209906333ffce261128462", - "kql-telemetry": "d12a98a6f19a2d273696597547e064ee", - "lens": "21c3ea0763beb1ecb0162529706b88c5", - "lens-ui-telemetry": "509bfa5978586998e05f9e303c07a327", - "map": "23d7aa4a720d4938ccde3983f87bd58d", - "maps-telemetry": "a4229f8b16a6820c6d724b7e0c1f729d", - "metrics-explorer-view": "53c5365793677328df0ccb6138bf3cdd", - "migrationVersion": "4a1746014a75ade3a714e1db5763276f", - "ml-telemetry": "257fd1d4b4fdbb9cb4b8a3b27da201e9", - "namespace": "2f4316de49999235636386fe51dc06c1", - "query": "11aaeb7f5f7fa5bb43f25e18ce26e7d9", - "references": "7997cf5a56cc02bdc9c93361bde732b0", - "sample-data-telemetry": "7d3cfeb915303c9641c59681967ffeb4", - "search": "181661168bbadd1eff5902361e2a0d5c", - "server": "ec97f1c5da1a19609a60874e5af1100c", - "siem-ui-timeline": "6485ab095be8d15246667b98a1a34295", - "siem-ui-timeline-note": "8874706eedc49059d4cf0f5094559084", - "siem-ui-timeline-pinned-event": "20638091112f0e14f0e443d512301c29", - "space": "c5ca8acafa0beaa4d08d014a97b6bc6b", - "telemetry": "358ffaa88ba34a97d55af0933a117de4", - "timelion-sheet": "9a2a2748877c7a7b582fef201ab1d4cf", - "type": "2f4316de49999235636386fe51dc06c1", - "ui-metric": "0d409297dc5ebe1e3a1da691c6ee32e3", - "updated_at": "00da57df13e94e9d98437d13ace4bfe0", - "upgrade-assistant-reindex-operation": "a53a20fe086b72c9a86da3cc12dad8a6", - "upgrade-assistant-telemetry": "56702cec857e0a9dacfb696655b4ff7b", - "url": "c7f66a0df8b1b52f17c28c4adb111105", - "visualization": "52d7a13ad68a150c4525b292d23e12cc" - } - }, - "dynamic": "strict", - "properties": { - "action": { - "properties": { - "actionTypeId": { - "type": "keyword" - }, - "config": { - "enabled": false, - "type": "object" - }, - "description": { - "type": "text" - }, - "secrets": { - "type": "binary" - } - } - }, - "action_task_params": { - "properties": { - "actionId": { - "type": "keyword" - }, - "apiKey": { - "type": "binary" - }, - "params": { - "enabled": false, - "type": "object" - } - } - }, - "alert": { - "properties": { - "actions": { - "properties": { - "actionRef": { - "type": "keyword" - }, - "group": { - "type": "keyword" - }, - "params": { - "enabled": false, - "type": "object" - } - }, - "type": "nested" - }, - "alertTypeId": { - "type": "keyword" - }, - "apiKey": { - "type": "binary" - }, - "apiKeyOwner": { - "type": "keyword" - }, - "createdBy": { - "type": "keyword" - }, - "enabled": { - "type": "boolean" - }, - "interval": { - "type": "keyword" - }, - "muteAll": { - "type": "boolean" - }, - "mutedInstanceIds": { - "type": "keyword" - }, - "name": { - "type": "text" - }, - "params": { - "enabled": false, - "type": "object" - }, - "scheduledTaskId": { - "type": "keyword" - }, - "tags": { - "type": "keyword" - }, - "throttle": { - "type": "keyword" - }, - "updatedBy": { - "type": "keyword" - } - } - }, - "apm-indices": { - "properties": { - "apm_oss": { - "properties": { - "errorIndices": { - "type": "keyword" - }, - "metricsIndices": { - "type": "keyword" - }, - "onboardingIndices": { - "type": "keyword" - }, - "sourcemapIndices": { - "type": "keyword" - }, - "spanIndices": { - "type": "keyword" - }, - "transactionIndices": { - "type": "keyword" - } - } - } - } - }, - "apm-services-telemetry": { - "properties": { - "has_any_services": { - "type": "boolean" - }, - "services_per_agent": { - "properties": { - "dotnet": { - "null_value": 0, - "type": "long" - }, - "go": { - "null_value": 0, - "type": "long" - }, - "java": { - "null_value": 0, - "type": "long" - }, - "js-base": { - "null_value": 0, - "type": "long" - }, - "nodejs": { - "null_value": 0, - "type": "long" - }, - "python": { - "null_value": 0, - "type": "long" - }, - "ruby": { - "null_value": 0, - "type": "long" - }, - "rum-js": { - "null_value": 0, - "type": "long" - } - } - } - } - }, - "canvas-element": { - "dynamic": "false", - "properties": { - "@created": { - "type": "date" - }, - "@timestamp": { - "type": "date" - }, - "content": { - "type": "text" - }, - "help": { - "type": "text" - }, - "image": { - "type": "text" - }, - "name": { - "fields": { - "keyword": { - "type": "keyword" - } - }, - "type": "text" - } - } - }, - "canvas-workpad": { - "dynamic": "false", - "properties": { - "@created": { - "type": "date" - }, - "@timestamp": { - "type": "date" - }, - "name": { - "fields": { - "keyword": { - "type": "keyword" - } - }, - "type": "text" - } - } - }, - "config": { - "dynamic": "true", - "properties": { - "buildNum": { - "type": "keyword" - }, - "dateFormat:tz": { - "fields": { - "keyword": { - "ignore_above": 256, - "type": "keyword" - } - }, - "type": "text" - }, - "defaultIndex": { - "fields": { - "keyword": { - "ignore_above": 256, - "type": "keyword" - } - }, - "type": "text" - } - } - }, - "dashboard": { - "properties": { - "description": { - "type": "text" - }, - "hits": { - "type": "integer" - }, - "kibanaSavedObjectMeta": { - "properties": { - "searchSourceJSON": { - "type": "text" - } - } - }, - "optionsJSON": { - "type": "text" - }, - "panelsJSON": { - "type": "text" - }, - "refreshInterval": { - "properties": { - "display": { - "type": "keyword" - }, - "pause": { - "type": "boolean" - }, - "section": { - "type": "integer" - }, - "value": { - "type": "integer" - } - } - }, - "timeFrom": { - "type": "keyword" - }, - "timeRestore": { - "type": "boolean" - }, - "timeTo": { - "type": "keyword" - }, - "title": { - "type": "text" - }, - "version": { - "type": "integer" - } - } - }, - "file-upload-telemetry": { - "properties": { - "filesUploadedTotalCount": { - "type": "long" - } - } - }, - "graph-workspace": { - "properties": { - "description": { - "type": "text" - }, - "kibanaSavedObjectMeta": { - "properties": { - "searchSourceJSON": { - "type": "text" - } - } - }, - "numLinks": { - "type": "integer" - }, - "numVertices": { - "type": "integer" - }, - "title": { - "type": "text" - }, - "version": { - "type": "integer" - }, - "wsState": { - "type": "text" - } - } - }, - "index-pattern": { - "properties": { - "fieldFormatMap": { - "type": "text" - }, - "fields": { - "type": "text" - }, - "intervalName": { - "type": "keyword" - }, - "notExpandable": { - "type": "boolean" - }, - "sourceFilters": { - "type": "text" - }, - "timeFieldName": { - "type": "keyword" - }, - "title": { - "type": "text" - }, - "type": { - "type": "keyword" - }, - "typeMeta": { - "type": "keyword" - } - } - }, - "infrastructure-ui-source": { - "properties": { - "description": { - "type": "text" - }, - "fields": { - "properties": { - "container": { - "type": "keyword" - }, - "host": { - "type": "keyword" - }, - "pod": { - "type": "keyword" - }, - "tiebreaker": { - "type": "keyword" - }, - "timestamp": { - "type": "keyword" - } - } - }, - "logAlias": { - "type": "keyword" - }, - "logColumns": { - "properties": { - "fieldColumn": { - "properties": { - "field": { - "type": "keyword" - }, - "id": { - "type": "keyword" - } - } - }, - "messageColumn": { - "properties": { - "id": { - "type": "keyword" - } - } - }, - "timestampColumn": { - "properties": { - "id": { - "type": "keyword" - } - } - } - }, - "type": "nested" - }, - "metricAlias": { - "type": "keyword" - }, - "name": { - "type": "text" - } - } - }, - "inventory-view": { - "properties": { - "autoBounds": { - "type": "boolean" - }, - "autoReload": { - "type": "boolean" - }, - "boundsOverride": { - "properties": { - "max": { - "type": "integer" - }, - "min": { - "type": "integer" - } - } - }, - "customOptions": { - "properties": { - "field": { - "type": "keyword" - }, - "text": { - "type": "keyword" - } - }, - "type": "nested" - }, - "filterQuery": { - "properties": { - "expression": { - "type": "keyword" - }, - "kind": { - "type": "keyword" - } - } - }, - "groupBy": { - "properties": { - "field": { - "type": "keyword" - }, - "label": { - "type": "keyword" - } - }, - "type": "nested" - }, - "metric": { - "properties": { - "type": { - "type": "keyword" - } - } - }, - "name": { - "type": "keyword" - }, - "nodeType": { - "type": "keyword" - }, - "time": { - "type": "integer" - }, - "view": { - "type": "keyword" - } - } - }, - "kql-telemetry": { - "properties": { - "optInCount": { - "type": "long" - }, - "optOutCount": { - "type": "long" - } - } - }, - "lens": { - "properties": { - "expression": { - "index": false, - "type": "keyword" - }, - "state": { - "type": "flattened" - }, - "title": { - "type": "text" - }, - "visualizationType": { - "type": "keyword" - } - } - }, - "lens-ui-telemetry": { - "properties": { - "count": { - "type": "integer" - }, - "date": { - "type": "date" - }, - "name": { - "type": "keyword" - }, - "type": { - "type": "keyword" - } - } - }, - "map": { - "properties": { - "bounds": { - "type": "geo_shape" - }, - "description": { - "type": "text" - }, - "layerListJSON": { - "type": "text" - }, - "mapStateJSON": { - "type": "text" - }, - "title": { - "type": "text" - }, - "uiStateJSON": { - "type": "text" - }, - "version": { - "type": "integer" - } - } - }, - "maps-telemetry": { - "properties": { - "attributesPerMap": { - "properties": { - "dataSourcesCount": { - "properties": { - "avg": { - "type": "long" - }, - "max": { - "type": "long" - }, - "min": { - "type": "long" - } - } - }, - "emsVectorLayersCount": { - "dynamic": "true", - "type": "object" - }, - "layerTypesCount": { - "dynamic": "true", - "type": "object" - }, - "layersCount": { - "properties": { - "avg": { - "type": "long" - }, - "max": { - "type": "long" - }, - "min": { - "type": "long" - } - } - } - } - }, - "mapsTotalCount": { - "type": "long" - }, - "timeCaptured": { - "type": "date" - } - } - }, - "metrics-explorer-view": { - "properties": { - "chartOptions": { - "properties": { - "stack": { - "type": "boolean" - }, - "type": { - "type": "keyword" - }, - "yAxisMode": { - "type": "keyword" - } - } - }, - "currentTimerange": { - "properties": { - "from": { - "type": "keyword" - }, - "interval": { - "type": "keyword" - }, - "to": { - "type": "keyword" - } - } - }, - "name": { - "type": "keyword" - }, - "options": { - "properties": { - "aggregation": { - "type": "keyword" - }, - "filterQuery": { - "type": "keyword" - }, - "groupBy": { - "type": "keyword" - }, - "limit": { - "type": "integer" - }, - "metrics": { - "properties": { - "aggregation": { - "type": "keyword" - }, - "color": { - "type": "keyword" - }, - "field": { - "type": "keyword" - }, - "label": { - "type": "keyword" - } - }, - "type": "nested" - } - } - } - } - }, - "migrationVersion": { - "dynamic": "true", - "properties": { - "index-pattern": { - "fields": { - "keyword": { - "ignore_above": 256, - "type": "keyword" - } - }, - "type": "text" - }, - "space": { - "fields": { - "keyword": { - "ignore_above": 256, - "type": "keyword" - } - }, - "type": "text" - } - } - }, - "ml-telemetry": { - "properties": { - "file_data_visualizer": { - "properties": { - "index_creation_count": { - "type": "long" - } - } - } - } - }, - "namespace": { - "type": "keyword" - }, - "query": { - "properties": { - "description": { - "type": "text" - }, - "filters": { - "enabled": false, - "type": "object" - }, - "query": { - "properties": { - "language": { - "type": "keyword" - }, - "query": { - "index": false, - "type": "keyword" - } - } - }, - "timefilter": { - "enabled": false, - "type": "object" - }, - "title": { - "type": "text" - } - } - }, - "references": { - "properties": { - "id": { - "type": "keyword" - }, - "name": { - "type": "keyword" - }, - "type": { - "type": "keyword" - } - }, - "type": "nested" - }, - "sample-data-telemetry": { - "properties": { - "installCount": { - "type": "long" - }, - "unInstallCount": { - "type": "long" - } - } - }, - "search": { - "properties": { - "columns": { - "type": "keyword" - }, - "description": { - "type": "text" - }, - "hits": { - "type": "integer" - }, - "kibanaSavedObjectMeta": { - "properties": { - "searchSourceJSON": { - "type": "text" - } - } - }, - "sort": { - "type": "keyword" - }, - "title": { - "type": "text" - }, - "version": { - "type": "integer" - } - } - }, - "server": { - "properties": { - "uuid": { - "type": "keyword" - } - } - }, - "siem-ui-timeline": { - "properties": { - "columns": { - "properties": { - "aggregatable": { - "type": "boolean" - }, - "category": { - "type": "keyword" - }, - "columnHeaderType": { - "type": "keyword" - }, - "description": { - "type": "text" - }, - "example": { - "type": "text" - }, - "id": { - "type": "keyword" - }, - "indexes": { - "type": "keyword" - }, - "name": { - "type": "text" - }, - "placeholder": { - "type": "text" - }, - "searchable": { - "type": "boolean" - }, - "type": { - "type": "keyword" - } - } - }, - "created": { - "type": "date" - }, - "createdBy": { - "type": "text" - }, - "dataProviders": { - "properties": { - "and": { - "properties": { - "enabled": { - "type": "boolean" - }, - "excluded": { - "type": "boolean" - }, - "id": { - "type": "keyword" - }, - "kqlQuery": { - "type": "text" - }, - "name": { - "type": "text" - }, - "queryMatch": { - "properties": { - "displayField": { - "type": "text" - }, - "displayValue": { - "type": "text" - }, - "field": { - "type": "text" - }, - "operator": { - "type": "text" - }, - "value": { - "type": "text" - } - } - } - } - }, - "enabled": { - "type": "boolean" - }, - "excluded": { - "type": "boolean" - }, - "id": { - "type": "keyword" - }, - "kqlQuery": { - "type": "text" - }, - "name": { - "type": "text" - }, - "queryMatch": { - "properties": { - "displayField": { - "type": "text" - }, - "displayValue": { - "type": "text" - }, - "field": { - "type": "text" - }, - "operator": { - "type": "text" - }, - "value": { - "type": "text" - } - } - } - } - }, - "dateRange": { - "properties": { - "end": { - "type": "date" - }, - "start": { - "type": "date" - } - } - }, - "description": { - "type": "text" - }, - "favorite": { - "properties": { - "favoriteDate": { - "type": "date" - }, - "fullName": { - "type": "text" - }, - "keySearch": { - "type": "text" - }, - "userName": { - "type": "text" - } - } - }, - "filters": { - "properties": { - "exists": { - "type": "text" - }, - "match_all": { - "type": "text" - }, - "meta": { - "properties": { - "alias": { - "type": "text" - }, - "controlledBy": { - "type": "text" - }, - "disabled": { - "type": "boolean" - }, - "field": { - "type": "text" - }, - "formattedValue": { - "type": "text" - }, - "index": { - "type": "keyword" - }, - "key": { - "type": "keyword" - }, - "negate": { - "type": "boolean" - }, - "params": { - "type": "text" - }, - "type": { - "type": "keyword" - }, - "value": { - "type": "text" - } - } - }, - "missing": { - "type": "text" - }, - "query": { - "type": "text" - }, - "range": { - "type": "text" - }, - "script": { - "type": "text" - } - } - }, - "kqlMode": { - "type": "keyword" - }, - "kqlQuery": { - "properties": { - "filterQuery": { - "properties": { - "kuery": { - "properties": { - "expression": { - "type": "text" - }, - "kind": { - "type": "keyword" - } - } - }, - "serializedQuery": { - "type": "text" - } - } - } - } - }, - "savedQueryId": { - "type": "keyword" - }, - "sort": { - "properties": { - "columnId": { - "type": "keyword" - }, - "sortDirection": { - "type": "keyword" - } - } - }, - "title": { - "type": "text" - }, - "updated": { - "type": "date" - }, - "updatedBy": { - "type": "text" - } - } - }, - "siem-ui-timeline-note": { - "properties": { - "created": { - "type": "date" - }, - "createdBy": { - "type": "text" - }, - "eventId": { - "type": "keyword" - }, - "note": { - "type": "text" - }, - "timelineId": { - "type": "keyword" - }, - "updated": { - "type": "date" - }, - "updatedBy": { - "type": "text" - } - } - }, - "siem-ui-timeline-pinned-event": { - "properties": { - "created": { - "type": "date" - }, - "createdBy": { - "type": "text" - }, - "eventId": { - "type": "keyword" - }, - "timelineId": { - "type": "keyword" - }, - "updated": { - "type": "date" - }, - "updatedBy": { - "type": "text" - } - } - }, - "space": { - "properties": { - "_reserved": { - "type": "boolean" - }, - "color": { - "type": "keyword" - }, - "description": { - "type": "text" - }, - "disabledFeatures": { - "type": "keyword" - }, - "imageUrl": { - "index": false, - "type": "text" - }, - "initials": { - "type": "keyword" - }, - "name": { - "fields": { - "keyword": { - "ignore_above": 2048, - "type": "keyword" - } - }, - "type": "text" - } - } - }, - "telemetry": { - "properties": { - "enabled": { - "type": "boolean" - }, - "lastReported": { - "type": "date" - }, - "lastVersionChecked": { - "ignore_above": 256, - "type": "keyword" - }, - "sendUsageFrom": { - "ignore_above": 256, - "type": "keyword" - }, - "userHasSeenNotice": { - "type": "boolean" - } - } - }, - "timelion-sheet": { - "properties": { - "description": { - "type": "text" - }, - "hits": { - "type": "integer" - }, - "kibanaSavedObjectMeta": { - "properties": { - "searchSourceJSON": { - "type": "text" - } - } - }, - "timelion_chart_height": { - "type": "integer" - }, - "timelion_columns": { - "type": "integer" - }, - "timelion_interval": { - "type": "keyword" - }, - "timelion_other_interval": { - "type": "keyword" - }, - "timelion_rows": { - "type": "integer" - }, - "timelion_sheet": { - "type": "text" - }, - "title": { - "type": "text" - }, - "version": { - "type": "integer" - } - } - }, - "type": { - "type": "keyword" - }, - "ui-metric": { - "properties": { - "count": { - "type": "integer" - } - } - }, - "updated_at": { - "type": "date" - }, - "upgrade-assistant-reindex-operation": { - "dynamic": "true", - "properties": { - "indexName": { - "type": "keyword" - }, - "status": { - "type": "integer" - } - } - }, - "upgrade-assistant-telemetry": { - "properties": { - "features": { - "properties": { - "deprecation_logging": { - "properties": { - "enabled": { - "null_value": true, - "type": "boolean" - } - } - } - } - }, - "ui_open": { - "properties": { - "cluster": { - "null_value": 0, - "type": "long" - }, - "indices": { - "null_value": 0, - "type": "long" - }, - "overview": { - "null_value": 0, - "type": "long" - } - } - }, - "ui_reindex": { - "properties": { - "close": { - "null_value": 0, - "type": "long" - }, - "open": { - "null_value": 0, - "type": "long" - }, - "start": { - "null_value": 0, - "type": "long" - }, - "stop": { - "null_value": 0, - "type": "long" - } - } - } - } - }, - "url": { - "properties": { - "accessCount": { - "type": "long" - }, - "accessDate": { - "type": "date" - }, - "createDate": { - "type": "date" - }, - "url": { - "fields": { - "keyword": { - "ignore_above": 2048, - "type": "keyword" - } - }, - "type": "text" - } - } - }, - "visualization": { - "properties": { - "description": { - "type": "text" - }, - "kibanaSavedObjectMeta": { - "properties": { - "searchSourceJSON": { - "type": "text" - } - } - }, - "savedSearchRefName": { - "type": "keyword" - }, - "title": { - "type": "text" - }, - "uiStateJSON": { - "type": "text" - }, - "version": { - "type": "integer" - }, - "visState": { - "type": "text" - } - } - } - } - }, - "settings": { - "index": { - "auto_expand_replicas": "0-1", - "number_of_replicas": "0", - "number_of_shards": "1" - } - } - } -} \ No newline at end of file diff --git a/x-pack/test/functional/es_archives/ml/event_rate_nanos/data.json.gz b/x-pack/test/functional/es_archives/ml/event_rate_nanos/data.json.gz index 838b8d1872c0a..6170d9affd5a6 100644 Binary files a/x-pack/test/functional/es_archives/ml/event_rate_nanos/data.json.gz and b/x-pack/test/functional/es_archives/ml/event_rate_nanos/data.json.gz differ diff --git a/x-pack/test/functional/es_archives/ml/event_rate_nanos/mappings.json b/x-pack/test/functional/es_archives/ml/event_rate_nanos/mappings.json index 6897e05e75c2e..dbed30f3f855c 100644 --- a/x-pack/test/functional/es_archives/ml/event_rate_nanos/mappings.json +++ b/x-pack/test/functional/es_archives/ml/event_rate_nanos/mappings.json @@ -3,7 +3,7 @@ "value": { "aliases": { }, - "index": "event_rate_gen_trend_nanos", + "index": "ft_event_rate_gen_trend_nanos", "mappings": { "properties": { "@timestamp": { @@ -23,1455 +23,3 @@ } } } - -{ - "type": "index", - "value": { - "aliases": { - ".kibana": { - } - }, - "index": ".kibana_1", - "mappings": { - "_meta": { - "migrationMappingPropertyHashes": { - "action": "c0c235fba02ebd2a2412bcda79009b58", - "action_task_params": "a9d49f184ee89641044be0ca2950fa3a", - "alert": "e588043a01d3d43477e7cad7efa0f5d8", - "apm-indices": "9bb9b2bf1fa636ed8619cbab5ce6a1dd", - "apm-services-telemetry": "07ee1939fa4302c62ddc052ec03fed90", - "canvas-element": "7390014e1091044523666d97247392fc", - "canvas-workpad": "b0a1706d356228dbdcb4a17e6b9eb231", - "config": "87aca8fdb053154f11383fce3dbf3edf", - "dashboard": "d00f614b29a80360e1190193fd333bab", - "file-upload-telemetry": "0ed4d3e1983d1217a30982630897092e", - "graph-workspace": "cd7ba1330e6682e9cc00b78850874be1", - "index-pattern": "66eccb05066c5a89924f48a9e9736499", - "infrastructure-ui-source": "ddc0ecb18383f6b26101a2fadb2dab0c", - "inventory-view": "84b320fd67209906333ffce261128462", - "kql-telemetry": "d12a98a6f19a2d273696597547e064ee", - "lens": "21c3ea0763beb1ecb0162529706b88c5", - "lens-ui-telemetry": "509bfa5978586998e05f9e303c07a327", - "map": "23d7aa4a720d4938ccde3983f87bd58d", - "maps-telemetry": "268da3a48066123fc5baf35abaa55014", - "metrics-explorer-view": "53c5365793677328df0ccb6138bf3cdd", - "migrationVersion": "4a1746014a75ade3a714e1db5763276f", - "ml-telemetry": "257fd1d4b4fdbb9cb4b8a3b27da201e9", - "namespace": "2f4316de49999235636386fe51dc06c1", - "query": "11aaeb7f5f7fa5bb43f25e18ce26e7d9", - "references": "7997cf5a56cc02bdc9c93361bde732b0", - "sample-data-telemetry": "7d3cfeb915303c9641c59681967ffeb4", - "search": "181661168bbadd1eff5902361e2a0d5c", - "server": "ec97f1c5da1a19609a60874e5af1100c", - "siem-detection-engine-rule-status": "0367e4d775814b56a4bee29384f9aafe", - "siem-ui-timeline": "ac8020190f5950dd3250b6499144e7fb", - "siem-ui-timeline-note": "8874706eedc49059d4cf0f5094559084", - "siem-ui-timeline-pinned-event": "20638091112f0e14f0e443d512301c29", - "space": "c5ca8acafa0beaa4d08d014a97b6bc6b", - "telemetry": "358ffaa88ba34a97d55af0933a117de4", - "timelion-sheet": "9a2a2748877c7a7b582fef201ab1d4cf", - "tsvb-validation-telemetry": "3a37ef6c8700ae6fc97d5c7da00e9215", - "type": "2f4316de49999235636386fe51dc06c1", - "ui-metric": "0d409297dc5ebe1e3a1da691c6ee32e3", - "updated_at": "00da57df13e94e9d98437d13ace4bfe0", - "upgrade-assistant-reindex-operation": "a53a20fe086b72c9a86da3cc12dad8a6", - "upgrade-assistant-telemetry": "56702cec857e0a9dacfb696655b4ff7b", - "url": "c7f66a0df8b1b52f17c28c4adb111105", - "visualization": "52d7a13ad68a150c4525b292d23e12cc" - } - }, - "dynamic": "strict", - "properties": { - "action": { - "properties": { - "actionTypeId": { - "type": "keyword" - }, - "config": { - "enabled": false, - "type": "object" - }, - "name": { - "type": "text" - }, - "secrets": { - "type": "binary" - } - } - }, - "action_task_params": { - "properties": { - "actionId": { - "type": "keyword" - }, - "apiKey": { - "type": "binary" - }, - "params": { - "enabled": false, - "type": "object" - } - } - }, - "alert": { - "properties": { - "actions": { - "properties": { - "actionRef": { - "type": "keyword" - }, - "actionTypeId": { - "type": "keyword" - }, - "group": { - "type": "keyword" - }, - "params": { - "enabled": false, - "type": "object" - } - }, - "type": "nested" - }, - "alertTypeId": { - "type": "keyword" - }, - "apiKey": { - "type": "binary" - }, - "apiKeyOwner": { - "type": "keyword" - }, - "consumer": { - "type": "keyword" - }, - "createdAt": { - "type": "date" - }, - "createdBy": { - "type": "keyword" - }, - "enabled": { - "type": "boolean" - }, - "muteAll": { - "type": "boolean" - }, - "mutedInstanceIds": { - "type": "keyword" - }, - "name": { - "type": "text" - }, - "params": { - "enabled": false, - "type": "object" - }, - "schedule": { - "properties": { - "interval": { - "type": "keyword" - } - } - }, - "scheduledTaskId": { - "type": "keyword" - }, - "tags": { - "type": "keyword" - }, - "throttle": { - "type": "keyword" - }, - "updatedBy": { - "type": "keyword" - } - } - }, - "apm-indices": { - "properties": { - "apm_oss": { - "properties": { - "errorIndices": { - "type": "keyword" - }, - "metricsIndices": { - "type": "keyword" - }, - "onboardingIndices": { - "type": "keyword" - }, - "sourcemapIndices": { - "type": "keyword" - }, - "spanIndices": { - "type": "keyword" - }, - "transactionIndices": { - "type": "keyword" - } - } - } - } - }, - "apm-services-telemetry": { - "properties": { - "has_any_services": { - "type": "boolean" - }, - "services_per_agent": { - "properties": { - "dotnet": { - "null_value": 0, - "type": "long" - }, - "go": { - "null_value": 0, - "type": "long" - }, - "java": { - "null_value": 0, - "type": "long" - }, - "js-base": { - "null_value": 0, - "type": "long" - }, - "nodejs": { - "null_value": 0, - "type": "long" - }, - "python": { - "null_value": 0, - "type": "long" - }, - "ruby": { - "null_value": 0, - "type": "long" - }, - "rum-js": { - "null_value": 0, - "type": "long" - } - } - } - } - }, - "canvas-element": { - "dynamic": "false", - "properties": { - "@created": { - "type": "date" - }, - "@timestamp": { - "type": "date" - }, - "content": { - "type": "text" - }, - "help": { - "type": "text" - }, - "image": { - "type": "text" - }, - "name": { - "fields": { - "keyword": { - "type": "keyword" - } - }, - "type": "text" - } - } - }, - "canvas-workpad": { - "dynamic": "false", - "properties": { - "@created": { - "type": "date" - }, - "@timestamp": { - "type": "date" - }, - "name": { - "fields": { - "keyword": { - "type": "keyword" - } - }, - "type": "text" - } - } - }, - "config": { - "dynamic": "true", - "properties": { - "buildNum": { - "type": "keyword" - }, - "dateFormat:tz": { - "fields": { - "keyword": { - "ignore_above": 256, - "type": "keyword" - } - }, - "type": "text" - }, - "defaultIndex": { - "fields": { - "keyword": { - "ignore_above": 256, - "type": "keyword" - } - }, - "type": "text" - } - } - }, - "dashboard": { - "properties": { - "description": { - "type": "text" - }, - "hits": { - "type": "integer" - }, - "kibanaSavedObjectMeta": { - "properties": { - "searchSourceJSON": { - "type": "text" - } - } - }, - "optionsJSON": { - "type": "text" - }, - "panelsJSON": { - "type": "text" - }, - "refreshInterval": { - "properties": { - "display": { - "type": "keyword" - }, - "pause": { - "type": "boolean" - }, - "section": { - "type": "integer" - }, - "value": { - "type": "integer" - } - } - }, - "timeFrom": { - "type": "keyword" - }, - "timeRestore": { - "type": "boolean" - }, - "timeTo": { - "type": "keyword" - }, - "title": { - "type": "text" - }, - "version": { - "type": "integer" - } - } - }, - "file-upload-telemetry": { - "properties": { - "filesUploadedTotalCount": { - "type": "long" - } - } - }, - "graph-workspace": { - "properties": { - "description": { - "type": "text" - }, - "kibanaSavedObjectMeta": { - "properties": { - "searchSourceJSON": { - "type": "text" - } - } - }, - "numLinks": { - "type": "integer" - }, - "numVertices": { - "type": "integer" - }, - "title": { - "type": "text" - }, - "version": { - "type": "integer" - }, - "wsState": { - "type": "text" - } - } - }, - "index-pattern": { - "properties": { - "fieldFormatMap": { - "type": "text" - }, - "fields": { - "type": "text" - }, - "intervalName": { - "type": "keyword" - }, - "notExpandable": { - "type": "boolean" - }, - "sourceFilters": { - "type": "text" - }, - "timeFieldName": { - "type": "keyword" - }, - "title": { - "type": "text" - }, - "type": { - "type": "keyword" - }, - "typeMeta": { - "type": "keyword" - } - } - }, - "infrastructure-ui-source": { - "properties": { - "description": { - "type": "text" - }, - "fields": { - "properties": { - "container": { - "type": "keyword" - }, - "host": { - "type": "keyword" - }, - "pod": { - "type": "keyword" - }, - "tiebreaker": { - "type": "keyword" - }, - "timestamp": { - "type": "keyword" - } - } - }, - "logAlias": { - "type": "keyword" - }, - "logColumns": { - "properties": { - "fieldColumn": { - "properties": { - "field": { - "type": "keyword" - }, - "id": { - "type": "keyword" - } - } - }, - "messageColumn": { - "properties": { - "id": { - "type": "keyword" - } - } - }, - "timestampColumn": { - "properties": { - "id": { - "type": "keyword" - } - } - } - }, - "type": "nested" - }, - "metricAlias": { - "type": "keyword" - }, - "name": { - "type": "text" - } - } - }, - "inventory-view": { - "properties": { - "autoBounds": { - "type": "boolean" - }, - "autoReload": { - "type": "boolean" - }, - "boundsOverride": { - "properties": { - "max": { - "type": "integer" - }, - "min": { - "type": "integer" - } - } - }, - "customOptions": { - "properties": { - "field": { - "type": "keyword" - }, - "text": { - "type": "keyword" - } - }, - "type": "nested" - }, - "filterQuery": { - "properties": { - "expression": { - "type": "keyword" - }, - "kind": { - "type": "keyword" - } - } - }, - "groupBy": { - "properties": { - "field": { - "type": "keyword" - }, - "label": { - "type": "keyword" - } - }, - "type": "nested" - }, - "metric": { - "properties": { - "type": { - "type": "keyword" - } - } - }, - "name": { - "type": "keyword" - }, - "nodeType": { - "type": "keyword" - }, - "time": { - "type": "integer" - }, - "view": { - "type": "keyword" - } - } - }, - "kql-telemetry": { - "properties": { - "optInCount": { - "type": "long" - }, - "optOutCount": { - "type": "long" - } - } - }, - "lens": { - "properties": { - "expression": { - "index": false, - "type": "keyword" - }, - "state": { - "type": "flattened" - }, - "title": { - "type": "text" - }, - "visualizationType": { - "type": "keyword" - } - } - }, - "lens-ui-telemetry": { - "properties": { - "count": { - "type": "integer" - }, - "date": { - "type": "date" - }, - "name": { - "type": "keyword" - }, - "type": { - "type": "keyword" - } - } - }, - "map": { - "properties": { - "bounds": { - "type": "geo_shape" - }, - "description": { - "type": "text" - }, - "layerListJSON": { - "type": "text" - }, - "mapStateJSON": { - "type": "text" - }, - "title": { - "type": "text" - }, - "uiStateJSON": { - "type": "text" - }, - "version": { - "type": "integer" - } - } - }, - "maps-telemetry": { - "properties": { - "attributesPerMap": { - "properties": { - "dataSourcesCount": { - "properties": { - "avg": { - "type": "long" - }, - "max": { - "type": "long" - }, - "min": { - "type": "long" - } - } - }, - "emsVectorLayersCount": { - "dynamic": "true", - "type": "object" - }, - "layerTypesCount": { - "dynamic": "true", - "type": "object" - }, - "layersCount": { - "properties": { - "avg": { - "type": "long" - }, - "max": { - "type": "long" - }, - "min": { - "type": "long" - } - } - } - } - }, - "indexPatternsWithGeoFieldCount": { - "type": "long" - }, - "mapsTotalCount": { - "type": "long" - }, - "settings": { - "properties": { - "showMapVisualizationTypes": { - "type": "boolean" - } - } - }, - "timeCaptured": { - "type": "date" - } - } - }, - "metrics-explorer-view": { - "properties": { - "chartOptions": { - "properties": { - "stack": { - "type": "boolean" - }, - "type": { - "type": "keyword" - }, - "yAxisMode": { - "type": "keyword" - } - } - }, - "currentTimerange": { - "properties": { - "from": { - "type": "keyword" - }, - "interval": { - "type": "keyword" - }, - "to": { - "type": "keyword" - } - } - }, - "name": { - "type": "keyword" - }, - "options": { - "properties": { - "aggregation": { - "type": "keyword" - }, - "filterQuery": { - "type": "keyword" - }, - "groupBy": { - "type": "keyword" - }, - "limit": { - "type": "integer" - }, - "metrics": { - "properties": { - "aggregation": { - "type": "keyword" - }, - "color": { - "type": "keyword" - }, - "field": { - "type": "keyword" - }, - "label": { - "type": "keyword" - } - }, - "type": "nested" - } - } - } - } - }, - "migrationVersion": { - "dynamic": "true", - "properties": { - "index-pattern": { - "fields": { - "keyword": { - "ignore_above": 256, - "type": "keyword" - } - }, - "type": "text" - }, - "space": { - "fields": { - "keyword": { - "ignore_above": 256, - "type": "keyword" - } - }, - "type": "text" - } - } - }, - "ml-telemetry": { - "properties": { - "file_data_visualizer": { - "properties": { - "index_creation_count": { - "type": "long" - } - } - } - } - }, - "namespace": { - "type": "keyword" - }, - "query": { - "properties": { - "description": { - "type": "text" - }, - "filters": { - "enabled": false, - "type": "object" - }, - "query": { - "properties": { - "language": { - "type": "keyword" - }, - "query": { - "index": false, - "type": "keyword" - } - } - }, - "timefilter": { - "enabled": false, - "type": "object" - }, - "title": { - "type": "text" - } - } - }, - "references": { - "properties": { - "id": { - "type": "keyword" - }, - "name": { - "type": "keyword" - }, - "type": { - "type": "keyword" - } - }, - "type": "nested" - }, - "sample-data-telemetry": { - "properties": { - "installCount": { - "type": "long" - }, - "unInstallCount": { - "type": "long" - } - } - }, - "search": { - "properties": { - "columns": { - "type": "keyword" - }, - "description": { - "type": "text" - }, - "hits": { - "type": "integer" - }, - "kibanaSavedObjectMeta": { - "properties": { - "searchSourceJSON": { - "type": "text" - } - } - }, - "sort": { - "type": "keyword" - }, - "title": { - "type": "text" - }, - "version": { - "type": "integer" - } - } - }, - "server": { - "properties": { - "uuid": { - "type": "keyword" - } - } - }, - "siem-detection-engine-rule-status": { - "properties": { - "alertId": { - "type": "keyword" - }, - "lastFailureAt": { - "type": "date" - }, - "lastFailureMessage": { - "type": "text" - }, - "lastSuccessAt": { - "type": "date" - }, - "lastSuccessMessage": { - "type": "text" - }, - "status": { - "type": "keyword" - }, - "statusDate": { - "type": "date" - } - } - }, - "siem-ui-timeline": { - "properties": { - "columns": { - "properties": { - "aggregatable": { - "type": "boolean" - }, - "category": { - "type": "keyword" - }, - "columnHeaderType": { - "type": "keyword" - }, - "description": { - "type": "text" - }, - "example": { - "type": "text" - }, - "id": { - "type": "keyword" - }, - "indexes": { - "type": "keyword" - }, - "name": { - "type": "text" - }, - "placeholder": { - "type": "text" - }, - "searchable": { - "type": "boolean" - }, - "type": { - "type": "keyword" - } - } - }, - "created": { - "type": "date" - }, - "createdBy": { - "type": "text" - }, - "dataProviders": { - "properties": { - "and": { - "properties": { - "enabled": { - "type": "boolean" - }, - "excluded": { - "type": "boolean" - }, - "id": { - "type": "keyword" - }, - "kqlQuery": { - "type": "text" - }, - "name": { - "type": "text" - }, - "queryMatch": { - "properties": { - "displayField": { - "type": "text" - }, - "displayValue": { - "type": "text" - }, - "field": { - "type": "text" - }, - "operator": { - "type": "text" - }, - "value": { - "type": "text" - } - } - } - } - }, - "enabled": { - "type": "boolean" - }, - "excluded": { - "type": "boolean" - }, - "id": { - "type": "keyword" - }, - "kqlQuery": { - "type": "text" - }, - "name": { - "type": "text" - }, - "queryMatch": { - "properties": { - "displayField": { - "type": "text" - }, - "displayValue": { - "type": "text" - }, - "field": { - "type": "text" - }, - "operator": { - "type": "text" - }, - "value": { - "type": "text" - } - } - } - } - }, - "dateRange": { - "properties": { - "end": { - "type": "date" - }, - "start": { - "type": "date" - } - } - }, - "description": { - "type": "text" - }, - "eventType": { - "type": "keyword" - }, - "favorite": { - "properties": { - "favoriteDate": { - "type": "date" - }, - "fullName": { - "type": "text" - }, - "keySearch": { - "type": "text" - }, - "userName": { - "type": "text" - } - } - }, - "filters": { - "properties": { - "exists": { - "type": "text" - }, - "match_all": { - "type": "text" - }, - "meta": { - "properties": { - "alias": { - "type": "text" - }, - "controlledBy": { - "type": "text" - }, - "disabled": { - "type": "boolean" - }, - "field": { - "type": "text" - }, - "formattedValue": { - "type": "text" - }, - "index": { - "type": "keyword" - }, - "key": { - "type": "keyword" - }, - "negate": { - "type": "boolean" - }, - "params": { - "type": "text" - }, - "type": { - "type": "keyword" - }, - "value": { - "type": "text" - } - } - }, - "missing": { - "type": "text" - }, - "query": { - "type": "text" - }, - "range": { - "type": "text" - }, - "script": { - "type": "text" - } - } - }, - "kqlMode": { - "type": "keyword" - }, - "kqlQuery": { - "properties": { - "filterQuery": { - "properties": { - "kuery": { - "properties": { - "expression": { - "type": "text" - }, - "kind": { - "type": "keyword" - } - } - }, - "serializedQuery": { - "type": "text" - } - } - } - } - }, - "savedQueryId": { - "type": "keyword" - }, - "sort": { - "properties": { - "columnId": { - "type": "keyword" - }, - "sortDirection": { - "type": "keyword" - } - } - }, - "title": { - "type": "text" - }, - "updated": { - "type": "date" - }, - "updatedBy": { - "type": "text" - } - } - }, - "siem-ui-timeline-note": { - "properties": { - "created": { - "type": "date" - }, - "createdBy": { - "type": "text" - }, - "eventId": { - "type": "keyword" - }, - "note": { - "type": "text" - }, - "timelineId": { - "type": "keyword" - }, - "updated": { - "type": "date" - }, - "updatedBy": { - "type": "text" - } - } - }, - "siem-ui-timeline-pinned-event": { - "properties": { - "created": { - "type": "date" - }, - "createdBy": { - "type": "text" - }, - "eventId": { - "type": "keyword" - }, - "timelineId": { - "type": "keyword" - }, - "updated": { - "type": "date" - }, - "updatedBy": { - "type": "text" - } - } - }, - "space": { - "properties": { - "_reserved": { - "type": "boolean" - }, - "color": { - "type": "keyword" - }, - "description": { - "type": "text" - }, - "disabledFeatures": { - "type": "keyword" - }, - "imageUrl": { - "index": false, - "type": "text" - }, - "initials": { - "type": "keyword" - }, - "name": { - "fields": { - "keyword": { - "ignore_above": 2048, - "type": "keyword" - } - }, - "type": "text" - } - } - }, - "telemetry": { - "properties": { - "enabled": { - "type": "boolean" - }, - "lastReported": { - "type": "date" - }, - "lastVersionChecked": { - "ignore_above": 256, - "type": "keyword" - }, - "sendUsageFrom": { - "ignore_above": 256, - "type": "keyword" - }, - "userHasSeenNotice": { - "type": "boolean" - } - } - }, - "timelion-sheet": { - "properties": { - "description": { - "type": "text" - }, - "hits": { - "type": "integer" - }, - "kibanaSavedObjectMeta": { - "properties": { - "searchSourceJSON": { - "type": "text" - } - } - }, - "timelion_chart_height": { - "type": "integer" - }, - "timelion_columns": { - "type": "integer" - }, - "timelion_interval": { - "type": "keyword" - }, - "timelion_other_interval": { - "type": "keyword" - }, - "timelion_rows": { - "type": "integer" - }, - "timelion_sheet": { - "type": "text" - }, - "title": { - "type": "text" - }, - "version": { - "type": "integer" - } - } - }, - "tsvb-validation-telemetry": { - "properties": { - "failedRequests": { - "type": "long" - } - } - }, - "type": { - "type": "keyword" - }, - "ui-metric": { - "properties": { - "count": { - "type": "integer" - } - } - }, - "updated_at": { - "type": "date" - }, - "upgrade-assistant-reindex-operation": { - "dynamic": "true", - "properties": { - "indexName": { - "type": "keyword" - }, - "status": { - "type": "integer" - } - } - }, - "upgrade-assistant-telemetry": { - "properties": { - "features": { - "properties": { - "deprecation_logging": { - "properties": { - "enabled": { - "null_value": true, - "type": "boolean" - } - } - } - } - }, - "ui_open": { - "properties": { - "cluster": { - "null_value": 0, - "type": "long" - }, - "indices": { - "null_value": 0, - "type": "long" - }, - "overview": { - "null_value": 0, - "type": "long" - } - } - }, - "ui_reindex": { - "properties": { - "close": { - "null_value": 0, - "type": "long" - }, - "open": { - "null_value": 0, - "type": "long" - }, - "start": { - "null_value": 0, - "type": "long" - }, - "stop": { - "null_value": 0, - "type": "long" - } - } - } - } - }, - "url": { - "properties": { - "accessCount": { - "type": "long" - }, - "accessDate": { - "type": "date" - }, - "createDate": { - "type": "date" - }, - "url": { - "fields": { - "keyword": { - "ignore_above": 2048, - "type": "keyword" - } - }, - "type": "text" - } - } - }, - "visualization": { - "properties": { - "description": { - "type": "text" - }, - "kibanaSavedObjectMeta": { - "properties": { - "searchSourceJSON": { - "type": "text" - } - } - }, - "savedSearchRefName": { - "type": "keyword" - }, - "title": { - "type": "text" - }, - "uiStateJSON": { - "type": "text" - }, - "version": { - "type": "integer" - }, - "visState": { - "type": "text" - } - } - } - } - }, - "settings": { - "index": { - "auto_expand_replicas": "0-1", - "number_of_replicas": "0", - "number_of_shards": "1" - } - } - } -} \ No newline at end of file diff --git a/x-pack/test/functional/es_archives/ml/farequote/data.json.gz b/x-pack/test/functional/es_archives/ml/farequote/data.json.gz index 17cf75a4624f7..7b56c3de6954b 100644 Binary files a/x-pack/test/functional/es_archives/ml/farequote/data.json.gz and b/x-pack/test/functional/es_archives/ml/farequote/data.json.gz differ diff --git a/x-pack/test/functional/es_archives/ml/farequote/mappings.json b/x-pack/test/functional/es_archives/ml/farequote/mappings.json index b00545c015a74..1252967ac9679 100644 --- a/x-pack/test/functional/es_archives/ml/farequote/mappings.json +++ b/x-pack/test/functional/es_archives/ml/farequote/mappings.json @@ -3,7 +3,7 @@ "value": { "aliases": { }, - "index": "farequote", + "index": "ft_farequote", "mappings": { "properties": { "@timestamp": { @@ -46,1042 +46,3 @@ } } } - -{ - "type": "index", - "value": { - "aliases": { - ".kibana": { - } - }, - "index": ".kibana_1", - "mappings": { - "_meta": { - "migrationMappingPropertyHashes": { - "action": "ecc01e367a369542bc2b15dae1fb1773", - "alert": "66fb7d877c9f102755357c30c7b98e02", - "apm-telemetry": "07ee1939fa4302c62ddc052ec03fed90", - "canvas-element": "7390014e1091044523666d97247392fc", - "canvas-workpad": "b0a1706d356228dbdcb4a17e6b9eb231", - "config": "87aca8fdb053154f11383fce3dbf3edf", - "dashboard": "d00f614b29a80360e1190193fd333bab", - "file-upload-telemetry": "0ed4d3e1983d1217a30982630897092e", - "graph-workspace": "cd7ba1330e6682e9cc00b78850874be1", - "index-pattern": "66eccb05066c5a89924f48a9e9736499", - "infrastructure-ui-source": "ddc0ecb18383f6b26101a2fadb2dab0c", - "kql-telemetry": "d12a98a6f19a2d273696597547e064ee", - "map": "23d7aa4a720d4938ccde3983f87bd58d", - "maps-telemetry": "a4229f8b16a6820c6d724b7e0c1f729d", - "migrationVersion": "4a1746014a75ade3a714e1db5763276f", - "ml-telemetry": "257fd1d4b4fdbb9cb4b8a3b27da201e9", - "namespace": "2f4316de49999235636386fe51dc06c1", - "references": "7997cf5a56cc02bdc9c93361bde732b0", - "sample-data-telemetry": "7d3cfeb915303c9641c59681967ffeb4", - "search": "181661168bbadd1eff5902361e2a0d5c", - "server": "ec97f1c5da1a19609a60874e5af1100c", - "siem-ui-timeline": "1f6f0860ad7bc0dba3e42467ca40470d", - "siem-ui-timeline-note": "8874706eedc49059d4cf0f5094559084", - "siem-ui-timeline-pinned-event": "20638091112f0e14f0e443d512301c29", - "space": "25de8c2deec044392922989cfcf24c54", - "telemetry": "e1c8bc94e443aefd9458932cc0697a4d", - "timelion-sheet": "9a2a2748877c7a7b582fef201ab1d4cf", - "type": "2f4316de49999235636386fe51dc06c1", - "ui-metric": "0d409297dc5ebe1e3a1da691c6ee32e3", - "updated_at": "00da57df13e94e9d98437d13ace4bfe0", - "upgrade-assistant-reindex-operation": "a53a20fe086b72c9a86da3cc12dad8a6", - "upgrade-assistant-telemetry": "56702cec857e0a9dacfb696655b4ff7b", - "url": "c7f66a0df8b1b52f17c28c4adb111105", - "visualization": "52d7a13ad68a150c4525b292d23e12cc" - } - }, - "dynamic": "strict", - "properties": { - "action": { - "properties": { - "actionTypeId": { - "type": "keyword" - }, - "config": { - "enabled": false, - "type": "object" - }, - "description": { - "type": "text" - }, - "secrets": { - "type": "binary" - } - } - }, - "alert": { - "properties": { - "actions": { - "properties": { - "actionRef": { - "type": "keyword" - }, - "group": { - "type": "keyword" - }, - "params": { - "enabled": false, - "type": "object" - } - }, - "type": "nested" - }, - "alertTypeId": { - "type": "keyword" - }, - "params": { - "enabled": false, - "type": "object" - }, - "enabled": { - "type": "boolean" - }, - "interval": { - "type": "keyword" - }, - "scheduledTaskId": { - "type": "keyword" - } - } - }, - "apm-telemetry": { - "properties": { - "has_any_services": { - "type": "boolean" - }, - "services_per_agent": { - "properties": { - "dotnet": { - "null_value": 0, - "type": "long" - }, - "go": { - "null_value": 0, - "type": "long" - }, - "java": { - "null_value": 0, - "type": "long" - }, - "js-base": { - "null_value": 0, - "type": "long" - }, - "nodejs": { - "null_value": 0, - "type": "long" - }, - "python": { - "null_value": 0, - "type": "long" - }, - "ruby": { - "null_value": 0, - "type": "long" - }, - "rum-js": { - "null_value": 0, - "type": "long" - } - } - } - } - }, - "canvas-element": { - "dynamic": "false", - "properties": { - "@created": { - "type": "date" - }, - "@timestamp": { - "type": "date" - }, - "content": { - "type": "text" - }, - "help": { - "type": "text" - }, - "image": { - "type": "text" - }, - "name": { - "fields": { - "keyword": { - "type": "keyword" - } - }, - "type": "text" - } - } - }, - "canvas-workpad": { - "dynamic": "false", - "properties": { - "@created": { - "type": "date" - }, - "@timestamp": { - "type": "date" - }, - "name": { - "fields": { - "keyword": { - "type": "keyword" - } - }, - "type": "text" - } - } - }, - "config": { - "dynamic": "true", - "properties": { - "buildNum": { - "type": "keyword" - }, - "dateFormat:tz": { - "fields": { - "keyword": { - "ignore_above": 256, - "type": "keyword" - } - }, - "type": "text" - }, - "defaultIndex": { - "fields": { - "keyword": { - "ignore_above": 256, - "type": "keyword" - } - }, - "type": "text" - } - } - }, - "dashboard": { - "properties": { - "description": { - "type": "text" - }, - "hits": { - "type": "integer" - }, - "kibanaSavedObjectMeta": { - "properties": { - "searchSourceJSON": { - "type": "text" - } - } - }, - "optionsJSON": { - "type": "text" - }, - "panelsJSON": { - "type": "text" - }, - "refreshInterval": { - "properties": { - "display": { - "type": "keyword" - }, - "pause": { - "type": "boolean" - }, - "section": { - "type": "integer" - }, - "value": { - "type": "integer" - } - } - }, - "timeFrom": { - "type": "keyword" - }, - "timeRestore": { - "type": "boolean" - }, - "timeTo": { - "type": "keyword" - }, - "title": { - "type": "text" - }, - "version": { - "type": "integer" - } - } - }, - "file-upload-telemetry": { - "properties": { - "filesUploadedTotalCount": { - "type": "long" - } - } - }, - "graph-workspace": { - "properties": { - "description": { - "type": "text" - }, - "kibanaSavedObjectMeta": { - "properties": { - "searchSourceJSON": { - "type": "text" - } - } - }, - "numLinks": { - "type": "integer" - }, - "numVertices": { - "type": "integer" - }, - "title": { - "type": "text" - }, - "version": { - "type": "integer" - }, - "wsState": { - "type": "text" - } - } - }, - "index-pattern": { - "properties": { - "fieldFormatMap": { - "type": "text" - }, - "fields": { - "type": "text" - }, - "intervalName": { - "type": "keyword" - }, - "notExpandable": { - "type": "boolean" - }, - "sourceFilters": { - "type": "text" - }, - "timeFieldName": { - "type": "keyword" - }, - "title": { - "type": "text" - }, - "type": { - "type": "keyword" - }, - "typeMeta": { - "type": "keyword" - } - } - }, - "infrastructure-ui-source": { - "properties": { - "description": { - "type": "text" - }, - "fields": { - "properties": { - "container": { - "type": "keyword" - }, - "host": { - "type": "keyword" - }, - "pod": { - "type": "keyword" - }, - "tiebreaker": { - "type": "keyword" - }, - "timestamp": { - "type": "keyword" - } - } - }, - "logAlias": { - "type": "keyword" - }, - "logColumns": { - "properties": { - "fieldColumn": { - "properties": { - "field": { - "type": "keyword" - }, - "id": { - "type": "keyword" - } - } - }, - "messageColumn": { - "properties": { - "id": { - "type": "keyword" - } - } - }, - "timestampColumn": { - "properties": { - "id": { - "type": "keyword" - } - } - } - }, - "type": "nested" - }, - "metricAlias": { - "type": "keyword" - }, - "name": { - "type": "text" - } - } - }, - "kql-telemetry": { - "properties": { - "optInCount": { - "type": "long" - }, - "optOutCount": { - "type": "long" - } - } - }, - "map": { - "properties": { - "bounds": { - "type": "geo_shape" - }, - "description": { - "type": "text" - }, - "layerListJSON": { - "type": "text" - }, - "mapStateJSON": { - "type": "text" - }, - "title": { - "type": "text" - }, - "uiStateJSON": { - "type": "text" - }, - "version": { - "type": "integer" - } - } - }, - "maps-telemetry": { - "properties": { - "attributesPerMap": { - "properties": { - "dataSourcesCount": { - "properties": { - "avg": { - "type": "long" - }, - "max": { - "type": "long" - }, - "min": { - "type": "long" - } - } - }, - "emsVectorLayersCount": { - "dynamic": "true", - "type": "object" - }, - "layerTypesCount": { - "dynamic": "true", - "type": "object" - }, - "layersCount": { - "properties": { - "avg": { - "type": "long" - }, - "max": { - "type": "long" - }, - "min": { - "type": "long" - } - } - } - } - }, - "mapsTotalCount": { - "type": "long" - }, - "timeCaptured": { - "type": "date" - } - } - }, - "migrationVersion": { - "dynamic": "true", - "properties": { - "index-pattern": { - "fields": { - "keyword": { - "ignore_above": 256, - "type": "keyword" - } - }, - "type": "text" - }, - "space": { - "fields": { - "keyword": { - "ignore_above": 256, - "type": "keyword" - } - }, - "type": "text" - } - } - }, - "ml-telemetry": { - "properties": { - "file_data_visualizer": { - "properties": { - "index_creation_count": { - "type": "long" - } - } - } - } - }, - "namespace": { - "type": "keyword" - }, - "references": { - "properties": { - "id": { - "type": "keyword" - }, - "name": { - "type": "keyword" - }, - "type": { - "type": "keyword" - } - }, - "type": "nested" - }, - "sample-data-telemetry": { - "properties": { - "installCount": { - "type": "long" - }, - "unInstallCount": { - "type": "long" - } - } - }, - "search": { - "properties": { - "columns": { - "type": "keyword" - }, - "description": { - "type": "text" - }, - "hits": { - "type": "integer" - }, - "kibanaSavedObjectMeta": { - "properties": { - "searchSourceJSON": { - "type": "text" - } - } - }, - "sort": { - "type": "keyword" - }, - "title": { - "type": "text" - }, - "version": { - "type": "integer" - } - } - }, - "server": { - "properties": { - "uuid": { - "type": "keyword" - } - } - }, - "siem-ui-timeline": { - "properties": { - "columns": { - "properties": { - "aggregatable": { - "type": "boolean" - }, - "category": { - "type": "keyword" - }, - "columnHeaderType": { - "type": "keyword" - }, - "description": { - "type": "text" - }, - "example": { - "type": "text" - }, - "id": { - "type": "keyword" - }, - "indexes": { - "type": "keyword" - }, - "name": { - "type": "text" - }, - "placeholder": { - "type": "text" - }, - "searchable": { - "type": "boolean" - }, - "type": { - "type": "keyword" - } - } - }, - "created": { - "type": "date" - }, - "createdBy": { - "type": "text" - }, - "dataProviders": { - "properties": { - "and": { - "properties": { - "enabled": { - "type": "boolean" - }, - "excluded": { - "type": "boolean" - }, - "id": { - "type": "keyword" - }, - "kqlQuery": { - "type": "text" - }, - "name": { - "type": "text" - }, - "queryMatch": { - "properties": { - "displayField": { - "type": "text" - }, - "displayValue": { - "type": "text" - }, - "field": { - "type": "text" - }, - "operator": { - "type": "text" - }, - "value": { - "type": "text" - } - } - } - } - }, - "enabled": { - "type": "boolean" - }, - "excluded": { - "type": "boolean" - }, - "id": { - "type": "keyword" - }, - "kqlQuery": { - "type": "text" - }, - "name": { - "type": "text" - }, - "queryMatch": { - "properties": { - "displayField": { - "type": "text" - }, - "displayValue": { - "type": "text" - }, - "field": { - "type": "text" - }, - "operator": { - "type": "text" - }, - "value": { - "type": "text" - } - } - } - } - }, - "dateRange": { - "properties": { - "end": { - "type": "date" - }, - "start": { - "type": "date" - } - } - }, - "description": { - "type": "text" - }, - "favorite": { - "properties": { - "favoriteDate": { - "type": "date" - }, - "fullName": { - "type": "text" - }, - "keySearch": { - "type": "text" - }, - "userName": { - "type": "text" - } - } - }, - "kqlMode": { - "type": "keyword" - }, - "kqlQuery": { - "properties": { - "filterQuery": { - "properties": { - "kuery": { - "properties": { - "expression": { - "type": "text" - }, - "kind": { - "type": "keyword" - } - } - }, - "serializedQuery": { - "type": "text" - } - } - } - } - }, - "sort": { - "properties": { - "columnId": { - "type": "keyword" - }, - "sortDirection": { - "type": "keyword" - } - } - }, - "title": { - "type": "text" - }, - "updated": { - "type": "date" - }, - "updatedBy": { - "type": "text" - } - } - }, - "siem-ui-timeline-note": { - "properties": { - "created": { - "type": "date" - }, - "createdBy": { - "type": "text" - }, - "eventId": { - "type": "keyword" - }, - "note": { - "type": "text" - }, - "timelineId": { - "type": "keyword" - }, - "updated": { - "type": "date" - }, - "updatedBy": { - "type": "text" - } - } - }, - "siem-ui-timeline-pinned-event": { - "properties": { - "created": { - "type": "date" - }, - "createdBy": { - "type": "text" - }, - "eventId": { - "type": "keyword" - }, - "timelineId": { - "type": "keyword" - }, - "updated": { - "type": "date" - }, - "updatedBy": { - "type": "text" - } - } - }, - "space": { - "properties": { - "_reserved": { - "type": "boolean" - }, - "color": { - "type": "keyword" - }, - "description": { - "type": "text" - }, - "disabledFeatures": { - "type": "keyword" - }, - "initials": { - "type": "keyword" - }, - "name": { - "fields": { - "keyword": { - "ignore_above": 2048, - "type": "keyword" - } - }, - "type": "text" - } - } - }, - "telemetry": { - "properties": { - "enabled": { - "type": "boolean" - } - } - }, - "timelion-sheet": { - "properties": { - "description": { - "type": "text" - }, - "hits": { - "type": "integer" - }, - "kibanaSavedObjectMeta": { - "properties": { - "searchSourceJSON": { - "type": "text" - } - } - }, - "timelion_chart_height": { - "type": "integer" - }, - "timelion_columns": { - "type": "integer" - }, - "timelion_interval": { - "type": "keyword" - }, - "timelion_other_interval": { - "type": "keyword" - }, - "timelion_rows": { - "type": "integer" - }, - "timelion_sheet": { - "type": "text" - }, - "title": { - "type": "text" - }, - "version": { - "type": "integer" - } - } - }, - "type": { - "type": "keyword" - }, - "ui-metric": { - "properties": { - "count": { - "type": "integer" - } - } - }, - "updated_at": { - "type": "date" - }, - "upgrade-assistant-reindex-operation": { - "dynamic": "true", - "properties": { - "indexName": { - "type": "keyword" - }, - "status": { - "type": "integer" - } - } - }, - "upgrade-assistant-telemetry": { - "properties": { - "features": { - "properties": { - "deprecation_logging": { - "properties": { - "enabled": { - "null_value": true, - "type": "boolean" - } - } - } - } - }, - "ui_open": { - "properties": { - "cluster": { - "null_value": 0, - "type": "long" - }, - "indices": { - "null_value": 0, - "type": "long" - }, - "overview": { - "null_value": 0, - "type": "long" - } - } - }, - "ui_reindex": { - "properties": { - "close": { - "null_value": 0, - "type": "long" - }, - "open": { - "null_value": 0, - "type": "long" - }, - "start": { - "null_value": 0, - "type": "long" - }, - "stop": { - "null_value": 0, - "type": "long" - } - } - } - } - }, - "url": { - "properties": { - "accessCount": { - "type": "long" - }, - "accessDate": { - "type": "date" - }, - "createDate": { - "type": "date" - }, - "url": { - "fields": { - "keyword": { - "ignore_above": 2048, - "type": "keyword" - } - }, - "type": "text" - } - } - }, - "visualization": { - "properties": { - "description": { - "type": "text" - }, - "kibanaSavedObjectMeta": { - "properties": { - "searchSourceJSON": { - "type": "text" - } - } - }, - "savedSearchRefName": { - "type": "keyword" - }, - "title": { - "type": "text" - }, - "uiStateJSON": { - "type": "text" - }, - "version": { - "type": "integer" - }, - "visState": { - "type": "text" - } - } - } - } - }, - "settings": { - "index": { - "auto_expand_replicas": "0-1", - "number_of_replicas": "0", - "number_of_shards": "1" - } - } - } -} diff --git a/x-pack/test/functional/es_archives/ml/ihp_outlier/data.json.gz b/x-pack/test/functional/es_archives/ml/ihp_outlier/data.json.gz index de0d2d6dd4ccc..34637dfd812d1 100644 Binary files a/x-pack/test/functional/es_archives/ml/ihp_outlier/data.json.gz and b/x-pack/test/functional/es_archives/ml/ihp_outlier/data.json.gz differ diff --git a/x-pack/test/functional/es_archives/ml/ihp_outlier/mappings.json b/x-pack/test/functional/es_archives/ml/ihp_outlier/mappings.json index f90c6e67daac4..a20b1de81010f 100644 --- a/x-pack/test/functional/es_archives/ml/ihp_outlier/mappings.json +++ b/x-pack/test/functional/es_archives/ml/ihp_outlier/mappings.json @@ -3,7 +3,7 @@ "value": { "aliases": { }, - "index": "ihp_outlier", + "index": "ft_ihp_outlier", "mappings": { "properties": { "1stFlrSF": { @@ -109,1395 +109,3 @@ } } } - -{ - "type": "index", - "value": { - "aliases": { - ".kibana": { - } - }, - "index": ".kibana_1", - "mappings": { - "_meta": { - "migrationMappingPropertyHashes": { - "action": "ecc01e367a369542bc2b15dae1fb1773", - "action_task_params": "a9d49f184ee89641044be0ca2950fa3a", - "alert": "3cdf52bff6f482e53b825b45686604db", - "apm-indices": "9bb9b2bf1fa636ed8619cbab5ce6a1dd", - "apm-services-telemetry": "07ee1939fa4302c62ddc052ec03fed90", - "canvas-element": "7390014e1091044523666d97247392fc", - "canvas-workpad": "b0a1706d356228dbdcb4a17e6b9eb231", - "config": "87aca8fdb053154f11383fce3dbf3edf", - "dashboard": "d00f614b29a80360e1190193fd333bab", - "file-upload-telemetry": "0ed4d3e1983d1217a30982630897092e", - "graph-workspace": "cd7ba1330e6682e9cc00b78850874be1", - "index-pattern": "66eccb05066c5a89924f48a9e9736499", - "infrastructure-ui-source": "ddc0ecb18383f6b26101a2fadb2dab0c", - "inventory-view": "84b320fd67209906333ffce261128462", - "kql-telemetry": "d12a98a6f19a2d273696597547e064ee", - "lens": "21c3ea0763beb1ecb0162529706b88c5", - "lens-ui-telemetry": "509bfa5978586998e05f9e303c07a327", - "map": "23d7aa4a720d4938ccde3983f87bd58d", - "maps-telemetry": "a4229f8b16a6820c6d724b7e0c1f729d", - "metrics-explorer-view": "53c5365793677328df0ccb6138bf3cdd", - "migrationVersion": "4a1746014a75ade3a714e1db5763276f", - "ml-telemetry": "257fd1d4b4fdbb9cb4b8a3b27da201e9", - "namespace": "2f4316de49999235636386fe51dc06c1", - "query": "11aaeb7f5f7fa5bb43f25e18ce26e7d9", - "references": "7997cf5a56cc02bdc9c93361bde732b0", - "sample-data-telemetry": "7d3cfeb915303c9641c59681967ffeb4", - "search": "181661168bbadd1eff5902361e2a0d5c", - "server": "ec97f1c5da1a19609a60874e5af1100c", - "siem-ui-timeline": "6485ab095be8d15246667b98a1a34295", - "siem-ui-timeline-note": "8874706eedc49059d4cf0f5094559084", - "siem-ui-timeline-pinned-event": "20638091112f0e14f0e443d512301c29", - "space": "c5ca8acafa0beaa4d08d014a97b6bc6b", - "telemetry": "358ffaa88ba34a97d55af0933a117de4", - "timelion-sheet": "9a2a2748877c7a7b582fef201ab1d4cf", - "type": "2f4316de49999235636386fe51dc06c1", - "ui-metric": "0d409297dc5ebe1e3a1da691c6ee32e3", - "updated_at": "00da57df13e94e9d98437d13ace4bfe0", - "upgrade-assistant-reindex-operation": "a53a20fe086b72c9a86da3cc12dad8a6", - "upgrade-assistant-telemetry": "56702cec857e0a9dacfb696655b4ff7b", - "url": "c7f66a0df8b1b52f17c28c4adb111105", - "visualization": "52d7a13ad68a150c4525b292d23e12cc" - } - }, - "dynamic": "strict", - "properties": { - "action": { - "properties": { - "actionTypeId": { - "type": "keyword" - }, - "config": { - "enabled": false, - "type": "object" - }, - "description": { - "type": "text" - }, - "secrets": { - "type": "binary" - } - } - }, - "action_task_params": { - "properties": { - "actionId": { - "type": "keyword" - }, - "apiKey": { - "type": "binary" - }, - "params": { - "enabled": false, - "type": "object" - } - } - }, - "alert": { - "properties": { - "actions": { - "properties": { - "actionRef": { - "type": "keyword" - }, - "group": { - "type": "keyword" - }, - "params": { - "enabled": false, - "type": "object" - } - }, - "type": "nested" - }, - "alertTypeId": { - "type": "keyword" - }, - "apiKey": { - "type": "binary" - }, - "apiKeyOwner": { - "type": "keyword" - }, - "createdBy": { - "type": "keyword" - }, - "enabled": { - "type": "boolean" - }, - "interval": { - "type": "keyword" - }, - "muteAll": { - "type": "boolean" - }, - "mutedInstanceIds": { - "type": "keyword" - }, - "name": { - "type": "text" - }, - "params": { - "enabled": false, - "type": "object" - }, - "scheduledTaskId": { - "type": "keyword" - }, - "tags": { - "type": "keyword" - }, - "throttle": { - "type": "keyword" - }, - "updatedBy": { - "type": "keyword" - } - } - }, - "apm-indices": { - "properties": { - "apm_oss": { - "properties": { - "errorIndices": { - "type": "keyword" - }, - "metricsIndices": { - "type": "keyword" - }, - "onboardingIndices": { - "type": "keyword" - }, - "sourcemapIndices": { - "type": "keyword" - }, - "spanIndices": { - "type": "keyword" - }, - "transactionIndices": { - "type": "keyword" - } - } - } - } - }, - "apm-services-telemetry": { - "properties": { - "has_any_services": { - "type": "boolean" - }, - "services_per_agent": { - "properties": { - "dotnet": { - "null_value": 0, - "type": "long" - }, - "go": { - "null_value": 0, - "type": "long" - }, - "java": { - "null_value": 0, - "type": "long" - }, - "js-base": { - "null_value": 0, - "type": "long" - }, - "nodejs": { - "null_value": 0, - "type": "long" - }, - "python": { - "null_value": 0, - "type": "long" - }, - "ruby": { - "null_value": 0, - "type": "long" - }, - "rum-js": { - "null_value": 0, - "type": "long" - } - } - } - } - }, - "canvas-element": { - "dynamic": "false", - "properties": { - "@created": { - "type": "date" - }, - "@timestamp": { - "type": "date" - }, - "content": { - "type": "text" - }, - "help": { - "type": "text" - }, - "image": { - "type": "text" - }, - "name": { - "fields": { - "keyword": { - "type": "keyword" - } - }, - "type": "text" - } - } - }, - "canvas-workpad": { - "dynamic": "false", - "properties": { - "@created": { - "type": "date" - }, - "@timestamp": { - "type": "date" - }, - "name": { - "fields": { - "keyword": { - "type": "keyword" - } - }, - "type": "text" - } - } - }, - "config": { - "dynamic": "true", - "properties": { - "buildNum": { - "type": "keyword" - }, - "dateFormat:tz": { - "fields": { - "keyword": { - "ignore_above": 256, - "type": "keyword" - } - }, - "type": "text" - }, - "defaultIndex": { - "fields": { - "keyword": { - "ignore_above": 256, - "type": "keyword" - } - }, - "type": "text" - } - } - }, - "dashboard": { - "properties": { - "description": { - "type": "text" - }, - "hits": { - "type": "integer" - }, - "kibanaSavedObjectMeta": { - "properties": { - "searchSourceJSON": { - "type": "text" - } - } - }, - "optionsJSON": { - "type": "text" - }, - "panelsJSON": { - "type": "text" - }, - "refreshInterval": { - "properties": { - "display": { - "type": "keyword" - }, - "pause": { - "type": "boolean" - }, - "section": { - "type": "integer" - }, - "value": { - "type": "integer" - } - } - }, - "timeFrom": { - "type": "keyword" - }, - "timeRestore": { - "type": "boolean" - }, - "timeTo": { - "type": "keyword" - }, - "title": { - "type": "text" - }, - "version": { - "type": "integer" - } - } - }, - "file-upload-telemetry": { - "properties": { - "filesUploadedTotalCount": { - "type": "long" - } - } - }, - "graph-workspace": { - "properties": { - "description": { - "type": "text" - }, - "kibanaSavedObjectMeta": { - "properties": { - "searchSourceJSON": { - "type": "text" - } - } - }, - "numLinks": { - "type": "integer" - }, - "numVertices": { - "type": "integer" - }, - "title": { - "type": "text" - }, - "version": { - "type": "integer" - }, - "wsState": { - "type": "text" - } - } - }, - "index-pattern": { - "properties": { - "fieldFormatMap": { - "type": "text" - }, - "fields": { - "type": "text" - }, - "intervalName": { - "type": "keyword" - }, - "notExpandable": { - "type": "boolean" - }, - "sourceFilters": { - "type": "text" - }, - "timeFieldName": { - "type": "keyword" - }, - "title": { - "type": "text" - }, - "type": { - "type": "keyword" - }, - "typeMeta": { - "type": "keyword" - } - } - }, - "infrastructure-ui-source": { - "properties": { - "description": { - "type": "text" - }, - "fields": { - "properties": { - "container": { - "type": "keyword" - }, - "host": { - "type": "keyword" - }, - "pod": { - "type": "keyword" - }, - "tiebreaker": { - "type": "keyword" - }, - "timestamp": { - "type": "keyword" - } - } - }, - "logAlias": { - "type": "keyword" - }, - "logColumns": { - "properties": { - "fieldColumn": { - "properties": { - "field": { - "type": "keyword" - }, - "id": { - "type": "keyword" - } - } - }, - "messageColumn": { - "properties": { - "id": { - "type": "keyword" - } - } - }, - "timestampColumn": { - "properties": { - "id": { - "type": "keyword" - } - } - } - }, - "type": "nested" - }, - "metricAlias": { - "type": "keyword" - }, - "name": { - "type": "text" - } - } - }, - "inventory-view": { - "properties": { - "autoBounds": { - "type": "boolean" - }, - "autoReload": { - "type": "boolean" - }, - "boundsOverride": { - "properties": { - "max": { - "type": "integer" - }, - "min": { - "type": "integer" - } - } - }, - "customOptions": { - "properties": { - "field": { - "type": "keyword" - }, - "text": { - "type": "keyword" - } - }, - "type": "nested" - }, - "filterQuery": { - "properties": { - "expression": { - "type": "keyword" - }, - "kind": { - "type": "keyword" - } - } - }, - "groupBy": { - "properties": { - "field": { - "type": "keyword" - }, - "label": { - "type": "keyword" - } - }, - "type": "nested" - }, - "metric": { - "properties": { - "type": { - "type": "keyword" - } - } - }, - "name": { - "type": "keyword" - }, - "nodeType": { - "type": "keyword" - }, - "time": { - "type": "integer" - }, - "view": { - "type": "keyword" - } - } - }, - "kql-telemetry": { - "properties": { - "optInCount": { - "type": "long" - }, - "optOutCount": { - "type": "long" - } - } - }, - "lens": { - "properties": { - "expression": { - "index": false, - "type": "keyword" - }, - "state": { - "type": "flattened" - }, - "title": { - "type": "text" - }, - "visualizationType": { - "type": "keyword" - } - } - }, - "lens-ui-telemetry": { - "properties": { - "count": { - "type": "integer" - }, - "date": { - "type": "date" - }, - "name": { - "type": "keyword" - }, - "type": { - "type": "keyword" - } - } - }, - "map": { - "properties": { - "bounds": { - "type": "geo_shape" - }, - "description": { - "type": "text" - }, - "layerListJSON": { - "type": "text" - }, - "mapStateJSON": { - "type": "text" - }, - "title": { - "type": "text" - }, - "uiStateJSON": { - "type": "text" - }, - "version": { - "type": "integer" - } - } - }, - "maps-telemetry": { - "properties": { - "attributesPerMap": { - "properties": { - "dataSourcesCount": { - "properties": { - "avg": { - "type": "long" - }, - "max": { - "type": "long" - }, - "min": { - "type": "long" - } - } - }, - "emsVectorLayersCount": { - "dynamic": "true", - "type": "object" - }, - "layerTypesCount": { - "dynamic": "true", - "type": "object" - }, - "layersCount": { - "properties": { - "avg": { - "type": "long" - }, - "max": { - "type": "long" - }, - "min": { - "type": "long" - } - } - } - } - }, - "mapsTotalCount": { - "type": "long" - }, - "timeCaptured": { - "type": "date" - } - } - }, - "metrics-explorer-view": { - "properties": { - "chartOptions": { - "properties": { - "stack": { - "type": "boolean" - }, - "type": { - "type": "keyword" - }, - "yAxisMode": { - "type": "keyword" - } - } - }, - "currentTimerange": { - "properties": { - "from": { - "type": "keyword" - }, - "interval": { - "type": "keyword" - }, - "to": { - "type": "keyword" - } - } - }, - "name": { - "type": "keyword" - }, - "options": { - "properties": { - "aggregation": { - "type": "keyword" - }, - "filterQuery": { - "type": "keyword" - }, - "groupBy": { - "type": "keyword" - }, - "limit": { - "type": "integer" - }, - "metrics": { - "properties": { - "aggregation": { - "type": "keyword" - }, - "color": { - "type": "keyword" - }, - "field": { - "type": "keyword" - }, - "label": { - "type": "keyword" - } - }, - "type": "nested" - } - } - } - } - }, - "migrationVersion": { - "dynamic": "true", - "properties": { - "index-pattern": { - "fields": { - "keyword": { - "ignore_above": 256, - "type": "keyword" - } - }, - "type": "text" - }, - "space": { - "fields": { - "keyword": { - "ignore_above": 256, - "type": "keyword" - } - }, - "type": "text" - } - } - }, - "ml-telemetry": { - "properties": { - "file_data_visualizer": { - "properties": { - "index_creation_count": { - "type": "long" - } - } - } - } - }, - "namespace": { - "type": "keyword" - }, - "query": { - "properties": { - "description": { - "type": "text" - }, - "filters": { - "enabled": false, - "type": "object" - }, - "query": { - "properties": { - "language": { - "type": "keyword" - }, - "query": { - "index": false, - "type": "keyword" - } - } - }, - "timefilter": { - "enabled": false, - "type": "object" - }, - "title": { - "type": "text" - } - } - }, - "references": { - "properties": { - "id": { - "type": "keyword" - }, - "name": { - "type": "keyword" - }, - "type": { - "type": "keyword" - } - }, - "type": "nested" - }, - "sample-data-telemetry": { - "properties": { - "installCount": { - "type": "long" - }, - "unInstallCount": { - "type": "long" - } - } - }, - "search": { - "properties": { - "columns": { - "type": "keyword" - }, - "description": { - "type": "text" - }, - "hits": { - "type": "integer" - }, - "kibanaSavedObjectMeta": { - "properties": { - "searchSourceJSON": { - "type": "text" - } - } - }, - "sort": { - "type": "keyword" - }, - "title": { - "type": "text" - }, - "version": { - "type": "integer" - } - } - }, - "server": { - "properties": { - "uuid": { - "type": "keyword" - } - } - }, - "siem-ui-timeline": { - "properties": { - "columns": { - "properties": { - "aggregatable": { - "type": "boolean" - }, - "category": { - "type": "keyword" - }, - "columnHeaderType": { - "type": "keyword" - }, - "description": { - "type": "text" - }, - "example": { - "type": "text" - }, - "id": { - "type": "keyword" - }, - "indexes": { - "type": "keyword" - }, - "name": { - "type": "text" - }, - "placeholder": { - "type": "text" - }, - "searchable": { - "type": "boolean" - }, - "type": { - "type": "keyword" - } - } - }, - "created": { - "type": "date" - }, - "createdBy": { - "type": "text" - }, - "dataProviders": { - "properties": { - "and": { - "properties": { - "enabled": { - "type": "boolean" - }, - "excluded": { - "type": "boolean" - }, - "id": { - "type": "keyword" - }, - "kqlQuery": { - "type": "text" - }, - "name": { - "type": "text" - }, - "queryMatch": { - "properties": { - "displayField": { - "type": "text" - }, - "displayValue": { - "type": "text" - }, - "field": { - "type": "text" - }, - "operator": { - "type": "text" - }, - "value": { - "type": "text" - } - } - } - } - }, - "enabled": { - "type": "boolean" - }, - "excluded": { - "type": "boolean" - }, - "id": { - "type": "keyword" - }, - "kqlQuery": { - "type": "text" - }, - "name": { - "type": "text" - }, - "queryMatch": { - "properties": { - "displayField": { - "type": "text" - }, - "displayValue": { - "type": "text" - }, - "field": { - "type": "text" - }, - "operator": { - "type": "text" - }, - "value": { - "type": "text" - } - } - } - } - }, - "dateRange": { - "properties": { - "end": { - "type": "date" - }, - "start": { - "type": "date" - } - } - }, - "description": { - "type": "text" - }, - "favorite": { - "properties": { - "favoriteDate": { - "type": "date" - }, - "fullName": { - "type": "text" - }, - "keySearch": { - "type": "text" - }, - "userName": { - "type": "text" - } - } - }, - "filters": { - "properties": { - "exists": { - "type": "text" - }, - "match_all": { - "type": "text" - }, - "meta": { - "properties": { - "alias": { - "type": "text" - }, - "controlledBy": { - "type": "text" - }, - "disabled": { - "type": "boolean" - }, - "field": { - "type": "text" - }, - "formattedValue": { - "type": "text" - }, - "index": { - "type": "keyword" - }, - "key": { - "type": "keyword" - }, - "negate": { - "type": "boolean" - }, - "params": { - "type": "text" - }, - "type": { - "type": "keyword" - }, - "value": { - "type": "text" - } - } - }, - "missing": { - "type": "text" - }, - "query": { - "type": "text" - }, - "range": { - "type": "text" - }, - "script": { - "type": "text" - } - } - }, - "kqlMode": { - "type": "keyword" - }, - "kqlQuery": { - "properties": { - "filterQuery": { - "properties": { - "kuery": { - "properties": { - "expression": { - "type": "text" - }, - "kind": { - "type": "keyword" - } - } - }, - "serializedQuery": { - "type": "text" - } - } - } - } - }, - "savedQueryId": { - "type": "keyword" - }, - "sort": { - "properties": { - "columnId": { - "type": "keyword" - }, - "sortDirection": { - "type": "keyword" - } - } - }, - "title": { - "type": "text" - }, - "updated": { - "type": "date" - }, - "updatedBy": { - "type": "text" - } - } - }, - "siem-ui-timeline-note": { - "properties": { - "created": { - "type": "date" - }, - "createdBy": { - "type": "text" - }, - "eventId": { - "type": "keyword" - }, - "note": { - "type": "text" - }, - "timelineId": { - "type": "keyword" - }, - "updated": { - "type": "date" - }, - "updatedBy": { - "type": "text" - } - } - }, - "siem-ui-timeline-pinned-event": { - "properties": { - "created": { - "type": "date" - }, - "createdBy": { - "type": "text" - }, - "eventId": { - "type": "keyword" - }, - "timelineId": { - "type": "keyword" - }, - "updated": { - "type": "date" - }, - "updatedBy": { - "type": "text" - } - } - }, - "space": { - "properties": { - "_reserved": { - "type": "boolean" - }, - "color": { - "type": "keyword" - }, - "description": { - "type": "text" - }, - "disabledFeatures": { - "type": "keyword" - }, - "imageUrl": { - "index": false, - "type": "text" - }, - "initials": { - "type": "keyword" - }, - "name": { - "fields": { - "keyword": { - "ignore_above": 2048, - "type": "keyword" - } - }, - "type": "text" - } - } - }, - "telemetry": { - "properties": { - "enabled": { - "type": "boolean" - }, - "lastReported": { - "type": "date" - }, - "lastVersionChecked": { - "ignore_above": 256, - "type": "keyword" - }, - "sendUsageFrom": { - "ignore_above": 256, - "type": "keyword" - }, - "userHasSeenNotice": { - "type": "boolean" - } - } - }, - "timelion-sheet": { - "properties": { - "description": { - "type": "text" - }, - "hits": { - "type": "integer" - }, - "kibanaSavedObjectMeta": { - "properties": { - "searchSourceJSON": { - "type": "text" - } - } - }, - "timelion_chart_height": { - "type": "integer" - }, - "timelion_columns": { - "type": "integer" - }, - "timelion_interval": { - "type": "keyword" - }, - "timelion_other_interval": { - "type": "keyword" - }, - "timelion_rows": { - "type": "integer" - }, - "timelion_sheet": { - "type": "text" - }, - "title": { - "type": "text" - }, - "version": { - "type": "integer" - } - } - }, - "type": { - "type": "keyword" - }, - "ui-metric": { - "properties": { - "count": { - "type": "integer" - } - } - }, - "updated_at": { - "type": "date" - }, - "upgrade-assistant-reindex-operation": { - "dynamic": "true", - "properties": { - "indexName": { - "type": "keyword" - }, - "status": { - "type": "integer" - } - } - }, - "upgrade-assistant-telemetry": { - "properties": { - "features": { - "properties": { - "deprecation_logging": { - "properties": { - "enabled": { - "null_value": true, - "type": "boolean" - } - } - } - } - }, - "ui_open": { - "properties": { - "cluster": { - "null_value": 0, - "type": "long" - }, - "indices": { - "null_value": 0, - "type": "long" - }, - "overview": { - "null_value": 0, - "type": "long" - } - } - }, - "ui_reindex": { - "properties": { - "close": { - "null_value": 0, - "type": "long" - }, - "open": { - "null_value": 0, - "type": "long" - }, - "start": { - "null_value": 0, - "type": "long" - }, - "stop": { - "null_value": 0, - "type": "long" - } - } - } - } - }, - "url": { - "properties": { - "accessCount": { - "type": "long" - }, - "accessDate": { - "type": "date" - }, - "createDate": { - "type": "date" - }, - "url": { - "fields": { - "keyword": { - "ignore_above": 2048, - "type": "keyword" - } - }, - "type": "text" - } - } - }, - "visualization": { - "properties": { - "description": { - "type": "text" - }, - "kibanaSavedObjectMeta": { - "properties": { - "searchSourceJSON": { - "type": "text" - } - } - }, - "savedSearchRefName": { - "type": "keyword" - }, - "title": { - "type": "text" - }, - "uiStateJSON": { - "type": "text" - }, - "version": { - "type": "integer" - }, - "visState": { - "type": "text" - } - } - } - } - }, - "settings": { - "index": { - "auto_expand_replicas": "0-1", - "number_of_replicas": "0", - "number_of_shards": "1" - } - } - } -} \ No newline at end of file diff --git a/x-pack/test/functional/es_archives/ml/sample_logs/data.json.gz b/x-pack/test/functional/es_archives/ml/sample_logs/data.json.gz index 03ceb319a6afe..88937d484d24e 100644 Binary files a/x-pack/test/functional/es_archives/ml/sample_logs/data.json.gz and b/x-pack/test/functional/es_archives/ml/sample_logs/data.json.gz differ diff --git a/x-pack/test/functional/es_archives/ml/sample_logs/mappings.json b/x-pack/test/functional/es_archives/ml/sample_logs/mappings.json index 1c7490e139be5..62f69063695e7 100644 --- a/x-pack/test/functional/es_archives/ml/sample_logs/mappings.json +++ b/x-pack/test/functional/es_archives/ml/sample_logs/mappings.json @@ -165,2998 +165,3 @@ } } } - -{ - "type": "index", - "value": { - "aliases": { - ".kibana": { - } - }, - "index": ".kibana_1", - "mappings": { - "_meta": { - "migrationMappingPropertyHashes": { - "action": "6e96ac5e648f57523879661ea72525b7", - "action_task_params": "a9d49f184ee89641044be0ca2950fa3a", - "agent_configs": "38abaf89513877745c359e7700c0c66a", - "agent_events": "3231653fafe4ef3196fe3b32ab774bf2", - "agents": "75c0f4a11560dbc38b65e5e1d98fc9da", - "alert": "7b44fba6773e37c806ce290ea9b7024e", - "apm-indices": "9bb9b2bf1fa636ed8619cbab5ce6a1dd", - "apm-telemetry": "e8619030e08b671291af04c4603b4944", - "application_usage_totals": "c897e4310c5f24b07caaff3db53ae2c1", - "application_usage_transactional": "965839e75f809fefe04f92dc4d99722a", - "canvas-element": "7390014e1091044523666d97247392fc", - "canvas-workpad": "b0a1706d356228dbdcb4a17e6b9eb231", - "cases": "08b8b110dbca273d37e8aef131ecab61", - "cases-comments": "c2061fb929f585df57425102fa928b4b", - "cases-configure": "42711cbb311976c0687853f4c1354572", - "cases-user-actions": "32277330ec6b721abe3b846cfd939a71", - "config": "ae24d22d5986d04124cc6568f771066f", - "dashboard": "d00f614b29a80360e1190193fd333bab", - "datasources": "d4bc0c252b2b5683ff21ea32d00acffc", - "enrollment_api_keys": "28b91e20b105b6f928e2012600085d8f", - "epm-package": "75d12cd13c867fd713d7dfb27366bc20", - "file-upload-telemetry": "0ed4d3e1983d1217a30982630897092e", - "graph-workspace": "cd7ba1330e6682e9cc00b78850874be1", - "index-pattern": "66eccb05066c5a89924f48a9e9736499", - "infrastructure-ui-source": "ddc0ecb18383f6b26101a2fadb2dab0c", - "inventory-view": "9ecce5b58867403613d82fe496470b34", - "kql-telemetry": "d12a98a6f19a2d273696597547e064ee", - "lens": "21c3ea0763beb1ecb0162529706b88c5", - "lens-ui-telemetry": "509bfa5978586998e05f9e303c07a327", - "map": "23d7aa4a720d4938ccde3983f87bd58d", - "maps-telemetry": "268da3a48066123fc5baf35abaa55014", - "metrics-explorer-view": "53c5365793677328df0ccb6138bf3cdd", - "migrationVersion": "4a1746014a75ade3a714e1db5763276f", - "ml-telemetry": "257fd1d4b4fdbb9cb4b8a3b27da201e9", - "namespace": "2f4316de49999235636386fe51dc06c1", - "outputs": "aee9782e0d500b867859650a36280165", - "query": "11aaeb7f5f7fa5bb43f25e18ce26e7d9", - "references": "7997cf5a56cc02bdc9c93361bde732b0", - "sample-data-telemetry": "7d3cfeb915303c9641c59681967ffeb4", - "search": "181661168bbadd1eff5902361e2a0d5c", - "server": "ec97f1c5da1a19609a60874e5af1100c", - "siem-detection-engine-rule-status": "ae783f41c6937db6b7a2ef5c93a9e9b0", - "siem-ui-timeline": "ac8020190f5950dd3250b6499144e7fb", - "siem-ui-timeline-note": "8874706eedc49059d4cf0f5094559084", - "siem-ui-timeline-pinned-event": "20638091112f0e14f0e443d512301c29", - "space": "c5ca8acafa0beaa4d08d014a97b6bc6b", - "telemetry": "36a616f7026dfa617d6655df850fe16d", - "timelion-sheet": "9a2a2748877c7a7b582fef201ab1d4cf", - "tsvb-validation-telemetry": "3a37ef6c8700ae6fc97d5c7da00e9215", - "type": "2f4316de49999235636386fe51dc06c1", - "ui-metric": "0d409297dc5ebe1e3a1da691c6ee32e3", - "updated_at": "00da57df13e94e9d98437d13ace4bfe0", - "upgrade-assistant-reindex-operation": "a53a20fe086b72c9a86da3cc12dad8a6", - "upgrade-assistant-telemetry": "56702cec857e0a9dacfb696655b4ff7b", - "uptime-dynamic-settings": "b6289473c8985c79b6c47eebc19a0ca5", - "url": "c7f66a0df8b1b52f17c28c4adb111105", - "visualization": "52d7a13ad68a150c4525b292d23e12cc" - } - }, - "dynamic": "strict", - "properties": { - "action": { - "properties": { - "actionTypeId": { - "type": "keyword" - }, - "config": { - "enabled": false, - "type": "object" - }, - "name": { - "fields": { - "keyword": { - "type": "keyword" - } - }, - "type": "text" - }, - "secrets": { - "type": "binary" - } - } - }, - "action_task_params": { - "properties": { - "actionId": { - "type": "keyword" - }, - "apiKey": { - "type": "binary" - }, - "params": { - "enabled": false, - "type": "object" - } - } - }, - "agent_configs": { - "properties": { - "datasources": { - "type": "keyword" - }, - "description": { - "type": "text" - }, - "id": { - "type": "keyword" - }, - "is_default": { - "type": "boolean" - }, - "name": { - "type": "text" - }, - "namespace": { - "type": "keyword" - }, - "revision": { - "type": "integer" - }, - "status": { - "type": "keyword" - }, - "updated_by": { - "type": "keyword" - }, - "updated_on": { - "type": "keyword" - } - } - }, - "agent_events": { - "properties": { - "action_id": { - "type": "keyword" - }, - "agent_id": { - "type": "keyword" - }, - "config_id": { - "type": "keyword" - }, - "data": { - "type": "text" - }, - "message": { - "type": "text" - }, - "payload": { - "type": "text" - }, - "stream_id": { - "type": "keyword" - }, - "subtype": { - "type": "keyword" - }, - "timestamp": { - "type": "date" - }, - "type": { - "type": "keyword" - } - } - }, - "agents": { - "properties": { - "access_api_key_id": { - "type": "keyword" - }, - "actions": { - "properties": { - "created_at": { - "type": "date" - }, - "data": { - "type": "text" - }, - "id": { - "type": "keyword" - }, - "sent_at": { - "type": "date" - }, - "type": { - "type": "keyword" - } - }, - "type": "nested" - }, - "active": { - "type": "boolean" - }, - "config_id": { - "type": "keyword" - }, - "config_newest_revision": { - "type": "integer" - }, - "config_revision": { - "type": "integer" - }, - "current_error_events": { - "type": "text" - }, - "default_api_key": { - "type": "keyword" - }, - "enrolled_at": { - "type": "date" - }, - "last_checkin": { - "type": "date" - }, - "last_updated": { - "type": "date" - }, - "local_metadata": { - "type": "text" - }, - "shared_id": { - "type": "keyword" - }, - "type": { - "type": "keyword" - }, - "updated_at": { - "type": "date" - }, - "user_provided_metadata": { - "type": "text" - }, - "version": { - "type": "keyword" - } - } - }, - "alert": { - "properties": { - "actions": { - "properties": { - "actionRef": { - "type": "keyword" - }, - "actionTypeId": { - "type": "keyword" - }, - "group": { - "type": "keyword" - }, - "params": { - "enabled": false, - "type": "object" - } - }, - "type": "nested" - }, - "alertTypeId": { - "type": "keyword" - }, - "apiKey": { - "type": "binary" - }, - "apiKeyOwner": { - "type": "keyword" - }, - "consumer": { - "type": "keyword" - }, - "createdAt": { - "type": "date" - }, - "createdBy": { - "type": "keyword" - }, - "enabled": { - "type": "boolean" - }, - "muteAll": { - "type": "boolean" - }, - "mutedInstanceIds": { - "type": "keyword" - }, - "name": { - "fields": { - "keyword": { - "type": "keyword" - } - }, - "type": "text" - }, - "params": { - "enabled": false, - "type": "object" - }, - "schedule": { - "properties": { - "interval": { - "type": "keyword" - } - } - }, - "scheduledTaskId": { - "type": "keyword" - }, - "tags": { - "type": "keyword" - }, - "throttle": { - "type": "keyword" - }, - "updatedBy": { - "type": "keyword" - } - } - }, - "apm-indices": { - "properties": { - "apm_oss": { - "properties": { - "errorIndices": { - "type": "keyword" - }, - "metricsIndices": { - "type": "keyword" - }, - "onboardingIndices": { - "type": "keyword" - }, - "sourcemapIndices": { - "type": "keyword" - }, - "spanIndices": { - "type": "keyword" - }, - "transactionIndices": { - "type": "keyword" - } - } - } - } - }, - "apm-telemetry": { - "properties": { - "agents": { - "properties": { - "dotnet": { - "properties": { - "agent": { - "properties": { - "version": { - "ignore_above": 256, - "type": "keyword" - } - } - }, - "service": { - "properties": { - "framework": { - "properties": { - "composite": { - "ignore_above": 256, - "type": "keyword" - }, - "name": { - "ignore_above": 256, - "type": "keyword" - }, - "version": { - "ignore_above": 256, - "type": "keyword" - } - } - }, - "language": { - "properties": { - "name": { - "ignore_above": 256, - "type": "keyword" - } - } - }, - "runtime": { - "properties": { - "composite": { - "ignore_above": 256, - "type": "keyword" - }, - "name": { - "ignore_above": 256, - "type": "keyword" - }, - "version": { - "ignore_above": 256, - "type": "keyword" - } - } - } - } - } - } - }, - "go": { - "properties": { - "agent": { - "properties": { - "version": { - "ignore_above": 256, - "type": "keyword" - } - } - }, - "service": { - "properties": { - "framework": { - "properties": { - "composite": { - "ignore_above": 256, - "type": "keyword" - }, - "name": { - "ignore_above": 256, - "type": "keyword" - }, - "version": { - "ignore_above": 256, - "type": "keyword" - } - } - }, - "language": { - "properties": { - "composite": { - "ignore_above": 256, - "type": "keyword" - }, - "name": { - "ignore_above": 256, - "type": "keyword" - }, - "version": { - "ignore_above": 256, - "type": "keyword" - } - } - }, - "runtime": { - "properties": { - "composite": { - "ignore_above": 256, - "type": "keyword" - }, - "name": { - "ignore_above": 256, - "type": "keyword" - }, - "version": { - "ignore_above": 256, - "type": "keyword" - } - } - } - } - } - } - }, - "java": { - "properties": { - "agent": { - "properties": { - "version": { - "ignore_above": 256, - "type": "keyword" - } - } - }, - "service": { - "properties": { - "framework": { - "properties": { - "composite": { - "ignore_above": 256, - "type": "keyword" - }, - "name": { - "ignore_above": 256, - "type": "keyword" - }, - "version": { - "ignore_above": 256, - "type": "keyword" - } - } - }, - "language": { - "properties": { - "composite": { - "ignore_above": 256, - "type": "keyword" - }, - "name": { - "ignore_above": 256, - "type": "keyword" - }, - "version": { - "ignore_above": 256, - "type": "keyword" - } - } - }, - "runtime": { - "properties": { - "composite": { - "ignore_above": 256, - "type": "keyword" - }, - "name": { - "ignore_above": 256, - "type": "keyword" - }, - "version": { - "ignore_above": 256, - "type": "keyword" - } - } - } - } - } - } - }, - "js-base": { - "properties": { - "agent": { - "properties": { - "version": { - "ignore_above": 256, - "type": "keyword" - } - } - }, - "service": { - "properties": { - "framework": { - "properties": { - "composite": { - "ignore_above": 256, - "type": "keyword" - }, - "name": { - "ignore_above": 256, - "type": "keyword" - }, - "version": { - "ignore_above": 256, - "type": "keyword" - } - } - }, - "language": { - "properties": { - "composite": { - "ignore_above": 256, - "type": "keyword" - }, - "name": { - "ignore_above": 256, - "type": "keyword" - }, - "version": { - "ignore_above": 256, - "type": "keyword" - } - } - }, - "runtime": { - "properties": { - "composite": { - "ignore_above": 256, - "type": "keyword" - }, - "name": { - "ignore_above": 256, - "type": "keyword" - }, - "version": { - "ignore_above": 256, - "type": "keyword" - } - } - } - } - } - } - }, - "nodejs": { - "properties": { - "agent": { - "properties": { - "version": { - "ignore_above": 256, - "type": "keyword" - } - } - }, - "service": { - "properties": { - "framework": { - "properties": { - "composite": { - "ignore_above": 256, - "type": "keyword" - }, - "name": { - "ignore_above": 256, - "type": "keyword" - }, - "version": { - "ignore_above": 256, - "type": "keyword" - } - } - }, - "language": { - "properties": { - "composite": { - "ignore_above": 256, - "type": "keyword" - }, - "name": { - "ignore_above": 256, - "type": "keyword" - }, - "version": { - "ignore_above": 256, - "type": "keyword" - } - } - }, - "runtime": { - "properties": { - "composite": { - "ignore_above": 256, - "type": "keyword" - }, - "name": { - "ignore_above": 256, - "type": "keyword" - }, - "version": { - "ignore_above": 256, - "type": "keyword" - } - } - } - } - } - } - }, - "python": { - "properties": { - "agent": { - "properties": { - "version": { - "ignore_above": 256, - "type": "keyword" - } - } - }, - "service": { - "properties": { - "framework": { - "properties": { - "composite": { - "ignore_above": 256, - "type": "keyword" - }, - "name": { - "ignore_above": 256, - "type": "keyword" - }, - "version": { - "ignore_above": 256, - "type": "keyword" - } - } - }, - "language": { - "properties": { - "composite": { - "ignore_above": 256, - "type": "keyword" - }, - "name": { - "ignore_above": 256, - "type": "keyword" - }, - "version": { - "ignore_above": 256, - "type": "keyword" - } - } - }, - "runtime": { - "properties": { - "composite": { - "ignore_above": 256, - "type": "keyword" - }, - "name": { - "ignore_above": 256, - "type": "keyword" - }, - "version": { - "ignore_above": 256, - "type": "keyword" - } - } - } - } - } - } - }, - "ruby": { - "properties": { - "agent": { - "properties": { - "version": { - "ignore_above": 256, - "type": "keyword" - } - } - }, - "service": { - "properties": { - "framework": { - "properties": { - "composite": { - "ignore_above": 256, - "type": "keyword" - }, - "name": { - "ignore_above": 256, - "type": "keyword" - }, - "version": { - "ignore_above": 256, - "type": "keyword" - } - } - }, - "language": { - "properties": { - "composite": { - "ignore_above": 256, - "type": "keyword" - }, - "name": { - "ignore_above": 256, - "type": "keyword" - }, - "version": { - "ignore_above": 256, - "type": "keyword" - } - } - }, - "runtime": { - "properties": { - "composite": { - "ignore_above": 256, - "type": "keyword" - }, - "name": { - "ignore_above": 256, - "type": "keyword" - }, - "version": { - "ignore_above": 256, - "type": "keyword" - } - } - } - } - } - } - }, - "rum-js": { - "properties": { - "agent": { - "properties": { - "version": { - "ignore_above": 256, - "type": "keyword" - } - } - }, - "service": { - "properties": { - "framework": { - "properties": { - "composite": { - "ignore_above": 256, - "type": "keyword" - }, - "name": { - "ignore_above": 256, - "type": "keyword" - }, - "version": { - "ignore_above": 256, - "type": "keyword" - } - } - }, - "language": { - "properties": { - "composite": { - "ignore_above": 256, - "type": "keyword" - }, - "name": { - "ignore_above": 256, - "type": "keyword" - }, - "version": { - "ignore_above": 256, - "type": "keyword" - } - } - }, - "runtime": { - "properties": { - "composite": { - "ignore_above": 256, - "type": "keyword" - }, - "name": { - "ignore_above": 256, - "type": "keyword" - }, - "version": { - "ignore_above": 256, - "type": "keyword" - } - } - } - } - } - } - } - } - }, - "cardinality": { - "properties": { - "transaction": { - "properties": { - "name": { - "properties": { - "all_agents": { - "properties": { - "1d": { - "type": "long" - } - } - }, - "rum": { - "properties": { - "1d": { - "type": "long" - } - } - } - } - } - } - }, - "user_agent": { - "properties": { - "original": { - "properties": { - "all_agents": { - "properties": { - "1d": { - "type": "long" - } - } - }, - "rum": { - "properties": { - "1d": { - "type": "long" - } - } - } - } - } - } - } - } - }, - "counts": { - "properties": { - "agent_configuration": { - "properties": { - "all": { - "type": "long" - } - } - }, - "error": { - "properties": { - "1d": { - "type": "long" - }, - "all": { - "type": "long" - } - } - }, - "max_error_groups_per_service": { - "properties": { - "1d": { - "type": "long" - } - } - }, - "max_transaction_groups_per_service": { - "properties": { - "1d": { - "type": "long" - } - } - }, - "metric": { - "properties": { - "1d": { - "type": "long" - }, - "all": { - "type": "long" - } - } - }, - "onboarding": { - "properties": { - "1d": { - "type": "long" - }, - "all": { - "type": "long" - } - } - }, - "services": { - "properties": { - "1d": { - "type": "long" - } - } - }, - "sourcemap": { - "properties": { - "1d": { - "type": "long" - }, - "all": { - "type": "long" - } - } - }, - "span": { - "properties": { - "1d": { - "type": "long" - }, - "all": { - "type": "long" - } - } - }, - "traces": { - "properties": { - "1d": { - "type": "long" - } - } - }, - "transaction": { - "properties": { - "1d": { - "type": "long" - }, - "all": { - "type": "long" - } - } - } - } - }, - "has_any_services": { - "type": "boolean" - }, - "indices": { - "properties": { - "all": { - "properties": { - "total": { - "properties": { - "docs": { - "properties": { - "count": { - "type": "long" - } - } - }, - "store": { - "properties": { - "size_in_bytes": { - "type": "long" - } - } - } - } - } - } - }, - "shards": { - "properties": { - "total": { - "type": "long" - } - } - } - } - }, - "integrations": { - "properties": { - "ml": { - "properties": { - "all_jobs_count": { - "type": "long" - } - } - } - } - }, - "retainment": { - "properties": { - "error": { - "properties": { - "ms": { - "type": "long" - } - } - }, - "metric": { - "properties": { - "ms": { - "type": "long" - } - } - }, - "onboarding": { - "properties": { - "ms": { - "type": "long" - } - } - }, - "span": { - "properties": { - "ms": { - "type": "long" - } - } - }, - "transaction": { - "properties": { - "ms": { - "type": "long" - } - } - } - } - }, - "services_per_agent": { - "properties": { - "dotnet": { - "null_value": 0, - "type": "long" - }, - "go": { - "null_value": 0, - "type": "long" - }, - "java": { - "null_value": 0, - "type": "long" - }, - "js-base": { - "null_value": 0, - "type": "long" - }, - "nodejs": { - "null_value": 0, - "type": "long" - }, - "python": { - "null_value": 0, - "type": "long" - }, - "ruby": { - "null_value": 0, - "type": "long" - }, - "rum-js": { - "null_value": 0, - "type": "long" - } - } - }, - "tasks": { - "properties": { - "agent_configuration": { - "properties": { - "took": { - "properties": { - "ms": { - "type": "long" - } - } - } - } - }, - "agents": { - "properties": { - "took": { - "properties": { - "ms": { - "type": "long" - } - } - } - } - }, - "cardinality": { - "properties": { - "took": { - "properties": { - "ms": { - "type": "long" - } - } - } - } - }, - "groupings": { - "properties": { - "took": { - "properties": { - "ms": { - "type": "long" - } - } - } - } - }, - "indices_stats": { - "properties": { - "took": { - "properties": { - "ms": { - "type": "long" - } - } - } - } - }, - "integrations": { - "properties": { - "took": { - "properties": { - "ms": { - "type": "long" - } - } - } - } - }, - "processor_events": { - "properties": { - "took": { - "properties": { - "ms": { - "type": "long" - } - } - } - } - }, - "services": { - "properties": { - "took": { - "properties": { - "ms": { - "type": "long" - } - } - } - } - }, - "versions": { - "properties": { - "took": { - "properties": { - "ms": { - "type": "long" - } - } - } - } - } - } - }, - "version": { - "properties": { - "apm_server": { - "properties": { - "major": { - "type": "long" - }, - "minor": { - "type": "long" - }, - "patch": { - "type": "long" - } - } - } - } - } - } - }, - "application_usage_totals": { - "properties": { - "appId": { - "type": "keyword" - }, - "minutesOnScreen": { - "type": "float" - }, - "numberOfClicks": { - "type": "long" - } - } - }, - "application_usage_transactional": { - "properties": { - "appId": { - "type": "keyword" - }, - "minutesOnScreen": { - "type": "float" - }, - "numberOfClicks": { - "type": "long" - }, - "timestamp": { - "type": "date" - } - } - }, - "canvas-element": { - "dynamic": "false", - "properties": { - "@created": { - "type": "date" - }, - "@timestamp": { - "type": "date" - }, - "content": { - "type": "text" - }, - "help": { - "type": "text" - }, - "image": { - "type": "text" - }, - "name": { - "fields": { - "keyword": { - "type": "keyword" - } - }, - "type": "text" - } - } - }, - "canvas-workpad": { - "dynamic": "false", - "properties": { - "@created": { - "type": "date" - }, - "@timestamp": { - "type": "date" - }, - "name": { - "fields": { - "keyword": { - "type": "keyword" - } - }, - "type": "text" - } - } - }, - "cases": { - "properties": { - "closed_at": { - "type": "date" - }, - "closed_by": { - "properties": { - "email": { - "type": "keyword" - }, - "full_name": { - "type": "keyword" - }, - "username": { - "type": "keyword" - } - } - }, - "created_at": { - "type": "date" - }, - "created_by": { - "properties": { - "email": { - "type": "keyword" - }, - "full_name": { - "type": "keyword" - }, - "username": { - "type": "keyword" - } - } - }, - "description": { - "type": "text" - }, - "external_service": { - "properties": { - "connector_id": { - "type": "keyword" - }, - "connector_name": { - "type": "keyword" - }, - "external_id": { - "type": "keyword" - }, - "external_title": { - "type": "text" - }, - "external_url": { - "type": "text" - }, - "pushed_at": { - "type": "date" - }, - "pushed_by": { - "properties": { - "email": { - "type": "keyword" - }, - "full_name": { - "type": "keyword" - }, - "username": { - "type": "keyword" - } - } - } - } - }, - "status": { - "type": "keyword" - }, - "tags": { - "type": "keyword" - }, - "title": { - "type": "keyword" - }, - "updated_at": { - "type": "date" - }, - "updated_by": { - "properties": { - "email": { - "type": "keyword" - }, - "full_name": { - "type": "keyword" - }, - "username": { - "type": "keyword" - } - } - } - } - }, - "cases-comments": { - "properties": { - "comment": { - "type": "text" - }, - "created_at": { - "type": "date" - }, - "created_by": { - "properties": { - "email": { - "type": "keyword" - }, - "full_name": { - "type": "keyword" - }, - "username": { - "type": "keyword" - } - } - }, - "pushed_at": { - "type": "date" - }, - "pushed_by": { - "properties": { - "email": { - "type": "keyword" - }, - "full_name": { - "type": "keyword" - }, - "username": { - "type": "keyword" - } - } - }, - "updated_at": { - "type": "date" - }, - "updated_by": { - "properties": { - "email": { - "type": "keyword" - }, - "full_name": { - "type": "keyword" - }, - "username": { - "type": "keyword" - } - } - } - } - }, - "cases-configure": { - "properties": { - "closure_type": { - "type": "keyword" - }, - "connector_id": { - "type": "keyword" - }, - "connector_name": { - "type": "keyword" - }, - "created_at": { - "type": "date" - }, - "created_by": { - "properties": { - "email": { - "type": "keyword" - }, - "full_name": { - "type": "keyword" - }, - "username": { - "type": "keyword" - } - } - }, - "updated_at": { - "type": "date" - }, - "updated_by": { - "properties": { - "email": { - "type": "keyword" - }, - "full_name": { - "type": "keyword" - }, - "username": { - "type": "keyword" - } - } - } - } - }, - "cases-user-actions": { - "properties": { - "action": { - "type": "keyword" - }, - "action_at": { - "type": "date" - }, - "action_by": { - "properties": { - "email": { - "type": "keyword" - }, - "full_name": { - "type": "keyword" - }, - "username": { - "type": "keyword" - } - } - }, - "action_field": { - "type": "keyword" - }, - "new_value": { - "type": "text" - }, - "old_value": { - "type": "text" - } - } - }, - "config": { - "dynamic": "true", - "properties": { - "buildNum": { - "type": "keyword" - }, - "dateFormat:tz": { - "fields": { - "keyword": { - "ignore_above": 256, - "type": "keyword" - } - }, - "type": "text" - }, - "defaultIndex": { - "fields": { - "keyword": { - "ignore_above": 256, - "type": "keyword" - } - }, - "type": "text" - } - } - }, - "dashboard": { - "properties": { - "description": { - "type": "text" - }, - "hits": { - "type": "integer" - }, - "kibanaSavedObjectMeta": { - "properties": { - "searchSourceJSON": { - "type": "text" - } - } - }, - "optionsJSON": { - "type": "text" - }, - "panelsJSON": { - "type": "text" - }, - "refreshInterval": { - "properties": { - "display": { - "type": "keyword" - }, - "pause": { - "type": "boolean" - }, - "section": { - "type": "integer" - }, - "value": { - "type": "integer" - } - } - }, - "timeFrom": { - "type": "keyword" - }, - "timeRestore": { - "type": "boolean" - }, - "timeTo": { - "type": "keyword" - }, - "title": { - "type": "text" - }, - "version": { - "type": "integer" - } - } - }, - "datasources": { - "properties": { - "config_id": { - "type": "keyword" - }, - "description": { - "type": "text" - }, - "enabled": { - "type": "boolean" - }, - "inputs": { - "properties": { - "config": { - "type": "flattened" - }, - "enabled": { - "type": "boolean" - }, - "processors": { - "type": "keyword" - }, - "streams": { - "properties": { - "config": { - "type": "flattened" - }, - "dataset": { - "type": "keyword" - }, - "enabled": { - "type": "boolean" - }, - "id": { - "type": "keyword" - }, - "processors": { - "type": "keyword" - } - }, - "type": "nested" - }, - "type": { - "type": "keyword" - } - }, - "type": "nested" - }, - "name": { - "type": "keyword" - }, - "namespace": { - "type": "keyword" - }, - "output_id": { - "type": "keyword" - }, - "package": { - "properties": { - "name": { - "type": "keyword" - }, - "title": { - "type": "keyword" - }, - "version": { - "type": "keyword" - } - } - }, - "revision": { - "type": "integer" - } - } - }, - "enrollment_api_keys": { - "properties": { - "active": { - "type": "boolean" - }, - "api_key": { - "type": "binary" - }, - "api_key_id": { - "type": "keyword" - }, - "config_id": { - "type": "keyword" - }, - "created_at": { - "type": "date" - }, - "expire_at": { - "type": "date" - }, - "name": { - "type": "keyword" - }, - "type": { - "type": "keyword" - }, - "updated_at": { - "type": "date" - } - } - }, - "epm-package": { - "properties": { - "installed": { - "properties": { - "id": { - "type": "keyword" - }, - "type": { - "type": "keyword" - } - }, - "type": "nested" - }, - "name": { - "type": "keyword" - }, - "version": { - "type": "keyword" - } - } - }, - "file-upload-telemetry": { - "properties": { - "filesUploadedTotalCount": { - "type": "long" - } - } - }, - "graph-workspace": { - "properties": { - "description": { - "type": "text" - }, - "kibanaSavedObjectMeta": { - "properties": { - "searchSourceJSON": { - "type": "text" - } - } - }, - "numLinks": { - "type": "integer" - }, - "numVertices": { - "type": "integer" - }, - "title": { - "type": "text" - }, - "version": { - "type": "integer" - }, - "wsState": { - "type": "text" - } - } - }, - "index-pattern": { - "properties": { - "fieldFormatMap": { - "type": "text" - }, - "fields": { - "type": "text" - }, - "intervalName": { - "type": "keyword" - }, - "notExpandable": { - "type": "boolean" - }, - "sourceFilters": { - "type": "text" - }, - "timeFieldName": { - "type": "keyword" - }, - "title": { - "type": "text" - }, - "type": { - "type": "keyword" - }, - "typeMeta": { - "type": "keyword" - } - } - }, - "infrastructure-ui-source": { - "properties": { - "description": { - "type": "text" - }, - "fields": { - "properties": { - "container": { - "type": "keyword" - }, - "host": { - "type": "keyword" - }, - "pod": { - "type": "keyword" - }, - "tiebreaker": { - "type": "keyword" - }, - "timestamp": { - "type": "keyword" - } - } - }, - "logAlias": { - "type": "keyword" - }, - "logColumns": { - "properties": { - "fieldColumn": { - "properties": { - "field": { - "type": "keyword" - }, - "id": { - "type": "keyword" - } - } - }, - "messageColumn": { - "properties": { - "id": { - "type": "keyword" - } - } - }, - "timestampColumn": { - "properties": { - "id": { - "type": "keyword" - } - } - } - }, - "type": "nested" - }, - "metricAlias": { - "type": "keyword" - }, - "name": { - "type": "text" - } - } - }, - "inventory-view": { - "properties": { - "autoBounds": { - "type": "boolean" - }, - "autoReload": { - "type": "boolean" - }, - "boundsOverride": { - "properties": { - "max": { - "type": "integer" - }, - "min": { - "type": "integer" - } - } - }, - "customMetrics": { - "properties": { - "aggregation": { - "type": "keyword" - }, - "field": { - "type": "keyword" - }, - "id": { - "type": "keyword" - }, - "label": { - "type": "keyword" - }, - "type": { - "type": "keyword" - } - }, - "type": "nested" - }, - "customOptions": { - "properties": { - "field": { - "type": "keyword" - }, - "text": { - "type": "keyword" - } - }, - "type": "nested" - }, - "filterQuery": { - "properties": { - "expression": { - "type": "keyword" - }, - "kind": { - "type": "keyword" - } - } - }, - "groupBy": { - "properties": { - "field": { - "type": "keyword" - }, - "label": { - "type": "keyword" - } - }, - "type": "nested" - }, - "metric": { - "properties": { - "aggregation": { - "type": "keyword" - }, - "field": { - "type": "keyword" - }, - "id": { - "type": "keyword" - }, - "label": { - "type": "keyword" - }, - "type": { - "type": "keyword" - } - } - }, - "name": { - "type": "keyword" - }, - "nodeType": { - "type": "keyword" - }, - "time": { - "type": "integer" - }, - "view": { - "type": "keyword" - } - } - }, - "kql-telemetry": { - "properties": { - "optInCount": { - "type": "long" - }, - "optOutCount": { - "type": "long" - } - } - }, - "lens": { - "properties": { - "expression": { - "index": false, - "type": "keyword" - }, - "state": { - "type": "flattened" - }, - "title": { - "type": "text" - }, - "visualizationType": { - "type": "keyword" - } - } - }, - "lens-ui-telemetry": { - "properties": { - "count": { - "type": "integer" - }, - "date": { - "type": "date" - }, - "name": { - "type": "keyword" - }, - "type": { - "type": "keyword" - } - } - }, - "map": { - "properties": { - "bounds": { - "type": "geo_shape" - }, - "description": { - "type": "text" - }, - "layerListJSON": { - "type": "text" - }, - "mapStateJSON": { - "type": "text" - }, - "title": { - "type": "text" - }, - "uiStateJSON": { - "type": "text" - }, - "version": { - "type": "integer" - } - } - }, - "maps-telemetry": { - "properties": { - "attributesPerMap": { - "properties": { - "dataSourcesCount": { - "properties": { - "avg": { - "type": "long" - }, - "max": { - "type": "long" - }, - "min": { - "type": "long" - } - } - }, - "emsVectorLayersCount": { - "dynamic": "true", - "type": "object" - }, - "layerTypesCount": { - "dynamic": "true", - "type": "object" - }, - "layersCount": { - "properties": { - "avg": { - "type": "long" - }, - "max": { - "type": "long" - }, - "min": { - "type": "long" - } - } - } - } - }, - "indexPatternsWithGeoFieldCount": { - "type": "long" - }, - "mapsTotalCount": { - "type": "long" - }, - "settings": { - "properties": { - "showMapVisualizationTypes": { - "type": "boolean" - } - } - }, - "timeCaptured": { - "type": "date" - } - } - }, - "metrics-explorer-view": { - "properties": { - "chartOptions": { - "properties": { - "stack": { - "type": "boolean" - }, - "type": { - "type": "keyword" - }, - "yAxisMode": { - "type": "keyword" - } - } - }, - "currentTimerange": { - "properties": { - "from": { - "type": "keyword" - }, - "interval": { - "type": "keyword" - }, - "to": { - "type": "keyword" - } - } - }, - "name": { - "type": "keyword" - }, - "options": { - "properties": { - "aggregation": { - "type": "keyword" - }, - "filterQuery": { - "type": "keyword" - }, - "groupBy": { - "type": "keyword" - }, - "limit": { - "type": "integer" - }, - "metrics": { - "properties": { - "aggregation": { - "type": "keyword" - }, - "color": { - "type": "keyword" - }, - "field": { - "type": "keyword" - }, - "label": { - "type": "keyword" - } - }, - "type": "nested" - } - } - } - } - }, - "migrationVersion": { - "dynamic": "true", - "properties": { - "canvas-workpad": { - "fields": { - "keyword": { - "ignore_above": 256, - "type": "keyword" - } - }, - "type": "text" - }, - "dashboard": { - "fields": { - "keyword": { - "ignore_above": 256, - "type": "keyword" - } - }, - "type": "text" - }, - "graph-workspace": { - "fields": { - "keyword": { - "ignore_above": 256, - "type": "keyword" - } - }, - "type": "text" - }, - "index-pattern": { - "fields": { - "keyword": { - "ignore_above": 256, - "type": "keyword" - } - }, - "type": "text" - }, - "map": { - "fields": { - "keyword": { - "ignore_above": 256, - "type": "keyword" - } - }, - "type": "text" - }, - "space": { - "fields": { - "keyword": { - "ignore_above": 256, - "type": "keyword" - } - }, - "type": "text" - }, - "visualization": { - "fields": { - "keyword": { - "ignore_above": 256, - "type": "keyword" - } - }, - "type": "text" - } - } - }, - "ml-telemetry": { - "properties": { - "file_data_visualizer": { - "properties": { - "index_creation_count": { - "type": "long" - } - } - } - } - }, - "namespace": { - "type": "keyword" - }, - "outputs": { - "properties": { - "api_key": { - "type": "keyword" - }, - "ca_sha256": { - "type": "keyword" - }, - "config": { - "type": "flattened" - }, - "fleet_enroll_password": { - "type": "binary" - }, - "fleet_enroll_username": { - "type": "binary" - }, - "hosts": { - "type": "keyword" - }, - "is_default": { - "type": "boolean" - }, - "name": { - "type": "keyword" - }, - "type": { - "type": "keyword" - } - } - }, - "query": { - "properties": { - "description": { - "type": "text" - }, - "filters": { - "enabled": false, - "type": "object" - }, - "query": { - "properties": { - "language": { - "type": "keyword" - }, - "query": { - "index": false, - "type": "keyword" - } - } - }, - "timefilter": { - "enabled": false, - "type": "object" - }, - "title": { - "type": "text" - } - } - }, - "references": { - "properties": { - "id": { - "type": "keyword" - }, - "name": { - "type": "keyword" - }, - "type": { - "type": "keyword" - } - }, - "type": "nested" - }, - "sample-data-telemetry": { - "properties": { - "installCount": { - "type": "long" - }, - "unInstallCount": { - "type": "long" - } - } - }, - "search": { - "properties": { - "columns": { - "type": "keyword" - }, - "description": { - "type": "text" - }, - "hits": { - "type": "integer" - }, - "kibanaSavedObjectMeta": { - "properties": { - "searchSourceJSON": { - "type": "text" - } - } - }, - "sort": { - "type": "keyword" - }, - "title": { - "type": "text" - }, - "version": { - "type": "integer" - } - } - }, - "server": { - "properties": { - "uuid": { - "type": "keyword" - } - } - }, - "siem-detection-engine-rule-status": { - "properties": { - "alertId": { - "type": "keyword" - }, - "bulkCreateTimeDurations": { - "type": "float" - }, - "gap": { - "type": "text" - }, - "lastFailureAt": { - "type": "date" - }, - "lastFailureMessage": { - "type": "text" - }, - "lastLookBackDate": { - "type": "date" - }, - "lastSuccessAt": { - "type": "date" - }, - "lastSuccessMessage": { - "type": "text" - }, - "searchAfterTimeDurations": { - "type": "float" - }, - "status": { - "type": "keyword" - }, - "statusDate": { - "type": "date" - } - } - }, - "siem-ui-timeline": { - "properties": { - "columns": { - "properties": { - "aggregatable": { - "type": "boolean" - }, - "category": { - "type": "keyword" - }, - "columnHeaderType": { - "type": "keyword" - }, - "description": { - "type": "text" - }, - "example": { - "type": "text" - }, - "id": { - "type": "keyword" - }, - "indexes": { - "type": "keyword" - }, - "name": { - "type": "text" - }, - "placeholder": { - "type": "text" - }, - "searchable": { - "type": "boolean" - }, - "type": { - "type": "keyword" - } - } - }, - "created": { - "type": "date" - }, - "createdBy": { - "type": "text" - }, - "dataProviders": { - "properties": { - "and": { - "properties": { - "enabled": { - "type": "boolean" - }, - "excluded": { - "type": "boolean" - }, - "id": { - "type": "keyword" - }, - "kqlQuery": { - "type": "text" - }, - "name": { - "type": "text" - }, - "queryMatch": { - "properties": { - "displayField": { - "type": "text" - }, - "displayValue": { - "type": "text" - }, - "field": { - "type": "text" - }, - "operator": { - "type": "text" - }, - "value": { - "type": "text" - } - } - } - } - }, - "enabled": { - "type": "boolean" - }, - "excluded": { - "type": "boolean" - }, - "id": { - "type": "keyword" - }, - "kqlQuery": { - "type": "text" - }, - "name": { - "type": "text" - }, - "queryMatch": { - "properties": { - "displayField": { - "type": "text" - }, - "displayValue": { - "type": "text" - }, - "field": { - "type": "text" - }, - "operator": { - "type": "text" - }, - "value": { - "type": "text" - } - } - } - } - }, - "dateRange": { - "properties": { - "end": { - "type": "date" - }, - "start": { - "type": "date" - } - } - }, - "description": { - "type": "text" - }, - "eventType": { - "type": "keyword" - }, - "favorite": { - "properties": { - "favoriteDate": { - "type": "date" - }, - "fullName": { - "type": "text" - }, - "keySearch": { - "type": "text" - }, - "userName": { - "type": "text" - } - } - }, - "filters": { - "properties": { - "exists": { - "type": "text" - }, - "match_all": { - "type": "text" - }, - "meta": { - "properties": { - "alias": { - "type": "text" - }, - "controlledBy": { - "type": "text" - }, - "disabled": { - "type": "boolean" - }, - "field": { - "type": "text" - }, - "formattedValue": { - "type": "text" - }, - "index": { - "type": "keyword" - }, - "key": { - "type": "keyword" - }, - "negate": { - "type": "boolean" - }, - "params": { - "type": "text" - }, - "type": { - "type": "keyword" - }, - "value": { - "type": "text" - } - } - }, - "missing": { - "type": "text" - }, - "query": { - "type": "text" - }, - "range": { - "type": "text" - }, - "script": { - "type": "text" - } - } - }, - "kqlMode": { - "type": "keyword" - }, - "kqlQuery": { - "properties": { - "filterQuery": { - "properties": { - "kuery": { - "properties": { - "expression": { - "type": "text" - }, - "kind": { - "type": "keyword" - } - } - }, - "serializedQuery": { - "type": "text" - } - } - } - } - }, - "savedQueryId": { - "type": "keyword" - }, - "sort": { - "properties": { - "columnId": { - "type": "keyword" - }, - "sortDirection": { - "type": "keyword" - } - } - }, - "title": { - "type": "text" - }, - "updated": { - "type": "date" - }, - "updatedBy": { - "type": "text" - } - } - }, - "siem-ui-timeline-note": { - "properties": { - "created": { - "type": "date" - }, - "createdBy": { - "type": "text" - }, - "eventId": { - "type": "keyword" - }, - "note": { - "type": "text" - }, - "timelineId": { - "type": "keyword" - }, - "updated": { - "type": "date" - }, - "updatedBy": { - "type": "text" - } - } - }, - "siem-ui-timeline-pinned-event": { - "properties": { - "created": { - "type": "date" - }, - "createdBy": { - "type": "text" - }, - "eventId": { - "type": "keyword" - }, - "timelineId": { - "type": "keyword" - }, - "updated": { - "type": "date" - }, - "updatedBy": { - "type": "text" - } - } - }, - "space": { - "properties": { - "_reserved": { - "type": "boolean" - }, - "color": { - "type": "keyword" - }, - "description": { - "type": "text" - }, - "disabledFeatures": { - "type": "keyword" - }, - "imageUrl": { - "index": false, - "type": "text" - }, - "initials": { - "type": "keyword" - }, - "name": { - "fields": { - "keyword": { - "ignore_above": 2048, - "type": "keyword" - } - }, - "type": "text" - } - } - }, - "telemetry": { - "properties": { - "allowChangingOptInStatus": { - "type": "boolean" - }, - "enabled": { - "type": "boolean" - }, - "lastReported": { - "type": "date" - }, - "lastVersionChecked": { - "type": "keyword" - }, - "reportFailureCount": { - "type": "integer" - }, - "reportFailureVersion": { - "type": "keyword" - }, - "sendUsageFrom": { - "type": "keyword" - }, - "userHasSeenNotice": { - "type": "boolean" - } - } - }, - "timelion-sheet": { - "properties": { - "description": { - "type": "text" - }, - "hits": { - "type": "integer" - }, - "kibanaSavedObjectMeta": { - "properties": { - "searchSourceJSON": { - "type": "text" - } - } - }, - "timelion_chart_height": { - "type": "integer" - }, - "timelion_columns": { - "type": "integer" - }, - "timelion_interval": { - "type": "keyword" - }, - "timelion_other_interval": { - "type": "keyword" - }, - "timelion_rows": { - "type": "integer" - }, - "timelion_sheet": { - "type": "text" - }, - "title": { - "type": "text" - }, - "version": { - "type": "integer" - } - } - }, - "tsvb-validation-telemetry": { - "properties": { - "failedRequests": { - "type": "long" - } - } - }, - "type": { - "type": "keyword" - }, - "ui-metric": { - "properties": { - "count": { - "type": "integer" - } - } - }, - "updated_at": { - "type": "date" - }, - "upgrade-assistant-reindex-operation": { - "dynamic": "true", - "properties": { - "indexName": { - "type": "keyword" - }, - "status": { - "type": "integer" - } - } - }, - "upgrade-assistant-telemetry": { - "properties": { - "features": { - "properties": { - "deprecation_logging": { - "properties": { - "enabled": { - "null_value": true, - "type": "boolean" - } - } - } - } - }, - "ui_open": { - "properties": { - "cluster": { - "null_value": 0, - "type": "long" - }, - "indices": { - "null_value": 0, - "type": "long" - }, - "overview": { - "null_value": 0, - "type": "long" - } - } - }, - "ui_reindex": { - "properties": { - "close": { - "null_value": 0, - "type": "long" - }, - "open": { - "null_value": 0, - "type": "long" - }, - "start": { - "null_value": 0, - "type": "long" - }, - "stop": { - "null_value": 0, - "type": "long" - } - } - } - } - }, - "uptime-dynamic-settings": { - "properties": { - "heartbeatIndices": { - "type": "keyword" - } - } - }, - "url": { - "properties": { - "accessCount": { - "type": "long" - }, - "accessDate": { - "type": "date" - }, - "createDate": { - "type": "date" - }, - "url": { - "fields": { - "keyword": { - "ignore_above": 2048, - "type": "keyword" - } - }, - "type": "text" - } - } - }, - "visualization": { - "properties": { - "description": { - "type": "text" - }, - "kibanaSavedObjectMeta": { - "properties": { - "searchSourceJSON": { - "type": "text" - } - } - }, - "savedSearchRefName": { - "type": "keyword" - }, - "title": { - "type": "text" - }, - "uiStateJSON": { - "type": "text" - }, - "version": { - "type": "integer" - }, - "visState": { - "type": "text" - } - } - } - } - }, - "settings": { - "index": { - "auto_expand_replicas": "0-1", - "number_of_replicas": "0", - "number_of_shards": "1" - } - } - } -} \ No newline at end of file diff --git a/x-pack/test/functional/es_archives/reporting/ecommerce_kibana/data.json.gz b/x-pack/test/functional/es_archives/reporting/ecommerce_kibana/data.json.gz index 2b204d0bde271..454d260a518cd 100644 Binary files a/x-pack/test/functional/es_archives/reporting/ecommerce_kibana/data.json.gz and b/x-pack/test/functional/es_archives/reporting/ecommerce_kibana/data.json.gz differ diff --git a/x-pack/test/functional/es_archives/reporting/ecommerce_kibana/mappings.json b/x-pack/test/functional/es_archives/reporting/ecommerce_kibana/mappings.json index cf647f5c53212..fbcfa4cbe49b3 100644 --- a/x-pack/test/functional/es_archives/reporting/ecommerce_kibana/mappings.json +++ b/x-pack/test/functional/es_archives/reporting/ecommerce_kibana/mappings.json @@ -9,45 +9,62 @@ "mappings": { "_meta": { "migrationMappingPropertyHashes": { - "action": "c0c235fba02ebd2a2412bcda79009b58", + "action": "6e96ac5e648f57523879661ea72525b7", "action_task_params": "a9d49f184ee89641044be0ca2950fa3a", - "alert": "d49f9b8d1277c6004506eec20dc0b108", + "agent_actions": "ed270b46812f0fa1439366c428a2cf17", + "agent_configs": "38abaf89513877745c359e7700c0c66a", + "agent_events": "3231653fafe4ef3196fe3b32ab774bf2", + "agents": "c3eeb7b9d97176f15f6d126370ab23c7", + "alert": "7b44fba6773e37c806ce290ea9b7024e", "apm-indices": "9bb9b2bf1fa636ed8619cbab5ce6a1dd", - "apm-services-telemetry": "07ee1939fa4302c62ddc052ec03fed90", + "apm-telemetry": "3525d7c22c42bc80f5e6e9cb3f2b26a2", + "application_usage_totals": "c897e4310c5f24b07caaff3db53ae2c1", + "application_usage_transactional": "965839e75f809fefe04f92dc4d99722a", "canvas-element": "7390014e1091044523666d97247392fc", "canvas-workpad": "b0a1706d356228dbdcb4a17e6b9eb231", - "config": "87aca8fdb053154f11383fce3dbf3edf", + "cases": "08b8b110dbca273d37e8aef131ecab61", + "cases-comments": "c2061fb929f585df57425102fa928b4b", + "cases-configure": "42711cbb311976c0687853f4c1354572", + "cases-user-actions": "32277330ec6b721abe3b846cfd939a71", + "config": "ae24d22d5986d04124cc6568f771066f", "dashboard": "d00f614b29a80360e1190193fd333bab", + "datasources": "d4bc0c252b2b5683ff21ea32d00acffc", + "enrollment_api_keys": "28b91e20b105b6f928e2012600085d8f", + "epm-package": "0be91c6758421dd5d0f1a58e9e5bc7c3", "file-upload-telemetry": "0ed4d3e1983d1217a30982630897092e", "graph-workspace": "cd7ba1330e6682e9cc00b78850874be1", "index-pattern": "66eccb05066c5a89924f48a9e9736499", "infrastructure-ui-source": "ddc0ecb18383f6b26101a2fadb2dab0c", - "inventory-view": "84b320fd67209906333ffce261128462", + "inventory-view": "9ecce5b58867403613d82fe496470b34", "kql-telemetry": "d12a98a6f19a2d273696597547e064ee", "lens": "21c3ea0763beb1ecb0162529706b88c5", "lens-ui-telemetry": "509bfa5978586998e05f9e303c07a327", "map": "23d7aa4a720d4938ccde3983f87bd58d", - "maps-telemetry": "a4229f8b16a6820c6d724b7e0c1f729d", + "maps-telemetry": "268da3a48066123fc5baf35abaa55014", "metrics-explorer-view": "53c5365793677328df0ccb6138bf3cdd", "migrationVersion": "4a1746014a75ade3a714e1db5763276f", "ml-telemetry": "257fd1d4b4fdbb9cb4b8a3b27da201e9", "namespace": "2f4316de49999235636386fe51dc06c1", + "outputs": "aee9782e0d500b867859650a36280165", "query": "11aaeb7f5f7fa5bb43f25e18ce26e7d9", "references": "7997cf5a56cc02bdc9c93361bde732b0", "sample-data-telemetry": "7d3cfeb915303c9641c59681967ffeb4", "search": "181661168bbadd1eff5902361e2a0d5c", "server": "ec97f1c5da1a19609a60874e5af1100c", - "siem-ui-timeline": "6485ab095be8d15246667b98a1a34295", + "siem-detection-engine-rule-actions": "90eee2e4635260f4be0a1da8f5bc0aa0", + "siem-detection-engine-rule-status": "ae783f41c6937db6b7a2ef5c93a9e9b0", + "siem-ui-timeline": "ac8020190f5950dd3250b6499144e7fb", "siem-ui-timeline-note": "8874706eedc49059d4cf0f5094559084", "siem-ui-timeline-pinned-event": "20638091112f0e14f0e443d512301c29", - "space": "c5ca8acafa0beaa4d08d014a97b6bc6b", - "telemetry": "358ffaa88ba34a97d55af0933a117de4", + "telemetry": "36a616f7026dfa617d6655df850fe16d", "timelion-sheet": "9a2a2748877c7a7b582fef201ab1d4cf", + "tsvb-validation-telemetry": "3a37ef6c8700ae6fc97d5c7da00e9215", "type": "2f4316de49999235636386fe51dc06c1", "ui-metric": "0d409297dc5ebe1e3a1da691c6ee32e3", "updated_at": "00da57df13e94e9d98437d13ace4bfe0", "upgrade-assistant-reindex-operation": "a53a20fe086b72c9a86da3cc12dad8a6", "upgrade-assistant-telemetry": "56702cec857e0a9dacfb696655b4ff7b", + "uptime-dynamic-settings": "b6289473c8985c79b6c47eebc19a0ca5", "url": "c7f66a0df8b1b52f17c28c4adb111105", "visualization": "52d7a13ad68a150c4525b292d23e12cc" } @@ -64,6 +81,11 @@ "type": "object" }, "name": { + "fields": { + "keyword": { + "type": "keyword" + } + }, "type": "text" }, "secrets": { @@ -85,6 +107,145 @@ } } }, + "agent_actions": { + "properties": { + "agent_id": { + "type": "keyword" + }, + "created_at": { + "type": "date" + }, + "data": { + "type": "flattened" + }, + "sent_at": { + "type": "date" + }, + "type": { + "type": "keyword" + } + } + }, + "agent_configs": { + "properties": { + "datasources": { + "type": "keyword" + }, + "description": { + "type": "text" + }, + "id": { + "type": "keyword" + }, + "is_default": { + "type": "boolean" + }, + "name": { + "type": "text" + }, + "namespace": { + "type": "keyword" + }, + "revision": { + "type": "integer" + }, + "status": { + "type": "keyword" + }, + "updated_by": { + "type": "keyword" + }, + "updated_on": { + "type": "keyword" + } + } + }, + "agent_events": { + "properties": { + "action_id": { + "type": "keyword" + }, + "agent_id": { + "type": "keyword" + }, + "config_id": { + "type": "keyword" + }, + "data": { + "type": "text" + }, + "message": { + "type": "text" + }, + "payload": { + "type": "text" + }, + "stream_id": { + "type": "keyword" + }, + "subtype": { + "type": "keyword" + }, + "timestamp": { + "type": "date" + }, + "type": { + "type": "keyword" + } + } + }, + "agents": { + "properties": { + "access_api_key_id": { + "type": "keyword" + }, + "active": { + "type": "boolean" + }, + "config_id": { + "type": "keyword" + }, + "config_newest_revision": { + "type": "integer" + }, + "config_revision": { + "type": "integer" + }, + "current_error_events": { + "type": "text" + }, + "default_api_key": { + "type": "keyword" + }, + "enrolled_at": { + "type": "date" + }, + "last_checkin": { + "type": "date" + }, + "last_updated": { + "type": "date" + }, + "local_metadata": { + "type": "text" + }, + "shared_id": { + "type": "keyword" + }, + "type": { + "type": "keyword" + }, + "updated_at": { + "type": "date" + }, + "user_provided_metadata": { + "type": "text" + }, + "version": { + "type": "keyword" + } + } + }, "alert": { "properties": { "actions": { @@ -114,15 +275,18 @@ "apiKeyOwner": { "type": "keyword" }, + "consumer": { + "type": "keyword" + }, + "createdAt": { + "type": "date" + }, "createdBy": { "type": "keyword" }, "enabled": { "type": "boolean" }, - "interval": { - "type": "keyword" - }, "muteAll": { "type": "boolean" }, @@ -130,12 +294,24 @@ "type": "keyword" }, "name": { + "fields": { + "keyword": { + "type": "keyword" + } + }, "type": "text" }, "params": { "enabled": false, "type": "object" }, + "schedule": { + "properties": { + "interval": { + "type": "keyword" + } + } + }, "scheduledTaskId": { "type": "keyword" }, @@ -221,52 +397,947 @@ }, "apm-telemetry": { "properties": { - "has_any_services": { - "type": "boolean" - }, - "services_per_agent": { + "agents": { "properties": { "dotnet": { - "null_value": 0, - "type": "long" + "properties": { + "agent": { + "properties": { + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "service": { + "properties": { + "framework": { + "properties": { + "composite": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "language": { + "properties": { + "composite": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "runtime": { + "properties": { + "composite": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + } + } }, "go": { - "null_value": 0, - "type": "long" + "properties": { + "agent": { + "properties": { + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "service": { + "properties": { + "framework": { + "properties": { + "composite": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "language": { + "properties": { + "composite": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "runtime": { + "properties": { + "composite": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + } + } }, "java": { - "null_value": 0, - "type": "long" + "properties": { + "agent": { + "properties": { + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "service": { + "properties": { + "framework": { + "properties": { + "composite": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "language": { + "properties": { + "composite": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "runtime": { + "properties": { + "composite": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + } + } }, "js-base": { - "null_value": 0, - "type": "long" + "properties": { + "agent": { + "properties": { + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "service": { + "properties": { + "framework": { + "properties": { + "composite": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "language": { + "properties": { + "composite": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "runtime": { + "properties": { + "composite": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + } + } }, "nodejs": { - "null_value": 0, - "type": "long" + "properties": { + "agent": { + "properties": { + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "service": { + "properties": { + "framework": { + "properties": { + "composite": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "language": { + "properties": { + "composite": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "runtime": { + "properties": { + "composite": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + } + } }, "python": { - "null_value": 0, - "type": "long" + "properties": { + "agent": { + "properties": { + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "service": { + "properties": { + "framework": { + "properties": { + "composite": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "language": { + "properties": { + "composite": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "runtime": { + "properties": { + "composite": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + } + } }, "ruby": { - "null_value": 0, - "type": "long" + "properties": { + "agent": { + "properties": { + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "service": { + "properties": { + "framework": { + "properties": { + "composite": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "language": { + "properties": { + "composite": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "runtime": { + "properties": { + "composite": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + } + } }, "rum-js": { - "null_value": 0, - "type": "long" + "properties": { + "agent": { + "properties": { + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "service": { + "properties": { + "framework": { + "properties": { + "composite": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "language": { + "properties": { + "composite": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "runtime": { + "properties": { + "composite": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + } + } } } - } - } - }, - "canvas-element": { - "dynamic": "false", - "properties": { - "@created": { - "type": "date" + }, + "cardinality": { + "properties": { + "transaction": { + "properties": { + "name": { + "properties": { + "all_agents": { + "properties": { + "1d": { + "type": "long" + } + } + }, + "rum": { + "properties": { + "1d": { + "type": "long" + } + } + } + } + } + } + }, + "user_agent": { + "properties": { + "original": { + "properties": { + "all_agents": { + "properties": { + "1d": { + "type": "long" + } + } + }, + "rum": { + "properties": { + "1d": { + "type": "long" + } + } + } + } + } + } + } + } + }, + "counts": { + "properties": { + "agent_configuration": { + "properties": { + "all": { + "type": "long" + } + } + }, + "error": { + "properties": { + "1d": { + "type": "long" + }, + "all": { + "type": "long" + } + } + }, + "max_error_groups_per_service": { + "properties": { + "1d": { + "type": "long" + } + } + }, + "max_transaction_groups_per_service": { + "properties": { + "1d": { + "type": "long" + } + } + }, + "metric": { + "properties": { + "1d": { + "type": "long" + }, + "all": { + "type": "long" + } + } + }, + "onboarding": { + "properties": { + "1d": { + "type": "long" + }, + "all": { + "type": "long" + } + } + }, + "services": { + "properties": { + "1d": { + "type": "long" + } + } + }, + "sourcemap": { + "properties": { + "1d": { + "type": "long" + }, + "all": { + "type": "long" + } + } + }, + "span": { + "properties": { + "1d": { + "type": "long" + }, + "all": { + "type": "long" + } + } + }, + "traces": { + "properties": { + "1d": { + "type": "long" + } + } + }, + "transaction": { + "properties": { + "1d": { + "type": "long" + }, + "all": { + "type": "long" + } + } + } + } + }, + "has_any_services": { + "type": "boolean" + }, + "indices": { + "properties": { + "all": { + "properties": { + "total": { + "properties": { + "docs": { + "properties": { + "count": { + "type": "long" + } + } + }, + "store": { + "properties": { + "size_in_bytes": { + "type": "long" + } + } + } + } + } + } + }, + "shards": { + "properties": { + "total": { + "type": "long" + } + } + } + } + }, + "integrations": { + "properties": { + "ml": { + "properties": { + "all_jobs_count": { + "type": "long" + } + } + } + } + }, + "retainment": { + "properties": { + "error": { + "properties": { + "ms": { + "type": "long" + } + } + }, + "metric": { + "properties": { + "ms": { + "type": "long" + } + } + }, + "onboarding": { + "properties": { + "ms": { + "type": "long" + } + } + }, + "span": { + "properties": { + "ms": { + "type": "long" + } + } + }, + "transaction": { + "properties": { + "ms": { + "type": "long" + } + } + } + } + }, + "services_per_agent": { + "properties": { + "dotnet": { + "null_value": 0, + "type": "long" + }, + "go": { + "null_value": 0, + "type": "long" + }, + "java": { + "null_value": 0, + "type": "long" + }, + "js-base": { + "null_value": 0, + "type": "long" + }, + "nodejs": { + "null_value": 0, + "type": "long" + }, + "python": { + "null_value": 0, + "type": "long" + }, + "ruby": { + "null_value": 0, + "type": "long" + }, + "rum-js": { + "null_value": 0, + "type": "long" + } + } + }, + "tasks": { + "properties": { + "agent_configuration": { + "properties": { + "took": { + "properties": { + "ms": { + "type": "long" + } + } + } + } + }, + "agents": { + "properties": { + "took": { + "properties": { + "ms": { + "type": "long" + } + } + } + } + }, + "cardinality": { + "properties": { + "took": { + "properties": { + "ms": { + "type": "long" + } + } + } + } + }, + "groupings": { + "properties": { + "took": { + "properties": { + "ms": { + "type": "long" + } + } + } + } + }, + "indices_stats": { + "properties": { + "took": { + "properties": { + "ms": { + "type": "long" + } + } + } + } + }, + "integrations": { + "properties": { + "took": { + "properties": { + "ms": { + "type": "long" + } + } + } + } + }, + "processor_events": { + "properties": { + "took": { + "properties": { + "ms": { + "type": "long" + } + } + } + } + }, + "services": { + "properties": { + "took": { + "properties": { + "ms": { + "type": "long" + } + } + } + } + }, + "versions": { + "properties": { + "took": { + "properties": { + "ms": { + "type": "long" + } + } + } + } + } + } + }, + "version": { + "properties": { + "apm_server": { + "properties": { + "major": { + "type": "long" + }, + "minor": { + "type": "long" + }, + "patch": { + "type": "long" + } + } + } + } + } + } + }, + "application_usage_totals": { + "properties": { + "appId": { + "type": "keyword" + }, + "minutesOnScreen": { + "type": "float" + }, + "numberOfClicks": { + "type": "long" + } + } + }, + "application_usage_transactional": { + "properties": { + "appId": { + "type": "keyword" + }, + "minutesOnScreen": { + "type": "float" + }, + "numberOfClicks": { + "type": "long" + }, + "timestamp": { + "type": "date" + } + } + }, + "canvas-element": { + "dynamic": "false", + "properties": { + "@created": { + "type": "date" }, "@timestamp": { "type": "date" @@ -285,26 +1356,257 @@ "keyword": { "type": "keyword" } - }, - "type": "text" + }, + "type": "text" + } + } + }, + "canvas-workpad": { + "dynamic": "false", + "properties": { + "@created": { + "type": "date" + }, + "@timestamp": { + "type": "date" + }, + "name": { + "fields": { + "keyword": { + "type": "keyword" + } + }, + "type": "text" + } + } + }, + "cases": { + "properties": { + "closed_at": { + "type": "date" + }, + "closed_by": { + "properties": { + "email": { + "type": "keyword" + }, + "full_name": { + "type": "keyword" + }, + "username": { + "type": "keyword" + } + } + }, + "created_at": { + "type": "date" + }, + "created_by": { + "properties": { + "email": { + "type": "keyword" + }, + "full_name": { + "type": "keyword" + }, + "username": { + "type": "keyword" + } + } + }, + "description": { + "type": "text" + }, + "external_service": { + "properties": { + "connector_id": { + "type": "keyword" + }, + "connector_name": { + "type": "keyword" + }, + "external_id": { + "type": "keyword" + }, + "external_title": { + "type": "text" + }, + "external_url": { + "type": "text" + }, + "pushed_at": { + "type": "date" + }, + "pushed_by": { + "properties": { + "email": { + "type": "keyword" + }, + "full_name": { + "type": "keyword" + }, + "username": { + "type": "keyword" + } + } + } + } + }, + "status": { + "type": "keyword" + }, + "tags": { + "type": "keyword" + }, + "title": { + "type": "keyword" + }, + "updated_at": { + "type": "date" + }, + "updated_by": { + "properties": { + "email": { + "type": "keyword" + }, + "full_name": { + "type": "keyword" + }, + "username": { + "type": "keyword" + } + } + } + } + }, + "cases-comments": { + "properties": { + "comment": { + "type": "text" + }, + "created_at": { + "type": "date" + }, + "created_by": { + "properties": { + "email": { + "type": "keyword" + }, + "full_name": { + "type": "keyword" + }, + "username": { + "type": "keyword" + } + } + }, + "pushed_at": { + "type": "date" + }, + "pushed_by": { + "properties": { + "email": { + "type": "keyword" + }, + "full_name": { + "type": "keyword" + }, + "username": { + "type": "keyword" + } + } + }, + "updated_at": { + "type": "date" + }, + "updated_by": { + "properties": { + "email": { + "type": "keyword" + }, + "full_name": { + "type": "keyword" + }, + "username": { + "type": "keyword" + } + } + } + } + }, + "cases-configure": { + "properties": { + "closure_type": { + "type": "keyword" + }, + "connector_id": { + "type": "keyword" + }, + "connector_name": { + "type": "keyword" + }, + "created_at": { + "type": "date" + }, + "created_by": { + "properties": { + "email": { + "type": "keyword" + }, + "full_name": { + "type": "keyword" + }, + "username": { + "type": "keyword" + } + } + }, + "updated_at": { + "type": "date" + }, + "updated_by": { + "properties": { + "email": { + "type": "keyword" + }, + "full_name": { + "type": "keyword" + }, + "username": { + "type": "keyword" + } + } } } }, - "canvas-workpad": { - "dynamic": "false", + "cases-user-actions": { "properties": { - "@created": { - "type": "date" + "action": { + "type": "keyword" }, - "@timestamp": { + "action_at": { "type": "date" }, - "name": { - "fields": { - "keyword": { + "action_by": { + "properties": { + "email": { + "type": "keyword" + }, + "full_name": { + "type": "keyword" + }, + "username": { "type": "keyword" } - }, + } + }, + "action_field": { + "type": "keyword" + }, + "new_value": { + "type": "text" + }, + "old_value": { "type": "text" } } @@ -389,6 +1691,136 @@ } } }, + "datasources": { + "properties": { + "config_id": { + "type": "keyword" + }, + "description": { + "type": "text" + }, + "enabled": { + "type": "boolean" + }, + "inputs": { + "properties": { + "config": { + "type": "flattened" + }, + "enabled": { + "type": "boolean" + }, + "processors": { + "type": "keyword" + }, + "streams": { + "properties": { + "config": { + "type": "flattened" + }, + "dataset": { + "type": "keyword" + }, + "enabled": { + "type": "boolean" + }, + "id": { + "type": "keyword" + }, + "processors": { + "type": "keyword" + } + }, + "type": "nested" + }, + "type": { + "type": "keyword" + } + }, + "type": "nested" + }, + "name": { + "type": "keyword" + }, + "namespace": { + "type": "keyword" + }, + "output_id": { + "type": "keyword" + }, + "package": { + "properties": { + "name": { + "type": "keyword" + }, + "title": { + "type": "keyword" + }, + "version": { + "type": "keyword" + } + } + }, + "revision": { + "type": "integer" + } + } + }, + "enrollment_api_keys": { + "properties": { + "active": { + "type": "boolean" + }, + "api_key": { + "type": "binary" + }, + "api_key_id": { + "type": "keyword" + }, + "config_id": { + "type": "keyword" + }, + "created_at": { + "type": "date" + }, + "expire_at": { + "type": "date" + }, + "name": { + "type": "keyword" + }, + "type": { + "type": "keyword" + }, + "updated_at": { + "type": "date" + } + } + }, + "epm-package": { + "properties": { + "installed": { + "properties": { + "id": { + "type": "keyword" + }, + "type": { + "type": "keyword" + } + }, + "type": "nested" + }, + "internal": { + "type": "boolean" + }, + "name": { + "type": "keyword" + }, + "version": { + "type": "keyword" + } + } + }, "file-upload-telemetry": { "properties": { "filesUploadedTotalCount": { @@ -538,6 +1970,26 @@ } } }, + "customMetrics": { + "properties": { + "aggregation": { + "type": "keyword" + }, + "field": { + "type": "keyword" + }, + "id": { + "type": "keyword" + }, + "label": { + "type": "keyword" + }, + "type": { + "type": "keyword" + } + }, + "type": "nested" + }, "customOptions": { "properties": { "field": { @@ -572,6 +2024,18 @@ }, "metric": { "properties": { + "aggregation": { + "type": "keyword" + }, + "field": { + "type": "keyword" + }, + "id": { + "type": "keyword" + }, + "label": { + "type": "keyword" + }, "type": { "type": "keyword" } @@ -699,9 +2163,19 @@ } } }, + "indexPatternsWithGeoFieldCount": { + "type": "long" + }, "mapsTotalCount": { "type": "long" }, + "settings": { + "properties": { + "showMapVisualizationTypes": { + "type": "boolean" + } + } + }, "timeCaptured": { "type": "date" } @@ -803,7 +2277,7 @@ }, "type": "text" }, - "space": { + "visualization": { "fields": { "keyword": { "ignore_above": 256, @@ -828,6 +2302,37 @@ "namespace": { "type": "keyword" }, + "outputs": { + "properties": { + "api_key": { + "type": "keyword" + }, + "ca_sha256": { + "type": "keyword" + }, + "config": { + "type": "flattened" + }, + "fleet_enroll_password": { + "type": "binary" + }, + "fleet_enroll_username": { + "type": "binary" + }, + "hosts": { + "type": "keyword" + }, + "is_default": { + "type": "boolean" + }, + "name": { + "type": "keyword" + }, + "type": { + "type": "keyword" + } + } + }, "query": { "properties": { "description": { @@ -917,6 +2422,73 @@ } } }, + "siem-detection-engine-rule-actions": { + "properties": { + "actions": { + "properties": { + "action_type_id": { + "type": "keyword" + }, + "group": { + "type": "keyword" + }, + "id": { + "type": "keyword" + }, + "params": { + "dynamic": "true", + "type": "object" + } + } + }, + "alertThrottle": { + "type": "keyword" + }, + "ruleAlertId": { + "type": "keyword" + }, + "ruleThrottle": { + "type": "keyword" + } + } + }, + "siem-detection-engine-rule-status": { + "properties": { + "alertId": { + "type": "keyword" + }, + "bulkCreateTimeDurations": { + "type": "float" + }, + "gap": { + "type": "text" + }, + "lastFailureAt": { + "type": "date" + }, + "lastFailureMessage": { + "type": "text" + }, + "lastLookBackDate": { + "type": "date" + }, + "lastSuccessAt": { + "type": "date" + }, + "lastSuccessMessage": { + "type": "text" + }, + "searchAfterTimeDurations": { + "type": "float" + }, + "status": { + "type": "keyword" + }, + "statusDate": { + "type": "date" + } + } + }, "siem-ui-timeline": { "properties": { "columns": { @@ -1051,6 +2623,9 @@ "description": { "type": "text" }, + "eventType": { + "type": "keyword" + }, "favorite": { "properties": { "favoriteDate": { @@ -1255,6 +2830,9 @@ }, "telemetry": { "properties": { + "allowChangingOptInStatus": { + "type": "boolean" + }, "enabled": { "type": "boolean" }, @@ -1262,11 +2840,15 @@ "type": "date" }, "lastVersionChecked": { - "ignore_above": 256, + "type": "keyword" + }, + "reportFailureCount": { + "type": "integer" + }, + "reportFailureVersion": { "type": "keyword" }, "sendUsageFrom": { - "ignore_above": 256, "type": "keyword" }, "userHasSeenNotice": { @@ -1315,6 +2897,13 @@ } } }, + "tsvb-validation-telemetry": { + "properties": { + "failedRequests": { + "type": "long" + } + } + }, "type": { "type": "keyword" }, @@ -1391,6 +2980,13 @@ } } }, + "uptime-dynamic-settings": { + "properties": { + "heartbeatIndices": { + "type": "keyword" + } + } + }, "url": { "properties": { "accessCount": { diff --git a/x-pack/test/functional/es_archives/reporting/historic/data.json.gz b/x-pack/test/functional/es_archives/reporting/historic/data.json.gz deleted file mode 100644 index ecb85ec6faca4..0000000000000 Binary files a/x-pack/test/functional/es_archives/reporting/historic/data.json.gz and /dev/null differ diff --git a/x-pack/test/functional/es_archives/reporting/historic/mappings.json b/x-pack/test/functional/es_archives/reporting/historic/mappings.json deleted file mode 100644 index 3a5af0158ce00..0000000000000 --- a/x-pack/test/functional/es_archives/reporting/historic/mappings.json +++ /dev/null @@ -1,386 +0,0 @@ -{ - "type": "index", - "value": { - "index": ".kibana", - "mappings": { - "properties": { - "config": { - "dynamic": "true", - "properties": { - "buildNum": { - "type": "keyword" - }, - "defaultIndex": { - "fields": { - "keyword": { - "ignore_above": 256, - "type": "keyword" - } - }, - "type": "text" - }, - "xPackMonitoring:showBanner": { - "type": "boolean" - } - } - }, - "dashboard": { - "dynamic": "strict", - "properties": { - "description": { - "type": "text" - }, - "hits": { - "type": "integer" - }, - "kibanaSavedObjectMeta": { - "properties": { - "searchSourceJSON": { - "type": "text" - } - } - }, - "optionsJSON": { - "type": "text" - }, - "panelsJSON": { - "type": "text" - }, - "refreshInterval": { - "properties": { - "display": { - "type": "keyword" - }, - "pause": { - "type": "boolean" - }, - "section": { - "type": "integer" - }, - "value": { - "type": "integer" - } - } - }, - "timeFrom": { - "type": "keyword" - }, - "timeRestore": { - "type": "boolean" - }, - "timeTo": { - "type": "keyword" - }, - "title": { - "type": "text" - }, - "uiStateJSON": { - "type": "text" - }, - "version": { - "type": "integer" - } - } - }, - "graph-workspace": { - "dynamic": "strict", - "properties": { - "description": { - "type": "text" - }, - "kibanaSavedObjectMeta": { - "properties": { - "searchSourceJSON": { - "type": "text" - } - } - }, - "numLinks": { - "type": "integer" - }, - "numVertices": { - "type": "integer" - }, - "title": { - "type": "text" - }, - "version": { - "type": "integer" - }, - "wsState": { - "type": "text" - } - } - }, - "index-pattern": { - "dynamic": "strict", - "properties": { - "fieldFormatMap": { - "type": "text" - }, - "fields": { - "type": "text" - }, - "intervalName": { - "type": "keyword" - }, - "notExpandable": { - "type": "boolean" - }, - "sourceFilters": { - "type": "text" - }, - "timeFieldName": { - "type": "keyword" - }, - "title": { - "type": "text" - } - } - }, - "search": { - "dynamic": "strict", - "properties": { - "columns": { - "type": "keyword" - }, - "description": { - "type": "text" - }, - "hits": { - "type": "integer" - }, - "kibanaSavedObjectMeta": { - "properties": { - "searchSourceJSON": { - "type": "text" - } - } - }, - "sort": { - "type": "keyword" - }, - "title": { - "type": "text" - }, - "version": { - "type": "integer" - } - } - }, - "server": { - "dynamic": "strict", - "properties": { - "uuid": { - "type": "keyword" - } - } - }, - "space": { - "properties": { - "_reserved": { - "type": "boolean" - }, - "color": { - "type": "keyword" - }, - "description": { - "type": "text" - }, - "initials": { - "type": "keyword" - }, - "name": { - "fields": { - "keyword": { - "ignore_above": 2048, - "type": "keyword" - } - }, - "type": "text" - } - } - }, - "spaceId": { - "type": "keyword" - }, - "timelion-sheet": { - "dynamic": "strict", - "properties": { - "description": { - "type": "text" - }, - "hits": { - "type": "integer" - }, - "kibanaSavedObjectMeta": { - "properties": { - "searchSourceJSON": { - "type": "text" - } - } - }, - "timelion_chart_height": { - "type": "integer" - }, - "timelion_columns": { - "type": "integer" - }, - "timelion_interval": { - "type": "keyword" - }, - "timelion_other_interval": { - "type": "keyword" - }, - "timelion_rows": { - "type": "integer" - }, - "timelion_sheet": { - "type": "text" - }, - "title": { - "type": "text" - }, - "version": { - "type": "integer" - } - } - }, - "type": { - "type": "keyword" - }, - "updated_at": { - "type": "date" - }, - "url": { - "dynamic": "strict", - "properties": { - "accessCount": { - "type": "long" - }, - "accessDate": { - "type": "date" - }, - "createDate": { - "type": "date" - }, - "url": { - "fields": { - "keyword": { - "ignore_above": 2048, - "type": "keyword" - } - }, - "type": "text" - } - } - }, - "visualization": { - "dynamic": "strict", - "properties": { - "description": { - "type": "text" - }, - "kibanaSavedObjectMeta": { - "properties": { - "searchSourceJSON": { - "type": "text" - } - } - }, - "savedSearchId": { - "type": "keyword" - }, - "title": { - "type": "text" - }, - "uiStateJSON": { - "type": "text" - }, - "version": { - "type": "integer" - }, - "visState": { - "type": "text" - } - } - } - } - }, - "settings": { - "index": { - "number_of_replicas": "1", - "number_of_shards": "1" - } - } - } -} - -{ - "type": "index", - "value": { - "index": ".reporting-2018.03.11", - "mappings": { - "properties": { - "attempts": { - "type": "short" - }, - "completed_at": { - "type": "date" - }, - "created_at": { - "type": "date" - }, - "created_by": { - "type": "keyword" - }, - "jobtype": { - "type": "keyword" - }, - "max_attempts": { - "type": "short" - }, - "output": { - "properties": { - "content": { - "enabled": false, - "type": "object" - }, - "content_type": { - "type": "keyword" - }, - "max_size_reached": { - "type": "boolean" - } - } - }, - "payload": { - "enabled": false, - "type": "object" - }, - "priority": { - "type": "byte" - }, - "process_expiration": { - "type": "date" - }, - "started_at": { - "type": "date" - }, - "status": { - "type": "keyword" - }, - "timeout": { - "type": "long" - } - } - }, - "settings": { - "index": { - "auto_expand_replicas": "0-1", - "number_of_replicas": "0", - "number_of_shards": "1" - } - } - } -} \ No newline at end of file diff --git a/x-pack/test/functional/es_archives/uptime/blank/mappings.json b/x-pack/test/functional/es_archives/uptime/blank/mappings.json index 7879c82612a96..fff4ef47bce0c 100644 --- a/x-pack/test/functional/es_archives/uptime/blank/mappings.json +++ b/x-pack/test/functional/es_archives/uptime/blank/mappings.json @@ -12,79 +12,115 @@ "beat": "heartbeat", "version": "8.0.0" }, - "date_detection": false, "dynamic_templates": [ { "labels": { + "path_match": "labels.*", + "match_mapping_type": "string", "mapping": { "type": "keyword" - }, - "match_mapping_type": "string", - "path_match": "labels.*" + } } }, { "container.labels": { + "path_match": "container.labels.*", + "match_mapping_type": "string", "mapping": { "type": "keyword" - }, - "match_mapping_type": "string", - "path_match": "container.labels.*" + } } }, { "dns.answers": { + "path_match": "dns.answers.*", + "match_mapping_type": "string", "mapping": { "type": "keyword" - }, + } + } + }, + { + "log.syslog": { + "path_match": "log.syslog.*", "match_mapping_type": "string", - "path_match": "dns.answers.*" + "mapping": { + "type": "keyword" + } } }, { - "fields": { + "network.inner": { + "path_match": "network.inner.*", + "match_mapping_type": "string", "mapping": { "type": "keyword" - }, + } + } + }, + { + "observer.egress": { + "path_match": "observer.egress.*", "match_mapping_type": "string", - "path_match": "fields.*" + "mapping": { + "type": "keyword" + } } }, { - "docker.container.labels": { + "observer.ingress": { + "path_match": "observer.ingress.*", + "match_mapping_type": "string", "mapping": { "type": "keyword" - }, + } + } + }, + { + "fields": { + "path_match": "fields.*", + "match_mapping_type": "string", + "mapping": { + "type": "keyword" + } + } + }, + { + "docker.container.labels": { + "path_match": "docker.container.labels.*", "match_mapping_type": "string", - "path_match": "docker.container.labels.*" + "mapping": { + "type": "keyword" + } } }, { "kubernetes.labels.*": { + "path_match": "kubernetes.labels.*", "mapping": { "type": "keyword" - }, - "path_match": "kubernetes.labels.*" + } } }, { "kubernetes.annotations.*": { + "path_match": "kubernetes.annotations.*", "mapping": { "type": "keyword" - }, - "path_match": "kubernetes.annotations.*" + } } }, { "strings_as_keyword": { + "match_mapping_type": "string", "mapping": { "ignore_above": 1024, "type": "keyword" - }, - "match_mapping_type": "string" + } } } ], + "date_detection": false, "properties": { "@timestamp": { "type": "date" @@ -92,28 +128,28 @@ "agent": { "properties": { "ephemeral_id": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "hostname": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "id": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "name": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "type": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "version": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 } } }, @@ -125,8 +161,14 @@ "organization": { "properties": { "name": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "fields": { + "text": { + "type": "text", + "norms": false + } + }, + "ignore_above": 1024 } } } @@ -135,8 +177,8 @@ "client": { "properties": { "address": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "as": { "properties": { @@ -146,8 +188,14 @@ "organization": { "properties": { "name": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "fields": { + "text": { + "type": "text", + "norms": false + } + }, + "ignore_above": 1024 } } } @@ -157,41 +205,41 @@ "type": "long" }, "domain": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "geo": { "properties": { "city_name": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "continent_name": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "country_iso_code": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "country_name": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "location": { "type": "geo_point" }, "name": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "region_iso_code": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "region_name": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 } } }, @@ -199,8 +247,8 @@ "type": "ip" }, "mac": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "nat": { "properties": { @@ -218,43 +266,67 @@ "port": { "type": "long" }, + "registered_domain": { + "type": "keyword", + "ignore_above": 1024 + }, + "top_level_domain": { + "type": "keyword", + "ignore_above": 1024 + }, "user": { "properties": { "domain": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "email": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "full_name": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "fields": { + "text": { + "type": "text", + "norms": false + } + }, + "ignore_above": 1024 }, "group": { "properties": { + "domain": { + "type": "keyword", + "ignore_above": 1024 + }, "id": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "name": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 } } }, "hash": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "id": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "name": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "fields": { + "text": { + "type": "text", + "norms": false + } + }, + "ignore_above": 1024 } } } @@ -265,76 +337,97 @@ "account": { "properties": { "id": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 } } }, "availability_zone": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "image": { "properties": { "id": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 } } }, "instance": { "properties": { "id": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "name": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 } } }, "machine": { "properties": { "type": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 } } }, "project": { "properties": { "id": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 } } }, "provider": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "region": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 + } + } + }, + "code_signature": { + "properties": { + "exists": { + "type": "boolean" + }, + "status": { + "type": "keyword", + "ignore_above": 1024 + }, + "subject_name": { + "type": "keyword", + "ignore_above": 1024 + }, + "trusted": { + "type": "boolean" + }, + "valid": { + "type": "boolean" } } }, "container": { "properties": { "id": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "image": { "properties": { "name": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "tag": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 } } }, @@ -342,20 +435,20 @@ "type": "object" }, "name": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "runtime": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 } } }, "destination": { "properties": { "address": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "as": { "properties": { @@ -365,8 +458,14 @@ "organization": { "properties": { "name": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "fields": { + "text": { + "type": "text", + "norms": false + } + }, + "ignore_above": 1024 } } } @@ -376,41 +475,41 @@ "type": "long" }, "domain": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "geo": { "properties": { "city_name": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "continent_name": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "country_iso_code": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "country_name": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "location": { "type": "geo_point" }, "name": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "region_iso_code": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "region_name": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 } } }, @@ -418,8 +517,8 @@ "type": "ip" }, "mac": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "nat": { "properties": { @@ -437,43 +536,144 @@ "port": { "type": "long" }, + "registered_domain": { + "type": "keyword", + "ignore_above": 1024 + }, + "top_level_domain": { + "type": "keyword", + "ignore_above": 1024 + }, "user": { "properties": { "domain": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "email": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "full_name": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "fields": { + "text": { + "type": "text", + "norms": false + } + }, + "ignore_above": 1024 }, "group": { "properties": { + "domain": { + "type": "keyword", + "ignore_above": 1024 + }, "id": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "name": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 } } }, "hash": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "id": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "name": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "fields": { + "text": { + "type": "text", + "norms": false + } + }, + "ignore_above": 1024 + } + } + } + } + }, + "dll": { + "properties": { + "code_signature": { + "properties": { + "exists": { + "type": "boolean" + }, + "status": { + "type": "keyword", + "ignore_above": 1024 + }, + "subject_name": { + "type": "keyword", + "ignore_above": 1024 + }, + "trusted": { + "type": "boolean" + }, + "valid": { + "type": "boolean" + } + } + }, + "hash": { + "properties": { + "md5": { + "type": "keyword", + "ignore_above": 1024 + }, + "sha1": { + "type": "keyword", + "ignore_above": 1024 + }, + "sha256": { + "type": "keyword", + "ignore_above": 1024 + }, + "sha512": { + "type": "keyword", + "ignore_above": 1024 + } + } + }, + "name": { + "type": "keyword", + "ignore_above": 1024 + }, + "path": { + "type": "keyword", + "ignore_above": 1024 + }, + "pe": { + "properties": { + "company": { + "type": "keyword", + "ignore_above": 1024 + }, + "description": { + "type": "keyword", + "ignore_above": 1024 + }, + "file_version": { + "type": "keyword", + "ignore_above": 1024 + }, + "original_file_name": { + "type": "keyword", + "ignore_above": 1024 + }, + "product": { + "type": "keyword", + "ignore_above": 1024 } } } @@ -484,55 +684,63 @@ "answers": { "properties": { "class": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "data": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "name": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "ttl": { "type": "long" }, "type": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 } } }, "header_flags": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "id": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "op_code": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "question": { "properties": { "class": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "name": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "registered_domain": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 + }, + "subdomain": { + "type": "keyword", + "ignore_above": 1024 + }, + "top_level_domain": { + "type": "keyword", + "ignore_above": 1024 }, "type": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 } } }, @@ -540,12 +748,12 @@ "type": "ip" }, "response_code": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "type": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 } } }, @@ -563,51 +771,61 @@ "ecs": { "properties": { "version": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 } } }, "error": { "properties": { "code": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "id": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "message": { - "norms": false, - "type": "text" + "type": "text", + "norms": false + }, + "stack_trace": { + "type": "keyword", + "fields": { + "text": { + "type": "text", + "norms": false + } + }, + "ignore_above": 1024 }, "type": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 } } }, "event": { "properties": { "action": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "category": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "code": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "created": { "type": "date" }, "dataset": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "duration": { "type": "long" @@ -616,32 +834,39 @@ "type": "date" }, "hash": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "id": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 + }, + "ingested": { + "type": "date" }, "kind": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "module": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "original": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "outcome": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "provider": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 + }, + "reference": { + "type": "keyword", + "ignore_above": 1024 }, "risk_score": { "type": "float" @@ -659,12 +884,16 @@ "type": "date" }, "timezone": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "type": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 + }, + "url": { + "type": "keyword", + "ignore_above": 1024 } } }, @@ -676,6 +905,31 @@ "accessed": { "type": "date" }, + "attributes": { + "type": "keyword", + "ignore_above": 1024 + }, + "code_signature": { + "properties": { + "exists": { + "type": "boolean" + }, + "status": { + "type": "keyword", + "ignore_above": 1024 + }, + "subject_name": { + "type": "keyword", + "ignore_above": 1024 + }, + "trusted": { + "type": "boolean" + }, + "valid": { + "type": "boolean" + } + } + }, "created": { "type": "date" }, @@ -683,254 +937,318 @@ "type": "date" }, "device": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "directory": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 + }, + "drive_letter": { + "type": "keyword", + "ignore_above": 1 }, "extension": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "gid": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "group": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "hash": { "properties": { "md5": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "sha1": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "sha256": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "sha512": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 } } }, "inode": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 + }, + "mime_type": { + "type": "keyword", + "ignore_above": 1024 }, "mode": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "mtime": { "type": "date" }, "name": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "owner": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "path": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "fields": { + "text": { + "type": "text", + "norms": false + } + }, + "ignore_above": 1024 + }, + "pe": { + "properties": { + "company": { + "type": "keyword", + "ignore_above": 1024 + }, + "description": { + "type": "keyword", + "ignore_above": 1024 + }, + "file_version": { + "type": "keyword", + "ignore_above": 1024 + }, + "original_file_name": { + "type": "keyword", + "ignore_above": 1024 + }, + "product": { + "type": "keyword", + "ignore_above": 1024 + } + } }, "size": { "type": "long" }, "target_path": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "fields": { + "text": { + "type": "text", + "norms": false + } + }, + "ignore_above": 1024 }, "type": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "uid": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 } } }, "geo": { "properties": { "city_name": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "continent_name": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "country_iso_code": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "country_name": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "location": { "type": "geo_point" }, "name": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "region_iso_code": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "region_name": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 } } }, "group": { "properties": { + "domain": { + "type": "keyword", + "ignore_above": 1024 + }, "id": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "name": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 } } }, "hash": { "properties": { "md5": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "sha1": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "sha256": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "sha512": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 } } }, "host": { "properties": { "architecture": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "containerized": { "type": "boolean" }, + "domain": { + "type": "keyword", + "ignore_above": 1024 + }, "geo": { "properties": { "city_name": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "continent_name": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "country_iso_code": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "country_name": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "location": { "type": "geo_point" }, "name": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "region_iso_code": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "region_name": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 } } }, "hostname": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "id": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "ip": { "type": "ip" }, "mac": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "name": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "os": { "properties": { "build": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "codename": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "family": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "full": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "fields": { + "text": { + "type": "text", + "norms": false + } + }, + "ignore_above": 1024 }, "kernel": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "name": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "fields": { + "text": { + "type": "text", + "norms": false + } + }, + "ignore_above": 1024 }, "platform": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "version": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 } } }, "type": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "uptime": { "type": "long" @@ -938,40 +1256,56 @@ "user": { "properties": { "domain": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "email": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "full_name": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "fields": { + "text": { + "type": "text", + "norms": false + } + }, + "ignore_above": 1024 }, "group": { "properties": { + "domain": { + "type": "keyword", + "ignore_above": 1024 + }, "id": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "name": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 } } }, "hash": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "id": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "name": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "fields": { + "text": { + "type": "text", + "norms": false + } + }, + "ignore_above": 1024 } } } @@ -987,8 +1321,14 @@ "type": "long" }, "content": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "fields": { + "text": { + "type": "text", + "norms": false + } + }, + "ignore_above": 1024 } } }, @@ -996,12 +1336,12 @@ "type": "long" }, "method": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "referrer": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 } } }, @@ -1013,18 +1353,28 @@ "type": "long" }, "content": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "fields": { + "text": { + "type": "text", + "norms": false + } + }, + "ignore_above": 1024 }, "hash": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 } } }, "bytes": { "type": "long" }, + "redirects": { + "type": "keyword", + "ignore_above": 1024 + }, "status_code": { "type": "long" } @@ -1077,8 +1427,8 @@ } }, "version": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 } } }, @@ -1096,17 +1446,33 @@ } } }, + "interface": { + "properties": { + "alias": { + "type": "keyword", + "ignore_above": 1024 + }, + "id": { + "type": "keyword", + "ignore_above": 1024 + }, + "name": { + "type": "keyword", + "ignore_above": 1024 + } + } + }, "jolokia": { "properties": { "agent": { "properties": { "id": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "version": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 } } }, @@ -1116,22 +1482,22 @@ "server": { "properties": { "product": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "vendor": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "version": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 } } }, "url": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 } } }, @@ -1147,20 +1513,20 @@ "container": { "properties": { "image": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "name": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 } } }, "deployment": { "properties": { "name": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 } } }, @@ -1172,42 +1538,42 @@ } }, "namespace": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "node": { "properties": { "name": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 } } }, "pod": { "properties": { "name": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "uid": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 } } }, "replicaset": { "properties": { "name": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 } } }, "statefulset": { "properties": { "name": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 } } } @@ -1219,28 +1585,76 @@ "log": { "properties": { "level": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "logger": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 + }, + "origin": { + "properties": { + "file": { + "properties": { + "line": { + "type": "long" + }, + "name": { + "type": "keyword", + "ignore_above": 1024 + } + } + }, + "function": { + "type": "keyword", + "ignore_above": 1024 + } + } }, "original": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 + }, + "syslog": { + "properties": { + "facility": { + "properties": { + "code": { + "type": "long" + }, + "name": { + "type": "keyword", + "ignore_above": 1024 + } + } + }, + "priority": { + "type": "long" + }, + "severity": { + "properties": { + "code": { + "type": "long" + }, + "name": { + "type": "keyword", + "ignore_above": 1024 + } + } + } + } } } }, "message": { - "norms": false, - "type": "text" + "type": "text", + "norms": false }, "monitor": { "properties": { "check_group": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "duration": { "properties": { @@ -1250,282 +1664,766 @@ } }, "id": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "ip": { "type": "ip" }, "name": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "status": { - "ignore_above": 1024, - "type": "keyword" - }, - "type": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "timespan": { "type": "date_range" + }, + "type": { + "type": "keyword", + "ignore_above": 1024 } } }, "network": { "properties": { "application": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "bytes": { "type": "long" }, "community_id": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "direction": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "forwarded_ip": { "type": "ip" }, "iana_number": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 + }, + "inner": { + "properties": { + "vlan": { + "properties": { + "id": { + "type": "keyword", + "ignore_above": 1024 + }, + "name": { + "type": "keyword", + "ignore_above": 1024 + } + } + } + } }, "name": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "packets": { "type": "long" }, "protocol": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "transport": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "type": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 + }, + "vlan": { + "properties": { + "id": { + "type": "keyword", + "ignore_above": 1024 + }, + "name": { + "type": "keyword", + "ignore_above": 1024 + } + } } } }, "observer": { "properties": { + "egress": { + "properties": { + "interface": { + "properties": { + "alias": { + "type": "keyword", + "ignore_above": 1024 + }, + "id": { + "type": "keyword", + "ignore_above": 1024 + }, + "name": { + "type": "keyword", + "ignore_above": 1024 + } + } + }, + "vlan": { + "properties": { + "id": { + "type": "keyword", + "ignore_above": 1024 + }, + "name": { + "type": "keyword", + "ignore_above": 1024 + } + } + }, + "zone": { + "type": "keyword", + "ignore_above": 1024 + } + } + }, "geo": { "properties": { "city_name": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "continent_name": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "country_iso_code": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "country_name": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "location": { "type": "geo_point" }, "name": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "region_iso_code": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "region_name": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 } } }, "hostname": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 + }, + "ingress": { + "properties": { + "interface": { + "properties": { + "alias": { + "type": "keyword", + "ignore_above": 1024 + }, + "id": { + "type": "keyword", + "ignore_above": 1024 + }, + "name": { + "type": "keyword", + "ignore_above": 1024 + } + } + }, + "vlan": { + "properties": { + "id": { + "type": "keyword", + "ignore_above": 1024 + }, + "name": { + "type": "keyword", + "ignore_above": 1024 + } + } + }, + "zone": { + "type": "keyword", + "ignore_above": 1024 + } + } }, "ip": { "type": "ip" }, "mac": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 + }, + "name": { + "type": "keyword", + "ignore_above": 1024 }, "os": { "properties": { "family": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "full": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "fields": { + "text": { + "type": "text", + "norms": false + } + }, + "ignore_above": 1024 }, "kernel": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "name": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "fields": { + "text": { + "type": "text", + "norms": false + } + }, + "ignore_above": 1024 }, "platform": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "version": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 } } }, + "product": { + "type": "keyword", + "ignore_above": 1024 + }, "serial_number": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "type": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "vendor": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "version": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 } } }, "organization": { "properties": { "id": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "name": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "fields": { + "text": { + "type": "text", + "norms": false + } + }, + "ignore_above": 1024 } } }, "os": { "properties": { "family": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "full": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "fields": { + "text": { + "type": "text", + "norms": false + } + }, + "ignore_above": 1024 }, "kernel": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "name": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "fields": { + "text": { + "type": "text", + "norms": false + } + }, + "ignore_above": 1024 }, "platform": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "version": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 } } }, - "process": { + "package": { "properties": { - "args": { - "ignore_above": 1024, - "type": "keyword" + "architecture": { + "type": "keyword", + "ignore_above": 1024 }, - "executable": { - "ignore_above": 1024, - "type": "keyword" + "build_version": { + "type": "keyword", + "ignore_above": 1024 }, - "hash": { - "properties": { - "md5": { - "ignore_above": 1024, - "type": "keyword" - }, - "sha1": { - "ignore_above": 1024, - "type": "keyword" - }, - "sha256": { - "ignore_above": 1024, - "type": "keyword" - }, - "sha512": { - "ignore_above": 1024, - "type": "keyword" - } - } + "checksum": { + "type": "keyword", + "ignore_above": 1024 + }, + "description": { + "type": "keyword", + "ignore_above": 1024 + }, + "install_scope": { + "type": "keyword", + "ignore_above": 1024 + }, + "installed": { + "type": "date" + }, + "license": { + "type": "keyword", + "ignore_above": 1024 }, "name": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, - "pgid": { - "type": "long" + "path": { + "type": "keyword", + "ignore_above": 1024 }, - "pid": { - "type": "long" + "reference": { + "type": "keyword", + "ignore_above": 1024 }, - "ppid": { + "size": { "type": "long" }, - "start": { - "type": "date" + "type": { + "type": "keyword", + "ignore_above": 1024 }, - "thread": { - "properties": { - "id": { - "type": "long" - }, - "name": { - "ignore_above": 1024, - "type": "keyword" - } + "version": { + "type": "keyword", + "ignore_above": 1024 + } + } + }, + "pe": { + "properties": { + "company": { + "type": "keyword", + "ignore_above": 1024 + }, + "description": { + "type": "keyword", + "ignore_above": 1024 + }, + "file_version": { + "type": "keyword", + "ignore_above": 1024 + }, + "original_file_name": { + "type": "keyword", + "ignore_above": 1024 + }, + "product": { + "type": "keyword", + "ignore_above": 1024 + } + } + }, + "process": { + "properties": { + "args": { + "type": "keyword", + "ignore_above": 1024 + }, + "args_count": { + "type": "long" + }, + "code_signature": { + "properties": { + "exists": { + "type": "boolean" + }, + "status": { + "type": "keyword", + "ignore_above": 1024 + }, + "subject_name": { + "type": "keyword", + "ignore_above": 1024 + }, + "trusted": { + "type": "boolean" + }, + "valid": { + "type": "boolean" + } + } + }, + "command_line": { + "type": "keyword", + "fields": { + "text": { + "type": "text", + "norms": false + } + }, + "ignore_above": 1024 + }, + "entity_id": { + "type": "keyword", + "ignore_above": 1024 + }, + "executable": { + "type": "keyword", + "fields": { + "text": { + "type": "text", + "norms": false + } + }, + "ignore_above": 1024 + }, + "exit_code": { + "type": "long" + }, + "hash": { + "properties": { + "md5": { + "type": "keyword", + "ignore_above": 1024 + }, + "sha1": { + "type": "keyword", + "ignore_above": 1024 + }, + "sha256": { + "type": "keyword", + "ignore_above": 1024 + }, + "sha512": { + "type": "keyword", + "ignore_above": 1024 + } + } + }, + "name": { + "type": "keyword", + "fields": { + "text": { + "type": "text", + "norms": false + } + }, + "ignore_above": 1024 + }, + "parent": { + "properties": { + "args": { + "type": "keyword", + "ignore_above": 1024 + }, + "args_count": { + "type": "long" + }, + "code_signature": { + "properties": { + "exists": { + "type": "boolean" + }, + "status": { + "type": "keyword", + "ignore_above": 1024 + }, + "subject_name": { + "type": "keyword", + "ignore_above": 1024 + }, + "trusted": { + "type": "boolean" + }, + "valid": { + "type": "boolean" + } + } + }, + "command_line": { + "type": "keyword", + "fields": { + "text": { + "type": "text", + "norms": false + } + }, + "ignore_above": 1024 + }, + "entity_id": { + "type": "keyword", + "ignore_above": 1024 + }, + "executable": { + "type": "keyword", + "fields": { + "text": { + "type": "text", + "norms": false + } + }, + "ignore_above": 1024 + }, + "exit_code": { + "type": "long" + }, + "hash": { + "properties": { + "md5": { + "type": "keyword", + "ignore_above": 1024 + }, + "sha1": { + "type": "keyword", + "ignore_above": 1024 + }, + "sha256": { + "type": "keyword", + "ignore_above": 1024 + }, + "sha512": { + "type": "keyword", + "ignore_above": 1024 + } + } + }, + "name": { + "type": "keyword", + "fields": { + "text": { + "type": "text", + "norms": false + } + }, + "ignore_above": 1024 + }, + "pgid": { + "type": "long" + }, + "pid": { + "type": "long" + }, + "ppid": { + "type": "long" + }, + "start": { + "type": "date" + }, + "thread": { + "properties": { + "id": { + "type": "long" + }, + "name": { + "type": "keyword", + "ignore_above": 1024 + } + } + }, + "title": { + "type": "keyword", + "fields": { + "text": { + "type": "text", + "norms": false + } + }, + "ignore_above": 1024 + }, + "uptime": { + "type": "long" + }, + "working_directory": { + "type": "keyword", + "fields": { + "text": { + "type": "text", + "norms": false + } + }, + "ignore_above": 1024 + } + } + }, + "pe": { + "properties": { + "company": { + "type": "keyword", + "ignore_above": 1024 + }, + "description": { + "type": "keyword", + "ignore_above": 1024 + }, + "file_version": { + "type": "keyword", + "ignore_above": 1024 + }, + "original_file_name": { + "type": "keyword", + "ignore_above": 1024 + }, + "product": { + "type": "keyword", + "ignore_above": 1024 + } + } + }, + "pgid": { + "type": "long" + }, + "pid": { + "type": "long" + }, + "ppid": { + "type": "long" + }, + "start": { + "type": "date" + }, + "thread": { + "properties": { + "id": { + "type": "long" + }, + "name": { + "type": "keyword", + "ignore_above": 1024 + } } }, "title": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "fields": { + "text": { + "type": "text", + "norms": false + } + }, + "ignore_above": 1024 }, "uptime": { "type": "long" }, "working_directory": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "fields": { + "text": { + "type": "text", + "norms": false + } + }, + "ignore_above": 1024 + } + } + }, + "registry": { + "properties": { + "data": { + "properties": { + "bytes": { + "type": "keyword", + "ignore_above": 1024 + }, + "strings": { + "type": "keyword", + "ignore_above": 1024 + }, + "type": { + "type": "keyword", + "ignore_above": 1024 + } + } + }, + "hive": { + "type": "keyword", + "ignore_above": 1024 + }, + "key": { + "type": "keyword", + "ignore_above": 1024 + }, + "path": { + "type": "keyword", + "ignore_above": 1024 + }, + "value": { + "type": "keyword", + "ignore_above": 1024 } } }, "related": { "properties": { + "hash": { + "type": "keyword", + "ignore_above": 1024 + }, "ip": { "type": "ip" + }, + "user": { + "type": "keyword", + "ignore_above": 1024 } } }, @@ -1543,11 +2441,55 @@ } } }, + "rule": { + "properties": { + "author": { + "type": "keyword", + "ignore_above": 1024 + }, + "category": { + "type": "keyword", + "ignore_above": 1024 + }, + "description": { + "type": "keyword", + "ignore_above": 1024 + }, + "id": { + "type": "keyword", + "ignore_above": 1024 + }, + "license": { + "type": "keyword", + "ignore_above": 1024 + }, + "name": { + "type": "keyword", + "ignore_above": 1024 + }, + "reference": { + "type": "keyword", + "ignore_above": 1024 + }, + "ruleset": { + "type": "keyword", + "ignore_above": 1024 + }, + "uuid": { + "type": "keyword", + "ignore_above": 1024 + }, + "version": { + "type": "keyword", + "ignore_above": 1024 + } + } + }, "server": { "properties": { "address": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "as": { "properties": { @@ -1557,8 +2499,14 @@ "organization": { "properties": { "name": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "fields": { + "text": { + "type": "text", + "norms": false + } + }, + "ignore_above": 1024 } } } @@ -1568,41 +2516,41 @@ "type": "long" }, "domain": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "geo": { "properties": { "city_name": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "continent_name": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "country_iso_code": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "country_name": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "location": { "type": "geo_point" }, "name": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "region_iso_code": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "region_name": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 } } }, @@ -1610,8 +2558,8 @@ "type": "ip" }, "mac": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "nat": { "properties": { @@ -1629,43 +2577,67 @@ "port": { "type": "long" }, + "registered_domain": { + "type": "keyword", + "ignore_above": 1024 + }, + "top_level_domain": { + "type": "keyword", + "ignore_above": 1024 + }, "user": { "properties": { "domain": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "email": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "full_name": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "fields": { + "text": { + "type": "text", + "norms": false + } + }, + "ignore_above": 1024 }, "group": { "properties": { + "domain": { + "type": "keyword", + "ignore_above": 1024 + }, "id": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "name": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 } } }, "hash": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "id": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "name": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "fields": { + "text": { + "type": "text", + "norms": false + } + }, + "ignore_above": 1024 } } } @@ -1674,28 +2646,36 @@ "service": { "properties": { "ephemeral_id": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "id": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "name": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 + }, + "node": { + "properties": { + "name": { + "type": "keyword", + "ignore_above": 1024 + } + } }, "state": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "type": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "version": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 } } }, @@ -1717,8 +2697,8 @@ "source": { "properties": { "address": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "as": { "properties": { @@ -1728,8 +2708,14 @@ "organization": { "properties": { "name": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "fields": { + "text": { + "type": "text", + "norms": false + } + }, + "ignore_above": 1024 } } } @@ -1739,41 +2725,41 @@ "type": "long" }, "domain": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "geo": { "properties": { "city_name": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "continent_name": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "country_iso_code": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "country_name": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "location": { "type": "geo_point" }, "name": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "region_iso_code": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "region_name": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 } } }, @@ -1781,8 +2767,8 @@ "type": "ip" }, "mac": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "nat": { "properties": { @@ -1800,43 +2786,67 @@ "port": { "type": "long" }, + "registered_domain": { + "type": "keyword", + "ignore_above": 1024 + }, + "top_level_domain": { + "type": "keyword", + "ignore_above": 1024 + }, "user": { "properties": { "domain": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "email": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "full_name": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "fields": { + "text": { + "type": "text", + "norms": false + } + }, + "ignore_above": 1024 }, "group": { "properties": { + "domain": { + "type": "keyword", + "ignore_above": 1024 + }, "id": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "name": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 } } }, "hash": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "id": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "name": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "fields": { + "text": { + "type": "text", + "norms": false + } + }, + "ignore_above": 1024 } } } @@ -1853,8 +2863,8 @@ } }, "tags": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "tcp": { "properties": { @@ -1878,11 +2888,57 @@ } } }, + "threat": { + "properties": { + "framework": { + "type": "keyword", + "ignore_above": 1024 + }, + "tactic": { + "properties": { + "id": { + "type": "keyword", + "ignore_above": 1024 + }, + "name": { + "type": "keyword", + "ignore_above": 1024 + }, + "reference": { + "type": "keyword", + "ignore_above": 1024 + } + } + }, + "technique": { + "properties": { + "id": { + "type": "keyword", + "ignore_above": 1024 + }, + "name": { + "type": "keyword", + "fields": { + "text": { + "type": "text", + "norms": false + } + }, + "ignore_above": 1024 + }, + "reference": { + "type": "keyword", + "ignore_above": 1024 + } + } + } + } + }, "timeseries": { "properties": { "instance": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 } } }, @@ -1894,6 +2950,78 @@ "certificate_not_valid_before": { "type": "date" }, + "cipher": { + "type": "keyword", + "ignore_above": 1024 + }, + "client": { + "properties": { + "certificate": { + "type": "keyword", + "ignore_above": 1024 + }, + "certificate_chain": { + "type": "keyword", + "ignore_above": 1024 + }, + "hash": { + "properties": { + "md5": { + "type": "keyword", + "ignore_above": 1024 + }, + "sha1": { + "type": "keyword", + "ignore_above": 1024 + }, + "sha256": { + "type": "keyword", + "ignore_above": 1024 + } + } + }, + "issuer": { + "type": "keyword", + "ignore_above": 1024 + }, + "ja3": { + "type": "keyword", + "ignore_above": 1024 + }, + "not_after": { + "type": "date" + }, + "not_before": { + "type": "date" + }, + "server_name": { + "type": "keyword", + "ignore_above": 1024 + }, + "subject": { + "type": "keyword", + "ignore_above": 1024 + }, + "supported_ciphers": { + "type": "keyword", + "ignore_above": 1024 + } + } + }, + "curve": { + "type": "keyword", + "ignore_above": 1024 + }, + "established": { + "type": "boolean" + }, + "next_protocol": { + "type": "keyword", + "ignore_above": 1024 + }, + "resumed": { + "type": "boolean" + }, "rtt": { "properties": { "handshake": { @@ -1904,6 +3032,114 @@ } } } + }, + "server": { + "properties": { + "certificate": { + "type": "keyword", + "ignore_above": 1024 + }, + "certificate_chain": { + "type": "keyword", + "ignore_above": 1024 + }, + "hash": { + "properties": { + "md5": { + "type": "keyword", + "ignore_above": 1024 + }, + "sha1": { + "type": "keyword", + "ignore_above": 1024 + }, + "sha256": { + "type": "keyword", + "ignore_above": 1024 + } + } + }, + "issuer": { + "type": "keyword", + "ignore_above": 1024 + }, + "ja3s": { + "type": "keyword", + "ignore_above": 1024 + }, + "not_after": { + "type": "date" + }, + "not_before": { + "type": "date" + }, + "subject": { + "type": "keyword", + "ignore_above": 1024 + }, + "x509": { + "properties": { + "issuer": { + "properties": { + "common_name": { + "type": "keyword", + "ignore_above": 1024 + }, + "distinguished_name": { + "type": "keyword", + "ignore_above": 1024 + } + } + }, + "not_after": { + "type": "keyword", + "ignore_above": 1024 + }, + "not_before": { + "type": "keyword", + "ignore_above": 1024 + }, + "public_key_algorithm": { + "type": "keyword", + "ignore_above": 1024 + }, + "public_key_exponent": { + "type": "long" + }, + "public_key_size": { + "type": "long" + }, + "serial_number": { + "type": "keyword", + "ignore_above": 1024 + }, + "signature_algorithm": { + "type": "keyword", + "ignore_above": 1024 + }, + "subject": { + "properties": { + "common_name": { + "type": "keyword", + "ignore_above": 1024 + }, + "distinguished_name": { + "type": "keyword", + "ignore_above": 1024 + } + } + } + } + } + } + }, + "version": { + "type": "keyword", + "ignore_above": 1024 + }, + "version_protocol": { + "type": "keyword", + "ignore_above": 1024 } } }, @@ -1912,16 +3148,16 @@ "trace": { "properties": { "id": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 } } }, "transaction": { "properties": { "id": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 } } } @@ -1930,83 +3166,123 @@ "url": { "properties": { "domain": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 + }, + "extension": { + "type": "keyword", + "ignore_above": 1024 }, "fragment": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "full": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "fields": { + "text": { + "type": "text", + "norms": false + } + }, + "ignore_above": 1024 }, "original": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "fields": { + "text": { + "type": "text", + "norms": false + } + }, + "ignore_above": 1024 }, "password": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "path": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "port": { "type": "long" }, "query": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 + }, + "registered_domain": { + "type": "keyword", + "ignore_above": 1024 }, "scheme": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 + }, + "top_level_domain": { + "type": "keyword", + "ignore_above": 1024 }, "username": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 } } }, "user": { "properties": { "domain": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "email": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "full_name": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "fields": { + "text": { + "type": "text", + "norms": false + } + }, + "ignore_above": 1024 }, "group": { "properties": { + "domain": { + "type": "keyword", + "ignore_above": 1024 + }, "id": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "name": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 } } }, "hash": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "id": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "name": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "fields": { + "text": { + "type": "text", + "norms": false + } + }, + "ignore_above": 1024 } } }, @@ -2015,370 +3291,467 @@ "device": { "properties": { "name": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 } } }, "name": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "original": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "fields": { + "text": { + "type": "text", + "norms": false + } + }, + "ignore_above": 1024 }, "os": { "properties": { "family": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "full": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "fields": { + "text": { + "type": "text", + "norms": false + } + }, + "ignore_above": 1024 }, "kernel": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "name": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "fields": { + "text": { + "type": "text", + "norms": false + } + }, + "ignore_above": 1024 }, "platform": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "version": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 } } }, "version": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 } } - } - } - }, - "settings": { - "index": { - "mapping": { - "total_fields": { - "limit": "10000" + }, + "vlan": { + "properties": { + "id": { + "type": "keyword", + "ignore_above": 1024 + }, + "name": { + "type": "keyword", + "ignore_above": 1024 + } } }, - "number_of_replicas": "1", - "number_of_shards": "1", - "query": { - "default_field": [ - "message", - "tags", - "agent.ephemeral_id", - "agent.id", - "agent.name", - "agent.type", - "agent.version", - "as.organization.name", - "client.address", - "client.as.organization.name", - "client.domain", - "client.geo.city_name", - "client.geo.continent_name", - "client.geo.country_iso_code", - "client.geo.country_name", - "client.geo.name", - "client.geo.region_iso_code", - "client.geo.region_name", - "client.mac", - "client.user.domain", - "client.user.email", - "client.user.full_name", - "client.user.group.id", - "client.user.group.name", - "client.user.hash", - "client.user.id", - "client.user.name", - "cloud.account.id", - "cloud.availability_zone", - "cloud.instance.id", - "cloud.instance.name", - "cloud.machine.type", - "cloud.provider", - "cloud.region", - "container.id", - "container.image.name", - "container.image.tag", - "container.name", - "container.runtime", - "destination.address", - "destination.as.organization.name", - "destination.domain", - "destination.geo.city_name", - "destination.geo.continent_name", - "destination.geo.country_iso_code", - "destination.geo.country_name", - "destination.geo.name", - "destination.geo.region_iso_code", - "destination.geo.region_name", - "destination.mac", - "destination.user.domain", - "destination.user.email", - "destination.user.full_name", - "destination.user.group.id", - "destination.user.group.name", - "destination.user.hash", - "destination.user.id", - "destination.user.name", - "dns.answers.class", - "dns.answers.data", - "dns.answers.name", - "dns.answers.type", - "dns.header_flags", - "dns.id", - "dns.op_code", - "dns.question.class", - "dns.question.name", - "dns.question.registered_domain", - "dns.question.type", - "dns.response_code", - "dns.type", - "ecs.version", - "error.code", - "error.id", - "error.message", - "event.action", - "event.category", - "event.code", - "event.dataset", - "event.hash", - "event.id", - "event.kind", - "event.module", - "event.original", - "event.outcome", - "event.provider", - "event.timezone", - "event.type", - "file.device", - "file.directory", - "file.extension", - "file.gid", - "file.group", - "file.hash.md5", - "file.hash.sha1", - "file.hash.sha256", - "file.hash.sha512", - "file.inode", - "file.mode", - "file.name", - "file.owner", - "file.path", - "file.target_path", - "file.type", - "file.uid", - "geo.city_name", - "geo.continent_name", - "geo.country_iso_code", - "geo.country_name", - "geo.name", - "geo.region_iso_code", - "geo.region_name", - "group.id", - "group.name", - "hash.md5", - "hash.sha1", - "hash.sha256", - "hash.sha512", - "host.architecture", - "host.geo.city_name", - "host.geo.continent_name", - "host.geo.country_iso_code", - "host.geo.country_name", - "host.geo.name", - "host.geo.region_iso_code", - "host.geo.region_name", - "host.hostname", - "host.id", - "host.mac", - "host.name", - "host.os.family", - "host.os.full", - "host.os.kernel", - "host.os.name", - "host.os.platform", - "host.os.version", - "host.type", - "host.user.domain", - "host.user.email", - "host.user.full_name", - "host.user.group.id", - "host.user.group.name", - "host.user.hash", - "host.user.id", - "host.user.name", - "http.request.body.content", - "http.request.method", - "http.request.referrer", - "http.response.body.content", - "http.version", - "log.level", - "log.logger", - "log.original", - "network.application", - "network.community_id", - "network.direction", - "network.iana_number", - "network.name", - "network.protocol", - "network.transport", - "network.type", - "observer.geo.city_name", - "observer.geo.continent_name", - "observer.geo.country_iso_code", - "observer.geo.country_name", - "observer.geo.name", - "observer.geo.region_iso_code", - "observer.geo.region_name", - "observer.hostname", - "observer.mac", - "observer.os.family", - "observer.os.full", - "observer.os.kernel", - "observer.os.name", - "observer.os.platform", - "observer.os.version", - "observer.serial_number", - "observer.type", - "observer.vendor", - "observer.version", - "organization.id", - "organization.name", - "os.family", - "os.full", - "os.kernel", - "os.name", - "os.platform", - "os.version", - "process.args", - "process.executable", - "process.hash.md5", - "process.hash.sha1", - "process.hash.sha256", - "process.hash.sha512", - "process.name", - "process.thread.name", - "process.title", - "process.working_directory", - "server.address", - "server.as.organization.name", - "server.domain", - "server.geo.city_name", - "server.geo.continent_name", - "server.geo.country_iso_code", - "server.geo.country_name", - "server.geo.name", - "server.geo.region_iso_code", - "server.geo.region_name", - "server.mac", - "server.user.domain", - "server.user.email", - "server.user.full_name", - "server.user.group.id", - "server.user.group.name", - "server.user.hash", - "server.user.id", - "server.user.name", - "service.ephemeral_id", - "service.id", - "service.name", - "service.state", - "service.type", - "service.version", - "source.address", - "source.as.organization.name", - "source.domain", - "source.geo.city_name", - "source.geo.continent_name", - "source.geo.country_iso_code", - "source.geo.country_name", - "source.geo.name", - "source.geo.region_iso_code", - "source.geo.region_name", - "source.mac", - "source.user.domain", - "source.user.email", - "source.user.full_name", - "source.user.group.id", - "source.user.group.name", - "source.user.hash", - "source.user.id", - "source.user.name", - "tracing.trace.id", - "tracing.transaction.id", - "url.domain", - "url.fragment", - "url.full", - "url.original", - "url.password", - "url.path", - "url.query", - "url.scheme", - "url.username", - "user.domain", - "user.email", - "user.full_name", - "user.group.id", - "user.group.name", - "user.hash", - "user.id", - "user.name", - "user_agent.device.name", - "user_agent.name", - "user_agent.original", - "user_agent.os.family", - "user_agent.os.full", - "user_agent.os.kernel", - "user_agent.os.name", - "user_agent.os.platform", - "user_agent.os.version", - "user_agent.version", - "agent.hostname", - "error.type", - "timeseries.instance", - "cloud.project.id", - "cloud.image.id", - "host.os.build", - "host.os.codename", - "kubernetes.pod.name", - "kubernetes.pod.uid", - "kubernetes.namespace", - "kubernetes.node.name", - "kubernetes.replicaset.name", - "kubernetes.deployment.name", - "kubernetes.statefulset.name", - "kubernetes.container.name", - "kubernetes.container.image", - "jolokia.agent.version", - "jolokia.agent.id", - "jolokia.server.product", - "jolokia.server.version", - "jolokia.server.vendor", - "jolokia.url", - "monitor.type", - "monitor.name", - "monitor.id", - "monitor.status", - "monitor.check_group", - "http.response.body.hash", - "fields.*" - ] - }, - "refresh_interval": "5s" + "vulnerability": { + "properties": { + "category": { + "type": "keyword", + "ignore_above": 1024 + }, + "classification": { + "type": "keyword", + "ignore_above": 1024 + }, + "description": { + "type": "keyword", + "fields": { + "text": { + "type": "text", + "norms": false + } + }, + "ignore_above": 1024 + }, + "enumeration": { + "type": "keyword", + "ignore_above": 1024 + }, + "id": { + "type": "keyword", + "ignore_above": 1024 + }, + "reference": { + "type": "keyword", + "ignore_above": 1024 + }, + "report_id": { + "type": "keyword", + "ignore_above": 1024 + }, + "scanner": { + "properties": { + "vendor": { + "type": "keyword", + "ignore_above": 1024 + } + } + }, + "score": { + "properties": { + "base": { + "type": "float" + }, + "environmental": { + "type": "float" + }, + "temporal": { + "type": "float" + }, + "version": { + "type": "keyword", + "ignore_above": 1024 + } + } + }, + "severity": { + "type": "keyword", + "ignore_above": 1024 + } + } + } } } + }, + "settings": { + "index": { + "mapping": { + "total_fields": { + "limit": "10000" + } + }, + "number_of_replicas": "1", + "number_of_shards": "1", + "query": { + "default_field": [ + "message", + "tags", + "agent.ephemeral_id", + "agent.id", + "agent.name", + "agent.type", + "agent.version", + "as.organization.name", + "client.address", + "client.as.organization.name", + "client.domain", + "client.geo.city_name", + "client.geo.continent_name", + "client.geo.country_iso_code", + "client.geo.country_name", + "client.geo.name", + "client.geo.region_iso_code", + "client.geo.region_name", + "client.mac", + "client.user.domain", + "client.user.email", + "client.user.full_name", + "client.user.group.id", + "client.user.group.name", + "client.user.hash", + "client.user.id", + "client.user.name", + "cloud.account.id", + "cloud.availability_zone", + "cloud.instance.id", + "cloud.instance.name", + "cloud.machine.type", + "cloud.provider", + "cloud.region", + "container.id", + "container.image.name", + "container.image.tag", + "container.name", + "container.runtime", + "destination.address", + "destination.as.organization.name", + "destination.domain", + "destination.geo.city_name", + "destination.geo.continent_name", + "destination.geo.country_iso_code", + "destination.geo.country_name", + "destination.geo.name", + "destination.geo.region_iso_code", + "destination.geo.region_name", + "destination.mac", + "destination.user.domain", + "destination.user.email", + "destination.user.full_name", + "destination.user.group.id", + "destination.user.group.name", + "destination.user.hash", + "destination.user.id", + "destination.user.name", + "dns.answers.class", + "dns.answers.data", + "dns.answers.name", + "dns.answers.type", + "dns.header_flags", + "dns.id", + "dns.op_code", + "dns.question.class", + "dns.question.name", + "dns.question.registered_domain", + "dns.question.type", + "dns.response_code", + "dns.type", + "ecs.version", + "error.code", + "error.id", + "error.message", + "event.action", + "event.category", + "event.code", + "event.dataset", + "event.hash", + "event.id", + "event.kind", + "event.module", + "event.original", + "event.outcome", + "event.provider", + "event.timezone", + "event.type", + "file.device", + "file.directory", + "file.extension", + "file.gid", + "file.group", + "file.hash.md5", + "file.hash.sha1", + "file.hash.sha256", + "file.hash.sha512", + "file.inode", + "file.mode", + "file.name", + "file.owner", + "file.path", + "file.target_path", + "file.type", + "file.uid", + "geo.city_name", + "geo.continent_name", + "geo.country_iso_code", + "geo.country_name", + "geo.name", + "geo.region_iso_code", + "geo.region_name", + "group.id", + "group.name", + "hash.md5", + "hash.sha1", + "hash.sha256", + "hash.sha512", + "host.architecture", + "host.geo.city_name", + "host.geo.continent_name", + "host.geo.country_iso_code", + "host.geo.country_name", + "host.geo.name", + "host.geo.region_iso_code", + "host.geo.region_name", + "host.hostname", + "host.id", + "host.mac", + "host.name", + "host.os.family", + "host.os.full", + "host.os.kernel", + "host.os.name", + "host.os.platform", + "host.os.version", + "host.type", + "host.user.domain", + "host.user.email", + "host.user.full_name", + "host.user.group.id", + "host.user.group.name", + "host.user.hash", + "host.user.id", + "host.user.name", + "http.request.body.content", + "http.request.method", + "http.request.referrer", + "http.response.body.content", + "http.version", + "log.level", + "log.logger", + "log.original", + "network.application", + "network.community_id", + "network.direction", + "network.iana_number", + "network.name", + "network.protocol", + "network.transport", + "network.type", + "observer.geo.city_name", + "observer.geo.continent_name", + "observer.geo.country_iso_code", + "observer.geo.country_name", + "observer.geo.name", + "observer.geo.region_iso_code", + "observer.geo.region_name", + "observer.hostname", + "observer.mac", + "observer.os.family", + "observer.os.full", + "observer.os.kernel", + "observer.os.name", + "observer.os.platform", + "observer.os.version", + "observer.serial_number", + "observer.type", + "observer.vendor", + "observer.version", + "organization.id", + "organization.name", + "os.family", + "os.full", + "os.kernel", + "os.name", + "os.platform", + "os.version", + "process.args", + "process.executable", + "process.hash.md5", + "process.hash.sha1", + "process.hash.sha256", + "process.hash.sha512", + "process.name", + "process.thread.name", + "process.title", + "process.working_directory", + "server.address", + "server.as.organization.name", + "server.domain", + "server.geo.city_name", + "server.geo.continent_name", + "server.geo.country_iso_code", + "server.geo.country_name", + "server.geo.name", + "server.geo.region_iso_code", + "server.geo.region_name", + "server.mac", + "server.user.domain", + "server.user.email", + "server.user.full_name", + "server.user.group.id", + "server.user.group.name", + "server.user.hash", + "server.user.id", + "server.user.name", + "service.ephemeral_id", + "service.id", + "service.name", + "service.state", + "service.type", + "service.version", + "source.address", + "source.as.organization.name", + "source.domain", + "source.geo.city_name", + "source.geo.continent_name", + "source.geo.country_iso_code", + "source.geo.country_name", + "source.geo.name", + "source.geo.region_iso_code", + "source.geo.region_name", + "source.mac", + "source.user.domain", + "source.user.email", + "source.user.full_name", + "source.user.group.id", + "source.user.group.name", + "source.user.hash", + "source.user.id", + "source.user.name", + "tracing.trace.id", + "tracing.transaction.id", + "url.domain", + "url.fragment", + "url.full", + "url.original", + "url.password", + "url.path", + "url.query", + "url.scheme", + "url.username", + "user.domain", + "user.email", + "user.full_name", + "user.group.id", + "user.group.name", + "user.hash", + "user.id", + "user.name", + "user_agent.device.name", + "user_agent.name", + "user_agent.original", + "user_agent.os.family", + "user_agent.os.full", + "user_agent.os.kernel", + "user_agent.os.name", + "user_agent.os.platform", + "user_agent.os.version", + "user_agent.version", + "agent.hostname", + "error.type", + "timeseries.instance", + "cloud.project.id", + "cloud.image.id", + "host.os.build", + "host.os.codename", + "kubernetes.pod.name", + "kubernetes.pod.uid", + "kubernetes.namespace", + "kubernetes.node.name", + "kubernetes.replicaset.name", + "kubernetes.deployment.name", + "kubernetes.statefulset.name", + "kubernetes.container.name", + "kubernetes.container.image", + "jolokia.agent.version", + "jolokia.agent.id", + "jolokia.server.product", + "jolokia.server.version", + "jolokia.server.vendor", + "jolokia.url", + "monitor.type", + "monitor.name", + "monitor.id", + "monitor.status", + "monitor.check_group", + "http.response.body.hash", + "fields.*" + ] + }, + "refresh_interval": "5s" + } } } diff --git a/x-pack/test/functional/page_objects/reporting_page.js b/x-pack/test/functional/page_objects/reporting_page.js index 7cdd1c083239b..cdfafeec1bf46 100644 --- a/x-pack/test/functional/page_objects/reporting_page.js +++ b/x-pack/test/functional/page_objects/reporting_page.js @@ -19,24 +19,10 @@ export function ReportingPageProvider({ getService, getPageObjects }) { const log = getService('log'); const config = getService('config'); const testSubjects = getService('testSubjects'); - const esArchiver = getService('esArchiver'); const browser = getService('browser'); - const kibanaServer = getService('kibanaServer'); - const PageObjects = getPageObjects(['common', 'security', 'settings', 'share', 'timePicker']); + const PageObjects = getPageObjects(['common', 'security', 'share', 'timePicker']); class ReportingPage { - async initTests() { - log.debug('ReportingPage:initTests'); - await PageObjects.settings.navigateTo(); - await esArchiver.loadIfNeeded('../../functional/es_archives/logstash_functional'); - await esArchiver.load('reporting/historic'); - await kibanaServer.uiSettings.replace({ - defaultIndex: 'logstash-*', - }); - - await browser.setWindowSize(1600, 850); - } - async forceSharedItemsContainerSize({ width }) { await browser.execute(` var el = document.querySelector('[data-shared-items-container]'); @@ -130,6 +116,20 @@ export function ReportingPageProvider({ getService, getPageObjects }) { return await retry.try(async () => await testSubjects.find('generateReportButton')); } + async isGenerateReportButtonDisabled() { + const generateReportButton = await this.getGenerateReportButton(); + return await retry.try(async () => { + const isDisabled = await generateReportButton.getAttribute('disabled'); + return isDisabled; + }); + } + + async canReportBeCreated() { + await this.clickGenerateReportButton(); + const success = await this.checkForReportingToasts(); + return success; + } + async checkUsePrintLayout() { // The print layout checkbox slides in as part of an animation, and tests can // attempt to click it too quickly, leading to flaky tests. The 500ms wait allows diff --git a/x-pack/test/functional/page_objects/uptime_page.ts b/x-pack/test/functional/page_objects/uptime_page.ts index 39c3c46adddbb..0ebcb5c87deee 100644 --- a/x-pack/test/functional/page_objects/uptime_page.ts +++ b/x-pack/test/functional/page_objects/uptime_page.ts @@ -9,42 +9,38 @@ import { FtrProviderContext } from '../ftr_provider_context'; export function UptimePageProvider({ getPageObjects, getService }: FtrProviderContext) { const pageObjects = getPageObjects(['common', 'timePicker']); - const { common: commonService, navigation, alerts } = getService('uptime'); + const { alerts, common: commonService, monitor, navigation } = getService('uptime'); const retry = getService('retry'); return new (class UptimePage { public async goToRoot() { - await pageObjects.common.navigateToApp('uptime'); + await navigation.goToUptime(); } - public async goToUptimePageAndSetDateRange( - datePickerStartValue: string, - datePickerEndValue: string - ) { - await pageObjects.common.navigateToApp('uptime'); - await pageObjects.timePicker.setAbsoluteRange(datePickerStartValue, datePickerEndValue); + public async setDateRange(start: string, end: string) { + const { start: prevStart, end: prevEnd } = await pageObjects.timePicker.getTimeConfig(); + if (start !== prevStart || prevEnd !== end) { + await pageObjects.timePicker.setAbsoluteRange(start, end); + } else { + await navigation.refreshApp(); + } } public async goToUptimeOverviewAndLoadData( - datePickerStartValue: string, - datePickerEndValue: string, + dateStart: string, + dateEnd: string, monitorIdToCheck?: string ) { - await pageObjects.common.navigateToApp('uptime'); - await pageObjects.timePicker.setAbsoluteRange(datePickerStartValue, datePickerEndValue); + await navigation.goToUptime(); + await this.setDateRange(dateStart, dateEnd); if (monitorIdToCheck) { await commonService.monitorIdExists(monitorIdToCheck); } } - public async loadDataAndGoToMonitorPage( - datePickerStartValue: string, - datePickerEndValue: string, - monitorId: string, - monitorName?: string - ) { - await pageObjects.timePicker.setAbsoluteRange(datePickerStartValue, datePickerEndValue); - await navigation.goToMonitor(monitorId, monitorName); + public async loadDataAndGoToMonitorPage(dateStart: string, dateEnd: string, monitorId: string) { + await this.setDateRange(dateStart, dateEnd); + await navigation.goToMonitor(monitorId); } public async inputFilterQuery(filterQuery: string) { @@ -140,5 +136,24 @@ export function UptimePageProvider({ getPageObjects, getService }: FtrProviderCo await commonService.openPageSizeSelectPopover(); return commonService.clickPageSizeSelectPopoverItem(size); } + + public async checkPingListInteractions( + timestamps: string[], + location?: string, + status?: string + ): Promise { + if (location) { + await monitor.setPingListLocation(location); + } + if (status) { + await monitor.setPingListStatus(status); + } + return monitor.checkForPingListTimestamps(timestamps); + } + + public async resetFilters() { + await this.inputFilterQuery(''); + await commonService.resetStatusFilter(); + } })(); } diff --git a/x-pack/test/functional/services/machine_learning/index.ts b/x-pack/test/functional/services/machine_learning/index.ts index f5adf63825163..b792f17efcbca 100644 --- a/x-pack/test/functional/services/machine_learning/index.ts +++ b/x-pack/test/functional/services/machine_learning/index.ts @@ -30,3 +30,4 @@ export { MachineLearningSecurityCommonProvider } from './security_common'; export { MachineLearningSecurityUIProvider } from './security_ui'; export { MachineLearningSettingsProvider } from './settings'; export { MachineLearningSingleMetricViewerProvider } from './single_metric_viewer'; +export { MachineLearningTestResourcesProvider } from './test_resources'; diff --git a/x-pack/test/functional/services/machine_learning/test_resources.ts b/x-pack/test/functional/services/machine_learning/test_resources.ts new file mode 100644 index 0000000000000..0d61c1413b988 --- /dev/null +++ b/x-pack/test/functional/services/machine_learning/test_resources.ts @@ -0,0 +1,219 @@ +/* + * 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 { ProvidedType } from '@kbn/test/types/ftr'; + +import { savedSearches } from './test_resources_data'; + +import { FtrProviderContext } from '../../ftr_provider_context'; + +const COMMON_HEADERS = { + 'kbn-xsrf': 'some-xsrf-token', +}; + +export enum SavedObjectType { + CONFIG = 'config', + DASHBOARD = 'dashboard', + INDEX_PATTERN = 'index-pattern', + SEARCH = 'search', + VISUALIZATION = 'visualization', +} + +export type MlTestResourcesi = ProvidedType; + +export function MachineLearningTestResourcesProvider({ getService }: FtrProviderContext) { + const kibanaServer = getService('kibanaServer'); + const log = getService('log'); + const supertest = getService('supertest'); + + return { + async setKibanaTimeZoneToUTC() { + await kibanaServer.uiSettings.update({ + 'dateFormat:tz': 'UTC', + }); + }, + + async resetKibanaTimeZone() { + await kibanaServer.uiSettings.unset('dateFormat:tz'); + }, + + async savedObjectExists(id: string, objectType: SavedObjectType): Promise { + const response = await supertest.get(`/api/saved_objects/${objectType}/${id}`); + return response.status === 200; + }, + + async getSavedObjectIdByTitle( + title: string, + objectType: SavedObjectType + ): Promise { + log.debug(`Searching for '${objectType}' with title '${title}'...`); + const findResponse = await supertest + .get(`/api/saved_objects/_find?type=${objectType}`) + .set(COMMON_HEADERS) + .expect(200) + .then((res: any) => res.body); + + for (const savedObject of findResponse.saved_objects) { + const objectTitle = savedObject.attributes.title; + if (objectTitle === title) { + log.debug(` > Found '${savedObject.id}'`); + return savedObject.id; + } + } + log.debug(` > Not found`); + }, + + async getIndexPatternId(title: string): Promise { + return this.getSavedObjectIdByTitle(title, SavedObjectType.INDEX_PATTERN); + }, + + async getSavedSearchId(title: string): Promise { + return this.getSavedObjectIdByTitle(title, SavedObjectType.SEARCH); + }, + + async createIndexPattern(title: string, timeFieldName?: string): Promise { + log.debug( + `Creating index pattern with title '${title}'${ + timeFieldName !== undefined ? ` and time field '${timeFieldName}'` : '' + }` + ); + + const createResponse = await supertest + .post(`/api/saved_objects/${SavedObjectType.INDEX_PATTERN}`) + .set(COMMON_HEADERS) + .send({ attributes: { title, timeFieldName } }) + .expect(200) + .then((res: any) => res.body); + + log.debug(` > Created with id '${createResponse.id}'`); + return createResponse.id; + }, + + async createIndexPatternIfNeeded(title: string, timeFieldName?: string): Promise { + const indexPatternId = await this.getIndexPatternId(title); + if (indexPatternId !== undefined) { + log.debug(`Index pattern with title '${title}' already exists. Nothing to create.`); + return indexPatternId; + } else { + return await this.createIndexPattern(title, timeFieldName); + } + }, + + async createSavedSearch(title: string, body: object): Promise { + log.debug(`Creating saved search with title '${title}'`); + + const createResponse = await supertest + .post(`/api/saved_objects/${SavedObjectType.SEARCH}`) + .set(COMMON_HEADERS) + .send(body) + .expect(200) + .then((res: any) => res.body); + + log.debug(` > Created with id '${createResponse.id}'`); + return createResponse.id; + }, + + async createSavedSearchIfNeeded(savedSearch: any): Promise { + const title = savedSearch.requestBody.attributes.title; + const savedSearchId = await this.getSavedSearchId(title); + if (savedSearchId !== undefined) { + log.debug(`Saved search with title '${title}' already exists. Nothing to create.`); + return savedSearchId; + } else { + const body = await this.updateSavedSearchRequestBody( + savedSearch.requestBody, + savedSearch.indexPatternTitle + ); + return await this.createSavedSearch(title, body); + } + }, + + async updateSavedSearchRequestBody(body: object, indexPatternTitle: string): Promise { + const indexPatternId = await this.getIndexPatternId(indexPatternTitle); + if (indexPatternId === undefined) { + throw new Error( + `Index pattern '${indexPatternTitle}' to base saved search on does not exist. ` + ); + } + + // inject index pattern id + const updatedBody = JSON.parse(JSON.stringify(body), (_key, value) => { + if (value === 'INDEX_PATTERN_ID_PLACEHOLDER') { + return indexPatternId; + } else { + return value; + } + }); + + // make searchSourceJSON node a string + const searchSourceJsonNode = updatedBody.attributes.kibanaSavedObjectMeta.searchSourceJSON; + const searchSourceJsonString = JSON.stringify(searchSourceJsonNode); + updatedBody.attributes.kibanaSavedObjectMeta.searchSourceJSON = searchSourceJsonString; + + return updatedBody; + }, + + async createSavedSearchFarequoteFilterIfNeeded() { + await this.createSavedSearchIfNeeded(savedSearches.farequoteFilter); + }, + + async createSavedSearchFarequoteLuceneIfNeeded() { + await this.createSavedSearchIfNeeded(savedSearches.farequoteLucene); + }, + + async createSavedSearchFarequoteKueryIfNeeded() { + await this.createSavedSearchIfNeeded(savedSearches.farequoteKuery); + }, + + async createSavedSearchFarequoteFilterAndLuceneIfNeeded() { + await this.createSavedSearchIfNeeded(savedSearches.farequoteFilterAndLucene); + }, + + async createSavedSearchFarequoteFilterAndKueryIfNeeded() { + await this.createSavedSearchIfNeeded(savedSearches.farequoteFilterAndKuery); + }, + + async deleteIndexPattern(title: string) { + log.debug(`Deleting index pattern with title '${title}'...`); + + const indexPatternId = await this.getIndexPatternId(title); + if (indexPatternId === undefined) { + log.debug(`Index pattern with title '${title}' does not exists. Nothing to delete.`); + return; + } else { + await supertest + .delete(`/api/saved_objects/${SavedObjectType.INDEX_PATTERN}/${indexPatternId}`) + .set(COMMON_HEADERS) + .expect(200); + + log.debug(` > Deleted index pattern with id '${indexPatternId}'`); + } + }, + + async deleteSavedSearch(title: string) { + log.debug(`Deleting saved search with title '${title}'...`); + + const savedSearchId = await this.getSavedSearchId(title); + if (savedSearchId === undefined) { + log.debug(`Saved search with title '${title}' does not exists. Nothing to delete.`); + return; + } else { + await supertest + .delete(`/api/saved_objects/${SavedObjectType.SEARCH}/${savedSearchId}`) + .set(COMMON_HEADERS) + .expect(200); + + log.debug(` > Deleted saved searchwith id '${savedSearchId}'`); + } + }, + + async deleteSavedSearches() { + for (const search of Object.values(savedSearches)) { + await this.deleteSavedSearch(search.requestBody.attributes.title); + } + }, + }; +} diff --git a/x-pack/test/functional/services/machine_learning/test_resources_data.ts b/x-pack/test/functional/services/machine_learning/test_resources_data.ts new file mode 100644 index 0000000000000..dd600077182f9 --- /dev/null +++ b/x-pack/test/functional/services/machine_learning/test_resources_data.ts @@ -0,0 +1,249 @@ +/* + * 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 savedSearches = { + farequoteFilter: { + indexPatternTitle: 'ft_farequote', + requestBody: { + attributes: { + title: 'ft_farequote_filter', + description: '', + hits: 0, + columns: ['_source'], + sort: ['@timestamp', 'desc'], + version: 1, + kibanaSavedObjectMeta: { + searchSourceJSON: { + highlightAll: true, + version: true, + query: { + query: '', + language: 'lucene', + }, + filter: [ + { + meta: { + index: 'INDEX_PATTERN_ID_PLACEHOLDER', + negate: false, + disabled: false, + alias: null, + type: 'phrase', + key: 'airline', + value: 'ASA', + params: { + query: 'ASA', + type: 'phrase', + }, + }, + query: { + match: { + airline: { + query: 'ASA', + type: 'phrase', + }, + }, + }, + $state: { + store: 'appState', + }, + }, + ], + indexRefName: 'kibanaSavedObjectMeta.searchSourceJSON.index', + }, + }, + }, + references: [ + { + name: 'kibanaSavedObjectMeta.searchSourceJSON.index', + type: 'index-pattern', + id: 'INDEX_PATTERN_ID_PLACEHOLDER', + }, + ], + }, + }, + farequoteLucene: { + indexPatternTitle: 'ft_farequote', + requestBody: { + attributes: { + title: 'ft_farequote_lucene', + description: '', + hits: 0, + columns: ['_source'], + sort: ['@timestamp', 'desc'], + version: 1, + kibanaSavedObjectMeta: { + searchSourceJSON: { + highlightAll: true, + version: true, + query: { + query: 'airline:A*', + language: 'lucene', + }, + filter: [], + indexRefName: 'kibanaSavedObjectMeta.searchSourceJSON.index', + }, + }, + }, + references: [ + { + name: 'kibanaSavedObjectMeta.searchSourceJSON.index', + type: 'index-pattern', + id: 'INDEX_PATTERN_ID_PLACEHOLDER', + }, + ], + }, + }, + farequoteKuery: { + indexPatternTitle: 'ft_farequote', + requestBody: { + attributes: { + title: 'ft_farequote_kuery', + description: '', + hits: 0, + columns: ['_source'], + sort: ['@timestamp', 'desc'], + version: 1, + kibanaSavedObjectMeta: { + searchSourceJSON: { + highlightAll: true, + version: true, + query: { + query: 'airline: A* and responsetime > 5', + language: 'kuery', + }, + filter: [], + indexRefName: 'kibanaSavedObjectMeta.searchSourceJSON.index', + }, + }, + }, + references: [ + { + name: 'kibanaSavedObjectMeta.searchSourceJSON.index', + type: 'index-pattern', + id: 'INDEX_PATTERN_ID_PLACEHOLDER', + }, + ], + }, + }, + farequoteFilterAndLucene: { + indexPatternTitle: 'ft_farequote', + requestBody: { + attributes: { + title: 'ft_farequote_filter_and_lucene', + description: '', + hits: 0, + columns: ['_source'], + sort: ['@timestamp', 'desc'], + version: 1, + kibanaSavedObjectMeta: { + searchSourceJSON: { + highlightAll: true, + version: true, + query: { + query: 'responsetime:>50', + language: 'lucene', + }, + filter: [ + { + meta: { + index: 'INDEX_PATTERN_ID_PLACEHOLDER', + negate: false, + disabled: false, + alias: null, + type: 'phrase', + key: 'airline', + value: 'ASA', + params: { + query: 'ASA', + type: 'phrase', + }, + }, + query: { + match: { + airline: { + query: 'ASA', + type: 'phrase', + }, + }, + }, + $state: { + store: 'appState', + }, + }, + ], + indexRefName: 'kibanaSavedObjectMeta.searchSourceJSON.index', + }, + }, + }, + references: [ + { + name: 'kibanaSavedObjectMeta.searchSourceJSON.index', + type: 'index-pattern', + id: 'INDEX_PATTERN_ID_PLACEHOLDER', + }, + ], + }, + }, + farequoteFilterAndKuery: { + indexPatternTitle: 'ft_farequote', + requestBody: { + attributes: { + title: 'ft_farequote_filter_and_kuery', + description: '', + hits: 0, + columns: ['_source'], + sort: ['@timestamp', 'desc'], + version: 1, + kibanaSavedObjectMeta: { + searchSourceJSON: { + highlightAll: true, + version: true, + query: { + query: 'responsetime > 49', + language: 'kuery', + }, + filter: [ + { + meta: { + index: 'INDEX_PATTERN_ID_PLACEHOLDER', + negate: false, + disabled: false, + alias: null, + type: 'phrase', + key: 'airline', + value: 'ASA', + params: { + query: 'ASA', + type: 'phrase', + }, + }, + query: { + match: { + airline: { + query: 'ASA', + type: 'phrase', + }, + }, + }, + $state: { + store: 'appState', + }, + }, + ], + indexRefName: 'kibanaSavedObjectMeta.searchSourceJSON.index', + }, + }, + }, + references: [ + { + name: 'kibanaSavedObjectMeta.searchSourceJSON.index', + type: 'index-pattern', + id: 'INDEX_PATTERN_ID_PLACEHOLDER', + }, + ], + }, + }, +}; diff --git a/x-pack/test/functional/services/ml.ts b/x-pack/test/functional/services/ml.ts index af7cb51f4e3f0..be3d3e0c96016 100644 --- a/x-pack/test/functional/services/ml.ts +++ b/x-pack/test/functional/services/ml.ts @@ -33,6 +33,7 @@ import { MachineLearningSecurityUIProvider, MachineLearningSettingsProvider, MachineLearningSingleMetricViewerProvider, + MachineLearningTestResourcesProvider, } from './machine_learning'; export function MachineLearningProvider(context: FtrProviderContext) { @@ -67,6 +68,7 @@ export function MachineLearningProvider(context: FtrProviderContext) { const securityUI = MachineLearningSecurityUIProvider(context, securityCommon); const settings = MachineLearningSettingsProvider(context); const singleMetricViewer = MachineLearningSingleMetricViewerProvider(context); + const testResources = MachineLearningTestResourcesProvider(context); return { anomaliesTable, @@ -95,5 +97,6 @@ export function MachineLearningProvider(context: FtrProviderContext) { securityUI, settings, singleMetricViewer, + testResources, }; } diff --git a/x-pack/test/functional/services/transform.ts b/x-pack/test/functional/services/transform.ts index 74416ea070882..f0ca5f81bd5ec 100644 --- a/x-pack/test/functional/services/transform.ts +++ b/x-pack/test/functional/services/transform.ts @@ -17,6 +17,8 @@ import { TransformWizardProvider, } from './transform_ui'; +import { MachineLearningTestResourcesProvider } from './machine_learning'; + export function TransformProvider(context: FtrProviderContext) { const api = TransformAPIProvider(context); const management = TransformManagementProvider(context); @@ -25,6 +27,7 @@ export function TransformProvider(context: FtrProviderContext) { const securityUI = TransformSecurityUIProvider(context, securityCommon); const sourceSelection = TransformSourceSelectionProvider(context); const table = TransformTableProvider(context); + const testResources = MachineLearningTestResourcesProvider(context); const wizard = TransformWizardProvider(context); return { @@ -35,6 +38,7 @@ export function TransformProvider(context: FtrProviderContext) { securityUI, sourceSelection, table, + testResources, wizard, }; } diff --git a/x-pack/test/functional/services/uptime/common.ts b/x-pack/test/functional/services/uptime/common.ts index ed465eee343f9..b5be1e29a0e8c 100644 --- a/x-pack/test/functional/services/uptime/common.ts +++ b/x-pack/test/functional/services/uptime/common.ts @@ -10,6 +10,7 @@ export function UptimeCommonProvider({ getService }: FtrProviderContext) { const testSubjects = getService('testSubjects'); const browser = getService('browser'); const retry = getService('retry'); + const find = getService('find'); return { async assertExists(key: string) { @@ -52,6 +53,20 @@ export function UptimeCommonProvider({ getService }: FtrProviderContext) { async setStatusFilterDown() { await testSubjects.click('xpack.uptime.filterBar.filterStatusDown'); }, + async resetStatusFilter() { + const upFilter = await find.byCssSelector( + '[data-test-subj="xpack.uptime.filterBar.filterStatusUp"]' + ); + if (await upFilter.elementHasClass('euiFilterButton-hasActiveFilters')) { + this.setStatusFilterUp(); + } + const downFilter = await find.byCssSelector( + '[data-test-subj="xpack.uptime.filterBar.filterStatusDown"]' + ); + if (await downFilter.elementHasClass('euiFilterButton-hasActiveFilters')) { + this.setStatusFilterDown(); + } + }, async selectFilterItem(filterType: string, option: string) { const popoverId = `filter-popover_${filterType}`; const optionId = `filter-popover-item_${option}`; diff --git a/x-pack/test/functional/services/uptime/ml_anomaly.ts b/x-pack/test/functional/services/uptime/ml_anomaly.ts new file mode 100644 index 0000000000000..e15f47ddd9709 --- /dev/null +++ b/x-pack/test/functional/services/uptime/ml_anomaly.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 { FtrProviderContext } from '../../ftr_provider_context'; + +export function UptimeMLAnomalyProvider({ getService }: FtrProviderContext) { + const testSubjects = getService('testSubjects'); + const retry = getService('retry'); + const log = getService('log'); + + return { + async openMLFlyout() { + return retry.tryForTime(15000, async () => { + await testSubjects.click('uptimeEnableAnomalyBtn'); + await testSubjects.existOrFail('uptimeMLFlyout'); + }); + }, + + async openMLManageMenu() { + return retry.tryForTime(30000, async () => { + await testSubjects.click('uptimeManageMLJobBtn'); + await testSubjects.existOrFail('uptimeManageMLContextMenu'); + }); + }, + + async alreadyHasJob() { + return await testSubjects.exists('uptimeManageMLJobBtn'); + }, + + async createMLJob() { + await testSubjects.click('uptimeMLCreateJobBtn'); + return retry.tryForTime(10000, async () => { + await testSubjects.existOrFail('uptimeMLJobSuccessfullyCreated'); + log.info('Job successfully created'); + }); + }, + + async deleteMLJob() { + await testSubjects.click('uptimeDeleteMLJobBtn'); + return retry.tryForTime(10000, async () => { + await testSubjects.click('uptimeMLJobDeleteConfirmModel > confirmModalConfirmButton'); + await testSubjects.existOrFail('uptimeMLJobSuccessfullyDeleted'); + log.info('Job successfully deleted'); + }); + }, + + async canCreateJob() { + const createJobBtn = await testSubjects.find('uptimeMLCreateJobBtn'); + return !!(await createJobBtn.getAttribute('disabled')); + }, + + async hasNoLicenseInfo() { + return await testSubjects.missingOrFail('uptimeMLLicenseInfo', { timeout: 1000 }); + }, + }; +} diff --git a/x-pack/test/functional/services/uptime/monitor.ts b/x-pack/test/functional/services/uptime/monitor.ts index 3bdec4b6749d4..a3e3d953e2eb7 100644 --- a/x-pack/test/functional/services/uptime/monitor.ts +++ b/x-pack/test/functional/services/uptime/monitor.ts @@ -27,5 +27,22 @@ export function UptimeMonitorProvider({ getService }: FtrProviderContext) { await find.descendantExistsByCssSelector('canvas.mapboxgl-canvas', mapPanel); }); }, + async setPingListLocation(location: string) { + await testSubjects.click('xpack.uptime.pingList.locationSelect', 5000); + return testSubjects.click(`xpack.uptime.pingList.locationOptions.${location}`, 5000); + }, + async setPingListStatus(status: string) { + await testSubjects.click('xpack.uptime.pingList.statusSelect', 5000); + return testSubjects.click(`xpack.uptime.pingList.statusOptions.${status}`, 5000); + }, + async checkForPingListTimestamps(timestamps: string[]): Promise { + return retry.tryForTime(10000, async () => { + await Promise.all( + timestamps.map(timestamp => + testSubjects.existOrFail(`xpack.uptime.pingList.ping-${timestamp}`) + ) + ); + }); + }, }; } diff --git a/x-pack/test/functional/services/uptime/navigation.ts b/x-pack/test/functional/services/uptime/navigation.ts index 15ee869da1e6a..36a5d7c9702f8 100644 --- a/x-pack/test/functional/services/uptime/navigation.ts +++ b/x-pack/test/functional/services/uptime/navigation.ts @@ -9,17 +9,27 @@ import { FtrProviderContext } from '../../ftr_provider_context'; export function UptimeNavigationProvider({ getService, getPageObjects }: FtrProviderContext) { const retry = getService('retry'); const testSubjects = getService('testSubjects'); - const PageObjects = getPageObjects(['common', 'header']); + const PageObjects = getPageObjects(['common', 'timePicker', 'header']); const goToUptimeRoot = async () => { - await retry.tryForTime(30 * 1000, async () => { - await PageObjects.common.navigateToApp('uptime'); - await PageObjects.header.waitUntilLoadingHasFinished(); - await testSubjects.existOrFail('uptimeOverviewPage', { timeout: 2000 }); + // Check if are already on overview uptime page, we don't need to repeat the step + await retry.tryForTime(60 * 1000, async () => { + if (await testSubjects.exists('uptimeSettingsToOverviewLink', { timeout: 0 })) { + await testSubjects.click('uptimeSettingsToOverviewLink'); + await testSubjects.existOrFail('uptimeOverviewPage', { timeout: 2000 }); + } else if (!(await testSubjects.exists('uptimeOverviewPage', { timeout: 0 }))) { + await PageObjects.common.navigateToApp('uptime'); + await PageObjects.header.waitUntilLoadingHasFinished(); + await testSubjects.existOrFail('uptimeOverviewPage', { timeout: 2000 }); + } }); }; return { + async refreshApp() { + await testSubjects.click('superDatePickerApplyTimeButton'); + }, + async goToUptime() { await goToUptimeRoot(); }, @@ -30,17 +40,29 @@ export function UptimeNavigationProvider({ getService, getPageObjects }: FtrProv await testSubjects.existOrFail('uptimeSettingsPage', { timeout: 2000 }); }, - goToMonitor: async (monitorId: string, monitorName?: string) => { - await testSubjects.click(`monitor-page-link-${monitorId}`, 5000); - if ( - monitorName && - (await testSubjects.getVisibleText('monitor-page-title')) !== monitorName - ) { - throw new Error('Expected monitor name not found'); + checkIfOnMonitorPage: async (monitorId: string) => { + const monitorPage = await testSubjects.exists('uptimeMonitorPage', { timeout: 1000 }); + if (monitorId && monitorPage) { + const thisMonitorPage = + (await testSubjects.getVisibleText('monitor-page-title')) === monitorId; + return monitorPage && thisMonitorPage; + } else { + return monitorPage; + } + }, + + goToMonitor: async (monitorId: string) => { + if (!(await testSubjects.exists('uptimeMonitorPage', { timeout: 0 }))) { + await testSubjects.click(`monitor-page-link-${monitorId}`); + await testSubjects.existOrFail('uptimeMonitorPage', { + timeout: 30000, + }); } - await testSubjects.existOrFail('uptimeMonitorPage', { - timeout: 30000, - }); + }, + + async loadDataAndGoToMonitorPage(dateStart: string, dateEnd: string, monitorId: string) { + await PageObjects.timePicker.setAbsoluteRange(dateStart, dateEnd); + await this.goToMonitor(monitorId); }, }; } diff --git a/x-pack/test/functional/services/uptime/settings.ts b/x-pack/test/functional/services/uptime/settings.ts index a64d39cd62a6d..14cab368b766a 100644 --- a/x-pack/test/functional/services/uptime/settings.ts +++ b/x-pack/test/functional/services/uptime/settings.ts @@ -10,20 +10,41 @@ export function UptimeSettingsProvider({ getService }: FtrProviderContext) { const testSubjects = getService('testSubjects'); const retry = getService('retry'); + const changeInputField = async (text: string, field: string) => { + const input = await testSubjects.find(field, 5000); + await input.clearValueWithKeyboard(); + await input.type(text); + }; + return { go: async () => { await testSubjects.click('settings-page-link', 5000); }, + changeHeartbeatIndicesInput: async (text: string) => { - const input = await testSubjects.find('heartbeat-indices-input-loaded', 5000); - await input.clearValueWithKeyboard(); - await input.type(text); + await changeInputField(text, 'heartbeat-indices-input-loaded'); + }, + changeErrorThresholdInput: async (text: string) => { + await changeInputField(text, 'error-state-threshold-input-loaded'); + }, + changeWarningThresholdInput: async (text: string) => { + await changeInputField(text, 'warning-state-threshold-input-loaded'); }, loadFields: async () => { - const input = await testSubjects.find('heartbeat-indices-input-loaded', 5000); - const heartbeatIndices = await input.getAttribute('value'); + const indInput = await testSubjects.find('heartbeat-indices-input-loaded', 5000); + const errorInput = await testSubjects.find('error-state-threshold-input-loaded', 5000); + const warningInput = await testSubjects.find('warning-state-threshold-input-loaded', 5000); + const heartbeatIndices = await indInput.getAttribute('value'); + const errorThreshold = await errorInput.getAttribute('value'); + const warningThreshold = await warningInput.getAttribute('value'); - return { heartbeatIndices }; + return { + heartbeatIndices, + certificatesThresholds: { + errorState: errorThreshold, + warningState: warningThreshold, + }, + }; }, applyButtonIsDisabled: async () => { return !!(await (await testSubjects.find('apply-settings-button')).getAttribute('disabled')); diff --git a/x-pack/test/functional/services/uptime/uptime.ts b/x-pack/test/functional/services/uptime/uptime.ts index c96bd0e0c4675..601feb6b0646e 100644 --- a/x-pack/test/functional/services/uptime/uptime.ts +++ b/x-pack/test/functional/services/uptime/uptime.ts @@ -11,6 +11,7 @@ import { UptimeCommonProvider } from './common'; import { UptimeMonitorProvider } from './monitor'; import { UptimeNavigationProvider } from './navigation'; import { UptimeAlertsProvider } from './alerts'; +import { UptimeMLAnomalyProvider } from './ml_anomaly'; export function UptimeProvider(context: FtrProviderContext) { const common = UptimeCommonProvider(context); @@ -18,6 +19,7 @@ export function UptimeProvider(context: FtrProviderContext) { const monitor = UptimeMonitorProvider(context); const navigation = UptimeNavigationProvider(context); const alerts = UptimeAlertsProvider(context); + const ml = UptimeMLAnomalyProvider(context); return { common, @@ -25,5 +27,6 @@ export function UptimeProvider(context: FtrProviderContext) { monitor, navigation, alerts, + ml, }; } diff --git a/x-pack/test/functional_endpoint/apps/endpoint/host_list.ts b/x-pack/test/functional_endpoint/apps/endpoint/host_list.ts index 2e204775808c9..35843dc6a76db 100644 --- a/x-pack/test/functional_endpoint/apps/endpoint/host_list.ts +++ b/x-pack/test/functional_endpoint/apps/endpoint/host_list.ts @@ -12,7 +12,8 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { const esArchiver = getService('esArchiver'); const testSubjects = getService('testSubjects'); - describe('host list', function() { + // FLAKY: https://github.com/elastic/kibana/issues/63621 + describe.skip('host list', function() { this.tags('ciGroup7'); const sleep = (ms = 100) => new Promise(resolve => setTimeout(resolve, ms)); before(async () => { @@ -167,7 +168,7 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { '', '0', '00000000-0000-0000-0000-000000000000', - 'active', + 'Successful', '10.101.149.262606:a000:ffc0:39:11ef:37b9:3371:578c', 'rezzani-7.example.com', '6.8.0', diff --git a/x-pack/test/functional_endpoint/config.ts b/x-pack/test/functional_endpoint/config.ts index 37bf57b67b47e..6ae78ab9d48ac 100644 --- a/x-pack/test/functional_endpoint/config.ts +++ b/x-pack/test/functional_endpoint/config.ts @@ -30,6 +30,7 @@ export default async function({ readConfigFile }: FtrConfigProviderContext) { ...xpackFunctionalConfig.get('kbnTestServer.serverArgs'), '--xpack.endpoint.enabled=true', '--xpack.ingestManager.enabled=true', + '--xpack.ingestManager.epm.enabled=true', '--xpack.ingestManager.fleet.enabled=true', ], }, diff --git a/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/alerts.ts b/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/alerts.ts index 029af1ea06e4f..bbf8881f0c62a 100644 --- a/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/alerts.ts +++ b/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/alerts.ts @@ -17,6 +17,7 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { const pageObjects = getPageObjects(['common', 'triggersActionsUI', 'header']); const supertest = getService('supertest'); const find = getService('find'); + const retry = getService('retry'); async function createAlert(overwrites: Record = {}) { const { body: createdAlert } = await supertest @@ -38,8 +39,7 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { return createdAlert; } - // FLAKY: https://github.com/elastic/kibana/issues/62472 - describe.skip('alerts', function() { + describe('alerts', function() { before(async () => { await pageObjects.common.navigateToApp('triggersActions'); await testSubjects.click('alertsTab'); @@ -48,10 +48,7 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { it('should create an alert', async () => { const alertName = generateUniqueKey(); await pageObjects.triggersActionsUI.clickCreateAlertButton(); - const nameInput = await testSubjects.find('alertNameInput'); - await nameInput.click(); - await nameInput.clearValue(); - await nameInput.type(alertName); + await testSubjects.setValue('alertNameInput', alertName); await testSubjects.click('.index-threshold-SelectOption'); await testSubjects.click('selectIndexExpression'); const comboBox = await find.byCssSelector('#indexSelectSearchBox'); @@ -60,30 +57,28 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { const filterSelectItem = await find.byCssSelector(`.euiFilterSelectItem`); await filterSelectItem.click(); await testSubjects.click('thresholdAlertTimeFieldSelect'); - const fieldOptions = await find.allByCssSelector('#thresholdTimeField option'); - await fieldOptions[1].click(); + await retry.try(async () => { + const fieldOptions = await find.allByCssSelector('#thresholdTimeField option'); + expect(fieldOptions[1]).not.to.be(undefined); + await fieldOptions[1].click(); + }); + await testSubjects.click('closePopover'); // need this two out of popup clicks to close them + const nameInput = await testSubjects.find('alertNameInput'); await nameInput.click(); await testSubjects.click('.slack-ActionTypeSelectOption'); await testSubjects.click('createActionConnectorButton'); - const connectorNameInput = await testSubjects.find('nameInput'); - await connectorNameInput.click(); - await connectorNameInput.clearValue(); - const connectorName = generateUniqueKey(); - await connectorNameInput.type(connectorName); - const slackWebhookUrlInput = await testSubjects.find('slackWebhookUrlInput'); - await slackWebhookUrlInput.click(); - await slackWebhookUrlInput.clearValue(); - await slackWebhookUrlInput.type('https://test'); + const slackConnectorName = generateUniqueKey(); + await testSubjects.setValue('nameInput', slackConnectorName); + await testSubjects.setValue('slackWebhookUrlInput', 'https://test'); await find.clickByCssSelector('[data-test-subj="saveActionButtonModal"]:not(disabled)'); - const loggingMessageInput = await testSubjects.find('slackMessageTextArea'); - await loggingMessageInput.click(); - await loggingMessageInput.clearValue(); - await loggingMessageInput.type('test message'); - await testSubjects.click('slackAddVariableButton'); - const variableMenuButton = await testSubjects.find('variableMenuButton-0'); - await variableMenuButton.click(); + const createdConnectorToastTitle = await pageObjects.common.closeToast(); + expect(createdConnectorToastTitle).to.eql(`Created '${slackConnectorName}'`); + await testSubjects.setValue('slackMessageTextArea', 'test message'); + await testSubjects.click('messageAddVariableButton'); + await testSubjects.click('variableMenuButton-0'); + await testSubjects.click('saveAlertButton'); const toastTitle = await pageObjects.common.closeToast(); expect(toastTitle).to.eql(`Saved '${alertName}'`); @@ -134,7 +129,7 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { it('should edit an alert', async () => { const createdAlert = await createAlert({ alertTypeId: '.index-threshold', - name: 'new alert', + name: generateUniqueKey(), params: { aggType: 'count', termSize: 5, @@ -162,11 +157,8 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { const editLink = await testSubjects.findAll('alertsTableCell-editLink'); await editLink[0].click(); - const updatedAlertName = 'Changed Alert Name'; - const nameInputToUpdate = await testSubjects.find('alertNameInput'); - await nameInputToUpdate.click(); - await nameInputToUpdate.clearValue(); - await nameInputToUpdate.type(updatedAlertName); + const updatedAlertName = `Changed Alert Name ${generateUniqueKey()}`; + await testSubjects.setValue('alertNameInput', updatedAlertName, { clearWithKeyboard: true }); await find.clickByCssSelector('[data-test-subj="saveEditedAlertButton"]:not(disabled)'); @@ -219,10 +211,7 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { const editLink = await testSubjects.findAll('alertsTableCell-editLink'); await editLink[0].click(); - const throttleInputToSetInitialValue = await testSubjects.find('throttleInput'); - await throttleInputToSetInitialValue.click(); - await throttleInputToSetInitialValue.clearValue(); - await throttleInputToSetInitialValue.type('1'); + await testSubjects.setValue('throttleInput', '1', { clearWithKeyboard: true }); await find.clickByCssSelector('[data-test-subj="saveEditedAlertButton"]:not(disabled)'); @@ -310,11 +299,8 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { const editLink = await testSubjects.findAll('alertsTableCell-editLink'); await editLink[0].click(); - const updatedAlertName = 'Changed Alert Name'; - const nameInputToUpdate = await testSubjects.find('alertNameInput'); - await nameInputToUpdate.click(); - await nameInputToUpdate.clearValue(); - await nameInputToUpdate.type(updatedAlertName); + const updatedAlertName = `Changed Alert Name ${generateUniqueKey()}`; + await testSubjects.setValue('alertNameInput', updatedAlertName); await testSubjects.click('cancelSaveEditedAlertButton'); await find.waitForDeletedByCssSelector('[data-test-subj="cancelSaveEditedAlertButton"]'); @@ -357,15 +343,15 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { await testSubjects.click('collapsedItemActions'); - await pageObjects.triggersActionsUI.toggleSwitch('enableSwitch'); + await pageObjects.triggersActionsUI.toggleSwitch('disableSwitch'); await pageObjects.triggersActionsUI.searchAlerts(createdAlert.name); await testSubjects.click('collapsedItemActions'); - const enableSwitchAfterDisable = await testSubjects.find('enableSwitch'); - const isChecked = await enableSwitchAfterDisable.getAttribute('aria-checked'); - expect(isChecked).to.eql('false'); + const disableSwitchAfterDisable = await testSubjects.find('disableSwitch'); + const isChecked = await disableSwitchAfterDisable.getAttribute('aria-checked'); + expect(isChecked).to.eql('true'); }); it('should re-enable single alert', async () => { @@ -375,21 +361,21 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { await testSubjects.click('collapsedItemActions'); - await pageObjects.triggersActionsUI.toggleSwitch('enableSwitch'); + await pageObjects.triggersActionsUI.toggleSwitch('disableSwitch'); await pageObjects.triggersActionsUI.searchAlerts(createdAlert.name); await testSubjects.click('collapsedItemActions'); - await pageObjects.triggersActionsUI.toggleSwitch('enableSwitch'); + await pageObjects.triggersActionsUI.toggleSwitch('disableSwitch'); await pageObjects.triggersActionsUI.searchAlerts(createdAlert.name); await testSubjects.click('collapsedItemActions'); - const enableSwitchAfterReEnable = await testSubjects.find('enableSwitch'); - const isChecked = await enableSwitchAfterReEnable.getAttribute('aria-checked'); - expect(isChecked).to.eql('true'); + const disableSwitchAfterReEnable = await testSubjects.find('disableSwitch'); + const isChecked = await disableSwitchAfterReEnable.getAttribute('aria-checked'); + expect(isChecked).to.eql('false'); }); it('should mute single alert', async () => { @@ -521,9 +507,9 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { await testSubjects.click('collapsedItemActions'); - const enableSwitch = await testSubjects.find('enableSwitch'); - const isChecked = await enableSwitch.getAttribute('aria-checked'); - expect(isChecked).to.eql('false'); + const disableSwitch = await testSubjects.find('disableSwitch'); + const isChecked = await disableSwitch.getAttribute('aria-checked'); + expect(isChecked).to.eql('true'); }); it('should enable all selection', async () => { @@ -546,9 +532,9 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { await testSubjects.click('collapsedItemActions'); - const enableSwitch = await testSubjects.find('enableSwitch'); - const isChecked = await enableSwitch.getAttribute('aria-checked'); - expect(isChecked).to.eql('true'); + const disableSwitch = await testSubjects.find('disableSwitch'); + const isChecked = await disableSwitch.getAttribute('aria-checked'); + expect(isChecked).to.eql('false'); }); it('should delete all selection', async () => { diff --git a/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/connectors.ts b/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/connectors.ts index c2013ba3502e2..562f64656319e 100644 --- a/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/connectors.ts +++ b/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/connectors.ts @@ -13,12 +13,20 @@ function generateUniqueKey() { } export default ({ getPageObjects, getService }: FtrProviderContext) => { + const alerting = getService('alerting'); const testSubjects = getService('testSubjects'); const pageObjects = getPageObjects(['common', 'triggersActionsUI', 'header']); const find = getService('find'); describe('Connectors', function() { before(async () => { + await alerting.actions.createAction({ + name: `server-log-${Date.now()}`, + actionTypeId: '.server-log', + config: {}, + secrets: {}, + }); + await pageObjects.common.navigateToApp('triggersActions'); await testSubjects.click('connectorsTab'); }); @@ -176,5 +184,30 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { const searchResultsAfterDelete = await pageObjects.triggersActionsUI.getConnectorsList(); expect(searchResultsAfterDelete.length).to.eql(0); }); + + it('should not be able to delete a preconfigured connector', async () => { + const preconfiguredConnectorName = 'xyz'; + await pageObjects.triggersActionsUI.searchConnectors(preconfiguredConnectorName); + + const searchResults = await pageObjects.triggersActionsUI.getConnectorsList(); + expect(searchResults.length).to.eql(1); + + expect(await testSubjects.exists('deleteConnector')).to.be(false); + expect(await testSubjects.exists('preConfiguredTitleMessage')).to.be(true); + }); + + it('should not be able to edit a preconfigured connector', async () => { + const preconfiguredConnectorName = 'xyz'; + + await pageObjects.triggersActionsUI.searchConnectors(preconfiguredConnectorName); + + const searchResultsBeforeEdit = await pageObjects.triggersActionsUI.getConnectorsList(); + expect(searchResultsBeforeEdit.length).to.eql(1); + + await find.clickByCssSelector('[data-test-subj="connectorsTableCell-name"] button'); + + expect(await testSubjects.exists('preconfiguredBadge')).to.be(true); + expect(await testSubjects.exists('saveEditedActionButton')).to.be(false); + }); }); }; diff --git a/x-pack/test/functional_with_es_ssl/apps/uptime/alert_flyout.ts b/x-pack/test/functional_with_es_ssl/apps/uptime/alert_flyout.ts index 2a0358160da51..3e5a8c57c4c7e 100644 --- a/x-pack/test/functional_with_es_ssl/apps/uptime/alert_flyout.ts +++ b/x-pack/test/functional_with_es_ssl/apps/uptime/alert_flyout.ts @@ -33,7 +33,7 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { // put the fetch code in a retry block with a timeout. let alert: any; await retry.tryForTime(15000, async () => { - const apiResponse = await supertest.get('/api/alert/_find'); + const apiResponse = await supertest.get('/api/alert/_find?search=uptime-test'); const alertsFromThisTest = apiResponse.body.data.filter( ({ name }: { name: string }) => name === 'uptime-test' ); @@ -54,25 +54,27 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { tags, } = alert; - // we're not testing the flyout's ability to associate alerts with action connectors - expect(actions).to.eql([]); + try { + // we're not testing the flyout's ability to associate alerts with action connectors + expect(actions).to.eql([]); - expect(alertTypeId).to.eql('xpack.uptime.alerts.monitorStatus'); - expect(consumer).to.eql('uptime'); - expect(interval).to.eql('11m'); - expect(tags).to.eql(['uptime', 'another']); - expect(numTimes).to.be(3); - expect(timerange.from).to.be('now-1h'); - expect(timerange.to).to.be('now'); - expect(locations).to.eql(['mpls']); - expect(filters).to.eql( - '{"bool":{"should":[{"match_phrase":{"monitor.id":"0001-up"}}],"minimum_should_match":1}}' - ); - - await supertest - .delete(`/api/alert/${id}`) - .set('kbn-xsrf', 'true') - .expect(204); + expect(alertTypeId).to.eql('xpack.uptime.alerts.monitorStatus'); + expect(consumer).to.eql('uptime'); + expect(interval).to.eql('11m'); + expect(tags).to.eql(['uptime', 'another']); + expect(numTimes).to.be(3); + expect(timerange.from).to.be('now-1h'); + expect(timerange.to).to.be('now'); + expect(locations).to.eql(['mpls']); + expect(filters).to.eql( + '{"bool":{"should":[{"match_phrase":{"monitor.id":"0001-up"}}],"minimum_should_match":1}}' + ); + } finally { + await supertest + .delete(`/api/alert/${id}`) + .set('kbn-xsrf', 'true') + .expect(204); + } }); }); }; diff --git a/x-pack/test/functional_with_es_ssl/config.ts b/x-pack/test/functional_with_es_ssl/config.ts index 538817bd9d14c..a620b1d953376 100644 --- a/x-pack/test/functional_with_es_ssl/config.ts +++ b/x-pack/test/functional_with_es_ssl/config.ts @@ -52,6 +52,16 @@ export default async function({ readConfigFile }: FtrConfigProviderContext) { `--plugin-path=${join(__dirname, 'fixtures', 'plugins', 'alerts')}`, '--xpack.actions.enabled=true', '--xpack.alerting.enabled=true', + `--xpack.actions.preconfigured=${JSON.stringify([ + { + id: 'my-slack1', + actionTypeId: '.slack', + name: 'Slack#xyz', + config: { + webhookUrl: 'https://hooks.slack.com/services/abcd/efgh/ijklmnopqrstuvwxyz', + }, + }, + ])}`, ], }, }; diff --git a/x-pack/test/licensing_plugin/legacy/updates.ts b/x-pack/test/licensing_plugin/legacy/updates.ts index 5fa1299d1f285..03b61b9db87f8 100644 --- a/x-pack/test/licensing_plugin/legacy/updates.ts +++ b/x-pack/test/licensing_plugin/legacy/updates.ts @@ -50,14 +50,9 @@ export default function(ftrContext: FtrProviderContext) { await scenario.startBasic(); await scenario.waitForPluginToDetectLicenseUpdate(); - const { body: legacyBasicLicense, header: legacyBasicLicenseHeaders } = await supertest - .get('/api/xpack/v1/info') - .expect(200); + const { body: legacyBasicLicense } = await supertest.get('/api/xpack/v1/info').expect(200); expect(legacyBasicLicense.license?.type).to.be('basic'); expect(legacyBasicLicense.features).to.have.property('security'); - expect(legacyBasicLicenseHeaders['kbn-xpack-sig']).to.not.be( - legacyInitialLicenseHeaders['kbn-xpack-sig'] - ); // banner shown only when license expired not just deleted await testSubjects.missingOrFail('licenseExpiredBanner'); diff --git a/x-pack/test/plugin_api_integration/plugins/event_log/server/init_routes.ts b/x-pack/test/plugin_api_integration/plugins/event_log/server/init_routes.ts new file mode 100644 index 0000000000000..c5f3e65581df9 --- /dev/null +++ b/x-pack/test/plugin_api_integration/plugins/event_log/server/init_routes.ts @@ -0,0 +1,228 @@ +/* + * 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 { + RequestHandlerContext, + KibanaRequest, + KibanaResponseFactory, + IKibanaResponse, + IRouter, + Logger, + RouteValidationResultFactory, +} from 'kibana/server'; +import { IEventLogService, IEventLogger } from '../../../../../plugins/event_log/server'; +import { IValidatedEvent } from '../../../../../plugins/event_log/server/types'; + +export const logEventRoute = (router: IRouter, eventLogger: IEventLogger, logger: Logger) => { + router.post( + { + path: `/api/log_event_fixture/{id}/_log`, + validate: { + // removed validation as schema is currently broken in tests + // blocked by: https://github.com/elastic/kibana/issues/61652 + params: (value: any, { ok }: RouteValidationResultFactory) => ok(value), + body: (value: any, { ok }: RouteValidationResultFactory) => ok(value), + }, + }, + async function( + context: RequestHandlerContext, + req: KibanaRequest, + res: KibanaResponseFactory + ): Promise> { + const { id } = req.params as { id: string }; + const event: IValidatedEvent = req.body; + logger.info(`test fixture: log event: ${id} ${JSON.stringify(event)}`); + try { + await context.core.savedObjects.client.get('event_log_test', id); + logger.info(`found existing saved object`); + } catch (ex) { + logger.info(`log event error: ${ex}`); + await context.core.savedObjects.client.create('event_log_test', {}, { id }); + logger.info(`created saved object`); + } + eventLogger.logEvent(event); + logger.info(`logged`); + return res.ok({}); + } + ); +}; + +export const registerProviderActionsRoute = ( + router: IRouter, + eventLogService: IEventLogService, + logger: Logger +) => { + router.post( + { + path: '/api/log_event_fixture/{provider}/_registerProviderActions', + validate: { + body: value => ({ value }), + params: (value: any, { ok }: RouteValidationResultFactory) => ok(value), + }, + options: { authRequired: false }, + }, + (context, request, response) => { + const { provider } = request.params as { provider: string }; + const actions = request.body; + try { + logger.info( + `test register provider actions: ${provider}, actions: ${JSON.stringify(actions)}` + ); + + eventLogService.registerProviderActions(provider, actions); + logger.info(`registered`); + } catch (e) { + return response.badRequest({ body: e }); + } + return response.ok({ body: {} }); + } + ); +}; + +export const isProviderActionRegisteredRoute = ( + router: IRouter, + eventLogService: IEventLogService, + logger: Logger +) => { + router.get( + { + path: `/api/log_event_fixture/{provider}/{action}/_isProviderActionRegistered`, + validate: { + params: (value: any, { ok }: RouteValidationResultFactory) => ok(value), + }, + }, + async function( + context: RequestHandlerContext, + req: KibanaRequest, + res: KibanaResponseFactory + ): Promise> { + const { provider, action } = req.params as { provider: string; action: string }; + logger.info(`test provider actions is registered: ${provider} for action: ${action}`); + + return res.ok({ + body: { + isProviderActionRegistered: eventLogService.isProviderActionRegistered(provider, action), + }, + }); + } + ); +}; + +export const getProviderActionsRoute = ( + router: IRouter, + eventLogService: IEventLogService, + logger: Logger +) => { + router.get( + { + path: `/api/log_event_fixture/{provider}/getProviderActions`, + validate: { + params: (value: any, { ok }: RouteValidationResultFactory) => ok(value), + }, + }, + async function( + context: RequestHandlerContext, + req: KibanaRequest, + res: KibanaResponseFactory + ): Promise> { + const { provider } = req.params as { provider: string }; + + logger.info(`test if get all provider actions is registered`); + return res.ok({ + body: { actions: [...(eventLogService.getProviderActions().get(provider) ?? [])] }, + }); + } + ); +}; + +export const getLoggerRoute = ( + router: IRouter, + eventLogService: IEventLogService, + logger: Logger +) => { + router.get( + { + path: `/api/log_event_fixture/getEventLogger/{event}`, + validate: { + params: (value: any, { ok }: RouteValidationResultFactory) => ok(value), + }, + }, + async function( + context: RequestHandlerContext, + req: KibanaRequest, + res: KibanaResponseFactory + ): Promise> { + const { event } = req.params as { event: string }; + logger.info(`test get event logger for event: ${event}`); + + return res.ok({ + body: { eventLogger: eventLogService.getLogger({ event: { provider: event } }) }, + }); + } + ); +}; + +export const isIndexingEntriesRoute = ( + router: IRouter, + eventLogService: IEventLogService, + logger: Logger +) => { + router.get( + { + path: `/api/log_event_fixture/isIndexingEntries`, + validate: {}, + }, + async function( + context: RequestHandlerContext, + req: KibanaRequest, + res: KibanaResponseFactory + ): Promise> { + logger.info(`test if event logger is indexing entries`); + return res.ok({ body: { isIndexingEntries: eventLogService.isIndexingEntries() } }); + } + ); +}; + +export const isEventLogServiceEnabledRoute = ( + router: IRouter, + eventLogService: IEventLogService, + logger: Logger +) => { + router.get( + { + path: `/api/log_event_fixture/isEventLogServiceEnabled`, + validate: {}, + }, + async function( + context: RequestHandlerContext, + req: KibanaRequest, + res: KibanaResponseFactory + ): Promise> { + logger.info(`test if event logger is enabled`); + return res.ok({ body: { isEnabled: eventLogService.isEnabled() } }); + } + ); +}; + +export const isEventLogServiceLoggingEntriesRoute = ( + router: IRouter, + eventLogService: IEventLogService, + logger: Logger +) => { + router.get( + { + path: `/api/log_event_fixture/isEventLogServiceLoggingEntries`, + validate: {}, + }, + async function( + context: RequestHandlerContext, + req: KibanaRequest, + res: KibanaResponseFactory + ): Promise> { + logger.info(`test if event logger is logging entries`); + return res.ok({ body: { isLoggingEntries: eventLogService.isLoggingEntries() } }); + } + ); +}; diff --git a/x-pack/test/plugin_api_integration/plugins/event_log/server/plugin.ts b/x-pack/test/plugin_api_integration/plugins/event_log/server/plugin.ts index eccbd4fb7f90b..2ef932d19e9ee 100644 --- a/x-pack/test/plugin_api_integration/plugins/event_log/server/plugin.ts +++ b/x-pack/test/plugin_api_integration/plugins/event_log/server/plugin.ts @@ -4,24 +4,18 @@ * you may not use this file except in compliance with the Elastic License. */ +import { Plugin, CoreSetup, Logger, PluginInitializerContext } from 'kibana/server'; +import { IEventLogService, IEventLogClientService } from '../../../../../plugins/event_log/server'; import { - Plugin, - CoreSetup, - RequestHandlerContext, - KibanaRequest, - KibanaResponseFactory, - IKibanaResponse, - IRouter, - Logger, - PluginInitializerContext, - RouteValidationResultFactory, -} from 'kibana/server'; -import { - IEventLogService, - IEventLogClientService, - IEventLogger, -} from '../../../../../plugins/event_log/server'; -import { IValidatedEvent } from '../../../../../plugins/event_log/server/types'; + logEventRoute, + registerProviderActionsRoute, + isProviderActionRegisteredRoute, + getProviderActionsRoute, + getLoggerRoute, + isIndexingEntriesRoute, + isEventLogServiceLoggingEntriesRoute, + isEventLogServiceEnabledRoute, +} from './init_routes'; // this plugin's dependendencies export interface EventLogFixtureSetupDeps { @@ -50,49 +44,24 @@ export class EventLogFixturePlugin core.savedObjects.registerType({ name: 'event_log_test', hidden: false, - namespaceAgnostic: true, + namespaceType: 'agnostic', mappings: { properties: {}, }, }); logEventRoute(router, eventLogger, this.logger); + + // event log service api routes + registerProviderActionsRoute(router, eventLog, this.logger); + isProviderActionRegisteredRoute(router, eventLog, this.logger); + getProviderActionsRoute(router, eventLog, this.logger); + getLoggerRoute(router, eventLog, this.logger); + isIndexingEntriesRoute(router, eventLog, this.logger); + isEventLogServiceLoggingEntriesRoute(router, eventLog, this.logger); + isEventLogServiceEnabledRoute(router, eventLog, this.logger); } public start() {} public stop() {} } - -const logEventRoute = (router: IRouter, eventLogger: IEventLogger, logger: Logger) => { - router.post( - { - path: `/api/log_event_fixture/{id}/_log`, - validate: { - // removed validation as schema is currently broken in tests - // blocked by: https://github.com/elastic/kibana/issues/61652 - params: (value: any, { ok }: RouteValidationResultFactory) => ok(value), - body: (value: any, { ok }: RouteValidationResultFactory) => ok(value), - }, - }, - async function( - context: RequestHandlerContext, - req: KibanaRequest, - res: KibanaResponseFactory - ): Promise> { - const { id } = req.params as { id: string }; - const event: IValidatedEvent = req.body; - logger.info(`test fixture: log event: ${id} ${JSON.stringify(event)}`); - try { - await context.core.savedObjects.client.get('event_log_test', id); - logger.info(`found existing saved object`); - } catch (ex) { - logger.info(`log event error: ${ex}`); - await context.core.savedObjects.client.create('event_log_test', {}, { id }); - logger.info(`created saved object`); - } - eventLogger.logEvent(event); - logger.info(`logged`); - return res.ok({}); - } - ); -}; diff --git a/x-pack/test/plugin_api_integration/test_suites/event_log/public_api_integration.ts b/x-pack/test/plugin_api_integration/test_suites/event_log/public_api_integration.ts index c440971225d78..d664357c3ba12 100644 --- a/x-pack/test/plugin_api_integration/test_suites/event_log/public_api_integration.ts +++ b/x-pack/test/plugin_api_integration/test_suites/event_log/public_api_integration.ts @@ -220,9 +220,9 @@ export default function({ getService }: FtrProviderContext) { duration: 1000000, }, kibana: { - namespace: 'default', saved_objects: [ { + namespace: 'default', type: 'event_log_test', id, }, diff --git a/x-pack/test/plugin_api_integration/test_suites/event_log/service_api_integration.ts b/x-pack/test/plugin_api_integration/test_suites/event_log/service_api_integration.ts index b055b22879bf9..2de395308ce74 100644 --- a/x-pack/test/plugin_api_integration/test_suites/event_log/service_api_integration.ts +++ b/x-pack/test/plugin_api_integration/test_suites/event_log/service_api_integration.ts @@ -3,9 +3,181 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ +import expect from '@kbn/expect/expect.js'; +import { IEvent } from '../../../../plugins/event_log/server'; +import { FtrProviderContext } from '../../ftr_provider_context'; + +export default function({ getService }: FtrProviderContext) { + const es = getService('legacyEs'); + const supertest = getService('supertest'); + const log = getService('log'); + const config = getService('config'); + const retry = getService('retry'); -export default function() { describe('Event Log service API', () => { - it('should allow logging an event', async () => {}); + it('should check if it is enabled', async () => { + const configValue = config + .get('kbnTestServer.serverArgs') + .find((val: string) => val === '--xpack.eventLog.enabled=true'); + const result = await isEventLogServiceEnabled(); + expect(configValue).to.be.eql(`--xpack.eventLog.enabled=${result.body.isEnabled}`); + }); + + it('should check if logging entries is enabled', async () => { + const configValue = config + .get('kbnTestServer.serverArgs') + .find((val: string) => val === '--xpack.eventLog.logEntries=true'); + const result = await isEventLogServiceLoggingEntries(); + expect(configValue).to.be.eql(`--xpack.eventLog.logEntries=${result.body.isLoggingEntries}`); + }); + + it('should check if indexing entries is enabled', async () => { + const configValue = config + .get('kbnTestServer.serverArgs') + .find((val: string) => val === '--xpack.eventLog.indexEntries=true'); + const result = await isIndexingEntries(); + const exists = await es.indices.exists({ index: '.kibana-event-log-*' }); + expect(exists).to.be.eql(true); + expect(configValue).to.be.eql( + `--xpack.eventLog.indexEntries=${result.body.isIndexingEntries}` + ); + }); + + it('should be able to check if provider actions is registered', async () => { + const initResult = await isProviderActionRegistered('provider3', 'action1'); + + if (!initResult.body.isProviderActionRegistered) { + await registerProviderActions('provider3', ['action1']); + } + const result1 = await isProviderActionRegistered('provider3', 'action1'); + expect(result1.body.isProviderActionRegistered).to.be.eql(true); + + const result = await isProviderActionRegistered('provider3', 'action2'); + expect(result.body.isProviderActionRegistered).to.be.eql(false); + }); + + it('should return error message if provider is registered', async () => { + const initResult = await isProviderActionRegistered('duplication', 'action1'); + + if (!initResult.body.isProviderActionRegistered) { + await registerProviderActions('duplication', ['action1', 'action2']); + } + + const result = await registerProviderActions('duplication', ['action1', 'action2']); + expect(result.badRequest).to.be.eql(true); + }); + + it('should allow to register provider actions and return all provider actions', async () => { + const initResult = await isProviderActionRegistered('provider1', 'action1'); + + if (!initResult.body.isProviderActionRegistered) { + await registerProviderActions('provider1', ['action1', 'action2']); + } + + const providerActions = await getProviderActions('provider1'); + expect(providerActions.body.actions).to.be.eql(['action1', 'action2']); + }); + + it('should allow to get event logger event log service', async () => { + const initResult = await isProviderActionRegistered('provider2', 'action1'); + + if (!initResult.body.isProviderActionRegistered) { + await registerProviderActions('provider2', ['action1', 'action2']); + } + const eventLogger = await getEventLogger('provider2'); + expect(eventLogger.body.eventLogger.initialProperties).to.be.eql({ + event: { provider: 'provider2' }, + }); + }); + + it('should allow write an event to index document if indexing entries is enabled', async () => { + const initResult = await isProviderActionRegistered('provider4', 'action1'); + + if (!initResult.body.isProviderActionRegistered) { + await registerProviderActions('provider4', ['action1', 'action2']); + } + + const eventId = '1'; + const event: IEvent = { + event: { action: 'action1', provider: 'provider4' }, + kibana: { saved_objects: [{ type: 'event_log_test', id: eventId }] }, + }; + await logTestEvent(eventId, event); + + await retry.try(async () => { + const uri = `/api/event_log/event_log_test/${eventId}/_find`; + log.debug(`calling ${uri}`); + const result = await supertest + .get(uri) + .set('kbn-xsrf', 'foo') + .expect(200); + expect(result.body.data.length).to.be.eql(1); + }); + }); }); + + async function registerProviderActions(provider: string, actions: string[]) { + log.debug(`registerProviderActions ${provider}`); + return await supertest + .post(`/api/log_event_fixture/${provider}/_registerProviderActions`) + .set('kbn-xsrf', 'xxx') + .send(actions); + } + + async function isProviderActionRegistered(provider: string, action: string) { + log.debug(`isProviderActionRegistered ${provider} for action ${action}`); + return await supertest + .get(`/api/log_event_fixture/${provider}/${action}/_isProviderActionRegistered`) + .set('kbn-xsrf', 'foo') + .expect(200); + } + + async function getProviderActions(provider: string) { + log.debug(`getProviderActions ${provider}`); + return await supertest + .get(`/api/log_event_fixture/${provider}/getProviderActions`) + .set('kbn-xsrf', 'xxx') + .expect(200); + } + + async function getEventLogger(event: string) { + log.debug(`isProviderActionRegistered for event ${event}`); + return await supertest + .get(`/api/log_event_fixture/getEventLogger/${event}`) + .set('kbn-xsrf', 'foo') + .expect(200); + } + + async function isIndexingEntries() { + log.debug(`isIndexingEntries`); + return await supertest + .get(`/api/log_event_fixture/isIndexingEntries`) + .set('kbn-xsrf', 'foo') + .expect(200); + } + + async function isEventLogServiceEnabled() { + log.debug(`isEventLogServiceEnabled`); + return await supertest + .get(`/api/log_event_fixture/isEventLogServiceEnabled`) + .set('kbn-xsrf', 'foo') + .expect(200); + } + + async function isEventLogServiceLoggingEntries() { + log.debug(`isEventLogServiceLoggingEntries`); + return await supertest + .get(`/api/log_event_fixture/isEventLogServiceLoggingEntries`) + .set('kbn-xsrf', 'foo') + .expect(200); + } + + async function logTestEvent(id: string, event: IEvent) { + log.debug(`Logging Event for Saved Object ${id}`); + return await supertest + .post(`/api/log_event_fixture/${id}/_log`) + .set('kbn-xsrf', 'foo') + .send(event) + .expect(200); + } } diff --git a/x-pack/test/reporting/.gitignore b/x-pack/test/reporting/.gitignore new file mode 100644 index 0000000000000..99ee4c44686a0 --- /dev/null +++ b/x-pack/test/reporting/.gitignore @@ -0,0 +1 @@ +functional/reports/session/ diff --git a/x-pack/test/reporting/configs/chromium_functional.js b/x-pack/test/reporting/configs/chromium_functional.js index 05c3b6c142946..753d2b2a20ab9 100644 --- a/x-pack/test/reporting/configs/chromium_functional.js +++ b/x-pack/test/reporting/configs/chromium_functional.js @@ -5,6 +5,7 @@ */ export default async function({ readConfigFile }) { + // TODO move reporting tests to x-pack/test/functional/apps//reporting const functionalConfig = await readConfigFile(require.resolve('../../functional/config.js')); return { diff --git a/x-pack/test/reporting/functional/reporting.js b/x-pack/test/reporting/functional/reporting.js index 012f0922c28cf..6107363986a40 100644 --- a/x-pack/test/reporting/functional/reporting.js +++ b/x-pack/test/reporting/functional/reporting.js @@ -15,8 +15,14 @@ const mkdirAsync = promisify(fs.mkdir); const REPORTS_FOLDER = path.resolve(__dirname, 'reports'); +/* + * TODO Remove this file and spread the tests to various apps + */ + export default function({ getService, getPageObjects }) { - const retry = getService('retry'); + const esArchiver = getService('esArchiver'); + const browser = getService('browser'); + const log = getService('log'); const config = getService('config'); const PageObjects = getPageObjects([ 'reporting', @@ -27,65 +33,32 @@ export default function({ getService, getPageObjects }) { 'visualize', 'visEditor', ]); - const log = getService('log'); describe('Reporting', () => { - before('initialize tests', async () => { - await PageObjects.reporting.initTests(); - }); - - const expectDisabledGenerateReportButton = async () => { - const generateReportButton = await PageObjects.reporting.getGenerateReportButton(); - await retry.try(async () => { - const isDisabled = await generateReportButton.getAttribute('disabled'); - expect(isDisabled).to.be('true'); + describe('Dashboard', () => { + before('initialize tests', async () => { + log.debug('ReportingPage:initTests'); + await esArchiver.loadIfNeeded('reporting/ecommerce'); + await esArchiver.loadIfNeeded('reporting/ecommerce_kibana'); + await browser.setWindowSize(1600, 850); }); - }; - - const expectEnabledGenerateReportButton = async () => { - const generateReportButton = await PageObjects.reporting.getGenerateReportButton(); - await retry.try(async () => { - const isDisabled = await generateReportButton.getAttribute('disabled'); - expect(isDisabled).to.be(null); + after('clean up archives', async () => { + await esArchiver.unload('reporting/ecommerce'); + await esArchiver.unload('reporting/ecommerce_kibana'); }); - }; - - const expectReportCanBeCreated = async () => { - await PageObjects.reporting.clickGenerateReportButton(); - const success = await PageObjects.reporting.checkForReportingToasts(); - expect(success).to.be(true); - }; - - const writeSessionReport = async (name, rawPdf, reportExt = 'pdf') => { - const sessionDirectory = path.resolve(REPORTS_FOLDER, 'session'); - await mkdirAsync(sessionDirectory, { recursive: true }); - const sessionReportPath = path.resolve(sessionDirectory, `${name}.${reportExt}`); - await writeFileAsync(sessionReportPath, rawPdf); - return sessionReportPath; - }; - - const getBaselineReportPath = (fileName, reportExt = 'pdf') => { - const baselineFolder = path.resolve(REPORTS_FOLDER, 'baseline'); - const fullPath = path.resolve(baselineFolder, `${fileName}.${reportExt}`); - log.debug(`getBaselineReportPath (${fullPath})`); - return fullPath; - }; - - describe('Dashboard', () => { - beforeEach(() => PageObjects.reporting.clearToastNotifications()); describe('Print PDF button', () => { it('is not available if new', async () => { await PageObjects.common.navigateToApp('dashboard'); await PageObjects.dashboard.clickNewDashboard(); await PageObjects.reporting.openPdfReportingPanel(); - await expectDisabledGenerateReportButton(); + expect(await PageObjects.reporting.isGenerateReportButtonDisabled()).to.be('true'); }); it('becomes available when saved', async () => { await PageObjects.dashboard.saveDashboard('My PDF Dashboard'); await PageObjects.reporting.openPdfReportingPanel(); - await expectEnabledGenerateReportButton(); + expect(await PageObjects.reporting.isGenerateReportButtonDisabled()).to.be(null); }); }); @@ -95,15 +68,7 @@ export default function({ getService, getPageObjects }) { // function is taking about 15 seconds per comparison in jenkins. this.timeout(300000); await PageObjects.common.navigateToApp('dashboard'); - await PageObjects.dashboard.gotoDashboardEditMode('My PDF Dashboard'); - await PageObjects.reporting.setTimepickerInDataRange(); - const visualizations = PageObjects.dashboard.getTestVisualizationNames(); - - const tileMapIndex = visualizations.indexOf('Visualization TileMap'); - visualizations.splice(tileMapIndex, 1); - - await PageObjects.dashboard.addVisualizations(visualizations); - await PageObjects.dashboard.saveDashboard('report test'); + await PageObjects.dashboard.loadSavedDashboard('Ecom Dashboard'); await PageObjects.reporting.openPdfReportingPanel(); await PageObjects.reporting.checkUsePrintLayout(); await PageObjects.reporting.clickGenerateReportButton(); @@ -121,30 +86,36 @@ export default function({ getService, getPageObjects }) { await PageObjects.common.navigateToApp('dashboard'); await PageObjects.dashboard.clickNewDashboard(); await PageObjects.reporting.openPngReportingPanel(); - await expectDisabledGenerateReportButton(); + expect(await PageObjects.reporting.isGenerateReportButtonDisabled()).to.be('true'); }); it('becomes available when saved', async () => { await PageObjects.dashboard.saveDashboard('My PNG Dash'); await PageObjects.reporting.openPngReportingPanel(); - await expectEnabledGenerateReportButton(); + expect(await PageObjects.reporting.isGenerateReportButtonDisabled()).to.be(null); }); }); describe('Preserve Layout', () => { it('matches baseline report', async function() { + const writeSessionReport = async (name, rawPdf, reportExt) => { + const sessionDirectory = path.resolve(REPORTS_FOLDER, 'session'); + await mkdirAsync(sessionDirectory, { recursive: true }); + const sessionReportPath = path.resolve(sessionDirectory, `${name}.${reportExt}`); + await writeFileAsync(sessionReportPath, rawPdf); + return sessionReportPath; + }; + const getBaselineReportPath = (fileName, reportExt) => { + const baselineFolder = path.resolve(REPORTS_FOLDER, 'baseline'); + const fullPath = path.resolve(baselineFolder, `${fileName}.${reportExt}`); + log.debug(`getBaselineReportPath (${fullPath})`); + return fullPath; + }; + this.timeout(300000); await PageObjects.common.navigateToApp('dashboard'); - await PageObjects.dashboard.gotoDashboardEditMode('My PNG Dash'); - await PageObjects.reporting.setTimepickerInDataRange(); - - const visualizations = PageObjects.dashboard.getTestVisualizationNames(); - const tileMapIndex = visualizations.indexOf('Visualization TileMap'); - visualizations.splice(tileMapIndex, 1); - - await PageObjects.dashboard.addVisualizations(visualizations); - await PageObjects.dashboard.saveDashboard('PNG report test'); + await PageObjects.dashboard.loadSavedDashboard('Ecom Dashboard'); await PageObjects.reporting.openPngReportingPanel(); await PageObjects.reporting.forceSharedItemsContainerSize({ width: 1405 }); await PageObjects.reporting.clickGenerateReportButton(); @@ -167,44 +138,64 @@ export default function({ getService, getPageObjects }) { }); describe('Discover', () => { + before('initialize tests', async () => { + log.debug('ReportingPage:initTests'); + await esArchiver.loadIfNeeded('reporting/ecommerce'); + await browser.setWindowSize(1600, 850); + }); + after('clean up archives', async () => { + await esArchiver.unload('reporting/ecommerce'); + }); + describe('Generate CSV button', () => { beforeEach(() => PageObjects.common.navigateToApp('discover')); it('is not available if new', async () => { await PageObjects.reporting.openCsvReportingPanel(); - await expectDisabledGenerateReportButton(); + expect(await PageObjects.reporting.isGenerateReportButtonDisabled()).to.be('true'); }); it('becomes available when saved', async () => { await PageObjects.discover.saveSearch('my search - expectEnabledGenerateReportButton'); await PageObjects.reporting.openCsvReportingPanel(); - await expectEnabledGenerateReportButton(); + expect(await PageObjects.reporting.isGenerateReportButtonDisabled()).to.be(null); }); it('generates a report with data', async () => { await PageObjects.reporting.setTimepickerInDataRange(); await PageObjects.discover.saveSearch('my search - with data - expectReportCanBeCreated'); await PageObjects.reporting.openCsvReportingPanel(); - await expectReportCanBeCreated(); + expect(await PageObjects.reporting.canReportBeCreated()).to.be(true); }); it('generates a report with no data', async () => { await PageObjects.reporting.setTimepickerInNoDataRange(); await PageObjects.discover.saveSearch('my search - no data - expectReportCanBeCreated'); await PageObjects.reporting.openCsvReportingPanel(); - await expectReportCanBeCreated(); + expect(await PageObjects.reporting.canReportBeCreated()).to.be(true); }); }); }); describe('Visualize', () => { + before('initialize tests', async () => { + log.debug('ReportingPage:initTests'); + await esArchiver.loadIfNeeded('reporting/ecommerce'); + await esArchiver.loadIfNeeded('reporting/ecommerce_kibana'); + await browser.setWindowSize(1600, 850); + }); + after('clean up archives', async () => { + await esArchiver.unload('reporting/ecommerce'); + await esArchiver.unload('reporting/ecommerce_kibana'); + }); + describe('Print PDF button', () => { it('is not available if new', async () => { await PageObjects.common.navigateToUrl('visualize', 'new'); await PageObjects.visualize.clickAreaChart(); - await PageObjects.visualize.clickNewSearch(); + await PageObjects.visualize.clickNewSearch('ecommerce'); await PageObjects.reporting.openPdfReportingPanel(); - await expectDisabledGenerateReportButton(); + expect(await PageObjects.reporting.isGenerateReportButtonDisabled()).to.be('true'); }); it('becomes available when saved', async () => { @@ -214,14 +205,16 @@ export default function({ getService, getPageObjects }) { await PageObjects.visEditor.clickGo(); await PageObjects.visualize.saveVisualization('my viz'); await PageObjects.reporting.openPdfReportingPanel(); - await expectEnabledGenerateReportButton(); + expect(await PageObjects.reporting.isGenerateReportButtonDisabled()).to.be(null); }); - it('matches baseline report', async function() { + it('downloaded PDF has OK status', async function() { // Generating and then comparing reports can take longer than the default 60s timeout because the comparePngs // function is taking about 15 seconds per comparison in jenkins. this.timeout(180000); + await PageObjects.common.navigateToApp('dashboard'); + await PageObjects.dashboard.loadSavedDashboard('Ecom Dashboard'); await PageObjects.reporting.openPdfReportingPanel(); await PageObjects.reporting.clickGenerateReportButton(); diff --git a/x-pack/test/reporting/functional/reports/baseline/dashboard_preserve_layout.pdf b/x-pack/test/reporting/functional/reports/baseline/dashboard_preserve_layout.pdf deleted file mode 100644 index b7370109c687c..0000000000000 Binary files a/x-pack/test/reporting/functional/reports/baseline/dashboard_preserve_layout.pdf and /dev/null differ diff --git a/x-pack/test/reporting/functional/reports/baseline/dashboard_preserve_layout.png b/x-pack/test/reporting/functional/reports/baseline/dashboard_preserve_layout.png index a0dfea9ef4fa7..1eb5f29d212c2 100644 Binary files a/x-pack/test/reporting/functional/reports/baseline/dashboard_preserve_layout.png and b/x-pack/test/reporting/functional/reports/baseline/dashboard_preserve_layout.png differ diff --git a/x-pack/test/reporting/functional/reports/baseline/dashboard_print.pdf b/x-pack/test/reporting/functional/reports/baseline/dashboard_print.pdf deleted file mode 100644 index bf2bca54ca2d7..0000000000000 Binary files a/x-pack/test/reporting/functional/reports/baseline/dashboard_print.pdf and /dev/null differ diff --git a/x-pack/test/reporting/functional/reports/baseline/visualize_print.pdf b/x-pack/test/reporting/functional/reports/baseline/visualize_print.pdf deleted file mode 100644 index c11967c12ebf5..0000000000000 Binary files a/x-pack/test/reporting/functional/reports/baseline/visualize_print.pdf and /dev/null differ diff --git a/x-pack/test/saved_object_api_integration/common/config.ts b/x-pack/test/saved_object_api_integration/common/config.ts index dda7934ce875a..eaf7230a832a8 100644 --- a/x-pack/test/saved_object_api_integration/common/config.ts +++ b/x-pack/test/saved_object_api_integration/common/config.ts @@ -56,8 +56,10 @@ export function createTestConfig(name: string, options: CreateTestConfigOptions) ...config.xpack.api.get('kbnTestServer.serverArgs'), '--optimize.enabled=false', '--server.xsrf.disableProtection=true', + `--plugin-path=${path.join(__dirname, 'fixtures', 'isolated_type_plugin')}`, `--plugin-path=${path.join(__dirname, 'fixtures', 'namespace_agnostic_type_plugin')}`, `--plugin-path=${path.join(__dirname, 'fixtures', 'hidden_type_plugin')}`, + `--plugin-path=${path.join(__dirname, 'fixtures', 'shared_type_plugin')}`, ...disabledPlugins.map(key => `--xpack.${key}.enabled=false`), ], }, diff --git a/x-pack/test/saved_object_api_integration/common/fixtures/es_archiver/saved_objects/spaces/data.json b/x-pack/test/saved_object_api_integration/common/fixtures/es_archiver/saved_objects/spaces/data.json index 34361ad9df542..d2c14189e2529 100644 --- a/x-pack/test/saved_object_api_integration/common/fixtures/es_archiver/saved_objects/spaces/data.json +++ b/x-pack/test/saved_object_api_integration/common/fixtures/es_archiver/saved_objects/spaces/data.json @@ -53,7 +53,7 @@ { "type": "doc", "value": { - "id": "index-pattern:91200a00-9efd-11e7-acb3-3dab96693fab", + "id": "index-pattern:defaultspace-index-pattern-id", "index": ".kibana", "source": { "index-pattern": { @@ -76,7 +76,7 @@ "source": { "config": { "buildNum": 8467, - "defaultIndex": "91200a00-9efd-11e7-acb3-3dab96693fab" + "defaultIndex": "defaultspace-index-pattern-id" }, "type": "config", "updated_at": "2017-09-21T18:49:16.302Z" @@ -88,15 +88,15 @@ { "type": "doc", "value": { - "id": "visualization:dd7caf20-9efd-11e7-acb3-3dab96693fab", + "id": "isolatedtype:defaultspace-isolatedtype-id", "index": ".kibana", "source": { - "type": "visualization", + "type": "isolatedtype", "updated_at": "2017-09-21T18:51:23.794Z", - "visualization": { + "isolatedtype": { "description": "", "kibanaSavedObjectMeta": { - "searchSourceJSON": "{\"index\":\"91200a00-9efd-11e7-acb3-3dab96693fab\",\"filter\":[],\"query\":{\"query\":\"\",\"language\":\"lucene\"}}" + "searchSourceJSON": "{\"index\":\"defaultspace-index-pattern-id\",\"filter\":[],\"query\":{\"query\":\"\",\"language\":\"lucene\"}}" }, "title": "Count of requests", "uiStateJSON": "{\"spy\":{\"mode\":{\"name\":null,\"fill\":false}}}", @@ -111,7 +111,7 @@ { "type": "doc", "value": { - "id": "dashboard:be3733a0-9efe-11e7-acb3-3dab96693fab", + "id": "dashboard:defaultspace-dashboard-id", "index": ".kibana", "source": { "dashboard": { @@ -121,7 +121,7 @@ "searchSourceJSON": "{\"query\":{\"query\":\"\",\"language\":\"lucene\"},\"filter\":[],\"highlightAll\":true,\"version\":true}" }, "optionsJSON": "{}", - "panelsJSON": "[{\"size_x\":6,\"size_y\":3,\"panelIndex\":1,\"type\":\"visualization\",\"id\":\"dd7caf20-9efd-11e7-acb3-3dab96693fab\",\"col\":1,\"row\":1}]", + "panelsJSON": "[{\"size_x\":6,\"size_y\":3,\"panelIndex\":1,\"type\":\"isolatedtype\",\"id\":\"defaultspace-isolatedtype-id\",\"col\":1,\"row\":1}]", "refreshInterval": { "display": "Off", "pause": false, @@ -144,7 +144,7 @@ { "type": "doc", "value": { - "id": "space_1:index-pattern:space_1-91200a00-9efd-11e7-acb3-3dab96693fab", + "id": "space_1:index-pattern:space1-index-pattern-id", "index": ".kibana", "source": { "index-pattern": { @@ -168,7 +168,7 @@ "source": { "config": { "buildNum": 8467, - "defaultIndex": "91200a00-9efd-11e7-acb3-3dab96693fab" + "defaultIndex": "defaultspace-index-pattern-id" }, "namespace": "space_1", "type": "config", @@ -181,16 +181,16 @@ { "type": "doc", "value": { - "id": "space_1:visualization:space_1-dd7caf20-9efd-11e7-acb3-3dab96693fab", + "id": "space_1:isolatedtype:space1-isolatedtype-id", "index": ".kibana", "source": { "namespace": "space_1", - "type": "visualization", + "type": "isolatedtype", "updated_at": "2017-09-21T18:51:23.794Z", - "visualization": { + "isolatedtype": { "description": "", "kibanaSavedObjectMeta": { - "searchSourceJSON": "{\"index\":\"space_1-91200a00-9efd-11e7-acb3-3dab96693fab\",\"filter\":[],\"query\":{\"query\":\"\",\"language\":\"lucene\"}}" + "searchSourceJSON": "{\"index\":\"space1-index-pattern-id\",\"filter\":[],\"query\":{\"query\":\"\",\"language\":\"lucene\"}}" }, "title": "Count of requests", "uiStateJSON": "{\"spy\":{\"mode\":{\"name\":null,\"fill\":false}}}", @@ -205,7 +205,7 @@ { "type": "doc", "value": { - "id": "space_1:dashboard:space_1-be3733a0-9efe-11e7-acb3-3dab96693fab", + "id": "space_1:dashboard:space1-dashboard-id", "index": ".kibana", "source": { "dashboard": { @@ -215,7 +215,7 @@ "searchSourceJSON": "{\"query\":{\"query\":\"\",\"language\":\"lucene\"},\"filter\":[],\"highlightAll\":true,\"version\":true}" }, "optionsJSON": "{}", - "panelsJSON": "[{\"size_x\":6,\"size_y\":3,\"panelIndex\":1,\"type\":\"visualization\",\"id\":\"space_1-dd7caf20-9efd-11e7-acb3-3dab96693fab\",\"col\":1,\"row\":1}]", + "panelsJSON": "[{\"size_x\":6,\"size_y\":3,\"panelIndex\":1,\"type\":\"isolatedtype\",\"id\":\"space1-isolatedtype-id\",\"col\":1,\"row\":1}]", "refreshInterval": { "display": "Off", "pause": false, @@ -239,7 +239,7 @@ { "type": "doc", "value": { - "id": "space_2:index-pattern:space_2-91200a00-9efd-11e7-acb3-3dab96693fab", + "id": "space_2:index-pattern:space2-index-pattern-id", "index": ".kibana", "source": { "index-pattern": { @@ -263,7 +263,7 @@ "source": { "config": { "buildNum": 8467, - "defaultIndex": "91200a00-9efd-11e7-acb3-3dab96693fab" + "defaultIndex": "defaultspace-index-pattern-id" }, "namespace": "space_2", "type": "config", @@ -276,16 +276,16 @@ { "type": "doc", "value": { - "id": "space_2:visualization:space_2-dd7caf20-9efd-11e7-acb3-3dab96693fab", + "id": "space_2:isolatedtype:space2-isolatedtype-id", "index": ".kibana", "source": { "namespace": "space_2", - "type": "visualization", + "type": "isolatedtype", "updated_at": "2017-09-21T18:51:23.794Z", - "visualization": { + "isolatedtype": { "description": "", "kibanaSavedObjectMeta": { - "searchSourceJSON": "{\"index\":\"space_2-91200a00-9efd-11e7-acb3-3dab96693fab\",\"filter\":[],\"query\":{\"query\":\"\",\"language\":\"lucene\"}}" + "searchSourceJSON": "{\"index\":\"space2-index-pattern-id\",\"filter\":[],\"query\":{\"query\":\"\",\"language\":\"lucene\"}}" }, "title": "Count of requests", "uiStateJSON": "{\"spy\":{\"mode\":{\"name\":null,\"fill\":false}}}", @@ -300,7 +300,7 @@ { "type": "doc", "value": { - "id": "space_2:dashboard:space_2-be3733a0-9efe-11e7-acb3-3dab96693fab", + "id": "space_2:dashboard:space2-dashboard-id", "index": ".kibana", "source": { "dashboard": { @@ -310,7 +310,7 @@ "searchSourceJSON": "{\"query\":{\"query\":\"\",\"language\":\"lucene\"},\"filter\":[],\"highlightAll\":true,\"version\":true}" }, "optionsJSON": "{}", - "panelsJSON": "[{\"size_x\":6,\"size_y\":3,\"panelIndex\":1,\"type\":\"visualization\",\"id\":\"space_2-dd7caf20-9efd-11e7-acb3-3dab96693fab\",\"col\":1,\"row\":1}]", + "panelsJSON": "[{\"size_x\":6,\"size_y\":3,\"panelIndex\":1,\"type\":\"isolatedtype\",\"id\":\"space2-isolatedtype-id\",\"col\":1,\"row\":1}]", "refreshInterval": { "display": "Off", "pause": false, @@ -334,11 +334,11 @@ { "type": "doc", "value": { - "id": "globaltype:8121a00-8efd-21e7-1cb3-34ab966434445", + "id": "globaltype:globaltype-id", "index": ".kibana", "source": { "globaltype": { - "name": "My favorite global object" + "title": "My favorite global object" }, "type": "globaltype", "updated_at": "2017-09-21T18:59:16.270Z" @@ -346,3 +346,54 @@ "type": "doc" } } + +{ + "type": "doc", + "value": { + "id": "sharedtype:default_and_space_1", + "index": ".kibana", + "source": { + "sharedtype": { + "title": "A shared saved-object in the default and space_1 spaces" + }, + "type": "sharedtype", + "namespaces": ["default", "space_1"], + "updated_at": "2017-09-21T18:59:16.270Z" + }, + "type": "doc" + } +} + +{ + "type": "doc", + "value": { + "id": "sharedtype:only_space_1", + "index": ".kibana", + "source": { + "sharedtype": { + "title": "A shared saved-object only in space_1" + }, + "type": "sharedtype", + "namespaces": ["space_1"], + "updated_at": "2017-09-21T18:59:16.270Z" + }, + "type": "doc" + } +} + +{ + "type": "doc", + "value": { + "id": "sharedtype:only_space_2", + "index": ".kibana", + "source": { + "sharedtype": { + "title": "A shared saved-object only in space_2" + }, + "type": "sharedtype", + "namespaces": ["space_2"], + "updated_at": "2017-09-21T18:59:16.270Z" + }, + "type": "doc" + } +} diff --git a/x-pack/test/saved_object_api_integration/common/fixtures/es_archiver/saved_objects/spaces/mappings.json b/x-pack/test/saved_object_api_integration/common/fixtures/es_archiver/saved_objects/spaces/mappings.json index c2489f2a906c8..7b5b1d86f6bcc 100644 --- a/x-pack/test/saved_object_api_integration/common/fixtures/es_archiver/saved_objects/spaces/mappings.json +++ b/x-pack/test/saved_object_api_integration/common/fixtures/es_archiver/saved_objects/spaces/mappings.json @@ -82,7 +82,7 @@ }, "globaltype": { "properties": { - "name": { + "title": { "fields": { "keyword": { "ignore_above": 2048, @@ -147,9 +147,41 @@ } } }, + "isolatedtype": { + "properties": { + "description": { + "type": "text" + }, + "kibanaSavedObjectMeta": { + "properties": { + "searchSourceJSON": { + "type": "text" + } + } + }, + "savedSearchId": { + "type": "keyword" + }, + "title": { + "type": "text" + }, + "uiStateJSON": { + "type": "text" + }, + "version": { + "type": "integer" + }, + "visState": { + "type": "text" + } + } + }, "namespace": { "type": "keyword" }, + "namespaces": { + "type": "keyword" + }, "search": { "properties": { "columns": { @@ -186,6 +218,19 @@ } } }, + "sharedtype": { + "properties": { + "title": { + "type": "text", + "fields": { + "keyword": { + "type": "keyword", + "ignore_above": 2048 + } + } + } + } + }, "space": { "properties": { "_reserved": { @@ -282,35 +327,6 @@ "type": "text" } } - }, - "visualization": { - "properties": { - "description": { - "type": "text" - }, - "kibanaSavedObjectMeta": { - "properties": { - "searchSourceJSON": { - "type": "text" - } - } - }, - "savedSearchId": { - "type": "keyword" - }, - "title": { - "type": "text" - }, - "uiStateJSON": { - "type": "text" - }, - "version": { - "type": "integer" - }, - "visState": { - "type": "text" - } - } } } }, @@ -322,4 +338,4 @@ } } } -} \ No newline at end of file +} diff --git a/x-pack/test/saved_object_api_integration/common/fixtures/hidden_type_plugin/index.js b/x-pack/test/saved_object_api_integration/common/fixtures/hidden_type_plugin/index.js index ea32811794c47..5989db84e2290 100644 --- a/x-pack/test/saved_object_api_integration/common/fixtures/hidden_type_plugin/index.js +++ b/x-pack/test/saved_object_api_integration/common/fixtures/hidden_type_plugin/index.js @@ -21,5 +21,7 @@ export default function(kibana) { }, config() {}, + + init() {}, // need empty init for plugin to load }); } diff --git a/x-pack/test/saved_object_api_integration/common/fixtures/hidden_type_plugin/mappings.json b/x-pack/test/saved_object_api_integration/common/fixtures/hidden_type_plugin/mappings.json index e4815273964a1..45f898e10e2ba 100644 --- a/x-pack/test/saved_object_api_integration/common/fixtures/hidden_type_plugin/mappings.json +++ b/x-pack/test/saved_object_api_integration/common/fixtures/hidden_type_plugin/mappings.json @@ -1,7 +1,7 @@ { "hiddentype": { "properties": { - "name": { + "title": { "type": "text", "fields": { "keyword": { diff --git a/x-pack/test/saved_object_api_integration/common/fixtures/isolated_type_plugin/index.js b/x-pack/test/saved_object_api_integration/common/fixtures/isolated_type_plugin/index.js new file mode 100644 index 0000000000000..a406c6737da5f --- /dev/null +++ b/x-pack/test/saved_object_api_integration/common/fixtures/isolated_type_plugin/index.js @@ -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 mappings from './mappings.json'; + +export default function(kibana) { + return new kibana.Plugin({ + require: ['kibana', 'elasticsearch', 'xpack_main'], + name: 'isolated_type_plugin', + uiExports: { + savedObjectsManagement: { + isolatedtype: { + isImportableAndExportable: true, + }, + }, + mappings, + }, + + config() {}, + + init() {}, // need empty init for plugin to load + }); +} diff --git a/x-pack/test/saved_object_api_integration/common/fixtures/isolated_type_plugin/mappings.json b/x-pack/test/saved_object_api_integration/common/fixtures/isolated_type_plugin/mappings.json new file mode 100644 index 0000000000000..141ebbc93c290 --- /dev/null +++ b/x-pack/test/saved_object_api_integration/common/fixtures/isolated_type_plugin/mappings.json @@ -0,0 +1,31 @@ +{ + "isolatedtype": { + "properties": { + "description": { + "type": "text" + }, + "kibanaSavedObjectMeta": { + "properties": { + "searchSourceJSON": { + "type": "text" + } + } + }, + "savedSearchId": { + "type": "keyword" + }, + "title": { + "type": "text" + }, + "uiStateJSON": { + "type": "text" + }, + "version": { + "type": "integer" + }, + "visState": { + "type": "text" + } + } + } +} diff --git a/x-pack/test/saved_object_api_integration/common/fixtures/isolated_type_plugin/package.json b/x-pack/test/saved_object_api_integration/common/fixtures/isolated_type_plugin/package.json new file mode 100644 index 0000000000000..665ecb1b31d7e --- /dev/null +++ b/x-pack/test/saved_object_api_integration/common/fixtures/isolated_type_plugin/package.json @@ -0,0 +1,7 @@ +{ + "name": "isolated_type_plugin", + "version": "0.0.0", + "kibana": { + "version": "kibana" + } +} diff --git a/x-pack/test/saved_object_api_integration/common/fixtures/namespace_agnostic_type_plugin/mappings.json b/x-pack/test/saved_object_api_integration/common/fixtures/namespace_agnostic_type_plugin/mappings.json index b30a2c3877b88..64d309b4209a2 100644 --- a/x-pack/test/saved_object_api_integration/common/fixtures/namespace_agnostic_type_plugin/mappings.json +++ b/x-pack/test/saved_object_api_integration/common/fixtures/namespace_agnostic_type_plugin/mappings.json @@ -1,7 +1,7 @@ { "globaltype": { "properties": { - "name": { + "title": { "type": "text", "fields": { "keyword": { diff --git a/x-pack/test/saved_object_api_integration/common/fixtures/shared_type_plugin/index.js b/x-pack/test/saved_object_api_integration/common/fixtures/shared_type_plugin/index.js new file mode 100644 index 0000000000000..91a24fb9f4f56 --- /dev/null +++ b/x-pack/test/saved_object_api_integration/common/fixtures/shared_type_plugin/index.js @@ -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 mappings from './mappings.json'; + +export default function(kibana) { + return new kibana.Plugin({ + require: ['kibana', 'elasticsearch', 'xpack_main'], + name: 'shared_type_plugin', + uiExports: { + savedObjectsManagement: {}, + savedObjectSchemas: { + sharedtype: { + multiNamespace: true, + }, + }, + mappings, + }, + + config() {}, + + init() {}, // need empty init for plugin to load + }); +} diff --git a/x-pack/test/saved_object_api_integration/common/fixtures/shared_type_plugin/mappings.json b/x-pack/test/saved_object_api_integration/common/fixtures/shared_type_plugin/mappings.json new file mode 100644 index 0000000000000..918958aec0d6d --- /dev/null +++ b/x-pack/test/saved_object_api_integration/common/fixtures/shared_type_plugin/mappings.json @@ -0,0 +1,15 @@ +{ + "sharedtype": { + "properties": { + "title": { + "type": "text", + "fields": { + "keyword": { + "type": "keyword", + "ignore_above": 2048 + } + } + } + } + } +} diff --git a/x-pack/test/saved_object_api_integration/common/fixtures/shared_type_plugin/package.json b/x-pack/test/saved_object_api_integration/common/fixtures/shared_type_plugin/package.json new file mode 100644 index 0000000000000..c52f4256c5c06 --- /dev/null +++ b/x-pack/test/saved_object_api_integration/common/fixtures/shared_type_plugin/package.json @@ -0,0 +1,7 @@ +{ + "name": "shared_type_plugin", + "version": "0.0.0", + "kibana": { + "version": "kibana" + } +} diff --git a/x-pack/test/saved_object_api_integration/common/lib/saved_object_test_cases.ts b/x-pack/test/saved_object_api_integration/common/lib/saved_object_test_cases.ts new file mode 100644 index 0000000000000..b32950538f8e5 --- /dev/null +++ b/x-pack/test/saved_object_api_integration/common/lib/saved_object_test_cases.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. + */ + +export const SAVED_OBJECT_TEST_CASES = Object.freeze({ + SINGLE_NAMESPACE_DEFAULT_SPACE: Object.freeze({ + type: 'isolatedtype', + id: 'defaultspace-isolatedtype-id', + }), + SINGLE_NAMESPACE_SPACE_1: Object.freeze({ + type: 'isolatedtype', + id: 'space1-isolatedtype-id', + }), + SINGLE_NAMESPACE_SPACE_2: Object.freeze({ + type: 'isolatedtype', + id: 'space2-isolatedtype-id', + }), + MULTI_NAMESPACE_DEFAULT_AND_SPACE_1: Object.freeze({ + type: 'sharedtype', + id: 'default_and_space_1', + }), + MULTI_NAMESPACE_ONLY_SPACE_1: Object.freeze({ + type: 'sharedtype', + id: 'only_space_1', + }), + MULTI_NAMESPACE_ONLY_SPACE_2: Object.freeze({ + type: 'sharedtype', + id: 'only_space_2', + }), + NAMESPACE_AGNOSTIC: Object.freeze({ + type: 'globaltype', + id: 'globaltype-id', + }), + HIDDEN: Object.freeze({ + type: 'hiddentype', + id: 'any', + }), +}); diff --git a/x-pack/test/saved_object_api_integration/common/lib/saved_object_test_utils.ts b/x-pack/test/saved_object_api_integration/common/lib/saved_object_test_utils.ts new file mode 100644 index 0000000000000..5640dfefa4f8d --- /dev/null +++ b/x-pack/test/saved_object_api_integration/common/lib/saved_object_test_utils.ts @@ -0,0 +1,302 @@ +/* + * 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 expect from '@kbn/expect'; +import { SavedObjectsErrorHelpers } from '../../../../../src/core/server'; +import { SAVED_OBJECT_TEST_CASES as CASES } from './saved_object_test_cases'; +import { SPACES } from './spaces'; +import { AUTHENTICATION } from './authentication'; +import { TestCase, TestUser, ExpectResponseBody } from './types'; + +const { + DEFAULT: { spaceId: DEFAULT_SPACE_ID }, + SPACE_1: { spaceId: SPACE_1_ID }, + SPACE_2: { spaceId: SPACE_2_ID }, +} = SPACES; +const { + NOT_A_KIBANA_USER, + SUPERUSER, + KIBANA_LEGACY_USER, + KIBANA_DUAL_PRIVILEGES_USER, + KIBANA_DUAL_PRIVILEGES_DASHBOARD_ONLY_USER, + KIBANA_RBAC_USER, + KIBANA_RBAC_DASHBOARD_ONLY_USER, + KIBANA_RBAC_DEFAULT_SPACE_ALL_USER, + KIBANA_RBAC_DEFAULT_SPACE_READ_USER, + KIBANA_RBAC_SPACE_1_ALL_USER, + KIBANA_RBAC_SPACE_1_READ_USER, +} = AUTHENTICATION; + +export function getUrlPrefix(spaceId: string) { + return spaceId && spaceId !== DEFAULT_SPACE_ID ? `/s/${spaceId}` : ``; +} + +export function getExpectedSpaceIdProperty(spaceId: string) { + if (spaceId === DEFAULT_SPACE_ID) { + return {}; + } + return { + spaceId, + }; +} + +export const getTestTitle = ( + testCaseOrCases: TestCase | TestCase[], + bulkStatusCode?: 200 | 403 // only used for bulk test suites; other suites specify forbidden/permitted in each test case +) => { + const testCases = Array.isArray(testCaseOrCases) ? testCaseOrCases : [testCaseOrCases]; + const stringify = (array: TestCase[]) => array.map(x => `${x.type}/${x.id}`).join(); + if (bulkStatusCode === 403 || (testCases.length === 1 && testCases[0].failure === 403)) { + return `forbidden [${stringify(testCases)}]`; + } + if (testCases.find(x => x.failure === 403)) { + throw new Error( + 'Cannot create test title for multiple forbidden test cases; specify individual tests for each of these test cases' + ); + } + // permitted + const list: string[] = []; + Object.entries({ + success: undefined, + 'bad request': 400, + 'not found': 404, + conflict: 409, + }).forEach(([descriptor, failure]) => { + const filtered = testCases.filter(x => x.failure === failure); + if (filtered.length) { + list.push(`${descriptor} [${stringify(filtered)}]`); + } + }); + return `${list.join(' and ')}`; +}; + +export const testCaseFailures = { + // test suites need explicit return types for number primitives + fail400: (condition?: boolean): { failure?: 400 } => + condition !== false ? { failure: 400 } : {}, + fail404: (condition?: boolean): { failure?: 404 } => + condition !== false ? { failure: 404 } : {}, + fail409: (condition?: boolean): { failure?: 409 } => + condition !== false ? { failure: 409 } : {}, +}; + +/** + * Test cases have additional properties that we don't want to send in HTTP Requests + */ +export const createRequest = ({ type, id }: TestCase) => ({ type, id }); + +const uniq = (arr: T[]): T[] => Array.from(new Set(arr)); +const isNamespaceAgnostic = (type: string) => type === 'globaltype'; +const isMultiNamespace = (type: string) => type === 'sharedtype'; +export const expectResponses = { + forbidden: (action: string) => (typeOrTypes: string | string[]): ExpectResponseBody => async ( + response: Record + ) => { + const types = Array.isArray(typeOrTypes) ? typeOrTypes : [typeOrTypes]; + const uniqueSorted = uniq(types).sort(); + expect(response.body).to.eql({ + statusCode: 403, + error: 'Forbidden', + message: `Unable to ${action} ${uniqueSorted.join()}`, + }); + }, + permitted: async (object: Record, testCase: TestCase) => { + const { type, id, failure } = testCase; + if (failure) { + let error: ReturnType; + if (failure === 400) { + error = SavedObjectsErrorHelpers.createUnsupportedTypeError(type); + } else if (failure === 404) { + error = SavedObjectsErrorHelpers.createGenericNotFoundError(type, id); + } else if (failure === 409) { + error = SavedObjectsErrorHelpers.createConflictError(type, id); + } else { + throw new Error(`Encountered unexpected error code ${failure}`); + } + // should not call permitted with a 403 failure case + if (object.type && object.id) { + // bulk request error + expect(object.type).to.eql(type); + expect(object.id).to.eql(id); + expect(object.error).to.eql(error.output.payload); + } else { + // non-bulk request error + expect(object.error).to.eql(error.output.payload.error); + expect(object.statusCode).to.eql(error.output.payload.statusCode); + // ignore the error.message, because it can vary for decorated non-bulk errors (e.g., conflict) + } + } else { + // fall back to default behavior of testing the success outcome + expect(object.type).to.eql(type); + if (id) { + expect(object.id).to.eql(id); + } else { + // created an object without specifying the ID, so it was auto-generated + expect(object.id).to.match(/^[0-9a-f-]{36}$/); + } + expect(object).not.to.have.property('error'); + expect(object.updated_at).to.match(/^[\d-]{10}T[\d:\.]{12}Z$/); + // don't test attributes, version, or references + } + }, + /** + * Additional assertions that we use in `bulk_create` and `create` to ensure that + * newly-created (or overwritten) objects don't have unexpected properties + */ + successCreated: async (es: any, spaceId: string, type: string, id: string) => { + const isNamespaceUndefined = + spaceId === SPACES.DEFAULT.spaceId || isNamespaceAgnostic(type) || isMultiNamespace(type); + const expectedSpacePrefix = isNamespaceUndefined ? '' : `${spaceId}:`; + const savedObject = await es.get({ + id: `${expectedSpacePrefix}${type}:${id}`, + index: '.kibana', + }); + const { namespace: actualNamespace, namespaces: actualNamespaces } = savedObject._source; + if (isNamespaceUndefined) { + expect(actualNamespace).to.eql(undefined); + } else { + expect(actualNamespace).to.eql(spaceId); + } + if (isMultiNamespace(type)) { + if (id === CASES.MULTI_NAMESPACE_DEFAULT_AND_SPACE_1.id) { + expect(actualNamespaces).to.eql([DEFAULT_SPACE_ID, SPACE_1_ID]); + } else if (id === CASES.MULTI_NAMESPACE_ONLY_SPACE_1.id) { + expect(actualNamespaces).to.eql([SPACE_1_ID]); + } else if (id === CASES.MULTI_NAMESPACE_ONLY_SPACE_2.id) { + expect(actualNamespaces).to.eql([SPACE_2_ID]); + } else { + // newly created in this space + expect(actualNamespaces).to.eql([spaceId]); + } + } + return savedObject; + }, +}; + +/** + * Get test scenarios for each type of suite. + * @param modifier Use this to generate additional permutations of test scenarios. + * For instance, a modifier of ['foo', 'bar'] would return + * a `securityAndSpaces` of: [ + * { spaceId: DEFAULT_SPACE_ID, users: {...}, modifier: 'foo' }, + * { spaceId: DEFAULT_SPACE_ID, users: {...}, modifier: 'bar' }, + * { spaceId: SPACE_1_ID, users: {...}, modifier: 'foo' }, + * { spaceId: SPACE_1_ID, users: {...}, modifier: 'bar' }, + * ] + */ +export const getTestScenarios = (modifiers?: T[]) => { + const commonUsers = { + noAccess: { ...NOT_A_KIBANA_USER, description: 'user with no access' }, + superuser: { ...SUPERUSER, description: 'superuser' }, + legacyAll: { ...KIBANA_LEGACY_USER, description: 'legacy user' }, + allGlobally: { ...KIBANA_RBAC_USER, description: 'rbac user with all globally' }, + readGlobally: { + ...KIBANA_RBAC_DASHBOARD_ONLY_USER, + description: 'rbac user with read globally', + }, + dualAll: { ...KIBANA_DUAL_PRIVILEGES_USER, description: 'dual-privileges user' }, + dualRead: { + ...KIBANA_DUAL_PRIVILEGES_DASHBOARD_ONLY_USER, + description: 'dual-privileges readonly user', + }, + }; + + interface Security { + modifier?: T; + users: Record< + | keyof typeof commonUsers + | 'allAtDefaultSpace' + | 'readAtDefaultSpace' + | 'allAtSpace1' + | 'readAtSpace1', + TestUser + >; + } + interface SecurityAndSpaces { + modifier?: T; + users: Record< + keyof typeof commonUsers | 'allAtSpace' | 'readAtSpace' | 'allAtOtherSpace', + TestUser + >; + spaceId: string; + } + interface Spaces { + modifier?: T; + spaceId: string; + } + + let spaces: Spaces[] = [DEFAULT_SPACE_ID, SPACE_1_ID, SPACE_2_ID].map(x => ({ spaceId: x })); + let security: Security[] = [ + { + users: { + ...commonUsers, + allAtDefaultSpace: { + ...KIBANA_RBAC_DEFAULT_SPACE_ALL_USER, + description: 'rbac user with all at default space', + }, + readAtDefaultSpace: { + ...KIBANA_RBAC_DEFAULT_SPACE_READ_USER, + description: 'rbac user with read at default space', + }, + allAtSpace1: { + ...KIBANA_RBAC_SPACE_1_ALL_USER, + description: 'rbac user with all at space_1', + }, + readAtSpace1: { + ...KIBANA_RBAC_SPACE_1_READ_USER, + description: 'rbac user with read at space_1', + }, + }, + }, + ]; + let securityAndSpaces: SecurityAndSpaces[] = [ + { + spaceId: DEFAULT_SPACE_ID, + users: { + ...commonUsers, + allAtSpace: { + ...KIBANA_RBAC_DEFAULT_SPACE_ALL_USER, + description: 'user with all at the space', + }, + readAtSpace: { + ...KIBANA_RBAC_DEFAULT_SPACE_READ_USER, + description: 'user with read at the space', + }, + allAtOtherSpace: { + ...KIBANA_RBAC_SPACE_1_ALL_USER, + description: 'user with all at other space', + }, + }, + }, + { + spaceId: SPACE_1_ID, + users: { + ...commonUsers, + allAtSpace: { ...KIBANA_RBAC_SPACE_1_ALL_USER, description: 'user with all at the space' }, + readAtSpace: { + ...KIBANA_RBAC_SPACE_1_READ_USER, + description: 'user with read at the space', + }, + allAtOtherSpace: { + ...KIBANA_RBAC_DEFAULT_SPACE_ALL_USER, + description: 'user with all at other space', + }, + }, + }, + ]; + if (modifiers) { + const addModifier = (list: T[]) => + list.map(x => modifiers.map(modifier => ({ ...x, modifier }))).flat(); + spaces = addModifier(spaces); + security = addModifier(security); + securityAndSpaces = addModifier(securityAndSpaces); + } + return { + spaces, + security, + securityAndSpaces, + }; +}; diff --git a/x-pack/test/saved_object_api_integration/common/lib/space_test_utils.ts b/x-pack/test/saved_object_api_integration/common/lib/space_test_utils.ts deleted file mode 100644 index 1619d77761c84..0000000000000 --- a/x-pack/test/saved_object_api_integration/common/lib/space_test_utils.ts +++ /dev/null @@ -1,24 +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 { DEFAULT_SPACE_ID } from '../../../../plugins/spaces/common/constants'; - -export function getUrlPrefix(spaceId: string) { - return spaceId && spaceId !== DEFAULT_SPACE_ID ? `/s/${spaceId}` : ``; -} - -export function getIdPrefix(spaceId: string) { - return spaceId === DEFAULT_SPACE_ID ? '' : `${spaceId}-`; -} - -export function getExpectedSpaceIdProperty(spaceId: string) { - if (spaceId === DEFAULT_SPACE_ID) { - return {}; - } - return { - spaceId, - }; -} diff --git a/x-pack/test/saved_object_api_integration/common/lib/types.ts b/x-pack/test/saved_object_api_integration/common/lib/types.ts index 487afff1494c0..f6e6d391ae905 100644 --- a/x-pack/test/saved_object_api_integration/common/lib/types.ts +++ b/x-pack/test/saved_object_api_integration/common/lib/types.ts @@ -4,9 +4,28 @@ * you may not use this file except in compliance with the Elastic License. */ -export type DescribeFn = (text: string, fn: () => void) => void; +export type ExpectResponseBody = (response: Record) => Promise; -export interface TestDefinitionAuthentication { - username?: string; - password?: string; +export interface TestDefinition { + title: string; + responseStatusCode: number; + responseBody: ExpectResponseBody; +} + +export interface TestSuite { + user?: TestUser; + spaceId?: string; + tests: T[]; +} + +export interface TestCase { + type: string; + id: string; + failure?: 400 | 403 | 404 | 409; +} + +export interface TestUser { + username: string; + password: string; + description: string; } diff --git a/x-pack/test/saved_object_api_integration/common/suites/bulk_create.ts b/x-pack/test/saved_object_api_integration/common/suites/bulk_create.ts index b6f1bb956d72d..0dafe6b7b386d 100644 --- a/x-pack/test/saved_object_api_integration/common/suites/bulk_create.ts +++ b/x-pack/test/saved_object_api_integration/common/suites/bulk_create.ts @@ -6,224 +6,137 @@ import expect from '@kbn/expect'; import { SuperTest } from 'supertest'; -import { DEFAULT_SPACE_ID } from '../../../../plugins/spaces/common/constants'; -import { getIdPrefix, getUrlPrefix } from '../lib/space_test_utils'; -import { DescribeFn, TestDefinitionAuthentication } from '../lib/types'; - -interface BulkCreateTest { - statusCode: number; - response: (resp: { [key: string]: any }) => void; -} - -interface BulkCreateCustomTest extends BulkCreateTest { - description: string; - requestBody: { - [key: string]: any; - }; -} - -interface BulkCreateTests { - default: BulkCreateTest; - includingSpace: BulkCreateTest; - custom?: BulkCreateCustomTest; +import { SAVED_OBJECT_TEST_CASES as CASES } from '../lib/saved_object_test_cases'; +import { SPACES } from '../lib/spaces'; +import { + createRequest, + expectResponses, + getUrlPrefix, + getTestTitle, +} from '../lib/saved_object_test_utils'; +import { ExpectResponseBody, TestCase, TestDefinition, TestSuite } from '../lib/types'; + +export interface BulkCreateTestDefinition extends TestDefinition { + request: Array<{ type: string; id: string }>; + overwrite: boolean; } - -interface BulkCreateTestDefinition { - user?: TestDefinitionAuthentication; - spaceId?: string; - tests: BulkCreateTests; +export type BulkCreateTestSuite = TestSuite; +export interface BulkCreateTestCase extends TestCase { + failure?: 400 | 409; // only used for permitted response case } -const createBulkRequests = (spaceId: string) => [ - { - type: 'visualization', - id: `${getIdPrefix(spaceId)}dd7caf20-9efd-11e7-acb3-3dab96693fab`, - attributes: { - title: 'An existing visualization', - }, - }, - { - type: 'dashboard', - id: `${getIdPrefix(spaceId)}a01b2f57-fcfd-4864-b735-09e28f0d815e`, - attributes: { - title: 'A great new dashboard', - }, - }, - { - type: 'globaltype', - id: '05976c65-1145-4858-bbf0-d225cc78a06e', - attributes: { - name: 'A new globaltype object', - }, - }, - { - type: 'globaltype', - id: '8121a00-8efd-21e7-1cb3-34ab966434445', - attributes: { - name: 'An existing globaltype', - }, - }, -]; +const NEW_ATTRIBUTE_KEY = 'title'; // all type mappings include this attribute, for simplicity's sake +const NEW_ATTRIBUTE_VAL = `New attribute value ${Date.now()}`; -const isGlobalType = (type: string) => type === 'globaltype'; +const NEW_SINGLE_NAMESPACE_OBJ = Object.freeze({ type: 'dashboard', id: 'new-dashboard-id' }); +const NEW_MULTI_NAMESPACE_OBJ = Object.freeze({ type: 'sharedtype', id: 'new-sharedtype-id' }); +const NEW_NAMESPACE_AGNOSTIC_OBJ = Object.freeze({ type: 'globaltype', id: 'new-globaltype-id' }); +export const TEST_CASES = Object.freeze({ + ...CASES, + NEW_SINGLE_NAMESPACE_OBJ, + NEW_MULTI_NAMESPACE_OBJ, + NEW_NAMESPACE_AGNOSTIC_OBJ, +}); export function bulkCreateTestSuiteFactory(es: any, esArchiver: any, supertest: SuperTest) { - const createExpectResults = (spaceId = DEFAULT_SPACE_ID) => async (resp: { - [key: string]: any; - }) => { - expect(resp.body).to.eql({ - saved_objects: [ - { - type: 'visualization', - id: `${getIdPrefix(spaceId)}dd7caf20-9efd-11e7-acb3-3dab96693fab`, - error: { - message: 'version conflict, document already exists', - statusCode: 409, - }, - }, - { - type: 'dashboard', - id: `${getIdPrefix(spaceId)}a01b2f57-fcfd-4864-b735-09e28f0d815e`, - updated_at: resp.body.saved_objects[1].updated_at, - version: resp.body.saved_objects[1].version, - attributes: { - title: 'A great new dashboard', - }, - references: [], - }, - { - type: 'globaltype', - id: `05976c65-1145-4858-bbf0-d225cc78a06e`, - updated_at: resp.body.saved_objects[2].updated_at, - version: resp.body.saved_objects[2].version, - attributes: { - name: 'A new globaltype object', - }, - references: [], - }, - { - type: 'globaltype', - id: '8121a00-8efd-21e7-1cb3-34ab966434445', - error: { - message: 'version conflict, document already exists', - statusCode: 409, - }, - }, - ], - }); - - for (const savedObject of createBulkRequests(spaceId)) { - const expectedSpacePrefix = - spaceId === DEFAULT_SPACE_ID || isGlobalType(savedObject.type) ? '' : `${spaceId}:`; - - // query ES directory to ensure namespace was or wasn't specified - const { _source } = await es.get({ - id: `${expectedSpacePrefix}${savedObject.type}:${savedObject.id}`, - index: '.kibana', - }); - - const { namespace: actualNamespace } = _source; - - if (spaceId === DEFAULT_SPACE_ID || isGlobalType(savedObject.type)) { - expect(actualNamespace).to.eql(undefined); - } else { - expect(actualNamespace).to.eql(spaceId); + const expectForbidden = expectResponses.forbidden('bulk_create'); + const expectResponseBody = ( + testCases: BulkCreateTestCase | BulkCreateTestCase[], + statusCode: 200 | 403, + spaceId = SPACES.DEFAULT.spaceId + ): ExpectResponseBody => async (response: Record) => { + const testCaseArray = Array.isArray(testCases) ? testCases : [testCases]; + if (statusCode === 403) { + const types = testCaseArray.map(x => x.type); + await expectForbidden(types)(response); + } else { + // permitted + const savedObjects = response.body.saved_objects; + expect(savedObjects).length(testCaseArray.length); + for (let i = 0; i < savedObjects.length; i++) { + const object = savedObjects[i]; + const testCase = testCaseArray[i]; + await expectResponses.permitted(object, testCase); + if (!testCase.failure) { + expect(object.attributes[NEW_ATTRIBUTE_KEY]).to.eql(NEW_ATTRIBUTE_VAL); + await expectResponses.successCreated(es, spaceId, object.type, object.id); + } } } }; - - const expectBadRequestForHiddenType = (resp: { [key: string]: any }) => { - const spaceEntry = resp.body.saved_objects.find( - (entry: any) => entry.id === 'my-hiddentype' && entry.type === 'hiddentype' - ); - expect(spaceEntry).to.eql({ - id: 'my-hiddentype', - type: 'hiddentype', - error: { - message: "Unsupported saved object type: 'hiddentype': Bad Request", - statusCode: 400, - error: 'Bad Request', + const createTestDefinitions = ( + testCases: BulkCreateTestCase | BulkCreateTestCase[], + forbidden: boolean, + overwrite: boolean, + options?: { + spaceId?: string; + singleRequest?: boolean; + responseBodyOverride?: ExpectResponseBody; + } + ): BulkCreateTestDefinition[] => { + const cases = Array.isArray(testCases) ? testCases : [testCases]; + const responseStatusCode = forbidden ? 403 : 200; + if (!options?.singleRequest) { + // if we are testing cases that should result in a forbidden response, we can do each case individually + // this ensures that multiple test cases of a single type will each result in a forbidden error + return cases.map(x => ({ + title: getTestTitle(x, responseStatusCode), + request: [createRequest(x)], + responseStatusCode, + responseBody: + options?.responseBodyOverride || + expectResponseBody(x, responseStatusCode, options?.spaceId), + overwrite, + })); + } + // batch into a single request to save time during test execution + return [ + { + title: getTestTitle(cases, responseStatusCode), + request: cases.map(x => createRequest(x)), + responseStatusCode, + responseBody: + options?.responseBodyOverride || + expectResponseBody(cases, responseStatusCode, options?.spaceId), + overwrite, }, - }); + ]; }; - const expectedForbiddenTypes = ['dashboard', 'globaltype', 'visualization']; - const expectedForbiddenTypesWithHiddenType = [ - 'dashboard', - 'globaltype', - 'hiddentype', - 'visualization', - ]; - const createExpectRbacForbidden = (types: string[] = expectedForbiddenTypes) => (resp: { - [key: string]: any; - }) => { - expect(resp.body).to.eql({ - statusCode: 403, - error: 'Forbidden', - message: `Unable to bulk_create ${types.join(',')}`, - }); - }; - - const makeBulkCreateTest = (describeFn: DescribeFn) => ( + const makeBulkCreateTest = (describeFn: Mocha.SuiteFunction) => ( description: string, - definition: BulkCreateTestDefinition + definition: BulkCreateTestSuite ) => { - const { user = {}, spaceId = DEFAULT_SPACE_ID, tests } = definition; + const { user, spaceId = SPACES.DEFAULT.spaceId, tests } = definition; describeFn(description, () => { before(() => esArchiver.load('saved_objects/spaces')); after(() => esArchiver.unload('saved_objects/spaces')); - it(`should return ${tests.default.statusCode}`, async () => { - await supertest - .post(`${getUrlPrefix(spaceId)}/api/saved_objects/_bulk_create`) - .auth(user.username, user.password) - .send(createBulkRequests(spaceId)) - .expect(tests.default.statusCode) - .then(tests.default.response); - }); - - it(`including a hiddentype saved object should return ${tests.includingSpace.statusCode}`, async () => { - await supertest - .post(`${getUrlPrefix(spaceId)}/api/saved_objects/_bulk_create`) - .auth(user.username, user.password) - .send( - createBulkRequests(spaceId).concat([ - { - type: 'hiddentype', - id: `my-hiddentype`, - attributes: { - name: 'My awesome hiddentype', - }, - }, - ]) - ) - .expect(tests.includingSpace.statusCode) - .then(tests.includingSpace.response); - }); + const attrs = { attributes: { [NEW_ATTRIBUTE_KEY]: NEW_ATTRIBUTE_VAL } }; - if (tests.custom) { - it(tests.custom!.description, async () => { + for (const test of tests) { + it(`should return ${test.responseStatusCode} ${test.title}`, async () => { + const requestBody = test.request.map(x => ({ ...x, ...attrs })); + const query = test.overwrite ? '?overwrite=true' : ''; await supertest - .post(`${getUrlPrefix(spaceId)}/api/saved_objects/_bulk_create`) - .auth(user.username, user.password) - .send(tests.custom!.requestBody) - .expect(tests.custom!.statusCode) - .then(tests.custom!.response); + .post(`${getUrlPrefix(spaceId)}/api/saved_objects/_bulk_create${query}`) + .auth(user?.username, user?.password) + .send(requestBody) + .expect(test.responseStatusCode) + .then(test.responseBody); }); } }); }; - const bulkCreateTest = makeBulkCreateTest(describe); + const addTests = makeBulkCreateTest(describe); // @ts-ignore - bulkCreateTest.only = makeBulkCreateTest(describe.only); + addTests.only = makeBulkCreateTest(describe.only); return { - bulkCreateTest, - createExpectResults, - createExpectRbacForbidden, - expectBadRequestForHiddenType, - expectedForbiddenTypesWithHiddenType, + addTests, + createTestDefinitions, + expectForbidden, }; } diff --git a/x-pack/test/saved_object_api_integration/common/suites/bulk_get.ts b/x-pack/test/saved_object_api_integration/common/suites/bulk_get.ts index 9c5cc375502d1..f03dac597294c 100644 --- a/x-pack/test/saved_object_api_integration/common/suites/bulk_get.ts +++ b/x-pack/test/saved_object_api_integration/common/suites/bulk_get.ts @@ -6,203 +6,110 @@ import expect from '@kbn/expect'; import { SuperTest } from 'supertest'; -import { DEFAULT_SPACE_ID } from '../../../../plugins/spaces/common/constants'; -import { getIdPrefix, getUrlPrefix } from '../lib/space_test_utils'; -import { DescribeFn, TestDefinitionAuthentication } from '../lib/types'; +import { SAVED_OBJECT_TEST_CASES as CASES } from '../lib/saved_object_test_cases'; +import { SPACES } from '../lib/spaces'; +import { + createRequest, + expectResponses, + getUrlPrefix, + getTestTitle, +} from '../lib/saved_object_test_utils'; +import { ExpectResponseBody, TestCase, TestDefinition, TestSuite } from '../lib/types'; -interface BulkGetTest { - statusCode: number; - response: (resp: { [key: string]: any }) => void; +export interface BulkGetTestDefinition extends TestDefinition { + request: Array<{ type: string; id: string }>; } - -interface BulkGetTests { - default: BulkGetTest; - includingHiddenType: BulkGetTest; -} - -interface BulkGetTestDefinition { - user?: TestDefinitionAuthentication; - spaceId?: string; - otherSpaceId?: string; - tests: BulkGetTests; +export type BulkGetTestSuite = TestSuite; +export interface BulkGetTestCase extends TestCase { + failure?: 400 | 404; // only used for permitted response case } -const createBulkRequests = (spaceId: string) => [ - { - type: 'visualization', - id: `${getIdPrefix(spaceId)}dd7caf20-9efd-11e7-acb3-3dab96693fab`, - }, - { - type: 'dashboard', - id: `${getIdPrefix(spaceId)}does not exist`, - }, - { - type: 'globaltype', - id: '8121a00-8efd-21e7-1cb3-34ab966434445', - }, -]; +const DOES_NOT_EXIST = Object.freeze({ type: 'dashboard', id: 'does-not-exist' }); +export const TEST_CASES = Object.freeze({ ...CASES, DOES_NOT_EXIST }); export function bulkGetTestSuiteFactory(esArchiver: any, supertest: SuperTest) { - const createExpectNotFoundResults = (spaceId: string) => (resp: { [key: string]: any }) => { - expect(resp.body).to.eql({ - saved_objects: [ - { - id: `${getIdPrefix(spaceId)}dd7caf20-9efd-11e7-acb3-3dab96693fab`, - type: 'visualization', - error: { - statusCode: 404, - message: 'Not found', - }, - }, - { - id: `${getIdPrefix(spaceId)}does not exist`, - type: 'dashboard', - error: { - statusCode: 404, - message: 'Not found', - }, - }, - { - id: `8121a00-8efd-21e7-1cb3-34ab966434445`, - type: 'globaltype', - updated_at: '2017-09-21T18:59:16.270Z', - version: resp.body.saved_objects[2].version, - attributes: { - name: 'My favorite global object', - }, - references: [], - }, - ], - }); + const expectForbidden = expectResponses.forbidden('bulk_get'); + const expectResponseBody = ( + testCases: BulkGetTestCase | BulkGetTestCase[], + statusCode: 200 | 403 + ): ExpectResponseBody => async (response: Record) => { + const testCaseArray = Array.isArray(testCases) ? testCases : [testCases]; + if (statusCode === 403) { + const types = testCaseArray.map(x => x.type); + await expectForbidden(types)(response); + } else { + // permitted + const savedObjects = response.body.saved_objects; + expect(savedObjects).length(testCaseArray.length); + for (let i = 0; i < savedObjects.length; i++) { + const object = savedObjects[i]; + const testCase = testCaseArray[i]; + await expectResponses.permitted(object, testCase); + } + } }; - - const expectBadRequestForHiddenType = (resp: { [key: string]: any }) => { - const spaceEntry = resp.body.saved_objects.find( - (entry: any) => entry.id === 'my-hiddentype' && entry.type === 'hiddentype' - ); - expect(spaceEntry).to.eql({ - id: 'my-hiddentype', - type: 'hiddentype', - error: { - message: "Unsupported saved object type: 'hiddentype': Bad Request", - statusCode: 400, - error: 'Bad Request', + const createTestDefinitions = ( + testCases: BulkGetTestCase | BulkGetTestCase[], + forbidden: boolean, + options?: { + singleRequest?: boolean; + responseBodyOverride?: ExpectResponseBody; + } + ): BulkGetTestDefinition[] => { + const cases = Array.isArray(testCases) ? testCases : [testCases]; + const responseStatusCode = forbidden ? 403 : 200; + if (!options?.singleRequest) { + // if we are testing cases that should result in a forbidden response, we can do each case individually + // this ensures that multiple test cases of a single type will each result in a forbidden error + return cases.map(x => ({ + title: getTestTitle(x, responseStatusCode), + request: [createRequest(x)], + responseStatusCode, + responseBody: options?.responseBodyOverride || expectResponseBody(x, responseStatusCode), + })); + } + // batch into a single request to save time during test execution + return [ + { + title: getTestTitle(cases, responseStatusCode), + request: cases.map(x => createRequest(x)), + responseStatusCode, + responseBody: + options?.responseBodyOverride || expectResponseBody(cases, responseStatusCode), }, - }); + ]; }; - const expectedForbiddenTypes = ['dashboard', 'globaltype', 'visualization']; - const expectedForbiddenTypesWithHiddenType = [ - 'dashboard', - 'globaltype', - 'hiddentype', - 'visualization', - ]; - const createExpectRbacForbidden = (types: string[] = expectedForbiddenTypes) => (resp: { - [key: string]: any; - }) => { - expect(resp.body).to.eql({ - statusCode: 403, - error: 'Forbidden', - message: `Unable to bulk_get ${types.join(',')}`, - }); - }; - - const createExpectResults = (spaceId = DEFAULT_SPACE_ID) => (resp: { [key: string]: any }) => { - expect(resp.body).to.eql({ - saved_objects: [ - { - id: `${getIdPrefix(spaceId)}dd7caf20-9efd-11e7-acb3-3dab96693fab`, - type: 'visualization', - migrationVersion: resp.body.saved_objects[0].migrationVersion, - updated_at: '2017-09-21T18:51:23.794Z', - version: resp.body.saved_objects[0].version, - attributes: { - title: 'Count of requests', - description: '', - version: 1, - // cheat for some of the more complex attributes - visState: resp.body.saved_objects[0].attributes.visState, - uiStateJSON: resp.body.saved_objects[0].attributes.uiStateJSON, - kibanaSavedObjectMeta: resp.body.saved_objects[0].attributes.kibanaSavedObjectMeta, - }, - references: [ - { - name: 'kibanaSavedObjectMeta.searchSourceJSON.index', - type: 'index-pattern', - id: `${getIdPrefix(spaceId)}91200a00-9efd-11e7-acb3-3dab96693fab`, - }, - ], - }, - { - id: `${getIdPrefix(spaceId)}does not exist`, - type: 'dashboard', - error: { - statusCode: 404, - message: 'Not found', - }, - }, - { - id: `8121a00-8efd-21e7-1cb3-34ab966434445`, - type: 'globaltype', - updated_at: '2017-09-21T18:59:16.270Z', - version: resp.body.saved_objects[2].version, - attributes: { - name: 'My favorite global object', - }, - references: [], - }, - ], - }); - }; - - const makeBulkGetTest = (describeFn: DescribeFn) => ( + const makeBulkGetTest = (describeFn: Mocha.SuiteFunction) => ( description: string, - definition: BulkGetTestDefinition + definition: BulkGetTestSuite ) => { - const { user = {}, spaceId = DEFAULT_SPACE_ID, otherSpaceId, tests } = definition; + const { user, spaceId = SPACES.DEFAULT.spaceId, tests } = definition; describeFn(description, () => { before(() => esArchiver.load('saved_objects/spaces')); after(() => esArchiver.unload('saved_objects/spaces')); - it(`should return ${tests.default.statusCode}`, async () => { - await supertest - .post(`${getUrlPrefix(spaceId)}/api/saved_objects/_bulk_get`) - .auth(user.username, user.password) - .send(createBulkRequests(otherSpaceId || spaceId)) - .expect(tests.default.statusCode) - .then(tests.default.response); - }); - - it(`with a hiddentype saved object included should return ${tests.includingHiddenType.statusCode}`, async () => { - await supertest - .post(`${getUrlPrefix(spaceId)}/api/saved_objects/_bulk_get`) - .auth(user.username, user.password) - .send( - createBulkRequests(otherSpaceId || spaceId).concat([ - { - type: 'hiddentype', - id: `my-hiddentype`, - }, - ]) - ) - .expect(tests.includingHiddenType.statusCode) - .then(tests.includingHiddenType.response); - }); + for (const test of tests) { + it(`should return ${test.responseStatusCode} ${test.title}`, async () => { + await supertest + .post(`${getUrlPrefix(spaceId)}/api/saved_objects/_bulk_get`) + .auth(user?.username, user?.password) + .send(test.request) + .expect(test.responseStatusCode) + .then(test.responseBody); + }); + } }); }; - const bulkGetTest = makeBulkGetTest(describe); + const addTests = makeBulkGetTest(describe); // @ts-ignore - bulkGetTest.only = makeBulkGetTest(describe.only); + addTests.only = makeBulkGetTest(describe.only); return { - bulkGetTest, - createExpectNotFoundResults, - createExpectResults, - createExpectRbacForbidden, - expectBadRequestForHiddenType, - expectedForbiddenTypesWithHiddenType, + addTests, + createTestDefinitions, + expectForbidden, }; } diff --git a/x-pack/test/saved_object_api_integration/common/suites/bulk_update.ts b/x-pack/test/saved_object_api_integration/common/suites/bulk_update.ts index d14c5ccbd1d0e..e0e2118300ef4 100644 --- a/x-pack/test/saved_object_api_integration/common/suites/bulk_update.ts +++ b/x-pack/test/saved_object_api_integration/common/suites/bulk_update.ts @@ -6,234 +6,119 @@ import expect from '@kbn/expect'; import { SuperTest } from 'supertest'; -import { DEFAULT_SPACE_ID } from '../../../../plugins/spaces/common/constants'; -import { getIdPrefix, getUrlPrefix } from '../lib/space_test_utils'; -import { DescribeFn, TestDefinitionAuthentication } from '../lib/types'; - -interface BulkUpdateTest { - statusCode: number; - response: (resp: { [key: string]: any }) => void; -} - -interface BulkUpdateTests { - spaceAware: BulkUpdateTest; - notSpaceAware: BulkUpdateTest; - hiddenType: BulkUpdateTest; - doesntExist: BulkUpdateTest; +import { SAVED_OBJECT_TEST_CASES as CASES } from '../lib/saved_object_test_cases'; +import { SPACES } from '../lib/spaces'; +import { + createRequest, + expectResponses, + getUrlPrefix, + getTestTitle, +} from '../lib/saved_object_test_utils'; +import { ExpectResponseBody, TestCase, TestDefinition, TestSuite } from '../lib/types'; + +export interface BulkUpdateTestDefinition extends TestDefinition { + request: Array<{ type: string; id: string }>; } - -interface BulkUpdateTestDefinition { - user?: TestDefinitionAuthentication; - spaceId?: string; - otherSpaceId?: string; - tests: BulkUpdateTests; +export type BulkUpdateTestSuite = TestSuite; +export interface BulkUpdateTestCase extends TestCase { + failure?: 404; // only used for permitted response case } -export function bulkUpdateTestSuiteFactory(esArchiver: any, supertest: SuperTest) { - const createExpectNotFound = (type: string, id: string, spaceId = DEFAULT_SPACE_ID) => (resp: { - [key: string]: any; - }) => { - const [, savedObject] = resp.body.saved_objects; - expect(savedObject.error).eql({ - statusCode: 404, - error: 'Not Found', - message: `Saved object [${type}/${getIdPrefix(spaceId)}${id}] not found`, - }); - }; - - const createExpectDoesntExistNotFound = (spaceId?: string) => { - return createExpectNotFound('visualization', 'not an id', spaceId); - }; - - const createExpectSpaceAwareNotFound = (spaceId?: string) => { - return createExpectNotFound('visualization', 'dd7caf20-9efd-11e7-acb3-3dab96693fab', spaceId); - }; - - const expectHiddenTypeNotFound = createExpectNotFound( - 'hiddentype', - 'hiddentype_1', - DEFAULT_SPACE_ID - ); - - const createExpectRbacForbidden = (types: string[]) => (resp: { [key: string]: any }) => { - expect(resp.body).to.eql({ - statusCode: 403, - error: 'Forbidden', - message: `Unable to bulk_update ${types.join()}`, - }); - }; - - const expectDoesntExistRbacForbidden = createExpectRbacForbidden(['globaltype', 'visualization']); +const NEW_ATTRIBUTE_KEY = 'title'; // all type mappings include this attribute, for simplicity's sake +const NEW_ATTRIBUTE_VAL = `Updated attribute value ${Date.now()}`; - const expectNotSpaceAwareRbacForbidden = createExpectRbacForbidden(['globaltype']); +const DOES_NOT_EXIST = Object.freeze({ type: 'dashboard', id: 'does-not-exist' }); +export const TEST_CASES = Object.freeze({ ...CASES, DOES_NOT_EXIST }); - const expectHiddenTypeRbacForbidden = createExpectRbacForbidden(['globaltype', 'hiddentype']); - const expectHiddenTypeRbacForbiddenWithGlobalAllowed = createExpectRbacForbidden(['hiddentype']); - - const expectSpaceAwareRbacForbidden = createExpectRbacForbidden(['globaltype', 'visualization']); - - const expectNotSpaceAwareResults = (resp: { [key: string]: any }) => { - const [, savedObject] = resp.body.saved_objects; - // loose uuid validation - expect(savedObject) - .to.have.property('id') - .match(/^[0-9a-f-]{36}$/); - - // loose ISO8601 UTC time with milliseconds validation - expect(savedObject) - .to.have.property('updated_at') - .match(/^[\d-]{10}T[\d:\.]{12}Z$/); - - expect(savedObject).to.eql({ - id: savedObject.id, - type: 'globaltype', - updated_at: savedObject.updated_at, - version: savedObject.version, - attributes: { - name: 'My second favorite', - }, - }); +export function bulkUpdateTestSuiteFactory(esArchiver: any, supertest: SuperTest) { + const expectForbidden = expectResponses.forbidden('bulk_update'); + const expectResponseBody = ( + testCases: BulkUpdateTestCase | BulkUpdateTestCase[], + statusCode: 200 | 403 + ): ExpectResponseBody => async (response: Record) => { + const testCaseArray = Array.isArray(testCases) ? testCases : [testCases]; + if (statusCode === 403) { + const types = testCaseArray.map(x => x.type); + await expectForbidden(types)(response); + } else { + // permitted + const savedObjects = response.body.saved_objects; + expect(savedObjects).length(testCaseArray.length); + for (let i = 0; i < savedObjects.length; i++) { + const object = savedObjects[i]; + const testCase = testCaseArray[i]; + await expectResponses.permitted(object, testCase); + if (!testCase.failure) { + expect(object.attributes[NEW_ATTRIBUTE_KEY]).to.eql(NEW_ATTRIBUTE_VAL); + } + } + } }; - - const expectSpaceAwareResults = (resp: { [key: string]: any }) => { - const [, savedObject] = resp.body.saved_objects; - // loose uuid validation ignoring prefix - expect(savedObject) - .to.have.property('id') - .match(/[0-9a-f-]{36}$/); - - // loose ISO8601 UTC time with milliseconds validation - expect(savedObject) - .to.have.property('updated_at') - .match(/^[\d-]{10}T[\d:\.]{12}Z$/); - - expect(savedObject).to.eql({ - id: savedObject.id, - type: 'visualization', - updated_at: savedObject.updated_at, - version: savedObject.version, - attributes: { - title: 'My second favorite vis', + const createTestDefinitions = ( + testCases: BulkUpdateTestCase | BulkUpdateTestCase[], + forbidden: boolean, + options?: { + singleRequest?: boolean; + responseBodyOverride?: ExpectResponseBody; + } + ): BulkUpdateTestDefinition[] => { + const cases = Array.isArray(testCases) ? testCases : [testCases]; + const responseStatusCode = forbidden ? 403 : 200; + if (!options?.singleRequest) { + // if we are testing cases that should result in a forbidden response, we can do each case individually + // this ensures that multiple test cases of a single type will each result in a forbidden error + return cases.map(x => ({ + title: getTestTitle(x, responseStatusCode), + request: [createRequest(x)], + responseStatusCode, + responseBody: options?.responseBodyOverride || expectResponseBody(x, responseStatusCode), + })); + } + // batch into a single request to save time during test execution + return [ + { + title: getTestTitle(cases, responseStatusCode), + request: cases.map(x => createRequest(x)), + responseStatusCode, + responseBody: + options?.responseBodyOverride || expectResponseBody(cases, responseStatusCode), }, - }); + ]; }; - const makeBulkUpdateTest = (describeFn: DescribeFn) => ( + const makeBulkUpdateTest = (describeFn: Mocha.SuiteFunction) => ( description: string, - definition: BulkUpdateTestDefinition + definition: BulkUpdateTestSuite ) => { - const { user = {}, spaceId = DEFAULT_SPACE_ID, otherSpaceId, tests } = definition; - - // We add this type into all bulk updates - // to ensure that having additional items in the bulk - // update doesn't change the expected outcome overall - let updateCount = 0; - const generateNonSpaceAwareGlobalSavedObject = () => ({ - type: 'globaltype', - id: `8121a00-8efd-21e7-1cb3-34ab966434445`, - attributes: { - name: `Update #${++updateCount}`, - }, - }); + const { user, spaceId = SPACES.DEFAULT.spaceId, tests } = definition; describeFn(description, () => { before(() => esArchiver.load('saved_objects/spaces')); after(() => esArchiver.unload('saved_objects/spaces')); - it(`should return ${tests.spaceAware.statusCode} for a space-aware doc`, async () => { - await supertest - .put(`${getUrlPrefix(spaceId)}/api/saved_objects/_bulk_update`) - .auth(user.username, user.password) - .send([ - generateNonSpaceAwareGlobalSavedObject(), - { - type: 'visualization', - id: `${getIdPrefix(otherSpaceId || spaceId)}dd7caf20-9efd-11e7-acb3-3dab96693fab`, - attributes: { - title: 'My second favorite vis', - }, - }, - generateNonSpaceAwareGlobalSavedObject(), - ]) - .expect(tests.spaceAware.statusCode) - .then(tests.spaceAware.response); - }); - - it(`should return ${tests.notSpaceAware.statusCode} for a non space-aware doc`, async () => { - await supertest - .put(`${getUrlPrefix(otherSpaceId || spaceId)}/api/saved_objects/_bulk_update`) - .auth(user.username, user.password) - .send([ - generateNonSpaceAwareGlobalSavedObject(), - { - type: 'globaltype', - id: `8121a00-8efd-21e7-1cb3-34ab966434445`, - attributes: { - name: 'My second favorite', - }, - }, - generateNonSpaceAwareGlobalSavedObject(), - ]) - .expect(tests.notSpaceAware.statusCode) - .then(tests.notSpaceAware.response); - }); - it(`should return ${tests.hiddenType.statusCode} for hiddentype doc`, async () => { - await supertest - .put(`${getUrlPrefix(otherSpaceId || spaceId)}/api/saved_objects/_bulk_update`) - .auth(user.username, user.password) - .send([ - generateNonSpaceAwareGlobalSavedObject(), - { - type: 'hiddentype', - id: 'hiddentype_1', - attributes: { - name: 'My favorite hidden type', - }, - }, - generateNonSpaceAwareGlobalSavedObject(), - ]) - .expect(tests.hiddenType.statusCode) - .then(tests.hiddenType.response); - }); + const attrs = { attributes: { [NEW_ATTRIBUTE_KEY]: NEW_ATTRIBUTE_VAL } }; - describe('unknown id', () => { - it(`should return ${tests.doesntExist.statusCode}`, async () => { + for (const test of tests) { + it(`should return ${test.responseStatusCode} ${test.title}`, async () => { + const requestBody = test.request.map(x => ({ ...x, ...attrs })); await supertest .put(`${getUrlPrefix(spaceId)}/api/saved_objects/_bulk_update`) - .auth(user.username, user.password) - .send([ - generateNonSpaceAwareGlobalSavedObject(), - { - type: 'visualization', - id: `${getIdPrefix(spaceId)}not an id`, - attributes: { - title: 'My second favorite vis', - }, - }, - generateNonSpaceAwareGlobalSavedObject(), - ]) - .expect(tests.doesntExist.statusCode) - .then(tests.doesntExist.response); + .auth(user?.username, user?.password) + .send(requestBody) + .expect(test.responseStatusCode) + .then(test.responseBody); }); - }); + } }); }; - const bulkUpdateTest = makeBulkUpdateTest(describe); + const addTests = makeBulkUpdateTest(describe); // @ts-ignore - bulkUpdateTest.only = makeBulkUpdateTest(describe.only); + addTests.only = makeBulkUpdateTest(describe.only); return { - createExpectDoesntExistNotFound, - createExpectSpaceAwareNotFound, - expectSpaceNotFound: expectHiddenTypeNotFound, - expectDoesntExistRbacForbidden, - expectNotSpaceAwareRbacForbidden, - expectNotSpaceAwareResults, - expectSpaceAwareRbacForbidden, - expectSpaceAwareResults, - expectHiddenTypeRbacForbidden, - expectHiddenTypeRbacForbiddenWithGlobalAllowed, - bulkUpdateTest, + addTests, + createTestDefinitions, + expectForbidden, }; } diff --git a/x-pack/test/saved_object_api_integration/common/suites/create.ts b/x-pack/test/saved_object_api_integration/common/suites/create.ts index 29960c513d40f..f657756be92cd 100644 --- a/x-pack/test/saved_object_api_integration/common/suites/create.ts +++ b/x-pack/test/saved_object_api_integration/common/suites/create.ts @@ -3,206 +3,117 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ + import expect from '@kbn/expect'; import { SuperTest } from 'supertest'; -import { DEFAULT_SPACE_ID } from '../../../../plugins/spaces/common/constants'; -import { getUrlPrefix } from '../lib/space_test_utils'; -import { DescribeFn, TestDefinitionAuthentication } from '../lib/types'; - -interface CreateTest { - statusCode: number; - response: (resp: { [key: string]: any }) => void; -} - -interface CreateCustomTest extends CreateTest { - type: string; - description: string; - requestBody: any; -} - -interface CreateTests { - spaceAware: CreateTest; - notSpaceAware: CreateTest; - hiddenType: CreateTest; - custom?: CreateCustomTest; +import { SAVED_OBJECT_TEST_CASES as CASES } from '../lib/saved_object_test_cases'; +import { SPACES } from '../lib/spaces'; +import { + createRequest, + expectResponses, + getUrlPrefix, + getTestTitle, +} from '../lib/saved_object_test_utils'; +import { ExpectResponseBody, TestCase, TestDefinition, TestSuite } from '../lib/types'; + +export interface CreateTestDefinition extends TestDefinition { + request: { type: string; id: string }; + overwrite: boolean; } - -interface CreateTestDefinition { - user?: TestDefinitionAuthentication; - spaceId?: string; - tests: CreateTests; +export type CreateTestSuite = TestSuite; +export interface CreateTestCase extends TestCase { + failure?: 400 | 403 | 409; } -const spaceAwareType = 'visualization'; -const notSpaceAwareType = 'globaltype'; +const NEW_ATTRIBUTE_KEY = 'title'; // all type mappings include this attribute, for simplicity's sake +const NEW_ATTRIBUTE_VAL = `New attribute value ${Date.now()}`; + +// ID intentionally left blank on NEW_SINGLE_NAMESPACE_OBJ to ensure we can create saved objects without specifying the ID +// we could create six separate test cases to test every permutation, but there's no real value in doing so +const NEW_SINGLE_NAMESPACE_OBJ = Object.freeze({ type: 'dashboard', id: '' }); +const NEW_MULTI_NAMESPACE_OBJ = Object.freeze({ type: 'sharedtype', id: 'new-sharedtype-id' }); +const NEW_NAMESPACE_AGNOSTIC_OBJ = Object.freeze({ type: 'globaltype', id: 'new-globaltype-id' }); +export const TEST_CASES = Object.freeze({ + ...CASES, + NEW_SINGLE_NAMESPACE_OBJ, + NEW_MULTI_NAMESPACE_OBJ, + NEW_NAMESPACE_AGNOSTIC_OBJ, +}); export function createTestSuiteFactory(es: any, esArchiver: any, supertest: SuperTest) { - const createExpectRbacForbidden = (type: string) => (resp: { [key: string]: any }) => { - expect(resp.body).to.eql({ - statusCode: 403, - error: 'Forbidden', - message: `Unable to create ${type}`, - }); - }; - - const expectBadRequestForHiddenType = (resp: { [key: string]: any }) => { - expect(resp.body).to.eql({ - message: "Unsupported saved object type: 'hiddentype': Bad Request", - statusCode: 400, - error: 'Bad Request', - }); - }; - - const createExpectSpaceAwareResults = (spaceId = DEFAULT_SPACE_ID) => async (resp: { - [key: string]: any; - }) => { - expect(resp.body) - .to.have.property('id') - .match(/^[0-9a-f-]{36}$/); - - // loose ISO8601 UTC time with milliseconds validation - expect(resp.body) - .to.have.property('updated_at') - .match(/^[\d-]{10}T[\d:\.]{12}Z$/); - - expect(resp.body).to.eql({ - id: resp.body.id, - migrationVersion: resp.body.migrationVersion, - type: spaceAwareType, - updated_at: resp.body.updated_at, - version: resp.body.version, - attributes: { - title: 'My favorite vis', - }, - references: [], - }); - - const expectedSpacePrefix = spaceId === DEFAULT_SPACE_ID ? '' : `${spaceId}:`; - - // query ES directory to ensure namespace was or wasn't specified - const { _source } = await es.get({ - id: `${expectedSpacePrefix}${spaceAwareType}:${resp.body.id}`, - index: '.kibana', - }); - - const { namespace: actualNamespace } = _source; - - if (spaceId === DEFAULT_SPACE_ID) { - expect(actualNamespace).to.eql(undefined); + const expectForbidden = expectResponses.forbidden('create'); + const expectResponseBody = ( + testCase: CreateTestCase, + spaceId = SPACES.DEFAULT.spaceId + ): ExpectResponseBody => async (response: Record) => { + if (testCase.failure === 403) { + await expectForbidden(testCase.type)(response); } else { - expect(actualNamespace).to.eql(spaceId); + // permitted + const object = response.body; + await expectResponses.permitted(object, testCase); + if (!testCase.failure) { + expect(object.attributes[NEW_ATTRIBUTE_KEY]).to.eql(NEW_ATTRIBUTE_VAL); + await expectResponses.successCreated(es, spaceId, object.type, object.id); + } } }; - - const expectNotSpaceAwareRbacForbidden = createExpectRbacForbidden(notSpaceAwareType); - - const expectNotSpaceAwareResults = async (resp: { [key: string]: any }) => { - expect(resp.body) - .to.have.property('id') - .match(/^[0-9a-f-]{36}$/); - - // loose ISO8601 UTC time with milliseconds validation - expect(resp.body) - .to.have.property('updated_at') - .match(/^[\d-]{10}T[\d:\.]{12}Z$/); - - expect(resp.body).to.eql({ - id: resp.body.id, - type: notSpaceAwareType, - updated_at: resp.body.updated_at, - version: resp.body.version, - attributes: { - name: `Can't be contained to a space`, - }, - references: [], - }); - - // query ES directory to ensure namespace wasn't specified - const { _source } = await es.get({ - id: `${notSpaceAwareType}:${resp.body.id}`, - index: '.kibana', - }); - - const { namespace: actualNamespace } = _source; - - expect(actualNamespace).to.eql(undefined); + const createTestDefinitions = ( + testCases: CreateTestCase | CreateTestCase[], + forbidden: boolean, + overwrite: boolean, + options?: { + spaceId?: string; + responseBodyOverride?: ExpectResponseBody; + } + ): CreateTestDefinition[] => { + let cases = Array.isArray(testCases) ? testCases : [testCases]; + if (forbidden) { + // override the expected result in each test case + cases = cases.map(x => ({ ...x, failure: 403 })); + } + return cases.map(x => ({ + title: getTestTitle(x), + responseStatusCode: x.failure ?? 200, + request: createRequest(x), + responseBody: options?.responseBodyOverride || expectResponseBody(x, options?.spaceId), + overwrite, + })); }; - const expectSpaceAwareRbacForbidden = createExpectRbacForbidden(spaceAwareType); - - const expectHiddenTypeRbacForbidden = createExpectRbacForbidden('hiddentype'); - - const makeCreateTest = (describeFn: DescribeFn) => ( + const makeCreateTest = (describeFn: Mocha.SuiteFunction) => ( description: string, - definition: CreateTestDefinition + definition: CreateTestSuite ) => { - const { user = {}, spaceId = DEFAULT_SPACE_ID, tests } = definition; + const { user, spaceId = SPACES.DEFAULT.spaceId, tests } = definition; + describeFn(description, () => { before(() => esArchiver.load('saved_objects/spaces')); after(() => esArchiver.unload('saved_objects/spaces')); - it(`should return ${tests.spaceAware.statusCode} for a space-aware type`, async () => { - await supertest - .post(`${getUrlPrefix(spaceId)}/api/saved_objects/${spaceAwareType}`) - .auth(user.username, user.password) - .send({ - attributes: { - title: 'My favorite vis', - }, - }) - .expect(tests.spaceAware.statusCode) - .then(tests.spaceAware.response); - }); - - it(`should return ${tests.notSpaceAware.statusCode} for a non space-aware type`, async () => { - await supertest - .post(`${getUrlPrefix(spaceId)}/api/saved_objects/${notSpaceAwareType}`) - .auth(user.username, user.password) - .send({ - attributes: { - name: `Can't be contained to a space`, - }, - }) - .expect(tests.notSpaceAware.statusCode) - .then(tests.notSpaceAware.response); - }); - - it(`should return ${tests.hiddenType.statusCode} for the hiddentype`, async () => { - await supertest - .post(`${getUrlPrefix(spaceId)}/api/saved_objects/hiddentype`) - .auth(user.username, user.password) - .send({ - attributes: { - name: `Can't be created via the Saved Objects API`, - }, - }) - .expect(tests.hiddenType.statusCode) - .then(tests.hiddenType.response); - }); - if (tests.custom) { - it(tests.custom.description, async () => { + for (const test of tests) { + it(`should return ${test.responseStatusCode} ${test.title}`, async () => { + const { type, id } = test.request; + const path = `${type}${id ? `/${id}` : ''}`; + const requestBody = { attributes: { [NEW_ATTRIBUTE_KEY]: NEW_ATTRIBUTE_VAL } }; + const query = test.overwrite ? '?overwrite=true' : ''; await supertest - .post(`${getUrlPrefix(spaceId)}/api/saved_objects/${tests.custom!.type}`) - .auth(user.username, user.password) - .send(tests.custom!.requestBody) - .expect(tests.custom!.statusCode) - .then(tests.custom!.response); + .post(`${getUrlPrefix(spaceId)}/api/saved_objects/${path}${query}`) + .auth(user?.username, user?.password) + .send(requestBody) + .expect(test.responseStatusCode) + .then(test.responseBody); }); } }); }; - const createTest = makeCreateTest(describe); + const addTests = makeCreateTest(describe); // @ts-ignore - createTest.only = makeCreateTest(describe.only); + addTests.only = makeCreateTest(describe.only); return { - createExpectSpaceAwareResults, - createTest, - expectNotSpaceAwareRbacForbidden, - expectNotSpaceAwareResults, - expectSpaceAwareRbacForbidden, - expectBadRequestForHiddenType, - expectHiddenTypeRbacForbidden, + addTests, + createTestDefinitions, }; } diff --git a/x-pack/test/saved_object_api_integration/common/suites/delete.ts b/x-pack/test/saved_object_api_integration/common/suites/delete.ts index d96ae5446d732..2222aa0c97267 100644 --- a/x-pack/test/saved_object_api_integration/common/suites/delete.ts +++ b/x-pack/test/saved_object_api_integration/common/suites/delete.ts @@ -4,147 +4,97 @@ * you may not use this file except in compliance with the Elastic License. */ -import expect from '@kbn/expect'; import { SuperTest } from 'supertest'; -import { DEFAULT_SPACE_ID } from '../../../../plugins/spaces/common/constants'; -import { getIdPrefix, getUrlPrefix } from '../lib/space_test_utils'; -import { DescribeFn, TestDefinitionAuthentication } from '../lib/types'; - -interface DeleteTest { - statusCode: number; - response: (resp: { [key: string]: any }) => void; +import expect from '@kbn/expect/expect.js'; +import { SAVED_OBJECT_TEST_CASES as CASES } from '../lib/saved_object_test_cases'; +import { SPACES } from '../lib/spaces'; +import { + createRequest, + expectResponses, + getUrlPrefix, + getTestTitle, +} from '../lib/saved_object_test_utils'; +import { ExpectResponseBody, TestCase, TestDefinition, TestSuite } from '../lib/types'; + +export interface DeleteTestDefinition extends TestDefinition { + request: { type: string; id: string }; } - -interface DeleteTests { - spaceAware: DeleteTest; - notSpaceAware: DeleteTest; - hiddenType: DeleteTest; - invalidId: DeleteTest; +export type DeleteTestSuite = TestSuite; +export interface DeleteTestCase extends TestCase { + failure?: 403 | 404; } -interface DeleteTestDefinition { - user?: TestDefinitionAuthentication; - spaceId?: string; - otherSpaceId?: string; - tests: DeleteTests; -} +const DOES_NOT_EXIST = Object.freeze({ type: 'dashboard', id: 'does-not-exist' }); +export const TEST_CASES = Object.freeze({ ...CASES, DOES_NOT_EXIST }); export function deleteTestSuiteFactory(esArchiver: any, supertest: SuperTest) { - const createExpectNotFound = (spaceId: string, type: string, id: string) => (resp: { - [key: string]: any; - }) => { - expect(resp.body).to.eql({ - statusCode: 404, - error: 'Not Found', - message: `Saved object [${type}/${getIdPrefix(spaceId)}${id}] not found`, - }); - }; - - const createExpectRbacForbidden = (type: string) => (resp: { [key: string]: any }) => { - expect(resp.body).to.eql({ - statusCode: 403, - error: 'Forbidden', - message: `Unable to delete ${type}`, - }); - }; - - const expectGenericNotFound = (resp: { [key: string]: any }) => { - expect(resp.body).to.eql({ - statusCode: 404, - error: 'Not Found', - message: `Not Found`, - }); - }; - - const createExpectSpaceAwareNotFound = (spaceId: string = DEFAULT_SPACE_ID) => (resp: { - [key: string]: any; - }) => { - createExpectNotFound(spaceId, 'dashboard', 'be3733a0-9efe-11e7-acb3-3dab96693fab')(resp); - }; - - const createExpectUnknownDocNotFound = (spaceId: string = DEFAULT_SPACE_ID) => (resp: { - [key: string]: any; - }) => { - createExpectNotFound(spaceId, 'dashboard', `not-a-real-id`)(resp); + const expectForbidden = expectResponses.forbidden('delete'); + const expectResponseBody = (testCase: DeleteTestCase): ExpectResponseBody => async ( + response: Record + ) => { + if (testCase.failure === 403) { + await expectForbidden(testCase.type)(response); + } else { + // permitted + const object = response.body; + if (testCase.failure) { + await expectResponses.permitted(object, testCase); + } else { + // the success response for `delete` is an empty object + expect(object).to.eql({}); + } + } }; - - const expectEmpty = (resp: { [key: string]: any }) => { - expect(resp.body).to.eql({}); + const createTestDefinitions = ( + testCases: DeleteTestCase | DeleteTestCase[], + forbidden: boolean, + options?: { + spaceId?: string; + responseBodyOverride?: ExpectResponseBody; + } + ): DeleteTestDefinition[] => { + let cases = Array.isArray(testCases) ? testCases : [testCases]; + if (forbidden) { + // override the expected result in each test case + cases = cases.map(x => ({ ...x, failure: 403 })); + } + return cases.map(x => ({ + title: getTestTitle(x), + responseStatusCode: x.failure ?? 200, + request: createRequest(x), + responseBody: options?.responseBodyOverride || expectResponseBody(x), + })); }; - const expectRbacInvalidIdForbidden = createExpectRbacForbidden('dashboard'); - - const expectRbacNotSpaceAwareForbidden = createExpectRbacForbidden('globaltype'); - - const expectRbacSpaceAwareForbidden = createExpectRbacForbidden('dashboard'); - - const expectRbacHiddenTypeForbidden = createExpectRbacForbidden('hiddentype'); - - const makeDeleteTest = (describeFn: DescribeFn) => ( + const makeDeleteTest = (describeFn: Mocha.SuiteFunction) => ( description: string, - definition: DeleteTestDefinition + definition: DeleteTestSuite ) => { - const { user = {}, spaceId = DEFAULT_SPACE_ID, otherSpaceId, tests } = definition; + const { user, spaceId = SPACES.DEFAULT.spaceId, tests } = definition; describeFn(description, () => { before(() => esArchiver.load('saved_objects/spaces')); after(() => esArchiver.unload('saved_objects/spaces')); - it(`should return ${tests.spaceAware.statusCode} when deleting a space-aware doc`, async () => - await supertest - .delete( - `${getUrlPrefix(spaceId)}/api/saved_objects/dashboard/${getIdPrefix( - otherSpaceId || spaceId - )}be3733a0-9efe-11e7-acb3-3dab96693fab` - ) - .auth(user.username, user.password) - .expect(tests.spaceAware.statusCode) - .then(tests.spaceAware.response)); - - it(`should return ${tests.notSpaceAware.statusCode} when deleting a non-space-aware doc`, async () => - await supertest - .delete( - `${getUrlPrefix( - spaceId - )}/api/saved_objects/globaltype/8121a00-8efd-21e7-1cb3-34ab966434445` - ) - .auth(user.username, user.password) - .expect(tests.notSpaceAware.statusCode) - .then(tests.notSpaceAware.response)); - - it(`should return ${tests.hiddenType.statusCode} when deleting a hiddentype doc`, async () => - await supertest - .delete(`${getUrlPrefix(spaceId)}/api/saved_objects/hiddentype/hiddentype_1`) - .auth(user.username, user.password) - .expect(tests.hiddenType.statusCode) - .then(tests.hiddenType.response)); - - it(`should return ${tests.invalidId.statusCode} when deleting an unknown doc`, async () => - await supertest - .delete( - `${getUrlPrefix(spaceId)}/api/saved_objects/dashboard/${getIdPrefix( - otherSpaceId || spaceId - )}not-a-real-id` - ) - .auth(user.username, user.password) - .expect(tests.invalidId.statusCode) - .then(tests.invalidId.response)); + for (const test of tests) { + it(`should return ${test.responseStatusCode} ${test.title}`, async () => { + const { type, id } = test.request; + await supertest + .delete(`${getUrlPrefix(spaceId)}/api/saved_objects/${type}/${id}`) + .auth(user?.username, user?.password) + .expect(test.responseStatusCode) + .then(test.responseBody); + }); + } }); }; - const deleteTest = makeDeleteTest(describe); + const addTests = makeDeleteTest(describe); // @ts-ignore - deleteTest.only = makeDeleteTest(describe.only); + addTests.only = makeDeleteTest(describe.only); return { - expectGenericNotFound, - createExpectSpaceAwareNotFound, - createExpectUnknownDocNotFound, - deleteTest, - expectEmpty, - expectRbacInvalidIdForbidden, - expectRbacNotSpaceAwareForbidden, - expectRbacSpaceAwareForbidden, - expectRbacHiddenTypeForbidden, + addTests, + createTestDefinitions, }; } diff --git a/x-pack/test/saved_object_api_integration/common/suites/export.ts b/x-pack/test/saved_object_api_integration/common/suites/export.ts index e6853096962ec..ddd43d42410ae 100644 --- a/x-pack/test/saved_object_api_integration/common/suites/export.ts +++ b/x-pack/test/saved_object_api_integration/common/suites/export.ts @@ -5,164 +5,191 @@ */ import expect from '@kbn/expect'; import { SuperTest } from 'supertest'; -import { DEFAULT_SPACE_ID } from '../../../../plugins/spaces/common/constants'; -import { getIdPrefix, getUrlPrefix } from '../lib/space_test_utils'; -import { DescribeFn, TestDefinitionAuthentication } from '../lib/types'; +import { SAVED_OBJECT_TEST_CASES as CASES } from '../lib/saved_object_test_cases'; +import { SPACES } from '../lib/spaces'; +import { expectResponses, getUrlPrefix } from '../lib/saved_object_test_utils'; +import { ExpectResponseBody, TestCase, TestDefinition, TestSuite } from '../lib/types'; -interface ExportTest { - statusCode: number; - description: string; - response: (resp: { [key: string]: any }) => void; -} +const { + DEFAULT: { spaceId: DEFAULT_SPACE_ID }, + SPACE_1: { spaceId: SPACE_1_ID }, + SPACE_2: { spaceId: SPACE_2_ID }, +} = SPACES; -interface ExportTests { - spaceAwareType: ExportTest; - hiddenType: ExportTest; - noTypeOrObjects: ExportTest; +export interface ExportTestDefinition extends TestDefinition { + request: ReturnType; } - -interface ExportTestDefinition { - user?: TestDefinitionAuthentication; - spaceId?: string; - tests: ExportTests; +export type ExportTestSuite = TestSuite; +export interface ExportTestCase { + title: string; + type: string; + id?: string; + successResult?: TestCase | TestCase[]; + failure?: 400 | 403; } +export const getTestCases = (spaceId?: string) => ({ + singleNamespaceObject: { + title: 'single-namespace object', + ...(spaceId === SPACE_1_ID + ? CASES.SINGLE_NAMESPACE_SPACE_1 + : spaceId === SPACE_2_ID + ? CASES.SINGLE_NAMESPACE_SPACE_2 + : CASES.SINGLE_NAMESPACE_DEFAULT_SPACE), + } as ExportTestCase, + singleNamespaceType: { + // this test explicitly ensures that single-namespace objects from other spaces are not returned + title: 'single-namespace type', + type: 'isolatedtype', + successResult: + spaceId === SPACE_1_ID + ? CASES.SINGLE_NAMESPACE_SPACE_1 + : spaceId === SPACE_2_ID + ? CASES.SINGLE_NAMESPACE_SPACE_2 + : CASES.SINGLE_NAMESPACE_DEFAULT_SPACE, + } as ExportTestCase, + multiNamespaceObject: { + title: 'multi-namespace object', + ...(spaceId === SPACE_1_ID + ? CASES.MULTI_NAMESPACE_ONLY_SPACE_1 + : spaceId === SPACE_2_ID + ? CASES.MULTI_NAMESPACE_ONLY_SPACE_2 + : CASES.MULTI_NAMESPACE_DEFAULT_AND_SPACE_1), + failure: 400, // multi-namespace types cannot be exported yet + } as ExportTestCase, + multiNamespaceType: { + title: 'multi-namespace type', + type: 'sharedtype', + // successResult: + // spaceId === SPACE_1_ID + // ? [CASES.MULTI_NAMESPACE_DEFAULT_AND_SPACE_1, CASES.MULTI_NAMESPACE_ONLY_SPACE_1] + // : spaceId === SPACE_2_ID + // ? CASES.MULTI_NAMESPACE_ONLY_SPACE_2 + // : CASES.MULTI_NAMESPACE_DEFAULT_AND_SPACE_1, + failure: 400, // multi-namespace types cannot be exported yet + } as ExportTestCase, + namespaceAgnosticObject: { + title: 'namespace-agnostic object', + ...CASES.NAMESPACE_AGNOSTIC, + } as ExportTestCase, + namespaceAgnosticType: { + title: 'namespace-agnostic type', + type: 'globaltype', + successResult: CASES.NAMESPACE_AGNOSTIC, + } as ExportTestCase, + hiddenObject: { title: 'hidden object', ...CASES.HIDDEN, failure: 400 } as ExportTestCase, + hiddenType: { title: 'hidden type', type: 'hiddentype', failure: 400 } as ExportTestCase, +}); +export const createRequest = ({ type, id }: ExportTestCase) => + id ? { objects: [{ type, id }] } : { type }; +const getTestTitle = ({ failure, title }: ExportTestCase) => { + let description = 'success'; + if (failure === 400) { + description = 'bad request'; + } else if (failure === 403) { + description = 'forbidden'; + } + return `${description} ["${title}"]`; +}; + export function exportTestSuiteFactory(esArchiver: any, supertest: SuperTest) { - const createExpectRbacForbidden = (type: string) => (resp: { [key: string]: any }) => { - // In export only, the API uses "bulk_get" or "find" depending on the parameters it receives. - // The best that could be done here is to have an if statement to ensure at least one of the - // two errors has been thrown. - if (resp.body.message.indexOf(`bulk_get`) !== -1) { - expect(resp.body).to.eql({ - statusCode: 403, - error: 'Forbidden', - message: `Unable to bulk_get ${type}`, + const expectForbiddenBulkGet = expectResponses.forbidden('bulk_get'); + const expectForbiddenFind = expectResponses.forbidden('find'); + const expectResponseBody = (testCase: ExportTestCase): ExpectResponseBody => async ( + response: Record + ) => { + const { type, id, successResult = { type, id }, failure } = testCase; + if (failure === 403) { + // In export only, the API uses "bulk_get" or "find" depending on the parameters it receives. + // The best that could be done here is to have an if statement to ensure at least one of the + // two errors has been thrown. + if (id) { + await expectForbiddenBulkGet(type)(response); + } else { + await expectForbiddenFind(type)(response); + } + } else if (failure === 400) { + // 400 + expect(response.body.error).to.eql('Bad Request'); + expect(response.body.statusCode).to.eql(failure); + if (id) { + expect(response.body.message).to.eql( + `Trying to export object(s) with non-exportable types: ${type}:${id}` + ); + } else { + expect(response.body.message).to.eql(`Trying to export non-exportable type(s): ${type}`); + } + } else { + // 2xx + expect(response.body).not.to.have.property('error'); + const ndjson = response.text.split('\n'); + const savedObjectsArray = Array.isArray(successResult) ? successResult : [successResult]; + expect(ndjson.length).to.eql(savedObjectsArray.length + 1); + for (let i = 0; i < savedObjectsArray.length; i++) { + const object = JSON.parse(ndjson[i]); + const { type: expectedType, id: expectedId } = savedObjectsArray[i]; + expect(object.type).to.eql(expectedType); + expect(object.id).to.eql(expectedId); + expect(object.updated_at).to.match(/^[\d-]{10}T[\d:\.]{12}Z$/); + // don't test attributes, version, or references + } + const exportDetails = JSON.parse(ndjson[ndjson.length - 1]); + expect(exportDetails).to.eql({ + exportedCount: ndjson.length - 1, + missingRefCount: 0, + missingReferences: [], }); - return; } - expect(resp.body).to.eql({ - statusCode: 403, - error: 'Forbidden', - message: `Unable to find ${type}`, - }); }; - - const expectTypeOrObjectsRequired = (resp: { [key: string]: any }) => { - expect(resp.body).to.eql({ - statusCode: 400, - error: 'Bad Request', - message: '[request body]: expected a plain object value, but found [null] instead.', - }); - }; - - const expectInvalidTypeSpecified = (resp: { [key: string]: any }) => { - expect(resp.body).to.eql({ - statusCode: 400, - error: 'Bad Request', - message: `Trying to export object(s) with non-exportable types: hiddentype:hiddentype_1`, - }); - }; - - const createExpectVisualizationResults = (spaceId = DEFAULT_SPACE_ID) => (resp: { - [key: string]: any; - }) => { - const response = JSON.parse(resp.text); - expect(response).to.eql({ - type: 'visualization', - id: `${getIdPrefix(spaceId)}dd7caf20-9efd-11e7-acb3-3dab96693fab`, - version: response.version, - attributes: response.attributes, - references: [ - { - name: 'kibanaSavedObjectMeta.searchSourceJSON.index', - type: 'index-pattern', - id: `${getIdPrefix(spaceId)}91200a00-9efd-11e7-acb3-3dab96693fab`, - }, - ], - migrationVersion: response.migrationVersion, - updated_at: '2017-09-21T18:51:23.794Z', - }); + const createTestDefinitions = ( + testCases: ExportTestCase | ExportTestCase[], + forbidden: boolean, + options?: { + responseBodyOverride?: ExpectResponseBody; + } + ): ExportTestDefinition[] => { + let cases = Array.isArray(testCases) ? testCases : [testCases]; + if (forbidden) { + // override the expected result in each test case + cases = cases.map(x => ({ ...x, failure: 403 })); + } + return cases.map(x => ({ + title: getTestTitle(x), + responseStatusCode: x.failure ?? 200, + request: createRequest(x), + responseBody: options?.responseBodyOverride || expectResponseBody(x), + })); }; - const makeExportTest = (describeFn: DescribeFn) => ( + const makeExportTest = (describeFn: Mocha.SuiteFunction) => ( description: string, - definition: ExportTestDefinition + definition: ExportTestSuite ) => { - const { user = {}, spaceId = DEFAULT_SPACE_ID, tests } = definition; + const { user, spaceId = DEFAULT_SPACE_ID, tests } = definition; describeFn(description, () => { before(() => esArchiver.load('saved_objects/spaces')); after(() => esArchiver.unload('saved_objects/spaces')); - it(`space aware type should return ${tests.spaceAwareType.statusCode} with ${tests.spaceAwareType.description} when querying by type`, async () => { - await supertest - .post(`${getUrlPrefix(spaceId)}/api/saved_objects/_export`) - .send({ - type: 'visualization', - excludeExportDetails: true, - }) - .auth(user.username, user.password) - .expect(tests.spaceAwareType.statusCode) - .then(tests.spaceAwareType.response); - }); - - it(`space aware type should return ${tests.spaceAwareType.statusCode} with ${tests.spaceAwareType.description} when querying by objects`, async () => { - await supertest - .post(`${getUrlPrefix(spaceId)}/api/saved_objects/_export`) - .send({ - objects: [ - { - type: 'visualization', - id: `${getIdPrefix(spaceId)}dd7caf20-9efd-11e7-acb3-3dab96693fab`, - }, - ], - excludeExportDetails: true, - }) - .auth(user.username, user.password) - .expect(tests.spaceAwareType.statusCode) - .then(tests.spaceAwareType.response); - }); - - describe('hidden type', () => { - it(`should return ${tests.hiddenType.statusCode} with ${tests.hiddenType.description}`, async () => { + for (const test of tests) { + it(`should return ${test.responseStatusCode} ${test.title}`, async () => { await supertest .post(`${getUrlPrefix(spaceId)}/api/saved_objects/_export`) - .send({ - objects: [ - { - type: 'hiddentype', - id: `hiddentype_1`, - }, - ], - excludeExportDetails: true, - }) - .auth(user.username, user.password) - .expect(tests.hiddenType.statusCode) - .then(tests.hiddenType.response); + .auth(user?.username, user?.password) + .send(test.request) + .expect(test.responseStatusCode) + .then(test.responseBody); }); - }); - - describe('no type or objects', () => { - it(`should return ${tests.noTypeOrObjects.statusCode} with ${tests.noTypeOrObjects.description}`, async () => { - await supertest - .post(`${getUrlPrefix(spaceId)}/api/saved_objects/_export`) - .auth(user.username, user.password) - .expect(tests.noTypeOrObjects.statusCode) - .then(tests.noTypeOrObjects.response); - }); - }); + } }); }; - const exportTest = makeExportTest(describe); + const addTests = makeExportTest(describe); // @ts-ignore - exportTest.only = makeExportTest(describe.only); + addTests.only = makeExportTest(describe.only); return { - createExpectRbacForbidden, - expectTypeOrObjectsRequired, - expectInvalidTypeSpecified, - createExpectVisualizationResults, - exportTest, + addTests, + createTestDefinitions, }; } diff --git a/x-pack/test/saved_object_api_integration/common/suites/find.ts b/x-pack/test/saved_object_api_integration/common/suites/find.ts index 5479960634ccb..75d6653365fdf 100644 --- a/x-pack/test/saved_object_api_integration/common/suites/find.ts +++ b/x-pack/test/saved_object_api_integration/common/suites/find.ts @@ -3,271 +3,190 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ + import expect from '@kbn/expect'; import { SuperTest } from 'supertest'; -import { DEFAULT_SPACE_ID } from '../../../../plugins/spaces/common/constants'; -import { getIdPrefix, getUrlPrefix } from '../lib/space_test_utils'; -import { DescribeFn, TestDefinitionAuthentication } from '../lib/types'; - -interface FindTest { - statusCode: number; - description: string; - response: (resp: { [key: string]: any }) => void; +import querystring from 'querystring'; +import { SAVED_OBJECT_TEST_CASES as CASES } from '../lib/saved_object_test_cases'; +import { SPACES } from '../lib/spaces'; +import { expectResponses, getUrlPrefix } from '../lib/saved_object_test_utils'; +import { ExpectResponseBody, TestCase, TestDefinition, TestSuite } from '../lib/types'; + +const { + DEFAULT: { spaceId: DEFAULT_SPACE_ID }, + SPACE_1: { spaceId: SPACE_1_ID }, + SPACE_2: { spaceId: SPACE_2_ID }, +} = SPACES; + +export interface FindTestDefinition extends TestDefinition { + request: { query: string }; } - -interface FindTests { - spaceAwareType: FindTest; - notSpaceAwareType: FindTest; - unknownType: FindTest; - pageBeyondTotal: FindTest; - unknownSearchField: FindTest; - hiddenType: FindTest; - noType: FindTest; - filterWithNotSpaceAwareType: FindTest; - filterWithHiddenType: FindTest; - filterWithUnknownType: FindTest; - filterWithNoType: FindTest; - filterWithUnAllowedType: FindTest; +export type FindTestSuite = TestSuite; +export interface FindTestCase { + title: string; + query: string; + successResult?: { + savedObjects?: TestCase | TestCase[]; + page?: number; + perPage?: number; + total?: number; + }; + failure?: 400 | 403; } -interface FindTestDefinition { - user?: TestDefinitionAuthentication; - spaceId?: string; - tests: FindTests; -} +export const getTestCases = (spaceId?: string) => ({ + singleNamespaceType: { + title: 'find single-namespace type', + query: 'type=isolatedtype&fields=title', + successResult: { + savedObjects: + spaceId === SPACE_1_ID + ? CASES.SINGLE_NAMESPACE_SPACE_1 + : spaceId === SPACE_2_ID + ? CASES.SINGLE_NAMESPACE_SPACE_2 + : CASES.SINGLE_NAMESPACE_DEFAULT_SPACE, + }, + } as FindTestCase, + multiNamespaceType: { + title: 'find multi-namespace type', + query: 'type=sharedtype&fields=title', + successResult: { + savedObjects: + spaceId === SPACE_1_ID + ? [CASES.MULTI_NAMESPACE_DEFAULT_AND_SPACE_1, CASES.MULTI_NAMESPACE_ONLY_SPACE_1] + : spaceId === SPACE_2_ID + ? CASES.MULTI_NAMESPACE_ONLY_SPACE_2 + : CASES.MULTI_NAMESPACE_DEFAULT_AND_SPACE_1, + }, + } as FindTestCase, + namespaceAgnosticType: { + title: 'find namespace-agnostic type', + query: 'type=globaltype&fields=title', + successResult: { savedObjects: CASES.NAMESPACE_AGNOSTIC }, + } as FindTestCase, + hiddenType: { title: 'find hidden type', query: 'type=hiddentype&fields=name' } as FindTestCase, + unknownType: { title: 'find unknown type', query: 'type=wigwags' } as FindTestCase, + pageBeyondTotal: { + title: 'find page beyond total', + query: 'type=isolatedtype&page=100&per_page=100', + successResult: { page: 100, perPage: 100, total: 1, savedObjects: [] }, + } as FindTestCase, + unknownSearchField: { + title: 'find unknown search field', + query: 'type=url&search_fields=a', + } as FindTestCase, + filterWithNamespaceAgnosticType: { + title: 'filter with namespace-agnostic type', + query: 'type=globaltype&filter=globaltype.attributes.title:*global*', + successResult: { savedObjects: CASES.NAMESPACE_AGNOSTIC }, + } as FindTestCase, + filterWithHiddenType: { + title: 'filter with hidden type', + query: `type=hiddentype&fields=name&filter=hiddentype.attributes.title:'hello'`, + } as FindTestCase, + filterWithUnknownType: { + title: 'filter with unknown type', + query: `type=wigwags&filter=wigwags.attributes.title:'unknown'`, + } as FindTestCase, + filterWithDisallowedType: { + title: 'filter with disallowed type', + query: `type=globaltype&filter=dashboard.title:'Requests'`, + failure: 400, + } as FindTestCase, +}); +export const createRequest = ({ query }: FindTestCase) => ({ query }); +const getTestTitle = ({ failure, title }: FindTestCase) => { + let description = 'success'; + if (failure === 400) { + description = 'bad request'; + } else if (failure === 403) { + description = 'forbidden'; + } + return `${description} ["${title}"]`; +}; export function findTestSuiteFactory(esArchiver: any, supertest: SuperTest) { - const createExpectEmpty = (page: number, perPage: number, total: number) => (resp: { - [key: string]: any; - }) => { - expect(resp.body).to.eql({ - page, - per_page: perPage, - total, - saved_objects: [], - }); - }; - - const createExpectRbacForbidden = (type?: string) => (resp: { [key: string]: any }) => { - const message = type ? `Unable to find ${type}` : `Not authorized to find saved_object`; - - expect(resp.body).to.eql({ - statusCode: 403, - error: 'Forbidden', - message, - }); - }; - - const expectNotSpaceAwareResults = (resp: { [key: string]: any }) => { - expect(resp.body).to.eql({ - page: 1, - per_page: 20, - total: 1, - saved_objects: [ - { - type: 'globaltype', - id: `8121a00-8efd-21e7-1cb3-34ab966434445`, - version: resp.body.saved_objects[0].version, - attributes: { - name: 'My favorite global object', - }, - references: [], - updated_at: '2017-09-21T18:59:16.270Z', - }, - ], - }); - }; - - const expectFilterWrongTypeError = (resp: { [key: string]: any }) => { - expect(resp.body).to.eql({ - error: 'Bad Request', - message: 'This type dashboard is not allowed: Bad Request', - statusCode: 400, - }); - }; - - const expectTypeRequired = (resp: { [key: string]: any }) => { - expect(resp.body).to.eql({ - error: 'Bad Request', - message: '[request query.type]: expected at least one defined value but got [undefined]', - statusCode: 400, - }); + const expectForbidden = expectResponses.forbidden('find'); + const expectResponseBody = (testCase: FindTestCase): ExpectResponseBody => async ( + response: Record + ) => { + const { failure, successResult = {}, query } = testCase; + const parsedQuery = querystring.parse(query); + if (failure === 403) { + const type = parsedQuery.type; + await expectForbidden(type)(response); + } else if (failure === 400) { + const type = (parsedQuery.filter as string).split('.')[0]; + expect(response.body.error).to.eql('Bad Request'); + expect(response.body.statusCode).to.eql(failure); + expect(response.body.message).to.eql(`This type ${type} is not allowed: Bad Request`); + } else { + // 2xx + expect(response.body).not.to.have.property('error'); + const { page = 1, perPage = 20, total, savedObjects = [] } = successResult; + const savedObjectsArray = Array.isArray(savedObjects) ? savedObjects : [savedObjects]; + expect(response.body.page).to.eql(page); + expect(response.body.per_page).to.eql(perPage); + expect(response.body.total).to.eql(total || savedObjectsArray.length); + for (let i = 0; i < savedObjectsArray.length; i++) { + const object = response.body.saved_objects[i]; + const { type: expectedType, id: expectedId } = savedObjectsArray[i]; + expect(object.type).to.eql(expectedType); + expect(object.id).to.eql(expectedId); + expect(object.updated_at).to.match(/^[\d-]{10}T[\d:\.]{12}Z$/); + // don't test attributes, version, or references + } + } }; - - const createExpectVisualizationResults = (spaceId = DEFAULT_SPACE_ID) => (resp: { - [key: string]: any; - }) => { - expect(resp.body).to.eql({ - page: 1, - per_page: 20, - total: 1, - saved_objects: [ - { - type: 'visualization', - id: `${getIdPrefix(spaceId)}dd7caf20-9efd-11e7-acb3-3dab96693fab`, - version: resp.body.saved_objects[0].version, - attributes: { - title: 'Count of requests', - }, - migrationVersion: resp.body.saved_objects[0].migrationVersion, - references: [ - { - id: `${getIdPrefix(spaceId)}91200a00-9efd-11e7-acb3-3dab96693fab`, - name: 'kibanaSavedObjectMeta.searchSourceJSON.index', - type: 'index-pattern', - }, - ], - updated_at: '2017-09-21T18:51:23.794Z', - }, - ], - }); + const createTestDefinitions = ( + testCases: FindTestCase | FindTestCase[], + forbidden: boolean, + options?: { + responseBodyOverride?: ExpectResponseBody; + } + ): FindTestDefinition[] => { + let cases = Array.isArray(testCases) ? testCases : [testCases]; + if (forbidden) { + // override the expected result in each test case + cases = cases.map(x => ({ ...x, failure: 403 })); + } + return cases.map(x => ({ + title: getTestTitle(x), + responseStatusCode: x.failure ?? 200, + request: createRequest(x), + responseBody: options?.responseBodyOverride || expectResponseBody(x), + })); }; - const makeFindTest = (describeFn: DescribeFn) => ( + const makeFindTest = (describeFn: Mocha.SuiteFunction) => ( description: string, - definition: FindTestDefinition + definition: FindTestSuite ) => { - const { user = {}, spaceId = DEFAULT_SPACE_ID, tests } = definition; + const { user, spaceId = DEFAULT_SPACE_ID, tests } = definition; describeFn(description, () => { before(() => esArchiver.load('saved_objects/spaces')); after(() => esArchiver.unload('saved_objects/spaces')); - it(`space aware type should return ${tests.spaceAwareType.statusCode} with ${tests.spaceAwareType.description}`, async () => - await supertest - .get(`${getUrlPrefix(spaceId)}/api/saved_objects/_find?type=visualization&fields=title`) - .auth(user.username, user.password) - .expect(tests.spaceAwareType.statusCode) - .then(tests.spaceAwareType.response)); - - it(`not space aware type should return ${tests.notSpaceAwareType.statusCode} with ${tests.notSpaceAwareType.description}`, async () => - await supertest - .get(`${getUrlPrefix(spaceId)}/api/saved_objects/_find?type=globaltype&fields=name`) - .auth(user.username, user.password) - .expect(tests.notSpaceAwareType.statusCode) - .then(tests.notSpaceAwareType.response)); - - it(`finding a hiddentype should return ${tests.hiddenType.statusCode} with ${tests.hiddenType.description}`, async () => - await supertest - .get(`${getUrlPrefix(spaceId)}/api/saved_objects/_find?type=hiddentype&fields=name`) - .auth(user.username, user.password) - .expect(tests.hiddenType.statusCode) - .then(tests.hiddenType.response)); - - describe('unknown type', () => { - it(`should return ${tests.unknownType.statusCode} with ${tests.unknownType.description}`, async () => - await supertest - .get(`${getUrlPrefix(spaceId)}/api/saved_objects/_find?type=wigwags`) - .auth(user.username, user.password) - .expect(tests.unknownType.statusCode) - .then(tests.unknownType.response)); - }); - - describe('page beyond total', () => { - it(`should return ${tests.pageBeyondTotal.statusCode} with ${tests.pageBeyondTotal.description}`, async () => - await supertest - .get( - `${getUrlPrefix( - spaceId - )}/api/saved_objects/_find?type=visualization&page=100&per_page=100` - ) - .auth(user.username, user.password) - .expect(tests.pageBeyondTotal.statusCode) - .then(tests.pageBeyondTotal.response)); - }); - - describe('unknown search field', () => { - it(`should return ${tests.unknownSearchField.statusCode} with ${tests.unknownSearchField.description}`, async () => - await supertest - .get(`${getUrlPrefix(spaceId)}/api/saved_objects/_find?type=url&search_fields=a`) - .auth(user.username, user.password) - .expect(tests.unknownSearchField.statusCode) - .then(tests.unknownSearchField.response)); - }); - - describe('no type', () => { - it(`should return ${tests.noType.statusCode} with ${tests.noType.description}`, async () => - await supertest - .get(`${getUrlPrefix(spaceId)}/api/saved_objects/_find`) - .auth(user.username, user.password) - .expect(tests.noType.statusCode) - .then(tests.noType.response)); - }); - - describe('filter', () => { - it(`by wrong type should return ${tests.filterWithUnAllowedType.statusCode} with ${tests.filterWithUnAllowedType.description}`, async () => - await supertest - .get( - `${getUrlPrefix( - spaceId - )}/api/saved_objects/_find?type=globaltype&filter=dashboard.title:'Requests'` - ) - .auth(user.username, user.password) - .expect(tests.filterWithUnAllowedType.statusCode) - .then(tests.filterWithUnAllowedType.response)); - - it(`not space aware type should return ${tests.filterWithNotSpaceAwareType.statusCode} with ${tests.filterWithNotSpaceAwareType.description}`, async () => + for (const test of tests) { + it(`should return ${test.responseStatusCode} ${test.title}`, async () => { + const query = test.request.query ? `?${test.request.query}` : ''; await supertest - .get( - `${getUrlPrefix( - spaceId - )}/api/saved_objects/_find?type=globaltype&filter=globaltype.attributes.name:*global*` - ) - .auth(user.username, user.password) - .expect(tests.filterWithNotSpaceAwareType.statusCode) - .then(tests.filterWithNotSpaceAwareType.response)); - - it(`finding a hiddentype should return ${tests.filterWithHiddenType.statusCode} with ${tests.filterWithHiddenType.description}`, async () => - await supertest - .get( - `${getUrlPrefix( - spaceId - )}/api/saved_objects/_find?type=hiddentype&fields=name&filter=hiddentype.attributes.name:'hello'` - ) - .auth(user.username, user.password) - .expect(tests.filterWithHiddenType.statusCode) - .then(tests.filterWithHiddenType.response)); - - describe('unknown type', () => { - it(`should return ${tests.filterWithUnknownType.statusCode} with ${tests.filterWithUnknownType.description}`, async () => - await supertest - .get( - `${getUrlPrefix( - spaceId - )}/api/saved_objects/_find?type=wigwags&filter=wigwags.attributes.title:'unknown'` - ) - .auth(user.username, user.password) - .expect(tests.filterWithUnknownType.statusCode) - .then(tests.filterWithUnknownType.response)); - }); - - describe('no type', () => { - it(`should return ${tests.filterWithNoType.statusCode} with ${tests.filterWithNoType.description}`, async () => - await supertest - .get( - `${getUrlPrefix( - spaceId - )}/api/saved_objects/_find?filter=global.attributes.name:*global*` - ) - .auth(user.username, user.password) - .expect(tests.filterWithNoType.statusCode) - .then(tests.filterWithNoType.response)); + .get(`${getUrlPrefix(spaceId)}/api/saved_objects/_find${query}`) + .auth(user?.username, user?.password) + .expect(test.responseStatusCode) + .then(test.responseBody); }); - }); + } }); }; - const findTest = makeFindTest(describe); + const addTests = makeFindTest(describe); // @ts-ignore - findTest.only = makeFindTest(describe.only); + addTests.only = makeFindTest(describe.only); return { - createExpectEmpty, - createExpectRbacForbidden, - createExpectVisualizationResults, - expectFilterWrongTypeError, - expectNotSpaceAwareResults, - expectTypeRequired, - findTest, + addTests, + createTestDefinitions, }; } diff --git a/x-pack/test/saved_object_api_integration/common/suites/get.ts b/x-pack/test/saved_object_api_integration/common/suites/get.ts index c98209ca1e105..d8fa4d91276d7 100644 --- a/x-pack/test/saved_object_api_integration/common/suites/get.ts +++ b/x-pack/test/saved_object_api_integration/common/suites/get.ts @@ -3,193 +3,89 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -import expect from '@kbn/expect'; import { SuperTest } from 'supertest'; -import { DEFAULT_SPACE_ID } from '../../../../plugins/spaces/common/constants'; -import { getIdPrefix, getUrlPrefix } from '../lib/space_test_utils'; -import { DescribeFn, TestDefinitionAuthentication } from '../lib/types'; - -interface GetTest { - statusCode: number; - response: (resp: { [key: string]: any }) => void; -} - -interface GetTests { - spaceAware: GetTest; - notSpaceAware: GetTest; - hiddenType: GetTest; - doesntExist: GetTest; +import { SAVED_OBJECT_TEST_CASES as CASES } from '../lib/saved_object_test_cases'; +import { SPACES } from '../lib/spaces'; +import { + createRequest, + expectResponses, + getUrlPrefix, + getTestTitle, +} from '../lib/saved_object_test_utils'; +import { ExpectResponseBody, TestCase, TestDefinition, TestSuite } from '../lib/types'; + +export interface GetTestDefinition extends TestDefinition { + request: { type: string; id: string }; } +export type GetTestSuite = TestSuite; +export type GetTestCase = TestCase; -interface GetTestDefinition { - user?: TestDefinitionAuthentication; - spaceId?: string; - otherSpaceId?: string; - tests: GetTests; -} - -const spaceAwareId = 'dd7caf20-9efd-11e7-acb3-3dab96693fab'; -const notSpaceAwareId = '8121a00-8efd-21e7-1cb3-34ab966434445'; -const doesntExistId = 'foobar'; +const DOES_NOT_EXIST = Object.freeze({ type: 'dashboard', id: 'does-not-exist' }); +export const TEST_CASES = Object.freeze({ ...CASES, DOES_NOT_EXIST }); export function getTestSuiteFactory(esArchiver: any, supertest: SuperTest) { - const createExpectDoesntExistNotFound = (spaceId = DEFAULT_SPACE_ID) => { - return createExpectNotFound('visualization', doesntExistId, spaceId); - }; - - const createExpectNotFound = (type: string, id: string, spaceId = DEFAULT_SPACE_ID) => (resp: { - [key: string]: any; - }) => { - expect(resp.body).to.eql({ - error: 'Not Found', - message: `Saved object [${type}/${getIdPrefix(spaceId)}${id}] not found`, - statusCode: 404, - }); - }; - - const expectHiddenTypeNotFound = createExpectNotFound( - 'hiddentype', - 'hiddentype_1', - DEFAULT_SPACE_ID - ); - - const createExpectNotSpaceAwareRbacForbidden = () => (resp: { [key: string]: any }) => { - expect(resp.body).to.eql({ - error: 'Forbidden', - message: `Unable to get globaltype`, - statusCode: 403, - }); - }; - - const createExpectNotSpaceAwareResults = (spaceId = DEFAULT_SPACE_ID) => (resp: { - [key: string]: any; - }) => { - expect(resp.body).to.eql({ - id: `${notSpaceAwareId}`, - type: 'globaltype', - updated_at: '2017-09-21T18:59:16.270Z', - version: resp.body.version, - attributes: { - name: 'My favorite global object', - }, - references: [], - }); - }; - - const createExpectRbacForbidden = (type: string) => (resp: { [key: string]: any }) => { - expect(resp.body).to.eql({ - error: 'Forbidden', - message: `Unable to get ${type}`, - statusCode: 403, - }); + const expectForbidden = expectResponses.forbidden('get'); + const expectResponseBody = (testCase: GetTestCase): ExpectResponseBody => async ( + response: Record + ) => { + if (testCase.failure === 403) { + await expectForbidden(testCase.type)(response); + } else { + // permitted + const object = response.body; + await expectResponses.permitted(object, testCase); + } }; - - const createExpectSpaceAwareNotFound = (spaceId = DEFAULT_SPACE_ID) => { - return createExpectNotFound('visualization', spaceAwareId, spaceId); + const createTestDefinitions = ( + testCases: GetTestCase | GetTestCase[], + forbidden: boolean, + options?: { + spaceId?: string; + responseBodyOverride?: ExpectResponseBody; + } + ): GetTestDefinition[] => { + let cases = Array.isArray(testCases) ? testCases : [testCases]; + if (forbidden) { + // override the expected result in each test case + cases = cases.map(x => ({ ...x, failure: 403 })); + } + return cases.map(x => ({ + title: getTestTitle(x), + responseStatusCode: x.failure ?? 200, + request: createRequest(x), + responseBody: options?.responseBodyOverride || expectResponseBody(x), + })); }; - const expectSpaceAwareRbacForbidden = createExpectRbacForbidden('visualization'); - const expectNotSpaceAwareRbacForbidden = createExpectRbacForbidden('globaltype'); - const expectHiddenTypeRbacForbidden = createExpectRbacForbidden('hiddentype'); - const expectDoesntExistRbacForbidden = createExpectRbacForbidden('visualization'); - - const createExpectSpaceAwareResults = (spaceId = DEFAULT_SPACE_ID) => (resp: { - [key: string]: any; - }) => { - expect(resp.body).to.eql({ - id: `${getIdPrefix(spaceId)}dd7caf20-9efd-11e7-acb3-3dab96693fab`, - type: 'visualization', - migrationVersion: resp.body.migrationVersion, - updated_at: '2017-09-21T18:51:23.794Z', - version: resp.body.version, - attributes: { - title: 'Count of requests', - description: '', - version: 1, - // cheat for some of the more complex attributes - visState: resp.body.attributes.visState, - uiStateJSON: resp.body.attributes.uiStateJSON, - kibanaSavedObjectMeta: resp.body.attributes.kibanaSavedObjectMeta, - }, - references: [ - { - name: 'kibanaSavedObjectMeta.searchSourceJSON.index', - type: 'index-pattern', - id: `${getIdPrefix(spaceId)}91200a00-9efd-11e7-acb3-3dab96693fab`, - }, - ], - }); - }; - - const makeGetTest = (describeFn: DescribeFn) => ( + const makeGetTest = (describeFn: Mocha.SuiteFunction) => ( description: string, - definition: GetTestDefinition + definition: GetTestSuite ) => { - const { user = {}, spaceId = DEFAULT_SPACE_ID, otherSpaceId, tests } = definition; + const { user, spaceId = SPACES.DEFAULT.spaceId, tests } = definition; describeFn(description, () => { before(() => esArchiver.load('saved_objects/spaces')); after(() => esArchiver.unload('saved_objects/spaces')); - it(`should return ${tests.spaceAware.statusCode} when getting a space aware doc`, async () => { - await supertest - .get( - `${getUrlPrefix(spaceId)}/api/saved_objects/visualization/${getIdPrefix( - otherSpaceId || spaceId - )}${spaceAwareId}` - ) - .auth(user.username, user.password) - .expect(tests.spaceAware.statusCode) - .then(tests.spaceAware.response); - }); - - it(`should return ${tests.notSpaceAware.statusCode} when getting a non-space-aware doc`, async () => { - await supertest - .get(`${getUrlPrefix(spaceId)}/api/saved_objects/globaltype/${notSpaceAwareId}`) - .auth(user.username, user.password) - .expect(tests.notSpaceAware.statusCode) - .then(tests.notSpaceAware.response); - }); - - it(`should return ${tests.hiddenType.statusCode} when getting a hiddentype doc`, async () => { - await supertest - .get(`${getUrlPrefix(spaceId)}/api/saved_objects/hiddentype/hiddentype_1`) - .auth(user.username, user.password) - .expect(tests.hiddenType.statusCode) - .then(tests.hiddenType.response); - }); - - describe('document does not exist', () => { - it(`should return ${tests.doesntExist.statusCode}`, async () => { + for (const test of tests) { + it(`should return ${test.responseStatusCode} ${test.title}`, async () => { + const { type, id } = test.request; await supertest - .get( - `${getUrlPrefix(spaceId)}/api/saved_objects/visualization/${getIdPrefix( - otherSpaceId || spaceId - )}${doesntExistId}` - ) - .auth(user.username, user.password) - .expect(tests.doesntExist.statusCode) - .then(tests.doesntExist.response); + .get(`${getUrlPrefix(spaceId)}/api/saved_objects/${type}/${id}`) + .auth(user?.username, user?.password) + .expect(test.responseStatusCode) + .then(test.responseBody); }); - }); + } }); }; - const getTest = makeGetTest(describe); + const addTests = makeGetTest(describe); // @ts-ignore - getTest.only = makeGetTest(describe.only); + addTests.only = makeGetTest(describe.only); return { - createExpectDoesntExistNotFound, - createExpectNotSpaceAwareRbacForbidden, - createExpectNotSpaceAwareResults, - createExpectSpaceAwareNotFound, - createExpectSpaceAwareResults, - expectHiddenTypeNotFound, - expectSpaceAwareRbacForbidden, - expectNotSpaceAwareRbacForbidden, - expectDoesntExistRbacForbidden, - expectHiddenTypeRbacForbidden, - getTest, + addTests, + createTestDefinitions, }; } diff --git a/x-pack/test/saved_object_api_integration/common/suites/import.ts b/x-pack/test/saved_object_api_integration/common/suites/import.ts index f6723c912f82e..2f631221c6955 100644 --- a/x-pack/test/saved_object_api_integration/common/suites/import.ts +++ b/x-pack/test/saved_object_api_integration/common/suites/import.ts @@ -6,195 +6,152 @@ import expect from '@kbn/expect'; import { SuperTest } from 'supertest'; -import { DEFAULT_SPACE_ID } from '../../../../plugins/spaces/common/constants'; -import { getIdPrefix, getUrlPrefix } from '../lib/space_test_utils'; -import { DescribeFn, TestDefinitionAuthentication } from '../lib/types'; - -interface ImportTest { - statusCode: number; - response: (resp: { [key: string]: any }) => void; +import { SAVED_OBJECT_TEST_CASES as CASES } from '../lib/saved_object_test_cases'; +import { SPACES } from '../lib/spaces'; +import { + createRequest, + expectResponses, + getUrlPrefix, + getTestTitle, +} from '../lib/saved_object_test_utils'; +import { ExpectResponseBody, TestCase, TestDefinition, TestSuite } from '../lib/types'; + +export interface ImportTestDefinition extends TestDefinition { + request: Array<{ type: string; id: string }>; } - -interface ImportTests { - default: ImportTest; - hiddenType: ImportTest; - unknownType: ImportTest; +export type ImportTestSuite = TestSuite; +export interface ImportTestCase extends TestCase { + failure?: 400 | 409; // only used for permitted response case } -interface ImportTestDefinition { - user?: TestDefinitionAuthentication; - spaceId?: string; - tests: ImportTests; -} +const NEW_ATTRIBUTE_KEY = 'title'; // all type mappings include this attribute, for simplicity's sake +const NEW_ATTRIBUTE_VAL = `New attribute value ${Date.now()}`; -const createImportData = (spaceId: string) => [ - { - type: 'dashboard', - id: `${getIdPrefix(spaceId)}a01b2f57-fcfd-4864-b735-09e28f0d815e`, - attributes: { - title: 'A great new dashboard', - }, - }, - { - type: 'globaltype', - id: '05976c65-1145-4858-bbf0-d225cc78a06e', - attributes: { - name: 'A new globaltype object', - }, - }, -]; +const NEW_SINGLE_NAMESPACE_OBJ = Object.freeze({ type: 'dashboard', id: 'new-dashboard-id' }); +const NEW_MULTI_NAMESPACE_OBJ = Object.freeze({ type: 'sharedtype', id: 'new-sharedtype-id' }); +const NEW_NAMESPACE_AGNOSTIC_OBJ = Object.freeze({ type: 'globaltype', id: 'new-globaltype-id' }); +export const TEST_CASES = Object.freeze({ + ...CASES, + NEW_SINGLE_NAMESPACE_OBJ, + NEW_MULTI_NAMESPACE_OBJ, + NEW_NAMESPACE_AGNOSTIC_OBJ, +}); export function importTestSuiteFactory(es: any, esArchiver: any, supertest: SuperTest) { - const createExpectResults = (spaceId = DEFAULT_SPACE_ID) => async (resp: { - [key: string]: any; - }) => { - expect(resp.body).to.eql({ - success: true, - successCount: 2, - }); - }; - - const expectResultsWithUnsupportedHiddenType = async (resp: { [key: string]: any }) => { - expect(resp.body).to.eql({ - success: false, - successCount: 2, - errors: [ - { - error: { - type: 'unsupported_type', - }, - id: '1', - type: 'hiddentype', - }, - ], - }); + const expectForbidden = expectResponses.forbidden('bulk_create'); + const expectResponseBody = ( + testCases: ImportTestCase | ImportTestCase[], + statusCode: 200 | 403, + spaceId = SPACES.DEFAULT.spaceId + ): ExpectResponseBody => async (response: Record) => { + const testCaseArray = Array.isArray(testCases) ? testCases : [testCases]; + if (statusCode === 403) { + const types = testCaseArray.map(x => x.type); + await expectForbidden(types)(response); + } else { + // permitted + const { success, successCount, errors } = response.body; + const expectedSuccesses = testCaseArray.filter(x => !x.failure); + const expectedFailures = testCaseArray.filter(x => x.failure); + expect(success).to.eql(expectedFailures.length === 0); + expect(successCount).to.eql(expectedSuccesses.length); + if (expectedFailures.length) { + expect(errors).to.have.length(expectedFailures.length); + } else { + expect(response.body).not.to.have.property('errors'); + } + for (let i = 0; i < expectedSuccesses.length; i++) { + const { type, id } = expectedSuccesses[i]; + const { _source } = await expectResponses.successCreated(es, spaceId, type, id); + expect(_source[type][NEW_ATTRIBUTE_KEY]).to.eql(NEW_ATTRIBUTE_VAL); + } + for (let i = 0; i < expectedFailures.length; i++) { + const { type, id, failure } = expectedFailures[i]; + // we don't know the order of the returned errors; search for each one + const object = (errors as Array>).find( + x => x.type === type && x.id === id + ); + expect(object).not.to.be(undefined); + if (failure === 400) { + expect(object!.error).to.eql({ type: 'unsupported_type' }); + } else { + // 409 + expect(object!.error).to.eql({ type: 'conflict' }); + } + } + } }; - - const expectUnknownTypeUnsupported = (resp: { [key: string]: any }) => { - expect(resp.body).to.eql({ - success: false, - successCount: 2, - errors: [ - { - id: '1', - type: 'wigwags', - title: 'Wigwags title', - error: { - type: 'unsupported_type', - }, - }, - ], - }); + const createTestDefinitions = ( + testCases: ImportTestCase | ImportTestCase[], + forbidden: boolean, + options?: { + spaceId?: string; + singleRequest?: boolean; + responseBodyOverride?: ExpectResponseBody; + } + ): ImportTestDefinition[] => { + const cases = Array.isArray(testCases) ? testCases : [testCases]; + const responseStatusCode = forbidden ? 403 : 200; + if (!options?.singleRequest) { + // if we are testing cases that should result in a forbidden response, we can do each case individually + // this ensures that multiple test cases of a single type will each result in a forbidden error + return cases.map(x => ({ + title: getTestTitle(x, responseStatusCode), + request: [createRequest(x)], + responseStatusCode, + responseBody: + options?.responseBodyOverride || + expectResponseBody(x, responseStatusCode, options?.spaceId), + })); + } + // batch into a single request to save time during test execution + return [ + { + title: getTestTitle(cases, responseStatusCode), + request: cases.map(x => createRequest(x)), + responseStatusCode, + responseBody: + options?.responseBodyOverride || + expectResponseBody(cases, responseStatusCode, options?.spaceId), + }, + ]; }; - const expectHiddenTypeUnsupported = (resp: { [key: string]: any }) => { - expect(resp.body).to.eql({ - success: false, - successCount: 2, - errors: [ - { - id: '1', - type: 'hiddentype', - error: { - type: 'unsupported_type', - }, - }, - ], - }); - }; - - const expectRbacForbidden = (resp: { [key: string]: any }) => { - expect(resp.body).to.eql({ - statusCode: 403, - error: 'Forbidden', - message: `Unable to bulk_create dashboard,globaltype`, - }); - }; - - const makeImportTest = (describeFn: DescribeFn) => ( + const makeImportTest = (describeFn: Mocha.SuiteFunction) => ( description: string, - definition: ImportTestDefinition + definition: ImportTestSuite ) => { - const { user = {}, spaceId = DEFAULT_SPACE_ID, tests } = definition; + const { user, spaceId = SPACES.DEFAULT.spaceId, tests } = definition; describeFn(description, () => { before(() => esArchiver.load('saved_objects/spaces')); after(() => esArchiver.unload('saved_objects/spaces')); - it(`should return ${tests.default.statusCode}`, async () => { - const data = createImportData(spaceId); - await supertest - .post(`${getUrlPrefix(spaceId)}/api/saved_objects/_import`) - .auth(user.username, user.password) - .attach( - 'file', - Buffer.from(data.map(obj => JSON.stringify(obj)).join('\n'), 'utf8'), - 'export.ndjson' - ) - .expect(tests.default.statusCode) - .then(tests.default.response); - }); - - describe('hiddentype', () => { - it(`should return ${tests.hiddenType.statusCode}`, async () => { - const data = createImportData(spaceId); - data.push({ - type: 'hiddentype', - id: '1', - attributes: { - name: 'My Hidden Type', - }, - }); - await supertest - .post(`${getUrlPrefix(spaceId)}/api/saved_objects/_import`) - .query({ overwrite: true }) - .auth(user.username, user.password) - .attach( - 'file', - Buffer.from(data.map(obj => JSON.stringify(obj)).join('\n'), 'utf8'), - 'export.ndjson' - ) - .expect(tests.hiddenType.statusCode) - .then(tests.hiddenType.response); - }); - }); + const attrs = { attributes: { [NEW_ATTRIBUTE_KEY]: NEW_ATTRIBUTE_VAL } }; - describe('unknown type', () => { - it(`should return ${tests.unknownType.statusCode}`, async () => { - const data = createImportData(spaceId); - data.push({ - type: 'wigwags', - id: '1', - attributes: { - title: 'Wigwags title', - }, - }); + for (const test of tests) { + it(`should return ${test.responseStatusCode} ${test.title}`, async () => { + const requestBody = test.request + .map(obj => JSON.stringify({ ...obj, ...attrs })) + .join('\n'); await supertest .post(`${getUrlPrefix(spaceId)}/api/saved_objects/_import`) - .query({ overwrite: true }) - .auth(user.username, user.password) - .attach( - 'file', - Buffer.from(data.map(obj => JSON.stringify(obj)).join('\n'), 'utf8'), - 'export.ndjson' - ) - .expect(tests.unknownType.statusCode) - .then(tests.unknownType.response); + .auth(user?.username, user?.password) + .attach('file', Buffer.from(requestBody, 'utf8'), 'export.ndjson') + .expect(test.responseStatusCode) + .then(test.responseBody); }); - }); + } }); }; - const importTest = makeImportTest(describe); + const addTests = makeImportTest(describe); // @ts-ignore - importTest.only = makeImportTest(describe.only); + addTests.only = makeImportTest(describe.only); return { - importTest, - createExpectResults, - expectResultsWithUnsupportedHiddenType, - expectRbacForbidden, - expectUnknownTypeUnsupported, - expectHiddenTypeUnsupported, + addTests, + createTestDefinitions, + expectForbidden, }; } diff --git a/x-pack/test/saved_object_api_integration/common/suites/resolve_import_errors.ts b/x-pack/test/saved_object_api_integration/common/suites/resolve_import_errors.ts index 1b538b9b1b65d..47c4babc5fcf9 100644 --- a/x-pack/test/saved_object_api_integration/common/suites/resolve_import_errors.ts +++ b/x-pack/test/saved_object_api_integration/common/suites/resolve_import_errors.ts @@ -6,219 +6,165 @@ import expect from '@kbn/expect'; import { SuperTest } from 'supertest'; -import { DEFAULT_SPACE_ID } from '../../../../plugins/spaces/common/constants'; -import { getIdPrefix, getUrlPrefix } from '../lib/space_test_utils'; -import { DescribeFn, TestDefinitionAuthentication } from '../lib/types'; +import { SAVED_OBJECT_TEST_CASES as CASES } from '../lib/saved_object_test_cases'; +import { SPACES } from '../lib/spaces'; +import { + createRequest, + expectResponses, + getUrlPrefix, + getTestTitle, +} from '../lib/saved_object_test_utils'; +import { ExpectResponseBody, TestCase, TestDefinition, TestSuite } from '../lib/types'; -interface ResolveImportErrorsTest { - statusCode: number; - response: (resp: { [key: string]: any }) => void; +export interface ResolveImportErrorsTestDefinition extends TestDefinition { + request: Array<{ type: string; id: string }>; + overwrite: boolean; } - -interface ResolveImportErrorsTests { - default: ResolveImportErrorsTest; - hiddenType: ResolveImportErrorsTest; - unknownType: ResolveImportErrorsTest; +export type ResolveImportErrorsTestSuite = TestSuite; +export interface ResolveImportErrorsTestCase extends TestCase { + failure?: 400 | 409; // only used for permitted response case } -interface ResolveImportErrorsTestDefinition { - user?: TestDefinitionAuthentication; - spaceId?: string; - tests: ResolveImportErrorsTests; -} +const NEW_ATTRIBUTE_KEY = 'title'; // all type mappings include this attribute, for simplicity's sake +const NEW_ATTRIBUTE_VAL = `New attribute value ${Date.now()}`; -const createImportData = (spaceId: string) => [ - { - type: 'dashboard', - id: `${getIdPrefix(spaceId)}a01b2f57-fcfd-4864-b735-09e28f0d815e`, - attributes: { - title: 'A great new dashboard', - }, - }, - { - type: 'globaltype', - id: '05976c65-1145-4858-bbf0-d225cc78a06e', - attributes: { - name: 'A new globaltype object', - }, - }, -]; +const NEW_SINGLE_NAMESPACE_OBJ = Object.freeze({ type: 'dashboard', id: 'new-dashboard-id' }); +const NEW_MULTI_NAMESPACE_OBJ = Object.freeze({ type: 'sharedtype', id: 'new-sharedtype-id' }); +const NEW_NAMESPACE_AGNOSTIC_OBJ = Object.freeze({ type: 'globaltype', id: 'new-globaltype-id' }); +export const TEST_CASES = Object.freeze({ + ...CASES, + NEW_SINGLE_NAMESPACE_OBJ, + NEW_MULTI_NAMESPACE_OBJ, + NEW_NAMESPACE_AGNOSTIC_OBJ, +}); export function resolveImportErrorsTestSuiteFactory( es: any, esArchiver: any, supertest: SuperTest ) { - const createExpectResults = (spaceId = DEFAULT_SPACE_ID) => async (resp: { - [key: string]: any; - }) => { - expect(resp.body).to.eql({ - success: true, - successCount: 1, - }); - }; - - const expectUnknownTypeUnsupported = (resp: { [key: string]: any }) => { - expect(resp.body).to.eql({ - success: false, - successCount: 1, - errors: [ - { - id: '1', - type: 'wigwags', - title: 'Wigwags title', - error: { - type: 'unsupported_type', - }, - }, - ], - }); - }; - - const expectHiddenTypeUnsupported = (resp: { [key: string]: any }) => { - expect(resp.body).to.eql({ - success: false, - successCount: 1, - errors: [ - { - id: '1', - type: 'hiddentype', - error: { - type: 'unsupported_type', - }, - }, - ], - }); + const expectForbidden = expectResponses.forbidden('bulk_create'); + const expectResponseBody = ( + testCases: ResolveImportErrorsTestCase | ResolveImportErrorsTestCase[], + statusCode: 200 | 403, + spaceId = SPACES.DEFAULT.spaceId + ): ExpectResponseBody => async (response: Record) => { + const testCaseArray = Array.isArray(testCases) ? testCases : [testCases]; + if (statusCode === 403) { + const types = testCaseArray.map(x => x.type); + await expectForbidden(types)(response); + } else { + // permitted + const { success, successCount, errors } = response.body; + const expectedSuccesses = testCaseArray.filter(x => !x.failure); + const expectedFailures = testCaseArray.filter(x => x.failure); + expect(success).to.eql(expectedFailures.length === 0); + expect(successCount).to.eql(expectedSuccesses.length); + if (expectedFailures.length) { + expect(errors).to.have.length(expectedFailures.length); + } else { + expect(response.body).not.to.have.property('errors'); + } + for (let i = 0; i < expectedSuccesses.length; i++) { + const { type, id } = expectedSuccesses[i]; + const { _source } = await expectResponses.successCreated(es, spaceId, type, id); + expect(_source[type][NEW_ATTRIBUTE_KEY]).to.eql(NEW_ATTRIBUTE_VAL); + } + for (let i = 0; i < expectedFailures.length; i++) { + const { type, id, failure } = expectedFailures[i]; + // we don't know the order of the returned errors; search for each one + const object = (errors as Array>).find( + x => x.type === type && x.id === id + ); + expect(object).not.to.be(undefined); + if (failure === 400) { + expect(object!.error).to.eql({ type: 'unsupported_type' }); + } else { + // 409 + expect(object!.error).to.eql({ type: 'conflict' }); + } + } + } }; - - const expectRbacForbidden = (resp: { [key: string]: any }) => { - expect(resp.body).to.eql({ - statusCode: 403, - error: 'Forbidden', - message: `Unable to bulk_create dashboard`, - }); + const createTestDefinitions = ( + testCases: ResolveImportErrorsTestCase | ResolveImportErrorsTestCase[], + forbidden: boolean, + overwrite: boolean, + options?: { + spaceId?: string; + singleRequest?: boolean; + responseBodyOverride?: ExpectResponseBody; + } + ): ResolveImportErrorsTestDefinition[] => { + const cases = Array.isArray(testCases) ? testCases : [testCases]; + const responseStatusCode = forbidden ? 403 : 200; + if (!options?.singleRequest) { + // if we are testing cases that should result in a forbidden response, we can do each case individually + // this ensures that multiple test cases of a single type will each result in a forbidden error + return cases.map(x => ({ + title: getTestTitle(x, responseStatusCode), + request: [createRequest(x)], + responseStatusCode, + responseBody: + options?.responseBodyOverride || + expectResponseBody(x, responseStatusCode, options?.spaceId), + overwrite, + })); + } + // batch into a single request to save time during test execution + return [ + { + title: getTestTitle(cases, responseStatusCode), + request: cases.map(x => createRequest(x)), + responseStatusCode, + responseBody: + options?.responseBodyOverride || + expectResponseBody(cases, responseStatusCode, options?.spaceId), + overwrite, + }, + ]; }; - const makeResolveImportErrorsTest = (describeFn: DescribeFn) => ( + const makeResolveImportErrorsTest = (describeFn: Mocha.SuiteFunction) => ( description: string, - definition: ResolveImportErrorsTestDefinition + definition: ResolveImportErrorsTestSuite ) => { - const { user = {}, spaceId = DEFAULT_SPACE_ID, tests } = definition; + const { user, spaceId = SPACES.DEFAULT.spaceId, tests } = definition; describeFn(description, () => { before(() => esArchiver.load('saved_objects/spaces')); after(() => esArchiver.unload('saved_objects/spaces')); - it(`should return ${tests.default.statusCode}`, async () => { - const data = createImportData(spaceId); - await supertest - .post(`${getUrlPrefix(spaceId)}/api/saved_objects/_resolve_import_errors`) - .auth(user.username, user.password) - .field( - 'retries', - JSON.stringify([ - { - type: 'dashboard', - id: `${getIdPrefix(spaceId)}a01b2f57-fcfd-4864-b735-09e28f0d815e`, - overwrite: true, - }, - ]) - ) - .attach( - 'file', - Buffer.from(data.map(obj => JSON.stringify(obj)).join('\n'), 'utf8'), - 'export.ndjson' - ) - .expect(tests.default.statusCode) - .then(tests.default.response); - }); + const attrs = { attributes: { [NEW_ATTRIBUTE_KEY]: NEW_ATTRIBUTE_VAL } }; - describe('unknown type', () => { - it(`should return ${tests.unknownType.statusCode}`, async () => { - const data = createImportData(spaceId); - data.push({ - type: 'wigwags', - id: '1', - attributes: { - title: 'Wigwags title', - }, - }); - await supertest - .post(`${getUrlPrefix(spaceId)}/api/saved_objects/_resolve_import_errors`) - .auth(user.username, user.password) - .field( - 'retries', - JSON.stringify([ - { - type: 'wigwags', - id: '1', - overwrite: true, - }, - { - type: 'dashboard', - id: `${getIdPrefix(spaceId)}a01b2f57-fcfd-4864-b735-09e28f0d815e`, - overwrite: true, - }, - ]) - ) - .attach( - 'file', - Buffer.from(data.map(obj => JSON.stringify(obj)).join('\n'), 'utf8'), - 'export.ndjson' - ) - .expect(tests.unknownType.statusCode) - .then(tests.unknownType.response); - }); - }); - describe('hidden type', () => { - it(`should return ${tests.hiddenType.statusCode}`, async () => { - const data = createImportData(spaceId); - data.push({ - type: 'hiddentype', - id: '1', - attributes: { - name: 'My Hidden Type', - }, - }); + for (const test of tests) { + it(`should return ${test.responseStatusCode} ${test.title}`, async () => { + const retryAttrs = test.overwrite ? { overwrite: true } : {}; + const retries = JSON.stringify( + test.request.map(({ type, id }) => ({ type, id, ...retryAttrs })) + ); + const requestBody = test.request + .map(obj => JSON.stringify({ ...obj, ...attrs })) + .join('\n'); await supertest .post(`${getUrlPrefix(spaceId)}/api/saved_objects/_resolve_import_errors`) - .auth(user.username, user.password) - .field( - 'retries', - JSON.stringify([ - { - type: 'hiddentype', - id: '1', - overwrite: true, - }, - { - type: 'dashboard', - id: `${getIdPrefix(spaceId)}a01b2f57-fcfd-4864-b735-09e28f0d815e`, - overwrite: true, - }, - ]) - ) - .attach( - 'file', - Buffer.from(data.map(obj => JSON.stringify(obj)).join('\n'), 'utf8'), - 'export.ndjson' - ) - .expect(tests.hiddenType.statusCode) - .then(tests.hiddenType.response); + .auth(user?.username, user?.password) + .field('retries', retries) + .attach('file', Buffer.from(requestBody, 'utf8'), 'export.ndjson') + .expect(test.responseStatusCode) + .then(test.responseBody); }); - }); + } }); }; - const resolveImportErrorsTest = makeResolveImportErrorsTest(describe); + const addTests = makeResolveImportErrorsTest(describe); // @ts-ignore - resolveImportErrorsTest.only = makeResolveImportErrorsTest(describe.only); + addTests.only = makeResolveImportErrorsTest(describe.only); return { - resolveImportErrorsTest, - createExpectResults, - expectRbacForbidden, - expectUnknownTypeUnsupported, - expectHiddenTypeUnsupported, + addTests, + createTestDefinitions, + expectForbidden, }; } diff --git a/x-pack/test/saved_object_api_integration/common/suites/update.ts b/x-pack/test/saved_object_api_integration/common/suites/update.ts index d6b7602c0114a..587e8cf320a4f 100644 --- a/x-pack/test/saved_object_api_integration/common/suites/update.ts +++ b/x-pack/test/saved_object_api_integration/common/suites/update.ts @@ -6,205 +6,97 @@ import expect from '@kbn/expect'; import { SuperTest } from 'supertest'; -import { DEFAULT_SPACE_ID } from '../../../../plugins/spaces/common/constants'; -import { getIdPrefix, getUrlPrefix } from '../lib/space_test_utils'; -import { DescribeFn, TestDefinitionAuthentication } from '../lib/types'; - -interface UpdateTest { - statusCode: number; - response: (resp: { [key: string]: any }) => void; -} - -interface UpdateTests { - spaceAware: UpdateTest; - notSpaceAware: UpdateTest; - hiddenType: UpdateTest; - doesntExist: UpdateTest; +import { SAVED_OBJECT_TEST_CASES as CASES } from '../lib/saved_object_test_cases'; +import { SPACES } from '../lib/spaces'; +import { + createRequest, + expectResponses, + getUrlPrefix, + getTestTitle, +} from '../lib/saved_object_test_utils'; +import { ExpectResponseBody, TestCase, TestDefinition, TestSuite } from '../lib/types'; + +export interface UpdateTestDefinition extends TestDefinition { + request: { type: string; id: string }; } - -interface UpdateTestDefinition { - user?: TestDefinitionAuthentication; - spaceId?: string; - otherSpaceId?: string; - tests: UpdateTests; +export type UpdateTestSuite = TestSuite; +export interface UpdateTestCase extends TestCase { + failure?: 403 | 404; } -export function updateTestSuiteFactory(esArchiver: any, supertest: SuperTest) { - const createExpectNotFound = (type: string, id: string, spaceId = DEFAULT_SPACE_ID) => (resp: { - [key: string]: any; - }) => { - expect(resp.body).eql({ - statusCode: 404, - error: 'Not Found', - message: `Saved object [${type}/${getIdPrefix(spaceId)}${id}] not found`, - }); - }; - - const createExpectDoesntExistNotFound = (spaceId?: string) => { - return createExpectNotFound('visualization', 'not an id', spaceId); - }; - - const createExpectSpaceAwareNotFound = (spaceId?: string) => { - return createExpectNotFound('visualization', 'dd7caf20-9efd-11e7-acb3-3dab96693fab', spaceId); - }; - - const expectHiddenTypeNotFound = createExpectNotFound( - 'hiddentype', - 'hiddentype_1', - DEFAULT_SPACE_ID - ); - - const createExpectRbacForbidden = (type: string) => (resp: { [key: string]: any }) => { - expect(resp.body).to.eql({ - statusCode: 403, - error: 'Forbidden', - message: `Unable to update ${type}`, - }); - }; +const NEW_ATTRIBUTE_KEY = 'title'; // all type mappings include this attribute, for simplicity's sake +const NEW_ATTRIBUTE_VAL = `Updated attribute value ${Date.now()}`; - const expectDoesntExistRbacForbidden = createExpectRbacForbidden('visualization'); +const DOES_NOT_EXIST = Object.freeze({ type: 'dashboard', id: 'does-not-exist' }); +export const TEST_CASES = Object.freeze({ ...CASES, DOES_NOT_EXIST }); - const expectNotSpaceAwareRbacForbidden = createExpectRbacForbidden('globaltype'); - - const expectHiddenTypeRbacForbidden = createExpectRbacForbidden('hiddentype'); - - const expectNotSpaceAwareResults = (resp: { [key: string]: any }) => { - // loose uuid validation - expect(resp.body) - .to.have.property('id') - .match(/^[0-9a-f-]{36}$/); - - // loose ISO8601 UTC time with milliseconds validation - expect(resp.body) - .to.have.property('updated_at') - .match(/^[\d-]{10}T[\d:\.]{12}Z$/); - - expect(resp.body).to.eql({ - id: resp.body.id, - type: 'globaltype', - updated_at: resp.body.updated_at, - version: resp.body.version, - attributes: { - name: 'My second favorite', - }, - }); +export function updateTestSuiteFactory(esArchiver: any, supertest: SuperTest) { + const expectForbidden = expectResponses.forbidden('update'); + const expectResponseBody = (testCase: UpdateTestCase): ExpectResponseBody => async ( + response: Record + ) => { + if (testCase.failure === 403) { + await expectForbidden(testCase.type)(response); + } else { + // permitted + const object = response.body; + await expectResponses.permitted(object, testCase); + if (!testCase.failure) { + expect(object.attributes[NEW_ATTRIBUTE_KEY]).to.eql(NEW_ATTRIBUTE_VAL); + } + } }; - - const expectSpaceAwareRbacForbidden = createExpectRbacForbidden('visualization'); - - const expectSpaceAwareResults = (resp: { [key: string]: any }) => { - // loose uuid validation ignoring prefix - expect(resp.body) - .to.have.property('id') - .match(/[0-9a-f-]{36}$/); - - // loose ISO8601 UTC time with milliseconds validation - expect(resp.body) - .to.have.property('updated_at') - .match(/^[\d-]{10}T[\d:\.]{12}Z$/); - - expect(resp.body).to.eql({ - id: resp.body.id, - type: 'visualization', - updated_at: resp.body.updated_at, - version: resp.body.version, - attributes: { - title: 'My second favorite vis', - }, - }); + const createTestDefinitions = ( + testCases: UpdateTestCase | UpdateTestCase[], + forbidden: boolean, + options?: { + responseBodyOverride?: ExpectResponseBody; + } + ): UpdateTestDefinition[] => { + let cases = Array.isArray(testCases) ? testCases : [testCases]; + if (forbidden) { + // override the expected result in each test case + cases = cases.map(x => ({ ...x, failure: 403 })); + } + return cases.map(x => ({ + title: getTestTitle(x), + responseStatusCode: x.failure ?? 200, + request: createRequest(x), + responseBody: options?.responseBodyOverride || expectResponseBody(x), + })); }; - const makeUpdateTest = (describeFn: DescribeFn) => ( + const makeUpdateTest = (describeFn: Mocha.SuiteFunction) => ( description: string, - definition: UpdateTestDefinition + definition: UpdateTestSuite ) => { - const { user = {}, spaceId = DEFAULT_SPACE_ID, otherSpaceId, tests } = definition; + const { user, spaceId = SPACES.DEFAULT.spaceId, tests } = definition; describeFn(description, () => { before(() => esArchiver.load('saved_objects/spaces')); after(() => esArchiver.unload('saved_objects/spaces')); - it(`should return ${tests.spaceAware.statusCode} for a space-aware doc`, async () => { - await supertest - .put( - `${getUrlPrefix(spaceId)}/api/saved_objects/visualization/${getIdPrefix( - otherSpaceId || spaceId - )}dd7caf20-9efd-11e7-acb3-3dab96693fab` - ) - .auth(user.username, user.password) - .send({ - attributes: { - title: 'My second favorite vis', - }, - }) - .expect(tests.spaceAware.statusCode) - .then(tests.spaceAware.response); - }); - - it(`should return ${tests.notSpaceAware.statusCode} for a non space-aware doc`, async () => { - await supertest - .put( - `${getUrlPrefix( - otherSpaceId || spaceId - )}/api/saved_objects/globaltype/8121a00-8efd-21e7-1cb3-34ab966434445` - ) - .auth(user.username, user.password) - .send({ - attributes: { - name: 'My second favorite', - }, - }) - .expect(tests.notSpaceAware.statusCode) - .then(tests.notSpaceAware.response); - }); - - it(`should return ${tests.hiddenType.statusCode} for hiddentype doc`, async () => { - await supertest - .put(`${getUrlPrefix(otherSpaceId || spaceId)}/api/saved_objects/hiddentype/hiddentype_1`) - .auth(user.username, user.password) - .send({ - attributes: { - name: 'My favorite hidden type', - }, - }) - .expect(tests.hiddenType.statusCode) - .then(tests.hiddenType.response); - }); - describe('unknown id', () => { - it(`should return ${tests.doesntExist.statusCode}`, async () => { + for (const test of tests) { + it(`should return ${test.responseStatusCode} ${test.title}`, async () => { + const { type, id } = test.request; + const requestBody = { attributes: { [NEW_ATTRIBUTE_KEY]: NEW_ATTRIBUTE_VAL } }; await supertest - .put( - `${getUrlPrefix(spaceId)}/api/saved_objects/visualization/${getIdPrefix( - spaceId - )}not an id` - ) - .auth(user.username, user.password) - .send({ - attributes: { - title: 'My second favorite vis', - }, - }) - .expect(tests.doesntExist.statusCode) - .then(tests.doesntExist.response); + .put(`${getUrlPrefix(spaceId)}/api/saved_objects/${type}/${id}`) + .auth(user?.username, user?.password) + .send(requestBody) + .expect(test.responseStatusCode) + .then(test.responseBody); }); - }); + } }); }; - const updateTest = makeUpdateTest(describe); + const addTests = makeUpdateTest(describe); // @ts-ignore - updateTest.only = makeUpdateTest(describe.only); + addTests.only = makeUpdateTest(describe.only); return { - createExpectDoesntExistNotFound, - createExpectSpaceAwareNotFound, - expectSpaceNotFound: expectHiddenTypeNotFound, - expectDoesntExistRbacForbidden, - expectNotSpaceAwareRbacForbidden, - expectNotSpaceAwareResults, - expectSpaceAwareRbacForbidden, - expectSpaceAwareResults, - expectHiddenTypeRbacForbidden, - updateTest, + addTests, + createTestDefinitions, }; } diff --git a/x-pack/test/saved_object_api_integration/security_and_spaces/apis/bulk_create.ts b/x-pack/test/saved_object_api_integration/security_and_spaces/apis/bulk_create.ts index 7768665f3b941..70d3afbfc9af3 100644 --- a/x-pack/test/saved_object_api_integration/security_and_spaces/apis/bulk_create.ts +++ b/x-pack/test/saved_object_api_integration/security_and_spaces/apis/bulk_create.ts @@ -4,206 +4,104 @@ * you may not use this file except in compliance with the Elastic License. */ -import { AUTHENTICATION } from '../../common/lib/authentication'; import { SPACES } from '../../common/lib/spaces'; +import { testCaseFailures, getTestScenarios } from '../../common/lib/saved_object_test_utils'; +import { TestUser } from '../../common/lib/types'; import { FtrProviderContext } from '../../common/ftr_provider_context'; -import { bulkCreateTestSuiteFactory } from '../../common/suites/bulk_create'; +import { + bulkCreateTestSuiteFactory, + TEST_CASES as CASES, + BulkCreateTestDefinition, +} from '../../common/suites/bulk_create'; + +const { + DEFAULT: { spaceId: DEFAULT_SPACE_ID }, + SPACE_1: { spaceId: SPACE_1_ID }, + SPACE_2: { spaceId: SPACE_2_ID }, +} = SPACES; +const { fail400, fail409 } = testCaseFailures; + +const createTestCases = (overwrite: boolean, spaceId: string) => { + // for each permitted (non-403) outcome, if failure !== undefined then we expect + // to receive an error; otherwise, we expect to receive a success result + const normalTypes = [ + { + ...CASES.SINGLE_NAMESPACE_DEFAULT_SPACE, + ...fail409(!overwrite && spaceId === DEFAULT_SPACE_ID), + }, + { ...CASES.SINGLE_NAMESPACE_SPACE_1, ...fail409(!overwrite && spaceId === SPACE_1_ID) }, + { ...CASES.SINGLE_NAMESPACE_SPACE_2, ...fail409(!overwrite && spaceId === SPACE_2_ID) }, + { + ...CASES.MULTI_NAMESPACE_DEFAULT_AND_SPACE_1, + ...fail409(!overwrite || (spaceId !== DEFAULT_SPACE_ID && spaceId !== SPACE_1_ID)), + }, + { ...CASES.MULTI_NAMESPACE_ONLY_SPACE_1, ...fail409(!overwrite || spaceId !== SPACE_1_ID) }, + { ...CASES.MULTI_NAMESPACE_ONLY_SPACE_2, ...fail409(!overwrite || spaceId !== SPACE_2_ID) }, + { ...CASES.NAMESPACE_AGNOSTIC, ...fail409(!overwrite) }, + CASES.NEW_SINGLE_NAMESPACE_OBJ, + CASES.NEW_MULTI_NAMESPACE_OBJ, + CASES.NEW_NAMESPACE_AGNOSTIC_OBJ, + ]; + const hiddenType = [{ ...CASES.HIDDEN, ...fail400() }]; + const allTypes = normalTypes.concat(hiddenType); + return { normalTypes, hiddenType, allTypes }; +}; export default function({ getService }: FtrProviderContext) { const supertest = getService('supertestWithoutAuth'); const esArchiver = getService('esArchiver'); const es = getService('legacyEs'); - const { - bulkCreateTest, - createExpectResults, - createExpectRbacForbidden, - expectBadRequestForHiddenType, - expectedForbiddenTypesWithHiddenType, - } = bulkCreateTestSuiteFactory(es, esArchiver, supertest); + const { addTests, createTestDefinitions, expectForbidden } = bulkCreateTestSuiteFactory( + es, + esArchiver, + supertest + ); + const createTests = (overwrite: boolean, spaceId: string) => { + const { normalTypes, hiddenType, allTypes } = createTestCases(overwrite, spaceId); + // use singleRequest to reduce execution time and/or test combined cases + return { + unauthorized: createTestDefinitions(allTypes, true, overwrite, { spaceId }), + authorized: [ + createTestDefinitions(normalTypes, false, overwrite, { spaceId, singleRequest: true }), + createTestDefinitions(hiddenType, true, overwrite, { spaceId }), + createTestDefinitions(allTypes, true, overwrite, { + spaceId, + singleRequest: true, + responseBodyOverride: expectForbidden(['hiddentype']), + }), + ].flat(), + superuser: createTestDefinitions(allTypes, false, overwrite, { + spaceId, + singleRequest: true, + }), + }; + }; describe('_bulk_create', () => { - [ - { - spaceId: SPACES.DEFAULT.spaceId, - users: { - noAccess: AUTHENTICATION.NOT_A_KIBANA_USER, - superuser: AUTHENTICATION.SUPERUSER, - legacyAll: AUTHENTICATION.KIBANA_LEGACY_USER, - allGlobally: AUTHENTICATION.KIBANA_RBAC_USER, - readGlobally: AUTHENTICATION.KIBANA_RBAC_DASHBOARD_ONLY_USER, - dualAll: AUTHENTICATION.KIBANA_DUAL_PRIVILEGES_USER, - dualRead: AUTHENTICATION.KIBANA_DUAL_PRIVILEGES_DASHBOARD_ONLY_USER, - allAtSpace: AUTHENTICATION.KIBANA_RBAC_DEFAULT_SPACE_ALL_USER, - readAtSpace: AUTHENTICATION.KIBANA_RBAC_DEFAULT_SPACE_READ_USER, - allAtOtherSpace: AUTHENTICATION.KIBANA_RBAC_SPACE_1_ALL_USER, - }, - }, - { - spaceId: SPACES.SPACE_1.spaceId, - users: { - noAccess: AUTHENTICATION.NOT_A_KIBANA_USER, - superuser: AUTHENTICATION.SUPERUSER, - legacyAll: AUTHENTICATION.KIBANA_LEGACY_USER, - allGlobally: AUTHENTICATION.KIBANA_RBAC_USER, - readGlobally: AUTHENTICATION.KIBANA_RBAC_DASHBOARD_ONLY_USER, - dualAll: AUTHENTICATION.KIBANA_DUAL_PRIVILEGES_USER, - dualRead: AUTHENTICATION.KIBANA_DUAL_PRIVILEGES_DASHBOARD_ONLY_USER, - allAtSpace: AUTHENTICATION.KIBANA_RBAC_SPACE_1_ALL_USER, - readAtSpace: AUTHENTICATION.KIBANA_RBAC_SPACE_1_READ_USER, - allAtOtherSpace: AUTHENTICATION.KIBANA_RBAC_DEFAULT_SPACE_ALL_USER, - }, - }, - ].forEach(scenario => { - bulkCreateTest(`user with no access within the ${scenario.spaceId} space`, { - user: scenario.users.noAccess, - spaceId: scenario.spaceId, - tests: { - default: { - statusCode: 403, - response: createExpectRbacForbidden(), - }, - includingSpace: { - statusCode: 403, - response: createExpectRbacForbidden(expectedForbiddenTypesWithHiddenType), - }, - }, - }); - - bulkCreateTest(`superuser within the ${scenario.spaceId} space`, { - user: scenario.users.superuser, - spaceId: scenario.spaceId, - tests: { - default: { - statusCode: 200, - response: createExpectResults(scenario.spaceId), - }, - includingSpace: { - statusCode: 200, - response: expectBadRequestForHiddenType, - }, - }, - }); - - bulkCreateTest(`legacy user within the ${scenario.spaceId} space`, { - user: scenario.users.legacyAll, - spaceId: scenario.spaceId, - tests: { - default: { - statusCode: 403, - response: createExpectRbacForbidden(), - }, - includingSpace: { - statusCode: 403, - response: createExpectRbacForbidden(expectedForbiddenTypesWithHiddenType), - }, - }, - }); - - bulkCreateTest(`dual-privileges user within the ${scenario.spaceId} space`, { - user: scenario.users.dualAll, - spaceId: scenario.spaceId, - tests: { - default: { - statusCode: 200, - response: createExpectResults(scenario.spaceId), - }, - includingSpace: { - statusCode: 403, - response: createExpectRbacForbidden(['hiddentype']), - }, - }, - }); - - bulkCreateTest(`dual-privileges readonly user within the ${scenario.spaceId} space`, { - user: scenario.users.dualRead, - spaceId: scenario.spaceId, - tests: { - default: { - statusCode: 403, - response: createExpectRbacForbidden(), - }, - includingSpace: { - statusCode: 403, - response: createExpectRbacForbidden(expectedForbiddenTypesWithHiddenType), - }, - }, - }); - - bulkCreateTest(`rbac user with all globally within the ${scenario.spaceId} space`, { - user: scenario.users.allGlobally, - spaceId: scenario.spaceId, - tests: { - default: { - statusCode: 200, - response: createExpectResults(scenario.spaceId), - }, - includingSpace: { - statusCode: 403, - response: createExpectRbacForbidden(['hiddentype']), - }, - }, - }); - - bulkCreateTest(`rbac user with read globally within the ${scenario.spaceId} space`, { - user: scenario.users.readGlobally, - spaceId: scenario.spaceId, - tests: { - default: { - statusCode: 403, - response: createExpectRbacForbidden(), - }, - includingSpace: { - statusCode: 403, - response: createExpectRbacForbidden(expectedForbiddenTypesWithHiddenType), - }, - }, - }); - - bulkCreateTest(`rbac user with all at the space within the ${scenario.spaceId} space`, { - user: scenario.users.allAtSpace, - spaceId: scenario.spaceId, - tests: { - default: { - statusCode: 200, - response: createExpectResults(scenario.spaceId), - }, - includingSpace: { - statusCode: 403, - response: createExpectRbacForbidden(['hiddentype']), - }, - }, - }); - - bulkCreateTest(`rbac user with read at the space within the ${scenario.spaceId} space`, { - user: scenario.users.readAtSpace, - spaceId: scenario.spaceId, - tests: { - default: { - statusCode: 403, - response: createExpectRbacForbidden(), - }, - includingSpace: { - statusCode: 403, - response: createExpectRbacForbidden(expectedForbiddenTypesWithHiddenType), - }, - }, - }); + getTestScenarios([false, true]).securityAndSpaces.forEach( + ({ spaceId, users, modifier: overwrite }) => { + const suffix = ` within the ${spaceId} space${overwrite ? ' with overwrite enabled' : ''}`; + const { unauthorized, authorized, superuser } = createTests(overwrite!, spaceId); + const _addTests = (user: TestUser, tests: BulkCreateTestDefinition[]) => { + addTests(`${user.description}${suffix}`, { user, spaceId, tests }); + }; - bulkCreateTest(`rbac user with all at other space within the ${scenario.spaceId} space`, { - user: scenario.users.allAtOtherSpace, - spaceId: scenario.spaceId, - tests: { - default: { - statusCode: 403, - response: createExpectRbacForbidden(), - }, - includingSpace: { - statusCode: 403, - response: createExpectRbacForbidden(expectedForbiddenTypesWithHiddenType), - }, - }, - }); - }); + [ + users.noAccess, + users.legacyAll, + users.dualRead, + users.readGlobally, + users.readAtSpace, + users.allAtOtherSpace, + ].forEach(user => { + _addTests(user, unauthorized); + }); + [users.dualAll, users.allGlobally, users.allAtSpace].forEach(user => { + _addTests(user, authorized); + }); + _addTests(users.superuser, superuser); + } + ); }); } diff --git a/x-pack/test/saved_object_api_integration/security_and_spaces/apis/bulk_get.ts b/x-pack/test/saved_object_api_integration/security_and_spaces/apis/bulk_get.ts index ec5bce1707569..09ea867bff371 100644 --- a/x-pack/test/saved_object_api_integration/security_and_spaces/apis/bulk_get.ts +++ b/x-pack/test/saved_object_api_integration/security_and_spaces/apis/bulk_get.ts @@ -4,205 +4,91 @@ * you may not use this file except in compliance with the Elastic License. */ -import { AUTHENTICATION } from '../../common/lib/authentication'; import { SPACES } from '../../common/lib/spaces'; +import { testCaseFailures, getTestScenarios } from '../../common/lib/saved_object_test_utils'; +import { TestUser } from '../../common/lib/types'; import { FtrProviderContext } from '../../common/ftr_provider_context'; -import { bulkGetTestSuiteFactory } from '../../common/suites/bulk_get'; +import { + bulkGetTestSuiteFactory, + TEST_CASES as CASES, + BulkGetTestDefinition, +} from '../../common/suites/bulk_get'; + +const { + DEFAULT: { spaceId: DEFAULT_SPACE_ID }, + SPACE_1: { spaceId: SPACE_1_ID }, + SPACE_2: { spaceId: SPACE_2_ID }, +} = SPACES; +const { fail400, fail404 } = testCaseFailures; + +const createTestCases = (spaceId: string) => { + // for each permitted (non-403) outcome, if failure !== undefined then we expect + // to receive an error; otherwise, we expect to receive a success result + const normalTypes = [ + { ...CASES.SINGLE_NAMESPACE_DEFAULT_SPACE, ...fail404(spaceId !== DEFAULT_SPACE_ID) }, + { ...CASES.SINGLE_NAMESPACE_SPACE_1, ...fail404(spaceId !== SPACE_1_ID) }, + { ...CASES.SINGLE_NAMESPACE_SPACE_2, ...fail404(spaceId !== SPACE_2_ID) }, + { + ...CASES.MULTI_NAMESPACE_DEFAULT_AND_SPACE_1, + ...fail404(spaceId !== DEFAULT_SPACE_ID && spaceId !== SPACE_1_ID), + }, + { ...CASES.MULTI_NAMESPACE_ONLY_SPACE_1, ...fail404(spaceId !== SPACE_1_ID) }, + { ...CASES.MULTI_NAMESPACE_ONLY_SPACE_2, ...fail404(spaceId !== SPACE_2_ID) }, + CASES.NAMESPACE_AGNOSTIC, + { ...CASES.DOES_NOT_EXIST, ...fail404() }, + ]; + const hiddenType = [{ ...CASES.HIDDEN, ...fail400() }]; + const allTypes = normalTypes.concat(hiddenType); + return { normalTypes, hiddenType, allTypes }; +}; export default function({ getService }: FtrProviderContext) { const supertest = getService('supertestWithoutAuth'); const esArchiver = getService('esArchiver'); - const { - bulkGetTest, - createExpectResults, - createExpectRbacForbidden, - expectBadRequestForHiddenType, - expectedForbiddenTypesWithHiddenType, - } = bulkGetTestSuiteFactory(esArchiver, supertest); + const { addTests, createTestDefinitions, expectForbidden } = bulkGetTestSuiteFactory( + esArchiver, + supertest + ); + const createTests = (spaceId: string) => { + const { normalTypes, hiddenType, allTypes } = createTestCases(spaceId); + // use singleRequest to reduce execution time and/or test combined cases + return { + unauthorized: createTestDefinitions(allTypes, true), + authorized: [ + createTestDefinitions(normalTypes, false, { singleRequest: true }), + createTestDefinitions(hiddenType, true), + createTestDefinitions(allTypes, true, { + singleRequest: true, + responseBodyOverride: expectForbidden(['hiddentype']), + }), + ].flat(), + superuser: createTestDefinitions(allTypes, false, { singleRequest: true }), + }; + }; describe('_bulk_get', () => { - [ - { - spaceId: SPACES.DEFAULT.spaceId, - users: { - noAccess: AUTHENTICATION.NOT_A_KIBANA_USER, - superuser: AUTHENTICATION.SUPERUSER, - legacyAll: AUTHENTICATION.KIBANA_LEGACY_USER, - allGlobally: AUTHENTICATION.KIBANA_RBAC_USER, - readGlobally: AUTHENTICATION.KIBANA_RBAC_DASHBOARD_ONLY_USER, - dualAll: AUTHENTICATION.KIBANA_DUAL_PRIVILEGES_USER, - dualRead: AUTHENTICATION.KIBANA_DUAL_PRIVILEGES_DASHBOARD_ONLY_USER, - allAtSpace: AUTHENTICATION.KIBANA_RBAC_DEFAULT_SPACE_ALL_USER, - readAtSpace: AUTHENTICATION.KIBANA_RBAC_DEFAULT_SPACE_READ_USER, - allAtOtherSpace: AUTHENTICATION.KIBANA_RBAC_SPACE_1_ALL_USER, - }, - }, - { - spaceId: SPACES.SPACE_1.spaceId, - users: { - noAccess: AUTHENTICATION.NOT_A_KIBANA_USER, - superuser: AUTHENTICATION.SUPERUSER, - legacyAll: AUTHENTICATION.KIBANA_LEGACY_USER, - allGlobally: AUTHENTICATION.KIBANA_RBAC_USER, - readGlobally: AUTHENTICATION.KIBANA_RBAC_DASHBOARD_ONLY_USER, - dualAll: AUTHENTICATION.KIBANA_DUAL_PRIVILEGES_USER, - dualRead: AUTHENTICATION.KIBANA_DUAL_PRIVILEGES_DASHBOARD_ONLY_USER, - allAtSpace: AUTHENTICATION.KIBANA_RBAC_SPACE_1_ALL_USER, - readAtSpace: AUTHENTICATION.KIBANA_RBAC_SPACE_1_READ_USER, - allAtOtherSpace: AUTHENTICATION.KIBANA_RBAC_DEFAULT_SPACE_ALL_USER, - }, - }, - ].forEach(scenario => { - bulkGetTest(`user with no access within the ${scenario.spaceId} space`, { - user: scenario.users.noAccess, - spaceId: scenario.spaceId, - tests: { - default: { - statusCode: 403, - response: createExpectRbacForbidden(), - }, - includingHiddenType: { - statusCode: 403, - response: createExpectRbacForbidden(expectedForbiddenTypesWithHiddenType), - }, - }, - }); - - bulkGetTest(`superuser within the ${scenario.spaceId} space`, { - user: scenario.users.superuser, - spaceId: scenario.spaceId, - tests: { - default: { - statusCode: 200, - response: createExpectResults(scenario.spaceId), - }, - includingHiddenType: { - statusCode: 200, - response: expectBadRequestForHiddenType, - }, - }, - }); - - bulkGetTest(`legacy user within the ${scenario.spaceId} space`, { - user: scenario.users.legacyAll, - spaceId: scenario.spaceId, - tests: { - default: { - statusCode: 403, - response: createExpectRbacForbidden(), - }, - includingHiddenType: { - statusCode: 403, - response: createExpectRbacForbidden(expectedForbiddenTypesWithHiddenType), - }, - }, - }); - - bulkGetTest(`dual-privileges user within the ${scenario.spaceId} space`, { - user: scenario.users.dualAll, - spaceId: scenario.spaceId, - tests: { - default: { - statusCode: 200, - response: createExpectResults(scenario.spaceId), - }, - includingHiddenType: { - statusCode: 403, - response: createExpectRbacForbidden(['hiddentype']), - }, - }, - }); + getTestScenarios().securityAndSpaces.forEach(({ spaceId, users }) => { + const suffix = ` within the ${spaceId} space`; + const { unauthorized, authorized, superuser } = createTests(spaceId); + const _addTests = (user: TestUser, tests: BulkGetTestDefinition[]) => { + addTests(`${user.description}${suffix}`, { user, spaceId, tests }); + }; - bulkGetTest(`dual-privileges readonly user within the ${scenario.spaceId} space`, { - user: scenario.users.dualRead, - spaceId: scenario.spaceId, - tests: { - default: { - statusCode: 200, - response: createExpectResults(scenario.spaceId), - }, - includingHiddenType: { - statusCode: 403, - response: createExpectRbacForbidden(['hiddentype']), - }, - }, + [users.noAccess, users.legacyAll, users.allAtOtherSpace].forEach(user => { + _addTests(user, unauthorized); }); - - bulkGetTest(`rbac user with all globally within the ${scenario.spaceId} space`, { - user: scenario.users.allGlobally, - spaceId: scenario.spaceId, - tests: { - default: { - statusCode: 200, - response: createExpectResults(scenario.spaceId), - }, - includingHiddenType: { - statusCode: 403, - response: createExpectRbacForbidden(['hiddentype']), - }, - }, - }); - - bulkGetTest(`rbac user with read globally within the ${scenario.spaceId} space`, { - user: scenario.users.readGlobally, - spaceId: scenario.spaceId, - tests: { - default: { - statusCode: 200, - response: createExpectResults(scenario.spaceId), - }, - includingHiddenType: { - statusCode: 403, - response: createExpectRbacForbidden(['hiddentype']), - }, - }, - }); - - bulkGetTest(`rbac user with all at the space within the ${scenario.spaceId} space`, { - user: scenario.users.allAtSpace, - spaceId: scenario.spaceId, - tests: { - default: { - statusCode: 200, - response: createExpectResults(scenario.spaceId), - }, - includingHiddenType: { - statusCode: 403, - response: createExpectRbacForbidden(['hiddentype']), - }, - }, - }); - - bulkGetTest(`rbac user with read at the space within the ${scenario.spaceId} space`, { - user: scenario.users.readAtSpace, - spaceId: scenario.spaceId, - tests: { - default: { - statusCode: 200, - response: createExpectResults(scenario.spaceId), - }, - includingHiddenType: { - statusCode: 403, - response: createExpectRbacForbidden(['hiddentype']), - }, - }, - }); - - bulkGetTest(`rbac user with all at other space within the ${scenario.spaceId} space`, { - user: scenario.users.allAtOtherSpace, - spaceId: scenario.spaceId, - tests: { - default: { - statusCode: 403, - response: createExpectRbacForbidden(), - }, - includingHiddenType: { - statusCode: 403, - response: createExpectRbacForbidden(expectedForbiddenTypesWithHiddenType), - }, - }, + [ + users.dualAll, + users.dualRead, + users.allGlobally, + users.readGlobally, + users.allAtSpace, + users.readAtSpace, + ].forEach(user => { + _addTests(user, authorized); }); + _addTests(users.superuser, superuser); }); }); } diff --git a/x-pack/test/saved_object_api_integration/security_and_spaces/apis/bulk_update.ts b/x-pack/test/saved_object_api_integration/security_and_spaces/apis/bulk_update.ts index 06240647b37a8..987209653b347 100644 --- a/x-pack/test/saved_object_api_integration/security_and_spaces/apis/bulk_update.ts +++ b/x-pack/test/saved_object_api_integration/security_and_spaces/apis/bulk_update.ts @@ -4,290 +4,91 @@ * you may not use this file except in compliance with the Elastic License. */ -import { AUTHENTICATION } from '../../common/lib/authentication'; import { SPACES } from '../../common/lib/spaces'; +import { testCaseFailures, getTestScenarios } from '../../common/lib/saved_object_test_utils'; +import { TestUser } from '../../common/lib/types'; import { FtrProviderContext } from '../../common/ftr_provider_context'; -import { bulkUpdateTestSuiteFactory } from '../../common/suites/bulk_update'; +import { + bulkUpdateTestSuiteFactory, + TEST_CASES as CASES, + BulkUpdateTestDefinition, +} from '../../common/suites/bulk_update'; + +const { + DEFAULT: { spaceId: DEFAULT_SPACE_ID }, + SPACE_1: { spaceId: SPACE_1_ID }, + SPACE_2: { spaceId: SPACE_2_ID }, +} = SPACES; +const { fail404 } = testCaseFailures; + +const createTestCases = (spaceId: string) => { + // for each permitted (non-403) outcome, if failure !== undefined then we expect + // to receive an error; otherwise, we expect to receive a success result + const normalTypes = [ + { ...CASES.SINGLE_NAMESPACE_DEFAULT_SPACE, ...fail404(spaceId !== DEFAULT_SPACE_ID) }, + { ...CASES.SINGLE_NAMESPACE_SPACE_1, ...fail404(spaceId !== SPACE_1_ID) }, + { ...CASES.SINGLE_NAMESPACE_SPACE_2, ...fail404(spaceId !== SPACE_2_ID) }, + { + ...CASES.MULTI_NAMESPACE_DEFAULT_AND_SPACE_1, + ...fail404(spaceId !== DEFAULT_SPACE_ID && spaceId !== SPACE_1_ID), + }, + { ...CASES.MULTI_NAMESPACE_ONLY_SPACE_1, ...fail404(spaceId !== SPACE_1_ID) }, + { ...CASES.MULTI_NAMESPACE_ONLY_SPACE_2, ...fail404(spaceId !== SPACE_2_ID) }, + CASES.NAMESPACE_AGNOSTIC, + { ...CASES.DOES_NOT_EXIST, ...fail404() }, + ]; + const hiddenType = [{ ...CASES.HIDDEN, ...fail404() }]; + const allTypes = normalTypes.concat(hiddenType); + return { normalTypes, hiddenType, allTypes }; +}; export default function({ getService }: FtrProviderContext) { const supertest = getService('supertestWithoutAuth'); const esArchiver = getService('esArchiver'); - describe('bulkUpdate', () => { - const { - createExpectDoesntExistNotFound, - expectDoesntExistRbacForbidden, - expectNotSpaceAwareResults, - expectNotSpaceAwareRbacForbidden, - expectSpaceAwareRbacForbidden, - expectSpaceAwareResults, - expectSpaceNotFound, - expectHiddenTypeRbacForbidden, - expectHiddenTypeRbacForbiddenWithGlobalAllowed, - bulkUpdateTest, - } = bulkUpdateTestSuiteFactory(esArchiver, supertest); - - [ - { - spaceId: SPACES.DEFAULT.spaceId, - users: { - noAccess: AUTHENTICATION.NOT_A_KIBANA_USER, - superuser: AUTHENTICATION.SUPERUSER, - legacyAll: AUTHENTICATION.KIBANA_LEGACY_USER, - allGlobally: AUTHENTICATION.KIBANA_RBAC_USER, - readGlobally: AUTHENTICATION.KIBANA_RBAC_DASHBOARD_ONLY_USER, - dualAll: AUTHENTICATION.KIBANA_DUAL_PRIVILEGES_USER, - dualRead: AUTHENTICATION.KIBANA_DUAL_PRIVILEGES_DASHBOARD_ONLY_USER, - allAtSpace: AUTHENTICATION.KIBANA_RBAC_DEFAULT_SPACE_ALL_USER, - readAtSpace: AUTHENTICATION.KIBANA_RBAC_DEFAULT_SPACE_READ_USER, - allAtOtherSpace: AUTHENTICATION.KIBANA_RBAC_SPACE_1_ALL_USER, - }, - }, - { - spaceId: SPACES.SPACE_1.spaceId, - users: { - noAccess: AUTHENTICATION.NOT_A_KIBANA_USER, - superuser: AUTHENTICATION.SUPERUSER, - legacyAll: AUTHENTICATION.KIBANA_LEGACY_USER, - allGlobally: AUTHENTICATION.KIBANA_RBAC_USER, - readGlobally: AUTHENTICATION.KIBANA_RBAC_DASHBOARD_ONLY_USER, - dualAll: AUTHENTICATION.KIBANA_DUAL_PRIVILEGES_USER, - dualRead: AUTHENTICATION.KIBANA_DUAL_PRIVILEGES_DASHBOARD_ONLY_USER, - allAtSpace: AUTHENTICATION.KIBANA_RBAC_SPACE_1_ALL_USER, - readAtSpace: AUTHENTICATION.KIBANA_RBAC_SPACE_1_READ_USER, - allAtOtherSpace: AUTHENTICATION.KIBANA_RBAC_DEFAULT_SPACE_ALL_USER, - }, - }, - ].forEach(scenario => { - bulkUpdateTest(`user with no access within the ${scenario.spaceId} space`, { - user: scenario.users.noAccess, - spaceId: scenario.spaceId, - tests: { - spaceAware: { - statusCode: 403, - response: expectSpaceAwareRbacForbidden, - }, - notSpaceAware: { - statusCode: 403, - response: expectNotSpaceAwareRbacForbidden, - }, - hiddenType: { - statusCode: 403, - response: expectHiddenTypeRbacForbidden, - }, - doesntExist: { - statusCode: 403, - response: expectDoesntExistRbacForbidden, - }, - }, - }); - - bulkUpdateTest(`superuser within the ${scenario.spaceId} space`, { - user: scenario.users.superuser, - spaceId: scenario.spaceId, - tests: { - spaceAware: { - statusCode: 200, - response: expectSpaceAwareResults, - }, - notSpaceAware: { - statusCode: 200, - response: expectNotSpaceAwareResults, - }, - hiddenType: { - statusCode: 200, - response: expectSpaceNotFound, - }, - doesntExist: { - statusCode: 200, - response: createExpectDoesntExistNotFound(scenario.spaceId), - }, - }, - }); - - bulkUpdateTest(`legacy user within the ${scenario.spaceId} space`, { - user: scenario.users.legacyAll, - spaceId: scenario.spaceId, - tests: { - spaceAware: { - statusCode: 403, - response: expectSpaceAwareRbacForbidden, - }, - notSpaceAware: { - statusCode: 403, - response: expectNotSpaceAwareRbacForbidden, - }, - hiddenType: { - statusCode: 403, - response: expectHiddenTypeRbacForbidden, - }, - doesntExist: { - statusCode: 403, - response: expectDoesntExistRbacForbidden, - }, - }, - }); - - bulkUpdateTest(`dual-privileges user within the ${scenario.spaceId} space`, { - user: scenario.users.dualAll, - spaceId: scenario.spaceId, - tests: { - spaceAware: { - statusCode: 200, - response: expectSpaceAwareResults, - }, - notSpaceAware: { - statusCode: 200, - response: expectNotSpaceAwareResults, - }, - hiddenType: { - statusCode: 403, - response: expectHiddenTypeRbacForbiddenWithGlobalAllowed, - }, - doesntExist: { - statusCode: 200, - response: createExpectDoesntExistNotFound(scenario.spaceId), - }, - }, - }); + const { addTests, createTestDefinitions, expectForbidden } = bulkUpdateTestSuiteFactory( + esArchiver, + supertest + ); + const createTests = (spaceId: string) => { + const { normalTypes, hiddenType, allTypes } = createTestCases(spaceId); + // use singleRequest to reduce execution time and/or test combined cases + return { + unauthorized: createTestDefinitions(allTypes, true), + authorized: [ + createTestDefinitions(normalTypes, false, { singleRequest: true }), + createTestDefinitions(hiddenType, true), + createTestDefinitions(allTypes, true, { + singleRequest: true, + responseBodyOverride: expectForbidden(['hiddentype']), + }), + ].flat(), + superuser: createTestDefinitions(allTypes, false, { singleRequest: true }), + }; + }; - bulkUpdateTest(`dual-privileges readonly user within the ${scenario.spaceId} space`, { - user: scenario.users.dualRead, - spaceId: scenario.spaceId, - tests: { - spaceAware: { - statusCode: 403, - response: expectSpaceAwareRbacForbidden, - }, - notSpaceAware: { - statusCode: 403, - response: expectNotSpaceAwareRbacForbidden, - }, - hiddenType: { - statusCode: 403, - response: expectHiddenTypeRbacForbidden, - }, - doesntExist: { - statusCode: 403, - response: expectDoesntExistRbacForbidden, - }, - }, - }); + describe('_bulk_update', () => { + getTestScenarios().securityAndSpaces.forEach(({ spaceId, users }) => { + const suffix = ` within the ${spaceId} space`; + const { unauthorized, authorized, superuser } = createTests(spaceId); + const _addTests = (user: TestUser, tests: BulkUpdateTestDefinition[]) => { + addTests(`${user.description}${suffix}`, { user, spaceId, tests }); + }; - bulkUpdateTest(`rbac user with all globally within the ${scenario.spaceId} space`, { - user: scenario.users.allGlobally, - spaceId: scenario.spaceId, - tests: { - spaceAware: { - statusCode: 200, - response: expectSpaceAwareResults, - }, - notSpaceAware: { - statusCode: 200, - response: expectNotSpaceAwareResults, - }, - hiddenType: { - statusCode: 403, - response: expectHiddenTypeRbacForbiddenWithGlobalAllowed, - }, - doesntExist: { - statusCode: 200, - response: createExpectDoesntExistNotFound(scenario.spaceId), - }, - }, + [ + users.noAccess, + users.legacyAll, + users.dualRead, + users.readGlobally, + users.readAtSpace, + users.allAtOtherSpace, + ].forEach(user => { + _addTests(user, unauthorized); }); - - bulkUpdateTest(`rbac user with read globally within the ${scenario.spaceId} space`, { - user: scenario.users.readGlobally, - spaceId: scenario.spaceId, - tests: { - spaceAware: { - statusCode: 403, - response: expectSpaceAwareRbacForbidden, - }, - notSpaceAware: { - statusCode: 403, - response: expectNotSpaceAwareRbacForbidden, - }, - hiddenType: { - statusCode: 403, - response: expectHiddenTypeRbacForbidden, - }, - doesntExist: { - statusCode: 403, - response: expectDoesntExistRbacForbidden, - }, - }, - }); - - bulkUpdateTest(`rbac user with all at the space within the ${scenario.spaceId} space`, { - user: scenario.users.allAtSpace, - spaceId: scenario.spaceId, - tests: { - spaceAware: { - statusCode: 200, - response: expectSpaceAwareResults, - }, - notSpaceAware: { - statusCode: 200, - response: expectNotSpaceAwareResults, - }, - hiddenType: { - statusCode: 403, - response: expectHiddenTypeRbacForbiddenWithGlobalAllowed, - }, - doesntExist: { - statusCode: 200, - response: createExpectDoesntExistNotFound(scenario.spaceId), - }, - }, - }); - - bulkUpdateTest(`rbac user with read at the space within the ${scenario.spaceId} space`, { - user: scenario.users.readAtSpace, - spaceId: scenario.spaceId, - tests: { - spaceAware: { - statusCode: 403, - response: expectSpaceAwareRbacForbidden, - }, - notSpaceAware: { - statusCode: 403, - response: expectNotSpaceAwareRbacForbidden, - }, - hiddenType: { - statusCode: 403, - response: expectHiddenTypeRbacForbidden, - }, - doesntExist: { - statusCode: 403, - response: expectDoesntExistRbacForbidden, - }, - }, - }); - - bulkUpdateTest(`rbac user with all at other space within the ${scenario.spaceId} space`, { - user: scenario.users.allAtOtherSpace, - spaceId: scenario.spaceId, - tests: { - spaceAware: { - statusCode: 403, - response: expectSpaceAwareRbacForbidden, - }, - notSpaceAware: { - statusCode: 403, - response: expectNotSpaceAwareRbacForbidden, - }, - hiddenType: { - statusCode: 403, - response: expectHiddenTypeRbacForbidden, - }, - doesntExist: { - statusCode: 403, - response: expectDoesntExistRbacForbidden, - }, - }, + [users.dualAll, users.allGlobally, users.allAtSpace].forEach(user => { + _addTests(user, authorized); }); + _addTests(users.superuser, superuser); }); }); } diff --git a/x-pack/test/saved_object_api_integration/security_and_spaces/apis/create.ts b/x-pack/test/saved_object_api_integration/security_and_spaces/apis/create.ts index e4adaa580c1db..7278504b8f0e8 100644 --- a/x-pack/test/saved_object_api_integration/security_and_spaces/apis/create.ts +++ b/x-pack/test/saved_object_api_integration/security_and_spaces/apis/create.ts @@ -4,248 +4,91 @@ * you may not use this file except in compliance with the Elastic License. */ -import { AUTHENTICATION } from '../../common/lib/authentication'; import { SPACES } from '../../common/lib/spaces'; +import { testCaseFailures, getTestScenarios } from '../../common/lib/saved_object_test_utils'; +import { TestUser } from '../../common/lib/types'; import { FtrProviderContext } from '../../common/ftr_provider_context'; -import { createTestSuiteFactory } from '../../common/suites/create'; +import { + createTestSuiteFactory, + TEST_CASES as CASES, + CreateTestDefinition, +} from '../../common/suites/create'; -export default function({ getService }: FtrProviderContext) { - const supertestWithoutAuth = getService('supertestWithoutAuth'); - const es = getService('legacyEs'); - const esArchiver = getService('esArchiver'); - - const { - createTest, - createExpectSpaceAwareResults, - expectNotSpaceAwareResults, - expectNotSpaceAwareRbacForbidden, - expectSpaceAwareRbacForbidden, - expectBadRequestForHiddenType, - expectHiddenTypeRbacForbidden, - } = createTestSuiteFactory(es, esArchiver, supertestWithoutAuth); - - describe('create', () => { - [ - { - spaceId: SPACES.DEFAULT.spaceId, - users: { - noAccess: AUTHENTICATION.NOT_A_KIBANA_USER, - superuser: AUTHENTICATION.SUPERUSER, - legacyAll: AUTHENTICATION.KIBANA_LEGACY_USER, - allGlobally: AUTHENTICATION.KIBANA_RBAC_USER, - readGlobally: AUTHENTICATION.KIBANA_RBAC_DASHBOARD_ONLY_USER, - dualAll: AUTHENTICATION.KIBANA_DUAL_PRIVILEGES_USER, - dualRead: AUTHENTICATION.KIBANA_DUAL_PRIVILEGES_DASHBOARD_ONLY_USER, - allAtSpace: AUTHENTICATION.KIBANA_RBAC_DEFAULT_SPACE_ALL_USER, - readAtSpace: AUTHENTICATION.KIBANA_RBAC_DEFAULT_SPACE_READ_USER, - allAtOtherSpace: AUTHENTICATION.KIBANA_RBAC_SPACE_1_ALL_USER, - }, - }, - { - spaceId: SPACES.SPACE_1.spaceId, - users: { - noAccess: AUTHENTICATION.NOT_A_KIBANA_USER, - superuser: AUTHENTICATION.SUPERUSER, - legacyAll: AUTHENTICATION.KIBANA_LEGACY_USER, - allGlobally: AUTHENTICATION.KIBANA_RBAC_USER, - readGlobally: AUTHENTICATION.KIBANA_RBAC_DASHBOARD_ONLY_USER, - dualAll: AUTHENTICATION.KIBANA_DUAL_PRIVILEGES_USER, - dualRead: AUTHENTICATION.KIBANA_DUAL_PRIVILEGES_DASHBOARD_ONLY_USER, - allAtSpace: AUTHENTICATION.KIBANA_RBAC_SPACE_1_ALL_USER, - readAtSpace: AUTHENTICATION.KIBANA_RBAC_SPACE_1_READ_USER, - allAtOtherSpace: AUTHENTICATION.KIBANA_RBAC_DEFAULT_SPACE_ALL_USER, - }, - }, - ].forEach(scenario => { - createTest(`user with no access within the ${scenario.spaceId} space`, { - user: scenario.users.noAccess, - spaceId: scenario.spaceId, - tests: { - spaceAware: { - statusCode: 403, - response: expectSpaceAwareRbacForbidden, - }, - notSpaceAware: { - statusCode: 403, - response: expectNotSpaceAwareRbacForbidden, - }, - hiddenType: { - statusCode: 403, - response: expectHiddenTypeRbacForbidden, - }, - }, - }); - - createTest(`superuser within the ${scenario.spaceId} space`, { - user: scenario.users.superuser, - spaceId: scenario.spaceId, - tests: { - spaceAware: { - statusCode: 200, - response: createExpectSpaceAwareResults(scenario.spaceId), - }, - notSpaceAware: { - statusCode: 200, - response: expectNotSpaceAwareResults, - }, - hiddenType: { - statusCode: 400, - response: expectBadRequestForHiddenType, - }, - }, - }); +const { + DEFAULT: { spaceId: DEFAULT_SPACE_ID }, + SPACE_1: { spaceId: SPACE_1_ID }, + SPACE_2: { spaceId: SPACE_2_ID }, +} = SPACES; +const { fail400, fail409 } = testCaseFailures; - createTest(`legacy user within the ${scenario.spaceId} space`, { - user: scenario.users.legacyAll, - spaceId: scenario.spaceId, - tests: { - spaceAware: { - statusCode: 403, - response: expectSpaceAwareRbacForbidden, - }, - notSpaceAware: { - statusCode: 403, - response: expectNotSpaceAwareRbacForbidden, - }, - hiddenType: { - statusCode: 403, - response: expectHiddenTypeRbacForbidden, - }, - }, - }); +const createTestCases = (overwrite: boolean, spaceId: string) => { + // for each permitted (non-403) outcome, if failure !== undefined then we expect + // to receive an error; otherwise, we expect to receive a success result + const normalTypes = [ + { + ...CASES.SINGLE_NAMESPACE_DEFAULT_SPACE, + ...fail409(!overwrite && spaceId === DEFAULT_SPACE_ID), + }, + { ...CASES.SINGLE_NAMESPACE_SPACE_1, ...fail409(!overwrite && spaceId === SPACE_1_ID) }, + { ...CASES.SINGLE_NAMESPACE_SPACE_2, ...fail409(!overwrite && spaceId === SPACE_2_ID) }, + { + ...CASES.MULTI_NAMESPACE_DEFAULT_AND_SPACE_1, + ...fail409(!overwrite || (spaceId !== DEFAULT_SPACE_ID && spaceId !== SPACE_1_ID)), + }, + { ...CASES.MULTI_NAMESPACE_ONLY_SPACE_1, ...fail409(!overwrite || spaceId !== SPACE_1_ID) }, + { ...CASES.MULTI_NAMESPACE_ONLY_SPACE_2, ...fail409(!overwrite || spaceId !== SPACE_2_ID) }, + { ...CASES.NAMESPACE_AGNOSTIC, ...fail409(!overwrite) }, + CASES.NEW_SINGLE_NAMESPACE_OBJ, + CASES.NEW_MULTI_NAMESPACE_OBJ, + CASES.NEW_NAMESPACE_AGNOSTIC_OBJ, + ]; + const hiddenType = [{ ...CASES.HIDDEN, ...fail400() }]; + const allTypes = normalTypes.concat(hiddenType); + return { normalTypes, hiddenType, allTypes }; +}; - createTest(`dual-privileges user within the ${scenario.spaceId} space`, { - user: scenario.users.dualAll, - spaceId: scenario.spaceId, - tests: { - spaceAware: { - statusCode: 200, - response: createExpectSpaceAwareResults(scenario.spaceId), - }, - notSpaceAware: { - statusCode: 200, - response: expectNotSpaceAwareResults, - }, - hiddenType: { - statusCode: 403, - response: expectHiddenTypeRbacForbidden, - }, - }, - }); - - createTest(`dual-privileges readonly user within the ${scenario.spaceId} space`, { - user: scenario.users.dualRead, - spaceId: scenario.spaceId, - tests: { - spaceAware: { - statusCode: 403, - response: expectSpaceAwareRbacForbidden, - }, - notSpaceAware: { - statusCode: 403, - response: expectNotSpaceAwareRbacForbidden, - }, - hiddenType: { - statusCode: 403, - response: expectHiddenTypeRbacForbidden, - }, - }, - }); - - createTest(`rbac user with all globally within the ${scenario.spaceId} space`, { - user: scenario.users.allGlobally, - spaceId: scenario.spaceId, - tests: { - spaceAware: { - statusCode: 200, - response: createExpectSpaceAwareResults(scenario.spaceId), - }, - notSpaceAware: { - statusCode: 200, - response: expectNotSpaceAwareResults, - }, - hiddenType: { - statusCode: 403, - response: expectHiddenTypeRbacForbidden, - }, - }, - }); - - createTest(`rbac user with read globally within the ${scenario.spaceId} space`, { - user: scenario.users.readGlobally, - spaceId: scenario.spaceId, - tests: { - spaceAware: { - statusCode: 403, - response: expectSpaceAwareRbacForbidden, - }, - notSpaceAware: { - statusCode: 403, - response: expectNotSpaceAwareRbacForbidden, - }, - hiddenType: { - statusCode: 403, - response: expectHiddenTypeRbacForbidden, - }, - }, - }); +export default function({ getService }: FtrProviderContext) { + const supertest = getService('supertestWithoutAuth'); + const esArchiver = getService('esArchiver'); + const es = getService('legacyEs'); - createTest(`rbac user with all at the space within the ${scenario.spaceId} space`, { - user: scenario.users.allAtSpace, - spaceId: scenario.spaceId, - tests: { - spaceAware: { - statusCode: 200, - response: createExpectSpaceAwareResults(scenario.spaceId), - }, - notSpaceAware: { - statusCode: 200, - response: expectNotSpaceAwareResults, - }, - hiddenType: { - statusCode: 403, - response: expectHiddenTypeRbacForbidden, - }, - }, - }); + const { addTests, createTestDefinitions } = createTestSuiteFactory(es, esArchiver, supertest); + const createTests = (overwrite: boolean, spaceId: string) => { + const { normalTypes, hiddenType, allTypes } = createTestCases(overwrite, spaceId); + return { + unauthorized: createTestDefinitions(allTypes, true, overwrite, { spaceId }), + authorized: [ + createTestDefinitions(normalTypes, false, overwrite, { spaceId }), + createTestDefinitions(hiddenType, true, overwrite, { spaceId }), + ].flat(), + superuser: createTestDefinitions(allTypes, false, overwrite, { spaceId }), + }; + }; - createTest(`rbac user with read at the space within the ${scenario.spaceId} space`, { - user: scenario.users.readAtSpace, - spaceId: scenario.spaceId, - tests: { - spaceAware: { - statusCode: 403, - response: expectSpaceAwareRbacForbidden, - }, - notSpaceAware: { - statusCode: 403, - response: expectNotSpaceAwareRbacForbidden, - }, - hiddenType: { - statusCode: 403, - response: expectHiddenTypeRbacForbidden, - }, - }, - }); + describe('_create', () => { + getTestScenarios([false, true]).securityAndSpaces.forEach( + ({ spaceId, users, modifier: overwrite }) => { + const suffix = ` within the ${spaceId} space${overwrite ? ' with overwrite enabled' : ''}`; + const { unauthorized, authorized, superuser } = createTests(overwrite!, spaceId); + const _addTests = (user: TestUser, tests: CreateTestDefinition[]) => { + addTests(`${user.description}${suffix}`, { user, spaceId, tests }); + }; - createTest(`rbac user with all at other space within the ${scenario.spaceId} space`, { - user: scenario.users.allAtOtherSpace, - spaceId: scenario.spaceId, - tests: { - spaceAware: { - statusCode: 403, - response: expectSpaceAwareRbacForbidden, - }, - notSpaceAware: { - statusCode: 403, - response: expectNotSpaceAwareRbacForbidden, - }, - hiddenType: { - statusCode: 403, - response: expectHiddenTypeRbacForbidden, - }, - }, - }); - }); + [ + users.noAccess, + users.legacyAll, + users.dualRead, + users.readGlobally, + users.readAtSpace, + users.allAtOtherSpace, + ].forEach(user => { + _addTests(user, unauthorized); + }); + [users.dualAll, users.allGlobally, users.allAtSpace].forEach(user => { + _addTests(user, authorized); + }); + _addTests(users.superuser, superuser); + } + ); }); } diff --git a/x-pack/test/saved_object_api_integration/security_and_spaces/apis/delete.ts b/x-pack/test/saved_object_api_integration/security_and_spaces/apis/delete.ts index bfd2112428db4..995b8fc2422d9 100644 --- a/x-pack/test/saved_object_api_integration/security_and_spaces/apis/delete.ts +++ b/x-pack/test/saved_object_api_integration/security_and_spaces/apis/delete.ts @@ -4,288 +4,83 @@ * you may not use this file except in compliance with the Elastic License. */ -import { AUTHENTICATION } from '../../common/lib/authentication'; import { SPACES } from '../../common/lib/spaces'; +import { testCaseFailures, getTestScenarios } from '../../common/lib/saved_object_test_utils'; +import { TestUser } from '../../common/lib/types'; import { FtrProviderContext } from '../../common/ftr_provider_context'; -import { deleteTestSuiteFactory } from '../../common/suites/delete'; +import { + deleteTestSuiteFactory, + TEST_CASES as CASES, + DeleteTestDefinition, +} from '../../common/suites/delete'; + +const { + DEFAULT: { spaceId: DEFAULT_SPACE_ID }, + SPACE_1: { spaceId: SPACE_1_ID }, + SPACE_2: { spaceId: SPACE_2_ID }, +} = SPACES; +const { fail404 } = testCaseFailures; + +const createTestCases = (spaceId: string) => { + // for each permitted (non-403) outcome, if failure !== undefined then we expect + // to receive an error; otherwise, we expect to receive a success result + const normalTypes = [ + { ...CASES.SINGLE_NAMESPACE_DEFAULT_SPACE, ...fail404(spaceId !== DEFAULT_SPACE_ID) }, + { ...CASES.SINGLE_NAMESPACE_SPACE_1, ...fail404(spaceId !== SPACE_1_ID) }, + { ...CASES.SINGLE_NAMESPACE_SPACE_2, ...fail404(spaceId !== SPACE_2_ID) }, + { + ...CASES.MULTI_NAMESPACE_DEFAULT_AND_SPACE_1, + ...fail404(spaceId !== DEFAULT_SPACE_ID && spaceId !== SPACE_1_ID), + }, + { ...CASES.MULTI_NAMESPACE_ONLY_SPACE_1, ...fail404(spaceId !== SPACE_1_ID) }, + { ...CASES.MULTI_NAMESPACE_ONLY_SPACE_2, ...fail404(spaceId !== SPACE_2_ID) }, + CASES.NAMESPACE_AGNOSTIC, + { ...CASES.DOES_NOT_EXIST, ...fail404() }, + ]; + const hiddenType = [{ ...CASES.HIDDEN, ...fail404() }]; + const allTypes = normalTypes.concat(hiddenType); + return { normalTypes, hiddenType, allTypes }; +}; export default function({ getService }: FtrProviderContext) { const supertest = getService('supertestWithoutAuth'); const esArchiver = getService('esArchiver'); - describe('delete', () => { - const { - createExpectUnknownDocNotFound, - deleteTest, - expectEmpty, - expectRbacSpaceAwareForbidden, - expectRbacNotSpaceAwareForbidden, - expectRbacInvalidIdForbidden, - expectGenericNotFound, - expectRbacHiddenTypeForbidden, - } = deleteTestSuiteFactory(esArchiver, supertest); - - [ - { - spaceId: SPACES.DEFAULT.spaceId, - users: { - noAccess: AUTHENTICATION.NOT_A_KIBANA_USER, - superuser: AUTHENTICATION.SUPERUSER, - legacyAll: AUTHENTICATION.KIBANA_LEGACY_USER, - allGlobally: AUTHENTICATION.KIBANA_RBAC_USER, - readGlobally: AUTHENTICATION.KIBANA_RBAC_DASHBOARD_ONLY_USER, - dualAll: AUTHENTICATION.KIBANA_DUAL_PRIVILEGES_USER, - dualRead: AUTHENTICATION.KIBANA_DUAL_PRIVILEGES_DASHBOARD_ONLY_USER, - allAtSpace: AUTHENTICATION.KIBANA_RBAC_DEFAULT_SPACE_ALL_USER, - readAtSpace: AUTHENTICATION.KIBANA_RBAC_DEFAULT_SPACE_READ_USER, - allAtOtherSpace: AUTHENTICATION.KIBANA_RBAC_SPACE_1_ALL_USER, - }, - }, - { - spaceId: SPACES.SPACE_1.spaceId, - users: { - noAccess: AUTHENTICATION.NOT_A_KIBANA_USER, - superuser: AUTHENTICATION.SUPERUSER, - legacyAll: AUTHENTICATION.KIBANA_LEGACY_USER, - allGlobally: AUTHENTICATION.KIBANA_RBAC_USER, - readGlobally: AUTHENTICATION.KIBANA_RBAC_DASHBOARD_ONLY_USER, - dualAll: AUTHENTICATION.KIBANA_DUAL_PRIVILEGES_USER, - dualRead: AUTHENTICATION.KIBANA_DUAL_PRIVILEGES_DASHBOARD_ONLY_USER, - allAtSpace: AUTHENTICATION.KIBANA_RBAC_SPACE_1_ALL_USER, - readAtSpace: AUTHENTICATION.KIBANA_RBAC_SPACE_1_READ_USER, - allAtOtherSpace: AUTHENTICATION.KIBANA_RBAC_DEFAULT_SPACE_ALL_USER, - }, - }, - ].forEach(scenario => { - deleteTest(`user with no access within the ${scenario.spaceId} space`, { - user: scenario.users.noAccess, - spaceId: scenario.spaceId, - tests: { - spaceAware: { - statusCode: 403, - response: expectRbacSpaceAwareForbidden, - }, - notSpaceAware: { - statusCode: 403, - response: expectRbacNotSpaceAwareForbidden, - }, - hiddenType: { - statusCode: 403, - response: expectRbacHiddenTypeForbidden, - }, - invalidId: { - statusCode: 403, - response: expectRbacInvalidIdForbidden, - }, - }, - }); - - deleteTest(`superuser within the ${scenario.spaceId} space`, { - user: scenario.users.superuser, - spaceId: scenario.spaceId, - tests: { - spaceAware: { - statusCode: 200, - response: expectEmpty, - }, - notSpaceAware: { - statusCode: 200, - response: expectEmpty, - }, - hiddenType: { - statusCode: 404, - response: expectGenericNotFound, - }, - invalidId: { - statusCode: 404, - response: createExpectUnknownDocNotFound(scenario.spaceId), - }, - }, - }); - - deleteTest(`legacy user within the ${scenario.spaceId} space`, { - user: scenario.users.legacyAll, - spaceId: scenario.spaceId, - tests: { - spaceAware: { - statusCode: 403, - response: expectRbacSpaceAwareForbidden, - }, - notSpaceAware: { - statusCode: 403, - response: expectRbacNotSpaceAwareForbidden, - }, - hiddenType: { - statusCode: 403, - response: expectRbacHiddenTypeForbidden, - }, - invalidId: { - statusCode: 403, - response: expectRbacInvalidIdForbidden, - }, - }, - }); - - deleteTest(`dual-privileges user within the ${scenario.spaceId} space`, { - user: scenario.users.dualAll, - spaceId: scenario.spaceId, - tests: { - spaceAware: { - statusCode: 200, - response: expectEmpty, - }, - notSpaceAware: { - statusCode: 200, - response: expectEmpty, - }, - hiddenType: { - statusCode: 403, - response: expectRbacHiddenTypeForbidden, - }, - invalidId: { - statusCode: 404, - response: createExpectUnknownDocNotFound(scenario.spaceId), - }, - }, - }); + const { addTests, createTestDefinitions } = deleteTestSuiteFactory(esArchiver, supertest); + const createTests = (spaceId: string) => { + const { normalTypes, hiddenType, allTypes } = createTestCases(spaceId); + return { + unauthorized: createTestDefinitions(allTypes, true, { spaceId }), + authorized: [ + createTestDefinitions(normalTypes, false, { spaceId }), + createTestDefinitions(hiddenType, true, { spaceId }), + ].flat(), + superuser: createTestDefinitions(allTypes, false, { spaceId }), + }; + }; - deleteTest(`dual-privileges readonly user within the ${scenario.spaceId} space`, { - user: scenario.users.dualRead, - spaceId: scenario.spaceId, - tests: { - spaceAware: { - statusCode: 403, - response: expectRbacSpaceAwareForbidden, - }, - notSpaceAware: { - statusCode: 403, - response: expectRbacNotSpaceAwareForbidden, - }, - hiddenType: { - statusCode: 403, - response: expectRbacHiddenTypeForbidden, - }, - invalidId: { - statusCode: 403, - response: expectRbacInvalidIdForbidden, - }, - }, - }); + describe('_delete', () => { + getTestScenarios().securityAndSpaces.forEach(({ spaceId, users }) => { + const suffix = ` within the ${spaceId} space`; + const { unauthorized, authorized, superuser } = createTests(spaceId); + const _addTests = (user: TestUser, tests: DeleteTestDefinition[]) => { + addTests(`${user.description}${suffix}`, { user, spaceId, tests }); + }; - deleteTest(`rbac user with all globally within the ${scenario.spaceId} space`, { - user: scenario.users.allGlobally, - spaceId: scenario.spaceId, - tests: { - spaceAware: { - statusCode: 200, - response: expectEmpty, - }, - notSpaceAware: { - statusCode: 200, - response: expectEmpty, - }, - hiddenType: { - statusCode: 403, - response: expectRbacHiddenTypeForbidden, - }, - invalidId: { - statusCode: 404, - response: createExpectUnknownDocNotFound(scenario.spaceId), - }, - }, + [ + users.noAccess, + users.legacyAll, + users.dualRead, + users.readGlobally, + users.readAtSpace, + users.allAtOtherSpace, + ].forEach(user => { + _addTests(user, unauthorized); }); - - deleteTest(`rbac user with read globally within the ${scenario.spaceId} space`, { - user: scenario.users.readGlobally, - spaceId: scenario.spaceId, - tests: { - spaceAware: { - statusCode: 403, - response: expectRbacSpaceAwareForbidden, - }, - notSpaceAware: { - statusCode: 403, - response: expectRbacNotSpaceAwareForbidden, - }, - hiddenType: { - statusCode: 403, - response: expectRbacHiddenTypeForbidden, - }, - invalidId: { - statusCode: 403, - response: expectRbacInvalidIdForbidden, - }, - }, - }); - - deleteTest(`rbac user with all at the space within the ${scenario.spaceId} space`, { - user: scenario.users.allAtSpace, - spaceId: scenario.spaceId, - tests: { - spaceAware: { - statusCode: 200, - response: expectEmpty, - }, - notSpaceAware: { - statusCode: 200, - response: expectEmpty, - }, - hiddenType: { - statusCode: 403, - response: expectRbacHiddenTypeForbidden, - }, - invalidId: { - statusCode: 404, - response: createExpectUnknownDocNotFound(scenario.spaceId), - }, - }, - }); - - deleteTest(`rbac user with read at the space within the ${scenario.spaceId} space`, { - user: scenario.users.readAtSpace, - spaceId: scenario.spaceId, - tests: { - spaceAware: { - statusCode: 403, - response: expectRbacSpaceAwareForbidden, - }, - notSpaceAware: { - statusCode: 403, - response: expectRbacNotSpaceAwareForbidden, - }, - hiddenType: { - statusCode: 403, - response: expectRbacHiddenTypeForbidden, - }, - invalidId: { - statusCode: 403, - response: expectRbacInvalidIdForbidden, - }, - }, - }); - - deleteTest(`rbac user with all at other space within the ${scenario.spaceId} space`, { - user: scenario.users.allAtOtherSpace, - spaceId: scenario.spaceId, - tests: { - spaceAware: { - statusCode: 403, - response: expectRbacSpaceAwareForbidden, - }, - notSpaceAware: { - statusCode: 403, - response: expectRbacNotSpaceAwareForbidden, - }, - hiddenType: { - statusCode: 403, - response: expectRbacHiddenTypeForbidden, - }, - invalidId: { - statusCode: 403, - response: expectRbacInvalidIdForbidden, - }, - }, + [users.dualAll, users.allGlobally, users.allAtSpace].forEach(user => { + _addTests(user, authorized); }); + _addTests(users.superuser, superuser); }); }); } diff --git a/x-pack/test/saved_object_api_integration/security_and_spaces/apis/export.ts b/x-pack/test/saved_object_api_integration/security_and_spaces/apis/export.ts index b64c3ed87c35d..6f2426e55c6a6 100644 --- a/x-pack/test/saved_object_api_integration/security_and_spaces/apis/export.ts +++ b/x-pack/test/saved_object_api_integration/security_and_spaces/apis/export.ts @@ -4,274 +4,70 @@ * you may not use this file except in compliance with the Elastic License. */ -import { AUTHENTICATION } from '../../common/lib/authentication'; -import { SPACES } from '../../common/lib/spaces'; +import { getTestScenarios } from '../../common/lib/saved_object_test_utils'; +import { TestUser } from '../../common/lib/types'; import { FtrProviderContext } from '../../common/ftr_provider_context'; -import { exportTestSuiteFactory } from '../../common/suites/export'; +import { + exportTestSuiteFactory, + getTestCases, + ExportTestDefinition, +} from '../../common/suites/export'; + +const createTestCases = (spaceId: string) => { + const cases = getTestCases(spaceId); + const exportableTypes = [ + cases.singleNamespaceObject, + cases.singleNamespaceType, + cases.namespaceAgnosticObject, + cases.namespaceAgnosticType, + ]; + const nonExportableTypes = [ + cases.multiNamespaceObject, + cases.multiNamespaceType, + cases.hiddenObject, + cases.hiddenType, + ]; + const allTypes = exportableTypes.concat(nonExportableTypes); + return { exportableTypes, nonExportableTypes, allTypes }; +}; export default function({ getService }: FtrProviderContext) { const supertest = getService('supertestWithoutAuth'); const esArchiver = getService('esArchiver'); - describe('export', () => { - const { - createExpectRbacForbidden, - expectTypeOrObjectsRequired, - createExpectVisualizationResults, - expectInvalidTypeSpecified, - exportTest, - } = exportTestSuiteFactory(esArchiver, supertest); + const { addTests, createTestDefinitions } = exportTestSuiteFactory(esArchiver, supertest); + const createTests = (spaceId: string) => { + const { exportableTypes, nonExportableTypes, allTypes } = createTestCases(spaceId); + return { + unauthorized: [ + createTestDefinitions(exportableTypes, true), + createTestDefinitions(nonExportableTypes, false), + ].flat(), + authorized: createTestDefinitions(allTypes, false), + }; + }; - [ - { - spaceId: SPACES.DEFAULT.spaceId, - users: { - noAccess: AUTHENTICATION.NOT_A_KIBANA_USER, - superuser: AUTHENTICATION.SUPERUSER, - legacyAll: AUTHENTICATION.KIBANA_LEGACY_USER, - allGlobally: AUTHENTICATION.KIBANA_RBAC_USER, - readGlobally: AUTHENTICATION.KIBANA_RBAC_DASHBOARD_ONLY_USER, - dualAll: AUTHENTICATION.KIBANA_DUAL_PRIVILEGES_USER, - dualRead: AUTHENTICATION.KIBANA_DUAL_PRIVILEGES_DASHBOARD_ONLY_USER, - allAtSpace: AUTHENTICATION.KIBANA_RBAC_DEFAULT_SPACE_ALL_USER, - readAtSpace: AUTHENTICATION.KIBANA_RBAC_DEFAULT_SPACE_READ_USER, - allAtOtherSpace: AUTHENTICATION.KIBANA_RBAC_SPACE_1_ALL_USER, - }, - }, - { - spaceId: SPACES.SPACE_1.spaceId, - users: { - noAccess: AUTHENTICATION.NOT_A_KIBANA_USER, - superuser: AUTHENTICATION.SUPERUSER, - legacyAll: AUTHENTICATION.KIBANA_LEGACY_USER, - allGlobally: AUTHENTICATION.KIBANA_RBAC_USER, - readGlobally: AUTHENTICATION.KIBANA_RBAC_DASHBOARD_ONLY_USER, - dualAll: AUTHENTICATION.KIBANA_DUAL_PRIVILEGES_USER, - dualRead: AUTHENTICATION.KIBANA_DUAL_PRIVILEGES_DASHBOARD_ONLY_USER, - allAtSpace: AUTHENTICATION.KIBANA_RBAC_SPACE_1_ALL_USER, - readAtSpace: AUTHENTICATION.KIBANA_RBAC_SPACE_1_READ_USER, - allAtOtherSpace: AUTHENTICATION.KIBANA_RBAC_DEFAULT_SPACE_ALL_USER, - }, - }, - ].forEach(scenario => { - exportTest(`user with no access within the ${scenario.spaceId} space`, { - user: scenario.users.noAccess, - spaceId: scenario.spaceId, - tests: { - spaceAwareType: { - description: 'forbidden login and find visualization message', - statusCode: 403, - response: createExpectRbacForbidden('visualization'), - }, - hiddenType: { - description: 'exporting space not allowed', - statusCode: 400, - response: expectInvalidTypeSpecified, - }, - noTypeOrObjects: { - description: 'bad request, type or object is required', - statusCode: 400, - response: expectTypeOrObjectsRequired, - }, - }, - }); + describe('_export', () => { + getTestScenarios().securityAndSpaces.forEach(({ spaceId, users }) => { + const suffix = ` within the ${spaceId} space`; + const { unauthorized, authorized } = createTests(spaceId); + const _addTests = (user: TestUser, tests: ExportTestDefinition[]) => { + addTests(`${user.description}${suffix}`, { user, spaceId, tests }); + }; - exportTest(`superuser with the ${scenario.spaceId} space`, { - user: scenario.users.superuser, - spaceId: scenario.spaceId, - tests: { - spaceAwareType: { - description: 'only the visualization', - statusCode: 200, - response: createExpectVisualizationResults(scenario.spaceId), - }, - hiddenType: { - description: 'exporting space not allowed', - statusCode: 400, - response: expectInvalidTypeSpecified, - }, - noTypeOrObjects: { - description: 'bad request, type or object is required', - statusCode: 400, - response: expectTypeOrObjectsRequired, - }, - }, + [users.noAccess, users.legacyAll, users.allAtOtherSpace].forEach(user => { + _addTests(user, unauthorized); }); - - exportTest(`legacy user within the ${scenario.spaceId} space`, { - user: scenario.users.legacyAll, - spaceId: scenario.spaceId, - tests: { - spaceAwareType: { - description: 'forbidden login and find visualization message', - statusCode: 403, - response: createExpectRbacForbidden('visualization'), - }, - hiddenType: { - description: 'exporting space not allowed', - statusCode: 400, - response: expectInvalidTypeSpecified, - }, - noTypeOrObjects: { - description: 'bad request, type or object is required', - statusCode: 400, - response: expectTypeOrObjectsRequired, - }, - }, - }); - - exportTest(`dual-privileges user within the ${scenario.spaceId} space`, { - user: scenario.users.dualAll, - spaceId: scenario.spaceId, - tests: { - spaceAwareType: { - description: 'only the visualization', - statusCode: 200, - response: createExpectVisualizationResults(scenario.spaceId), - }, - hiddenType: { - description: 'exporting space not allowed', - statusCode: 400, - response: expectInvalidTypeSpecified, - }, - noTypeOrObjects: { - description: 'bad request, type or object is required', - statusCode: 400, - response: expectTypeOrObjectsRequired, - }, - }, - }); - - exportTest(`dual-privileges readonly user within the ${scenario.spaceId} space`, { - user: scenario.users.dualRead, - spaceId: scenario.spaceId, - tests: { - spaceAwareType: { - description: 'only the visualization', - statusCode: 200, - response: createExpectVisualizationResults(scenario.spaceId), - }, - hiddenType: { - description: 'exporting space not allowed', - statusCode: 400, - response: expectInvalidTypeSpecified, - }, - noTypeOrObjects: { - description: 'bad request, type or object is required', - statusCode: 400, - response: expectTypeOrObjectsRequired, - }, - }, - }); - - exportTest(`rbac user with all globally within the ${scenario.spaceId} space`, { - user: scenario.users.allGlobally, - spaceId: scenario.spaceId, - tests: { - spaceAwareType: { - description: 'only the visualization', - statusCode: 200, - response: createExpectVisualizationResults(scenario.spaceId), - }, - hiddenType: { - description: 'exporting space not allowed', - statusCode: 400, - response: expectInvalidTypeSpecified, - }, - noTypeOrObjects: { - description: 'bad request, type or object is required', - statusCode: 400, - response: expectTypeOrObjectsRequired, - }, - }, - }); - - exportTest(`rbac user with read globally within the ${scenario.spaceId} space`, { - user: scenario.users.readGlobally, - spaceId: scenario.spaceId, - tests: { - spaceAwareType: { - description: 'only the visualization', - statusCode: 200, - response: createExpectVisualizationResults(scenario.spaceId), - }, - hiddenType: { - description: 'exporting space not allowed', - statusCode: 400, - response: expectInvalidTypeSpecified, - }, - noTypeOrObjects: { - description: 'bad request, type or object is required', - statusCode: 400, - response: expectTypeOrObjectsRequired, - }, - }, - }); - - exportTest(`rbac user with all at the space within the ${scenario.spaceId} space`, { - user: scenario.users.allAtSpace, - spaceId: scenario.spaceId, - tests: { - spaceAwareType: { - description: 'only the visualization', - statusCode: 200, - response: createExpectVisualizationResults(scenario.spaceId), - }, - hiddenType: { - description: 'exporting space not allowed', - statusCode: 400, - response: expectInvalidTypeSpecified, - }, - noTypeOrObjects: { - description: 'bad request, type or object is required', - statusCode: 400, - response: expectTypeOrObjectsRequired, - }, - }, - }); - - exportTest(`rbac user with read at the space within the ${scenario.spaceId} space`, { - user: scenario.users.readAtSpace, - spaceId: scenario.spaceId, - tests: { - spaceAwareType: { - description: 'only the visualization', - statusCode: 200, - response: createExpectVisualizationResults(scenario.spaceId), - }, - hiddenType: { - description: 'exporting space not allowed', - statusCode: 400, - response: expectInvalidTypeSpecified, - }, - noTypeOrObjects: { - description: 'bad request, type or object is required', - statusCode: 400, - response: expectTypeOrObjectsRequired, - }, - }, - }); - - exportTest(`rbac user with all at the other space within ${scenario.spaceId} space`, { - user: scenario.users.allAtOtherSpace, - spaceId: scenario.spaceId, - tests: { - spaceAwareType: { - description: 'forbidden login and find visualization message', - statusCode: 403, - response: createExpectRbacForbidden('visualization'), - }, - hiddenType: { - description: 'exporting space not allowed', - statusCode: 400, - response: expectInvalidTypeSpecified, - }, - noTypeOrObjects: { - description: 'bad request, type or object is required', - statusCode: 400, - response: expectTypeOrObjectsRequired, - }, - }, + [ + users.dualAll, + users.dualRead, + users.allGlobally, + users.readGlobally, + users.allAtSpace, + users.readAtSpace, + users.superuser, + ].forEach(user => { + _addTests(user, authorized); }); }); }); diff --git a/x-pack/test/saved_object_api_integration/security_and_spaces/apis/find.ts b/x-pack/test/saved_object_api_integration/security_and_spaces/apis/find.ts index 366b8b44585cd..7c16c01d203c0 100644 --- a/x-pack/test/saved_object_api_integration/security_and_spaces/apis/find.ts +++ b/x-pack/test/saved_object_api_integration/security_and_spaces/apis/find.ts @@ -4,727 +4,71 @@ * you may not use this file except in compliance with the Elastic License. */ -import { AUTHENTICATION } from '../../common/lib/authentication'; -import { SPACES } from '../../common/lib/spaces'; +import { getTestScenarios } from '../../common/lib/saved_object_test_utils'; +import { TestUser } from '../../common/lib/types'; import { FtrProviderContext } from '../../common/ftr_provider_context'; -import { findTestSuiteFactory } from '../../common/suites/find'; +import { findTestSuiteFactory, getTestCases, FindTestDefinition } from '../../common/suites/find'; + +const createTestCases = (spaceId: string) => { + const cases = getTestCases(spaceId); + const normalTypes = [ + cases.singleNamespaceType, + cases.multiNamespaceType, + cases.namespaceAgnosticType, + cases.pageBeyondTotal, + cases.unknownSearchField, + cases.filterWithNamespaceAgnosticType, + cases.filterWithDisallowedType, + ]; + const hiddenAndUnknownTypes = [ + cases.hiddenType, + cases.unknownType, + cases.filterWithHiddenType, + cases.filterWithUnknownType, + ]; + const allTypes = normalTypes.concat(hiddenAndUnknownTypes); + return { normalTypes, hiddenAndUnknownTypes, allTypes }; +}; export default function({ getService }: FtrProviderContext) { const supertest = getService('supertestWithoutAuth'); const esArchiver = getService('esArchiver'); - describe('find', () => { - const { - createExpectEmpty, - createExpectRbacForbidden, - createExpectVisualizationResults, - expectFilterWrongTypeError, - expectNotSpaceAwareResults, - expectTypeRequired, - findTest, - } = findTestSuiteFactory(esArchiver, supertest); + const { addTests, createTestDefinitions } = findTestSuiteFactory(esArchiver, supertest); + const createTests = (spaceId: string) => { + const { normalTypes, hiddenAndUnknownTypes, allTypes } = createTestCases(spaceId); + return { + unauthorized: createTestDefinitions(allTypes, true), + authorized: [ + createTestDefinitions(normalTypes, false), + createTestDefinitions(hiddenAndUnknownTypes, true), + ].flat(), + superuser: createTestDefinitions(allTypes, false), + }; + }; - [ - { - spaceId: SPACES.DEFAULT.spaceId, - users: { - noAccess: AUTHENTICATION.NOT_A_KIBANA_USER, - superuser: AUTHENTICATION.SUPERUSER, - legacyAll: AUTHENTICATION.KIBANA_LEGACY_USER, - allGlobally: AUTHENTICATION.KIBANA_RBAC_USER, - readGlobally: AUTHENTICATION.KIBANA_RBAC_DASHBOARD_ONLY_USER, - dualAll: AUTHENTICATION.KIBANA_DUAL_PRIVILEGES_USER, - dualRead: AUTHENTICATION.KIBANA_DUAL_PRIVILEGES_DASHBOARD_ONLY_USER, - allAtSpace: AUTHENTICATION.KIBANA_RBAC_DEFAULT_SPACE_ALL_USER, - readAtSpace: AUTHENTICATION.KIBANA_RBAC_DEFAULT_SPACE_READ_USER, - allAtOtherSpace: AUTHENTICATION.KIBANA_RBAC_SPACE_1_ALL_USER, - }, - }, - { - spaceId: SPACES.SPACE_1.spaceId, - users: { - noAccess: AUTHENTICATION.NOT_A_KIBANA_USER, - superuser: AUTHENTICATION.SUPERUSER, - legacyAll: AUTHENTICATION.KIBANA_LEGACY_USER, - allGlobally: AUTHENTICATION.KIBANA_RBAC_USER, - readGlobally: AUTHENTICATION.KIBANA_RBAC_DASHBOARD_ONLY_USER, - dualAll: AUTHENTICATION.KIBANA_DUAL_PRIVILEGES_USER, - dualRead: AUTHENTICATION.KIBANA_DUAL_PRIVILEGES_DASHBOARD_ONLY_USER, - allAtSpace: AUTHENTICATION.KIBANA_RBAC_SPACE_1_ALL_USER, - readAtSpace: AUTHENTICATION.KIBANA_RBAC_SPACE_1_READ_USER, - allAtOtherSpace: AUTHENTICATION.KIBANA_RBAC_DEFAULT_SPACE_ALL_USER, - }, - }, - ].forEach(scenario => { - findTest(`user with no access within the ${scenario.spaceId} space`, { - user: scenario.users.noAccess, - spaceId: scenario.spaceId, - tests: { - spaceAwareType: { - description: 'forbidden login and find visualization message', - statusCode: 403, - response: createExpectRbacForbidden('visualization'), - }, - notSpaceAwareType: { - description: 'forbidden login and find globaltype message', - statusCode: 403, - response: createExpectRbacForbidden('globaltype'), - }, - hiddenType: { - description: 'forbidden find hiddentype message', - statusCode: 403, - response: createExpectRbacForbidden('hiddentype'), - }, - unknownType: { - description: 'forbidden find wigwags message', - statusCode: 403, - response: createExpectRbacForbidden('wigwags'), - }, - pageBeyondTotal: { - description: 'forbidden login and find visualization message', - statusCode: 403, - response: createExpectRbacForbidden('visualization'), - }, - unknownSearchField: { - description: 'forbidden login and find url message', - statusCode: 403, - response: createExpectRbacForbidden('url'), - }, - noType: { - description: 'bad request, type is required', - statusCode: 400, - response: expectTypeRequired, - }, - filterWithNotSpaceAwareType: { - description: 'forbidden login and find globaltype message', - statusCode: 403, - response: createExpectRbacForbidden('globaltype'), - }, - filterWithHiddenType: { - description: 'forbidden find hiddentype message', - statusCode: 403, - response: createExpectRbacForbidden('hiddentype'), - }, - filterWithUnknownType: { - description: 'forbidden find wigwags message', - statusCode: 403, - response: createExpectRbacForbidden('wigwags'), - }, - filterWithNoType: { - description: 'bad request, type is required', - statusCode: 400, - response: expectTypeRequired, - }, - filterWithUnAllowedType: { - description: 'forbidden', - statusCode: 403, - response: createExpectRbacForbidden('globaltype'), - }, - }, - }); + describe('_find', () => { + getTestScenarios().securityAndSpaces.forEach(({ spaceId, users }) => { + const suffix = ` within the ${spaceId} space`; + const { unauthorized, authorized, superuser } = createTests(spaceId); + const _addTests = (user: TestUser, tests: FindTestDefinition[]) => { + addTests(`${user.description}${suffix}`, { user, spaceId, tests }); + }; - findTest(`superuser within the ${scenario.spaceId} space`, { - user: scenario.users.superuser, - spaceId: scenario.spaceId, - tests: { - spaceAwareType: { - description: 'only the visualization', - statusCode: 200, - response: createExpectVisualizationResults(scenario.spaceId), - }, - notSpaceAwareType: { - description: 'only the globaltype', - statusCode: 200, - response: expectNotSpaceAwareResults, - }, - hiddenType: { - description: 'empty result', - statusCode: 200, - response: createExpectEmpty(1, 20, 0), - }, - unknownType: { - description: 'empty result', - statusCode: 200, - response: createExpectEmpty(1, 20, 0), - }, - pageBeyondTotal: { - description: 'empty result', - statusCode: 200, - response: createExpectEmpty(100, 100, 1), - }, - unknownSearchField: { - description: 'empty result', - statusCode: 200, - response: createExpectEmpty(1, 20, 0), - }, - noType: { - description: 'bad request, type is required', - statusCode: 400, - response: expectTypeRequired, - }, - filterWithNotSpaceAwareType: { - description: 'only the globaltype', - statusCode: 200, - response: expectNotSpaceAwareResults, - }, - filterWithHiddenType: { - description: 'empty result', - statusCode: 200, - response: createExpectEmpty(1, 20, 0), - }, - filterWithUnknownType: { - description: 'empty result', - statusCode: 200, - response: createExpectEmpty(1, 20, 0), - }, - filterWithNoType: { - description: 'bad request, type is required', - statusCode: 400, - response: expectTypeRequired, - }, - filterWithUnAllowedType: { - description: 'Bad Request', - statusCode: 400, - response: expectFilterWrongTypeError, - }, - }, + [users.noAccess, users.legacyAll, users.allAtOtherSpace].forEach(user => { + _addTests(user, unauthorized); }); - - findTest(`legacy user within the ${scenario.spaceId} space`, { - user: scenario.users.legacyAll, - spaceId: scenario.spaceId, - tests: { - spaceAwareType: { - description: 'forbidden login and find visualization message', - statusCode: 403, - response: createExpectRbacForbidden('visualization'), - }, - notSpaceAwareType: { - description: 'forbidden login and find globaltype message', - statusCode: 403, - response: createExpectRbacForbidden('globaltype'), - }, - hiddenType: { - description: 'forbidden find hiddentype message', - statusCode: 403, - response: createExpectRbacForbidden('hiddentype'), - }, - unknownType: { - description: 'forbidden find wigwags message', - statusCode: 403, - response: createExpectRbacForbidden('wigwags'), - }, - pageBeyondTotal: { - description: 'forbidden login and find visualization message', - statusCode: 403, - response: createExpectRbacForbidden('visualization'), - }, - unknownSearchField: { - description: 'forbidden login and find url message', - statusCode: 403, - response: createExpectRbacForbidden('url'), - }, - noType: { - description: 'bad request, type is required', - statusCode: 400, - response: expectTypeRequired, - }, - filterWithNotSpaceAwareType: { - description: 'forbidden login and find globaltype message', - statusCode: 403, - response: createExpectRbacForbidden('globaltype'), - }, - filterWithHiddenType: { - description: 'forbidden find hiddentype message', - statusCode: 403, - response: createExpectRbacForbidden('hiddentype'), - }, - filterWithUnknownType: { - description: 'forbidden find wigwags message', - statusCode: 403, - response: createExpectRbacForbidden('wigwags'), - }, - filterWithNoType: { - description: 'bad request, type is required', - statusCode: 400, - response: expectTypeRequired, - }, - filterWithUnAllowedType: { - description: 'forbidden', - statusCode: 403, - response: createExpectRbacForbidden('globaltype'), - }, - }, - }); - - findTest(`dual-privileges user within the ${scenario.spaceId} space`, { - user: scenario.users.dualAll, - spaceId: scenario.spaceId, - tests: { - spaceAwareType: { - description: 'only the visualization', - statusCode: 200, - response: createExpectVisualizationResults(scenario.spaceId), - }, - notSpaceAwareType: { - description: 'only the globaltype', - statusCode: 200, - response: expectNotSpaceAwareResults, - }, - hiddenType: { - description: 'forbidden find hiddentype message', - statusCode: 403, - response: createExpectRbacForbidden('hiddentype'), - }, - unknownType: { - description: 'forbidden find wigwags message', - statusCode: 403, - response: createExpectRbacForbidden('wigwags'), - }, - pageBeyondTotal: { - description: 'empty result', - statusCode: 200, - response: createExpectEmpty(100, 100, 1), - }, - unknownSearchField: { - description: 'empty result', - statusCode: 200, - response: createExpectEmpty(1, 20, 0), - }, - noType: { - description: 'bad request, type is required', - statusCode: 400, - response: expectTypeRequired, - }, - filterWithNotSpaceAwareType: { - description: 'only the globaltype', - statusCode: 200, - response: expectNotSpaceAwareResults, - }, - filterWithHiddenType: { - description: 'forbidden find hiddentype message', - statusCode: 403, - response: createExpectRbacForbidden('hiddentype'), - }, - filterWithUnknownType: { - description: 'forbidden find wigwags message', - statusCode: 403, - response: createExpectRbacForbidden('wigwags'), - }, - filterWithNoType: { - description: 'bad request, type is required', - statusCode: 400, - response: expectTypeRequired, - }, - filterWithUnAllowedType: { - description: 'Bad Request', - statusCode: 400, - response: expectFilterWrongTypeError, - }, - }, - }); - - findTest(`dual-privileges readonly user within the ${scenario.spaceId} space`, { - user: scenario.users.dualRead, - spaceId: scenario.spaceId, - tests: { - spaceAwareType: { - description: 'only the visualization', - statusCode: 200, - response: createExpectVisualizationResults(scenario.spaceId), - }, - notSpaceAwareType: { - description: 'only the globaltype', - statusCode: 200, - response: expectNotSpaceAwareResults, - }, - hiddenType: { - description: 'forbidden find hiddentype message', - statusCode: 403, - response: createExpectRbacForbidden('hiddentype'), - }, - unknownType: { - description: 'forbidden find wigwags message', - statusCode: 403, - response: createExpectRbacForbidden('wigwags'), - }, - pageBeyondTotal: { - description: 'empty result', - statusCode: 200, - response: createExpectEmpty(100, 100, 1), - }, - unknownSearchField: { - description: 'empty result', - statusCode: 200, - response: createExpectEmpty(1, 20, 0), - }, - noType: { - description: 'bad request, type is required', - statusCode: 400, - response: expectTypeRequired, - }, - filterWithNotSpaceAwareType: { - description: 'only the globaltype', - statusCode: 200, - response: expectNotSpaceAwareResults, - }, - filterWithHiddenType: { - description: 'forbidden find hiddentype message', - statusCode: 403, - response: createExpectRbacForbidden('hiddentype'), - }, - filterWithUnknownType: { - description: 'forbidden find wigwags message', - statusCode: 403, - response: createExpectRbacForbidden('wigwags'), - }, - filterWithNoType: { - description: 'bad request, type is required', - statusCode: 400, - response: expectTypeRequired, - }, - filterWithUnAllowedType: { - description: 'Bad Request', - statusCode: 400, - response: expectFilterWrongTypeError, - }, - }, - }); - - findTest(`rbac user with all globally within the ${scenario.spaceId} space`, { - user: scenario.users.allGlobally, - spaceId: scenario.spaceId, - tests: { - spaceAwareType: { - description: 'only the visualization', - statusCode: 200, - response: createExpectVisualizationResults(scenario.spaceId), - }, - notSpaceAwareType: { - description: 'only the globaltype', - statusCode: 200, - response: expectNotSpaceAwareResults, - }, - hiddenType: { - description: 'forbidden find hiddentype message', - statusCode: 403, - response: createExpectRbacForbidden('hiddentype'), - }, - unknownType: { - description: 'forbidden find wigwags message', - statusCode: 403, - response: createExpectRbacForbidden('wigwags'), - }, - pageBeyondTotal: { - description: 'empty result', - statusCode: 200, - response: createExpectEmpty(100, 100, 1), - }, - unknownSearchField: { - description: 'empty result', - statusCode: 200, - response: createExpectEmpty(1, 20, 0), - }, - noType: { - description: 'bad request, type is required', - statusCode: 400, - response: expectTypeRequired, - }, - filterWithNotSpaceAwareType: { - description: 'only the globaltype', - statusCode: 200, - response: expectNotSpaceAwareResults, - }, - filterWithHiddenType: { - description: 'forbidden find hiddentype message', - statusCode: 403, - response: createExpectRbacForbidden('hiddentype'), - }, - filterWithUnknownType: { - description: 'forbidden find wigwags message', - statusCode: 403, - response: createExpectRbacForbidden('wigwags'), - }, - filterWithNoType: { - description: 'bad request, type is required', - statusCode: 400, - response: expectTypeRequired, - }, - filterWithUnAllowedType: { - description: 'Bad Request', - statusCode: 400, - response: expectFilterWrongTypeError, - }, - }, - }); - - findTest(`rbac user with read globally within the ${scenario.spaceId} space`, { - user: scenario.users.readGlobally, - spaceId: scenario.spaceId, - tests: { - spaceAwareType: { - description: 'only the visualization', - statusCode: 200, - response: createExpectVisualizationResults(scenario.spaceId), - }, - notSpaceAwareType: { - description: 'only the globaltype', - statusCode: 200, - response: expectNotSpaceAwareResults, - }, - hiddenType: { - description: 'forbidden find hiddentype message', - statusCode: 403, - response: createExpectRbacForbidden('hiddentype'), - }, - unknownType: { - description: 'forbidden find wigwags message', - statusCode: 403, - response: createExpectRbacForbidden('wigwags'), - }, - pageBeyondTotal: { - description: 'empty result', - statusCode: 200, - response: createExpectEmpty(100, 100, 1), - }, - unknownSearchField: { - description: 'empty result', - statusCode: 200, - response: createExpectEmpty(1, 20, 0), - }, - noType: { - description: 'bad request, type is required', - statusCode: 400, - response: expectTypeRequired, - }, - filterWithNotSpaceAwareType: { - description: 'only the globaltype', - statusCode: 200, - response: expectNotSpaceAwareResults, - }, - filterWithHiddenType: { - description: 'forbidden find hiddentype message', - statusCode: 403, - response: createExpectRbacForbidden('hiddentype'), - }, - filterWithUnknownType: { - description: 'forbidden find wigwags message', - statusCode: 403, - response: createExpectRbacForbidden('wigwags'), - }, - filterWithNoType: { - description: 'bad request, type is required', - statusCode: 400, - response: expectTypeRequired, - }, - filterWithUnAllowedType: { - description: 'Bad Request', - statusCode: 400, - response: expectFilterWrongTypeError, - }, - }, - }); - - findTest(`rbac user with all at the space within the ${scenario.spaceId} space`, { - user: scenario.users.allAtSpace, - spaceId: scenario.spaceId, - tests: { - spaceAwareType: { - description: 'only the visualization', - statusCode: 200, - response: createExpectVisualizationResults(scenario.spaceId), - }, - notSpaceAwareType: { - description: 'only the globaltype', - statusCode: 200, - response: expectNotSpaceAwareResults, - }, - hiddenType: { - description: 'forbidden find hiddentype message', - statusCode: 403, - response: createExpectRbacForbidden('hiddentype'), - }, - unknownType: { - description: 'forbidden find wigwags message', - statusCode: 403, - response: createExpectRbacForbidden('wigwags'), - }, - pageBeyondTotal: { - description: 'empty result', - statusCode: 200, - response: createExpectEmpty(100, 100, 1), - }, - unknownSearchField: { - description: 'empty result', - statusCode: 200, - response: createExpectEmpty(1, 20, 0), - }, - noType: { - description: 'bad request, type is required', - statusCode: 400, - response: expectTypeRequired, - }, - filterWithNotSpaceAwareType: { - description: 'only the globaltype', - statusCode: 200, - response: expectNotSpaceAwareResults, - }, - filterWithHiddenType: { - description: 'forbidden find hiddentype message', - statusCode: 403, - response: createExpectRbacForbidden('hiddentype'), - }, - filterWithUnknownType: { - description: 'forbidden find wigwags message', - statusCode: 403, - response: createExpectRbacForbidden('wigwags'), - }, - filterWithNoType: { - description: 'bad request, type is required', - statusCode: 400, - response: expectTypeRequired, - }, - filterWithUnAllowedType: { - description: 'Bad Request', - statusCode: 400, - response: expectFilterWrongTypeError, - }, - }, - }); - - findTest(`rbac user with read at the space within the ${scenario.spaceId} space`, { - user: scenario.users.readAtSpace, - spaceId: scenario.spaceId, - tests: { - spaceAwareType: { - description: 'only the visualization', - statusCode: 200, - response: createExpectVisualizationResults(scenario.spaceId), - }, - notSpaceAwareType: { - description: 'only the globaltype', - statusCode: 200, - response: expectNotSpaceAwareResults, - }, - hiddenType: { - description: 'forbidden find hiddentype message', - statusCode: 403, - response: createExpectRbacForbidden('hiddentype'), - }, - unknownType: { - description: 'forbidden find wigwags message', - statusCode: 403, - response: createExpectRbacForbidden('wigwags'), - }, - pageBeyondTotal: { - description: 'empty result', - statusCode: 200, - response: createExpectEmpty(100, 100, 1), - }, - unknownSearchField: { - description: 'empty result', - statusCode: 200, - response: createExpectEmpty(1, 20, 0), - }, - noType: { - description: 'bad request, type is required', - statusCode: 400, - response: expectTypeRequired, - }, - filterWithNotSpaceAwareType: { - description: 'only the globaltype', - statusCode: 200, - response: expectNotSpaceAwareResults, - }, - filterWithHiddenType: { - description: 'forbidden find hiddentype message', - statusCode: 403, - response: createExpectRbacForbidden('hiddentype'), - }, - filterWithUnknownType: { - description: 'forbidden find wigwags message', - statusCode: 403, - response: createExpectRbacForbidden('wigwags'), - }, - filterWithNoType: { - description: 'bad request, type is required', - statusCode: 400, - response: expectTypeRequired, - }, - filterWithUnAllowedType: { - description: 'Bad Request', - statusCode: 400, - response: expectFilterWrongTypeError, - }, - }, - }); - - findTest(`rbac user with all at other space within the ${scenario.spaceId} space`, { - user: scenario.users.allAtOtherSpace, - spaceId: scenario.spaceId, - tests: { - spaceAwareType: { - description: 'forbidden login and find visualization message', - statusCode: 403, - response: createExpectRbacForbidden('visualization'), - }, - notSpaceAwareType: { - description: 'forbidden login and find globaltype message', - statusCode: 403, - response: createExpectRbacForbidden('globaltype'), - }, - hiddenType: { - description: 'forbidden find hiddentype message', - statusCode: 403, - response: createExpectRbacForbidden('hiddentype'), - }, - unknownType: { - description: 'forbidden find wigwags message', - statusCode: 403, - response: createExpectRbacForbidden('wigwags'), - }, - pageBeyondTotal: { - description: 'forbidden login and find visualization message', - statusCode: 403, - response: createExpectRbacForbidden('visualization'), - }, - unknownSearchField: { - description: 'forbidden login and find url message', - statusCode: 403, - response: createExpectRbacForbidden('url'), - }, - noType: { - description: 'bad request, type is required', - statusCode: 400, - response: expectTypeRequired, - }, - filterWithNotSpaceAwareType: { - description: 'forbidden login and find globaltype message', - statusCode: 403, - response: createExpectRbacForbidden('globaltype'), - }, - filterWithHiddenType: { - description: 'forbidden find hiddentype message', - statusCode: 403, - response: createExpectRbacForbidden('hiddentype'), - }, - filterWithUnknownType: { - description: 'forbidden find wigwags message', - statusCode: 403, - response: createExpectRbacForbidden('wigwags'), - }, - filterWithNoType: { - description: 'bad request, type is required', - statusCode: 400, - response: expectTypeRequired, - }, - filterWithUnAllowedType: { - description: 'forbidden', - statusCode: 403, - response: createExpectRbacForbidden('globaltype'), - }, - }, + [ + users.dualAll, + users.dualRead, + users.allGlobally, + users.readGlobally, + users.allAtSpace, + users.readAtSpace, + ].forEach(user => { + _addTests(user, authorized); }); + _addTests(users.superuser, superuser); }); }); } diff --git a/x-pack/test/saved_object_api_integration/security_and_spaces/apis/get.ts b/x-pack/test/saved_object_api_integration/security_and_spaces/apis/get.ts index 9667abcc5e57a..9e3203e147493 100644 --- a/x-pack/test/saved_object_api_integration/security_and_spaces/apis/get.ts +++ b/x-pack/test/saved_object_api_integration/security_and_spaces/apis/get.ts @@ -4,289 +4,84 @@ * you may not use this file except in compliance with the Elastic License. */ -import { AUTHENTICATION } from '../../common/lib/authentication'; import { SPACES } from '../../common/lib/spaces'; +import { testCaseFailures, getTestScenarios } from '../../common/lib/saved_object_test_utils'; +import { TestUser } from '../../common/lib/types'; import { FtrProviderContext } from '../../common/ftr_provider_context'; -import { getTestSuiteFactory } from '../../common/suites/get'; +import { + getTestSuiteFactory, + TEST_CASES as CASES, + GetTestDefinition, +} from '../../common/suites/get'; + +const { + DEFAULT: { spaceId: DEFAULT_SPACE_ID }, + SPACE_1: { spaceId: SPACE_1_ID }, + SPACE_2: { spaceId: SPACE_2_ID }, +} = SPACES; +const { fail404 } = testCaseFailures; + +const createTestCases = (spaceId: string) => { + // for each permitted (non-403) outcome, if failure !== undefined then we expect + // to receive an error; otherwise, we expect to receive a success result + const normalTypes = [ + { ...CASES.SINGLE_NAMESPACE_DEFAULT_SPACE, ...fail404(spaceId !== DEFAULT_SPACE_ID) }, + { ...CASES.SINGLE_NAMESPACE_SPACE_1, ...fail404(spaceId !== SPACE_1_ID) }, + { ...CASES.SINGLE_NAMESPACE_SPACE_2, ...fail404(spaceId !== SPACE_2_ID) }, + { + ...CASES.MULTI_NAMESPACE_DEFAULT_AND_SPACE_1, + ...fail404(spaceId !== DEFAULT_SPACE_ID && spaceId !== SPACE_1_ID), + }, + { ...CASES.MULTI_NAMESPACE_ONLY_SPACE_1, ...fail404(spaceId !== SPACE_1_ID) }, + { ...CASES.MULTI_NAMESPACE_ONLY_SPACE_2, ...fail404(spaceId !== SPACE_2_ID) }, + CASES.NAMESPACE_AGNOSTIC, + { ...CASES.DOES_NOT_EXIST, ...fail404() }, + ]; + const hiddenType = [{ ...CASES.HIDDEN, ...fail404() }]; + const allTypes = normalTypes.concat(hiddenType); + return { normalTypes, hiddenType, allTypes }; +}; export default function({ getService }: FtrProviderContext) { const supertest = getService('supertestWithoutAuth'); const esArchiver = getService('esArchiver'); - const { - createExpectDoesntExistNotFound, - createExpectSpaceAwareResults, - createExpectNotSpaceAwareResults, - expectSpaceAwareRbacForbidden, - expectNotSpaceAwareRbacForbidden, - expectDoesntExistRbacForbidden, - expectHiddenTypeRbacForbidden, - expectHiddenTypeNotFound, - getTest, - } = getTestSuiteFactory(esArchiver, supertest); - - describe('get', () => { - [ - { - spaceId: SPACES.DEFAULT.spaceId, - users: { - noAccess: AUTHENTICATION.NOT_A_KIBANA_USER, - superuser: AUTHENTICATION.SUPERUSER, - legacyAll: AUTHENTICATION.KIBANA_LEGACY_USER, - allGlobally: AUTHENTICATION.KIBANA_RBAC_USER, - readGlobally: AUTHENTICATION.KIBANA_RBAC_DASHBOARD_ONLY_USER, - dualAll: AUTHENTICATION.KIBANA_DUAL_PRIVILEGES_USER, - dualRead: AUTHENTICATION.KIBANA_DUAL_PRIVILEGES_DASHBOARD_ONLY_USER, - allAtSpace: AUTHENTICATION.KIBANA_RBAC_DEFAULT_SPACE_ALL_USER, - readAtSpace: AUTHENTICATION.KIBANA_RBAC_DEFAULT_SPACE_READ_USER, - allAtOtherSpace: AUTHENTICATION.KIBANA_RBAC_SPACE_1_ALL_USER, - }, - }, - { - spaceId: SPACES.SPACE_1.spaceId, - users: { - noAccess: AUTHENTICATION.NOT_A_KIBANA_USER, - superuser: AUTHENTICATION.SUPERUSER, - legacyAll: AUTHENTICATION.KIBANA_LEGACY_USER, - allGlobally: AUTHENTICATION.KIBANA_RBAC_USER, - readGlobally: AUTHENTICATION.KIBANA_RBAC_DASHBOARD_ONLY_USER, - dualAll: AUTHENTICATION.KIBANA_DUAL_PRIVILEGES_USER, - dualRead: AUTHENTICATION.KIBANA_DUAL_PRIVILEGES_DASHBOARD_ONLY_USER, - allAtSpace: AUTHENTICATION.KIBANA_RBAC_SPACE_1_ALL_USER, - readAtSpace: AUTHENTICATION.KIBANA_RBAC_SPACE_1_READ_USER, - allAtOtherSpace: AUTHENTICATION.KIBANA_RBAC_DEFAULT_SPACE_ALL_USER, - }, - }, - ].forEach(scenario => { - getTest(`user with no access within the ${scenario.spaceId} space`, { - user: scenario.users.noAccess, - spaceId: scenario.spaceId, - tests: { - spaceAware: { - statusCode: 403, - response: expectSpaceAwareRbacForbidden, - }, - notSpaceAware: { - statusCode: 403, - response: expectNotSpaceAwareRbacForbidden, - }, - hiddenType: { - statusCode: 403, - response: expectHiddenTypeRbacForbidden, - }, - doesntExist: { - statusCode: 403, - response: expectDoesntExistRbacForbidden, - }, - }, - }); - - getTest(`superuser within the ${scenario.spaceId} space`, { - user: scenario.users.superuser, - spaceId: scenario.spaceId, - tests: { - spaceAware: { - statusCode: 200, - response: createExpectSpaceAwareResults(scenario.spaceId), - }, - notSpaceAware: { - statusCode: 200, - response: createExpectNotSpaceAwareResults(scenario.spaceId), - }, - hiddenType: { - statusCode: 404, - response: expectHiddenTypeNotFound, - }, - doesntExist: { - statusCode: 404, - response: createExpectDoesntExistNotFound(scenario.spaceId), - }, - }, - }); - - getTest(`legacy user within the ${scenario.spaceId} space`, { - user: scenario.users.legacyAll, - spaceId: scenario.spaceId, - tests: { - spaceAware: { - statusCode: 403, - response: expectSpaceAwareRbacForbidden, - }, - notSpaceAware: { - statusCode: 403, - response: expectNotSpaceAwareRbacForbidden, - }, - hiddenType: { - statusCode: 403, - response: expectHiddenTypeRbacForbidden, - }, - doesntExist: { - statusCode: 403, - response: expectDoesntExistRbacForbidden, - }, - }, - }); - - getTest(`dual-privileges user within the ${scenario.spaceId} space`, { - user: scenario.users.dualAll, - spaceId: scenario.spaceId, - tests: { - spaceAware: { - statusCode: 200, - response: createExpectSpaceAwareResults(scenario.spaceId), - }, - notSpaceAware: { - statusCode: 200, - response: createExpectNotSpaceAwareResults(scenario.spaceId), - }, - hiddenType: { - statusCode: 403, - response: expectHiddenTypeRbacForbidden, - }, - doesntExist: { - statusCode: 404, - response: createExpectDoesntExistNotFound(scenario.spaceId), - }, - }, - }); + const { addTests, createTestDefinitions } = getTestSuiteFactory(esArchiver, supertest); + const createTests = (spaceId: string) => { + const { normalTypes, hiddenType, allTypes } = createTestCases(spaceId); + // use singleRequest to reduce execution time and/or test combined cases + return { + unauthorized: createTestDefinitions(allTypes, true), + authorized: [ + createTestDefinitions(normalTypes, false), + createTestDefinitions(hiddenType, true), + ].flat(), + superuser: createTestDefinitions(allTypes, false), + }; + }; - getTest(`dual-privileges readonly user within the ${scenario.spaceId} space`, { - user: scenario.users.dualRead, - spaceId: scenario.spaceId, - tests: { - spaceAware: { - statusCode: 200, - response: createExpectSpaceAwareResults(scenario.spaceId), - }, - notSpaceAware: { - statusCode: 200, - response: createExpectNotSpaceAwareResults(scenario.spaceId), - }, - hiddenType: { - statusCode: 403, - response: expectHiddenTypeRbacForbidden, - }, - doesntExist: { - statusCode: 404, - response: createExpectDoesntExistNotFound(scenario.spaceId), - }, - }, - }); + describe('_get', () => { + getTestScenarios().securityAndSpaces.forEach(({ spaceId, users }) => { + const suffix = ` within the ${spaceId} space`; + const { unauthorized, authorized, superuser } = createTests(spaceId); + const _addTests = (user: TestUser, tests: GetTestDefinition[]) => { + addTests(`${user.description}${suffix}`, { user, spaceId, tests }); + }; - getTest(`rbac user with all globally within the ${scenario.spaceId} space`, { - user: scenario.users.allGlobally, - spaceId: scenario.spaceId, - tests: { - spaceAware: { - statusCode: 200, - response: createExpectSpaceAwareResults(scenario.spaceId), - }, - notSpaceAware: { - statusCode: 200, - response: createExpectNotSpaceAwareResults(scenario.spaceId), - }, - hiddenType: { - statusCode: 403, - response: expectHiddenTypeRbacForbidden, - }, - doesntExist: { - statusCode: 404, - response: createExpectDoesntExistNotFound(scenario.spaceId), - }, - }, + [users.noAccess, users.legacyAll, users.allAtOtherSpace].forEach(user => { + _addTests(user, unauthorized); }); - - getTest(`rbac user with read globall within the ${scenario.spaceId} space`, { - user: scenario.users.readGlobally, - spaceId: scenario.spaceId, - tests: { - spaceAware: { - statusCode: 200, - response: createExpectSpaceAwareResults(scenario.spaceId), - }, - notSpaceAware: { - statusCode: 200, - response: createExpectNotSpaceAwareResults(scenario.spaceId), - }, - hiddenType: { - statusCode: 403, - response: expectHiddenTypeRbacForbidden, - }, - doesntExist: { - statusCode: 404, - response: createExpectDoesntExistNotFound(scenario.spaceId), - }, - }, - }); - - getTest(`rbac user with all at the space within the ${scenario.spaceId} space`, { - user: scenario.users.allAtSpace, - spaceId: scenario.spaceId, - tests: { - spaceAware: { - statusCode: 200, - response: createExpectSpaceAwareResults(scenario.spaceId), - }, - notSpaceAware: { - statusCode: 200, - response: createExpectNotSpaceAwareResults(scenario.spaceId), - }, - hiddenType: { - statusCode: 403, - response: expectHiddenTypeRbacForbidden, - }, - doesntExist: { - statusCode: 404, - response: createExpectDoesntExistNotFound(scenario.spaceId), - }, - }, - }); - - getTest(`rbac user with read at the space within the ${scenario.spaceId} space`, { - user: scenario.users.readAtSpace, - spaceId: scenario.spaceId, - tests: { - spaceAware: { - statusCode: 200, - response: createExpectSpaceAwareResults(scenario.spaceId), - }, - notSpaceAware: { - statusCode: 200, - response: createExpectNotSpaceAwareResults(scenario.spaceId), - }, - hiddenType: { - statusCode: 403, - response: expectHiddenTypeRbacForbidden, - }, - doesntExist: { - statusCode: 404, - response: createExpectDoesntExistNotFound(scenario.spaceId), - }, - }, - }); - - getTest(`rbac user with all at other space within the ${scenario.spaceId} space`, { - user: scenario.users.allAtOtherSpace, - spaceId: scenario.spaceId, - tests: { - spaceAware: { - statusCode: 403, - response: expectSpaceAwareRbacForbidden, - }, - notSpaceAware: { - statusCode: 403, - response: expectNotSpaceAwareRbacForbidden, - }, - hiddenType: { - statusCode: 403, - response: expectHiddenTypeRbacForbidden, - }, - doesntExist: { - statusCode: 403, - response: expectDoesntExistRbacForbidden, - }, - }, + [ + users.dualAll, + users.dualRead, + users.allGlobally, + users.readGlobally, + users.allAtSpace, + users.readAtSpace, + ].forEach(user => { + _addTests(user, authorized); }); + _addTests(users.superuser, superuser); }); }); } diff --git a/x-pack/test/saved_object_api_integration/security_and_spaces/apis/import.ts b/x-pack/test/saved_object_api_integration/security_and_spaces/apis/import.ts index 58859c292ce35..10c7f61dce5cc 100644 --- a/x-pack/test/saved_object_api_integration/security_and_spaces/apis/import.ts +++ b/x-pack/test/saved_object_api_integration/security_and_spaces/apis/import.ts @@ -4,245 +4,92 @@ * you may not use this file except in compliance with the Elastic License. */ -import { AUTHENTICATION } from '../../common/lib/authentication'; import { SPACES } from '../../common/lib/spaces'; +import { testCaseFailures, getTestScenarios } from '../../common/lib/saved_object_test_utils'; +import { TestUser } from '../../common/lib/types'; import { FtrProviderContext } from '../../common/ftr_provider_context'; -import { importTestSuiteFactory } from '../../common/suites/import'; +import { + importTestSuiteFactory, + TEST_CASES as CASES, + ImportTestDefinition, +} from '../../common/suites/import'; + +const { + DEFAULT: { spaceId: DEFAULT_SPACE_ID }, + SPACE_1: { spaceId: SPACE_1_ID }, + SPACE_2: { spaceId: SPACE_2_ID }, +} = SPACES; +const { fail400, fail409 } = testCaseFailures; + +const createTestCases = (spaceId: string) => { + // for each permitted (non-403) outcome, if failure !== undefined then we expect + // to receive an error; otherwise, we expect to receive a success result + const importableTypes = [ + { ...CASES.SINGLE_NAMESPACE_DEFAULT_SPACE, ...fail409(spaceId === DEFAULT_SPACE_ID) }, + { ...CASES.SINGLE_NAMESPACE_SPACE_1, ...fail409(spaceId === SPACE_1_ID) }, + { ...CASES.SINGLE_NAMESPACE_SPACE_2, ...fail409(spaceId === SPACE_2_ID) }, + { ...CASES.NAMESPACE_AGNOSTIC, ...fail409() }, + CASES.NEW_SINGLE_NAMESPACE_OBJ, + CASES.NEW_NAMESPACE_AGNOSTIC_OBJ, + ]; + const nonImportableTypes = [ + { ...CASES.MULTI_NAMESPACE_DEFAULT_AND_SPACE_1, ...fail400() }, + { ...CASES.MULTI_NAMESPACE_ONLY_SPACE_1, ...fail400() }, + { ...CASES.MULTI_NAMESPACE_ONLY_SPACE_2, ...fail400() }, + { ...CASES.HIDDEN, ...fail400() }, + { ...CASES.NEW_MULTI_NAMESPACE_OBJ, ...fail400() }, + ]; + const allTypes = importableTypes.concat(nonImportableTypes); + return { importableTypes, nonImportableTypes, allTypes }; +}; export default function({ getService }: FtrProviderContext) { const supertest = getService('supertestWithoutAuth'); const esArchiver = getService('esArchiver'); const es = getService('legacyEs'); - const { - importTest, - createExpectResults, - expectRbacForbidden, - expectUnknownTypeUnsupported: expectUnknownTypeUnsupported, - expectHiddenTypeUnsupported, - } = importTestSuiteFactory(es, esArchiver, supertest); + const { addTests, createTestDefinitions, expectForbidden } = importTestSuiteFactory( + es, + esArchiver, + supertest + ); + const createTests = (spaceId: string) => { + const { importableTypes, nonImportableTypes, allTypes } = createTestCases(spaceId); + // use singleRequest to reduce execution time and/or test combined cases + return { + unauthorized: [ + createTestDefinitions(importableTypes, true, { spaceId }), + createTestDefinitions(nonImportableTypes, false, { spaceId, singleRequest: true }), + createTestDefinitions(allTypes, true, { + spaceId, + singleRequest: true, + responseBodyOverride: expectForbidden(['dashboard', 'globaltype', 'isolatedtype']), + }), + ].flat(), + authorized: createTestDefinitions(allTypes, false, { spaceId, singleRequest: true }), + }; + }; describe('_import', () => { - [ - { - spaceId: SPACES.DEFAULT.spaceId, - users: { - noAccess: AUTHENTICATION.NOT_A_KIBANA_USER, - superuser: AUTHENTICATION.SUPERUSER, - legacyAll: AUTHENTICATION.KIBANA_LEGACY_USER, - allGlobally: AUTHENTICATION.KIBANA_RBAC_USER, - readGlobally: AUTHENTICATION.KIBANA_RBAC_DASHBOARD_ONLY_USER, - dualAll: AUTHENTICATION.KIBANA_DUAL_PRIVILEGES_USER, - dualRead: AUTHENTICATION.KIBANA_DUAL_PRIVILEGES_DASHBOARD_ONLY_USER, - allAtSpace: AUTHENTICATION.KIBANA_RBAC_DEFAULT_SPACE_ALL_USER, - readAtSpace: AUTHENTICATION.KIBANA_RBAC_DEFAULT_SPACE_READ_USER, - allAtOtherSpace: AUTHENTICATION.KIBANA_RBAC_SPACE_1_ALL_USER, - }, - }, - { - spaceId: SPACES.SPACE_1.spaceId, - users: { - noAccess: AUTHENTICATION.NOT_A_KIBANA_USER, - superuser: AUTHENTICATION.SUPERUSER, - legacyAll: AUTHENTICATION.KIBANA_LEGACY_USER, - allGlobally: AUTHENTICATION.KIBANA_RBAC_USER, - readGlobally: AUTHENTICATION.KIBANA_RBAC_DASHBOARD_ONLY_USER, - dualAll: AUTHENTICATION.KIBANA_DUAL_PRIVILEGES_USER, - dualRead: AUTHENTICATION.KIBANA_DUAL_PRIVILEGES_DASHBOARD_ONLY_USER, - allAtSpace: AUTHENTICATION.KIBANA_RBAC_SPACE_1_ALL_USER, - readAtSpace: AUTHENTICATION.KIBANA_RBAC_SPACE_1_READ_USER, - allAtOtherSpace: AUTHENTICATION.KIBANA_RBAC_DEFAULT_SPACE_ALL_USER, - }, - }, - ].forEach(scenario => { - importTest(`user with no access within the ${scenario.spaceId} space`, { - user: scenario.users.noAccess, - spaceId: scenario.spaceId, - tests: { - default: { - statusCode: 403, - response: expectRbacForbidden, - }, - hiddenType: { - statusCode: 403, - response: expectRbacForbidden, - }, - unknownType: { - statusCode: 403, - response: expectRbacForbidden, - }, - }, - }); - - importTest(`superuser within the ${scenario.spaceId} space`, { - user: scenario.users.superuser, - spaceId: scenario.spaceId, - tests: { - default: { - statusCode: 200, - response: createExpectResults(scenario.spaceId), - }, - hiddenType: { - statusCode: 200, - response: expectHiddenTypeUnsupported, - }, - unknownType: { - statusCode: 200, - response: expectUnknownTypeUnsupported, - }, - }, - }); - - importTest(`legacy user within the ${scenario.spaceId} space`, { - user: scenario.users.legacyAll, - spaceId: scenario.spaceId, - tests: { - default: { - statusCode: 403, - response: expectRbacForbidden, - }, - hiddenType: { - statusCode: 403, - response: expectRbacForbidden, - }, - unknownType: { - statusCode: 403, - response: expectRbacForbidden, - }, - }, - }); - - importTest(`dual-privileges user within the ${scenario.spaceId} space`, { - user: scenario.users.dualAll, - spaceId: scenario.spaceId, - tests: { - default: { - statusCode: 200, - response: createExpectResults(scenario.spaceId), - }, - hiddenType: { - statusCode: 200, - response: expectHiddenTypeUnsupported, - }, - unknownType: { - statusCode: 200, - response: expectUnknownTypeUnsupported, - }, - }, - }); + getTestScenarios().securityAndSpaces.forEach(({ spaceId, users }) => { + const suffix = ` within the ${spaceId} space`; + const { unauthorized, authorized } = createTests(spaceId); + const _addTests = (user: TestUser, tests: ImportTestDefinition[]) => { + addTests(`${user.description}${suffix}`, { user, spaceId, tests }); + }; - importTest(`dual-privileges readonly user within the ${scenario.spaceId} space`, { - user: scenario.users.dualRead, - spaceId: scenario.spaceId, - tests: { - default: { - statusCode: 403, - response: expectRbacForbidden, - }, - hiddenType: { - statusCode: 403, - response: expectRbacForbidden, - }, - unknownType: { - statusCode: 403, - response: expectRbacForbidden, - }, - }, + [ + users.noAccess, + users.legacyAll, + users.dualRead, + users.readGlobally, + users.readAtSpace, + users.allAtOtherSpace, + ].forEach(user => { + _addTests(user, unauthorized); }); - - importTest(`rbac user with all globally within the ${scenario.spaceId} space`, { - user: scenario.users.allGlobally, - spaceId: scenario.spaceId, - tests: { - default: { - statusCode: 200, - response: createExpectResults(scenario.spaceId), - }, - hiddenType: { - statusCode: 200, - response: expectHiddenTypeUnsupported, - }, - unknownType: { - statusCode: 200, - response: expectUnknownTypeUnsupported, - }, - }, - }); - - importTest(`rbac user with read globally within the ${scenario.spaceId} space`, { - user: scenario.users.readGlobally, - spaceId: scenario.spaceId, - tests: { - default: { - statusCode: 403, - response: expectRbacForbidden, - }, - hiddenType: { - statusCode: 403, - response: expectRbacForbidden, - }, - unknownType: { - statusCode: 403, - response: expectRbacForbidden, - }, - }, - }); - - importTest(`rbac user with all at the space within the ${scenario.spaceId} space`, { - user: scenario.users.allAtSpace, - spaceId: scenario.spaceId, - tests: { - default: { - statusCode: 200, - response: createExpectResults(scenario.spaceId), - }, - hiddenType: { - statusCode: 200, - response: expectHiddenTypeUnsupported, - }, - unknownType: { - statusCode: 200, - response: expectUnknownTypeUnsupported, - }, - }, - }); - - importTest(`rbac user with read at the space within the ${scenario.spaceId} space`, { - user: scenario.users.readAtSpace, - spaceId: scenario.spaceId, - tests: { - default: { - statusCode: 403, - response: expectRbacForbidden, - }, - hiddenType: { - statusCode: 403, - response: expectRbacForbidden, - }, - unknownType: { - statusCode: 403, - response: expectRbacForbidden, - }, - }, - }); - - importTest(`rbac user with all at other space within the ${scenario.spaceId} space`, { - user: scenario.users.allAtOtherSpace, - spaceId: scenario.spaceId, - tests: { - default: { - statusCode: 403, - response: expectRbacForbidden, - }, - hiddenType: { - statusCode: 403, - response: expectRbacForbidden, - }, - unknownType: { - statusCode: 403, - response: expectRbacForbidden, - }, - }, + [users.dualAll, users.allGlobally, users.allAtSpace, users.superuser].forEach(user => { + _addTests(user, authorized); }); }); }); diff --git a/x-pack/test/saved_object_api_integration/security_and_spaces/apis/index.ts b/x-pack/test/saved_object_api_integration/security_and_spaces/apis/index.ts index bb42c5422ece5..46d7ab6425989 100644 --- a/x-pack/test/saved_object_api_integration/security_and_spaces/apis/index.ts +++ b/x-pack/test/saved_object_api_integration/security_and_spaces/apis/index.ts @@ -20,6 +20,7 @@ export default function({ getService, loadTestFile }: FtrProviderContext) { loadTestFile(require.resolve('./bulk_create')); loadTestFile(require.resolve('./bulk_get')); + loadTestFile(require.resolve('./bulk_update')); loadTestFile(require.resolve('./create')); loadTestFile(require.resolve('./delete')); loadTestFile(require.resolve('./export')); @@ -28,6 +29,5 @@ export default function({ getService, loadTestFile }: FtrProviderContext) { loadTestFile(require.resolve('./import')); loadTestFile(require.resolve('./resolve_import_errors')); loadTestFile(require.resolve('./update')); - loadTestFile(require.resolve('./bulk_update')); }); } diff --git a/x-pack/test/saved_object_api_integration/security_and_spaces/apis/resolve_import_errors.ts b/x-pack/test/saved_object_api_integration/security_and_spaces/apis/resolve_import_errors.ts index 6c91fe6310170..8e8fe874b4317 100644 --- a/x-pack/test/saved_object_api_integration/security_and_spaces/apis/resolve_import_errors.ts +++ b/x-pack/test/saved_object_api_integration/security_and_spaces/apis/resolve_import_errors.ts @@ -4,258 +4,99 @@ * you may not use this file except in compliance with the Elastic License. */ -import { AUTHENTICATION } from '../../common/lib/authentication'; import { SPACES } from '../../common/lib/spaces'; +import { testCaseFailures, getTestScenarios } from '../../common/lib/saved_object_test_utils'; +import { TestUser } from '../../common/lib/types'; import { FtrProviderContext } from '../../common/ftr_provider_context'; -import { resolveImportErrorsTestSuiteFactory } from '../../common/suites/resolve_import_errors'; +import { + resolveImportErrorsTestSuiteFactory, + TEST_CASES as CASES, + ResolveImportErrorsTestDefinition, +} from '../../common/suites/resolve_import_errors'; + +const { + DEFAULT: { spaceId: DEFAULT_SPACE_ID }, + SPACE_1: { spaceId: SPACE_1_ID }, + SPACE_2: { spaceId: SPACE_2_ID }, +} = SPACES; +const { fail400, fail409 } = testCaseFailures; + +const createTestCases = (overwrite: boolean, spaceId: string) => { + // for each permitted (non-403) outcome, if failure !== undefined then we expect + // to receive an error; otherwise, we expect to receive a success result + const importableTypes = [ + { + ...CASES.SINGLE_NAMESPACE_DEFAULT_SPACE, + ...fail409(!overwrite && spaceId === DEFAULT_SPACE_ID), + }, + { ...CASES.SINGLE_NAMESPACE_SPACE_1, ...fail409(!overwrite && spaceId === SPACE_1_ID) }, + { ...CASES.SINGLE_NAMESPACE_SPACE_2, ...fail409(!overwrite && spaceId === SPACE_2_ID) }, + { ...CASES.NAMESPACE_AGNOSTIC, ...fail409(!overwrite) }, + CASES.NEW_SINGLE_NAMESPACE_OBJ, + CASES.NEW_NAMESPACE_AGNOSTIC_OBJ, + ]; + const nonImportableTypes = [ + { ...CASES.MULTI_NAMESPACE_DEFAULT_AND_SPACE_1, ...fail400() }, + { ...CASES.MULTI_NAMESPACE_ONLY_SPACE_1, ...fail400() }, + { ...CASES.MULTI_NAMESPACE_ONLY_SPACE_2, ...fail400() }, + { ...CASES.HIDDEN, ...fail400() }, + { ...CASES.NEW_MULTI_NAMESPACE_OBJ, ...fail400() }, + ]; + const allTypes = importableTypes.concat(nonImportableTypes); + return { importableTypes, nonImportableTypes, allTypes }; +}; export default function({ getService }: FtrProviderContext) { const supertest = getService('supertestWithoutAuth'); const esArchiver = getService('esArchiver'); const es = getService('legacyEs'); - const { - resolveImportErrorsTest, - createExpectResults, - expectRbacForbidden, - expectUnknownTypeUnsupported, - expectHiddenTypeUnsupported, - } = resolveImportErrorsTestSuiteFactory(es, esArchiver, supertest); + const { addTests, createTestDefinitions, expectForbidden } = resolveImportErrorsTestSuiteFactory( + es, + esArchiver, + supertest + ); + const createTests = (overwrite: boolean, spaceId: string) => { + const { importableTypes, nonImportableTypes, allTypes } = createTestCases(overwrite, spaceId); + const singleRequest = true; + // use singleRequest to reduce execution time and/or test combined cases + return { + unauthorized: [ + createTestDefinitions(importableTypes, true, overwrite, { spaceId }), + createTestDefinitions(nonImportableTypes, false, overwrite, { spaceId, singleRequest }), + createTestDefinitions(allTypes, true, overwrite, { + spaceId, + singleRequest, + responseBodyOverride: expectForbidden(['dashboard', 'globaltype', 'isolatedtype']), + }), + ].flat(), + authorized: createTestDefinitions(allTypes, false, overwrite, { spaceId, singleRequest }), + }; + }; describe('_resolve_import_errors', () => { - [ - { - spaceId: SPACES.DEFAULT.spaceId, - users: { - noAccess: AUTHENTICATION.NOT_A_KIBANA_USER, - superuser: AUTHENTICATION.SUPERUSER, - legacyAll: AUTHENTICATION.KIBANA_LEGACY_USER, - allGlobally: AUTHENTICATION.KIBANA_RBAC_USER, - readGlobally: AUTHENTICATION.KIBANA_RBAC_DASHBOARD_ONLY_USER, - dualAll: AUTHENTICATION.KIBANA_DUAL_PRIVILEGES_USER, - dualRead: AUTHENTICATION.KIBANA_DUAL_PRIVILEGES_DASHBOARD_ONLY_USER, - allAtSpace: AUTHENTICATION.KIBANA_RBAC_DEFAULT_SPACE_ALL_USER, - readAtSpace: AUTHENTICATION.KIBANA_RBAC_DEFAULT_SPACE_READ_USER, - allAtOtherSpace: AUTHENTICATION.KIBANA_RBAC_SPACE_1_ALL_USER, - }, - }, - { - spaceId: SPACES.SPACE_1.spaceId, - users: { - noAccess: AUTHENTICATION.NOT_A_KIBANA_USER, - superuser: AUTHENTICATION.SUPERUSER, - legacyAll: AUTHENTICATION.KIBANA_LEGACY_USER, - allGlobally: AUTHENTICATION.KIBANA_RBAC_USER, - readGlobally: AUTHENTICATION.KIBANA_RBAC_DASHBOARD_ONLY_USER, - dualAll: AUTHENTICATION.KIBANA_DUAL_PRIVILEGES_USER, - dualRead: AUTHENTICATION.KIBANA_DUAL_PRIVILEGES_DASHBOARD_ONLY_USER, - allAtSpace: AUTHENTICATION.KIBANA_RBAC_SPACE_1_ALL_USER, - readAtSpace: AUTHENTICATION.KIBANA_RBAC_SPACE_1_READ_USER, - allAtOtherSpace: AUTHENTICATION.KIBANA_RBAC_DEFAULT_SPACE_ALL_USER, - }, - }, - ].forEach(scenario => { - resolveImportErrorsTest(`user with no access within the ${scenario.spaceId} space`, { - user: scenario.users.noAccess, - spaceId: scenario.spaceId, - tests: { - default: { - statusCode: 403, - response: expectRbacForbidden, - }, - hiddenType: { - statusCode: 403, - response: expectRbacForbidden, - }, - unknownType: { - statusCode: 403, - response: expectRbacForbidden, - }, - }, - }); - - resolveImportErrorsTest(`superuser within the ${scenario.spaceId} space`, { - user: scenario.users.superuser, - spaceId: scenario.spaceId, - tests: { - default: { - statusCode: 200, - response: createExpectResults(scenario.spaceId), - }, - hiddenType: { - statusCode: 200, - response: expectHiddenTypeUnsupported, - }, - unknownType: { - statusCode: 200, - response: expectUnknownTypeUnsupported, - }, - }, - }); - - resolveImportErrorsTest(`legacy user within the ${scenario.spaceId} space`, { - user: scenario.users.legacyAll, - spaceId: scenario.spaceId, - tests: { - default: { - statusCode: 403, - response: expectRbacForbidden, - }, - hiddenType: { - statusCode: 403, - response: expectRbacForbidden, - }, - unknownType: { - statusCode: 403, - response: expectRbacForbidden, - }, - }, - }); - - resolveImportErrorsTest(`dual-privileges user within the ${scenario.spaceId} space`, { - user: scenario.users.dualAll, - spaceId: scenario.spaceId, - tests: { - default: { - statusCode: 200, - response: createExpectResults(scenario.spaceId), - }, - hiddenType: { - statusCode: 200, - response: expectHiddenTypeUnsupported, - }, - unknownType: { - statusCode: 200, - response: expectUnknownTypeUnsupported, - }, - }, - }); - - resolveImportErrorsTest( - `dual-privileges readonly user within the ${scenario.spaceId} space`, - { - user: scenario.users.dualRead, - spaceId: scenario.spaceId, - tests: { - default: { - statusCode: 403, - response: expectRbacForbidden, - }, - hiddenType: { - statusCode: 403, - response: expectRbacForbidden, - }, - unknownType: { - statusCode: 403, - response: expectRbacForbidden, - }, - }, - } - ); - - resolveImportErrorsTest(`rbac user with all globally within the ${scenario.spaceId} space`, { - user: scenario.users.allGlobally, - spaceId: scenario.spaceId, - tests: { - default: { - statusCode: 200, - response: createExpectResults(scenario.spaceId), - }, - hiddenType: { - statusCode: 200, - response: expectHiddenTypeUnsupported, - }, - unknownType: { - statusCode: 200, - response: expectUnknownTypeUnsupported, - }, - }, - }); - - resolveImportErrorsTest(`rbac user with read globally within the ${scenario.spaceId} space`, { - user: scenario.users.readGlobally, - spaceId: scenario.spaceId, - tests: { - default: { - statusCode: 403, - response: expectRbacForbidden, - }, - hiddenType: { - statusCode: 403, - response: expectRbacForbidden, - }, - unknownType: { - statusCode: 403, - response: expectRbacForbidden, - }, - }, - }); - - resolveImportErrorsTest( - `rbac user with all at the space within the ${scenario.spaceId} space`, - { - user: scenario.users.allAtSpace, - spaceId: scenario.spaceId, - tests: { - default: { - statusCode: 200, - response: createExpectResults(scenario.spaceId), - }, - hiddenType: { - statusCode: 200, - response: expectHiddenTypeUnsupported, - }, - unknownType: { - statusCode: 200, - response: expectUnknownTypeUnsupported, - }, - }, - } - ); - - resolveImportErrorsTest( - `rbac user with read at the space within the ${scenario.spaceId} space`, - { - user: scenario.users.readAtSpace, - spaceId: scenario.spaceId, - tests: { - default: { - statusCode: 403, - response: expectRbacForbidden, - }, - hiddenType: { - statusCode: 403, - response: expectRbacForbidden, - }, - unknownType: { - statusCode: 403, - response: expectRbacForbidden, - }, - }, - } - ); + getTestScenarios([false, true]).securityAndSpaces.forEach( + ({ spaceId, users, modifier: overwrite }) => { + const suffix = ` within the ${spaceId} space${overwrite ? ' with overwrite enabled' : ''}`; + const { unauthorized, authorized } = createTests(overwrite!, spaceId); + const _addTests = (user: TestUser, tests: ResolveImportErrorsTestDefinition[]) => { + addTests(`${user.description}${suffix}`, { user, spaceId, tests }); + }; - resolveImportErrorsTest( - `rbac user with all at other space within the ${scenario.spaceId} space`, - { - user: scenario.users.allAtOtherSpace, - spaceId: scenario.spaceId, - tests: { - default: { - statusCode: 403, - response: expectRbacForbidden, - }, - hiddenType: { - statusCode: 403, - response: expectRbacForbidden, - }, - unknownType: { - statusCode: 403, - response: expectRbacForbidden, - }, - }, - } - ); - }); + [ + users.noAccess, + users.legacyAll, + users.dualRead, + users.readGlobally, + users.readAtSpace, + users.allAtOtherSpace, + ].forEach(user => { + _addTests(user, unauthorized); + }); + [users.dualAll, users.allGlobally, users.allAtSpace, users.superuser].forEach(user => { + _addTests(user, authorized); + }); + } + ); }); } diff --git a/x-pack/test/saved_object_api_integration/security_and_spaces/apis/update.ts b/x-pack/test/saved_object_api_integration/security_and_spaces/apis/update.ts index 8eb06e41e2a41..21f354d2a8e76 100644 --- a/x-pack/test/saved_object_api_integration/security_and_spaces/apis/update.ts +++ b/x-pack/test/saved_object_api_integration/security_and_spaces/apis/update.ts @@ -4,289 +4,83 @@ * you may not use this file except in compliance with the Elastic License. */ -import { AUTHENTICATION } from '../../common/lib/authentication'; import { SPACES } from '../../common/lib/spaces'; +import { testCaseFailures, getTestScenarios } from '../../common/lib/saved_object_test_utils'; +import { TestUser } from '../../common/lib/types'; import { FtrProviderContext } from '../../common/ftr_provider_context'; -import { updateTestSuiteFactory } from '../../common/suites/update'; +import { + updateTestSuiteFactory, + TEST_CASES as CASES, + UpdateTestDefinition, +} from '../../common/suites/update'; + +const { + DEFAULT: { spaceId: DEFAULT_SPACE_ID }, + SPACE_1: { spaceId: SPACE_1_ID }, + SPACE_2: { spaceId: SPACE_2_ID }, +} = SPACES; +const { fail404 } = testCaseFailures; + +const createTestCases = (spaceId: string) => { + // for each permitted (non-403) outcome, if failure !== undefined then we expect + // to receive an error; otherwise, we expect to receive a success result + const normalTypes = [ + { ...CASES.SINGLE_NAMESPACE_DEFAULT_SPACE, ...fail404(spaceId !== DEFAULT_SPACE_ID) }, + { ...CASES.SINGLE_NAMESPACE_SPACE_1, ...fail404(spaceId !== SPACE_1_ID) }, + { ...CASES.SINGLE_NAMESPACE_SPACE_2, ...fail404(spaceId !== SPACE_2_ID) }, + { + ...CASES.MULTI_NAMESPACE_DEFAULT_AND_SPACE_1, + ...fail404(spaceId !== DEFAULT_SPACE_ID && spaceId !== SPACE_1_ID), + }, + { ...CASES.MULTI_NAMESPACE_ONLY_SPACE_1, ...fail404(spaceId !== SPACE_1_ID) }, + { ...CASES.MULTI_NAMESPACE_ONLY_SPACE_2, ...fail404(spaceId !== SPACE_2_ID) }, + CASES.NAMESPACE_AGNOSTIC, + { ...CASES.DOES_NOT_EXIST, ...fail404() }, + ]; + const hiddenType = [{ ...CASES.HIDDEN, ...fail404() }]; + const allTypes = normalTypes.concat(hiddenType); + return { normalTypes, hiddenType, allTypes }; +}; export default function({ getService }: FtrProviderContext) { const supertest = getService('supertestWithoutAuth'); const esArchiver = getService('esArchiver'); - describe('update', () => { - const { - createExpectDoesntExistNotFound, - expectDoesntExistRbacForbidden, - expectNotSpaceAwareResults, - expectNotSpaceAwareRbacForbidden, - expectSpaceAwareRbacForbidden, - expectSpaceAwareResults, - expectSpaceNotFound, - expectHiddenTypeRbacForbidden, - updateTest, - } = updateTestSuiteFactory(esArchiver, supertest); - - [ - { - spaceId: SPACES.DEFAULT.spaceId, - users: { - noAccess: AUTHENTICATION.NOT_A_KIBANA_USER, - superuser: AUTHENTICATION.SUPERUSER, - legacyAll: AUTHENTICATION.KIBANA_LEGACY_USER, - allGlobally: AUTHENTICATION.KIBANA_RBAC_USER, - readGlobally: AUTHENTICATION.KIBANA_RBAC_DASHBOARD_ONLY_USER, - dualAll: AUTHENTICATION.KIBANA_DUAL_PRIVILEGES_USER, - dualRead: AUTHENTICATION.KIBANA_DUAL_PRIVILEGES_DASHBOARD_ONLY_USER, - allAtSpace: AUTHENTICATION.KIBANA_RBAC_DEFAULT_SPACE_ALL_USER, - readAtSpace: AUTHENTICATION.KIBANA_RBAC_DEFAULT_SPACE_READ_USER, - allAtOtherSpace: AUTHENTICATION.KIBANA_RBAC_SPACE_1_ALL_USER, - }, - }, - { - spaceId: SPACES.SPACE_1.spaceId, - users: { - noAccess: AUTHENTICATION.NOT_A_KIBANA_USER, - superuser: AUTHENTICATION.SUPERUSER, - legacyAll: AUTHENTICATION.KIBANA_LEGACY_USER, - allGlobally: AUTHENTICATION.KIBANA_RBAC_USER, - readGlobally: AUTHENTICATION.KIBANA_RBAC_DASHBOARD_ONLY_USER, - dualAll: AUTHENTICATION.KIBANA_DUAL_PRIVILEGES_USER, - dualRead: AUTHENTICATION.KIBANA_DUAL_PRIVILEGES_DASHBOARD_ONLY_USER, - allAtSpace: AUTHENTICATION.KIBANA_RBAC_SPACE_1_ALL_USER, - readAtSpace: AUTHENTICATION.KIBANA_RBAC_SPACE_1_READ_USER, - allAtOtherSpace: AUTHENTICATION.KIBANA_RBAC_DEFAULT_SPACE_ALL_USER, - }, - }, - ].forEach(scenario => { - updateTest(`user with no access within the ${scenario.spaceId} space`, { - user: scenario.users.noAccess, - spaceId: scenario.spaceId, - tests: { - spaceAware: { - statusCode: 403, - response: expectSpaceAwareRbacForbidden, - }, - notSpaceAware: { - statusCode: 403, - response: expectNotSpaceAwareRbacForbidden, - }, - hiddenType: { - statusCode: 403, - response: expectHiddenTypeRbacForbidden, - }, - doesntExist: { - statusCode: 403, - response: expectDoesntExistRbacForbidden, - }, - }, - }); - - updateTest(`superuser within the ${scenario.spaceId} space`, { - user: scenario.users.superuser, - spaceId: scenario.spaceId, - tests: { - spaceAware: { - statusCode: 200, - response: expectSpaceAwareResults, - }, - notSpaceAware: { - statusCode: 200, - response: expectNotSpaceAwareResults, - }, - hiddenType: { - statusCode: 404, - response: expectSpaceNotFound, - }, - doesntExist: { - statusCode: 404, - response: createExpectDoesntExistNotFound(scenario.spaceId), - }, - }, - }); - - updateTest(`legacy user within the ${scenario.spaceId} space`, { - user: scenario.users.legacyAll, - spaceId: scenario.spaceId, - tests: { - spaceAware: { - statusCode: 403, - response: expectSpaceAwareRbacForbidden, - }, - notSpaceAware: { - statusCode: 403, - response: expectNotSpaceAwareRbacForbidden, - }, - hiddenType: { - statusCode: 403, - response: expectHiddenTypeRbacForbidden, - }, - doesntExist: { - statusCode: 403, - response: expectDoesntExistRbacForbidden, - }, - }, - }); - - updateTest(`dual-privileges user within the ${scenario.spaceId} space`, { - user: scenario.users.dualAll, - spaceId: scenario.spaceId, - tests: { - spaceAware: { - statusCode: 200, - response: expectSpaceAwareResults, - }, - notSpaceAware: { - statusCode: 200, - response: expectNotSpaceAwareResults, - }, - hiddenType: { - statusCode: 403, - response: expectHiddenTypeRbacForbidden, - }, - doesntExist: { - statusCode: 404, - response: createExpectDoesntExistNotFound(scenario.spaceId), - }, - }, - }); + const { addTests, createTestDefinitions } = updateTestSuiteFactory(esArchiver, supertest); + const createTests = (spaceId: string) => { + const { normalTypes, hiddenType, allTypes } = createTestCases(spaceId); + return { + unauthorized: createTestDefinitions(allTypes, true), + authorized: [ + createTestDefinitions(normalTypes, false), + createTestDefinitions(hiddenType, true), + ].flat(), + superuser: createTestDefinitions(allTypes, false), + }; + }; - updateTest(`dual-privileges readonly user within the ${scenario.spaceId} space`, { - user: scenario.users.dualRead, - spaceId: scenario.spaceId, - tests: { - spaceAware: { - statusCode: 403, - response: expectSpaceAwareRbacForbidden, - }, - notSpaceAware: { - statusCode: 403, - response: expectNotSpaceAwareRbacForbidden, - }, - hiddenType: { - statusCode: 403, - response: expectHiddenTypeRbacForbidden, - }, - doesntExist: { - statusCode: 403, - response: expectDoesntExistRbacForbidden, - }, - }, - }); + describe('_update', () => { + getTestScenarios().securityAndSpaces.forEach(({ spaceId, users }) => { + const suffix = ` within the ${spaceId} space`; + const { unauthorized, authorized, superuser } = createTests(spaceId); + const _addTests = (user: TestUser, tests: UpdateTestDefinition[]) => { + addTests(`${user.description}${suffix}`, { user, spaceId, tests }); + }; - updateTest(`rbac user with all globally within the ${scenario.spaceId} space`, { - user: scenario.users.allGlobally, - spaceId: scenario.spaceId, - tests: { - spaceAware: { - statusCode: 200, - response: expectSpaceAwareResults, - }, - notSpaceAware: { - statusCode: 200, - response: expectNotSpaceAwareResults, - }, - hiddenType: { - statusCode: 403, - response: expectHiddenTypeRbacForbidden, - }, - doesntExist: { - statusCode: 404, - response: createExpectDoesntExistNotFound(scenario.spaceId), - }, - }, + [ + users.noAccess, + users.legacyAll, + users.dualRead, + users.readGlobally, + users.readAtSpace, + users.allAtOtherSpace, + ].forEach(user => { + _addTests(user, unauthorized); }); - - updateTest(`rbac user with read globally within the ${scenario.spaceId} space`, { - user: scenario.users.readGlobally, - spaceId: scenario.spaceId, - tests: { - spaceAware: { - statusCode: 403, - response: expectSpaceAwareRbacForbidden, - }, - notSpaceAware: { - statusCode: 403, - response: expectNotSpaceAwareRbacForbidden, - }, - hiddenType: { - statusCode: 403, - response: expectHiddenTypeRbacForbidden, - }, - doesntExist: { - statusCode: 403, - response: expectDoesntExistRbacForbidden, - }, - }, - }); - - updateTest(`rbac user with all at the space within the ${scenario.spaceId} space`, { - user: scenario.users.allAtSpace, - spaceId: scenario.spaceId, - tests: { - spaceAware: { - statusCode: 200, - response: expectSpaceAwareResults, - }, - notSpaceAware: { - statusCode: 200, - response: expectNotSpaceAwareResults, - }, - hiddenType: { - statusCode: 403, - response: expectHiddenTypeRbacForbidden, - }, - doesntExist: { - statusCode: 404, - response: createExpectDoesntExistNotFound(scenario.spaceId), - }, - }, - }); - - updateTest(`rbac user with read at the space within the ${scenario.spaceId} space`, { - user: scenario.users.readAtSpace, - spaceId: scenario.spaceId, - tests: { - spaceAware: { - statusCode: 403, - response: expectSpaceAwareRbacForbidden, - }, - notSpaceAware: { - statusCode: 403, - response: expectNotSpaceAwareRbacForbidden, - }, - hiddenType: { - statusCode: 403, - response: expectHiddenTypeRbacForbidden, - }, - doesntExist: { - statusCode: 403, - response: expectDoesntExistRbacForbidden, - }, - }, - }); - - updateTest(`rbac user with all at other space within the ${scenario.spaceId} space`, { - user: scenario.users.allAtOtherSpace, - spaceId: scenario.spaceId, - tests: { - spaceAware: { - statusCode: 403, - response: expectSpaceAwareRbacForbidden, - }, - notSpaceAware: { - statusCode: 403, - response: expectNotSpaceAwareRbacForbidden, - }, - hiddenType: { - statusCode: 403, - response: expectHiddenTypeRbacForbidden, - }, - doesntExist: { - statusCode: 403, - response: expectDoesntExistRbacForbidden, - }, - }, + [users.dualAll, users.allGlobally, users.allAtSpace].forEach(user => { + _addTests(user, authorized); }); + _addTests(users.superuser, superuser); }); }); } diff --git a/x-pack/test/saved_object_api_integration/security_only/apis/bulk_create.ts b/x-pack/test/saved_object_api_integration/security_only/apis/bulk_create.ts index 943a22c4399c7..5b3397c7909ae 100644 --- a/x-pack/test/saved_object_api_integration/security_only/apis/bulk_create.ts +++ b/x-pack/test/saved_object_api_integration/security_only/apis/bulk_create.ts @@ -4,176 +4,88 @@ * you may not use this file except in compliance with the Elastic License. */ -import { AUTHENTICATION } from '../../common/lib/authentication'; +import { testCaseFailures, getTestScenarios } from '../../common/lib/saved_object_test_utils'; +import { TestUser } from '../../common/lib/types'; import { FtrProviderContext } from '../../common/ftr_provider_context'; -import { bulkCreateTestSuiteFactory } from '../../common/suites/bulk_create'; +import { + bulkCreateTestSuiteFactory, + TEST_CASES as CASES, + BulkCreateTestDefinition, +} from '../../common/suites/bulk_create'; + +const { fail400, fail409 } = testCaseFailures; + +const createTestCases = (overwrite: boolean) => { + // for each permitted (non-403) outcome, if failure !== undefined then we expect + // to receive an error; otherwise, we expect to receive a success result + const normalTypes = [ + { ...CASES.SINGLE_NAMESPACE_DEFAULT_SPACE, ...fail409(!overwrite) }, + CASES.SINGLE_NAMESPACE_SPACE_1, + CASES.SINGLE_NAMESPACE_SPACE_2, + { ...CASES.MULTI_NAMESPACE_DEFAULT_AND_SPACE_1, ...fail409(!overwrite) }, + { ...CASES.MULTI_NAMESPACE_ONLY_SPACE_1, ...fail409() }, + { ...CASES.MULTI_NAMESPACE_ONLY_SPACE_2, ...fail409() }, + { ...CASES.NAMESPACE_AGNOSTIC, ...fail409(!overwrite) }, + CASES.NEW_SINGLE_NAMESPACE_OBJ, + CASES.NEW_MULTI_NAMESPACE_OBJ, + CASES.NEW_NAMESPACE_AGNOSTIC_OBJ, + ]; + const hiddenType = [{ ...CASES.HIDDEN, ...fail400() }]; + const allTypes = normalTypes.concat(hiddenType); + return { normalTypes, hiddenType, allTypes }; +}; export default function({ getService }: FtrProviderContext) { const supertest = getService('supertestWithoutAuth'); const esArchiver = getService('esArchiver'); const es = getService('legacyEs'); - const { - bulkCreateTest, - createExpectResults, - createExpectRbacForbidden, - expectBadRequestForHiddenType, - expectedForbiddenTypesWithHiddenType: expectedForbiddenTypesWithHiddenType, - } = bulkCreateTestSuiteFactory(es, esArchiver, supertest); + const { addTests, createTestDefinitions, expectForbidden } = bulkCreateTestSuiteFactory( + es, + esArchiver, + supertest + ); + const createTests = (overwrite: boolean) => { + const { normalTypes, hiddenType, allTypes } = createTestCases(overwrite); + // use singleRequest to reduce execution time and/or test combined cases + return { + unauthorized: createTestDefinitions(allTypes, true, overwrite), + authorized: [ + createTestDefinitions(normalTypes, false, overwrite, { singleRequest: true }), + createTestDefinitions(hiddenType, true, overwrite), + createTestDefinitions(allTypes, true, overwrite, { + singleRequest: true, + responseBodyOverride: expectForbidden(['hiddentype']), + }), + ].flat(), + superuser: createTestDefinitions(allTypes, false, overwrite, { singleRequest: true }), + }; + }; describe('_bulk_create', () => { - bulkCreateTest(`user with no access`, { - user: AUTHENTICATION.NOT_A_KIBANA_USER, - tests: { - default: { - statusCode: 403, - response: createExpectRbacForbidden(), - }, - includingSpace: { - statusCode: 403, - response: createExpectRbacForbidden(expectedForbiddenTypesWithHiddenType), - }, - }, - }); - - bulkCreateTest(`superuser`, { - user: AUTHENTICATION.SUPERUSER, - tests: { - default: { - statusCode: 200, - response: createExpectResults(), - }, - includingSpace: { - statusCode: 200, - response: expectBadRequestForHiddenType, - }, - }, - }); - - bulkCreateTest(`legacy user`, { - user: AUTHENTICATION.KIBANA_LEGACY_USER, - tests: { - default: { - statusCode: 403, - response: createExpectRbacForbidden(), - }, - includingSpace: { - statusCode: 403, - response: createExpectRbacForbidden(expectedForbiddenTypesWithHiddenType), - }, - }, - }); - - bulkCreateTest(`dual-privileges user`, { - user: AUTHENTICATION.KIBANA_DUAL_PRIVILEGES_USER, - tests: { - default: { - statusCode: 200, - response: createExpectResults(), - }, - includingSpace: { - statusCode: 403, - response: createExpectRbacForbidden(['hiddentype']), - }, - }, - }); - - bulkCreateTest(`dual-privileges readonly user`, { - user: AUTHENTICATION.KIBANA_DUAL_PRIVILEGES_DASHBOARD_ONLY_USER, - tests: { - default: { - statusCode: 403, - response: createExpectRbacForbidden(), - }, - includingSpace: { - statusCode: 403, - response: createExpectRbacForbidden(expectedForbiddenTypesWithHiddenType), - }, - }, - }); - - bulkCreateTest(`rbac user with all globally`, { - user: AUTHENTICATION.KIBANA_RBAC_USER, - tests: { - default: { - statusCode: 200, - response: createExpectResults(), - }, - includingSpace: { - statusCode: 403, - response: createExpectRbacForbidden(['hiddentype']), - }, - }, - }); - - bulkCreateTest(`rbac readonly user`, { - user: AUTHENTICATION.KIBANA_RBAC_DASHBOARD_ONLY_USER, - tests: { - default: { - statusCode: 403, - response: createExpectRbacForbidden(), - }, - includingSpace: { - statusCode: 403, - response: createExpectRbacForbidden(expectedForbiddenTypesWithHiddenType), - }, - }, - }); - - bulkCreateTest(`rbac user with all at default space`, { - user: AUTHENTICATION.KIBANA_RBAC_DEFAULT_SPACE_ALL_USER, - tests: { - default: { - statusCode: 403, - response: createExpectRbacForbidden(), - }, - includingSpace: { - statusCode: 403, - response: createExpectRbacForbidden(expectedForbiddenTypesWithHiddenType), - }, - }, - }); - - bulkCreateTest(`rbac user with read at default space`, { - user: AUTHENTICATION.KIBANA_RBAC_DEFAULT_SPACE_READ_USER, - tests: { - default: { - statusCode: 403, - response: createExpectRbacForbidden(), - }, - includingSpace: { - statusCode: 403, - response: createExpectRbacForbidden(expectedForbiddenTypesWithHiddenType), - }, - }, - }); - - bulkCreateTest(`rbac user with all at space_1`, { - user: AUTHENTICATION.KIBANA_RBAC_SPACE_1_ALL_USER, - tests: { - default: { - statusCode: 403, - response: createExpectRbacForbidden(), - }, - includingSpace: { - statusCode: 403, - response: createExpectRbacForbidden(expectedForbiddenTypesWithHiddenType), - }, - }, - }); + getTestScenarios([false, true]).security.forEach(({ users, modifier: overwrite }) => { + const suffix = overwrite ? ' with overwrite enabled' : ''; + const { unauthorized, authorized, superuser } = createTests(overwrite!); + const _addTests = (user: TestUser, tests: BulkCreateTestDefinition[]) => { + addTests(`${user.description}${suffix}`, { user, tests }); + }; - bulkCreateTest(`rbac user with read at space_1`, { - user: AUTHENTICATION.KIBANA_RBAC_SPACE_1_READ_USER, - tests: { - default: { - statusCode: 403, - response: createExpectRbacForbidden(), - }, - includingSpace: { - statusCode: 403, - response: createExpectRbacForbidden(expectedForbiddenTypesWithHiddenType), - }, - }, + [ + users.noAccess, + users.legacyAll, + users.dualRead, + users.readGlobally, + users.allAtDefaultSpace, + users.readAtDefaultSpace, + users.allAtSpace1, + users.readAtSpace1, + ].forEach(user => { + _addTests(user, unauthorized); + }); + [users.dualAll, users.allGlobally].forEach(user => { + _addTests(user, authorized); + }); + _addTests(users.superuser, superuser); }); }); } diff --git a/x-pack/test/saved_object_api_integration/security_only/apis/bulk_get.ts b/x-pack/test/saved_object_api_integration/security_only/apis/bulk_get.ts index fde98694fe575..69494ed254669 100644 --- a/x-pack/test/saved_object_api_integration/security_only/apis/bulk_get.ts +++ b/x-pack/test/saved_object_api_integration/security_only/apis/bulk_get.ts @@ -4,175 +4,81 @@ * you may not use this file except in compliance with the Elastic License. */ -import { AUTHENTICATION } from '../../common/lib/authentication'; +import { testCaseFailures, getTestScenarios } from '../../common/lib/saved_object_test_utils'; +import { TestUser } from '../../common/lib/types'; import { FtrProviderContext } from '../../common/ftr_provider_context'; -import { bulkGetTestSuiteFactory } from '../../common/suites/bulk_get'; +import { + bulkGetTestSuiteFactory, + TEST_CASES as CASES, + BulkGetTestDefinition, +} from '../../common/suites/bulk_get'; + +const { fail400, fail404 } = testCaseFailures; + +const createTestCases = () => { + // for each permitted (non-403) outcome, if failure !== undefined then we expect + // to receive an error; otherwise, we expect to receive a success result + const normalTypes = [ + CASES.SINGLE_NAMESPACE_DEFAULT_SPACE, + { ...CASES.SINGLE_NAMESPACE_SPACE_1, ...fail404() }, + { ...CASES.SINGLE_NAMESPACE_SPACE_2, ...fail404() }, + CASES.MULTI_NAMESPACE_DEFAULT_AND_SPACE_1, + { ...CASES.MULTI_NAMESPACE_ONLY_SPACE_1, ...fail404() }, + { ...CASES.MULTI_NAMESPACE_ONLY_SPACE_2, ...fail404() }, + CASES.NAMESPACE_AGNOSTIC, + { ...CASES.DOES_NOT_EXIST, ...fail404() }, + ]; + const hiddenType = [{ ...CASES.HIDDEN, ...fail400() }]; + const allTypes = normalTypes.concat(hiddenType); + return { normalTypes, hiddenType, allTypes }; +}; export default function({ getService }: FtrProviderContext) { const supertest = getService('supertestWithoutAuth'); const esArchiver = getService('esArchiver'); - const { - bulkGetTest, - createExpectResults, - createExpectRbacForbidden, - expectedForbiddenTypesWithHiddenType, - expectBadRequestForHiddenType, - } = bulkGetTestSuiteFactory(esArchiver, supertest); + const { addTests, createTestDefinitions, expectForbidden } = bulkGetTestSuiteFactory( + esArchiver, + supertest + ); + const createTests = () => { + const { normalTypes, hiddenType, allTypes } = createTestCases(); + // use singleRequest to reduce execution time and/or test combined cases + return { + unauthorized: createTestDefinitions(allTypes, true), + authorized: [ + createTestDefinitions(normalTypes, false, { singleRequest: true }), + createTestDefinitions(hiddenType, true), + createTestDefinitions(allTypes, true, { + singleRequest: true, + responseBodyOverride: expectForbidden(['hiddentype']), + }), + ].flat(), + superuser: createTestDefinitions(allTypes, false, { singleRequest: true }), + }; + }; describe('_bulk_get', () => { - bulkGetTest(`user with no access`, { - user: AUTHENTICATION.NOT_A_KIBANA_USER, - tests: { - default: { - statusCode: 403, - response: createExpectRbacForbidden(), - }, - includingHiddenType: { - statusCode: 403, - response: createExpectRbacForbidden(expectedForbiddenTypesWithHiddenType), - }, - }, - }); - - bulkGetTest(`superuser`, { - user: AUTHENTICATION.SUPERUSER, - tests: { - default: { - statusCode: 200, - response: createExpectResults(), - }, - includingHiddenType: { - statusCode: 200, - response: expectBadRequestForHiddenType, - }, - }, - }); - - bulkGetTest(`legacy user`, { - user: AUTHENTICATION.KIBANA_LEGACY_USER, - tests: { - default: { - statusCode: 403, - response: createExpectRbacForbidden(), - }, - includingHiddenType: { - statusCode: 403, - response: createExpectRbacForbidden(expectedForbiddenTypesWithHiddenType), - }, - }, - }); - - bulkGetTest(`dual-privileges user`, { - user: AUTHENTICATION.KIBANA_DUAL_PRIVILEGES_USER, - tests: { - default: { - statusCode: 200, - response: createExpectResults(), - }, - includingHiddenType: { - statusCode: 403, - response: createExpectRbacForbidden(['hiddentype']), - }, - }, - }); - - bulkGetTest(`dual-privileges readonly user`, { - user: AUTHENTICATION.KIBANA_DUAL_PRIVILEGES_DASHBOARD_ONLY_USER, - tests: { - default: { - statusCode: 200, - response: createExpectResults(), - }, - includingHiddenType: { - statusCode: 403, - response: createExpectRbacForbidden(['hiddentype']), - }, - }, - }); - - bulkGetTest(`rbac user with all globally`, { - user: AUTHENTICATION.KIBANA_RBAC_USER, - tests: { - default: { - statusCode: 200, - response: createExpectResults(), - }, - includingHiddenType: { - statusCode: 403, - response: createExpectRbacForbidden(['hiddentype']), - }, - }, - }); - - bulkGetTest(`rbac user with read globally`, { - user: AUTHENTICATION.KIBANA_RBAC_DASHBOARD_ONLY_USER, - tests: { - default: { - statusCode: 200, - response: createExpectResults(), - }, - includingHiddenType: { - statusCode: 403, - response: createExpectRbacForbidden(['hiddentype']), - }, - }, - }); - - bulkGetTest(`rbac user with all at default space`, { - user: AUTHENTICATION.KIBANA_RBAC_DEFAULT_SPACE_ALL_USER, - tests: { - default: { - statusCode: 403, - response: createExpectRbacForbidden(), - }, - includingHiddenType: { - statusCode: 403, - response: createExpectRbacForbidden(expectedForbiddenTypesWithHiddenType), - }, - }, - }); - - bulkGetTest(`rbac user with read at default space`, { - user: AUTHENTICATION.KIBANA_RBAC_DEFAULT_SPACE_READ_USER, - tests: { - default: { - statusCode: 403, - response: createExpectRbacForbidden(), - }, - includingHiddenType: { - statusCode: 403, - response: createExpectRbacForbidden(expectedForbiddenTypesWithHiddenType), - }, - }, - }); - - bulkGetTest(`rbac user with all at space_1`, { - user: AUTHENTICATION.KIBANA_RBAC_SPACE_1_ALL_USER, - tests: { - default: { - statusCode: 403, - response: createExpectRbacForbidden(), - }, - includingHiddenType: { - statusCode: 403, - response: createExpectRbacForbidden(expectedForbiddenTypesWithHiddenType), - }, - }, - }); + getTestScenarios().security.forEach(({ users }) => { + const { unauthorized, authorized, superuser } = createTests(); + const _addTests = (user: TestUser, tests: BulkGetTestDefinition[]) => { + addTests(user.description, { user, tests }); + }; - bulkGetTest(`rbac user with read at space_1`, { - user: AUTHENTICATION.KIBANA_RBAC_SPACE_1_READ_USER, - tests: { - default: { - statusCode: 403, - response: createExpectRbacForbidden(), - }, - includingHiddenType: { - statusCode: 403, - response: createExpectRbacForbidden(expectedForbiddenTypesWithHiddenType), - }, - }, + [ + users.noAccess, + users.legacyAll, + users.allAtDefaultSpace, + users.readAtDefaultSpace, + users.allAtSpace1, + users.readAtSpace1, + ].forEach(user => { + _addTests(user, unauthorized); + }); + [users.dualAll, users.dualRead, users.allGlobally, users.readGlobally].forEach(user => { + _addTests(user, authorized); + }); + _addTests(users.superuser, superuser); }); }); } diff --git a/x-pack/test/saved_object_api_integration/security_only/apis/bulk_update.ts b/x-pack/test/saved_object_api_integration/security_only/apis/bulk_update.ts index 6f4635f17cf8c..fb169f4c6fb86 100644 --- a/x-pack/test/saved_object_api_integration/security_only/apis/bulk_update.ts +++ b/x-pack/test/saved_object_api_integration/security_only/apis/bulk_update.ts @@ -4,268 +4,83 @@ * you may not use this file except in compliance with the Elastic License. */ -import { AUTHENTICATION } from '../../common/lib/authentication'; +import { testCaseFailures, getTestScenarios } from '../../common/lib/saved_object_test_utils'; +import { TestUser } from '../../common/lib/types'; import { FtrProviderContext } from '../../common/ftr_provider_context'; -import { bulkUpdateTestSuiteFactory } from '../../common/suites/bulk_update'; +import { + bulkUpdateTestSuiteFactory, + TEST_CASES as CASES, + BulkUpdateTestDefinition, +} from '../../common/suites/bulk_update'; + +const { fail404 } = testCaseFailures; + +const createTestCases = () => { + // for each permitted (non-403) outcome, if failure !== undefined then we expect + // to receive an error; otherwise, we expect to receive a success result + const normalTypes = [ + CASES.SINGLE_NAMESPACE_DEFAULT_SPACE, + { ...CASES.SINGLE_NAMESPACE_SPACE_1, ...fail404() }, + { ...CASES.SINGLE_NAMESPACE_SPACE_2, ...fail404() }, + CASES.MULTI_NAMESPACE_DEFAULT_AND_SPACE_1, + { ...CASES.MULTI_NAMESPACE_ONLY_SPACE_1, ...fail404() }, + { ...CASES.MULTI_NAMESPACE_ONLY_SPACE_2, ...fail404() }, + CASES.NAMESPACE_AGNOSTIC, + { ...CASES.DOES_NOT_EXIST, ...fail404() }, + ]; + const hiddenType = [{ ...CASES.HIDDEN, ...fail404() }]; + const allTypes = normalTypes.concat(hiddenType); + return { normalTypes, hiddenType, allTypes }; +}; export default function({ getService }: FtrProviderContext) { const supertest = getService('supertestWithoutAuth'); const esArchiver = getService('esArchiver'); - describe('bulkUpdate', () => { - const { - createExpectDoesntExistNotFound, - expectDoesntExistRbacForbidden, - expectNotSpaceAwareResults, - expectNotSpaceAwareRbacForbidden, - expectSpaceAwareRbacForbidden, - expectSpaceAwareResults, - expectSpaceNotFound, - expectHiddenTypeRbacForbidden, - expectHiddenTypeRbacForbiddenWithGlobalAllowed, - bulkUpdateTest, - } = bulkUpdateTestSuiteFactory(esArchiver, supertest); - - bulkUpdateTest(`user with no access`, { - user: AUTHENTICATION.NOT_A_KIBANA_USER, - tests: { - spaceAware: { - statusCode: 403, - response: expectSpaceAwareRbacForbidden, - }, - notSpaceAware: { - statusCode: 403, - response: expectNotSpaceAwareRbacForbidden, - }, - hiddenType: { - statusCode: 403, - response: expectHiddenTypeRbacForbidden, - }, - doesntExist: { - statusCode: 403, - response: expectDoesntExistRbacForbidden, - }, - }, - }); - - bulkUpdateTest(`superuser`, { - user: AUTHENTICATION.SUPERUSER, - tests: { - spaceAware: { - statusCode: 200, - response: expectSpaceAwareResults, - }, - notSpaceAware: { - statusCode: 200, - response: expectNotSpaceAwareResults, - }, - hiddenType: { - statusCode: 200, - response: expectSpaceNotFound, - }, - doesntExist: { - statusCode: 200, - response: createExpectDoesntExistNotFound(), - }, - }, - }); + const { addTests, createTestDefinitions, expectForbidden } = bulkUpdateTestSuiteFactory( + esArchiver, + supertest + ); + const createTests = () => { + const { normalTypes, hiddenType, allTypes } = createTestCases(); + // use singleRequest to reduce execution time and/or test combined cases + return { + unauthorized: createTestDefinitions(allTypes, true), + authorized: [ + createTestDefinitions(normalTypes, false, { singleRequest: true }), + createTestDefinitions(hiddenType, true), + createTestDefinitions(allTypes, true, { + singleRequest: true, + responseBodyOverride: expectForbidden(['hiddentype']), + }), + ].flat(), + superuser: createTestDefinitions(allTypes, false, { singleRequest: true }), + }; + }; - bulkUpdateTest(`legacy user`, { - user: AUTHENTICATION.KIBANA_LEGACY_USER, - tests: { - spaceAware: { - statusCode: 403, - response: expectSpaceAwareRbacForbidden, - }, - notSpaceAware: { - statusCode: 403, - response: expectNotSpaceAwareRbacForbidden, - }, - hiddenType: { - statusCode: 403, - response: expectHiddenTypeRbacForbidden, - }, - doesntExist: { - statusCode: 403, - response: expectDoesntExistRbacForbidden, - }, - }, - }); - - bulkUpdateTest(`dual-privileges user`, { - user: AUTHENTICATION.KIBANA_DUAL_PRIVILEGES_USER, - tests: { - spaceAware: { - statusCode: 200, - response: expectSpaceAwareResults, - }, - notSpaceAware: { - statusCode: 200, - response: expectNotSpaceAwareResults, - }, - hiddenType: { - statusCode: 403, - response: expectHiddenTypeRbacForbiddenWithGlobalAllowed, - }, - doesntExist: { - statusCode: 200, - response: createExpectDoesntExistNotFound(), - }, - }, - }); - - bulkUpdateTest(`dual-privileges readonly user`, { - user: AUTHENTICATION.KIBANA_DUAL_PRIVILEGES_DASHBOARD_ONLY_USER, - tests: { - spaceAware: { - statusCode: 403, - response: expectSpaceAwareRbacForbidden, - }, - notSpaceAware: { - statusCode: 403, - response: expectNotSpaceAwareRbacForbidden, - }, - hiddenType: { - statusCode: 403, - response: expectHiddenTypeRbacForbidden, - }, - doesntExist: { - statusCode: 403, - response: expectDoesntExistRbacForbidden, - }, - }, - }); - - bulkUpdateTest(`rbac user with all globally`, { - user: AUTHENTICATION.KIBANA_RBAC_USER, - tests: { - spaceAware: { - statusCode: 200, - response: expectSpaceAwareResults, - }, - notSpaceAware: { - statusCode: 200, - response: expectNotSpaceAwareResults, - }, - hiddenType: { - statusCode: 403, - response: expectHiddenTypeRbacForbiddenWithGlobalAllowed, - }, - doesntExist: { - statusCode: 200, - response: createExpectDoesntExistNotFound(), - }, - }, - }); - - bulkUpdateTest(`rbac user with read globally`, { - user: AUTHENTICATION.KIBANA_RBAC_DASHBOARD_ONLY_USER, - tests: { - spaceAware: { - statusCode: 403, - response: expectSpaceAwareRbacForbidden, - }, - notSpaceAware: { - statusCode: 403, - response: expectNotSpaceAwareRbacForbidden, - }, - hiddenType: { - statusCode: 403, - response: expectHiddenTypeRbacForbidden, - }, - doesntExist: { - statusCode: 403, - response: expectDoesntExistRbacForbidden, - }, - }, - }); - - bulkUpdateTest(`rbac user with all at default space`, { - user: AUTHENTICATION.KIBANA_RBAC_DEFAULT_SPACE_ALL_USER, - tests: { - spaceAware: { - statusCode: 403, - response: expectSpaceAwareRbacForbidden, - }, - notSpaceAware: { - statusCode: 403, - response: expectNotSpaceAwareRbacForbidden, - }, - hiddenType: { - statusCode: 403, - response: expectHiddenTypeRbacForbidden, - }, - doesntExist: { - statusCode: 403, - response: expectDoesntExistRbacForbidden, - }, - }, - }); - - bulkUpdateTest(`rbac user with read at default space`, { - user: AUTHENTICATION.KIBANA_RBAC_DEFAULT_SPACE_READ_USER, - tests: { - spaceAware: { - statusCode: 403, - response: expectSpaceAwareRbacForbidden, - }, - notSpaceAware: { - statusCode: 403, - response: expectNotSpaceAwareRbacForbidden, - }, - hiddenType: { - statusCode: 403, - response: expectHiddenTypeRbacForbidden, - }, - doesntExist: { - statusCode: 403, - response: expectDoesntExistRbacForbidden, - }, - }, - }); - - bulkUpdateTest(`rbac user with all at space_1`, { - user: AUTHENTICATION.KIBANA_RBAC_SPACE_1_ALL_USER, - tests: { - spaceAware: { - statusCode: 403, - response: expectSpaceAwareRbacForbidden, - }, - notSpaceAware: { - statusCode: 403, - response: expectNotSpaceAwareRbacForbidden, - }, - hiddenType: { - statusCode: 403, - response: expectHiddenTypeRbacForbidden, - }, - doesntExist: { - statusCode: 403, - response: expectDoesntExistRbacForbidden, - }, - }, - }); + describe('_bulk_update', () => { + getTestScenarios().security.forEach(({ users }) => { + const { unauthorized, authorized, superuser } = createTests(); + const _addTests = (user: TestUser, tests: BulkUpdateTestDefinition[]) => { + addTests(user.description, { user, tests }); + }; - bulkUpdateTest(`rbac user with read at space_1`, { - user: AUTHENTICATION.KIBANA_RBAC_SPACE_1_READ_USER, - tests: { - spaceAware: { - statusCode: 403, - response: expectSpaceAwareRbacForbidden, - }, - notSpaceAware: { - statusCode: 403, - response: expectNotSpaceAwareRbacForbidden, - }, - hiddenType: { - statusCode: 403, - response: expectHiddenTypeRbacForbidden, - }, - doesntExist: { - statusCode: 403, - response: expectDoesntExistRbacForbidden, - }, - }, + [ + users.noAccess, + users.legacyAll, + users.dualRead, + users.readGlobally, + users.allAtDefaultSpace, + users.readAtDefaultSpace, + users.allAtSpace1, + users.readAtSpace1, + ].forEach(user => { + _addTests(user, unauthorized); + }); + [users.dualAll, users.allGlobally].forEach(user => { + _addTests(user, authorized); + }); + _addTests(users.superuser, superuser); }); }); } diff --git a/x-pack/test/saved_object_api_integration/security_only/apis/create.ts b/x-pack/test/saved_object_api_integration/security_only/apis/create.ts index 60a9fa0a86aa6..dc8e564e42477 100644 --- a/x-pack/test/saved_object_api_integration/security_only/apis/create.ts +++ b/x-pack/test/saved_object_api_integration/security_only/apis/create.ts @@ -4,222 +4,79 @@ * you may not use this file except in compliance with the Elastic License. */ -import { AUTHENTICATION } from '../../common/lib/authentication'; +import { testCaseFailures, getTestScenarios } from '../../common/lib/saved_object_test_utils'; +import { TestUser } from '../../common/lib/types'; import { FtrProviderContext } from '../../common/ftr_provider_context'; -import { createTestSuiteFactory } from '../../common/suites/create'; +import { + createTestSuiteFactory, + TEST_CASES as CASES, + CreateTestDefinition, +} from '../../common/suites/create'; -export default function({ getService }: FtrProviderContext) { - const supertestWithoutAuth = getService('supertestWithoutAuth'); - const es = getService('legacyEs'); - const esArchiver = getService('esArchiver'); - - const { - createTest, - createExpectSpaceAwareResults, - expectNotSpaceAwareResults, - expectNotSpaceAwareRbacForbidden, - expectSpaceAwareRbacForbidden, - expectBadRequestForHiddenType, - expectHiddenTypeRbacForbidden, - } = createTestSuiteFactory(es, esArchiver, supertestWithoutAuth); - - describe('create', () => { - createTest(`user with no access`, { - user: AUTHENTICATION.NOT_A_KIBANA_USER, - tests: { - spaceAware: { - statusCode: 403, - response: expectSpaceAwareRbacForbidden, - }, - notSpaceAware: { - statusCode: 403, - response: expectNotSpaceAwareRbacForbidden, - }, - hiddenType: { - statusCode: 403, - response: expectHiddenTypeRbacForbidden, - }, - }, - }); +const { fail400, fail409 } = testCaseFailures; - createTest(`superuser`, { - user: AUTHENTICATION.SUPERUSER, - tests: { - spaceAware: { - statusCode: 200, - response: createExpectSpaceAwareResults(), - }, - notSpaceAware: { - statusCode: 200, - response: expectNotSpaceAwareResults, - }, - hiddenType: { - statusCode: 400, - response: expectBadRequestForHiddenType, - }, - }, - }); - - createTest(`legacy user`, { - user: AUTHENTICATION.KIBANA_LEGACY_USER, - tests: { - spaceAware: { - statusCode: 403, - response: expectSpaceAwareRbacForbidden, - }, - notSpaceAware: { - statusCode: 403, - response: expectNotSpaceAwareRbacForbidden, - }, - hiddenType: { - statusCode: 403, - response: expectHiddenTypeRbacForbidden, - }, - }, - }); +const createTestCases = (overwrite: boolean) => { + // for each permitted (non-403) outcome, if failure !== undefined then we expect + // to receive an error; otherwise, we expect to receive a success result + const normalTypes = [ + { ...CASES.SINGLE_NAMESPACE_DEFAULT_SPACE, ...fail409(!overwrite) }, + CASES.SINGLE_NAMESPACE_SPACE_1, + CASES.SINGLE_NAMESPACE_SPACE_2, + { ...CASES.MULTI_NAMESPACE_DEFAULT_AND_SPACE_1, ...fail409(!overwrite) }, + { ...CASES.MULTI_NAMESPACE_ONLY_SPACE_1, ...fail409() }, + { ...CASES.MULTI_NAMESPACE_ONLY_SPACE_2, ...fail409() }, + { ...CASES.NAMESPACE_AGNOSTIC, ...fail409(!overwrite) }, + CASES.NEW_SINGLE_NAMESPACE_OBJ, + CASES.NEW_MULTI_NAMESPACE_OBJ, + CASES.NEW_NAMESPACE_AGNOSTIC_OBJ, + ]; + const hiddenType = [{ ...CASES.HIDDEN, ...fail400() }]; + const allTypes = normalTypes.concat(hiddenType); + return { normalTypes, hiddenType, allTypes }; +}; - createTest(`dual-privileges user`, { - user: AUTHENTICATION.KIBANA_DUAL_PRIVILEGES_USER, - tests: { - spaceAware: { - statusCode: 200, - response: createExpectSpaceAwareResults(), - }, - notSpaceAware: { - statusCode: 200, - response: expectNotSpaceAwareResults, - }, - hiddenType: { - statusCode: 403, - response: expectHiddenTypeRbacForbidden, - }, - }, - }); - - createTest(`dual-privileges readonly user`, { - user: AUTHENTICATION.KIBANA_DUAL_PRIVILEGES_DASHBOARD_ONLY_USER, - tests: { - spaceAware: { - statusCode: 403, - response: expectSpaceAwareRbacForbidden, - }, - notSpaceAware: { - statusCode: 403, - response: expectNotSpaceAwareRbacForbidden, - }, - hiddenType: { - statusCode: 403, - response: expectHiddenTypeRbacForbidden, - }, - }, - }); - - createTest(`rbac user with all globally`, { - user: AUTHENTICATION.KIBANA_RBAC_USER, - tests: { - spaceAware: { - statusCode: 200, - response: createExpectSpaceAwareResults(), - }, - notSpaceAware: { - statusCode: 200, - response: expectNotSpaceAwareResults, - }, - hiddenType: { - statusCode: 403, - response: expectHiddenTypeRbacForbidden, - }, - }, - }); - - createTest(`rbac user with read globally`, { - user: AUTHENTICATION.KIBANA_RBAC_DASHBOARD_ONLY_USER, - tests: { - spaceAware: { - statusCode: 403, - response: expectSpaceAwareRbacForbidden, - }, - notSpaceAware: { - statusCode: 403, - response: expectNotSpaceAwareRbacForbidden, - }, - hiddenType: { - statusCode: 403, - response: expectHiddenTypeRbacForbidden, - }, - }, - }); - - createTest(`rbac user with all at default space`, { - user: AUTHENTICATION.KIBANA_RBAC_DEFAULT_SPACE_ALL_USER, - tests: { - spaceAware: { - statusCode: 403, - response: expectSpaceAwareRbacForbidden, - }, - notSpaceAware: { - statusCode: 403, - response: expectNotSpaceAwareRbacForbidden, - }, - hiddenType: { - statusCode: 403, - response: expectHiddenTypeRbacForbidden, - }, - }, - }); +export default function({ getService }: FtrProviderContext) { + const supertest = getService('supertestWithoutAuth'); + const esArchiver = getService('esArchiver'); + const es = getService('legacyEs'); - createTest(`rbac user with read at default space`, { - user: AUTHENTICATION.KIBANA_RBAC_DEFAULT_SPACE_READ_USER, - tests: { - spaceAware: { - statusCode: 403, - response: expectSpaceAwareRbacForbidden, - }, - notSpaceAware: { - statusCode: 403, - response: expectNotSpaceAwareRbacForbidden, - }, - hiddenType: { - statusCode: 403, - response: expectHiddenTypeRbacForbidden, - }, - }, - }); + const { addTests, createTestDefinitions } = createTestSuiteFactory(es, esArchiver, supertest); + const createTests = (overwrite: boolean) => { + const { normalTypes, hiddenType, allTypes } = createTestCases(overwrite); + return { + unauthorized: createTestDefinitions(allTypes, true, overwrite), + authorized: [ + createTestDefinitions(normalTypes, false, overwrite), + createTestDefinitions(hiddenType, true, overwrite), + ].flat(), + superuser: createTestDefinitions(allTypes, false, overwrite), + }; + }; - createTest(`rbac user with all at space_1`, { - user: AUTHENTICATION.KIBANA_RBAC_SPACE_1_ALL_USER, - tests: { - spaceAware: { - statusCode: 403, - response: expectSpaceAwareRbacForbidden, - }, - notSpaceAware: { - statusCode: 403, - response: expectNotSpaceAwareRbacForbidden, - }, - hiddenType: { - statusCode: 403, - response: expectHiddenTypeRbacForbidden, - }, - }, - }); + describe('_create', () => { + getTestScenarios([false, true]).security.forEach(({ users, modifier: overwrite }) => { + const suffix = overwrite ? ' with overwrite enabled' : ''; + const { unauthorized, authorized, superuser } = createTests(overwrite!); + const _addTests = (user: TestUser, tests: CreateTestDefinition[]) => { + addTests(`${user.description}${suffix}`, { user, tests }); + }; - createTest(`rbac user with read at space_1`, { - user: AUTHENTICATION.KIBANA_RBAC_SPACE_1_READ_USER, - tests: { - spaceAware: { - statusCode: 403, - response: expectSpaceAwareRbacForbidden, - }, - notSpaceAware: { - statusCode: 403, - response: expectNotSpaceAwareRbacForbidden, - }, - hiddenType: { - statusCode: 403, - response: expectHiddenTypeRbacForbidden, - }, - }, + [ + users.noAccess, + users.legacyAll, + users.dualRead, + users.readGlobally, + users.allAtDefaultSpace, + users.readAtDefaultSpace, + users.allAtSpace1, + users.readAtSpace1, + ].forEach(user => { + _addTests(user, unauthorized); + }); + [users.dualAll, users.allGlobally].forEach(user => { + _addTests(user, authorized); + }); + _addTests(users.superuser, superuser); }); }); } diff --git a/x-pack/test/saved_object_api_integration/security_only/apis/delete.ts b/x-pack/test/saved_object_api_integration/security_only/apis/delete.ts index f775b5a365d6b..05939197be352 100644 --- a/x-pack/test/saved_object_api_integration/security_only/apis/delete.ts +++ b/x-pack/test/saved_object_api_integration/security_only/apis/delete.ts @@ -4,266 +4,75 @@ * you may not use this file except in compliance with the Elastic License. */ -import { AUTHENTICATION } from '../../common/lib/authentication'; +import { testCaseFailures, getTestScenarios } from '../../common/lib/saved_object_test_utils'; +import { TestUser } from '../../common/lib/types'; import { FtrProviderContext } from '../../common/ftr_provider_context'; -import { deleteTestSuiteFactory } from '../../common/suites/delete'; +import { + deleteTestSuiteFactory, + TEST_CASES as CASES, + DeleteTestDefinition, +} from '../../common/suites/delete'; + +const { fail404 } = testCaseFailures; + +const createTestCases = () => { + // for each permitted (non-403) outcome, if failure !== undefined then we expect + // to receive an error; otherwise, we expect to receive a success result + const normalTypes = [ + CASES.SINGLE_NAMESPACE_DEFAULT_SPACE, + { ...CASES.SINGLE_NAMESPACE_SPACE_1, ...fail404() }, + { ...CASES.SINGLE_NAMESPACE_SPACE_2, ...fail404() }, + CASES.MULTI_NAMESPACE_DEFAULT_AND_SPACE_1, + { ...CASES.MULTI_NAMESPACE_ONLY_SPACE_1, ...fail404() }, + { ...CASES.MULTI_NAMESPACE_ONLY_SPACE_2, ...fail404() }, + CASES.NAMESPACE_AGNOSTIC, + { ...CASES.DOES_NOT_EXIST, ...fail404() }, + ]; + const hiddenType = [{ ...CASES.HIDDEN, ...fail404() }]; + const allTypes = normalTypes.concat(hiddenType); + return { normalTypes, hiddenType, allTypes }; +}; export default function({ getService }: FtrProviderContext) { const supertest = getService('supertestWithoutAuth'); const esArchiver = getService('esArchiver'); - describe('delete', () => { - const { - createExpectUnknownDocNotFound, - deleteTest, - expectEmpty, - expectRbacSpaceAwareForbidden, - expectRbacNotSpaceAwareForbidden, - expectRbacInvalidIdForbidden, - expectRbacHiddenTypeForbidden: expectRbacSpaceTypeForbidden, - expectGenericNotFound, - } = deleteTestSuiteFactory(esArchiver, supertest); - - deleteTest(`user with no access`, { - user: AUTHENTICATION.NOT_A_KIBANA_USER, - tests: { - spaceAware: { - statusCode: 403, - response: expectRbacSpaceAwareForbidden, - }, - notSpaceAware: { - statusCode: 403, - response: expectRbacNotSpaceAwareForbidden, - }, - hiddenType: { - statusCode: 403, - response: expectRbacSpaceTypeForbidden, - }, - invalidId: { - statusCode: 403, - response: expectRbacInvalidIdForbidden, - }, - }, - }); - - deleteTest(`superuser`, { - user: AUTHENTICATION.SUPERUSER, - tests: { - spaceAware: { - statusCode: 200, - response: expectEmpty, - }, - notSpaceAware: { - statusCode: 200, - response: expectEmpty, - }, - hiddenType: { - statusCode: 404, - response: expectGenericNotFound, - }, - invalidId: { - statusCode: 404, - response: createExpectUnknownDocNotFound(), - }, - }, - }); + const { addTests, createTestDefinitions } = deleteTestSuiteFactory(esArchiver, supertest); + const createTests = () => { + const { normalTypes, hiddenType, allTypes } = createTestCases(); + return { + unauthorized: createTestDefinitions(allTypes, true), + authorized: [ + createTestDefinitions(normalTypes, false), + createTestDefinitions(hiddenType, true), + ].flat(), + superuser: createTestDefinitions(allTypes, false), + }; + }; - deleteTest(`legacy user`, { - user: AUTHENTICATION.KIBANA_LEGACY_USER, - tests: { - spaceAware: { - statusCode: 403, - response: expectRbacSpaceAwareForbidden, - }, - notSpaceAware: { - statusCode: 403, - response: expectRbacNotSpaceAwareForbidden, - }, - hiddenType: { - statusCode: 403, - response: expectRbacSpaceTypeForbidden, - }, - invalidId: { - statusCode: 403, - response: expectRbacInvalidIdForbidden, - }, - }, - }); - - deleteTest(`dual-privileges user`, { - user: AUTHENTICATION.KIBANA_DUAL_PRIVILEGES_USER, - tests: { - spaceAware: { - statusCode: 200, - response: expectEmpty, - }, - notSpaceAware: { - statusCode: 200, - response: expectEmpty, - }, - hiddenType: { - statusCode: 403, - response: expectRbacSpaceTypeForbidden, - }, - invalidId: { - statusCode: 404, - response: createExpectUnknownDocNotFound(), - }, - }, - }); - - deleteTest(`dual-privileges readonly user`, { - user: AUTHENTICATION.KIBANA_DUAL_PRIVILEGES_DASHBOARD_ONLY_USER, - tests: { - spaceAware: { - statusCode: 403, - response: expectRbacSpaceAwareForbidden, - }, - notSpaceAware: { - statusCode: 403, - response: expectRbacNotSpaceAwareForbidden, - }, - hiddenType: { - statusCode: 403, - response: expectRbacSpaceTypeForbidden, - }, - invalidId: { - statusCode: 403, - response: expectRbacInvalidIdForbidden, - }, - }, - }); - - deleteTest(`rbac user with all globally`, { - user: AUTHENTICATION.KIBANA_RBAC_USER, - tests: { - spaceAware: { - statusCode: 200, - response: expectEmpty, - }, - notSpaceAware: { - statusCode: 200, - response: expectEmpty, - }, - hiddenType: { - statusCode: 403, - response: expectRbacSpaceTypeForbidden, - }, - invalidId: { - statusCode: 404, - response: createExpectUnknownDocNotFound(), - }, - }, - }); - - deleteTest(`rbac user with read globally`, { - user: AUTHENTICATION.KIBANA_RBAC_DASHBOARD_ONLY_USER, - tests: { - spaceAware: { - statusCode: 403, - response: expectRbacSpaceAwareForbidden, - }, - notSpaceAware: { - statusCode: 403, - response: expectRbacNotSpaceAwareForbidden, - }, - hiddenType: { - statusCode: 403, - response: expectRbacSpaceTypeForbidden, - }, - invalidId: { - statusCode: 403, - response: expectRbacInvalidIdForbidden, - }, - }, - }); - - deleteTest(`rbac user with all at default space`, { - user: AUTHENTICATION.KIBANA_RBAC_DEFAULT_SPACE_ALL_USER, - tests: { - spaceAware: { - statusCode: 403, - response: expectRbacSpaceAwareForbidden, - }, - notSpaceAware: { - statusCode: 403, - response: expectRbacNotSpaceAwareForbidden, - }, - hiddenType: { - statusCode: 403, - response: expectRbacSpaceTypeForbidden, - }, - invalidId: { - statusCode: 403, - response: expectRbacInvalidIdForbidden, - }, - }, - }); - - deleteTest(`rbac user with read at default space`, { - user: AUTHENTICATION.KIBANA_RBAC_DEFAULT_SPACE_READ_USER, - tests: { - spaceAware: { - statusCode: 403, - response: expectRbacSpaceAwareForbidden, - }, - notSpaceAware: { - statusCode: 403, - response: expectRbacNotSpaceAwareForbidden, - }, - hiddenType: { - statusCode: 403, - response: expectRbacSpaceTypeForbidden, - }, - invalidId: { - statusCode: 403, - response: expectRbacInvalidIdForbidden, - }, - }, - }); - - deleteTest(`rbac user with all at space_1`, { - user: AUTHENTICATION.KIBANA_RBAC_SPACE_1_ALL_USER, - tests: { - spaceAware: { - statusCode: 403, - response: expectRbacSpaceAwareForbidden, - }, - notSpaceAware: { - statusCode: 403, - response: expectRbacNotSpaceAwareForbidden, - }, - hiddenType: { - statusCode: 403, - response: expectRbacSpaceTypeForbidden, - }, - invalidId: { - statusCode: 403, - response: expectRbacInvalidIdForbidden, - }, - }, - }); + describe('_delete', () => { + getTestScenarios().security.forEach(({ users }) => { + const { unauthorized, authorized, superuser } = createTests(); + const _addTests = (user: TestUser, tests: DeleteTestDefinition[]) => { + addTests(user.description, { user, tests }); + }; - deleteTest(`rbac user with readonly at space_1`, { - user: AUTHENTICATION.KIBANA_RBAC_SPACE_1_READ_USER, - tests: { - spaceAware: { - statusCode: 403, - response: expectRbacSpaceAwareForbidden, - }, - notSpaceAware: { - statusCode: 403, - response: expectRbacNotSpaceAwareForbidden, - }, - hiddenType: { - statusCode: 403, - response: expectRbacSpaceTypeForbidden, - }, - invalidId: { - statusCode: 403, - response: expectRbacInvalidIdForbidden, - }, - }, + [ + users.noAccess, + users.legacyAll, + users.dualRead, + users.readGlobally, + users.allAtDefaultSpace, + users.readAtDefaultSpace, + users.allAtSpace1, + users.readAtSpace1, + ].forEach(user => { + _addTests(user, unauthorized); + }); + [users.dualAll, users.allGlobally].forEach(user => { + _addTests(user, authorized); + }); + _addTests(users.superuser, superuser); }); }); } diff --git a/x-pack/test/saved_object_api_integration/security_only/apis/export.ts b/x-pack/test/saved_object_api_integration/security_only/apis/export.ts index 2a2c3a9b90b08..0fae45a1897a7 100644 --- a/x-pack/test/saved_object_api_integration/security_only/apis/export.ts +++ b/x-pack/test/saved_object_api_integration/security_only/apis/export.ts @@ -4,252 +4,75 @@ * you may not use this file except in compliance with the Elastic License. */ -import { AUTHENTICATION } from '../../common/lib/authentication'; +import { getTestScenarios } from '../../common/lib/saved_object_test_utils'; +import { TestUser } from '../../common/lib/types'; import { FtrProviderContext } from '../../common/ftr_provider_context'; -import { exportTestSuiteFactory } from '../../common/suites/export'; +import { + exportTestSuiteFactory, + getTestCases, + ExportTestDefinition, +} from '../../common/suites/export'; + +const createTestCases = () => { + const cases = getTestCases(); + const exportableTypes = [ + cases.singleNamespaceObject, + cases.singleNamespaceType, + cases.namespaceAgnosticObject, + cases.namespaceAgnosticType, + ]; + const nonExportableTypes = [ + cases.multiNamespaceObject, + cases.multiNamespaceType, + cases.hiddenObject, + cases.hiddenType, + ]; + const allTypes = exportableTypes.concat(nonExportableTypes); + return { exportableTypes, nonExportableTypes, allTypes }; +}; export default function({ getService }: FtrProviderContext) { const supertest = getService('supertestWithoutAuth'); const esArchiver = getService('esArchiver'); - describe('export', () => { - const { - createExpectRbacForbidden, - expectTypeOrObjectsRequired, - createExpectVisualizationResults, - expectInvalidTypeSpecified, - exportTest, - } = exportTestSuiteFactory(esArchiver, supertest); - - exportTest('user with no access', { - user: AUTHENTICATION.NOT_A_KIBANA_USER, - tests: { - spaceAwareType: { - description: 'forbidden login and find visualization message', - statusCode: 403, - response: createExpectRbacForbidden('visualization'), - }, - hiddenType: { - description: 'exporting space not allowed', - statusCode: 400, - response: expectInvalidTypeSpecified, - }, - noTypeOrObjects: { - description: 'bad request, type or object is required', - statusCode: 400, - response: expectTypeOrObjectsRequired, - }, - }, - }); - - exportTest('superuser', { - user: AUTHENTICATION.SUPERUSER, - tests: { - spaceAwareType: { - description: 'only the visualization', - statusCode: 200, - response: createExpectVisualizationResults(), - }, - hiddenType: { - description: 'exporting space not allowed', - statusCode: 400, - response: expectInvalidTypeSpecified, - }, - noTypeOrObjects: { - description: 'bad request, type or object is required', - statusCode: 400, - response: expectTypeOrObjectsRequired, - }, - }, - }); - - exportTest('legacy user', { - user: AUTHENTICATION.KIBANA_LEGACY_USER, - tests: { - spaceAwareType: { - description: 'forbidden login and find visualization message', - statusCode: 403, - response: createExpectRbacForbidden('visualization'), - }, - hiddenType: { - description: 'exporting space not allowed', - statusCode: 400, - response: expectInvalidTypeSpecified, - }, - noTypeOrObjects: { - description: 'bad request, type or object is required', - statusCode: 400, - response: expectTypeOrObjectsRequired, - }, - }, - }); - - exportTest('dual-privileges user', { - user: AUTHENTICATION.KIBANA_DUAL_PRIVILEGES_USER, - tests: { - spaceAwareType: { - description: 'only the visualization', - statusCode: 200, - response: createExpectVisualizationResults(), - }, - hiddenType: { - description: 'exporting space not allowed', - statusCode: 400, - response: expectInvalidTypeSpecified, - }, - noTypeOrObjects: { - description: 'bad request, type or object is required', - statusCode: 400, - response: expectTypeOrObjectsRequired, - }, - }, - }); - - exportTest('dual-privileges readonly user', { - user: AUTHENTICATION.KIBANA_DUAL_PRIVILEGES_DASHBOARD_ONLY_USER, - tests: { - spaceAwareType: { - description: 'only the visualization', - statusCode: 200, - response: createExpectVisualizationResults(), - }, - hiddenType: { - description: 'exporting space not allowed', - statusCode: 400, - response: expectInvalidTypeSpecified, - }, - noTypeOrObjects: { - description: 'bad request, type or object is required', - statusCode: 400, - response: expectTypeOrObjectsRequired, - }, - }, - }); - - exportTest('rbac user with all globally', { - user: AUTHENTICATION.KIBANA_RBAC_USER, - tests: { - spaceAwareType: { - description: 'only the visualization', - statusCode: 200, - response: createExpectVisualizationResults(), - }, - hiddenType: { - description: 'exporting space not allowed', - statusCode: 400, - response: expectInvalidTypeSpecified, - }, - noTypeOrObjects: { - description: 'bad request, type or object is required', - statusCode: 400, - response: expectTypeOrObjectsRequired, - }, - }, - }); - - exportTest('rbac user with read globally', { - user: AUTHENTICATION.KIBANA_RBAC_DASHBOARD_ONLY_USER, - tests: { - spaceAwareType: { - description: 'only the visualization', - statusCode: 200, - response: createExpectVisualizationResults(), - }, - hiddenType: { - description: 'exporting space not allowed', - statusCode: 400, - response: expectInvalidTypeSpecified, - }, - noTypeOrObjects: { - description: 'bad request, type or object is required', - statusCode: 400, - response: expectTypeOrObjectsRequired, - }, - }, - }); + const { addTests, createTestDefinitions } = exportTestSuiteFactory(esArchiver, supertest); + const createTests = () => { + const { exportableTypes, nonExportableTypes, allTypes } = createTestCases(); + return { + unauthorized: [ + createTestDefinitions(exportableTypes, true), + createTestDefinitions(nonExportableTypes, false), + ].flat(), + authorized: createTestDefinitions(allTypes, false), + }; + }; - exportTest('rbac user with all at default space', { - user: AUTHENTICATION.KIBANA_RBAC_DEFAULT_SPACE_ALL_USER, - tests: { - spaceAwareType: { - description: 'only the visualization', - statusCode: 403, - response: createExpectRbacForbidden('visualization'), - }, - hiddenType: { - description: 'exporting space not allowed', - statusCode: 400, - response: expectInvalidTypeSpecified, - }, - noTypeOrObjects: { - description: 'bad request, type or object is required', - statusCode: 400, - response: expectTypeOrObjectsRequired, - }, - }, - }); - - exportTest('rbac user with read at default space', { - user: AUTHENTICATION.KIBANA_RBAC_DEFAULT_SPACE_READ_USER, - tests: { - spaceAwareType: { - description: 'only the visualization', - statusCode: 403, - response: createExpectRbacForbidden('visualization'), - }, - hiddenType: { - description: 'exporting space not allowed', - statusCode: 400, - response: expectInvalidTypeSpecified, - }, - noTypeOrObjects: { - description: 'bad request, type or object is required', - statusCode: 400, - response: expectTypeOrObjectsRequired, - }, - }, - }); - - exportTest('rbac user with all at space_1', { - user: AUTHENTICATION.KIBANA_RBAC_SPACE_1_ALL_USER, - tests: { - spaceAwareType: { - description: 'only the visualization', - statusCode: 403, - response: createExpectRbacForbidden('visualization'), - }, - hiddenType: { - description: 'exporting space not allowed', - statusCode: 400, - response: expectInvalidTypeSpecified, - }, - noTypeOrObjects: { - description: 'bad request, type or object is required', - statusCode: 400, - response: expectTypeOrObjectsRequired, - }, - }, - }); + describe('_export', () => { + getTestScenarios().security.forEach(({ users }) => { + const { unauthorized, authorized } = createTests(); + const _addTests = (user: TestUser, tests: ExportTestDefinition[]) => { + addTests(user.description, { user, tests }); + }; - exportTest('rbac user with read at space_1', { - user: AUTHENTICATION.KIBANA_RBAC_SPACE_1_READ_USER, - tests: { - spaceAwareType: { - description: 'only the visualization', - statusCode: 403, - response: createExpectRbacForbidden('visualization'), - }, - hiddenType: { - description: 'exporting space not allowed', - statusCode: 400, - response: expectInvalidTypeSpecified, - }, - noTypeOrObjects: { - description: 'bad request, type or object is required', - statusCode: 400, - response: expectTypeOrObjectsRequired, - }, - }, + [ + users.noAccess, + users.legacyAll, + users.allAtDefaultSpace, + users.readAtDefaultSpace, + users.allAtSpace1, + users.readAtSpace1, + ].forEach(user => { + _addTests(user, unauthorized); + }); + [ + users.dualAll, + users.dualRead, + users.allGlobally, + users.readGlobally, + users.superuser, + ].forEach(user => { + _addTests(user, authorized); + }); }); }); } diff --git a/x-pack/test/saved_object_api_integration/security_only/apis/find.ts b/x-pack/test/saved_object_api_integration/security_only/apis/find.ts index 64d85a199e7bc..97513783b94b9 100644 --- a/x-pack/test/saved_object_api_integration/security_only/apis/find.ts +++ b/x-pack/test/saved_object_api_integration/security_only/apis/find.ts @@ -4,749 +4,70 @@ * you may not use this file except in compliance with the Elastic License. */ -import { AUTHENTICATION } from '../../common/lib/authentication'; +import { getTestScenarios } from '../../common/lib/saved_object_test_utils'; +import { TestUser } from '../../common/lib/types'; import { FtrProviderContext } from '../../common/ftr_provider_context'; -import { findTestSuiteFactory } from '../../common/suites/find'; +import { findTestSuiteFactory, getTestCases, FindTestDefinition } from '../../common/suites/find'; + +const createTestCases = () => { + const cases = getTestCases(); + const normalTypes = [ + cases.singleNamespaceType, + cases.multiNamespaceType, + cases.namespaceAgnosticType, + cases.pageBeyondTotal, + cases.unknownSearchField, + cases.filterWithNamespaceAgnosticType, + cases.filterWithDisallowedType, + ]; + const hiddenAndUnknownTypes = [ + cases.hiddenType, + cases.unknownType, + cases.filterWithHiddenType, + cases.filterWithUnknownType, + ]; + const allTypes = normalTypes.concat(hiddenAndUnknownTypes); + return { normalTypes, hiddenAndUnknownTypes, allTypes }; +}; export default function({ getService }: FtrProviderContext) { const supertest = getService('supertestWithoutAuth'); const esArchiver = getService('esArchiver'); - describe('find', () => { - const { - createExpectEmpty, - createExpectRbacForbidden, - createExpectVisualizationResults, - expectFilterWrongTypeError, - expectNotSpaceAwareResults, - expectTypeRequired, - findTest, - } = findTestSuiteFactory(esArchiver, supertest); - - findTest(`user with no access`, { - user: AUTHENTICATION.NOT_A_KIBANA_USER, - tests: { - spaceAwareType: { - description: 'forbidden login and find visualization message', - statusCode: 403, - response: createExpectRbacForbidden('visualization'), - }, - notSpaceAwareType: { - description: 'forbidden login and find globaltype message', - statusCode: 403, - response: createExpectRbacForbidden('globaltype'), - }, - hiddenType: { - description: 'forbidden login and find hiddentype message', - statusCode: 403, - response: createExpectRbacForbidden('hiddentype'), - }, - unknownType: { - description: 'forbidden find wigwags message', - statusCode: 403, - response: createExpectRbacForbidden('wigwags'), - }, - pageBeyondTotal: { - description: 'forbidden login and find visualization message', - statusCode: 403, - response: createExpectRbacForbidden('visualization'), - }, - unknownSearchField: { - description: 'forbidden login and unknown search field', - statusCode: 403, - response: createExpectRbacForbidden('url'), - }, - noType: { - description: 'bad request, type is required', - statusCode: 400, - response: expectTypeRequired, - }, - filterWithNotSpaceAwareType: { - description: 'forbidden login and find globaltype message', - statusCode: 403, - response: createExpectRbacForbidden('globaltype'), - }, - filterWithHiddenType: { - description: 'forbidden login and find hiddentype message', - statusCode: 403, - response: createExpectRbacForbidden('hiddentype'), - }, - filterWithUnknownType: { - description: 'forbidden find wigwags message', - statusCode: 403, - response: createExpectRbacForbidden('wigwags'), - }, - filterWithNoType: { - description: 'bad request, type is required', - statusCode: 400, - response: expectTypeRequired, - }, - filterWithUnAllowedType: { - description: 'forbidden', - statusCode: 403, - response: createExpectRbacForbidden('globaltype'), - }, - }, - }); - - findTest(`superuser`, { - user: AUTHENTICATION.SUPERUSER, - tests: { - spaceAwareType: { - description: 'only the visualization', - statusCode: 200, - response: createExpectVisualizationResults(), - }, - notSpaceAwareType: { - description: 'only the globaltype', - statusCode: 200, - response: expectNotSpaceAwareResults, - }, - hiddenType: { - description: 'empty result', - statusCode: 200, - response: createExpectEmpty(1, 20, 0), - }, - unknownType: { - description: 'empty result', - statusCode: 200, - response: createExpectEmpty(1, 20, 0), - }, - pageBeyondTotal: { - description: 'empty result', - statusCode: 200, - response: createExpectEmpty(100, 100, 1), - }, - unknownSearchField: { - description: 'empty result', - statusCode: 200, - response: createExpectEmpty(1, 20, 0), - }, - noType: { - description: 'bad request, type is required', - statusCode: 400, - response: expectTypeRequired, - }, - filterWithNotSpaceAwareType: { - description: 'only the globaltype', - statusCode: 200, - response: expectNotSpaceAwareResults, - }, - filterWithHiddenType: { - description: 'empty result', - statusCode: 200, - response: createExpectEmpty(1, 20, 0), - }, - filterWithUnknownType: { - description: 'empty result', - statusCode: 200, - response: createExpectEmpty(1, 20, 0), - }, - filterWithNoType: { - description: 'bad request, type is required', - statusCode: 400, - response: expectTypeRequired, - }, - filterWithUnAllowedType: { - description: 'Bad Request', - statusCode: 400, - response: expectFilterWrongTypeError, - }, - }, - }); - - findTest(`legacy user`, { - user: AUTHENTICATION.KIBANA_LEGACY_USER, - tests: { - spaceAwareType: { - description: 'forbidden login and find visualization message', - statusCode: 403, - response: createExpectRbacForbidden('visualization'), - }, - notSpaceAwareType: { - description: 'forbidden login and find globaltype message', - statusCode: 403, - response: createExpectRbacForbidden('globaltype'), - }, - hiddenType: { - description: 'forbidden login and find hiddentype message', - statusCode: 403, - response: createExpectRbacForbidden('hiddentype'), - }, - unknownType: { - description: 'forbidden find wigwags message', - statusCode: 403, - response: createExpectRbacForbidden('wigwags'), - }, - pageBeyondTotal: { - description: 'forbidden login and find visualization message', - statusCode: 403, - response: createExpectRbacForbidden('visualization'), - }, - unknownSearchField: { - description: 'forbidden login and unknown search field', - statusCode: 403, - response: createExpectRbacForbidden('url'), - }, - noType: { - description: 'bad request, type is required', - statusCode: 400, - response: expectTypeRequired, - }, - filterWithNotSpaceAwareType: { - description: 'forbidden login and find globaltype message', - statusCode: 403, - response: createExpectRbacForbidden('globaltype'), - }, - filterWithHiddenType: { - description: 'forbidden login and find hiddentype message', - statusCode: 403, - response: createExpectRbacForbidden('hiddentype'), - }, - filterWithUnknownType: { - description: 'forbidden find wigwags message', - statusCode: 403, - response: createExpectRbacForbidden('wigwags'), - }, - filterWithNoType: { - description: 'bad request, type is required', - statusCode: 400, - response: expectTypeRequired, - }, - filterWithUnAllowedType: { - description: 'forbidden', - statusCode: 403, - response: createExpectRbacForbidden('globaltype'), - }, - }, - }); - - findTest(`dual-privileges user`, { - user: AUTHENTICATION.KIBANA_DUAL_PRIVILEGES_USER, - tests: { - spaceAwareType: { - description: 'only the visualization', - statusCode: 200, - response: createExpectVisualizationResults(), - }, - notSpaceAwareType: { - description: 'only the globaltype', - statusCode: 200, - response: expectNotSpaceAwareResults, - }, - hiddenType: { - description: 'forbidden find hiddentype message', - statusCode: 403, - response: createExpectRbacForbidden('hiddentype'), - }, - unknownType: { - description: 'forbidden find wigwags message', - statusCode: 403, - response: createExpectRbacForbidden('wigwags'), - }, - pageBeyondTotal: { - description: 'empty result', - statusCode: 200, - response: createExpectEmpty(100, 100, 1), - }, - unknownSearchField: { - description: 'empty result', - statusCode: 200, - response: createExpectEmpty(1, 20, 0), - }, - noType: { - description: 'bad request, type is required', - statusCode: 400, - response: expectTypeRequired, - }, - filterWithNotSpaceAwareType: { - description: 'only the globaltype', - statusCode: 200, - response: expectNotSpaceAwareResults, - }, - filterWithHiddenType: { - description: 'forbidden find hiddentype message', - statusCode: 403, - response: createExpectRbacForbidden('hiddentype'), - }, - filterWithUnknownType: { - description: 'forbidden find wigwags message', - statusCode: 403, - response: createExpectRbacForbidden('wigwags'), - }, - filterWithNoType: { - description: 'bad request, type is required', - statusCode: 400, - response: expectTypeRequired, - }, - filterWithUnAllowedType: { - description: 'Bad Request', - statusCode: 400, - response: expectFilterWrongTypeError, - }, - }, - }); - - findTest(`dual-privileges readonly user`, { - user: AUTHENTICATION.KIBANA_DUAL_PRIVILEGES_DASHBOARD_ONLY_USER, - tests: { - spaceAwareType: { - description: 'only the visualization', - statusCode: 200, - response: createExpectVisualizationResults(), - }, - notSpaceAwareType: { - description: 'only the globaltype', - statusCode: 200, - response: expectNotSpaceAwareResults, - }, - hiddenType: { - description: 'forbidden find hiddentype message', - statusCode: 403, - response: createExpectRbacForbidden('hiddentype'), - }, - unknownType: { - description: 'forbidden find wigwags message', - statusCode: 403, - response: createExpectRbacForbidden('wigwags'), - }, - pageBeyondTotal: { - description: 'empty result', - statusCode: 200, - response: createExpectEmpty(100, 100, 1), - }, - unknownSearchField: { - description: 'empty result', - statusCode: 200, - response: createExpectEmpty(1, 20, 0), - }, - noType: { - description: 'bad request, type is required', - statusCode: 400, - response: expectTypeRequired, - }, - filterWithNotSpaceAwareType: { - description: 'only the globaltype', - statusCode: 200, - response: expectNotSpaceAwareResults, - }, - filterWithHiddenType: { - description: 'forbidden find hiddentype message', - statusCode: 403, - response: createExpectRbacForbidden('hiddentype'), - }, - filterWithUnknownType: { - description: 'forbidden find wigwags message', - statusCode: 403, - response: createExpectRbacForbidden('wigwags'), - }, - filterWithNoType: { - description: 'bad request, type is required', - statusCode: 400, - response: expectTypeRequired, - }, - filterWithUnAllowedType: { - description: 'Bad Request', - statusCode: 400, - response: expectFilterWrongTypeError, - }, - }, - }); - - findTest(`rbac user with all globally`, { - user: AUTHENTICATION.KIBANA_RBAC_USER, - tests: { - spaceAwareType: { - description: 'only the visualization', - statusCode: 200, - response: createExpectVisualizationResults(), - }, - notSpaceAwareType: { - description: 'only the globaltype', - statusCode: 200, - response: expectNotSpaceAwareResults, - }, - hiddenType: { - description: 'forbidden find hiddentype message', - statusCode: 403, - response: createExpectRbacForbidden('hiddentype'), - }, - unknownType: { - description: 'forbidden find wigwags message', - statusCode: 403, - response: createExpectRbacForbidden('wigwags'), - }, - pageBeyondTotal: { - description: 'empty result', - statusCode: 200, - response: createExpectEmpty(100, 100, 1), - }, - unknownSearchField: { - description: 'empty result', - statusCode: 200, - response: createExpectEmpty(1, 20, 0), - }, - noType: { - description: 'bad request, type is required', - statusCode: 400, - response: expectTypeRequired, - }, - filterWithNotSpaceAwareType: { - description: 'only the globaltype', - statusCode: 200, - response: expectNotSpaceAwareResults, - }, - filterWithHiddenType: { - description: 'forbidden find hiddentype message', - statusCode: 403, - response: createExpectRbacForbidden('hiddentype'), - }, - filterWithUnknownType: { - description: 'forbidden find wigwags message', - statusCode: 403, - response: createExpectRbacForbidden('wigwags'), - }, - filterWithNoType: { - description: 'bad request, type is required', - statusCode: 400, - response: expectTypeRequired, - }, - filterWithUnAllowedType: { - description: 'Bad Request', - statusCode: 400, - response: expectFilterWrongTypeError, - }, - }, - }); - - findTest(`rbac user with read globally`, { - user: AUTHENTICATION.KIBANA_RBAC_DASHBOARD_ONLY_USER, - tests: { - spaceAwareType: { - description: 'only the visualization', - statusCode: 200, - response: createExpectVisualizationResults(), - }, - notSpaceAwareType: { - description: 'only the globaltype', - statusCode: 200, - response: expectNotSpaceAwareResults, - }, - hiddenType: { - description: 'forbidden find hiddentype message', - statusCode: 403, - response: createExpectRbacForbidden('hiddentype'), - }, - unknownType: { - description: 'forbidden find wigwags message', - statusCode: 403, - response: createExpectRbacForbidden('wigwags'), - }, - pageBeyondTotal: { - description: 'empty result', - statusCode: 200, - response: createExpectEmpty(100, 100, 1), - }, - unknownSearchField: { - description: 'empty result', - statusCode: 200, - response: createExpectEmpty(1, 20, 0), - }, - noType: { - description: 'bad request, type is required', - statusCode: 400, - response: expectTypeRequired, - }, - filterWithNotSpaceAwareType: { - description: 'only the globaltype', - statusCode: 200, - response: expectNotSpaceAwareResults, - }, - filterWithHiddenType: { - description: 'forbidden find hiddentype message', - statusCode: 403, - response: createExpectRbacForbidden('hiddentype'), - }, - filterWithUnknownType: { - description: 'forbidden find wigwags message', - statusCode: 403, - response: createExpectRbacForbidden('wigwags'), - }, - filterWithNoType: { - description: 'bad request, type is required', - statusCode: 400, - response: expectTypeRequired, - }, - filterWithUnAllowedType: { - description: 'Bad Request', - statusCode: 400, - response: expectFilterWrongTypeError, - }, - }, - }); + const { addTests, createTestDefinitions } = findTestSuiteFactory(esArchiver, supertest); + const createTests = () => { + const { normalTypes, hiddenAndUnknownTypes, allTypes } = createTestCases(); + return { + unauthorized: createTestDefinitions(allTypes, true), + authorized: [ + createTestDefinitions(normalTypes, false), + createTestDefinitions(hiddenAndUnknownTypes, true), + ].flat(), + superuser: createTestDefinitions(allTypes, false), + }; + }; - findTest(`rbac user with all at default space`, { - user: AUTHENTICATION.KIBANA_RBAC_DEFAULT_SPACE_ALL_USER, - tests: { - spaceAwareType: { - description: 'only the visualization', - statusCode: 403, - response: createExpectRbacForbidden('visualization'), - }, - notSpaceAwareType: { - description: 'only the globaltype', - statusCode: 403, - response: createExpectRbacForbidden('globaltype'), - }, - hiddenType: { - description: 'forbidden find hiddentype message', - statusCode: 403, - response: createExpectRbacForbidden('hiddentype'), - }, - unknownType: { - description: 'forbidden find wigwags message', - statusCode: 403, - response: createExpectRbacForbidden('wigwags'), - }, - pageBeyondTotal: { - description: 'empty result', - statusCode: 403, - response: createExpectRbacForbidden('visualization'), - }, - unknownSearchField: { - description: 'forbidden login and unknown search field', - statusCode: 403, - response: createExpectRbacForbidden('url'), - }, - noType: { - description: 'bad request, type is required', - statusCode: 400, - response: expectTypeRequired, - }, - filterWithNotSpaceAwareType: { - description: 'only the globaltype', - statusCode: 403, - response: createExpectRbacForbidden('globaltype'), - }, - filterWithHiddenType: { - description: 'forbidden find hiddentype message', - statusCode: 403, - response: createExpectRbacForbidden('hiddentype'), - }, - filterWithUnknownType: { - description: 'forbidden find wigwags message', - statusCode: 403, - response: createExpectRbacForbidden('wigwags'), - }, - filterWithNoType: { - description: 'bad request, type is required', - statusCode: 400, - response: expectTypeRequired, - }, - filterWithUnAllowedType: { - description: 'forbidden', - statusCode: 403, - response: createExpectRbacForbidden('globaltype'), - }, - }, - }); - - findTest(`rbac user with read at default space`, { - user: AUTHENTICATION.KIBANA_RBAC_DEFAULT_SPACE_READ_USER, - tests: { - spaceAwareType: { - description: 'only the visualization', - statusCode: 403, - response: createExpectRbacForbidden('visualization'), - }, - notSpaceAwareType: { - description: 'only the globaltype', - statusCode: 403, - response: createExpectRbacForbidden('globaltype'), - }, - hiddenType: { - description: 'forbidden find hiddentype message', - statusCode: 403, - response: createExpectRbacForbidden('hiddentype'), - }, - unknownType: { - description: 'forbidden find wigwags message', - statusCode: 403, - response: createExpectRbacForbidden('wigwags'), - }, - pageBeyondTotal: { - description: 'empty result', - statusCode: 403, - response: createExpectRbacForbidden('visualization'), - }, - unknownSearchField: { - description: 'forbidden login and unknown search field', - statusCode: 403, - response: createExpectRbacForbidden('url'), - }, - noType: { - description: 'bad request, type is required', - statusCode: 400, - response: expectTypeRequired, - }, - filterWithNotSpaceAwareType: { - description: 'only the globaltype', - statusCode: 403, - response: createExpectRbacForbidden('globaltype'), - }, - filterWithHiddenType: { - description: 'forbidden find hiddentype message', - statusCode: 403, - response: createExpectRbacForbidden('hiddentype'), - }, - filterWithUnknownType: { - description: 'forbidden find wigwags message', - statusCode: 403, - response: createExpectRbacForbidden('wigwags'), - }, - filterWithNoType: { - description: 'bad request, type is required', - statusCode: 400, - response: expectTypeRequired, - }, - filterWithUnAllowedType: { - description: 'forbidden', - statusCode: 403, - response: createExpectRbacForbidden('globaltype'), - }, - }, - }); - - findTest(`rbac user with all at space_1`, { - user: AUTHENTICATION.KIBANA_RBAC_SPACE_1_ALL_USER, - tests: { - spaceAwareType: { - description: 'only the visualization', - statusCode: 403, - response: createExpectRbacForbidden('visualization'), - }, - notSpaceAwareType: { - description: 'only the globaltype', - statusCode: 403, - response: createExpectRbacForbidden('globaltype'), - }, - hiddenType: { - description: 'forbidden find hiddentype message', - statusCode: 403, - response: createExpectRbacForbidden('hiddentype'), - }, - unknownType: { - description: 'forbidden find wigwags message', - statusCode: 403, - response: createExpectRbacForbidden('wigwags'), - }, - pageBeyondTotal: { - description: 'empty result', - statusCode: 403, - response: createExpectRbacForbidden('visualization'), - }, - unknownSearchField: { - description: 'forbidden login and unknown search field', - statusCode: 403, - response: createExpectRbacForbidden('url'), - }, - noType: { - description: 'bad request, type is required', - statusCode: 400, - response: expectTypeRequired, - }, - filterWithNotSpaceAwareType: { - description: 'only the globaltype', - statusCode: 403, - response: createExpectRbacForbidden('globaltype'), - }, - filterWithHiddenType: { - description: 'forbidden find hiddentype message', - statusCode: 403, - response: createExpectRbacForbidden('hiddentype'), - }, - filterWithUnknownType: { - description: 'forbidden find wigwags message', - statusCode: 403, - response: createExpectRbacForbidden('wigwags'), - }, - filterWithNoType: { - description: 'bad request, type is required', - statusCode: 400, - response: expectTypeRequired, - }, - filterWithUnAllowedType: { - description: 'forbidden', - statusCode: 403, - response: createExpectRbacForbidden('globaltype'), - }, - }, - }); + describe('_find', () => { + getTestScenarios().security.forEach(({ users }) => { + const { unauthorized, authorized, superuser } = createTests(); + const _addTests = (user: TestUser, tests: FindTestDefinition[]) => { + addTests(user.description, { user, tests }); + }; - findTest(`rbac user with read at space_1`, { - user: AUTHENTICATION.KIBANA_RBAC_SPACE_1_READ_USER, - tests: { - spaceAwareType: { - description: 'only the visualization', - statusCode: 403, - response: createExpectRbacForbidden('visualization'), - }, - notSpaceAwareType: { - description: 'only the globaltype', - statusCode: 403, - response: createExpectRbacForbidden('globaltype'), - }, - hiddenType: { - description: 'forbidden find hiddentype message', - statusCode: 403, - response: createExpectRbacForbidden('hiddentype'), - }, - unknownType: { - description: 'forbidden find wigwags message', - statusCode: 403, - response: createExpectRbacForbidden('wigwags'), - }, - pageBeyondTotal: { - description: 'empty result', - statusCode: 403, - response: createExpectRbacForbidden('visualization'), - }, - unknownSearchField: { - description: 'forbidden login and unknown search field', - statusCode: 403, - response: createExpectRbacForbidden('url'), - }, - noType: { - description: 'bad request, type is required', - statusCode: 400, - response: expectTypeRequired, - }, - filterWithNotSpaceAwareType: { - description: 'only the globaltype', - statusCode: 403, - response: createExpectRbacForbidden('globaltype'), - }, - filterWithHiddenType: { - description: 'forbidden find hiddentype message', - statusCode: 403, - response: createExpectRbacForbidden('hiddentype'), - }, - filterWithUnknownType: { - description: 'forbidden find wigwags message', - statusCode: 403, - response: createExpectRbacForbidden('wigwags'), - }, - filterWithNoType: { - description: 'bad request, type is required', - statusCode: 400, - response: expectTypeRequired, - }, - filterWithUnAllowedType: { - description: 'forbidden', - statusCode: 403, - response: createExpectRbacForbidden('globaltype'), - }, - }, + [ + users.noAccess, + users.legacyAll, + users.allAtDefaultSpace, + users.readAtDefaultSpace, + users.allAtSpace1, + users.readAtSpace1, + ].forEach(user => { + _addTests(user, unauthorized); + }); + [users.dualAll, users.dualRead, users.allGlobally, users.readGlobally].forEach(user => { + _addTests(user, authorized); + }); + _addTests(users.superuser, superuser); }); }); } diff --git a/x-pack/test/saved_object_api_integration/security_only/apis/get.ts b/x-pack/test/saved_object_api_integration/security_only/apis/get.ts index 2a31463fce8b2..7cd50fe4cea61 100644 --- a/x-pack/test/saved_object_api_integration/security_only/apis/get.ts +++ b/x-pack/test/saved_object_api_integration/security_only/apis/get.ts @@ -4,267 +4,73 @@ * you may not use this file except in compliance with the Elastic License. */ -import { AUTHENTICATION } from '../../common/lib/authentication'; +import { testCaseFailures, getTestScenarios } from '../../common/lib/saved_object_test_utils'; +import { TestUser } from '../../common/lib/types'; import { FtrProviderContext } from '../../common/ftr_provider_context'; -import { getTestSuiteFactory } from '../../common/suites/get'; +import { + getTestSuiteFactory, + TEST_CASES as CASES, + GetTestDefinition, +} from '../../common/suites/get'; + +const { fail404 } = testCaseFailures; + +const createTestCases = () => { + // for each permitted (non-403) outcome, if failure !== undefined then we expect + // to receive an error; otherwise, we expect to receive a success result + const normalTypes = [ + CASES.SINGLE_NAMESPACE_DEFAULT_SPACE, + { ...CASES.SINGLE_NAMESPACE_SPACE_1, ...fail404() }, + { ...CASES.SINGLE_NAMESPACE_SPACE_2, ...fail404() }, + CASES.MULTI_NAMESPACE_DEFAULT_AND_SPACE_1, + { ...CASES.MULTI_NAMESPACE_ONLY_SPACE_1, ...fail404() }, + { ...CASES.MULTI_NAMESPACE_ONLY_SPACE_2, ...fail404() }, + CASES.NAMESPACE_AGNOSTIC, + { ...CASES.DOES_NOT_EXIST, ...fail404() }, + ]; + const hiddenType = [{ ...CASES.HIDDEN, ...fail404() }]; + const allTypes = normalTypes.concat(hiddenType); + return { normalTypes, hiddenType, allTypes }; +}; export default function({ getService }: FtrProviderContext) { const supertest = getService('supertestWithoutAuth'); const esArchiver = getService('esArchiver'); - const { - createExpectDoesntExistNotFound, - createExpectSpaceAwareResults, - createExpectNotSpaceAwareResults, - expectSpaceAwareRbacForbidden, - expectNotSpaceAwareRbacForbidden, - expectDoesntExistRbacForbidden, - expectHiddenTypeRbacForbidden, - expectHiddenTypeNotFound, - getTest, - } = getTestSuiteFactory(esArchiver, supertest); - - describe('get', () => { - getTest(`user with no access`, { - user: AUTHENTICATION.NOT_A_KIBANA_USER, - tests: { - spaceAware: { - statusCode: 403, - response: expectSpaceAwareRbacForbidden, - }, - hiddenType: { - statusCode: 403, - response: expectHiddenTypeRbacForbidden, - }, - notSpaceAware: { - statusCode: 403, - response: expectNotSpaceAwareRbacForbidden, - }, - doesntExist: { - statusCode: 403, - response: expectDoesntExistRbacForbidden, - }, - }, - }); - - getTest(`superuser`, { - user: AUTHENTICATION.SUPERUSER, - tests: { - spaceAware: { - statusCode: 200, - response: createExpectSpaceAwareResults(), - }, - notSpaceAware: { - statusCode: 200, - response: createExpectNotSpaceAwareResults(), - }, - hiddenType: { - statusCode: 404, - response: expectHiddenTypeNotFound, - }, - doesntExist: { - statusCode: 404, - response: createExpectDoesntExistNotFound(), - }, - }, - }); + const { addTests, createTestDefinitions } = getTestSuiteFactory(esArchiver, supertest); + const createTests = () => { + const { normalTypes, hiddenType, allTypes } = createTestCases(); + return { + unauthorized: createTestDefinitions(allTypes, true), + authorized: [ + createTestDefinitions(normalTypes, false), + createTestDefinitions(hiddenType, true), + ].flat(), + superuser: createTestDefinitions(allTypes, false), + }; + }; - getTest(`legacy user`, { - user: AUTHENTICATION.KIBANA_LEGACY_USER, - tests: { - spaceAware: { - statusCode: 403, - response: expectSpaceAwareRbacForbidden, - }, - notSpaceAware: { - statusCode: 403, - response: expectNotSpaceAwareRbacForbidden, - }, - hiddenType: { - statusCode: 403, - response: expectHiddenTypeRbacForbidden, - }, - doesntExist: { - statusCode: 403, - response: expectDoesntExistRbacForbidden, - }, - }, - }); - - getTest(`dual-privileges user`, { - user: AUTHENTICATION.KIBANA_DUAL_PRIVILEGES_USER, - tests: { - spaceAware: { - statusCode: 200, - response: createExpectSpaceAwareResults(), - }, - notSpaceAware: { - statusCode: 200, - response: createExpectNotSpaceAwareResults(), - }, - hiddenType: { - statusCode: 403, - response: expectHiddenTypeRbacForbidden, - }, - doesntExist: { - statusCode: 404, - response: createExpectDoesntExistNotFound(), - }, - }, - }); - - getTest(`dual-privileges readonly user`, { - user: AUTHENTICATION.KIBANA_DUAL_PRIVILEGES_DASHBOARD_ONLY_USER, - tests: { - spaceAware: { - statusCode: 200, - response: createExpectSpaceAwareResults(), - }, - notSpaceAware: { - statusCode: 200, - response: createExpectNotSpaceAwareResults(), - }, - hiddenType: { - statusCode: 403, - response: expectHiddenTypeRbacForbidden, - }, - doesntExist: { - statusCode: 404, - response: createExpectDoesntExistNotFound(), - }, - }, - }); - - getTest(`rbac user with all globally`, { - user: AUTHENTICATION.KIBANA_RBAC_USER, - tests: { - spaceAware: { - statusCode: 200, - response: createExpectSpaceAwareResults(), - }, - notSpaceAware: { - statusCode: 200, - response: createExpectNotSpaceAwareResults(), - }, - hiddenType: { - statusCode: 403, - response: expectHiddenTypeRbacForbidden, - }, - doesntExist: { - statusCode: 404, - response: createExpectDoesntExistNotFound(), - }, - }, - }); - - getTest(`rbac user with read globally`, { - user: AUTHENTICATION.KIBANA_RBAC_DASHBOARD_ONLY_USER, - tests: { - spaceAware: { - statusCode: 200, - response: createExpectSpaceAwareResults(), - }, - notSpaceAware: { - statusCode: 200, - response: createExpectNotSpaceAwareResults(), - }, - hiddenType: { - statusCode: 403, - response: expectHiddenTypeRbacForbidden, - }, - doesntExist: { - statusCode: 404, - response: createExpectDoesntExistNotFound(), - }, - }, - }); - - getTest(`rbac user with all at default space`, { - user: AUTHENTICATION.KIBANA_RBAC_DEFAULT_SPACE_ALL_USER, - tests: { - spaceAware: { - statusCode: 403, - response: expectSpaceAwareRbacForbidden, - }, - notSpaceAware: { - statusCode: 403, - response: expectNotSpaceAwareRbacForbidden, - }, - hiddenType: { - statusCode: 403, - response: expectHiddenTypeRbacForbidden, - }, - doesntExist: { - statusCode: 403, - response: expectDoesntExistRbacForbidden, - }, - }, - }); - - getTest(`rbac user with read at default space`, { - user: AUTHENTICATION.KIBANA_RBAC_DEFAULT_SPACE_READ_USER, - tests: { - spaceAware: { - statusCode: 403, - response: expectSpaceAwareRbacForbidden, - }, - notSpaceAware: { - statusCode: 403, - response: expectNotSpaceAwareRbacForbidden, - }, - hiddenType: { - statusCode: 403, - response: expectHiddenTypeRbacForbidden, - }, - doesntExist: { - statusCode: 403, - response: expectDoesntExistRbacForbidden, - }, - }, - }); - - getTest(`rbac user with all at space_1`, { - user: AUTHENTICATION.KIBANA_RBAC_SPACE_1_ALL_USER, - tests: { - spaceAware: { - statusCode: 403, - response: expectSpaceAwareRbacForbidden, - }, - notSpaceAware: { - statusCode: 403, - response: expectNotSpaceAwareRbacForbidden, - }, - hiddenType: { - statusCode: 403, - response: expectHiddenTypeRbacForbidden, - }, - doesntExist: { - statusCode: 403, - response: expectDoesntExistRbacForbidden, - }, - }, - }); + describe('_get', () => { + getTestScenarios().security.forEach(({ users }) => { + const { unauthorized, authorized, superuser } = createTests(); + const _addTests = (user: TestUser, tests: GetTestDefinition[]) => { + addTests(user.description, { user, tests }); + }; - getTest(`rbac user with read at space_1`, { - user: AUTHENTICATION.KIBANA_RBAC_SPACE_1_READ_USER, - tests: { - spaceAware: { - statusCode: 403, - response: expectSpaceAwareRbacForbidden, - }, - notSpaceAware: { - statusCode: 403, - response: expectNotSpaceAwareRbacForbidden, - }, - hiddenType: { - statusCode: 403, - response: expectHiddenTypeRbacForbidden, - }, - doesntExist: { - statusCode: 403, - response: expectDoesntExistRbacForbidden, - }, - }, + [ + users.noAccess, + users.legacyAll, + users.allAtDefaultSpace, + users.readAtDefaultSpace, + users.allAtSpace1, + users.readAtSpace1, + ].forEach(user => { + _addTests(user, unauthorized); + }); + [users.dualAll, users.dualRead, users.allGlobally, users.readGlobally].forEach(user => { + _addTests(user, authorized); + }); + _addTests(users.superuser, superuser); }); }); } diff --git a/x-pack/test/saved_object_api_integration/security_only/apis/import.ts b/x-pack/test/saved_object_api_integration/security_only/apis/import.ts index 770410dcfed81..5a6e530b02939 100644 --- a/x-pack/test/saved_object_api_integration/security_only/apis/import.ts +++ b/x-pack/test/saved_object_api_integration/security_only/apis/import.ts @@ -4,223 +4,87 @@ * you may not use this file except in compliance with the Elastic License. */ -import { AUTHENTICATION } from '../../common/lib/authentication'; +import { testCaseFailures, getTestScenarios } from '../../common/lib/saved_object_test_utils'; +import { TestUser } from '../../common/lib/types'; import { FtrProviderContext } from '../../common/ftr_provider_context'; -import { importTestSuiteFactory } from '../../common/suites/import'; +import { + importTestSuiteFactory, + TEST_CASES as CASES, + ImportTestDefinition, +} from '../../common/suites/import'; + +const { fail400, fail409 } = testCaseFailures; + +const createTestCases = () => { + // for each permitted (non-403) outcome, if failure !== undefined then we expect + // to receive an error; otherwise, we expect to receive a success result + const importableTypes = [ + { ...CASES.SINGLE_NAMESPACE_DEFAULT_SPACE, ...fail409() }, + CASES.SINGLE_NAMESPACE_SPACE_1, + CASES.SINGLE_NAMESPACE_SPACE_2, + { ...CASES.NAMESPACE_AGNOSTIC, ...fail409() }, + CASES.NEW_SINGLE_NAMESPACE_OBJ, + CASES.NEW_NAMESPACE_AGNOSTIC_OBJ, + ]; + const nonImportableTypes = [ + { ...CASES.MULTI_NAMESPACE_DEFAULT_AND_SPACE_1, ...fail400() }, + { ...CASES.MULTI_NAMESPACE_ONLY_SPACE_1, ...fail400() }, + { ...CASES.MULTI_NAMESPACE_ONLY_SPACE_2, ...fail400() }, + { ...CASES.HIDDEN, ...fail400() }, + { ...CASES.NEW_MULTI_NAMESPACE_OBJ, ...fail400() }, + ]; + const allTypes = importableTypes.concat(nonImportableTypes); + return { importableTypes, nonImportableTypes, allTypes }; +}; export default function({ getService }: FtrProviderContext) { const supertest = getService('supertestWithoutAuth'); const esArchiver = getService('esArchiver'); const es = getService('legacyEs'); - const { - importTest, - createExpectResults, - expectRbacForbidden, - expectUnknownTypeUnsupported, - expectResultsWithUnsupportedHiddenType, - } = importTestSuiteFactory(es, esArchiver, supertest); + const { addTests, createTestDefinitions, expectForbidden } = importTestSuiteFactory( + es, + esArchiver, + supertest + ); + const createTests = () => { + const { importableTypes, nonImportableTypes, allTypes } = createTestCases(); + // use singleRequest to reduce execution time and/or test combined cases + return { + unauthorized: [ + createTestDefinitions(importableTypes, true), + createTestDefinitions(nonImportableTypes, false, { singleRequest: true }), + createTestDefinitions(allTypes, true, { + singleRequest: true, + responseBodyOverride: expectForbidden(['dashboard', 'globaltype', 'isolatedtype']), + }), + ].flat(), + authorized: createTestDefinitions(allTypes, false, { singleRequest: true }), + }; + }; describe('_import', () => { - importTest(`user with no access`, { - user: AUTHENTICATION.NOT_A_KIBANA_USER, - tests: { - default: { - statusCode: 403, - response: expectRbacForbidden, - }, - hiddenType: { - statusCode: 403, - response: expectRbacForbidden, - }, - unknownType: { - statusCode: 403, - response: expectRbacForbidden, - }, - }, - }); - - importTest(`superuser`, { - user: AUTHENTICATION.SUPERUSER, - tests: { - default: { - statusCode: 200, - response: createExpectResults(), - }, - hiddenType: { - // import filters out the space type, so the remaining objects will import successfully - statusCode: 200, - response: expectResultsWithUnsupportedHiddenType, - }, - unknownType: { - statusCode: 200, - response: expectUnknownTypeUnsupported, - }, - }, - }); - - importTest(`legacy user`, { - user: AUTHENTICATION.KIBANA_LEGACY_USER, - tests: { - default: { - statusCode: 403, - response: expectRbacForbidden, - }, - hiddenType: { - statusCode: 403, - response: expectRbacForbidden, - }, - unknownType: { - statusCode: 403, - response: expectRbacForbidden, - }, - }, - }); - - importTest(`dual-privileges user`, { - user: AUTHENTICATION.KIBANA_DUAL_PRIVILEGES_USER, - tests: { - default: { - statusCode: 200, - response: createExpectResults(), - }, - hiddenType: { - // import filters out the space type, so the remaining objects will import successfully - statusCode: 200, - response: expectResultsWithUnsupportedHiddenType, - }, - unknownType: { - statusCode: 200, - response: expectUnknownTypeUnsupported, - }, - }, - }); - - importTest(`dual-privileges readonly user`, { - user: AUTHENTICATION.KIBANA_DUAL_PRIVILEGES_DASHBOARD_ONLY_USER, - tests: { - default: { - statusCode: 403, - response: expectRbacForbidden, - }, - hiddenType: { - statusCode: 403, - response: expectRbacForbidden, - }, - unknownType: { - statusCode: 403, - response: expectRbacForbidden, - }, - }, - }); - - importTest(`rbac user with all globally`, { - user: AUTHENTICATION.KIBANA_RBAC_USER, - tests: { - default: { - statusCode: 200, - response: createExpectResults(), - }, - hiddenType: { - // import filters out the space type, so the remaining objects will import successfully - statusCode: 200, - response: expectResultsWithUnsupportedHiddenType, - }, - unknownType: { - statusCode: 200, - response: expectUnknownTypeUnsupported, - }, - }, - }); - - importTest(`rbac readonly user`, { - user: AUTHENTICATION.KIBANA_RBAC_DASHBOARD_ONLY_USER, - tests: { - default: { - statusCode: 403, - response: expectRbacForbidden, - }, - hiddenType: { - statusCode: 403, - response: expectRbacForbidden, - }, - unknownType: { - statusCode: 403, - response: expectRbacForbidden, - }, - }, - }); - - importTest(`rbac user with all at default space`, { - user: AUTHENTICATION.KIBANA_RBAC_DEFAULT_SPACE_ALL_USER, - tests: { - default: { - statusCode: 403, - response: expectRbacForbidden, - }, - hiddenType: { - statusCode: 403, - response: expectRbacForbidden, - }, - unknownType: { - statusCode: 403, - response: expectRbacForbidden, - }, - }, - }); - - importTest(`rbac user with read at default space`, { - user: AUTHENTICATION.KIBANA_RBAC_DEFAULT_SPACE_READ_USER, - tests: { - default: { - statusCode: 403, - response: expectRbacForbidden, - }, - hiddenType: { - statusCode: 403, - response: expectRbacForbidden, - }, - unknownType: { - statusCode: 403, - response: expectRbacForbidden, - }, - }, - }); - - importTest(`rbac user with all at space_1`, { - user: AUTHENTICATION.KIBANA_RBAC_SPACE_1_ALL_USER, - tests: { - default: { - statusCode: 403, - response: expectRbacForbidden, - }, - hiddenType: { - statusCode: 403, - response: expectRbacForbidden, - }, - unknownType: { - statusCode: 403, - response: expectRbacForbidden, - }, - }, - }); + getTestScenarios().security.forEach(({ users }) => { + const { unauthorized, authorized } = createTests(); + const _addTests = (user: TestUser, tests: ImportTestDefinition[]) => { + addTests(user.description, { user, tests }); + }; - importTest(`rbac user with read at space_1`, { - user: AUTHENTICATION.KIBANA_RBAC_SPACE_1_READ_USER, - tests: { - default: { - statusCode: 403, - response: expectRbacForbidden, - }, - hiddenType: { - statusCode: 403, - response: expectRbacForbidden, - }, - unknownType: { - statusCode: 403, - response: expectRbacForbidden, - }, - }, + [ + users.noAccess, + users.legacyAll, + users.dualRead, + users.readGlobally, + users.allAtDefaultSpace, + users.readAtDefaultSpace, + users.allAtSpace1, + users.readAtSpace1, + ].forEach(user => { + _addTests(user, unauthorized); + }); + [users.dualAll, users.allGlobally, users.superuser].forEach(user => { + _addTests(user, authorized); + }); }); }); } diff --git a/x-pack/test/saved_object_api_integration/security_only/apis/index.ts b/x-pack/test/saved_object_api_integration/security_only/apis/index.ts index bb637a9bc4c90..f581e18ff17af 100644 --- a/x-pack/test/saved_object_api_integration/security_only/apis/index.ts +++ b/x-pack/test/saved_object_api_integration/security_only/apis/index.ts @@ -20,6 +20,7 @@ export default function({ getService, loadTestFile }: FtrProviderContext) { loadTestFile(require.resolve('./bulk_create')); loadTestFile(require.resolve('./bulk_get')); + loadTestFile(require.resolve('./bulk_update')); loadTestFile(require.resolve('./create')); loadTestFile(require.resolve('./delete')); loadTestFile(require.resolve('./export')); @@ -28,6 +29,5 @@ export default function({ getService, loadTestFile }: FtrProviderContext) { loadTestFile(require.resolve('./import')); loadTestFile(require.resolve('./resolve_import_errors')); loadTestFile(require.resolve('./update')); - loadTestFile(require.resolve('./bulk_update')); }); } diff --git a/x-pack/test/saved_object_api_integration/security_only/apis/resolve_import_errors.ts b/x-pack/test/saved_object_api_integration/security_only/apis/resolve_import_errors.ts index 59d50c16c259a..f945d2b64c432 100644 --- a/x-pack/test/saved_object_api_integration/security_only/apis/resolve_import_errors.ts +++ b/x-pack/test/saved_object_api_integration/security_only/apis/resolve_import_errors.ts @@ -4,221 +4,88 @@ * you may not use this file except in compliance with the Elastic License. */ -import { AUTHENTICATION } from '../../common/lib/authentication'; +import { testCaseFailures, getTestScenarios } from '../../common/lib/saved_object_test_utils'; +import { TestUser } from '../../common/lib/types'; import { FtrProviderContext } from '../../common/ftr_provider_context'; -import { resolveImportErrorsTestSuiteFactory } from '../../common/suites/resolve_import_errors'; +import { + resolveImportErrorsTestSuiteFactory, + TEST_CASES as CASES, + ResolveImportErrorsTestDefinition, +} from '../../common/suites/resolve_import_errors'; + +const { fail400, fail409 } = testCaseFailures; + +const createTestCases = (overwrite: boolean) => { + // for each permitted (non-403) outcome, if failure !== undefined then we expect + // to receive an error; otherwise, we expect to receive a success result + const importableTypes = [ + { ...CASES.SINGLE_NAMESPACE_DEFAULT_SPACE, ...fail409(!overwrite) }, + CASES.SINGLE_NAMESPACE_SPACE_1, + CASES.SINGLE_NAMESPACE_SPACE_2, + { ...CASES.NAMESPACE_AGNOSTIC, ...fail409(!overwrite) }, + CASES.NEW_SINGLE_NAMESPACE_OBJ, + CASES.NEW_NAMESPACE_AGNOSTIC_OBJ, + ]; + const nonImportableTypes = [ + { ...CASES.MULTI_NAMESPACE_DEFAULT_AND_SPACE_1, ...fail400() }, + { ...CASES.MULTI_NAMESPACE_ONLY_SPACE_1, ...fail400() }, + { ...CASES.MULTI_NAMESPACE_ONLY_SPACE_2, ...fail400() }, + { ...CASES.HIDDEN, ...fail400() }, + { ...CASES.NEW_MULTI_NAMESPACE_OBJ, ...fail400() }, + ]; + const allTypes = importableTypes.concat(nonImportableTypes); + return { importableTypes, nonImportableTypes, allTypes }; +}; export default function({ getService }: FtrProviderContext) { const supertest = getService('supertestWithoutAuth'); const esArchiver = getService('esArchiver'); const es = getService('legacyEs'); - const { - resolveImportErrorsTest, - createExpectResults, - - expectRbacForbidden, - expectUnknownTypeUnsupported, - expectHiddenTypeUnsupported, - } = resolveImportErrorsTestSuiteFactory(es, esArchiver, supertest); + const { addTests, createTestDefinitions, expectForbidden } = resolveImportErrorsTestSuiteFactory( + es, + esArchiver, + supertest + ); + const createTests = (overwrite: boolean) => { + const { importableTypes, nonImportableTypes, allTypes } = createTestCases(overwrite); + // use singleRequest to reduce execution time and/or test combined cases + return { + unauthorized: [ + createTestDefinitions(importableTypes, true, overwrite), + createTestDefinitions(nonImportableTypes, false, overwrite, { singleRequest: true }), + createTestDefinitions(allTypes, true, overwrite, { + singleRequest: true, + responseBodyOverride: expectForbidden(['dashboard', 'globaltype', 'isolatedtype']), + }), + ].flat(), + authorized: createTestDefinitions(allTypes, false, overwrite, { singleRequest: true }), + }; + }; describe('_resolve_import_errors', () => { - resolveImportErrorsTest(`user with no access`, { - user: AUTHENTICATION.NOT_A_KIBANA_USER, - tests: { - default: { - statusCode: 403, - response: expectRbacForbidden, - }, - hiddenType: { - statusCode: 403, - response: expectRbacForbidden, - }, - unknownType: { - statusCode: 403, - response: expectRbacForbidden, - }, - }, - }); - - resolveImportErrorsTest(`superuser`, { - user: AUTHENTICATION.SUPERUSER, - tests: { - default: { - statusCode: 200, - response: createExpectResults(), - }, - hiddenType: { - statusCode: 200, - response: expectHiddenTypeUnsupported, - }, - unknownType: { - statusCode: 200, - response: expectUnknownTypeUnsupported, - }, - }, - }); - - resolveImportErrorsTest(`legacy user`, { - user: AUTHENTICATION.KIBANA_LEGACY_USER, - tests: { - default: { - statusCode: 403, - response: expectRbacForbidden, - }, - hiddenType: { - statusCode: 403, - response: expectRbacForbidden, - }, - unknownType: { - statusCode: 403, - response: expectRbacForbidden, - }, - }, - }); - - resolveImportErrorsTest(`dual-privileges user`, { - user: AUTHENTICATION.KIBANA_DUAL_PRIVILEGES_USER, - tests: { - default: { - statusCode: 200, - response: createExpectResults(), - }, - hiddenType: { - statusCode: 200, - response: expectHiddenTypeUnsupported, - }, - unknownType: { - statusCode: 200, - response: expectUnknownTypeUnsupported, - }, - }, - }); - - resolveImportErrorsTest(`dual-privileges readonly user`, { - user: AUTHENTICATION.KIBANA_DUAL_PRIVILEGES_DASHBOARD_ONLY_USER, - tests: { - default: { - statusCode: 403, - response: expectRbacForbidden, - }, - hiddenType: { - statusCode: 403, - response: expectRbacForbidden, - }, - unknownType: { - statusCode: 403, - response: expectRbacForbidden, - }, - }, - }); - - resolveImportErrorsTest(`rbac user with all globally`, { - user: AUTHENTICATION.KIBANA_RBAC_USER, - tests: { - default: { - statusCode: 200, - response: createExpectResults(), - }, - hiddenType: { - statusCode: 200, - response: expectHiddenTypeUnsupported, - }, - unknownType: { - statusCode: 200, - response: expectUnknownTypeUnsupported, - }, - }, - }); - - resolveImportErrorsTest(`rbac readonly user`, { - user: AUTHENTICATION.KIBANA_RBAC_DASHBOARD_ONLY_USER, - tests: { - default: { - statusCode: 403, - response: expectRbacForbidden, - }, - hiddenType: { - statusCode: 403, - response: expectRbacForbidden, - }, - unknownType: { - statusCode: 403, - response: expectRbacForbidden, - }, - }, - }); - - resolveImportErrorsTest(`rbac user with all at default space`, { - user: AUTHENTICATION.KIBANA_RBAC_DEFAULT_SPACE_ALL_USER, - tests: { - default: { - statusCode: 403, - response: expectRbacForbidden, - }, - hiddenType: { - statusCode: 403, - response: expectRbacForbidden, - }, - unknownType: { - statusCode: 403, - response: expectRbacForbidden, - }, - }, - }); - - resolveImportErrorsTest(`rbac user with read at default space`, { - user: AUTHENTICATION.KIBANA_RBAC_DEFAULT_SPACE_READ_USER, - tests: { - default: { - statusCode: 403, - response: expectRbacForbidden, - }, - hiddenType: { - statusCode: 403, - response: expectRbacForbidden, - }, - unknownType: { - statusCode: 403, - response: expectRbacForbidden, - }, - }, - }); - - resolveImportErrorsTest(`rbac user with all at space_1`, { - user: AUTHENTICATION.KIBANA_RBAC_SPACE_1_ALL_USER, - tests: { - default: { - statusCode: 403, - response: expectRbacForbidden, - }, - hiddenType: { - statusCode: 403, - response: expectRbacForbidden, - }, - unknownType: { - statusCode: 403, - response: expectRbacForbidden, - }, - }, - }); + getTestScenarios([false, true]).security.forEach(({ users, modifier: overwrite }) => { + const suffix = overwrite ? ' with overwrite enabled' : ''; + const { unauthorized, authorized } = createTests(overwrite!); + const _addTests = (user: TestUser, tests: ResolveImportErrorsTestDefinition[]) => { + addTests(`${user.description}${suffix}`, { user, tests }); + }; - resolveImportErrorsTest(`rbac user with read at space_1`, { - user: AUTHENTICATION.KIBANA_RBAC_SPACE_1_READ_USER, - tests: { - default: { - statusCode: 403, - response: expectRbacForbidden, - }, - hiddenType: { - statusCode: 403, - response: expectRbacForbidden, - }, - unknownType: { - statusCode: 403, - response: expectRbacForbidden, - }, - }, + [ + users.noAccess, + users.legacyAll, + users.dualRead, + users.readGlobally, + users.allAtDefaultSpace, + users.readAtDefaultSpace, + users.allAtSpace1, + users.readAtSpace1, + ].forEach(user => { + _addTests(user, unauthorized); + }); + [users.dualAll, users.allGlobally, users.superuser].forEach(user => { + _addTests(user, authorized); + }); }); }); } diff --git a/x-pack/test/saved_object_api_integration/security_only/apis/update.ts b/x-pack/test/saved_object_api_integration/security_only/apis/update.ts index 8564296bdf558..e1e3a5f8a7dc7 100644 --- a/x-pack/test/saved_object_api_integration/security_only/apis/update.ts +++ b/x-pack/test/saved_object_api_integration/security_only/apis/update.ts @@ -4,267 +4,75 @@ * you may not use this file except in compliance with the Elastic License. */ -import { AUTHENTICATION } from '../../common/lib/authentication'; +import { testCaseFailures, getTestScenarios } from '../../common/lib/saved_object_test_utils'; +import { TestUser } from '../../common/lib/types'; import { FtrProviderContext } from '../../common/ftr_provider_context'; -import { updateTestSuiteFactory } from '../../common/suites/update'; +import { + updateTestSuiteFactory, + TEST_CASES as CASES, + UpdateTestDefinition, +} from '../../common/suites/update'; + +const { fail404 } = testCaseFailures; + +const createTestCases = () => { + // for each permitted (non-403) outcome, if failure !== undefined then we expect + // to receive an error; otherwise, we expect to receive a success result + const normalTypes = [ + CASES.SINGLE_NAMESPACE_DEFAULT_SPACE, + { ...CASES.SINGLE_NAMESPACE_SPACE_1, ...fail404() }, + { ...CASES.SINGLE_NAMESPACE_SPACE_2, ...fail404() }, + CASES.MULTI_NAMESPACE_DEFAULT_AND_SPACE_1, + { ...CASES.MULTI_NAMESPACE_ONLY_SPACE_1, ...fail404() }, + { ...CASES.MULTI_NAMESPACE_ONLY_SPACE_2, ...fail404() }, + CASES.NAMESPACE_AGNOSTIC, + { ...CASES.DOES_NOT_EXIST, ...fail404() }, + ]; + const hiddenType = [{ ...CASES.HIDDEN, ...fail404() }]; + const allTypes = normalTypes.concat(hiddenType); + return { normalTypes, hiddenType, allTypes }; +}; export default function({ getService }: FtrProviderContext) { const supertest = getService('supertestWithoutAuth'); const esArchiver = getService('esArchiver'); - describe('update', () => { - const { - createExpectDoesntExistNotFound, - expectDoesntExistRbacForbidden, - expectNotSpaceAwareResults, - expectNotSpaceAwareRbacForbidden, - expectSpaceAwareRbacForbidden, - expectSpaceAwareResults, - expectSpaceNotFound, - expectHiddenTypeRbacForbidden, - updateTest, - } = updateTestSuiteFactory(esArchiver, supertest); - - updateTest(`user with no access`, { - user: AUTHENTICATION.NOT_A_KIBANA_USER, - tests: { - spaceAware: { - statusCode: 403, - response: expectSpaceAwareRbacForbidden, - }, - notSpaceAware: { - statusCode: 403, - response: expectNotSpaceAwareRbacForbidden, - }, - hiddenType: { - statusCode: 403, - response: expectHiddenTypeRbacForbidden, - }, - doesntExist: { - statusCode: 403, - response: expectDoesntExistRbacForbidden, - }, - }, - }); - - updateTest(`superuser`, { - user: AUTHENTICATION.SUPERUSER, - tests: { - spaceAware: { - statusCode: 200, - response: expectSpaceAwareResults, - }, - notSpaceAware: { - statusCode: 200, - response: expectNotSpaceAwareResults, - }, - hiddenType: { - statusCode: 404, - response: expectSpaceNotFound, - }, - doesntExist: { - statusCode: 404, - response: createExpectDoesntExistNotFound(), - }, - }, - }); + const { addTests, createTestDefinitions } = updateTestSuiteFactory(esArchiver, supertest); + const createTests = () => { + const { normalTypes, hiddenType, allTypes } = createTestCases(); + return { + unauthorized: createTestDefinitions(allTypes, true), + authorized: [ + createTestDefinitions(normalTypes, false), + createTestDefinitions(hiddenType, true), + ].flat(), + superuser: createTestDefinitions(allTypes, false), + }; + }; - updateTest(`legacy user`, { - user: AUTHENTICATION.KIBANA_LEGACY_USER, - tests: { - spaceAware: { - statusCode: 403, - response: expectSpaceAwareRbacForbidden, - }, - notSpaceAware: { - statusCode: 403, - response: expectNotSpaceAwareRbacForbidden, - }, - hiddenType: { - statusCode: 403, - response: expectHiddenTypeRbacForbidden, - }, - doesntExist: { - statusCode: 403, - response: expectDoesntExistRbacForbidden, - }, - }, - }); - - updateTest(`dual-privileges user`, { - user: AUTHENTICATION.KIBANA_DUAL_PRIVILEGES_USER, - tests: { - spaceAware: { - statusCode: 200, - response: expectSpaceAwareResults, - }, - notSpaceAware: { - statusCode: 200, - response: expectNotSpaceAwareResults, - }, - hiddenType: { - statusCode: 403, - response: expectHiddenTypeRbacForbidden, - }, - doesntExist: { - statusCode: 404, - response: createExpectDoesntExistNotFound(), - }, - }, - }); - - updateTest(`dual-privileges readonly user`, { - user: AUTHENTICATION.KIBANA_DUAL_PRIVILEGES_DASHBOARD_ONLY_USER, - tests: { - spaceAware: { - statusCode: 403, - response: expectSpaceAwareRbacForbidden, - }, - notSpaceAware: { - statusCode: 403, - response: expectNotSpaceAwareRbacForbidden, - }, - hiddenType: { - statusCode: 403, - response: expectHiddenTypeRbacForbidden, - }, - doesntExist: { - statusCode: 403, - response: expectDoesntExistRbacForbidden, - }, - }, - }); - - updateTest(`rbac user with all globally`, { - user: AUTHENTICATION.KIBANA_RBAC_USER, - tests: { - spaceAware: { - statusCode: 200, - response: expectSpaceAwareResults, - }, - notSpaceAware: { - statusCode: 200, - response: expectNotSpaceAwareResults, - }, - hiddenType: { - statusCode: 403, - response: expectHiddenTypeRbacForbidden, - }, - doesntExist: { - statusCode: 404, - response: createExpectDoesntExistNotFound(), - }, - }, - }); - - updateTest(`rbac user with read globally`, { - user: AUTHENTICATION.KIBANA_RBAC_DASHBOARD_ONLY_USER, - tests: { - spaceAware: { - statusCode: 403, - response: expectSpaceAwareRbacForbidden, - }, - notSpaceAware: { - statusCode: 403, - response: expectNotSpaceAwareRbacForbidden, - }, - hiddenType: { - statusCode: 403, - response: expectHiddenTypeRbacForbidden, - }, - doesntExist: { - statusCode: 403, - response: expectDoesntExistRbacForbidden, - }, - }, - }); - - updateTest(`rbac user with all at default space`, { - user: AUTHENTICATION.KIBANA_RBAC_DEFAULT_SPACE_ALL_USER, - tests: { - spaceAware: { - statusCode: 403, - response: expectSpaceAwareRbacForbidden, - }, - notSpaceAware: { - statusCode: 403, - response: expectNotSpaceAwareRbacForbidden, - }, - hiddenType: { - statusCode: 403, - response: expectHiddenTypeRbacForbidden, - }, - doesntExist: { - statusCode: 403, - response: expectDoesntExistRbacForbidden, - }, - }, - }); - - updateTest(`rbac user with read at default space`, { - user: AUTHENTICATION.KIBANA_RBAC_DEFAULT_SPACE_READ_USER, - tests: { - spaceAware: { - statusCode: 403, - response: expectSpaceAwareRbacForbidden, - }, - notSpaceAware: { - statusCode: 403, - response: expectNotSpaceAwareRbacForbidden, - }, - hiddenType: { - statusCode: 403, - response: expectHiddenTypeRbacForbidden, - }, - doesntExist: { - statusCode: 403, - response: expectDoesntExistRbacForbidden, - }, - }, - }); - - updateTest(`rbac user with all at space_1`, { - user: AUTHENTICATION.KIBANA_RBAC_SPACE_1_ALL_USER, - tests: { - spaceAware: { - statusCode: 403, - response: expectSpaceAwareRbacForbidden, - }, - notSpaceAware: { - statusCode: 403, - response: expectNotSpaceAwareRbacForbidden, - }, - hiddenType: { - statusCode: 403, - response: expectHiddenTypeRbacForbidden, - }, - doesntExist: { - statusCode: 403, - response: expectDoesntExistRbacForbidden, - }, - }, - }); + describe('_update', () => { + getTestScenarios().security.forEach(({ users }) => { + const { unauthorized, authorized, superuser } = createTests(); + const _addTests = (user: TestUser, tests: UpdateTestDefinition[]) => { + addTests(user.description, { user, tests }); + }; - updateTest(`rbac user with read at space_1`, { - user: AUTHENTICATION.KIBANA_RBAC_SPACE_1_READ_USER, - tests: { - spaceAware: { - statusCode: 403, - response: expectSpaceAwareRbacForbidden, - }, - notSpaceAware: { - statusCode: 403, - response: expectNotSpaceAwareRbacForbidden, - }, - hiddenType: { - statusCode: 403, - response: expectHiddenTypeRbacForbidden, - }, - doesntExist: { - statusCode: 403, - response: expectDoesntExistRbacForbidden, - }, - }, + [ + users.noAccess, + users.legacyAll, + users.dualRead, + users.readGlobally, + users.allAtDefaultSpace, + users.readAtDefaultSpace, + users.allAtSpace1, + users.readAtSpace1, + ].forEach(user => { + _addTests(user, unauthorized); + }); + [users.dualAll, users.allGlobally].forEach(user => { + _addTests(user, authorized); + }); + _addTests(users.superuser, superuser); }); }); } diff --git a/x-pack/test/saved_object_api_integration/spaces_only/apis/bulk_create.ts b/x-pack/test/saved_object_api_integration/spaces_only/apis/bulk_create.ts index 690e358b744d5..70d74822a8b0f 100644 --- a/x-pack/test/saved_object_api_integration/spaces_only/apis/bulk_create.ts +++ b/x-pack/test/saved_object_api_integration/spaces_only/apis/bulk_create.ts @@ -6,83 +6,72 @@ import expect from '@kbn/expect'; import { SPACES } from '../../common/lib/spaces'; +import { testCaseFailures, getTestScenarios } from '../../common/lib/saved_object_test_utils'; import { FtrProviderContext } from '../../common/ftr_provider_context'; -import { bulkCreateTestSuiteFactory } from '../../common/suites/bulk_create'; +import { bulkCreateTestSuiteFactory, TEST_CASES as CASES } from '../../common/suites/bulk_create'; -const expectNamespaceSpecifiedBadRequest = (resp: { [key: string]: any }) => { - expect(resp.body).to.eql({ - error: 'Bad Request', - message: '[request body.0.namespace]: definition for this key is missing', - statusCode: 400, - }); -}; +const { + DEFAULT: { spaceId: DEFAULT_SPACE_ID }, + SPACE_1: { spaceId: SPACE_1_ID }, + SPACE_2: { spaceId: SPACE_2_ID }, +} = SPACES; +const { fail400, fail409 } = testCaseFailures; + +const createTestCases = (overwrite: boolean, spaceId: string) => [ + // for each outcome, if failure !== undefined then we expect to receive + // an error; otherwise, we expect to receive a success result + { + ...CASES.SINGLE_NAMESPACE_DEFAULT_SPACE, + ...fail409(!overwrite && spaceId === DEFAULT_SPACE_ID), + }, + { ...CASES.SINGLE_NAMESPACE_SPACE_1, ...fail409(!overwrite && spaceId === SPACE_1_ID) }, + { ...CASES.SINGLE_NAMESPACE_SPACE_2, ...fail409(!overwrite && spaceId === SPACE_2_ID) }, + { + ...CASES.MULTI_NAMESPACE_DEFAULT_AND_SPACE_1, + ...fail409(!overwrite || (spaceId !== DEFAULT_SPACE_ID && spaceId !== SPACE_1_ID)), + }, + { ...CASES.MULTI_NAMESPACE_ONLY_SPACE_1, ...fail409(!overwrite || spaceId !== SPACE_1_ID) }, + { ...CASES.MULTI_NAMESPACE_ONLY_SPACE_2, ...fail409(!overwrite || spaceId !== SPACE_2_ID) }, + { ...CASES.NAMESPACE_AGNOSTIC, ...fail409(!overwrite) }, + { ...CASES.HIDDEN, ...fail400() }, + CASES.NEW_SINGLE_NAMESPACE_OBJ, + CASES.NEW_MULTI_NAMESPACE_OBJ, + CASES.NEW_NAMESPACE_AGNOSTIC_OBJ, +]; export default function({ getService }: FtrProviderContext) { const supertest = getService('supertest'); const esArchiver = getService('esArchiver'); const es = getService('legacyEs'); - const { - bulkCreateTest, - createExpectResults, - expectBadRequestForHiddenType, - } = bulkCreateTestSuiteFactory(es, esArchiver, supertest); - - describe('_bulk_create', () => { - bulkCreateTest('in the current space (space_1)', { - ...SPACES.SPACE_1, - tests: { - default: { - statusCode: 200, - response: createExpectResults(SPACES.SPACE_1.spaceId), - }, - includingSpace: { - statusCode: 200, - response: expectBadRequestForHiddenType, - }, - custom: { - description: 'when a namespace is specified on the saved object', - requestBody: [ - { - type: 'visualization', - namespace: 'space_1', - attributes: { - title: 'something', - }, - }, - ], - statusCode: 400, - response: expectNamespaceSpecifiedBadRequest, + const { addTests, createTestDefinitions } = bulkCreateTestSuiteFactory(es, esArchiver, supertest); + const createTests = (overwrite: boolean, spaceId: string) => { + const testCases = createTestCases(overwrite, spaceId); + return createTestDefinitions(testCases, false, overwrite, { + spaceId, + singleRequest: true, + }).concat( + ['namespace', 'namespaces'].map(key => ({ + title: `(bad request) when ${key} is specified on the saved object`, + request: [{ type: 'isolatedtype', id: 'some-id', [key]: 'any-value' }] as any, + responseStatusCode: 400, + responseBody: async (response: Record) => { + expect(response.body).to.eql({ + statusCode: 400, + error: 'Bad Request', + message: `[request body.0.${key}]: definition for this key is missing`, + }); }, - }, - }); + overwrite, + })) + ); + }; - bulkCreateTest('in the default space', { - ...SPACES.DEFAULT, - tests: { - default: { - statusCode: 200, - response: createExpectResults(SPACES.DEFAULT.spaceId), - }, - includingSpace: { - statusCode: 200, - response: expectBadRequestForHiddenType, - }, - custom: { - description: 'when a namespace is specified on the saved object', - requestBody: [ - { - type: 'visualization', - namespace: 'space_1', - attributes: { - title: 'something', - }, - }, - ], - statusCode: 400, - response: expectNamespaceSpecifiedBadRequest, - }, - }, + describe('_bulk_create', () => { + getTestScenarios([false, true]).spaces.forEach(({ spaceId, modifier: overwrite }) => { + const suffix = overwrite ? ' with overwrite enabled' : ''; + const tests = createTests(overwrite!, spaceId); + addTests(`within the ${spaceId} space${suffix}`, { spaceId, tests }); }); }); } diff --git a/x-pack/test/saved_object_api_integration/spaces_only/apis/bulk_get.ts b/x-pack/test/saved_object_api_integration/spaces_only/apis/bulk_get.ts index bed29f8eb39a1..ad10719750585 100644 --- a/x-pack/test/saved_object_api_integration/spaces_only/apis/bulk_get.ts +++ b/x-pack/test/saved_object_api_integration/spaces_only/apis/bulk_get.ts @@ -5,48 +5,48 @@ */ import { SPACES } from '../../common/lib/spaces'; +import { testCaseFailures, getTestScenarios } from '../../common/lib/saved_object_test_utils'; import { FtrProviderContext } from '../../common/ftr_provider_context'; -import { bulkGetTestSuiteFactory } from '../../common/suites/bulk_get'; +import { bulkGetTestSuiteFactory, TEST_CASES as CASES } from '../../common/suites/bulk_get'; + +const { + DEFAULT: { spaceId: DEFAULT_SPACE_ID }, + SPACE_1: { spaceId: SPACE_1_ID }, + SPACE_2: { spaceId: SPACE_2_ID }, +} = SPACES; +const { fail400, fail404 } = testCaseFailures; + +const createTestCases = (spaceId: string) => [ + // for each outcome, if failure !== undefined then we expect to receive + // an error; otherwise, we expect to receive a success result + { ...CASES.SINGLE_NAMESPACE_DEFAULT_SPACE, ...fail404(spaceId !== DEFAULT_SPACE_ID) }, + { ...CASES.SINGLE_NAMESPACE_SPACE_1, ...fail404(spaceId !== SPACE_1_ID) }, + { ...CASES.SINGLE_NAMESPACE_SPACE_2, ...fail404(spaceId !== SPACE_2_ID) }, + { + ...CASES.MULTI_NAMESPACE_DEFAULT_AND_SPACE_1, + ...fail404(spaceId !== DEFAULT_SPACE_ID && spaceId !== SPACE_1_ID), + }, + { ...CASES.MULTI_NAMESPACE_ONLY_SPACE_1, ...fail404(spaceId !== SPACE_1_ID) }, + { ...CASES.MULTI_NAMESPACE_ONLY_SPACE_2, ...fail404(spaceId !== SPACE_2_ID) }, + CASES.NAMESPACE_AGNOSTIC, + { ...CASES.HIDDEN, ...fail400() }, + { ...CASES.DOES_NOT_EXIST, ...fail404() }, +]; export default function({ getService }: FtrProviderContext) { const supertest = getService('supertest'); const esArchiver = getService('esArchiver'); - const { - bulkGetTest, - createExpectResults, - createExpectNotFoundResults, - expectBadRequestForHiddenType, - } = bulkGetTestSuiteFactory(esArchiver, supertest); + const { addTests, createTestDefinitions } = bulkGetTestSuiteFactory(esArchiver, supertest); + const createTests = (spaceId: string) => { + const testCases = createTestCases(spaceId); + return createTestDefinitions(testCases, false, { singleRequest: true }); + }; describe('_bulk_get', () => { - bulkGetTest(`objects within the current space (space_1)`, { - ...SPACES.SPACE_1, - tests: { - default: { - statusCode: 200, - response: createExpectResults(SPACES.SPACE_1.spaceId), - }, - includingHiddenType: { - statusCode: 200, - response: expectBadRequestForHiddenType, - }, - }, - }); - - bulkGetTest(`objects within another space`, { - ...SPACES.SPACE_1, - otherSpaceId: SPACES.SPACE_2.spaceId, - tests: { - default: { - statusCode: 200, - response: createExpectNotFoundResults(SPACES.SPACE_2.spaceId), - }, - includingHiddenType: { - statusCode: 200, - response: expectBadRequestForHiddenType, - }, - }, + getTestScenarios().spaces.forEach(({ spaceId }) => { + const tests = createTests(spaceId); + addTests(`within the ${spaceId} space`, { spaceId, tests }); }); }); } diff --git a/x-pack/test/saved_object_api_integration/spaces_only/apis/bulk_update.ts b/x-pack/test/saved_object_api_integration/spaces_only/apis/bulk_update.ts index 0681908a765ce..be6ce9e30c56e 100644 --- a/x-pack/test/saved_object_api_integration/spaces_only/apis/bulk_update.ts +++ b/x-pack/test/saved_object_api_integration/spaces_only/apis/bulk_update.ts @@ -5,88 +5,48 @@ */ import { SPACES } from '../../common/lib/spaces'; +import { testCaseFailures, getTestScenarios } from '../../common/lib/saved_object_test_utils'; import { FtrProviderContext } from '../../common/ftr_provider_context'; -import { bulkUpdateTestSuiteFactory } from '../../common/suites/bulk_update'; +import { bulkUpdateTestSuiteFactory, TEST_CASES as CASES } from '../../common/suites/bulk_update'; + +const { + DEFAULT: { spaceId: DEFAULT_SPACE_ID }, + SPACE_1: { spaceId: SPACE_1_ID }, + SPACE_2: { spaceId: SPACE_2_ID }, +} = SPACES; +const { fail404 } = testCaseFailures; + +const createTestCases = (spaceId: string) => [ + // for each outcome, if failure !== undefined then we expect to receive + // an error; otherwise, we expect to receive a success result + { ...CASES.SINGLE_NAMESPACE_DEFAULT_SPACE, ...fail404(spaceId !== DEFAULT_SPACE_ID) }, + { ...CASES.SINGLE_NAMESPACE_SPACE_1, ...fail404(spaceId !== SPACE_1_ID) }, + { ...CASES.SINGLE_NAMESPACE_SPACE_2, ...fail404(spaceId !== SPACE_2_ID) }, + { + ...CASES.MULTI_NAMESPACE_DEFAULT_AND_SPACE_1, + ...fail404(spaceId !== DEFAULT_SPACE_ID && spaceId !== SPACE_1_ID), + }, + { ...CASES.MULTI_NAMESPACE_ONLY_SPACE_1, ...fail404(spaceId !== SPACE_1_ID) }, + { ...CASES.MULTI_NAMESPACE_ONLY_SPACE_2, ...fail404(spaceId !== SPACE_2_ID) }, + CASES.NAMESPACE_AGNOSTIC, + { ...CASES.HIDDEN, ...fail404() }, + { ...CASES.DOES_NOT_EXIST, ...fail404() }, +]; export default function({ getService }: FtrProviderContext) { const supertest = getService('supertest'); const esArchiver = getService('esArchiver'); - describe('bulkUpdate', () => { - const { - createExpectSpaceAwareNotFound, - expectSpaceAwareResults, - createExpectDoesntExistNotFound, - expectNotSpaceAwareResults, - expectSpaceNotFound, - bulkUpdateTest, - } = bulkUpdateTestSuiteFactory(esArchiver, supertest); - - bulkUpdateTest(`in the default space`, { - spaceId: SPACES.DEFAULT.spaceId, - tests: { - spaceAware: { - statusCode: 200, - response: expectSpaceAwareResults, - }, - notSpaceAware: { - statusCode: 200, - response: expectNotSpaceAwareResults, - }, - hiddenType: { - statusCode: 200, - response: expectSpaceNotFound, - }, - doesntExist: { - statusCode: 200, - response: createExpectDoesntExistNotFound(SPACES.DEFAULT.spaceId), - }, - }, - }); - - bulkUpdateTest('in the current space (space_1)', { - spaceId: SPACES.SPACE_1.spaceId, - tests: { - spaceAware: { - statusCode: 200, - response: expectSpaceAwareResults, - }, - notSpaceAware: { - statusCode: 200, - response: expectNotSpaceAwareResults, - }, - hiddenType: { - statusCode: 200, - response: expectSpaceNotFound, - }, - doesntExist: { - statusCode: 200, - response: createExpectDoesntExistNotFound(SPACES.SPACE_1.spaceId), - }, - }, - }); + const { addTests, createTestDefinitions } = bulkUpdateTestSuiteFactory(esArchiver, supertest); + const createTests = (spaceId: string) => { + const testCases = createTestCases(spaceId); + return createTestDefinitions(testCases, false, { singleRequest: true }); + }; - bulkUpdateTest('objects that exist in another space (space_1)', { - spaceId: SPACES.DEFAULT.spaceId, - otherSpaceId: SPACES.SPACE_1.spaceId, - tests: { - spaceAware: { - statusCode: 200, - response: createExpectSpaceAwareNotFound(SPACES.SPACE_1.spaceId), - }, - notSpaceAware: { - statusCode: 200, - response: expectNotSpaceAwareResults, - }, - hiddenType: { - statusCode: 200, - response: expectSpaceNotFound, - }, - doesntExist: { - statusCode: 200, - response: createExpectDoesntExistNotFound(), - }, - }, + describe('_bulk_update', () => { + getTestScenarios().spaces.forEach(({ spaceId }) => { + const tests = createTests(spaceId); + addTests(`within the ${spaceId} space`, { spaceId, tests }); }); }); } diff --git a/x-pack/test/saved_object_api_integration/spaces_only/apis/create.ts b/x-pack/test/saved_object_api_integration/spaces_only/apis/create.ts index 3bd4019649363..d0c6a21e73971 100644 --- a/x-pack/test/saved_object_api_integration/spaces_only/apis/create.ts +++ b/x-pack/test/saved_object_api_integration/spaces_only/apis/create.ts @@ -4,90 +4,56 @@ * you may not use this file except in compliance with the Elastic License. */ -import expect from '@kbn/expect'; import { SPACES } from '../../common/lib/spaces'; +import { testCaseFailures, getTestScenarios } from '../../common/lib/saved_object_test_utils'; import { FtrProviderContext } from '../../common/ftr_provider_context'; -import { createTestSuiteFactory } from '../../common/suites/create'; +import { createTestSuiteFactory, TEST_CASES as CASES } from '../../common/suites/create'; -const expectNamespaceSpecifiedBadRequest = (resp: { [key: string]: any }) => { - expect(resp.body).to.eql({ - error: 'Bad Request', - message: '[request body.namespace]: definition for this key is missing', - statusCode: 400, - }); -}; +const { + DEFAULT: { spaceId: DEFAULT_SPACE_ID }, + SPACE_1: { spaceId: SPACE_1_ID }, + SPACE_2: { spaceId: SPACE_2_ID }, +} = SPACES; +const { fail400, fail409 } = testCaseFailures; + +const createTestCases = (overwrite: boolean, spaceId: string) => [ + // for each outcome, if failure !== undefined then we expect to receive + // an error; otherwise, we expect to receive a success result + { + ...CASES.SINGLE_NAMESPACE_DEFAULT_SPACE, + ...fail409(!overwrite && spaceId === DEFAULT_SPACE_ID), + }, + { ...CASES.SINGLE_NAMESPACE_SPACE_1, ...fail409(!overwrite && spaceId === SPACE_1_ID) }, + { ...CASES.SINGLE_NAMESPACE_SPACE_2, ...fail409(!overwrite && spaceId === SPACE_2_ID) }, + { + ...CASES.MULTI_NAMESPACE_DEFAULT_AND_SPACE_1, + ...fail409(!overwrite || (spaceId !== DEFAULT_SPACE_ID && spaceId !== SPACE_1_ID)), + }, + { ...CASES.MULTI_NAMESPACE_ONLY_SPACE_1, ...fail409(!overwrite || spaceId !== SPACE_1_ID) }, + { ...CASES.MULTI_NAMESPACE_ONLY_SPACE_2, ...fail409(!overwrite || spaceId !== SPACE_2_ID) }, + { ...CASES.NAMESPACE_AGNOSTIC, ...fail409(!overwrite) }, + { ...CASES.HIDDEN, ...fail400() }, + CASES.NEW_SINGLE_NAMESPACE_OBJ, + CASES.NEW_MULTI_NAMESPACE_OBJ, + CASES.NEW_NAMESPACE_AGNOSTIC_OBJ, +]; export default function({ getService }: FtrProviderContext) { - const supertestWithoutAuth = getService('supertestWithoutAuth'); - const es = getService('legacyEs'); + const supertest = getService('supertestWithoutAuth'); const esArchiver = getService('esArchiver'); + const es = getService('legacyEs'); - const { - createTest, - createExpectSpaceAwareResults, - expectNotSpaceAwareResults, - expectBadRequestForHiddenType, - } = createTestSuiteFactory(es, esArchiver, supertestWithoutAuth); - - describe('create', () => { - createTest('in the current space (space_1)', { - ...SPACES.SPACE_1, - tests: { - spaceAware: { - statusCode: 200, - response: createExpectSpaceAwareResults(SPACES.SPACE_1.spaceId), - }, - notSpaceAware: { - statusCode: 200, - response: expectNotSpaceAwareResults, - }, - hiddenType: { - statusCode: 400, - response: expectBadRequestForHiddenType, - }, - custom: { - description: 'when a namespace is specified on the saved object', - type: 'visualization', - requestBody: { - namespace: 'space_1', - attributes: { - title: 'something', - }, - }, - statusCode: 400, - response: expectNamespaceSpecifiedBadRequest, - }, - }, - }); + const { addTests, createTestDefinitions } = createTestSuiteFactory(es, esArchiver, supertest); + const createTests = (overwrite: boolean, spaceId: string) => { + const testCases = createTestCases(overwrite, spaceId); + return createTestDefinitions(testCases, false, overwrite, { spaceId }); + }; - createTest('in the default space', { - ...SPACES.DEFAULT, - tests: { - spaceAware: { - statusCode: 200, - response: createExpectSpaceAwareResults(SPACES.DEFAULT.spaceId), - }, - notSpaceAware: { - statusCode: 200, - response: expectNotSpaceAwareResults, - }, - hiddenType: { - statusCode: 400, - response: expectBadRequestForHiddenType, - }, - custom: { - description: 'when a namespace is specified on the saved object', - type: 'visualization', - requestBody: { - namespace: 'space_1', - attributes: { - title: 'something', - }, - }, - statusCode: 400, - response: expectNamespaceSpecifiedBadRequest, - }, - }, + describe('_create', () => { + getTestScenarios([false, true]).spaces.forEach(({ spaceId, modifier: overwrite }) => { + const suffix = overwrite ? ' with overwrite enabled' : ''; + const tests = createTests(overwrite!, spaceId); + addTests(`within the ${spaceId} space${suffix}`, { spaceId, tests }); }); }); } diff --git a/x-pack/test/saved_object_api_integration/spaces_only/apis/delete.ts b/x-pack/test/saved_object_api_integration/spaces_only/apis/delete.ts index 437b3f95024c6..bb48e06d93a09 100644 --- a/x-pack/test/saved_object_api_integration/spaces_only/apis/delete.ts +++ b/x-pack/test/saved_object_api_integration/spaces_only/apis/delete.ts @@ -5,87 +5,48 @@ */ import { SPACES } from '../../common/lib/spaces'; +import { testCaseFailures, getTestScenarios } from '../../common/lib/saved_object_test_utils'; import { FtrProviderContext } from '../../common/ftr_provider_context'; -import { deleteTestSuiteFactory } from '../../common/suites/delete'; +import { deleteTestSuiteFactory, TEST_CASES as CASES } from '../../common/suites/delete'; + +const { + DEFAULT: { spaceId: DEFAULT_SPACE_ID }, + SPACE_1: { spaceId: SPACE_1_ID }, + SPACE_2: { spaceId: SPACE_2_ID }, +} = SPACES; +const { fail404 } = testCaseFailures; + +const createTestCases = (spaceId: string) => [ + // for each outcome, if failure !== undefined then we expect to receive + // an error; otherwise, we expect to receive a success result + { ...CASES.SINGLE_NAMESPACE_DEFAULT_SPACE, ...fail404(spaceId !== DEFAULT_SPACE_ID) }, + { ...CASES.SINGLE_NAMESPACE_SPACE_1, ...fail404(spaceId !== SPACE_1_ID) }, + { ...CASES.SINGLE_NAMESPACE_SPACE_2, ...fail404(spaceId !== SPACE_2_ID) }, + { + ...CASES.MULTI_NAMESPACE_DEFAULT_AND_SPACE_1, + ...fail404(spaceId !== DEFAULT_SPACE_ID && spaceId !== SPACE_1_ID), + }, + { ...CASES.MULTI_NAMESPACE_ONLY_SPACE_1, ...fail404(spaceId !== SPACE_1_ID) }, + { ...CASES.MULTI_NAMESPACE_ONLY_SPACE_2, ...fail404(spaceId !== SPACE_2_ID) }, + CASES.NAMESPACE_AGNOSTIC, + { ...CASES.HIDDEN, ...fail404() }, + { ...CASES.DOES_NOT_EXIST, ...fail404() }, +]; export default function({ getService }: FtrProviderContext) { const supertest = getService('supertest'); const esArchiver = getService('esArchiver'); - describe('delete', () => { - const { - createExpectSpaceAwareNotFound, - createExpectUnknownDocNotFound, - deleteTest, - expectEmpty, - expectGenericNotFound, - } = deleteTestSuiteFactory(esArchiver, supertest); - - deleteTest(`in the default space`, { - ...SPACES.DEFAULT, - tests: { - spaceAware: { - statusCode: 200, - response: expectEmpty, - }, - notSpaceAware: { - statusCode: 200, - response: expectEmpty, - }, - hiddenType: { - statusCode: 404, - response: expectGenericNotFound, - }, - invalidId: { - statusCode: 404, - response: createExpectUnknownDocNotFound(SPACES.DEFAULT.spaceId), - }, - }, - }); - - deleteTest(`in the current space (space_1)`, { - ...SPACES.SPACE_1, - tests: { - spaceAware: { - statusCode: 200, - response: expectEmpty, - }, - notSpaceAware: { - statusCode: 200, - response: expectEmpty, - }, - hiddenType: { - statusCode: 404, - response: expectGenericNotFound, - }, - invalidId: { - statusCode: 404, - response: createExpectUnknownDocNotFound(SPACES.SPACE_1.spaceId), - }, - }, - }); + const { addTests, createTestDefinitions } = deleteTestSuiteFactory(esArchiver, supertest); + const createTests = (spaceId: string) => { + const testCases = createTestCases(spaceId); + return createTestDefinitions(testCases, false, { spaceId }); + }; - deleteTest(`in another space (space_2)`, { - spaceId: SPACES.SPACE_1.spaceId, - otherSpaceId: SPACES.SPACE_2.spaceId, - tests: { - spaceAware: { - statusCode: 404, - response: createExpectSpaceAwareNotFound(SPACES.SPACE_2.spaceId), - }, - notSpaceAware: { - statusCode: 200, - response: expectEmpty, - }, - hiddenType: { - statusCode: 404, - response: expectGenericNotFound, - }, - invalidId: { - statusCode: 404, - response: createExpectUnknownDocNotFound(SPACES.SPACE_2.spaceId), - }, - }, + describe('_delete', () => { + getTestScenarios().spaces.forEach(({ spaceId }) => { + const tests = createTests(spaceId); + addTests(`within the ${spaceId} space`, { spaceId, tests }); }); }); } diff --git a/x-pack/test/saved_object_api_integration/spaces_only/apis/export.ts b/x-pack/test/saved_object_api_integration/spaces_only/apis/export.ts index f4be02c05ac88..25d4fbfae990b 100644 --- a/x-pack/test/saved_object_api_integration/spaces_only/apis/export.ts +++ b/x-pack/test/saved_object_api_integration/spaces_only/apis/export.ts @@ -4,62 +4,29 @@ * you may not use this file except in compliance with the Elastic License. */ -import { SPACES } from '../../common/lib/spaces'; +import { getTestScenarios } from '../../common/lib/saved_object_test_utils'; import { FtrProviderContext } from '../../common/ftr_provider_context'; -import { exportTestSuiteFactory } from '../../common/suites/export'; +import { exportTestSuiteFactory, getTestCases } from '../../common/suites/export'; + +const createTestCases = (spaceId: string) => { + const cases = getTestCases(spaceId); + return Object.values(cases); +}; export default function({ getService }: FtrProviderContext) { const supertest = getService('supertest'); const esArchiver = getService('esArchiver'); - const { - expectTypeOrObjectsRequired, - createExpectVisualizationResults, - expectInvalidTypeSpecified, - exportTest, - } = exportTestSuiteFactory(esArchiver, supertest); - - describe('export', () => { - exportTest('objects only within the current space (space_1)', { - ...SPACES.SPACE_1, - tests: { - spaceAwareType: { - description: 'only the visualization', - statusCode: 200, - response: createExpectVisualizationResults(SPACES.SPACE_1.spaceId), - }, - hiddenType: { - description: 'exporting space not allowed', - statusCode: 400, - response: expectInvalidTypeSpecified, - }, - noTypeOrObjects: { - description: 'bad request, type or object is required', - statusCode: 400, - response: expectTypeOrObjectsRequired, - }, - }, - }); + const { addTests, createTestDefinitions } = exportTestSuiteFactory(esArchiver, supertest); + const createTests = (spaceId: string) => { + const testCases = createTestCases(spaceId); + return createTestDefinitions(testCases, false); + }; - exportTest('objects only within the current space (default)', { - ...SPACES.DEFAULT, - tests: { - spaceAwareType: { - description: 'only the visualization', - statusCode: 200, - response: createExpectVisualizationResults(SPACES.DEFAULT.spaceId), - }, - hiddenType: { - description: 'exporting space not allowed', - statusCode: 400, - response: expectInvalidTypeSpecified, - }, - noTypeOrObjects: { - description: 'bad request, type or object is required', - statusCode: 400, - response: expectTypeOrObjectsRequired, - }, - }, + describe('_export', () => { + getTestScenarios().spaces.forEach(({ spaceId }) => { + const tests = createTests(spaceId); + addTests(`within the ${spaceId} space`, { spaceId, tests }); }); }); } diff --git a/x-pack/test/saved_object_api_integration/spaces_only/apis/find.ts b/x-pack/test/saved_object_api_integration/spaces_only/apis/find.ts index a07d3edf834e9..a15f7de404db8 100644 --- a/x-pack/test/saved_object_api_integration/spaces_only/apis/find.ts +++ b/x-pack/test/saved_object_api_integration/spaces_only/apis/find.ts @@ -4,154 +4,29 @@ * you may not use this file except in compliance with the Elastic License. */ -import { SPACES } from '../../common/lib/spaces'; +import { getTestScenarios } from '../../common/lib/saved_object_test_utils'; import { FtrProviderContext } from '../../common/ftr_provider_context'; -import { findTestSuiteFactory } from '../../common/suites/find'; +import { findTestSuiteFactory, getTestCases } from '../../common/suites/find'; + +const createTestCases = (spaceId: string) => { + const cases = getTestCases(spaceId); + return Object.values(cases); +}; export default function({ getService }: FtrProviderContext) { const supertest = getService('supertest'); const esArchiver = getService('esArchiver'); - const { - createExpectEmpty, - createExpectVisualizationResults, - expectFilterWrongTypeError, - expectNotSpaceAwareResults, - expectTypeRequired, - findTest, - } = findTestSuiteFactory(esArchiver, supertest); - - describe('find', () => { - findTest(`objects only within the current space (space_1)`, { - ...SPACES.SPACE_1, - tests: { - spaceAwareType: { - description: 'only the visualization', - statusCode: 200, - response: createExpectVisualizationResults(SPACES.SPACE_1.spaceId), - }, - notSpaceAwareType: { - description: 'only the visualization', - statusCode: 200, - response: expectNotSpaceAwareResults, - }, - hiddenType: { - description: 'empty result', - statusCode: 200, - response: createExpectEmpty(1, 20, 0), - }, - unknownType: { - description: 'empty result', - statusCode: 200, - response: createExpectEmpty(1, 20, 0), - }, - pageBeyondTotal: { - description: 'empty result', - statusCode: 200, - response: createExpectEmpty(100, 100, 1), - }, - unknownSearchField: { - description: 'empty result', - statusCode: 200, - response: createExpectEmpty(1, 20, 0), - }, - noType: { - description: 'bad request, type is required', - statusCode: 400, - response: expectTypeRequired, - }, - filterWithNotSpaceAwareType: { - description: 'only the visualization', - statusCode: 200, - response: expectNotSpaceAwareResults, - }, - filterWithHiddenType: { - description: 'empty result', - statusCode: 200, - response: createExpectEmpty(1, 20, 0), - }, - filterWithUnknownType: { - description: 'empty result', - statusCode: 200, - response: createExpectEmpty(1, 20, 0), - }, - filterWithNoType: { - description: 'bad request, type is required', - statusCode: 400, - response: expectTypeRequired, - }, - filterWithUnAllowedType: { - description: 'Bad Request', - statusCode: 400, - response: expectFilterWrongTypeError, - }, - }, - }); + const { addTests, createTestDefinitions } = findTestSuiteFactory(esArchiver, supertest); + const createTests = (spaceId: string) => { + const testCases = createTestCases(spaceId); + return createTestDefinitions(testCases, false); + }; - findTest(`objects only within the current space (default)`, { - ...SPACES.DEFAULT, - tests: { - spaceAwareType: { - description: 'only the visualization', - statusCode: 200, - response: createExpectVisualizationResults(SPACES.DEFAULT.spaceId), - }, - notSpaceAwareType: { - description: 'only the visualization', - statusCode: 200, - response: expectNotSpaceAwareResults, - }, - hiddenType: { - description: 'empty result', - statusCode: 200, - response: createExpectEmpty(1, 20, 0), - }, - unknownType: { - description: 'empty result', - statusCode: 200, - response: createExpectEmpty(1, 20, 0), - }, - pageBeyondTotal: { - description: 'empty result', - statusCode: 200, - response: createExpectEmpty(100, 100, 1), - }, - unknownSearchField: { - description: 'empty result', - statusCode: 200, - response: createExpectEmpty(1, 20, 0), - }, - noType: { - description: 'bad request, type is required', - statusCode: 400, - response: expectTypeRequired, - }, - filterWithNotSpaceAwareType: { - description: 'only the visualization', - statusCode: 200, - response: expectNotSpaceAwareResults, - }, - filterWithHiddenType: { - description: 'empty result', - statusCode: 200, - response: createExpectEmpty(1, 20, 0), - }, - filterWithUnknownType: { - description: 'empty result', - statusCode: 200, - response: createExpectEmpty(1, 20, 0), - }, - filterWithNoType: { - description: 'bad request, type is required', - statusCode: 400, - response: expectTypeRequired, - }, - filterWithUnAllowedType: { - description: 'Bad Request', - statusCode: 400, - response: expectFilterWrongTypeError, - }, - }, + describe('_find', () => { + getTestScenarios().spaces.forEach(({ spaceId }) => { + const tests = createTests(spaceId); + addTests(`within the ${spaceId} space`, { spaceId, tests }); }); }); } diff --git a/x-pack/test/saved_object_api_integration/spaces_only/apis/get.ts b/x-pack/test/saved_object_api_integration/spaces_only/apis/get.ts index 592bedd92b4d7..512ae968dd0dd 100644 --- a/x-pack/test/saved_object_api_integration/spaces_only/apis/get.ts +++ b/x-pack/test/saved_object_api_integration/spaces_only/apis/get.ts @@ -5,88 +5,48 @@ */ import { SPACES } from '../../common/lib/spaces'; +import { testCaseFailures, getTestScenarios } from '../../common/lib/saved_object_test_utils'; import { FtrProviderContext } from '../../common/ftr_provider_context'; -import { getTestSuiteFactory } from '../../common/suites/get'; +import { getTestSuiteFactory, TEST_CASES as CASES } from '../../common/suites/get'; + +const { + DEFAULT: { spaceId: DEFAULT_SPACE_ID }, + SPACE_1: { spaceId: SPACE_1_ID }, + SPACE_2: { spaceId: SPACE_2_ID }, +} = SPACES; +const { fail404 } = testCaseFailures; + +const createTestCases = (spaceId: string) => [ + // for each outcome, if failure !== undefined then we expect to receive + // an error; otherwise, we expect to receive a success result + { ...CASES.SINGLE_NAMESPACE_DEFAULT_SPACE, ...fail404(spaceId !== DEFAULT_SPACE_ID) }, + { ...CASES.SINGLE_NAMESPACE_SPACE_1, ...fail404(spaceId !== SPACE_1_ID) }, + { ...CASES.SINGLE_NAMESPACE_SPACE_2, ...fail404(spaceId !== SPACE_2_ID) }, + { + ...CASES.MULTI_NAMESPACE_DEFAULT_AND_SPACE_1, + ...fail404(spaceId !== DEFAULT_SPACE_ID && spaceId !== SPACE_1_ID), + }, + { ...CASES.MULTI_NAMESPACE_ONLY_SPACE_1, ...fail404(spaceId !== SPACE_1_ID) }, + { ...CASES.MULTI_NAMESPACE_ONLY_SPACE_2, ...fail404(spaceId !== SPACE_2_ID) }, + CASES.NAMESPACE_AGNOSTIC, + { ...CASES.HIDDEN, ...fail404() }, + { ...CASES.DOES_NOT_EXIST, ...fail404() }, +]; export default function({ getService }: FtrProviderContext) { const supertest = getService('supertest'); const esArchiver = getService('esArchiver'); - const { - createExpectDoesntExistNotFound, - createExpectSpaceAwareNotFound, - createExpectSpaceAwareResults, - createExpectNotSpaceAwareResults, - expectHiddenTypeNotFound: expectHiddenTypeNotFound, - getTest, - } = getTestSuiteFactory(esArchiver, supertest); - - describe('get', () => { - getTest(`can access objects belonging to the current space (default)`, { - ...SPACES.DEFAULT, - tests: { - spaceAware: { - statusCode: 200, - response: createExpectSpaceAwareResults(SPACES.DEFAULT.spaceId), - }, - notSpaceAware: { - statusCode: 200, - response: createExpectNotSpaceAwareResults(SPACES.DEFAULT.spaceId), - }, - hiddenType: { - statusCode: 404, - response: expectHiddenTypeNotFound, - }, - doesntExist: { - statusCode: 404, - response: createExpectDoesntExistNotFound(SPACES.DEFAULT.spaceId), - }, - }, - }); - - getTest(`can access objects belonging to the current space (space_1)`, { - ...SPACES.SPACE_1, - tests: { - spaceAware: { - statusCode: 200, - response: createExpectSpaceAwareResults(SPACES.SPACE_1.spaceId), - }, - notSpaceAware: { - statusCode: 200, - response: createExpectNotSpaceAwareResults(SPACES.SPACE_1.spaceId), - }, - hiddenType: { - statusCode: 404, - response: expectHiddenTypeNotFound, - }, - doesntExist: { - statusCode: 404, - response: createExpectDoesntExistNotFound(SPACES.SPACE_1.spaceId), - }, - }, - }); + const { addTests, createTestDefinitions } = getTestSuiteFactory(esArchiver, supertest); + const createTests = (spaceId: string) => { + const testCases = createTestCases(spaceId); + return createTestDefinitions(testCases, false, { spaceId }); + }; - getTest(`can't access space aware objects belonging to another space (space_1)`, { - spaceId: SPACES.DEFAULT.spaceId, - otherSpaceId: SPACES.SPACE_1.spaceId, - tests: { - spaceAware: { - statusCode: 404, - response: createExpectSpaceAwareNotFound(SPACES.SPACE_1.spaceId), - }, - notSpaceAware: { - statusCode: 200, - response: createExpectNotSpaceAwareResults(SPACES.SPACE_1.spaceId), - }, - hiddenType: { - statusCode: 404, - response: expectHiddenTypeNotFound, - }, - doesntExist: { - statusCode: 404, - response: createExpectDoesntExistNotFound(SPACES.SPACE_1.spaceId), - }, - }, + describe('_get', () => { + getTestScenarios().spaces.forEach(({ spaceId }) => { + const tests = createTests(spaceId); + addTests(`within the ${spaceId} space`, { spaceId, tests }); }); }); } diff --git a/x-pack/test/saved_object_api_integration/spaces_only/apis/import.ts b/x-pack/test/saved_object_api_integration/spaces_only/apis/import.ts index c78a0e1cc2cce..5fe4b08d91b54 100644 --- a/x-pack/test/saved_object_api_integration/spaces_only/apis/import.ts +++ b/x-pack/test/saved_object_api_integration/spaces_only/apis/import.ts @@ -5,56 +5,48 @@ */ import { SPACES } from '../../common/lib/spaces'; +import { testCaseFailures, getTestScenarios } from '../../common/lib/saved_object_test_utils'; import { FtrProviderContext } from '../../common/ftr_provider_context'; -import { importTestSuiteFactory } from '../../common/suites/import'; +import { importTestSuiteFactory, TEST_CASES as CASES } from '../../common/suites/import'; + +const { + DEFAULT: { spaceId: DEFAULT_SPACE_ID }, + SPACE_1: { spaceId: SPACE_1_ID }, + SPACE_2: { spaceId: SPACE_2_ID }, +} = SPACES; +const { fail400, fail409 } = testCaseFailures; + +const createTestCases = (spaceId: string) => [ + // for each outcome, if failure !== undefined then we expect to receive + // an error; otherwise, we expect to receive a success result + { ...CASES.SINGLE_NAMESPACE_DEFAULT_SPACE, ...fail409(spaceId === DEFAULT_SPACE_ID) }, + { ...CASES.SINGLE_NAMESPACE_SPACE_1, ...fail409(spaceId === SPACE_1_ID) }, + { ...CASES.SINGLE_NAMESPACE_SPACE_2, ...fail409(spaceId === SPACE_2_ID) }, + { ...CASES.MULTI_NAMESPACE_DEFAULT_AND_SPACE_1, ...fail400() }, + { ...CASES.MULTI_NAMESPACE_ONLY_SPACE_1, ...fail400() }, + { ...CASES.MULTI_NAMESPACE_ONLY_SPACE_2, ...fail400() }, + { ...CASES.NAMESPACE_AGNOSTIC, ...fail409() }, + { ...CASES.HIDDEN, ...fail400() }, + CASES.NEW_SINGLE_NAMESPACE_OBJ, + { ...CASES.NEW_MULTI_NAMESPACE_OBJ, ...fail400() }, + CASES.NEW_NAMESPACE_AGNOSTIC_OBJ, +]; export default function({ getService }: FtrProviderContext) { const supertest = getService('supertest'); const esArchiver = getService('esArchiver'); const es = getService('legacyEs'); - const { - importTest, - createExpectResults, - expectUnknownTypeUnsupported, - expectHiddenTypeUnsupported, - } = importTestSuiteFactory(es, esArchiver, supertest); + const { addTests, createTestDefinitions } = importTestSuiteFactory(es, esArchiver, supertest); + const createTests = (spaceId: string) => { + const testCases = createTestCases(spaceId); + return createTestDefinitions(testCases, false, { spaceId, singleRequest: true }); + }; describe('_import', () => { - importTest('in the current space (space_1)', { - ...SPACES.SPACE_1, - tests: { - default: { - statusCode: 200, - response: createExpectResults(SPACES.SPACE_1.spaceId), - }, - hiddenType: { - statusCode: 200, - response: expectHiddenTypeUnsupported, - }, - unknownType: { - statusCode: 200, - response: expectUnknownTypeUnsupported, - }, - }, - }); - - importTest('in the default space', { - ...SPACES.DEFAULT, - tests: { - default: { - statusCode: 200, - response: createExpectResults(SPACES.DEFAULT.spaceId), - }, - hiddenType: { - statusCode: 200, - response: expectHiddenTypeUnsupported, - }, - unknownType: { - statusCode: 200, - response: expectUnknownTypeUnsupported, - }, - }, + getTestScenarios().spaces.forEach(({ spaceId }) => { + const tests = createTests(spaceId); + addTests(`within the ${spaceId} space`, { spaceId, tests }); }); }); } diff --git a/x-pack/test/saved_object_api_integration/spaces_only/apis/index.ts b/x-pack/test/saved_object_api_integration/spaces_only/apis/index.ts index bb481e0c98bc1..c2f8339d38c97 100644 --- a/x-pack/test/saved_object_api_integration/spaces_only/apis/index.ts +++ b/x-pack/test/saved_object_api_integration/spaces_only/apis/index.ts @@ -12,6 +12,7 @@ export default function({ loadTestFile }: FtrProviderContext) { loadTestFile(require.resolve('./bulk_create')); loadTestFile(require.resolve('./bulk_get')); + loadTestFile(require.resolve('./bulk_update')); loadTestFile(require.resolve('./create')); loadTestFile(require.resolve('./delete')); loadTestFile(require.resolve('./export')); @@ -20,6 +21,5 @@ export default function({ loadTestFile }: FtrProviderContext) { loadTestFile(require.resolve('./import')); loadTestFile(require.resolve('./resolve_import_errors')); loadTestFile(require.resolve('./update')); - loadTestFile(require.resolve('./bulk_update')); }); } diff --git a/x-pack/test/saved_object_api_integration/spaces_only/apis/resolve_import_errors.ts b/x-pack/test/saved_object_api_integration/spaces_only/apis/resolve_import_errors.ts index 22a7ab81e5530..04f9ac8414afd 100644 --- a/x-pack/test/saved_object_api_integration/spaces_only/apis/resolve_import_errors.ts +++ b/x-pack/test/saved_object_api_integration/spaces_only/apis/resolve_import_errors.ts @@ -5,56 +5,59 @@ */ import { SPACES } from '../../common/lib/spaces'; +import { testCaseFailures, getTestScenarios } from '../../common/lib/saved_object_test_utils'; import { FtrProviderContext } from '../../common/ftr_provider_context'; -import { resolveImportErrorsTestSuiteFactory } from '../../common/suites/resolve_import_errors'; +import { + resolveImportErrorsTestSuiteFactory, + TEST_CASES as CASES, +} from '../../common/suites/resolve_import_errors'; + +const { + DEFAULT: { spaceId: DEFAULT_SPACE_ID }, + SPACE_1: { spaceId: SPACE_1_ID }, + SPACE_2: { spaceId: SPACE_2_ID }, +} = SPACES; +const { fail400, fail409 } = testCaseFailures; + +const createTestCases = (overwrite: boolean, spaceId: string) => [ + // for each outcome, if failure !== undefined then we expect to receive + // an error; otherwise, we expect to receive a success result + { + ...CASES.SINGLE_NAMESPACE_DEFAULT_SPACE, + ...fail409(!overwrite && spaceId === DEFAULT_SPACE_ID), + }, + { ...CASES.SINGLE_NAMESPACE_SPACE_1, ...fail409(!overwrite && spaceId === SPACE_1_ID) }, + { ...CASES.SINGLE_NAMESPACE_SPACE_2, ...fail409(!overwrite && spaceId === SPACE_2_ID) }, + { ...CASES.MULTI_NAMESPACE_DEFAULT_AND_SPACE_1, ...fail400() }, + { ...CASES.MULTI_NAMESPACE_ONLY_SPACE_1, ...fail400() }, + { ...CASES.MULTI_NAMESPACE_ONLY_SPACE_2, ...fail400() }, + { ...CASES.NAMESPACE_AGNOSTIC, ...fail409(!overwrite) }, + { ...CASES.HIDDEN, ...fail400() }, + CASES.NEW_SINGLE_NAMESPACE_OBJ, + { ...CASES.NEW_MULTI_NAMESPACE_OBJ, ...fail400() }, + CASES.NEW_NAMESPACE_AGNOSTIC_OBJ, +]; export default function({ getService }: FtrProviderContext) { const supertest = getService('supertest'); const esArchiver = getService('esArchiver'); const es = getService('legacyEs'); - const { - resolveImportErrorsTest, - createExpectResults, - expectUnknownTypeUnsupported, - expectHiddenTypeUnsupported, - } = resolveImportErrorsTestSuiteFactory(es, esArchiver, supertest); + const { addTests, createTestDefinitions } = resolveImportErrorsTestSuiteFactory( + es, + esArchiver, + supertest + ); + const createTests = (overwrite: boolean, spaceId: string) => { + const testCases = createTestCases(overwrite, spaceId); + return createTestDefinitions(testCases, false, overwrite, { spaceId, singleRequest: true }); + }; describe('_resolve_import_errors', () => { - resolveImportErrorsTest('in the current space (space_1)', { - ...SPACES.SPACE_1, - tests: { - default: { - statusCode: 200, - response: createExpectResults(SPACES.SPACE_1.spaceId), - }, - hiddenType: { - statusCode: 200, - response: expectHiddenTypeUnsupported, - }, - unknownType: { - statusCode: 200, - response: expectUnknownTypeUnsupported, - }, - }, - }); - - resolveImportErrorsTest('in the default space', { - ...SPACES.DEFAULT, - tests: { - default: { - statusCode: 200, - response: createExpectResults(SPACES.DEFAULT.spaceId), - }, - hiddenType: { - statusCode: 200, - response: expectHiddenTypeUnsupported, - }, - unknownType: { - statusCode: 200, - response: expectUnknownTypeUnsupported, - }, - }, + getTestScenarios([false, true]).spaces.forEach(({ spaceId, modifier: overwrite }) => { + const suffix = overwrite ? ' with overwrite enabled' : ''; + const tests = createTests(overwrite!, spaceId); + addTests(`within the ${spaceId} space${suffix}`, { spaceId, tests }); }); }); } diff --git a/x-pack/test/saved_object_api_integration/spaces_only/apis/update.ts b/x-pack/test/saved_object_api_integration/spaces_only/apis/update.ts index 6ffbda511d871..381861e33b68d 100644 --- a/x-pack/test/saved_object_api_integration/spaces_only/apis/update.ts +++ b/x-pack/test/saved_object_api_integration/spaces_only/apis/update.ts @@ -5,88 +5,48 @@ */ import { SPACES } from '../../common/lib/spaces'; +import { testCaseFailures, getTestScenarios } from '../../common/lib/saved_object_test_utils'; import { FtrProviderContext } from '../../common/ftr_provider_context'; -import { updateTestSuiteFactory } from '../../common/suites/update'; +import { updateTestSuiteFactory, TEST_CASES as CASES } from '../../common/suites/update'; + +const { + DEFAULT: { spaceId: DEFAULT_SPACE_ID }, + SPACE_1: { spaceId: SPACE_1_ID }, + SPACE_2: { spaceId: SPACE_2_ID }, +} = SPACES; +const { fail404 } = testCaseFailures; + +const createTestCases = (spaceId: string) => [ + // for each outcome, if failure !== undefined then we expect to receive + // an error; otherwise, we expect to receive a success result + { ...CASES.SINGLE_NAMESPACE_DEFAULT_SPACE, ...fail404(spaceId !== DEFAULT_SPACE_ID) }, + { ...CASES.SINGLE_NAMESPACE_SPACE_1, ...fail404(spaceId !== SPACE_1_ID) }, + { ...CASES.SINGLE_NAMESPACE_SPACE_2, ...fail404(spaceId !== SPACE_2_ID) }, + { + ...CASES.MULTI_NAMESPACE_DEFAULT_AND_SPACE_1, + ...fail404(spaceId !== DEFAULT_SPACE_ID && spaceId !== SPACE_1_ID), + }, + { ...CASES.MULTI_NAMESPACE_ONLY_SPACE_1, ...fail404(spaceId !== SPACE_1_ID) }, + { ...CASES.MULTI_NAMESPACE_ONLY_SPACE_2, ...fail404(spaceId !== SPACE_2_ID) }, + CASES.NAMESPACE_AGNOSTIC, + { ...CASES.HIDDEN, ...fail404() }, + { ...CASES.DOES_NOT_EXIST, ...fail404() }, +]; export default function({ getService }: FtrProviderContext) { const supertest = getService('supertest'); const esArchiver = getService('esArchiver'); - describe('update', () => { - const { - createExpectSpaceAwareNotFound, - expectSpaceAwareResults, - createExpectDoesntExistNotFound, - expectNotSpaceAwareResults, - expectSpaceNotFound, - updateTest, - } = updateTestSuiteFactory(esArchiver, supertest); - - updateTest(`in the default space`, { - spaceId: SPACES.DEFAULT.spaceId, - tests: { - spaceAware: { - statusCode: 200, - response: expectSpaceAwareResults, - }, - notSpaceAware: { - statusCode: 200, - response: expectNotSpaceAwareResults, - }, - hiddenType: { - statusCode: 404, - response: expectSpaceNotFound, - }, - doesntExist: { - statusCode: 404, - response: createExpectDoesntExistNotFound(SPACES.DEFAULT.spaceId), - }, - }, - }); - - updateTest('in the current space (space_1)', { - spaceId: SPACES.SPACE_1.spaceId, - tests: { - spaceAware: { - statusCode: 200, - response: expectSpaceAwareResults, - }, - notSpaceAware: { - statusCode: 200, - response: expectNotSpaceAwareResults, - }, - hiddenType: { - statusCode: 404, - response: expectSpaceNotFound, - }, - doesntExist: { - statusCode: 404, - response: createExpectDoesntExistNotFound(SPACES.SPACE_1.spaceId), - }, - }, - }); + const { addTests, createTestDefinitions } = updateTestSuiteFactory(esArchiver, supertest); + const createTests = (spaceId: string) => { + const testCases = createTestCases(spaceId); + return createTestDefinitions(testCases, false); + }; - updateTest('objects that exist in another space (space_1)', { - spaceId: SPACES.DEFAULT.spaceId, - otherSpaceId: SPACES.SPACE_1.spaceId, - tests: { - spaceAware: { - statusCode: 404, - response: createExpectSpaceAwareNotFound(SPACES.SPACE_1.spaceId), - }, - notSpaceAware: { - statusCode: 200, - response: expectNotSpaceAwareResults, - }, - hiddenType: { - statusCode: 404, - response: expectSpaceNotFound, - }, - doesntExist: { - statusCode: 404, - response: createExpectDoesntExistNotFound(), - }, - }, + describe('_update', () => { + getTestScenarios().spaces.forEach(({ spaceId }) => { + const tests = createTests(spaceId); + addTests(`within the ${spaceId} space`, { spaceId, tests }); }); }); } diff --git a/x-pack/test/siem_cypress/runner.ts b/x-pack/test/siem_cypress/runner.ts index 2462f75d4d0a4..b84e2953cc142 100644 --- a/x-pack/test/siem_cypress/runner.ts +++ b/x-pack/test/siem_cypress/runner.ts @@ -23,7 +23,7 @@ export async function SiemCypressTestRunner({ getService }: FtrProviderContext) await procs.run('cypress', { cmd: 'yarn', args: ['cypress:run'], - cwd: resolve(__dirname, '../../legacy/plugins/siem'), + cwd: resolve(__dirname, '../../plugins/siem'), env: { FORCE_COLOR: '1', CYPRESS_baseUrl: Url.format(config.get('servers.kibana')), diff --git a/x-pack/test/spaces_api_integration/common/config.ts b/x-pack/test/spaces_api_integration/common/config.ts index dffc6c524cc6e..19743e09f9420 100644 --- a/x-pack/test/spaces_api_integration/common/config.ts +++ b/x-pack/test/spaces_api_integration/common/config.ts @@ -67,6 +67,7 @@ export function createTestConfig(name: string, options: CreateTestConfigOptions) // disable anonymouse access so that we're testing both on and off in different suites '--status.allowAnonymous=false', '--server.xsrf.disableProtection=true', + `--plugin-path=${path.join(__dirname, 'fixtures', 'shared_type_plugin')}`, ...disabledPlugins.map(key => `--xpack.${key}.enabled=false`), ], }, diff --git a/x-pack/test/spaces_api_integration/common/fixtures/es_archiver/saved_objects/spaces/data.json b/x-pack/test/spaces_api_integration/common/fixtures/es_archiver/saved_objects/spaces/data.json index 64c1be0b90071..9a8a0a1fdda14 100644 --- a/x-pack/test/spaces_api_integration/common/fixtures/es_archiver/saved_objects/spaces/data.json +++ b/x-pack/test/spaces_api_integration/common/fixtures/es_archiver/saved_objects/spaces/data.json @@ -376,3 +376,123 @@ "type": "_doc" } } + +{ + "type": "doc", + "value": { + "id": "sharedtype:default_space_only", + "index": ".kibana", + "source": { + "sharedtype": { + "title": "A shared saved-object in the default space" + }, + "type": "sharedtype", + "namespaces": ["default"], + "updated_at": "2017-09-21T18:59:16.270Z" + }, + "type": "doc" + } +} + +{ + "type": "doc", + "value": { + "id": "sharedtype:space_1_only", + "index": ".kibana", + "source": { + "sharedtype": { + "title": "A shared saved-object in the space_1 space" + }, + "type": "sharedtype", + "namespaces": ["space_1"], + "updated_at": "2017-09-21T18:59:16.270Z" + }, + "type": "doc" + } +} + +{ + "type": "doc", + "value": { + "id": "sharedtype:space_2_only", + "index": ".kibana", + "source": { + "sharedtype": { + "title": "A shared saved-object in the space_2 space" + }, + "type": "sharedtype", + "namespaces": ["space_2"], + "updated_at": "2017-09-21T18:59:16.270Z" + }, + "type": "doc" + } +} + +{ + "type": "doc", + "value": { + "id": "sharedtype:default_and_space_1", + "index": ".kibana", + "source": { + "sharedtype": { + "title": "A shared saved-object in the default and space_1 spaces" + }, + "type": "sharedtype", + "namespaces": ["default", "space_1"], + "updated_at": "2017-09-21T18:59:16.270Z" + }, + "type": "doc" + } +} + +{ + "type": "doc", + "value": { + "id": "sharedtype:default_and_space_2", + "index": ".kibana", + "source": { + "sharedtype": { + "title": "A shared saved-object in the default and space_2 spaces" + }, + "type": "sharedtype", + "namespaces": ["default", "space_2"], + "updated_at": "2017-09-21T18:59:16.270Z" + }, + "type": "doc" + } +} + +{ + "type": "doc", + "value": { + "id": "sharedtype:space_1_and_space_2", + "index": ".kibana", + "source": { + "sharedtype": { + "title": "A shared saved-object in the space_1 and space_2 spaces" + }, + "type": "sharedtype", + "namespaces": ["space_1", "space_2"], + "updated_at": "2017-09-21T18:59:16.270Z" + }, + "type": "doc" + } +} + +{ + "type": "doc", + "value": { + "id": "sharedtype:all_spaces", + "index": ".kibana", + "source": { + "sharedtype": { + "title": "A shared saved-object in the default, space_1, and space_2 spaces" + }, + "type": "sharedtype", + "namespaces": ["default", "space_1", "space_2"], + "updated_at": "2017-09-21T18:59:16.270Z" + }, + "type": "doc" + } +} + diff --git a/x-pack/test/spaces_api_integration/common/fixtures/es_archiver/saved_objects/spaces/mappings.json b/x-pack/test/spaces_api_integration/common/fixtures/es_archiver/saved_objects/spaces/mappings.json index 1440585b625b9..508de68c32f70 100644 --- a/x-pack/test/spaces_api_integration/common/fixtures/es_archiver/saved_objects/spaces/mappings.json +++ b/x-pack/test/spaces_api_integration/common/fixtures/es_archiver/saved_objects/spaces/mappings.json @@ -159,6 +159,9 @@ "namespace": { "type": "keyword" }, + "namespaces": { + "type": "keyword" + }, "search": { "properties": { "columns": { @@ -320,6 +323,19 @@ "type": "text" } } + }, + "sharedtype": { + "properties": { + "title": { + "type": "text", + "fields": { + "keyword": { + "type": "keyword", + "ignore_above": 2048 + } + } + } + } } } }, diff --git a/x-pack/test/spaces_api_integration/common/fixtures/shared_type_plugin/index.js b/x-pack/test/spaces_api_integration/common/fixtures/shared_type_plugin/index.js new file mode 100644 index 0000000000000..91a24fb9f4f56 --- /dev/null +++ b/x-pack/test/spaces_api_integration/common/fixtures/shared_type_plugin/index.js @@ -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 mappings from './mappings.json'; + +export default function(kibana) { + return new kibana.Plugin({ + require: ['kibana', 'elasticsearch', 'xpack_main'], + name: 'shared_type_plugin', + uiExports: { + savedObjectsManagement: {}, + savedObjectSchemas: { + sharedtype: { + multiNamespace: true, + }, + }, + mappings, + }, + + config() {}, + + init() {}, // need empty init for plugin to load + }); +} diff --git a/x-pack/test/spaces_api_integration/common/fixtures/shared_type_plugin/mappings.json b/x-pack/test/spaces_api_integration/common/fixtures/shared_type_plugin/mappings.json new file mode 100644 index 0000000000000..918958aec0d6d --- /dev/null +++ b/x-pack/test/spaces_api_integration/common/fixtures/shared_type_plugin/mappings.json @@ -0,0 +1,15 @@ +{ + "sharedtype": { + "properties": { + "title": { + "type": "text", + "fields": { + "keyword": { + "type": "keyword", + "ignore_above": 2048 + } + } + } + } + } +} diff --git a/x-pack/test/spaces_api_integration/common/fixtures/shared_type_plugin/package.json b/x-pack/test/spaces_api_integration/common/fixtures/shared_type_plugin/package.json new file mode 100644 index 0000000000000..c52f4256c5c06 --- /dev/null +++ b/x-pack/test/spaces_api_integration/common/fixtures/shared_type_plugin/package.json @@ -0,0 +1,7 @@ +{ + "name": "shared_type_plugin", + "version": "0.0.0", + "kibana": { + "version": "kibana" + } +} diff --git a/x-pack/test/spaces_api_integration/common/lib/saved_object_test_cases.ts b/x-pack/test/spaces_api_integration/common/lib/saved_object_test_cases.ts new file mode 100644 index 0000000000000..67f5d737ba010 --- /dev/null +++ b/x-pack/test/spaces_api_integration/common/lib/saved_object_test_cases.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. + */ + +export const MULTI_NAMESPACE_SAVED_OBJECT_TEST_CASES = Object.freeze({ + DEFAULT_SPACE_ONLY: Object.freeze({ + id: 'default_space_only', + existingNamespaces: ['default'], + }), + SPACE_1_ONLY: Object.freeze({ + id: 'space_1_only', + existingNamespaces: ['space_1'], + }), + SPACE_2_ONLY: Object.freeze({ + id: 'space_2_only', + existingNamespaces: ['space_2'], + }), + DEFAULT_AND_SPACE_1: Object.freeze({ + id: 'default_and_space_1', + existingNamespaces: ['default', 'space_1'], + }), + DEFAULT_AND_SPACE_2: Object.freeze({ + id: 'default_and_space_2', + existingNamespaces: ['default', 'space_2'], + }), + SPACE_1_AND_SPACE_2: Object.freeze({ + id: 'space_1_and_space_2', + existingNamespaces: ['space_1', 'space_2'], + }), + ALL_SPACES: Object.freeze({ + id: 'all_spaces', + existingNamespaces: ['default', 'space_1', 'space_2'], + }), + DOES_NOT_EXIST: Object.freeze({ + id: 'does_not_exist', + existingNamespaces: [] as string[], + }), +}); diff --git a/x-pack/test/spaces_api_integration/common/suites/delete.ts b/x-pack/test/spaces_api_integration/common/suites/delete.ts index 9036fcbf7a8dd..0d8728fdf622e 100644 --- a/x-pack/test/spaces_api_integration/common/suites/delete.ts +++ b/x-pack/test/spaces_api_integration/common/suites/delete.ts @@ -6,6 +6,7 @@ import expect from '@kbn/expect'; import { SuperTest } from 'supertest'; import { getUrlPrefix } from '../lib/space_test_utils'; +import { MULTI_NAMESPACE_SAVED_OBJECT_TEST_CASES as CASES } from '../lib/saved_object_test_cases'; import { DescribeFn, TestDefinitionAuthentication } from '../lib/types'; interface DeleteTest { @@ -128,6 +129,25 @@ export function deleteTestSuiteFactory(es: any, esArchiver: any, supertest: Supe ]; expect(buckets).to.eql(expectedBuckets); + + // There were seven multi-namespace objects. + // Since Space 2 was deleted, any multi-namespace objects that existed in that space + // are updated to remove it, and of those, any that don't exist in any space are deleted. + const multiNamespaceResponse = await es.search({ + index: '.kibana', + body: { query: { terms: { type: ['sharedtype'] } } }, + }); + const docs: [Record] = multiNamespaceResponse.hits.hits; + expect(docs).length(6); // just six results, since spaces_2_only got deleted + Object.values(CASES).forEach(({ id, existingNamespaces }) => { + const remainingNamespaces = existingNamespaces.filter(x => x !== 'space_2'); + const doc = docs.find(x => x._id === `sharedtype:${id}`); + if (remainingNamespaces.length > 0) { + expect(doc?._source?.namespaces).to.eql(remainingNamespaces); + } else { + expect(doc).to.be(undefined); + } + }); }; const expectNotFound = (resp: { [key: string]: any }) => { diff --git a/x-pack/test/spaces_api_integration/common/suites/share_add.ts b/x-pack/test/spaces_api_integration/common/suites/share_add.ts new file mode 100644 index 0000000000000..b9a012b606da3 --- /dev/null +++ b/x-pack/test/spaces_api_integration/common/suites/share_add.ts @@ -0,0 +1,118 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import expect from '@kbn/expect'; +import { SuperTest } from 'supertest'; +import { SavedObjectsErrorHelpers } from '../../../../../src/core/server'; +import { SPACES } from '../lib/spaces'; +import { + expectResponses, + getUrlPrefix, +} from '../../../saved_object_api_integration/common/lib/saved_object_test_utils'; +import { + ExpectResponseBody, + TestDefinition, + TestSuite, +} from '../../../saved_object_api_integration/common/lib/types'; + +export interface ShareAddTestDefinition extends TestDefinition { + request: { spaces: string[]; object: { type: string; id: string } }; +} +export type ShareAddTestSuite = TestSuite; +export interface ShareAddTestCase { + id: string; + namespaces: string[]; + failure?: 400 | 403 | 404; + fail400Param?: string; + fail403Param?: string; +} + +const TYPE = 'sharedtype'; +const createRequest = ({ id, namespaces }: ShareAddTestCase) => ({ + spaces: namespaces, + object: { type: TYPE, id }, +}); +const getTestTitle = ({ id, namespaces }: ShareAddTestCase) => + `{id: ${id}, namespaces: [${namespaces.join(',')}]}`; + +export function shareAddTestSuiteFactory(esArchiver: any, supertest: SuperTest) { + const expectResponseBody = (testCase: ShareAddTestCase): ExpectResponseBody => async ( + response: Record + ) => { + const { id, failure, fail400Param, fail403Param } = testCase; + const object = response.body; + if (failure === 403) { + await expectResponses.forbidden(fail403Param!)(TYPE)(response); + } else if (failure) { + let error: any; + if (failure === 400) { + error = SavedObjectsErrorHelpers.createBadRequestError( + `${id} already exists in the following namespace(s): ${fail400Param}` + ); + } else if (failure === 404) { + error = SavedObjectsErrorHelpers.createGenericNotFoundError(TYPE, id); + } + expect(object.error).to.eql(error.output.payload.error); + expect(object.statusCode).to.eql(error.output.payload.statusCode); + } else { + // success + expect(object).to.eql({}); + } + }; + const createTestDefinitions = ( + testCases: ShareAddTestCase | ShareAddTestCase[], + forbidden: boolean, + options?: { + responseBodyOverride?: ExpectResponseBody; + fail403Param?: string; + } + ): ShareAddTestDefinition[] => { + let cases = Array.isArray(testCases) ? testCases : [testCases]; + if (forbidden) { + // override the expected result in each test case + cases = cases.map(x => ({ ...x, failure: 403, fail403Param: options?.fail403Param })); + } + return cases.map(x => ({ + title: getTestTitle(x), + responseStatusCode: x.failure ?? 204, + request: createRequest(x), + responseBody: options?.responseBodyOverride || expectResponseBody(x), + })); + }; + + const makeShareAddTest = (describeFn: Mocha.SuiteFunction) => ( + description: string, + definition: ShareAddTestSuite + ) => { + const { user, spaceId = SPACES.DEFAULT.spaceId, tests } = definition; + + describeFn(description, () => { + before(() => esArchiver.load('saved_objects/spaces')); + after(() => esArchiver.unload('saved_objects/spaces')); + + for (const test of tests) { + it(`should return ${test.responseStatusCode} ${test.title}`, async () => { + const requestBody = test.request; + await supertest + .post(`${getUrlPrefix(spaceId)}/api/spaces/_share_saved_object_add`) + .auth(user?.username, user?.password) + .send(requestBody) + .expect(test.responseStatusCode) + .then(test.responseBody); + }); + } + }); + }; + + const addTests = makeShareAddTest(describe); + // @ts-ignore + addTests.only = makeShareAddTest(describe.only); + + return { + addTests, + createTestDefinitions, + }; +} diff --git a/x-pack/test/spaces_api_integration/common/suites/share_remove.ts b/x-pack/test/spaces_api_integration/common/suites/share_remove.ts new file mode 100644 index 0000000000000..b5fcbe5a1cf2c --- /dev/null +++ b/x-pack/test/spaces_api_integration/common/suites/share_remove.ts @@ -0,0 +1,116 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import expect from '@kbn/expect'; +import { SuperTest } from 'supertest'; +import { SavedObjectsErrorHelpers } from '../../../../../src/core/server'; +import { SPACES } from '../lib/spaces'; +import { + expectResponses, + getUrlPrefix, + getTestTitle, +} from '../../../saved_object_api_integration/common/lib/saved_object_test_utils'; +import { + ExpectResponseBody, + TestDefinition, + TestSuite, +} from '../../../saved_object_api_integration/common/lib/types'; + +export interface ShareRemoveTestDefinition extends TestDefinition { + request: { spaces: string[]; object: { type: string; id: string } }; +} +export type ShareRemoveTestSuite = TestSuite; +export interface ShareRemoveTestCase { + id: string; + namespaces: string[]; + failure?: 400 | 403 | 404; + fail400Param?: string; +} + +const TYPE = 'sharedtype'; +const createRequest = ({ id, namespaces }: ShareRemoveTestCase) => ({ + spaces: namespaces, + object: { type: TYPE, id }, +}); + +export function shareRemoveTestSuiteFactory(esArchiver: any, supertest: SuperTest) { + const expectForbidden = expectResponses.forbidden('delete'); + const expectResponseBody = (testCase: ShareRemoveTestCase): ExpectResponseBody => async ( + response: Record + ) => { + const { id, failure, fail400Param } = testCase; + const object = response.body; + if (failure === 403) { + await expectForbidden(TYPE)(response); + } else if (failure) { + let error: any; + if (failure === 400) { + error = SavedObjectsErrorHelpers.createBadRequestError( + `${id} doesn't exist in the following namespace(s): ${fail400Param}` + ); + } else if (failure === 404) { + error = SavedObjectsErrorHelpers.createGenericNotFoundError(TYPE, id); + } + expect(object.error).to.eql(error.output.payload.error); + expect(object.statusCode).to.eql(error.output.payload.statusCode); + } else { + // success + expect(object).to.eql({}); + } + }; + const createTestDefinitions = ( + testCases: ShareRemoveTestCase | ShareRemoveTestCase[], + forbidden: boolean, + options?: { + responseBodyOverride?: ExpectResponseBody; + } + ): ShareRemoveTestDefinition[] => { + let cases = Array.isArray(testCases) ? testCases : [testCases]; + if (forbidden) { + // override the expected result in each test case + cases = cases.map(x => ({ ...x, failure: 403 })); + } + return cases.map(x => ({ + title: getTestTitle({ ...x, type: TYPE }), + responseStatusCode: x.failure ?? 204, + request: createRequest(x), + responseBody: options?.responseBodyOverride || expectResponseBody(x), + })); + }; + + const makeShareRemoveTest = (describeFn: Mocha.SuiteFunction) => ( + description: string, + definition: ShareRemoveTestSuite + ) => { + const { user, spaceId = SPACES.DEFAULT.spaceId, tests } = definition; + + describeFn(description, () => { + before(() => esArchiver.load('saved_objects/spaces')); + after(() => esArchiver.unload('saved_objects/spaces')); + + for (const test of tests) { + it(`should return ${test.responseStatusCode} ${test.title}`, async () => { + const requestBody = test.request; + await supertest + .post(`${getUrlPrefix(spaceId)}/api/spaces/_share_saved_object_remove`) + .auth(user?.username, user?.password) + .send(requestBody) + .expect(test.responseStatusCode) + .then(test.responseBody); + }); + } + }); + }; + + const addTests = makeShareRemoveTest(describe); + // @ts-ignore + addTests.only = makeShareRemoveTest(describe.only); + + return { + addTests, + createTestDefinitions, + }; +} diff --git a/x-pack/test/spaces_api_integration/security_and_spaces/apis/index.ts b/x-pack/test/spaces_api_integration/security_and_spaces/apis/index.ts index e918ab0b53841..8d85d95e6812f 100644 --- a/x-pack/test/spaces_api_integration/security_and_spaces/apis/index.ts +++ b/x-pack/test/spaces_api_integration/security_and_spaces/apis/index.ts @@ -25,6 +25,8 @@ export default function({ loadTestFile, getService }: TestInvoker) { loadTestFile(require.resolve('./delete')); loadTestFile(require.resolve('./get_all')); loadTestFile(require.resolve('./get')); + loadTestFile(require.resolve('./share_add')); + loadTestFile(require.resolve('./share_remove')); loadTestFile(require.resolve('./update')); }); } diff --git a/x-pack/test/spaces_api_integration/security_and_spaces/apis/share_add.ts b/x-pack/test/spaces_api_integration/security_and_spaces/apis/share_add.ts new file mode 100644 index 0000000000000..c7e65ac424776 --- /dev/null +++ b/x-pack/test/spaces_api_integration/security_and_spaces/apis/share_add.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 { SPACES } from '../../common/lib/spaces'; +import { + testCaseFailures, + getTestScenarios, +} from '../../../saved_object_api_integration/common/lib/saved_object_test_utils'; +import { TestUser } from '../../../saved_object_api_integration/common/lib/types'; +import { MULTI_NAMESPACE_SAVED_OBJECT_TEST_CASES as CASES } from '../../common/lib/saved_object_test_cases'; +import { TestInvoker } from '../../common/lib/types'; +import { shareAddTestSuiteFactory, ShareAddTestDefinition } from '../../common/suites/share_add'; + +const { + DEFAULT: { spaceId: DEFAULT_SPACE_ID }, + SPACE_1: { spaceId: SPACE_1_ID }, + SPACE_2: { spaceId: SPACE_2_ID }, +} = SPACES; +const { fail404 } = testCaseFailures; + +const createTestCases = (spaceId: string) => { + const namespaces = [spaceId]; + return [ + // Test cases to check adding the target namespace to different saved objects + { ...CASES.DEFAULT_SPACE_ONLY, namespaces, ...fail404(spaceId !== DEFAULT_SPACE_ID) }, + { ...CASES.SPACE_1_ONLY, namespaces, ...fail404(spaceId !== SPACE_1_ID) }, + { ...CASES.SPACE_2_ONLY, namespaces, ...fail404(spaceId !== SPACE_2_ID) }, + { ...CASES.DEFAULT_AND_SPACE_1, namespaces, ...fail404(spaceId === SPACE_2_ID) }, + { ...CASES.DEFAULT_AND_SPACE_2, namespaces, ...fail404(spaceId === SPACE_1_ID) }, + { ...CASES.SPACE_1_AND_SPACE_2, namespaces, ...fail404(spaceId === DEFAULT_SPACE_ID) }, + { ...CASES.ALL_SPACES, namespaces }, + { ...CASES.DOES_NOT_EXIST, namespaces, ...fail404() }, + // Test cases to check adding multiple namespaces to different saved objects that exist in one space + // These are non-exhaustive, they only check cases for adding two additional namespaces to a saved object + // More permutations are covered in the corresponding spaces_only test suite + { + ...CASES.DEFAULT_SPACE_ONLY, + namespaces: [SPACE_1_ID, SPACE_2_ID], + ...fail404(spaceId !== DEFAULT_SPACE_ID), + }, + { + ...CASES.SPACE_1_ONLY, + namespaces: [DEFAULT_SPACE_ID, SPACE_2_ID], + ...fail404(spaceId !== SPACE_1_ID), + }, + { + ...CASES.SPACE_2_ONLY, + namespaces: [DEFAULT_SPACE_ID, SPACE_1_ID], + ...fail404(spaceId !== SPACE_2_ID), + }, + ]; +}; +const calculateSingleSpaceAuthZ = ( + testCases: ReturnType, + spaceId: string +) => { + const targetsOtherSpace = testCases.filter( + x => !x.namespaces.includes(spaceId) || x.namespaces.length > 1 + ); + const tmp = testCases.filter(x => !targetsOtherSpace.includes(x)); // doesn't target other space + const doesntExistInThisSpace = tmp.filter(x => !x.existingNamespaces.includes(spaceId)); + const existsInThisSpace = tmp.filter(x => x.existingNamespaces.includes(spaceId)); + return { targetsOtherSpace, doesntExistInThisSpace, existsInThisSpace }; +}; +// eslint-disable-next-line import/no-default-export +export default function({ getService }: TestInvoker) { + const supertest = getService('supertestWithoutAuth'); + const esArchiver = getService('esArchiver'); + + const { addTests, createTestDefinitions } = shareAddTestSuiteFactory(esArchiver, supertest); + const createTests = (spaceId: string) => { + const testCases = createTestCases(spaceId); + const thisSpace = calculateSingleSpaceAuthZ(testCases, spaceId); + const otherSpaceId = spaceId === DEFAULT_SPACE_ID ? SPACE_1_ID : DEFAULT_SPACE_ID; + const otherSpace = calculateSingleSpaceAuthZ(testCases, otherSpaceId); + return { + unauthorized: createTestDefinitions(testCases, true, { fail403Param: 'create' }), + authorizedInSpace: [ + createTestDefinitions(thisSpace.targetsOtherSpace, true, { fail403Param: 'create' }), + createTestDefinitions(thisSpace.doesntExistInThisSpace, false), + createTestDefinitions(thisSpace.existsInThisSpace, false), + ].flat(), + authorizedInOtherSpace: [ + createTestDefinitions(otherSpace.targetsOtherSpace, true, { fail403Param: 'create' }), + // If the preflight GET request fails, it will return a 404 error; users who are authorized to create saved objects in the target + // space(s) but are not authorized to update saved objects in this space will see a 403 error instead of 404. This is a safeguard to + // prevent potential information disclosure of the spaces that a given saved object may exist in. + createTestDefinitions(otherSpace.doesntExistInThisSpace, true, { fail403Param: 'update' }), + createTestDefinitions(otherSpace.existsInThisSpace, false), + ].flat(), + authorized: createTestDefinitions(testCases, false), + }; + }; + + describe('_share_saved_object_add', () => { + getTestScenarios().securityAndSpaces.forEach(({ spaceId, users }) => { + const suffix = ` targeting the ${spaceId} space`; + const { unauthorized, authorizedInSpace, authorizedInOtherSpace, authorized } = createTests( + spaceId + ); + const _addTests = (user: TestUser, tests: ShareAddTestDefinition[]) => { + addTests(`${user.description}${suffix}`, { user, spaceId, tests }); + }; + + [ + users.noAccess, + users.legacyAll, + users.dualRead, + users.readGlobally, + users.readAtSpace, + ].forEach(user => { + _addTests(user, unauthorized); + }); + _addTests(users.allAtSpace, authorizedInSpace); + _addTests(users.allAtOtherSpace, authorizedInOtherSpace); + [users.dualAll, users.allGlobally, users.superuser].forEach(user => { + _addTests(user, authorized); + }); + }); + }); +} diff --git a/x-pack/test/spaces_api_integration/security_and_spaces/apis/share_remove.ts b/x-pack/test/spaces_api_integration/security_and_spaces/apis/share_remove.ts new file mode 100644 index 0000000000000..3a8d42f620a3e --- /dev/null +++ b/x-pack/test/spaces_api_integration/security_and_spaces/apis/share_remove.ts @@ -0,0 +1,99 @@ +/* + * 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 { SPACES } from '../../common/lib/spaces'; +import { + testCaseFailures, + getTestScenarios, +} from '../../../saved_object_api_integration/common/lib/saved_object_test_utils'; +import { TestUser } from '../../../saved_object_api_integration/common/lib/types'; +import { MULTI_NAMESPACE_SAVED_OBJECT_TEST_CASES as CASES } from '../../common/lib/saved_object_test_cases'; +import { TestInvoker } from '../../common/lib/types'; +import { + shareRemoveTestSuiteFactory, + ShareRemoveTestCase, + ShareRemoveTestDefinition, +} from '../../common/suites/share_remove'; + +const { + DEFAULT: { spaceId: DEFAULT_SPACE_ID }, + SPACE_1: { spaceId: SPACE_1_ID }, + SPACE_2: { spaceId: SPACE_2_ID }, +} = SPACES; +const { fail404 } = testCaseFailures; + +const createTestCases = (spaceId: string) => { + // Test cases to check removing the target namespace from different saved objects + let namespaces = [spaceId]; + const singleSpace = [ + { id: CASES.DEFAULT_SPACE_ONLY.id, namespaces, ...fail404(spaceId !== DEFAULT_SPACE_ID) }, + { id: CASES.SPACE_1_ONLY.id, namespaces, ...fail404(spaceId !== SPACE_1_ID) }, + { id: CASES.SPACE_2_ONLY.id, namespaces, ...fail404(spaceId !== SPACE_2_ID) }, + { id: CASES.DEFAULT_AND_SPACE_1.id, namespaces, ...fail404(spaceId === SPACE_2_ID) }, + { id: CASES.DEFAULT_AND_SPACE_2.id, namespaces, ...fail404(spaceId === SPACE_1_ID) }, + { id: CASES.SPACE_1_AND_SPACE_2.id, namespaces, ...fail404(spaceId === DEFAULT_SPACE_ID) }, + { id: CASES.ALL_SPACES.id, namespaces }, + { id: CASES.DOES_NOT_EXIST.id, namespaces, ...fail404() }, + ] as ShareRemoveTestCase[]; + + // Test cases to check removing all three namespaces from different saved objects that exist in two spaces + // These are non-exhaustive, they only check some cases -- each object will result in a 404, either because + // it never existed in the target namespace, or it was removed in one of the test cases above + // More permutations are covered in the corresponding spaces_only test suite + namespaces = [DEFAULT_SPACE_ID, SPACE_1_ID, SPACE_2_ID]; + const multipleSpaces = [ + { id: CASES.DEFAULT_AND_SPACE_1.id, namespaces, ...fail404() }, + { id: CASES.DEFAULT_AND_SPACE_2.id, namespaces, ...fail404() }, + { id: CASES.SPACE_1_AND_SPACE_2.id, namespaces, ...fail404() }, + ] as ShareRemoveTestCase[]; + + const allCases = singleSpace.concat(multipleSpaces); + return { singleSpace, multipleSpaces, allCases }; +}; + +// eslint-disable-next-line import/no-default-export +export default function({ getService }: TestInvoker) { + const supertest = getService('supertestWithoutAuth'); + const esArchiver = getService('esArchiver'); + + const { addTests, createTestDefinitions } = shareRemoveTestSuiteFactory(esArchiver, supertest); + const createTests = (spaceId: string) => { + const { singleSpace, multipleSpaces, allCases } = createTestCases(spaceId); + return { + unauthorized: createTestDefinitions(allCases, true), + authorizedThisSpace: [ + createTestDefinitions(singleSpace, false), + createTestDefinitions(multipleSpaces, true), + ].flat(), + authorizedGlobally: createTestDefinitions(allCases, false), + }; + }; + + describe('_share_saved_object_remove', () => { + getTestScenarios().securityAndSpaces.forEach(({ spaceId, users }) => { + const suffix = ` targeting the ${spaceId} space`; + const { unauthorized, authorizedThisSpace, authorizedGlobally } = createTests(spaceId); + const _addTests = (user: TestUser, tests: ShareRemoveTestDefinition[]) => { + addTests(`${user.description}${suffix}`, { user, spaceId, tests }); + }; + + [ + users.noAccess, + users.legacyAll, + users.dualRead, + users.readGlobally, + users.readAtSpace, + users.allAtOtherSpace, + ].forEach(user => { + _addTests(user, unauthorized); + }); + _addTests(users.allAtSpace, authorizedThisSpace); + [users.dualAll, users.allGlobally, users.superuser].forEach(user => { + _addTests(user, authorizedGlobally); + }); + }); + }); +} diff --git a/x-pack/test/spaces_api_integration/spaces_only/apis/index.ts b/x-pack/test/spaces_api_integration/spaces_only/apis/index.ts index 1182f6bdabcff..b02dc35b58b56 100644 --- a/x-pack/test/spaces_api_integration/spaces_only/apis/index.ts +++ b/x-pack/test/spaces_api_integration/spaces_only/apis/index.ts @@ -17,6 +17,8 @@ export default function spacesOnlyTestSuite({ loadTestFile }: TestInvoker) { loadTestFile(require.resolve('./delete')); loadTestFile(require.resolve('./get_all')); loadTestFile(require.resolve('./get')); + loadTestFile(require.resolve('./share_add')); + loadTestFile(require.resolve('./share_remove')); loadTestFile(require.resolve('./update')); }); } diff --git a/x-pack/test/spaces_api_integration/spaces_only/apis/share_add.ts b/x-pack/test/spaces_api_integration/spaces_only/apis/share_add.ts new file mode 100644 index 0000000000000..f1e603836fa21 --- /dev/null +++ b/x-pack/test/spaces_api_integration/spaces_only/apis/share_add.ts @@ -0,0 +1,84 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { SPACES } from '../../common/lib/spaces'; +import { + testCaseFailures, + getTestScenarios, +} from '../../../saved_object_api_integration/common/lib/saved_object_test_utils'; +import { TestInvoker } from '../../common/lib/types'; +import { MULTI_NAMESPACE_SAVED_OBJECT_TEST_CASES as CASES } from '../../common/lib/saved_object_test_cases'; +import { shareAddTestSuiteFactory } from '../../common/suites/share_add'; + +const { + DEFAULT: { spaceId: DEFAULT_SPACE_ID }, + SPACE_1: { spaceId: SPACE_1_ID }, + SPACE_2: { spaceId: SPACE_2_ID }, +} = SPACES; +const { fail404 } = testCaseFailures; + +/** + * Single-namespace test cases + * @param spaceId the namespace to add to each saved object + */ +const createSingleTestCases = (spaceId: string) => { + const namespaces = ['some-space-id']; + return [ + { ...CASES.DEFAULT_SPACE_ONLY, namespaces, ...fail404(spaceId !== DEFAULT_SPACE_ID) }, + { ...CASES.SPACE_1_ONLY, namespaces, ...fail404(spaceId !== SPACE_1_ID) }, + { ...CASES.SPACE_2_ONLY, namespaces, ...fail404(spaceId !== SPACE_2_ID) }, + { ...CASES.DEFAULT_AND_SPACE_1, namespaces, ...fail404(spaceId === SPACE_2_ID) }, + { ...CASES.DEFAULT_AND_SPACE_2, namespaces, ...fail404(spaceId === SPACE_1_ID) }, + { ...CASES.SPACE_1_AND_SPACE_2, namespaces, ...fail404(spaceId === DEFAULT_SPACE_ID) }, + { ...CASES.ALL_SPACES, namespaces }, + { ...CASES.DOES_NOT_EXIST, namespaces, ...fail404() }, + ]; +}; +/** + * Multi-namespace test cases + * These are non-exhaustive, but they check different permutations of saved objects and spaces to add + */ +const createMultiTestCases = () => { + const allSpaces = [DEFAULT_SPACE_ID, SPACE_1_ID, SPACE_2_ID]; + let id = CASES.DEFAULT_SPACE_ONLY.id; + const one = [{ id, namespaces: allSpaces }]; + id = CASES.DEFAULT_AND_SPACE_1.id; + const two = [{ id, namespaces: allSpaces }]; + id = CASES.ALL_SPACES.id; + const three = [{ id, namespaces: allSpaces }]; + return { one, two, three }; +}; + +// eslint-disable-next-line import/no-default-export +export default function({ getService }: TestInvoker) { + const supertest = getService('supertest'); + const esArchiver = getService('esArchiver'); + + const { addTests, createTestDefinitions } = shareAddTestSuiteFactory(esArchiver, supertest); + const createSingleTests = (spaceId: string) => { + const testCases = createSingleTestCases(spaceId); + return createTestDefinitions(testCases, false); + }; + const createMultiTests = () => { + const testCases = createMultiTestCases(); + return { + one: createTestDefinitions(testCases.one, false), + two: createTestDefinitions(testCases.two, false), + three: createTestDefinitions(testCases.three, false), + }; + }; + + describe('_share_saved_object_add', () => { + getTestScenarios().spaces.forEach(({ spaceId }) => { + const tests = createSingleTests(spaceId); + addTests(`targeting the ${spaceId} space`, { spaceId, tests }); + }); + const { one, two, three } = createMultiTests(); + addTests('for a saved object in the default space', { tests: one }); + addTests('for a saved object in the default and space_1 spaces', { tests: two }); + addTests('for a saved object in the default, space_1, and space_2 spaces', { tests: three }); + }); +} diff --git a/x-pack/test/spaces_api_integration/spaces_only/apis/share_remove.ts b/x-pack/test/spaces_api_integration/spaces_only/apis/share_remove.ts new file mode 100644 index 0000000000000..15be72c9f09ac --- /dev/null +++ b/x-pack/test/spaces_api_integration/spaces_only/apis/share_remove.ts @@ -0,0 +1,99 @@ +/* + * 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 { SPACES } from '../../common/lib/spaces'; +import { + testCaseFailures, + getTestScenarios, +} from '../../../saved_object_api_integration/common/lib/saved_object_test_utils'; +import { TestInvoker } from '../../common/lib/types'; +import { MULTI_NAMESPACE_SAVED_OBJECT_TEST_CASES as CASES } from '../../common/lib/saved_object_test_cases'; +import { shareRemoveTestSuiteFactory } from '../../common/suites/share_remove'; + +const { + DEFAULT: { spaceId: DEFAULT_SPACE_ID }, + SPACE_1: { spaceId: SPACE_1_ID }, + SPACE_2: { spaceId: SPACE_2_ID }, +} = SPACES; +const { fail404 } = testCaseFailures; + +/** + * Single-namespace test cases + * @param spaceId the namespace to remove from each saved object + */ +const createSingleTestCases = (spaceId: string) => { + const namespaces = [spaceId]; + return [ + { ...CASES.DEFAULT_SPACE_ONLY, namespaces, ...fail404(spaceId !== DEFAULT_SPACE_ID) }, + { ...CASES.SPACE_1_ONLY, namespaces, ...fail404(spaceId !== SPACE_1_ID) }, + { ...CASES.SPACE_2_ONLY, namespaces, ...fail404(spaceId !== SPACE_2_ID) }, + { ...CASES.DEFAULT_AND_SPACE_1, namespaces, ...fail404(spaceId === SPACE_2_ID) }, + { ...CASES.DEFAULT_AND_SPACE_2, namespaces, ...fail404(spaceId === SPACE_1_ID) }, + { ...CASES.SPACE_1_AND_SPACE_2, namespaces, ...fail404(spaceId === DEFAULT_SPACE_ID) }, + { ...CASES.ALL_SPACES, namespaces }, + { ...CASES.DOES_NOT_EXIST, namespaces, ...fail404() }, + ]; +}; +/** + * Multi-namespace test cases + * These are non-exhaustive, but they check different permutations of saved objects and spaces to remove + */ +const createMultiTestCases = () => { + const nonExistentSpaceId = 'does_not_exist'; // space that doesn't exist + let id = CASES.DEFAULT_SPACE_ONLY.id; + const one = [ + { id, namespaces: [nonExistentSpaceId] }, + { id, namespaces: [DEFAULT_SPACE_ID, SPACE_1_ID, SPACE_2_ID] }, + { id, namespaces: [DEFAULT_SPACE_ID], ...fail404() }, // this saved object no longer exists + ]; + id = CASES.DEFAULT_AND_SPACE_1.id; + const two = [ + { id, namespaces: [DEFAULT_SPACE_ID, nonExistentSpaceId] }, + // this saved object will not be found in the context of the current namespace ('default') + { id, namespaces: [DEFAULT_SPACE_ID], ...fail404() }, // this object's namespaces no longer contains DEFAULT_SPACE_ID + { id, namespaces: [SPACE_1_ID], ...fail404() }, // this object's namespaces does contain SPACE_1_ID + ]; + id = CASES.ALL_SPACES.id; + const three = [ + { id, namespaces: [DEFAULT_SPACE_ID, SPACE_1_ID, nonExistentSpaceId] }, + // this saved object will not be found in the context of the current namespace ('default') + { id, namespaces: [DEFAULT_SPACE_ID], ...fail404() }, // this object's namespaces no longer contains DEFAULT_SPACE_ID + { id, namespaces: [SPACE_1_ID], ...fail404() }, // this object's namespaces no longer contains SPACE_1_ID + { id, namespaces: [SPACE_2_ID], ...fail404() }, // this object's namespaces does contain SPACE_2_ID + ]; + return { one, two, three }; +}; + +// eslint-disable-next-line import/no-default-export +export default function({ getService }: TestInvoker) { + const supertest = getService('supertest'); + const esArchiver = getService('esArchiver'); + + const { addTests, createTestDefinitions } = shareRemoveTestSuiteFactory(esArchiver, supertest); + const createSingleTests = (spaceId: string) => { + const testCases = createSingleTestCases(spaceId); + return createTestDefinitions(testCases, false); + }; + const createMultiTests = () => { + const testCases = createMultiTestCases(); + return { + one: createTestDefinitions(testCases.one, false), + two: createTestDefinitions(testCases.two, false), + three: createTestDefinitions(testCases.three, false), + }; + }; + + describe('_share_saved_object_remove', () => { + getTestScenarios().spaces.forEach(({ spaceId }) => { + const tests = createSingleTests(spaceId); + addTests(`targeting the ${spaceId} space`, { spaceId, tests }); + }); + const { one, two, three } = createMultiTests(); + addTests('for a saved object in the default space', { tests: one }); + addTests('for a saved object in the default and space_1 spaces', { tests: two }); + addTests('for a saved object in the default, space_1, and space_2 spaces', { tests: three }); + }); +} diff --git a/x-pack/test/tsconfig.json b/x-pack/test/tsconfig.json index e9aab9b47535f..df1d7e789507b 100644 --- a/x-pack/test/tsconfig.json +++ b/x-pack/test/tsconfig.json @@ -3,7 +3,8 @@ "compilerOptions": { "types": [ "mocha", - "node" + "node", + "flot" ] }, "include": [ diff --git a/x-pack/tsconfig.json b/x-pack/tsconfig.json index a6c94ff74620e..a540c7e3c9786 100644 --- a/x-pack/tsconfig.json +++ b/x-pack/tsconfig.json @@ -11,7 +11,7 @@ ], "exclude": [ "test/**/*", - "legacy/plugins/siem/cypress/**/*", + "plugins/siem/cypress/**/*", "legacy/plugins/apm/e2e/cypress/**/*", "**/typespec_tests.ts" ], @@ -40,7 +40,8 @@ }, "types": [ "node", - "jest" + "jest", + "flot" ] } } diff --git a/x-pack/typings/rison_node.d.ts b/x-pack/typings/rison_node.d.ts index ec8e5c1f407ad..f830adc897445 100644 --- a/x-pack/typings/rison_node.d.ts +++ b/x-pack/typings/rison_node.d.ts @@ -5,7 +5,7 @@ */ declare module 'rison-node' { - export type RisonValue = null | boolean | number | string | RisonObject | RisonArray; + export type RisonValue = undefined | null | boolean | number | string | RisonObject | RisonArray; // eslint-disable-next-line @typescript-eslint/no-empty-interface export interface RisonArray extends Array {} diff --git a/yarn.lock b/yarn.lock index 3f04b2d26a013..b47befbf9057b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -61,7 +61,7 @@ semver "^5.4.1" source-map "^0.5.0" -"@babel/core@^7.0.0", "@babel/core@^7.0.1", "@babel/core@^7.1.0", "@babel/core@^7.4.3", "@babel/core@^7.9.0": +"@babel/core@^7.0.0", "@babel/core@^7.0.1", "@babel/core@^7.1.0", "@babel/core@^7.4.3", "@babel/core@^7.7.5", "@babel/core@^7.9.0": version "7.9.0" resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.9.0.tgz#ac977b538b77e132ff706f3b8a4dbad09c03c56e" integrity sha512-kWc7L0fw1xwvI0zi8OKVBuxRVefwGOrKSQMvrQ3dW+bIIavBY3/NpXmpjMy7bQnLgwgzWQZ8TlM57YHpHNHz4w== @@ -83,7 +83,7 @@ semver "^5.4.1" source-map "^0.5.0" -"@babel/generator@^7.0.0", "@babel/generator@^7.4.0", "@babel/generator@^7.5.5", "@babel/generator@^7.9.0": +"@babel/generator@^7.0.0", "@babel/generator@^7.5.5", "@babel/generator@^7.9.0": version "7.9.4" resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.9.4.tgz#12441e90c3b3c4159cdecf312075bf1a8ce2dbce" integrity sha512-rjP8ahaDy/ouhrvCoU1E5mqaitWrxwuNGU+dy1EpaoK48jZay4MdkskKGIMHLZNewg8sAsqpGSREJwP0zH3YQA== @@ -93,6 +93,16 @@ lodash "^4.17.13" source-map "^0.5.0" +"@babel/generator@^7.9.5": + version "7.9.5" + resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.9.5.tgz#27f0917741acc41e6eaaced6d68f96c3fa9afaf9" + integrity sha512-GbNIxVB3ZJe3tLeDm1HSn2AhuD/mVcyLDpgtLXa5tplmWrJdF/elxB56XNqCuD6szyNkDi6wuoKXln3QeBmCHQ== + dependencies: + "@babel/types" "^7.9.5" + jsesc "^2.5.1" + lodash "^4.17.13" + source-map "^0.5.0" + "@babel/helper-annotate-as-pure@^7.0.0", "@babel/helper-annotate-as-pure@^7.8.3": version "7.8.3" resolved "https://registry.yarnpkg.com/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.8.3.tgz#60bc0bc657f63a0924ff9a4b4a0b24a13cf4deee" @@ -183,6 +193,15 @@ "@babel/template" "^7.8.3" "@babel/types" "^7.8.3" +"@babel/helper-function-name@^7.9.5": + version "7.9.5" + resolved "https://registry.yarnpkg.com/@babel/helper-function-name/-/helper-function-name-7.9.5.tgz#2b53820d35275120e1874a82e5aabe1376920a5c" + integrity sha512-JVcQZeXM59Cd1qanDUxv9fgJpt3NeKUaqBqUEvfmQ+BCOKq2xUgaWZW2hr0dkbyJgezYuplEoh5knmrnS68efw== + dependencies: + "@babel/helper-get-function-arity" "^7.8.3" + "@babel/template" "^7.8.3" + "@babel/types" "^7.9.5" + "@babel/helper-get-function-arity@^7.8.3": version "7.8.3" resolved "https://registry.yarnpkg.com/@babel/helper-get-function-arity/-/helper-get-function-arity-7.8.3.tgz#b894b947bd004381ce63ea1db9f08547e920abd5" @@ -284,6 +303,11 @@ resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.9.0.tgz#ad53562a7fc29b3b9a91bbf7d10397fd146346ed" integrity sha512-6G8bQKjOh+of4PV/ThDm/rRqlU7+IGoJuofpagU5GlEl29Vv0RGqqt86ZGRV8ZuSOY3o+8yXl5y782SMcG7SHw== +"@babel/helper-validator-identifier@^7.9.5": + version "7.9.5" + resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.9.5.tgz#90977a8e6fbf6b431a7dc31752eee233bf052d80" + integrity sha512-/8arLKUFq882w4tWGj9JYzRpAlZgiWUJ+dtteNTDqrRBz9Iguck9Rn3ykuBDoUwh2TO4tSAJlrxDUOXWklJe4g== + "@babel/helper-wrap-function@^7.8.3": version "7.8.3" resolved "https://registry.yarnpkg.com/@babel/helper-wrap-function/-/helper-wrap-function-7.8.3.tgz#9dbdb2bb55ef14aaa01fe8c99b629bd5352d8610" @@ -312,7 +336,7 @@ chalk "^2.0.0" js-tokens "^4.0.0" -"@babel/parser@^7.0.0", "@babel/parser@^7.1.0", "@babel/parser@^7.2.0", "@babel/parser@^7.4.3", "@babel/parser@^7.5.5", "@babel/parser@^7.8.6", "@babel/parser@^7.9.0", "@babel/parser@^7.9.3": +"@babel/parser@^7.0.0", "@babel/parser@^7.1.0", "@babel/parser@^7.2.0", "@babel/parser@^7.5.5", "@babel/parser@^7.7.5", "@babel/parser@^7.8.6", "@babel/parser@^7.9.0", "@babel/parser@^7.9.3": version "7.9.4" resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.9.4.tgz#68a35e6b0319bbc014465be43828300113f2f2e8" integrity sha512-bC49otXX6N0/VYhgOMh4gnP26E9xnDZK3TmbNpxYzzz9BQLBosQwfyOe9/cXUU3txYhTzLCbcqd5c8y/OmCjHA== @@ -1101,7 +1125,7 @@ dependencies: regenerator-runtime "^0.13.4" -"@babel/template@^7.0.0", "@babel/template@^7.4.0", "@babel/template@^7.4.4", "@babel/template@^7.8.3", "@babel/template@^7.8.6": +"@babel/template@^7.0.0", "@babel/template@^7.4.4", "@babel/template@^7.7.4", "@babel/template@^7.8.3", "@babel/template@^7.8.6": version "7.8.6" resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.8.6.tgz#86b22af15f828dfb086474f964dcc3e39c43ce2b" integrity sha512-zbMsPMy/v0PWFZEhQJ66bqjhH+z0JgMoBWuikXybgG3Gkd/3t5oQ1Rw2WQhnSrsOmsKXnZOx15tkC4qON/+JPg== @@ -1110,7 +1134,7 @@ "@babel/parser" "^7.8.6" "@babel/types" "^7.8.6" -"@babel/traverse@^7.0.0", "@babel/traverse@^7.1.0", "@babel/traverse@^7.1.6", "@babel/traverse@^7.4.3", "@babel/traverse@^7.4.5", "@babel/traverse@^7.5.5", "@babel/traverse@^7.8.3", "@babel/traverse@^7.8.6", "@babel/traverse@^7.9.0": +"@babel/traverse@^7.0.0", "@babel/traverse@^7.1.0", "@babel/traverse@^7.1.6", "@babel/traverse@^7.4.5", "@babel/traverse@^7.5.5", "@babel/traverse@^7.8.3", "@babel/traverse@^7.8.6", "@babel/traverse@^7.9.0": version "7.9.0" resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.9.0.tgz#d3882c2830e513f4fe4cec9fe76ea1cc78747892" integrity sha512-jAZQj0+kn4WTHO5dUZkZKhbFrqZE7K5LAQ5JysMnmvGij+wOdr+8lWqPeW0BcF4wFwrEXXtdGO7wcV6YPJcf3w== @@ -1125,6 +1149,21 @@ globals "^11.1.0" lodash "^4.17.13" +"@babel/traverse@^7.7.4": + version "7.9.5" + resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.9.5.tgz#6e7c56b44e2ac7011a948c21e283ddd9d9db97a2" + integrity sha512-c4gH3jsvSuGUezlP6rzSJ6jf8fYjLj3hsMZRx/nX0h+fmHN0w+ekubRrHPqnMec0meycA2nwCsJ7dC8IPem2FQ== + dependencies: + "@babel/code-frame" "^7.8.3" + "@babel/generator" "^7.9.5" + "@babel/helper-function-name" "^7.9.5" + "@babel/helper-split-export-declaration" "^7.8.3" + "@babel/parser" "^7.9.0" + "@babel/types" "^7.9.5" + debug "^4.1.0" + globals "^11.1.0" + lodash "^4.17.13" + "@babel/types@^7.0.0", "@babel/types@^7.2.0", "@babel/types@^7.3.0", "@babel/types@^7.4.0", "@babel/types@^7.4.4", "@babel/types@^7.5.5", "@babel/types@^7.8.3", "@babel/types@^7.8.6", "@babel/types@^7.9.0": version "7.9.0" resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.9.0.tgz#00b064c3df83ad32b2dbf5ff07312b15c7f1efb5" @@ -1134,6 +1173,15 @@ lodash "^4.17.13" to-fast-properties "^2.0.0" +"@babel/types@^7.9.5": + version "7.9.5" + resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.9.5.tgz#89231f82915a8a566a703b3b20133f73da6b9444" + integrity sha512-XjnvNqenk818r5zMaba+sLQjnbda31UfUURv3ei0qPQw4u+j2jMyJ5b11y8ZHYTRSI3NnInQkkkRT4fLqqPdHg== + dependencies: + "@babel/helper-validator-identifier" "^7.9.5" + lodash "^4.17.13" + to-fast-properties "^2.0.0" + "@cnakazawa/watch@^1.0.3": version "1.0.3" resolved "https://registry.yarnpkg.com/@cnakazawa/watch/-/watch-1.0.3.tgz#099139eaec7ebf07a27c1786a3ff64f39464d2ef" @@ -1172,35 +1220,34 @@ debug "^3.1.0" lodash.once "^4.1.1" -"@elastic/apm-rum-core@^4.7.0": - version "4.7.0" - resolved "https://registry.yarnpkg.com/@elastic/apm-rum-core/-/apm-rum-core-4.7.0.tgz#b00b58bf7380f2e36652e5333e3ca97608986e40" - integrity sha512-/lTZWfA3ces3qoKCx72Sc+w43lZkyktaQlbYoYO86h3tNX7tScc/7YBBHI9oxKMcXweqkKOcpnwNZFy71bb86w== +"@elastic/apm-rum-core@^5.2.0": + version "5.2.0" + resolved "https://registry.yarnpkg.com/@elastic/apm-rum-core/-/apm-rum-core-5.2.0.tgz#2ed30dc226c9b5779532ab2e6065a155587bcea4" + integrity sha512-3ti2dhrqfxjHFXgArQI/sVAG2AgZH0kB1nx+2WjLpuAh8gGS4R772M5VXcWcGQb8UW9jrANwwbW2hT2GKv+uOA== dependencies: error-stack-parser "^1.3.5" - es6-promise "^4.2.8" opentracing "^0.14.3" - uuid "^3.1.0" + promise-polyfill "^8.1.3" -"@elastic/apm-rum-react@^0.3.2": - version "0.3.2" - resolved "https://registry.yarnpkg.com/@elastic/apm-rum-react/-/apm-rum-react-0.3.2.tgz#134634643e15ebcf97b6f17b2c74a50afdbe1c64" - integrity sha512-hU1srW9noygppyrLmipulu30c+LWEie8V/dQjEqLYMx2mRZRwNIue3midYgWa6qrWqgYZhwpAtWrWcXc+AWk2Q== +"@elastic/apm-rum-react@^1.1.1": + version "1.1.1" + resolved "https://registry.yarnpkg.com/@elastic/apm-rum-react/-/apm-rum-react-1.1.1.tgz#3a2ba91efc9260da55ef6c31cce642a476eba828" + integrity sha512-ZMixw+82VbZIDBnz0dj5Fo4PZ7pnXlLjAA7XTi3AtSIEjpsZa7YIuCFzJdrgb/nOq7MOFkODkFPTiIYAM/yCqg== dependencies: - "@elastic/apm-rum" "^4.6.0" + "@elastic/apm-rum" "^5.1.1" hoist-non-react-statics "^3.3.0" -"@elastic/apm-rum@^4.6.0": - version "4.6.0" - resolved "https://registry.yarnpkg.com/@elastic/apm-rum/-/apm-rum-4.6.0.tgz#e2ac560dd4a4761c0e9b08c301418b1d4063bdd2" - integrity sha512-hsqvyTm5rT6lKgV06wvm8ID9aMsuJyw8wIOPjRwKmvzlTjayabxKTcr50lJJV8jY9OWfDkqymIqpHyCEChQAHQ== +"@elastic/apm-rum@^5.1.1": + version "5.1.1" + resolved "https://registry.yarnpkg.com/@elastic/apm-rum/-/apm-rum-5.1.1.tgz#02d29fa606e66f9a3a88f629d468b02457b1b183" + integrity sha512-A0O/0ZffcHm1taLuXyFoUmV2+aARr+9+xbmsEDIExLV0yKaNlaLl3UaZrodSOZ1ijlCEsRRS+y2i0md93YKQTA== dependencies: - "@elastic/apm-rum-core" "^4.7.0" + "@elastic/apm-rum-core" "^5.2.0" -"@elastic/charts@^18.1.1": - version "18.2.0" - resolved "https://registry.yarnpkg.com/@elastic/charts/-/charts-18.2.0.tgz#e141151b4d7ecc71c9f6f235f8ce141665c67195" - integrity sha512-OWsARaHI/4Ict/GkeKIO3a+e2c86esGw3FtSGRLPFVgzpwBXdjvjYyraGntKOIVs/NAGNVWYj5XoRRb5C6cMlQ== +"@elastic/charts@18.3.0": + version "18.3.0" + resolved "https://registry.yarnpkg.com/@elastic/charts/-/charts-18.3.0.tgz#cbdeec1860af274edc7a5f5b9dd26ec48c64bb64" + integrity sha512-4kSlSwdDRsVKVX8vRUkwxOu1IT6WIepgLnP0OZT7cFjgrC1SV/16c3YLw2NZDaVe0M/H4rpeNWW30VyrzZVhyw== dependencies: classnames "^2.2.6" d3-array "^1.2.4" @@ -1353,10 +1400,10 @@ resolved "https://registry.yarnpkg.com/@elastic/numeral/-/numeral-2.4.0.tgz#883197b7f4bf3c2dd994f53b274769ddfa2bf79a" integrity sha512-uGBKGCNghTgUZPHClji/00v+AKt5nidPTGOIbcT+lbTPVxNB6QPpPLGWtXyrg3QZAxobPM/LAZB1mAqtJeq44Q== -"@elastic/request-crypto@1.1.2": - version "1.1.2" - resolved "https://registry.yarnpkg.com/@elastic/request-crypto/-/request-crypto-1.1.2.tgz#2e323550f546f6286994126d462a9ea480a3bfb1" - integrity sha512-i73wjj1Qi8dGJIy170Z8xyJ760mFNjTbdmcp/nEczqWD0miNW6I5wZ5MNrv7M6CXn2m1wMXiT6qzDYd93Hv1Dw== +"@elastic/request-crypto@1.1.4": + version "1.1.4" + resolved "https://registry.yarnpkg.com/@elastic/request-crypto/-/request-crypto-1.1.4.tgz#2189d5fea65f7afe1de9f5fa3d0dd420e93e3124" + integrity sha512-D5CzSGKkM6BdrVB/HRRTheMsNQOcd2FMUup0O/1hIGUBE8zHh2AYbmSNSpD6LyQAgY39mGkARUi/x+SO0ccVvg== dependencies: "@elastic/node-crypto" "1.1.1" "@types/node-jose" "1.1.0" @@ -1542,6 +1589,21 @@ resolved "https://registry.yarnpkg.com/@icons/material/-/material-0.2.4.tgz#e90c9f71768b3736e76d7dd6783fc6c2afa88bc8" integrity sha512-QPcGmICAPbGLGb6F/yNf/KzKqvFx8z5qx3D1yFqVAjoFmXK35EgyW+cJ57Te3CNsmzblwtzakLGFqHPqrfb4Tw== +"@istanbuljs/load-nyc-config@^1.0.0": + version "1.0.0" + resolved "https://registry.yarnpkg.com/@istanbuljs/load-nyc-config/-/load-nyc-config-1.0.0.tgz#10602de5570baea82f8afbfa2630b24e7a8cfe5b" + integrity sha512-ZR0rq/f/E4f4XcgnDvtMWXCUJpi8eO0rssVhmztsZqLIEFA9UUP9zmpE0VxlM+kv/E1ul2I876Fwil2ayptDVg== + dependencies: + camelcase "^5.3.1" + find-up "^4.1.0" + js-yaml "^3.13.1" + resolve-from "^5.0.0" + +"@istanbuljs/schema@^0.1.2": + version "0.1.2" + resolved "https://registry.yarnpkg.com/@istanbuljs/schema/-/schema-0.1.2.tgz#26520bf09abe4a5644cd5414e37125a8954241dd" + integrity sha512-tsAQNx32a8CoFhjhijUIhI4kccIAgmGhy8LZMZgGfmXcpMbPRUqn5LWmgRttILi6yeGmBJd2xsPkFMs0PzgPCw== + "@jest/console@^24.7.1": version "24.7.1" resolved "https://registry.yarnpkg.com/@jest/console/-/console-24.7.1.tgz#32a9e42535a97aedfe037e725bd67e954b459545" @@ -2494,6 +2556,11 @@ resolved "https://registry.yarnpkg.com/@sheerun/mutationobserver-shim/-/mutationobserver-shim-0.3.2.tgz#8013f2af54a2b7d735f71560ff360d3a8176a87b" integrity sha512-vTCdPp/T/Q3oSqwHmZ5Kpa9oI7iLtGl3RQaA/NyLHikvcrPxACkkKVr/XzkSPJWXHRhKGzVvb0urJsbMlRxi1Q== +"@sindresorhus/is@^0.14.0": + version "0.14.0" + resolved "https://registry.yarnpkg.com/@sindresorhus/is/-/is-0.14.0.tgz#9fb3a3cf3132328151f353de4632e01e52102bea" + integrity sha512-9NET910DNaIPngYnLLPeg+Ogzqsi9uM4mSboU5y6p8S5DzMTVEsJZrawi+BoDNUVBa2DhJqQYUFvMDfgU062LQ== + "@sindresorhus/is@^0.7.0": version "0.7.0" resolved "https://registry.yarnpkg.com/@sindresorhus/is/-/is-0.7.0.tgz#9a06f4f137ee84d7df0460c1fdb1135ffa6c50fd" @@ -3403,6 +3470,13 @@ "@svgr/plugin-svgo" "^4.2.0" loader-utils "^1.2.3" +"@szmarczak/http-timer@^1.1.2": + version "1.1.2" + resolved "https://registry.yarnpkg.com/@szmarczak/http-timer/-/http-timer-1.1.2.tgz#b1665e2c461a2cd92f4c1bbf50d5454de0d4b421" + integrity sha512-XIB2XbzHTN6ieIjfIMV9hlVcfPU26s2vafYWQcZHWXHOxiaRZYEDKEwdl129Zyg50+foYV2jCgtrqSA6qNuNSA== + dependencies: + defer-to-connect "^1.0.1" + "@szmarczak/http-timer@^4.0.0": version "4.0.5" resolved "https://registry.yarnpkg.com/@szmarczak/http-timer/-/http-timer-4.0.5.tgz#bfbd50211e9dfa51ba07da58a14cdfd333205152" @@ -4173,7 +4247,7 @@ dependencies: "@types/node" "*" -"@types/keyv@*": +"@types/keyv@*", "@types/keyv@^3.1.1": version "3.1.1" resolved "https://registry.yarnpkg.com/@types/keyv/-/keyv-3.1.1.tgz#e45a45324fca9dab716ab1230ee249c9fb52cfa7" integrity sha512-MPtoySlAZQ37VoLaPcTHCu1RWJ4llDkULYZIzOYxlhxBqYPB0RsRlmMU0R6tahtFe27mIdkHV+551ZWV4PLmVw== @@ -4403,10 +4477,10 @@ dependencies: "@types/node" "*" -"@types/papaparse@^4.5.11": - version "4.5.11" - resolved "https://registry.yarnpkg.com/@types/papaparse/-/papaparse-4.5.11.tgz#dcd4f64da55f768c2e2cf92ccac1973c67a73890" - integrity sha512-zOw6K7YyA/NuZ2yZ8lzZFe2U3fn+vFfcRfiQp4ZJHG6y8WYWy2SYFbq6mp4yUgpIruJHBjKZtgyE0vvCoWEq+A== +"@types/papaparse@^5.0.3": + version "5.0.3" + resolved "https://registry.yarnpkg.com/@types/papaparse/-/papaparse-5.0.3.tgz#7cedc1ebc9484819af8306a8b42f9f08ca9bdb44" + integrity sha512-SgWGWnBGxl6XgjKDM2eoDg163ZFQtH6m6C2aOuaAf1T2gUB3rjaiPDDARbY9WlacRgZqieRG9imAfJaJ+5ouDA== dependencies: "@types/node" "*" @@ -4673,7 +4747,7 @@ resolved "https://registry.yarnpkg.com/@types/seedrandom/-/seedrandom-2.4.28.tgz#9ce8fa048c1e8c85cb71d7fe4d704e000226036f" integrity sha512-SMA+fUwULwK7sd/ZJicUztiPs8F1yCPwF3O23Z9uQ32ME5Ha0NmDK9+QTsYE4O2tHXChzXomSWWeIhCnoN1LqA== -"@types/selenium-webdriver@4.0.9": +"@types/selenium-webdriver@^4.0.9": version "4.0.9" resolved "https://registry.yarnpkg.com/@types/selenium-webdriver/-/selenium-webdriver-4.0.9.tgz#12621e55b2ef8f6c98bd17fe23fa720c6cba16bd" integrity sha512-HopIwBE7GUXsscmt/J0DhnFXLSmO04AfxT6b8HAprknwka7pqEWquWDMXxCjd+NUHK9MkCe1SDKKsMiNmCItbQ== @@ -4998,6 +5072,13 @@ dependencies: "@types/yargs-parser" "*" +"@types/yauzl@^2.9.1": + version "2.9.1" + resolved "https://registry.yarnpkg.com/@types/yauzl/-/yauzl-2.9.1.tgz#d10f69f9f522eef3cf98e30afb684a1e1ec923af" + integrity sha512-A1b8SU4D10uoPjwb0lnHmmu8wZhR9d+9o2PKBQT2jU5YPTKsxac6M2qGAdY7VcL+dHHhARVUDmeg0rOrcd9EjA== + dependencies: + "@types/node" "*" + "@types/zen-observable@^0.8.0": version "0.8.0" resolved "https://registry.yarnpkg.com/@types/zen-observable/-/zen-observable-0.8.0.tgz#8b63ab7f1aa5321248aad5ac890a485656dcea4d" @@ -5897,6 +5978,40 @@ anymatch@~3.1.1: normalize-path "^3.0.0" picomatch "^2.0.4" +apidoc-core@^0.11.1: + version "0.11.1" + resolved "https://registry.yarnpkg.com/apidoc-core/-/apidoc-core-0.11.1.tgz#b04a7e0292e4ac0d714b40789f1b92f414486c81" + integrity sha512-pt/ICBdFQCZTgL38Aw1XB3G9AajDU1JA5E3yoDEgg0mqbPTCkOL8AyWdysjvNtQS/kkXgSPazCZaZzZYqrPHog== + dependencies: + fs-extra "^8.1.0" + glob "^7.1.4" + iconv-lite "^0.5.0" + klaw-sync "^6.0.0" + lodash "~4.17.15" + semver "~6.3.0" + +apidoc-markdown@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/apidoc-markdown/-/apidoc-markdown-5.0.0.tgz#e2d59d7cbbaa10402b09cec3e8ec17a03a27be59" + integrity sha512-gp4I4MvtgJvZPikEd7lwn149jjnC454CanPhm5demROdHCuakY+3YtIKEgVrJOqnS2iwbeeF+u4riB9CoO11+A== + dependencies: + ejs "^3.0.1" + semver "^7.1.3" + yargs "^15.1.0" + +apidoc@^0.20.1: + version "0.20.1" + resolved "https://registry.yarnpkg.com/apidoc/-/apidoc-0.20.1.tgz#b29a2e2ae47e2df6a29e1f1527b94edf91853072" + integrity sha512-V54vkZ2lDFBiGn0qusZmHbMi4svuFBq0rjZAIe3nwYvBY7iztW78vKOyHyTr9ASaTB7EGe8hhLbpEnYAIO31TQ== + dependencies: + apidoc-core "^0.11.1" + commander "^2.20.0" + fs-extra "^8.1.0" + lodash "^4.17.15" + markdown-it "^10.0.0" + nodemon "^2.0.2" + winston "^3.2.1" + apollo-cache-control@^0.1.0: version "0.1.1" resolved "https://registry.yarnpkg.com/apollo-cache-control/-/apollo-cache-control-0.1.1.tgz#173d14ceb3eb9e7cb53de7eb8b61bee6159d4171" @@ -6109,12 +6224,12 @@ append-buffer@^1.0.2: dependencies: buffer-equal "^1.0.0" -append-transform@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/append-transform/-/append-transform-1.0.0.tgz#046a52ae582a228bd72f58acfbe2967c678759ab" - integrity sha512-P009oYkeHyU742iSZJzZZywj4QRJdnTWffaKuJQLablCZ1uz6/cW4yaRgcDaoQ+uwOxxnt0gRUcwfsNP2ri0gw== +append-transform@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/append-transform/-/append-transform-2.0.0.tgz#99d9d29c7b38391e6f428d28ce136551f0b77e12" + integrity sha512-7yeyCEurROLQJFv5Xj4lEGTy0borxepjFv1g22oAdqFu//SrAlDl1O1Nxx15SH1RoliUml6p8dwJW9jvZughhg== dependencies: - default-require-extensions "^2.0.0" + default-require-extensions "^3.0.0" aproba@^1.0.3, aproba@^1.1.1: version "1.2.0" @@ -6867,15 +6982,16 @@ babel-plugin-istanbul@^5.1.0: istanbul-lib-instrument "^3.0.0" test-exclude "^5.0.0" -babel-plugin-istanbul@^5.2.0: - version "5.2.0" - resolved "https://registry.yarnpkg.com/babel-plugin-istanbul/-/babel-plugin-istanbul-5.2.0.tgz#df4ade83d897a92df069c4d9a25cf2671293c854" - integrity sha512-5LphC0USA8t4i1zCtjbbNb6jJj/9+X6P37Qfirc/70EQ34xKlMW+a1RHGwxGI+SwWpNwZ27HqvzAobeqaXwiZw== +babel-plugin-istanbul@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/babel-plugin-istanbul/-/babel-plugin-istanbul-6.0.0.tgz#e159ccdc9af95e0b570c75b4573b7c34d671d765" + integrity sha512-AF55rZXpe7trmEylbaE1Gv54wn6rwU03aptvRoVIGP8YykoSxqdVLV1TfwflBCE/QtHmqtP8SWlTENqbK8GCSQ== dependencies: "@babel/helper-plugin-utils" "^7.0.0" - find-up "^3.0.0" - istanbul-lib-instrument "^3.3.0" - test-exclude "^5.2.3" + "@istanbuljs/load-nyc-config" "^1.0.0" + "@istanbuljs/schema" "^0.1.2" + istanbul-lib-instrument "^4.0.0" + test-exclude "^6.0.0" babel-plugin-jest-hoist@^24.9.0: version "24.9.0" @@ -7399,18 +7515,13 @@ binaryextensions@2: resolved "https://registry.yarnpkg.com/binaryextensions/-/binaryextensions-2.1.1.tgz#3209a51ca4a4ad541a3b8d3d6a6d5b83a2485935" integrity sha512-XBaoWE9RW8pPdPQNibZsW2zh8TW6gcarXp1FZPwT8Uop8ScSNldJEWf2k9l3HeTqdrEwsOsFcq74RiJECW34yA== -bindings@1, bindings@^1.5.0: +bindings@^1.5.0: version "1.5.0" resolved "https://registry.yarnpkg.com/bindings/-/bindings-1.5.0.tgz#10353c9e945334bc0511a6d90b38fbc7c9c504df" integrity sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ== dependencies: file-uri-to-path "1.0.0" -bindings@~1.2.0: - version "1.2.1" - resolved "https://registry.yarnpkg.com/bindings/-/bindings-1.2.1.tgz#14ad6113812d2d37d72e67b4cacb4bb726505f11" - integrity sha1-FK1hE4EtLTfXLme0ystLtyZQXxE= - bit-twiddle@^1.0.0: version "1.0.2" resolved "https://registry.yarnpkg.com/bit-twiddle/-/bit-twiddle-1.0.2.tgz#0c6c1fabe2b23d17173d9a61b7b7093eb9e1769e" @@ -7635,6 +7746,20 @@ boxen@^3.0.0: type-fest "^0.3.0" widest-line "^2.0.0" +boxen@^4.2.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/boxen/-/boxen-4.2.0.tgz#e411b62357d6d6d36587c8ac3d5d974daa070e64" + integrity sha512-eB4uT9RGzg2odpER62bBwSLvUeGC+WbRjjyyFhGsKnc8wp/m0+hQsMUvUe3H2V0D5vw0nBdO1hCJoZo5mKeuIQ== + dependencies: + ansi-align "^3.0.0" + camelcase "^5.3.1" + chalk "^3.0.0" + cli-boxes "^2.2.0" + string-width "^4.1.0" + term-size "^2.1.0" + type-fest "^0.8.1" + widest-line "^3.1.0" + brace-expansion@^1.1.7: version "1.1.8" resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.8.tgz#c07b211c7c952ec1f8efd51a77ef0d1d3990a292" @@ -7944,7 +8069,7 @@ buffer@^5.1.0, buffer@^5.2.0: base64-js "^1.0.2" ieee754 "^1.1.4" -builtin-modules@^1.0.0, builtin-modules@^1.1.1: +builtin-modules@^1.0.0: version "1.1.1" resolved "https://registry.yarnpkg.com/builtin-modules/-/builtin-modules-1.1.1.tgz#270f076c5a72c02f5b65a47df94c5fe3a278892f" integrity sha1-Jw8HbFpywC9bZaR9+Uxf46J4iS8= @@ -8092,10 +8217,11 @@ cache-loader@^4.1.0: schema-utils "^2.0.0" cacheable-lookup@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/cacheable-lookup/-/cacheable-lookup-2.0.0.tgz#33b1e56f17507f5cf9bb46075112d65473fb7713" - integrity sha512-s2piO6LvA7xnL1AR03wuEdSx3BZT3tIJpZ56/lcJwzO/6DTJZlTs7X3lrvPxk6d1PlDe6PrVe2TjlUIZNFglAQ== + version "2.0.1" + resolved "https://registry.yarnpkg.com/cacheable-lookup/-/cacheable-lookup-2.0.1.tgz#87be64a18b925234875e10a9bb1ebca4adce6b38" + integrity sha512-EMMbsiOTcdngM/K6gV/OxF2x0t07+vMOWxZNSCRQMjO2MY2nhZQ6OYhOOpyQrbhqsgtvKGI7hcq6xjnA92USjg== dependencies: + "@types/keyv" "^3.1.1" keyv "^4.0.0" cacheable-request@^2.1.1: @@ -8111,6 +8237,19 @@ cacheable-request@^2.1.1: normalize-url "2.0.1" responselike "1.0.2" +cacheable-request@^6.0.0: + version "6.1.0" + resolved "https://registry.yarnpkg.com/cacheable-request/-/cacheable-request-6.1.0.tgz#20ffb8bd162ba4be11e9567d823db651052ca912" + integrity sha512-Oj3cAGPCqOZX7Rz64Uny2GYAZNliQSqfbePrgAQ1wKAihYmCUnraBtJtKcGR4xz7wF+LoJC+ssFZvv5BgF9Igg== + dependencies: + clone-response "^1.0.2" + get-stream "^5.1.0" + http-cache-semantics "^4.0.0" + keyv "^3.0.0" + lowercase-keys "^2.0.0" + normalize-url "^4.1.0" + responselike "^1.0.2" + cacheable-request@^7.0.1: version "7.0.1" resolved "https://registry.yarnpkg.com/cacheable-request/-/cacheable-request-7.0.1.tgz#062031c2856232782ed694a257fa35da93942a58" @@ -8129,15 +8268,15 @@ cachedir@2.3.0: resolved "https://registry.yarnpkg.com/cachedir/-/cachedir-2.3.0.tgz#0c75892a052198f0b21c7c1804d8331edfcae0e8" integrity sha512-A+Fezp4zxnit6FanDmv9EqXNAi3vt9DWp51/71UEhXukb7QUuvtv9344h91dyAxuTLoSYJFU299qzR3tzwPAhw== -caching-transform@^3.0.2: - version "3.0.2" - resolved "https://registry.yarnpkg.com/caching-transform/-/caching-transform-3.0.2.tgz#601d46b91eca87687a281e71cef99791b0efca70" - integrity sha512-Mtgcv3lh3U0zRii/6qVgQODdPA4G3zhG+jtbCWj39RXuUFTMzH0vcdMtaJS1jPowd+It2Pqr6y3NJMQqOqCE2w== +caching-transform@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/caching-transform/-/caching-transform-4.0.0.tgz#00d297a4206d71e2163c39eaffa8157ac0651f0f" + integrity sha512-kpqOvwXnjjN44D89K5ccQC+RUrsy7jB/XLlRrx0D7/2HNcTPqzsb6XgYoErwko6QsV184CA2YgS1fxDiiDZMWA== dependencies: - hasha "^3.0.0" - make-dir "^2.0.0" - package-hash "^3.0.0" - write-file-atomic "^2.4.2" + hasha "^5.0.0" + make-dir "^3.0.0" + package-hash "^4.0.0" + write-file-atomic "^3.0.0" call-me-maybe@^1.0.1: version "1.0.1" @@ -8630,6 +8769,21 @@ chokidar@^2.0.0, chokidar@^2.1.2, chokidar@^2.1.8: optionalDependencies: fsevents "^1.2.7" +chokidar@^3.2.2: + version "3.3.1" + resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.3.1.tgz#c84e5b3d18d9a4d77558fef466b1bf16bbeb3450" + integrity sha512-4QYCEWOcK3OJrxwvyyAOxFuhpvOVCYkr33LPfFNBjAD/w3sEzWsp2BUOkI4l9bHvWioAd0rc6NlHUOEaWkTeqg== + dependencies: + anymatch "~3.1.1" + braces "~3.0.2" + glob-parent "~5.1.0" + is-binary-path "~2.1.0" + is-glob "~4.0.1" + normalize-path "~3.0.0" + readdirp "~3.3.0" + optionalDependencies: + fsevents "~2.1.2" + chownr@^1.0.1, chownr@^1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/chownr/-/chownr-1.1.1.tgz#54726b8b8fff4df053c42187e801fb4412df1494" @@ -8659,16 +8813,16 @@ chrome-trace-event@^1.0.2: dependencies: tslib "^1.9.0" -chromedriver@^80.0.1: - version "80.0.1" - resolved "https://registry.yarnpkg.com/chromedriver/-/chromedriver-80.0.1.tgz#35c1642e2d864b9e8262f291003e455b0e422917" - integrity sha512-VfRtZUpBUIjeypS+xM40+VD9g4Drv7L2VibG/4+0zX3mMx4KayN6gfKETycPfO6JwQXTLSxEr58fRcrsa8r5xQ== +chromedriver@^81.0.0: + version "81.0.0" + resolved "https://registry.yarnpkg.com/chromedriver/-/chromedriver-81.0.0.tgz#690ba333aedf2b4c4933b6590c3242d3e5f28f3c" + integrity sha512-BA++IQ7O1FzHmNpzMlOfLiSBvPZ946uuhtJjZHEIr/Gb+Ha9jiuGbHiT45l6O3XGbQ8BAwvbmdisjl4rTxro4A== dependencies: "@testim/chrome-version" "^1.0.7" axios "^0.19.2" del "^5.1.0" - extract-zip "^1.6.7" - mkdirp "^1.0.3" + extract-zip "^2.0.0" + mkdirp "^1.0.4" tcp-port-used "^1.0.1" ci-info@^1.0.0: @@ -9216,16 +9370,16 @@ commander@4.1.0: resolved "https://registry.yarnpkg.com/commander/-/commander-4.1.0.tgz#545983a0603fe425bc672d66c9e3c89c42121a83" integrity sha512-NIQrwvv9V39FHgGFm36+U9SMQzbiHvU79k+iADraJTpmrFFfx7Ds0IvDoAdZsDrknlkRk14OYoWXb57uTh7/sw== -commander@^2.12.1, commander@^2.20.0, commander@^2.7.1: - version "2.20.3" - resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.3.tgz#fd485e84c03eb4881c20722ba48035e8531aeb33" - integrity sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ== - commander@^2.13.0, commander@^2.15.1, commander@^2.16.0, commander@^2.19.0: version "2.20.0" resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.0.tgz#d58bb2b5c1ee8f87b0d340027e9e94e222c5a422" integrity sha512-7j2y+40w61zy6YC2iRNpUe/NwhNyoXrYpHMrSunaMG64nRnaf96zO/KMQR4OyN/UnE5KLyEBnKHd4aG3rskjpQ== +commander@^2.20.0, commander@^2.7.1: + version "2.20.3" + resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.3.tgz#fd485e84c03eb4881c20722ba48035e8531aeb33" + integrity sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ== + commander@^2.8.1: version "2.18.0" resolved "https://registry.yarnpkg.com/commander/-/commander-2.18.0.tgz#2bf063ddee7c7891176981a2cc798e5754bc6970" @@ -9418,6 +9572,18 @@ configstore@^3.1.2: write-file-atomic "^2.0.0" xdg-basedir "^3.0.0" +configstore@^5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/configstore/-/configstore-5.0.1.tgz#d365021b5df4b98cdd187d6a3b0e3f6a7cc5ed96" + integrity sha512-aMKprgk5YhBNyH25hj8wGt2+D52Sw1DRRIzqBwLp2Ya9mFmY8KPvvtvmna8SxVR9JMZ4kzMD68N22vlaRpkeFA== + dependencies: + dot-prop "^5.2.0" + graceful-fs "^4.1.2" + make-dir "^3.0.0" + unique-string "^2.0.0" + write-file-atomic "^3.0.0" + xdg-basedir "^4.0.0" + connect-history-api-fallback@^1.6.0: version "1.6.0" resolved "https://registry.yarnpkg.com/connect-history-api-fallback/-/connect-history-api-fallback-1.6.0.tgz#8b32089359308d111115d81cad3fceab888f97bc" @@ -9564,7 +9730,7 @@ convert-source-map@^0.3.3: resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-0.3.5.tgz#f1d802950af7dd2631a1febe0596550c86ab3190" integrity sha1-8dgClQr33SYxof6+BZZVDIarMZA= -convert-source-map@^1.5.1, convert-source-map@^1.6.0: +convert-source-map@^1.5.1: version "1.6.0" resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-1.6.0.tgz#51b537a8c43e0f04dec1993bffcdd504e758ac20" integrity sha512-eFu7XigvxdZ1ETfbgPBohgyQ/Z++C0eEhTor0qRwBw9unw+L0/6V8wkSuGgzdThkiS5lSpdptOQPD8Ak40a+7A== @@ -9768,17 +9934,6 @@ cosmiconfig@^5.2.0, cosmiconfig@^5.2.1: js-yaml "^3.13.1" parse-json "^4.0.0" -cp-file@^6.2.0: - version "6.2.0" - resolved "https://registry.yarnpkg.com/cp-file/-/cp-file-6.2.0.tgz#40d5ea4a1def2a9acdd07ba5c0b0246ef73dc10d" - integrity sha512-fmvV4caBnofhPe8kOcitBwSn2f39QLjnAnGq3gO9dfd75mUytzKNZB1hde6QHunW2Rt+OwuBOMc3i1tNElbszA== - dependencies: - graceful-fs "^4.1.2" - make-dir "^2.0.0" - nested-error-stacks "^2.0.0" - pify "^4.0.1" - safe-buffer "^5.0.1" - cp-file@^7.0.0: version "7.0.0" resolved "https://registry.yarnpkg.com/cp-file/-/cp-file-7.0.0.tgz#b9454cfd07fe3b974ab9ea0e5f29655791a9b8cd" @@ -9948,7 +10103,7 @@ cross-spawn@^3.0.0: lru-cache "^4.0.1" which "^1.2.9" -cross-spawn@^4, cross-spawn@^4.0.2: +cross-spawn@^4.0.2: version "4.0.2" resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-4.0.2.tgz#7b9247621c23adfdd3856004a823cbe397424d41" integrity sha1-e5JHYhwjrf3ThWAEqCPL45dCTUE= @@ -10027,6 +10182,11 @@ crypto-random-string@^1.0.0: resolved "https://registry.yarnpkg.com/crypto-random-string/-/crypto-random-string-1.0.0.tgz#a230f64f568310e1498009940790ec99545bca7e" integrity sha1-ojD2T1aDEOFJgAmUB5DsmVRbyn4= +crypto-random-string@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/crypto-random-string/-/crypto-random-string-2.0.0.tgz#ef2a7a966ec11083388369baa02ebead229b30d5" + integrity sha512-v1plID3y9r/lPhviJ1wrXpLeyUIGAZ2SHNYTEapm7/8A9nLPoyvVp3RK/EPFqn5kEznyWgYZNsRtYYIWbuG8KA== + cson-parser@^1.3.4: version "1.3.5" resolved "https://registry.yarnpkg.com/cson-parser/-/cson-parser-1.3.5.tgz#7ec675e039145533bf2a6a856073f1599d9c2d24" @@ -10641,7 +10801,7 @@ debug-fabulous@1.X: memoizee "0.4.X" object-assign "4.X" -debug@2, debug@2.6.9, debug@^2.0.0, debug@^2.1.0, debug@^2.1.1, debug@^2.2.0, debug@^2.3.3, debug@^2.6.0, debug@^2.6.8, debug@^2.6.9: +debug@2.6.9, debug@^2.0.0, debug@^2.1.0, debug@^2.1.1, debug@^2.2.0, debug@^2.3.3, debug@^2.6.0, debug@^2.6.8, debug@^2.6.9: version "2.6.9" resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f" integrity sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA== @@ -10847,12 +11007,12 @@ default-gateway@^4.2.0: execa "^1.0.0" ip-regex "^2.1.0" -default-require-extensions@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/default-require-extensions/-/default-require-extensions-2.0.0.tgz#f5f8fbb18a7d6d50b21f641f649ebb522cfe24f7" - integrity sha1-9fj7sYp9bVCyH2QfZJ67Uiz+JPc= +default-require-extensions@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/default-require-extensions/-/default-require-extensions-3.0.0.tgz#e03f93aac9b2b6443fc52e5e4a37b3ad9ad8df96" + integrity sha512-ek6DpXq/SCpvjhpFsLFRVtIxJCRw6fUR42lYMVZuUMK7n8eMz4Uh5clckdBjEpLhn/gEBZo7hDJnJcwdKLKQjg== dependencies: - strip-bom "^3.0.0" + strip-bom "^4.0.0" default-resolution@^2.0.0: version "2.0.0" @@ -10871,6 +11031,11 @@ defaults@^1.0.3: dependencies: clone "^1.0.2" +defer-to-connect@^1.0.1: + version "1.1.3" + resolved "https://registry.yarnpkg.com/defer-to-connect/-/defer-to-connect-1.1.3.tgz#331ae050c08dcf789f8c83a7b81f0ed94f4ac591" + integrity sha512-0ISdNousHvZT2EiFlZeZAHBUvSxmKswVCEf8hW7KWgG4a8MVEu/3Vb6uWYozkjylyCxe0JBIiRB1jV45S70WVQ== + defer-to-connect@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/defer-to-connect/-/defer-to-connect-2.0.0.tgz#83d6b199db041593ac84d781b5222308ccf4c2c1" @@ -11424,6 +11589,13 @@ dot-prop@^4.1.0, dot-prop@^4.1.1: dependencies: is-obj "^1.0.0" +dot-prop@^5.2.0: + version "5.2.0" + resolved "https://registry.yarnpkg.com/dot-prop/-/dot-prop-5.2.0.tgz#c34ecc29556dc45f1f4c22697b6f4904e0cc4fcb" + integrity sha512-uEUyaDKoSQ1M4Oq8l45hSE26SnTxL6snNnqvK/VWx5wJhmff5z0FUVJDKDanor/6w3kzE3i7XZOk+7wC0EXr1A== + dependencies: + is-obj "^2.0.0" + dotenv-defaults@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/dotenv-defaults/-/dotenv-defaults-1.0.2.tgz#441cf5f067653fca4bbdce9dd3b803f6f84c585d" @@ -11621,6 +11793,11 @@ ejs@^2.6.1: resolved "https://registry.yarnpkg.com/ejs/-/ejs-2.6.1.tgz#498ec0d495655abc6f23cd61868d926464071aa0" integrity sha512-0xy4A/twfrRCnkhfk8ErDi5DqdAsAqeGxht4xkCUrsvhhbQNs7E+4jV0CN7+NKIY0aHE72+XvqtBIXzD31ZbXQ== +ejs@^3.0.1: + version "3.0.2" + resolved "https://registry.yarnpkg.com/ejs/-/ejs-3.0.2.tgz#745b01cdcfe38c1c6a2da3bbb2d9957060a31226" + integrity sha512-IncmUpn1yN84hy2shb0POJ80FWrfGNY0cxO9f4v+/sG7qcBvAtVWUA1IdzY/8EYUmOVhoKJVdJjNd3AZcnxOjA== + elastic-apm-http-client@^9.2.0: version "9.2.1" resolved "https://registry.yarnpkg.com/elastic-apm-http-client/-/elastic-apm-http-client-9.2.1.tgz#e0e980ceb9975ff770bdbf2f5cdaac39fd70e8e6" @@ -12101,11 +12278,6 @@ es6-promise@^4.2.5: resolved "https://registry.yarnpkg.com/es6-promise/-/es6-promise-4.2.6.tgz#b685edd8258886365ea62b57d30de28fadcd974f" integrity sha512-aRVgGdnmW2OiySVPUC9e6m+plolMAJKjZnQlCwNSuK5yQ0JN61DZSO1X1Ufd1foqWRAlig0rhduTCHe7sVtK5Q== -es6-promise@^4.2.8: - version "4.2.8" - resolved "https://registry.yarnpkg.com/es6-promise/-/es6-promise-4.2.8.tgz#4eb21594c972bc40553d276e510539143db53e0a" - integrity sha512-HJDGx5daxeIvxdBxvG2cb9g4tEvwIk3i8+nhX0yGrYmZUzbkdg8QbDevheDB8gd0//uPj4c1EQua8Q+MViT0/w== - es6-promisify@^5.0.0: version "5.0.0" resolved "https://registry.yarnpkg.com/es6-promisify/-/es6-promisify-5.0.0.tgz#5109d62f3e56ea967c4b63505aef08291c8a5203" @@ -12155,6 +12327,11 @@ es6-weak-map@^2.0.1, es6-weak-map@^2.0.2: es6-iterator "^2.0.1" es6-symbol "^3.1.1" +escape-goat@^2.0.0: + version "2.1.1" + resolved "https://registry.yarnpkg.com/escape-goat/-/escape-goat-2.1.1.tgz#1b2dc77003676c457ec760b2dc68edb648188675" + integrity sha512-8/uIhbG12Csjy2JEW7D9pHbreaVaS/OpN3ycnyvElTdwM5n6GY6W6e2IPemfvGZeUMqZ9A/3GqIZMgKnBhAw/Q== + escape-html@^1.0.3, escape-html@~1.0.3: version "1.0.3" resolved "https://registry.yarnpkg.com/escape-html/-/escape-html-1.0.3.tgz#0258eae4d3d0c0974de1c169188ef0051d1d1988" @@ -13098,7 +13275,7 @@ extract-zip@1.6.7: mkdirp "0.5.1" yauzl "2.4.1" -extract-zip@^1.6.6, extract-zip@^1.6.7, extract-zip@^1.7.0: +extract-zip@^1.6.6, extract-zip@^1.7.0: version "1.7.0" resolved "https://registry.yarnpkg.com/extract-zip/-/extract-zip-1.7.0.tgz#556cc3ae9df7f452c493a0cfb51cc30277940927" integrity sha512-xoh5G1W/PB0/27lXgMQyIhP5DSY/LhoCsOyZgb+6iMmRtCwVBo55uKaMoEYrDCKQhWvqEip5ZPKAc6eFNyf/MA== @@ -13108,6 +13285,17 @@ extract-zip@^1.6.6, extract-zip@^1.6.7, extract-zip@^1.7.0: mkdirp "^0.5.4" yauzl "^2.10.0" +extract-zip@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/extract-zip/-/extract-zip-2.0.0.tgz#f53b71d44f4ff5a4527a2259ade000fb8b303492" + integrity sha512-i42GQ498yibjdvIhivUsRslx608whtGoFIhF26Z7O4MYncBxp8CwalOs1lnHy21A9sIohWO2+uiE4SRtC9JXDg== + dependencies: + debug "^4.1.1" + get-stream "^5.1.0" + yauzl "^2.10.0" + optionalDependencies: + "@types/yauzl" "^2.9.1" + extsprintf@1.3.0: version "1.3.0" resolved "https://registry.yarnpkg.com/extsprintf/-/extsprintf-1.3.0.tgz#96918440e3041a7a414f8c52e3c574eb3c3e1e05" @@ -13317,17 +13505,6 @@ fetch-mock@^7.3.9: path-to-regexp "^2.2.1" whatwg-url "^6.5.0" -ffi@^2.0.0: - version "2.3.0" - resolved "https://registry.yarnpkg.com/ffi/-/ffi-2.3.0.tgz#fa1a7b3d85c0fa8c83d96947a64b5192bc47f858" - integrity sha512-vkPA9Hf9CVuQ5HeMZykYvrZF2QNJ/iKGLkyDkisBnoOOFeFXZQhUPxBARPBIZMJVulvBI2R+jgofW03gyPpJcQ== - dependencies: - bindings "~1.2.0" - debug "2" - nan "2" - ref "1" - ref-struct "1" - figgy-pudding@^3.5.1: version "3.5.1" resolved "https://registry.yarnpkg.com/figgy-pudding/-/figgy-pudding-3.5.1.tgz#862470112901c727a0e495a80744bd5baa1d6790" @@ -13842,13 +14019,13 @@ foreachasync@^3.0.0: resolved "https://registry.yarnpkg.com/foreachasync/-/foreachasync-3.0.0.tgz#5502987dc8714be3392097f32e0071c9dee07cf6" integrity sha1-VQKYfchxS+M5IJfzLgBxyd7gfPY= -foreground-child@^1.5.6: - version "1.5.6" - resolved "https://registry.yarnpkg.com/foreground-child/-/foreground-child-1.5.6.tgz#4fd71ad2dfde96789b980a5c0a295937cb2f5ce9" - integrity sha1-T9ca0t/elnibmApcCilZN8svXOk= +foreground-child@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/foreground-child/-/foreground-child-2.0.0.tgz#71b32800c9f15aa8f2f83f4a6bd9bff35d861a53" + integrity sha512-dCIq9FpEcyQyXKCkyzmlPTFNgrCzPudOe+mhvJU5zAtlBnGVy2yKxtfsxK2tQBThwq225jcvBjpw1Gr40uzZCA== dependencies: - cross-spawn "^4" - signal-exit "^3.0.0" + cross-spawn "^7.0.0" + signal-exit "^3.0.2" forever-agent@~0.6.1: version "0.6.1" @@ -13954,6 +14131,11 @@ from2@^2.1.0, from2@^2.1.1, from2@^2.3.0: inherits "^2.0.1" readable-stream "^2.0.0" +fromentries@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/fromentries/-/fromentries-1.2.0.tgz#e6aa06f240d6267f913cea422075ef88b63e7897" + integrity sha512-33X7H/wdfO99GdRLLgkjUrD4geAFdq/Uv0kl3HD4da6HDixd2GUg8Mw7dahLCV9r/EARkmtYBB6Tch4EEokFTQ== + front-matter@2.1.2: version "2.1.2" resolved "https://registry.yarnpkg.com/front-matter/-/front-matter-2.1.2.tgz#f75983b9f2f413be658c93dfd7bd8ce4078f5cdb" @@ -13978,7 +14160,7 @@ fs-exists-sync@^0.1.0: resolved "https://registry.yarnpkg.com/fs-exists-sync/-/fs-exists-sync-0.1.0.tgz#982d6893af918e72d08dec9e8673ff2b5a8d6add" integrity sha1-mC1ok6+RjnLQjeyehnP/K1qNat0= -fs-extra@8.1.0, fs-extra@^8.0.1: +fs-extra@8.1.0, fs-extra@^8.0.1, fs-extra@^8.1.0: version "8.1.0" resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-8.1.0.tgz#49d43c45a88cd9677668cb7be1b46efdb8d2e1c0" integrity sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g== @@ -14075,7 +14257,7 @@ fsevents@^1.2.7: bindings "^1.5.0" nan "^2.12.1" -fsevents@~2.1.0, fsevents@~2.1.1: +fsevents@~2.1.0, fsevents@~2.1.1, fsevents@~2.1.2: version "2.1.2" resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.1.2.tgz#4c0a1fb34bc68e543b4b82a9ec392bfbda840805" integrity sha512-R4wDiBwZ0KzpgOWetKDug1FZcYhqYnUYKtfZYt4mD5SBz76q0KR4Q9o7GIPamsVPGmW3EYPPJ0dOOjvx32ldZA== @@ -14307,7 +14489,7 @@ get-stream@^2.2.0: object-assign "^4.0.1" pinkie-promise "^2.0.0" -get-stream@^4.0.0: +get-stream@^4.0.0, get-stream@^4.1.0: version "4.1.0" resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-4.1.0.tgz#c1b255575f3dc21d59bfc79cd3d2b46b1c3a54b5" integrity sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w== @@ -14561,6 +14743,13 @@ global-dirs@^0.1.0: dependencies: ini "^1.3.4" +global-dirs@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/global-dirs/-/global-dirs-2.0.1.tgz#acdf3bb6685bcd55cb35e8a052266569e9469201" + integrity sha512-5HqUqdhkEovj2Of/ms3IeS/EekcO54ytHRLV4PEY2rhRwrHXLQjeVEES0Lhka0xwNDtGYn58wyC4s5+MHsOO6A== + dependencies: + ini "^1.3.5" + global-modules@2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/global-modules/-/global-modules-2.0.0.tgz#997605ad2345f27f51539bea26574421215c7780" @@ -14805,10 +14994,10 @@ got@5.6.0: unzip-response "^1.0.0" url-parse-lax "^1.0.0" -got@^10.6.0: - version "10.6.0" - resolved "https://registry.yarnpkg.com/got/-/got-10.6.0.tgz#ac3876261a4d8e5fc4f81186f79955ce7b0501dc" - integrity sha512-3LIdJNTdCFbbJc+h/EH0V5lpNpbJ6Bfwykk21lcQvQsEcrzdi/ltCyQehFHLzJ/ka0UMH4Slg0hkYvAZN9qUDg== +got@^10.7.0: + version "10.7.0" + resolved "https://registry.yarnpkg.com/got/-/got-10.7.0.tgz#62889dbcd6cca32cd6a154cc2d0c6895121d091f" + integrity sha512-aWTDeNw9g+XqEZNcTjMMZSy7B7yE9toWOFYip7ofFTLleJhvZwUxxTxkTpKvF+p1SAA4VHmuEy7PiHTHyq8tJg== dependencies: "@sindresorhus/is" "^2.0.0" "@szmarczak/http-timer" "^4.0.0" @@ -14902,6 +15091,23 @@ got@^8.3.1, got@^8.3.2: url-parse-lax "^3.0.0" url-to-options "^1.0.1" +got@^9.6.0: + version "9.6.0" + resolved "https://registry.yarnpkg.com/got/-/got-9.6.0.tgz#edf45e7d67f99545705de1f7bbeeeb121765ed85" + integrity sha512-R7eWptXuGYxwijs0eV+v3o6+XH1IqVK8dJOEecQfTmkncw9AV4dcw/Dhxi8MdlqPthxxpZyizMzyg8RTmEsG+Q== + dependencies: + "@sindresorhus/is" "^0.14.0" + "@szmarczak/http-timer" "^1.1.2" + cacheable-request "^6.0.0" + decompress-response "^3.3.0" + duplexer3 "^0.1.4" + get-stream "^4.1.0" + lowercase-keys "^1.0.1" + mimic-response "^1.0.1" + p-cancelable "^1.0.0" + to-readable-stream "^1.0.0" + url-parse-lax "^3.0.0" + graceful-fs@4.X, graceful-fs@^4.0.0, graceful-fs@^4.1.10, graceful-fs@^4.1.11, graceful-fs@^4.1.4: version "4.1.11" resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.1.11.tgz#0e8bdfe4d1ddb8854d64e04ea7c00e2a026e5658" @@ -15606,6 +15812,11 @@ has-values@^1.0.0: is-number "^3.0.0" kind-of "^4.0.0" +has-yarn@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/has-yarn/-/has-yarn-2.1.0.tgz#137e11354a7b5bf11aa5cb649cf0c6f3ff2b2e77" + integrity sha512-UqBRqi4ju7T+TqGNdqAO0PaSVGsDGJUBQvk9eUWNGRY1CFGDzYhLWoM7JQEemnlvVcv/YEmc2wNW8BC24EnUsw== + has@^1.0.1, has@^1.0.3, has@~1.0.3: version "1.0.3" resolved "https://registry.yarnpkg.com/has/-/has-1.0.3.tgz#722d7cbfc1f6aa8241f16dd814e011e1f41e8796" @@ -15636,12 +15847,13 @@ hash.js@^1.0.0, hash.js@^1.0.3: inherits "^2.0.3" minimalistic-assert "^1.0.0" -hasha@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/hasha/-/hasha-3.0.0.tgz#52a32fab8569d41ca69a61ff1a214f8eb7c8bd39" - integrity sha1-UqMvq4Vp1BymmmH/GiFPjrfIvTk= +hasha@^5.0.0: + version "5.2.0" + resolved "https://registry.yarnpkg.com/hasha/-/hasha-5.2.0.tgz#33094d1f69c40a4a6ac7be53d5fe3ff95a269e0c" + integrity sha512-2W+jKdQbAdSIrggA8Q35Br8qKadTrqCTC8+XZvBWepKDK6m9XkX6Iz1a2yh2KP01kzAR/dpuMeUnocoLYDcskw== dependencies: - is-stream "^1.0.1" + is-stream "^2.0.0" + type-fest "^0.8.0" hast-util-from-parse5@^5.0.0: version "5.0.0" @@ -15845,6 +16057,11 @@ html-entities@^1.2.0, html-entities@^1.2.1: resolved "https://registry.yarnpkg.com/html-entities/-/html-entities-1.2.1.tgz#0df29351f0721163515dfb9e5543e5f6eed5162f" integrity sha1-DfKTUfByEWNRXfueVUPl9u7VFi8= +html-escaper@^2.0.0: + version "2.0.2" + resolved "https://registry.yarnpkg.com/html-escaper/-/html-escaper-2.0.2.tgz#dfd60027da36a36dfcbe236262c00a5822681453" + integrity sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg== + html-loader@^0.5.5: version "0.5.5" resolved "https://registry.yarnpkg.com/html-loader/-/html-loader-0.5.5.tgz#6356dbeb0c49756d8ebd5ca327f16ff06ab5faea" @@ -16190,11 +16407,21 @@ ieee754@^1.1.12, ieee754@^1.1.4: resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.1.13.tgz#ec168558e95aa181fd87d37f55c32bbcb6708b84" integrity sha512-4vf7I2LYV/HaWerSo3XmlMkp5eZ83i+/CDluXi/IGTs/O1sejBNhTtnxzmRZfvOUqj7lZjqHkeTvpgSFDlWZTg== +if-async@^3.7.4: + version "3.7.4" + resolved "https://registry.yarnpkg.com/if-async/-/if-async-3.7.4.tgz#55868deb0093d3c67bf7166e745353fb9bcb21a2" + integrity sha1-VYaN6wCT08Z79xZudFNT+5vLIaI= + iferr@^0.1.5: version "0.1.5" resolved "https://registry.yarnpkg.com/iferr/-/iferr-0.1.5.tgz#c60eed69e6d8fdb6b3104a1fcbca1c192dc5b501" integrity sha1-xg7taebY/bazEEofy8ocGS3FtQE= +ignore-by-default@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/ignore-by-default/-/ignore-by-default-1.0.1.tgz#48ca6d72f6c6a3af00a9ad4ae6876be3889e2b09" + integrity sha1-SMptcvbGo68Aqa1K5odr44ieKwk= + ignore@^3.1.2, ignore@^3.3.5: version "3.3.10" resolved "https://registry.yarnpkg.com/ignore/-/ignore-3.3.10.tgz#0a97fb876986e8081c631160f8f9f389157f0043" @@ -17015,6 +17242,14 @@ is-installed-globally@0.1.0, is-installed-globally@^0.1.0: global-dirs "^0.1.0" is-path-inside "^1.0.0" +is-installed-globally@^0.3.1: + version "0.3.2" + resolved "https://registry.yarnpkg.com/is-installed-globally/-/is-installed-globally-0.3.2.tgz#fd3efa79ee670d1187233182d5b0a1dd00313141" + integrity sha512-wZ8x1js7Ia0kecP/CHM/3ABkAmujX7WPvQk6uu3Fly/Mk44pySulQpnHG46OMjHGXApINnV4QhY3SWnECO2z5g== + dependencies: + global-dirs "^2.0.1" + is-path-inside "^3.0.1" + is-integer@^1.0.6: version "1.0.7" resolved "https://registry.yarnpkg.com/is-integer/-/is-integer-1.0.7.tgz#6bde81aacddf78b659b6629d629cadc51a886d5c" @@ -17085,6 +17320,11 @@ is-npm@^1.0.0: resolved "https://registry.yarnpkg.com/is-npm/-/is-npm-1.0.0.tgz#f2fb63a65e4905b406c86072765a1a4dc793b9f4" integrity sha1-8vtjpl5JBbQGyGBydloaTceTufQ= +is-npm@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/is-npm/-/is-npm-4.0.0.tgz#c90dd8380696df87a7a6d823c20d0b12bbe3c84d" + integrity sha512-96ECIfh9xtDDlPylNPXhzjsykHsMJZ18ASpaWzQyBr4YRTcVjUvzaHayDAES2oU/3KpljhHUjtSRNiDwi0F0ig== + is-number-object@^1.0.4: version "1.0.4" resolved "https://registry.yarnpkg.com/is-number-object/-/is-number-object-1.0.4.tgz#36ac95e741cf18b283fc1ddf5e83da798e3ec197" @@ -17117,6 +17357,11 @@ is-obj@^1.0.0, is-obj@^1.0.1: resolved "https://registry.yarnpkg.com/is-obj/-/is-obj-1.0.1.tgz#3e4729ac1f5fde025cd7d83a896dab9f4f67db0f" integrity sha1-PkcprB9f3gJc19g6iW2rn09n2w8= +is-obj@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/is-obj/-/is-obj-2.0.0.tgz#473fb05d973705e3fd9620545018ca8e22ef4982" + integrity sha512-drqDG3cbczxxEJRoOXcOjtdp1J/lyp1mNn0xaznRs8+muBhgQcrnbspox5X5fOw0HnMnbfDzvnEMEtqDEJEo8w== + is-object@^1.0.1, is-object@~1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/is-object/-/is-object-1.0.1.tgz#8952688c5ec2ffd6b03ecc85e769e02903083470" @@ -17320,7 +17565,7 @@ is-symbol@^1.0.2: dependencies: has-symbols "^1.0.0" -is-typedarray@~1.0.0: +is-typedarray@^1.0.0, is-typedarray@~1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/is-typedarray/-/is-typedarray-1.0.0.tgz#e479c80858df0c1b11ddda6940f96011fcda4a9a" integrity sha1-5HnICFjfDBsR3dppQPlgEfzaSpo= @@ -17381,6 +17626,11 @@ is-wsl@^1.1.0: resolved "https://registry.yarnpkg.com/is-wsl/-/is-wsl-1.1.0.tgz#1f16e4aa22b04d1336b66188a66af3c600c3a66d" integrity sha1-HxbkqiKwTRM2tmGIpmrzxgDDpm0= +is-yarn-global@^0.3.0: + version "0.3.0" + resolved "https://registry.yarnpkg.com/is-yarn-global/-/is-yarn-global-0.3.0.tgz#d502d3382590ea3004893746754c89139973e232" + integrity sha512-VjSeb/lHmkoyd8ryPVIKvOCn4D1koMqY+vqyjjUfc3xyKtP4dYOxM44sZrnqQSzSds3xyOrUTLTC9LVCVgLngw== + is2@2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/is2/-/is2-2.0.1.tgz#8ac355644840921ce435d94f05d3a94634d3481a" @@ -17482,17 +17732,17 @@ istanbul-lib-coverage@^2.0.2, istanbul-lib-coverage@^2.0.3: resolved "https://registry.yarnpkg.com/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.3.tgz#0b891e5ad42312c2b9488554f603795f9a2211ba" integrity sha512-dKWuzRGCs4G+67VfW9pBFFz2Jpi4vSp/k7zBcJ888ofV5Mi1g5CUML5GvMvV6u9Cjybftu+E8Cgp+k0dI1E5lw== -istanbul-lib-coverage@^2.0.5: - version "2.0.5" - resolved "https://registry.yarnpkg.com/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.5.tgz#675f0ab69503fad4b1d849f736baaca803344f49" - integrity sha512-8aXznuEPCJvGnMSRft4udDRDtb1V3pkQkMMI5LI+6HuQz5oQ4J2UFn1H82raA3qJtyOLkkwVqICBQkjnGtn5mA== +istanbul-lib-coverage@^3.0.0, istanbul-lib-coverage@^3.0.0-alpha.1: + version "3.0.0" + resolved "https://registry.yarnpkg.com/istanbul-lib-coverage/-/istanbul-lib-coverage-3.0.0.tgz#f5944a37c70b550b02a78a5c3b2055b280cec8ec" + integrity sha512-UiUIqxMgRDET6eR+o5HbfRYP1l0hqkWOs7vNxC/mggutCMUIhWMm8gAHb8tHlyfD3/l6rlgNA5cKdDzEAf6hEg== -istanbul-lib-hook@^2.0.7: - version "2.0.7" - resolved "https://registry.yarnpkg.com/istanbul-lib-hook/-/istanbul-lib-hook-2.0.7.tgz#c95695f383d4f8f60df1f04252a9550e15b5b133" - integrity sha512-vrRztU9VRRFDyC+aklfLoeXyNdTfga2EI3udDGn4cZ6fpSXpHLV9X6CHvfoMCPtggg8zvDDmC4b9xfu0z6/llA== +istanbul-lib-hook@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/istanbul-lib-hook/-/istanbul-lib-hook-3.0.0.tgz#8f84c9434888cc6b1d0a9d7092a76d239ebf0cc6" + integrity sha512-Pt/uge1Q9s+5VAZ+pCo16TYMWPBIl+oaNIjgLQxcX0itS6ueeaA+pEfThZpH8WxhFgCiEb8sAJY6MdUKgiIWaQ== dependencies: - append-transform "^1.0.0" + append-transform "^2.0.0" istanbul-lib-instrument@^1.7.3: version "1.10.2" @@ -17520,18 +17770,31 @@ istanbul-lib-instrument@^3.0.0, istanbul-lib-instrument@^3.0.1: istanbul-lib-coverage "^2.0.3" semver "^5.5.0" -istanbul-lib-instrument@^3.3.0: - version "3.3.0" - resolved "https://registry.yarnpkg.com/istanbul-lib-instrument/-/istanbul-lib-instrument-3.3.0.tgz#a5f63d91f0bbc0c3e479ef4c5de027335ec6d630" - integrity sha512-5nnIN4vo5xQZHdXno/YDXJ0G+I3dAm4XgzfSVTPLQpj/zAV2dV6Juy0yaf10/zrJOJeHoN3fraFe+XRq2bFVZA== +istanbul-lib-instrument@^4.0.0: + version "4.0.1" + resolved "https://registry.yarnpkg.com/istanbul-lib-instrument/-/istanbul-lib-instrument-4.0.1.tgz#61f13ac2c96cfefb076fe7131156cc05907874e6" + integrity sha512-imIchxnodll7pvQBYOqUu88EufLCU56LMeFPZZM/fJZ1irYcYdqroaV+ACK1Ila8ls09iEYArp+nqyC6lW1Vfg== + dependencies: + "@babel/core" "^7.7.5" + "@babel/parser" "^7.7.5" + "@babel/template" "^7.7.4" + "@babel/traverse" "^7.7.4" + "@istanbuljs/schema" "^0.1.2" + istanbul-lib-coverage "^3.0.0" + semver "^6.3.0" + +istanbul-lib-processinfo@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/istanbul-lib-processinfo/-/istanbul-lib-processinfo-2.0.2.tgz#e1426514662244b2f25df728e8fd1ba35fe53b9c" + integrity sha512-kOwpa7z9hme+IBPZMzQ5vdQj8srYgAtaRqeI48NGmAQ+/5yKiHLV0QbYqQpxsdEF0+w14SoB8YbnHKcXE2KnYw== dependencies: - "@babel/generator" "^7.4.0" - "@babel/parser" "^7.4.3" - "@babel/template" "^7.4.0" - "@babel/traverse" "^7.4.3" - "@babel/types" "^7.4.0" - istanbul-lib-coverage "^2.0.5" - semver "^6.0.0" + archy "^1.0.0" + cross-spawn "^7.0.0" + istanbul-lib-coverage "^3.0.0-alpha.1" + make-dir "^3.0.0" + p-map "^3.0.0" + rimraf "^3.0.0" + uuid "^3.3.3" istanbul-lib-report@^2.0.4: version "2.0.4" @@ -17542,14 +17805,14 @@ istanbul-lib-report@^2.0.4: make-dir "^1.3.0" supports-color "^6.0.0" -istanbul-lib-report@^2.0.8: - version "2.0.8" - resolved "https://registry.yarnpkg.com/istanbul-lib-report/-/istanbul-lib-report-2.0.8.tgz#5a8113cd746d43c4889eba36ab10e7d50c9b4f33" - integrity sha512-fHBeG573EIihhAblwgxrSenp0Dby6tJMFR/HvlerBsrCTD5bkUuoNtn3gVh29ZCS824cGGBPn7Sg7cNk+2xUsQ== +istanbul-lib-report@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/istanbul-lib-report/-/istanbul-lib-report-3.0.0.tgz#7518fe52ea44de372f460a76b5ecda9ffb73d8a6" + integrity sha512-wcdi+uAKzfiGT2abPpKZ0hSU1rGQjUQnLvtY5MpQ7QCTahD3VODhcu4wcfY1YtkGaDD5yuydOLINXsfbus9ROw== dependencies: - istanbul-lib-coverage "^2.0.5" - make-dir "^2.1.0" - supports-color "^6.1.0" + istanbul-lib-coverage "^3.0.0" + make-dir "^3.0.0" + supports-color "^7.1.0" istanbul-lib-source-maps@^3.0.1: version "3.0.2" @@ -17562,24 +17825,30 @@ istanbul-lib-source-maps@^3.0.1: rimraf "^2.6.2" source-map "^0.6.1" -istanbul-lib-source-maps@^3.0.6: - version "3.0.6" - resolved "https://registry.yarnpkg.com/istanbul-lib-source-maps/-/istanbul-lib-source-maps-3.0.6.tgz#284997c48211752ec486253da97e3879defba8c8" - integrity sha512-R47KzMtDJH6X4/YW9XTx+jrLnZnscW4VpNN+1PViSYTejLVPWv7oov+Duf8YQSPyVRUvueQqz1TcsC6mooZTXw== +istanbul-lib-source-maps@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/istanbul-lib-source-maps/-/istanbul-lib-source-maps-4.0.0.tgz#75743ce6d96bb86dc7ee4352cf6366a23f0b1ad9" + integrity sha512-c16LpFRkR8vQXyHZ5nLpY35JZtzj1PQY1iZmesUbf1FZHbIupcWfjgOXBY9YHkLEQ6puz1u4Dgj6qmU/DisrZg== dependencies: debug "^4.1.1" - istanbul-lib-coverage "^2.0.5" - make-dir "^2.1.0" - rimraf "^2.6.3" + istanbul-lib-coverage "^3.0.0" source-map "^0.6.1" -istanbul-reports@^2.2.4, istanbul-reports@^2.2.6: +istanbul-reports@^2.2.6: version "2.2.6" resolved "https://registry.yarnpkg.com/istanbul-reports/-/istanbul-reports-2.2.6.tgz#7b4f2660d82b29303a8fe6091f8ca4bf058da1af" integrity sha512-SKi4rnMyLBKe0Jy2uUdx28h8oG7ph2PPuQPvIAh31d+Ci+lSiEu4C+h3oBPuJ9+mPKhOyW0M8gY4U5NM1WLeXA== dependencies: handlebars "^4.1.2" +istanbul-reports@^3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/istanbul-reports/-/istanbul-reports-3.0.2.tgz#d593210e5000683750cb09fc0644e4b6e27fd53b" + integrity sha512-9tZvz7AiR3PEDNGiV9vIouQ/EAcqMXFmkcA1CDFTwOB98OZVDL0PH9glHotf5Ugp6GCOTypfzGWI/OqjWNCRUw== + dependencies: + html-escaper "^2.0.0" + istanbul-lib-report "^3.0.0" + istanbul@^0.4.0: version "0.4.5" resolved "https://registry.yarnpkg.com/istanbul/-/istanbul-0.4.5.tgz#65c7d73d4c4da84d4f3ac310b918fb0b8033733b" @@ -18485,9 +18754,9 @@ jsx-to-string@^1.4.0: react "^0.14.0" jszip@^3.2.2: - version "3.2.2" - resolved "https://registry.yarnpkg.com/jszip/-/jszip-3.2.2.tgz#b143816df7e106a9597a94c77493385adca5bd1d" - integrity sha512-NmKajvAFQpbg3taXQXr/ccS2wcucR1AZ+NtyWp2Nq7HHVsXhcJFR8p0Baf32C2yVvBylFWVeKf+WI2AnvlPhpA== + version "3.3.0" + resolved "https://registry.yarnpkg.com/jszip/-/jszip-3.3.0.tgz#29d72c21a54990fa885b11fc843db320640d5271" + integrity sha512-EJ9k766htB1ZWnsV5ZMDkKLgA+201r/ouFF8R2OigVjVdcm2rurcBrrdXaeqBJbqnUVMko512PYmlncBKE1Huw== dependencies: lie "~3.3.0" pako "~1.0.2" @@ -18660,6 +18929,13 @@ keyv@3.0.0: dependencies: json-buffer "3.0.0" +keyv@^3.0.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/keyv/-/keyv-3.1.0.tgz#ecc228486f69991e49e9476485a5be1e8fc5c4d9" + integrity sha512-9ykJ/46SN/9KPM/sichzQ7OvXyGDYKGTaDlKMGCAlg2UK8KRy4jb0d8sFc+0Tt0YYnThq8X2RZgCg74RPxgcVA== + dependencies: + json-buffer "3.0.0" + keyv@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/keyv/-/keyv-4.0.0.tgz#2d1dab694926b2d427e4c74804a10850be44c12f" @@ -18703,6 +18979,13 @@ kind-of@^6.0.0, kind-of@^6.0.2: resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-6.0.3.tgz#07c05034a6c349fa06e24fa35aa76db4580ce4dd" integrity sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw== +klaw-sync@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/klaw-sync/-/klaw-sync-6.0.0.tgz#1fd2cfd56ebb6250181114f0a581167099c2b28c" + integrity sha512-nIeuVSzdCCs6TDPTqI8w1Yre34sSq7AkZ4B3sfOBbI2CgVSB4Du4aLQijFU2+lhAFCwt9+42Hel6lQNIv6AntQ== + dependencies: + graceful-fs "^4.1.11" + klaw@^1.0.0: version "1.3.1" resolved "https://registry.yarnpkg.com/klaw/-/klaw-1.3.1.tgz#4088433b46b3b1ba259d78785d8e96f73ba02439" @@ -18763,6 +19046,13 @@ latest-version@^3.0.0, latest-version@^3.1.0: dependencies: package-json "^4.0.0" +latest-version@^5.0.0: + version "5.1.0" + resolved "https://registry.yarnpkg.com/latest-version/-/latest-version-5.1.0.tgz#119dfe908fe38d15dfa43ecd13fa12ec8832face" + integrity sha512-weT+r0kTkRQdCdYCNtkMwWXQTMEswKrFBkm4ckQOMVhhqhIMI1UT2hMj+1iigIhgSZm5gTmrRXBNoGUgaTY1xA== + dependencies: + package-json "^6.3.0" + lazy-ass@1.6.0: version "1.6.0" resolved "https://registry.yarnpkg.com/lazy-ass/-/lazy-ass-1.6.0.tgz#7999655e8646c17f089fdd187d150d3324d54513" @@ -19630,7 +19920,7 @@ lowercase-keys@1.0.0: resolved "https://registry.yarnpkg.com/lowercase-keys/-/lowercase-keys-1.0.0.tgz#4e3366b39e7f5457e35f1324bdf6f88d0bfc7306" integrity sha1-TjNms55/VFfjXxMkvfb4jQv8cwY= -lowercase-keys@^1.0.0: +lowercase-keys@^1.0.0, lowercase-keys@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/lowercase-keys/-/lowercase-keys-1.0.1.tgz#6f9e30b47084d971a7c820ff15a6c5167b74c26f" integrity sha512-G2Lj61tXDnVFFOi8VZds+SoQjtQC3dgokKdDG2mTm1tx4m50NUHBOZSBwQQHyy0V12A0JTG4icfZQH+xPyh8VA== @@ -20121,13 +20411,6 @@ merge-source-map@1.0.4: dependencies: source-map "^0.5.6" -merge-source-map@^1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/merge-source-map/-/merge-source-map-1.1.0.tgz#2fdde7e6020939f70906a68f2d7ae685e4c8c646" - integrity sha512-Qkcp7P2ygktpMPh2mCQZaf3jhN6D3Z/qVZHSdWvQ+2Ef5HgRAPBO57A77+ENm0CPx2+1Ce/MYKi3ymqdfuqibw== - dependencies: - source-map "^0.6.1" - merge-stream@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/merge-stream/-/merge-stream-1.0.1.tgz#4041202d508a342ba00174008df0c251b8c135e1" @@ -20290,7 +20573,7 @@ mimic-fn@^2.1.0: resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-2.1.0.tgz#7ed2c2ccccaf84d3ffcb7a69b57711fc2083401b" integrity sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg== -mimic-response@^1.0.0: +mimic-response@^1.0.0, mimic-response@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/mimic-response/-/mimic-response-1.0.1.tgz#4923538878eef42063cb8a3e3b0798781487ab1b" integrity sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ== @@ -20546,10 +20829,10 @@ mkdirp@0.5.x, "mkdirp@>=0.5 0", mkdirp@^0.5.0, mkdirp@^0.5.1, mkdirp@^0.5.4, mkd dependencies: minimist "^1.2.5" -mkdirp@^1.0.3: - version "1.0.3" - resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-1.0.3.tgz#4cf2e30ad45959dddea53ad97d518b6c8205e1ea" - integrity sha512-6uCP4Qc0sWsgMLy1EOqqS/3rjDHOEnsStVr/4vtAIK2Y5i2kA7lFFejYrpIyiN9w0pYf4ckeCYT9f1r1P9KX5g== +mkdirp@^1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-1.0.4.tgz#3eb5ed62622756d79a5f0e2a221dfebad75c2f7e" + integrity sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw== mocha-junit-reporter@^1.23.1: version "1.23.1" @@ -20733,18 +21016,16 @@ move-concurrently@^1.0.1: rimraf "^2.5.4" run-queue "^1.0.3" -ms-chromium-edge-driver@^0.2.0: - version "0.2.0" - resolved "https://registry.yarnpkg.com/ms-chromium-edge-driver/-/ms-chromium-edge-driver-0.2.0.tgz#0e0c6fd9fd1d1d36db97b2b3d7e9d4ba4d2de456" - integrity sha512-RkDsBPnMLjRna7q4LlvtLb+CHPei9gZapnlxm3ayWKk3Ab6HmDsz/17xG2eyqkKX5UcKeo04YlLZ345tO7OolA== +ms-chromium-edge-driver@^0.2.3: + version "0.2.3" + resolved "https://registry.yarnpkg.com/ms-chromium-edge-driver/-/ms-chromium-edge-driver-0.2.3.tgz#74effa9280c112a447d8dd4d4c1589fce398a5b6" + integrity sha512-dgGxRdYyz69yhAdJk4BGFY4o5TnKe+LOceTnRQMIl5Qww1pL+1meUuoAIAPVRd5V7kB7ZfgYQNxtxQj/fVUmUA== dependencies: - extract-zip "^1.6.7" - got "^10.6.0" - lodash "4.17.15" - tslint "^6.1.0" - tslint-config-prettier "^1.18.0" + extract-zip "^2.0.0" + got "^10.7.0" + lodash "^4.17.15" + regedit "^3.0.3" util "^0.12.2" - windows-registry "^0.1.5" ms@2.0.0: version "2.0.0" @@ -20858,7 +21139,7 @@ mute-stream@0.0.8: resolved "https://registry.yarnpkg.com/mute-stream/-/mute-stream-0.0.8.tgz#1630c42b2251ff81e2a283de96a5497ea92e5e0d" integrity sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA== -nan@2, nan@^2.12.1, nan@^2.13.2: +nan@^2.12.1, nan@^2.13.2: version "2.14.0" resolved "https://registry.yarnpkg.com/nan/-/nan-2.14.0.tgz#7818f722027b2459a86f0295d434d1fc2336c52c" integrity sha512-INOFj37C7k3AfaNTtX8RhsTw7qRy7eLET14cROi9+5HAVbbHuIWUHEauBv5qT4Av2tWasiTY1Jw6puUNqRJXQg== @@ -21224,6 +21505,13 @@ node-notifier@^5.4.2: shellwords "^0.1.1" which "^1.3.0" +node-preload@^0.2.1: + version "0.2.1" + resolved "https://registry.yarnpkg.com/node-preload/-/node-preload-0.2.1.tgz#c03043bb327f417a18fee7ab7ee57b408a144301" + integrity sha512-RM5oyBy45cLEoHqCeh+MNuFAxO0vTFBLskvQbOKnEE7YTTSN4tbN8QWDIPQ6L+WvKsB/qLEGpYe2ZZ9d4W9OIQ== + dependencies: + process-on-spawn "^1.0.0" + node-releases@^1.1.25, node-releases@^1.1.46: version "1.1.47" resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-1.1.47.tgz#c59ef739a1fd7ecbd9f0b7cf5b7871e8a8b591e4" @@ -21278,6 +21566,22 @@ nodemailer@^4.7.0: resolved "https://registry.yarnpkg.com/nodemailer/-/nodemailer-4.7.0.tgz#4420e06abfffd77d0618f184ea49047db84f4ad8" integrity sha512-IludxDypFpYw4xpzKdMAozBSkzKHmNBvGanUREjJItgJ2NYcK/s8+PggVhj7c2yGFQykKsnnmv1+Aqo0ZfjHmw== +nodemon@^2.0.2: + version "2.0.3" + resolved "https://registry.yarnpkg.com/nodemon/-/nodemon-2.0.3.tgz#e9c64df8740ceaef1cb00e1f3da57c0a93ef3714" + integrity sha512-lLQLPS90Lqwc99IHe0U94rDgvjo+G9I4uEIxRG3evSLROcqQ9hwc0AxlSHKS4T1JW/IMj/7N5mthiN58NL/5kw== + dependencies: + chokidar "^3.2.2" + debug "^3.2.6" + ignore-by-default "^1.0.1" + minimatch "^3.0.4" + pstree.remy "^1.1.7" + semver "^5.7.1" + supports-color "^5.5.0" + touch "^3.1.0" + undefsafe "^2.0.2" + update-notifier "^4.0.0" + "nomnom@>= 1.5.x": version "1.8.1" resolved "https://registry.yarnpkg.com/nomnom/-/nomnom-1.8.1.tgz#2151f722472ba79e50a76fc125bb8c8f2e4dc2a7" @@ -21300,6 +21604,13 @@ nopt@^2.2.0: dependencies: abbrev "1" +nopt@~1.0.10: + version "1.0.10" + resolved "https://registry.yarnpkg.com/nopt/-/nopt-1.0.10.tgz#6ddd21bd2a31417b92727dd585f8a6f37608ebee" + integrity sha1-bd0hvSoxQXuScn3Vhfim83YI6+4= + dependencies: + abbrev "1" + normalize-package-data@^2.0.0, normalize-package-data@^2.3.2, normalize-package-data@^2.3.4: version "2.4.0" resolved "https://registry.yarnpkg.com/normalize-package-data/-/normalize-package-data-2.4.0.tgz#12f95a307d58352075a04907b84ac8be98ac012f" @@ -21490,36 +21801,37 @@ nwsapi@^2.2.0: resolved "https://registry.yarnpkg.com/nwsapi/-/nwsapi-2.2.0.tgz#204879a9e3d068ff2a55139c2c772780681a38b7" integrity sha512-h2AatdwYH+JHiZpv7pt/gSX1XoRGb7L/qSIeuqA6GwYoF9w1vP1cw42TO0aI2pNyshRK5893hNSl+1//vHK7hQ== -nyc@^14.1.1: - version "14.1.1" - resolved "https://registry.yarnpkg.com/nyc/-/nyc-14.1.1.tgz#151d64a6a9f9f5908a1b73233931e4a0a3075eeb" - integrity sha512-OI0vm6ZGUnoGZv/tLdZ2esSVzDwUC88SNs+6JoSOMVxA+gKMB8Tk7jBwgemLx4O40lhhvZCVw1C+OYLOBOPXWw== +nyc@^15.0.1: + version "15.0.1" + resolved "https://registry.yarnpkg.com/nyc/-/nyc-15.0.1.tgz#bd4d5c2b17f2ec04370365a5ca1fc0ed26f9f93d" + integrity sha512-n0MBXYBYRqa67IVt62qW1r/d9UH/Qtr7SF1w/nQLJ9KxvWF6b2xCHImRAixHN9tnMMYHC2P14uo6KddNGwMgGg== dependencies: - archy "^1.0.0" - caching-transform "^3.0.2" - convert-source-map "^1.6.0" - cp-file "^6.2.0" - find-cache-dir "^2.1.0" - find-up "^3.0.0" - foreground-child "^1.5.6" - glob "^7.1.3" - istanbul-lib-coverage "^2.0.5" - istanbul-lib-hook "^2.0.7" - istanbul-lib-instrument "^3.3.0" - istanbul-lib-report "^2.0.8" - istanbul-lib-source-maps "^3.0.6" - istanbul-reports "^2.2.4" - js-yaml "^3.13.1" - make-dir "^2.1.0" - merge-source-map "^1.1.0" - resolve-from "^4.0.0" - rimraf "^2.6.3" + "@istanbuljs/load-nyc-config" "^1.0.0" + "@istanbuljs/schema" "^0.1.2" + caching-transform "^4.0.0" + convert-source-map "^1.7.0" + decamelize "^1.2.0" + find-cache-dir "^3.2.0" + find-up "^4.1.0" + foreground-child "^2.0.0" + glob "^7.1.6" + istanbul-lib-coverage "^3.0.0" + istanbul-lib-hook "^3.0.0" + istanbul-lib-instrument "^4.0.0" + istanbul-lib-processinfo "^2.0.2" + istanbul-lib-report "^3.0.0" + istanbul-lib-source-maps "^4.0.0" + istanbul-reports "^3.0.2" + make-dir "^3.0.0" + node-preload "^0.2.1" + p-map "^3.0.0" + process-on-spawn "^1.0.0" + resolve-from "^5.0.0" + rimraf "^3.0.0" signal-exit "^3.0.2" - spawn-wrap "^1.4.2" - test-exclude "^5.2.3" - uuid "^3.3.2" - yargs "^13.2.2" - yargs-parser "^13.0.0" + spawn-wrap "^2.0.0" + test-exclude "^6.0.0" + yargs "^15.0.2" oauth-sign@~0.8.1, oauth-sign@~0.8.2: version "0.8.2" @@ -21958,7 +22270,7 @@ os-browserify@^0.3.0: resolved "https://registry.yarnpkg.com/os-browserify/-/os-browserify-0.3.0.tgz#854373c7f5c2315914fc9bfc6bd8238fdda1ec27" integrity sha1-hUNzx/XCMVkU/Jv8a9gjj92h7Cc= -os-homedir@^1.0.0, os-homedir@^1.0.1: +os-homedir@^1.0.0: version "1.0.2" resolved "https://registry.yarnpkg.com/os-homedir/-/os-homedir-1.0.2.tgz#ffbc4988336e0e833de0c168c7ef152121aa7fb3" integrity sha1-/7xJiDNuDoM94MFox+8VISGqf7M= @@ -22051,6 +22363,11 @@ p-cancelable@^0.4.0: resolved "https://registry.yarnpkg.com/p-cancelable/-/p-cancelable-0.4.1.tgz#35f363d67d52081c8d9585e37bcceb7e0bbcb2a0" integrity sha512-HNa1A8LvB1kie7cERyy21VNeHb2CWJJYqyyC2o3klWFfMGlFmWv2Z7sFgZH8ZiaYL95ydToKTFVXgMV/Os0bBQ== +p-cancelable@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/p-cancelable/-/p-cancelable-1.1.0.tgz#d078d15a3af409220c886f1d9a0ca2e441ab26cc" + integrity sha512-s73XxOZ4zpt1edZYZzvhqFa6uvQc1vwUa0K0BdtIZgQMAJj9IbebH+JkgKZc9h+B05PKHLOTl4ajG1BmNrVZlw== + p-cancelable@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/p-cancelable/-/p-cancelable-2.0.0.tgz#4a3740f5bdaf5ed5d7c3e34882c6fb5d6b266a6e" @@ -22200,13 +22517,13 @@ p-try@^2.0.0: resolved "https://registry.yarnpkg.com/p-try/-/p-try-2.2.0.tgz#cb2868540e313d61de58fafbe35ce9004d5540e6" integrity sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ== -package-hash@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/package-hash/-/package-hash-3.0.0.tgz#50183f2d36c9e3e528ea0a8605dff57ce976f88e" - integrity sha512-lOtmukMDVvtkL84rJHI7dpTYq+0rli8N2wlnqUcBuDWCfVhRUfOmnR9SsoHFMLpACvEV60dX7rd0rFaYDZI+FA== +package-hash@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/package-hash/-/package-hash-4.0.0.tgz#3537f654665ec3cc38827387fc904c163c54f506" + integrity sha512-whdkPIooSu/bASggZ96BWVvZTRMOFxnyUG5PnTSGKoJE2gd5mbVNmR2Nj20QFzxYYgAXpoqC+AiXzl+UMRh7zQ== dependencies: graceful-fs "^4.1.15" - hasha "^3.0.0" + hasha "^5.0.0" lodash.flattendeep "^4.4.0" release-zalgo "^1.0.0" @@ -22238,6 +22555,16 @@ package-json@^5.0.0: registry-url "^3.1.0" semver "^5.5.0" +package-json@^6.3.0: + version "6.5.0" + resolved "https://registry.yarnpkg.com/package-json/-/package-json-6.5.0.tgz#6feedaca35e75725876d0b0e64974697fed145b0" + integrity sha512-k3bdm2n25tkyxcjSKzB5x8kfVxlMdgsbPr0GkZcwHsLpba6cBjqCt1KlcChKEvxHIcTB1FVMuwoijZ26xex5MQ== + dependencies: + got "^9.6.0" + registry-auth-token "^4.0.0" + registry-url "^5.0.0" + semver "^6.2.0" + pad-component@0.0.1: version "0.0.1" resolved "https://registry.yarnpkg.com/pad-component/-/pad-component-0.0.1.tgz#ad1f22ce1bf0fdc0d6ddd908af17f351a404b8ac" @@ -22258,10 +22585,10 @@ pako@~1.0.5: resolved "https://registry.yarnpkg.com/pako/-/pako-1.0.6.tgz#0101211baa70c4bca4a0f63f2206e97b7dfaf258" integrity sha512-lQe48YPsMJAig+yngZ87Lus+NF+3mtu7DVOBu6b/gHO1YpKwIj5AWjZ/TOS7i46HD/UixzWb1zeWDZfGZ3iYcg== -papaparse@^4.6.3: - version "4.6.3" - resolved "https://registry.yarnpkg.com/papaparse/-/papaparse-4.6.3.tgz#742e5eaaa97fa6c7e1358d2934d8f18f44aee781" - integrity sha512-LRq7BrHC2kHPBYSD50aKuw/B/dGcg29omyJbKWY3KsYUZU69RKwaBHu13jGmCYBtOc4odsLCrFyk6imfyNubJQ== +papaparse@^5.2.0: + version "5.2.0" + resolved "https://registry.yarnpkg.com/papaparse/-/papaparse-5.2.0.tgz#97976a1b135c46612773029153dc64995caa3b7b" + integrity sha512-ylq1wgUSnagU+MKQtNeVqrPhZuMYBvOSL00DHycFTCxownF95gpLAk1HiHdUW77N8yxRq1qHXLdlIPyBSG9NSA== parallel-transform@^1.1.0: version "1.1.0" @@ -22741,6 +23068,11 @@ picomatch@^2.0.4, picomatch@^2.0.5: resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.0.7.tgz#514169d8c7cd0bdbeecc8a2609e34a7163de69f6" integrity sha512-oLHIdio3tZ0qH76NybpeneBhYVj0QFTfXEFTc/B3zKQspYfYYkWYgFsmzo+4kvId/bQRcNkVeguI3y+CD22BtA== +picomatch@^2.0.7: + version "2.2.2" + resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.2.2.tgz#21f333e9b6b8eaff02468f5146ea406d345f4dad" + integrity sha512-q0M/9eZHzmr0AulXyPwNfZjtwZ/RBZlbN3K3CErVrk50T2ASYI7Bye0EvekFY3IP1Nt2DHu0re+V2ZHIpMkuWg== + pify@^2.0.0, pify@^2.2.0, pify@^2.3.0: version "2.3.0" resolved "https://registry.yarnpkg.com/pify/-/pify-2.3.0.tgz#ed141a6ac043a849ea588498e7dca8b15330e90c" @@ -23202,6 +23534,13 @@ process-nextick-args@~2.0.0: resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-2.0.1.tgz#7820d9b16120cc55ca9ae7792680ae7dba6d7fe2" integrity sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag== +process-on-spawn@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/process-on-spawn/-/process-on-spawn-1.0.0.tgz#95b05a23073d30a17acfdc92a440efd2baefdc93" + integrity sha512-1WsPDsUSMmZH5LeMLegqkPDrsGgsWwk1Exipy2hvB0o/F0ASzbpIctSCcZIK1ykJvtTJULEH+20WOFjMvGnCTg== + dependencies: + fromentries "^1.2.0" + process@^0.11.10: version "0.11.10" resolved "https://registry.yarnpkg.com/process/-/process-0.11.10.tgz#7332300e840161bda3e69a1d1d91a7d4bc16f182" @@ -23232,6 +23571,11 @@ promise-inflight@^1.0.1: resolved "https://registry.yarnpkg.com/promise-inflight/-/promise-inflight-1.0.1.tgz#98472870bf228132fcbdd868129bad12c3c029e3" integrity sha1-mEcocL8igTL8vdhoEputEsPAKeM= +promise-polyfill@^8.1.3: + version "8.1.3" + resolved "https://registry.yarnpkg.com/promise-polyfill/-/promise-polyfill-8.1.3.tgz#8c99b3cf53f3a91c68226ffde7bde81d7f904116" + integrity sha512-MG5r82wBzh7pSKDRa9y+vllNHz3e3d4CNj1PQE4BQYxLme0gKYYBm9YENq+UkEikyZ0XbiGWxYlVw3Rl9O/U8g== + promise.prototype.finally@^3.1.0: version "3.1.0" resolved "https://registry.yarnpkg.com/promise.prototype.finally/-/promise.prototype.finally-3.1.0.tgz#66f161b1643636e50e7cf201dc1b84a857f3864e" @@ -23378,6 +23722,11 @@ psl@^1.1.28: resolved "https://registry.yarnpkg.com/psl/-/psl-1.4.0.tgz#5dd26156cdb69fa1fdb8ab1991667d3f80ced7c2" integrity sha512-HZzqCGPecFLyoRj5HLfuDSKYTJkAfB5thKBIkRHtGjWwY7p1dAyveIbXIq4tO0KYfDF2tHqPUgY9SDnGm00uFw== +pstree.remy@^1.1.7: + version "1.1.7" + resolved "https://registry.yarnpkg.com/pstree.remy/-/pstree.remy-1.1.7.tgz#c76963a28047ed61542dc361aa26ee55a7fa15f3" + integrity sha512-xsMgrUwRpuGskEzBFkH8NmTimbZ5PcPup0LA8JJkHIm2IMUbQcpo3yeLNWVrufEYjh8YwtSVh0xz6UeWc5Oh5A== + public-encrypt@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/public-encrypt/-/public-encrypt-4.0.0.tgz#39f699f3a46560dd5ebacbca693caf7c65c18cc6" @@ -23565,6 +23914,13 @@ punycode@^1.2.4, punycode@^1.4.1: resolved "https://registry.yarnpkg.com/punycode/-/punycode-1.4.1.tgz#c0d5a63b2718800ad8e1eb0fa5269c84dd41845e" integrity sha1-wNWmOycYgArY4esPpSachN1BhF4= +pupa@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/pupa/-/pupa-2.0.1.tgz#dbdc9ff48ffbea4a26a069b6f9f7abb051008726" + integrity sha512-hEJH0s8PXLY/cdXh66tNEQGndDrIKNqNC5xmrysZy3i5C3oEoLna7YAOad+7u125+zH1HNXUmGEkrhb3c2VriA== + dependencies: + escape-goat "^2.0.0" + puppeteer-core@^1.19.0: version "1.19.0" resolved "https://registry.yarnpkg.com/puppeteer-core/-/puppeteer-core-1.19.0.tgz#3c3f98edb5862583e3a9c19cbc0da57ccc63ba5c" @@ -23822,7 +24178,7 @@ raw-loader@~0.5.1: resolved "https://registry.yarnpkg.com/raw-loader/-/raw-loader-0.5.1.tgz#0c3d0beaed8a01c966d9787bf778281252a979aa" integrity sha1-DD0L6u2KAclm2Xh793goElKpeao= -rc@^1.0.1, rc@^1.1.6, rc@^1.2.7: +rc@^1.0.1, rc@^1.1.6, rc@^1.2.7, rc@^1.2.8: version "1.2.8" resolved "https://registry.yarnpkg.com/rc/-/rc-1.2.8.tgz#cd924bf5200a075b83c188cd6b9e211b7fc0d3ed" integrity sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw== @@ -24819,7 +25175,7 @@ read-pkg@^5.1.1, read-pkg@^5.2.0: string_decoder "~1.1.1" util-deprecate "~1.0.1" -readable-stream@1.0: +readable-stream@1.0, "readable-stream@>=1.0.33-1 <1.1.0-0": version "1.0.34" resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-1.0.34.tgz#125820e34bc842d2f2aaafafe4c2916ee32c157c" integrity sha1-Elgg40vIQtLyqq+v5MKRbuMsFXw= @@ -24906,6 +25262,13 @@ readdirp@~3.2.0: dependencies: picomatch "^2.0.4" +readdirp@~3.3.0: + version "3.3.0" + resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-3.3.0.tgz#984458d13a1e42e2e9f5841b129e162f369aff17" + integrity sha512-zz0pAkSPOXXm1viEwygWIPSPkcBYjW1xU5j/JBh5t9bGCJwa6f9+BJa6VaB2g+b55yVrmXzqkyLf4xaWYM0IkQ== + dependencies: + picomatch "^2.0.7" + readline2@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/readline2/-/readline2-1.0.1.tgz#41059608ffc154757b715d9989d199ffbf372e35" @@ -25117,31 +25480,6 @@ redux@^4.0.5: loose-envify "^1.4.0" symbol-observable "^1.2.0" -ref-struct@1, ref-struct@^1.0.2: - version "1.1.0" - resolved "https://registry.yarnpkg.com/ref-struct/-/ref-struct-1.1.0.tgz#5d5ee65ad41cefc3a5c5feb40587261e479edc13" - integrity sha1-XV7mWtQc78Olxf60BYcmHkee3BM= - dependencies: - debug "2" - ref "1" - -ref-union@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/ref-union/-/ref-union-1.0.1.tgz#3a2397f862f1e75171d687268f43b3f17729f120" - integrity sha1-OiOX+GLx51Fx1ocmj0Oz8Xcp8SA= - dependencies: - debug "2" - ref "1" - -ref@1, ref@^1.2.0: - version "1.3.5" - resolved "https://registry.yarnpkg.com/ref/-/ref-1.3.5.tgz#0e33f080cdb94a3d95312b2b3b1fd0f82044ca0f" - integrity sha512-2cBCniTtxcGUjDpvFfVpw323a83/0RLSGJJY5l5lcomZWhYpU2cuLdsvYqMixvsdLJ9+sTdzEkju8J8ZHDM2nA== - dependencies: - bindings "1" - debug "2" - nan "2" - reflect.ownkeys@^0.2.0: version "0.2.0" resolved "https://registry.yarnpkg.com/reflect.ownkeys/-/reflect.ownkeys-0.2.0.tgz#749aceec7f3fdf8b63f927a04809e90c5c0b3460" @@ -25156,6 +25494,16 @@ refractor@^2.4.1: parse-entities "^1.1.2" prismjs "~1.16.0" +regedit@^3.0.3: + version "3.0.3" + resolved "https://registry.yarnpkg.com/regedit/-/regedit-3.0.3.tgz#0c2188e15f670de7d5740c5cea9bbebe99497749" + integrity sha512-SpHmMKOtiEYx0MiRRC48apBsmThoZ4svZNsYoK8leHd5bdUHV1nYb8pk8gh6Moou7/S9EDi1QsjBTpyXVQrPuQ== + dependencies: + debug "^4.1.0" + if-async "^3.7.4" + stream-slicer "0.0.6" + through2 "^0.6.3" + regenerate-unicode-properties@^8.2.0: version "8.2.0" resolved "https://registry.yarnpkg.com/regenerate-unicode-properties/-/regenerate-unicode-properties-8.2.0.tgz#e5de7111d655e7ba60c057dbe9ff37c87e65cdec" @@ -25268,6 +25616,13 @@ registry-auth-token@^3.0.1, registry-auth-token@^3.3.2: rc "^1.1.6" safe-buffer "^5.0.1" +registry-auth-token@^4.0.0: + version "4.1.1" + resolved "https://registry.yarnpkg.com/registry-auth-token/-/registry-auth-token-4.1.1.tgz#40a33be1e82539460f94328b0f7f0f84c16d9479" + integrity sha512-9bKS7nTl9+/A1s7tnPeGrUpRcVY+LUh7bfFgzpndALdPfXQBfQV77rQVtqgUV3ti4vc/Ik81Ex8UJDWDQ12zQA== + dependencies: + rc "^1.2.8" + registry-url@^3.0.0, registry-url@^3.0.3, registry-url@^3.1.0: version "3.1.0" resolved "https://registry.yarnpkg.com/registry-url/-/registry-url-3.1.0.tgz#3d4ef870f73dde1d77f0cf9a381432444e174942" @@ -25275,6 +25630,13 @@ registry-url@^3.0.0, registry-url@^3.0.3, registry-url@^3.1.0: dependencies: rc "^1.0.1" +registry-url@^5.0.0: + version "5.1.0" + resolved "https://registry.yarnpkg.com/registry-url/-/registry-url-5.1.0.tgz#e98334b50d5434b81136b44ec638d9c2009c5009" + integrity sha512-8acYXXTI0AkQv6RAOjE3vOaIXZkT9wo4LOFbBKYQEEnnMNBpKqdUrI6S4NT0KPIo/WVvJ5tE/X5LF/TQUf0ekw== + dependencies: + rc "^1.2.8" + regjsgen@^0.5.1: version "0.5.1" resolved "https://registry.yarnpkg.com/regjsgen/-/regjsgen-0.5.1.tgz#48f0bf1a5ea205196929c0d9798b42d1ed98443c" @@ -25874,7 +26236,7 @@ resolve@~1.10.1: dependencies: path-parse "^1.0.6" -responselike@1.0.2: +responselike@1.0.2, responselike@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/responselike/-/responselike-1.0.2.tgz#918720ef3b631c5642be068f15ade5a46f4ba1e7" integrity sha1-kYcg7ztjHFZCvgaPFa3lpG9Loec= @@ -26469,6 +26831,13 @@ semver-diff@^2.0.0: dependencies: semver "^5.0.3" +semver-diff@^3.1.1: + version "3.1.1" + resolved "https://registry.yarnpkg.com/semver-diff/-/semver-diff-3.1.1.tgz#05f77ce59f325e00e2706afd67bb506ddb1ca32b" + integrity sha512-GX0Ix/CJcHyB8c4ykpHGIAvLyOwOobtM/8d+TQkAd81/bEjgPHrfba41Vpesr7jX/t8Uh+R3EX9eAS5be+jQYg== + dependencies: + semver "^6.3.0" + semver-greatest-satisfied-range@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/semver-greatest-satisfied-range/-/semver-greatest-satisfied-range-1.1.0.tgz#13e8c2658ab9691cb0cd71093240280d36f77a5b" @@ -26518,7 +26887,7 @@ semver@^5.5.1: resolved "https://registry.yarnpkg.com/semver/-/semver-5.5.1.tgz#7dfdd8814bdb7cabc7be0fb1d734cfb66c940477" integrity sha512-PqpAxfrEhlSUWge8dwIp4tZnQ25DIOthpiaHNIthsjEFQD6EvqUKUDM7L8O2rShkFccYo1VjJR0coWfNkCubRw== -semver@^6.0.0, semver@^6.1.1, semver@^6.1.2, semver@^6.2.0, semver@^6.3.0: +semver@^6.0.0, semver@^6.1.1, semver@^6.1.2, semver@^6.2.0, semver@^6.3.0, semver@~6.3.0: version "6.3.0" resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.0.tgz#ee0a64c8af5e8ceea67687b133761e1becbd1d3d" integrity sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw== @@ -26528,6 +26897,11 @@ semver@^6.1.0: resolved "https://registry.yarnpkg.com/semver/-/semver-6.1.1.tgz#53f53da9b30b2103cd4f15eab3a18ecbcb210c9b" integrity sha512-rWYq2e5iYW+fFe/oPPtYJxYgjBm8sC4rmoGdUOgBB7VnwKt6HrL793l2voH1UlsyYZpJ4g0wfjnTEO1s1NP2eQ== +semver@^7.1.3: + version "7.3.0" + resolved "https://registry.yarnpkg.com/semver/-/semver-7.3.0.tgz#91f7c70ec944a63e5dc7a74cde2da375d8e0853c" + integrity sha512-uyvgU/igkrMgNHwLgXvlpD9jEADbJhB0+JXSywoO47JgJ6c16iau9F9cjtc/E5o0PoqRYTiTIAPRKaYe84z6eQ== + semver@~5.3.0: version "5.3.0" resolved "https://registry.yarnpkg.com/semver/-/semver-5.3.0.tgz#9b2ce5d3de02d17c6012ad326aa6b4d0cf54f94f" @@ -27207,17 +27581,17 @@ spawn-sync@^1.0.15: concat-stream "^1.4.7" os-shim "^0.1.2" -spawn-wrap@^1.4.2: - version "1.4.2" - resolved "https://registry.yarnpkg.com/spawn-wrap/-/spawn-wrap-1.4.2.tgz#cff58e73a8224617b6561abdc32586ea0c82248c" - integrity sha512-vMwR3OmmDhnxCVxM8M+xO/FtIp6Ju/mNaDfCMMW7FDcLRTPFWUswec4LXJHTJE2hwTI9O0YBfygu4DalFl7Ylg== +spawn-wrap@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/spawn-wrap/-/spawn-wrap-2.0.0.tgz#103685b8b8f9b79771318827aa78650a610d457e" + integrity sha512-EeajNjfN9zMnULLwhZZQU3GWBoFNkbngTUPfaawT4RkMiviTxcX0qfhVbGey39mfctfDHkWtuecgQ8NJcyQWHg== dependencies: - foreground-child "^1.5.6" - mkdirp "^0.5.0" - os-homedir "^1.0.1" - rimraf "^2.6.2" + foreground-child "^2.0.0" + is-windows "^1.0.2" + make-dir "^3.0.0" + rimraf "^3.0.0" signal-exit "^3.0.2" - which "^1.3.0" + which "^2.0.1" spdx-compare@^0.1.2: version "0.1.2" @@ -27605,6 +27979,11 @@ stream-shift@^1.0.0: resolved "https://registry.yarnpkg.com/stream-shift/-/stream-shift-1.0.0.tgz#d5c752825e5367e786f78e18e445ea223a155952" integrity sha1-1cdSgl5TZ+eG944Y5EXqIjoVWVI= +stream-slicer@0.0.6: + version "0.0.6" + resolved "https://registry.yarnpkg.com/stream-slicer/-/stream-slicer-0.0.6.tgz#f86b2ac5c2440b7a0a87b71f33665c0788046138" + integrity sha1-+GsqxcJEC3oKh7cfM2ZcB4gEYTg= + stream-spigot@~2.1.2: version "2.1.2" resolved "https://registry.yarnpkg.com/stream-spigot/-/stream-spigot-2.1.2.tgz#7de145e819f8dd0db45090d13dcf73a8ed3cc035" @@ -27681,7 +28060,7 @@ string-width@^3.0.0, string-width@^3.1.0: is-fullwidth-code-point "^2.0.0" strip-ansi "^5.1.0" -string-width@^4.1.0, string-width@^4.2.0: +string-width@^4.0.0, string-width@^4.1.0, string-width@^4.2.0: version "4.2.0" resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.0.tgz#952182c46cc7b2c313d1596e623992bd163b72b5" integrity sha512-zUz5JD+tgqtuDjMhwIg5uFVV3dtqZ9yQJlZVfq4I01/K5Paj5UHj7VyrQOJvzawSVlKpObApbfD0Ed6yJc+1eg== @@ -28459,6 +28838,11 @@ term-size@^1.2.0: dependencies: execa "^0.7.0" +term-size@^2.1.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/term-size/-/term-size-2.2.0.tgz#1f16adedfe9bdc18800e1776821734086fcc6753" + integrity sha512-a6sumDlzyHVJWb8+YofY4TW112G6p2FCPEAFk+59gIYHv3XHRhm9ltVQ9kli4hNWeQBwSpe8cRN25x0ROunMOw== + terser-webpack-plugin@^1.2.4: version "1.4.1" resolved "https://registry.yarnpkg.com/terser-webpack-plugin/-/terser-webpack-plugin-1.4.1.tgz#61b18e40eaee5be97e771cdbb10ed1280888c2b4" @@ -28536,7 +28920,7 @@ terser@^4.4.3: source-map "~0.6.1" source-map-support "~0.5.12" -test-exclude@^5.0.0, test-exclude@^5.2.3: +test-exclude@^5.0.0: version "5.2.3" resolved "https://registry.yarnpkg.com/test-exclude/-/test-exclude-5.2.3.tgz#c3d3e1e311eb7ee405e092dac10aefd09091eac0" integrity sha512-M+oxtseCFO3EDtAaGH7iiej3CBkzXqFMbzqYAACdzKui4eZA+pq3tZEwChvOdNfa7xxy8BfbmgJSIr43cC/+2g== @@ -28546,6 +28930,15 @@ test-exclude@^5.0.0, test-exclude@^5.2.3: read-pkg-up "^4.0.0" require-main-filename "^2.0.0" +test-exclude@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/test-exclude/-/test-exclude-6.0.0.tgz#04a8698661d805ea6fa293b6cb9e63ac044ef15e" + integrity sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w== + dependencies: + "@istanbuljs/schema" "^0.1.2" + glob "^7.1.4" + minimatch "^3.0.4" + text-hex@1.0.x: version "1.0.0" resolved "https://registry.yarnpkg.com/text-hex/-/text-hex-1.0.0.tgz#69dc9c1b17446ee79a92bf5b884bb4b9127506f5" @@ -28609,6 +29002,14 @@ through2@2.X, through2@^2.0.0, through2@^2.0.3, through2@~2.0.0: readable-stream "^2.1.5" xtend "~4.0.1" +through2@^0.6.3: + version "0.6.5" + resolved "https://registry.yarnpkg.com/through2/-/through2-0.6.5.tgz#41ab9c67b29d57209071410e1d7a7a968cd3ad48" + integrity sha1-QaucZ7KdVyCQcUEOHXp6lozTrUg= + dependencies: + readable-stream ">=1.0.33-1 <1.1.0-0" + xtend ">=4.0.0 <4.1.0-0" + through2@^3.0.1: version "3.0.1" resolved "https://registry.yarnpkg.com/through2/-/through2-3.0.1.tgz#39276e713c3302edf9e388dd9c812dd3b825bd5a" @@ -28844,6 +29245,11 @@ to-object-path@^0.3.0: dependencies: kind-of "^3.0.2" +to-readable-stream@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/to-readable-stream/-/to-readable-stream-1.0.0.tgz#ce0aa0c2f3df6adf852efb404a783e77c0475771" + integrity sha512-Iq25XBt6zD5npPhlLVXGFN3/gyR2/qODcKNNyTMd4vbm39HUaOiAM4PMq0eMVC/Tkxz+Zjdsc55g9yyz+Yq00Q== + to-readable-stream@^2.0.0: version "2.1.0" resolved "https://registry.yarnpkg.com/to-readable-stream/-/to-readable-stream-2.1.0.tgz#82880316121bea662cdc226adb30addb50cb06e8" @@ -28933,6 +29339,13 @@ topojson-client@3.0.0, topojson-client@^3.0.0: dependencies: commander "2" +touch@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/touch/-/touch-3.1.0.tgz#fe365f5f75ec9ed4e56825e0bb76d24ab74af83b" + integrity sha512-WBx8Uy5TLtOSRtIq+M03/sKDrXCLHxwDcquSP2c43Le03/9serjQBIztjRz6FkJez9D/hleyAXTBGLwwZUw9lA== + dependencies: + nopt "~1.0.10" + tough-cookie@>=2.3.3, tough-cookie@^2.0.0, tough-cookie@^2.3.3, tough-cookie@~2.4.3: version "2.4.3" resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-2.4.3.tgz#53f36da3f47783b0925afa06ff9f3b165280f781" @@ -29128,37 +29541,6 @@ tslib@^1.8.1, tslib@^1.9.0, tslib@^1.9.2, tslib@^1.9.3: resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.9.3.tgz#d7e4dd79245d85428c4d7e4822a79917954ca286" integrity sha512-4krF8scpejhaOgqzBEcGM7yDIEfi0/8+8zDRZhNZZ2kjmHJ4hv3zCbQWxoJGz1iw5U0Jl0nma13xzHXcncMavQ== -tslint-config-prettier@^1.18.0: - version "1.18.0" - resolved "https://registry.yarnpkg.com/tslint-config-prettier/-/tslint-config-prettier-1.18.0.tgz#75f140bde947d35d8f0d238e0ebf809d64592c37" - integrity sha512-xPw9PgNPLG3iKRxmK7DWr+Ea/SzrvfHtjFt5LBl61gk2UBG/DB9kCXRjv+xyIU1rUtnayLeMUVJBcMX8Z17nDg== - -tslint@^6.1.0: - version "6.1.0" - resolved "https://registry.yarnpkg.com/tslint/-/tslint-6.1.0.tgz#c6c611b8ba0eed1549bf5a59ba05a7732133d851" - integrity sha512-fXjYd/61vU6da04E505OZQGb2VCN2Mq3doeWcOIryuG+eqdmFUXTYVwdhnbEu2k46LNLgUYt9bI5icQze/j0bQ== - dependencies: - "@babel/code-frame" "^7.0.0" - builtin-modules "^1.1.1" - chalk "^2.3.0" - commander "^2.12.1" - diff "^4.0.1" - glob "^7.1.1" - js-yaml "^3.13.1" - minimatch "^3.0.4" - mkdirp "^0.5.1" - resolve "^1.3.2" - semver "^5.3.0" - tslib "^1.10.0" - tsutils "^2.29.0" - -tsutils@^2.29.0: - version "2.29.0" - resolved "https://registry.yarnpkg.com/tsutils/-/tsutils-2.29.0.tgz#32b488501467acbedd4b85498673a0812aca0b99" - integrity sha512-g5JVHCIJwzfISaXpXE1qvNalca5Jwob6FjI4AoPlqMusJ6ftFE7IkkFoMhVLRgK+4Kx3gkzb8UZK5t5yTTvEmA== - dependencies: - tslib "^1.8.1" - tsutils@^3.17.1: version "3.17.1" resolved "https://registry.yarnpkg.com/tsutils/-/tsutils-3.17.1.tgz#ed719917f11ca0dee586272b2ac49e015a2dd759" @@ -29688,7 +30070,7 @@ type-fest@^0.6.0: resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.6.0.tgz#8d2a2370d3df886eb5c90ada1c5bf6188acf838b" integrity sha512-q+MB8nYR1KDLrgr4G5yemftpMC7/QLqVndBmEEdqzmNj5dcFOO4Oo8qlwZE3ULT3+Zim1F8Kq4cBnikNhlCMlg== -type-fest@^0.8.1: +type-fest@^0.8.0, type-fest@^0.8.1: version "0.8.1" resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.8.1.tgz#09e249ebde851d3b1e48d27c105444667f17b83d" integrity sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA== @@ -29726,6 +30108,13 @@ typed-styles@^0.0.7: resolved "https://registry.yarnpkg.com/typed-styles/-/typed-styles-0.0.7.tgz#93392a008794c4595119ff62dde6809dbc40a3d9" integrity sha512-pzP0PWoZUhsECYjABgCGQlRGL1n7tOHsgwYv3oIiEpJwGhFTuty/YNeduxQYzXXa3Ge5BdT6sHYIQYpl4uJ+5Q== +typedarray-to-buffer@^3.1.5: + version "3.1.5" + resolved "https://registry.yarnpkg.com/typedarray-to-buffer/-/typedarray-to-buffer-3.1.5.tgz#a97ee7a9ff42691b9f783ff1bc5112fe3fca9080" + integrity sha512-zdu8XMNEDepKKR+XYOXAVPtWui0ly0NtohUscw+UmaHiAWT8hrV1rr//H6V+0DvJ3OQ19S979M0laLfX8rm82Q== + dependencies: + is-typedarray "^1.0.0" + typedarray@^0.0.6, typedarray@~0.0.5: version "0.0.6" resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777" @@ -29840,6 +30229,13 @@ unc-path-regex@^0.1.2: resolved "https://registry.yarnpkg.com/unc-path-regex/-/unc-path-regex-0.1.2.tgz#e73dd3d7b0d7c5ed86fbac6b0ae7d8c6a69d50fa" integrity sha1-5z3T17DXxe2G+6xrCufYxqadUPo= +undefsafe@^2.0.2: + version "2.0.3" + resolved "https://registry.yarnpkg.com/undefsafe/-/undefsafe-2.0.3.tgz#6b166e7094ad46313b2202da7ecc2cd7cc6e7aae" + integrity sha512-nrXZwwXrD/T/JXeygJqdCO6NZZ1L66HrxM/Z7mIq2oPanoN0F1nLx3lwJMu6AwJY69hdixaFQOuoYsMjE5/C2A== + dependencies: + debug "^2.2.0" + underscore.string@~3.3.4: version "3.3.5" resolved "https://registry.yarnpkg.com/underscore.string/-/underscore.string-3.3.5.tgz#fc2ad255b8bd309e239cbc5816fd23a9b7ea4023" @@ -30027,6 +30423,13 @@ unique-string@^1.0.0: dependencies: crypto-random-string "^1.0.0" +unique-string@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/unique-string/-/unique-string-2.0.0.tgz#39c6451f81afb2749de2b233e3f7c5e8843bd89d" + integrity sha512-uNaeirEPvpZWSgzwsPGtU2zVSTrn/8L5q/IexZmH0eH6SA73CmAA5U4GwORTxQAZs95TAXLNqeLoPPNO5gZfWg== + dependencies: + crypto-random-string "^2.0.0" + unist-util-is@^2.1.1: version "2.1.1" resolved "https://registry.yarnpkg.com/unist-util-is/-/unist-util-is-2.1.1.tgz#0c312629e3f960c66e931e812d3d80e77010947b" @@ -30196,6 +30599,25 @@ update-notifier@^2.5.0: semver-diff "^2.0.0" xdg-basedir "^3.0.0" +update-notifier@^4.0.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/update-notifier/-/update-notifier-4.1.0.tgz#4866b98c3bc5b5473c020b1250583628f9a328f3" + integrity sha512-w3doE1qtI0/ZmgeoDoARmI5fjDoT93IfKgEGqm26dGUOh8oNpaSTsGNdYRN/SjOuo10jcJGwkEL3mroKzktkew== + dependencies: + boxen "^4.2.0" + chalk "^3.0.0" + configstore "^5.0.1" + has-yarn "^2.1.0" + import-lazy "^2.1.0" + is-ci "^2.0.0" + is-installed-globally "^0.3.1" + is-npm "^4.0.0" + is-yarn-global "^0.3.0" + latest-version "^5.0.0" + pupa "^2.0.1" + semver-diff "^3.1.1" + xdg-basedir "^4.0.0" + upper-case-first@^1.1.0, upper-case-first@^1.1.2: version "1.1.2" resolved "https://registry.yarnpkg.com/upper-case-first/-/upper-case-first-1.1.2.tgz#5d79bedcff14419518fd2edb0a0507c9b6859115" @@ -30438,7 +30860,7 @@ utils-regex-from-string@^1.0.0: regex-regex "^1.0.0" validate.io-string-primitive "^1.0.0" -uuid@3.3.2, uuid@^3.0.1, uuid@^3.1.0, uuid@^3.3.2: +uuid@3.3.2, uuid@^3.0.1, uuid@^3.3.2: version "3.3.2" resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.3.2.tgz#1b4af4955eb3077c501c23872fc6513811587131" integrity sha512-yXJmeNaw3DnnKAOKJE51sL/ZaYfWJRl1pK9dr19YFCu0ObS231AB1/LbqTKRAQ5kw8A90rA6fr4riOUpTZvQZA== @@ -30453,6 +30875,11 @@ uuid@^3.0.0: resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.1.0.tgz#3dd3d3e790abc24d7b0d3a034ffababe28ebbc04" integrity sha512-DIWtzUkw04M4k3bf1IcpS2tngXEL26YUD2M0tMDUpnUrz2hgzUBlD55a4FjdLGPvfHxS6uluGWvaVEqgBcVa+g== +uuid@^3.1.0, uuid@^3.3.3: + version "3.4.0" + resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.4.0.tgz#b23e4358afa8a202fe7a100af1f5f883f02007ee" + integrity sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A== + v8-compile-cache@2.0.3: version "2.0.3" resolved "https://registry.yarnpkg.com/v8-compile-cache/-/v8-compile-cache-2.0.3.tgz#00f7494d2ae2b688cfe2899df6ed2c54bef91dbe" @@ -31502,6 +31929,13 @@ widest-line@^2.0.1: dependencies: string-width "^2.1.1" +widest-line@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/widest-line/-/widest-line-3.1.0.tgz#8292333bbf66cb45ff0de1603b136b7ae1496eca" + integrity sha512-NsmoXalsWVDMGupxZ5R08ka9flZjjiLvHVAWYOKtiKM8ujtZWr9cRffak+uSE48+Ob8ObalXpwyeUiyDD6QFgg== + dependencies: + string-width "^4.0.0" + win-release@^1.0.0: version "1.1.1" resolved "https://registry.yarnpkg.com/win-release/-/win-release-1.1.1.tgz#5fa55e02be7ca934edfc12665632e849b72e5209" @@ -31524,17 +31958,6 @@ window-size@^0.2.0: resolved "https://registry.yarnpkg.com/window-size/-/window-size-0.2.0.tgz#b4315bb4214a3d7058ebeee892e13fa24d98b075" integrity sha1-tDFbtCFKPXBY6+7okuE/ok2YsHU= -windows-registry@^0.1.5: - version "0.1.5" - resolved "https://registry.yarnpkg.com/windows-registry/-/windows-registry-0.1.5.tgz#92c25c960884b0d215e69395f52d8dfaa0ba4ad0" - integrity sha512-gMN3ets1fbdP+TApEbbX2TIfBK3MIH5+p9GMvIFS3CNLr7U0Khe5mRj/T5zvwo/pKdhJgDrCLYyaNSs7HYiBCw== - dependencies: - debug "^2.2.0" - ffi "^2.0.0" - ref "^1.2.0" - ref-struct "^1.0.2" - ref-union "^1.0.0" - windows-release@^3.1.0: version "3.2.0" resolved "https://registry.yarnpkg.com/windows-release/-/windows-release-3.2.0.tgz#8122dad5afc303d833422380680a79cdfa91785f" @@ -31713,6 +32136,16 @@ write-file-atomic@^2.4.2: imurmurhash "^0.1.4" signal-exit "^3.0.2" +write-file-atomic@^3.0.0: + version "3.0.3" + resolved "https://registry.yarnpkg.com/write-file-atomic/-/write-file-atomic-3.0.3.tgz#56bd5c5a5c70481cd19c571bd39ab965a5de56e8" + integrity sha512-AvHcyZ5JnSfq3ioSyjrBkH9yW4m7Ayk8/9My/DD9onKeu/94fwrMocemO2QAJFAlnnDN+ZDS+ZjAR5ua1/PV/Q== + dependencies: + imurmurhash "^0.1.4" + is-typedarray "^1.0.0" + signal-exit "^3.0.2" + typedarray-to-buffer "^3.1.5" + write-json-file@^3.2.0: version "3.2.0" resolved "https://registry.yarnpkg.com/write-json-file/-/write-json-file-3.2.0.tgz#65bbdc9ecd8a1458e15952770ccbadfcff5fe62a" @@ -31800,6 +32233,11 @@ xdg-basedir@^3.0.0: resolved "https://registry.yarnpkg.com/xdg-basedir/-/xdg-basedir-3.0.0.tgz#496b2cc109eca8dbacfe2dc72b603c17c5870ad4" integrity sha1-SWsswQnsqNus/i3HK2A8F8WHCtQ= +xdg-basedir@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/xdg-basedir/-/xdg-basedir-4.0.0.tgz#4bc8d9984403696225ef83a1573cbbcb4e79db13" + integrity sha512-PSNhEJDejZYV7h50BohL09Er9VaIefr2LMAf3OEmpCkjOi34eYyQYAXUTjEQtZJTKcF0E2UKTh+osDLsgNim9Q== + xhr@^2.0.1: version "2.4.1" resolved "https://registry.yarnpkg.com/xhr/-/xhr-2.4.1.tgz#ba982cced205ae5eec387169ac9dc77ca4853d38" @@ -31902,16 +32340,16 @@ xregexp@4.2.4: dependencies: "@babel/runtime-corejs2" "^7.2.0" +"xtend@>=4.0.0 <4.1.0-0", xtend@^4.0.2: + version "4.0.2" + resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.2.tgz#bb72779f5fa465186b1f438f674fa347fdb5db54" + integrity sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ== + xtend@^4.0.0, xtend@^4.0.1, xtend@~4.0.0, xtend@~4.0.1: version "4.0.1" resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.1.tgz#a5c6d532be656e23db820efb943a1f04998d63af" integrity sha1-pcbVMr5lbiPbgg77lDofBJmNY68= -xtend@^4.0.2: - version "4.0.2" - resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.2.tgz#bb72779f5fa465186b1f438f674fa347fdb5db54" - integrity sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ== - xxhashjs@^0.2.1: version "0.2.2" resolved "https://registry.yarnpkg.com/xxhashjs/-/xxhashjs-0.2.2.tgz#8a6251567621a1c46a5ae204da0249c7f8caa9d8" @@ -31954,7 +32392,7 @@ yallist@^4.0.0: resolved "https://registry.yarnpkg.com/yallist/-/yallist-4.0.0.tgz#9bb92790d9c0effec63be73519e11a35019a3a72" integrity sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A== -yargs-parser@13.1.2, yargs-parser@^13.0.0, yargs-parser@^13.1.0, yargs-parser@^13.1.1, yargs-parser@^13.1.2: +yargs-parser@13.1.2, yargs-parser@^13.1.0, yargs-parser@^13.1.1, yargs-parser@^13.1.2: version "13.1.2" resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-13.1.2.tgz#130f09702ebaeef2650d54ce6e3e5706f7a4fb38" integrity sha512-3lbsNRf/j+A4QuSZfDRA7HRSfWrzO0YjqTJd5kjAq37Zep1CEgaYmrH9Q3GwPiB9cHyd1Y1UwggGhJGoxipbzg== @@ -32121,7 +32559,7 @@ yargs@^13.2.2, yargs@^13.3.0: y18n "^4.0.0" yargs-parser "^13.1.1" -yargs@^15.3.1: +yargs@^15.0.2, yargs@^15.1.0, yargs@^15.3.1: version "15.3.1" resolved "https://registry.yarnpkg.com/yargs/-/yargs-15.3.1.tgz#9505b472763963e54afe60148ad27a330818e98b" integrity sha512-92O1HWEjw27sBfgmXiixJWT5hRBp2eobqXicLtPBIDBhYB+1HpwZlXmbW2luivBJHBzki+7VyCLRtAkScbTBQA==